手写简单分布式锁
public class deductionStock {
@Autowired
public StringRedisTemplate stringRedisTemplate;
public void deductStock() throws InterruptedException {
//设置锁
String lockKey = "product_01";
//剩余库存key
String surplusStock = "stock";
//设置线程ID,保证不会删除其他线程的锁
String clientId = UUID.randomUUID().toString();
try {
//设置锁,并设置超时时间,防止服务器宕机情况下造成死锁
//此处其实还需要创建一个子线程用来定时监测锁状态,实现锁续命,监测周期一般是超时时间的3分之1
//作用是防止业务代码还没执行完,但到达了超时时间,锁就被释放了。
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "jiasuo", 30, TimeUnit.SECONDS);
//设置失败
if (!result) {
System.out.println("当前锁已存在");
return;
}
//获取剩余库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(surplusStock));
//如果库存 > 0则可以扣减
if (stock > 0) {
//计算扣减后的库存
int realStock = stock - 1;
//设置剩余库存
stringRedisTemplate.opsForValue().set(surplusStock, String.valueOf(realStock));
System.out.println("库存扣减成功,剩余库存:" + String.valueOf(realStock));
}else {
System.out.println("库存扣减失败,库存不足");
}
} finally {
//删除锁,保证只删除自己线程的锁
if(StringUtils.equals(clientId,stringRedisTemplate.opsForValue().get(lockKey))){
stringRedisTemplate.delete(lockKey);
}
}
}
}
上述代码,在并发量不高的时候是基本没有什么问题的,但是服务器宕机可能会导致多条命令不能实现原子性操作,例如删除锁时,如果线程执行到判断哪一行代码时服务器宕机了,就会导致无法删除key,重启后再设置这个锁时就会导致死锁一段时间,这种情况只能等超时时间结束释放锁。
使用Redisson实现分布式锁
public class deductionStock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private Redisson redisson;
public void deductStock() throws InterruptedException {
//设置锁
String lockKey = "product_01";
//剩余库存key
String surplusStock = "stock";
//获取锁
RLock redissonLock = redisson.getLock(lockKey);
try {
//加锁
redissonLock.lock();
//获取剩余库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(surplusStock));
//如果库存 > 0则可以扣减
if (stock > 0) {
//计算扣减后的库存
int realStock = stock - 1;
//设置剩余库存
stringRedisTemplate.opsForValue().set(surplusStock, String.valueOf(realStock));
System.out.println("库存扣减成功,剩余库存:" + String.valueOf(realStock));
}else {
System.out.println("库存扣减失败,库存不足");
}
} finally {
//解锁
redissonLock.unlock();
}
}
}
使用Redisson实现分布式读写锁
读写锁中,读读操作不互斥,读写操作互斥,读写在写锁加锁后是无法进行读取的,只有当写锁释放以后,读锁才可以执行。也就是说无论是读请求先执行还是写请求先执行,只要涉及到写锁,则都会阻塞,如果是先写再读,则读锁等待,如果是先读再写,则写锁等待。
要保证读锁和写锁使用的是同一个key
读锁
public String read(){
//设置锁
String lockKey = "product_01";
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(lockKey);
//读之前加读锁,读锁的作用就是等待该lockkey释放写锁以后再读
RLock rLock = readWriteLock.readLock();
try {
rLock.lock();
String uuid = redisTemplate.opsForValue().get(lockKey);
return uuid;
}finally {
rLock.unlock();
}
}
写锁
public String write() throws InterruptedException {
//设置锁
String lockKey = "product_01";
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(lockKey);
//写之前加写锁,写锁加锁成功,读锁只能等待
RLock rLock = readWriteLock.writeLock();
String s = "";
try {
rLock.lock();
s = "10";
Thread.sleep(10000);
redisTemplate.opsForValue().set(lockKey,s);
}finally {
rLock.unlock();
}
return s;
}
Redisson分布式锁实现原理
加锁源码分析
接口:
void lock(long leaseTime, TimeUnit unit);
进入实现方法:
@Override
public void lock(long leaseTime, TimeUnit unit) {
try {
lockInterruptibly(leaseTime, unit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
lockInterruptibly方法:
@Override
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// 尝试加锁,tryLockInnerAsync加锁成功返回null,加锁失败返回锁的剩余过期时间
Long ttl = tryAcquire(leaseTime, unit, threadId);
// ==null表示加锁成功,直接return
if (ttl == null) {
return;
}
// 加锁失败对该线程进行订阅等待
RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);
try {
// 加锁失败,进入自旋
while (true) {
// 继续尝试获取锁
ttl = tryAcquire(leaseTime, unit, threadId);
// 直到获取到锁,终止循环
if (ttl == null) {
break;
}
// 如果锁被占用,则在等待时间结束后再重试,否则直接尝试加锁
if (ttl >= 0) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().acquire();
}
}
} finally {
// 加锁完成后取消对该线程的订阅
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
tryAcquireAsync方法:
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
// 在lockInterruptibly(-1, null);传入了-1,表示使用默认的leastTime
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
// 使用异步方式尝试加锁
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
// 将该锁加入订阅,并进行锁续命
ttlRemainingFuture.addListener(new FutureListener<Long>() {
@Override
public void operationComplete(Future<Long> future) throws Exception {
if (!future.isSuccess()) {
return;
}
Long ttlRemaining = future.getNow();
// 如果锁再次获取成功,获取新的超时时间,对锁进行续命
if (ttlRemaining == null) {
// 定时任务,实现自动续期
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
tryLockInnerAsync方法(加锁核心方法):
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
//时间转化为毫秒值
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// 判断reids有没有当前key
"if (redis.call('exists', KEYS[1]) == 0) then " +
// 如果没有,用hash类型存储key,并且存储当前主线程id,把值设置为1
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
// 设置当前key的过期时间
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 如果该锁的name和锁标识都相同,表示是重入锁,将value+1,并设置超时时间
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// 调用hash的incrby方法,对值+1
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
// 设置超时时间
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 如果不是当前主线程来获取锁,返回当前锁的剩余过期时间
"return redis.call('pttl', KEYS[1]);",
// getName()传入KEYS[1],表示传入加锁的keyName
// internalLockLeaseTime传入ARGV[1],表示锁的超时时间
// getLockName(threadId)传入ARGV[2],表示锁的唯一标识,由UUID+":"+线程id组成
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
scheduleExpirationRenewal方法:
这个方法是在加锁后开启一个守护线程进行监听,也就是看门狗。
private void scheduleExpirationRenewal(final long threadId) {
if (expirationRenewalMap.containsKey(getEntryName())) {
return;
}
// 启用定时任务线程更新锁的超时时间,每internalLockLeaseTime/3秒执行一次
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判断redis中是否存在该锁,并且判断是否是当前主线程加的锁
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// 锁续命
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
future.addListener(new FutureListener<Boolean>() {
@Override
public void operationComplete(Future<Boolean> future) throws Exception {
expirationRenewalMap.remove(getEntryName());
if (!future.isSuccess()) {
log.error("Can't update lock " + getName() + " expiration", future.cause());
return;
}
// 返回true证明续期成功,则递归调用续期方法
// 返回false证明续期失败,说明对应的锁已经不存在,直接返回
if (future.getNow()) {
// 将自己线程再次加入定时任务队列,实现不断地续命
scheduleExpirationRenewal(threadId);
}
}
});
}
//设置执行频率为leaseTime转换为ms单位下的三分之一,也就是默认值30/3=10
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
task.cancel();
}
}
解锁源码分析
unlock方法:
@Override
public void unlock() {
// 调用异步解锁方法
Boolean opStatus = get(unlockInnerAsync(Thread.currentThread().getId()));
if (opStatus == null) {
throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + Thread.currentThread().getId());
}
if (opStatus) {
// 取消续命订阅
cancelExpirationRenewal();
}
}
unlockInnerAsync方法:
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 如果该锁不存在,则发布已经解锁的消息
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
// 如果该锁对应的name和锁标识不匹配,说明该客户端没有该锁,无法解锁
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
// 可重入锁的value-1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
// 如果重入锁的计数器>0表示该锁仍然有效,更新锁的超时时间
"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; " +
"return nil;",
// getName()传入KEYS[1],表示传入解锁的keyName
// getChannelName()传入KEYS[2],表示redis内部的消息订阅channel
// LockPubSub.unlockMessage传入ARGV[1],表示向其他redis客户端线程发送解锁消息
// internalLockLeaseTime传入ARGV[2],表示锁的超时时间
// getLockName(threadId)传入ARGV[3],表示锁的唯一标识,由UUID+":"+线程id组成
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
cancelExpirationRenewal方法:
void cancelExpirationRenewal() {
// 将该线程从定时任务中删除
Timeout task = expirationRenewalMap.remove(getEntryName());
if (task != null) {
task.cancel();
}
}
Redisson问题:
1、客户端长时间内阻塞导致锁失效
客户端 1 得到了锁,因为网络问题或者 GC 等原因导致长时间阻塞,然后业务程序还没执行完锁就过期了,这时候客户端 2 也能正常拿到锁,可能会导致线程安全的问题。
2、Redis 服务器时钟漂移
如果 Redis 服务器的机器时间发生了向前跳跃,就会导致这个 key 过早超时失效,比如说客户端 1 拿到锁后,key 还没有到过期时间,但是 Redis 服务器的时间比客户端快了 2 分钟,导致 key 提前就失效了,这时候,如果客户端 1 还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的问题。
3、单点实例安全问题
如果 Redis 是单机模式的,如果挂了的话,那所有的客户端都获取不到锁了,但如果是主从模式,Redis 的主从同步是异步进行的,如果 Redis 主宕机了,这个时候从机并没有同步到这一把锁,那么机器 B 再次申请的时候就会再次申请到这把锁。
为了解决这些问题 Redis 作者提出了 RedLock 红锁的算法,在 Redission 中也对 RedLock 进行了实现。
RedissonRedLock代码示例
Config config1 = new Config();
config1.useSingleServer().setAddress("127.0.0.1:6379");
RedissonClient redissonClient1 = Redisson.create(config1);
Config config2 = new Config();
config2.useSingleServer().setAddress("127.0.0.1:6378");
RedissonClient redissonClient2 = Redisson.create(config2);
Config config3 = new Config();
config3.useSingleServer().setAddress("127.0.0.1:6377");
RedissonClient redissonClient3 = Redisson.create(config3);
// 1.获取多个 RLock 对象
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);
// 2.根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
//3.尝试获取锁
//waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
//leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
boolean res = redLock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
//成功获得锁,在这里处理业务
}
} catch (Exception e) {
throw new RuntimeException("aquire lock fail");
}finally{
//无论如何, 最后都要解锁
redLock.unlock();
}
RedLock原理
客户端会在多个Redission node上申请加锁,必须半数以上节点加锁成功,才视为加锁成功,所以需要构建多个 RLock ,然后根据多个 RLock 构建成一个 RedissonRedLock,RedLock 算法是建立在多个互相独立的Redission node之上的,Redission node 节点既可以是单机模式(single),也可以是主从模式(master/salve),哨兵模式(sentinal),或者集群模式(cluster)。因为要在多个节点加锁,并且如果某个节点出现异常还需要释放锁并且回滚数据,还有最后释放锁时需要释放所有节点,所以RedLock存在性能问题。
RedLock注意事项
1、客户端在多个 Redis 实例上申请加锁,必须半数以上节点加锁成功,才视为加锁成功,解决了部分实例异常,容错性问题。并且要保证大多数节点加锁的总耗时,要小于锁设置的过期时间。
2、多实例操作可能存在网络延迟、丢包、超时等问题,所以就算是大多数节点加锁成功,如果加锁的累积耗时超过了锁的过期时间,那有些节点上的锁可能也已经失效了,还是没有意义的。
3、释放锁要向全部节点发起释放锁请求,如果部分节点加锁成功,但最后由于异常导致大部分节点没加锁成功,就要释放掉所有的,各节点要保持一致。