今天就讨论一个问题,幂等设计
幂等设计是指无论操作执行多少次,结果都相同的设计原则。在分布式系统中尤为重要,可以防止重复请求导致的重复操作,产生不必要的异常数据。
这个问题往往与另一个分布式问题有所牵连,即:分布式系统数据一致性问题。幂等设计可以在某些场景下用于解决一致性问题。
在实际业务中,非幂等设计往往也不会有什么大的问题,只有少数一些场景需要这么做,但这确实是一个比较关键,且简单易用的设计方案,简单就意味着不容易出错,故而掌握这种设计方法对做好 CRUD 工作同样非常重要!
那么为什么需要幂等设计?
幂等设计的思想并不新颖,老程序员应该都遇到过一个问题,印象中互联网刚盛行的时候,技术比较差,前台用户往往容易双击甚至多击操作,以“加快”其操作,但这种操作很有可能造成数据重复创建,所有个技术叫表单防重复提交,这东西的设计思想与幂等设计是同宗的,核心理念都是通过对提交数据中的某个特定数据不变性进行验证,以防止操作重复执行。而在当下分布式时代,只是把前端接口改成了服务调用,其道理是一样的。
产生这种重复提交的原因主要有两类:
- 业务原因,某些场景业务上天然存在并发请求的情况,而最后只允许保留一个结果;这种重复请求场景往往伴随着并发;
- 技术原因,因为是 RPC 操作,不能百分百保证一个请求在结束后,一定会有结果,但没有结果也并不表示在对方系统中操作是失败的,所以请求方可能会设计一些重试机制,这种重复请求场景往往一前一后;
解决的基本思路也简单:先判断是否存在,若不存在则写入,否则直接返回,更新也是一样的道理,无非是存在与否,其返回结果保持一致即可。话虽如此,但却有些细节需要注意。
一者,何为不变的东西,当我们要求一个服务为我保存一项数据的时候,首先应考虑该数据与我自己服务中的某些数据如何关联起来,而这就是关键,举个例子:用户创建一个购买订单,同时需要去创建一个支付订单,但支付订单创建的服务调用未必能够成功返回结果,若遇到这种情况,系统或用户很有可能会进行重试操作,而此时若创建多个支付订单则可能与业务期望不符,故需要对该接口做幂等设计,以防止重复创建支付订单,而判定的依据就是销售订单号。也就是同一个销售订单编号,只允许创建一个支付订单。
二是并发问题,一般来说,幂等设计的过程很简单,if 不存在 then 写入,但在并发场景下,这是一个明显的竞态条件写法,因此,他并不能阻止并发写入,此时如何保证并发写的时候也只有一条数据?比较简便的办法就是用数据库的唯一索引,通过DupicateKeyException异常进行处理,完整的处理过程如下:
有些人会用redis来解决幂等问题,我不是很赞成,一来redis的设计终归是缓存,与持久化设计的数据库多少有些差别,在某些场景下可能存在没拦住的情况,二来,根据业务情况,为表设计一个biz_no用来表示该数据与其他业务的关联关系,是一个很基本的设计,且数据库作为最后的保障,必须要保障该数据的唯一性,既如此,何必再用redis;有人可能会说redis快,但按唯一索引查询如果还不能快,那这个数据库设计本身就已经有问题了,其实我的实践结果看来,并不会比数据库快多少。
当然数据库也不是无脑用就行,也有一些特殊场景会导致问题出现,比如主从分离就有可能会造成一些问题,查询时若从从库查,但主库还未同步过来,就可能导致第一道拦不住的情况!
三者,调用者和被调用者,都应该互相记录单据的信息,以便在未来需要的时候,进行查询和重试等操作。这里指的单据信息,就是代表各自业务数据的唯一编号或其他信息,在上面这个例子,订单系统在调用完支付系统后,应更新其订单信息,保存支付订单号,而支付系统则在创建时,应在支付订单中保存订单编号,这么做给后面留足了可操作空间,系统扩展性更好!