接口幂等性适用场景及设计方法
说明:
本文主要内容并非所原创,而是对网上已有文章的收集整理和自我
提炼总结,仅作学习笔记之用,如有冒犯,请联系本人删除。
1.幂等地定义
1.1数学定义
在数学里,幂等有两种主要的定义:
- 在某二元运算下,幂等元素是指被自己重复运算(或对于函数是为复合)的结果等于它自己的元素。如,乘法运算下,0和1符合的自乘运算符和幂等,即s*s=s
- 某一元运算为幂等的时,其作用在任一元素两次后会和其作用一次的结果相同。例如,高斯符号便是幂等的,即f(f(x))=f(x)
1.2 计算机中的幂等
在计算机中,表示对同一个过程应用相同的参数多次和应用一次产生的效果是一样,这样的过程即被称为满足幂等性
幂等:
update test_user set user_age = 25 where user_id = 2 ,这中情况无论执行多少次,结果都不受影响,所以是幂等的。
非幂等:
update test_user set user_times = user_times + 1 where user_id = 2, 这样的更新语句每执行一次,结果都会不一样,所以是非幂等的。
1.2.1Http规范定义
在HTTP/1.1规范中幂等性的定义是:
A request method is considered "idempotent" if the intended
effect on the server of multiple identical requests with that
method is the same as the effect for a single such request. Of
the request methods defined by this specification, PUT,
DELETE, and safe request methods are idempotent.
即:
一个请求方法,如果被请求多次和被请求一次效果相同,被认为是幂等的,比如PUT、DELETE和其他安全的请求方法都是幂等的。
1.2.2 微服务场景中幂等
由于微服务的普及,原有的单体应用,被设计成不同的功能模块作为服务,独立部署在不同的物理环境中。要完成一个完整的业务流程,就需要在多个微服务中间进行调用,而调用的过程当然是经由网络来完成的。
因此,网络通信的不确定性因素,比如网络的抖动,对端微服务的异常,可能会导致微服务间调用,产生超时的现象。为保证业务流程的顺利完成,调用过程必须建立重试机制。
然而,一旦建立了重试机制,那就可能会将同一个请求发送多次,导致接受方重复消费,多次执行相同操作,进而可能产生错误的数据。为避免这个问题,必须保证可能产生错误数据的接口(方法),请求一次和请求多次的效果相同,即幂等。
2.需要幂等的场景
可能会发生重复请求或消费的场景,在微服务架构中是随处可见的。以下是笔者梳理的几个常见场景:
-
网络波动:
因网络波动,可能会引起重复请求 -
分布式消息消费:
任务发布后,使用分布式消息服务来进行消费,参考【消息总线真的能保证幂等?】 -
用户重复操作:
用户在使用产品时,可能会误操作而触发多笔交易,或者因为长时间没有响应,而有意触发多笔交易。 -
未关闭的重试机制:
技术人员人为的错误,因开发人员、测试人员或运维人员没有检查出来,而开启的重试机制(如Nginx重试、RPC通信重试或业务层重试等)
3.“天然”的幂等和需要“人工”的幂等
3.1CRUD分析
CRUD是指在做计算处理时的[增加](Create)、读取(Read)、更新(Update)和删除(Delete)几个单词的首字母简写。主要被用在描述软件系统中数据库或者持久层的基本操作功能。
所以CRUD角度分析幂等性,是从操作目的层面来看问题的:
操作 | 幂等性 |
---|---|
新增类请求(C) | 数据库自增主键,不具备幂等性 |
查询类动作(R) | 重复查询不会产生或变更新的数据,因此查询是天然具备幂等性 |
基于主键的计算式更新(U) | 不具备幂等性,即:UPDATE goods SET number=number-1 WHERE id=1 |
基于主键的非计算式更新(U) | 具备幂等性,即:UPDATE goods SET number=newNumber WHERE id=1 |
基于条件查询的更新(U) | 不一定具有幂等性(需要根据实际情况进行分析判断) |
基于主建的删除(D) | 具备幂等性 |
业务层面都是逻辑删除(即Update操作)(U) | 不具备幂等性 |
3.2 HTTP方法分析
按照restful规范定义的接口,使用http方法,应该严格遵循http方法语义:
方法 | 幂等性 | 对应CRUD操作 |
---|---|---|
POST | 不安全且不幂等 | C |
GET | 安全且幂等 | R |
PUT | 不安全但幂等 | U |
DELETE | 不安全但幂等 | D |
3.3 “天然”的幂等
GET,PUT,DELETE都是幂等操作,而POST不是,以下进行分析:
首先GET请求很好理解,对资源做查询多次,此实现的结果都是一样的。
PUT请求的幂等性可以这样理解,将A修改为B,它第一次请求值变为了B,再进行多次此操作,最终的结果还是B,与一次执行的结果是一样的,即属于CURD中所说的基于主键的非计算式更新,所以PUT是幂等操作。
同理可以理解DELETE操作,第一次将资源删除后,后面多次进行此删除请求,最终结果是一样的,将资源删除掉了。
3.4需要“人工”的幂等
POST不是幂等操作,因为一次请求添加一份新资源,二次请求则添加了两份新资源,多次请求会产生不同的结果,因此POST不是幂等操作。
如果需要在POST方法的接口实现幂等,需要人为加上幂等的机制。
下面我们来说说,幂等地实现方法。
4.幂等实现方法
4.1 全局唯一ID
如果使用全局唯一ID,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID,存储到存储系统中,比如数据库、Redis等。如果存在则表示该方法已经执行。
使用全局唯一ID是一个通用方案,可以支持插入、更新、删除业务操作。但是这个方案看起来很美但是实现起来比较麻烦,下面的方案适用于特定的场景,但是实现起来比较简单。
4.2 去重表
这种方法适用于在业务中有唯一标的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,用以记录订单支付信息,在我们实现时,把创建支付单据和写入去去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。这个方法其实也是用到唯一ID,与上面全局唯一ID不同的是,他是针对具体单个业务流程的,实现起来相对简单。
4.3 插入或更新
这种方法插入并且有唯一索引的情况,比如我们要关联商品品类,其中商品的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()
4.4 多版本控制
这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等:
boolean updateGoodsName(int id,String newName,int version);
在实现时可以如下:
update goods set name=#{newName},version=#{version} where
id=#{id} and version<${version}
4.5 状态机控制
这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100,付款失败为99。在做状态机更新时,我们就这可以这样控制:
update goods_order set status=#{status} where id=#{id} and
status<#{status}
以上就是保证接口幂等性的一些方法。
5.总结
幂等性设计不能脱离业务来讨论,一般情况下,去重表同时也是业务数据表,而针对分布式的去重ID,可以参考以下几种方式:
- UUID
- Snowflake
- 数据库自增ID
- 业务本身的唯一约束
- 业务字段+时间戳拼接