每日一面 - 假设 Redis 基本可靠,如何用单进程 Redis 实现分布式悲观锁

麻烦你们帮我投一票哈,谢谢redis

什么是分布式锁

针对共享内存模型的程序(例如JAVA程序),锁就是一个很是经常使用的机制。 通常简单分为悲观锁和乐观锁。悲观锁就是你获取这块数据的锁以后,别人就没法访问或操做这块数据,直到你释放这个锁。乐观锁通常就是CAS更新。 在单进程内内存的锁,只控制进程内数据的,就是非分布式锁。相反的,跨进程,须要锁住多个进程访问数据的锁就是分布式锁。缓存

悲观锁通常由Redis的SETNXEX实现,Key 为资源名,EX 设置一个合理的超时时间, Value 设置为一个客户端生成的在超时时间内不会重复的随机字符串。安全

lua脚本特性说明

因为redis是单线程的,执行lua脚本的时候,不会同时执行其余客户端命令,这必定程度上保证了并发安全网络

单进程Redis分布式悲观锁实现思路

悲观锁通常由Redis的SETNX实现,经过SETNX获取一个锁,释放锁就把这个Key删除便可。 例如,获取一个名为TestLock的锁,经过SETNX TestLock TestLockValue,TestLockValue随意设置一个值。SETNX是SET IF NOT EXISTS,若是不存在就会设置成功。 设置成功会返回1,没设置成功会返回0。返回1表明获取锁成功,0表明没获取成功。 释放锁调用DEL命令,经过DEL TestLock释放锁。返回1表明释放锁成功,0表明没有这个锁。并发

可是若是这么简单实现,在以下场景时,会有问题:dom

  1. 客户端1获取锁成功。
  2. 客户端1死掉了,没能释放锁。
  3. 其余客户端再也获取不了这个锁了。

因此,考虑这个,咱们通常会给锁设置一个超时时间。在超过这个超时时间以后,redis会自动把这个key删除掉。这样,在客户端1死掉了以后,不会致使这个锁永远不释放。分布式

这样redis命令集合就变成了: 获取锁:lua

> SETNX TestLock TestLockValue
> EXPIRE TestLock 8

释放锁:url

> DEL TestLock

因为SETNX还有EXPIRE是两条命令,在以下场景还可能出问题:.net

  1. 客户端1执行SETNX TestLock TestLockValue成功。
  2. 客户端1死掉了。没有设置过时时间。
  3. 其余客户端再也获取不了这个锁了。

Redis 2.8 版本中做者加入了 set 指令的扩展参数,使得 setnx 和 expire 指令能够一块儿执行

这样redis命令集合就变成了: 获取锁:

> SET TestLock TestLockValue EX 5 NX

释放锁:

> DEL TestLock

也能够经过Redis事务来执行这两条命令,可是在Redis被kill掉时,可能只有部分命令被执行,因此仍是经过上面的命令更安全简洁。

但在以下场景又会有新的问题:

  1. 客户端1获取锁成功。
  2. 客户端1在某个操做上阻塞了很长时间(GC等等)或者很长时间的网络延迟。
  3. 过时时间到了,锁自动释放了。
  4. 客户端2获取到了对应同一个资源的锁。
  5. 客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。

为了不这种状况发生,咱们通常会将锁的KEY的VALUE设置为一个随机值,这个随机值只要在一段连续时间内不会重复就行。在释放锁的时候,判断当前锁的VALUE与本身设置的是否一致,只有一致的时候才会释放

这样redis命令集合就变成了: 获取锁:

> SET TestLock RandomValue EX 5 NX

释放锁:

> GET TestLock
//程序判断是否一致
> DEL TestLock

这个在以下场景也会有问题:

  1. 客户端1获取锁成功。
  2. 客户端1访问共享资源。
  3. 客户端1为了释放锁,先执行’GET’操做获取随机字符串的值。
  4. 客户端1判断随机字符串的值,与预期的值相等。
  5. 客户端1因为某个缘由阻塞住了很长时间。
  6. 过时时间到了,锁自动释放了。
  7. 客户端2获取到了对应同一个资源的锁。
  8. 客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。

因此,释放锁必定要放在一个事务内,经过lua脚本。也就是:

传入Lua脚本参数 TestLock,当前程序缓存的随机值
判断TestLock的值与传入值是否一致,一致则删除
返回是否解锁成功

这样在Redis单进程一直正常运行的状况下,分布式悲观锁就实现了。

每日一刷,轻松提高技术,斩获各类offer:

image

相关文章
相关标签/搜索