Escuela de combate real | ¡Juega al caché del cliente Redis6.0 en el proyecto Java!

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 Nettypara 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 RedisURIconfigurar 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 CacheFrontendconsultar continuamente el valor correspondiente a la misma tecla e imprimir

Cuando se inicia el programa anterior, la consola imprimirá continuamente el usercaché 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 lettucerealmente se usa el caché del cliente. Aunque el resultado es correcto, ¿tal vez vuelve a ejecutar el getcomando 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()
);
复制代码

首先调用了ClientSideCachingenable()方法,我们看一下它的源码:

解释一下传入的3个参数:

  • CacheAccessor:一个定义对客户端缓存进行访问接口,上面调用它的forMap方法返回的是一个MapCacheAccessor,它的底层使用的我们自定义的Map来存放本地缓存,并且提供了getputevict等方法操作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的那行)添加的,看一下方法的定义:

而实际传入的方法引用则是下面MapCacheAccessorevict()方法,也就是说,当收到key作废的消息后,会移除掉本地缓存Map中缓存的这个数据。

客户端缓存的作废逻辑我们梳理清楚了,再来看看它是何时写入的,直接看ClientSideCachingget()方法:

可以看到,get方法会先从本地缓存MapCacheAccessor中尝试获取,如果取到则直接返回,如果没有再使用RedisCache读取redis中的缓存,并将返回的结果存入到MapCacheAccessor中。

图解

源码看到这里,是不是基本逻辑就串联起来了,我们再画两张图来梳理一下这个流程。先看get的过程:

再来看一下通知客户端缓存失效的过程:

怎么样,配合这两张图再理解一下,是不是很完美?

其实也不是…回忆一下我们之前使用两级缓存Caffeine+Redis时,当时使用的通知机制,会在修改redis缓存后通知所有主机修改本地缓存,修改成为最新的值。目前的lettuce看来,显然不满足这一功能,只能做到作废删除缓存但是不会主动更新。

扩展

那么,如果想实现本地客户端缓存的实时更新,我们应该如何在现在的基础上进行扩展呢?仔细想一下的话,思路也很简单:

  • 首先,移除掉lettuce的客户端缓存本身自带的作废消息监听器
  • 然后,添加我们自己的作废消息监听器

回顾一下上面源码分析的图,在调用DefaultRedisCacheaddInvalidationListener()方法时,其实是调用的是StatefulRedisConnectionaddListener()方法,也就是说,这个监听器其实是添加在redis连接上的。

如果我们再看一下这个方法源码的话,就会发现,在它的附近还有一个对应的removeListener()方法,一看就是我们要找的东西,准备用它来移除消息监听。

不过再仔细看看,这个方法是要传参数的啊,我们明显不知道现在里面已经存在的PushListener有什么,所以没法直接使用,那么无奈只能再接着往下看看这个pushHandler是什么玩意…

通过注释可以知道,这个PushHandler就是一个用来操作PushListener的处理工具,虽然我们不知道具体要移除的PushListener是哪一个,但是惊喜的是,它提供了一个getPushListeners()方法,可以获取当前所有的监听器。

这样一来就简单了,我上来直接清除掉这个集合中的所有监听器,问题就迎刃而解了~

不过,在StatefulRedisConnectionImpl中的pushHandler是一个私有对象,也没有对外进行暴露,想要操作起来还是需要费上一点功夫的。下面,我们就在分析的结果上进行代码的修改。

魔改

首先,我们需要自定义一个工具类,它的主要功能是操作监听器,所以就命名为ListenerChanger好了。它要完成的功能主要有三个:

  • 移除原有的全部消息监听
  • 添加新的自定义消息监听
  • 更新本地缓存MapCacheAccessor中的数据

首先定义构造方法,需要传入StatefulRedisConnectionCacheAccessor作为参数,在后面的方法中会用到,并且创建一个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();
    }
}
复制代码

添加监听

这里我们模仿DefaultRedisCacheaddInvalidationListener()方法的写法,添加一个监听器,除了最后处理的代码基本一致。对于监听到的要作废的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);
    }
}
复制代码

至于为什么执行这个方法时额外启动了一个新线程,是因为我在测试中发现,当在PushListeneronPushMessage方法中执行RedisCommandsget()方法时,会一直取不到值,但是像这样新启动一个线程就没有问题。

测试

下面,我们来写一段测试代码,来测试上面的改动。

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中:

CacheFrontendCuando el método en el ciclo get()se ejecuta nuevamente, el valor actualizado se obtendrá directamente cacheAccessorde él necesario acceder nuevamente al servidor redis:

Resumir

En este punto, lettuceel 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 lettucecliente 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

Con la introducción del "almacenamiento en caché del lado del cliente", Redis6 puede comprender la reproducción del caché...

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 .

Supongo que te gusta

Origin juejin.im/post/7120148572846178335
Recomendado
Clasificación