Redis客户端二级缓存功能笔记

前言

redis+内存做二级缓存是很常见的套路了 但是一直有不太好处理的地方 例如 多客户端下 本地缓存和服务端缓存一致性问题
redis在6.x之后 增加了一个功能 client side caching tracking

https://lettuce.io/core/6.0.0.RC1/api/io/lettuce/core/support/caching/ClientSideCaching.html lettuce客户端接入实例
https://redis.io/docs/manual/client-side-caching/#other-hints-for-implementing-client-libraries redis client side caching tracking 官方文档
https://stackoverflow.com/questions/64885694/how-to-configure-client-side-caching-in-lettuce-6-spring-boot-2-4

官方文档 重要功能记录

https://redis.io/docs/manual/client-side-caching/#other-hints-for-implementing-client-libraries

跟踪模式
默认模式

在默认模式下,服务器会记住给定客户端访问的密钥,并在修改相同的密钥时发送失效消息。这会消耗服务器端的内存,但仅针对客户端可能在内存中拥有的密钥集发送失效消息。

  • 交互步骤
1. 客户端启用tracking  
2. 服务器记住每个客户端的的请求过的key 
3. 当key被修改后 所有启用tracking 和 可能缓存的key的客户端都会接受到失效消息通知 
4. 当客户端接受失效消息  删除本地的key 
大概示例:   
client1->server: client tracking on 
client1->server: get foo 
client2->server: del foo 
server->client1: invalidate "foo"
  • 服务端记录客户端和使用过的key原理
1. 服务器会记住可能在单个全局表中缓存给定密钥的客户端列表。此表称为失效表。失效表可以包含最大条目数。如果插入了新密钥,服务器可能会通过假装该密钥已被修改(即使未被修改)并向客户端发送失效消息来逐出较旧的条目。这样做,它可以回收用于此密钥的内存,即使这会强制具有密钥本地副本的客户端逐出它。    
2. 在失效表中,我们实际上不需要存储指向客户端结构的指针,这将在客户端断开连接时强制执行垃圾回收过程:相反,我们所做的只是存储客户端 ID(每个 Redis 客户端都有一个唯一的数字 ID)。如果客户端断开连接,则随着缓存槽失效,将增量对信息进行垃圾回收。      
3. 有一个 keys 命名空间,不按数据库编号划分。因此,如果客户端正在数据库 2 中缓存密钥,并且其他某个客户端更改了数据库 3 中的密钥值,则仍将发送失效消息。这样,我们可以忽略数据库数量,从而减少内存使用量和实现复杂性。foofoo       
  • redis协议实现tracking区别
    resp2: 使用两个连接 connection1 使用pub/sub 订阅 ’SUBSCRIBE __redis__:invalidate’消息来清理本地cache, connection2用来 开启tracking ‘CLIENT TRACKING on REDIRECT 4’
    resp3: 在一个连接中 跟踪和接受失效消息 使用push 类型来处理 resp3协议新增的
    使用hello 可以升级当前redis协议版本

https://redis.io/docs/reference/protocol-spec/ https://github.com/redis/redis-doc/blob/master/docs/reference/protocol-spec.md https://blog.csdn.net/LZH984294471/article/details/114233835

  • 跟踪内容
    默认情况下,客户端不需要告诉服务器哪些密钥 他们正在缓存。在只读上下文中提到的每个键 命令由服务器跟踪,因为它可以被缓存。
    使用【OPTIN】 模式 默认不跟踪查询类的key 使用 【CLIENT CACHING YES】命令之后下一个key会被跟踪
    使用【OPTOUT】 模式 默认跟踪查询类的key 使用 【CLIENT CACHING NO】命令之后下一个key不会被跟踪
    使用【NOLOOP】 模式 不发送当前连接本身关联key的变更通知
广播模式

在广播模式下,服务器不会尝试记住给定客户端访问的密钥,因此此模式在服务器端根本不会消耗内存。相反,客户端订阅键前缀(如 或 ),并在每次触摸与订阅前缀匹配的键时收到通知消息。object:user:
如果没有指定 前缀 则发送所有修改的key消息 如果使用 N个前缀 会匹配对应的前缀的key修改消息发送 注意是前缀匹配 如果存在前缀覆盖了 也会匹配上 例如 foo 和food前缀
如果不想接受key失效消息 使用 【NOLOOP】模式

广播模式客户端实现的时候 要注意在 断开连接、心跳异常等情况清理本地缓存
和使用比较合理的本地缓存实现 例如使用guava caffeine 之类的 不要直接使用简易map

使用 BCAST模式 几乎不消耗内存 非广播模式使用的内存和跟踪的key数量和客户端数量成正比

示例

https://www.lanmper.cn/redis/t9524 client tracking 命令

# CLIENT TRACKING ON|OFF[REDIRECT client-id][PREFIX prefix[PREFIX prefix ...]][BCAST][OPTIN][OPTOUT][NOLOOP]
# 停止跟踪 
CLIENT TRACKING OFF   
# 开启默认模式
CLIENT TRACKING  ON  
# 开启默认模式 指定id的连接
CLIENT TRACKING  ON  REDIRECT 【id】 
# 开启读取模式不跟踪 
CLIENT TRACKING  ON  REDIRECT 【id】 OPTIN 
# 读取模式不跟踪情况下  跟踪指定key 下一个key会被跟踪
CLIENT CACHING YES 
GET 【key】
# 开启读取模式跟踪   
CLIENT TRACKING  ON  REDIRECT 【id】 OPTOUT
# 读取模式跟踪情况下  不跟踪指定key 下一个key不会会被跟踪
CLIENT CACHING NO 
GET 【key】
# 开启广播模式 不带前缀 
BCASTPREFIXCLIENT TRACKING on REDIRECT 【id】 BCAST 
# 开启广播模式 带前缀 
BCASTPREFIXCLIENT TRACKING on REDIRECT 【id】 BCAST PREFIX 【前綴1】 PREFIX 【前綴2】
lettuce客户端demo

使用lettuce接入 client side cache tracking
使用map或者自定义的cache缓存实现

package com.ming.admin.cache;


import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.TrackingArgs;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.support.caching.CacheAccessor;
import io.lettuce.core.support.caching.CacheFrontend;
import io.lettuce.core.support.caching.ClientSideCaching;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * 测试演示  redis 两级缓存demo
 * https://lettuce.io/core/6.0.0.RC1/api/io/lettuce/core/support/caching/ClientSideCaching.html
 * https://redis.io/docs/manual/client-side-caching/#other-hints-for-implementing-client-libraries
 * https://stackoverflow.com/questions/64885694/how-to-configure-client-side-caching-in-lettuce-6-spring-boot-2-4
 * <p>
 * redis+本地缓存 痛点是 redis和本地数据同步问题
 * 在新的redis中  使用tracking 方式来进行通精致和同步
 * @author ming
 * @date 2023-03-02 11:25:25
 */
@Slf4j
public class RedisClientSideCachingDemoTest {

    private static final String HOST = "localhost";
    private static final int PORT = 6379;
    private static final String PASSWORD = "";

    @Test
    public void testClient() {
        CacheInfo cache = getCacheInfo("m-client");
        String key = "ming";
        String value = "fffff";
        //清理历史的key
        cache.getConnection().sync().del(key);
        printlnCacheData(cache);
        log.info("获取不存在的key:{}", cache.getCacheFrontend().get(key));
        printlnCacheData(cache);
        log.info("获取key如果不存在则加载value:{}", cache.getCacheFrontend().get(key, () -> key + ":" + value + "-" + System.currentTimeMillis()));
        printlnCacheData(cache);
        log.info("获取key:{}", cache.getCacheFrontend().get(key));
        printlnCacheData(cache);
    }

    @SneakyThrows
    @Test
    public void testMultiClient() {
        CacheInfo cacheInfo1 = getCacheInfo("client-1");
        CacheInfo cacheInfo2 = getCacheInfo("client-2");
        new Thread(() -> {
            for (; ; ) {
                cacheInfo1.getCacheFrontend().get("ming" + System.currentTimeMillis(), () -> "value" + System.currentTimeMillis());
                printlnCacheData(cacheInfo1);
            }
        }).start();

        new Thread(() -> {
            for (; ; ) {
                cacheInfo2.getCacheFrontend().get("ming2" + System.currentTimeMillis(), () -> "value2" + System.currentTimeMillis());
                printlnCacheData(cacheInfo2);
            }
        }).start();

        cacheInfo2.getConnection().sync().keys("*")
                .forEach(f -> cacheInfo2.getConnection().sync().del(f));
        Thread.sleep(100000L);
    }

    @Test
    public void testCustomLocalCache() {
        StatefulRedisConnection<String, String> connection = RedisClient.create(RedisURI.builder()
                .withHost(HOST)
                .withPort(PORT)
                .withPassword(PASSWORD.toCharArray())
                .build()).connect();
        MyCacheAccessor myCacheAccessor = new MyCacheAccessor();
        CacheFrontend<String, String> cacheFrontend = ClientSideCaching.enable(myCacheAccessor,
                connection,
                TrackingArgs.Builder.enabled());

        log.info(cacheFrontend.get("ming", () -> "value" + System.currentTimeMillis()));
        log.info(cacheFrontend.get("ming"));
        log.info(myCacheAccessor.toString());
    }

    public static class MyCacheAccessor implements CacheAccessor<String, String> {
        private static Cache<String, String> CACHE = CacheBuilder.newBuilder()
                .expireAfterWrite(1, TimeUnit.HOURS)
                .build();

        @Override
        public String get(String key) {
            return CACHE.getIfPresent(key);
        }


        @Override
        public void put(String key, String value) {
            CACHE.put(key, value);
        }


        @Override
        public void evict(String key) {
            CACHE.invalidate(key);
        }

        @Override
        public String toString() {
            return CACHE.stats().toString();
        }
    }


    private void printlnCacheData(CacheInfo cacheInfo) {
        log.info("{}-本地缓存数量:{},当前redis的缓存数量:{},\r\n---------------------------\r\n",
                cacheInfo.getName(),
                cacheInfo.getLocalMap().size(),
                cacheInfo.getConnection().sync().dbsize());
    }


    private CacheInfo getCacheInfo(String name) {
        StatefulRedisConnection<String, String> connection = RedisClient.create(RedisURI.builder()
                .withHost(HOST)
                .withPort(PORT)
                .withPassword(PASSWORD.toCharArray())
                .build()).connect();
        Map<String, String> map = new ConcurrentHashMap<>();
        CacheFrontend<String, String> cacheFrontend = ClientSideCaching.enable(CacheAccessor.forMap(map),
                connection,
                TrackingArgs.Builder.enabled());

        return new CacheInfo(name, cacheFrontend, connection, map);
    }


    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class CacheInfo {
        private String name;
        private CacheFrontend<String, String> cacheFrontend;
        private StatefulRedisConnection<String, String> connection;
        private Map<String, String> localMap;
    }
}

总结

多级缓存 很多情况下都用 就是有时候实现很纠结
redis在6.x之后增加了相关支持 用redis+内存实现两级缓存变的更加简单方便了

© 2024 ming博客. All rights reserved.基于rust salvo性能猛的很!