实战派 | Java项目中玩转Redis6.0客户端缓存
nanshan 2025-05-11 17:26 16 浏览 0 评论
铺垫
首先介绍一下今天要使用到的工具Lettuce,它是一个可伸缩线程安全的redis客户端。多个线程可以共享同一个RedisConnection,利用nio框架Netty来高效地管理多个连接。
放眼望向现在常用的redis客户端开发工具包,虽然能用的不少,但是目前率先拥抱redis6.0,支持客户端缓存功能的却不多,而lettuce就是其中的领跑者。
我们先在项目中引入最新版本的依赖,下面正式开始实战环节:
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.1.8.RELEASE</version>
</dependency>
实战
在项目中应用lettuce,开启并使用客户端缓存功能,只需要下面这一段代码:
public static void main(String[] args) throws InterruptedException {
// 创建 RedisClient 连接信息
RedisURI redisURI= RedisURI.builder()
.withHost("127.0.0.1")
.withPort(6379)
.build();
RedisClient client = RedisClient.create(redisURI);
StatefulRedisConnection<String, String> connect = client.connect();
Map<String, String> map = new HashMap<>();
CacheFrontend<String,String> frontend=ClientSideCaching.enable(CacheAccessor.forMap(map),
connect, TrackingArgs.Builder.enabled().noloop());
String key="user";
while (true){
String value = frontend.get(key);
System.out.println(value);
TimeUnit.SECONDS.sleep(10);
}
}
上面的代码主要完成了几项工作:
- 通过RedisURI配置redis连接的标准信息,并建立连接
- 创建用于充当本地缓存的Map,开启客户端缓存功能,建立一个缓存访问器CacheFrontend
- 在循环中使用CacheFrontend,不断查询同一个key对应的值并打印
启动上面的程序,控制台会不断的打印user对应的缓存,在启动一段时间后,我们在其他的客户端修改user对应的值,运行的结果如下:
可以看到,在其他客户端修改了key所对应的值后,打印结果也发生了变化。但是到这里,我们也不知道lettuce是不是真的使用了客户端缓存,虽然结果正确,但是说不定是它每次都重新执行了get命令呢?
所以我们下面来看看源码,分析一下具体的代码执行流程。
分析
在上面的代码中,最关键的类就是CacheFrontend了,我们再来仔细看一下上面具体实例化时的语句:
CacheFrontend<String,String> frontend=ClientSideCaching.enable(
CacheAccessor.forMap(map),
connect,
TrackingArgs.Builder.enabled().noloop()
);
首先调用了ClientSideCaching的enable()方法,我们看一下它的源码:
解释一下传入的3个参数:
- CacheAccessor:一个定义对客户端缓存进行访问接口,上面调用它的forMap方法返回的是一个MapCacheAccessor,它的底层使用的我们自定义的Map来存放本地缓存,并且提供了get、put、evict等方法操作Map
- StatefulRedisConnection:使用到的redis连接
- TrackingArgs:客户端缓存的参数配置,使用noloop后不会接收当前连接修改key后的通知
向redis服务端发送开启tracking的命令后,继续向下调用create()方法:
这个过程中实例化了一个重要对象,它就是实现了RedisCache接口的DefaultRedisCache对象,实际向redis执行查询时的get请求、写入的put请求,都是由它来完成。
实例化完成后,继续向下调用同名的create()方法:
在这个方法中,实例化了ClientSideCaching对象,注意一下传入的两个参数,通过前面的介绍也很好理解它们的分工:
- 当本地缓存存在时,直接从CacheAccessor中读取
- 当本地缓存不存在时,使用RedisCache从服务端读取
需要额外注意一下的是返回前的两行代码,先看第一句(行号114的那行)。
这里向RedisCache添加了一个监听,当监听到类型为invalidate的作废消息时,拿到要作废的key,传递给消费者。一般情况下,keys中只会有一个元素。
消费时会遍历当前ClientSideCaching的消费者列表invalidationListeners:
而这个列表中的所有,就是在上面的第二行代码中(行号115的那行)添加的,看一下方法的定义:
而实际传入的方法引用则是下面MapCacheAccessor的evict()方法,也就是说,当收到key作废的消息后,会移除掉本地缓存Map中缓存的这个数据。
客户端缓存的作废逻辑我们梳理清楚了,再来看看它是何时写入的,直接看ClientSideCaching的get()方法:
可以看到,get方法会先从本地缓存MapCacheAccessor中尝试获取,如果取到则直接返回,如果没有再使用RedisCache读取redis中的缓存,并将返回的结果存入到MapCacheAccessor中。
图解
源码看到这里,是不是基本逻辑就串联起来了,我们再画两张图来梳理一下这个流程。先看get的过程:
再来看一下通知客户端缓存失效的过程:
怎么样,配合这两张图再理解一下,是不是很完美?
其实也不是…回忆一下我们之前使用两级缓存Caffeine+Redis时,当时使用的通知机制,会在修改redis缓存后通知所有主机修改本地缓存,修改成为最新的值。目前的lettuce看来,显然不满足这一功能,只能做到作废删除缓存但是不会主动更新。
扩展
那么,如果想实现本地客户端缓存的实时更新,我们应该如何在现在的基础上进行扩展呢?仔细想一下的话,思路也很简单:
- 首先,移除掉lettuce的客户端缓存本身自带的作废消息监听器
- 然后,添加我们自己的作废消息监听器
回顾一下上面源码分析的图,在调用DefaultRedisCache的addInvalidationListener()方法时,其实是调用的是StatefulRedisConnection的addListener()方法,也就是说,这个监听器其实是添加在redis连接上的。
如果我们再看一下这个方法源码的话,就会发现,在它的附近还有一个对应的removeListener()方法,一看就是我们要找的东西,准备用它来移除消息监听。
不过再仔细看看,这个方法是要传参数的啊,我们明显不知道现在里面已经存在的PushListener有什么,所以没法直接使用,那么无奈只能再接着往下看看这个pushHandler是什么玩意…
通过注释可以知道,这个PushHandler就是一个用来操作PushListener的处理工具,虽然我们不知道具体要移除的PushListener是哪一个,但是惊喜的是,它提供了一个getPushListeners()方法,可以获取当前所有的监听器。
这样一来就简单了,我上来直接清除掉这个集合中的所有监听器,问题就迎刃而解了~
不过,在StatefulRedisConnectionImpl中的pushHandler是一个私有对象,也没有对外进行暴露,想要操作起来还是需要费上一点功夫的。下面,我们就在分析的结果上进行代码的修改。
魔改
首先,我们需要自定义一个工具类,它的主要功能是操作监听器,所以就命名为ListenerChanger好了。它要完成的功能主要有三个:
- 移除原有的全部消息监听
- 添加新的自定义消息监听
- 更新本地缓存MapCacheAccessor中的数据
首先定义构造方法,需要传入StatefulRedisConnection和CacheAccessor作为参数,在后面的方法中会用到,并且创建一个RedisCommands,用于后面向redis服务端发送get命令请求。
public class ListenerChanger<K, V> {
private StatefulRedisConnection<K, V> connection;
private CacheAccessor<K, V> mapCacheAccessor;
private RedisCommands<K, V> command;
public ListenerChanger(StatefulRedisConnection<K, V> connection,
CacheAccessor<K, V> mapCacheAccessor) {
this.connection = connection;
this.mapCacheAccessor = mapCacheAccessor;
this.command = connection.sync();
}
//其他方法先省略……
}
移除监听
前面说过,pushHandler是一个私有对象,我们无法直接获取和操作,所以只能先使用反射获得。PushHandler中的监听器列表存储在一个CopyOnWriteArrayList中,我们直接使用迭代器移除掉所有内容即可。
public void removeAllListeners() {
try {
Class connectionClass = StatefulRedisConnectionImpl.class;
Field pushHandlerField = connectionClass.getDeclaredField("pushHandler");
pushHandlerField.setAccessible(true);
PushHandler pushHandler = (PushHandler) pushHandlerField.get(this.connection);
CopyOnWriteArrayList<PushListener> pushListeners
= (CopyOnWriteArrayList) pushHandler.getPushListeners();
Iterator<PushListener> it = pushListeners.iterator();
while (it.hasNext()) {
PushListener listener = it.next();
pushListeners.remove(listener);
}
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
添加监听
这里我们模仿DefaultRedisCache中addInvalidationListener()方法的写法,添加一个监听器,除了最后处理的代码基本一致。对于监听到的要作废的keys集合,另外启动一个线程更新本地数据。
public void addNewListener() {
this.connection.addListener(new PushListener() {
@Override
public void onPushMessage(PushMessage message) {
if (message.getType().equals("invalidate")) {
List<Object> content = message.getContent(StringCodec.UTF8::decodeKey);
List<K> keys = (List<K>) content.get(1);
System.out.println("modifyKeys:"+keys);
// start a new thread to update cacheAccessor
new Thread(()-> updateMap(keys)).start();
}
}
});
}
本地更新
使用RedisCommands重新从redis服务端获取最新的数据,并更新本地缓存mapCacheAccessor中的数据。
private void updateMap(List<K> keys){
for (K key : keys) {
V newValue = this.command.get(key);
System.out.println("newValue:"+newValue);
mapCacheAccessor.put(key, newValue);
}
}
至于为什么执行这个方法时额外启动了一个新线程,是因为我在测试中发现,当在PushListener的onPushMessage方法中执行RedisCommands的get()方法时,会一直取不到值,但是像这样新启动一个线程就没有问题。
测试
下面,我们来写一段测试代码,来测试上面的改动。
public static void main(String[] args) throws InterruptedException {
// 省略之前创建连接代码……
Map<String, String> map = new HashMap<>();
CacheAccessor<String, String> mapCacheAccessor = CacheAccessor.forMap(map);
CacheFrontend<String, String> frontend = ClientSideCaching.enable(mapCacheAccessor,
connect,
TrackingArgs.Builder.enabled().noloop());
ListenerChanger<String, String> listenerChanger
= new ListenerChanger<>(connect, mapCacheAccessor);
// 移除原有的listeners
listenerChanger.removeAllListeners();
// 添加新的监听器
listenerChanger.addNewListener();
String key = "user";
while (true) {
String value = frontend.get(key);
System.out.println(value);
TimeUnit.SECONDS.sleep(30);
}
}
可以看到,代码基本上在之前的基础上没有做什么改动,只是在创建完ClientSideCaching后,执行了我们自己实现的ListenerChanger的两个方法。先移除所有监听器、再添加新的监听器。下面我们以debug模式启动测试代码,简单看一下代码的执行逻辑。
首先,在未执行移除操作前,pushHandler中的监听器列表中有一个监听器:
移除后,监听器列表为空:
在添加完自定义监听器、并且执行完第一次查询操作后,在另外一个redis客户端中修改user的值,这时PushListener会收到作废类型的消息监听:
启动一个新线程,查询redis中user对应的最新值,并放入cacheAccessor中:
当循环中CacheFrontend的get()方法再被执行时,会直接从cacheAccessor中取到刷新后的值,不需要再次去访问redis服务端了:
总结
到这里,我们基于lettuce的客户端缓存的基本使用、以及在这个基础上进行的魔改就基本完成了。可以看到,lettuce客户端已经在底层封装了一套比较成熟的API,能让我们在将redis升级到6.0以后,开箱即用式地使用客户端缓存这一新特性。在使用中,不需要我们关注底层原理,也不用做什么业务逻辑的改造,总的来说,使用起来还是挺香的。
来源:
https://mp.weixin.qq.com/s/QOq2zUI0AFGqc8lNtSmShQ
相关推荐
- 服务器数据恢复—Raid5数据灾难不用愁,Raid5数据恢复原理了解下
-
Raid5数据恢复算法原理:分布式奇偶校验的独立磁盘结构(被称之为raid5)的数据恢复有一个“奇偶校验”的概念。可以简单的理解为二进制运算中的“异或运算”,通常使用的标识是xor。运算规则:若二者值...
- 服务器数据恢复—多次异常断电导致服务器raid不可用的数据恢复
-
服务器数据恢复环境&故障:由于机房多次断电导致一台服务器中raid阵列信息丢失。该阵列中存放的是文档,上层安装的是Windowsserver操作系统,没有配置ups。因为服务器异常断电重启后,rai...
- 服务器数据恢复-V7000存储更换磁盘数据同步失败的数据恢复案例
-
服务器数据恢复环境:P740+AIX+Sybase+V7000存储,存储阵列柜上共12块SAS机械硬盘(其中一块为热备盘)。服务器故障:存储阵列柜中有磁盘出现故障,工作人员发现后更换磁盘,新更换的磁盘...
- 「服务器数据恢复」重装系统导致XFS文件系统分区丢失的数据恢复
-
服务器数据恢复环境:DellPowerVault系列磁盘柜;用RAID卡创建的一组RAID5;分配一个LUN。服务器故障:在Linux系统层面对LUN进行分区,划分sdc1和sdc2两个分区。将sd...
- 服务器数据恢复-ESXi虚拟机被误删的数据恢复案例
-
服务器数据恢复环境:一台服务器安装的ESXi虚拟化系统,该虚拟化系统连接了多个LUN,其中一个LUN上运行了数台虚拟机,虚拟机安装WindowsServer操作系统。服务器故障&分析:管理员因误操作...
- 「服务器数据恢复」Raid5阵列两块硬盘亮黄灯掉线的数据恢复案例
-
服务器数据恢复环境:HPStorageWorks某型号存储;虚拟化平台为vmwareexsi;10块磁盘组成raid5(有1块热备盘)。服务器故障:raid5阵列中两块硬盘指示灯变黄掉线,无法读取...
- 服务器数据恢复—基于oracle数据库的SAP数据恢复案例
-
服务器存储数据恢复环境:某品牌服务器存储中有一组由6块SAS硬盘组建的RAID5阵列,其中有1块硬盘作为热备盘使用。上层划分若干lun,存放Oracle数据库数据。服务器存储故障&分析:该RAID5阵...
- 「服务器虚拟化数据恢复」Xen Server环境下数据库数据恢复案例
-
服务器虚拟化数据恢复环境:Dell某型号服务器;数块STAT硬盘通过raid卡组建的RAID10;XenServer服务器虚拟化系统;故障虚拟机操作系统:WindowsServer,部署Web服务...
- 服务器数据恢复—RAID故障导致oracle无法启动的数据恢复案例
-
服务器数据恢复环境:某品牌服务器中有一组由4块SAS磁盘做的RAID5磁盘阵列。该服务器操作系统为windowsserver,运行了一个单节点Oracle,数据存储为文件系统,无归档。该oracle...
- 服务器数据恢复—服务器磁盘阵列常见故障表现&解决方案
-
RAID(磁盘阵列)是一种将多块物理硬盘整合成一个虚拟存储的技术,raid模块相当于一个存储管理的中间层,上层接收并执行操作系统及文件系统的数据读写指令,下层管理数据在各个物理硬盘上的存储及读写。相对...
- 「服务器数据恢复」IBM某型号服务器RAID5磁盘阵列数据恢复案例
-
服务器数据恢复环境:IBM某型号服务器;5块SAS硬盘组成RAID5磁盘阵列;存储划分为1个LUN和3个分区:第一个分区存放windowsserver系统,第二个分区存放SQLServer数据库,...
- 服务器数据恢复—Zfs文件系统下误删除文件如何恢复数据?
-
服务器故障:一台zfs文件系统服务器,管理员误操作删除服务器上的数据。服务器数据恢复过程:1、将故障服务器所有磁盘编号后取出,硬件工程师检测所有硬盘后没有发现有磁盘存在硬件故障。以只读方式将全部磁盘做...
- 服务器数据恢复—Linux+raid5服务器数据恢复案例
-
服务器数据恢复环境:某品牌linux操作系统服务器,服务器中有4块SAS接口硬盘组建一组raid5阵列。服务器中存放的数据有数据库、办公文档、代码文件等。服务器故障&检测:服务器在运行过程中突然瘫痪,...
- 服务器数据恢复—Sql Server数据库数据恢复案例
-
服务器数据恢复环境:一台安装windowsserver操作系统的服务器。一组由8块硬盘组建的RAID5,划分LUN供这台服务器使用。在windows服务器内装有SqlServer数据库。存储空间LU...
- 服务器数据恢复—阿里云ECS网站服务器数据恢复案例
-
云服务器数据恢复环境:阿里云ECS网站服务器,linux操作系统+mysql数据库。云服务器故障:在执行数据库版本更新测试时,在生产库误执行了本来应该在测试库执行的sql脚本,导致生产库部分表被tru...
你 发表评论:
欢迎- 一周热门
-
-
爱折腾的特斯拉车主必看!手把手教你TESLAMATE的备份和恢复
-
如何在安装前及安装后修改黑群晖的Mac地址和Sn系列号
-
[常用工具] OpenCV_contrib库在windows下编译使用指南
-
WindowsServer2022|配置NTP服务器的命令
-
Ubuntu系统Daphne + Nginx + supervisor部署Django项目
-
WIN11 安装配置 linux 子系统 Ubuntu 图形界面 桌面系统
-
解决Linux终端中“-bash: nano: command not found”问题
-
NBA 2K25虚拟内存不足/爆内存/内存占用100% 一文速解
-
Linux 中的文件描述符是什么?(linux 打开文件表 文件描述符)
-
K3s禁用Service Load Balancer,解决获取浏览器IP不正确问题
-
- 最近发表
-
- 服务器数据恢复—Raid5数据灾难不用愁,Raid5数据恢复原理了解下
- 服务器数据恢复—多次异常断电导致服务器raid不可用的数据恢复
- 服务器数据恢复-V7000存储更换磁盘数据同步失败的数据恢复案例
- 「服务器数据恢复」重装系统导致XFS文件系统分区丢失的数据恢复
- 服务器数据恢复-ESXi虚拟机被误删的数据恢复案例
- 「服务器数据恢复」Raid5阵列两块硬盘亮黄灯掉线的数据恢复案例
- 服务器数据恢复—基于oracle数据库的SAP数据恢复案例
- 「服务器虚拟化数据恢复」Xen Server环境下数据库数据恢复案例
- 服务器数据恢复—RAID故障导致oracle无法启动的数据恢复案例
- 服务器数据恢复—服务器磁盘阵列常见故障表现&解决方案
- 标签列表
-
- linux 查询端口号 (58)
- docker映射容器目录到宿主机 (66)
- 杀端口 (60)
- yum更换阿里源 (62)
- internet explorer 增强的安全配置已启用 (65)
- linux自动挂载 (56)
- 禁用selinux (55)
- sysv-rc-conf (69)
- ubuntu防火墙状态查看 (64)
- windows server 2022激活密钥 (56)
- 无法与服务器建立安全连接是什么意思 (74)
- 443/80端口被占用怎么解决 (56)
- ping无法访问目标主机怎么解决 (58)
- fdatasync (59)
- 405 not allowed (56)
- 免备案虚拟主机zxhost (55)
- linux根据pid查看进程 (60)
- dhcp工具 (62)
- mysql 1045 (57)
- 宝塔远程工具 (56)
- ssh服务器拒绝了密码 请再试一次 (56)
- ubuntu卸载docker (56)
- linux查看nginx状态 (63)
- tomcat 乱码 (76)
- 2008r2激活序列号 (65)