架构设计 | 幂等性


什么是幂等性

抄用一段数学上的定义:f(f(x)) = f(x)。x被函数f作用一次和作用无限次的结果是一样的。幂等性应用在软件系统中,我把它简单定义为:某个函数或者某个接口使用相同参数调用一次或者无限次,其造成的后果是一样的,在实际应用中一般针对于接口进行幂等性设计。举个栗子,在系统中,调用方A调用系统B的接口进行用户的扣费操作时,由于网络不稳定,A重试了N次该请求,那么不管B是否接收到多少次请求,都应该保证只会扣除该用户一次费用。

幂等性设计

幂等性一般应用于协议设计,TCP协议支持幂等吗?答案是肯定的,在网络不稳定时,操作系统可以肆无忌惮的重发TCP报文片段。TCP协议能够保证幂等的核心在于sequence number字段,一个序列号的在较长的一段时间内均不会出现重复。对于应用层的协议设计,原理和TCP是类似的,我们需要一个不重复的序列号。再简单一点说,在一个业务流程的处理中,我们需要一个不重复的业务流水号,以保证幂等性。

举个实际应用场景:用户A在网页上发起一笔游戏充值请求,浏览器引导用户去银行支付,支付成功后系统给用户进行充值。

协议设计上,我们通过全局唯一的充值订单号贯穿整个业务流程,使该业务支持幂等。

应用实现上,我们列举在银行支付成功后回调系统,进行充值的步骤进行说明。

func pay_notify(orderid,value,state){//有问题的实现  
         order = db.query("select * from payorder where orderid=$orderid");  
         check(order,orderid,value,state);//判断支付金额是否与订单金额一致,判断是否是支付成功回调。  
         if(order.state==未支付){  
            db.update("update payorder set state=已支付 where orderid=$orderid");  
            charge(order.username,value);//执行充值  
         }else{  
         return result("订单已处理")//返回订单已处理,或者返回处理成功  
        }  
    }

上述实现的问题在于,当回调出现并发时,order.state已经是脏读了,有可能重复充值,该实现并不能100%保证幂等。

列举三种改进方式:

  1. 悲观锁:select for update,整个执行过程中锁定该订单对应的记录;
  2. 乐观锁:affectrows = db.update(“update payorder set state=’已支付’ where orderid=$orderid and state=’未支付’ “),如果affectrows=1,执行充值,否则返回已处理;
  3. 定义notifylog表,orderid为unique key或者primary key,执行前,先insert,若insert成功则执行充值,否则返回已处理;

以上简单例子用以说明幂等性常用应用实现,在SOA化系统中,可能很多原子功能都被拆分到不同的进程里,如charge充值这个函数,可能在另一个进程中,那么整个业务的链路就会更长,可能回调成功了,但是充值失败。同理,只要充值接口保证幂等性,对于已经回调过但是充值结果未返回的请求,回调接收程序,应当重复发起充值请求。更深入更复杂的应用场景,在数据一致性中再细讲。

总结

业务层设计协议时,要求请求方定义不重复的业务流水号。应用实现时,利用数据库乐观锁、插入unique key的日志等方式保证并发时的幂等。

幂等性把关环节,在协议设计评审中,评审重要业务RPC或者http接口是否支持幂等,代码评审中,重点把关请求并发时,是否仍旧能够保证幂等性。设计人员和具体实现人员在实现过程中,也应该时刻自审幂等性的实现是否过关。

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/290360.html

(0)
上一篇 2022年10月2日
下一篇 2022年10月2日

相关推荐

发表回复

登录后才能评论