在电商秒杀、库存扣减、订单处理等高并发场景中,多个服务实例同时操作共享资源时,会出现超卖、数据不一致等典型问题。去年双十一大促期间,我们系统就曾因库存竞争导致超卖200多件商品。分布式锁正是为解决这类跨进程、跨服务器的互斥访问问题而生的关键技术方案。
传统单机锁(如Java的synchronized)在分布式环境下完全失效,因为:
使用SETNX命令已逐渐被淘汰,现在推荐用Redis 2.6.12+的SET扩展参数:
bash复制SET lock_key unique_value NX PX 30000
这个命令的原子性体现在:
关键细节:过期时间要大于业务操作耗时,我们一般设置为平均耗时的2-3倍。曾经因为设得太短(5s),导致批量导出任务频繁锁失效。
错误示范(会导致删除其他客户端的锁):
lua复制if redis.get("lock_key") == "my_value" then
redis.del("lock_key")
end
正确姿势是用Lua脚本保证原子性:
lua复制if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
对于执行时间不确定的长任务,需要后台线程定期续期:
java复制private void renewExpiration() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('pexpire', KEYS[1], ARGV[2]) " +
"else return 0 end";
while (!stopRenewal) {
redisTemplate.execute(script,
Collections.singletonList(lockKey),
clientId,
String.valueOf(expireTime));
Thread.sleep(expireTime / 3);
}
}
Redis主从切换可能导致锁丢失。官方推荐的RedLock算法需要至少5个独立节点:
争议提示:Redis作者和分布式系统专家Martin有过著名论战,实际使用中建议根据业务容忍度选择方案。我们金融业务最终采用了ZooKeeper方案。
本地可重入锁在分布式场景需要额外处理:
java复制public class RedisReentrantLock {
private ThreadLocal<Map<String, Integer>> lockCount =
ThreadLocal.withInitial(HashMap::new);
public boolean tryLock(String key, long expire) {
Map<String, Integer> counts = lockCount.get();
Integer count = counts.get(key);
if (count != null) {
counts.put(key, count + 1);
return true;
}
boolean acquired = redisLock.tryLock(key, expire);
if (acquired) {
counts.put(key, 1);
}
return acquired;
}
}
| 指标名称 | 报警阈值 | 排查方法 |
|---|---|---|
| 锁等待时间P99 | >500ms | 检查Redis慢查询/网络延迟 |
| 锁获取失败率 | >1% | 分析业务峰值/考虑分段锁 |
| 锁自动释放次数 | 突然增长 | 检查业务超时或死循环 |
| Redis节点内存使用率 | >70% | 清理过期的锁key/扩容 |
我们内部封装的分布式锁组件,在100节点集群实测数据:
最后分享一个真实案例:某次全站促销时,由于没有对分布式锁做限流,Redis CPU飙升至98%。后来增加了令牌桶限流:
java复制RateLimiter limiter = RateLimiter.create(5000); // QPS限制
if (limiter.tryAcquire()) {
try {
lock.lock();
// 业务逻辑
} finally {
lock.unlock();
}
}