【分布式锁的演化】手撕redis分布式锁,隔壁张小帅都看懂了!

前言

上一篇老猫和小伙伴们分享了为什么要使用分布式锁以及分布式锁的实现思路原理,目前我们主要采用第三方的组件作为分布式锁的工具。上一篇运用了Mysql中的select ...for update实现了分布式锁,但是我们说这种实现方式并不常用,因为当大并发量的时候,会给数据库带来比较大的压力。当然也有小伙伴给老猫留言说“ 在quartz的集群模式中,就是使用了基于mysql的分布式锁,select for update ”。没错,其实quartz的集群模式中,任务执行的节点个数是可预知的,而且没有那么大的量级,所以是没有问题的。但是如果像千万级别的并发秒杀场景的情况下,那么这种方案其实是不可行的。因为mysql操作是需要IO的,IO的速度比内存速度慢,因此mysql如果在那种场景下使用的话是会存在系统瓶颈的。所以本篇就和小伙伴们分享基于内存操作的比较常用的分布式锁——redis分布式锁。

手撸Redis分布式锁

实现原理

redis分布式锁实现原理其实也是比较简单的,主要是依赖于redis的 set nx命令,我们来看一下完整的设置redis的命令:“Set resource_name my_random_value NX PX 30000”。看到这串命令,了解redis的小伙伴应该都看得懂这条命令是在redis中存入一个带有过期时间的值。具体上述设值语句解释如下:

  1. resource_name:资源名称,可以根据不同的业务区分不同的锁。(其实就是对应我们上一篇myql锁中的business_code)。
  2. my_random_value:随机值,每个线程的随机值都不相同,主要用于释放锁的时候用来校验。
  3. NX:key不存在的时候设置成功,key存在则设置不成功。
  4. PX:自动失效时间,如果出现异常情况,锁可以过期实现,因此达到了自动释放。

那么为什么可以使用这个思路呢?其实很简单,主要就是利用了set nx的原子性,在多个线程并发执行时,只有一个线程可以设置成功,如果设置成功,那么就代表着获得了锁,就可以执行后续的业务。如果出现了异常,过了锁的有效期,锁会自动释放,释放锁主要采用了redis的delete命令,释放锁之前会校验当前redis存储的随机数,只有当前的随机数和存储的随机数一致的时候才允许释放。具体的redis的删除,我们可以通过lua脚本进行删除,具体Lua脚本如下:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

那么我们为什么要采用这种方式释放锁呢?其实使用这种方式释放锁可以避免删除别的客户端获取成功的锁 。

如下图:

redis释放锁

客户端A取得资源锁,但是紧接着被一个其他操作阻塞了,当客户端A运行完毕其他操作后要释放锁时,原来的锁早已超时并且被Redis自动释放,并且在这期间资源锁又被客户端B再次获取到。如果仅使用DEL命令将key删除,那么这种情况就会把客户端B的锁给删除掉。使用Lua脚本就不会存在这种情况,因为脚本仅会删除value等于客户端A的value的key(value相当于客户端的一个签名)(说明:其实这些例子在redis的官网都有介绍)。

代码实现方式

老猫对redis锁机制进行了相关的抽取,并且封装成了工具类,核心工具类代码如下:

/**
 * @author kdaddy@163.com
 * @date 2021/1/7 22:36
 * 公众号“程序员老猫”
 */
@Service
public class RedisLockUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    private String value = UUID.randomUUID().toString();

    public Boolean lock(String key){
        RedisCallback<Boolean> redisCallback = redisConnection -> {
            //表示set nx 存在key的话就不设置,不存在则设置
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            //设置过期时间
            Expiration expiration = Expiration.seconds(30);
            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            byte[] redisValue = redisTemplate.getKeySerializer().serialize(value);
            Boolean result = redisConnection.set(redisKey,redisValue,expiration,setOption);
            return result;
        };
        //获取分布式锁
        Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
        return lock;
    }
    //释放分布式锁
    public Boolean releaseLock(String key){
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
        List<String> keys = Arrays.asList(key);

        boolean result = (Boolean) redisTemplate.execute(redisScript,keys,value);
        return result;
    }
}

当然相关的业务代码,老猫还是使用了之前并发扣减库存的例子,在此相关的代码以及最终运行的结果也不一一进行举例。小伙伴们可以自行去老猫的github获取相关的示例源码信息,然后运行一下即可。github地址:https://github.com/maoba/kd-distribute。代码已经完成了更新。

Redisson分布式锁

介绍和使用

那么Redisson究竟为何物呢?Redisson 是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。 充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。 (摘自redisson官网:https://redisson.org/)

下面我们来看一下具体用redisson实现分布式锁实战,其实是相当简单的,redisson已经给我们进行了相关的封装,我们开箱即用。

/**
 * @author kdaddy@163.com
 * @date 2021/1/9 14:23
 * @公众号“程序员老猫”
 */
public  Integer createOrder() throws Exception{
    log.info("进入了方法");
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("ktdaddy");
    RedissonClient redissonClient = Redisson.create(config);
    RLock rlock = redissonClient.getLock(ORDER_KEY);
    rlock.lock(30, TimeUnit.SECONDS);

    try {
        log.info("拿到了锁");
        //....具体可以参考老猫的github
        return order.getId();
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        rlock.unlock();
    }
    return null;
}

原理

redisson简单架构

老猫上文中自己实现redis锁的时候用到了lua脚本,redisson实现的时候其实所有的指令都是通过lua脚本去实现的。上述为redisson的简单架构图,画的比较粗糙。老猫稍微作一下解释。上图中有个看门狗(watchdog)概念。其实这就是一个定时任务,在线程获取锁之后,它会每隔10s帮忙将key的超时时间设置为30s,这样就不会出现线程一直持有锁从而影响其他线程获取锁的问题。小伙伴们可以发现该功能其实就是set px,只是换成了定时任务去实现。当然看门狗的存在保证了出现死锁的情况下会自动释放。

以上只是针对redisson做了一个简单的应用介绍,redisson其实是相当强大的,首先说配置,老猫上述连接redis的方式其实很简单,由于搭建的是单机redis,所以就使用了单机redis的连接方式,当然redisson还支持主从、哨兵、集群等等连接方式;当然锁的种类也相当丰富,以上老猫提供的是可重入锁的流程。其实还包括公平锁、联锁、红锁、读写锁等等,另外的redisson对分布式的容器、队列等等进行了特有的封装,包括分布式的Blocking Queue、分布式Map、分布式Set、分布式List等等。redisson的强大之处老猫在此不一一枚举,有兴趣的小伙伴可以深入研究一下。

缺陷

redis锁可以比较完美地解决高并发的时候分布式系统的线程安全性的问题,但是这种锁机制也并不是完美的。在哨兵模式下,客户端对master节点加了锁,此时会异步复制给slave节点,此时如果master发生宕机,主备切换,slave变成了master。因为之前是异步复制,所以此时正好又有个线程来尝试加锁的时候,就会导致多个客户端对同一个分布式锁完成了加锁操作,这时候业务上会出现脏数据了。关于redis的相关知识,大家可以访问老猫之前的一些文章,包括redis的哨兵模式、持久化等等。

写在最后

本篇主要和小伙伴们分享了redis锁,从老猫自己实现的乞丐版的redis锁到大牛实现的redisson。相信大家也会有一定的收货。其实关于分布式锁,出了redis锁之外还有基于zookeeper的实现。后续老猫会整理并且分享给大家,敬请期待。

当然更多技术干货也欢迎大家搜索关注公众号“程序员老猫”

posted @ 2021-01-09 21:43  程序员老猫  阅读(610)  评论(0编辑  收藏  举报