SpringBoot中通过Redis的setnx和自定义注解@Idempotent实现API幂等处理
作者:互联网
1.简述
- 目的:一定时间内,同样的请求(业务参数相同)访问同一个接口,则只能成功一次,其余被拒绝。
2.引入redis支持
因为需要通过redis
的setnx
确保只有一个接口能够正常访问,所以需要引入redis。
2.1.pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!-- 需要排除哪些包由具体项目觉得 -->
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
2.2.application.properties
spring.redis.host=11.22.33.44
spring.redis.port=26379
spring.redis.database=1
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=500
spring.redis.pool.min-idle=0
spring.redis.timeout=0
2.3.Redis JUnit Test Case
/**
* @author hanchao
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTemplateTest {
@Resource
private RedisTemplate<String,String > redisTemplate;
@Test
public void simpleTest() {
ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
String key = "RedisTemplateTest-simpleTest-001";
valueOperations.set(key,key+key);
System.out.println(valueOperations.get(key));
}
}
3.引入幂等
3.1.幂等异常
/**
* 用于专门处理幂等相关异常。
* @author hanchao
*/
public class IdempotentException extends RuntimeException {
public IdempotentException(String message) {
super(message);
}
@Override
public String getMessage() {
return super.getMessage();
}
}
3.2.幂等注解
/**
* 幂等注解
* @author wangchao
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
/**
* 幂等名称,作为redis缓存Key的一部分。
*/
String value();
/**
* 幂等过期时间,即:在此时间段内,对API进行幂等处理。
*/
long expireMillis();
}
3.3.幂等切面
/**
* 幂等切面
* @author wangchao
*/
@Aspect
@Component
@ConditionalOnClass(RedisTemplate.class)
public class IdempotentAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(IdempotentAspect.class);
/**
* redis缓存key的模板
*/
private static final String KEY_TEMPLATE = "idempotent_%s";
@Resource
private RedisTemplate<String,String> redisTemplate;
/**
* 根据实际路径进行调整
*/
@Pointcut("@annotation(pers.hanchao......anno.Idempotent)")
public void executeIdempotent() {
}
@Around("executeIdempotent()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//获取幂等注解
Idempotent idempotent = method.getAnnotation(Idempotent.class);
//根据 key前缀 + @Idempotent.value() + 方法签名 + 参数 构建缓存键值
//确保幂等处理的操作对象是:同样的 @Idempotent.value() + 方法签名 + 参数
String key = String.format(KEY_TEMPLATE, idempotent.value() + "_" + KeyUtil.generate(method, joinPoint.getArgs()));
//通过setnx确保只有一个接口能够正常访问
//调用KeyUtil工具类生成key
String redisRes = redisTemplate.execute((RedisCallback<String>) conn -> ((JedisCommands) conn.getNativeConnection()).set(key, key, "NX", "PX", idempotent.expireMillis()));
if (Objects.equals("OK", redisRes)) {
return joinPoint.proceed();
} else {
LOGGER.debug("Idempotent hits, key=" + key);
throw new IdempotentException("Idempotent hits, key=" + key);
}
}
}
3.4.工具类
/**
* Key生成工具
* @author hanchao
*/
public class KeyUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(KeyUtil.class);
/**
* 根据{方法名 + 参数列表}和md5转换生成key
*/
public static String generate(Method method, Object... args) {
StringBuilder sb = new StringBuilder(method.toString());
for (Object arg : args) {
sb.append(toString(arg));
}
return DigestUtils.md5Hex(sb.toString());
}
private static String toString(Object object) {
if (object == null) {
return "null";
}
if (object instanceof Number) {
return object.toString();
}
//调用json工具类转换成String
return JsonUtil.toJson(object);
}
}
/**
* Json格式化工具
* @author hanchao
*/
public class JsonUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtil.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
static {
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).setSerializationInclusion(Include.NON_NULL);
}
/**
* Java Object Maps To Json
*/
public static String toJson(Object obj) {
String result;
if (obj == null || obj instanceof String) {
return (String) obj;
}
try {
result = MAPPER.writeValueAsString(obj);
} catch (Exception e) {
LOGGER.error("Java Object Maps To Json Error !");
throw new RuntimeException("Java Object Maps To Json Error !", e);
}
return result;
}
}
4.对接口标记幂等注解
@RestController
public class DemoController {
@Resource
private DemoService demoService;
/**
* @Idempotent的value值随意,一般保持与接口url一致接口。
*/
@Idempotent(value = "/cock/alarm", expireMillis = 1000L)
@PostMapping(value = "/cock/alarm")
public String demo(@RequestBody DemoPo po) {
//..
}
}
标签:SpringBoot,自定义,Idempotent,redis,class,key,public,String 来源: https://blog.csdn.net/hanchao5272/article/details/92073405