根据字节面试改编原创小说:分布式锁的隐私太多了系列—锁被不安全的释放

320 阅读3分钟

​非广告:这张图片拍摄于四川彭州-星空泡泡屋,抖音网红民宿。

前文回顾

上一篇文章是分布式锁的第二篇-是谁偷偷释放了我们的锁。

回顾:

如果没有正确评估同步代码块的执行时长,线程1的同步代码块在设定的超时时间内没有执行完成,自动删除了key,导致线程2竞争拿到了锁。

可是线程1业务执行后,在finally代码块中,仍然会释放锁。释放锁的过程并没有判断是哪个线程加的锁,造成误伤,进而导致了线程2的加的锁被意外的释放。

我们采用的解决方案是,当加锁时,设置value为token值(可以是一个UUID),释放锁的时候判断token是否等于加锁时设置的token,由此只释放自己加的锁,并不会释放其他线程的锁。

问题反馈

并发经典问题

这个方案遭到了质疑:有小伙伴微信给我留言,反馈依然存在线程安全的问题。
首先,非常感谢小伙伴给与的宝贵意见:

图1:jsonL反馈问题

@jsonL 反馈存在线程安全的代码,我截图并用红色框框标注,如下图:

图2:疑似存在线程安全问题代码

可能你研究过并发编程,看到这个代码片段会比较敏感,比较典型的“读-写”操作,比如常见:i++。 看似一步操作,实际上,是由三步组成:

  • 获取i的值
  • 对i值+1
  • 将新的值赋值给i

因此,i++不具备原子性。同样,上述图片中的红色框框的代码,第一步去查询key的值,第二步删除key,一样不具备原子性。因此也是线程不安全的。那在分布式锁的场景下,这段代码会存在什么问题呢?我用时序图来说明:

图3:线程1和线程2时序图

不知道说到这里,你是否已经发现了问题,由于get(key)和del(key)的原子性被破坏,箭头所指向的位置,错误地释放了线程2的锁,线程2的锁被释放,分布式锁便真的失去了意义——因为同步代码块被多个线程或者多个进程中的线程同时访问。

如何保证两条命令的原子性

Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。

有了Lua脚本的支持,我们只需要在脚本中写好get+del逻辑,就可以保证两条命令的原子性,我们可以Lua脚本来安全释放锁:

if redis.call("GET",KEYS[1]) == ARGV[1]
then
    return redis.call("DEL",KEYS[1])
else
    return 0
end

图4:get+del的Lua脚本

你肯定有这样的疑问,Java如何调用Lua脚本呢?我们可以借助Java的客户端工具比如Jedis。

调用Lua的方法:jedis.eval(script, key, params)。

阶段小结

经过几波周折,分布式锁似乎已经看到雏形。能够解决我们大部分的场景和雏形。

图5:分布式锁阶段小结

我在一些互联网企业工作和甚至一些大厂对分布式锁的使用实际上也仅仅应用到如此。

然而,在使用分布式锁的时候,并不是没有发现还有其他的问题,只是走了一个捷径而已,在付出成本与实现上作了权衡。毕竟,其他的一些问题出现的概率还是比较小。并且一些老的系统自己封装的Redis分布式锁并未使用像Redission这样的框架,也是略显粗糙。

然而,分布式锁存在的问题我们并不能视而不见,下篇文章我们继续来聊。

高能提示:访问面试怪圈官网可获取更多面试、架构、算法、中间件等重量级资料。

image.png