本作品采用知识共享署名-非商业性使用 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

最新文章

  1. [转]基于display:table的CSS布局
  2. Hibernate uniqueResult方法的使用
  3. Sqlite 数据库出现database disk image is malformed报错的解决方法
  4. “Adobe Flash Player因过期而遭到阻止”的解决办法
  5. 常州培训 day7 解题报告
  6. C++ STL之排序算法
  7. 不相交集合ADT
  8. Eclipse开发PHP环境配置
  9. C 和 OC 字符串转换 NSString 和 char * 转换 const char* 与 char *
  10. PyQt IDE 环境搭建
  11. Javascript高级编程学习笔记(51)—— DOM2和DOM3(3)操作样式表
  12. Java思维理清思路
  13. SqlServer 技术点总结(持续更新)
  14. openx 添加新表和据库表和字段
  15. python3.6 连接mysql数据库
  16. 新电脑装不了win7?来试试我的方法!
  17. 高性能网站架构缓存——redis集群
  18. 【LeetCode】45. Jump Game II
  19. springboot和quartz整合实现动态定时任务(持久化单节点)
  20. (转)Asp.Net生命周期系列五

热门文章

  1. Centos7 搭建sonarQube
  2. javascript Date 日期格式化 formatDate, require.js 模块 支持全局js引入 / amd方式加载
  3. JDBC封装的工具类
  4. 安装redis3.0.5
  5. [转载]Nginx负载均衡配置实例详解
  6. vm 将宿主机文件夹 映射至 虚拟机
  7. Chrome浏览器启动参数大全(命令行参数)
  8. Qt和JavaScript使用QWebChannel交互一——和Qt内嵌网页交互
  9. CI/CD-企业级DevOps
  10. 熊猫分布密度制图(ArcPy实现)