Redis 中如何实现分布式锁?
1. 基于 SETNX
命令实现
-
原理:
- 使用
SETNX
命令(SET if Not eXists)尝试获取锁。如果键不存在,则设置成功,表示获取锁;如果键已存在,则设置失败,表示锁已被其他客户端持有。 - 为锁设置过期时间(
EX
或PX
参数),以防止死锁。
- 使用
-
实现代码:
import redis.clients.jedis.Jedis; public class RedisLockExample { public static void main(String[] args) { Jedis jedis = new Jedis("localhost", 6379); String lockKey = "lock_key"; String requestId = UUID.randomUUID().toString(); // 尝试获取锁 String result = jedis.set(lockKey, requestId, "NX", "EX", 10); if ("OK".equals(result)) { try { // 执行业务逻辑 } finally { // 释放锁 if (requestId.equals(jedis.get(lockKey))) { jedis.del(lockKey); } } } else { // 获取锁失败,重试或返回 } } }
2. 基于 Redisson 客户端实现
-
原理:
- Redisson 是一个基于 Java 的 Redis 客户端,提供了高级的分布式锁功能。
- 使用
RLock
接口实现分布式锁,支持可重入锁、锁续期(Watch Dog)等功能。
-
实现代码:
import org.redisson.Redisson; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; public class RedissonLockExample { public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://localhost:6379"); RedissonClient redisson = Redisson.create(config); RLock lock = redisson.getLock("myLock"); try { // 尝试获取锁,最多等待 10 秒,锁持有时间为 10 秒 if (lock.tryLock(10, 10, TimeUnit.SECONDS)) { try { // 执行业务逻辑 } finally { // 释放锁 lock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
3. 基于 RedLock 算法实现
-
原理:
- RedLock 算法通过多个独立的 Redis 主节点来实现分布式锁,确保即使部分节点故障,锁仍然安全。
- 客户端尝试从多个 Redis 主节点获取锁,只有在大多数节点(超过半数)获取成功时,才认为获取锁成功。
-
实现步骤:
- 客户端获取当前时间戳
T1
。 - 依次向多个 Redis 主节点发起加锁请求,每个请求设置超时时间。
- 如果在大多数节点上加锁成功,则认为获取锁成功。
- 如果获取锁失败,释放已获取的锁。
- 客户端获取当前时间戳
-
实现代码:
import org.redisson.Redisson; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; public class RedLockExample { public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://localhost:6379"); RedissonClient redisson = Redisson.create(config); RLock lock = redisson.getLock("myLock"); try { if (lock.tryLock(10, 10, TimeUnit.SECONDS)) { try { // 执行业务逻辑 } finally { // 释放锁 lock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
4. 基于 Lua 脚本实现
-
原理:
- 使用 Lua 脚本确保加锁和解锁操作的原子性,避免竞态条件。
-
实现代码:
import redis.clients.jedis.Jedis; public class LuaLockExample { public static void main(String[] args) { Jedis jedis = new Jedis("localhost", 6379); String lockKey = "lock_key"; String requestId = UUID.randomUUID().toString(); // 加锁 Lua 脚本 String lockScript = "if redis.call('exists', KEYS[1]) == 0 then " + "redis.call('hset', KEYS[1], ARGV[1], 1);" + "redis.call('pexpire', KEYS[1], ARGV[2]);" + "return nil;" + "elseif redis.call('hexists', KEYS[1], ARGV[1]) == 1 then " + "redis.call('hincrby', KEYS[1], ARGV[1], 1);" + "redis.call('pexpire', KEYS[1], ARGV[2]);" + "return nil;" + "end;" + "return redis.call('pttl', KEYS[1]);"; // 释放锁 Lua 脚本 String unlockScript = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then " + "return nil;" + "end;" + "local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);" + "if counter > 0 then " + "redis.call('pexpire', KEYS[1], ARGV[2]);" + "return 0;" + "else " + "redis.call('del', KEYS[1]);" + "return 1;" + "end;" + "return nil;"; // 加锁 jedis.eval(lockScript, Collections.singletonList(lockKey), Arrays.asList(requestId, "10000")); try { // 执行业务逻辑 } finally { // 释放锁 jedis.eval(unlockScript, Collections.singletonList(lockKey), Arrays.asList(requestId, "10000")); } } }
总结
- 简单实现:使用
SETNX
命令结合过期时间,适用于简单场景。 - 高级实现:使用 Redisson 客户端或 RedLock 算法,适用于复杂场景,提供更高的可靠性和安全性。
- 原子性保证:使用 Lua 脚本确保加锁和解锁操作的原子性,避免竞态条件。
根据具体业务需求和场景选择合适的实现方式。
Redis 的 Red Lock 是什么?你了解吗?
1. Red Lock 的核心思想
Red Lock 的核心思想是通过多个独立的 Redis 实例来实现分布式锁,以提高锁的可靠性和安全性。具体步骤如下:
- 多个独立的 Redis 实例:假设部署了 5 个独立的 Redis 主节点,这些节点之间没有主从关系,也不进行数据同步。
- 客户端尝试加锁:客户端依次向这些 Redis 实例发送加锁请求,每个请求设置一个较短的超时时间(远小于锁的有效时间)。
- 多数节点加锁成功:如果客户端在大多数节点(例如 3 个或更多)上成功获取锁,并且获取锁的总时间小于锁的有效时间,则认为加锁成功。
- 释放锁:客户端在任务完成后,向所有 Redis 实例发送释放锁的请求,无论这些节点是否成功加锁。
2. Red Lock 的实现步骤
- 获取当前时间戳:客户端获取当前时间戳
T1
。 - 依次向 Redis 实例发起加锁请求:客户端依次向多个 Redis 实例发送加锁请求,每个请求设置超时时间(例如 50 毫秒)。如果某个节点加锁失败(如锁被占用或网络超时),立即向下一个节点发起请求。
- 计算获取锁的总耗时:如果客户端在大多数节点上成功获取锁,获取当前时间戳
T2
,计算总耗时T2 - T1
。 - 判断加锁是否成功:如果总耗时小于锁的有效时间,则加锁成功;否则加锁失败,客户端需要释放已获取的锁。
- 释放锁:客户端在任务完成后,向所有 Redis 实例发送释放锁的请求,无论这些节点是否成功加锁。
3. Red Lock 的优势
- 高可靠性:通过多个独立的 Redis 实例,避免了单点故障的问题,即使部分节点宕机,锁仍然有效。
- 容错性:不依赖单点 Redis 实例,避免了单点故障导致锁不可用的问题。
- 时间一致性:使用锁的有效期来限制锁的持有时间,即使客户端异常退出,锁也会自动释放。
4. Red Lock 的劣势
- 复杂性:需要维护多个 Redis 实例,部署成本较高。
- 网络延迟:加锁和释放锁的操作需要同时与多个 Redis 实例通信,延迟较单实例锁更高。
- 时钟漂移:Red Lock 假设 Redis 节点的时钟一致。如果节点时钟漂移较大,可能导致锁的有效期计算错误。
5. Red Lock 的实际应用
Red Lock 适用于需要高可靠性和高可用性的分布式锁场景,例如分布式事务、定时任务调度、库存扣减等。在实际应用中,可以根据业务需求和系统架构,灵活地运用 Red Lock 来保护关键资源的访问。
6. Red Lock 的实现示例
以下是一个简单的 Red Lock 实现示例(使用 Lua 脚本):
-- 加锁脚本
if redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then
return 1
else
return 0
end
-- 解锁脚本
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
7. 注意事项
- 锁有效期与任务执行时间:锁的有效期应比预估任务执行时间稍长,防止锁过期导致多个客户端同时持有锁。
- 多数节点原则:Red Lock 假定大多数节点是正常工作的。如果超过一半节点同时故障,加锁可能失效。
- 网络分区:如果发生网络分区,某些 Redis 节点不可用,加锁和解锁的成功率会降低。
Red Lock 是一种在分布式系统中实现高可靠分布式锁的解决方案,通过多个独立的 Redis 实例协同工作,确保锁的安全性和可靠性。
Redis 实现分布式锁时可能遇到的问题有哪些?
1. 锁过期与业务未完成(锁续期问题)
- 问题:客户端业务处理时间超过锁过期时间,导致锁提前释放,其他客户端获得锁,引发数据不一致。
- 解决方案:
- 看门狗机制:启动后台线程定期(如每隔10秒)续期锁的过期时间。
- 合理设置过期时间:根据业务处理耗时动态调整锁的过期时间(如预估时间+缓冲时间)。
2. 锁误删(非原子性释放)
- 问题:非原子性操作(先
GET
再DEL
)可能导致释放其他客户端的锁。 - 解决方案:使用 Lua 脚本保证验证与删除的原子性。
3. 主从切换导致锁丢失
- 问题:Redis 主节点宕机后,从节点可能未同步锁信息,新主节点上锁丢失。
- 解决方案:
- RedLock 算法:向多个独立 Redis 实例申请锁,当多数节点(如5个中的3个)加锁成功时,认为锁获取成功。
- 集群模式:使用 Redis Cluster 或 Sentinel 保障高可用性。
4. 时钟漂移问题
- 问题:如果 Redis 服务器的时钟向前跳跃,会使锁的 Key 过早超时失效。例如,客户端 1 获取到锁后,设置 Key 的过期时间是 10:02 分,但 Redis 服务器的时钟比客户端快了 2 分钟,那么 Key 可能在 10:00 就失效了。要是此时客户端 1 还没释放锁,就会出现多个客户端同时持有同一把锁的情况。
- 解决方案:使用 NTP 服务同步服务器时钟,减少时钟漂移的影响。
5. 单点安全问题
- 问题:当 Redis 集群采用单 Master 模式时,如果这台 Master 服务器宕机,所有客户端都可能无法获取到锁。为了提高可用性,会给 Master 引入 Slave 节点。但由于 Redis 的主从同步是异步进行的,可能会出现这样的情况:客户端 1 设置好锁后,Master 突然挂掉,Slave 晋升为新的 Master,而由于异步复制的特性,客户端 1 设置的锁丢失了。这时,客户端 2 也能成功设置锁,导致客户端 1 和 2 同时拥有同一把锁。
- 解决方案:使用 Redis Cluster 或 Sentinel 保障高可用性。
6. 锁阻塞问题
- 问题:在实际业务中,为了实现一个分布式锁,独立部署多个 Redis 实例,整体成本直线上升。实现复杂,整体加锁效率有所降低。
- 解决方案:使用 Redis Cluster 或 Sentinel 保障高可用性,减少独立部署多个 Redis 实例的成本。
7. 锁重入问题
- 问题:在秒杀场景中,假设库存有 2000 个商品可以供用户秒杀。为了防止出现超卖,通常会对库存加锁。如果有 1W 的用户竞争同一把锁,显然系统吞吐量会非常的低。
- 解决方案:将库存分段,例如分为 100 段,每段有 20 个商品可以参与秒杀。在秒杀过程中,先获取用户 ID 的 Hash 值,然后除以 100 取模。模为 1 的用户访问第 1 段库存,模为 2 的用户访问第 2 段库存,以此类推,最后模为 100 的用户访问第 100 段库存。这样在多线程环境中,就可以大大减少锁的冲突。
8. 锁超时失效或锁提前过期问题
- 问题:如果线程 A 加锁成功,但由于某些原因执行耗时过长,超过锁的超时时间,这时 Redis 会自动释放线程 A 加的锁。但线程 A 还没执行完,还在对共享数据进行访问。如果此时线程 B 尝试加锁,那么也可以加锁成功,并对共享数据进行访问。这样就出现了多个线程对共享数据进行操作的问题。
- 解决方案:如果达到了超时时间,但业务代码还没执行完,则需要给锁自动续期。可以使用
TimerTask
类来实现自动续期的功能,比如获取锁之后自动开启一个定时任务,每隔 10 秒自动刷新一次过期时间。
9. 原子操作问题
- 问题:使用
SETNX
命令加锁和设置锁的超时时间是分开的,并非原子操作。假如加锁成功,但设置超时时间失败了,该lockKey
就变成永不失效。极端情况下,获取锁的客户端如果宕机了,那么就没法释放锁了。 - 解决方案:使用
SET
命令的NX
和PX
选项,可以在一个原子操作中完成获取锁和设置过期时间的两个步骤。
10. 加锁失败的处理
- 问题:如果有两个线程同时上传文件到 SFTP,上传文件前先要创建目录,假设两个线程需要创建的目录名都是当天的日期。如果不做任何控制,直接并发创建目录,第二个线程必然会失败。如果加一个 Redis 分布式锁后在目录不存在时才进行创建,那么第二个请求加锁失败时,是返回失败,还是返回成功?
- 解决方案:在规定的时间内,通过自旋 + 睡眠去尝试加锁。比如在规定的 500 毫秒内,不断自旋尝试加锁。如果成功,则直接返回。如果失败,则睡眠 50 毫秒,再发起新一轮加锁的尝试。如果到了超时时间还未成功加锁,则直接返回失败。
11. 锁竞争问题
- 问题:在高并发场景下,多个客户端同时尝试获取同一把锁,可能导致锁竞争激烈,影响系统性能。
- 解决方案:使用读写锁或分段锁,减少锁的冲突。例如,将库存分段,每个段使用独立的锁,这样可以提高系统的并发性能。
12. RedLock 算法的问题
- 问题:RedLock 算法虽然提高了锁的可靠性,但实现过程复杂,且依赖多个 Redis 实例的时钟同步。如果某个实例的时钟发生漂移,可能导致锁提前失效,进而引发并发问题。
- 解决方案:使用 Redis Cluster 或 Sentinel 保障高可用性,减少对多个 Redis 实例的依赖。
通过以上解决方案,可以有效应对 Redis 分布式锁在实际应用中可能遇到的问题,提高系统的稳定性和可靠性。