Spring AOP+自定义注解实现函数并发重入控制

一、前言

线上接口在并发请求时会有概率重复执行,导致数据操作被重复处理,对涉及到数据操作的接口都应做系统级同步。实现思路是对有同步需求的接口或方法进行加锁处理,采用ReentrantLock防止重入,同步并发线程。

二、设计

使用自定义注解和AOP切面编程实现快捷的锁功能,在进入方法时根据注解上自定义的key生成或获取锁,然后打开锁,在方法执行完成后关闭锁,最后根据锁的等待数量判断是否从锁缓存中移除。在具体业务需求中锁常常是和业务参数相互匹配的,比如在用户注册时需要根据电话号码开启锁,所以注解上的key需要与业务参数动态关联。可以使用AOP提供的参数动态解析key中指定的参数名称,然后按照一定规则组合生成特定的字符串,由该字符串来获取或生成锁对象。这样就可以保证同一个业务方法可以被并发执行,只对可能冲突的参数同步执行。

Spring AOP+自定义注解实现函数并发重入控制_第1张图片

三、实现

自定义注解

注解包含value和type字段,value表示同步内容的键,以此来生成和获取锁,type表示键的类型,0为常量键,1为动态键。此处注解为方法级别,在程序运行时也应存在。注解只是一个标记作用,方便AOP扫描从而找到AOP的切入点。动态value的生成规则较为简单,起始位表示源方法中的参数位置(以0开始),如果该参数为对象,可以支持执行对象的方法,获取该方法的返回值再toString作为结果,支持多层级调用。如果只指定起始位,则直接使用源方法的参数进行toString生成结果。value可以指定多个内容,获取后使用下划线(“_”)进行连接生成字符串,如type为0时则默认使用value的第0为作为键,如value为空则默认使用方法名。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SyncLock {
    /**
     * 同步内容的键,会将多个值组合为一个字符串,支持动态获取方法参数
     * 语法:{index}[.{method}]
     * index为参数位置
     * method为对象中定义的方法名
     * eg: ["0.getType", "1.getDate.getTime", "2"]
     */
    String[] value();

    /**
     * 同步键的类型(0字符串 1动态参数)
     */
    int type() default 0;
}

AOP编程

AOP对指定的注解扫描,使用around包围方法,对方法的请求参数进行解析,使用注解value中定义的规则获取生成锁的键,然后使用该键进行加锁和释放锁处理。使用@Aspect标记类是AOP切面类,@Around标记方法会执行AOP环绕操作。在@Around方法回调中会传入ProceedingJoinPoint对象,由该对象可以解析出源返回的相关信息,此处我们主要关注注解内容和方法入参。使用method.getAnnotation获取指定的注解,从而得到value和type字段。联合使用Object、Class、Method可以解析得到value中规则指定的内容。

@Aspect
// 设置AOP执行顺序高优先级,在与其它切面共用时保证最先执行。
// 如在与@Transactional共用时,在事务提交后再释放锁。@Transactional默认order为Ordered.LOWEST_PRECEDENCE
@Order(1)
@Component
public class SyncLockAspect {
    private static final Logger logger = LoggerFactory.getLogger(SyncLockAspect.class);

    @Around(value="@annotation(com.trantour.common.async.SyncLock)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取方法
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = methodSignature.getMethod();
        //获取方法上的注解
        SyncLock syncLock = targetMethod.getAnnotation(SyncLock.class);
        //获取注解参数
        String[] values = syncLock.value();
        int type = syncLock.type();
        String key;
        if(values.length <= 0){
            //默认使用方法名
            key = targetMethod.getName();
        }else{
            if(type == 1){
                //动态获取方法参数
                key = getLockKey(values, joinPoint.getArgs(), targetMethod.getParameterTypes());
            }else{
                //固定常量键
                key = values[0];
            }
        }

        //同步
        Object result;
        if(key != null){
            boolean isLock = false;
            try{
                SyncLockUtil.tryLock(key, 1000);
                isLock = true;
                result = joinPoint.proceed(joinPoint.getArgs());
            } finally {
                if(isLock)
                    SyncLockUtil.unLock(key);
            }
        }else{
            logger.error("生成key失败,直接执行方法");
            result = joinPoint.proceed(joinPoint.getArgs());
        }
        return result;
    }

    private String getLockKey(String[] values, Object[] args, Class[] parameterTypes){
        if(values == null || args == null || parameterTypes == null
                || values.length <= 0 || args.length <= 0 || parameterTypes.length <= 0){
            return null;
        }
        StringBuilder keyStr = new StringBuilder();
        for (String argKey : values){
            //获取方法参数
            String argValue = getArgByKey(argKey, args, parameterTypes);
            if(argValue == null){
                return null;
            }
            //组合拼接成锁的键
            keyStr.append(argValue).append("_");
        }
        if(keyStr.length() <= 0){
            return null;
        }
        keyStr.deleteCharAt(keyStr.length()-1);
        return keyStr.toString();
    }

    private String getArgByKey(String argKey, Object[] args, Class[] parameterTypes){
        if(argKey == null || args == null || parameterTypes == null || args.length <= 0 || parameterTypes.length <= 0){
            return null;
        }
        try{
            Object object;
            //是否包含对象嵌套获取
            if(argKey.contains(".")){
                String[] strings = argKey.split("\\.");
                int index = Integer.parseInt(strings[0]);
                //获取拦截器的基础参数对象
                object = args[index];
                Class cls = parameterTypes[index];
                //循环获取参数对象
                for (int i = 1; i < strings.length; i++) {
                    if(object == null){
                        logger.info("上级对象值为空:{}", strings[i]);
                        break;
                    }
                    boolean isFind = false;
                    //获取已定义的方法
                    Method[] f = cls.getDeclaredMethods();
                    for(Method method : f){
                        //获取指定的方法
                        if(method.getName().equals(strings[i])){
                            cls = method.getReturnType();
                            //执行方法获取返回值
                            object = method.invoke(object);
                            isFind = true;
                            break;
                        }
                    }
                    if(!isFind){
                        logger.info("找不到方法:{}", strings[i]);
                        break;
                    }
                }
            }else{
                //直接获取参数值
                object = args[Integer.parseInt(argKey)];
            }
            if(object != null){
                return object.toString();
            }
        }catch (Exception e){
            logger.error("获取方法参数出错", e);
        }
        return null;
    }
}

ReentrantLock锁

使用最常见的ReentrantLock锁即可满足需求,配合线程安全的ConcurrentHashMap实现锁缓存保存同步键和锁的关系。通过key获取锁时先判断缓存是否存在,不存在则创建,使用缓存的锁进行tryLock加锁。在业务方法完成后使用key获取锁然后进行unLock释放锁,释放后使用getQueueLength判断锁是否被其它线程等待,如无线程等待则从缓存中移除锁,避免缓存累积。

public class SyncLockUtil {
    private static final Logger logger = LoggerFactory.getLogger(SyncLockUtil.class);
    private static Map lockMap = new ConcurrentHashMap<>();

    /**
     * 加锁
     * @param key 同步键,由锁住的内容生成
     * @param timeout 超时时间(毫秒)
     * @throws InterruptedException
     */
    public static void tryLock(String key, int timeout) throws Exception {
        //不存在则新建
        ReentrantLock lock = lockMap.computeIfAbsent(key, k -> new ReentrantLock());
        if(!lock.tryLock(timeout, TimeUnit.MILLISECONDS)){
            logger.info("加锁失败: {}", key);
            throw new Exception("获取锁资源失败");
        }else{
            logger.info("加锁: {}", key);
        }
    }

    /**
     * 释放锁
     * @param key 同步键,由锁住的内容生成
     */
    public static void unLock(String key){
        ReentrantLock lock = lockMap.get(key);
        if(lock == null){
            logger.error("找不到对应的锁,锁不存在或已被释放: {}", key);
            return;
        }
        //是否有线程在等待本锁
        int length = lock.getQueueLength();
        if(length <= 0){
            logger.info("释放锁: {}", key);
            lockMap.remove(key);
        }
        lock.unlock();
        logger.info("解锁: {}", key);
    }
}

四、使用

Maven

maven需要引入AOP的支持。


    org.springframework.boot
    spring-boot-starter-aop

代码拷贝

将SyncLockUtil、SyncLockAspect、SyncLock 拷贝到项目合适的位置。

使用实例1

在使用电话号码注册时避免多次点击或客户端重发导致的重复注册,只需在注册的方法上使用@SyncLock标记并指定通过电话号码加锁即可。

@SyncLock(value = ["0.getMail"], type = 1)
@Transactional(rollbackFor = [Exception::class])
fun register(user: User): RespBody {
    //todo
}

使用实例2

在三方账号绑定时避免多次点击或客户端重发导致的重复绑定,只需在绑定的方法上使用@SyncLock标记并指定通过平台类型和平台标识加锁即可。

@SyncLock(value = ["0.getOpenId", "0.getPlatformType"], type = 1)
@Transactional(rollbackFor = [Exception::class])
fun thirdBindingT(thirdPlatform: ThirdPlatform, userId: Int): Res {
    //todo
}

使用实例3

在生成群口令时为避免生成重复口令,只需在口令生成的方法上使用@SyncLock标记并指定通过方法标识加锁即可。

@SyncLock("getAccessCode")
fun getAccessCode(groupId: Int?): RespBody.Res {
    //todo
}

如不指定value则会默认使用方法名作为标识,故上述代码可以只用@SyncLock标记即可。

@SyncLock
fun getAccessCode(groupId: Int?): RespBody.Res {
    //todo
}

五、参考

  • Springmvc 使用Aspect 使用权限注解拦截

  • ReentrantLock 锁详解

  • Java多线程问题--方法getHoldCount()、getQueueLength()和getWaitQueueLength()的用法和区别

  • ConcurrentHashMap性能测试

你可能感兴趣的:(AOP,自定义注解,线程同步,接口请求,接口并发)