第十八节 SpringBoot使用Redis实现分布式锁

一、共享资源问题

        我们知道,在如果希望多个线程获取同一个共享资源,在Java里面一般使用Synchronized锁ReentrantLock锁来解决。对于单节点的项目,所有的线程都在同一个JVM进程里面,使用Java语言提供的锁机制可以起到对共享资源进行同步的作用。

        那么,我现在遇到的问题是:SpringBoot项目通常都是分布式部署,一般会部署好几个节点。这几个节点之间通常都会出现争夺共享资源的情况。因此,必须使用分布式锁来解决多个节点之间资源共享的问题。比如,这几天,我就遇到一个非常现实的问题。

        这个问题是:我现在的应用里有一个定时任务,而应用又部署在了公司的三台服务器上。这就会出现什么情况呢?就是在同一时刻,这个定时任务执行了三次。每个服务器里面的应用都是独立的JVM进程,它们之间没有联系,因此每个节点都理所当然的在固定时刻指定了定时任务。如何解决的呢?这里必须借助中间件----Redis缓存中间件。

二、Redis实现分布式锁

        当前普遍的实现思路为:SET my_key my_value NX PX milliseconds。即为每个要执行的任务,添加一个键值对构成的锁。NX表示只有当键key不存在的时候才会设置key的值,PX表示设置键key的过期时间,单位是毫秒。

转为代码思路或许清晰一点:

    (1)执行某任务之前,在Redis服务器里面检查是否已经有此任务对应的taskId。

                  如果没有,将taskId添加进Redis服务器。

                  如果有,说明当前任务已有其它服务器在执行,因此放弃执行。

    (2)任务执行完毕或出现异常,释放同步锁,即清空Redis缓存中此taskId。

         好,有了大牛们的思路,我们开始撸码,否则一切都是空谈。

         先写一个最简单的定时任务。我们假设上面的定时任务分配了一个taskId,规定每30秒执行一次。

import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@EnableScheduling
@Component
public class Task1111 {

    @Scheduled(cron = "*/5 * * * * ?")
    public void schedule() {

        System.out.println("定时任务执行");
    }
}

       好,根据之前的经验,在执行这个定时任务之前,先判断taskId在不在Redis缓存里面。

       如果Redis中存在taskId,那么说明有其它的节点正在处理这个定时任务。

             --所以当前节点放弃执行定时任务。

       如果Redis中不存在taskId,说明没有任何节点正在执行这个定时任务。

             --那么当前节点就执行这个任务。执行任务后释放同步锁即可。

      下面代码中的RedisService相关的代码未给出,但是见方法名可知意思。具体代码可以下载源码查看。

package com.zhoutianyu.learnspringboot.task;

import com.zhoutianyu.learnspringboot.redis.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@EnableScheduling
@Component
public class Task2222 {
    private static final Logger LOGGER = LoggerFactory.getLogger(Task2222.class);

    @Autowired
    private RedisService redisService;

    private static final String TASK_ID = "1";
    private static final String VALUE = "value";
    private static final Long TIME_OUT = 60L;

    //each 30s execute
    @Scheduled(cron = "*/30 * * * * ?")
    public void schedule() {
        final byte[] key = TASK_ID.getBytes();
        try {
            Boolean isExist = redisService.exists(key);
            if (isExist) {
                LOGGER.error("已经存在任务,放弃执行");
                return;
            }
            //add lock , time:60s
            redisService.setEx(key, VALUE, TIME_OUT);

            System.out.println("定时任务执行");

            //release lock
            redisService.del(key);
        } catch (Exception e) {
            redisService.del(key);
        }
    }
}

         好了,上面的程序基本上就是按照这个思路来的。执行定时任务之前判断key是否存在就可以了。

        但是,还有两个问题。第一个问题,在实际过程中,真实的业务可能会处理很久,如果我们强行规定缓存存活60秒。那么60s后,我们的锁过期了。

        假设我们当前是A节点,A节点处理任务超过了60s,但是这个锁已经过期了。

        B节点在Redis中没有发现这个锁,则判断没有任何其它节点拿到了锁,那么就顺理成章拿到锁并开始执行任务。

        A节点在业务执行完毕后,执行释放锁的代码,实际上可能释放了B的同步锁。

        卧槽,这不行啊!第二个问题,如果Redis服务器炸了怎么办。

        解决第一个问题的办法:增加随机值。在释放锁的时候,判断Redis中的随机数是不是当前节点生成的。

             ---如果不是当前节点生成的,那么就不要删除了,小心删除了别的节点的同步锁。

        因此,代码可以优化为:

package com.zhoutianyu.learnspringboot.task;

import com.zhoutianyu.learnspringboot.redis.RedisService;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@EnableScheduling
@Component
public class Task3333 {
    private static final Logger LOGGER = LoggerFactory.getLogger(Task3333.class);

    @Autowired
    private RedisService redisService;

    private static final String TASK_ID = "1";
    private static final Long TIME_OUT = 60L;

    //each 30s execute
    @Scheduled(cron = "*/30 * * * * ?")
    public void schedule() {
        final byte[] key = TASK_ID.getBytes();
        try {
            Boolean isExist = redisService.exists(key);
            if (isExist) {
                LOGGER.error("已经存在任务,放弃执行");
                return;
            }
            //add lock , time:60s
            String value = randomString();
            redisService.setEx(key, value, TIME_OUT);

            System.out.println("定时任务执行");
            //模拟40s的业务时间
            Thread.sleep(40);


            //release lock
            if (value.equals(redisService.get(key))) {
                redisService.del(key);
            }

        } catch (Exception e) {
            redisService.del(key);
        }
    }

    private String randomString() {
        return RandomStringUtils.randomAlphanumeric(10);
    }
}

        至于第二个问题,Redis服务器宕机的问题,解决办法就是搭建Redis集群,保证Redis服务器的高可用。

        综上所述,今天提供的Redis实现的分布式锁,实际上也只能适用于使用单个Redis服务器的应用,距离真正的应用环境还有一定的差距。实际上,我现在公司里使用分布式锁,它的是基于Zookeepers集群的分布式锁,共3个Zookeepers服务器同时工作。但是,我们不能否定Redis缓存实现分布式锁的价值,如果搭建出Redis集群的话,也能够实现有相同功能的高效的分布式锁。当然,这么高深的技术,博主现在肯定还不会啦(づ╥﹏╥)づ

三、源码下载

        本章节项目源码:点我下载源代码

        目录贴:跟着大宇学SpringBoot-------目录帖

如果本系列文章对你有帮助,不妨请我喝瓶可乐吧!

你的打赏是对我最好的支持!

                    

posted @ 2022-07-17 12:14  小大宇  阅读(1400)  评论(0编辑  收藏  举报