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

2023年 9月 5日 60.7k 0

一、前言

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

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

二、Redisson简介

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

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

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

「提供众多的锁:」

  • 可重入锁(Reentrant Lock)
  • 公平锁(Fair Lock)
  • 联锁(MultiLock)
  • 红锁(RedLock)
  • 读写锁(ReadWriteLock)

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

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

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配置

/**
 * @author wangzhenjun
 * @date 2022/2/9 9:57
 */
@Configuration
public class MyRedissonConfig {

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

    /**
     * 所有对redisson的使用都是通过RedissonClient来操作的
     * @return
     */
    @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序列化配置

/**
 * @author wangzhenjun
 * @date 2022/11/17 15:20
 */
@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);)这两种的加锁!等待锁超时时间、自动解锁时间、时间单位这是可选择的,大家按需,需要看门狗的有的就不需要,现在是有两种加锁机制,后面也是看大家的选择!

/**
 * @author wangzhenjun
 * @date 2023/8/30 10:45
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisLock {

    /**
     * 分布式锁的 key,必须:请保持唯一性,支持 spring el表达式
     *
     */
    String value();

    /**
     * 等待锁超时时间,默认30
     *
     */
    long waitTime() default 30;

    /**
     * 自动解锁时间,自动解锁时间一定得大于方法执行时间,否则会导致锁提前释放,默认100(根据场景配置)
     * 对时间没有把握可以使用默认的看门狗会自动续期
     */
    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)

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

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

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

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

/**
 * 分布式锁切片
 * @author wangzhenjun
 * @date 2023/8/31 9:28
 */
@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();
        }
    }

    /**
     * 解析el表达式
     * @param expression
     * @param point
     * @return
     */
    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中使用自定义注解实现分布式锁有一个清晰的理解,加锁的方式大家可以按需选择!

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论