Redis系列-生产应用篇-分布式锁(1)-单进程Redis分布式锁实现思路

什么是分布式锁

针对共享内存模型的程序(例如JAVA程序),锁就是一个非常常用的机制。
一般简单分为悲观锁和乐观锁。悲观锁就是你获取这块数据的锁之后,别人就无法访问或操作这块数据,直到你释放这个锁。乐观锁一般就是CAS更新。
在单进程内内存的锁,只控制进程内数据的,就是非分布式锁。相反的,跨进程,需要锁住多个进程访问数据的锁就是分布式锁。
悲观锁一般由Redis的SETNX实现,乐观锁一般由Redis的WATCH实现。

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代表没有这个锁。

但是如果这么简单实现,在如下场景时,会有问题:

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

所以,考虑这个,我们一般会给锁设置一个超时时间。在超过这个超时时间之后,redis会自动把这个key删除掉。这样,在客户端1死掉了之后,不会导致这个锁永远不释放。

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

> SETNX TestLock TestLockValue
> EXPIRE TestLock 8

释放锁:

> DEL TestLock

由于SETNX还有EXPIRE是两条命令,在如下场景还可能出问题:

  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脚本

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

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

乐观锁在Java最直观的实现类似是Atomic相关类,这些类的核心方法就是getAndIncrement,incrementAndGet还有compareAndSet。
对于getAndIncrement,利用命令INCR或者INCRBY即可实现。INCR或者INCRBY的返回就是最终的key值,减去参数就是getAndIncrement的返回值。incrementAndGet就更简单了,直接返回INCR或者INCRBY的返回就行。
compareAndSet一般通过lua脚本实现,先对比,后set。
示例lua脚本实现:

//获取当前值对比
local currValue = redis.call('get', KEYS[1]);
//如果值一样,则设置并返回1
if currValue == ARGV[1] 
 or (tonumber(ARGV[1]) == 0 and currValue == false) then 
    redis.call('set', KEYS[1], ARGV[2]);
    return 1
else 
    return 0 
end

你可能感兴趣的:(Nosql缓存,redis)