1. 高并发系统的本质与挑战
当面试官问"如何设计一个高并发系统"时,80%的候选人会条件反射般回答"加Redis缓存、用消息队列"。这种标准答案背后暴露的正是对高并发本质的误解。高并发不是简单的技术堆砌,而是对系统架构、数据一致性、资源调度等核心问题的系统性解决方案。
真正的并发挑战往往出现在这些场景:
- 电商秒杀活动中,10万QPS瞬间冲击商品库存系统
- 春运抢票时,同一车次的座位状态被高频查询和修改
- 社交平台热点事件爆发,千万级用户同时刷新动态流
这些场景的共同特点是:共享资源的竞争访问。以电商库存为例,当多个请求同时扣减库存时,如果没有正确处理并发,可能导致超卖。我曾见过一个案例:某促销活动显示售出2000件商品,实际发货时却发现库存多扣了300件——这正是典型的并发控制失效。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈的合理选型与误区
2.1 Spring Boot的定位与局限
Spring Boot确实是快速构建微服务的利器,但很多开发者把它当作高并发的"银弹"。实际上,Spring MVC默认的Tomcat容器在超过5000QPS时就会遇到性能瓶颈。我曾做过压力测试:
- 默认配置下:Tomcat线程池满负荷时平均响应时间从50ms飙升到800ms
- 优化后(Undertow+参数调优):同等压力下响应时间稳定在120ms以内
关键配置示例(application.yml):
yaml复制server:
undertow:
threads:
io: 16
worker: 256
buffer-size: 1024
direct-buffers: true
2.2 MyBatis的并发陷阱
MyBatis的ORM操作在高并发下容易成为性能黑洞。最常见的问题是N+1查询——当遍历用户列表并获取每个用户的详情时,会产生1次查列表+N次查详情的查询。某次系统优化中,我们发现一个原本应该200ms完成的查询,在并发量增大时竟然需要5秒,根源就是这种查询模式。
解决方案:
- 使用
标签实现结果集嵌套 - 开启二级缓存(需注意分布式环境的一致性)
- 批量操作代替循环单条处理
3. Redis的正确打开方式
3.1 缓存击穿与雪崩防护
很多项目虽然用了Redis,却栽在了缓存策略上。某金融项目在促销时Redis集群崩溃,原因正是缓存雪崩——大量Key同时过期导致请求直接打到数据库。我们最终采用多级缓存方案:
- 本地缓存(Caffeine):应对瞬时高峰
- Redis集群:分布式缓存层
- 数据库:最终数据源
关键防护措施:
- 过期时间添加随机抖动(基础300秒±60秒随机值)
- 热点Key永不过期,通过后台线程异步更新
- 使用Redis的LFU淘汰策略替代默认的LRU
3.2 分布式锁的坑与最佳实践
Redis分布式锁看似简单,实则暗藏杀机。某次线上事故中,由于锁过期时间设置不当,导致两个请求同时进入临界区,造成资金重复结算。正确的Redlock实现应该包含:
java复制// 获取锁
String lockValue = UUID.randomUUID().toString();
Boolean locked = redisTemplate.opsForValue().setIfAbsent(
"order_lock_" + orderId,
lockValue,
30, TimeUnit.SECONDS);
// 释放锁(Lua脚本保证原子性)
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList("order_lock_" + orderId),
lockValue);
4. 全链路监控与性能调优
4.1 基于Jaeger的调用链追踪
当并发量上去后,系统表现往往与预期不符。某次大促前压测时,我们发现平均响应时间异常升高,通过Jaeger追踪发现是某个非关键服务(商品评价)拖慢了主链路。解决方案:
- 非关键路径异步化(@Async)
- 设置合理的Hystrix熔断策略
- 关键路径服务独立线程池隔离
Jaeger的典型埋点方式:
java复制@GetMapping("/product/{id}")
public ProductDetail getProduct(@PathVariable Long id) {
try (Scope scope = tracer.buildSpan("getProductDetail").startActive(true)) {
scope.span().setTag("productId", id);
// 业务逻辑...
}
}
4.2 数据库层面的并发控制
高并发最终考验的是数据库的承载能力。某社交平台的私信功能在晚高峰经常超时,分析发现是MyBatis频繁创建新连接导致连接池耗尽。优化方案:
- 调整连接池参数(示例配置):
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
- 读写分离:查询走从库
- 分库分表:用户ID取模分片
5. 面试中的高频陷阱题解析
5.1 "你们的系统QPS是多少?"
这是个典型的考察系统理解深度的问题。初级开发者往往直接报测试数据,而高级开发者会区分:
- 系统设计容量(如5万QPS)
- 日常平均负载(如800QPS)
- 峰值承受能力(如双11期间的12万QPS)
- 不同接口的QPS差异(登录接口 vs 浏览接口)
更专业的回答应该包含:
- 压力测试方法(JMeter测试脚本设计)
- 监控指标(TP99、TP999)
- 限流策略(Guava RateLimiter或Sentinel配置)
5.2 "如何保证缓存与数据库的一致性?"
这是区分水货与真材实料的分水岭。常见错误方案:
- 先更新数据库再删缓存(仍有不一致窗口)
- 使用定时任务全量刷新缓存(延迟高)
我们采用的最终一致性方案:
- 数据库变更后写入binlog
- Canal监听binlog发送到MQ
- 消费者根据MQ消息更新Redis
- 关键业务路径增加版本号校验
6. 真实案例:秒杀系统架构演进
某电商平台最初版本的秒杀系统在第一次大促时就崩溃了。我们通过三次迭代实现了质的飞跃:
6.1 V1.0版本的问题
- 直接查询数据库库存
- 同步扣减库存
- 订单创建与支付强耦合
结果:数据库连接池耗尽,500错误率高达60%
6.2 V2.0优化方案
- 库存预热到Redis
- Lua脚本保证原子扣减
- 订单先创建后异步支付
关键Lua脚本:
lua复制local stock = tonumber(redis.call('GET', KEYS[1]))
if stock > 0 then
redis.call('DECR', KEYS[1])
return 1
else
return 0
end
6.3 V3.0最终架构
- 接入层:Nginx限流 + 令牌桶
- 服务层:库存分段(100个商品分10段)
- 数据层:Redis集群 + 数据库分库
- 降级方案:静态页面对非核心功能
效果:从每秒300订单提升到15000订单,且系统稳定性大幅提高
