async/await 是高并发的起点,不是终点——它只解决 IO 等待问题,不解决 CPU 争抢、缓存不一致或数据库写冲突。
用 async/await 做对了,但为什么还卡?
很多人以为加了
async/
await就自动高并发了,结果压测时 QPS 上不去、线程池饥饿、GC 频繁。根本原因是:它只释放线程,不减少资源竞争。 必须所有 I/O 调用都异步化:
DbContext.SaveChangesAsync()、
HttpClient.GetAsync()、
FileStream.ReadAsync()—— 混入一个
.Result或
.Wait()就可能死锁 避免在异步方法里调用同步阻塞 API(如
File.ReadAllText()),否则线程池线程被占住,新请求排队等待 高频小任务(比如每秒数万次订单校验)建议用
ValueTask替代
Task,减少 GC 压力;实测内存分配可降 60%+
库存扣减、计数器更新这类共享操作怎么不出错?
用
lock最简单,但会串行化,成为瓶颈;真正在意吞吐量时,得换更轻量、更可控的方式。 简单整数计数:优先用
Interlocked.Decrement(ref stock),零锁、原子、快 对象级状态变更(如订单状态机):用
ConcurrentDictionary<string order></string>+
TryRemove/
GetOrAdd,比手动锁安全得多 分布式场景(多实例部署):必须上
Redis分布式锁,但别用
SETNX手写——用
StackExchange.Redis的
LockTake+ 自动续期,否则容易锁失效
100 个用户同时下单,数据库写崩了怎么办?
数据库是高并发链路中最难横向扩展的一环,不能靠“加索引”或“读写分离”一劳永逸。
写操作先过队列:把下单请求发到RabbitMQ或
Kafka,Web 层立刻返回“已受理”,后台消费者按能力消费,削峰填谷 避免直接
UPDATE products SET stock = stock - 1 WHERE id = @id AND stock >= 1—— 这种乐观锁在高并发下会大量失败重试;改用带版本号的更新(
version字段)或 Redis Lua 脚本原子扣减 统计类查询(如“今日销量”)绝不实时算,用定时任务写入
summary_daily_sales表,查时直读
缓存更新策略选错,数据就永远对不上
“先删缓存再更新 DB” 和 “先更新 DB 再删缓存” 看似只差一步,但在并发下行为完全不同。
推荐组合:更新数据库 → 延迟双删(删一次 + 100ms 后再删一次),能覆盖大部分缓存穿透+脏读场景 绝对不要用“先更新缓存再更新 DB”——DB 更新失败时,缓存就是脏数据,且无法回滚 Redis 缓存键设计要带业务维度和版本号,比如order:detail:v2:12345,升级逻辑时直接切版本,避免热更新风险
真正难的从来不是“怎么实现并发”,而是“怎么让并发下的状态始终可预期”。锁、队列、缓存、数据库,每个环节的取舍都会影响最终一致性边界——这点在金融、库存、支付类业务里,错一步就是资损。
