从学生项目到真实系统

从学生项目到真实系统:为什么工作后更关注一致性、重试和并发

学生时期写代码,大多数时候都比较直接。

比如:

  • 写个小工具
  • 做课程项目
  • 刷算法题

通常只要程序能跑、结果正确,任务基本就完成了。

但真实系统里,问题往往不是:

代码能不能跑。

而是:

程序做到一半出问题了怎么办。

这篇文章尝试用一个简单的电商例子,介绍工作里很常见的一些真实问题。


单机系统:事务通常就够了

假设写一个电商系统。

用户下单以后,需要做两件事:

  • 创建订单
  • 扣库存

如果这两个操作都在同一个程序、同一个数据库里,其实比较简单。

因为可以使用事务。

事务的意思很简单:

要么一起成功,要么一起失败。

例如:

  • 订单创建成功
  • 扣库存失败

数据库会自动回滚。

这样系统不会停留在“做了一半”的状态。


真实系统通常是分布式的

真实业务变复杂以后,通常会拆成多个服务。

例如:

  • 订单服务:负责订单
  • 库存服务:负责库存
  • 通知服务:负责通知

这样做有几个好处:

  • 每个系统只负责自己的职责
  • 可以独立开发、独立部署
  • 后期维护和扩展更容易

这就是:

分布式系统。

简单理解就是:

原来一个程序做完的事情,现在由多个程序协作完成。


为什么事务不够用了

继续用下单举例。

订单服务先做:

第一步

创建订单。

数据库里出现:

订单 1001 已创建

然后:

第二步

通知库存服务扣库存。

如果此时程序突然挂了,会发生什么?

可能出现:

  • 订单已经创建
  • 库存还没扣
  • 库存服务根本不知道有这个订单

系统停在了:

做了一半的状态。

原因很简单。

这里已经不是:

  • 一个程序
  • 一个数据库

而是:

  • 订单服务
  • 库存服务

中间还隔着网络。

已经无法像单机事务那样:

一起提交,一起回滚。

所以分布式系统通常采用另一种思路:

允许中间失败,但系统最终要恢复到正确状态。

这就是:

最终一致性。


MQ(消息队列)是什么

实现最终一致性时,常常会用到 MQ(消息队列)

可以把它理解成:

一个任务待办列表。

例如:

订单创建完成后,不直接同步调用库存服务。

而是先放入一条消息:

订单 1001 需要扣库存

库存服务稍后自己来处理。

这样做有几个好处:

  • 用户不用一直等待
  • 某个服务短暂不可用时,后续仍然可以继续处理
  • 系统之间耦合更低

MQ 带来的新问题:重试

假设库存服务拿到消息后开始处理。

可能要执行:

  • 查询库存
  • 扣库存
  • 更新状态

如果执行到一半程序挂了,会发生什么?

MQ 会认为:

这次处理失败了

然后稍后重新投递这条消息。

这就是:

重试。


为什么重试会变复杂

问题在于:

系统此时并不知道上一次到底做到了哪里。

可能是:

  • 库存已经扣了
  • 扣到一半
  • 根本还没开始

所以第二次不能无脑重新执行。

系统需要先判断:

  • 上次做到哪一步?
  • 哪一步已经完成?
  • 这次应该从哪里继续?

例如:

  • 订单已经创建 → 跳过
  • 库存已经扣减 → 不重复扣
  • 通知还没发 → 补发通知

也就是说:

重试的核心不是重新开始,而是从正确位置恢复。

这也是为什么真实系统里经常强调:

  • 幂等
  • 状态推进
  • 恢复能力

并发:多个程序可能同时处理同一件事

除了“做到一半失败”,还有一个非常常见的问题:

多个程序可能同时处理同一件事。

例如:

系统中有一个任务:

订单 1001 扣库存

由于某些原因,可能出现两个程序几乎同时处理它:

  • 消息重复投递
  • 上一次处理超时
  • 程序发生重试

于是两个程序同时开始执行。

它们都先查数据库:

发现还没有处理

然后都继续往下做。

结果可能变成:

  • 库存被扣两次
  • 数据被重复创建
  • 状态被覆盖

这就是:

并发问题。


一个直接的办法:加锁

一个自然的想法是:

先加锁,只允许一个程序处理。

这样当然可以解决问题。

但加锁也有代价:

  • 会影响吞吐量
  • 分布式下锁本身也会复杂很多
  • 锁设计不当容易引入新的问题

所以很多系统不会优先选择锁。


更常见的做法:唯一约束

更常见的方式是:

用数据库保证最终只有一个成功。

例如规定:

一个订单只能扣一次库存

然后在数据库里加唯一约束。

这样即使两个程序同时执行:

  • 第一个成功
  • 第二个失败

数据库会自动兜底。

也就是说:

最终只有一个会成功。

很多时候这比提前加锁更自然。


为什么很多时候还会先查一次

这里经常会有一个问题:

既然已经有唯一约束,为什么不直接插入?

逻辑上通常确实可以。

但很多时候还是会先做一次判断。

例如:

  • 看这一步是不是已经做过
  • 如果已经做过,就直接跳过

这样做主要不是为了性能。

而是为了:

让链路更干净。

如果每次都直接插入:

  • 第二次一定报错
  • 日志里会出现很多失败记录
  • 排查问题时容易被干扰

因此很多时候会先判断:

这一步是不是已经完成

完成了就跳过。


真实系统关注的重点已经发生变化

学生项目通常更关注:

代码怎么写。

真实系统里更关注的是:

  • 当前做到哪一步
  • 哪一步已经成功
  • 下一次应该从哪里继续
  • 重试会不会重复执行
  • 并发情况下会不会出问题

换句话说:

系统在失败、重试和并发下,是否仍然能够恢复到正确状态。


小结

单机程序里,事务通常可以解决大部分问题。

但分布式系统里,会自然出现:

  • 中途失败
  • 消息重试
  • 状态不确定
  • 并发执行
  • 顺序错乱

因此系统设计的重点会从:

如何完成一次执行

转变成:

如何在各种异常情况下仍然保证最终正确。

这也是工程实践中非常重要的一类问题。

updatedupdated2026-05-032026-05-03