前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >故事|黑熊精 揭秘「补偿事务」

故事|黑熊精 揭秘「补偿事务」

作者头像
悟空聊架构
发布2022-05-13 14:34:21
4410
发布2022-05-13 14:34:21
举报
文章被收录于专栏:悟空聊架构 | 公众号

作者 | 悟空聊架构

阅读目录

  • 一、背景
  • 二、“大唐啥都有”网站的代码
  • 三、SQL 中的事务
  • 四、那如何优化无事务的代码?
  • 五、如何解决无事务的问题?
  • 六、具有补偿功能的解决方案

一、背景

悟空和师父一行人正在前往西天取经的路上,师父在线上买了一个福袋,订单状态显示订单已支付,但是电子福袋状态为未发送。

悟空来到了这家网站的后台,找到了开发人员小黑熊

悟空:嘿,快查下我师父的订单,钱都给了,福袋怎么还没有到? 小黑熊:大圣,我们也收到异常通知了,更新福袋表的时候因网络原因导致福袋记录没有更新成功,所以福袋还是未发送的。 悟空:福袋没发出来,那为什么订单状态还一直是已支付?你这小儿,可不要瞒我! 小黑熊:大圣,我们数据库用的是 MongoDB 3.0,不支持事务啊。 悟空:你说的事务是什么意思? 小黑熊:事务就是保持多个更新或删除或增加操作,要么都成功,要么都失败。 悟空:也就是说第一步顶单状态从未支付到订单成功已经执行成功了,但是第二步更新福袋的时候失败了,没有自动将第一步订单的状态给改回去? 小黑熊:是的,大圣。 悟空:那你们怎么没有退款啊? 小黑熊:大圣,我们也没有想到有这种异常发生。 悟空:容我看下你们的代码。

二、“大唐啥都有”网站的代码

该网站购物的内部逻辑简化后如下图所示:

代码语言:javascript
复制
try {
        order.status = "已支付"; //第一步,更新订单状态:订单已支付
        order.save(); //保存订单
        luckyBag.status = "已发送"; // 第二步,更新福袋状态:福袋已发送
        luckyBag.save(); //保存福袋
        goodCounts.count -= 1;// 第三步,更新库存
        goodCounts.save(); // 保存库存
        order.status="订单成功" // 第四步,更新订单状态:订单成功
        order.save(); //保存订单
    }
    catch (excption e) {
        logError();
    }

那这样的代码会有什么问题呢?

如果第一步执行成功,第二步执行失败了,抛出了异常,则第一步订单状态还是订单成功的,福袋状态未更新,也就是师父遇到的问题。

那如何保证两步操作的一致性呢?(要么都更新,要么都不更新。)

我们都知道SQL中是有事务这种解决方案的,我们先来看看SQL中的事务。

三、SQL 中的事务

之前写过一篇文章,专门来讲SQL中的事务:《30分钟全面解析-SQL事务+隔离级别+阻塞+死锁》。在这里用伪代码来说明下什么事务。

举个购买商品的例子:用户下了一笔单,付款了,然后发放福袋,涉及到订单表order更新,福袋表luckyBag更新。

代码语言:javascript
复制
start transaction // 开始事务
  try {
          update order // 第一步,更新订单状态
           update luckyBag // 第二步,更新福袋状态
           commit // 提交两部操作的更改
   } catch (excption e) {
        rollback // 回滚所有操作
     }
end transaction // 结束事务

更新订单状态和更新福袋状态两部操作成功,则全部提交到数据库执行,如果其中任意一步出现问题,则全部回滚,就像没有执行更新操作一样,以保证数据的一致性。

四、那如何优化无事务的代码?

由于MongoDB 3.0 不支持事务,所以很有可能出现数据不一致的情况(订单已支付,福袋未发送)。

那我们既然不能享受到事务的一致性,有什么办法来优化这部分代码呢?

我们先看下代码的时序图:

从上面的顺序图来看,分步保存是有问题的,第一步保存成功后,第二步如果保存失败,则数据不一致。那我们可以将保存往后移吗?

我们来看下优化后的时序图,整体将保存往后移。

伪代码如下:

代码语言:javascript
复制
try {
        order.status = "已支付"; //第一步,更新订单状态:订单已支付
        luckyBag.status = "已发送"; // 第二步,更新福袋状态:福袋已发送
        goodCounts.count -= 1;// 第三步,更新库存 
        order.status="订单成功" //第一步,更新订单状态:订单已支付
 
        luckyBag.save(); //保存福袋记录
        goodCounts.save(); // 保存库存记录
        order.save(); //保存订单记录
    }
    catch (excption e) {
        logError();
    }

那这种方式又有什么优缺点呢?

优点:前四步的业务逻辑处理任意一步如果出错了,并不会影响数据库的记录

缺点:后三步的保存如果出错了,和最开始的方案一样,存在数据不一致的问题。

那如何进行解决这种问题?

五、如何解决无事务的问题?

优化后的代码还是可能存在数据不一致的情况,那我们怎么来解决?

问题 1:如果福袋没有自动发出去,现在还可以补发吗?怎么补发?

问题 2:可以退款吗?手动退款还是自动退款?分别有什么优点和缺点?怎么优化?

问题 3:如果第三步更新库存失败,那又该怎么做呢?

问题 4:如何退款失败,那又该怎么做呢?

围绕上面几个问题,我们展开来论述。

问题1.1:对于补发问题,我们怎么来补发呢?

方案1:第二步失败时,立即重试几次(第一次 3s,第二次间隔 8s,第三次间隔 20s,为什么间隔时间不一样?可以留言讨论哦!^_^)

方案2:将失败的数据放到队列里面(可以是存到数据库或者 redis 里面,建议存放到数据库),定时从队列里面获取异常数据,进行重新发送。

问题 1.2:自动补发的优点和缺点分别是什么呢?

方案1的优点和缺点

优点

(1)如果是临时出现的网络问题,可以立即在短时间内重试几次,可以解决问题。

缺点

(1)如果是接口或数据问题,短时间内重试再多次也是会失败的;

(2)另外如果有大量失败,重试也是会占用系统资源的。

方案 2 的优点和缺点

优点

(1)将重试放到异步任务中来做,可以减少系统资源的占用;

(2)如果是长时间出现的网络问题,等网络恢复后,一定会重试成功;

缺点

(1)异常数据无法通过重试来解决,则队列里面的数据将一直会进行重试,无法终止;

(2)如果有大量数据因接口或代码问题导致失败,则会积累大量失败数据,而大量数据进行重试也会对系统资源造成一定压力;

(3)重试失败会进行error log的记录,大量的error log对线上排查问题会造成干扰。

那补发如果一直失败,是不是还有更好的方式?给用户退款是不是更合理?(顾客等得很着急,赶紧把钱先退了吧。)这其实就是一种补偿措施

问题 2.1 可以退款吗?

当然可以退款。

问题 2.2 自动退款的优缺点?

优点:减少运营人员的工作量

缺点:在某些情况下,异常订单需要多方排查核实才能退款,就不能走自动退款。比如代码的逻辑没有handle某些场景,一刀切的退款会导致钱退了,商品还发给了客户。

问题 2.3 怎么优化?

那怎么优化?提供自动和手动的两种方式,当某些异常场景需要手动退款的,等开发人员核实后,再进行手动退款。

账不平怎么处理?通过对账的方式找出哪些账不平。

问题 3 第三步更新库存失败怎么处理?

我们很容易想到的方案是及时retry或 队列retry。那有什么问题呢?对于秒杀活动,队列retry肯定不可行。

那我们可以做一次补偿操作吗?(发起退款,更新订单状态为失败。)

答案是可以的。

问题 4 如果退款失败怎么处理

每一步失败我们都会做补偿处理,但是中间某一步补偿失败,我们该怎么处理?比如最后钱退不了。

常见方案:

  • 1.退款失败后主动报警通知运维人员或开发人员
  • 2.手动退款(缺点:人工操作,容易出错,比如找订单找错了)
  • 3.加入队列,自动退款(缺点:一般退款失败都是代码级别问题或微信侧问题,所以还是需要排查问题原因,在这期间,所有退款失败异常都会报警,对日常的监控造成不必要的干扰)

在我现在做的项目都会将退款失败的消息以下面两种形式推送给我:

  • 1.微信的模板消息
  • 2.云服务商提供的日志报警短信服务

这样方便我去排查问题,以及快速退款。

模板消息

短信告警

或者用钉钉机器人报警,这里就不展开了。

六、具有补偿功能的解决方案

我们可以设计一个具有补偿功能的解决方案。

流程图如下所示:

  • 1.如果第一步失败,则发起退款
  • 2.如果第二步失败,则更新订单状态为失败,并发起退款
  • 3.如果第三步更新库存失败,则退回福袋,且更新订单状态为失败,并发起退款
  • 4.如果第四步更新订单为成功时失败,则库存 +1,退回福袋,更新订单状态失败,并发起退款

悟空顺利解决了问题,黑熊精崇拜地看着悟空,默默地去改代码去了...

- END -

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-06-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 悟空聊架构 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景
  • 二、“大唐啥都有”网站的代码
  • 三、SQL 中的事务
  • 四、那如何优化无事务的代码?
  • 五、如何解决无事务的问题?
    • 问题1.1:对于补发问题,我们怎么来补发呢?
      • 问题 1.2:自动补发的优点和缺点分别是什么呢?
        • 方案1的优点和缺点
        • 方案 2 的优点和缺点
      • 问题 2.1 可以退款吗?
        • 问题 2.2 自动退款的优缺点?
          • 问题 2.3 怎么优化?
            • 问题 3 第三步更新库存失败怎么处理?
              • 问题 4 如果退款失败怎么处理
              • 六、具有补偿功能的解决方案
              相关产品与服务
              数据库
              云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档