在微服务架构盛行的今天,我经常遇到开发者这样的困惑:"为什么简单的库存扣减在单机环境下运行良好,一旦上了分布式环境就会出现超卖?" 这背后隐藏的正是分布式锁要解决的核心问题。想象一下多个服务实例同时操作同一个商品库存的场景,如果没有协调机制,每个实例都认为自己看到的库存是准确的,最终必然导致数据不一致。
分布式锁的本质是分布式系统下的互斥机制,它需要解决单机锁无法应对的三个核心挑战:
关键认知:分布式锁不是简单的"把单机锁搬到网络上",而是需要重新设计的一套协调系统。这也是为什么像Redis、Zookeeper等中间件实现的分布式锁方案会有显著差异。
Redisson的锁实现基于Redis的原子操作和发布订阅机制。当执行lock()时,底层会执行以下Lua脚本:
lua复制if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[1])
return nil
end
这个脚本保证了"判断是否存在"和"设置值"这两个操作的原子性。其中:
Redisson通过watchdog机制解决锁过期问题。获取锁成功后,会启动一个后台线程,每隔10秒(默认)检查客户端是否还持有锁。如果是,则延长锁的生存时间。这个设计避免了因业务执行时间超过锁有效期导致的问题。
java复制private void scheduleExpirationRenewal(long threadId) {
// 实际实现中会通过Netty的定时任务来执行续期
timeout = commandExecutor.getConnectionManager().newTimeout(...);
}
解锁操作同样通过Lua脚本保证原子性:
lua复制if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil
end
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1)
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2])
return 0
else
redis.call('del', KEYS[1])
redis.call('publish', KEYS[2], ARGV[1])
return 1
end
这个脚本实现了:
| 特性 | 非公平锁(UNFAIR_LOCK) | 公平锁(FAIR_LOCK) |
|---|---|---|
| 获取顺序 | 竞争获取 | 先到先得 |
| 实现方式 | 直接尝试获取 | 使用Redis队列维护请求顺序 |
| 吞吐量 | 高(约15000qps) | 中(约8000qps) |
| 适用场景 | 对顺序不敏感的高并发场景 | 需要严格顺序的业务 |
实测数据表明,在100并发下:
多锁(MULTI_LOCK):
红锁(RED_LOCK):
经验法则:当业务对一致性要求极高(如金融交易)时使用红锁,普通资源协调使用多锁即可。
读写锁(READ_LOCK/WRITE_LOCK)的特殊性在于:
典型应用场景:
java复制@DistributeLock(lockType = LockType.READ_LOCK, key = "'product:'+#productId")
public Product getProductDetail(Long productId) {
// 高频读取操作
}
@DistributeLock(lockType = LockType.WRITE_LOCK, key = "'product:'+#productId")
public void updateProductStock(Long productId, int delta) {
// 低频写入操作
}
java复制@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setTimeout(3000)
.setConnectionPoolSize(64) // 根据业务规模调整
.setConnectionMinimumIdleSize(32)
.setRetryAttempts(3)
.setRetryInterval(1500)
.setPingConnectionInterval(30000); // 心跳检测间隔
// 启用看门狗线程
config.setLockWatchdogTimeout(30000);
return Redisson.create(config);
}
关键参数说明:
lockWatchdogTimeout:看门狗检查间隔,默认30秒connectionPoolSize:不宜过大,避免Redis连接数耗尽retryInterval:失败重试间隔,建议1-2秒java复制@DistributeLock(
lockType = LockType.FAIR_LOCK,
key = "'order:'+#orderId",
waitTime = 30, // 最长等待30秒
leaseTime = 60, // 持有锁最多60秒
timeUnit = TimeUnit.SECONDS
)
public void processOrder(Long orderId) {
// 业务逻辑
}
参数设置原则:
现象:日志中出现大量锁过期警告
排查步骤:
现象:系统吞吐量下降,平均等待时间上升
优化方案:
现象:红锁出现意外释放
解决方案:
基于Redisson的RRateLimiter实现:
java复制public boolean tryAcquire(String key, int permits) {
RRateLimiter limiter = redisson.getRateLimiter(key);
limiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.MINUTES);
return limiter.tryAcquire(permits);
}
适用于资源池管理:
java复制RSemaphore semaphore = redisson.getSemaphore("resourcePool");
semaphore.trySetPermits(10); // 资源池大小
if(semaphore.tryAcquire()) {
try {
// 使用资源
} finally {
semaphore.release();
}
}
使用JMeter对不同类型的锁进行压测(100并发):
| 锁类型 | TPS | 平均响应时间 | 错误率 |
|---|---|---|---|
| 非公平锁 | 14500 | 8ms | 0% |
| 公平锁 | 8200 | 32ms | 0% |
| 读写锁(读) | 21000 | 5ms | 0% |
| 读写锁(写) | 7500 | 28ms | 0% |
| 红锁(3节点) | 3800 | 65ms | 0.2% |
测试环境:
在实践中发现以下版本组合最稳定:
避免使用的组合:
当Redis不适合时,可以考虑:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Zookeeper | 强一致性 | 性能较低 | 配置管理、领导选举 |
| etcd | 高可用 | 运维复杂 | Kubernetes生态 |
| 数据库行锁 | 无需额外组件 | 性能差 | 低频操作 |
避免重复的try-finally代码:
java复制public <T> T executeWithLock(String lockKey, LockType lockType, Supplier<T> supplier) {
LockEntity lockEntity = new LockEntity(lockKey).setLockType(lockType);
try {
if (lockType.lock(lockEntity)) {
return supplier.get();
}
throw new BusException("获取锁失败");
} finally {
lockType.unlock(lockEntity);
}
}
结合Spring AOP实现声明式锁:
java复制@Around("@annotation(distributeLock)")
public Object around(ProceedingJoinPoint joinPoint, DistributeLock distributeLock) {
String lockKey = parseLockKey(distributeLock, joinPoint);
LockEntity lockEntity = new LockEntity(lockKey).parseLockAnno(distributeLock);
try {
if (lockEntity.getLockType().lock(lockEntity)) {
return joinPoint.proceed();
}
throw new BusException("系统繁忙,请稍后重试");
} finally {
lockEntity.getLockType().unlock(lockEntity);
}
}
在实际项目中,这种设计可以使业务代码保持干净,同时获得完善的锁功能。