在分布式系统、微服务架构大行其道的今天,服务间互相调用出现失败已经成为常态。如何处理异常,如何保证数据一致性,成为微服务设计过程中,绕不开的一个难题。在不同的业务场景下,解决方案会有所差异,常见的方式有:
本文侧重于其他几项,关于 2PC、3PC 传统事务,网上资料已经非常多了,这里不多做重复。
在微服务架构中,阻塞式重试是比较常见的一种方式。伪代码示例:
m := db.Insert(sql)
err := request(B-Service,m)
func request(url string,body interface{}){
for i:=0; i<3; i ++ {
result, err = request.POST(url,body)
if err == nil {
break
}else {
log.Print()
}
}
}
如上,当请求 B 服务的 API 失败后,发起最多三次重试。如果三次还是失败,就打印日志,继续执行下或向上层抛出错误。这种方式会带来以下问题
第一个问题:通过让 B 服务的 API 支持幂等性来解决。
第二个问题:可以通过后台定时脚步去修正数据,但这并不是一个很好的办法。
第三个问题:这是通过阻塞式重试提高一致性、可用性,必不可少的牺牲。
阻塞式重试适用于业务对一致性要求不敏感的场景下。如果对数据一致性有要求的话,就必须要引入额外的机制来解决。
在解决方案演化的过程中,引入队列是个比较常见也较好的方式。如下示例:
m := db.Insert(sql)
err := mq.Publish("B-Service-topic",m)
在当前服务将数据写入 DB 后,推送一条消息给 MQ,由独立的服务去消费 MQ 处理业务逻辑。和阻塞式重试相比,虽然 MQ 在稳定性上远高于普通的业务服务,但在推送消息到 MQ 中的调用,还是会有失败的可能性,比如网络问题、当前服务宕机等。这样还是会遇到阻塞式重试相同的问题,即 DB 写入成功了,但推送失败了。
理论上来讲,分布式系统下,涉及多个服务调用的代码都存在这样的情况,在长期运行中,调用失败的情况一定会出现。这也是分布式系统设计的难点之一。
在对事务有要求,且不方便解耦的情况下,TCC 补偿式事务是个较好的选择。
TCC 把调用每个服务都分成 2 个阶段、 3 个操作:
TCC 要求每个服务都实现上面 3 个操作的 API,服务接入 TCC 事务前一次调用就完成的操作,现在需要分 2 阶段完成、三次操作来完成。
比如一个商城应用需要调用 A 库存服务、B 金额服务、C 积分服务,如下伪代码:
m := db.Insert(sql)
aResult, aErr := A.Try(m)
bResult, bErr := B.Try(m)
cResult, cErr := C.Try(m)
if cErr != nil {
A.Cancel()
B.Cancel()
C.Cancel()
} else {
A.Confirm()
B.Confirm()
C.Confirm()
}
代码中分别调用 A、B、C 服务 API 检查并保留资源,都返回成功了再提交确认(Confirm)操作;如果 C 服务 Try 操作失败后,则分别调用 A、B、C 的 Cancel API 释放其保留的资源。
TCC 在业务上解决了分布式系统下,跨多个服务、跨多个数据库的数据一致性问题。但 TCC 方式依然存在一些问题,实际使用中需要注意,包括上面章节提到的调用失败的情况。
上面代码中如果 C.Try() 是真正调用失败,那下面多余的 C.Cancel() 调用会出现释放并没有锁定资源的行为。这是因为当前服务无法判断调用失败是不是真的锁定 C 资源了。如果不调用,实际上成功了,但由于网络原因返回失败了,这会导致 C 的资源被锁定,一直得不到释放。
空释放在生产环境经常出现,服务在实现 TCC 事务 API 时,应支持空释放的执行。
上面代码中如果 C.Try() 失败,接着调用 C.Cancel() 操作。因为网络原因,有可能会出现 C.Cancel() 请求会先到 C 服务,C.Try() 请求后到,这会导致空释放问题,同时引起 C 的资源被锁定,一直得不到释放。
所以 C 服务应拒绝释放资源之后的 Try() 操作。具体实现上,可以用唯一事务ID来区分第一次 Try() 还是释放后的 Try()。
Cancel 、Confirm 在调用过程中,还是会存在失败的情况,比如常见的网络原因。
Cancel() 或 Confirm() 操作失败都会导致资源被锁定,一直得不到释放。这种情况常见解决方案有:
理论上来讲非原子性、事务性的二段代码,都会存在中间态,有中间态就会有失败的可能性。
本地消息表最初是 ebay 提出的,它让本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来满足事务特性。
具体做法是在本地事务中插入业务数据时,也插入一条消息数据。然后在做后续操作,如果其他操作成功,则删除该消息;如果失败则不删除,异步监听这个消息,不断重试。
本地消息表是一个很好的思路,可以有多种使用方式:
示例伪代码:
messageTx := tc.NewTransaction("order")
messageTxSql := tx.TryPlan("content")
m,err := db.InsertTx(sql,messageTxSql)
if err!=nil {
return err
}
aErr := mq.Publish("B-Service-topic",m)
if aErr!=nil { // 推送到 MQ 失败
messageTx.Confirm() // 更新消息的状态为 confirm
}else {
messageTx.Cancel() // 删除消息
}
// 异步处理 confirm 的消息,继续推送
func OnMessage(task *Task){
err := mq.Publish("B-Service-topic", task.Value())
if err==nil {
messageTx.Cancel()
}
}
上面代码中其 messageTxSql 是插入本地消息表的一段 SQL :
insert into `tcc_async_task` (`uid`,`name`,`value`,`status`) values ('?','?','?','?')
它和业务 SQL 在同一个事务中去执行,要么成功,要么失败。
成功则推送到队列,推送成功,则调用 messageTx.Cancel() 删除本地消息;推送失败则标记消息为 confirm
。本地消息表中 status
有 2 种状态 try
、confirm
, 无论哪种状态在 OnMessage
都可以监听到,从而发起重试。
本地事务保障消息和业务一定会写入数据库,此后的执行无论宕机还是网络推送失败,异步监听都可以进行后续处理,从而保障了消息一定会推到 MQ。
而 MQ 则保障一定会到达消费者服务中,利用 MQ 的 QOS 策略,消费者服务一定能处理,或继续投递到下一个业务队列中,从而保障了事务的完整性。
示例伪代码:
messageTx := tc.NewTransaction("order")
messageTxSql := tx.TryPlan("content")
body,err := db.InsertTx(sql,messageTxSql)
if err!=nil {
return err
}
aErr := request.POST("B-Service",body)
if aErr!=nil { // 调用 B-Service 失败
messageTx.Confirm() // 更新消息的状态为 confirm
}else {
messageTx.Cancel() // 删除消息
}
// 异步处理 confirm 或 try 的消息,继续调用 B-Service
func OnMessage(task *Task){
// request.POST("B-Service",body)
}
这是本地消息表 + 调用其他服务的例子,没有 MQ 的引入。这种使用异步重试,并用本地消息表保障消息的可靠性,解决了阻塞式重试带来的问题,在日常开发中比较常见。
如果本地没有要写 DB 的操作,可以只写入本地消息表,同样在 OnMessage
中处理:
messageTx := tc.NewTransaction("order")
messageTx := tx.Try("content")
aErr := request.POST("B-Service",body)
// ....
配置本地消息表的 Try
和 Confirm
消息的处理器:
TCC.SetTryHandler(OnTryMessage())
TCC.SetConfirmHandler(OnConfirmMessage())
在消息处理函数中要判断当前消息任务是否存在过久,比如一直重试了一小时,还是失败,就考虑发邮件、短信、日志告警等方式,让人工介入。
TCC.SetTryHandler(OnTryMessage())
TCC.SetConfirmHandler(OnConfirmMessage())
在 Try
处理函数中,还要单独判断当前消息任务是否存在过短,因为 Try
状态的消息,可能才刚刚创建,还没被确认提交或删除。这会和正常业务逻辑的执行重复,意味着成功的调用,也会被重试;为尽量避免这种情况,可以检测消息的创建时间是否很短,短的话可以跳过。
重试机制必然依赖下游 API 在业务逻辑上的幂等性,虽然不处理也可行,但设计上还是要尽量避免干扰正常的请求。
独立消息服务是本地消息表的升级版,把本地消息表抽离成一个独立的服务。所有操作之前先在消息服务添加个消息,后续操作成功则删除消息,失败则提交确认消息。
然后用异步逻辑去监听消息,做对应的处理,和本地消息表的处理逻辑基本一致。但由于向消息服务添加消息,无法和本地操作放到一个事务里,所以会存在添加消息成功,后续失败,则此时的消息就是个无用消息。
如下示例场景:
func OnConfirmMessage(task *tcc.Task) {
if time.Now().Sub(task.CreatedAt) > time.Hour {
err := task.Cancel() // 删除该消息,停止重试。
// doSomeThing() 告警,人工介入
return
}
}
这个无用的消息,需要消息服务去确认这个消息是否执行成功,没有则删除,有继续执行后续逻辑。相比本地事务表 try
和 confirm
,消息服务在前面多了一种状态 prepare
。
有些 MQ 的实现支持事务,比如 RocketMQ 。MQ 的事务可以看作独立消息服务的一种具体实现,逻辑完全一致。
所有操作之前先在 MQ 投递个消息,后续操作成功则 Confirm
确认提交消息,失败则Cancel
删除消息。MQ 事务也会存在 prepare
状态,需要 MQ 的消费处理逻辑来确认业务是否成功。
从分布式系统实践中来看,要保障数据一致性的场景,必然要引入额外的机制处理。
TCC 的优点是作用于业务服务层,不依赖某个具体数据库、不与具体框架耦合、资源锁的粒度比较灵活,非常适用于微服务场景下。缺点是每个服务都要实现 3 个 API,对于业务侵入和改动较大,要处理各种失败异常。开发者很难完整处理各种情况,找个成熟的框架可以大大降低成本,比如阿里的 Fescar。
本地消息表的优点是简单、不依赖其他服务的改造、可以很好的配合服务调用和 MQ 一起使用,在大多业务场景下都比较实用。缺点是本地数据库多了消息表,和业务表耦合在一起。文中本地消息表方式的示例,来源于作者写的一个库,有兴趣的同学可以参考下 https://github.com/mushroomsir/tcc
MQ 事务和独立消息服务的优点是抽离出一个公共的服务来解决事务问题,避免每个服务都有消息表和服务耦合在一起,增加服务自身的处理复杂性。缺点是支持事务的 MQ 很少;且每次操作前都先调用 API 添加个消息,会增加整体调用的延迟,在绝大多数正常响应的业务场景下,是一种多余的开销。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/D380O0WOXMXRzdycQYcMTA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。