Redis 6.0新特性客户端缓存

doMore 81 2025-02-17

参考文章: https://redis.io/docs/latest/develop/reference/client-side-caching/

客户端缓存

顾名思义,客户端缓存就是Redis的客户端利用自身的存储缓存Redis的key,当需要读取某个key时,优先从自身的缓存中读取。Redis 6新增的客户端缓存功能是指Redis提供了一种机制,可以让Redis的客户端更好地实现自身的缓存。Redis通常作为数据库的缓存,用于降低数据库的压力和提高服务性能。对于一些高性能的服务而言,有时候也会将客户端的内存作为一级缓存,将Redis作为二级缓存。在这种模式下,客户端的一级缓存能够进一步缩短服务的处理时间,提高服务性能。对于Redis 6之前的版本而言,我们通常需要通过Pub/Sub来保证数据的一致性, Redis 6 能够更好地实现客户端缓存功能,本文将详细介绍Redis客户端缓存功能的实现。

使用

Tracking分为两种模式:默认模式和广播模式。
完整命令如下:
CLIENT TRACKING <ON | OFF> [REDIRECT client-id] [PREFIX prefix [PREFIX prefix …]] [BCAST] [OPTIN] [OPTOUT] [NOLOOP]

1)OPTIN:启用该选项后,默认所有的键都不会被追踪。需要配合 CLIENT CACHING yes 使用。

# 表示 key3 将会被追踪
CLIENT CACHING YES
GET key3

2)OPTOUT:与OPTIN相反,启用该选项后,默认所有的键都会被追踪,除非使用 CLIENT CACHING no
3)NOLOOP:启用该选项后,客户端不再收到自己修改的key的失效消息。
4)REDIRECT:重定向模式。该模式兼容RESP 2,可将待发送的失效消息发给另一个指定客户端。

默认模式

在默认模式下,Redis Server会记录每个Redis Client访问的key,当key发生变更时,Redis Server便向Redis Client推送数据过期消息。很明显,当数据量比较大时,Redis Serve会有很大的存储压力。

在默认模式下,Redis Server会记录每个Redis Client访问的key,当key发生变更时,Redis Server便向Redis Client推送数据过期消息。很明显,当数据量比较大时,Redis Serve会有很大的存储压力:

# 开启 resp 3 协议
> hello 3
# 开启客户端缓存 tracking 功能
> client tracking on 
OK
# 监听 test
> get test 

# 重新启动一个 redis 连接,修改 test 的值,就会收到推送的失效消息
>client 2
# 注意:此处是 resp 3 协议内容
$10
invalidate
*1
$4
test

使用默认模式需要注意以下几点。
1)客户端监听的key如果在别处被修改为与原值一样,客户端也会收到失效消息。
2)监听后,客户端只会收到key的一次失效消息,即该key再被修改时,客户端不会再收到消息,客户端需要再次查询该key,才能继续监听该key。
3)当监听的key由于服务端触发过期淘汰策略而被清除时,客户端也会收到消息。

广播模式

默认模式可以让Redis Client跟踪特定的key,但是实际使用时,我们经常需要跟踪满足某个前缀条件的所有key。针对这种情况,我们可以使用广播模式。在广播模式下,客户端可以订阅匹配某一前缀的广播(也可订阅空串,表示订阅所有广播)​。在这种模式下,服务端只需要记录被订阅的广播的前缀与Redis Client的对应关系即可,当满足条件的key发生变化时就通知对应的Redis Client。相比于采用默认模式,采用广播模式,服务端不再需要消耗过多内存用于存储Redis Client访问的key,但是可能会发送给Redis Client并不关心的key。

广播模式使用示例:

> hello 3

> client tracking on bcast prefix xxx

> get xxx_test

# 重新启动一个 redis 连接,修改 test 的值,就会收到推送的失效消息
>client 2
# 注意:此处是 resp 3 协议内容
$10
invalidate
*1
$8
xxx_test

使用广播模式需要注意以下几点。
1)符合前缀的key出现新增、修改、删除、过期、淘汰等动作,客户端都会收到通知。
2)与默认模式不同,客户端可多次收到符合前缀的key的失效消息,无须反复监听。

转发功能

默认模式及广播模式因为都需要Redis Server主动向Redis Client推送消息,所以都需要RESP 3协议。
对于使用 RESP 2 的用户可以通过客户端缓存的转发功能进行改造。
Redis Client 1启动Tracking重定向功能后,可以将后续消息转发给Redis Client 2,Redis Client 2需要订阅 redis:invalidate 频道。之后当Redis Client 3修改Redis Client 1监听的key后,Redis Server就会向 redis:invalidate 频道发送消息,Redis Client 2就可以接收到这个消息,进而更新自己的本地缓存。

使用转发功能需要注意以下几点。
1)转发功能只能指定一个Redis Client,这个Redis Client可以是自己。
2)转发功能可以基于默认模式,也可以基于广播模式。
3)转发功能需要Redis Client订阅 redis:invalidate 频道

Java 实现

注意:从 Lettuce 6.0 版本开始,它支持 RESP 3 协议。Jedis 客户端尚未官方支持 RESP 3 协议。

1. 添加依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
    </dependency>
</dependencies>

2. 配置 RedisTemplate

import io.lettuce.core.ClientOptions;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.codec.StringCodec;
import io.lettuce.core.protocol.ProtocolVersion;
import io.lettuce.core.resource.ClientResources;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        // 配置 Lettuce 客户端使用 RESP 3 协议
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
               .clientOptions(ClientOptions.builder()
                       .protocolVersion(ProtocolVersion.RESP3)
                       .build())
               .commandTimeout(Duration.ofSeconds(10))
               .build();

        // 创建 Redis URI
        RedisURI redisUri = RedisURI.create("redis://localhost:6379");

        // 创建 Lettuce 连接工厂
        return new LettuceConnectionFactory(redisUri, clientConfig);
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, String> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }
}

实现 CLIENT TRACKING

import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;

@Service
public class RedisTrackingService {

    private final RedisTemplate<String, String> redisTemplate;

    public RedisTrackingService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 开启客户端跟踪功能
     */
    public void enableClientTracking() {
        redisTemplate.execute((RedisConnection connection) -> {
            // 发送 CLIENT TRACKING ON 命令开启跟踪
            byte[] command = "CLIENT".getBytes(StandardCharsets.UTF_8);
            byte[][] args = {
                    "TRACKING".getBytes(StandardCharsets.UTF_8),
                    "ON".getBytes(StandardCharsets.UTF_8)
            };
            connection.execute(command, args);
            return null;
        });
    }

    /**
     * 处理跟踪通知
     */
    public void handleTrackingNotifications() {
        redisTemplate.execute((RedisConnection connection) -> {
            while (true) {
                // 从 Redis 接收消息
                byte[] message = connection.receive();
                if (message != null) {
                    String notification = new String(message, StandardCharsets.UTF_8);
                    System.out.println("Received tracking notification: " + notification);
                }
            }
        });
    }
}

4. 测试

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RedisTrackingApplication implements CommandLineRunner {

    @Autowired
    private RedisTrackingService redisTrackingService;

    public static void main(String[] args) {
        SpringApplication.run(RedisTrackingApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        // 开启客户端跟踪功能
        redisTrackingService.enableClientTracking();

        // 处理跟踪通知
        redisTrackingService.handleTrackingNotifications();
    }
}

注意事项

  • 确保你的 Redis 服务器版本为 6.0 或以上,因为 CLIENT TRACKING 是在 Redis 6.0 中引入的。
  • 上述代码中的 handleTrackingNotifications 方法使用了一个无限循环来接收通知,实际应用中需要根据需求进行调整。