自定义注解实现Redis分布式锁、手动控制事务和根据异常名字或内容限流的三合一的功能
文章目录
- @[toc]
- 1.依赖
- 2.Redisson配置
- 2.1单机模式配置
- 2.2主从模式
- 2.3集群模式
- 2.4哨兵模式
- 3.实现
- 3.1 RedisConfig
- 3.2 自定义注解IdempotentManualCtrlTransLimiterAnno
- 3.3自定义切面IdempotentManualCtrlTransAspect
- 4.测试验证
- 5.总结
文章目录
- @[toc]
- 1.依赖
- 2.Redisson配置
- 2.1单机模式配置
- 2.2主从模式
- 2.3集群模式
- 2.4哨兵模式
- 3.实现
- 3.1 RedisConfig
- 3.2 自定义注解IdempotentManualCtrlTransLimiterAnno
- 3.3自定义切面IdempotentManualCtrlTransAspect
- 4.测试验证
- 5.总结
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哥就是这样养成的,所以经过我的思考,突发了这个灵感,搞个注解全部搞定,这个多方便,要那个功能开哪个功能,而且还可以组合使用,优雅永不过时,本次分享就到这里,希望我的分享对你有所帮助,请一键三连,么么么哒!