自定义注解实现Redis分布式锁、手动控制事务和根据异常名字或内容限流的三合一的功能

自定义注解实现Redis分布式锁、手动控制事务和根据异常名字或内容限流的三合一的功能

1.依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
     <version>2.3.9.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.4</version>
</dependency>

2.Redisson配置

2.1单机模式配置

spring:
  redis:
    host: localhost
    port: 6379
    password: null
redisson:
  codec: org.redisson.codec.JsonJacksonCodec
  threads: 4
  netty:
    threads: 4
  single-server-config:
    address: "redis://localhost:6379"
    password: null

2.2主从模式

spring:
  redis:
    sentinel:
      master: my-master
      nodes: localhost:26379,localhost:26389
    password: your_password
redisson:
  master-slave-config:
    master-address: "redis://localhost:6379"
    slave-addresses: "redis://localhost:6380,redis://localhost:6381"
    password: ${spring.redis.password}

2.3集群模式

spring:
  redis:
    cluster:
      nodes: localhost:6379,localhost:6380,localhost:6381,localhost:6382,localhost:6383,localhost:6384
    password: your_password
redisson:
  cluster-config:
    node-addresses: "redis://localhost:6379,redis://localhost:6380,redis://localhost:6381,redis://localhost:6382,redis://localhost:6383,redis://localhost:6384"
    password: ${spring.redis.password}

2.4哨兵模式

spring:
  redis:
    sentinel:
      master: my-master
      nodes: localhost:26379,localhost:26389
    password: your_password
redisson:
  sentinel-config:
    master-name: my-master
    sentinel-addresses: "redis://localhost:26379,redis://localhost:26380,redis://localhost:26381"
    password: ${spring.redis.password}

Redission的集成还有很多种方式的,所以不局限于这种方式,条条大路通罗马,一万个读者就有一万个哈姆雷特。

3.实现

3.1 RedisConfig

package xxxxx.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 定义泛型为 <String, Object> 的 RedisTemplate
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        // 设置连接工厂
        template.setConnectionFactory(factory);
        // 定义 Json 序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // Json 转换工具
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        //方法二:解决jackson2无法反序列化LocalDateTime的问题
        om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        om.registerModule(new JavaTimeModule());
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 定义 String 序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    RedisTemplate<String, Long> redisTemplateLimit(RedisConnectionFactory factory) {
        final RedisTemplate<String, Long> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericToStringSerializer<>(Long.class));
        template.setValueSerializer(new GenericToStringSerializer<>(Long.class));
        return template;
    }

}

3.2 自定义注解IdempotentManualCtrlTransLimiterAnno

package xxxxx.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
 * @author zlf
 */
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IdempotentManualCtrlTransLimiterAnno {


    /**
     * 是否开启RedissonLock
     * 1:开启
     * 0:不开启
     *
     * @return
     */
    boolean isOpenRedissonLock() default false;

    /**
     * 是否开启手动控制事务提交
     * 1:开启
     * 0:不开启
     *
     * @return
     */
    boolean isOpenManualCtrlTrans() default false;

    /**
     * 分布式锁key格式:
     * keyFormat a:b:%s
     *
     * @return
     */
    String keyFormat() default "";

    /**
     * 锁定时间
     * 默认 3s
     *
     * @return
     */
    long lockTime() default 3l;

    /**
     * 锁定时间单位
     * TimeUnit.MILLISECONDS 毫秒
     * TimeUnit.SECONDS 秒
     * TimeUnit.MINUTES 分
     * TimeUnit.HOURS 小时
     * TimeUnit.DAYS 天
     *
     * @return
     */
    TimeUnit lockTimeUnit() default TimeUnit.SECONDS;

    /**
     * 是否开启限流
     *
     * @return
     */
    boolean isOpenLimit() default false;

    /**
     * 限流redis失败次数统计key
     * public方法第一个string参数就是%s
     *
     * @return
     */
    String limitRedisKeyPrefix() default "limit:redis:%s";

    /**
     * 限流redisKey统计的key的过期时间
     * 默认10分钟后过期
     *
     * @return
     */
    long limitRedisKeyExpireTime() default 10l;

    /**
     * 锁过期单位
     * TimeUnit.MILLISECONDS 毫秒
     * TimeUnit.SECONDS 秒
     * TimeUnit.MINUTES 分
     * TimeUnit.HOURS 小时
     * TimeUnit.DAYS 天
     *
     * @return
     */
    TimeUnit limitRedisKeyTimeUnit() default TimeUnit.MINUTES;

    /**
     * 默认限流策略:
     * 异常计数器限流:可以根据异常名称和异常内容来计数限制
     * 根据异常次数,当异常次数达到多少次后,限制访问(异常类型为RuntimeException类型) (实现)
     * RedisTemplate的配置文件中需要有这个类型的bean
     *
     * @return
     * @Bean RedisTemplate<String, Long> redisTemplateLimit(RedisConnectionFactory factory) {
     * final RedisTemplate<String, Long> template = new RedisTemplate<>();
     * template.setConnectionFactory(factory);
     * template.setKeySerializer(new StringRedisSerializer());
     * template.setHashValueSerializer(new GenericToStringSerializer<>(Long.class));
     * template.setValueSerializer(new GenericToStringSerializer<>(Long.class));
     * return template;
     * }
     * 滑动窗口限流 (未实现)
     * 令牌桶 (未实现)
     * ip限流 (未实现)
     * Redisson方式限流 (未实现)
     * <p>
     * limitTye() 异常计数器限流 可以写Exception的子类
     * 这个和下面的expContent()互斥,二选一配置即可
     */
    String limitTye() default "";

    /**
     * 异常信息内容匹配统计
     *
     * @return
     */
    String expContent() default "";

    /**
     * 异常统计次数上线默认为10次
     *
     * @return
     */
    int limitMaxErrorCount() default 10;

}

3.3自定义切面IdempotentManualCtrlTransAspect

package xxxxxx.annotation;

import cn.hutool.core.lang.Tuple;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
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.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;


@Slf4j
@Aspect
@Component
public class IdempotentManualCtrlTransAspect {

    @Autowired
    private TransactionDefinition transactionDefinition;

    @Autowired
    private DataSourceTransactionManager transactionManager;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RedisTemplate<String, Long> redisTemplateLimit;

    private static final List<String> KEY_FORMAT_MATCHS = new ArrayList<>();

    static {
        KEY_FORMAT_MATCHS.add("%s");
    }

    @Pointcut("@annotation(com.dy.member.annotation.IdempotentManualCtrlTransLimiterAnno)")
    public void idempotentManualCtrlTransPoint() {

    }

    @Around("idempotentManualCtrlTransPoint()")
    public Object deal(ProceedingJoinPoint pjp) throws Throwable {
        //当前线程名
        String threadName = Thread.currentThread().getName();
        log.info("-------------IdempotentManualCtrlTransLimiterAnno开始执行-----线程{}-----------", threadName);
        //获取参数列表
        Object[] objs = pjp.getArgs();
        String key = null;
        String redisLimitKey = null;
        String message = "";
        IdempotentManualCtrlTransLimiterAnno annotation = null;
        try {
            //注解加上的public方法的第一个参数就是key,只支持改参数为String类型
            key = (String) objs[0];
            if (Objects.isNull(key)) {
                return pjp.proceed();
            }
            //获取该注解的实例对象
            annotation = ((MethodSignature) pjp.getSignature()).
                    getMethod().getAnnotation(IdempotentManualCtrlTransLimiterAnno.class);
            //是否开启RedissonLock
            boolean openRedissonLock = annotation.isOpenRedissonLock();
            boolean openManualCtrlTrans = annotation.isOpenManualCtrlTrans();
            boolean openLimit = annotation.isOpenLimit();
            boolean bothFlag = openRedissonLock && openManualCtrlTrans;
            if (openLimit) {
                int errorCount = annotation.limitMaxErrorCount();
                String limitRedisKey = annotation.limitRedisKeyPrefix();
                redisLimitKey = String.format(limitRedisKey, key);
                TimeUnit timeUnit = annotation.limitRedisKeyTimeUnit();
                this.checkFailCount(redisLimitKey, errorCount, timeUnit);
                if (!openRedissonLock && !openManualCtrlTrans) {
                    return pjp.proceed();
                }
            }
            if (!bothFlag) {
                if (openRedissonLock) {
                    key = checkKeyFormatMatch(annotation, key);
                    RLock lock = redissonClient.getLock(key);
                    try {
                        Tuple lockAnnoParamsTuple = this.getLockAnnoParams(annotation);
                        long t = lockAnnoParamsTuple.get(0);
                        TimeUnit uint = lockAnnoParamsTuple.get(1);
                        if (lock.tryLock(t, uint)) {
                            return pjp.proceed();
                        }
                    } catch (Exception e) {
                        log.error("-------------IdempotentManualCtrlTransLimiterAnno锁异常ex:{}-----线程{}-----------", ExceptionUtils.getMessage(e), threadName);
                        throw new RuntimeException(ExceptionUtils.getMessage(e));
                    } finally {
                        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                            lock.unlock();
                            log.info("-------------IdempotentManualCtrlTransLimiterAnno释放锁成功-----线程{}-----------", threadName);
                        }
                    }
                }
                if (openManualCtrlTrans) {
                    TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
                    try {
                        Object proceed = pjp.proceed();
                        transactionManager.commit(transactionStatus);
                        return proceed;
                    } catch (Exception e) {
                        transactionManager.rollback(transactionStatus);
                        log.info("-------------IdempotentManualCtrlTransLimiterAnno执行异常事务回滚1-----线程{}-----------", threadName);
                        throw new RuntimeException(ExceptionUtils.getMessage(e));
                    }
                }
            }
            if (bothFlag) {
                key = checkKeyFormatMatch(annotation, key);
                RLock lock = redissonClient.getLock(key);
                TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
                try {
                    Tuple lockAnnoParamsTuple = this.getLockAnnoParams(annotation);
                    long t = lockAnnoParamsTuple.get(0);
                    TimeUnit uint = lockAnnoParamsTuple.get(1);
                    if (lock.tryLock(t, uint)) {
                        Object proceed = pjp.proceed();
                        transactionManager.commit(transactionStatus);
                        return proceed;
                    }
                } catch (Exception e) {
                    log.error("-------------IdempotentManualCtrlTransLimiterAnno处理异常ex:{}-----线程{}-----------", ExceptionUtils.getMessage(e), threadName);
                    transactionManager.rollback(transactionStatus);
                    log.info("-------------IdempotentManualCtrlTransLimiterAnno执行异常事务回滚2-----线程{}-----------", threadName);
                    throw new RuntimeException(ExceptionUtils.getMessage(e));
                } finally {
                    if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                        lock.unlock();
                        log.info("-------------IdempotentManualCtrlTransLimiterAnno释放锁成功2-----线程{}-----------", threadName);
                    }
                }
            }
        } catch (Exception e) {
            message = ExceptionUtils.getMessage(e);
            String stackTrace = ExceptionUtils.getStackTrace(e);
            String limitExType = annotation.limitTye();
            log.error("------------IdempotentManualCtrlTransLimiterAnno-------msg:{},stackTrace:{},limitExType:{}-------", message, stackTrace, limitExType);
            boolean openLimit = annotation.isOpenLimit();
            TimeUnit timeUnit = annotation.limitRedisKeyTimeUnit();
            long limitRedisKeyExpireTime = annotation.limitRedisKeyExpireTime();
            String expContent = annotation.expContent();
            if (openLimit) {
                if (StringUtils.isNotBlank(message) && StringUtils.isNotBlank(expContent) && message.indexOf(expContent) != -1) {
                    log.error("------------IdempotentManualCtrlTransLimiterAnno-------openLimit:{},message:{},expContent:{}-------", openLimit, message, expContent);
                    if (!redisTemplateLimit.hasKey(redisLimitKey)) {
                        redisTemplateLimit.opsForValue().set(redisLimitKey, 1L, limitRedisKeyExpireTime, timeUnit);
                    } else {
                        redisTemplateLimit.opsForValue().increment(redisLimitKey);
                    }
                } else if (stackTrace.indexOf(limitExType) != -1 && StringUtils.isBlank(expContent)) {
                    log.error("------------IdempotentManualCtrlTransLimiterAnno-------openLimit:{},stackTrace:{},expContent:{}-------", openLimit, stackTrace, limitExType);
                    if (!redisTemplateLimit.hasKey(redisLimitKey)) {
                        redisTemplateLimit.opsForValue().set(redisLimitKey, 1L, limitRedisKeyExpireTime, timeUnit);
                    } else {
                        redisTemplateLimit.opsForValue().increment(redisLimitKey);
                    }
                } else {
                    if (!redisTemplateLimit.hasKey(redisLimitKey)) {
                        redisTemplateLimit.opsForValue().set(redisLimitKey, 1L, limitRedisKeyExpireTime, timeUnit);
                    } else {
                        redisTemplateLimit.opsForValue().increment(redisLimitKey);
                    }
                }
            }
            log.error("-------------IdempotentManualCtrlTransLimiterAnno开始异常ex:{}-----线程{}-----------", ExceptionUtils.getMessage(e), threadName);
        }
        throw new RuntimeException(message.replaceAll("RuntimeException", "").replaceAll("Exception", "").replaceAll(":", "").replaceAll(" ", ""));
    }

    private void checkFailCount(String key, long errorCount, TimeUnit timeUnit) {
        boolean isExistKey = redisTemplateLimit.hasKey(key);
        if (isExistKey) {
            Long count = (Long) redisTemplateLimit.opsForValue().get(key);
            log.info("=========IdempotentManualCtrlTransLimiterAnno=====key:{}=======failCount:{}=========", key, count);
            if (Objects.nonNull(count) && count > errorCount) {
                Long expire = redisTemplateLimit.getExpire(key, timeUnit);
                String unitStr = "";
                if (timeUnit.equals(TimeUnit.DAYS)) {
                    unitStr = "天";
                } else if (timeUnit.equals(TimeUnit.HOURS)) {
                    unitStr = "小时";
                } else if (timeUnit.equals(TimeUnit.MINUTES)) {
                    unitStr = "分钟";
                } else if (timeUnit.equals(TimeUnit.SECONDS)) {
                    unitStr = "秒钟";
                } else if (timeUnit.equals(TimeUnit.MILLISECONDS)) {
                    unitStr = "毫秒";
                }
                log.error("IdempotentManualCtrlTransLimiterAnno异常次数限制,错误次数:{}", errorCount);
                throw new RuntimeException("请求异常,请" + expire + unitStr + "后重试!");
            }
        }
    }

    private Tuple getLockAnnoParams(IdempotentManualCtrlTransLimiterAnno annotation) {
        long t = annotation.lockTime();
        TimeUnit unit = annotation.lockTimeUnit();
        return new Tuple(t, unit);
    }

    private String checkKeyFormatMatch(IdempotentManualCtrlTransLimiterAnno annotation, String key) {
        String keyFormat = annotation.keyFormat();
        if (StringUtils.isNotBlank(keyFormat)) {
            if (!KEY_FORMAT_MATCHS.contains(keyFormat)) {
                throw new RuntimeException("注解key格式匹配有误!");
            }
            key = String.format(keyFormat, key);
        }
        return key;
    }

}

4.测试验证

在controller层新建TestController1

package com.xxxx.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@RequestMapping("/test")
public class TestController1 {

    @Autowired
    private xxxxRecordService xxxRecordService;

    @PostMapping("/hellWorld")
    @IdempotentManualCtrlTransLimiterAnno(isOpenRedissonLock = true, isOpenManualCtrlTrans = true, isOpenLimit = true, limitRedisKeyExpireTime = 10, expContent = "我是异常")
    public RestResponse hellWorld(@RequestParam String key) {
        xxxRecord pm = new xxxRecord();
        pm.setOrderNo(key);
        xxxRecordService.save(pm);
        if (key.equals("2")) {
            //int i = 10;
            //i = i / 0;
            throw new RuntimeException("我是异常");
        }
        return RestResponse.success(key);
    }
}

使用postMan请求接口,在自定定义的aspect的类中的deal方法内打上断点就可以调试了:

请添加图片描述

使用这个方法才可以调试,如果使用的是springBoot的单元测试,不会进断点,这个也是奇怪的很,切面表达式可以切一个注解,也可以切指定包下的某些方法,比如可以切所有controller下的xx类的xx方法的xxx参数,这个可以参考网上的教程实现,也可以把SPEL表达式的解析实现进去,key的解析通过SPEL表达式解析配偶到第一个参数中对应的String参数或者是Json参数匹配的字段上,这种灵活性就好一点了,本文的这个约定的也是好使的,一点也不影响,复杂度没有那么高,本文约定的是公共方法的第一个String的参数为key,可以读上面的代码实现,都有注释说明的。

5.总结

在日常开发当中,进场会遇到这三个场景,所以需要写一些重复性的代码,用一次写一次,CV哥就是这样养成的,所以经过我的思考,突发了这个灵感,搞个注解全部搞定,这个多方便,要那个功能开哪个功能,而且还可以组合使用,优雅永不过时,本次分享就到这里,希望我的分享对你有所帮助,请一键三连,么么么哒!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值