大家好,我是小趴菜,今天我们来一步一步的实现一个简易版本的Redission的分布式锁
我们从一个简单的扣减库存例子开始
public String deductStock(Integer id,Integer count){
//查询商品库存数量
Integer stockCount = stockMapper.selectStockById(id);
//判断库存是否充足
if(stockCount < count) {
return "库存不足";
}
//更新库存
stockMapper.updateStockById(id,stockCount - count);
return "库存扣减成功";
}
如果在并发情况下,是有可能发生超卖的情况的,为了避免这种情况发生,我们一般采取的方案之一就是加锁
jvm锁
1:ReentrantLock
public String deductStockJdkLock(Integer id,Integer count){
//获取锁
reentrantLock.lock();
try{
//查询商品库存数量
Integer stockCount = stockMapper.selectStockById(id);
//判断库存是否充足
if(stockCount < count) {
return "库存不足";
}
//更新库存
stockMapper.updateStockById(id,stockCount - count);
}finally {
reentrantLock.unlock();
}
return "库存扣减成功";
}
2:synchronized
public synchronized String deductStockSync(Integer id,Integer count){
//查询商品库存数量
Integer stockCount = stockMapper.selectStockById(id);
//判断库存是否充足
if(stockCount < count) {
return "库存不足";
}
//更新库存
stockMapper.updateStockById(id,stockCount - count);
return "库存扣减成功";
}
以上就是基于JVM来实现的锁,使用非常简单,但是也有它的局限性,比如:
- 1:在多例模式下,锁可能会失效(后续篇章解释)
- 2:在事务模式下,也有可能出现超卖的现象(后续篇章解释)
- 3:集群模式下会失效
以上两种锁简单,不是我们介绍的重点,大家可以忽略
基于MySql的悲观锁,乐观锁
public String deductStockMySqlPessimistic(Integer id,Integer count) {
Integer result = stockMapper.updateStockByGoodsIdAndCount(id, count);
if(result > 0) {
return "库存扣减成功";
}
return "库存不足";
}
@Update("update t_stock set stock = stock - #{count} where id = #{id} and stock >= #{count}")
Integer updateStockByGoodsIdAndCount(@Param("id") Integer id, @Param("count") Integer count);
悲观锁主要基于数据库的行锁来实现,乐观锁也就是基于版本号来实现了,这里就不做展示了
基于Redis实现分布式锁
首先我们要了解分布式锁的几种特性
- 1:互斥性
- 2:可重入性
- 3:死锁
- 4:锁的正确释放
- 5:阻塞和非阻塞
- 6:公平和非公平
那Redis实现分布式锁的原理是怎么样的呢?
如果这个key不存在,那么返回的就是null,所以我们可以在获取锁之前,判断这个key是否存在,如果存在,说明已经有其它线程获取到锁了,如果没有,那么就可以尝试去获取锁,Redis也提供了一个命令 setnx来实现
版本一:最简单版本
@Override
public void lock() {
//1:使用setnx指令进行加锁
while (true) {
Boolean lockResult = stringRedisTemplate.opsForValue().setIfAbsent(this.lockName, "1");
if(lockResult != null && lockResult) {
//加锁成功
break;
}
//获取锁失败,循环尝试获取锁
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
}
@Override
public void unlock() {
stringRedisTemplate.delete(this.lockName);
}
我们实现了一个最简单版本的一个分布式锁,但是会发现这个版本的存在很多问题,比如
- 不满足可重入性
- 存在死锁的可能
如果线程-A获取到锁之后,程序突然崩溃了,这时候没有执行释放锁的操作,那么就造成了死锁,因为我们没有给锁设置超时时间,所以即使程序恢复了,但是这把锁还是一直存在的,这时候其它线程就用员拿不到这把锁了
版本二:增加过期时间避免死锁
@Override
public void lock() {
lock(defaultExpireTime,TimeUnit.SECONDS);
}
@Override
public void lock(long expireTime, TimeUnit timeUnit) {
//1:使用setnx指令进行加锁
while (true) {
Boolean lockResult = stringRedisTemplate.opsForValue().setIfAbsent(this.lockName, "1",expireTime, timeUnit);
if(lockResult != null && lockResult) {
//加锁成功
break;
}
//加锁失败,循环获取锁
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
}
@Override
public void unlock() {
stringRedisTemplate.delete(this.lockName);
}
版本二我们给锁加了超时时间,即使出现线程获取到锁之后,程序崩溃了,在到达超时时间之后也会主动释放这把锁,就不会造成死锁了,其它线程之后也能正常的获取到锁,但是版本二依然存在问题
- 不满足可重入性
- 无法正确释放锁
假设一下,我们给锁的超时时间设置为10秒,这时候业务执行的时间要30秒,这时候线程-A获取到锁了,然后开始执行业务逻辑,这时候到第10秒的时候,由于锁的超时时间到了,就主动释放掉这把锁了,那么其它线程就能获取到这把锁了,然后线程-A在30秒的时候业务执行完了,执行了释放锁的操作,但是这时候获取到锁的线程并不是线程-A了,也就是线程-A把其它线程的锁给释放了
版本三:增加UUID实现准确释放锁
@Override
public void lock() {
lock(defaultExpireTime,TimeUnit.SECONDS);
}
@Override
public void lock(long expireTime, TimeUnit timeUnit) {
//1:使用setnx指令进行加锁
while (true) {
//设置value的值为uuid
Boolean lockResult = stringRedisTemplate.opsForValue().setIfAbsent(this.lockName, this.lockValue,expireTime, timeUnit);
if(lockResult != null && lockResult) {
//加锁成功
break;
}
//加锁失败,循环获取锁
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
}
@Override
public void unlock() {
/**
* 注意,这里查询+删除不是原子操作,现在线程一来查询,然后判断uuid相同,
* 然后进入if分支里面去,但是此时,因为锁的超时时间到了,线程一自己释放了这把锁,也就在此时,线程二获取到了这把锁
* 那么这时候线程一还是会执行delete操作,但是这时候删除的就是线程二的锁了,就造成了误删了
*/
//判断当前持有锁的线程是否是本线程
String lockValueResult = stringRedisTemplate.opsForValue().get(this.lockName);
if(this.lockValue.equalsIgnoreCase(lockValueResult)) {
//说明是当前线程
stringRedisTemplate.delete(this.lockName);
}
}
我们可以发现即使我们加了UUID也不能保证锁能准确的被释放,其实最主要的还是因为释放锁并不是原子性操作,所以接下来我们可以使用lua脚本来实现
版本四:redis+lua
@Override
public void lock() {
lock(defaultExpireTime,TimeUnit.SECONDS);
}
@Override
public void lock(Long expireTime, TimeUnit timeUnit) {
while (true) {
//使用lua脚本加锁
String luaLockScript = "if(redis.call('exists',KEYS[1]) == 0) then redis.call('set',KEYS[1],ARGV[1]) redis.call('pexpire',KEYS[1],ARGV[2]) return 1;else return 0;end;";
Long lockResult = stringRedisTemplate.execute(new DefaultRedisScript<>(luaLockScript, Long.class),
Collections.singletonList(this.lockName),
this.lockValue,
expireTime.toString());
if(lockResult != null && lockResult.equals(1L)) {
//加锁成功
break;
}
//加锁失败,循环获取锁
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
}
@Override
public void unlock() {
//释放锁
String luaUnLockScript = "if(redis.call('exists',KEYS[1]) == 0) then return 0;end;if(redis.call('get',KEYS[1]) == ARGV[1]) then redis.call('del', KEYS[1]) return 1;else return 0;end;";
stringRedisTemplate.execute(new DefaultRedisScript<>(luaUnLockScript, Long.class),
Collections.singletonList(this.lockName),
this.lockValue);
}
使用redis+lua就可以锁的【互斥性】,【死锁】,【锁的正确释放】,但是它依然不具备锁的可重入性
版本五:redis+lua+可重入性
@Override
public void lock() {
lock(defaultExpireTime,TimeUnit.SECONDS);
}
@Override
public void lock(Long expireTime, TimeUnit timeUnit) {
while (true) {
//使用lua脚本加锁
String luaLockScript = "if(redis.call('exists',KEYS[1]) == 0) then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('pexpire',KEYS[1],ARGV[2]) return 1;end;if(redis.call('hexists',KEYS[1],ARGV[1]) == 1) then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('pexpire',KEYS[1],ARGV[2]) return 1;else return 0;end;";
Long lockResult = stringRedisTemplate.execute(new DefaultRedisScript<>(luaLockScript, Long.class),
Collections.singletonList(this.lockName),
this.lockValue,
expireTime.toString());
if(lockResult != null && lockResult.equals(1L)) {
//加锁成功
break;
}
//加锁失败,循环获取锁
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
}
@Override
public void unlock() {
//释放锁
String luaUnLockScript = "if(redis.call('hexists',KEYS[1],ARGV[1]) == 0) then return 0;end local lockCount = redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount > 0) then redis.call('pexpire',KEYS[1],ARGV[2]) return 1;else redis.call('del',KEYS[1]) return 1; end;";
stringRedisTemplate.execute(new DefaultRedisScript<>(luaUnLockScript, Long.class),
Collections.singletonList(this.lockName),
this.lockValue,
this.defaultExpireTime);
}
现在我们通过lua脚本实现了【互斥性】,【死锁】,【锁的正确释放】,【可重入锁】,但是还有一个很重要的问题,就是锁的续期时间,在获取锁的时候,我们没办法准确的去评估我们的业务的执行时间,所以需要在锁即将过期的时候,给锁进行一个续期
版本五:redis + lua + 可重入性 + 异步线程实现锁自动续期
@Override
public void lock() {
lock(defaultExpireTime,TimeUnit.SECONDS);
}
@Override
public void lock(Long expireTime, TimeUnit timeUnit) {
while (true) {
//使用lua脚本加锁
String luaLockScript = "if(redis.call('exists',KEYS[1]) == 0) then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('pexpire',KEYS[1],ARGV[2]) return 1;end;if(redis.call('hexists',KEYS[1],ARGV[1]) == 1) then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('pexpire',KEYS[1],ARGV[2]) return 1;else return 0;end;";
Long lockResult = stringRedisTemplate.execute(new DefaultRedisScript<>(luaLockScript, Long.class),
Collections.singletonList(this.lockName),
this.lockValue,
expireTime.toString());
if(lockResult != null && lockResult.equals(1L)) {
//加锁成功,实现锁自动延期
new Thread(() -> {
while (true) {
String expireLuaScript = "if(redis.call('exists',KEYS[1],ARGV[1]) == 0) then return 0;else redis.call('pexpire',KEYS[1],ARGV[2]) return 1;end";
Long expireResult = stringRedisTemplate.execute(new DefaultRedisScript<>(expireLuaScript, Long.class),
Collections.singletonList(this.lockName),
this.lockValue,
expireTime.toString());
if(expireResult == null || expireResult.equals(0L)) {
break;
}
try {
//在锁超时的一半就开始尝试续期
Thread.sleep(expireTime / 2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
break;
}
//加锁失败,循环获取锁
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
}
@Override
public void unlock() {
//释放锁
String luaUnLockScript = "if(redis.call('hexists',KEYS[1],ARGV[1]) == 0) then return 0;end local lockCount = redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount > 0) then redis.call('pexpire',KEYS[1],ARGV[2]) return 1;else redis.call('del',KEYS[1]) return 1; end;";
stringRedisTemplate.execute(new DefaultRedisScript<>(luaUnLockScript, Long.class),
Collections.singletonList(this.lockName),
this.lockValue,
this.defaultExpireTime);
}
至此我们就完成了一个简易版本的Redission的分布式锁了,有兴趣的好兄弟可以关注下我的公众号:斌斌的小趴菜, 感谢大家