自定义注解基于redis实现频控限流、分布式id、分布式锁

最近整理下一些常用技术的自定义注解实现方式,具体关于自定义注解的概念可以参考如下链接

自定义注解介绍

1. 频控注解@AccessFrequencyAspect

1.1 注解介绍

        该注解主要通过aop切面配合redis实现,接口/方法访问的频率限制,可以根据IP和用户id等信息进行限制。

1.2 注解
package com.middlewares.common.redis.annotation;

import com.middlewares.common.core.enums.LimitType;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * 接口访问频次注解
 *
 * @author shawn
 * @date 2024年 03月 26日 9:02 09:02:40
 */
@Target({ ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessFrequency {

    String prefix() default "middlewares:middlewares-common:middlewares-common-redis:";

    /**
     * 限制模式 用户名限制 IP限制 用户名IP组合限制
     * @return {@link LimitType}
     */
    LimitType LimitType() default LimitType.USER;

    /**
     * 访问频率 频次/时间
     * @return {@link String}
     */
    String frequent() default "60/1";

    /**
     * 时间单位
     * @return {@link TimeUnit}
     */
    TimeUnit timeUnit() default TimeUnit.MINUTES;
}
1.3 注解切面
package com.middlewares.common.redis.aspect;

import com.middlewares.common.redis.constant.ExecuteOrder;
import com.middlewares.common.core.constant.SecurityConstants;
import com.middlewares.common.core.exception.AccessFrequencyException;
import com.middlewares.common.core.utils.ServletUtils;
import com.middlewares.common.core.utils.ip.IpUtils;
import com.middlewares.common.redis.annotation.AccessFrequency;
import com.middlewares.common.redis.service.RedisService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Objects;

/**
 * @author shawn
 * @date 2024年 03月 26日 9:59 09:59:24
 */
@Aspect
@Component
public class AccessFrequencyAspect implements Ordered{

    private final Logger log = LoggerFactory.getLogger(AccessFrequencyAspect.class);
    @Resource
    private RedisService redisService;

    public AccessFrequencyAspect() {
    }

    /**
     * within:within关键字用来匹配指定包或类下的所有方法。例如,within(com.middlewares.service.*)表示匹配com.middlewares.service包下的所有方法。
     * @target注解用于匹配目标对象的类是否具有指定的注解。例如,@target(com.middlewares.annotation.AccessFrequency)表示匹配目标对象的类是否标注了@AccessFrequency注解。
     * @annotation注解用于匹配目标方法是否具有指定的注解。例如,@annotation(com.middlewares.annotation.AccessFrequency)表示匹配目标对象的方法是否标注了@AccessFrequency注解。
     * execution:execution关键字用于匹配方法的执行。可以指定方法的访问修饰符、返回类型、方法名、参数等信息。例如,execution(* com.middlewares.service..(..))表示匹配com.middlewares.service包下的所有方法。
     * bean:bean关键字用于匹配Spring Bean的名称。例如,bean(myService)表示匹配名称为myService的Spring Bean。
     * */
    @Pointcut("@within(com.middlewares.common.redis.annotation.AccessFrequency) || @annotation(com.middlewares.common.redis.annotation.AccessFrequency)")
    public void pointcut()
    {
    }


    @Around("pointcut()")
    public Object AccessFrequency(ProceedingJoinPoint point) throws Throwable {
        AccessFrequency accessFrequency = null;
        if (point.getTarget().getClass().isAnnotationPresent(AccessFrequency.class)) {
            /* 首先查看类上有无注解*/
            accessFrequency = point.getTarget().getClass().getAnnotation(AccessFrequency.class);
        } else {
            /* 获取方法上注解 */
            MethodSignature signature = (MethodSignature) point.getSignature();
            accessFrequency = signature.getMethod().getAnnotation(AccessFrequency.class);
        }
        if (Objects.isNull(accessFrequency)){
            return point.proceed();
        }
        /* 0. 校验获取频率信息*/
        String frequent = accessFrequency.frequent();
        if (!frequent.contains("/")) {
            log.error("@AccessFrequency frequent format exception , lose '/' to split string , get " + frequent);
            return point.proceed();
        }
        String[] split = frequent.split("/");
        if (split.length != 2 || !isNumericArray(split)) {
            log.error("@AccessFrequency frequent format exception , expect a number split by '/', but got " + frequent);
            return point.proceed();
        }
        int frequency = Integer.parseInt(split[0]);
        int time = Integer.parseInt(split[1]);
        String redisKey = "";
        /* 1. 获取频率限制模式*/
        switch (accessFrequency.LimitType()) {
            case IP:
                /*1.1 IP限制逻辑*/
            {
                redisKey = accessFrequency.prefix() + IpUtils.getIpAddr();
            }
            break;
            case USER:
                /*1.2 用户限制逻辑*/
                String username = Objects.requireNonNull(ServletUtils.getRequest()).getHeader(SecurityConstants.DETAILS_USERNAME);
                redisKey = accessFrequency.prefix() + username;
                break;
            case USER_IP:
                // 用户IP限制逻辑
                redisKey = accessFrequency.prefix() + IpUtils.getIpAddr() + "-" + Objects.requireNonNull(ServletUtils.getRequest()).getHeader(SecurityConstants.DETAILS_USERNAME);
                break;
            default:
                // 默认逻辑
                log.error("unKnow access limit type " + accessFrequency.LimitType());
                return point.proceed();
        }
        /* 2. 获取请求次数*/
        Long count = redisService.increment(redisKey);
        assert count != null : "Count should not be null";
        if (count == 1) {
            /* 失效时间内,首次访问,设定失效时间*/
            redisService.expire(redisKey, time, accessFrequency.timeUnit());
        }
        /* 3. 比较访问次数,超次拒绝*/
        if (count > frequency) {
            throw new AccessFrequencyException("您的访问频次过高,已阻止本次访问!");
        }
        return point.proceed();
    }

    private static boolean isNumericArray(String[] array) {
        for (String str : array) {
            if (!isNumeric(str)) {
                return false;
            }
        }
        return true;
    }

    private static boolean isNumeric(String str) {
        if (str == null || str.isEmpty()) {
            return false;
        }
        for (char c : str.toCharArray()) {
            if (!Character.isDigit(c)) {
                return false;
            }
        }
        return true;
    }

    @Override
    public int getOrder() {
        return ExecuteOrder.getOrderByClass(this.getClass());
    }
}

2. 分布式id注解@RedisId

2.1 注解介绍

        该注解主要通过aop切面配合redis实现在多服务实例下,系统可以共享一套id。

2.2 注解
package com.middlewares.common.redis.annotation;

import java.lang.annotation.*;

/**
 * redis分布式id注解
 * @author shawn
 * @date 2024年 04月 09日 9:53 09:53:16
 */
@Target({ ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisId {
    /**
     * id名称
     * @return {@link String}
     */
    String key();

    /**
     * id初始值
     * @return long
     */
    long initValue() default 1L;

}
2.3 注解切面
package com.middlewares.common.redis.aspect;

import com.middlewares.common.redis.annotation.RedisId;
import com.middlewares.common.redis.constant.ExecuteOrder;
import com.middlewares.common.redis.service.RedisService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * @author shawn
 * @date 2024年 04月 09日 9:58 09:58:07
 */
@Aspect
@Component
public class RedisIdAspect implements Ordered {
    private final Logger log = LoggerFactory.getLogger(RedisIdAspect.class);
    @Resource
    private RedisService redisService;

    public RedisIdAspect() {
    }

    // 匹配Controller层的所有方法
    @Before("execution(* com.middlewares.*.controller.*.*(..)) && @annotation(org.springframework.web.bind.annotation.PostMapping)")
    public void checkRedisIdAnnotation(JoinPoint joinPoint) throws IllegalArgumentException {
        // 遍历所有参数
        for (Object arg : joinPoint.getArgs()) {
            // 如果参数为null,跳过
            if (arg == null) {
                continue;
            }
            fillIdValue(arg);
        }
    }

    /**
     * 装填id属性
     *
     * @param arg
     */
    private void fillIdValue(Object arg) {
        // 获取参数的所有字段
        Field[] fields = arg.getClass().getDeclaredFields();
        for (Field field : fields) {
            // 设置私有属性可访问
            field.setAccessible(true);
            // 检查字段是否被@RedisId标注
            try {
                if (field.isAnnotationPresent(RedisId.class)) {
                    // 执行你的逻辑,例如打印日志、验证字段值等
                    RedisId redisId = field.getAnnotation(RedisId.class);
                    Long id = redisService.getId(redisId.key());
                    if (field.getType().equals(Long.class) || field.getType().equals(long.class)) {
                        /* 为空则填充*/
                        Object origin = field.get(arg);
                        if (Objects.isNull(origin)) {
                            field.set(arg, id);
                        }
                    } else {
                        // 如果字段类型不是Long或long,打印错误或抛出异常
                        log.error("Field " + field.getName() + " is not of type Long or long.");
                    }
                } else if (isClass(field.getType())) {
                    // 如果字段不是基本类型或字符串,尝试递归填充
                    Object child = field.get(arg);
                    if (child != null) {
                        fillIdValue(child); // 递归调用
                    } else {
                        // 如果字段是null,尝试创建实例并填充
                        Class<?> fieldType = field.getType();
                        Object newInstance = fieldType.newInstance();
                        field.set(arg, newInstance);
                        fillIdValue(newInstance); // 递归调用
                    }
                }
            } catch (IllegalAccessException | InstantiationException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 判断属性是否为一般类
     *
     * @param clazz
     * @return boolean
     */
    private boolean isClass(Class<?> clazz) {
        /*0.提出基础类型,枚举类*/
        if (clazz.isPrimitive() || clazz.isEnum()) {
            return false;
        }
        /*1.判断包装类及特殊类*/
        List<Class<?>> classList = Arrays.asList(
                Integer.class, Byte.class, Short.class, Boolean.class, Long.class, Float.class, Double.class,
                String.class, Enum.class, Class.class, Constructor.class);
        for (Class<?> aClass : classList) {
            if (aClass.equals(clazz)) {
                return false;
            }
        }
        log.info("未过滤类:{}", clazz.getName());
        return true;
    }

    @Override
    public int getOrder() {
        return ExecuteOrder.getOrderByClass(this.getClass());
    }
}

3. 分布式锁注解@RedisLock

3.1 注解介绍

该注解主要通过aop切面配合redis实现在多服务实例并发下,可以实现共享资源的安全访问。该分布式实现两种获取锁策略①获取不到直接放弃执行 ②获取不到在指定获取锁超时时间内重复获取,同时支持锁的自动续约,保证在业务执行完毕前,不丢失锁,从而保证资源的安全访问。

3.2 注解
package com.middlewares.common.redis.annotation;

import com.middlewares.common.core.utils.DateUtils;
import com.middlewares.common.redis.constant.RedisConstant;

import java.lang.annotation.*;

/**
 * 方法级分布式锁
 * @author shawn
 * @date 2024年 04月 10日 20:02 20:02:41
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
    /**
     * 锁名称
     * @return {@link String}
     */
    String lockName() default "";

    /**
     * 锁失效时间,默认12秒
     * @return long
     */
    long expireTime() default 12000L;

    /**
     * 默认锁自动续约
     * @return boolean
     */
    boolean whetherRenewal() default true;
    /**
     * 默认拒绝策略
     *
     * @return {@link RedisConstant.ExecutionStrategy}
     */
    RedisConstant.ExecutionStrategy strategy() default RedisConstant.ExecutionStrategy.REFUSE;

    /**
     * 重试超时时间,默认20秒
     * @return long
     */
    long retryTimeOut() default 20000L;
}
3.3 注解切面
package com.middlewares.common.redis.aspect;

import com.middlewares.common.core.exception.RedisLockException;
import com.middlewares.common.core.utils.StringUtils;
import com.middlewares.common.redis.annotation.RedisLock;
import com.middlewares.common.redis.constant.ExecuteOrder;
import com.middlewares.common.redis.constant.RedisConstant;
import com.middlewares.common.redis.domain.LockRenewalTask;
import com.middlewares.common.redis.service.RedisService;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Objects;
import java.util.UUID;

/**
 * redis分布式锁注解
 * @author shawn
 * @date 2024年 04月 10日 20:30 20:30:49
 */
@Aspect
@Component
public class RedisLockAspect implements Ordered {

    private final static Logger log = LoggerFactory.getLogger(RedisLockAspect.class);
    @Resource
    private RedisService redisService;

    @Resource
    public RedisTemplate<String,String> redisTemplate;

    @Around("@annotation(redisLock)")
    public Object RedisLock(ProceedingJoinPoint pjp, RedisLock redisLock){
        /*1. 获取注解信息,校验参数*/
        String lockName = redisLock.lockName();
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        String methodName = signature.getMethod().getName();
        if (StringUtils.isEmpty(lockName)){
            /* 默认锁名为类全路径+方法名*/
            lockName = pjp.getTarget().getClass().getName()+"."+methodName;
        }

        Object proceed = null;
        com.middlewares.common.redis.domain.RedisLock lock = null;
        String uuid = UUID.randomUUID().toString();
        /*2. 判断策略*/
        if (RedisConstant.ExecutionStrategy.REFUSE.equals(redisLock.strategy())){
           /*2.1 拒绝策略*/
            if (!redisService.lock(lockName,uuid,redisLock.expireTime())) {
                throw new RedisLockException("当前系统正忙,请稍后再试!");
            }
        }else {
          /*2.2 重试策略*/
             lock = redisService.getLock(lockName, redisLock.expireTime(), redisLock.retryTimeOut());
            if (Objects.isNull(lock)){
                throw new RedisLockException("当前系统正忙,请稍后再试!");
            }
        }

        if (Objects.nonNull(lock)){
             uuid = lock.getValue();
        }
        log.info("成功获取分布式锁:【{}】",lockName);
        /*3.锁续期*/
        LockRenewalTask lockRenewalTask = null;
        if (redisLock.whetherRenewal()){
            lockRenewalTask = new LockRenewalTask(redisTemplate, redisService.getKey(lockName), uuid, redisLock.expireTime());
            Thread thread = new Thread(lockRenewalTask);
            thread.start();
        }
        /*4. 获取锁后执行业务*/
        try {
            proceed = pjp.proceed();
        } catch (Throwable e) {
            releaseResources(lockName, lock, lockRenewalTask);
            log.error(pjp.getClass().getName()+"-"+methodName+"发生未知异常。");
            throw new RedisLockException("当前系统正忙,请稍后再试!");
        } finally {
            /*5.释放资源*/
            releaseResources(lockName, lock, lockRenewalTask);
        }
        /*6. 返回结果*/
        return proceed;
    }

    private void releaseResources(String lockName, com.middlewares.common.redis.domain.RedisLock lock, LockRenewalTask lockRenewalTask) {
        /*1. 业务执行完毕,释放锁*/
        if (Objects.isNull(lock)){
            redisService.unlock(lockName);
        }else {
            redisService.releaseLock(lock);
        }
        log.info("成功释放分布式锁:【{}】",lockName);
        /*2. 关闭锁续期*/
        if (Objects.nonNull(lockRenewalTask)){
            lockRenewalTask.stop();
        }
    }

    @Override
    public int getOrder() {
        return ExecuteOrder.getOrderByClass(this.getClass());
    }
}
3.4 jmeter测试结果,基本符合预期

 


项目源码:https://gitee.com/XX-Space/middlewares-v2.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值