您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

Springboot+Redisson封装分布式锁Starter

时间:2023-08-28 13:58:51  来源:IT动力  作者:
我们将分布式锁基于缓存扩展了一版,也就是说本starter即有分布式缓存功能,又有分布式锁功能。而注解版的分布式锁能够解决大多数场景的并核问题,小粒度的Lock锁方式补全其他场景。

1、为什么要使用分布式锁?

在分布式,微服务环境中,我们的服务被拆分为很多个,并且每一个服务可能存在多个实例,部署在不同的服务器上。

此时JVM中的synchronized和lock锁,将只能对自己所在服务的JVM加锁,而跨机器,跨JMV的场景,仍然需要锁的场景就需要使用到分布式锁了。

2、为什么要使用redis实现分布式锁?

因为Redis的性能很好,并且Redis是单线程的,天生线程安全。

并且Redis的key过期效果与Zookeeper的临时节点的效果相似,都能实现锁超时自动释放的功能。

而且Redis还可以使用lua脚本来保证redis多条命令实现整体的原子性,Redisson就是使用lua脚本的原子性来实现分布式锁的。

3、我们如何基于Redisson封装分布式锁?

1)、基于RedissonClient实现手动加锁

2)、基于AOP+Redisson封装注解版的分布式锁

3)、将分布式锁功能封装成一个starter, 引入jar包即可实现分布式锁

4、代码实现

4.1、整合封装Redisson

我们前面封装了基于Redis扩展了SpringCache,封装了
redis-cache-spring-boot-starter。

我们的分布式锁基于这个模块实现,下面引入依赖。

引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.Apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>itdl-parent</artifactId>
        <groupId>com.itdl</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>redis-lock-spring-boot-starter</artifactId>
    <description>Redis实现分布式锁的自定义starter封装模块</description>

    <properties>
        <maven.compiler.source>${JAVA.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
    </properties>

    <dependencies>
        <!--redis cache-->
        <dependency>
            <groupId>com.itdl</groupId>
            <artifactId>redis-cache-spring-boot-starter</artifactId>
        </dependency>

        <!--redisson-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
        </dependency>

        <!--aop-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    </dependencies>
</project>

编写RedisLockConfig配置RedissonClient

/**
 * Redis实现分布式锁的配置(使用Redisson)
 */
@Configuration  // 标识为一个配置项,注入Spring容器
@AutoConfigureBefore({CustomRedisConfig.class, CacheNullValuesHandle.class})
@ConditionalOnProperty(value = "redis.enable", havingValue = "true")    // 开启redis.enable=true时生效
@Slf4j
public class RedisLockConfig {

    private volatile boolean isCluster = false;

    private volatile String redisHostsStr = "";

    @Bean
    @ConditionalOnMissingBean
    public RedissonClient redissonClient(CustomRedisProperties redisProperties){
        // 构建配置
        Config config =  buildConfig(redisProperties);

        RedissonClient redissonClient = Redisson.create(config);
        log.info("==============创建redisClient{}版成功:{}==================", isCluster ? "集群": "单机", redisHostsStr);
        return redissonClient;
    }

    private Config buildConfig(CustomRedisProperties redisProperties) {
        final Config config = new Config();
        // 根据逗号切割host列表
        Set<String> hosts = org.springframework.util.StringUtils.commaDelimitedListToSet(redisProperties.getHost());
        if (CollectionUtils.isEmpty(hosts)){
            throw new RuntimeException("redis host address cannot be empty");
        }
        // 只有一个host, 表示是单机host
        if (hosts.size() == 1){
            String hostPort = hosts.stream().findFirst().get();
            redisHostsStr = "redis://" + hostPort.trim();
            config.useSingleServer()
                    .setAddress(redisHostsStr)
                    .setDatabase(redisProperties.getDatabase())
                    .setPassword(StringUtils.isBlank(redisProperties.getPassword()) ? null : redisProperties.getPassword())
            ;
            isCluster = false;
        }else {
            // 集群处理
            String[] redisHosts = new String[hosts.size()];
            int i = 0;
            for (String host : hosts) {
                String[] split = host.split(":");
                if (split.length != 2){
                    throw new RuntimeException("host or port err");
                }
                redisHosts[i] = "redis://" + host.trim();
                i++;
            }
            redisHostsStr = String.join(",", redisHosts);
            // 配置集群
            config.useClusterServers()
                    .addNodeAddress(redisHosts)
                    .setPassword(StringUtils.isBlank(redisProperties.getPassword()) ? null : redisProperties.getPassword())
                    // 解决Not all slots covered! Only 10922 slots are avAIlable
                    .setCheckSlotsCoverage(false);
            isCluster = true;
        }

        return config;
    }
}

我们配置时需要优先配置好redis-cache-spring-boot-starter,使用@AutoConfigureBefore({CustomRedisConfig.class, CacheNullValuesHandle.class})

直接使用,不再重复造轮子,然后我们根据自定义属性配置文件CustomRedisProperties来创建RedissonClient的Bean。

编写META-INF/spring.factories进行自动配置

org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.itdl.lock.config.RedisLockConfig

在测试模块缓存service添加分布式锁

@Cacheable(cacheNames = "demo2#3", key = "#id")
public TestEntity getById2(Long id){
    // 创建分布式锁
    RLock lock = redissonClient.getLock("demo2_lock");
    // 加锁    
    lock.lock(10, TimeUnit.SECONDS);
    if (id > 1000){
        log.info("id={}没有查询到数据,返回空值", id);
        return null;
    }
    TestEntity testEntity = new TestEntity(new Random().nextLong(), UUID.randomUUID().toString(), new Random().nextInt(20) + 10);
    log.info("模拟查询数据库:{}", testEntity);
    // 释放锁
    lock.unlock();
    return testEntity;
}

我们这里的@Cacheable没有加sync=true, 此时并发请求会存在线程安全问题,但是我们在方法体局部添加了分布式锁,因此我们的程序会按照顺序执行。

如果我们的参数被定死了,最终请求会被先存储到缓存,所以后续的查询就会走缓存,这能很好的测试分布式锁的效果。

编写测试程序

@SpringBootTest
public class TestRedisLockRunner6 {

    @Autowired
    private MyTestService myTestService;

    // 创建一个固定线程池
    private ExecutorService executorService = Executors.newFixedThreadPool(16);

    /**
     * 多线程访问请求,测试切面的线程安全性
     */
    @Test
    public void testMultiMyTestService() throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            executorService.submit(() -> {
                // 每次查询同一个参数
                TestEntity t1 = myTestService.getById2(1L);
            });
        }

        // 主线程休息10秒种
        Thread.sleep(10000);
    }

}

我们可以看到,结果并没有符合我们预期,但是又部分符合我们预期,为什么呢?

因为我们的@Cacheable是存在线程安全问题的,因为它先查询缓存这个操作存在并发问题,查询时就同时有N个请求进入@Cacheable, 并且都查询没有缓存。

然后同时执行方法体,但方法体加了分布式锁,所以排队进行处理,因此序号有序。

但打印数量不足总数,是因为这一批次没有全部到达@Cacheable,而是执行完毕之后才将缓存回填,所以后续的请求就是走缓存了。

解决方案:我们加上sync=true之后就能实现,只查询一次数据库,就可以回填缓存了。如果我们去掉@Cacheable注解,则会每一次都查询数据库,但是时按照顺序执行的。

加上sync=true测试

效果达到了我们的预期,继续看一下去掉@Cacheable注解的情况。

去掉@Cacheable注解测试

我们的分布式锁功能是没有问题的,但是每次我们都需要执行getLock(), lock.lock(), lock.unlock(),是不是很麻烦,能不能一个注解搞定?

当然是可以的。

4.2、封装注解版分布式锁

编写@RedisLock注解

/**
 * 自定义Redis分布式锁
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {

    /**分布式锁的名称,支持el表达式*/
    String lockName() default "";

    /**锁类型 默认为可重入锁*/
    LockType lockType() default REENTRANT_LOCK;

    /**获取锁等待时间,默认60秒*/
    long waitTime() default 30000L;

    /** 锁自动释放时间,默认60秒*/
    long leaseTime() default 60000L;

    /**
     * 被加锁方法执行完是否立即释放锁
     */
    boolean immediatelyUnLock() default true;

    /** 时间单位, 默认毫秒*/
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}

编写分布式锁切面RedisLockAop

/**
 * Redis分布式锁的切面逻辑实现
 */
@ConditionalOnProperty(value = "redis.enable", havingValue = "true")    // 开启redis.enable=true时生效
@AutoConfigureBefore(RedisLockConfig.class)
@Aspect
@Configuration
@Slf4j
public class RedisLockAop {

    @Resource
    private RedissonClient redissonClient;

    /**
     * 切点
     */
    @Pointcut("@annotation(com.itdl.lock.anno.RedisLock)")
    public void pointcut(){

    }
    
    /**
     * 环绕通知 注解针对的是方法,这里切点也获取方法进行处理就可以了
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();

        // 获取方法上的分布式锁注解
        RedisLock redisLock = method.getDeclaredAnnotation(RedisLock.class);

        // 获取注解的参数
        // 锁名称
        String lockName = redisLock.lockName();
        // 锁类型
        LockType lockType = redisLock.lockType();

        // 获取RedissonClient的Lock
        RLock lock = getRLock(lockName, lockType, redisLock);

        //获取到锁后, 开始执行方法,执行完毕后释放锁
        try {
            log.debug("=========>>>获取锁成功, 即将执行业务逻辑:{}", lockName);
            Object proceed = joinPoint.proceed();
            // 释放锁
            if (redisLock.immediatelyUnLock()) {
                //是否立即释放锁
                lock.unlock();
            }
            log.debug("=========>>>获取锁成功且执行业务逻辑成功:{}", lockName);
            return proceed;
        } catch (Exception e) {
            log.error("=========>>>获取锁成功但执行业务逻辑失败:{}", lockName);
            e.printStackTrace();
            throw new RedisLockException(LockErrCode.EXEC_BUSINESS_ERR);
        }finally {
            // 查询当前线程是否保持此锁定 被锁定则解锁
            lock.unlock();
            log.debug("=========>>>释放锁成功:{}", lockName);
        }
    }

    /**
     * 根据锁名称和类型创建锁
     * @param lockName 锁名称
     * @param lockType 锁类型
     * @return 锁
     */
    private RLock getRLock(String lockName, LockType lockType, RedisLock redisLock) throws InterruptedException {
        RLock lock;
        switch (lockType){
            case FAIR_LOCK:
                lock = redissonClient.getFairLock(lockName);
                break;
            case READ_LOCK:
                lock = redissonClient.getReadWriteLock(lockName).readLock();
                break;
            case WRITE_LOCK:
                lock = redissonClient.getReadWriteLock(lockName).writeLock();
                break;
            default:
                // 默认加可重入锁,也就是普通的分布式锁
                lock = redissonClient.getLock(lockName);
                break;
        }
        // 首先尝试获取锁,如果在规定时间内没有获取到锁,则调用lock等待锁,直到获取锁为止
        if (lock.tryLock()) {
            lock.tryLock(redisLock.waitTime(), redisLock.leaseTime(), redisLock.timeUnit());
        }else {
            // 如果leaseTime>0,规定时间内获取锁,超时则自动释放锁
            long leaseTime = redisLock.leaseTime();
            if (leaseTime > 0) {
                lock.lock(redisLock.leaseTime(), redisLock.timeUnit());
            } else {
                // 自动释放锁时间设置为0或者负数,则加锁不设置超时时间
                lock.lock();
            }
        }
        return lock;
    }
}

话不多说,封装的逻辑已经在注释中写的很清晰了。

将切面也放入自动配置spring.factories中

org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.itdl.lock.config.RedisLockConfig,
com.itdl.lock.anno.RedisLockAop

测试注解版分布式锁

@RedisLock(lockName = "demo4_lock")
public TestEntity getById4(Long id) throws InterruptedException {
    index++;
    log.info("current index is : {}", index);
    Thread.sleep(new Random().nextInt(10) * 100);
    TestEntity testEntity = new TestEntity(new Random().nextLong(), UUID.randomUUID().toString(), new Random().nextInt(20) + 10);
    log.info("模拟查询数据库:{}", testEntity);
    return testEntity;
}

可以看到,我们就是一个注解分布式锁的效果,而分布式锁与缓存注解通常不会一起使用,因为一般会在存在事务问题的地方我们会使用锁,在多个JMV操作同一条数据做写操作时需要加分布式锁。

编写测试程序

@SpringBootTest
public class TestRedisLockRunner6 {

    @Autowired
    private MyTestService myTestService;

    // 创建一个固定线程池
    private ExecutorService executorService = Executors.newFixedThreadPool(16);

    /**
     * 多线程访问请求,测试切面的线程安全性
     */
    @Test
    public void testMultiMyTestService() throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            executorService.submit(() -> {
                try {
                    TestEntity t1 = myTestService.getById4(1L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 主线程休息10秒种
        Thread.sleep(60000);
    }

}

测试结果

5、小结

我们将分布式锁基于缓存扩展了一版,也就是说本starter即有分布式缓存功能,又有分布式锁功能。

而注解版的分布式锁能够解决大多数场景的并核问题,小粒度的Lock锁方式补全其他场景。

将两者封装成为一个starter,我们就可以很方便的使用分布式锁功能,引入相关包即可,开箱即用。



Tags:Springboot   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
详解基于SpringBoot的WebSocket应用开发
在现代Web应用中,实时交互和数据推送的需求日益增长。WebSocket协议作为一种全双工通信协议,允许服务端与客户端之间建立持久性的连接,实现实时、双向的数据传输,极大地提升了用...【详细内容】
2024-01-30  Search: Springboot  点击:(10)  评论:(0)  加入收藏
SpringBoot如何实现缓存预热?
缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。那么问题来了,在 Spring Boot 项目启动之后,在什么时候?在哪里可以将数据加载到缓存系...【详细内容】
2024-01-19  Search: Springboot  点击:(86)  评论:(0)  加入收藏
SpringBoot3+Vue3 开发高并发秒杀抢购系统
开发高并发秒杀抢购系统:使用SpringBoot3+Vue3的实践之旅随着互联网技术的发展,电商行业对秒杀抢购系统的需求越来越高。为了满足这种高并发、高流量的场景,我们决定使用Spring...【详细内容】
2024-01-14  Search: Springboot  点击:(90)  评论:(0)  加入收藏
公司用了六年的 SpringBoot 项目部署方案,稳得一批!
本篇和大家分享的是springboot打包并结合shell脚本命令部署,重点在分享一个shell程序启动工具,希望能便利工作。 profiles指定不同环境的配置 maven-assembly-plugin打发布压...【详细内容】
2024-01-10  Search: Springboot  点击:(163)  评论:(0)  加入收藏
简易版的SpringBoot是如何实现的!!!
SpringBoot作为目前最流行的框架之一,同时是每个程序员必须掌握的知识,其提供了丰富的功能模块和开箱即用的特性,极大地提高了开发效率和降低了学习成本,使得开发人员能够更专注...【详细内容】
2023-12-29  Search: Springboot  点击:(132)  评论:(0)  加入收藏
用 SpringBoot+Redis 解决海量重复提交问题
前言 一:搭建redis的服务Api 二:自定义注解AutoIdempotent 三:token创建和检验 四:拦截器的配置 五:测试用例 六:总结前言:在实际的开发项目中,一个对外暴露的接口往往会面临很多...【详细内容】
2023-12-20  Search: Springboot  点击:(53)  评论:(0)  加入收藏
SpringBoot中如何优雅地个性化定制Jackson
当使用 JSON 格式时,Spring Boot 将使用ObjectMapper实例来序列化响应和反序列化请求。在本教程中,我们将了解配置序列化和反序列化选项的最常用方法。一、默认配置默认情况下...【详细内容】
2023-12-20  Search: Springboot  点击:(132)  评论:(0)  加入收藏
springboot-如何集成Validation进行参数校验
一、步骤概览 二、步骤说明1.引入依赖包在 pom.xml 文件中引入 validation 组件,它提供了在 Spring Boot 应用程序中进行参数校验的支持。<!-- WEB 程序依赖包 --><dependen...【详细内容】
2023-12-13  Search: Springboot  点击:(156)  评论:(0)  加入收藏
优雅的springboot参数校验,你学会了吗?
前言在后端的接口开发过程,实际上每一个接口都或多或少有不同规则的参数校验,有一些是基础校验,如非空校验、长度校验、大小校验、格式校验;也有一些校验是业务校验,如学号不能重...【详细内容】
2023-11-29  Search: Springboot  点击:(199)  评论:(0)  加入收藏
Springboot扩展点之BeanDefinitionRegistryPostProcessor,你学会了吗?
前言通过这篇文章来大家分享一下,另外一个Springboot的扩展点BeanDefinitionRegistryPostProcessor,一般称这类扩展点为容器级后置处理器,另外一类是Bean级的后置处理器;容器级...【详细内容】
2023-11-27  Search: Springboot  点击:(174)  评论:(0)  加入收藏
▌简易百科推荐
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(5)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(12)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(8)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(10)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(8)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
Kubernetes 究竟有没有 LTS?
从一个有趣的问题引出很多人都在关注的 Kubernetes LTS 的问题。有趣的问题2019 年,一个名为 apiserver LoopbackClient Server cert expired after 1 year[1] 的 issue 中提...【详细内容】
2024-03-15  云原生散修  微信公众号  Tags:Kubernetes   点击:(6)  评论:(0)  加入收藏
站内最新
站内热门
站内头条