一、概念
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。
这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数.
幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次,比如:
订单接口, 不能多次创建订单
支付接口, 重复支付同一笔订单只能扣一次钱
支付宝回调接口, 可能会多次回调, 必须处理重复回调
普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次 等等
二、常见解决方案
三、本文实现方案
本文采用第2种方式实现, 即通过redis + token机制实现接口幂等性校验
为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token,如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示,如果不存在, 说明参数不合法或者是重复请求, 返回提示即可
四、核心代码
依赖库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <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> <version>3.1.3</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.28</version> <scope>compile</scope> </dependency>
|
自定义注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
|
@Service public class TokenServiceImpl implements TokenService {
private static final String TOKEN_NAME = "token";
@Autowired StringRedisTemplate stringRedisTemplate;
@Override public ServerResponse createToken() { String tokenValue = "idempotent:token:" + UUID.randomUUID().toString(); stringRedisTemplate.opsForValue().set(tokenValue, "0", 60, TimeUnit.SECONDS); return ServerResponse.success(tokenValue); }
@Override public void checkToken(HttpServletRequest request) { String token = request.getHeader(TOKEN_NAME); if (StringUtils.isBlank(token)) { token = request.getParameter(TOKEN_NAME); if (StringUtils.isBlank(token)) { throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg()); } }
if (!stringRedisTemplate.hasKey(token)) { throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg()); } boolean del = stringRedisTemplate.delete(token); if (!del) { throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg()); } }
}
|
IdempotentTokenInterceptor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
public class IdempotentTokenInterceptor implements HandlerInterceptor {
@Autowired private TokenService tokenService;
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; ApiIdempotent apiIdempotent = handlerMethod.getMethod().getAnnotation(ApiIdempotent.class); if (apiIdempotent != null) { tokenService.checkToken(request); }
return true; }
@Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { }
@Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
|
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
@RestController @RequestMapping("/test") public class TestController {
@Autowired private TestService testService;
@ApiIdempotent @PostMapping("testIdempotent") public ServerResponse testIdempotent() { return testService.testIdempotence(); }
}
|
获取token
http://localhost:8081/token
{
"status": 0,
"msg": "idempotent:token:80dd47ed-fa63-4b30-82fa-f3ee4fd64a50",
"data": null
}
验证接口安全性
http://localhost:8081/test/testIdempotent?token=idempotent:token:b9ae797d-ed1a-4dbc-a94f-b7e45897f0f5
第一次请求
{
"status": 0,
"msg": "test idempotent success",
"data": null
}
重复请求
{
"status": 1,
"msg": "请勿重复操作",
"data": null
}
利用jmeter测试工具模拟50个并发请求, 获取一个新的token作为参数
设置50个线程请求一次

设置请求IP、Path、参数等信息
查看执行结果,可以看到只有一个请求成功,其他的请求都返回错误

