目录
什么是幂等性?
应用场景分析
解决办法
实际使用
什么是幂等性?
接口的幂等性就是用户对于同一个操作发起的一次请求或者多次请求的结果都是一致的,不会因为多次点击而产生副作用,比如说经典的支付场景:用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果相同,用户发现多扣钱了,流水记录也变成了条,这就没有保证接口的幂等性;
防止接口重复提交就必须保证接口的幂等性
应用场景分析
查找修改和删除语句是支持幂等性的,因此我们只需要对插入语句进行幂等性判定
解决办法
目前主要的解决方法有:
1.token机制:前端带着在请求头上带着标识,后端验证
2.加锁机制:
数据库悲观锁(锁表)
数据库乐观锁(version号进行控制)
业务层分布式锁(加分布式锁redisson)
3.全局唯一索引机制
4.redis的set机制
5.前端按钮加限制
实际使用
本次选用的是redis的set机制,完全用后端进行限制,后端通过自定义注解,在需要防止幂等性的接口上添加注解,利用AOP切片减少和业务的耦合!再切片中获取用户的token、user_id、url构成redis唯一key!第一次请求会先判断key是否存在,如果不存在,则往redis中添加一个key,并设置过期时间
如果有异常会主动删除key,万一没有删除失败,等待1s,redis也会自动删除,时间误差是可以接受的! 第二个请求过来,先判断key是否存在,如果存在,则是重复提交,返回保存信息!!
1.导入pom依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</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-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
2.编写yml文件
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/aa?useUnicode=true&characterEncoding=utf-8
username: root
password: 2019
redis:
host: 127.0.0.1
port: 6379
password: 123456
3.redis序列化
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author 小如
*/
@Configuration
public class RedisConfig{
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
4.自定义注解
import java.lang.annotation.*;
/**
* 自定义注解防止表单重复提交
*/
@Target(ElementType.METHOD) // 注解只能用于方法
@Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期
@Documented
public @interface RepeatSubmit {
/**
* 防重复操作过期时间,默认1s
*/
long expireTime() default 1;
}
5.编写切片
@Slf4j
@Component
@Aspect
public class RepeatSubmitAspect {
@Autowired
private RedisTemplate redisTemplate;
/**
* 定义切点
*/
@Pointcut("@annotation(com.example.demo.config.RepeatSubmit)")
public void repeatSubmit() {}
@Around("repeatSubmit()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 获取防重复提交注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
// 获取token当做key,小编这里是新后端项目获取不到哈,先写死
// String token = request.getHeader("Authorization");
String tokenKey = "hhhhhhh,nihao";
String url = request.getRequestURI();
/**
* 通过前缀 + url + token 来生成redis上的 key
* 可以在加上用户id,小编这里没办法获取,大家可以在项目中加上
*/
String redisKey = "repeat_submit_key:"
.concat(url)
.concat(tokenKey);
log.info("==========redisKey ====== {}",redisKey);
if (!redisTemplate.hasKey(redisKey)) {
redisTemplate.opsForValue().set(redisKey, redisKey, annotation.expireTime(), TimeUnit.SECONDS);
try {
//正常执行方法并返回
return joinPoint.proceed();
} catch (Throwable throwable) {
redisTemplate.delete(redisKey);
throw new Throwable(throwable);
}
} else {
// 抛出异常
throw new Throwable("请勿重复提交");
}
}
}
6.同一结果返回值封装
@Data
public class ResuitUtiil<T> {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //提示信息
private T data; //数据
private Map map = new HashMap(); //动态数据
private boolean flag;//标记符
public ResuitUtiil() {
}
public ResuitUtiil(Integer code, String msg, T data, Map map, boolean flag) {
this.code = code;
this.msg = msg;
this.data = data;
this.map = map;
this.flag = flag;
}
public static <T> ResuitUtiil<T> success(T object) {
ResuitUtiil<T> r = new ResuitUtiil<T>();
r.data = object;
r.code = 200;
r.flag = true;
return r;
}
public static <T> ResuitUtiil<T> success(String msg) {
ResuitUtiil<T> r = new ResuitUtiil<T>();
r.code = 200;
r.msg = msg;
r.flag = true;
return r;
}
public static <T> ResuitUtiil<T> error(String msg) {
ResuitUtiil r = new ResuitUtiil();
r.msg = msg;
r.code = 300;
r.flag = false;
return r;
}
public ResuitUtiil<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
7.controller层进行结果测试
@RestController
@RequestMapping("/test")
public class Test {
@Resource
private MenuService menuService;//改写为你的service层
@RepeatSubmit(expireTime = 10)
@PostMapping("/saveSysLog")
public ResuitUtiil saveSysLog(@RequestBody Menu menu){
System.out.println(menu);
return ResuitUtiil.success(menuService.insert(menu));
}
}
利用postman进行测试,首次提交,发现成功
再次点击,出现请勿重复提交