• Redis分布式锁(下篇)


    我们在不久前介绍了SpringBoot定时任务,最近又一起探究了如何使用Redis实现简单的消息队列,都是一些不错的小知识点。为了能跟前面的内容产生联动,这次我们打算把Redis分布式锁相关的介绍融合进定时任务的案例中,学起来更带劲~

    Redis的锁长啥样?

    上一篇我们粗略介绍了JVM锁,比如synchronized关键字和ReentrantLock,它们都是实实在在已经实现的锁,而且还有标志位啥的。但Redis就是一个内存...怎么作为锁呢?

    有一点大家要明确,Redis之所以能用来做分布式锁,肯定不只是因为它是一片内存,否则JVM本身也占有内存,为什么无法自己实现分布式锁呢?

    我个人的理解是,要想自定义一个分布式锁,必须至少满足几个条件:

    独立于多节点系统之外的一片内存
    唯一性(可以通过单线程,也可以通过选举机制,能保证唯一即可)
    当然,如果性能高一点,甚至支持高可用就更好啦
    以上三点Redis都能满足。在上面三个条件下,其实怎么设计锁,完全取决于个人如何定义锁。就好比现实生活中,通常我们理解的锁就是有个钥匙孔、需要插入钥匙的金属小物件。然而锁的形态可不止这么一种,随着科技的发展,什么指纹锁、虹膜锁层出不穷,但归根结底它们之所以被称为“锁”,是因为都保证了“唯一”。

    如果我们能设计一种逻辑,它能造成某个场景下的“唯一事件”,那么它就可以被称为“锁”。比如,某家很有名的网红店,一天只接待一位客人。门口没有营业员,就放了一台取号机,里面放了一张票。你如果去迟了,票就没了,你就进不了这家店。这个场景下,没票的顾客进不去,被锁在门外。此时,取票机造成了“唯一事件”,那么它就可以叫做“锁”。

    而Redis提供了setnx指令,如果某个key当前不存在则设置成功并返回true,否则不再重复设置,直接返回false。这不就是编程界的取号机吗?当然,实际用到的命令可不止这一个,具体如何实现,大家等下看代码即可。

    Demo构思

    在我看来,同样需要使用锁,动机可能完全相反:

    在保证线程安全的前提下,尽量让所有线程都执行成功
    在保证线程安全的前提下,只让一个线程执行成功
    前者适用于秒杀等场景。作为商家,当然希望在不发生线程安全问题的前提下,让每一个订单都生效,直到商品售罄。此时分布式锁的写法可以是“不断重试”或“阻塞等待”,即:递归或while true循环尝试获取、阻塞等待。

    97626143a5634f31be3798b632d8ab89.png
    而后者适用于分布式系统或多节点项目的定时任务,比如同一份代码部署在A、B两台服务器上,而数据库共用同一个。如果不做限制,那么在同一时刻,两台服务器都会去拉取列表执行,会发生任务重复执行的情况。 

    e9b0043d29234aca8c340a8ff01ef8f5.png
    此时可以考虑使用分布式锁,在cron触发的时刻只允许一个线程去往数据库拉取任务: 

    9164d6e331dd40339fb1d59c5f300a97.png
    在实现Redis分布式锁控制定时任务唯一性的同时,我们引入之前的Redis消息队列。注意,这与Redis分布式锁本身无关,就是顺便复习一遍Redis消息队列而已,大家可以只实现Redis分布式锁+定时任务的部分。 

    整个Demo的结构大致如图:

    3895a726d7e4457fbf035ee34823d0cb.png
    当然,实际项目中一般是这样的: 

    8cd2164498404d778069057e7cb3c673.png
    分布式锁为什么难设计? 

    首先,要和大家说一下,但凡牵涉到分布式的处理,没有一个是简单的,上面的Demo设计也不过是玩具,用来启发 大家的思路。

    为什么要把demo设计得这么复杂呢?哈哈,因为这是我在上一家公司自己设计的,遇到了很多坑...拿出来自嘲一番,与各位共勉。

    我当时的设计思路是:

    由于小公司没有用什么Elastic-Job啥的,就是很普通的多节点部署。为了避免任务重复执行,我想设计一个分布式锁。但因为当时根本不知道redisson,所以就自己百度了redis实现分布式锁的方式,然后依葫芦画瓢自己手写了一个 。

    但我写完redis分布式锁后,在实际测试过程中发现还需要考虑锁的失效时间...

    这里有两个问题:

    为什么要设置锁的过期时间?
    锁的过期时间设置多久合适?
    最简单的实现方案是这样的,一般没问题:

    e9a7668525564a0b9938e8b23540e273.png
    但极端的情况下(项目在任务进行时重启或意外宕机),可能当前任务来不及解锁就挂了(死锁),那么下一个任务就会一直被锁在方法外等待。就好比厕所里有人被熏晕了,没法开门,而外面的人又进不去... 

    27663cad568b48369b6422f6bd496b8e.png
    此时需要装一个自动解锁的门,到时间自动开门,也就是要给锁设置一个过期时间。但紧接着又会有第二个问题:锁的失效时间设多长合适? 

    很难定。

    因为随着项目的发展,定时任务的执行时间很可能是变化的。

    如果设置时间过长,极端点,定为365天。假设任务正常执行,比如10分钟就结束,此时执行完毕的任务自己会主动解锁。但万一和上面一样宕机了,虽说你设置了过期时间,但下一个任务需要等一年才能执行...本质上和没有设置过期时间一样!就好比...你自己想想什么例子合适,能加深你的理解哦。

    如果设置时间过短,上一个人还没拉完,门就“咔嚓”一声开了,尴尬不,重复执行了。

    终上所述,我当时之所以设计得这么复杂,就是想尽量缩短任务执行的时间,让它尽可能短(拉取后直接丢给队列,自己不处理),这样锁的时间一般设置30分钟就没啥问题。另外,对于死锁问题,我当时没有考虑宕机的情况,只考虑了意外重启…问题还有很多,文末会再总结。

    请大家阅读下面代码时思考两个问题:

    Demo如何处理锁的过期时间
    Demo如何防止死锁
    项目搭建

    新建一个空的SpringBoot项目。

    拷贝下方代码,构建工程:

    bc159f0c89f34e43b1188076c31c56fc.png
    构建完以后,拷贝一份,修改端口号为8081,避免和原先的冲突 

    021d03fec0d44e919464432de0a51e62.png
    统一管理Redis Key:RedisKeyConst 

    /**
     * 统一管理Redis Key
     *
     * @author qiyu
     */
    public abstract class RedisKeyConst {
        /**
         * 分布式锁的KEY
         */
        public static final String RESUME_PULL_TASK_LOCK = "resume_pull_task_lock";
        /**
         * 简历异步解析任务队列
         */
        public static final String RESUME_PARSE_TASK_QUEUE = "resume_parse_task_queue";
    }
    Redis消息队列:RedisMessageQueueConsumer

    /**
     * 消费者,异步获取简历解析结果并存入数据库
     *
     * @author qiyu
     */
    @Slf4j
    @

  • 相关阅读:
    身边的那些信审人员都去哪了?
    Docker pull Images遇到的问题解决
    JVM 相关知识学习
    Delphi 开发过程中简单的版本管理与回退
    Unity3D 常用得内置函数(Cg与GLSL)详解
    windows上mysql安装
    vue3.0与vue2.0的区别
    【Verilog基础】【计算机体系结构】cache的分配策略和更新策略
    Kibana配置ES集群
    关系型数据库语言基础整理
  • 原文地址:https://blog.csdn.net/m0_72088858/article/details/126687365