分布式锁
首先有一个先验知识:SETNX命令。它的全称是set if not exists。即如果这个key不存在的话,就可以给它set值;如果存在,则报错。
如图,ab作为key,它的value已经被设置为love了,如果再想把它设置为kk,则会报错。
所以分布式锁就用了这一个命令的思路:多个线程通过同时来设置这个命令,谁先能把它set上值,就代表抢到了锁。反之就得等待锁的释放。
第一种
首先声明,前面几种太简单就不赘述了,我想从具有过期时间的例子开始说起。
在这个例子中,我们将 获取锁 + 设置锁的过期时间 设置成一个事务(因为redis支持)
原理图如下:
那一行关键代码是这样的:
\
但是这种方法有一个缺陷:
假如有ABC三个线程同时执行某段业务代码,A线程先抢到了锁(设置值为123),然后执行业务,10s之后,锁自动被释放了,但是A的业务代码还没有执行完成! 此时B线程看锁被释放了。立马抢占了锁(也设置值为123),同样执行业务代码。此时A和B线程就产生了冲突。15s后,A线程释放了值为123的锁,但这个锁是B锁上的,又发生了冲突。
此时C看锁被释放了,立马又抢占了该锁(也设置了值为123),此时B和C在执行任务上又发生了冲突。
所以给了我们一个提醒:
- 线程处理任务的时间一定要小于锁过期的时间
- 为什么BC会打开A的锁,因为他们都把value设置为了同一个,所以就可以相互打开各自的锁,所以value值必须设置为不同的,可以用uuid来替换。
第二种
基于上一种方法的缺陷,我们改进了下一种方法,
- 采用uuid来作为其value的值
- 主动删除锁时,需要先判断锁的value值是否和设置的一样,一致的话才主动删除。
- 生成一个uuid
- 用生成的uuid加锁
- 如果加锁成功,则执行业务
- 执行完业务之后,获取当前锁的value值
- 如果该value值和一开始生成的uuid相同,则可以安全释放锁
你以为这个方法很完美了吗?但实际上并不是。
比如A、B两个线程 ,A先抢到了锁,A执行完业务后,去查询当前value的值,但这个查询特别耗时。
到达10s时,锁释放了,B线程抢占了锁,并开始执行业务。此时A查到其value值了,然后A就把锁释放了,这里要注意,A的锁其实已经过期了,但它释放的锁,其实是B的,但此时B还没有执行完业务呢。
在这就会造成冲突。
解决方案就是把4、5步设置为原子操作,即查询当前锁的value值+释放锁作为一个事务来处理即可。
第三种(Redisson)
啰嗦了这么多,redis早就给了我们一个完美的解决方案
他的使用方法就是:
\
\
\
\
这个锁是默认30s过期的。但它又做了新的优化。但redission实例没被关闭之前,会不断延长锁的过期时间,即10s后,如果redission没关闭,自动将过期时间再次设置为30s,以此往复。
即使服务器宕机了,30s后,锁依然会释放,不会造成死锁操作。