用户明明改了资料、刷新却还是旧的,排查发现是我更新数据库后顺手更新缓存在高并发下把旧值写回了缓存:一次缓存与数据库双写一致性、Cache-Aside 该删缓存而非更新缓存的深度复盘

我们的用户资料服务为扛读量加了 Redis 缓存,读走缓存未命中查库回填,写用我自以为周到的先更新数据库再顺手更新缓存。功能测着没问题,线上却偶发:用户改了资料、缓存却长期是旧值,刷不新直到过期才好。在并发场景下推演才看明白:更新库和更新缓存两步非原子,两个并发写请求 A、B 都先更库再更缓存,DB 最终是 B 的新值(对),但更新缓存时因线程调度 B 先写、A 后写,缓存被覆盖回 A 的旧值——DB 对、缓存错、长期不一致。这篇复盘从故障现场讲到为什么会不一致、几种缓存更新策略及其问题,再到 Cache-Aside(更新 DB 后删缓存而非更新缓存、删除幂等不写回旧值)、读未命中回填、必设 TTL 兜底、延迟双删/binlog 的完整正解,以及删缓存优于更新缓存(失效重建优于主动同步)、冗余必然带来一致性代价、引入收益机制要连同附带的责任一起认领设计的认知。

用户明明改了资料、刷新却还是旧的,排查发现是我"更新数据库后顺手更新缓存"在高并发下把旧值写回了缓存:一次缓存与数据库双写一致性的深度复盘

那个 bug 是用户反复投诉"我改了昵称,刷新怎么还是旧的"才暴露的:我们有个用户资料服务,为了扛读量,数据加了缓存(Redis)。读走"缓存→没有再查库→回填缓存",写则是我自以为很周到的"先更新数据库,再顺手更新一下缓存"。功能测着没问题,可线上偶尔就有用户反馈:明明改了资料,缓存里却长期是旧值,怎么刷都不更新,直到缓存过期才好。我对着这"偶发的脏数据"查了好久,在并发场景下推演了一遍,才看明白,后背发凉:问题出在我"更新数据库后再更新缓存"这个写策略上,它在高并发下有竞态。设想两个并发的写请求 A(把昵称改成"")和 B(把昵称改成"更新"):它们都先更新数据库,再更新缓存;但这"更新库"和"更新缓存"不是原子的,中间可能被打断、顺序可能交错;完全可能出现:A 先更新了库,B 后更新了库(库里最终是 B 的"更新",正确),但在更新缓存时,因为线程调度,B 先写了缓存、A 后写了缓存——于是缓存里留下了 A 的旧值"";结果:数据库是对的(B),缓存是错的(A),而且这个错值会一直留在缓存里直到过期,造成长期的缓存-数据库不一致(脏数据)。根本问题是:"更新数据库 + 更新缓存"这种双写,在并发下两步的交错会让缓存存进旧值;而且"更新缓存"本身就是个有风险的动作(还可能更新了一个根本没人读、白算的缓存)。问题的根,是用了"更新 DB + 更新缓存"的双写策略,它在并发下有竞态、易产生缓存旧值脏数据。这篇就把这次"缓存与数据库一致性"的坑,从头到尾复盘一遍。

故障现场:更新数据库后又更新缓存

问题在于写操作采用"更新 DB + 更新缓存"双写,并发下两步交错导致缓存存旧值:

// ✗ 出问题的写策略: 更新数据库后, 再更新缓存
public void updateProfile(long userId, Profile p) {
    db.update(userId, p);                    // 1. 更新数据库
    cache.set("profile:" + userId, p);       // 2. ✗ 再更新缓存 —— 双写, 并发下有竞态!
}

// 并发竞态推演(两个并发写请求 A 和 B):
//   时刻1: 请求A 更新DB为"新"
//   时刻2: 请求B 更新DB为"更新"     ← DB最终是"更新"(正确)
//   时刻3: 请求B 更新缓存为"更新"   ← 因线程调度, B先写了缓存
//   时刻4: 请求A 更新缓存为"新"     ← A后写, 把缓存覆盖回了【旧值"新"】!
//   结果: DB="更新"(对), 缓存="新"(错) → 缓存留下旧值, 长期不一致(直到过期)。

// 问题1: "更新DB"和"更新缓存"是两步、非原子, 并发下两步的相对顺序会交错;
//        → 可能DB是新值、缓存却被覆盖成旧值 → 脏数据。
// 问题2: "更新缓存"还可能白算: 更新了一个其实没人会读的缓存(浪费);
//        若缓存值是"由多个字段算出来的", 每次写都重算更浪费。

// 另一个常见错误写法: 先更新缓存, 再更新数据库 —— 更糟:
//   若更新缓存成功、更新DB失败, 缓存是新值、DB是旧值 → 更严重的不一致。

// 关键: "更新DB+更新缓存"的双写, 两步非原子、并发下顺序交错, 会让缓存存进旧值造成长期不一致;
//       缓存与数据库的一致性, 不能靠"更新完库顺手更新缓存"这种想当然的双写来保证。

第一次在纸上推演出"两个写请求的更新缓存顺序一交错,旧值就被写回去了"时,我又懊恼又警醒:"我以为'改完库顺手把缓存也改了'是最自然、最一致的做法,完全没想到这两步在并发下会打架、把旧值留在缓存里。"这个坑最隐蔽的地方在于:只在并发写同一个 key 时偶发,单元测试、低并发下几乎不出现;而且数据库始终是对的(迷惑性强),只有缓存错了,查库一切正常、查缓存才发现脏;还因为"缓存会过期",所以脏数据"过一会儿自己好了",更难抓现行下面就来拆解,缓存与数据库的一致性到底该怎么保证。

第一件事:搞懂缓存更新的几种策略与各自的问题

我顺着这次事故,把缓存与数据库的一致性策略彻底理清了。

缓存与数据库一致性: 几种更新策略及其问题

【核心: 双写(更新DB+更新缓存)并发下易交错产生脏数据; 业界主流是Cache-Aside(更新DB后删缓存); 配合过期兜底+延迟双删】

1. 为什么会有不一致? —— 数据有两份(DB + 缓存), 要同步两份就有时序/并发问题
   - 写操作要同时影响DB和缓存这两个地方, 而这"两个动作"无法天然原子;
   - 并发的多个写、读, 它们对DB和缓存的操作交错, 就可能让两份数据对不上。

2. 策略A: 更新DB + 更新缓存(本文, 不推荐)
   - 问题①并发下两次"更新缓存"顺序交错 → 缓存可能存旧值(本文);
   - 问题②白算: 更新了没人读的缓存; 若缓存值要计算, 更浪费;
   - → 不推荐。

3. 策略B: 更新DB + 删除缓存 (Cache-Aside, 业界主流, 推荐)
   - 写: 先更新DB, 然后【删除】缓存(而不是更新它);
   - 读: 缓存命中就用; 没命中→查DB→回填缓存;
   - 好处: 删除是"幂等"的(删两次和删一次一样), 不存在"把旧值写回"的问题;
     下次读自然会从DB加载最新值回填 → 最终一致; 也不白算(用时才算)。
   - 仍有的小概率竞态: 见下面延迟双删。

4. 策略B的残余竞态 + 延迟双删:
   - 极端时序: 读请求读到旧DB值还没回填 → 此时写请求更新DB+删缓存 → 读请求才把旧值回填缓存 → 旧值残留;
   - 缓解: "延迟双删" —— 更新DB后删一次缓存, 隔一小段时间(如几百ms)再删一次, 清掉可能的旧回填;
   - 或用更强方案(下面)。

5. 更强的方案(高一致要求):
   - 先删缓存再更新DB + 延迟双删; 或加分布式锁串行化对同一key的读写;
   - 订阅DB binlog(如canal)异步删缓存, 解耦且可靠;
   - 但这些更复杂, 按一致性要求选择, 别过度设计。

6. 兜底: 给缓存设过期时间(TTL)
   - 无论用哪种策略, 都给缓存设合理TTL; 即使某次没删干净, 过期后也会重新从DB加载;
   - TTL是"最终一致"的最后一道保险, 必加。

一句话: 缓存与DB是两份数据、同步有并发时序问题; "更新DB+更新缓存"双写易交错产生脏数据;
   主流用Cache-Aside(更新DB后【删】缓存, 删是幂等的)+设TTL兜底+必要时延迟双删/binlog, 保证最终一致。

这套认知,是整个坑的根。为什么会有不一致:数据有两份(DB+缓存),写要同时影响两处而这两个动作无法天然原子,并发交错就对不上。策略 A:更新 DB+更新缓存(本文,不推荐)——并发下两次更新缓存顺序交错→缓存存旧值;还白算没人读的缓存策略 B:更新 DB+删除缓存(Cache-Aside,主流推荐)——写时删缓存(不是更新),读时未命中再查 DB 回填;删除是幂等的,不存在把旧值写回的问题,下次读自然加载最新值、最终一致、也不白算策略 B 的残余竞态+延迟双删:更新 DB 后删一次、隔几百 ms 再删一次,清掉可能的旧回填。更强方案:分布式锁串行化、订阅 binlog 异步删缓存(按一致性要求选,别过度设计)。兜底:给缓存设 TTL,即使没删干净过期后也会重新加载,是最终一致的最后保险。一句话:缓存与 DB 是两份数据、同步有并发时序问题;"更新 DB+更新缓存"双写易交错产生脏数据;主流用 Cache-Aside(更新 DB 后删缓存,删是幂等的)+设 TTL 兜底+必要时延迟双删/binlog,保证最终一致。

第二件事:正解——Cache-Aside(更新 DB 后删缓存)+ TTL 兜底 + 延迟双删

搞懂了原理,正解就清晰了:写操作改成"更新数据库后删除缓存(而非更新)";读走 Cache-Aside(未命中查库回填);给缓存设 TTL 兜底;高一致要求时加延迟双删或 binlog 异步删

// ====== 正解: Cache-Aside —— 更新DB后【删除】缓存 ======
public void updateProfile(long userId, Profile p) {
    db.update(userId, p);                       // 1. 更新数据库
    cache.delete("profile:" + userId);          // 2. ★ 删除缓存(不是更新!)
    // → 删除是幂等的: 并发下多个写都删, 删几次结果一样, 不会"把旧值写回";
    //   下次读未命中, 自然从DB加载最新值回填 → 最终一致。
}

public Profile getProfile(long userId) {        // 读: 标准 Cache-Aside
    String key = "profile:" + userId;
    Profile p = cache.get(key);
    if (p != null) return p;                     // 缓存命中
    p = db.query(userId);                        // 未命中 → 查DB
    cache.set(key, p, Duration.ofMinutes(10));   // ★ 回填, 并设TTL兜底(10分钟)
    return p;
}
// ====== 加强: 延迟双删(应对"读旧值回填"的残余竞态) ======
public void updateProfileStrong(long userId, Profile p) {
    cache.delete("profile:" + userId);          // (可选)先删一次
    db.update(userId, p);                        // 更新DB
    cache.delete("profile:" + userId);          // 更新后删一次
    // 延迟一小段时间后再删一次, 清掉"期间可能被读请求回填的旧值":
    scheduler.schedule(() -> cache.delete("profile:" + userId),
                       500, TimeUnit.MILLISECONDS);   // ★ 延迟双删
}
# ====== 策略选择与要点 ======
# 1. 默认用 Cache-Aside: 更新DB后【删】缓存(不是更新), 读未命中回填; 简单且足够多数场景;
# 2. 删除 vs 更新: 删除是幂等的(无"写回旧值"风险)、且懒加载不白算 → 优于更新缓存;
# 3. 必加 TTL: 给缓存设过期时间, 作为"最终一致"的最后兜底(再怎么没删干净, 过期就重载);
# 4. 高一致要求: 延迟双删 / 分布式锁串行化同key读写 / 订阅binlog异步删缓存;
# 5. 缓存穿透/击穿/雪崩也要一并考虑(空值缓存、热点key加锁重建、TTL加随机抖动)——同属缓存设计;
# 6. 想清"能接受多强的一致性": 多数业务能接受【短暂的最终一致】, 别为了"强一致"过度设计、拖垮性能。

# ====== 一句话决策 ======
# - 一般场景: Cache-Aside(更新DB→删缓存) + TTL 兜底, 就够了;
# - 并发写热点 + 一致性要求较高: 加延迟双删 或 binlog异步删;
# - 极高一致: 考虑别用缓存, 或用锁/事务消息等更重的手段(权衡性能)。

# 核心: 写用Cache-Aside(更新DB后【删】缓存, 删幂等不写回旧值)、读未命中回填、必设TTL兜底;
#   高一致再加延迟双删/binlog; 想清能接受的一致性强度, 别用"双写更新缓存"也别过度设计。

修复的核心,是"写时删缓存而非更新缓存,配 TTL 兜底"正解:Cache-Aside——写时"更新 DB 后删除缓存(不是更新)",删除是幂等的、不会把旧值写回,下次读未命中自然从 DB 加载最新值回填、最终一致、也不白算加强:延迟双删——更新后删一次、隔几百 ms 再删一次,清掉期间可能被读请求回填的旧值要点:默认 Cache-Aside、删除优于更新(幂等+懒加载不白算)、必加 TTL 作为最终一致的最后兜底、高一致再加延迟双删/分布式锁/binlog、一并考虑缓存穿透击穿雪崩、想清能接受的一致性强度别过度设计归根结底:写用 Cache-Aside(更新 DB 后删缓存,删幂等不写回旧值)、读未命中回填、必设 TTL 兜底;高一致再加延迟双删/binlog;想清能接受的一致性强度,别用"双写更新缓存"也别过度设计。

第三件事:缓存设计中其他常见的坑

排查后我把缓存设计中其他容易踩的坑也系统梳理了一遍。

缓存设计的其他常见坑

# 1. 双写更新缓存致脏数据(本文): 并发交错存旧值。→ Cache-Aside删缓存+TTL。

# 2. 缓存穿透: 查一个根本不存在的key, 每次都击穿到DB(恶意/异常)。→ 空值也缓存(短TTL)/布隆过滤器。

# 3. 缓存击穿: 某热点key过期瞬间, 大量请求同时打到DB。→ 热点key重建加锁/逻辑过期/不过期。

# 4. 缓存雪崩: 大量key同一时刻过期, DB被瞬间打垮。→ TTL加随机抖动, 别让大批key同时过期。

# 5. 缓存与DB不一致(本文是其一): 更新策略不当。→ Cache-Aside+TTL+必要时双删/binlog。

# 6. 大key/热key: 单key过大或过热, 拖慢/打爆某节点。→ 拆分大key、热key多副本/本地缓存。

# 7. 缓存了不该缓存的: 强一致/极少读/频繁变的数据加缓存, 得不偿失。→ 评估缓存收益。

# 8. 没有降级: 缓存挂了直接全打DB把DB也压垮。→ 限流/熔断/缓存挂了也要保护DB。

# 共同根源: 缓存的本质是"用一份冗余的、可能过期的数据副本, 换读取性能";
#   而"冗余的副本"必然带来"和源数据如何保持一致、副本失效时怎么办"等一系列问题——
#   缓存提升了性能, 但也引入了"一致性、穿透、击穿、雪崩"这些必须设计应对的新问题。

# 核心: 用缓存要清醒——它是"用一致性的复杂度、换读性能"的权衡; 用Cache-Aside管一致性、
#   设TTL、防穿透击穿雪崩、给降级; 别只享受缓存的快, 而不处理它引入的一致性与可用性问题。

排查让我把缓存设计的其他坑也梳理清了。一、双写更新缓存致脏数据(本文)。二、缓存穿透(查不存在的 key)。三、缓存击穿(热点 key 过期瞬间)。四、缓存雪崩(大量 key 同时过期)。五、缓存与 DB 不一致六、大 key/热 key七、缓存了不该缓存的八、没有降级它们的共同根源是:缓存的本质是"用一份冗余的、可能过期的数据副本,换读取性能";而冗余的副本必然带来"和源数据如何保持一致、副本失效时怎么办"等一系列问题——缓存提升了性能,但也引入了一致性、穿透、击穿、雪崩这些必须设计应对的新问题核心是:用缓存要清醒——它是"用一致性的复杂度换读性能"的权衡;用 Cache-Aside 管一致性、设 TTL、防穿透击穿雪崩、给降级;别只享受缓存的快,而不处理它引入的一致性与可用性问题下面这张图,是这次缓存一致性坑的成因与解法:

第四件事:几种缓存更新策略对比表

这次踩坑后,我把几种缓存更新策略对比成一张表。

策略 并发脏数据风险 是否白算 推荐度
更新DB + 更新缓存(本文) 高(顺序交错写回旧值) 会(更新没人读的缓存) 不推荐
更新缓存 + 更新DB 更高(缓存成功DB失败) 禁用
更新DB + 删缓存(Cache-Aside) 低(删幂等, 仅残余竞态) 不会(懒加载) ★ 主流推荐
+ 延迟双删 更低 不会 高一致场景
+ TTL 兜底 最终一致兜底 必加

这张表把几种策略钉清了。核心是:关键的洞察是"删缓存 优于 更新缓存"——"更新"是把一个具体的值写进去(并发下会把旧值写回, 且可能白算);"删除"只是"让它失效, 下次重新从源头取"(幂等、不会写回旧值、用时才算);面对"缓存可能过时"这个问题,"把它删掉让它重新加载" 比 "试图主动把它更新成正确的值" 更简单、更不容易错它给我的最大启发是:当一份"派生数据/副本"(缓存)可能和"源数据"(DB)不一致时,"让副本失效、需要时从源头重新派生" 往往比 "主动同步更新副本" 更可靠——因为"失效(删除)"是幂等的、不引入新的错误值,而"主动更新"要保证更新的内容、时机、顺序都对,容易出错;这就是"invalidate(失效) 优于 update(更新)" 的缓存哲学这给了我一种处理"派生数据一致性"的清醒:面对任何"从源头派生出来、需要和源头保持一致"的东西(缓存、索引、物化视图、冗余字段、前端的本地状态),优先考虑"源头变了就让派生数据失效、用时重新派生",而非"源头变了就同步地、主动地去更新每一处派生数据"——前者简单幂等可靠,后者复杂易错;"用'失效重建'而非'主动同步'来维护派生数据的一致性",是一个简单而强大的设计原则认清删缓存优于更新缓存、用失效重建而非主动同步维护派生数据——是这个坑带给我的认知。

第五件事:这次事故暴露的"冗余带来的一致性代价"

这次让我反思更深一层:这一切麻烦的根源,是我为了性能,引入了"缓存"这份数据的冗余副本。我把"引入冗余的收益与代价"对比成表。

维度 不加缓存(单一数据源) 加缓存(引入冗余副本)
读性能 每次查 DB, 慢 快(读缓存)
一致性 天然一致(只有一份) 要操心(两份要同步)
复杂度 高(穿透/击穿/雪崩/一致性)
新增问题 缓存与DB不一致等一系列
本质 简单但慢 快但要管理冗余的代价

这张表道出了缓存的本质权衡。核心是:缓存之所以带来这么多麻烦(一致性、穿透、击穿、雪崩),根源在于它引入了"数据的冗余副本"——本来数据只有 DB 一份(天然一致),加了缓存就变成两份,而"维持两份数据一致"本身就是一个有代价、有复杂度的难题;我享受了缓存带来的"读得快",就必须承担"维护冗余一致性"的代价它给我的深刻启发是:"冗余(同一份信息存了多份)" 是一把双刃剑——它常常能换来性能(缓存)、可用性(多副本)、便利(冗余字段),但必然带来"多份之间如何保持一致"的代价和复杂度;"没有免费的冗余":每多一份副本, 就多一份"要和别人对齐"的负担;很多系统的复杂性, 本质都来自"为了某种好处而引入的冗余, 所要求的一致性维护"这给了我一种引入冗余时的审慎:每当我要"为了性能/便利而引入一份数据冗余"(加缓存、加冗余字段、做数据同步、多级存储)时,都要清醒地意识到:"我同时也引入了'维护这份冗余与源头一致'的责任和复杂度"——并问"这份冗余的收益, 值得我为它付出的一致性维护成本吗?我想清楚怎么维护一致了吗?";"引入冗余前, 先想清它的一致性怎么维护、代价是否值得",是避免"只图冗余的好处、却被一致性问题反噬"的关键认清冗余必然带来一致性代价、引入冗余前先想清一致性怎么维护——是这个缓存坑带给我的工程态度。

第六件事:给数据加缓存时,我现在的自检习惯

现在每当我要给一份数据加缓存,我都会先按这张图问自己:

这张图的精髓,是"写时删缓存而非更新、必设 TTL、按一致性要求决定要不要加强"频繁变/强一致别加缓存、读多写少用 Cache-Aside、写时删缓存设 TTL、高一致加延迟双删/binlog这套习惯,让我从"改完库顺手更新缓存"变成了"用 Cache-Aside 删缓存+TTL、想清一致性"——核心始终是:写用 Cache-Aside(更新 DB 后删缓存而非更新缓存,删是幂等的)、读未命中回填、必设 TTL 兜底,别用双写更新缓存导致并发脏数据。

我立下的几条规矩

这场"缓存留旧值、用户改了资料还显示旧的"的事故,换来了我做缓存设计时,刻进骨子里的几条铁律:

  1. 缓存与 DB 是两份数据,并发下同步两份有时序问题。这是一切不一致的根源。
  2. 别用"更新 DB+更新缓存"双写。并发下顺序交错会把旧值写回缓存。
  3. 用 Cache-Aside:更新 DB 后【删】缓存,而非更新缓存。删除幂等、不写回旧值。
  4. 读未命中查 DB 回填,并设 TTL。TTL 是最终一致的最后兜底,必加。
  5. 高一致要求加延迟双删或订阅 binlog 异步删。按需,别过度设计。
  6. 缓存穿透/击穿/雪崩一并设计应对。空值缓存、热点加锁、TTL 随机抖动。
  7. 引入缓存=引入冗余,先想清一致性怎么维护、代价是否值得。

写在最后

回头看,这场由"更新数据库后顺手更新缓存"引发的、缓存脏数据的事故,真正教给我的,远不止"用 Cache-Aside 删缓存代替更新缓存"这一个技巧。它让我对"我们为了得到某种好处(性能), 引入了一份'冗余'(缓存); 而这份冗余, 会悄悄地、必然地, 带来一整套'如何保持一致'的新问题——好处不是免费的, 它附带着责任",有了一次刻骨的体会。我栽跟头,是因为我只盯着缓存带来的"好处"(读得快),却没有正视它附带的"责任"(这份冗余数据,要时刻和数据库保持一致)。我以为"加个缓存"只是"多一个读得快的地方"这么简单、单纯是赚的;可我忽略了:我一旦把同一份数据存了两份(DB 和缓存),就凭空给自己制造了一个"让这两份数据始终一致"的、并不简单的新任务;而我"更新完库顺手更新缓存"这个想当然的做法,正是没把这个新任务当回事、没考虑它在并发下的复杂性, 才翻的车这让我领悟到一个关于"收益与其附带责任"的深刻认知:我们在系统里引入的每一个"为了某种收益的机制"(缓存为性能、冗余为可用、异步为吞吐、分布式为扩展),都不是"纯赚"的——它在给你收益的同时, 必然附带一份"你必须妥善处理的新责任/新复杂度"(缓存→一致性、异步→时序与最终一致、分布式→网络分区与协调);"天下没有免费的架构收益":你为收益付出的代价, 往往就是"处理它引入的那份新复杂度"这给了我一种做架构决策时的清醒与负责:每引入一个"能带来收益的机制"时,都要同时问清"它附带了什么新责任/新问题?我准备好、也有能力妥善处理这份责任了吗?"——而不是只盯着收益就贸然引入;"引入一个机制时, 连同它附带的责任一起认领、一起设计",是做出真正稳健的架构决策、而非"只享受收益却被附带的复杂度反噬"的关键认清每个收益机制都附带必须处理的新责任、引入时连同责任一起认领设计——这,是我用一次缓存脏数据的事故,换来的、关于系统架构、也关于如何清醒地做技术决策的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次给数据加缓存时,把写策略改成"更新库后删缓存"、给缓存配上 TTL、并想清这份冗余的一致性怎么维护,那我对着那条"改了却不更新"的脏缓存排查的这段时间,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

同一句话调用大模型做意图分类,有时分对、有时分错,复现 bug 时还死活复现不出来,我查到底才发现是 temperature 把随机性引了进来:一次 LLM 采样参数设置不当、把概率组件当确定性函数用的深度复盘

2026-6-2 20:32:50

技术教程

我把一个 async 方法的返回类型写成了 async void,它里面抛的异常 try-catch 死活拦不住、还直接把整个进程干崩了:一次 C# async void 吞掉异常、让异常逃逸到顶层崩溃进程的深度复盘

2026-6-2 20:43:20

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索