接口幂等性设计
1. 接口调用存在的问题
现如今我们的系统大多拆分为分布式SOA,或者微服务,一套系统中包含了多个子系统服务,而一个子系统服务往往会去调用另一个服务,而服务调用服务无非就是使用RPC通信或者restful,既然是通信,那么就有可能在服务器处理完毕后返回结果的时候挂掉,这个时候用户端发现很久没有反应,那么就会多次点击按钮,这样请求有多次,那么处理数据的结果是否要统一呢?那是肯定的!尤其在支付场景。
2. 什么是接口幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...,这就没有保证接口的幂等性。
幂等与你是不是分布式高并发还有JavaEE都没有关系。关键是你的操作是不是幂等的。
典型的幂等操作: 把编号为5的记录的A字段设置为0 这种操作不管执行多少次都是幂等的。
典型的非幂等操作: 把编号为5的记录的A字段增加1 这种操作显然就不是幂等的。
3. 什么情况下需要保证接口的幂等性
在增删改查4个操作中,尤为注意就是增加或者修改,
查询操作:查询对于结果是不会有改变的,查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作
删除操作:删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个,在不考虑返回结果的情况下,删除操作也是具有幂等性的)
更新操作:修改在大多场景下结果一样,但是如果是增量修改是需要保证幂等性的,如下例子:
把表中id为XXX的记录的A字段值设置为1,这种操作不管执行多少次都是幂等的
把表中id为XXX的记录的A字段值增加1,这种操作就不是幂等的
新增操作:增加在重复提交的场景下会出现幂等性问题,如以上的支付问题
4. 那么如何设计接口才能做到幂等呢?
接口幂等性,只要保证接口内的逻辑不涉及接口外的对象状态累积或变迁即可。
接口的幂等性实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。有些接口可以天然的实现幂等性,比如查询接口,对于查询来说,你查询一次和两次,对于系统来说,没有任何影响,查出的结果也是一样。
除了查询功能具有天然的幂等性之外,增加、更新、删除都要保证幂等性。那么如何来保证幂等性呢?
全局唯一ID
如果使用全局唯一ID,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID,存储到存储系统中,比如数据库、redis等。如果存在则表示该方法已经执行。
从工程的角度来说,使用全局ID做幂等可以作为一个业务的基础的微服务存在,在很多的微服务中都会用到这样的服务,在每个微服务中都完成这样的功能,会存在工作量重复。另外打造一个高可靠的幂等服务还需要考虑很多问题,比如一台机器虽然把全局ID先写入了存储,但是在写入之后挂了,这就需要引入全局ID的超时机制。
使用token机制实现:每次提交都使用一个Token,Token保证临时且唯一即可
token生成规则(单机应用):token+UUID
- 每次调用接口前生成一个token存放到Redis中。
- 调用接口时,将生成的token放入请求头中。
- 接口获取请求头中的token,去redis中查询token是否存在,存在则删除该token并执行业务逻辑。
- 如果redis中查询不到该token则提示不可重复提交。
token生成规则(分布式):token+分布式全局id
使用全局唯一ID是一个通用方案,可以支持插入、更新、删除业务操作。但是这个方案看起来很美但是实现起来比较麻烦,下面的方案适用于特定的场景,但是实现起来比较简单。
去重表
这种方法适用于在业务中有唯一标识的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据和写入去去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。
插入或更新
这种方法插入并且有唯一索引的情况,比如我们要关联商品品类,其中商品的ID和品类的ID可以构成唯一索引,并且在数据表中也增加了唯一索引。这时就可以使用InsertOrUpdate操作。在mysql数据库中如下:
insert into goods_category (goods_id,category_id,create_time,update_time)
values(#{goodsId},#{categoryId},now(),now())
on DUPLICATE KEY UPDATE
update_time= now()
多版本控制
这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等
boolean updateGoodsName(int id,String newName,int version);
在实现时可以如下
update goods set name=#{newName},version=#{version} where id=#{id} and version<${version}
状态机控制
这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100。付款失败为99
在做状态机更新时,我们就这可以这样控制
update `order` set status=#{status} where id=#{id} and status<#{status}
以上就是保证接口幂等性的一些方法