幂等的概念
在数学里,幂等有两种主要的定义。
1、在某二元运算下,幂等元素是指被自己重复运算(或对于函数是为复合)的结果等于它自己的元素。例如,乘法下仅有两个幂等实数,为0和1。
2、某一元运算为幂等的时,其作用在任一元素两次后会和其作用一次的结果相同。例如,高斯符号便是幂等的。
一元运算的定义是二元运算定义的特例。
在计算机领域,幂等是指多次调用对系统产生的影响是一样的,即对资源的作用是一样的,但是返回值允许不同。
我们说一个 HTTP 方法是幂等的,指的是同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。在正确实现的条件下, GET , HEAD , PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。所有的 safe 方法也都是幂等的。
幂等性只与后端服务器的实际状态有关,而每一次请求接收到的状态码不一定相同。例如,第一次调用 DELETE 方法有可能返回 200 ,但是后续的请求可能会返回 404 。
举例来说,一个GET请求,每次返回的结果都相同,那么多次请求效果是相同的,我们就说这个请求是幂等的。而如果一个POST请求,每次请求后台都会新增一条记录,我们就说这个请求是不幂等的。
多次幂等请求:
GET /pageX HTTP/1.1 -> Return pageX
GET /pageX HTTP/1.1 -> Return pageX
GET /pageX HTTP/1.1 -> Return pageX
多次不幂等的请求:
POST /add_row HTTP/1.1 -> Adds a new row
POST /add_row HTTP/1.1 -> Adds a new row
POST /add_row HTTP/1.1 -> Adds a new row
对于SQL语句,如果多次执行产生的效果相同,我们就说这条SQL是幂等的,否则就不幂等。
举例来说,如果下面SQL中的name是有唯一索引的,多次执行以后数据库只能插入一条记录,那么这条SQL就是幂等的,否则不幂等。
INSERT INTO `table_name` (`name`) VALUES ('Sam');
在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
幂等设计
幂等性原本是数学上的概念,指的是使用相同的参数执行同一个函数时,无论执行多少次,都能输出相同的结果。在计算机编程中,幂等性指的是对于同一个方法来说,只要参数相同,无论执行多少次都与第一次执行时产生的影响相同。
在分布式系统中,为了保证一致性,业务服务对外提供操作业务数据的接口时,需要在接口的实现中保证对数据处理的幂等性。
但是在分布式环境中,难免会因为各种原因出现数据不一致的情况,此时,为了保证数据的最终一致性,系统会提供很多重试操作,保证最终的一致性。
如果这些重试操作涉及的方法中,某些方法的实现不具有幂等性,则即使重试操作成功了,也无法保证数据最终一致性。
通常有两种实现幂等性的方式:一种是通过业务操作本身实现幂等性;另一种是通过系统缓存所有的请求与处理结果,当再次检测到相同的请求时,直接返回之前缓存的处理结果。
幂等设计一般有两种处理方法:
(1)需要下游系统提供相关的查询接口。
上游系统第一次调用出现异常后,需要先调用下游系统提供的查询接口,如果查询到数据,表明上次的调用已经成功,就不需要做了,失败了就走失败流程。
(2)通过幂等性的方式。
也就是这个查询操作交给下游系统,上游系统只管重试,由下游系统保证一次和多次的请求结果是一样的。
幂等的解决方案非常多,需要根据具体的业务场景选择具体策略。
幂等方案一:数据库唯一主键实现幂等性
利用数据库中主键唯一约束的特性,保证一张表中只存在一条带该唯一主键的记录。
如果不是主键,也可以使用唯一索引保证数据库操作的幂等性,如果在分布式环境下使用唯一标识字段,可以使用分布式ID(可以使用Snowflake算法)充当主键,这样才能保证在分布式环境下ID的全局唯一性。
幂等方案二:数据库乐观锁实现幂等性
数据库乐观锁方案适用于执行更新操作,可以提前在对应的表中添加一个version字段,充当当前数据的版本标识。这样每次对表中的这条数据执行更新时,都会将该版本标识作为一个条件,值为待更新数据中的版本标识的值。
update table_name set name='NewName' where name='OldName' and version=1
幂等方案三:防重Token令牌实现幂等性
针对客户端连续点击或者调用方的超时重试等情况,例如提交表单,可以用Token机制实现防止重复提交。
简单地说,就是调用方在调用接口的时候,先向后端请求一个全局ID(Token),请求的时候携带这个全局ID一起请求(Token最好将其放到Headers中)。
后端需要将这个Token作为Key,用户信息作为Value,到Redis中进行键值内容校验,如果Key存在且Value匹配就执行删除命令,然后正常执行后面的业务逻辑,如果不存在对应的Key或Value不匹配就返回重复执行的错误信息,这样来保证幂等操作。
具体步骤如下:
步骤01:
客户端通过Token服务获取Token令牌(序列号/分布式ID/UUID字符串)。
步骤02:
Token服务将Token存入Redis缓存中,以该Token作为Redis的键(注意设置过期时间)。
步骤03:
将Token返回到客户端,客户端拿到后保存到表单隐藏域中。
步骤04:
客户端在执行提交表单时,把Token存到Headers中。
步骤05:
服务端接收到请求后,从请求头Headers中拿到Token,根据Token到Redis中查找该key是否存在。
步骤06:
服务端根据Redis中是否存在该key进行判断,如果存在就将该key删除,然后正常执行业务逻辑。如果不存在就抛出异常,返回重复提交的错误信息。
Token删除的时机有两种不同的处理方法:
(1)检验Token存在(表示第一次请求)后,就立刻删除Token,再进行业务处理。
先删除Token,这时如果业务处理出现异常,接口调用方也没有获取到明确的结果,就进行重试,但Token已经删除掉了,服务端判断Token不存在,认为是重复请求,因此直接返回,无法进行业务处理。
(2)检验Token存在(表示第一次请求)后,先进行业务处理,再删除Token。
后删除Token也是存在问题的,如果进行业务处理成功后,删除Redis中的Token失败,这样有可能导致发生重复请求,因为Token没有被删除。
综上所述,推荐先删除Token,先保证不会因为重复请求导致业务数据出现问题,最多让用户再请求处理一次。
另外,Token幂等方案还有一个问题,业务每次请求都会有额外的请求(获取Token请求、判断Token是否存在等)。
在生产环境中,1000个请求也许只会存在20个左右的请求会发生重试,为了这20个请求,让980个请求都发生额外的请求,显然有点浪费。
幂等方案四:分布式锁
分布式锁的实现方式可以基于Redis的SETNX命令实现。
SETNX命令的语法如下:
SETNX key value
将key的值设为value,当且仅当key不存在时,命令返回1。若给定的key已经存在,SETNX不做任何动作,命令返回0。
key可以取业务某个唯一字段的值,例如订单数据可以取订单ID,用户数据可以取用户ID,等等。
幂等方案五:去重表机制
去重表也叫幂等表,使用去重表方案需要业务中有唯一主键,去重表中只需要一个字段即可,用于设置唯一主键约束,当然也可以根据业务情况自行添加其他字段。
去重表机制的主要流程:
把唯一主键插入去重表,再进行业务操作,且它们处于同一个事务中。
当重复请求时,因为去重表有唯一约束,导致请求失败,可以避免幂等问题去重表和业务表应该在同一个库中,这样就保证了在同一个事务中,即使业务操作失败,也会把去重表的数据回滚。
这样可以很好地保证数据的一致性。
该方案也是比较常用的,去重表跟业务无关,很多业务可以共用同一个去重表,只要规划好唯一主键即可。
幂等方案六:状态机
在有状态的数据中可以使用,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样就保证了有限状态机的幂等。
例如,状态status只能进行1->2->3->4的顺序变更。
更新SQL可以写成:
update table_name set status=2 where id=1 and status=1
update table_name set status=3 where id=1 and status=2
update table_name set status=4 where id=1 and status=3
参考资料
- 百度百科:幂等 https://baike.baidu.com/item/%E5%B9%82%E7%AD%89
- https://developer.mozilla.org/zh-CN/docs/Glossary/Idempotent
- https://www.21ic.com/article/883663.html
- 《分布式高可用架构之道》
- 《分布式应用系统架构设计与实践》