聊一聊幂等
Posted PersistentCoder
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊一聊幂等相关的知识,希望对你有一定的参考价值。
幂等是什么?百度上给出的解释如下:
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的.更复杂的操作幂等保证是利用唯一交易号(流水号)实现。
1
编程中的幂等
在我们日常开发和业务实现中,对于相同的参数输入,多次调用相同的功能,对资源的影响是一样的,也就是一次和多次请求某一个资源应该具有同样的副作用。
表单重复提交;重复推送数据导致多次更新后端资源导致数据不一致问题
RPC超时重试;服务被多次调用导致数据不一致问题
SQL多次执行;程序问题导致sql多次调用带来数据不一致问题
我们在研发工作中,需要考虑幂等的场景也比较多,常见的就是接口幂等、消息幂等和数据库幂等等。
2
接口幂等
接口幂等又常分为http接口幂等和RPC接口幂等。
我们常用的http接口对应的请求方式中:
GET是幂等的,get方式是从服务器端获取资源,是单纯的查询操作,对服务端资源没有更新,所以是幂等的。
HEAD是幂等的,似于get请求,只不过返回的响应中没有具体的内容,用于获取报头。
POST一般是非幂等的,用于表单提交向服务端新增数据。
PUT 一般也是幂等的,用于更新服务端资源。
DELETE 一般也是幂等的,用于删除服务端资源。
其他请求方式我们平时基本很少用到,这里不再一一列举。
RPC接口用于领域设计后的功能拆分,调用是跨进程的,对于RPC接口中的幂等,其实是对于外部调用超时重试,或者同样参数多次调用同一个接口,要保证对服务端资源的影响和一次调用是一样的,我们经常遇到的情况是状态机的变更,比如客户端调用RPC服务完结退款的状态,那么多次调用要保证和一次调用一样,退款状态只能被修改一次,并且最终的状态是完结。
3
消息幂等
对于消息幂等,可能大家对其了解不是很多,我们应用中基本都会使用消息队列,那么肯定会涉及到消息投递和消息接收,消息幂等也要分两个维度来分析:
所谓消息投递幂等,就是同一条消息只能被投递一次,对于消息broker来说,就算同一条消息投递多次,我也只存储一条。我们举个例子来说明一下:
①消息发送者,尝试发送一条消息到消息broker。
②消息broker收到消息后理论上要给一个响应结果给发送者,但是这个响应可能丢失了。
③对于②中的响应丢失或者没有响应,消息发送者会认为没有发送成功,重复投递消息。
问题就在于响应丢失重复投递,有可能消息broker已经成功接收消息并且存储了,重复投递的消息有可能被消息broker接收并存储,导致broker接收了两条或者两条以上的相同消息,也就会导致消息接收方接收到多条相同的消息,在业务场景中可能造成业务异常。
对于这种重复投递的消息,消息broker层可以对每一条消息生成一个唯一的code,有重复消息过来的时候,生成的code也会相同,如果发现相同就丢弃。
消息接收幂等,是消息broker中的同一条消息只能被consumer接收处理一次,就算broker推送多次,也只能消费一条。同样举个例子来说明一下:
①消息broker尝试向consumer推送一条消息。
②消息consumer接收到消息后,向broker发送响应结果,但是丢失。
③消息broker由于没有接收到②中的响应结果,认为consumer没有收到消息,会重复推送。
消息broker重复推送相同消息的时候,有可能consumer已经接受成功并处理了业务,再次收到消息会导致consumer重复处理逻辑,假如在资金相关的场景出现这种情况,会导致重复出账造成资损。
对于消息broker重复推送的消息,consumer要对每一条消息生成唯一id或者code,如果发现重复消息,直接丢弃。
4
数据库幂等
所谓数据库幂等,也就是我们对DB层的操作幂等,说人话就是保证我们业务操作sql是幂等的,其实就是我们同一条sql执行多次和执行一次的效果是一样的,就拿CRUD来说,有些是幂等的,有些不是幂等但是可以通过调整转换成幂等的:
对于数据库查询,只是单穿的从数据库获取资源,不会更新数据,所以是幂等的。
对于新增数据操作,很多时候是非幂等的,执行一次insert就会新增一条数据(id是自增主键):
insert into User(id,name) values(null,'typhoon');
这条sql是非幂等的,每执行一次都会新增一条记录,但是通过改造我们可以将其变成幂等的:
insert into User(id,name) values(100,'typhoon');
这样就变成幂等的了,不使用自增序列,通过程序生成主键,这样重复执行,数据库也只会新增一条数据。
数据库更新操作,有些是幂等的,有些是非幂等的,典型的场景就是值递增,比如把小明的账户新增10000块:
update Account set money = money + 10000 where name = '小明';
这条sql是非幂等的,如果由于上层程序有bug,导致该sql重复被调用,那么就赶紧找地方哭去吧。我们通过简单的调整就可以把上述sql给造成幂等的:
var currentMoney = select money from Account where name = '小明';
var targetMoney = currentMoney + 10000;
update Account set money = targetMoney where name = '小明' and money = currentMoney;
这样上述的非幂等sql就改造成了幂等了,有两个关键的点需要注意:
使用"="代替"+",避免多次执行导致多次增加。
增加条件money = currentMoney,避免并发的其他程序把金额扣了,导致这条sql把扣掉的钱又加上了。
带精确匹配的删除sql是幂等的,其他的基本是非幂等的,典型的就是范围删除:
delete from Table where id > 1000;
这个sql是非幂等的,在删除数据的同时,有并发程序在新增数据,那么就导致每一次执行delete都删除了数据,也就违反了执行多次和一次对资源的副作用一样。经过改造把上述sql改成幂等的:
delete from Table where id in (?,?,?);
这样把范围删除变成了精确匹配删除,也就变成了幂等sql。
幂等对我们应用架构非常重要,大公司把幂等当做一个应用编码规范,可见其重要程度,不管是网络超时重试、还是程序有bug导致的多次调用,幂等在很大程度上保护了我们的系统资源。特别是牵扯到资金的场景,任何一层的实现都要做幂等。
以上是关于聊一聊幂等的主要内容,如果未能解决你的问题,请参考以下文章
阿里 Seata 新版本终于解决了 TCC 模式的幂等悬挂和空回滚问题
阿里 Seata 新版本终于解决了 TCC 模式的幂等悬挂和空回滚问题