在当今互联网应用中,服务集群化部署已成为常态。当多个服务实例同时操作共享资源时,传统的单机锁机制完全失效。我曾在一个电商秒杀系统中亲眼见证过这种场景:明明库存显示还剩100件商品,最终却超卖了30多件,这就是典型的集群环境并发问题。
JVM的锁机制(如synchronized或ReentrantLock)只能保证在单个JVM进程内的线程互斥。当服务以集群方式部署时,每个节点都有自己的锁监视器,这就如同多个小区各自的门禁系统——你无法用A小区的门禁卡控制B小区的大门。
通过简单的实验就能验证这点:在IDE中启动两个相同服务(不同端口),同时操作Redis中的同一个键。你会发现两个服务的线程都能"同时获得锁",这直接导致共享数据被错误修改。
一个可靠的分布式锁必须满足以下核心特性:
在实际项目中,我们还需要考虑锁的粒度、超时时间设置、可重入性等工程细节。这些因素直接影响系统的并发性能和可靠性。
最朴素的Redis锁实现只需要一个SETNX命令:
java复制Boolean result = redisTemplate.opsForValue()
.setIfAbsent("lock:order:123", "1", 30, TimeUnit.SECONDS);
但这存在明显缺陷:如果客户端在执行业务逻辑时崩溃,锁将无法释放。因此我们必须引入超时机制,这也是为什么上面的代码设置了30秒过期时间。
关键细节:设置锁值和过期时间必须是原子操作。如果分开执行setnx和expire命令,可能在两个命令之间发生进程崩溃,导致锁永远无法释放。
在早期项目中,我遇到过这样的生产事故:线程A获取锁后执行耗时操作,锁超时自动释放;线程B获取锁后开始执行,此时线程A完成操作并删除锁——结果删除了线程B的锁。
解决方案是为每个锁设置唯一标识(通常使用线程ID+UUID):
java复制String lockId = UUID.randomUUID().toString();
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent("lock:order:123", lockId, 30, TimeUnit.SECONDS);
// 释放锁时先验证
if (lockId.equals(redisTemplate.opsForValue().get("lock:order:123"))) {
redisTemplate.delete("lock:order:123");
}
即使有了锁标识,判断标识和删除锁两个操作之间的非原子性仍可能导致问题。Redis事务无法完美解决这个问题,因为WATCH命令在集群环境下存在限制。
最终方案是使用Lua脚本:
lua复制if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
在Spring中集成Lua脚本:
java复制private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public void unlock() {
redisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(lockKey),
lockId);
}
超时时间设置是个技术活:
建议方案:
在Redis Cluster模式下,需要考虑:
对于关键业务,建议采用:
java复制// 尝试获取多个资源的锁(有序获取避免死锁)
public boolean tryMultiLocks(List<String> lockKeys, long waitTime) {
Collections.sort(lockKeys); // 按固定顺序获取
// 实现略...
}
可能原因:
排查命令:
bash复制redis-cli --bigkeys
redis-cli info memory
典型表现:
解决方案:
可能原因:
优化方案:
java复制// 使用更细粒度的锁
String lockKey = "lock:order:" + orderId % 16;
虽然Redis分布式锁应用广泛,但在某些场景下其他方案可能更合适:
| 特性 | Redis | ZooKeeper |
|---|---|---|
| 性能 | 高 | 中等 |
| 一致性 | 最终一致 | 强一致 |
| 实现复杂度 | 中等 | 较高 |
| 适用场景 | 高并发、允许短暂不一致 | 强一致性要求场景 |
对于低频竞争场景,可以考虑:
sql复制UPDATE inventory
SET stock = stock - 1, version = version + 1
WHERE item_id = 100 AND version = 123
最高境界是避免使用锁:
在实际架构设计中,我通常会先评估是否真的需要分布式锁。很多场景通过合理的数据分片或异步处理可以避免锁的使用,从而获得更好的性能。