在一个分布式系统里,尤其是一个微服务的系统,实现的接口保证幂等性应该是一个工程师最基础的素养
场景
分布式系统接口幂等性如何设计?
1.浏览器客户端发起多次提交
浏览器客户端发起多次提交原因一般分为:客户短时间内多次提交,网络超时,客户端未能及时收到服务端返回而发起多次提交。
1、支付订单后台服务2台集群部署
2、(红色标识)用户发起支付订单时候、前端bug/用户恶意,支付订单默认发起了2次请求,比如都发送到服务A2;这个时候你可以弄一个java集合;记录好这个订单被支付过了,第二个请求就不管了,这个可以保证单机系统的幂等性。
3、(绿色标识)单机比较好做,但是假如,第一次请求给了服务A1,第二次请求给了服务A2;此时基于jvm内存是不能实现两个系统间内存共享。如果不保证幂等性,那么一个订单支付2次;用户会两次扣款。
2.微服务间超时重试
分布式系统微服务间调用,比如feign,一般都会设置重试机制,服务端某个接口重复调用多次。
1、(红色标识)用户发起1次请求,服务超时,发起重试两次请求都在boss服务A2,此时基于JVM内存可以实现结果准确。
2、(绿色标识)用户发起1次,服务超时,发起的重试跟前一次在不同的集群机器之间。
幂等性相关概念
幂等性
针对于服务端的接口,客户端发起多次相同请求后,最后接口产生的结果是准确的。
幂等性保障原理
保证系统幂等性需要满足3个条件:
1、请求唯一标识:每一个请求,必须有一个唯一标识。
2、处理唯一表识:每次处理完请求之后,必须有一个记录标识这个请求处理过了.
3、逻辑判断处理:每次接收请求需要进行判断之前是否处理过的逻辑处理。根据请求唯一标示得到处理唯一标识,然后判断结果准确性。
幂等性方案
保障幂等性一般有5种方案:
1、业务表唯一索引
使用请求唯一标示作为业务数据里面的唯一索引(unique key),
处理唯一标识是数据库里面是否已经插入记录。
比如:采购入库单创建,是根据采购单中商品采购单id作为唯一索引。如果接口被重试,由于同一个采购单创建一个采购入库单,多次调用一定会违反唯一索引。
2、业务表内状态机
处理唯一标示是根据请求唯一标识的状态。
比如:支付订单,支付的是“待支付”状态的订单,一旦订单支付完毕,状态会更改为:“已支付”。
update order set status = “已支付” where status = “待支付” and order_id = xxx(请求唯一标识);一旦这条sql执行完成后,订单的状态其实就变为了“已支付”。假如说id = xxx 的订单接口重复调用,又要执行一次这个操作的话,就不会生效了,就不会再次修改了(因为第一次请求的订单状态已经是已支付了,此时xxx对应的订单状态没有待支付的了)。
所以在业务模块中针对于状态相关的业务逻辑,使用设计模块:状态模式,天然解决了分布式系统幂等性问题;在订单模块里,我们用了状态模式,在进行状态流转的时候,其实都会去判断一下的,当前是否处于某个状态,然后才能流转到下一个状态。-----这个状态机就是保证了幂等性。
3、基于版本号的更新(一般不常用)
数据库里面有一个学生的信息,如下:
id name age version
1 张三 15 1
如果要调用人家的这个接口,更新他的这个年龄,先得查一下他的版本号是多少?比如:version = 1
调用人家的接口修改他的年龄,你需要携带这个版本号,并且还要修改这个版本号,在你的接口里为了保证分布式接口的幂等性(你修改时候,不仅要修改age,还得修改对应的version)
update student set age=16, version=version+1 where id=1 and version=1
如果这条SQL执行成功过后,数据会变成如下:
id name age version
1 张三 16 2
如果被重复调用了,此时会如何?将会执行如下sql:
update student set age=16, version=version+1 where id=1 and version=1
这个时候,由于版本1的张三已经不存在,所以最终张三年龄是16,最终结果准确。
我们一般不常用,对于接口调用方来说,要多做一些事情,他要先查出来数举的version,调用修改接口的时候,传过去这个version;多了一些操作。也可能接口调用方接口随便乱调用。
4/5、基于mysql的去重表 / 基于redis或者zk的去重(很常见)
基于mysql的去重表跟基于redis的去重其实都是一个方案。
就是说这个方案是很常见通用的一个方案:
基于mysql,单独搞一个表出来,就一个字段存储“请求唯一标识”,建一个唯一索引,第一次调用时候请求唯一索引插入进去。如果这个接口被重复调用的话,“请求唯一索引” 再次插入一个表的话,唯一索引会报一个冲突出来,这次插入就会失败。
这个方案并发不是特别高的话,接口被调用的并发不是特别高的话,每秒的并发请求量在1000左右,1000以内的话,用mysql的去重表也没什么问题。
如果接口调用量很大,并发很高,一秒请求量达到了几十万,mysql不能抵抗高并发,所以不用mysql,选择使用redis,拼接一个“请求唯一标识”串出来,直接set设置到redis里去,如果下一次人家请求再过来了,此时会发现这个key已经存在了,那么这个时候就不能执行了,因为已经出现重复调用了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。