谈谈web系统中的可重入,幂等性,分布式事务的那些事 – 上

先说说我对这几个概念的理解吧。

  • 可重入:在并发访问下,仍旧可以保证正确性。
  • 幂等性:相同的程序输入(重复输入多次),获得相同的程序输出(或者说产生相同的影响)。
  • 分布式事务:与单机事务相对,跨越网络,多实例,保证N件事情都做或者都不做。

如果是一个单机程序,

可重入可以通过加互斥锁解决,或者只用单线程,通过串行化访问绕开并发,正确性就得到保证。

幂等性在单机中理解起来比较困难,与api的语意关联很大,如果写一个程序统计网站uv,那么函数与输入大概这样:function stat(uid,day),同1用户同1天输入N次访问日志,最终产生的效果就是uv+1,而不是uv+N,按照相同的输入(重复输入多次)获得相同的程序输出(产生相同的影响,理解更加感性一点)。

分布式事务在单机中不存在,单机事务可以简化为一个单线程程序,收到一个完整的事务操作序列后,一次性全部做完,然后再继续响应其他请求即可,ACID都可以得到保证。

如果代码运行在很多服务器上,机器经常性的宕机,网络偶尔会堵塞或者中断,那么又如何保证服务能够正常运行呢?

假设实现一个交易订单系统,那么订单通常会经历几个基本状态:未付款,过期未支付,已付款,已使用,退款中,已退款。基于这个背景,分别看看有什么办法解决上述问题,让程序继续正确的运行下去。

可重入

因为订单(order)保存在mysql中,许多php程序会并发的去修改订单,最简单的办法就是在mysql中锁住订单所在的行记录,也就是在一个transaction中执行select for update,但是并不建议使用大量的慢事务和行锁,对innodb带来了很多锁管理成本和并发能力限制,所以不建议这样做。

mysql行锁是一种悲观锁,只能阻塞排队逐个修改。其实在解决可重入问题时,我们一般采用乐观锁来防止并发引起的数据更新错误,以付款场景为例,先来看看问题本质:

  1. 微信支付系统调用我们的接口,告知我们收到了order订单付款。
  2. 从mysql读取这个order。
  3. 如果order的status是”待付款”,那么调用mysql更新order为”已付款”。
  4. 如果order的status是”过期未支付”,那么拒绝微信支付的这笔付款,原路退回给用户。

如果步骤3发现order为”待付款”之后暂停几秒,在同一时刻有一个crontab定时任务通过扫描mysql发现订单超过30分钟没有支付,于是将order状态改为了”过期未支付”。此后步骤3继续执行,又将订单改为了”已付款”,那么相当于用户绕过了我们的支付时间限制,成功进行了付款,这是我们不允许的。

如果我们在步骤2中以及crontab任务中均通过select for update锁住这一个order,那么就不可能出现上述的并发场景了。

如果我们采用乐观锁思路,步骤2不加锁,将步骤3的更新语句改为update order_table set status=”已付款” where order_id=xxx and status=”待付款”, 其中红色部分是新增部分,通过增加这个判定,在上面的并发场景下,mysql的更新将不生效,因为update执行之前crontab已经将mysql中的status变更为”过期未支付”了。

此时,我们的代码逻辑发现update没有生效,那么就可以认为有其他并发更新的插入,所以理论上应当重新查询order获取最新状态重新处理,此时发现order的status已经变更为”过期未支付”,最终会进入步骤4,将这笔付款原路打回。实际上,为了代码的逻辑的清晰性与架构合理性,通常并不会立即重查订单,而是抛出一个异常结果给上游(微信支付),由上游(微信支付)稍后重新调用我们,从而触发新一轮的完整逻辑。

谈谈web系统中的可重入,幂等性,分布式事务的那些事 – 中 

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~