说明下:这里说的接口主要指非查询类接口,因为查询类接口天然具备幂等性。
一、背景
交易系统里用户下单提交订单时,由于用户连续快速点击,导致连续发送多次请求,分别命中到了不同的服务器, 那么就会生成多个内容完全相同的订单,只有订单号不同而已。
当然造成重复请求的原因,还有其他的可能:
- 网络波动,引起重复请求
- MQ重复消费
- Nginx重试
- 黑客拦截请求后重发
- ......
重复请求的影响
- 下单时,用户面对多个一模一样的订单,不知道应该支付哪个。
- 这些重复数据,对系统来说也是脏数据,还会影响正常的校验。
- 下单成功如果发送短信提醒用户,但发送了几个内容一样的短信。
所以接口支持幂等性非常重要,尤其是涉及交易的接口。
二、什么是幂等性?
所谓幂等性, 通俗点说就是一个接口, 无论执行几次所产生的影响与只执行一次产生的影响是一样的。
举例子说明下:
• 支付接口, 重复支付同一笔订单,最终也只能成功扣款一次。
• 支付宝、微信支付成功回调接口, 可能会多次回调, 必须只能成功处理一次。
• 普通表单提交接口, 多次点击提交, 数据只能成功落地一次。
三、常见解决方案
1. 唯一索引: 防止新增脏数据,保证最终插入数据库的数据只有一条。
2. token机制: 防止页面重复提交。
3. 悲观锁 -- 获取数据的时候加锁(一般是行锁)。
4. 乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据。
5. 分布式锁 -- 通过第三方的系统(redis或zookeeper),在业务系统插入或更新数据时,获取分布式锁,然后做操作,最后释放锁。
四、分布式锁实现方案详解
解决思路:同一用户在1秒内对同一URL的操作视为重复提交。
具体步骤:
- 服务器A接收到请求之后,尝试获取锁,获取成功 。
- 服务器A进行业务处理,订单提交成功。
- 服务器B接收到相同的请求,尝试获取锁,失败。因为锁被服务器A获取了,并且未释放。B 获取锁失败之后,直接返回。
- 服务器A处理完成,释放锁。
代码实现
@PostMapping(value = "/submit-order")
public Response<Boolean> submitOrder(HttpServletRequest request, OrderParam,orderParam){
String userId= "123456";//用户
String path = request.getServletPath();
String key = MD5(userId+path);
//1. 获取redis锁
String result = jedis.set(key, orderParam, "NX", "EX", 1000);
boolean success = "OK".equals(result);
try {
if (success) {
//2. 执行具体业务逻辑
//...
}else{
//3. 获取锁失败
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//4. 业务逻辑执行完成之后,释放锁
jedis.del(key);
}
return ResponseUtil.success(Boolean.TRUE);
}
总结
其实思路很简单, 我们也可以通过拦截器(AOP)+注解的方式实现一个通用的解决方案, 就不用每次请求都写重复代码。
幂等性在设计系统时,是需要首要考虑的问题,尤其是在像银行,支付宝等互联网金融公司等涉及到钱的系统。