Redis6系列20-分布式锁

276 阅读17分钟

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

0. 面试真题

  • Redis除了缓存,你还见过基于Redis的什么用法?
  • Redis做分布锁的时候需要注意的问题?
  • 如果redis是单点部署,会带来什么问题?怎么解决单点问题?
  • 集群模式下,比如主从模式,会存在什么问题?
  • 你知道redis是怎么解决集群模式下分布式锁不靠谱的问题吗?
  • 简单介绍下RedLock,redisson?
  • 你觉得RedLock存在什么问题?
  • Redis分布式锁如何续期?看门狗知道吗?

1. 概念

1.1 锁的种类

  • 单机版同一个JVM虚拟机内,synchronized和lock接口
  • 分布式不同JVM虚拟机内,单机的线程锁机制不在起作用

1.2 分布锁具备的条件和刚需

  1. 独占性:任何时刻只能有且仅有一个线程持有
  2. 高可用:若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
  3. 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止锁的方案
  4. 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放
  5. 重入性:同一个节点的同一个线程如果获得锁之后,它可以再次获取这个锁

2. 分布式锁

命令:SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX : key在多少秒后过期
  • PX : key在多少毫秒后过期
  • NX : 当key不存在时,才创建key,效果等同于setnx
  • XX : 当key存在时,覆盖key 注意:不能使用setnx+expire两条命令,这两条命令非原子性,不安全
    多个服务间保证同一时刻同一时间段内,一个用户只能有一个请求(防止出现并发攻击)

2.1 代码案例

public class GoodController {

    private static final String GOOD_KEY = "goods:001";
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/buy_goods")
    public String buy_Goods() {
        

        String result = stringRedisTemplate.opsForValue().get(GOOD_KEY);
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);

        if(goodsNumber > 0) {
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set(GOOD_KEY, String.valueOf(realNumber));
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件");
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件";
        }else{
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临");
        }

        return "商品已经售罄/活动结束/调用超时,欢迎下次光临";
    }
}

2.2 案例中存在的问题

2.2.1 单机版未加锁

问题:单机版未加锁,并发下数字不对,出现超卖现象 解决办法:使用synchronized 或lock ,如下

public class GoodController {

    private static final String GOOD_KEY = "goods:001";
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/buy_goods")
    public String buy_Goods() {
        
        synchronized ( this ) {
            String result = stringRedisTemplate.opsForValue().get(GOOD_KEY);
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set(GOOD_KEY, String.valueOf(realNumber));
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件");
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件";
            }else{
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临");
            }

            return "商品已经售罄/活动结束/调用超时,欢迎下次光临";
        }
    }
}

总结:

  • 在单机环境下,可以使用synchronized或Lock来实现。 
  • 但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中), 所以需要一个让所有进程都能访问到的锁来实现 (比如redis或者zookeeper来构建) 
  • 不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程

2.2.2 分布式部署

服务分布式部署后,单机锁还是会出现超卖现象,需要分布式锁

public class GoodController {

    private static final String GOOD_KEY = "goods:001";
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/buy_goods")
    public String buy_Goods() {
        
        String key =  "test_RedisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        Boolean flagLock =  stringRedisTemplate.opsForValue().setIfAbsent(key, value);
        if (!flagLock){
             return "抢夺锁失败" ;
        }
        
        String result = stringRedisTemplate.opsForValue().get(GOOD_KEY);
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);

        if(goodsNumber > 0) {
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set(GOOD_KEY, String.valueOf(realNumber));
            
            stringRedisTemplate.delete(key);
            
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件");
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件";
        }else{
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临");
        }

        return "商品已经售罄/活动结束/调用超时,欢迎下次光临";
    }
}

2.2.3 关闭锁资源

finally 关闭锁资源,如果出现异常可以无法释放锁,必须要在代码层面finally释放锁。

public class GoodController {

    private static final String GOOD_KEY = "goods:001";
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/buy_goods")
    public String buy_Goods() {
        
        String key =  "test_RedisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        try{
            Boolean flagLock =  stringRedisTemplate.opsForValue().setIfAbsent(key, value);
            if (!flagLock){
                 return "抢夺锁失败" ;
            }

            String result = stringRedisTemplate.opsForValue().get(GOOD_KEY);
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set(GOOD_KEY, String.valueOf(realNumber));
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件");
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件";
            }else{
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临");
            }

            return "商品已经售罄/活动结束/调用超时,欢迎下次光临";
        }finally{
            stringRedisTemplate.delete(key);
        } 
    }
}

2.2.4 宕机了

部署微服务的机器宕机了,代码就没有走到finally里面,没办法保证释放锁,这个key没有被删除,需要加一个过期时间限定key

public class GoodController {

    private static final String GOOD_KEY = "goods:001";
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/buy_goods")
    public String buy_Goods() {
        
        String key =  "test_RedisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        try{
            Boolean flagLock =  stringRedisTemplate.opsForValue().setIfAbsent(key, value);
            stringRedisTemplate.expire(key,10L,TimeUnit.SECONDS);
            if (!flagLock){
                 return "抢夺锁失败" ;
            }

            String result = stringRedisTemplate.opsForValue().get(GOOD_KEY);
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set(GOOD_KEY, String.valueOf(realNumber));
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件");
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件";
            }else{
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临");
            }

            return "商品已经售罄/活动结束/调用超时,欢迎下次光临";
        }finally{
            stringRedisTemplate.delete(key);
        } 
    }
}

2.2.5 保证原子性

设置key和过期时间是两个命令,不能保证原子性,必须合并成一行保证原子性

public class GoodController {

    private static final String GOOD_KEY = "goods:001";
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/buy_goods")
    public String buy_Goods() {
        
        String key =  "test_RedisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        try{
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key,value,10L,TimeUnit.SECONDS);
            if (!flagLock){
                 return "抢夺锁失败" ;
            }

            String result = stringRedisTemplate.opsForValue().get(GOOD_KEY);
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set(GOOD_KEY, String.valueOf(realNumber));
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件");
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件";
            }else{
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临");
            }

            return "商品已经售罄/活动结束/调用超时,欢迎下次光临";
        }finally{
            stringRedisTemplate.delete(key);
        } 
    }
}

2.2.6 删除了别人的锁

张冠李戴,删除了被人的锁,只能删除自己的锁

public class GoodController {

    private static final String GOOD_KEY = "goods:001";
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/buy_goods")
    public String buy_Goods() {
        
        String key =  "test_RedisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        try{
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key,value,10L,TimeUnit.SECONDS);
            if (!flagLock){
                 return "抢夺锁失败" ;
            }

            String result = stringRedisTemplate.opsForValue().get(GOOD_KEY);
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set(GOOD_KEY, String.valueOf(realNumber));
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件");
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件";
            }else{
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临");
            }

            return "商品已经售罄/活动结束/调用超时,欢迎下次光临";
        }finally{
            if (stringRedisTemplate.opsForValue().get(key).equals(value)) { 
                 stringRedisTemplate.delete(key);
            }
        } 
    }
}

2.2.7 删除锁不是原子性

finally的判断和删除锁不是原子性;redis调用Lua脚本使用eval命令保证代码的原子性

public class RedisUtils{
     private static JedisPool  jedisPool ;

     static {
        JedisPoolConfig jedisPoolConfig= new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal( 20 );
        jedisPoolConfig.setMaxIdle( 10 );
        jedisPool = new JedisPool(jedisPoolConfig, "192.168.111.147" , 6379 );
    }

     public static Jedis getJedis()  throws Exception {
         if ( null != jedisPool){
             return jedisPool.getResource();
        }
         throw new Exception( "Jedispool was not init");
    }
}
public class GoodController {

    private static final String GOOD_KEY = "goods:001";
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/buy_goods")
    public String buy_Goods() {
        
        String key =  "test_RedisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        try{
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key,value,10L,TimeUnit.SECONDS);
            if (!flagLock){
                 return "抢夺锁失败" ;
            }

            String result = stringRedisTemplate.opsForValue().get(GOOD_KEY);
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set(GOOD_KEY, String.valueOf(realNumber));
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件");
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件";
            }else{
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临");
            }

            return "商品已经售罄/活动结束/调用超时,欢迎下次光临";
        }finally{
            Jedis jedis = RedisUtils.getJedis ();
            String script =  "if redis.call('get', KEYS[1]) == ARGV[1] " +
                     "then " +
                     "return redis.call('del', KEYS[1]) " +
                     "else " +
                     "   return 0 " +
                     "end" ;

             try {
                Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
                 if ("1".equals(result.toString())) {
                    System.out.println( "------del REDIS_LOCK_KEY success");
                }else {
                    System.out.println("-----del REDIS_LOCK_KEY error");
                }
            }finally{
                 if(null != jedis) {
                    jedis.close();
                }
            }
        } 
    }
}

截止到这里,单个redis节点实现分布式锁了。

2.2.8 Redis集合和CAP

  • 确保锁的过期时间大于业务执行时间,redis分布式锁如何续期?看门狗
  • redis单个节点:CP
  • redis集群:AP,redis异步复制导致锁丢失,比如主节点没来得及把刚刚set进来的这条数据给从节点,主节点就挂了,从节点没有这个数据

2.2.9 redis集群实现分布式锁

redis集群,我们直接使用Redisson(RedLock算法)来实现。

@Configuration
public class RedisConfig {
    @Bean
    public Redisson redisson()
    {
        Config config = new Config();
        // 使用多个redis节点
        // config.useClusterServers().addNodeAddress("redis://192.168.111.147:6379", "redis://192.168.111.148:6379");
        // 使用单个redis节点
        config.useSingleServer().setAddress("redis://192.168.111.147:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}

public class GoodControllerV10 {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    public static final String KEY = "lock_0511";

    @Value("${server.port}")
    private String serverPort;
    @Autowired
    private Redisson redisson;

    @GetMapping("/buy_goods")
    public String buy_Goods() throws Exception
    {
        RLock redissonLock = redisson.getLock(KEY);
        redissonLock.lock();
        try {

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if(goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
            }else{
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+ "\t 服务器端口:"+serverPort;
        } finally 
            redissonLock.unlock();
        }
    }
}

redLock相关文档:github.com/redisson/re…

当我们执行代码会发现报错,如下: image.png

public class GoodControllerV10 {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    public static final String KEY = "lock_0511";

    @Value("${server.port}")
    private String serverPort;
    @Autowired
    private Redisson redisson;

    @GetMapping("/buy_goods")
    public String buy_Goods() throws Exception
    {
        RLock redissonLock = redisson.getLock(KEY);
        redissonLock.lock();
        try {

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if(goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
            }else{
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+ "\t 服务器端口:"+serverPort;
        } finally {
            if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
                redissonLock.unlock();
            }
        }
    }
}

2.2.10 总结

  1. synchronized 单机版是可以的,上分布式
  2. 分布式服务部署,上redis分布式锁setnx
  3. 只加了锁,但是没有释放锁,出异常的话可能没有释放锁,必须要在代码层面finally释放锁
  4. 宕机了,代码没有走到finally层面没有办法释放锁,所以给key设置过期时间
  5. 为redis分布式锁加过期时间,此外setnx和过期时间必须用一条命令
  6. 防止张冠李戴,只能删除自己的锁,不能删除别人的锁
  7. redis集群环境下,使用Redisson实现分布式锁

3. RedLock算法-分布式锁

3.1 前言

使用场景:多个服务间保证同一时刻同一时间段内,一个用户只能有一个请求(防止出现并发攻击),Redis分布式锁使用正确姿势是使用Redisson客户端工具。

3.2 单机案例

三个重要元素:

  • 加锁:实际上就是在redis中,给key键设置一个值,为避免死锁,并给定一个过期时间
  • 解锁:将key键删除,但也不能乱删,只能删除自己的锁;为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。
  • 超时:锁key要注意过期时间,不能长期占用

单机模式下,一般都是set/setnx +lua脚本搞定,想想它的缺点是什么?对于一般中小型公司,不是高并发场景,单机redis也是可以扛住的。

3.3 多机案例

3.3.1 setnx分布锁锁的缺点

image.png

  • 线程 1 首先获取锁成功,将键值对写入 redis 的 master 节点; 
  • 在 redis 将该键值对同步到 slave 节点之前,master 发生了故障; 
  • redis 触发故障转移,其中一个 slave 升级为新的 master; 
  • 此时新的 master 并不包含线程 1 写入的键值对,因此线程 2 尝试获取锁也可以成功拿到锁; 
  • 此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据。

我们加的是排它独占锁同一时间只能有一个建redis锁成功并持有锁, 严禁出现2个以上的请求线程拿到锁。危险的。

redis之父提出了使用RedLock算法解决这个问题:Redis也提供了Redlock算法,用来实现 基于多个实例的 分布式锁。 锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用。

3.3.2 RedLock算法

该方案也是基于(set 加锁、Lua 脚本解锁)进行改良的,所以redis之父antirez 只描述了差异的地方,大致方案如下。 

假设我们有N个Redis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统, 为了取到锁客户端执行以下操作:

  1. 获取当前时间,以毫秒为单位;
  2. 次尝试从5个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁;
  3. 客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功;
  4. 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。
  5. 如果由于某些原因未能获得锁(无法在至少 N/2 + 1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

该方案为了解决数据不一致的问题, 直接舍弃了异步复制只使用 master 节点 ,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5

客户端只有在满足下面的这两个条件时,才能认为是加锁成功。 

  • 条件1:客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁; 
  • 条件2:客户端获取锁的总耗时没有超过锁的有效时间

redis集群是AP,采用N个节点,N为奇数,N个节点各自完全独立,不是主从或者集群。为什么是奇数?  N = 2X + 1   (N是最终部署机器数,X是容错机器数)。

  1. 先知道什么是容错
    • 失败了多少个机器实例后我还是可以容忍的,所谓的容忍就是数据一致性还是可以Ok的,CP数据一致性还是可以满足 
    • 加入在集群环境中,redis失败1台,可接受。2X+1 = 2 * 1+1 =3,部署3台,死了1个剩下2个可以正常工作,那就部署3台。 
    • 加入在集群环境中,redis失败2台,可接受。2X+1 = 2 * 2+1 =5,部署5台,死了2个剩下3个可以正常工作,那就部署5台。
  2. 为什么是奇数?
    • 最少的机器,最多的产出效果 
    • 加入在集群环境中,redis失败1台,可接受。2N+2= 2 * 1+2 =4,部署4台 
    • 加入在集群环境中,redis失败2台,可接受。2N+2 = 2 * 2+2 =6,部署6台

代码如下:

public class RedLockController {

    public static final String CACHE_KEY_REDLOCK = "REDLOCK";

    @Autowired
    RedissonClient redissonClient1;

    @Autowired
    RedissonClient redissonClient2;

    @Autowired
    RedissonClient redissonClient3;

    @GetMapping(value = "/redlock")
    public void getlock() {
        //CACHE_KEY_REDLOCK为redis 分布式锁的key
        RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK);
        RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK);
        RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK);

        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        boolean isLockBoolean;

        try {
            //waitTime 抢锁的等待时间,正常情况下 等3秒
            //leaseTime就是redis key的过期时间,正常情况下等5分钟300秒。
            isLockBoolean = redLock.tryLock(3, 300, TimeUnit.SECONDS);
            log.info("线程{},是否拿到锁:{} ",Thread.currentThread().getName(),isLockBoolean);
            if (isLockBoolean) {
                System.out.println(Thread.currentThread().getName()+"\t"+"---come in biz");
                //业务逻辑,忙10分钟
                try { TimeUnit.MINUTES.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
            }
        } catch (Exception e) {
            log.error("redlock exception ",e);
        } finally {
            // 无论如何, 最后都要解锁
            redLock.unlock();
        }
    }
}

3.3.3 Redisson

对于java语言,RedLock落地实现就是Redisson。
Redisson理念官网
redisson官网
redisson值github
redisson之解决分布式锁 代码如下:

public class WatchDogDemo {
    public static final String LOCKKEY = "AAA";

    private static Config config;
    private static Redisson redisson;

    static {
        config = new Config();
        config.useSingleServer().setAddress("redis://"+"192.168.111.147"+":6379").setDatabase(0);
        redisson = (Redisson)Redisson.create(config);
    }

    public static void main(String[] args) {
        RLock redissonLock = redisson.getLock(LOCKKEY);

        redissonLock.lock();
        try {
            System.out.println("1111-------biz");
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(25); } catch (InterruptedException e) { e.printStackTrace(); }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
            {
                redissonLock.unlock();
            }
        }

        System.out.println(Thread.currentThread().getName() + " main ------ ends.");

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        redisson.shutdown();
    }
}
  • 缓存续命:redis分布式锁过期了,但是业务逻辑还没处理完怎么办?
  • 守护线程续命:额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间; 你觉得 守护线程‘续命’这个设计方案,还有什么问题吗?
  • 分布式难以避免的,系统时钟影响:如果线程 1 从 3 个实例获取到了锁。但是这 3 个实例中的某个实例的系统时间走的稍微快一点,则它持有的锁会提前过期被释放,当他释放后,此时又有 3 个实例是空闲的,则线程2也可以获取到锁,则可能出现两个线程同时持有锁了。

3.4 缓存续命-redsson源码分析

  1. 通过redisson新建出来的锁 key,默认都是30s。RedissionLock.tryAcquireAsync()方法: image.png 默认值为:
private long lockWatchdogTimeout = 30 * 1000;
  1. 加锁成功的回调,定时任务

image.png

  1. watch dog 自动延期机制 客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始。RedissonLock.scheduleExpirationRenewal()如下:
private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        renewExpiration();
    }
}

这里面初始化了一个定时器,dely 的时间是 internalLockLeaseTime/3。在 Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 续期一次,每次 30s。renewExpiration()如下:

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                
                if (res) {
                    // reschedule itself
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}
  1. 加锁 RedissonLock.tryLockInnerAsync()如下: image.png ||| |--|--| |KEYS[1]代表的是你加锁的那个key|RLock redissonLock = redisson.getLock("lockzzyy");这里你自己设置了加锁的那个锁key| |ARGV[2]代表的是加锁的客户端的ID|image.png| |ARGV[1]就是锁key的默认生存时间|默认30秒| |如何加锁|你要加锁的那个锁key不存在的话,你就进行加锁:(hincrby 7bcf6a9f-e7f7-49b0-9727-141df3b88038:117 1) 接着会执行 pexpire lockzzyy 30000|
  • 流程解释:
    • 通过 exist判断,如果锁不存在,则设置值和过期时间,加锁成功
    • 通过hexist判断,如果锁已经存在,并且锁的是当前线程,则证明是重入锁,加锁成功
    • 如果锁已经存在,但锁的不是当前线程,则证明有其他线程持有锁,返回当前锁的过期时间,加锁失败
  • 加锁
  • 可冲入锁
  • ttl 续期:加大业务逻辑处理时间,看redisson续命
  1. 解锁 RedissonLock.unlockInnerAsync()如下: image.png

存在的bug

删除锁直接调用unlock()方法会报错,如下:

image.png 正确写法如下:

if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()){
     redissonLock.unlock();
}