1. 问题现象:分布式锁失效的典型场景
上周排查一个线上事故时,发现个有趣现象:系统明明用Redis实现了分布式锁,但在促销活动期间,库存扣减仍然出现了超卖。查看日志发现多个节点的服务实例同时获得了锁,最终导致库存数据不一致。这种"假锁"现象在分布式系统中其实非常普遍,根据我的经验,90%的分布式锁问题都源于以下几个被忽视的细节:
- 锁过期时间设置不合理(业务未执行完锁已释放)
- 锁释放时未校验持有者身份(误删其他线程的锁)
- 锁获取与业务执行未保证原子性(获得锁后系统重启)
- 锁重入机制缺失(同一线程多次加锁失败)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分布式锁的四个核心要素
2.1 互斥性保障
真正的互斥需要满足:
java复制// 错误实现 - 非原子操作
if(redis.setnx(key,value)==1){
redis.expire(key,timeout);
}
// 正确实现 - Redis 2.6.12+支持原子操作
redis.set(key, value, "NX", "PX", expireTime);
关键点:setnx和expire必须原子操作,否则在设置过期时间前服务崩溃会导致死锁
2.2 锁释放安全性
常见的锁释放陷阱:
java复制// 危险操作 - 可能释放其他线程的锁
public void unlock(String key){
redis.delete(key);
}
// 安全实现 - Lua脚本保证原子性
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then " +
" return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
redis.eval(script, Collections.singletonList(key), Collections.singletonList(threadId));
2.3 过期时间动态调整
采用看门狗机制延长锁持有时间:
java复制private void scheduleExpirationRenewal(){
TimerTask task = new TimerTask() {
public void run() {
if(redis.get(key).equals(threadId)){
redis.expire(key, 30); // 续期30秒
scheduleExpirationRenewal();
}
}
};
new Timer().schedule(task, 10000); // 10秒后执行
}
2.4 可重入设计
通过计数器实现锁重入:
lua复制-- 加锁Lua脚本
local counter = redis.call('hget', KEYS[1], ARGV[1])
if counter then
redis.call('hincrby', KEYS[1], ARGV[1], 1)
redis.call('expire', KEYS[1], ARGV[2])
return 1
elseif redis.call('hlen', KEYS[1]) < 1 then
redis.call('hset', KEYS[1], ARGV[1], 1)
redis.call('expire', KEYS[1], ARGV[2])
return 1
end
return 0
3. Spring AOP中的锁陷阱
3.1 事务与锁的顺序问题
典型错误场景:
java复制@Transactional
@DistributedLock(key = "#orderId")
public void updateStock(Long orderId){
// 业务逻辑
}
执行顺序实际是:
- 获取分布式锁
- 开启数据库事务
- 提交事务
- 释放锁
致命问题:事务提交前锁可能已超时释放,其他线程读到未提交的中间状态
3.2 正确实现方式
调整注解顺序并控制超时:
java复制@DistributedLock(key = "#orderId", expire = 30, retry = false)
@Transactional(timeout = 25) // 事务超时 < 锁超时
public void updateStock(Long orderId){
// 业务逻辑
}
4. 高并发下的锁优化策略
4.1 分段锁实践
库存扣减场景优化:
sql复制-- 原表结构
CREATE TABLE inventory(
item_id BIGINT PRIMARY KEY,
stock INT
);
-- 优化为分段库存
CREATE TABLE inventory_segment(
item_id BIGINT,
segment_id INT,
stock INT,
PRIMARY KEY(item_id, segment_id)
);
Java实现示例:
java复制public boolean deductStock(Long itemId, int count){
int segment = ThreadLocalRandom.current().nextInt(10);
String lockKey = "stock:"+itemId+":"+segment;
// 获取分段锁...
}
4.2 锁等待队列设计
基于Redis的公平锁实现:
lua复制-- 获取锁时加入队列
local result = redis.call('zadd', KEYS[2], ARGV[1], ARGV[2])
if result == 1 then
redis.call('zrem', KEYS[1], ARGV[2])
redis.call('hset', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[3])
return 1
end
return 0
5. 生产环境问题排查指南
5.1 锁竞争监控方案
通过Redis命令分析锁状态:
bash复制# 查看所有锁键
redis-cli --scan --pattern 'lock:*'
# 获取锁详细信息
redis-cli hgetall lock:order:123
# 监控锁等待时间
redis-cli --latency -i 1
5.2 常见异常处理
- 锁泄漏检测脚本:
python复制def check_lock_leak(redis_conn):
locks = redis_conn.scan_iter(match='lock:*')
for lock in locks:
ttl = redis_conn.ttl(lock)
if ttl == -1: # 未设置过期时间
redis_conn.delete(lock)
- 死锁自动恢复方案:
java复制@Scheduled(fixedDelay = 300000)
public void checkDeadLock(){
Set<String> keys = redisTemplate.keys("lock:*");
keys.forEach(key -> {
Long holdTime = System.currentTimeMillis() -
Long.parseLong(redisTemplate.opsForValue().get(key));
if(holdTime > MAX_HOLD_TIME){
redisTemplate.delete(key);
}
});
}
6. 分布式锁的进阶思考
6.1 RedLock算法争议
Redis作者提出的多节点锁方案:
java复制List<RedisNode> nodes = getRedisNodes();
int successCount = 0;
long startTime = System.currentTimeMillis();
for(RedisNode node : nodes){
if(acquireLock(node)){
successCount++;
}
}
boolean locked = successCount >= nodes.size()/2 + 1
&& (System.currentTimeMillis()-startTime) < lockValidityTime;
实际争议点:时钟漂移可能导致锁状态误判,生产环境建议使用Zookeeper/etcd等一致性更强的方案
6.2 最终一致性方案
对于允许短暂不一致的场景:
java复制@DistributedLock(key = "#orderId")
public void updateStock(Long orderId){
// 1. 先扣减Redis缓存
redis.decr("stock:"+itemId);
// 2. 异步记录操作日志
mq.send(new StockLog(itemId, -1));
// 3. 定时任务补偿数据库
@Scheduled(fixedRate = 60000)
public void syncStock(){
// 比对Redis与DB差异...
}
}
7. 不同场景下的锁选型建议
| 场景特征 | 推荐方案 | 优缺点对比 |
|---|---|---|
| 强一致性要求高 | Zookeeper顺序节点 | 性能较低但可靠性最高 |
| 短时高频操作 | Redis+Lua | 性能最好,可能有时钟漂移问题 |
| 长事务场景 | 数据库行锁+版本号 | 避免锁超时,但影响并发 |
| 多资源协调 | 分布式事务框架(Seata等) | 功能全面,复杂度高 |
在电商库存场景中,我的实践组合是:
- 热点商品:Redis分段锁 + 本地缓存
- 普通商品:Zookeeper锁 + 异步日志
- 结算操作:Saga模式分布式事务
8. 从CAP理论看锁的本质
分布式锁本质是CP系统的实现,但需要注意:
-
网络分区时的处理策略:
- 默认快速失败(牺牲A)
- 设置降级策略(牺牲C)
-
脑裂场景下的安全措施:
java复制// 双写检测机制
public boolean safeLock(String key){
boolean mainLock = redisMaster.setnx(key, value);
boolean slaveLock = redisSlave.setnx(key, value);
return mainLock && slaveLock;
}
9. 性能压测数据参考
JMeter测试结果对比(单Redis节点):
| 并发线程数 | 纯Redis锁TPS | Redisson锁TPS | 错误率 |
|---|---|---|---|
| 100 | 2356 | 1987 | 0.1% |
| 500 | 1872 | 1534 | 1.2% |
| 1000 | 1245 | 1023 | 3.8% |
关键发现:
- 原生Redis锁在高并发下错误率飙升
- Redisson的看门狗机制会增加10-15%性能开销
- 当并发超过500时,建议采用分段锁方案
10. 开发 checklist
每次实现分布式锁时,建议对照检查:
- [ ] 锁获取和释放是否原子操作
- [ ] 过期时间是否大于业务最长执行时间
- [ ] 是否实现锁持有者验证
- [ ] 是否有锁续期机制
- [ ] 事务与锁的顺序是否正确
- [ ] 是否有锁重入需求
- [ ] 是否有降级方案
- [ ] 是否有监控埋点
在金融级场景中,我们还会额外:
- 记录所有锁操作日志
- 实现锁等待超时熔断
- 定期演练网络分区场景
