1. Redis 分布式锁过期问题的本质剖析
在分布式系统中,Redis 分布式锁是最常用的同步机制之一。但很多开发者第一次实现时都会遇到一个经典问题:锁已经过期释放了,但业务逻辑还在执行中。这种情况就像你去银行办理业务时,柜员突然离开,但你的业务还没办完,其他柜员又开始为你服务——最终可能导致账户状态混乱。
1.1 为什么单机锁不会出现这个问题?
在单机环境下,我们使用 synchronized 或 ReentrantLock 时,锁的生命周期与线程绑定:
- 线程启动时获取锁
- 线程结束时释放锁
- 如果线程卡住,锁会一直持有
这种机制保证了业务执行的原子性。但在分布式场景下,Redis 锁采用了完全不同的设计哲学:
bash复制SET lock:order:123 UUID NX EX 10
- NX 表示 key 不存在时才设置
- EX 10 表示 10 秒后自动过期
- UUID 是客户端唯一标识
这种设计带来了两个关键特性:
- 自动释放:避免因客户端崩溃导致的死锁
- 超时机制:防止资源被无限占用
1.2 过期问题的典型表现
假设我们有一个订单支付系统:
- 线程A获取锁,开始处理支付(预计需要15秒)
- 10秒后锁自动过期
- 线程B获取锁,开始处理同一订单
- 现在有两个线程同时操作同一个订单
这种场景下可能出现:
- 重复扣款
- 库存超卖
- 数据状态不一致
2. 锁过期问题的深度解决方案
2.1 错误方案及其危害
2.1.1 方案一:去掉过期时间
java复制// 危险代码示例
jedis.set("lock:order:123", UUID, "NX");
问题:如果客户端崩溃,锁将永远无法释放,导致系统死锁。
2.1.2 方案二:设置超长过期时间
java复制// 不推荐代码示例
jedis.set("lock:order:123", UUID, "NX", "EX", 3600);
问题:
- 业务可能只需10秒,却占用锁1小时
- 如果客户端崩溃,其他线程需要等待1小时才能继续
- 无法根本解决执行时间不确定的问题
2.2 正确方案:锁续期机制
2.2.1 看门狗模式原理
看门狗机制的核心思想是:
- 设置一个相对较短的初始过期时间(如30秒)
- 启动后台线程定期检查(如每10秒一次)
- 如果业务仍在执行,就延长锁的过期时间
- 业务完成时主动释放锁
java复制// 伪代码示例
public void watchDogRenew() {
while (isRunning) {
Thread.sleep(10000);
if (stillHoldLock()) {
jedis.expire(lockKey, 30);
}
}
}
2.2.2 实现注意事项
- 续期间隔:应小于锁过期时间(如过期30秒,间隔10秒)
- 续期条件:必须验证锁value是否仍属于当前客户端
- 异常处理:网络波动时要做好重试和日志记录
- 线程安全:确保续期线程不会造成资源泄漏
2.3 Redisson 的最佳实践
Redisson 的 RLock 实现了完善的看门狗机制:
java复制// 推荐用法
RLock lock = redisson.getLock("lock:order:"+orderId);
try {
lock.lock(); // 默认30秒过期,看门狗每10秒续期
processOrder(orderId);
} finally {
lock.unlock();
}
关键特性:
- 默认30秒过期
- 看门狗每10秒续期一次
- 支持可重入
- 自动释放保证
3. 生产环境中的防御性设计
3.1 业务幂等性设计
即使锁失效,业务也应该具备幂等处理能力:
sql复制-- 订单表设计示例
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
order_no VARCHAR(32) UNIQUE, -- 唯一约束
status ENUM('CREATED','PAID','CANCELED'),
...
);
处理逻辑:
java复制public void processPayment(String orderNo) {
Order order = orderRepo.findByOrderNo(orderNo);
if (order.getStatus() == PAID) {
return; // 已处理直接返回
}
// 处理支付逻辑
}
3.2 锁监控与告警
建议监控以下指标:
- 锁获取平均时间
- 锁续期次数
- 业务执行时间分布
- 锁竞争情况
prometheus复制# Prometheus监控示例
redis_lock_wait_seconds_sum{lock_name="order_lock"}
redis_lock_renew_count_total{lock_name="order_lock"}
3.3 锁粒度优化
错误的锁粒度:
java复制// 锁整个支付流程
lock.lock("payment_process");
正确的锁粒度:
java复制// 只锁特定订单
lock.lock("order:"+orderId);
4. 典型场景的解决方案
4.1 长事务处理方案
对于可能长时间执行的事务:
- 拆分为多个短操作
- 使用状态机管理流程
- 每个步骤单独加锁
java复制public void handleLongTransaction(String txId) {
// 第一阶段锁
RLock stage1 = redisson.getLock("tx:"+txId+":stage1");
stage1.lock();
try {
doStage1();
} finally {
stage1.unlock();
}
// 第二阶段锁
RLock stage2 = redisson.getLock("tx:"+txId+":stage2");
// ...
}
4.2 高并发优化方案
当锁竞争激烈时:
- 加入随机退避
- 实现锁等待队列
- 考虑分段锁
java复制public boolean tryLockWithBackoff(String lockName) {
Random random = new Random();
for (int i = 0; i < 3; i++) {
if (tryLock(lockName)) {
return true;
}
Thread.sleep(100 + random.nextInt(50));
}
return false;
}
5. 实现细节与避坑指南
5.1 正确的锁释放模式
常见错误:
java复制// 错误示例 - 可能删除别人的锁
public void unlock(String lockKey) {
jedis.del(lockKey);
}
正确做法:
lua复制-- Lua脚本保证原子性
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
5.2 锁续期的实现细节
完整续期流程:
- 获取锁时记录线程ID和客户端ID
- 续期时验证这两个标识
- 只有匹配时才执行续期
java复制public class RenewTask implements Runnable {
private final String lockKey;
private final String expectedValue;
public void run() {
String currentValue = jedis.get(lockKey);
if (expectedValue.equals(currentValue)) {
jedis.expire(lockKey, 30);
}
}
}
5.3 异常处理要点
必须处理的异常情况:
- Redis连接中断
- 续期失败
- 业务线程中断
- JVM崩溃
防御性代码示例:
java复制try {
lock.lock();
// 业务代码
} catch (Exception e) {
// 记录异常
metrics.increment("lock.failure");
throw e;
} finally {
if (lock.isHeldByCurrentThread()) {
try {
lock.unlock();
} catch (IllegalMonitorStateException e) {
// 锁可能已自动过期
}
}
}
6. 性能优化建议
6.1 锁参数调优
根据业务特点调整:
java复制Config config = new Config();
config.setLockWatchdogTimeout(30000); // 默认看门狗超时
config.setTimeBetweenLockWatchdogSessions(10000); // 检查间隔
6.2 连接池配置
推荐配置:
yaml复制# Redisson配置示例
singleServerConfig:
connectionPoolSize: 64
connectionMinimumIdleSize: 24
idleConnectionTimeout: 10000
connectTimeout: 1000
timeout: 3000
6.3 监控指标
关键监控点:
- 锁等待时间
- 续期成功率
- 业务执行时间
- Redis内存使用
7. 多语言实现方案
7.1 Python实现示例
python复制import redis
import threading
class RedisLock:
def __init__(self, redis_client):
self.redis = redis_client
self.renew_thread = None
def acquire(self, key, ttl=30):
identifier = str(uuid.uuid4())
if self.redis.set(key, identifier, nx=True, ex=ttl):
self.renew_thread = threading.Thread(
target=self._renew, args=(key, identifier, ttl)
)
self.renew_thread.start()
return identifier
return None
def _renew(self, key, identifier, ttl):
while True:
time.sleep(ttl/3)
if not self._check_and_renew(key, identifier, ttl):
break
def _check_and_renew(self, key, identifier, ttl):
with self.redis.pipeline() as pipe:
try:
pipe.watch(key)
if pipe.get(key) == identifier:
pipe.multi()
pipe.expire(key, ttl)
pipe.execute()
return True
return False
except redis.exceptions.WatchError:
return False
7.2 Go实现示例
go复制type RedisLock struct {
client *redis.Client
stopRenew chan struct{}
}
func (l *RedisLock) Lock(key string, ttl time.Duration) (bool, error) {
uuid := generateUUID()
ok, err := l.client.SetNX(key, uuid, ttl).Result()
if ok {
l.stopRenew = make(chan struct{})
go l.renew(key, uuid, ttl)
}
return ok, err
}
func (l *RedisLock) renew(key, uuid string, ttl time.Duration) {
ticker := time.NewTicker(ttl/3)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if !l.checkAndRenew(key, uuid, ttl) {
return
}
case <-l.stopRenew:
return
}
}
}
8. 架构层面的思考
8.1 何时不该用分布式锁
以下场景更适合其他方案:
- 高频读写:考虑乐观锁或CAS
- 跨服务事务:考虑Saga模式
- 最终一致性场景:考虑消息队列
8.2 多级锁设计
复杂系统可以采用:
- 本地锁:解决单机线程竞争
- 分布式锁:解决跨实例竞争
- 数据库锁:解决数据一致性
java复制// 多级锁示例
public void processOrder(String orderId) {
synchronized (localLockMap.get(orderId)) { // 本地锁
RLock distributedLock = redisson.getLock(orderId);
distributedLock.lock();
try {
// 业务处理
} finally {
distributedLock.unlock();
}
}
}
9. 常见问题排查
9.1 锁永远获取不到
可能原因:
- 锁未正确释放
- 网络分区
- Redis内存不足
排查步骤:
bash复制# 查看锁状态
redis-cli get lock:order:123
# 查看锁TTL
redis-cli ttl lock:order:123
9.2 续期失败
典型场景:
- Redis连接超时
- 锁value被意外修改
- 续期间隔设置不合理
解决方案:
- 增加重试机制
- 记录详细日志
- 优化网络配置
10. 终极解决方案建议
经过多年实践,我总结出分布式锁的最佳实践组合:
- 基础机制:Redisson看门狗自动续期
- 业务保障:幂等设计+状态检查
- 监控报警:锁等待时间>1s触发告警
- 降级方案:本地缓存+异步修复
- 定期演练:模拟锁超时场景测试系统容错
在实际项目中,这种组合方案能够应对99%以上的分布式锁场景。对于特别关键的业务,还可以考虑引入ZooKeeper的临时节点作为补充,但要注意ZooKeeper的性能特点。