【aop+redis+spel表达式】注解实现分布式锁


前言

我们在项目中往往会使用分布式锁去保证多个服务间在同一时刻只能有一名用户去访问资源。而分布式锁的key一般采用业务字段去设置,具有很强的可复用性,所以可以采用aop+redis+spel表达式去设计注解版分布式锁。
分布式锁实现链接如下:

项目链接🔗https://gitee.com/llbnk/spring-coding
在这里插入图片描述


实现原理

一、spel表达式

1.什么是spel

Spring表达式语言全称为“Spring Expression Language”,能在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,并且能与Spring功能完美整合,如能用来配置Bean定义。spel表达式给静态Java语言增加了动态功能。Spel是单独模块,只依赖于core模块,可以单独使用。

我们可以参照官网文档来学习,官方文档上给出了spel表达式支持了18钟语法,含有大量的实例,简单易懂。(学习官网一手资料真的是太酷啦)
在这里插入图片描述

官方文档:spring spel

在spring中,spel语法使用非常广泛,比如@value注解就是使用spel,具体用法可以参考官方文档 8.4.2 Annotation-based configuration。

我们用spel去填充redis的key值。

"'spring-coding:'+#userId"

其中#代表注入key元素,''代表保留的key元素。

分布式锁key - spel表达式 - 实例

		ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression("'spring-coding:'+#userId");
        EvaluationContext context = new StandardEvaluationContext();
        context.setVariable("userId", "123456");
        System.out.println(expression.getValue(context));

1)创建解析器:ExpressionParser接口表示解析器
2)解析表达式:使用ExpressionParser的parseExpression来解析相应的表达式为Expression对象。
3)构造上下文:EvaluationContext为变量定义等等表达式提供需要的上下文数据。
4)求值:通过Expression接口的getValue方法获取上下文中数据。

2.spel源码分析

在上述例子中最重要的一步就是求值,我们来看探究一下是怎么取值的。

getValue

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在OpPlus类中实现了getValueInternal方法。

@Override
	public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
		SpelNodeImpl leftOp = getLeftOperand();

		if (this.children.length < 2) {  // if only one operand, then this is unary plus
			Object operandOne = leftOp.getValueInternal(state).getValue();
			if (operandOne instanceof Number) {
				if (operandOne instanceof Double) {
					this.exitTypeDescriptor = "D";
				}
				else if (operandOne instanceof Float) {
					this.exitTypeDescriptor = "F";
				}
				else if (operandOne instanceof Long) {
					this.exitTypeDescriptor = "J";
				}
				else if (operandOne instanceof Integer) {
					this.exitTypeDescriptor = "I";
				}
				return new TypedValue(operandOne);
			}
			return state.operate(Operation.ADD, operandOne, null);
		}

		TypedValue operandOneValue = leftOp.getValueInternal(state);
		Object leftOperand = operandOneValue.getValue();
		TypedValue operandTwoValue = getRightOperand().getValueInternal(state);
		Object rightOperand = operandTwoValue.getValue();

		if (leftOperand instanceof Number && rightOperand instanceof Number) {
			Number leftNumber = (Number) leftOperand;
			Number rightNumber = (Number) rightOperand;

			if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
				BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
				BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class);
				return new TypedValue(leftBigDecimal.add(rightBigDecimal));
			}
			else if (leftNumber instanceof Double || rightNumber instanceof Double) {
				this.exitTypeDescriptor = "D";
				return new TypedValue(leftNumber.doubleValue() + rightNumber.doubleValue());
			}
			else if (leftNumber instanceof Float || rightNumber instanceof Float) {
				this.exitTypeDescriptor = "F";
				return new TypedValue(leftNumber.floatValue() + rightNumber.floatValue());
			}
			else if (leftNumber instanceof BigInteger || rightNumber instanceof BigInteger) {
				BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class);
				BigInteger rightBigInteger = NumberUtils.convertNumberToTargetClass(rightNumber, BigInteger.class);
				return new TypedValue(leftBigInteger.add(rightBigInteger));
			}
			else if (leftNumber instanceof Long || rightNumber instanceof Long) {
				this.exitTypeDescriptor = "J";
				return new TypedValue(leftNumber.longValue() + rightNumber.longValue());
			}
			else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNumericOp(rightNumber)) {
				this.exitTypeDescriptor = "I";
				return new TypedValue(leftNumber.intValue() + rightNumber.intValue());
			}
			else {
				// Unknown Number subtypes -> best guess is double addition
				return new TypedValue(leftNumber.doubleValue() + rightNumber.doubleValue());
			}
		}

		if (leftOperand instanceof String && rightOperand instanceof String) {
			this.exitTypeDescriptor = "Ljava/lang/String";
			return new TypedValue((String) leftOperand + rightOperand);
		}

		if (leftOperand instanceof String) {
			return new TypedValue(
					leftOperand + (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state)));
		}

		if (rightOperand instanceof String) {
			return new TypedValue(
					(leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state)) + rightOperand);
		}

		return state.operate(Operation.ADD, leftOperand, rightOperand);
	}

这个方法大致就是分别获取左,右侧操作数数值。如果获取不到就先获取类型,再通过类型,去动态拼接EvaluationContext的参数。这也是为什么上线问要采用StandardEvaluationContext,因为它实现了EvaluationContext。


在这里插入图片描述在这里插入图片描述


二、AOP

自定义注解的运行离不开spring AOP切面的支持,我们采用环绕的方式去实现注解版分布式锁。环绕的aop的执行方式是在切入点方法的前后执行,能配合分布式锁的加锁和解锁。

切面代码

@Aspect
@Slf4j
@Component
public class RedLockAspect {

    @Autowired
    private RedissonClient redisClient;

    @Pointcut("@annotation(com.llbnk.springcoding.annotation.RedLock)")
    public void redLockPointcut() {
    }

    @Around("redLockPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    }
}

三、Redisson

1.redisson的分布式锁

redisson官网连接:redisson官网

redisson中的redLock采用lua脚本+缓存续命机制实现了分布式锁的独占性,高可用,防死锁,不乱抢,可重入等特性。并且提供了redlock算法实现了基于多个主节点redis实例的分布式锁杜绝了在分布式情况下,master突然宕机,slave获取锁成功的可能。

2.redisson和springboot整合

1.pom导入redisson依赖
2.启动redis
3.配置redis配置类
4.测试类测试

1.pom导入redisson依赖

		<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.15.6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>de.ruedigermoeller</groupId>
            <artifactId>fst</artifactId>
            <version>2.57</version>
        </dependency>

2.启动redis

我这里采用的是vmware本地虚拟机
1)修改redis.conf配置

默认daemonize no 改为daemonize yes
默认protected-mode yes 改为protected-mode no
默认bind 127.0.0.1 改为直接注释掉(默认bind 127.0.0.1只能本机访问)或改成本机IP地址

2)关闭防火墙

sudo systemctl stop firewalld

3.配置redis配置类

这部官网给出三种配置方法,具体可以参考官网:github-redisson官网

@Configuration
public class RedisConfig {
    @Bean
    public RedissonClient redissonClient(){
        // 创建配置 指定redis地址及节点信息
        Config config = new Config();
        config.useSingleServer().setAddress("redis://你的ip地址:6379");

        // 根据config创建出RedissonClient实例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

4.测试类测试

@SpringBootTest
class SpringbootRedissonApplicationTests {

    @Autowired
    private RedissonClient redissonClient;

    @Test
    void contextLoads() {
        redissonClient.getBucket("hello").set("bug");
        String test = (String) redissonClient.getBucket("hello").get();
        System.out.println(test);
     }
}

在这里插入图片描述
成功连接redis

四、代码编写

RedLock

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedLock {
    /**分布式锁key*/
    String key();
    /**锁的持有时间,成功获取锁后,锁会在指定时间后自动释放*/
    int leaseTime() default 2000;
    /**最长等待时间,在此时间内尝试获取所有锁,如果在指定时间内未能获取所有锁,则放弃剩余未获取到的锁*/
    int waitTime() default 200;
}

RedLockAspect

@Aspect
@Slf4j
@Component
public class RedLockAspect {

    @Autowired
    private RedissonClient redisClient;

    @Pointcut("@annotation(com.llbnk.springcoding.annotation.RedLock)")
    public void redLockPointcut() {
    }

    @Around("redLockPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //1.获取切入点中的MethodSignature
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        //2.获取方法参数并保存在context
        StandardEvaluationContext context = new StandardEvaluationContext();
        context.setRootObject(signature.getMethod());
        String[] paramNames = signature.getParameterNames();
        Object[] params = joinPoint.getArgs();
        if (paramNames != null && params != null && paramNames.length == params.length) {
            for (int i = 0; i < paramNames.length; i++) {
                context.setVariable(paramNames[i], params[i]);
            }
        }

        //3.获取分布式锁注解参数
        RedLock redLock = signature.getMethod().getAnnotation(RedLock.class);
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(redLock.key());
        String key = (String) expression.getValue(context);
        int waitTime = redLock.waitTime();
        int leaseTime = redLock.leaseTime();

        //4.使用redisson获取分布式锁
        RLock redisLock = redisClient.getLock(key);
        RedissonRedLock redissonRedLock = new RedissonRedLock(redisLock);

        //5.分布式锁执行业务逻辑
        if (redissonRedLock.tryLock(waitTime, leaseTime,TimeUnit.SECONDS)) {
            try {
                return joinPoint.proceed(params);
            } finally {
                redissonRedLock.unlock();
            }
        } else {
            throw new Exception("[redlock] is using...");
        }
    }
}

TestController

@Slf4j
@RestController
public class TestController {

    @Autowired
    private BlockService blockService;

    @GetMapping("/test/red/lock")
    public String testAnnotationRedLock(@RequestParam("userId") Integer userId) {
        return blockService.testAnnotationRedLock(userId);
    }

}

BlockService

@Slf4j
@Service
public class BlockService {
@RedLock(key = "'spring-coding:'+#userId")
    public String testAnnotationRedLock(Integer userId) {
        //在分布式锁中,处理业务
        log.info("in redlock process,userId:{}",userId);
        //模拟处理时间
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "ok";
    }
}

功能测试

一、接口测试

postman的curl

curl --location --request GET 'http://localhost:8080/test/red/lock?userId=123456'

测试结果

在这里插入图片描述
在这里插入图片描述

测试成功

一、分布式锁测试

单个接口测试不足以证明使用了分布式锁,我们采用jmeter并发测试,在测试之前我们需要将锁的最大等待时间和最大获取锁时间设置小一些。

BlockService

@Slf4j
@Service
public class BlockService {
@RedLock(key = "'spring-coding:'+#userId",leaseTime = 1,waitTime = 1)
    public String testAnnotationRedLock(Integer userId) {
        //在分布式锁中,处理业务
        log.info("in redlock process,userId:{}",userId);
        //模拟处理时间
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "ok";
    }
}

jmeter客户端设置

在这里插入图片描述
在这里插入图片描述

测试结果
在这里插入图片描述
虽然仅仅本地一台服务测试,但基于redisson的强大,在分布式环境下也可以做到独占性,高可用,防死锁,不乱抢,可重入等特性。


总结(防踩坑)

虽然注解版redis分布式锁好用但是也需要注意一点就是spring的注解顺序。spring在对注解进行优先级排序的时候采用先spring注解后切面自定义注解的原则,具体可以参考spring源码。
spring源码注解排序如下:

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
		List<Advisor> candidateAdvisors = findCandidateAdvisors();
		List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
		extendAdvisors(eligibleAdvisors);
		if (!eligibleAdvisors.isEmpty()) {
			eligibleAdvisors = sortAdvisors(eligibleAdvisors);
		}
		return eligibleAdvisors;
	}
@Override
	protected List<Advisor> sortAdvisors(List<Advisor> advisors) {
		List<PartiallyComparableAdvisorHolder> partiallyComparableAdvisors = new ArrayList<>(advisors.size());
		for (Advisor advisor : advisors) {
			partiallyComparableAdvisors.add(
					new PartiallyComparableAdvisorHolder(advisor, DEFAULT_PRECEDENCE_COMPARATOR));
		}
		List<PartiallyComparableAdvisorHolder> sorted = PartialOrder.sort(partiallyComparableAdvisors);
		if (sorted != null) {
			List<Advisor> result = new ArrayList<>(advisors.size());
			for (PartiallyComparableAdvisorHolder pcAdvisor : sorted) {
				result.add(pcAdvisor.getAdvisor());
			}
			return result;
		}
		else {
			return super.sortAdvisors(advisors);
		}
	}
  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值