分布式系统中的幂等性
什么是幂等性现如今我们的系统大多拆分为分布式SOA,或者微服务,一套系统中包含了多个子系统服务,而一个子系统服务往往会去调用另一个服务,而服务调用服务无非就是使用RPC通信或者restful,既然是通信,那么就有可能再服务器处理完毕后返回结果的时候挂掉,这个时候用户端发现很久没有反应,那么就会多次点击按钮,这样请求有多次,那么处理数据的结果是否要统一呢?那是肯定的!尤其再支付场景...
什么是幂等性
现如今我们的系统大多拆分为分布式SOA,或者微服务,一套系统中包含了多个子系统服务,而一个子系统服务往往会去调用另一个服务,而服务调用服务无非就是使用RPC通信或者restful,既然是通信,那么就有可能再服务器处理完毕后返回结果的时候挂掉,这个时候用户端发现很久没有反应,那么就会多次点击按钮,这样请求有多次,那么处理数据的结果是否要统一呢?那是肯定的!尤其再支付场景。
幂等性:是指一次和多次请求某一个资源应该具有同样的副作用。举个最简单的例子,那就是支付,用户购买商品使用约支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条..
幂等性是系统的接口对外一种承诺(而不是实现), 承诺只要调用接口成功, 外部多次调用对系统的影响是一致的。幂等性是分布式系统设计中的一个重要概念,对超时处理、系统恢复等具有重要意义。声明为幂等的接口会认为外部调用失败是常态, 并且失败之后必然会有重试。例如,在因网络中断等原因导致请求方未能收到请求返回值的情况下,如果该资源具备幂等性,请求方只需要重新请求即可,而无需担心重复调用会产生错误。实际上,我们常用的HTTP协议的方法是具有幂等性语义要求的,比如:
- get方法用于获取资源,不应有副作用,因此是幂等的;
- post方法用于创建资源,每次请求都会产生新的资源,因此不具备幂等性;
- put方法用于更新资源,是幂等的;
- delete方法用于删除资源,也是幂等的。
那么如何设计接口才能做到幂等呢?
在分布式场景 或者微服务架构场景下,以一个订单量流程来说明接口的幂等性问题
- 订单的创建接口,第一次调用超时,然后重试了一次
- 减库存操作超时,调用方重试了一次
- 扣钱操作重试了一次
- 订单状态更新接口,连续发送了两个消息,一个是已经创建,一个是已经付款,但是你先接受到已付款,然后收到已创建
- 订单支付完成之后,需要发送一条短信,一台机器收到短信后,处理慢,消息中间件又把消息投递给另外的一台机器处理
以上的问题,就是在单体架构转为 分布式 微服务等结构之后,幂等性的问题凸显。
说了这么多,那么如何解决幂等性的问题呢?
1、Token机制
针对前端重复连续多次点击的情况,例如用户购物提交订单,提交订单的接口就可以通过 Token 的机制实现防止重复提交。
主要流程就是:
- 服务端提供了发送token的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存到redis中。(微服务肯定是分布式了,如果单机就适用jvm缓存)。
- 然后调用业务接口请求时,把token携带过去,一般放在请求头部。
- 服务器判断token是否存在redis中,存在表示第一次请求,可以继续执行业务,执行业务完成后,最后需要把redis中的token删除。
- 如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行。
2、数据库去重表
这种方法适用于在业务中有唯一标的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据和写入去去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。
3、状态机
对于很多业务是有一个业务流转状态的,每个状态都有前置状态和后置状态,以及最后的结束状态。例如流程的待审批,审批中,驳回,重新发起,审批通过,审批拒绝。订单的待提交,待支付,已支付,取消。
以订单为例,已支付的状态的前置状态只能是待支付,而取消状态的前置状态只能是待支付,通过这种状态机的流转我们就可以控制请求的幂等。
public enum OrderStatusEnum {
UN_SUBMIT(0, 0, "待提交"),
UN_PADING(0, 1, "待支付"),
PAYED(1, 2, "已支付待发货"),
DELIVERING(2, 3, "已发货"),
COMPLETE(3, 4, "已完成"),
CANCEL(0, 5, "已取消"),
;
//前置状态
private int preStatus;
//状态值
private int status;
//状态描述
private String desc;
OrderStatusEnum(int preStatus, int status, String desc) {
this.preStatus = preStatus;
this.status = status;
this.desc = desc;
}
//...
}
假设当前状态是已支付,这时候如果支付接口又接收到了支付请求,则会抛异常或拒绝此次处理。
4、乐观锁
- 数据库乐观锁:
update order set orderStatus=#{orderStatus},version=version+1 where orderId=#{orderId} and version= #{version};
先获取锁
Order.setOrderId("12"); Order.setOrderStatus("3"); Order.setOrderVersion("0"); return 1 == orderMapper.updateOrderByLock(Order);
第一个请求过来时,数据库中version为0,执行update后version为1.当第二个请求过来时,执行这条sql会失败,因为此时version不为1,从而保证接口的幂等性。
总结
通过以上的了解我们可以知道,针对不同的业务场景我们需要灵活的选择幂等性的实现方式。
例如防止类似于前端重复提交、重复下单的场景就可以通过 Token 的机制实现,而那些有状态前置和后置转换的场景则可以通过状态机的方式实现幂等性,对于那些重复消费和接口重试的场景则使用数据库唯一索引的方式实现更合理。
更多推荐
所有评论(0)