注解式方法级业务锁-支持分布式
需求描述、分析
在工作中,难免遇到需要进行并发控制的操作,要保证同一个数据,只允许一个进程来进行操作,其余进程等待解锁或返回失败的场景,遇到这样的情况,每次都需要在方法内编写很多并发控制的代码,不仅维护困难,也影响代码的可读性,所以考虑把这些代码写到切面里,并通过注解指定唯一的主键ID,即可实现分布式的并发控制,类似下面的场景都适用:
1、存在同一时间多个用户操作同一物资、商品、订单等的库存的情况。(也是我遇到的场景)
2、轮询支付结果,支付成功后进行缴费等操作。
实现
编写方法注解
现在就是需要写一个切面的载体,所有加了这个注解的方法,都会进行并发控制。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author : haoyz
* @Description
* @date : 2022/11/12
* @Version 1.0
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BusinessLock {
//锁的名称,根据业务名称自定义
String lockName();
//是否生效,1生效,0不生效-即使加锁成功,仍不执行代码
String enable() default "1";
//锁的失效时间,默认5秒,单位秒,即redis中键的过期时间
long leaseTime() default 5;
}
编写指定唯一id的注解
还需要一个指定id的注解,用来区分不同的订单,来保证只对同一订单的访问进行限制,而不是限制这个方法的执行
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author : haoyz
* @Description
* @date : 2022/11/12
* @Version 1.0
**/
@Target(ElementType.PARAMETER) //指定加在方法的参数上
@Retention(RetentionPolicy.RUNTIME)
public @interface BusinessLockId {
}
为方法注解编织切面(核心)
切面中需要做的事就是,判断锁是否打开,然后去redis里判断key是否存在,如果存在说明当前同一业务已经有进程在处理,此时进行等待或返回错误。
核心部分是拿到切点的业务id。
import com.ai.bss.res.console.util.JedisUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestParam;
import java.lang.annotation.Annotation;
import java.util.concurrent.TimeUnit;
/**
* @Author:hyz
* @Date:2020/12/24
* @Desc:
**/
@Aspect
@Component
public class RedissonLockAspect {
@Autowired
private JedisUtil jedisUtil;
@Around("@annotation(businessLock)")
public Object around(ProceedingJoinPoint joinPoint, BusinessLock businessLock) throws Throwable {
if (!"1".equals(businessLock.enable())){
return null;
}
String lockName = businessLock.lockName();
long leaseTime = businessLock.leaseTime();
Object[] args = joinPoint.getArgs(); //获取切点的参数数组
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//然后拿到参数的注解数组,注意这是一个二维数组,里面一维是参数的数组,二维是该参数的注解数组
Annotation[][] pa = signature.getMethod().getParameterAnnotations();
int paIndex = this.getPAIndex(pa);
//如果没有加BusinessLockId注解,这里需要指定处理,可以返回错误,可以继续执行,这里返回null,表示执行失败
if (paIndex == -1){
return null;
}
//根据业务名称(lockName)和业务ID(加了BusinessLockId注解的参数)加锁
String lockFullFame = lockName+args[paIndex];
int tryTimes = 0;
boolean isFinish=false;
Object result = null;
do {
if (jedisUtil.hasKey(lockFullFame)){
//指定等待、或返回错误。当然如果是操作库存的情况需要视情况来等待或返回错误
//如果是以订单id为id加锁,如果操作数量不同可以进行等待,如果数量相同或其他唯一id相同则返回错误
Thread.sleep(100);
tryTimes++;
}else {
try {
//执行方法
if (jedisUtil.set(lockFullFame,"busy",leaseTime)){
result = joinPoint.proceed();
isFinish=true;
}else {
jedisUtil.del(lockFullFame);
return null;
}
}catch (Exception e){
e.printStackTrace();
}finally{
jedisUtil.del(lockFullFame);
}
}
}while (!isFinish&&tryTimes<=10);
return result;
}
private int getPAIndex(Annotation[][] pa){
//遍历所有参数
for (int i=0;i<pa.length;i++){
//拿到索引为i的参数的注解数组
Annotation[] b = pa[i];
for (int j=0;j< b.length;j++){
//有多重判断方法,这里通过名称判断
if (b[j].annotationType().getSimpleName().equals("BusinessLockId")){
return i;
}
}
}
return -1;
}
}
使用
@Transactional(rollbackFor = Exception.class)
@BusinessLock(lockName = ContextUtil.DS_MATERIAL_ISSUE_STORE_KEY)
public JSONObject materialIssue(MaterialIssueDto param,@BusinessLockId String applyId){
...
}
总结
通过上面的一顿操作,只需要在方法上添加注解并指定锁的id,即可实现并发控制,再也不用在方法里套一堆ifelse来进行redis的操作了
但依然有一些不足之处:
1、BusinessLockId注解是一个参数注解,每次使用都需要给方法多加一个参数用来指定id,如果是本来传入对象就行,结果还得像上面一样改成对象加String参数的样子,不够优雅。
欢迎指正
tips
点赞、收藏加关注,找的时候不迷路。点赞搞起来ψ(*`ー´)ψ