SpringBoot Redission实现分布式锁

为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。
而这个分布式协调技术的核心就是来实现这个分布式锁

分布式锁应该具备条件

互斥性 防止死锁 可重入 非阻塞 锁的力度

目前我所知道的有3种方式

  1. 通过 数据库实现排他锁 -- 性能比较差 不推荐
  2. 通过 zookeeper 实现 -- 目前还不了解
  3. 通过 redis(Redisson) 实现 -- 通过设置过期时间 来控制锁的获取及释放

Redisson 原理

Redisson分布式锁的实现是基于实现RLock接口

SpringBoot Redission实现分布式锁_第1张图片

1、加锁机制

线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。
线程去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。

2、watch dog自动延期机制(性能较差)

在一个分布式环境下,假如一个线程获得锁后,突然服务器宕机了,那么这个时候在一定时间后这个锁会自动释放,你也可以设置锁的有效时间(不设置默认30秒),这样的目的主要是防止死锁的发生。

3、使用lua脚本

通过封装在lua脚本中发送给redis,而且redis是单线程的,这样就保证这段复杂业务逻辑执行的原子性

Redis分布式锁的缺点

Redis分布式锁会有个缺陷,就是在Redis哨兵模式下:

客户端1 对某个master节点写入了redisson锁,此时会异步复制给对应的 slave节点。但是这个过程中一旦发生 master节点宕机,主备切换,slave节点从变为了 master节点。

这时客户端2 来尝试加锁的时候,在新的master节点上也能加锁,此时就会导致多个客户端对同一个分布式锁完成了加锁。

这时系统在业务语义上一定会出现问题,导致各种脏数据的产生

缺陷在哨兵模式或者主从模式下,如果 master实例宕机的时候,可能导致多个客户端同时完成加锁。

 

Demo

自定义starter 封装Redission 结构如下   打成jar包

源码地址:https://gitee.com/love_yu_0698/distributed-redission-spring-boot-starter.git

SpringBoot Redission实现分布式锁_第2张图片

 

新建项目测试(引入jar包)

application.yml

yu:
  redisson:
    lock:
      server:
        address: 127.0.0.1:6379
        password: redis
        type: standalone
        database: 0
/**
 * 不基于注解方式锁操作
 */
@RestController
@Slf4j
public class LockController {

    @Autowired
    RedissonLock redissonLock;

    /**
     * 模拟这个是商品库存
     */
    public static volatile Integer TOTAL = 10;


    @GetMapping("lock-decrease-stock")
    public String lockDecreaseStock() throws InterruptedException {
        redissonLock.lock("lock", 10L);
        if (TOTAL > 0) {
            TOTAL--;
        }
        Thread.sleep(50);
        log.info("===lock===减完库存后,当前库存===" + TOTAL);
        //如果该线程还持有该锁,那么释放该锁。如果该线程不持有该锁,说明该线程的锁已到过期时间,自动释放锁
        if (redissonLock.isHeldByCurrentThread("lock")) {
            redissonLock.unlock("lock");
        }
        return "=================================";
    }

    @GetMapping("trylock-decrease-stock")
    public String trylockDecreaseStock() throws InterruptedException {
        if (redissonLock.tryLock("trylock", 5L, 200L)) {
            if (TOTAL > 0) {
                TOTAL--;
            }
            Thread.sleep(50);
            redissonLock.unlock("trylock");
            log.info("====tryLock===减完库存后,当前库存===" + TOTAL);
        } else {
            log.info("[ExecutorRedisson]获取锁失败");
        }
        return "===================================";
    }

    @GetMapping("annotatin-lock-decrease-stock")
    @DistributedLock(value="goods", leaseTime=5)
    public String lockDecreaseStock() throws InterruptedException {
        if (TOTAL > 0) {
            TOTAL--;
        }
        log.info("===注解模式=== 减完库存后,当前库存===" + TOTAL);
        return "=================================";
    }

}

使用 jmeter 开启1000个线程 测试 是否会有超卖的问题

PS:大部分情况 都是使用 lock

1、tryLock锁是可能会等待的,因为当过了等待时间还没有获取锁,就会返回false,对于性能来说,这显然很致命!

2、注解锁只能用于方法上,颗粒度太大,满足不了方法内加锁。

在使用RedissonLock锁时,很容易报这类异常,比如如下操作

        //设置锁1秒过去
        redissonLock.lock("redisson", 1);
        //业务逻辑需要咨询2秒
        redissonLock.release("redisson");

原因:

线程1 进来获得锁后,但它的业务逻辑需要执行2秒,在 线程1 执行1秒后,这个锁就自动过期了,那么这个时候
线程2 进来了获得了锁。在线程1去解锁就会抛上面这个异常(因为解锁和当前锁已经不是同一线程了)

解决:

  //如果为false就说明该线程的锁已经自动释放,无需解锁
  if (redissonLock.isHeldByCurrentThread("lock")) {
      redissonLock.unlock("lock");
  }

 

参考大牛的文章:https://www.cnblogs.com/qdhxhz/p/11046905.html

你可能感兴趣的:(SpringBoot)