幂等性定义
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
例如,getUsername()、setTrue()函数就是一个幂等函数。其中多次getUsername()有可能得到结果不一致,但不会影响系统状态,查询功能具有天然的幂等性。
在实际的业务场景中,在完成一个订单、付款流程时经常遇到以下场景:
- 一个订单创建接口,第一次调用超时了,然后调用方重试了一次
- 在订单创建时,我们需要去扣减库存,这时接口发生了超时,调用方重试了一次
- 由于网络抖动,第三方支付(支付宝、微信)在订单支付完成后短时进行多次重复回调
- 当这笔订单开始支付,在支付请求发出之后,在服务端发生了扣钱操作,接口响应超时了,调用方重试了一次。
- 一个订单状态更新接口,调用方连续发送了两个消息,一个是“已创建”,一个是“已付款”。但是你先接收到“已付款”,然后又接收到了“已创建”
- 在支付完成订单之后,需要发送一条短信,当一台机器接收到短信发送的消息之后,处理较慢,消息中间件又把消息投递给另外一台机器处理。
为了解决上述问题,就需要保证接口幂等性,保证调用方多次调用额情况下,系统的最终状态是一直的。
保证幂等性的方式
1)无需幂等操作:查询操作/删除操作
查询操作:查询一次和多次,在数据不变的情况下,查询结果是一样的,select是天然的幂等操作。即使数据发生改变,但查询操作也不影响系统状态。
删除操作:删除操作也是幂等的,删除一次和多次删除都是把数据删除,最终状态是符合条件的数据不存在。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果为数量)
2)增加唯一索引
插入数据的唯一性,可以通过业务主键、唯一键来进行约束。唯一索引或唯一组合索引可以防止新增数据存在脏数据,当表存在唯一索引,并发过程中新增数据报错时,根据错误码提示,再查询一次,已经存在了的数据,直接返回结果即可)
例如:支付宝有资金账户、也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建多个资金账户,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录。
3)TOKEN机制,防止页面重复提交
由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交,为防止这种情况,页面的数据只能被点击提交一次。解决方法可以采用token加redis:
-
数据提交前要向服务的申请token,将token存放到redis中,并设置token有效时间。
-
提交后后台校验token,同时删除token,生成新的token返回。
token的特点: 要申请,一次有效性,可以限流。
注意:redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用
4)去重表(幂等表)
这种方法适用于在业务中有唯一标的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张数据库去重表,并且把唯一标识(幂等号idemNo)作为唯一索引,在我们实现时,把创建支付单据和写入去去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。通过该种基于数据库层次的幂等,可以保证相同idemNo的请求只会被处理一次。并且幂等控制数据不会丢失。
另外,也可以基于redis的set( key, value, nxxx, expx, time)方法实现去重表,该方法可以保证只有一个请求的返回值为true,且幂等的过期时间为time。全局并发幂等性校验可以控制相同key的请求在一定时间内只被系统进行一次处理。防止系统并发处理相同请求。
基于去重表机制,其处理流程如下:
幂等性处理流程.png5) 多版本控制
这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等。类似乐观锁接口实现方式。
boolean updateGoodsName(int id, String newName, int version);
在数据库Sql操作实现时可以如下
update goods set name=${newName}, version=${version} where id=${id} and version<${version}
乐观锁的更新操作,最好用主键或者唯一索引来更新,这样是行锁,否则更新时会锁表。
6. 状态机控制
这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100,付款失败为99。在做状态机更新时,我们就这可以这样控制。
update `order` set status=${status} where id=${id} and status<${status}
网友评论