一、用一个真实例子,先把“脏数据”跑出来(必须具体)

1️⃣ 业务场景:订单金额计算(真实、常见、危险)

假设我们有一个订单结算服务

  • 每个请求进来,计算:

    • 商品金额
    • 运费
    • 优惠
  • 最终返回订单应付金额

在 Spring 项目里,这个 Service 默认是单例 Bean

为了“复用计算过程”,有人写了这样的代码。


2️⃣ 最小可理解示例代码(明确共享状态)

@Service
public class OrderPriceService {

    // ⚠️ 共享状态:单例 Bean 的成员变量
    private BigDecimal totalPrice = BigDecimal.ZERO;

    public BigDecimal calculate(Order order) {

        // 1. 商品金额
        totalPrice = order.getItemPrice();

        // 2. 运费
        totalPrice = totalPrice.add(order.getShippingFee());

        // 3. 优惠
        totalPrice = totalPrice.subtract(order.getDiscount());

        return totalPrice;
    }
}

先别急着说「这代码谁会这么写」
——我可以明确告诉你:
线上系统里,这种写法一点都不少。

关键点只有一个:

  • OrderPriceService单例
  • totalPrice成员变量
  • 所有请求 共用这一份状态

3️⃣ 并发时间线:脏数据是怎么一步一步发生的

现在系统并发来了两个请求:

  • 请求 A:订单 A(100 元商品,10 运费,0 优惠)
  • 请求 B:订单 B(200 元商品,20 运费,50 优惠)
⏱ 时间线(真实可能发生)
T1:线程 A 进入 calculate(A)
    totalPrice = 100

T2:线程 B 进入 calculate(B)
    totalPrice = 200   ← 覆盖了 A 的计算中间态

T3:线程 A 继续执行
    totalPrice = 200 + 10 = 210   ← 用的是 B 的值

T4:线程 B 继续执行
    totalPrice = 210 + 20 = 230

T5:线程 A 执行优惠
    totalPrice = 230 - 0 = 230
    返回给 A:230 ❌

T6:线程 B 执行优惠
    totalPrice = 230 - 50 = 180
    返回给 B:180 ❌
❗ 发生了什么?
  • A 的计算过程,被 B 中途插队
  • B 的计算,又基于 A 被污染过的状态
  • 两个结果都错
  • 日志、异常、报警:全部没有

4️⃣ 关键结论(非常重要)

❌ 这不是“概率问题”
❌ 也不是“极端并发才会发生”

只要满足三个条件,这是必然事件:

  1. 单例 Bean
  2. 成员变量存储请求态数据
  3. 并发请求

👉 并发一来,脏数据一定会发生,只是你有没有撞到而已。

到这里为止,你必须已经能回答:

“原来脏数据是这样一步一步产生的。”

如果你还觉得模糊,说明你对并发执行的真实形态,理解仍然停留在“顺序想象”。


二、1–2 万工程师如何看这个问题(功能交付视角)

站在这个阶段工程师的角度,刚才那个例子,他们通常会这样想:

1️⃣ 第一反应:“加锁就行了”

public synchronized BigDecimal calculate(Order order) {
    ...
}

或者:

private final Object lock = new Object();

public BigDecimal calculate(Order order) {
    synchronized (lock) {
        ...
    }
}

2️⃣ 为什么他们“以为问题解决了”

  • 本地压测 OK
  • 再也复现不了脏数据
  • 没有线上报警

在他们的认知里:

“并发问题 = 线程不安全
线程不安全 = 加锁
加锁 = 修好了”

3️⃣ 他们忽略了什么前提

  • 这是一个高频调用的核心服务

  • 锁会直接:

    • 拉长 RT
    • 降低吞吐
    • 制造排队
  • 更重要的是:

    • 这个 Service 的职责本身就不应该有状态

但这些问题,在他们的交付压力下,不是第一优先级


三、3–5 万工程师如何看这个问题(系统责任视角)

这个层级的人,看完第一段代码,关注点已经完全不同。

1️⃣ 他们看到的是危险信号

  • 单例 Service
  • 成员变量存请求态
  • 无任何并发隔离

在他们眼里,这不是“bug”,而是:

设计级违规

2️⃣ 为什么他们不急着改代码

因为他们知道:

  • 今天是 totalPrice

  • 明天就可能是:

    • 用户上下文
    • 权限结果
    • 风控判断

这不是某一行代码的问题,是认知模型的问题。

3️⃣ 他们真正害怕的后果

  • 金额错算 → 财务对账异常
  • 结果不可复现 → 无法追责
  • 偶现 bug → 最贵的一类事故

4️⃣ 为什么他们会否定“表面正确”的修法

在他们看来:

用锁保住一个本不该有状态的单例

本身就是在延长系统寿命之前的隐性负债


四、100 万级工程师 / 架构负责人怎么看(系统定价视角)

到这个层级,讨论点已经完全变了。

1️⃣ 他们还会讨论“怎么修代码吗”?

几乎不会。

他们问的是:

  • 为什么 Code Review 没挡住?
  • 为什么设计阶段没暴露?
  • 为什么这类问题反复出现?

2️⃣ 如果真的出了事故,他们追责哪一层?

不是写代码的人。

而是:

  • 代码规范
  • 设计审查
  • 技术负责人
  • 组织是否默许“快写快交付”

3️⃣ 在他们眼里,这是什么问题?

❌ 不是 Java 技术问题
❌ 不是并发 API 问题

这是一个:

系统对“状态、并发、责任边界”的理解水平问题


五、为什么这是工程师身价差异的分水岭?

我们用一句话总结三类人:

层级 关注点
1–2 万 这段代码能不能跑
3–5 万 这个设计会不会失控
100 万 为什么系统允许它存在

差距不在会不会写代码,而在于:

是否理解系统是如何一步步走向失控的

很多人技术不差,
但他们的认知永远停留在“修问题”
而不是**“防问题出现”**。


六、给读者的认知校准

1️⃣ 最容易踩的认知陷阱

  • “这是个小问题”
  • “线上并发没那么高”
  • “加锁就安全了”

这些话,事故复盘会上我听过太多次

2️⃣ 哪些能力不是靠多学 API 得到的

  • 对并发真实执行模型的理解
  • 对状态边界的敏感度
  • 对系统失控路径的预判能力

3️⃣ 一个自检问题(很重要)

你看到最开始那段代码时:

  • ❌ 第一反应是“怎么修”
  • ✅ 还是第一反应是“为什么允许这样设计”

这,基本决定了你的工程定价上限。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐