手写实现简易版本的Redission分布式锁

我们从一个简单的扣减库存例子开始

 

js

代码解读

复制代码

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
 

typescript

代码解读

复制代码

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
 

arduino

代码解读

复制代码

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的悲观锁,乐观锁

 

sql

代码解读

复制代码

public String deductStockMySqlPessimistic(Integer id,Integer count) { Integer result = stockMapper.updateStockByGoodsIdAndCount(id, count); if(result > 0) { return "库存扣减成功"; } return "库存不足"; }

 

less

代码解读

复制代码

@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来实现

版本一:最简单版本
 

typescript

代码解读

复制代码

@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获取到锁之后,程序突然崩溃了,这时候没有执行释放锁的操作,那么就造成了死锁,因为我们没有给锁设置超时时间,所以即使程序恢复了,但是这把锁还是一直存在的,这时候其它线程就用员拿不到这把锁了

版本二:增加过期时间避免死锁
 

typescript

代码解读

复制代码

@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实现准确释放锁
 

typescript

代码解读

复制代码

@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
 

typescript

代码解读

复制代码

@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+可重入性
 

scss

代码解读

复制代码

@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 + 可重入性 + 异步线程实现锁自动续期
 

scss

代码解读

复制代码

@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的分布式锁了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值