当前位置:首页 > 问答 > 正文

分布式事务那些坑和前后端分离部署时遇到的各种奇怪问题探讨

主要来源于我在实际工作中,特别是在一家电商公司进行系统架构升级时的亲身经历,以及和几位后端架构师同事的讨论总结,我们当时正从传统的单体应用向微服务架构迁移,并且前端也彻底分离出去,独立部署,这个过程可以说是“坑”出不穷,尤其是分布式事务和前后端分离部署这两个方面,遇到了很多书本上没写的奇怪问题。

第一部分:分布式事务的那些“坑”

来源:主要来自我们重构订单和支付系统时的惨痛教训。

分布式事务听起来很高大上,但简单说就是:一个业务操作需要调用好几个独立的服务(比如下单要同时调用订单服务、库存服务、优惠券服务),我们必须保证这些服务要么全部成功,要么全部失败,不能出现订单创建了但库存没扣,或者优惠券用了但订单没成的情况。

我们最开始尝试的是所谓的“两阶段提交”(2PC),这被很多人认为是标准解法,但一用就发现坑太大了。第一个坑是“性能坑”,两阶段提交有个准备阶段和提交阶段,需要所有参与的服务都锁住相关的资源(比如锁住库存、锁住优惠券),等待事务协调者发号施令,这个等待过程非常漫长,在高并发下单的场景下,大量请求被阻塞,系统响应速度慢得像蜗牛,数据库连接也很快被占满,直接导致服务不可用,这就像一群人要一起出门,必须等最慢的那个人磨蹭完,所有人才能动,效率极低。

第二个坑是“协调者单点故障坑”,如果那个负责发号施令的事务协调者服务自己挂掉了,那就全乱套了,那些已经执行了第一步、锁定了资源的服务会一直傻等着下一步指令,这些资源就被永远锁住了,形成“死锁”,比如库存被锁定了但无法完成扣减或释放,其他用户就看到有货但不能买,这是致命的。

因为这些坑太深,我们后来转向了更流行的“最终一致性”方案,比如基于消息队列的 Saga 模式,简单说就是,把一个大的分布式事务拆成一系列本地小事务,每个小事务完成后,通过发消息触发下一个事务,如果某个小事务失败了,就执行一系列补偿操作(比如扣库存成功了,但创建订单失败了,那就发个消息再触发一个“释放库存”的操作)。

但这个方案也有自己的坑。最大的坑是“补偿操作的幂等性和可靠性”。“幂等性”就是说,同一个补偿操作你执行一次和执行多次效果要一样,因为网络可能会重传,你可能收到多条一样的补偿消息,如果释放库存的代码没做好幂等,可能因为重复执行导致库存数量不对,补偿消息本身也可能丢失,导致数据最终无法一致,这就要求我们在写每一个服务和补偿逻辑时,都要像侦探一样思考各种异常情况,代码复杂度大大增加,这感觉就像是拆除了一个大地雷,却换来了满屋子需要小心翼翼处理的小炸弹。

第二部分:前后端分离部署的奇怪问题

来源:前端团队和后端团队在分离部署后,日常联调和支持中遇到的五花八门的问题。

前后端分离后,前端代码(比如用 Vue 或 React 写的)部署在一个独立的服务器或 CDN 上,后端则是一堆 API 服务,浏览器先加载前端页面,然后页面里的 JavaScript 代码再通过 HTTP 请求去调用后端的 API,这个模式听起来清晰,但奇怪问题特别多。

第一个最常见的问题是“跨域”(CORS),这是前后端分离的“入门坑”,因为浏览器出于安全考虑,默认禁止一个域名下的网页去请求另一个不同域名(或同域名不同端口)的接口,我们明明前后端都部署在公司内网,但就因为是frontend.company.comapi.company.com这样不同的子域名,浏览器就无情地拦截了请求,虽然解决方法是在后端配置 CORS 响应头,但这个问题在开发、测试、生产不同环境切换时经常被遗忘或配错,导致前端页面一片空白或者报一堆看不懂的跨域错误。

第二个奇怪问题是“Cookie 和认证的坑”,用户登录后,后端通常会下发一个 Cookie 或 Token 来维持登录状态,在分离部署下,前端域名和后端 API 域名不同,Cookie 的传递就成了问题,后端设置在api.company.com的 Cookie,默认情况下前端页面frontend.company.com是发不出去的,我们不得不使用withCredentials等一些特殊设置,并且要确保后端 CORS 配置允许凭证传输,这个过程非常琐碎,一旦某个环节配置不对,就会出现“明明登录成功了,但一下个请求就提示未登录”的灵异现象。

第三个问题更隐蔽,是“环境切换和配置的坑”,在开发时,前端可能直接连本地启动的后端服务(localhost:8080);测试时,要连测试环境的 API 地址(test-api.company.com);上线了,又要改成生产环境的地址(api.company.com),如何让前端代码在不同环境下自动切换后端 API 基地址,成了一个配置管理难题,如果打包时配置错了,就可能发生“测试环境的前端连到了生产环境的数据库”这种灾难性事故。

第四个问题是“路由和刷新的坑”,前端使用 Vue Router 或 React Router 做了很漂亮的路由,比如有一个商品详情页的路由是/product/123,你在浏览器里点击页面内的链接跳转过去,一切正常,但如果你在这个页面直接按 F5 刷新,浏览器会把这个路由/product/123当作一个真实的路径去向静态资源服务器请求,但静态服务器上根本没有这个文件,于是返回 404 错误,解决这个问题需要在静态服务器(如 Nginx)上做配置,把所有前端路由的请求都重定向到index.html,再由前端路由来接管,这个问题不遇到则已,一遇到就让新手百思不得其解。

分布式事务和前后端分离都是现代软件开发中提升效率和可扩展性的好方法,但它们绝不是银弹,它们引入的复杂性远远超出我们的初始想象,需要开发者和架构师对网络、协议、浏览器行为、分布式系统有更深刻的理解,并且要具备极强的排查和解决各种非预期问题的能力,这些“坑”和“奇怪问题”正是技术成长路上最真实的挑战。

分布式事务那些坑和前后端分离部署时遇到的各种奇怪问题探讨