一、概念
幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次
比如:
- 订单接口, 不能多次创建订单
- 支付接口, 重复支付同一笔订单只能扣一次钱
- 支付宝回调接口, 可能会多次回调, 必须处理重复回调
- 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次
等等
二、常见解决方案
- 唯一索引 -- 防止新增脏数据
- token机制 -- 防止页面重复提交
- 悲观锁 -- 获取数据的时候加锁(锁表或锁行)
- 乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据
- 分布式锁 -- redis(jedis、redisson)或zookeeper实现
- 状态机 -- 状态变更, 更新数据时判断状态
三、本文实现
本文采用第2种方式实现, 即通过redis + token机制实现接口幂等性校验
四、实现思路
为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token:
- 如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示
- 如果不存在, 说明参数不合法或者是重复请求, 返回提示即可
五、项目简介
- springboot
- redis
- @ApiIdempotent注解 + 拦截器对请求进行拦截
- @ControllerAdvice全局异常处理
- 压测工具: jmeter
说明:本文重点介绍幂等性核心实现, 关于springboot如何集成redis、ServerResponse、ResponseCode等细枝末节不在本文讨论范围之内, 有兴趣的小伙伴可以查看我的Github项目:
https:// github.com/wangzaiplus/ springboot/tree/wxw
六、代码实现
pom
<!--
JedisUtil
@Component
自定义注解@ApiIdempotent
/**
ApiIdempotentInterceptor拦截器
/**
TokenServiceImpl
@Service
TestApplication
@SpringBootApplication
OK, 目前为止, 校验代码准备就绪, 接下来测试验证
七、测试验证
1、获取token的控制器TokenController
@RestController
2、TestController, 注意@ApiIdempotent注解, 在需要幂等性校验的方法上声明此注解即可, 不需要校验的无影响
@RestController
3、获取token
查看redis
4、测试接口安全性: 利用jmeter测试工具模拟50个并发请求, 将上一步获取到的token作为参数
5、header或参数均不传token, 或者token值为空, 或者token值乱填, 均无法通过校验, 如token值为"abcd"
八、注意点(非常重要)
上图中, 不能单纯的直接删除token而不校验是否删除成功, 会出现并发安全性问题, 因为, 有可能多个线程同时走到第46行, 此时token还未被删除, 所以继续往下执行, 如果不校验jedisUtil.del(token)的删除结果而直接放行, 那么还是会出现重复提交问题, 即使实际上只有一次真正的删除操作, 下面重现一下稍微修改一下代码:
再次请求
再看看控制台
虽然只有一个真正删除掉token, 但由于没有对删除结果进行校验, 所以还是有并发问题, 因此, 必须校验
九、总结
其实思路很简单, 就是每次请求保证唯一性, 从而保证幂等性, 通过拦截器+注解, 就不用每次请求都写重复代码, 其实也可以利用spring aop实现, 无所谓。