方案1: spring-integration-redis组件
使用spring-integration-redis组件
private static void tryLock() {
LettuceConnectionFactory connectionFactory = createConnectionFactory();
RedisLockRegistry lockRegistry = new RedisLockRegistry(connectionFactory, "testKey");
Lock lockKey = lockRegistry.obtain("lockKey");
System.out.println(lockKey.tryLock());
}
使用Redis实现分布式锁的原理具体就是使用Lua脚本来保证多个命令的原子性。
RedisLockRegistry实现原理
- 如果有多个项目实例,就表示有多个RedisLockRegistry对象在多个JVM中
- 每一个的clientId都不一致,先锁本地,再锁Redis。
- 如果不使用本地锁,每次请求都必须有一个clientId。
青铜方案:
缺陷:业务代码出现异常或者服务器宕机,没有执行主动删除锁的逻辑,就造成了死锁。
改进:设置锁的自动过期时间,过一段时间后,自动删除锁,这样其他线程就能获取到锁了。
白银方案:
缺陷:占锁和设置锁过期时间是分步两步执行的,不是原子操作。
改进:占锁和设置锁过期时间保证原子操作。
黄金方案:
缺陷:主动删除锁时,因锁的值都是相同的,将其他客户端占用的锁删除了。
改进:每次占用的锁,随机设为较大的值,主动删除锁时,比较锁的值和自己设置的值是否相等。
铂金方案:
缺陷:获取锁、比较锁的值、删除锁,这三步是非原子性的。中途又可能锁自动过期了,又被其他客户端抢占了锁,导致删锁时把其他客户端占用的锁删了。
改进:使用 Lua 脚本进行获取锁、比较锁、删除锁的原子操作。
钻石方案:
缺陷:非专业的分布式锁方案。
改进:Redission 分布式锁。
方案2: Redission
使用方便,leaseTime参数表示锁的自动释放时间
// 1.设置分布式锁
RLock lock = redisson.getLock("lock");
// 2.占用锁
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();
看门狗机制:如果没有设置锁的自动释放时间,每隔10S都会自动续期,这样就避免了业务处理时间过长导致锁被自动释放导致锁被其他线程获取的问题。
如果设置了锁的自动释放时间,那么业务处理时间不能超过这个时间,不然报错,两次释放锁。
if (redis.call('exists', KEYS[1]) == 0) then //第一次进入,lock不存在,创建
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then //第二次进入,当前线程已经获取到了lock
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);//当前线程不能获取到锁
lua脚本原理解析
KEYS[1]=lock锁名称,如lock名称为test_lock001,类型为hash
ARGV[1]=lock锁的占用时间,毫秒
ARGV[2]=hash的key,实际值为uuid:threadId,如e6818ee9-1155-428d-8032-280b184209f8:17
参考
Spring Boot Redis 实现分布式锁,真香!!
Redis 分布式锁原理看这篇就够了, 循循渐进
Redis 分布式锁|从青铜到钻石的五种演进方案
分布式锁中的王者方案 - Redisson