作者:旷野说
定位:Spring Boot 开发者速查手册 + 高并发系统设计认知指南
关键澄清:单体应用并非高并发的“原罪”,真正的瓶颈常在于“无状态化缺失 + 数据一致性粗放 + 异常处理失控”。优化应始于微观,而非盲目拆微服务。
很多人一提“高并发”,就立刻想到“上微服务”“上 Kubernetes”“上 Service Mesh”。
但现实是:我们“C 端”的核心打赏系统,在日活千万级时,仍是单体架构。
为什么?因为过早拆分,只会把简单问题复杂化。
真正的高并发优化,应始于微观层面的精准控制:
- 如何用 Redis + Lua 保证“扣库存 + 防重复”原子性?
- 如何让落库刷盘不成为瓶颈?
- 如何通过 Java 异常体系与 Servlet 容器选型,守住系统稳定性?
今天,我就以“用户打赏心动钻石”这个高频交易场景为例,带你看单体应用如何通过微观优化,扛住百万级并发。
一、问题起点:单体应用的“高并发幻觉”
初期,我们的打赏逻辑很简单:
@Transactional
public void sendGift(Long senderId, Long receiverId, int diamond) {
User sender = userMapper.selectById(senderId);
if (sender.getBalance() < diamond) throw new InsufficientBalanceException();
sender.setBalance(sender.getBalance() - diamond);
userMapper.updateById(sender);
// 记录打赏 + 发通知
giftRecordMapper.insert(...);
notificationService.send(...);
}
表面看没问题,实则埋雷:
- 每次打赏都查 DB 用户余额 → 高 QPS 下 DB 打满
- 无防重机制 → 网络重试导致重复扣款
- 通知失败 → 整个事务回滚,用户以为没打赏成功
结果:QPS 超 500 就开始超时,资损投诉不断。
二、微观第一招:用 Redis + Lua 实现“业务完整性原子化”
我们意识到:高频交易的核心状态(如余额、库存)必须脱离 DB,前置到缓存。
但普通 Redis 操作不行——“查余额 → 判断 → 扣减”非原子,高并发下必然超扣。
解法:Lua 脚本 + Redis 原子执行
-- KEYS[1] = user_balance_key, ARGV[1] = user_id, ARGV[2] = amount
local balance = tonumber(redis.call('HGET', KEYS[1], ARGV[1]))
if not balance or balance < tonumber(ARGV[2]) then
return 0 -- 余额不足
end
redis.call('HINCRBY', KEYS[1], ARGV[1], -tonumber(ARGV[2]))
redis.call('SADD', 'gift_claimed:' .. ARGV[3], ARGV[1]) -- 防重 Set
return 1
在 Java 中调用:
public boolean tryDeductBalance(Long userId, int amount, String giftId) {
String luaScript = loadLua("deduct_balance.lua");
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList("user:balance"),
userId.toString(), String.valueOf(amount), giftId
);
return result == 1;
}
✅ 效果:
- 原子性:扣余额 + 记录防重,一步完成
- 高性能:Redis 单线程执行,10 万 QPS 毫秒响应
- 防重:基于
giftId + userId的 Set,天然去重
💡 注意:Lua 脚本必须无状态、无外部依赖,否则会阻塞 Redis 单线程。
三、微观第二招:异步落库刷盘,解耦核心路径
用户通过 Lua 预扣后,我们立即返回“打赏成功”,但此时 DB 还未更新。
关键设计:落库不是同步操作,而是异步事件。
public ResponseEntity<?> sendGiftAsync(GiftRequest req) {
if (!giftService.tryDeductBalance(req.getSenderId(), req.getAmount(), req.getGiftId())) {
return ResponseEntity.badRequest().body("余额不足或重复请求");
}
// 发送异步事件,100ms 内落库
mqProducer.send(new GiftEvent(req));
return ResponseEntity.ok("打赏成功");
}
消费者端:
@KafkaListener(topics = "gift-event")
public void handleGiftEvent(ConsumerRecord<String, String> record) {
try {
GiftEvent event = parse(record.value());
// 直接插入,靠唯一索引兜底
giftRecordMapper.insertIgnoreDuplicate(event);
userBalanceLogMapper.insert(event); // 用于对账
} catch (Exception e) {
// 记录失败消息,供对账补偿
failedEventStore.save(record);
}
}
✅ 优势:
- 核心路径无 DB I/O,响应 < 50ms
- DB 压力降低 90%,连接池不再打满
- 失败可追溯,不丢失任何一笔交易
四、微观第三招:对账兜底,构建最终一致性
即使 Redis + MQ 再可靠,分布式系统永远有极端故障(如 Redis 主从切换丢数据)。
解法:每日对账 + 实时补偿
// 对账任务:比对 Redis 扣减日志 vs DB 落库记录
@Scheduled(cron = "0 0 2 * * ?")
public void reconcileGiftClaims() {
List<RedisDeductLog> redisLogs = redisLogService.getRecentLogs();
List<GiftRecord> dbRecords = giftRecordMapper.getRecordsSince(...);
Set<String> redisKeys = redisLogs.stream().map(l -> l.getTxId()).collect(toSet());
Set<String> dbKeys = dbRecords.stream().map(r -> r.getTxId()).collect(toSet());
// Redis 有,DB 无 → 补发
redisKeys.removeAll(dbKeys);
if (!redisKeys.isEmpty()) {
compensationService.replayEvents(redisKeys);
}
}
✅ 经验:对账不是“可有可无”,而是高并发交易系统的最后一道保险。
五、微观第四招:Java 异常与 Servlet 容器选型——稳住最后一道防线
很多人忽略:高并发下,异常处理不当会直接拖垮线程池。
1. 异常分层设计
- 业务异常(如余额不足)→ 返回 400,不打印 error 日志
- 系统异常(如 DB 连接超时)→ 返回 500,触发告警
- 可恢复异常(如 MQ 重试)→ 捕获后入死信队列,不抛出
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(InsufficientBalanceException.class)
public ResponseEntity<?> handleBizEx(InsufficientBalanceException e) {
return ResponseEntity.status(400).body(e.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleSysEx(Exception e) {
log.error("System error", e); // 告警
return ResponseEntity.status(500).body("系统繁忙");
}
}
2. Servlet 容器选型:Undertow vs Tomcat
- Tomcat:兼容性好,但 NIO 模型在高并发下线程开销大
- Undertow:基于 XNIO,非阻塞 I/O,内存占用低 30%,吞吐高 20%
我们在 Spring Boot 中切换:
# application.properties
server.servlet.container=undertow
server.undertow.io-threads=16
server.undertow.worker-threads=200
✅ 效果:单机 QPS 从 1200 提升到 1800,GC 频率下降 40%。
单体高并发优化口诀(我的实战总结)
Lua 原子保正确,
异步落库解耦合;
对账兜底守底线,
异常分层稳如山;
Undertow 轻上阵,
单体也能扛百万。
结语:优化始于微观,成于体系
我们没有一上来就拆微服务,而是在单体内部构建了一套“高内聚、低耦合、强可观测”的交易子系统。
结果:单机扛住 2000+ QPS,资损率连续 18 个月为 0。
高并发不是架构的胜利,而是微观细节的胜利。
当你能把 Redis Lua、异步落库、对账补偿、异常治理、容器调优做到极致,
你会发现:单体,也可以很强大。
技术是手段,稳定是目的。
在你急着“拆”之前,先问问自己:“我是否已经榨干了单体的每一滴性能?”
欢迎在评论区交流你的高并发优化经验!


被折叠的 条评论
为什么被折叠?



