目录
一、为什么需要分布式锁?
二、Redis分布式锁核心特性
三、实现方案与代码详解
方案1:基础版 SETNX + EXPIRE
原理
代码示例
问题
方案2:Redisson框架(生产推荐)
核心特性
代码示例
优势
方案3:RedLock算法(Redis集群)
适用场景
实现步骤
代码片段
方案4:Lua脚本原子化操作
解决基础版问题
Lua脚本示例
Java调用方式
四、高级场景与解决方案
场景1:公平锁(按顺序获取)
实现思路
代码逻辑
场景2:可重入锁
实现机制
Redisson实现
五、避坑指南与最佳实践
1. 锁误删问题
2. 锁续期问题
3. 主从一致性问题
4. 性能优化
六、总结与选型建议
一、为什么需要分布式锁?
在微服务、多机部署场景中,多个进程可能同时竞争同一资源(如库存、订单)。传统JVM锁仅作用于本地,无法保证分布式环境的互斥访问。例如:
- 电商库存超卖:两个服务实例同时查询库存>0,均执行扣减操作。
- 定时任务重叠:多节点触发相同任务(如对账),导致数据混乱。
Redis凭借高性能、原子操作和丰富数据结构,成为分布式锁的首选方案。
二、Redis分布式锁核心特性
- 互斥性:仅一个客户端能持有锁
- 防死锁:锁需自动过期(如SET EX)
- 容错性:节点故障不影响锁机制
- 可重入性(可选):同一线程可多次加锁
- 公平性(可选):按申请顺序分配锁
三、实现方案与代码详解
方案1:基础版 SETNX + EXPIRE
原理
SET key value NX EX seconds
:原子设置键值并设置过期时间- 若返回
true
则获取锁,否则重试
代码示例
String lockKey = "product_stock_lock";
String lockValue = UUID.randomUUID().toString(); // 唯一标识// 尝试加锁
Boolean success = jedis.set(lockKey, lockValue, "NX", "EX", 30);
if (success != null && success) {// 获取锁成功,执行业务逻辑try {// 扣减库存操作} finally {// 释放锁if (lockValue.equals(jedis.get(lockKey))) {jedis.del(lockKey);}}
} else {// 获取锁失败,重试或返回
}
问题
- 锁误删:业务未完成时锁过期,其他线程可能删除当前线程的锁
- 非原子操作:
set
和get
存在竞态条件
方案2:Redisson框架(生产推荐)
核心特性
- 可重入锁:同一线程可多次加锁,计数器管理
- 看门狗机制:默认每10秒续期锁至30秒,防止业务超时
- 异步兼容:支持
tryLock()
阻塞等待和isLocked()
状态检查
代码示例
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);RLock lock = redisson.getLock("order_lock");
try {// 尝试加锁(最多等待10秒,锁过期30秒)if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {// 处理订单逻辑} else {log.warn("获取锁失败");}
} catch (InterruptedException e) {Thread.currentThread().interrupt();
} finally {lock.unlock(); // 必须在finally中释放
}
优势
- 自动处理锁续期、可重入、跨进程兼容
- 支持主从切换(通过Redis主从复制)
方案3:RedLock算法(Redis集群)
适用场景
- Redis集群环境(至少3个独立节点)
- 需容忍半数节点故障仍保持锁可用
实现步骤
- 向多数节点(N/2+1)发送加锁请求
- 所有节点设置相同键值和过期时间
- 成功加锁后,锁有效时间为
T - 网络延迟
代码片段
// 配置多个Redisson客户端连接不同节点
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://node1:6379");
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://node2:6380");
// 创建RedLock对象
RedissonRedLock redLock = new RedissonRedLock(Redisson.create(config1),Redisson.create(config2)
);// 加锁逻辑
try {boolean locked = redLock.tryLock(10, 30, TimeUnit.SECONDS);if (locked) {// 业务逻辑}
} finally {redLock.unlock();
}
方案4:Lua脚本原子化操作
解决基础版问题
- 原子验证+删除:通过Lua脚本确保操作原子性
- 唯一标识:用UUID区分锁归属
Lua脚本示例
-- 释放锁脚本(判断键值是否匹配)
if redis.call('get', KEYS[1]) == ARGV[1] thenreturn redis.call('del', KEYS[1])
elsereturn 0
end
Java调用方式
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, 1, lockKey, lockValue);
四、高级场景与解决方案
场景1:公平锁(按顺序获取)
实现思路
- 使用
List
存储等待队列 - 通过
ZSet
记录申请时间戳,优先分配最早请求
代码逻辑
// 申请锁时加入队列
jedis.lpush("lock_queue", clientId);
jedis.zadd("lock_timestamp", System.currentTimeMillis(), clientId);// 检查队列头部是否是自己
String head = jedis.lrange("lock_queue", 0, 0).get(0);
if (head.equals(clientId) && currentTimeMatch(jedis.zscore("lock_timestamp", head))) {// 获取锁成功
}
场景2:可重入锁
实现机制
- 使用
Hash
存储线程ID和重入次数 - 加锁时递增计数器,解锁时递减(归零后删除锁)
Redisson实现
RLock lock = redisson.getLock("reentrant_lock");
lock.lock(); // 第一次加锁
// 嵌套调用同一锁
lock.lock(); // 重入计数+1
...
lock.unlock(); // 计数-1,归零后删除锁
五、避坑指南与最佳实践
1. 锁误删问题
- 原因:锁过期后,其他线程删除了当前线程的锁
- 解决方案:
- 使用唯一标识(如UUID+线程ID)作为锁值
- 释放锁前通过Lua脚本校验归属
2. 锁续期问题
- 看门狗机制:Redisson自动续期,业务长时间运行时需显式指定超时时间
- 手动续期:调用
lock.expire(seconds)
延长锁时间
3. 主从一致性问题
- 症状:主节点写锁后宕机,从节点未同步锁信息
- 解决方案:
- 使用RedLock算法(需多数节点加锁成功)
- 开启Redis主从复制的
WAIT
命令(Redis 7+)
4. 性能优化
- 减少锁粒度:按业务ID(如订单号)细化锁范围
- 避免嵌套锁:同一线程内多次加锁需谨慎处理重入
- 监控指标:锁获取成功率、平均等待时间、超时次数
六、总结与选型建议
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
SETNX + EXPIRE | 快速原型、单节点场景 | 简单高效 | 需手动处理细节 |
Redisson | Java项目、生产环境 | 功能完善,自动续期 | 依赖第三方库 |
RedLock | Redis集群、高容错需求 | 强一致性,容错性强 | 性能较低,实现复杂 |
Lua脚本 | 需严格原子操作的场景 | 彻底解决非原子问题 | 需维护脚本 |
发布/订阅 | 高并发减少轮询 | 节省资源 | 消息可靠性需保障 |
最佳实践:
- 优先使用Redisson框架(生产环境)
- 单节点快速实现可选SETNX+Lua脚本
- 集群环境采用RedLock算法
- 锁名称统一规范(如
resource_type:id
) - 超时时间设为业务平均耗时的2倍