使用 Redisson 实现支持 TTL 和 LRU 策略的本地缓存
mhr18 2025-05-05 17:20 16 浏览 0 评论
在分布式系统中,高性能缓存 是保障系统响应速度的关键。在使用 Redis 的同时,很多开发者也希望在本地保留一个缓存副本,以进一步提升读写效率,降低 Redis 的访问频率。Redisson 提供了丰富的工具来简化这种需求的实现。
在这篇文章中,我们将介绍如何基于 Redisson 的 RMapCache 实现:
- 带有 TTL(过期时间)控制;
- 支持本地 LRU 缓存淘汰策略;
- 自动监听 Redis 数据变更并同步更新本地缓存;
- 并且只缓存设置了 TTL 的 key,防止无限制增长。
为什么不用 RLocalCachedMap?
Redisson 提供了 RLocalCachedMap 来实现本地缓存,但我们在使用过程中发现了一些问题:
- 异步 put 后远程 Redis 更新成功但本地未更新;
- Redis 中数据为字符串格式,但 RLocalCachedMap 读取失败;
- 结构内部使用 Lua 脚本做同步,有兼容性 bug(如 unpack 异常);
- 不支持仅缓存 Redis 中有 TTL 的数据。
因此,我们决定舍弃 RLocalCachedMap,改为自己封装一个缓存类,只使用 RMapCache 加上 Caffeine 本地缓存实现相同甚至更灵活的能力。
最终方案:TTLLocalCachedMap
我们将该封装命名为 TTLLocalCachedMap,它具备以下特性:
- Redis 端使用 Redisson 提供的 RMapCache,天然支持 TTL;
- 本地缓存使用 Caffeine,支持 LRU 和 TTL;
- 自动监听 Redis 端的 put/remove 操作并同步本地缓存;
- 初始化时只预热 Redis 中有 TTL 的 key,避免缓存过多无用数据。
核心代码实现
import com.github.benmanes.caffeine.cache.*;
import org.redisson.api.*;
import org.redisson.api.listener.*;
import org.redisson.api.map.event.EntryCreatedListener;
import org.redisson.api.map.event.EntryExpiredListener;
import org.redisson.api.map.event.EntryRemovedListener;
import org.redisson.api.map.event.EntryUpdatedListener;
import org.redisson.client.codec.Codec;
import org.redisson.client.codec.StringCodec;
import java.util.Map;
import java.util.concurrent.*;
import java.util.function.Function;
public class TTLLocalCachedMap<K, V> {
private final RMapCache<K, V> redisMap;
private final Cache<K, V> localCache;
public TTLLocalCachedMap(RedissonClient redisson, String mapName, Codec codec, long maxLocalSize, long localTTLSeconds) {
this.redisMap = redisson.getMapCache(mapName, codec);
this.localCache = Caffeine.newBuilder()
.maximumSize(maxLocalSize)
.expireAfterWrite(localTTLSeconds, TimeUnit.SECONDS)
.build();
preloadFromRedis();
// 设置监听器:远端写入 -> 同步本地
redisMap.addListener((EntryCreatedListener<K, V>) event -> cacheIfTtlSet(event.getKey()));
redisMap.addListener((EntryUpdatedListener<K, V>) event -> cacheIfTtlSet(event.getKey()));
redisMap.addListener((EntryRemovedListener<K, V>) event -> localCache.invalidate(event.getKey()));
redisMap.addListener((EntryExpiredListener<K, V>) event -> localCache.invalidate(event.getKey()));
}
public TTLLocalCachedMap(RedissonClient redisson, String mapName, long maxLocalSize, long localTTLSeconds) {
this(redisson, mapName, StringCodec.INSTANCE, maxLocalSize, localTTLSeconds);
}
private void preloadFromRedis() {
Map<K, V> allEntries = redisMap.readAllMap();
for (Map.Entry<K, V> entry : allEntries.entrySet()) {
cacheIfTtlSet(entry.getKey());
}
}
private void cacheIfTtlSet(K key) {
long ttl = redisMap.remainTimeToLive(key);
if (ttl > 0) {
V value = redisMap.get(key);
if (value != null) {
localCache.put(key, value);
}
} else {
localCache.invalidate(key);
}
}
public void put(K key, V value, long ttl, TimeUnit unit) {
redisMap.put(key, value, ttl, unit);
if (ttl > 0) {
localCache.put(key, value);
}
}
public void put(K key, V value) {
redisMap.put(key, value); // no TTL => 本地不缓存
localCache.invalidate(key);
}
public CompletableFuture<Void> putAsync(K key, V value, long ttl, TimeUnit unit) {
return redisMap.putAsync(key, value, ttl, unit)
.toCompletableFuture()
.thenAccept(r -> {
if (ttl > 0) localCache.put(key, value);
});
}
public V get(K key) {
return localCache.get(key, k -> {
long ttl = redisMap.remainTimeToLive(k);
if (ttl > 0) {
return redisMap.get(k);
} else {
return null;
}
});
}
public void remove(K key) {
redisMap.remove(key);
localCache.invalidate(key);
}
public void clear() {
redisMap.clear();
localCache.invalidateAll();
}
public boolean containsKey(K key) {
return get(key) != null;
}
}
总结
本方案舍弃了 RLocalCachedMap 的复杂同步机制,而选择自己组合 RMapCache + Caffeine 的方式构建本地缓存,带来以下优势:
- 更好的可控性(只缓存 TTL key);
- 更灵活的替换策略(支持 LRU);
- 更少的 bug;
- 更清晰的同步逻辑。
这是一个简单但有效的分布式缓存优化实践,推荐在高性能系统中采用这种方案,尤其在对 TTL 管理要求严格的场景中。
如果你有类似的分布式缓存需求,欢迎交流或评论探讨~
你是否也遇到过 Redisson 中 RLocalCachedMap 带来的坑呢?
实践建议
- 确保 key 的 TTL 设置正确,否则会被自动踢出本地缓存;
- Caffeine 中的 TTL 应比 Redis 稍短一些,防止读取已过期值;
- 使用一致的 Codec(如 StringCodec)避免序列化异常;
- 如果 Redis 中有大量无 TTL 的 key,可考虑定期清理或隔离管理;
- 在服务启动时调用 preloadFromRedis(),能避免首次读取时的性能抖动。
相关推荐
- SQL入门知识篇(sql入门新手教程视频)
-
一、什么是数据库?什么是SQL?1、数据库:存放数据,可以很多人一起使用2、关系数据库:多张表+各表之间的关系3、一张表需要包含列、列名、行4、主键:一列(或一组列),其值能够唯一区分表中的每个行。5...
- postgresql实现跨库查询-dblink的妙用
-
技术导语:用惯了oracle的dblink,转战postgresql,会一时摸不着头脑。本期就重点详细讲解postgresql如何安装dblink模块及如何使用dblink实现跨库查询。安装cont...
- Oracle VM VirtualBox虚拟机软件(oracle vm virtualbox win10)
-
OracleVMVirtualBox是一款完全免费的虚拟机软件,下载银行有提供下载,软件支持安装windows、linux等多个操作系统,让用户可以在一台设备上实现多个操作系统的操作。同时软件有着...
- 开源 SPL 轻松应对 T+0(开源srs)
-
T+0问题T+0查询是指实时数据查询,数据查询统计时将涉及到最新产生的数据。在数据量不大时,T+0很容易完成,直接基于生产数据库查询就可以了。但是,当数据量积累到一定程度时,在生产库中进行大数据...
- 中小企业佳选正睿ZI1TS4-4536服务器评测
-
随着科技的不断发展,各行各业对于数据使用越加频繁,同时针对服务器的选择方面也就越来越多样化和细分化。那么对于我们用户来说,如何选择符合自身业务需求和最优性价比的产品呢?笔者将通过刚刚购买的这台服务器的...
- MFC转QT:Qt基础知识(mfc和qt的区别)
-
1.Qt框架概述Qt的历史和版本Qt是一个跨平台的C++应用程序开发框架,由挪威公司Trolltech(现为QtCompany)于1991年创建。Qt的发展历程:1991年:Qt项目启动1995年...
- 数据库,QSqlTableModel(数据库有哪些)
-
QMYSQL——mysqlQSQLITE——sqliteQOICQ——orcale所需头文件.pro增加sql#include<QSqlDatabase>#include<Q...
- python通过oledb连接dbf数据库(python连接jdbc)
-
起因:因为工作需要,需要读取dbf文件和系统数据中数据进行校对,因为知道dbf文件可以用sql查询,所以想能不能像mysql/oracle那样连接,再调用执行sql方法,通过一系列百度,尝试,最终通过...
- Excel常用技能分享与探讨(5-宏与VBA简介 VBA与数据库)
-
在VBA(VisualBasicforApplications)中使用数据库(如Access、SQLServer、MySQL等)具有以下优点,适用于需要高效数据管理和复杂业务逻辑的场景:1....
- Excel常用技能分享与探讨(5-宏与VBA简介 VBA与数据库-二)
-
以下是常见数据库软件的详细配置步骤,涵盖安装、驱动配置、服务启动及基本设置,确保VBA能够顺利连接:一、MicrosoftAccess适用场景:小型本地数据库,无需独立服务。配置步骤:安装Acces...
- Windows Docker 安装(docker安装windows容器)
-
Docker并非是一个通用的容器工具,它依赖于已存在并运行的Linux内核环境。Docker实质上是在已经运行的Linux下制造了一个隔离的文件环境,因此它执行的效率几乎等同于所部署的L...
- Windows下安装Ubuntu虚拟机方法(windows下安装ubuntu20)
-
在Windows下安装Ubuntu虚拟机。选择使OracleVMVirtualBox安装Ubuntu虚拟机。1.下载和安装OracleVMVirtualBox:访问OracleVMVir...
- java入门教程1 - 安装和配置(win和linux)
-
windows安装和配置安装javahttps://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html目前大部分项目的...
- Centos7 安装Tomcat8服务及配置jdk1.8教程
-
1、下载jdk1.8压缩包下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.htmltom...
- 全网最完整的免费java教程讲义(一)——java配置和安装
-
一,安装Java1)安装JDK要学习和使用java,首先需要安装JDK(JavaDevelopemntKit),相当于java安装包。Java的下载页在甲骨文官网上:https://www.or...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- SQL入门知识篇(sql入门新手教程视频)
- postgresql实现跨库查询-dblink的妙用
- Oracle VM VirtualBox虚拟机软件(oracle vm virtualbox win10)
- 开源 SPL 轻松应对 T+0(开源srs)
- 中小企业佳选正睿ZI1TS4-4536服务器评测
- MFC转QT:Qt基础知识(mfc和qt的区别)
- 数据库,QSqlTableModel(数据库有哪些)
- python通过oledb连接dbf数据库(python连接jdbc)
- Excel常用技能分享与探讨(5-宏与VBA简介 VBA与数据库)
- Excel常用技能分享与探讨(5-宏与VBA简介 VBA与数据库-二)
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)