在分布式系统中,协调多个服务实例对共享资源的访问是一个经典难题。Redis分布式锁因其简单高效的特性,成为解决这一问题的首选方案。让我们从最基础的实现开始,逐步剖析其演进过程。
早期开发者最直观的实现方式是组合使用SETNX和EXPIRE命令:
bash复制SETNX lock:order 1 # 尝试获取锁
EXPIRE lock:order 30 # 设置过期时间
这个方案存在三个致命缺陷:
原子性问题:两个命令非原子执行,若在SETNX后客户端崩溃,EXPIRE不会执行,导致锁永远无法释放。我在实际项目中就遇到过因服务器突然宕机,导致订单系统锁死长达数小时的事故。
无持有者标识:锁的value只是简单的"1",任何客户端都能执行DEL命令释放锁。曾有一次线上故障,因服务A持有的锁被服务B误删,导致重复业务处理,造成数据不一致。
不可重入:同一线程多次获取锁会导致死锁。比如递归调用场景下,内层方法尝试获取已持有的锁时会被阻塞。
重要提示:这种实现方案目前已被业界废弃,仅作为反面教材用于教学。新项目绝对不要采用这种方式。
Redis 2.6.12版本后,SET命令支持NX和PX选项,实现了原子化的锁获取:
bash复制SET lock:order client_001 NX PX 30000
这个单条命令解决了基础版的所有问题:
参数说明:
NX:等效于SETNX,仅当key不存在时设置PX 30000:设置毫秒级过期时间(30秒)client_001:建议使用客户端标识+线程ID的组合但实际使用中仍发现两个问题:
java复制// 典型的安全释放锁实现
if (redis.get("lock:order").equals(clientId)) {
redis.del("lock:order")
}
Redisson的分布式锁实现包含以下几个关键组件:
锁状态存储:使用Redis Hash结构存储锁信息
看门狗机制:后台线程定期检查并延长锁有效期
发布订阅:用于实现公平锁的排队通知机制
Redisson通过组合UUID和线程ID实现可重入:
java复制// 获取锁对象
RLock lock = redisson.getLock("order_lock");
// 第一次加锁
lock.lock(); // 内部:hincrby lock_name uuid:threadId 1
// 同一线程再次加锁
lock.lock(); // 重入计数器+1
// 需要对应次数的解锁
lock.unlock();
lock.unlock(); // 计数器归零时才真正释放
底层数据结构示例:
code复制HGETALL lock:order
1) "b983c153-7421-453a-a715-8fc6b1b7a7a8:thread-1"
2) "2" # 重入次数
公平锁通过Redis的List和Pub/Sub实现排队机制:
java复制// 公平锁使用示例
RLock fairLock = redisson.getFairLock("fairLock");
fairLock.lock();
try {
// 保证先到先得
} finally {
fairLock.unlock();
}
性能对比:
联锁(MultiLock)适用场景:
当需要同时锁定多个资源时,比如转账业务需要锁定转出和转入两个账户:
java复制RLock lock1 = redisson.getLock("account:A");
RLock lock2 = redisson.getLock("account:B");
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2);
multiLock.lock();
try {
// 原子化转账操作
transfer(A, B, 100);
} finally {
multiLock.unlock();
}
红锁(RedLock)注意事项:
java复制// 红锁配置示例
RedissonClient[] clients = new RedissonClient[5];
// 初始化各客户端...
RLock[] locks = new RLock[5];
for (int i = 0; i < 5; i++) {
locks[i] = clients[i].getLock("order_lock");
}
RedissonRedLock redLock = new RedissonRedLock(locks);
redLock.lock();
try {
// 关键业务操作
} finally {
redLock.unlock();
}
java复制public class OrderService {
@Autowired
private RedissonClient redisson;
public void processOrder(String orderId) {
String lockKey = "order:lock:" + orderId;
RLock lock = redisson.getLock(lockKey);
try {
// 建议设置明确的等待时间和持有时间
boolean acquired = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!acquired) {
throw new BusyException("系统繁忙,请重试");
}
// 业务逻辑
handleOrder(orderId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceException("处理中断");
} finally {
// 双重检查防止误释放
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
yaml复制redisson:
singleServerConfig:
connectionPoolSize: 64 # 连接池大小(默认64)
idleConnectionTimeout: 10000 # 空闲连接超时(ms)
connectTimeout: 5000 # 连接超时(ms)
timeout: 3000 # 命令等待超时(ms)
retryAttempts: 3 # 重试次数
retryInterval: 1000 # 重试间隔(ms)
threads: 16 # 处理线程数
nettyThreads: 32 # Netty线程数
建议监控以下关键指标:
锁等待时间:histogram分布
200ms:需要扩容或优化
锁竞争频率:
bash复制# Redis监控命令
redis-cli --latency -i 1
看门狗异常:监控日志中的"WatchDog timeout"警告
现象:锁长期持有导致业务阻塞
排查步骤:
TTL lock:order解决方案:
java复制// 安全释放锁的改进方案
if (lock.isHeldByCurrentThread()) {
try {
lock.unlock();
} catch (IllegalMonitorStateException e) {
// 可能已被自动释放
}
}
典型日志:
code复制WARN o.r.RedissonLock - Watchdog timeout for lock: order_lock
可能原因:
优化方案:
java复制// 适当延长锁有效期
lock.tryLock(5, 120, TimeUnit.SECONDS);
// 或者拆分大事务
batchProcess();
当锁竞争激烈时,可以考虑:
锁分段:将大锁拆分为多个小锁
java复制// 原始:锁整个订单表
RLock globalLock = redisson.getLock("orders");
// 优化:按订单ID哈希分段
int segment = orderId.hashCode() % 16;
RLock segmentLock = redisson.getLock("orders:" + segment);
乐观锁替代:
sql复制UPDATE orders SET status = 'PAID'
WHERE id = 100 AND status = 'NEW'
本地缓存:结合Caffeine等本地缓存减少锁竞争
在实际生产环境中使用Redis分布式锁多年,我总结了以下几点深刻体会:
锁粒度选择:不是越小越好。过细的锁粒度会增加系统复杂度,建议根据业务语义而非技术便利性划分锁边界。比如电商系统中,一个订单的所有操作应该共享同一个锁。
超时时间设置:需要根据业务场景科学计算。我常用的公式是:
code复制最小锁时间 = 平均处理时间 + 3σ(标准差)
最大等待时间 = 最小锁时间 × 实例数 / 并行度
故障演练必要:定期模拟Redis节点宕机、网络分区等场景,验证锁机制的可靠性。我曾通过Chaos Engineering发现过看门狗线程在Full GC时可能停止工作的隐患。
监控指标设计:除了常规的QPS、耗时,建议特别关注:
混合锁策略:对于特别关键的业务,可以采用Redis锁+数据库乐观锁的双重保障。虽然牺牲部分性能,但能确保极端情况下的数据安全。
最后提醒:分布式锁是解决并发问题的银弹,但过度使用会导致系统复杂度剧增。在设计架构时,应该优先考虑无锁设计(如事件溯源、CQRS等模式),把锁作为最后的选择。