在高并发系统中,分布式锁 是解决资源竞争的关键技术。Redis 因其高性能和原子性操作,成为实现分布式锁的常用方案。本文将详细介绍 Redis 分布式锁的核心原理、实现方式、常见问题及优化方案,帮助你构建健壮的分布式锁机制。
1. 为什么需要分布式锁?
在单机环境下,可以使用 synchronized
或 ReentrantLock
保证线程安全。但在分布式系统中,多个服务实例可能同时操作共享资源(如库存扣减、订单创建),这时就需要 分布式锁 来确保:
✅ 互斥性:同一时间只有一个客户端能持有锁。
✅ 避免死锁:即使客户端崩溃,锁也能自动释放。
✅ 高性能:加锁、解锁操作要高效,避免成为系统瓶颈。
2. Redis 分布式锁的核心实现
2.1 基础实现(SETNX + EXPIRE)
最简单的 Redis 锁使用 SETNX
(SET if Not eXists)命令:
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock:order:123", "1");
if (lock) {
redisTemplate.expire("lock:order:123", 30, TimeUnit.SECONDS); // 设置过期时间
try {
// 执行业务逻辑...
} finally {
redisTemplate.delete("lock:order:123"); // 释放锁
}
} else {
throw new RuntimeException("系统繁忙,请重试!");
}
问题:SETNX
和 EXPIRE
不是原子操作,如果在这两步之间程序崩溃,会导致死锁!
2.2 原子性加锁(SET NX EX)
Redis 2.6.12+ 支持 SET key value NX EX seconds
,可以一步完成加锁和设置过期时间:
Boolean lock = redisTemplate.opsForValue().setIfAbsent(
"lock:order:123",
"1",
30, // 过期时间
TimeUnit.SECONDS
);
if (!lock) {
throw new RuntimeException("系统繁忙,请重试!");
}
优点:
✔ 原子性操作,避免死锁
✔ 代码简洁
2.3 更安全的锁(UUID + Lua 释放)
问题:如果锁过期后业务仍在执行,其他客户端可能误删锁!优化方案:
锁的值使用唯一标识(如 UUID),防止误删。
释放锁时用 Lua 脚本,保证原子性。
String lockKey = "lock:order:123";
String lockValue = UUID.randomUUID().toString(); // 唯一标识
// 加锁(原子操作)
Boolean locked = redisTemplate.opsForValue().setIfAbsent(
lockKey,
lockValue,
30,
TimeUnit.SECONDS
);if (!locked) {
throw new RuntimeException("系统繁忙,请重试!");
}try {
// 执行业务逻辑…
} finally {
// 释放锁(Lua 脚本,保证原子性)
String luaScript =
"if redis.call(‘get’, KEYS[1]) == ARGV[1] then " +
" return redis.call(‘del’, KEYS[1]) " +
"else " +
" return 0 " +
"end";redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
lockValue
);
}
优点:
✔ 防止误删其他客户端的锁
✔ 释放锁是原子操作
3. 进阶优化方案
3.1 锁自动续期(WatchDog)
问题:如果业务执行时间超过锁的过期时间,会导致锁提前释放!解决方案:
Redisson 提供了
WatchDog
机制,后台线程定期检查锁,如果业务未完成,自动续期。
RLock lock = redissonClient.getLock("lock:order:123");
try {
lock.lock(30, TimeUnit.SECONDS); // 加锁,默认 WatchDog 每 10 秒续期
// 执行业务...
} finally {
lock.unlock();
}
3.2 可重入锁
问题:同一线程多次获取锁时,普通 Redis 锁会阻塞自己!解决方案:
Redisson 可重入锁:记录锁的持有者和重入次数。
RLock lock = redissonClient.getLock("lock:order:123");
lock.lock(); // 第一次获取
lock.lock(); // 可重入,不会阻塞
lock.unlock();
lock.unlock();
3.3 高可用 Redis 锁(RedLock)
问题:单点 Redis 宕机可能导致锁失效!解决方案:
RedLock 算法:在多个 Redis 节点上加锁,多数成功才算加锁成功。
RedissonRedLock redLock = new RedissonRedLock(
redissonClient.getLock("lock:1"),
redissonClient.getLock("lock:2"),
redissonClient.getLock("lock:3")
);
redLock.lock();
try {
// 执行业务...
} finally {
redLock.unlock();
}
4. Redis 锁的常见问题
5. 最佳实践总结
优先使用
SET key value NX EX seconds
(原子性加锁)。锁的值用 UUID,防止误删。
释放锁用 Lua 脚本,保证原子性。
长时间任务使用 WatchDog 自动续期(如 Redisson)。
高可用场景用 RedLock(多 Redis 节点)。
6. 完整代码示例
// 加锁
String lockKey = "lock:order:123";
String lockValue = UUID.randomUUID().toString();
Boolean locked = redisTemplate.opsForValue().setIfAbsent(
lockKey,
lockValue,
30,
TimeUnit.SECONDS
);if (!locked) {
throw new RuntimeException("系统繁忙,请重试!");
}try {
// 执行业务逻辑…
} finally {
// 释放锁(Lua 脚本)
String luaScript =
"if redis.call(‘get’, KEYS[1]) == ARGV[1] then " +
" return redis.call(‘del’, KEYS[1]) " +
"else " +
" return 0 " +
"end";redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
lockValue
);
}
7. 结论
Redis 分布式锁是实现高并发控制的重要手段,但直接使用 SETNX
容易遇到死锁、误删等问题。推荐:
简单场景:
SET NX EX
+ Lua 释放。复杂场景:使用 Redisson(支持可重入锁、WatchDog、RedLock)。
正确使用 Redis 锁,可以让你的分布式系统更稳定、更高效! 🚀