黑马点评项目实战(五)——分布式锁

一 一人一单的并发安全问题

原来使用synchronized悲观锁用户id来保证一人一单,如下图:

image-20250216160638777

但在集群部署情况下,仍然会出现并发安全问题,不同jvm下的线程无法实现锁互斥,如下:

image-20250216160705182

二 分布式锁

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

image-20250216161631886

三 Redis分布式锁

实现

image-20250216175750707

image-20250216162347593

image-20250216162523678

安全问题

使用Redis锁可能存在这样的安全问题,当线程1因某种原因线程阻塞时,可能会释放其他线程获取的锁。:

image-20250216172309261

为了解决该问题,在释放锁时应该判断是否是自己的锁,如下:

image-20250216172442370

这时仍然有可能出现问题,如下图,当线程1获取锁标识并判断一致后,还未释放锁时遇到阻塞(如jvm垃圾回收),导致后面又释放了其他线程的锁。

image-20250216173426178

因此必须确保判断锁标识动作和释放锁动作的原子性。

Redis的Lua脚本

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。

1
2
3
4
5
6
7
8
-- 获取锁中的线程标识 get key
local id = redis.call('get', KEYS[1])
-- 比较线程标识与锁中的标识是否一致
if (id == ARGV[1]) then
-- 一致则释放锁 del key
return redis.call('del', KEYS[1])
end
return 0

四 Redisson

Redisson分布式锁

基于Redis实现的分布式锁有以下问题:

image-20250216180139034

因此使用Redisson来优化。Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

image-20250216180429234

Redisson分布式锁原理

可重入

可重入锁:同一个线程多次请求锁,可能会造成死锁。因此需要允许可重入锁。

利用redis中的hash数据结构,为锁额外记录一个值,代表该锁的重入次数。

image-20250216193242076

同一个线程每重入一次锁,将值+1,释放锁改为将值-1,值为0时才真正释放锁。

image-20250216193339921

为保证原子性,同样要使用Lua脚本。

重试机制

redisson可以设置等待时间waitTime,在获取锁失败后,会订阅并等待释放锁的信号,然后进行重试,直到获取锁成功或waitTime消耗完。

超时续约

redisson中的watchdog在获取锁时设置了一个定时任务,每隔一段时间刷新锁的有效期(releaseTime/3),直到释放锁时取消定时任务。注意,如果设置了leaseTime就没有watchdog了。

image-20250216231353719

主从一致性问题

问题产生原因:

image-20250216231742145

redisson解决方案:MultiLock

将每一个redis节点都视为主节点,只有向每个节点获取锁成功,才算成功。此时如果一个redis节点宕机,并不影响锁的正常获取和释放。此外也可以给每个节点建立主从关系,此时如果一个主节点宕机且刚好没有完成同步,由于从节点没有锁的标识,其他线程也无法成功获取锁,满足要求。

image-20250216231841738 image-20250216232050725

image-20250216233314954