关于后端:别再随意说-Redis-的-SET-保障原子性在客户端不一定

38次阅读

共计 3944 个字符,预计需要花费 10 分钟才能阅读完成。

一、分布式系统有一个特点,就是无论你学习积攒多少知识点,只有在分布式的阵线中,总能遇到各种超出主观意识的神奇问题。比方前文应用 Jedis 来实现分布式锁的技术知识点储备,本认为很稳不会再遇到什么问题,但理论状况却是啪啪打脸。
二、技术背景同步
为了关照一些同学不喜爱看连载,这里就必须把上下文再粘贴过去,否则内容不连贯,看起来不晦涩。
如果曾经看过《分布式锁中 - 基于 Redis 的实现如何防重入》和《分布式锁实战 - 偶遇 etcd 后就想摈弃 Redis?》的同学,能够跳过本大节【技术背景同步】,间接进入第三大节【诊断过程】。
2.1 如何应用 SET 指令来加锁
咱们应用的是 SET 指令来实现加锁的逻辑,指令模式如下:
SET 键值[NX | XX] [GET] [EX 秒 | PX 毫秒 | EXAT unix 工夫秒 | PXAT unix 工夫毫秒 | 放弃]

1)加锁胜利的逻辑是这样:

判断 key 是否存在
若 key 不存在,就设置 key
给 key 指定过期工夫

2)加锁不胜利的逻辑是这样:

判断 key 是否存在
若 key 已存在,则返回

SetParams params = SetParams.setParams().nx().ex(lockState.getLeaseTTL());
String result = client.set(lockState.getLockKey(), lockState.getLockValue(), params);

上边代码是之前《分布式锁中 - 基于 Redis 的实现需避坑 – Jedis 篇》中写的加锁逻辑,其中只依据失常加锁的返回值来判断是否加锁胜利,即 result 是不是 “OK”,但 key 已存在导致加锁不胜利的返回值到底是什么,应该如何判断呢?
2.2 SET 的返回值都有什么
在官网中,查看 SET 返回值的形容,为不便大家,这里间接贴出后果,应该很多同学都没看过这段形容吧。

简略字符串回复:OK 如果 SET 正确执行。
空回复:(nil)如果 SET 因为用户指定了 NX 或 XX 选项但不满足条件而未执行操作。
如果命令与 GET 选项一起收回,则上述内容不实用。它会改为如下回复,无论是否 SET 理论执行:
批量字符串回复:存储在键中的旧字符串值。
空回复:(nil)如果密钥不存在。

2.3 SET 指令加锁的论断
通过官网给出的形容能够得悉,以后 SET 指令的应用形式,只有返回的不是“OK”,就是锁已存在了,所以将《分布式锁中 - 基于 Redis 的实现需避坑 – Jedis 篇》示例中 tryLock 的逻辑中,退出一个判断锁类型的逻辑即可,即如果锁 key 已存在,并且锁是”一次性“锁,则不循环期待而是立刻返回。
2.4 有情的事实
应用 Jedis 客户端来实现分布式锁性能的时候,咱们发现并确认了,从客户端用户的视角来看 SET 指令的原子性语义并不一定能失去保障。
三、诊断过程
1)用户反馈,偶发一次防重入锁的加锁失败了
从日志的后果看,与这个 key 相干的加锁日志中,只有 SET 返回空,即 key 已存在的信息。
是不是有其余的程序也能够加锁,比方人工在 Redis 里设置了 key 或 还有其余的实例也在运行?
经确认,没有人工设置 key 的景象,整个程序在测试环境中只有 1 个实例,没有其余实例
2)没有足够的可观测信息,确实是看不出来哪里有问题
用 SkyWalking 中 @Trace 的办法 通过 Trace 以及 Tag 来记录几个狐疑点:

  1. 从用户申请进入到完结,加锁 SET 指令执行了几次
  2. SET 不胜利的时候,返回的后果到底是 OK 还是 空
  3. 如果 SET 返回的是空,通过 GET 查问一下,记录其 value,能够判断跟加锁时的 value 是否统一
    3)用户反馈,又呈现了
    我:通过 TraceId 信息查看 Trace,越不置信什么越出现什么:

只有一次无效的 SET 指令
SET 返回的是空
GET 返回有后果,并且 value 是 SET 指定的 value
SET 的耗时也不算太长,是 208ms

4)难道 SET 指令 并非官网所讲的成果,有什么坑?
通过直观的 Trace 信息,不再狐疑下层加锁逻辑和应用程序的逻辑,而把 Jedis 客户端和定位成最大怀疑对象,但一次景象还是短少一些研判的根据,再复现一下找一找法则,甚至也狐疑 Reids 服务端
5)法则呈现了,耗时偏长
问题再次出现,通过 Trace 信息来比照出问题的 SET 与 无问题的 SET 体现出了哪些差别,很快一个显著的特色被找了进去,出问题的 SET 指令的执行耗时 都在 200ms 以上,而没问题的 SET 的耗时 都在 20ms 以下。
6)200ms 是什么?
通过排查发现,Jedis 客户端几个超时工夫设置的是 200ms,莫非是哪个环节的超时导致了问题?
7)调试源码
从下边的调用堆栈,你是不是也发现一个单词挺让人生疑?没错 runWithRetries,它会重试。
execute:112, JedisCluster$2 (redis.clients.jedis)
execute:109, JedisCluster$2 (redis.clients.jedis)
runWithRetries:120, JedisClusterCommand (redis.clients.jedis)//》这里
run:31, JedisClusterCommand (redis.clients.jedis)
set:109, JedisCluster (redis.clients.jedis)

8)再看一看那几个超时工夫都是什么意思
public BinaryJedisCluster(Set<HostAndPort> jedisClusterNode, int connectionTimeout, int soTimeout, int maxAttempts, String password, GenericObjectPoolConfig poolConfig) {
this.connectionHandler = new JedisSlotBasedConnectionHandler(jedisClusterNode, poolConfig,

      connectionTimeout, soTimeout, password);

this.maxAttempts = maxAttempts;
}

构造函数里,能看到 几个要害参数的信息:

connectionTimeout = 200
soTimeout = 200
maxAttempts = 3

9)剖析 connectionTimeout
这是建连的耗时,推理一下,如果 200ms 都没连贯上,那么 200ms 后会有第二次连贯,连贯胜利后,再发指令。
这种状况下应该发一次指令就够了。
10)剖析 soTimeout
soTimeout 指定给了 socket。
public void connect() {
if (!isConnected()) {

try {socket = new Socket();
  ...
  socket.connect(new InetSocketAddress(host, port), connectionTimeout);
  socket.setSoTimeout(soTimeout);// 在这里

看权威解释:

Enable/disable SO_TIMEOUT with the specified timeout, in milliseconds. With this option set to a non-zero timeout, a read() call on the InputStream associated with this Socket will block for only this amount of time. If the timeout expires, a java.net.SocketTimeoutException is raised, though the Socket is still valid. The option must be enabled prior to entering the blocking operation to have effect. The timeout must be > 0. A timeout of zero is interpreted as an infinite timeout.

联合 JDK 正文解释一下本次遇到的状况:
通过 socket.setSoTimeout(int timeout)办法设置,socket 关联的 InputStream 的 read()办法会阻塞,直到超过设置的 soTimeout,就会抛出 SocketTimeoutException。当不设置这个参数时,默认值为无穷大,即 InputStream 的 read()办法会始终阻塞上来,除非连贯断开。
但重试逻辑外部把异样吞掉了,并从新收回执行指令的申请。
11)所以是重试 + soTimeout 的问题
模仿一个场景不便了解:

0ms 客户端收回第一个 SET 的指令
30ms 服务端收到第一个 SET 指令,存储后给客户端响应说第一个 SET 胜利,但响应返回的有点慢
200ms 客户端仍未收到 服务端的响应,呈现了超时异样,捕捉后,发动重试
201ms 客户端开始重试,收回第二个 SET 的指令
202ms 服务端给第一个 SET 的响应到了,但客户端不关怀了
204ms 服务端收到第二个 SET 指令,判断发现 key 已存在,给客户端响应说第二个 SET 失败
208ms 客户端收到 服务端第二个 SET 失败的响应。
而对于 Client 端最上层的 SET 使用者来说,成果是 SET 失败了,但 key 设置胜利了。

四、如何防止
既然是重试 + 超时工夫引发的,那么能够从此个性登程,将其配置的值进行调整,比方:

把 soTimeout 设置的足够大
勾销掉 Jedis 外部重试

但这两个参数既然能裸露给咱们应用,那么他们必然有其很重要的价值,这两种办法都只是尝试去防止问题,但并不能根治。
咱们既须要这些外围能力,又要防止遇到这类毁坏原子性语义的问题。读者敌人,您有没有什么好的方法来解决呢?

正文完
 0