spring boot 实现幂等性
背景
在分布式服务中,业务在高并发的情况下会消费者出现多次请求的情况.这个时候如果执行插入的业务操作,则数据库中出现多条数据.造成脏数据的产生.而且也是对资源的浪费.此时我们需要做的就是阻止多页业务的处理操作.
执行方案
实现接口的幂等性,让请求只成功一次.这里需要保存一个唯一标示,在下一个请求执行时获取标示如果重复提交则阻止执行.
代码实现
创建一个自定义异常,阻止接口的下一步执行
/**
* 自定义异常类
*/
public class IdempotentException extends RuntimeException{
private static final long serialVersionUID = 1L;
public IdempotentException(String message){
super(message);
}
@Override
public String getMessage() {
return super.getMessage();
}
}
生成key值工具类
import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import com.alibaba.fastjson.JSON;
/**
* IdempotentKeyUtil
* 生成key值工具类
*/
public class IdempotentKeyUtil {
/**
* 对接口的参数进行处理生成固定key
* @param method
* @param args
* @return
*/
public static String generate(Method method,Object... args) {
StringBuilder stringBuilder = new StringBuilder(method.toString());
for (Object arg : args) {
stringBuilder.append(toStrinhg(arg));
}
//进行md5等长加密
return md5(stringBuilder.toString());
}
/**
* 使用jsonObject对数据进行toString,(保持数据一致性)
* @param object
* @return
*/
public static String toStrinhg(Object obj){
if( obj == null ){
return "-";
}
return JSON.toJSONString(obj);
}
/**
* 对数据进行MD5等长加密
* @param str
* @return
*/
public static String md5(String str){
StringBuilder stringBuilder = new StringBuilder();
try {
//选择MD5作为加密方式
MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(str.getBytes());
byte b[] = mDigest.digest();
int j = 0;
for (int i = 0,max = b.length; i < max; i++) {
j = b[i];
if(j < 0 ){
i += 256;
}else if(j < 16){
stringBuilder.append(0);
}
stringBuilder.append(Integer.toHexString(j));
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return stringBuilder.toString();
}
}
自定义注解
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;
/**
* 自定义注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent{
//注解自定义redis的key的一部分
String key();
//过期时间
long expirMillis();
}
AOP对我们自定义注解进行拦截处理
import java.lang.reflect.Method;
import java.util.Objects;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisCommands;
/**
* 自定义切点
*/
@Component
@Aspect
@ConditionalOnClass(RedisTemplate.class)
public class IdempotentAspect{
private static final String KEY_TEMPLATE = "idempotent_%S";
@Autowired
private RedisTemplate<String,String> redisTemplate;
/**
* 切点(自定义注解)
*/
@Pointcut("@annotation(com.idempotent.core.Idempotent)")
public void executeIdempotent(){
}
/**
* 切点业务
*
* @throws Throwable
*/
@Around("executeIdempotent()")
public Object arountd(ProceedingJoinPoint jPoint) throws Throwable {
//获取当前方法信息
Method method = ((MethodSignature)jPoint.getSignature()).getMethod();
//获取注解
Idempotent idempotent = method.getAnnotation(Idempotent.class);
//生成Key
String key = String.format(KEY_TEMPLATE,idempotent.key()+"_"+IdempotentKeyUtil.generate(method, jPoint.getArgs()));
//https://segmentfault.com/a/1190000002870317 -- JedisCommands接口的分析
//nxxx的值只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
//expx expx的值只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒
// key value nxxx(set规则) expx(取值规则) time(过期时间)
String redisRes = redisTemplate.execute((RedisCallback<String>)conn ->((JedisCommands)conn.getNativeConnection()).set(key, key,"NX","EX",idempotent.expirMillis()));
// Jedis jedis = new Jedis("127.0.0.1",6379);
// jedis.auth("xuzz");
// jedis.select(0);
// String redisRes = jedis.set(key, key,"NX","EX",idempotent.expirMillis());
if(Objects.equals("OK", redisRes)){
return jPoint.proceed();
}else{
//throw new IdempotentException("sorry!! Interface duplicates requests, violating idempotency.");
System.err.println("数据错误");
return null;
}
}
}
service 接口
import com.alibaba.fastjson.JSONObject;
/**
* TestService
*/
public interface TestService {
/**
* 数据测试
*/
public void print(JSONObject params);
}
service 实现类
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.idempotent.core.Idempotent;
import org.springframework.stereotype.Service;
/**
* TestServiceImpl
*/
@Service
public class TestServiceImpl implements TestService {
/**
*
*/
@Idempotent(key = "com.idempotent.controller.IdempotentController",expirMillis = 100)
@Override
public void print(JSONObject params) {
System.err.println(JSON.toJSONString(params));
}
}
controller
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.idempotent.core.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* IdempotentController
*/
@RestController
public class IdempotentController {
@Autowired
private TestService testService;
/**
* 测试push
*/
//@Idempotent(key = "com.idempotent.controller.IdempotentController",expirMillis = 100)
@PostMapping("/push/test")
public String pushTest(@RequestBody JSONObject params){
for (int i = 0; i < 5; i++) {
new Thread(()->{
try {
testService.print(params);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
return JSON.toJSONString(params);
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.idempotent</groupId>
<artifactId>idempotent-dome</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency> -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
<version>2.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.10.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.59</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
server:
port: 7001
spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
password: xxxx
timeout: 1000
pool:
max-wait: 10
max-active: 1
max-idle: 2
min-idle: 50
环境
jdk1.8,docker-redis