本文主要是介绍这样用redission分布式锁才优雅-自定义redission分布式锁注解(含spel表达式),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
废话后面说,先上干货。
最终的使用效果是这样的:
/*** 这里只是一个简单的示例,实际业务中,可能需要根据订单号查询订单信息,然后进行发货操作* 仅仅是为了证明相同订单号不能够同时操作,但是在实际的业务场景中,每个订单只能发货一次*/// 这个注解(@RedisLock)就是主角,它是一个自定义的注解,用于实现分布式锁。被注解的方法会在执行时,先获取锁,然后执行方法体,最后释放锁。@RedisLock(lockName = "deliver_goods", key = "#payParam.orderNumbers")@PostMapping("/deliver-goods")public ServerResponseEntity<Void> deliverGoods(@RequestBody PayParam payParam) {// 休眠两秒,模拟发货操作try {log.info("开始发货,订单号:{}", payParam.getOrderNumbers());TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}log.info("发货成功,订单号:{}", payParam.getOrderNumbers());return ServerResponseEntity.success();}
// 实体类
public class PayParam {/*** 订单号*/private String orderNumbers;/*** 支付方式*/private Integer payType;}
上面的方法是模拟订单支付成功,商品发货的场景。同一笔订单只能发货一次,但是在服务器多节点且高并发的场景下,有可能两个服务器节点同时查询到商品待发货的状态,进而导致重复发货。此时,就需要将发货这一个逻辑加锁,异步变同步,保证线程安全。但是传统的synchronized无法在集群部署的服务器发挥作用,此时我们就需要用到传说中的分布式锁。
上面的例子,并不是采用传统的分布式锁的写法,而是仅仅用了一个注解搞定,相当的优雅,下面是这个注解背后的实现:
开发环境是JDK8+,项目基于SpringBoot搭建,先导包如下:
<redisson.version>3.25.2</redisson.version>………<!-- 使用redisson集成分布式锁等 --><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>${redisson.version}</version></dependency><!-- 引入这个是为了间接引入SPEL表达式所需要的工具包 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 自定义注解作切面必备依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
配置文件中配置redis地址
spring:data:redis:host: 192.168.*.*port: 6379password: *****
注意,上面的redis配置是spring.data.redis,是对 spring.redis 的扩展和增强,提供了更多的 Redis 配置选项和功能,例如支持 Redis Sentinel 和 Redis Cluster 等模式。因此,在 Spring Boot 2.x 及以上的版本中,推荐使用 spring.data.redis 进行 Redis 相关的配置。但对于 Spring Boot 1.x 版本仍然可以使用 spring.redis 进行配置。
自定义注解
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;/*** 使用redis进行分布式锁*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {/*** redis锁 名字*/String lockName() default "";/*** redis锁 key 支持spel表达式*/String key() default "";/*** 过期秒数,默认为5毫秒** @return 轮询锁的时间*/int expire() default 5000;/*** 超时时间单位** @return 秒*/TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
切面实现类
@Aspect
@Component
@Slf4j
public class RedisLockAspect {/*** redisson 客户端*/@Autowiredprivate RedissonClient redissonClient;/*** redis锁前缀*/private static final String REDISSON_LOCK_PREFIX = "redisson_lock:";/*** 针对注解redisLock进行环绕,切面实现方法** @param joinPoint 切点* @param redisLock 注解* @return 方法响应* @throws Throwable*/@Around("@annotation(redisLock)")public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {String spel = redisLock.key();String lockName = redisLock.lockName();RLock rLock = redissonClient.getLock(getRedisKey(joinPoint,lockName,spel));rLock.lock(redisLock.expire(),redisLock.timeUnit());Object result = null;try {//执行方法result = joinPoint.proceed();} finally {rLock.unlock();}return result;}/*** 将spel表达式转换为字符串* @param joinPoint 切点* @return redisKey*/private String getRedisKey(ProceedingJoinPoint joinPoint,String lockName,String spel) {Signature signature = joinPoint.getSignature();MethodSignature methodSignature = (MethodSignature) signature;Method targetMethod = methodSignature.getMethod();Object target = joinPoint.getTarget();Object[] arguments = joinPoint.getArgs();return REDISSON_LOCK_PREFIX + lockName + StrUtil.COLON + parse(target,spel, targetMethod, arguments);}/*** 支持 #p0 参数索引的表达式解析* @param rootObject 根对象,method 所在的对象* @param spel 表达式* @param method ,目标方法* @param args 方法入参* @return 解析后的字符串*/public String parse(Object rootObject,String spel, Method method, Object[] args) {if (StrUtil.isBlank(spel)) {return StrUtil.EMPTY;}if (!spel.contains("#")) {return spel;}// 语法校验checkSpEL(spel);//获取被拦截方法参数名列表(使用Spring支持类库)StandardReflectionParameterNameDiscoverer standardReflectionParameterNameDiscoverer = new StandardReflectionParameterNameDiscoverer();String[] paraNameArr = standardReflectionParameterNameDiscoverer.getParameterNames(method);if (ArrayUtil.isEmpty(paraNameArr)) {return spel;}//使用SPEL进行key的解析ExpressionParser parser = new SpelExpressionParser();//SPEL上下文StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject,method,args,standardReflectionParameterNameDiscoverer);//把方法参数放入SPEL上下文中for (int i = 0; i < paraNameArr.length; i++) {context.setVariable(paraNameArr[i], args[i]);}return parser.parseExpression(spel).getValue(context, String.class);}/*** SpEL 表达式校验*/private void checkSpEL(String spEL) {try {ExpressionParser parser = new SpelExpressionParser();parser.parseExpression(spEL, new TemplateParserContext());} catch (Exception e) {log.error("spEL表达式解析异常", e);throw new LittleException("Invalid SpEL expression [" + spEL + "]");}}}
搞定,就是这么简约且优雅,让我们再回顾一下开头提出的那个订单支付成功发货的案例,@RedisLock(lockName = “deliver_goods”, key = “#payParam.orderNumbers”)
这个注解其中的参数key就是支持固定字符和SPEL表达式,这里的key的写法就是SPEL表达式,表示获取请求入参payParam对象的orderNumbers参数值。
为了测试效果和便于理解,写个测试类测试一下:
测试类
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class NewTest {@Autowiredprivate OrderController orderController;@Testpublic void testDeliverGoods() {Runnable runnable = () -> {log.info("线程{}启动", Thread.currentThread().getName());PayParam payParam1 = new PayParam();payParam1.setOrderNumbers("1234");orderController.deliverGoods(payParam1);};new Thread(runnable).start(); // 线程1启动new Thread(runnable).start(); // 线程2启动// 主线程休眠3秒,等待线程执行完毕try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}}}
测试方法的主要思想就是同时开启两个线程,用同一个订单号同时请求发货方法,看看能不能实现异步变同步,执行结果如下:
2024-03-09T14:07:23.900+08:00 INFO 29796 — [ Thread-2] : 线程Thread-2启动
2024-03-09T14:07:23.900+08:00 INFO 29796 — [ Thread-3] : 线程Thread-3启动
2024-03-09T14:07:23.927+08:00 INFO 29796 — [ Thread-3] : 开始发货,订单号:1234
2024-03-09T14:07:25.934+08:00 INFO 29796 — [ Thread-3] : 发货成功,订单号:1234
2024-03-09T14:07:25.944+08:00 INFO 29796 — [ Thread-2] : 开始发货,订单号:1234
2024-03-09T14:07:27.952+08:00 INFO 29796 — [ Thread-2] : 发货成功,订单号:1234
通过执行日志我们发现,虽然两个线程几乎同时启动,但是两个线程确实是依次执行发货方法,尽管方法执行需要两秒,但是Thread-2还是等待Thread-3释放了锁之后才能够获取锁执行方法。
以上就是干货的全部内容,下面是一些闲谈。
上面的分布式锁能够发挥效果,主要是利用了redission的力量,在切面实现类中,我们调用了redission提供的lock方法:
rLock.lock(redisLock.expire(),redisLock.timeUnit());
这个方法是那我们设置的“key”去redis服务器设值,如果该key不存在于redis中则设置成功,程序继续运行;如果是redis中已有该“key”,则当前线程阻塞,等待该key释放并完成设值。
所以上述测试方法中Thread-2一直等待Thread-3释放了锁,才能继续执行。
想必很多同学已经发现了这个方法的风险,
- 如果Thread-3一直不释放锁获取占用锁时间过长,那么其他线程只能一直等待,造成资源浪费甚至死锁
- 如果有心之人发现你的方法存在阻塞,有可能利用这个进行DOS攻击,造成服务器瘫痪
下一篇我们进行一点小优化,规避以上风险
这篇关于这样用redission分布式锁才优雅-自定义redission分布式锁注解(含spel表达式)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!