Redis 客户端重试指南

本作品采用知识共享署名-非商业性使用 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
  1. 对于标准版或者带 Proxy 的集群版实例,即 JedisPool 模式,您需要使用大于 4.0.0 的版本,推荐使用最新的 Jedis 版本,下面的代码以 Jedis 5.0.0 为例。​
    ● 添加 Jedis 的 Pom 依赖。
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>5.0.0</version>
</dependency>

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

PooledConnectionProvider provider = new PooledConnectionProvider(HostAndPort.from("127.0.0.1:6379"));
int maxAttempts = 5; // 最大重试次数
Duration maxTotalRetriesDuration = Duration.ofSeconds(10); // 最大的重试时间
UnifiedJedis jedis = new UnifiedJedis(provider, maxAttempts, maxTotalRetriesDuration);
try {
    System.out.println("set key: " + jedis.set("key", "value"));
} catch (Exception e) {
    // 表示尝试 maxAttempts 次或者到达了最大查询时间 maxTotalRetriesDuration 仍旧没有访问成功
    e.printStackTrace();
}
  1. 对于集群直连版实例,即 JedisCluster 模式,也可以通过配置 maxAttempts 参数来定义失败情况下的重试次数,默认值为 5,代码示例如下:
HostAndPort hostAndPort = HostAndPort.from("127.0.0.1:30001");
int connectionTimeout = 5000;
int soTimeout = 2000;
int maxAttempts = 5;
ConnectionPoolConfig config = new ConnectionPoolConfig();
JedisCluster jedisCluster = new JedisCluster(hostAndPort, connectionTimeout, soTimeout, maxAttempts, config);
try {
    System.out.println("set key: " + jedisCluster.set("key", "value"));
} catch (Exception e) {
    // 表示尝试 maxAttempts 之后仍旧没有访问成功
    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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨博东的博客

请我喝瓶可乐鼓励下~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值