springboot整合redisson实现分布式锁

前言

上一篇文章中,着重介绍了线程池的搭建和jdk8发起任务的API CompletableFuture。说这些事情的意义是什么,强调的是在面对大量请求的时候,为了更好地支持并发、管控资源,咱们使用手动创建线程池的方法把许多的任务牢牢地拿在手中。 本篇文章介绍另外一个知识点,分布式锁,锁这个概念大家都不陌生,java提供了volatile锁、synchronized锁、Lock锁等等为我们解决线程的安全问题,但是除了这些之外,还存在一个非常重要的概念,分布式锁。下面将从为什么需要分布式锁、如何利用springboot框架、redisson框架结合redis实现分布式锁。展开

一、为什么需要分布式锁

在开发应用时,当多个客户或者多个线程需要对某个共享的数据进行操作时,就需要使用线程同步。在Java开发中,对于单机应用,因为是在同一个JVM内部,所以我们可以采用Java提供的各种多线程操作的技巧来实现线程同步。 而对于分布式系统来说,由于多个请求可能被分发到不同的机器上去处理,如果这多个请求都是对同一个资源进行操作,那么使用基本的Java多线程线程同步技术可能就解决不了这个问题。 大家能理解上面两段话吗,假设我现在有一个共享的变量存在于数据库,我将它抽象为A。如果我只是单机模式访问这个A,那么在操作这个大A的地方加一个synchronized锁是一点问题都没有的,线程都需要排队去操作A。 但问题在于,如果不是单机模式这就显然可能出现问题的,比方两台服务都可以去操作A,及时你给每个操作A的服务都在操作A的地方增加了分布式锁,那么依旧可能出现多个线程同时操作A的情形,毕竟两个服务之间又不感知!

springboot整合redisson实现分布式锁_第1张图片
如上图,请求A、B、C都是发起扣减同一个商品的库存操作,三个请求被分发到三台不同的服务部署机器上进行处理。而三台机器并不在同一个JVM,所以Java提供的线程同步技巧就发挥不了作用了。但是对于扣减库存这样的场景,必须要使用线程同步来保证同一个商品的库存不会被漏扣或者多扣。

为了保证在高并发的场景下,临界资源(共享资源)同时只能被一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。在单机环境中,Java中提供了很多并发处理相关的API。

但是在分布式系统中,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

二、springboot整合redisson实现分布式锁

1.引入依赖、编写启动类、编写配置文件

首先来引入下依赖,其实不需要这么多,我太懒了,就不改了

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.4.RELEASEversion>
    parent>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-aopartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
            <version>3.9.1version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintagegroupId>
                    <artifactId>junit-vintage-engineartifactId>
                exclusion>
            exclusions>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.10version>
        dependency>
        <dependency>
            <groupId>javax.persistencegroupId>
            <artifactId>persistence-apiartifactId>
            <version>1.0version>
        dependency>
        <dependency>
            <groupId>com.google.guavagroupId>
            <artifactId>guavaartifactId>
            <version>28.1-jreversion>
        dependency>
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>net.minidevgroupId>
            <artifactId>json-smartartifactId>
            <version>2.3version>
            <scope>compilescope>
        dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

编写启动类

package com.cmdc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RedissonApplication {
     
    /**
     * 启动类方法
     * @param args 参数
     */
    public static void main(String[] args) {
     
        SpringApplication.run(RedissonApplication.class,args);
    }
}

编写springboot默认会读取的配置文件application.yml(其实你不写也会走默认)

server:
  port: 8111
spring:
  application:
    name: springboot-redisson

编写redisson需要的配置文件redisson-config.yml

#Redisson配置
singleServerConfig:
  address: "redis://127.0.0.1:6379"
  password: null
  clientName: null
  database: 7 #选择使用哪个数据库0~15
  idleConnectionTimeout: 10000
  pingTimeout: 1000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  reconnectionTimeout: 3000
  failedAttempts: 3
  subscriptionsPerConnection: 5
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  connectionMinimumIdleSize: 32
  connectionPoolSize: 64
  dnsMonitoringInterval: 5000
  #dnsMonitoring: false

threads: 0
nettyThreads: 0
codec:
  class: "org.redisson.codec.JsonJacksonCodec"
transportMode: "NIO"

ok.现在项目长下面这个样子
springboot整合redisson实现分布式锁_第2张图片

2.整合

1.编写类RedissonConfig,将redisson的核心类RedissonClient交给spring管理。

package com.cmdc.config;

import com.cmdc.lockimpl.RedissonDistributeLocker;

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;

import java.io.IOException;
 
/**
 * redisson bean管理
 */
@Configuration
public class RedissonConfig {
     
    
    /**
     * Redisson客户端注册
     * 单机模式
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient createRedissonClient() throws IOException {
     
 
//       Config config = new Config();
//        SingleServerConfig singleServerConfig = config.useSingleServer();
//        singleServerConfig.setAddress("redis://127.0.0.1:6379");
//        singleServerConfig.setPassword("12345");
//        singleServerConfig.setTimeout(3000);
//        return Redisson.create(config)
 
        // 本例子使用的是yaml格式的配置文件,读取使用Config.fromYAML,如果是Json文件,则使用Config.fromJSON
        Config config = Config.fromYAML(RedissonConfig.class.getClassLoader().getResource("redisson-config.yml"));
        return Redisson.create(config);
    }
 
 
    /**
     * 主从模式 哨兵模式
     *
     **/
   /* @Bean
    public RedissonClient getRedisson() {
        RedissonClient redisson;
        Config config = new Config();
        config.useMasterSlaveServers()
                //可以用"rediss://"来启用SSL连接
                .setMasterAddress("redis://***(主服务器IP):6379").setPassword("web2017")
                .addSlaveAddress("redis://***(从服务器IP):6379")
                .setReconnectionTimeout(10000)
                .setRetryInterval(5000)
                .setTimeout(10000)
                .setConnectTimeout(10000);//(连接超时,单位:毫秒 默认值:3000);
        //  哨兵模式config.useSentinelServers().setMasterName("mymaster").setPassword("web2017").addSentinelAddress("***(哨兵IP):26379", "***(哨兵IP):26379", "***(哨兵IP):26380");
        redisson = Redisson.create(config);
        return redisson;
    }*/
}

编写类DistributeLocker,这是一个接口。用来操作redisson提供给我们的核心类RedissonClient。

package com.cmdc.abstractlock;

import java.util.concurrent.TimeUnit;

/**
 *
 */
public interface  DistributeLocker {
     
 
    /**
     * 加锁
     * @param lockKey key
     */
    void lock(String lockKey);

    /**
     * 加锁锁,设置有效期
     *
     * @param lockKey key
     * @param timeout 有效时间,默认时间单位在实现类传入
     */
    void lock(String lockKey, int timeout);

    /**
     * 加锁,设置有效期并指定时间单位
     * @param lockKey key
     * @param timeout 有效时间
     * @param unit    时间单位
     */
    void lock(String lockKey, int timeout, TimeUnit unit);

    /**
     * 释放锁
     *
     * @param lockKey key
     */
    void unlock(String lockKey);

    /**
     * 尝试获取锁,获取到则持有该锁返回true,未获取到立即返回false
     * @param lockKey 锁
     * @return true-获取锁成功 false-获取锁失败
     */
    boolean tryLock(String lockKey);
 
    /**
     * 尝试获取锁,获取到则持有该锁leaseTime时间.
     * 若未获取到,在waitTime时间内一直尝试获取,超过waitTime还未获取到则返回false
     * @param lockKey   key
     * @param waitTime  尝试获取时间
     * @param leaseTime 锁持有时间
     * @param unit      时间单位
     * @return true-获取锁成功 false-获取锁失败
     * @throws InterruptedException e
     */
    boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit)
            throws InterruptedException;
 
    /**
     * 锁是否被任意一个线程锁持有
     * @param lockKey 锁
     * @return true-被锁 false-未被锁
     */
    boolean isLocked(String lockKey);

    /**
     * isHeldByCurrentThread()的作用是查询当前线程是否保持此锁定
     * @param lockKey 锁
     * @return true or false
     */
    boolean isHeldByCurrentThread(String lockKey);

}

下面将其实现,编写DistributeLocker的实现类RedissonDistributeLocker,将上述方法全部实现,方法的作用接口类中已经描述的比较清楚了。咱们这把不研究API,感兴趣的童鞋自己看看鸭

package com.cmdc.lockimpl;

import com.cmdc.abstractlock.DistributeLocker;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

import java.util.concurrent.TimeUnit;
 
/**
 * redisson实现分布式锁接口
 */
public class RedissonDistributeLocker implements DistributeLocker {
     
 
    private final RedissonClient redissonClient;

    /**
     * 构造方法 赋予本类的redisClient以实例
     * @param redissonClient client
     */
    public  RedissonDistributeLocker(RedissonClient redissonClient) {
     
        this.redissonClient = redissonClient;
    }
 
    @Override
    public void lock(String lockKey) {
     
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
    }

    @Override
    public void lock(String lockKey, int leaseTime) {
     
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(leaseTime, TimeUnit.MILLISECONDS);
    }
 
    @Override
    public void lock(String lockKey, int timeout, TimeUnit unit) {
     
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
    }

    @Override
    public void unlock(String lockKey) {
     
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }

    @Override
    public boolean tryLock(String lockKey) {
     
        RLock lock = redissonClient.getLock(lockKey);
        return lock.tryLock();
    }
 
    @Override
    public boolean tryLock(String lockKey, long waitTime, long leaseTime,
                           TimeUnit unit) throws InterruptedException {
     
        RLock lock = redissonClient.getLock(lockKey);
        return lock.tryLock(waitTime, leaseTime, unit);
    }
 
    @Override
    public boolean isLocked(String lockKey) {
     
        RLock lock = redissonClient.getLock(lockKey);
        return lock.isLocked();
    }
 
    @Override
    public boolean isHeldByCurrentThread(String lockKey) {
     
        RLock lock = redissonClient.getLock(lockKey);
        return lock.isHeldByCurrentThread();
    }
 
}

ok,将RedissonDistributeLocker交给spring进行管理。别问,问就是习惯了~
这个需要在之前的配置类RedissonConfig中增加一个注入的方法。

    @Bean
    public RedissonDistributeLocker redissonLocker(RedissonClient redissonClient) {
     
        // redissonClient 是本来就由redisson提供给我们,我们创建RedissonDistributeLocker实例交给spring进行管理
        RedissonDistributeLocker locker = new RedissonDistributeLocker(redissonClient);
        return locker;
    }

继续,增加一个注解RedissonLockAnnotation用于标记需要用上分布式锁的方法以及提供表示锁的字符串

package com.cmdc.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
/**
 * 分布式锁自定义注解
 * 注解在方法
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLockAnnotation {
     

    /**
     * 指定组成分布式锁的key
     * @return 分布式锁的key
     */
    String lockRedisKey();
}

将这个注解AOP增强,赋予它生命

package com.cmdc.annotationimpl;

import com.cmdc.abstractlock.DistributeLocker;
import com.cmdc.annotation.RedissonLockAnnotation;

import lombok.extern.slf4j.Slf4j;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 分布式锁的 aop
 *
 * 无论是否抛出异常,也无论从什么地方return返回,finally语句块总是会执行,这样你有机会调用Close来关闭数据库连接(即使未打开或打开失败,关闭操作永远是可以执行的),以便于释放已经产生的连接,释放资源。
 *
 *  顺便说明,return是可以放在try语句块中的。但不管在什么时机返回,在返回前,finally将会执行。
 *  小结:
 *  try { //执行的代码,其中可能有异常。一旦发现异常,则立即跳到catch执行。否则不会执行catch里面的内容 }
 *  catch { //除非try里面执行代码发生了异常,否则这里的代码不会执行 }
 *  finally { //不管什么情况都会执行,包括try catch 里面用了return ,可以理解为只要执行了try或者catch,就一定会执行 finally }
 *
 *  Case2:
 *  至少有两种情况下finally语句是不会被执行的:
 *  (1)try语句没有被执行到,如在try语句之前return就返回了,这样finally语句就不会执行。这也说明了finally语句被执行的必要而非充分条件是:相应的try语句一定被执行到。
 *  (2)在try块|catch块中有System.exit(0);这样的语句。System.exit(0)是终止Java虚拟机JVM的,连JVM都停止了,所有都结束了,当然finally语句也不会被执行到。
 *
 *  在try-catch-finally中, 当return遇到finally,return对finally无效,即:
 *
 *       1.在try catch块里return的时候,finally也会被执行。
 *
 *       2.finally里的return语句会把try catch块里的return语句效果给覆盖掉。
 *
 *  结论:return语句并不一定都是函数的出口,执行return时,只是把return后面的值复制了一份到返回值变量里去了。
 */
@Aspect
@Component
@Slf4j
public class RedissonLockAop {
     
    public static final int WAIT_GET_LOCK_TIME = 3000;

    public static final int WAIT_RELEASE_LOCK_TIME = 5000;

    @Autowired
    private DistributeLocker locker;

    /**
     * 切点,拦截被 @RedissonLockAnnotation 修饰的方法
     * 说白了就是你这把面向切面从哪里切
     */
    @Pointcut("@annotation(com.cmdc.annotation.RedissonLockAnnotation)")
    public void redissonLockPoint() {
     
    }

    /**
     *
     * @param pjp 代表当前正在运行的方法
     * @return string
     * @throws InterruptedException e
     */
    @Around("redissonLockPoint()")
    @ResponseBody
    public String checkLock(ProceedingJoinPoint pjp) throws InterruptedException {
     
        // 当前线程名
        String threadName = Thread.currentThread().getName();
        log.info("线程{}------进入分布式锁aop------", threadName);
        // 获取该注解的实例对象
        RedissonLockAnnotation annotation = ((MethodSignature) pjp.getSignature()).
                getMethod().getAnnotation(RedissonLockAnnotation.class);
        // 生成分布式锁key的键名,以逗号分隔
        String lockRedisKey = annotation.lockRedisKey();
        log.info("存在于注解中的key值是:{}",lockRedisKey);

        // 获取存在于请求头中的唯一id值
        String lockRedisValue = ((ServletRequestAttributes) Objects.requireNonNull(
            RequestContextHolder.getRequestAttributes()))
            .getRequest()
            .getHeader(lockRedisKey);

        if (StringUtils.isEmpty(lockRedisKey)) {
     
            log.info("线程{} lockRedisKey设置为空,不加锁", threadName);
            try {
     
                pjp.proceed();
            } catch (Throwable throwable) {
     
                throwable.printStackTrace();
                log.info("process method failed...now print message:{}",throwable.getMessage());
            }
            return "NULL LOCK";
        } else {
     
            log.info("线程{} 锁的value值是:{}", threadName, lockRedisValue);
            // 获取锁  3000 等到获取锁的时间  leaseTime 获取锁后持有时间   时间单位 MILLISECONDS:毫秒
            if (locker.tryLock(lockRedisValue, WAIT_GET_LOCK_TIME, WAIT_RELEASE_LOCK_TIME, TimeUnit.MILLISECONDS)) {
     
                // 下面的逻辑我想说一下,大家应该非常清楚try catch finally的逻辑 关于这一块的逻辑已经卸载顶层注释上了
                try {
     
                    log.info("线程{} 获取锁成功", threadName);
                    return (String) pjp.proceed();
                } catch (Throwable throwable) {
     
                    throwable.printStackTrace();
                    log.info("process method failed...now print message:{}",throwable.getMessage());
                } finally {
     
                    if (locker.isLocked(lockRedisValue)) {
     
                        log.info("key={}对应的锁被持有,线程{}",lockRedisValue, threadName);
                        if (locker.isHeldByCurrentThread(lockRedisValue)) {
     
                            log.info("当前线程 {} 保持锁定", threadName);
                            locker.unlock(lockRedisValue);
                            log.info("线程{} 释放锁", threadName);
                        }
                    }
                }
            } else {
     
                log.info("线程{} 获取锁失败", threadName);
                return " GET LOCK FAIL";
            }
        }
        return null;
    }
}

这边稍微说一下,增强的思想就是,需要分布式锁的接口有多个线程进来的时候,每个线程都在请求头中放置一个唯一的id,这个唯一的id就抽象地对应一个共享变量。利用redis set值是一个原子操作,让线程去set 这个唯一的id,完成这个set的线程才能执行业务处理,不能完成的则不能进行业务的处理。当持有锁的线程完成业务处理之后即释放锁,以此循环往复。具体的low一眼代码也就明白啦。

下面咱们弄一个接口出来执行测试~

package com.cmdc.controller;

import com.cmdc.annotation.RedissonLockAnnotation;

import lombok.extern.slf4j.Slf4j;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 *
 */
@RestController
@Slf4j
public class TestController {
     

    public static final int THREAD_SLEEP_TIME = 5000;
    /**
     * 测试接口
     * @return 返回值
     */

    @PostMapping(value = "testLock", consumes = "application/json")
    @RedissonLockAnnotation(lockRedisKey = "the-only-id")
    public String testLock() {
     
        /**
         * 请求总携带一个唯一的id 谁拿到谁执行,非常的好理解
         */
        try {
     
            Thread.sleep(THREAD_SLEEP_TIME);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        log.info("执行相关业务...");
        log.info("业务执行中.....");
        log.info("业务执行结束.....");

        return "success";
    }
 
 
}

项目的结构最终长下面这个样子
springboot整合redisson实现分布式锁_第3张图片

沉睡5秒钟模拟业务的处理,咱们同时发送两个请求,看看会发生什么,我有postman和Insomnia两个测试的工具,就分别发请求,如果你只有一个工具,就一个请求复制两份就可以了

先发postman,看测试结果
springboot整合redisson实现分布式锁_第4张图片
接着是Insomnia来看测试的结果
springboot整合redisson实现分布式锁_第5张图片
Insomnia后发的,未成功获取到锁,和我们的预期一致的!

总结

线程池是为了支持并发、管控资源。
分布式锁是为了限制并发、解决线程安全问题,这两个都非常深刻,我们慢慢体会。

其实不一定需要用redisson,纯redis实现分布式锁也一点问题没有!我这里只是不想自己造轮子了~

不用redis用zookeeper也行,都没问题,看你喜欢什么啦!

你可能感兴趣的