使用redis实现分布式锁
This is a hidden message
在我们的修改数据时,需要先读取数据,然后进行保存,这时就会遇到并发问题;由于修改和保存不是原子操作,所以在并发场景下,出现脏读幻读等现象;在单机情况下,我们通常使用本地锁来避免并发问题,但是当服务采用集群式的部署方式时,我们就需要使用分布式锁来保证数据的一致性。
如何实现
Redis
分布式锁主要使用 setnx
命令。
- 加锁:使用
setnx <key> <value>
命令获取锁,如果对应的key不存在,则进行赋值,并返回成功,如果key存在则直接返回失败; - 解锁:使用
del <key>
命令释放锁; - 锁超时:使用
expire <key> <timeout>
命令设置超时时间,如果锁长时间没有被释放,会在一定时间内自动释放,避免造成死锁。
伪代码:
1 | if (setnx(key, 1) == 1){ |
SETNX 和 EXPIRE 非原子性
如果获取锁成功,但是出现了宕机等异常,导致后续的设置超时时间的命令没有被执行,就会出现死锁。
可以使用set
扩展命令:
1 | SET <key> <value> NX EX <timeout> |
Spring Boot
中RedisTemplate
的实现:
锁误解除
如果线程A成功获取到了锁,并且设置了10秒的超时时间,但是线程A执行时间超过了10秒,锁超时释放,此时线程B又获取到了锁,然后线程A执行完成,执行释放锁的命令,但是线程A释放了线程B的锁。
可以将线程标识(比如UUID)设置为锁的value,再删除锁之前验证锁的key是否与当先线程的标识相同,如果相同再释放锁。
1 | if (setIfAbsent(key, UUID, timeOut) == 1){ |
解锁超时导致并发执行
让我们继续来看刚才的问题,如果A的执行时间超过了锁自动释放的时间,而恰巧B又获取到了锁,就会导致AB线程并发执行。
解决方式:
- 将超时时间设置的足够长,确保业务逻辑在锁释放之前能够执行完成,但是这样会导致并发能力下降;
- 为获取锁的线程创建守护线程,在线程执行完成之前为锁添加有效时间。
使用redis实现分布式锁