分布式锁的引入
在单机单线程环境中,程序运行无须担心并发问题,一切都按部就班;而在单机多线程情况下,JUC包中提供的锁机制已能很好地解决并发问题,为代码保驾护航。但随着业务规模扩大,系统逐步转向分布式架构,传统的单机锁(如JUC锁)就显得不足,因为数据往往分布在多个节点上。如果继续采用单机锁机制,只能锁定单个节点,而无法防止跨节点的数据重复操作。以秒杀场景为例,若高并发请求没有得到妥善处理,就可能引发超卖问题,这时分布式锁就显得尤为必要。
基于Redis实现分布式锁
-
初步实现:SETNX加锁
初期方案中,我们可以使用Redis的
SETNX key value
命令来加锁。该命令只有在key不存在时才会设置成功,返回值为整数:1代表成功,0代表失败。业务执行完毕后,通过删除该key释放锁。 -
防止死锁:设置超时时间
但这种方案存在风险:如果在加锁后节点突然宕机,锁无法释放,导致后续其它节点无法加锁,形成死锁。为此,我们需要为锁设置超时时间。简单地将加锁和设置超时拆成两个操作依然无法保证原子性,因此我们可以利用Redis的SET命令扩展参数,将加锁和设置过期时间整合为一条原子命令。
// 仅在key不存在时设置值,并同时设置10秒的过期时间 SET mykey myvalue EX 10 NX
这样可以在加锁的同时为锁添加超时保护,防止因节点故障导致锁永久存在。
-
解决解锁安全问题
如果没有唯一标识,不同线程可能会错误地释放非自己加的锁。为避免这种情况,我们在加锁时加入唯一标识,在释放锁前验证标识是否匹配。由于验证与删除若分开执行将失去原子性,故可以使用Lua脚本来保证这一步骤的原子性。
-
续期机制的必要性
另外,如果业务处理时间超过锁的过期时间,锁会被提前释放,从而产生并发问题。因此,除了加锁和解锁的原子性,还需要在业务执行过程中对锁进行续期,确保业务完成前锁不会失效。
分布式锁应满足的四个核心条件
为确保分布式锁的有效性,其实现通常应满足以下要求:
- 互斥性:在任意时刻,只有一个客户端能持有锁。
- 无死锁:即使客户端在持有锁期间崩溃,也能保证其他客户端能继续获取锁。
- 锁的归属:加锁和解锁必须由同一客户端完成,防止错误释放。
- 容错性:只要大部分Redis节点正常运行,客户端就能正确获取和释放锁。
这四个条件共同保证了无论是加锁还是解锁,都必须以原子操作为前提。
Redisson分布式锁的实现原理
Redisson提供了一套完善的分布式锁方案,其核心特点包括:
- 原子性保障:加锁与解锁均通过Lua脚本实现,确保操作不可拆分。
- 重入机制:如果同一线程多次请求加锁,系统会增加重入计数,并在解锁时逐步释放。
- 锁续期机制:Redisson会在后台启动异步线程,定期检查并延长锁的过期时间(默认30秒),确保业务处理过程中锁不会意外失效。
- 等待唤醒策略:未获得锁的线程会进入等待队列,当锁释放后,通过发布订阅机制唤醒等待线程竞争锁。
下面展示一个Redisson实现分布式锁的示例代码:
// 1. 构造Redisson的必要配置,指定服务器地址、密码和数据库信息
Config config = new Config();
config.useSingleServer()
.setAddress("IP地址加端口号")
.setPassword("密码")
.setDatabase(0);
// 2. 创建RedissonClient实例
RedissonClient redissonClient = Redisson.create(config);
// 3. 获取锁对象(注意:获取锁的顺序不一定是严格FIFO)
RLock rLock = redissonClient.getLock(lockKey);
try {
/**
* 4. 尝试获取锁:
* waitTimeout:尝试获取锁的最大等待时间,超过则认为获取失败;
* leaseTime :锁的持有时间,超过后锁会自动失效(建议设置大于业务执行时间)
*/
boolean res = rLock.tryLock((long) waitTimeout, (long) leaseTime, TimeUnit.SECONDS);
if (res) {
// 成功获取锁后执行业务逻辑
}
} catch (Exception e) {
throw new RuntimeException("获取锁失败");
} finally {
// 无论业务执行结果如何,最终都需要释放锁
rLock.unlock();
}
注意:加锁和解锁均利用Lua脚本实现原子性。
主从架构下锁失效问题
在高可用架构中,通常会配置主从节点以确保数据安全。如果在主节点成功加锁后,由于故障或宕机导致数据尚未同步到从节点,那么其他节点可能无法正确感知锁的状态,从而引发锁失效和并发安全问题。
CAP理论视角下的ZooKeeper与Redis锁对比
-
ZooKeeper分布式锁(CP架构)
强调一致性,依赖半数以上节点确认写入,确保数据同步,其操作过程为同步写入。 -
Redis分布式锁(AP架构)
注重高可用性,主节点在接收到请求后即可返回写入成功,采用异步复制至从节点,虽然存在少量数据丢失风险,但整体响应速度更快。
在实际应用中,选择哪种分布式锁方案需要根据业务对高可用性和一致性的不同需求做出平衡。
redlock:进阶的分布式锁方案及其局限
Redlock方案旨在通过多节点写入成功的半数机制解决主从复制带来的数据丢失问题。其基本原理与ZooKeeper类似:只有当多数节点写入成功后,才能认为锁真正生效。然而,Redlock仍存在以下局限:
- 如果主节点宕机后从节点顶替,仍可能出现锁失效问题。
- 当节点数量较多时,写入失败和回滚操作会消耗大量资源;当超过半数节点宕机时,整个Redlock机制将失效。
因此,在部署Redlock时需要综合考虑节点数量、业务需求和容错要求。
分布式锁性能优化建议
分布式锁虽能保证并发安全,但同时也将部分并行操作串行化,可能影响系统性能。以下是一些性能优化建议:
-
优化锁粒度
仅对存在并发风险的关键代码块加锁,避免无谓的锁定操作,以缩小锁的粒度。 -
采用分段锁策略
将资源分为多个部分,每个部分使用独立的锁进行控制。类似于ConcurrentHashMap
中的分段锁设计,可以减少竞争、提升并发性能。
在设计分布式锁时需在安全性与性能之间取得平衡,既防止并发问题,又确保系统效率不受过多锁操作拖累。