1. 分布式锁的核心需求与挑战
在分布式系统中,多个服务实例同时访问共享资源时,如何保证操作的互斥性是一个经典问题。想象一下多个收银员同时操作同一个库存系统,如果没有合理的互斥机制,就可能出现超卖或数据不一致的情况。
Redis的SETNX命令(SET if Not eXists)因其原子性和高性能成为实现分布式锁的常见选择。但单纯使用SETNX会遇到几个关键问题:
- 锁释放时的误删风险(自己的锁过期后误删别人的锁)
- 非原子操作导致的竞态条件
- 锁粒度控制不当造成的性能瓶颈
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础实现方案解析
2.1 锁的标识设计
锁的核心由两个部分组成:
java复制String lockName = String.format("prefix_%s_%s", tenantId, objectId);
String uuid = UUID.randomUUID().toString();
lockName(Key)的设计原则:
- 必须包含足够的信息来标识被保护的资源
- 示例中的
tenantId_objectId组合确保了:- 同一租户同一对象的操作会互斥
- 不同对象可以并行操作
- 前缀
prefix_的用途:- 避免与其他业务key冲突
- 方便通过
prefix_*模式搜索相关锁
uuid(Value)的作用:
- 作为锁持有者的唯一标识
- 防止锁被非持有者释放
- 推荐使用UUID而不是自增ID,因为:
- 无需中央协调生成
- 足够的唯一性保证
2.2 加锁逻辑实现
java复制if (!SquirrelClientUtil.setnx(lockName, uuid, 7200)) {
return "请勿重复操作";
}
关键点说明:
setnx的原子性保证了并发安全- 7200秒(2小时)的TTL是安全兜底,避免死锁
- 返回错误信息应包含足够上下文,方便排查
实际生产中建议将TTL设置为业务最长执行时间的2-3倍。例如业务通常执行需要5分钟,可以设置TTL为15-30分钟。
2.3 释放锁的标准流程
java复制Object lockValue = SquirrelClientUtil.get(lockName);
if (uuid.equals(lockValue)) {
SquirrelClientUtil.delete(lockName);
}
这个流程虽然简单,但存在一个致命缺陷:get和delete是两个独立操作,不是原子的。在分布式环境下,这会导致竞态条件。
3. 高级问题与解决方案
3.1 锁误删问题详解
没有UUID时的典型问题场景:
- 线程A获取锁(假设TTL=10s)
- 线程A执行时间超过10s(GC暂停或网络延迟)
- Redis自动删除过期的key
- 线程B获取同一个锁
- 线程A恢复执行,调用delete
- 结果:线程B的锁被意外删除
加入UUID后的保护机制:
- 释放锁时先检查value是否匹配
- 只有持有者才能删除自己的锁
- 即使锁过期后被其他客户端获取,也不会误删
3.2 原子性问题的终极方案
基础实现的get+delete是非原子的,解决方案是使用Lua脚本:
lua复制if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
Lua脚本的优势:
- 在Redis中原子执行
- 减少网络往返
- 避免客户端-服务器之间的竞态条件
Java中的调用示例:
java复制String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockName),
uuid);
3.3 锁粒度设计实践
锁的粒度直接影响系统并发度。过粗的锁会降低性能,过细的锁增加复杂度。
设计原则:
- 按业务语义划分锁范围
- 全局锁:
global_lock - 租户级锁:
tenant_123_lock - 资源级锁:
resource_456_lock
- 全局锁:
- 避免动态拼接过多参数导致key不可预测
- 保持key的可读性和可调试性
错误示例:
java复制// 过于宽泛的锁
String badLock1 = "export_lock";
// 过于细碎的锁
String badLock2 = String.format("lock_%s_%s_%s_%s_%s",
userId, date, module, subModule, action);
4. 生产环境中的最佳实践
4.1 锁的自动续期机制
长时间任务可能导致锁提前过期。解决方案是实现"看门狗"机制:
java复制private void scheduleRenewal() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> {
if (locked) {
redisTemplate.expire(lockName, 30, TimeUnit.SECONDS);
}
}, 0, 10, TimeUnit.SECONDS); // 每10秒续期一次
}
注意事项:
- 续期间隔应小于TTL(如TTL的1/3)
- 任务完成或异常时应取消续期
- 避免过度续期导致资源浪费
4.2 可重入锁实现
某些场景需要支持同一线程重入锁:
java复制private static ThreadLocal<Map<String, Integer>> lockCount = ThreadLocal.withInitial(HashMap::new);
public boolean tryLock(String key, String uuid, long ttl) {
Integer count = lockCount.get().get(key);
if (count != null) {
lockCount.get().put(key, count + 1);
return true;
}
if (setnx(key, uuid, ttl)) {
lockCount.get().put(key, 1);
return true;
}
return false;
}
4.3 锁等待与公平性
简单的轮询获取锁可能造成Redis压力,改进方案:
java复制public boolean tryLockWithWait(String key, String uuid, long ttl, long waitTime) throws InterruptedException {
long end = System.currentTimeMillis() + waitTime;
while (System.currentTimeMillis() < end) {
if (setnx(key, uuid, ttl)) {
return true;
}
Thread.sleep(100 + new Random().nextInt(100)); // 随机退避
}
return false;
}
5. 常见问题排查指南
5.1 锁泄漏检测
监控未释放锁的脚本示例:
bash复制# 查找超过预期TTL的锁
redis-cli --scan --pattern 'prefix_*' | while read key; do
ttl=$(redis-cli ttl $key)
if [ $ttl -eq -1 ]; then
echo "锁泄漏: $key"
fi
done
5.2 性能优化建议
- 避免锁粒度过细导致大量Redis连接
- 对于高频短操作,考虑本地锁+分布式锁的混合模式
- 监控锁等待时间,超过阈值报警
5.3 集群环境特殊考量
Redis集群模式下需注意:
- 确保锁key落在同一slot(使用hash tag)
java复制String lockName = "{resource}:" + id; // {}内的内容用于计算slot - 主从切换时的锁丢失问题(考虑RedLock算法)
- 跨机房部署时的网络延迟影响
6. 替代方案比较
6.1 与Zookeeper对比
| 特性 | Redis | Zookeeper |
|---|---|---|
| 性能 | 更高 | 较低 |
| 一致性 | 最终一致 | 强一致 |
| 实现复杂度 | 简单 | 较复杂 |
| 适用场景 | 高频短操作 | 低频长事务 |
6.2 Redisson框架的优势
Redisson提供了更完善的实现:
- 自动续期
- 可重入锁
- 红锁(RedLock)支持
- 丰富的API
使用示例:
java复制RLock lock = redisson.getLock("myLock");
lock.lock();
try {
// 业务代码
} finally {
lock.unlock();
}
在实际项目中,我通常会根据团队的技术栈选择方案。对于已经深度使用Redis的系统,基于SETNX的实现是合理选择;新建系统可以考虑直接使用Redisson等成熟框架。
