一文吃透 Redis 分布式锁:从原理到生产级实现全解析
一文吃透 Redis 分布式锁:从原理到生产级实现全解析
|
🌺The Begin🌺点点关注,收藏不迷路🌺
|
前言
在分布式系统中,当多个服务实例同时访问共享资源时,如何保证数据一致性?传统单机锁(如 synchronized、ReentrantLock)在多进程多线程环境下完全失效。这时候就需要分布式锁来协调多个节点对共享资源的互斥访问。
Redis 凭借其高性能、原子操作和丰富的数据结构,成为实现分布式锁最流行的方案之一。本文将深入剖析 Redis 分布式锁的核心原理,从最简单的 SETNX 到生产级可靠的 Redlock 算法,带你彻底掌握分布式锁的方方面面。
1. 分布式锁的核心要求
一个合格的分布式锁必须满足以下条件:
2. Redis 分布式锁的演进之路
2.1 第一代:SETNX + EXPIRE(有缺陷)
// ❌ 错误示例:非原子操作,存在死锁风险
public boolean wrongLock(String key, String value, long expireTime) {
Long result = jedis.setnx(key, value); // 设置锁
if (result == 1) {
// 如果这里服务宕机,EXPIRE 未执行 -> 死锁!
jedis.expire(key, expireTime); // 设置过期时间
return true;
}
return false;
}
问题:SETNX 和 EXPIRE 不是原子操作,中间若服务崩溃,锁永不过期。
2.2 第二代:SET NX EX(原子操作)
// ✅ Redis 2.6.12+ 支持原子操作
public boolean lock(String key, String value, long expireTime) {
String result = jedis.set(key, value, "NX", "EX", expireTime);
return "OK".equals(result);
}
命令详解:
NX:key 不存在时才设置(互斥性)EX:设置过期时间(秒)- 原子性:一条命令完成,无死锁风险
2.3 第三代:Lua 脚本保证原子解锁
-- unlock.lua
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
public boolean unlock(String key, String value) {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Object result = jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value));
return Long.valueOf(1).equals(result);
}
为什么需要 Lua 脚本?
Lua 脚本保证:先 GET 检查 value 是否是自己,是才 DEL,原子执行。
3. 完整的单节点分布式锁实现
3.1 核心代码(Jedis 实现)
public class RedisDistributedLock {
private Jedis jedis;
private static final String LOCK_SUCCESS = "OK";
private static final Long UNLOCK_SUCCESS = 1L;
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX"; // 毫秒
/**
* 加锁
* @param lockKey 锁的 key
* @param requestId 唯一标识(UUID)
* @param expireTime 过期时间(毫秒)
*/
public boolean lock(String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
return LOCK_SUCCESS.equals(result);
}
/**
* 解锁(Lua 脚本保证原子性)
*/
public boolean unlock(String lockKey, String requestId) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
return UNLOCK_SUCCESS.equals(result);
}
}
3.2 Redisson 封装的使用(推荐)
// Redisson 提供了更完善的分布式锁
@Autowired
private RedissonClient redissonClient;
public void doSomething() {
RLock lock = redissonClient.getLock("myLock");
try {
// 尝试加锁,最多等待 100 秒,锁自动释放时间 30 秒
if (lock.tryLock(100, 30, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
Redisson 的优势:
- 自动续期(Watch Dog 机制)
- 可重入锁支持
- 公平锁、读写锁等多种实现
4. 分布式锁的难点与解决方案
4.1 锁过期时间设置
问题:业务执行时间 > 锁过期时间,导致锁被提前释放。
解决方案 1:Redisson Watch Dog
// Redisson 自动续期机制
// 默认锁过期 30 秒,每 10 秒自动续期
RLock lock = redissonClient.getLock("myLock");
lock.lock(); // 内部自动启动续期线程
解决方案 2:手动续期
// 开启定时任务,在锁过期前续期
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Future<?> renewalFuture = executor.scheduleAtFixedRate(() -> {
jedis.expire(lockKey, expireTime);
}, expireTime / 3, expireTime / 3, TimeUnit.SECONDS);
4.2 可重入锁实现
问题:同一线程多次获取同一把锁时,需要支持重入。
// Redisson 支持可重入
RLock lock = redissonClient.getLock("myLock");
lock.lock();
lock.lock(); // 重入成功,计数器 +1
// ...
lock.unlock(); // 计数器 -1
lock.unlock(); // 计数器为 0 时真正释放
原理:使用 Redis Hash 存储
HSET myLock uuid:threadId 1 -- 存储持有者 + 重入次数
HINCRBY myLock uuid:threadId 1 -- 重入时递增
4.3 加锁失败的重试策略
public boolean lockWithRetry(String key, String requestId, int expireTime,
int retryTimes, long retryInterval) {
for (int i = 0; i < retryTimes; i++) {
if (lock(key, requestId, expireTime)) {
return true;
}
try {
Thread.sleep(retryInterval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
改进:指数退避
// 第 n 次重试等待 2^n * base
long waitTime = Math.min(baseWaitTime * (1 << retryCount), maxWaitTime);
5. 集群环境下的挑战:Redlock 算法
5.1 单节点问题
在 Redis 主从架构下,如果主节点加锁后未同步到从节点就宕机,会发生什么?
5.2 Redlock 算法(Redis 作者提出)
Redlock 在多个独立 Redis 节点上获取锁,需要超过半数节点成功才算成功。
Redlock 核心代码(简化):
public boolean redlock(String resource, String requestId, int ttl) {
int N = 5; // 5 个独立 Redis 节点
int quorum = N / 2 + 1; // 需要至少 3 个节点成功
long start = System.currentTimeMillis();
int successCount = 0;
for (Jedis jedis : jedisList) {
if (lock(jedis, resource, requestId, ttl)) {
successCount++;
}
}
long elapsed = System.currentTimeMillis() - start;
if (successCount >= quorum && elapsed < ttl) {
// 锁获取成功,有效剩余时间 = ttl - elapsed
return true;
}
// 获取失败,释放所有节点的锁
for (Jedis jedis : jedisList) {
unlock(jedis, resource, requestId);
}
return false;
}
5.3 Redlock 的争议
| 支持观点 | 反对观点 |
|---|---|
| 提供了多节点容错 | 依赖时钟同步,时钟跳跃可能导致问题 |
| 算法清晰,易于实现 | 性能开销大,需要 N 次网络请求 |
| 大多数场景够用 | 存在理论上的安全性边界问题 |
结论:大多数业务场景下,单节点 + 主从 + 合理超时配置已足够。Redlock 仅在极高标准的一致性场景下才需要。
6. 完整的生产级分布式锁工具类
@Component
public class RedisDistributedLockTemplate {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_PREFIX = "lock:";
/**
* 执行带锁的业务逻辑
* @param key 锁的 key
* @param expireSeconds 锁过期时间(秒)
* @param waitTimeout 获取锁等待超时(毫秒)
* @param task 业务逻辑
*/
public <T> T executeWithLock(String key, long expireSeconds,
long waitTimeout, Supplier<T> task) {
String requestId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
// 自旋获取锁
while (System.currentTimeMillis() - startTime < waitTimeout) {
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(LOCK_PREFIX + key, requestId,
Duration.ofSeconds(expireSeconds));
if (Boolean.TRUE.equals(success)) {
try {
return task.get();
} finally {
// 使用 Lua 脚本释放锁
releaseLock(key, requestId);
}
}
// 短暂休眠后重试
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁被中断", e);
}
}
throw new RuntimeException("获取锁超时: " + key);
}
private void releaseLock(String key, String requestId) {
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(LOCK_PREFIX + key),
requestId
);
}
}
使用示例:
@Autowired
private RedisDistributedLockTemplate lockTemplate;
public void deductStock(Long productId) {
lockTemplate.executeWithLock("product:" + productId, 10, 3000, () -> {
// 扣减库存业务逻辑
int stock = stockMapper.getStock(productId);
if (stock > 0) {
stockMapper.deduct(productId);
}
return null;
});
}
7. 使用场景与注意事项
7.1 典型应用场景
| 场景 | 说明 |
|---|---|
| 秒杀/抢购 | 防止库存超卖 |
| 重复提交 | 接口幂等性控制 |
| 分布式调度 | 多实例环境下只让一个执行定时任务 |
| 缓存重建 | 防止缓存击穿时的并发重建 |
7.2 注意事项清单
结语
Redis 分布式锁是每个后端开发者必须掌握的技能。从最早的 SETNX + EXPIRE,到原子 SET NX EX,再到 Lua 脚本的安全解锁,每一步演进都在解决之前的问题。
核心要点总结:
- 原子性:加锁和解锁必须原子操作
- 唯一标识:使用 UUID 区分不同客户端
- 超时机制:防止死锁,但注意业务执行时间
- 自动续期:Redisson 的 Watch Dog 解决了锁过期问题
- 集群容错:普通业务单节点够用,极高要求考虑 Redlock
记住一个原则:分布式锁不是为了解决 100% 的问题,而是把问题概率降低到可接受的范围。大多数场景下,主从架构 + 合理超时配置已经足够。
你在项目中用过 Redis 分布式锁吗?遇到过哪些坑?欢迎评论区分享!

|
🌺The End🌺点点关注,收藏不迷路🌺
|
更多推荐

所有评论(0)