分布式锁?一个注解就搞定了

Hello,大家好,我是祥仔,今天给大家介绍如何用注解实现分布式锁,简化我们的开发流程,欢迎大家在评论区讨论。

在我们开发过程中,很多业务场景下都需要添加锁,尤其是在分布式系统中,确保数据一致性和防止并发问题至关重要。其中,Redis 作为一个高性能的键值存储,常常被用来实现分布式锁。使用 Redisson 这个开源库,可以非常方便地在我们的 Java 应用中实现分布式锁。

接下来,我将展示如何通过自定义注解来实现这一功能,让我们可以在业务逻辑中以更简洁的方式使用分布式锁

使用的技术选型:

  • SpringBoot、EL表达式、Redisson。
  • 可能有的小伙伴对EL表达式不太熟悉,我简单介绍下,具体的大家可以到网上查阅

EL 表达式在 Spring 中用于动态获取和操作对象的属性,通常用于 JSP 和 Thymeleaf 等视图模板引擎中。它的特点包括:

  • 简洁性:使用简洁的语法来访问对象属性和方法。
  • 动态性:允许在运行时动态评估表达式。
  • 安全性:通过表达式语言提供的安全特性,可以避免直接暴露 Java 对象。

简单来说,就是我可以通过符合某种规则的表达式,轻松的实现在对Java对象属性的访问

设计思路及依赖

我们采用SpingBoot的AOP技术对指定的注解进行拦截,使用EL表达式实现动态获取方法参数,基于Redisson 锁的相关操作

  • 流程图如下
  • 依赖如下:
xml 复制代码
 <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.26.1</version>
</dependency>

注解定义

之后我们可以直接在方法上添加这个注解,就能实现分布式锁的功能了

less 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {

    String prefix() default ""; // 使用 SpEL 表达式动态生成前缀

    String key();               // 使用 SpEL 表达式从方法参数中获取

    long leaseTime() default 10; // 锁的持有时间

    TimeUnit timeUnit() default TimeUnit.SECONDS; // 持有时间单位

    long waitTime();// 锁的等待时间
}

在我们加锁的过程中,经常使用以下几个参数

  • leaseTime 锁的持有时间
  • waitTime 锁的等待时间
  • timeUnit 时间单位
  • key 锁的key是什么
  • prefix key的前缀

切面

基于Redisson的分布式锁实现

less 复制代码
@Aspect
@Component
@Slf4j
@ConditionalOnProperty(name = "cache.type", havingValue = "redis", matchIfMissing = true)
public class RedisLockAspect {

    @Autowired
    private RedissonClient redissonClient;

    @Around("@annotation(distributedLock)")
    public Object around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
        // 获取 SpEL 表达式的 key 值
        // 解析 prefix 和 key
        String prefix = ELUtils.parseExpression(distributedLock.prefix(), joinPoint);
        String key = ELUtils.parseExpression(distributedLock.key(), joinPoint);
        String lockKey = prefix + key; // 组合成完整的锁 key
        long leaseTime = distributedLock.leaseTime();
        TimeUnit timeUnit = distributedLock.timeUnit();
        RLock lock = redissonClient.getLock(lockKey);

        log.info("start lock {}", lockKey);
        try {
            // 尝试获取锁
            if (lock.tryLock(distributedLock.waitTime(), leaseTime, timeUnit)) {
                // 获取到锁,执行方法
                return joinPoint.proceed();
            } else {
                // 未获取到锁,抛出异常或自定义处理逻辑
                throw new RuntimeException("Unable to acquire lock for key: " + key);
            }
        } finally {
            if (lock.isHeldByCurrentThread()) {
                log.info("end lock {}", lockKey);
                // 只有是当前线程才去释放锁
                lock.unlock();
            }
        }
    }

}
  • 这里,我们通过EL表达式实现了在注解中动态获取请求参数的功能。

基于ReentrantLock 单机版本

  • 有的同学们的项目可能是单机部署,在这我也给出了,基于ReentrantLock的单机版本锁实现
less 复制代码
@Aspect
@Component
@Slf4j
@ConditionalOnProperty(name = "cache.type", havingValue = "local", matchIfMissing = true)
public class LocalLockAspect {

    private final ExpressionParser parser = new SpelExpressionParser();
    private final ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<>();

    @Around("@annotation(distributedLock)")
    public Object around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
        // 解析 prefix 和 key
        String prefix = ELUtils.parseExpression(distributedLock.prefix(), joinPoint);
        String key = ELUtils.parseExpression(distributedLock.key(), joinPoint);
        String lockKey = prefix + key; // 组合成完整的锁 key

        // 获取 leaseTime
        long leaseTime = distributedLock.leaseTime();
        TimeUnit timeUnit = distributedLock.timeUnit();

        // 从 map 中获取锁
        ReentrantLock lock = lockMap.computeIfAbsent(lockKey, k -> new ReentrantLock());
        boolean acquired = false;

        log.info("Attempting to lock {}", lockKey);
        try {
            // 尝试获取锁,设置等待时间
            acquired = lock.tryLock(leaseTime, timeUnit);
            if (acquired) {
                log.info("Lock acquired: {}", lockKey);
                // 获取到锁,执行方法
                return joinPoint.proceed();
            } else {
                log.warn("Unable to acquire lock for key: {}", lockKey);
                throw new RuntimeException("Unable to acquire lock for key: " + lockKey);
            }
        } finally {
            if (acquired) {
                lock.unlock();
                log.info("Lock released: {}", lockKey);
                // 锁释放后,移除锁对象,避免内存泄漏
                lockMap.remove(lockKey);
            }
        }
    }

}

工具类

ini 复制代码
public class ELUtils {

    private static final ExpressionParser parser = new SpelExpressionParser();

    public static String parseExpression(String expression, ProceedingJoinPoint joinPoint) {
        if (expression.contains("#") || expression.contains("T(")) {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();

            // 创建解析上下文并设置方法参数
            StandardEvaluationContext context = new StandardEvaluationContext();
            Object[] args = joinPoint.getArgs();
            String[] paramNames = signature.getParameterNames();
            for (int i = 0; i < paramNames.length; i++) {
                context.setVariable(paramNames[i], args[i]);
            }

            // 解析 SpEL 表达式
            return parser.parseExpression(expression).getValue(context, String.class);
        } else {
            // 如果是普通字符串,直接返回
            return expression;
        }
    }
}

使用

我们可以设置锁的key前缀是 login_lock:,key 是 请求参数中的 phone 的值,等待时间是0s,锁的持续时间是10s。

less 复制代码
@DistributedLock(prefix = "login_lock:",key = "#loginRequest.phone",waitTime = 0,leaseTime = 10)
public BaseResponse userLogin(@RequestBody LoginRequest loginRequest) {}
相关推荐
毕设源码-赖学姐1 分钟前
【开题答辩全过程】以 花卉交易系统为例,包含答辩的问题和答案
java
weixin_7042660513 分钟前
Spring整合MyBatis(一)
java·spring·mybatis
翘着二郎腿的程序猿13 分钟前
Maven本地化部署与使用全指南
java·maven
历程里程碑15 分钟前
Linux 49 HTTP请求与响应实战解析 带http模拟实现源码--万字长文解析
java·开发语言·网络·c++·网络协议·http·排序算法
IronMurphy18 分钟前
【算法二十】 114. 寻找两个正序数组的中位数 153. 寻找旋转排序数组中的最小值
java·算法·leetcode
代码探秘者20 分钟前
【Java集合】ArrayList :底层原理、数组互转与扩容计算
java·开发语言·jvm·数据库·后端·python·算法
寻见90321 分钟前
10 分钟吃透 MyBatis 核心|从底层原理到实战技巧,Java 开发者必藏(无废话干货)
java·mysql·mybatis
隔壁小邓22 分钟前
分布式事务
java·后端
啦啦啦_999942 分钟前
1. AI 学习目录
java·人工智能
不懂英语的程序猿1 小时前
【Java工具类】Java提取最新错误日志(附 AI 对接思路)
java