图解|订单系统中的补偿事务
阅读目录
一、背景
二、“大唐啥都有”网站的代码
三、SQL 中的事务
四、那如何优化无事务的代码?
五、如何解决无事务的问题?
六、具有补偿功能的解决方案
一、背景
悟空和师父一行人正在前往西天取经的路上,师父在线上买了一个福袋,订单状态显示订单已支付,但是电子福袋状态为未发送。
悟空来到了这家网站的后台,找到了开发人员“小黑熊”。
悟空:嘿,快查下我师父的订单,钱都给了,福袋怎么还没有到?
小黑熊:大圣,我们也收到异常通知了,更新福袋表的时候因网络原因导致福袋记录没有更新成功,所以福袋还是未发送的。
悟空:福袋没发出来,那为什么订单状态还一直是已支付?你这小儿,可不要瞒我!
小黑熊:大圣,我们数据库用的是MongoDB 3.0,不支持事务啊。
悟空:你说的事务是什么意思?
小黑熊:事务就是保持多个更新或删除或增加操作,要么都成功,要么都失败。
悟空:也就是说第一步顶单状态从未支付到订单成功已经执行成功了,但是第二步更新福袋的时候失败了,没有自动将第一步订单的状态给改回去?
小黑熊:是的,大圣。
悟空:那你们怎么没有退款啊?
小黑熊:大圣,我们也没有想到有这种异常发生。
悟空:容我看下你们的代码。
二、“大唐啥都有”网站的代码
该网站购物的内部逻辑简化后如下图所示:
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更新。
start transaction // 开始事务
try {
update order // 第一步,更新订单状态
update luckyBag // 第二步,更新福袋状态
commit // 提交两部操作的更改
} catch (excption e) {
rollback // 回滚所有操作
}
end transaction // 结束事务
更新订单状态和更新福袋状态两部操作成功,则全部提交到数据库执行,如果其中任意一步出现问题,则全部回滚,就像没有执行更新操作一样,以保证数据的一致性。
四、那如何优化无事务的代码?
由于MongoDB 3.0 不支持事务,所以很有可能出现数据不一致的情况(订单已支付,福袋未发送)。
那我们既然不能享受到事务的一致性,有什么办法来优化这部分代码呢?
我们先看下代码的时序图:
从上面的顺序图来看,分步保存是有问题的,第一步保存成功后,第二步如果保存失败,则数据不一致。那我们可以将保存往后移吗?
我们来看下优化后的时序图,整体将保存往后移。
伪代码如下:
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,退回福袋,更新订单状态失败,并发起退款
欢迎大家留言讨论自家系统是怎么做的?
本文分享自微信公众号 - 悟空聊架构(PassJava666)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
openGauss 理事会筹备会圆满成功
openGauss 理事会筹备会议在深圳大学城国际会议中心成功召开,会议重点讨论 openGauss 社区理事会章程草案,openGauss 社区开放治理迈出了新的一步。 本次理事会筹备会议邀请到国内著名的数据库技术方向上的多个公司、组织和机构,包括华为、招商银行、中国电信、云和恩墨、海量数据、人大金仓、神舟通用、虚谷伟业、快立方、亚信、超图软件、深信服、哈工大。 来自哈工大的教授、同时也是数据库领域的资深专家王宏志带领大家讨论了数据库的最新趋势,包括云边端协同,数据库赋能AI,数据库内核创新等。 openGauss 是一个面向全球的开源数据库社区,通过社区开放治理,吸引更多的厂商和开发者,参与数据库技术创新,营造良好的学术氛围,加强数据库基础人才的培养,推动数据库产业发展,将 openGauss 建设成一个有数据库情怀的开源社区。 openGauss 去年6月开源以来,总计迭代发布4个版本。2021年3月31日,openGauss 2.0.0 版本如期而至,该版本是openGauss社区发布的第一个Release版本,新增众多新特性: 4P 鲲鹏性能达到230万tpmC,满足1.5倍...
- 下一篇
为什么 DNS 协议使用 UDP?只使用了 UDP 吗?
为什么 DNS 协议使用 UDP 呢?这个问题可能大部分同学在各种博客或者面试过程中都或多或少遇见过,张口就来,UDP 快啊,DNS 使用 UDP 使得打开网页速度更快。 那各位有没有想过,既然 UDP 更快,为什么 HTTP 不使用 UDP 呢? 另外,为什么 DNS 协议使用 UDP 这个问题本身其实并不完全正确,DNS 并非只使用 UDP 协议,它同时占用了 UDP 和 TCP 的 53 端口,作为单个应用层的协议,DNS 同时使用两种传输协议也属实是个另类了。 DNS 为什么同时使用 TCP 和 UDP 我们从 TCP 与 UDP 的比较说起,老生常谈的话题,不过相信大部分同学都会忽略掉一个点,等下会指出来。 OK,轻松环节,闭着眼睛背: 1)TCP 需要三次握手建立连接,四次挥手释放连接;UDP 不需要,面向无连接 2)TCP 首部需要 20 个字节;而 UDP 首部只有 8 个字节 3)TCP 具有一系列保证可靠传输的机制;而 UDP 尽最大努力交付,不提供可靠传输的机制,如果在数据传输的过程中出现部分数据的丢失,UDP 协议本身并不能做出任何检测或补救措施 4)正是由于 ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果