Redis 客户端重试指南
阅读原文时间:2023年07月10日阅读:1

本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。

在互联网服务中,特别是在云环境下,网络及硬件环境复杂,所有应用程序都可能遇到暂时性故障。暂时性故障包括瞬时的网络抖动,服务暂时不可用,服务繁忙导致超时等,这些故障通常可以自我修复,在间隔一定时间后重试,则很大概率可以成功。

为什么会出现暂时性故障

云 Redis 集群的高可用

Redis 集群或者云环境中的很多产品,都是由大量机器组成的集群提供服务,机器可能发生各种各样的硬件故障,比如 CPU,内存,磁盘 都可能发生故障。当故障发生时,Redis 服务会自动做 HA (High Available),切换备用节点上来,客户端则可能遇到连接闪断,节点只读 (主动 HA 流程需要对主禁止写入,从而让数据完整同步到备)等暂时性故障。

客户已知的慢查询瞬间打满Redis

如果 Redis 在定期执行一些已知的慢查询,比如(keys, hgetall, smembers)等复杂度为 O(N) 的操作,则可能导致其余 O(1) 复杂度的 API 出现暂时性失败。

复杂的网络环境

云上网络环境复杂,客户端和服务器之间的网络状况会不时改变,偶现网络抖动,数据包重传等问题。

重试的准则

适当的重试次数与间隔

必须优化重试次数与间隔。如果重试次数不足或者间隔太长,应用程序无法完成操作,并且可能失败。如果重试次数太多或者间隔太短,则可能会增大程序对系统资源的占用,且有可能造成服务端堆积太多请求无法恢复。确定适当的重试间隔是最困难的部分,常见的重试间隔如指数退让、增量间隔、固定间隔[1]等。

避免重试嵌套

大多数情况下,应该避免重试嵌套,嵌套容易造成反复重试且无法停止。

记录重试过程异常,但只有最终失败被报告

重试过程中,建议按照 WARN 级别打印重试的错误日志,但是只有最终重试失败时候才抛出 Exception,重试过程中如果能完成请求的正确访问,则不会打扰到用户。

超时之后不一定就可以重试

超时是一种未决现象,配置超时时间 2s,那么客户端通常的行为是当命令发送之后开始计时,如果在 2s 内没有成功收到回复,则客户端抛出 Timeout Exception。但此时服务端有没有执行过命令呢?是未决的,因为超时可能发生在任何阶段:

  • 命令被客户端发出,但是还没有到达 Redis
  • 命令到达 Redis,但是执行时候超时
  • 命令执行结束,但是结果返回客户端时候超时

因此,对于有的命令,超时之后就不一定可以重试,是否可以重试的准则是命令执行是否幂等。

幂等操作才能重试

需要确定重试的操作是否为幂等的,例如:

  • set a b,就是幂等的,多次执行最终 a 的值只可能为 b 或者失败。
  • lpush list a,就不是幂等的,如果多次 lpush list a,则 list 中可能包含多个 a 元素。

Redis 常见客户端如何做重试

Redis 客户端开源生态繁荣,几乎各个编程语言都有对应的 Redis 客户端,Redis 官网(https://redis.io/clients)列出了很多 Redis 客户端,下面我们就常见的几个客户端讨论如何做重试。

Jedis

如果是 JedisCluster 模式,可以通过配置 maxAttempts 参数来定义失败情况下的重试次数,默认值为 5。此参数实际上是为了处理 Redis Cluster 集群路由变更时,客户端更新路由表用的,但是也可以用来作为 API 访问失败重试的控制参数。
如果是普通 JedisPool 模式,Jedis 本身不提供重试功能,因此我们通过 alibabacloud-tairjedis-sdk 封装了一个 Jedis 的重试类,可以方便你完成 Jedis 的重试操作。alibabacloud-tairjedis-sdk 是基于 Jedis 封装的,可以访问 Redis 社区版,并支持操作Redis企业版 的客户端,支持多种 Module 的操作命令。

1,添加依赖

<dependency>
  <groupId>com.aliyun.tair</groupId>
  <artifactId>alibabacloud-tairjedis-sdk</artifactId>
  <version>latest</version>
</dependency>

2,下面的代码会将 set key value 命令自动重试5次,且总的重试时间不超过10s,每次重试之间等待类指数间隔的时间,如果最终不成功,则抛出异常。

int maxRetries = 5; // 最大重试次数
Duration maxTotalRetriesDuration = Duration.ofSeconds(10); // 最大的重试时间
try {
    String ret = new JedisRetryCommand<String>(jedisPool, maxRetries, maxTotalRetriesDuration) {
        @Override
        public String execute(Jedis connection) {
            return connection.set("key", "value");
        }
    }.runWithRetries();
} catch (JedisException e) {
    // 表示尝试 maxRetries 次或者到达了最大查询时间 maxTotalRetriesDuration 仍旧没有访问成功
    e.printStackTrace();
}

Lettuce

Lettuce 本身不提供命令超时之后可以重试的参数,但是 Lettuce 有两种命令执行可靠性选项:

  • at-most-once execution:最多执行一次,即此命令执行零次或者一次,如果连接断开重连,则可能会丢失。
  • at-least-once execution(默认):最少执行一次,即使链接重连,也会将命令再次执行。

上面的两种选项都是通过 autoReconnect 这个客户端选项控制的:

clientOptions.isAutoReconnect() ? Reliability.AT_LEAST_ONCE : Reliability.AT_MOST_ONCE;

autoReconnect 默认是 true,即默认为 at-least-once,这个选项会带来的一个问题是,如果 Redis HA 之后,客户端可能堆积满的重试命令发过来会造成 Redis CPU 尖峰,社区也有人反馈这个问题,作者也在考虑引入一种新的过滤机制,可以控制重连之后重试逻辑。

Redisson

Redisson 提供了重试次数 retryAttempts (默认为3次) 和重试间隔 retryInterval (默认为1500ms) 两个参数来控制重试逻辑,如果想更改,可以参考下面的代码:

Config config = new Config();
config.useSingleServer()
    .setTimeout(1000)
    .setRetryAttempts(x)
    .setRetryInterval(x) //ms
    .setAddress("redis://127.0.0.1:6379");
RedissonClient connect = Redisson.create(config);

StackExchange.Redis

StackExchang.Redis 目前客户端本身只支持 connect 时候的重试,可参考

var conn = ConnectionMultiplexer.Connect("redis0:6380,redis1:6380,connectRetry=3");

对于 API 层面的重试,可以通过 Polly 来完成。

总结

本文总结了 Redis 为什么需要做重试,以及重试应当遵循的准则。并举例了常见客户端 Jedis、Lettuce、Redisson、StackExchange.Redis 如何做重试,对于 Jedis,因为其用户使用量最大且没有成熟方案,因此在 alibabacloud-tairjedis-sdk 中提供了重试类让用户集成使用。

参考:
[1] Transient fault handling https://docs.microsoft.com/zh-cn/azure/architecture/best-practices/transient-faults