Java基于Spring AOP+Redis+注解实现适用多种场景的分布式锁
本文于1665天之前发表,文中内容可能已经过时。
分布式系统开发中常常用到分布式锁,比如防止多个用户同时预订同一个商品,传统的synchronized
就无法实现了,而基于数据库的乐观锁实现又可能会对数据库产生较大的压力。而分布式锁相对较轻量,对性能影响也较小。目前主流的分布式锁都基于Redis实现。使用分布式锁的流程一般如下:
如果需要使用分布式锁的地方有多个,那么就需要写多个类似的代码。而重复代码是开发中最常见到的 bad smell 。我们可以使用 AOP 把这段逻辑抽象出来,这样就避免了重复代码,也极大地减去了工作量。
目标
- 对业务代码无侵入(或侵入性较小)
- 使用方便
- 对性能影响小
- 易维护
方案
- 使用注解(假设注解为
@Lock
)声明要使用分布式锁的业务method
、要锁定的对象(一般是业务主键)、失效时间等信息。 - 使用Spring AOP
arround
(环绕通知)增强被@Lock
注解的方法,把前面提到的”使用分布式锁的流程“逻辑抽象到切面中。 - 使用Redis实现分布式锁。一般是基于string类型的
set
命令实现。
难点
- 如何根据请求的不同,锁定不同的对象?
可以使用 Spring EL 表达式指定锁定对象,加锁时根据业务方法参数值、参数名称解析表达式,得出要加锁的对象(Redis string的key)。 - 分布式锁该如何选择?
- 可以选择自己实现(加锁使用
set
命令即可,解锁需要使用lua
脚本保证命令的原子性,先判断锁是否仍有效、是否由当前线程加锁,是的话才能通过del
来解锁)。 - 也可以选择使用
redisson
等第三方库。使用方式可以参考官方示例:Spring版/Spring Boot版
- 可以选择自己实现(加锁使用
优点
使用时只需在业务方法上加一个注解就可以了,使用灵活、开发效率高、侵入小、适用性强。业务方法只需专注于业务代码,可读性强,易维护。
适用场景
- 防止并发修改
- 防止重复提交
- 幂等校验
- ……
实现
1. 引入包
Spring Boot方式
1 | <dependency> |
Spring 方式
1 | <dependency> |
2. 定义注解
1 |
|
3. 定义注解数据模型
1 |
|
4. 定义切面逻辑
1 |
|
5. 打开编译开关
在项目的pom文件中的编译插件中添加参数:-parameters
(JDK8+才支持),用于在编译时把方法参数名称保留到 class 文件中。这样我们就可以通过Spring EL表达式动态指定要加锁的key。
1 | <plugin> |
JDK8以下版本可以使用别的方式获取方法参数名称,也可以将表达式写为类似于[0].getOrderId()
(获取第一个入参的orderId属性的值)
的格式动态指定key(因为方法入参可以看做是一个Object[]
)。
6. 使用
防止并发修改,可以把
Lock
注解在例如修改订单的接口方法上,waitTime设置为0,key为订单id,这样就可以防止多个人并发修改订单。1
2
3
4
5
public void modifyOrder(ModifyOrderDTO dto) {
// ......
}防止重复提交。例如防止重复下单,可以将waitTime设置为0,key为会员id,在订单未保存成功前,用户多次提交订单都会直接返回提示信息。
1
2
3
4
5
6
public OrderVO saveOrder(OrderVO orderVO) {
// ......
}
总结
基于注解的分布式锁可以帮助我们减少大量模板代码,使用方便,出现问题也很容易修复。对于具体的加锁逻辑可以选择自己实现,也可以选择使用redisson
等第三方库。
赞赏是不耍流氓的鼓励