注解式方法级业务锁-支持分布式

注解式方法级业务锁-支持分布式

需求描述、分析

在工作中,难免遇到需要进行并发控制的操作,要保证同一个数据,只允许一个进程来进行操作,其余进程等待解锁或返回失败的场景,遇到这样的情况,每次都需要在方法内编写很多并发控制的代码,不仅维护困难,也影响代码的可读性,所以考虑把这些代码写到切面里,并通过注解指定唯一的主键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

点赞、收藏加关注,找的时候不迷路。点赞搞起来ψ(*`ー´)ψ

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值