SpringBoot通过自定义注解整合Redisson实现分布式锁(单机+集群模式)

@ 作者: 一恍过去
@ 主页: https://blog.csdn.net/zhuocailing3390
@ 社区: Java技术栈交流
@ 主题: SpringBoot通过自定义注解整合Redisson实现分布式锁
⏱️ @ 创作时间: 2023年5月13日

目录

  • 概述
  • RedLock 机制
  • Watch Dog机制
  • 代码实现
    • 引入POM
    • Yaml配置(redis部分)
    • RedissonConfigProperties
    • RedissonConfig(配置redission)
    • RedisLock(自定义注解)
    • RedisLockAspect(定义切面)
    • 开启自动代理
    • 使用
    • 测试

概述

在使用分布式锁时,如果单节点的Redis发生故障,则整个业务的分布式锁都将无法使用。
如果是主从模式或者集群模式下的Redis发生故障,由于Master 节点是独立的数据不同步,在主从同步的期间,Master 节点发生故障,即使Slaver 节点被选举为 Master 节点,分布式锁的Key信息可能就会丢失,锁失效的情况。

RedLock 机制

对于Redis集群模式下,为了解决Master 节点发生故障带来的问题,通过Redisson封装的RedLock 可以进行解决该问题;

机制流程如下:
1、依次向 N 个 Redis 服务发出请求,用能够保证全局唯一的 value 申请锁 key;
2、如果从 N/2+1 个 redis 服务中都获取锁成功,那么,本次分布式锁的获取被视为成功,否则视为获取锁失败。
3、如果获取锁失败,或执行达到 超时时间,则向所有 Redis 服务都发出解锁请求。

Watch Dog机制

分布式事务锁最常见的一个问题就是如果已经获取到锁的 client 在 TTL 时间内没有完成竞争资源的处理,而此时锁会被自动释放,造成竞争条件的发生。

这种情况如果让 client 端设置定时任务自动延长锁的占用时间,会造成 client 端逻辑的复杂和冗余。

redisson 在实现的过程中,自然也考虑到了这一问题,redisson 提供了一个“看门狗”的可选特性,并且增加了 lockWatchdogTimeout 配置参数,看门狗线程会自动在 lockWatchdogTimeout 超时后顺延锁的占用时间,从而避免上述问题的发生。

但是,由于看门狗作为独立线程存在,对于性能有所影响,如果并非是处理高度竞争且处理时长不固定的特殊资源,那么并不建议启用 redisson 的看门狗特性。

代码实现

引入POM

<dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        
        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redisson-spring-boot-starterartifactId>
            <version>3.13.6version>
        dependency>
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-pool2artifactId>
        dependency>
        <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.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.8version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-configuration-processorartifactId>
        dependency>
    dependencies>

Yaml配置(redis部分)

spring:
  #redis配置
  redis:
    # Redis数据库索引(默认为0)
    database: 1
    # 连接地址
    host: 162.14.115.18
    #端口号
    port: 6379
    ##连接超时时间
    timeout: 3600
    #密码
    password: 123456
    #    集群模式
    #    cluster:
    #      nodes: 162.14.115.18:6379,162.14.115.18:6379
    lettuce:
      pool:
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池中的最小空闲连接
        min-idle: 1
      #关闭超时
      shutdown-timeout: 500ms
#    哨兵模式
#    sentinel:
#      master: master
#      nodes: 162.14.115.18:6379,162.14.115.18:6379

RedissonConfigProperties


@Component
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfigProperties {
    private String host;
    private String port;
    private String password;
    private Cluster cluster;
    private Integer timeout;
    private Sentinel sentinel;

    @Data
    public static class Sentinel {
        private String master;

        private List<String> nodes;

        public List<String> getNodes() {
            return nodes;
        }

        public void setNodes(List<String> nodes) {
            this.nodes = nodes;
        }
    }

    @Data
    public static class Cluster {
        private List<String> nodes;

        public List<String> getNodes() {
            return nodes;
        }

        public void setNodes(List<String> nodes) {
            this.nodes = nodes;
        }
    }
}

RedissonConfig(配置redission)

@Configuration
public class RedissonConfig {

    @Resource
    private RedissonConfigProperties redissonConfigProperties;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();

        String redisPort = redissonConfigProperties.getPort();
        Integer redisTimeout = redissonConfigProperties.getTimeout();
        String redisPassword = redissonConfigProperties.getPassword();
        RedissonConfigProperties.Cluster cluster = redissonConfigProperties.getCluster();
        RedissonConfigProperties.Sentinel sentinel = redissonConfigProperties.getSentinel();

        // 集群
        if (cluster != null && !CollectionUtils.isEmpty(cluster.getNodes())) {
            List<String> nodes = cluster.getNodes();
            List<String> collect = nodes.stream().map(x -> x = x.startsWith("redis://") ? x : "redis://" + x).collect(Collectors.toList());
            ClusterServersConfig serverConfig = config.useClusterServers()
                    .setScanInterval(2000)
                    .addNodeAddress(collect.toArray(new String[collect.size()]));
            if (StringUtils.isNotBlank(redisPassword)) {
                serverConfig.setPassword(redisPassword);
            }
        }
        //哨兵
        else if (sentinel != null && !CollectionUtils.isEmpty(sentinel.getNodes())) {
            List<String> nodes = sentinel.getNodes();
            List<String> collect = nodes.stream().map(x -> x = x.startsWith("redis://") ? x : "redis://" + x).collect(Collectors.toList());
            SentinelServersConfig serverConfig = config.useSentinelServers()
                    .setMasterName(sentinel.getMaster())
                    .addSentinelAddress(collect.toArray(new String[collect.size()]))
                    .setTimeout(redisTimeout);
            if (StringUtils.isNotBlank(redisPassword)) {
                serverConfig.setPassword(redisPassword);
            }
        } else {
            // 单机配置
            String redisHost = redissonConfigProperties.getHost();
            redisHost = redisHost.startsWith("redis://") ? redisHost : "redis://" + redisHost;
            SingleServerConfig serverConfig = config.useSingleServer()
                    .setAddress(redisHost + ":" + redisPort)
                    .setTimeout(redisTimeout);

            if (StringUtils.isNotBlank(redisPassword)) {
                serverConfig.setPassword(redisPassword);
            }
        }
        return Redisson.create(config);
    }
}

RedisLock(自定义注解)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
    /**
     * 上锁的方法中,key所在参数的位置,索引从0开始
     *
     * @return
     */
    int keyNum();

    /**
     * 上锁时长,默认设置时间
     *
     * @return
     */
    long lockTime() default -1;

    /**
     * 尝试时间,设置时间内通过自旋一致尝试获取锁,默认0ms 通常时间要小于lockTime时间
     *
     * @return
     */
    long tryTime() default 0;
}

RedisLockAspect(定义切面)

@Aspect
@Component
@Slf4j
public class RedisLockAspect {

    @Resource
    private RedissonClient redissonClient;

    private static final ThreadLocal<RLock> LOCK_THREAD = new ThreadLocal<>();

    @Pointcut("@annotation(lhz.lx.aspect.RedisLock)")
    public void lockPoint() {

    }

    /**
     * 环绕通知,调用目标方法
     */
    @Around("lockPoint()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 记录方法执行开始时间
        long startTime = System.currentTimeMillis();

        Object[] args = proceedingJoinPoint.getArgs();
        if (args.length <= 0) {
            throw new RuntimeException("keyName不存在!");
        }
        String[] argNames = ((CodeSignature) proceedingJoinPoint.getSignature()).getParameterNames();

        Signature signature = proceedingJoinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        RedisLock lock = method.getAnnotation(RedisLock.class);

        int keyNum = lock.keyNum();
        if (!"keyName".equals(argNames[keyNum])) {
            throw new RuntimeException("keyName不存在!");
        }
        String key = args[keyNum].toString();
        long lockTime = lock.lockTime();
        long tryTime = lock.tryTime();

        log.info("分布式锁上锁,key:{},lockTime:{}", key, lockTime);
        RLock clientLock = redissonClient.getLock(key);
        // 尝试加锁,最多等待 tryTime 毫秒,上锁以后 lockTime 毫秒自动解锁
        // 如果lockTime不设置,则会启动看门狗机制(默认30S),每10S会自动续锁
        boolean locked = clientLock.tryLock(tryTime, lockTime, TimeUnit.MILLISECONDS);
        if (!locked) {
            log.error("上锁失败");
            // 如果为了不影响业务,可以不抛出异常,继续向下执行
            throw new RuntimeException("上锁失败!");
        }
        clientLock.lock(lockTime, TimeUnit.MILLISECONDS);
        log.info("分布式锁上锁成功,key:{},lockTime:{}", key, lockTime);
        LOCK_THREAD.set(clientLock);
        // 调用目标方法
        return proceedingJoinPoint.proceed();
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(value = "lockPoint()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
        handleData();
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "lockPoint()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        handleData();
    }

    private void handleData() {
        RLock clientLock = LOCK_THREAD.get();
        if (clientLock != null) {
            try {
                log.info("任务执行完成,当前锁状态:{}", clientLock.isLocked());
                // 无需判断锁是否存在,直接调用unlock
                clientLock.unlock();
            } catch (Exception exception) {
                exception.printStackTrace();
            } finally {
                LOCK_THREAD.remove();
            }
        }
    }
}

开启自动代理

因为说明AOP无法拦截类内部的方法之间的调用,需要对启动类加上@EnableAspectJAutoProxy配置,代码如下:

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class RedisDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(RedisDemoApplication.class, args);
    }
}

使用

注意: 在调用使用分布式锁的方法时

TestController:

@RestController
@RequestMapping
@Slf4j
public class TestController {

    @Resource
    private RedisService redisService;

    @GetMapping(value = "/testLock")
    public String testLock() {	
        String key = "testKey";
//        String key = UUID.randomUUID().toString();
        redisService.testLock(key);
        return "success";
    }
}

RedisService :

public interface RedisService {
    /**
     * 方法直接调用使用锁
     *
     * @param keyName
     */
    void testLock(String keyName);
}

RedisServiceImpl:

@Service
@Slf4j
public class RedisServiceImpl implements RedisService{
    /**
     * 方法直接调用使用锁
     */
    @Override
    @RedisLock(keyNum = 0, lockTime = 1000, tryTime = 500)
    public void testLock(String keyName) {
        log.info("方法直接调用使用锁");
        try {
            // 如果业务执行过长
//            Thread.sleep(3200);
            System.out.println("加锁成功,执行业务逻辑");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

测试

测试一:
正常上锁情况
在这里插入图片描述

测试二:
设置tryTime,并且tryTime小于lockTime;当这样配置时,在第一个线程没有结束时,第二个线程,超过tryTime就会出现上锁失败;

修改代码如下:

    @RedisLock(keyNum = 0,lockTime = 3000,tryTime = 2000)
    public void testLock(String keyName) throws InterruptedException {
        log.info(keyName);
         TimeUnit.SECONDS.sleep(5);
    }

快速请求两次接口,截图如下:
在这里插入图片描述
通过截图可以看到,在第一个线程上锁后,过了2000ms出现了上锁失败的提示;

测试三:
设置tryTime,并且tryTime大于lockTime;当这样配置时,不会出现上锁失败,并且第二个线程会一直等到第一个线程结束;
修改代码如下:

    @RedisLock(keyNum = 0,lockTime = 3000,tryTime = 4000)
    public void testLock(String keyName) throws InterruptedException {
        log.info(keyName);
         TimeUnit.SECONDS.sleep(5);
    }

快速请求两次接口,截图如下:
在这里插入图片描述
通过截图可以看到,在第一个线程上锁后,超过了3000ms后,第二个线程开始上锁成功

你可能感兴趣的:(开发技巧,分布式,Redis,spring,boot,分布式,java,分布式锁,Redission)