什么是分布式锁
分布式锁其实可以理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥来保持一致性
为什么要分布式锁
当多个线程需要并发修改一个数据时,为了避免竞争,在单机的情况下,加synchronized或者Lock即可实现互斥
但在分布式的环境下,当多个server并发修改同一个资源时,为了避免竞争就需要使用分布式锁。
那为什么不能使用Java自带的锁(synchronized或者Lock)呢?
- 因为Java中的锁是面向多线程设计的,它只局限于当前的实例。
- 而多个server实际上是多进程,是不同的实例,所以Java自带的锁机制在这个场景下是无效的。
如何实现分布式锁
采用Redis实现分布式锁,就是在Redis里存一份代表锁的数据,通常用随机字符串即可。
加锁:
第一种方式
setnx key value
这种方式的缺点是容易产生死锁,因为客户端有可能忘记解锁,或者解锁失败。
第二种方式
setnx key value
expire key seconds
给锁增加了过期时间,避免出现死锁。但这两个命令不是原子的,第二步可能会失败,依然无法避免死锁问题。
第三种方式
set key value nx ex seconds
通过“set…nx…”命令,将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。
解锁:
del key
解锁就是删除代表锁的那份数据,直接删除redis上面的数据。
问题
上述方法看起来没有问题,但实际是有隐患的
- 进程A在任务没有执行完毕时,锁已经到期被释放了。
- 等进程A的任务执行结束后,A会尝试释放锁,但是,它的锁已经过期不存在了,它此时释放的可能是其他线程的锁,比如B进程;
- 红框时间内,两个进程同时操作数据,极有可能出现线程安全的问题;
如何解决
在加锁时就要给锁设置一个标识,加锁进程要记住这个标识。
- 当进程解锁的时候,进行判断,是自己持有的锁才能释放
- 否则无法释放。可以为key设置一个随机字符串,来充当进程的标识。
但是解锁的时候,判断、释放,这两步需要保证原子性,否则第二步失败的话,就会出现死锁,但判断和删除命令不是原子的。
在Redis中可以使用Lua脚本,通过Lua脚本将两个命令编排在一起,而整个Lua脚本的执行是原子的。
# 加锁
set key random-value nx ex seconds
# 解锁
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
|