En el artículo anterior, presentamos la nueva función de caché del clienteclient-side caching
en Redis 6.0, simulamos el cliente a través de una conexión telnet y probamos tres modos de funcionamiento de la caché del cliente. el almacenamiento en caché debe implementarse en un proyecto java.
lecho
Primero, presentemos la herramienta que se usará hoy Lettuce
, es un cliente redis escalable seguro para subprocesos. Múltiples subprocesos pueden compartir el mismo RedisConnection
, aprovechando el marco nio Netty
para administrar de manera eficiente múltiples conexiones.
Mirando los kits de desarrollo de clientes de redis que se usan comúnmente hoy en día, aunque hay muchos de ellos, es el primero en adoptar redis 6.0, pero no muchos admiten la función de almacenamiento en caché del lado del cliente, y la lechuga es el líder entre ellos.
Primero introducimos la última versión de las dependencias en el proyecto y luego comenzamos oficialmente el enlace de combate real:
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.1.8.RELEASE</version>
</dependency>
复制代码
combate real
Para aplicar lechuga en el proyecto, habilite y use la función de almacenamiento en caché del lado del cliente, solo necesita el siguiente código:
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);
}
}
复制代码
El código anterior completa principalmente varias tareas:
- Al
RedisURI
configurar la información estándar de la conexión redis y establecer la conexión - Cree un caché para que actúe como un caché local
Map
, habilite el almacenamiento en caché del lado del cliente y cree un acceso de cachéCacheFrontend
- Se utiliza en un bucle para
CacheFrontend
consultar continuamente el valor correspondiente a la misma tecla e imprimir
Cuando se inicia el programa anterior, la consola imprimirá continuamente el user
caché correspondiente, después de un período de tiempo, modificamos el valor correspondiente en otros clientes user
, el resultado de la operación es el siguiente:
Se puede ver que después de que otros clientes modifican el valor correspondiente a la clave, el resultado de la impresión también cambia. Pero aquí, no sabemos si lettuce
realmente se usa el caché del cliente. Aunque el resultado es correcto, ¿tal vez vuelve a ejecutar el get
comando cada vez?
Así que echemos un vistazo al código fuente y analicemos el proceso de ejecución de código específico.
analizar
在上面的代码中,最关键的类就是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
Cuando el método en el ciclo get()
se ejecuta nuevamente, el valor actualizado se obtendrá directamente cacheAccessor
de él necesario acceder nuevamente al servidor redis:
Resumir
En este punto, lettuce
el uso básico del caché del lado del cliente en el que nos basamos y los cambios mágicos realizados sobre esta base están básicamente completados. Se puede ver que el lettuce
cliente ha encapsulado una API relativamente madura en la capa inferior, lo que nos permite usar la nueva función de almacenamiento en caché del lado del cliente de forma inmediata después de actualizar redis a 6.0. En uso, no necesitamos prestar atención a los principios subyacentes, ni necesitamos hacer ninguna transformación de lógica de negocios. En general, es bastante delicioso de usar.
Entonces, eso es todo por este intercambio, soy Hydra, nos vemos en el próximo artículo.
Lectura recomendada
Sobre el autor,
码农参上
, una cuenta pública a la que le encanta compartir, interesante, profunda y directa, charlando contigo sobre tecnología. Bienvenido a agregar amigos para una mayor comunicación.
Estoy participando en el reclutamiento del programa de firma de creadores de la Comunidad Tecnológica de Nuggets, haga clic en el enlace para registrarse y enviar .