自学内容网 自学内容网

一个注解实现分布式锁加锁

目录

一、概述

二、代码的实现

1、引入依赖

2、配置Redisson

3、定义注解

4、添加aop的切面方法

5、 支持 SpEL 表达式

三、代码验证

四、总结


一、概述

     在微服务项目的开发进程中,分布式锁的应用场景屡见不鲜。此时,我们需要借助分布式组件来实现,常见的分布式组件包括 Redis、Zookeeper、Etcd 等。然而,结合实际业务状况分析,通常会优先选择 Redis,原因在于 Redis 往往是微服务系统的必备组件,无需另行搭建。而在其中,我们常常运用基于 Redis 实现的 Java 分布式对象存储和缓存框架 Redisson 来达成分布式锁的功能。

        在通过 Redisson 实现分布式锁时,我们都得编写如下代码。但这样一来,每次使用都要书写这些代码,不仅麻烦(主要是因为懒),代码重复率高,而且加分布式锁的代码和业务代码相互耦合。鉴于此,我采用了 Spring 中的注解与 AOP 方式,以实现代码复用,进而简化分布式锁的加锁与解锁流程。

public void process() {
    RLock lock = redissonClient.getKey(key);
    try{
        if(lock.tryLock()){
            //执行自己的业务逻辑
        }
    } finally {
        if(lock.isHeldByCurrentThread()){
            lock.unlock();
        }
    }
}

        优化之后,代码的调用方式如下,这极大地简化了分布式锁的使用。

@DistributedLock(key = "process")
public void process() {
    // 具体的业务逻辑
}

二、代码的实现

1、引入依赖

        首先,我们需要导入以下依赖:

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.8.14.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>2.15.2</version>
</dependency>


2、配置Redisson

      新建一个 RedissonConfig.java 文件,代码如下:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class RedissonConfig {
 
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
        //这里配置你redis的地址
        .setAddress("redis://127.0.0.1:6379");
        // 如果有密码
        .setPassword("xxxx");
        .setDatabase(0)
        .setConnectionPoolSize(10)
        .setConnectionMinimumIdleSize(2);
        return Redisson.create(config);
    }
}


3、定义注解

首先,我们定义一个注解 @DistributedLock ,该注解包含以下几个参数 :

  • key:redis 锁的键,支持 SpEL 表达式。
  • waitTime:等待时间,默认为 0 毫秒。
  • expireTime:过期秒数,默认为 -1,使用 watchDog。
  • timeUnit:超时时间单位。
  • errorMsg:报错信息。

4、添加aop的切面方法

        在 around() 方法中,参数 joinPoint 是切入点,distributedLock 是切入点形参,用于传入键。这里,我们的键使用的是 SpEL 表达式,通过 SpelExpressionParser 能够获取最终的键值,joinPoint.proceed() 执行的则是上文提及的原方法的执行内容。

import cn.hutool.core.util.StrUtil;
import com.fhey.common.annotation.DistributedLock;
import com.fhey.common.exception.BusinessException;
import com.fhey.common.utils.SpelUtil;
import lombok.extern.slf4j.Slf4j;
import net.logstash.logback.encoder.org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * @author fhey
 * @date 2023-01-02 13:53:39
 * @description: 分布式锁注解切面
 */
@Aspect
@Component
@Slf4j
public class DistributeLockAspect {

    @Autowired
    private RedissonClient redissonClient;

    private static final String REDISSON_LOCK_PREFIX = "fhey:lock:";

    public static final String DEFAULT_EXPRESSION_PREFIX = "#";

    @Around("@annotation(distributedLock)")
    public Object around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
        Object[] args = joinPoint.getArgs();
        if (distributedLock == null || redissonClient == null) {
            return joinPoint.proceed(args);
        }
        String key = distributedLock.key();
        String errorMsg = distributedLock.errorMsg();
        String lockKey = getLockKey(joinPoint, key);
        try {
            boolean tryLock = tryLock(lockKey, distributedLock.waitTime(), distributedLock.expireTime(), distributedLock.timeUnit());
            if (!tryLock){
                log.info("distributed lock fail, key: {}",lockKey);
                if(StringUtils.isNotBlank(errorMsg)){
                    throw new BusinessException(errorMsg);
                }
                return null;
            }
            return joinPoint.proceed(args);
        } finally {
            unlock(lockKey);
        }
    }

    private boolean tryLock(String lockKey, int waitTime, int expireTime, TimeUnit timeUnit) {
        try {
            RLock lock = redissonClient.getLock(lockKey);
            boolean res = lock.tryLock(waitTime, expireTime, timeUnit);
            log.debug("distributed lock state:{}, lockKey:{}", res, lockKey);
            return res;
        } catch (Exception e) {
            log.error("distributed lock err,lockKey:{}", lockKey, e);
            throw new RuntimeException(e);
        }
    }

    private void unlock(String lockKey) {
        try {
            RLock lock = redissonClient.getLock(lockKey);
            lock.unlock();
            log.debug("UnLock distributed lock successfully, lockKey:{}", lockKey);
        } catch (Exception e) {
            log.error("Can not unlock, lockKey:{}", lockKey, e);
        }
    }

    /**
     * 将spel表达式转换为字符串
     * @param joinPoint 切点
     * @return redisKey
     */
    private String getLockKey(ProceedingJoinPoint joinPoint,String key) {
        String lockKey;
        if (!key.contains(DEFAULT_EXPRESSION_PREFIX)) {
            lockKey = key;
        } else {
            Signature signature = joinPoint.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            Method targetMethod = methodSignature.getMethod();
            Object target = joinPoint.getTarget();
            Object[] arguments = joinPoint.getArgs();
            lockKey = SpelUtil.parse(target,key, targetMethod, arguments);
        }
        lockKey = REDISSON_LOCK_PREFIX  + StrUtil.COLON + lockKey;
        return lockKey;
    }
}

5、 支持 SpEL 表达式

        为了使 key 字段能够支持 SpEL 表达式,所以在 getLockKey 方法中进行了 SpEL 解析,解析的工具类方法如下:

package com.fhey.common.utils;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;

/**
 * @author fhey
 * @date 2023-01-02 13:21:43
 * @description: Spel解析工具类
 */
public class SpelUtil {

    /**
     * 支持 #p0 参数索引的表达式解析
     * @param rootObject 根对象,method 所在的对象
     * @param spel 表达式
     * @param method ,目标方法
     * @param args 方法入参
     * @return 解析后的字符串
     */
    public static String parse(Object rootObject, String spel, Method method, Object[] args) {
        if (StrUtil.isBlank(spel)) {
            return StrUtil.EMPTY;
        }
        //获取被拦截方法参数名列表(使用Spring支持类库)
        LocalVariableTableParameterNameDiscoverer u =
                new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = u.getParameterNames(method);
        if (ArrayUtil.isEmpty(paraNameArr)) {
            return spel;
        }
        //使用SPEL进行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        //SPEL上下文
        StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject,method,args,u);
        //把方法参数放入SPEL上下文中
        for (int i = 0; i < paraNameArr.length; i++) {
            context.setVariable(paraNameArr[i], args[i]);
        }
        return parser.parseExpression(spel).getValue(context, String.class);
    }
}

三、代码验证

package com.fhey.common.utils;

import com.fhey.common.annotation.DistributedLock;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;

@SpringBootTest
public class DistributedLockTest {

    @Resource
    private TestServiceWithDistributedLock testService;  // 注入使用分布式锁的服务

    @Test
    public void testDistributedLock() {
        // 模拟并发调用服务方法
        Thread thread1 = new Thread(() -> {
            testService.processWithLock("key1");
        });

        Thread thread2 = new Thread(() -> {
            testService.processWithLock("key1");
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    class TestServiceWithDistributedLock {
        @DistributedLock(key = "#key1",errorMsg = "操作失败,请稍后重试!")
        public void processWithLock(String key) {
            // 模拟关键业务逻辑执行
            System.out.println("获取key: " + key + " 成功!");
            try {
                Thread.sleep(5000);  // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行结果:

获取key: key1 成功!
操作失败,请稍后重试!

四、总结

        本文详细介绍了在微服务项目开发中使用分布式锁的优化方法。首先阐述了常见分布式组件及选择 Redis 与 Redisson 的原因接着重点说明了通过 Spring 注解加 AOP 方式实现代码复用、简化分布式锁加锁和解锁流程的具体步骤,包括引入依赖、配置 Redisson、定义注解、添加 AOP 切面方法、支持 SpEL 表达式等,并提供了相应的代码示例和解释,为开发者在处理分布式锁相关问题时提供了有效的参考和解决方案。


原文地址:https://blog.csdn.net/q2qwert/article/details/140638862

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!