springboot 接口幂等
********************
接口幂等
幂等:多次操作与一次操作,产生的结果相同
应用:解决表单重复提交造成的数据重复、超时重试引发的重复操作等问题
数据插入幂等
对数据记录中的字段做唯一性限制(如订单号),将字段设为主键、或者添加唯一索引
数据更新幂等
#使用乐观锁
update t set value=new_value and version=version+1
where value=#{value} and version=#{version}
重复提交,由于version值已经改变,后续更新操作不会执行
#业务自带状态标识(如订单status),处理方式类似乐观锁
status => 0:已下单、1:已支付、2已发货、3:确认收货、4:退款中、5:已退款
update order set status=3 where order_id=#{orderId} and status=2
重复提交时,由于status已经改变,后续更新操作不会执行
token 标识
客户端获取token,将token设置为下次请求的header
请求到达接口前先进行检验,如果没有token、或者token不等,则直接返回
如果token检验通过,执行接口,并返回操作结果
********************
示例:token实现接口幂等
****************
annotation 层
Idempotent
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
}
****************
util 层
StoreUtil
public class StoreUtil {
private static final Map<String,String> map=new HashMap<>();
public static void putValue(String key, String value){
map.put(key, value);
}
public static String getValue(String key){
return map.get(key);
}
public static void removeValue(String key){
map.remove(key);
}
}
****************
service 层
IdempotentService
@Service
public class IdempotentService {
public Map<String,String> createToken(){
Map<String,String> result = new HashMap<>();
String tokenName = RandomStringUtils.randomAlphanumeric(6);
String tokenValue = RandomStringUtils.randomNumeric(10);
result.put("tokenName",tokenName);
result.put("tokenValue",tokenValue);
StoreUtil.putValue(tokenName,tokenValue);
return result;
}
public boolean checkToken(String tokenName, String tokenValue){
if (tokenName==null || tokenValue==null){
return false;
}
synchronized (this){
if (tokenValue.equals(StoreUtil.getValue(tokenName))){
StoreUtil.removeValue(tokenName);
return true;
}
}
return false;
}
}
****************
interceptor 层
CustomInterceptor
@Component
public class CustomInterceptor implements HandlerInterceptor {
@Resource
private IdempotentService idempotentService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod){
Method method = ((HandlerMethod) handler).getMethod();
if (method.isAnnotationPresent(Idempotent.class)){
String tokenName = request.getHeader("tokenName");
String tokenValue = request.getHeader("tokenValue");
System.out.println(tokenName+" "+tokenValue);
if (!idempotentService.checkToken(tokenName, tokenValue)){
JSONObject object=new JSONObject();
object.put("code","111111");
object.put("status","error");
object.put("msg","token 检验失败");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(object);
return false;
}
}
}
return true;
}
}
****************
config 层
WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private CustomInterceptor customInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(customInterceptor);
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/index").setViewName("index");
}
}
****************
controller 层
HelloController
@RestController
public class HelloController {
@Resource
private IdempotentService idempotentService;
@RequestMapping("/token")
public Map<String, String> getToken(){
return idempotentService.createToken();
}
@Idempotent
@RequestMapping("/hello")
public Map<String,Object> hello(String name, Integer age){
System.out.println(name+" "+age);
Map<String,Object> result = new HashMap<>();
result.put("code","000000");
result.put("status","success");
return result;
}
}
****************
前端页面
index.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>$Title$</title>
<script src="/jquery/jquery-3.6.0.min.js"></script>
<script src="/layui-v2.6.8/layui/layui.js"></script>
<link rel="stylesheet" href="/layui-v2.6.8/layui/css/layui.css">
<script>
$(function (){
let tokenName, tokenValue;
$.get({
url: "/token",
success: function (result){
tokenName = result.tokenName;
tokenValue = result.tokenValue;
layer.msg(tokenName+" "+tokenValue);
$("#btn").click(function (){
$.ajax({
type: "get",
url: "/hello",
headers: {
tokenName: tokenName,
tokenValue: tokenValue
},
data: {
name: $("#name").val(),
age: $("#age").val()
},
success: function (result){
layer.msg(result.status.toString())
}
})
})
}
})
})
</script>
</head>
<body>
<div th:align="center">
<div class="layui-form-item">
<label class="layui-form-label">name</label>
<div class="layui-input-block">
<input type="text" id="name" required lay-verify="required" placeholder="请输入name" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">age</label>
<div class="layui-input-inline">
<input type="text" id="age" required lay-verify="required" placeholder="请输入age" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button id="btn" class="layui-btn">提交</button>
</div>
</div>
</div>
</body>
</html>
********************
使用测试
localhost:8080/index
控制台输出
2021-08-25 20:20:40.608 INFO 12508 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
2021-08-25 20:20:42.082 WARN 12508 --- [nio-8080-exec-9] o.a.c.util.SessionIdGeneratorBase : Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [126] milliseconds.
jkjBTq 4624310419
瓜田李下 20
********************
jmeter 测试
获取token
添加线程组
线程组下添加 HTTP Request
添加 HTTP Header Manager:设置请求头
添加 View Reqults Tree:查看请求结果
点击运行,查看请求结果
四次请求,只有一次请求执行成功