从学生项目到真实系统:为什么工作后更关注一致性、重试和并发
学生时期写代码,大多数时候都比较直接。
比如:
- 写个小工具
- 做课程项目
- 刷算法题
通常只要程序能跑、结果正确,任务基本就完成了。
但真实系统里,问题往往不是:
代码能不能跑。
而是:
程序做到一半出问题了怎么办。
这篇文章尝试用一个简单的电商例子,介绍工作里很常见的一些真实问题。
单机系统:事务通常就够了
假设写一个电商系统。
用户下单以后,需要做两件事:
- 创建订单
- 扣库存
如果这两个操作都在同一个程序、同一个数据库里,其实比较简单。
因为可以使用事务。
事务的意思很简单:
要么一起成功,要么一起失败。
例如:
- 订单创建成功
- 扣库存失败
数据库会自动回滚。
这样系统不会停留在“做了一半”的状态。
真实系统通常是分布式的
真实业务变复杂以后,通常会拆成多个服务。
例如:
- 订单服务:负责订单
- 库存服务:负责库存
- 通知服务:负责通知
这样做有几个好处:
- 每个系统只负责自己的职责
- 可以独立开发、独立部署
- 后期维护和扩展更容易
这就是:
分布式系统。
简单理解就是:
原来一个程序做完的事情,现在由多个程序协作完成。
为什么事务不够用了
继续用下单举例。
订单服务先做:
第一步
创建订单。
数据库里出现:
订单 1001 已创建
然后:
第二步
通知库存服务扣库存。
如果此时程序突然挂了,会发生什么?
可能出现:
- 订单已经创建
- 库存还没扣
- 库存服务根本不知道有这个订单
系统停在了:
做了一半的状态。
原因很简单。
这里已经不是:
- 一个程序
- 一个数据库
而是:
- 订单服务
- 库存服务
中间还隔着网络。
已经无法像单机事务那样:
一起提交,一起回滚。
所以分布式系统通常采用另一种思路:
允许中间失败,但系统最终要恢复到正确状态。
这就是:
最终一致性。
MQ(消息队列)是什么
实现最终一致性时,常常会用到 MQ(消息队列)。
可以把它理解成:
一个任务待办列表。
例如:
订单创建完成后,不直接同步调用库存服务。
而是先放入一条消息:
订单 1001 需要扣库存
库存服务稍后自己来处理。
这样做有几个好处:
- 用户不用一直等待
- 某个服务短暂不可用时,后续仍然可以继续处理
- 系统之间耦合更低
MQ 带来的新问题:重试
假设库存服务拿到消息后开始处理。
可能要执行:
- 查询库存
- 扣库存
- 更新状态
如果执行到一半程序挂了,会发生什么?
MQ 会认为:
这次处理失败了
然后稍后重新投递这条消息。
这就是:
重试。
为什么重试会变复杂
问题在于:
系统此时并不知道上一次到底做到了哪里。
可能是:
- 库存已经扣了
- 扣到一半
- 根本还没开始
所以第二次不能无脑重新执行。
系统需要先判断:
- 上次做到哪一步?
- 哪一步已经完成?
- 这次应该从哪里继续?
例如:
- 订单已经创建 → 跳过
- 库存已经扣减 → 不重复扣
- 通知还没发 → 补发通知
也就是说:
重试的核心不是重新开始,而是从正确位置恢复。
这也是为什么真实系统里经常强调:
- 幂等
- 状态推进
- 恢复能力
并发:多个程序可能同时处理同一件事
除了“做到一半失败”,还有一个非常常见的问题:
多个程序可能同时处理同一件事。
例如:
系统中有一个任务:
订单 1001 扣库存
由于某些原因,可能出现两个程序几乎同时处理它:
- 消息重复投递
- 上一次处理超时
- 程序发生重试
于是两个程序同时开始执行。
它们都先查数据库:
发现还没有处理
然后都继续往下做。
结果可能变成:
- 库存被扣两次
- 数据被重复创建
- 状态被覆盖
这就是:
并发问题。
一个直接的办法:加锁
一个自然的想法是:
先加锁,只允许一个程序处理。
这样当然可以解决问题。
但加锁也有代价:
- 会影响吞吐量
- 分布式下锁本身也会复杂很多
- 锁设计不当容易引入新的问题
所以很多系统不会优先选择锁。
更常见的做法:唯一约束
更常见的方式是:
用数据库保证最终只有一个成功。
例如规定:
一个订单只能扣一次库存
然后在数据库里加唯一约束。
这样即使两个程序同时执行:
- 第一个成功
- 第二个失败
数据库会自动兜底。
也就是说:
最终只有一个会成功。
很多时候这比提前加锁更自然。
为什么很多时候还会先查一次
这里经常会有一个问题:
既然已经有唯一约束,为什么不直接插入?
逻辑上通常确实可以。
但很多时候还是会先做一次判断。
例如:
- 看这一步是不是已经做过
- 如果已经做过,就直接跳过
这样做主要不是为了性能。
而是为了:
让链路更干净。
如果每次都直接插入:
- 第二次一定报错
- 日志里会出现很多失败记录
- 排查问题时容易被干扰
因此很多时候会先判断:
这一步是不是已经完成
完成了就跳过。
真实系统关注的重点已经发生变化
学生项目通常更关注:
代码怎么写。
真实系统里更关注的是:
- 当前做到哪一步
- 哪一步已经成功
- 下一次应该从哪里继续
- 重试会不会重复执行
- 并发情况下会不会出问题
换句话说:
系统在失败、重试和并发下,是否仍然能够恢复到正确状态。
小结
单机程序里,事务通常可以解决大部分问题。
但分布式系统里,会自然出现:
- 中途失败
- 消息重试
- 状态不确定
- 并发执行
- 顺序错乱
因此系统设计的重点会从:
如何完成一次执行
转变成:
如何在各种异常情况下仍然保证最终正确。
这也是工程实践中非常重要的一类问题。