文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

分布式进阶:Springboot自定义注解优雅的实现Redisson分布式锁

2024-11-30 09:20

关注

一、前言

在这个微服务多节点、多线程的环境中,多个任务可能会同时竞争访问共享资源,从而导致数据错误和不一致。一般的JVM层面的加锁显然无法满足多个节点的情况!分布式锁就出现了,在redis官网推荐Java使用Redisson去实现分布式锁!

这是基本api调用,今天我们使用自定义注解来完成,一劳永逸,减少出错!

二、Redisson简介

Redisson是一个用于Java应用程序的开源的、基于Redis的分布式和高性能数据结构服务库。它提供了一系列的分布式对象和服务,帮助开发人员更轻松地在分布式环境中使用Java编程语言。Redisson通过封装Redis的功能,使得开发者能够更方便地利用分布式特性,同时提供了许多额外的功能和工具。

比setnx简单的加锁机制,Redisson会提供更完善的加锁机制,比如:

「到期方法没有执行完成,引入看门狗机制自动续期,内部使用Lua脚本保证原子性!」

「提供众多的锁:」

对于今天的注解形式,只能实现可重入锁、公平锁两种形式,不过也满足大部分业务场景!

今天以实战为主,这些信息可以去官网看一下详细的文档:

Redisson文档:https://github.com/redisson/redisson/wiki/1.-Overview。

三、实战

1、导入依赖


    org.springframework.boot
    spring-boot-starter-data-redis


    org.redisson
    redisson
    3.12.0

2、配置文件

server:
  port: 8087
spring:
  redis:
    password: 123456
    # 一定要加redis://
    address: redis://127.0.0.1:6379
  datasource:
    #使用阿里的Druid
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test?serverTimeznotallow=UTC
    username: root
    password:

3、RedissonClient配置


@Configuration
public class MyRedissonConfig {

    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.address}")
    private String address;

    
    @Bean(destroyMethod="shutdown")
    public RedissonClient redissonClient(){
        // 1. 创建配置
        Config config = new Config();
        // 一定要加redis://
        config.useSingleServer().setAddress(address);
        config.useSingleServer().setPassword(password);
        // 2. 根据config创建出redissonClient实例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

4、Redis序列化配置


@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

5、自定义注解

我们自定义注解,key支持el表达式!这里的参数可以再加一个key的前缀或者锁的类型,根据类型判断:可重入锁(RLock getLock(String name))、公平锁(RLock getFairLock(String name);)这两种的加锁!等待锁超时时间、自动解锁时间、时间单位这是可选择的,大家按需,需要看门狗的有的就不需要,现在是有两种加锁机制,后面也是看大家的选择!


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisLock {

    
    String value();

    
    long waitTime() default 30;

    
    long leaseTime() default 100;

    
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

6、定义切片

现在有两种加锁方式,我们来详细说一下区别,大家按需选择:

「lock.tryLock():」

这是一个非阻塞的方法。如果获取锁成功,会立即返回 true,如果获取锁失败,会立即返回 false。

当然你可以添加等待时间,超过这个时间仍然没有获取到锁才会返回false。tryLock(long time, TimeUnit unit)tryLock(long waitTime, long leaseTime, TimeUnit unit)

如果你想尝试获取锁,但「不希望在获取失败时被阻塞」,可以使用这个方法。

这个方法通常用于获取锁后执行一个短时间的任务,避免长时间的等待。

「lock.lock():」

这是一个阻塞的方法,如果获取锁失败,它会阻塞当前线程,直到获取到锁或超时。因此要确保你的锁的使用不会导致长时间的等待,避免影响系统性能。

也可以添加锁的过期时间,一旦获取锁成功,锁会在指定的时间后自动释放。如果在这段时间内任务未完成,锁会自动释放,避免长时间的占用。这个时间要考虑清除,如果执行时间不可控建议还是不要传过期时间,默认会有看门狗来自动续期,防止方法执行中锁被释放了!

lock(long leaseTime, TimeUnit unit)

如果你希望一定能够获取锁,而且「不希望在获取失败时立即返回」,可以使用这个方法。

这个方法通常用于获取锁后需要执行一个相对耗时的任务,以及希望避免锁被长时间占用而引发的问题。

小编这里建议使用第一种,有锁正在执行,应该返回信息给用户,不应该让用户长时间等待造成不好的影响!

如果是第一个方法,我们需要判断返回值,加锁失败返回给用户!异常大家可以专门定义一个加锁失败异常,小编这里就使用业务异常了!


@Slf4j
@Aspect
@RequiredArgsConstructor
@Component
public class RedisLockAspect {

    private final RedissonClient redissonClient;
    private final SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
    private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();


    
    @Around("@annotation(redisLock)")
    public Object aroundRedisLock(ProceedingJoinPoint point, RedisLock redisLock) throws Throwable {
        log.info("=====请求来排队尝试获取锁=====");
        String value = redisLock.value();
        Assert.hasText(value, "@RedisLock key不能为空!");
        boolean el = redisLock.isEl();
        String lockKey;
        if (el) {
            lockKey = evaluateExpression(value, point);
        } else {
            lockKey = value;
        }
        log.info("========解析后的lockKey :{}", lockKey);
        long waitTime = redisLock.waitTime();
        long leaseTime = redisLock.leaseTime();
        TimeUnit timeUnit = redisLock.timeUnit();

        RLock lock = redissonClient.getLock(lockKey);
//        lock.tryLock(waitTime, leaseTime, timeUnit);
//        lock.lock(leaseTime, timeUnit);
//        lock.lock();
        boolean tryLock = lock.tryLock();
        if (!tryLock) {
            throw new ServiceException("锁被占用,请稍后提交!");
        }
        try {
            return point.proceed();
        } catch (Throwable throwable) {
            log.error("方法执行失败:", throwable.getMessage());
            throw throwable;
        } finally {
            lock.unlock();
        }
    }

    
    private String evaluateExpression(String expression, ProceedingJoinPoint point) {
        // 获取目标对象
        Object target = point.getTarget();
        // 获取方法参数
        Object[] args = point.getArgs();
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        Method method = methodSignature.getMethod();

        EvaluationContext context = new MethodBasedEvaluationContext(target, method, args, parameterNameDiscoverer);
        Expression exp = spelExpressionParser.parseExpression(expression);
        return exp.getValue(context, String.class);
    }
}

7、测试

我们测试一个el表达式的,模拟方法执行15s,方便我们测试!

@SneakyThrows
@RedisLock("#id")
@GetMapping("/listTest")
public Result listTest(@RequestParam("id") Long id){
    System.out.println("=====方法执行中");
    Thread.sleep(150000);
    System.out.println("=====方法执行完成");
    return Result.success("成功");
}

我们调用两次这个方法,看到控制台有报错信息,返回结果也是没有问题的!

四、总结

在本篇博客中,我们深入探讨了如何在Spring Boot应用中借助自定义注解来实现分布式锁,为分布式环境下的并发问题提供了优雅且高效的解决方案。通过自定义注解,我们成功地将分布式锁的复杂逻辑进行了封装,使得在业务代码中只需简单地使用注解,便能实现分布式锁的获取和释放。这不仅让代码更具可读性,还提升了开发效率,让开发人员能够更专注于业务逻辑的实现。

相信大家已经能够对Spring Boot中使用自定义注解实现分布式锁有一个清晰的理解,加锁的方式大家可以按需选择!

来源:小王博客基地内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯