在【黑马点评】优惠券秒杀(单体模式)的最后提到:使用userId.toString().intern()
作为锁,在集群模式下是有问题的。在本文中通过基于Redis实现的分布式锁来解决这一问题。
基于Redis实现分布式锁
-
加锁:
SET key value NX EX ttl
- key:锁。在该业务中为
order:用户Id
,其中order
为业务名,使用order:用户Id
作为锁可以保证在该业务中每个用户拥有一把锁。 - value:加锁线程的线程标识,
UUID + 线程Id
- 不能只用
线程Id
作为线程标识,因为在每一台JVM中线程Id都是从0开始递增的, 分布式环境下会重复,不再唯一。 UUID
的作用是区分集群中的每一台JVM,因此我们要为每一台JVM获取一个UUID,可以通过将用于存储UUID的变量设置为static final
实现,在加载ILock类时创建并初始化,而且此后不允许修改,这样就可以满足对每一台JVM都是唯一的。
- 不能只用
SETNX
用于保证锁的互斥性SET key value NX EX ttl
是将加锁SETNX key value
和为锁设置过期时间EXPIRE key ttl
两个命令合为一个命令了,这样可以保证原子性。
- key:锁。在该业务中为
-
释放锁:
-
手动释放:
DEL key
- 在释放前要先判断锁是不是自己的再删除锁,避免误删。
- “先判断锁是不是自己的,再删除锁” 是典型的线程安全问题,这一可以通过乐观锁来解决,也可以通过lua脚本来保证这两条命令的原子性,本项目中采用了第二种方式。
-
超时自动释放:兜底,避免锁无法正常释放,避免发生死锁问题。
-
误删问题
通过lua脚本确保多条命令执行时的原子性
用lua脚本操作Redis
Redis.call('命令名称', 'key', '其他参数', ...)
比如,在Redis中执行set age 10
命令:Redis.call('set', 'age', '10')
两个参数数组
在调用lua脚本时可以传入参数:
- 通过KEYS传入key类型参数
- 通过ARGV数组传入其他参数
这两个数组的下标都从1开始。
用lua脚本实现 “先判断锁是不是自己的,再删除锁” 逻辑
在Java代码中调用lua脚本