Redis实战篇《黑马点评》4 分布式锁

4.分布式锁

4.1基本原理和实现方式对比

  • 分布式锁:满足分布式系统或集群模式下多线程课件并且可以互斥的锁

  • 分布式锁的核心思想就是让大家共用同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路

  • 那么分布式锁应该满足一些什么条件呢?

    1. 可见性:多个线程都能看到相同的结果。

    注意:这里说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思

    1. 互斥:互斥是分布式锁的最基本条件,使得程序串行执行
    2. 高可用:程序不易崩溃,时时刻刻都保证较高的可用性
    3. 高性能:由于加锁本身就让性能降低,所以对于分布式锁需要他较高的加锁性能和释放锁性能
    4. 安全性:安全也是程序中必不可少的一环
  • 常见的分布式锁有三种

    1. MySQL:MySQL本身就带有锁机制,但是由于MySQL的性能一般,所以采用分布式锁的情况下,使用MySQL作为分布式锁比较少见
    2. Redis:Redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都是用Redis或者Zookeeper作为分布式锁,利用SETNX这个方法,如果插入Key成功,则表示获得到了锁,如果有人插入成功,那么其他人就回插入失败,无法获取到锁,利用这套逻辑完成互斥,从而实现分布式锁
    3. Zookeeper:Zookeeper也是企业级开发中较好的一种实现分布式锁的方案,但本文是学Redis的,所以这里就不过多阐述了
MySQLRedisZookeeper
互斥利用mysql本身的互斥锁机制利用setnx这样的互斥命令利用节点的唯一性和有序性实现互斥
高可用
高性能一般一般
安全性断开连接,自动释放锁利用锁超时时间,到期释放临时节点,断开连接自动释放

4.2Redis分布式锁的实现核心思路

  • 实现分布式锁时需要实现两个基本方法
    1. 获取锁
      • 互斥:确保只能有一个线程获取锁
      • 非阻塞:尝试一次,成功返回true,失败返回false
      SET lock thread01 NX EX 10
    2. 释放锁
      • 手动释放
      • 超时释放:获取锁的时候添加一个超时时间
      DEL lock
  • 核心思路
    • 我们利用redis的SETNX方法,当有多个线程进入时,我们就利用该方法来获取锁。第一个线程进入时,redis 中就有这个key了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁(返回了0)的线程,等待一定时间之后重试

4.3解决Redis分布式锁误删问题

  • 锁的基本接口
    public interface ILock {
        /**
         * 尝试获取锁
         *
         * @param timeoutSec 锁持有的超时时间,过期自动释放
         * @return true表示获取锁成功,false表示获取锁失败
         */
        boolean tryLock(long timeoutSec);
    
        /**
         * 释放锁
         */
        void unlock();
    }
  • 然后创建一个SimpleRedisLock类实现接口
    public class SimpleRedisLock implements ILock {
        //锁的前缀
        private static final String KEY_PREFIX = "lock:";
        //具体业务名称,将前缀和业务名拼接之后当做Key
        private String name;
        //这里不是@Autowired注入,采用的是构造器注入,在创建SimpleRedisLock时,将RedisTemplate作为参数传入
        private StringRedisTemplate stringRedisTemplate;
    
        public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
            this.name = name;
            this.stringRedisTemplate = stringRedisTemplate;
        }
    
        @Override
        public boolean tryLock(long timeoutSec) {
            //获取线程标识
            long threadId = Thread.currentThread().getId();
            //获取锁,使用SETNX方法进行加锁,同时设置过期时间,防止死锁
            Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
            //自动拆箱可能会出现null,这样写更稳妥
            return Boolean.TRUE.equals(success);
        }
    
        @Override
        public void unlock() {
            //通过DEL来删除锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
  • 修改业务代码
    @Override
    public Result seckillVoucher(Long voucherId) {
        LambdaQueryWrapper<SeckillVoucher> queryWrapper = new LambdaQueryWrapper<>();
        //1. 查询优惠券
        queryWrapper.eq(SeckillVoucher::getVoucherId, voucherId);
        SeckillVoucher seckillVoucher = seckillVoucherService.getOne(queryWrapper);
        //2. 判断秒杀时间是否开始
        if (LocalDateTime.now().isBefore(seckillVoucher.getBeginTime())) {
            return Result.fail("秒杀还未开始,请耐心等待");
        }
        //3. 判断秒杀时间是否结束
        if (LocalDateTime.now().isAfter(seckillVoucher.getEndTime())) {
            return Result.fail("秒杀已经结束!");
        }
        //4. 判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("优惠券已被抢光了哦,下次记得手速快点");
        }
        Long userId = UserHolder.getUser().getId();
        // 创建锁对象
        SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        // 获取锁对象
        boolean isLock = redisLock.tryLock(120);
        // 加锁失败,说明当前用户开了多个线程抢优惠券,但是由于key是SETNX的,所以不能创建key,得等key的TTL到期或释放锁(删除key)
        if (!isLock) {
            return Result.fail("不允许抢多张优惠券");
        }
        try {
            // 获取代理对象
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            // 释放锁
            redisLock.unlock();
        }
    }
  • 使用Jmeter进行压力测试,请求头中携带登录用户的token,最终只能抢到一张优惠券
  • 4.4Redis分布式锁误删情况说明

  • 逻辑说明
    • 持有锁的线程1在锁的内部出现了阻塞,导致他的锁TTL到期,自动释放
    • 此时线程2也来尝试获取锁,由于线程1已经释放了锁,所以线程2可以拿到
    • 但是现在线程1阻塞完了,继续往下执行,要开始释放锁了
    • 那么此时就会将属于线程2的锁释放,这就是误删别人锁的情况
  • 解决方案
    • 解决方案就是在每个线程释放锁的时候,都判断一下这个锁是不是自己的,如果不属于自己,则不进行删除操作。
    • 假设还是上面的情况,线程1阻塞,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1阻塞完了,继续往下执行,开始删除锁,但是线程1发现这把锁不是自己的,所以不进行删除锁的逻辑,当线程2执行到删除锁的逻辑时,如果TTL还未到期,则判断当前这把锁是自己的,于是删除这把锁

  • 需求:修改之前的分布式锁实现
  • 满足:在获取锁的时候存入线程标识(用UUID标识,在一个JVM中,ThreadId一般不会重复,但是我们现在是集群模式,有多个JVM,多个JVM之间可能会出现ThreadId重复的情况),在释放锁的时候先获取锁的线程标识,判断是否与当前线程标识一致
    • 如果一致则释放锁
    • 如果不一致则不释放锁
  • 核心逻辑:在存入锁的时候,放入自己的线程标识,在删除锁的时候,判断当前这把锁是不是自己存入的
    • 如果是,则进行删除
    • 如果不是,则不进行删除
  • 具体实现代码如下
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }
    
    @Override
    public void unlock() {
        // 获取当前线程的标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁中的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 判断标识是否一致
        if (threadId.equals(id)) {
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }

4.5分布式锁的原子性问题

  • 更为极端的误删逻辑说明
  • 假设线程1已经获取了锁,在判断标识一致之后,准备释放锁的时候,又出现了阻塞(例如JVM垃圾回收机制)
  • 于是锁的TTL到期了,自动释放了
  • 那么现在线程2趁虚而入,拿到了一把锁
  • 但是线程1的逻辑还没执行完,那么线程1就会执行删除锁的逻辑
  • 但是在阻塞前线程1已经判断了标识一致,所以现在线程1把线程2的锁给删了
  • 那么就相当于判断标识那行代码没有起到作用
  • 这就是删锁时的原子性问题
  • 因为线程1的拿锁,判断标识,删锁,不是原子操作,所以我们要防止刚刚的情况

4.6Lua脚本解决多条命令原子性问题

  • Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
  • Lua是一种编程语言,它的基本语法可以上菜鸟教程看看,链接:Lua 教程 | 菜鸟教程
  • 这里重点介绍Redis提供的调用函数,我们可以使用Lua去操作Redis,而且还能保证它的原子性,这样就可以实现拿锁判断标识删锁是一个原子性动作了
  • Redis提供的调用函数语法如下
    redis.call('命令名称','key','其他参数', ...)
  • 例如我们要执行set name Kyle,则脚本是这样
    redis.call('set', 'name', 'Kyle')
  • 例如我我们要执行set name David,在执行get name,则脚本如下
    ## 先执行set name David
    redis.call('set', 'name', 'David')
    ## 再执行get name
    local name = redis.call('get', 'name')
    ## 返回
    return name
  • 写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下
    EVAL script numkeys key [key ...] arg [arg ...]
  • 例如,我们要调用redis.call('set', 'name', 'Kyle') 0这个脚本,语法如下
    EVAL "return redis.call('set', 'name', 'Kyle')" 0

  • 如果脚本中的key和value不想写死,可以作为参数传递,key类型参数会放入KEYS数组,其他参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组中获取这些参数
  • 注意:在Lua中,数组下标从1开始

    EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Lucy

  • 那现在我们来使用Lua脚本来代替我们释放锁的逻辑
    -- 这里的KEYS[1]就是传入锁的key
    -- 这里的ARGV[1]就是线程标识
    -- 比较锁中的线程标识与线程标识是否一致
    if (redis.call('get', KEYS[1]) == ARGV[1]) then
        -- 一致则释放锁
        return redis.call('del', KEYS[1])
    end
    return 0

    4.7利用Java代码调用Lua脚本改造分布式锁

在resources下创建unlock.lua,会提示下载一个plugins点击install,然后只需要下载一个EmmyLua即可,实测如果下载了多个Lua相关的插件会产生冲突,最终导致IDEA打不开,这真是血泪的教训!

SimpleRedisLock中写入如下的代码,因为我们希望的是在一开始就将Lua的脚本加载好,而不是等到要调用释放锁的时候再去加载Lua脚本,所以采用静态变量和静态代码块,这些部分在类初始化的时候就会被加载:

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
     UNLOCK_SCRIPT = new DefaultRedisScript<>();
     UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
     UNLOCK_SCRIPT.setResultType(Long.class);
}

 在SimpleRedisLock类的unlock方法中写入如下的代码:

@Override
public void unlock() {
    stringRedisTemplate.execute(UNLOCK_SCRIPT,
            Collections.singletonList(KEY_PREFIX+name),
            ID_PREFIX + Thread.currentThread().getId());
}

在程序1和程序2的下面这个位置打上断点:

在测试API中测试访问如下的URL:

http://localhost:8080/api/voucher-order/seckill/14

分别测试秒杀优惠券1和2: 

 

在Redis中能看到程序1获取锁成功,然后直接把lock锁删掉,模拟超时释放的情况:

然后让程序2往下走一步,可以看到程序2获取到了锁

然后可以直接放行程序1,会看到结果是程序2加的锁没有被删除。

最后放行程序2,会看到程序2加的锁被删除。

总结:

基于Redis的分布式锁的实现思路:

1.利用set nx ex获取锁,并设置过期时间,保存线程标示。

2.释放锁时先判断线程标示是否与自己一致,一致则删除锁。

特性:

1.利用set nx满足互斥性。

2.利用set nx保障故障时锁依然能够释放,避免死锁,提高安全性。

3.利用Redis集群保障高可用和高并发的特性。

4.8分布式锁-Redisson功能介绍

  • 基于SETNX实现的分布式锁存在以下问题
    1. 重入问题
      • 重入问题是指获取锁的线程,可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,例如在HashTable这样的代码中,它的方法都是使用synchronized修饰的,加入它在一个方法内调用另一个方法,如果此时是不可重入的,那就死锁了。所以可重入锁的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的
    2. 不可重试
      • 我们编写的分布式锁只能尝试一次,失败了就返回false,没有重试机制。但合理的情况应该是:当线程获取锁失败后,他应该能再次尝试获取锁
    3. 超时释放
      • 我们在加锁的时候增加了TTL,这样我们可以防止死锁,但是如果卡顿(阻塞)时间太长,也会导致锁的释放。虽然我们采用Lua脚本来防止删锁的时候,误删别人的锁,但现在的新问题是没锁住,也有安全隐患
    4. 主从一致性
      • 如果Redis提供了主从集群,那么当我们向集群写数据时,主机需要异步的将数据同步给从机,万一在同步之前,主机宕机了(主从同步存在延迟,虽然时间很短,但还是发生了),那么又会出现死锁问题
  • 那么什么是Redisson呢
    • Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现
  • Redis提供了分布式锁的多种多样功能
    1. 可重入锁(Reentrant Lock)
    2. 公平锁(Fair Lock)
    3. 联锁(MultiLock)
    4. 红锁(RedLock)
    5. 读写锁(ReadWriteLock)
    6. 信号量(Semaphore)
    7. 可过期性信号量(PermitExpirableSemaphore)
    8. 闭锁(CountDownLatch)

4.9Redisson入门

  1. 导入依赖
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.13.6</version>
    </dependency>
  2. 配置Redisson客户端,在config包下新建RedissonConfig
    import org.redisson.Redisson;
    import org.redisson.api.RedissonClient;
    import org.redisson.config.Config;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class RedissonConfig {
        @Bean
        public RedissonClient redissonClient() {
            Config config = new Config();
            config.useSingleServer()
                .setAddress("redis://101.XXX.XXX.160:6379")
                .setPassword("root");
            return Redisson.create(config);
        }
    }
  3. 使用Redisson的分布式锁
    @Resource
    private RedissonClient redissonClient;
    
    @Test
    void testRedisson() throws InterruptedException {
        //获取可重入锁
        RLock lock = redissonClient.getLock("anyLock");
        //尝试获取锁,三个参数分别是:获取锁的最大等待时间(期间会重试),锁的自动释放时间,时间单位
        boolean success = lock.tryLock(1,10, TimeUnit.SECONDS);
        //判断获取锁成功
        if (success) {
            try {
                System.out.println("执行业务");
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    }
  4. 替换我们之前自己写的分布式锁
    @Resource
    private RedissonClient redissonClient;
    @Override
    public Result seckillVoucher(Long voucherId) {
        LambdaQueryWrapper<SeckillVoucher> queryWrapper = new LambdaQueryWrapper<>();
        //1. 查询优惠券
        queryWrapper.eq(SeckillVoucher::getVoucherId, voucherId);
        SeckillVoucher seckillVoucher = seckillVoucherService.getOne(queryWrapper);
        //2. 判断秒杀时间是否开始
        if (LocalDateTime.now().isBefore(seckillVoucher.getBeginTime())) {
            return Result.fail("秒杀还未开始,请耐心等待");
        }
        //3. 判断秒杀时间是否结束
        if (LocalDateTime.now().isAfter(seckillVoucher.getEndTime())) {
            return Result.fail("秒杀已经结束!");
        }
        //4. 判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("优惠券已被抢光了哦,下次记得手速快点");
        }
        Long userId = UserHolder.getUser().getId();
        RLock redisLock = redissonClient.getLock("order:" + userId);
        boolean isLock = redisLock.tryLock();
        if (!isLock) {
            return Result.fail("不允许抢多张优惠券");
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            redisLock.unlock();
        }
    }

4.10Redisson可重入锁原理

  • 在Lock锁中,他是借助于等曾的一个voaltile的一个state变量来记录重入的状态的

    • 如果当前没有人持有这把锁,那么state = 0
    • 如果人持有这把锁,那么state = 1
      • 如果持有者把锁的人再次持有这把锁,那么state会+1
    • 如果对于synchronize而言,他在c语言代码中会有一个count
    • 原理与state类似,也是重入一次就+1,释放一次就-1,直至减到0,表示这把锁没有被人持有
  • 在redisson中,我们也支持可重入锁

    • 在分布式锁中,它采用hash结构来存储锁,其中外层key表示这把锁是否存在,内层key则记录当前这把锁被哪个线程持有
  • method1在方法内部调用method2,method1和method2出于同一个线程,那么method1已经拿到一把锁了,想进入method2中拿另外一把锁,必然是拿不到的,于是就出现了死锁

@Resource
private RedissonClient redissonClient;

private RLock lock;

@BeforeEach
void setUp() {
    lock = redissonClient.getLock("lock");
}

@Test
void method1() {
    boolean success = lock.tryLock();
    if (!success) {
        log.error("获取锁失败,1");
        return;
    }
    try {
        log.info("获取锁成功");
        method2();
    } finally {
        log.info("释放锁,1");
        lock.unlock();
    }
}

void method2() {
    RLock lock = redissonClient.getLock("lock");
    boolean success = lock.tryLock();
    if (!success) {
        log.error("获取锁失败,2");
        return;
    }
    try {
        log.info("获取锁成功,2");
    } finally {
        log.info("释放锁,2");
        lock.unlock();
    }
}
  • 所以我们需要额外判断,method1和method2是否处于同一线程,如果是同一个线程,则可以拿到锁,但是state会+1,之后执行method2中的方法,释放锁,释放锁的时候也只是将state进行-1,只有减至0,才会真正释放锁
  • 由于我们需要额外存储一个state,所以用字符串型SET NX EX是不行的,需要用到Hash结构,但是Hash结构又没有NX这种方法,所以我们需要将原有的逻辑拆开,进行手动判断

  • 为了保证原子性,所以流程图中的业务逻辑也是需要我们用Lua来实现的
    • 获取锁的逻辑
      local key = KEYS[1]; -- 锁的key
      local threadId = ARGV[1]; -- 线程唯一标识
      local releaseTime = ARGV[2]; -- 锁的自动释放时间
      -- 锁不存在
      if (redis.call('exists', key) == 0) then
          -- 获取锁并添加线程标识,state设为1
          redis.call('hset', key, threadId, '1');
          -- 设置锁有效期
          redis.call('expire', key, releaseTime);
          return 1; -- 返回结果
      end;
      -- 锁存在,判断threadId是否为自己
      if (redis.call('hexists', key, threadId) == 1) then
          -- 锁存在,重入次数 +1,这里用的是hash结构的incrby增长
          redis.call('hincrby', key, thread, 1);
          -- 设置锁的有效期
          redis.call('expire', key, releaseTime);
          return 1; -- 返回结果
      end;
      return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败
    • 释放锁的逻辑
      local key = KEYS[1];
      local threadId = ARGV[1];
      local releaseTime = ARGV[2];
      -- 如果锁不是自己的
      if (redis.call('HEXISTS', key, threadId) == 0) then
          return nil; -- 直接返回
      end;
      -- 锁是自己的,锁计数-1,还是用hincrby,不过自增长的值为-1
      local count = redis.call('hincrby', key, threadId, -1);
      -- 判断重入次数为多少
      if (count > 0) then
          -- 大于0,重置有效期
          redis.call('expire', key, releaseTime);
          return nil;
      else
          -- 否则直接释放锁
          redis.call('del', key);
          return nil;
      end;
    • 获取锁源码
      查看源码,跟我们的实现方式几乎一致
      <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
          this.internalLockLeaseTime = unit.toMillis(leaseTime);
          return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then 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 redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
      }
    • 释放锁源码
      protected RFuture<Boolean> unlockInnerAsync(long threadId) {
          return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 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;", Arrays.asList(this.getName(), this.getChannelName()), LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId));
      }

      4.11Redisson锁重试和WatchDog机制

    • 前面我们分析的是空参的tryLock方法,现在我们来分析一下这个带参数的
      <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
          this.internalLockLeaseTime = unit.toMillis(leaseTime);
          return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then 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 redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
      }
    • 源码分析
    • tryAcquireAsync
      private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
          
          if (leaseTime != -1L) {
              return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
          } else {
              // 如果没有指定释放时间时间,则指定默认释放时间为getLockWatchdogTimeout,底层源码显示是30*1000ms,也就是30秒
              RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
              ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                  if (e == null) {
                      if (ttlRemaining == null) {
                          this.scheduleExpirationRenewal(threadId);
                      }
      
                  }
              });
              return ttlRemainingFuture;
          }
      }
    • tryLock
      public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
              long time = unit.toMillis(waitTime);
              long current = System.currentTimeMillis();
              long threadId = Thread.currentThread().getId();
              Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
              //判断ttl是否为null
              if (ttl == null) {
                  return true;
              } else {
                  //计算当前时间与获取锁时间的差值,让等待时间减去这个值
                  time -= System.currentTimeMillis() - current;
                  //如果消耗时间太长了,直接返回false,获取锁失败
                  if (time <= 0L) {
                      this.acquireFailed(waitTime, unit, threadId);
                      return false;
                  } else {
                      //等待时间还有剩余,再次获取当前时间
                      current = System.currentTimeMillis();
                      //订阅别人释放锁的信号
                      RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);
                      //在剩余时间内,等待这个信号
                      if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
                          if (!subscribeFuture.cancel(false)) {
                              subscribeFuture.onComplete((res, e) -> {
                                  if (e == null) {
                                      //取消订阅
                                      this.unsubscribe(subscribeFuture, threadId);
                                  }
      
                              });
                          }
                          //剩余时间内没等到,返回false
                          this.acquireFailed(waitTime, unit, threadId);
                          return false;
                      } else {
                          try {
                              //如果剩余时间内等到了别人释放锁的信号,再次计算当前剩余最大等待时间
                              time -= System.currentTimeMillis() - current;
                              if (time <= 0L) {
                                  //如果剩余时间为负数,则直接返回false
                                  this.acquireFailed(waitTime, unit, threadId);
                                  boolean var20 = false;
                                  return var20;
                              } else {
                                  boolean var16;
                                  do {
                                      //如果剩余时间等到了,dowhile循环重试获取锁
                                      long currentTime = System.currentTimeMillis();
                                      ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
                                      if (ttl == null) {
                                          var16 = true;
                                          return var16;
                                      }
      
                                      time -= System.currentTimeMillis() - currentTime;
                                      if (time <= 0L) {
                                          this.acquireFailed(waitTime, unit, threadId);
                                          var16 = false;
                                          return var16;
                                      }
      
                                      currentTime = System.currentTimeMillis();
                                      if (ttl >= 0L && ttl < time) {
                                          ((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                                      } else {
                                          ((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                                      }
      
                                      time -= System.currentTimeMillis() - currentTime;
                                  } while(time > 0L);
      
                                  this.acquireFailed(waitTime, unit, threadId);
                                  var16 = false;
                                  return var16;
                              }
                          } finally {
                              this.unsubscribe(subscribeFuture, threadId);
                          }
                      }
                  }
              }
          }
    • scheduleExpirationRenewal
      private void scheduleExpirationRenewal(long threadId) {
          ExpirationEntry entry = new ExpirationEntry();  
          //不存在,才put,表明是第一次进入,不是重入
          ExpirationEntry oldEntry = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
          if (oldEntry != null) {
              oldEntry.addThreadId(threadId);
          } else {
              //如果是第一次进入,则跟新有效期
              entry.addThreadId(threadId);
              this.renewExpiration();
          }
      }
    • renewExpiration
      private void renewExpiration() {
          ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
          if (ee != null) {
              //Timeout是一个定时任务
              Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                  public void run(Timeout timeout) throws Exception {
                      ExpirationEntry ent = (ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
                      if (ent != null) {
                          Long threadId = ent.getFirstThreadId();
                          if (threadId != null) {
                              //重置有效期
                              RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
                              future.onComplete((res, e) -> {
                                  if (e != null) {
                                      RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
                                  } else {
                                      if (res) {
                                          //然后调用自己,递归重置有效期
                                          RedissonLock.this.renewExpiration();
                                      }
      
                                  }
                              });
                          }
                      }
                  }
                  //internalLockLeaseTime是之前WatchDog默认有效期30秒,那这里就是 30 / 3 = 10秒之后,才会执行
              }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
              ee.setTimeout(task);
          }
      }
    • renewExpirationAsync
      重点看lua脚本,先判断锁是不是自己的,然后更新有效时间
      protected RFuture<Boolean> renewExpirationAsync(long threadId) {
          return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
      }
    • 那么之前的重置有效期的行为该怎么终止呢?当然是释放锁的时候会终止
    • cancelExpirationRenewal
      void cancelExpirationRenewal(Long threadId) {
          //将之前的线程终止掉
          ExpirationEntry task = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
          if (task != null) {
              if (threadId != null) {
                  task.removeThreadId(threadId);
              }
      
              if (threadId == null || task.hasNoThreads()) {
                  //获取之前的定时任务
                  Timeout timeout = task.getTimeout();
                  if (timeout != null) {
                      //取消
                      timeout.cancel();
                  }
      
                  EXPIRATION_RENEWAL_MAP.remove(this.getEntryName());
              }
      
          }
      }

      4.12Redisson锁的MutiLock原理

    • 为了提高Redis的可用性,我们会搭建集群或者主从,现在以主从为例

    • 此时我们去写命令,写在主机上,主机会将数据同步给从机,但是假设主机还没来得及把数据写入到从机去的时候,主机宕机了

    • 哨兵会发现主机宕机了,于是选举一个slave(从机)变成master(主机),而此时新的master(主机)上并没有锁的信息,那么其他线程就可以获取锁,又会引发安全问题

    • 为了解决这个问题。Redisson提出来了MutiLock锁,使用这把锁的话,那我们就不用主从了,每个节点的地位都是一样的,都可以当做是主机,那我们就需要将加锁的逻辑写入到每一个主从节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么他去获取锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性

             小结

  1. 不可重入Redis分布式锁
    • 原理:利用SETNX的互斥性;利用EX避免死锁;释放锁时判断线程标识
    • 缺陷:不可重入、无法重试、锁超时失效
  2. 可重入Redis分布式锁
    • 原理:利用Hash结构,记录线程标识与重入次数;利用WatchDog延续锁时间;利用信号量控制锁重试等待
    • 缺陷:Redis宕机引起锁失效问题
  3. Redisson的multiLock
    • 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值