嘿,老弟,别急着复制粘贴那行 System.out.println("Hello World") 了。我知道你现在可能正对着 IntelliJ IDEA 的欢迎界面发呆,或者刚写完第一个 Spring Boot 项目,跑得挺欢实,心里还美滋滋的。但我想问你一个问题:当你的用户从 1 个变成 1 万,再变成 100 万的时候,你的应用还能这么“欢实”吗?
大概率不能。它会慢得像蜗牛,卡得像 PPT,最后直接抛出 OutOfMemoryError 或者 ConnectionPoolException,让你在凌晨三点的会议室里对着老板和产品经理解释为什么服务器又崩了。
作为一名在代码坑里摔打过无数次的“老兵”,我今天不跟你讲那些枯燥的理论。咱们直接聊聊,怎么从一个只会写 CRUD 的菜鸟,一步步进化成能扛住双十一流量的架构师。这篇文章,我会用最通俗的大白话,配上真实的“踩坑”案例,带你把高并发项目搭起来。
第一阶段:别急着写代码,先想想“地基”在哪里
很多新手(包括曾经的我)拿到需求就开干:建模块、加依赖、写 Controller、写 Service、写 Repository。一气呵成,半小时搞定一个接口,爽!
但是,当你准备接入数据库的时候,问题就来了。
坑点 1:JDBC 连接的滥用
你有没有发现,每次请求进来,都 DriverManager.getConnection()?或者在 Service 层里随手 new 一个连接?
千万别这么干。
数据库连接是一种非常昂贵的资源。建立连接需要握手、验证权限、分配内存。如果每秒有 1000 个请求,你就需要创建 1000 个连接,数据库服务器会直接累死。
解决方案:使用连接池
在 Spring Boot 项目中,我们通常使用 HikariCP(Spring Boot 默认的)、Druid 或者 C3P0。
- HikariCP:性能最强,配置简单,Spring Boot 2.x+ 默认使用它。
- Druid:阿里出品,监控功能强大,适合需要详细监控 SQL 执行时间的场景。
避坑指南:
- 不要手动管理连接。让 Spring 的
DataSource来管理。 - 合理配置连接池大小。不是越大越好!连接池大小取决于你的 CPU 核心数、磁盘 IO 速度和数据库处理能力。
- 一般建议:
核心数 * 2 + 有效磁盘数。 - 如果是高并发且 IO 密集型,可以适当调大,但不要超过数据库的最大连接数限制。
- 一般建议:
- 设置合理的超时时间。
connectionTimeout(获取连接的超时时间)、idleTimeout(连接空闲超时时间)、maxLifetime(连接最大存活时间)。
# application.yml 示例配置
spring:
datasource:
hikari:
maximum-pool-size: 20 # 根据实际负载调整,不要盲目设成 100
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
坑点 2:忽略事务传播行为
在 Service 层,你是否习惯性地加 @Transactional?
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// 保存订单
orderRepository.save(order);
// 扣减库存
inventoryService.deductStock(order.getProductId(), order.getQuantity());
}
}
看起来没问题?但如果 deductStock 也加了 @Transactional,而且两个方法在同一个事务里调用,会发生什么?
答案:死锁风险增加。
因为 Spring 的事务默认是 PROPAGATION_REQUIRED,即如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务。
在高并发场景下,如果两个请求同时操作同一条库存记录,一个先锁住了行 A,请求 B 锁住了行 B,然后互相等待对方释放,死锁就发生了。
避坑指南:
- 明确事务边界。尽量让事务只在最外层方法上开启,内部方法不要重复开启事务,除非有特殊需求(如
REQUIRES_NEW)。 - 使用乐观锁。对于库存扣减这种场景,推荐使用
@Version字段配合乐观锁,或者使用数据库的UPDATE ... WHERE stock >= #quantity语句,避免行锁竞争。 - 合理设计表结构。尽量避免大事务,将大事务拆分成小事务,或者使用异步处理。
第二阶段:高并发的核心——缓存
数据库是瓶颈,缓存是救命稻草。
坑点 3:缓存穿透、缓存击穿、缓存雪崩
这三个词,面试官必问,实战中必死。
1. 缓存穿透:查询不存在的数据
用户恶意查询一个根本不存在的数据(比如 ID 为 -1 的商品),每次请求都会打到数据库,导致数据库压力过大,缓存形同虚设。
- 解决方案:
- 布隆过滤器:在缓存层之前加一个布隆过滤器,判断请求的 key 是否存在。如果不存在,直接返回。
- 缓存空值:即使数据库中没有该数据,也将空值缓存起来,设置较短的过期时间(如 5 分钟)。
2. 缓存击穿:热点 key 过期
某个热点数据(如秒杀商品)在过期瞬间,大量请求同时打到数据库,导致数据库崩溃。
- 解决方案:
- 互斥锁:当缓存失效时,不直接查数据库,而是先获取一个分布式锁,只有一个线程去查数据库并重建缓存,其他线程等待。
- 永不过期:将热点数据的过期时间设置为永久,或者使用后台异步更新机制。
3. 缓存雪崩:大量 key 同时过期
所有缓存都在同一时间点过期,导致大量请求打到数据库。
- 解决方案:
- 随机过期时间:给缓存的过期时间加上一个随机值,避免大量 key 同时过期。
- 高可用架构:使用 Redis 集群,避免单点故障。
- 限流降级:在缓存层失效时,对请求进行限流,保护数据库。
避坑指南:
- 不要迷信缓存。缓存是为了减轻数据库压力,但不能替代数据库。
- 合理设置过期时间。根据业务场景,设置合适的过期时间。
- 监控缓存命中率。使用 Redis 的
INFO stats命令监控命中率,及时发现异常。
第三阶段:高并发的利器——异步处理
坑点 4:同步阻塞导致的性能瓶颈
在电商系统中,用户下单后,需要发送短信、推送通知、积分更新等操作。如果这些操作都在主线程中同步执行,会导致请求响应时间变长,用户体验极差。
解决方案:使用异步处理
Spring Boot 提供了 @Async 注解,可以轻松实现异步方法调用。
@Service
public class OrderService {
@Async
public void sendSmsNotification(String phoneNumber, String message) {
// 发送短信
smsService.send(phoneNumber, message);
}
@Async
public void updatePoints(Long userId, int points) {
// 更新积分
pointsService.update(userId, points);
}
@Transactional
public void createOrder(Order order) {
// 保存订单
orderRepository.save(order);
// 异步发送通知
sendSmsNotification(order.getPhoneNumber(), "订单创建成功");
// 异步更新积分
updatePoints(order.getUserId(), 100);
}
}
注意:
- 必须启用异步支持。在配置类上添加
@EnableAsync。 - 线程池配置。默认线程池可能不够用,建议自定义线程池,合理配置核心线程数、最大线程数、队列容量等。
- 异常处理。异步方法中的异常不会被主线程捕获,需要配置全局异常处理器。
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
// 记录异常日志
log.error("异步方法执行异常: {}", ex.getMessage(), ex);
};
}
}
坑点 5:消息队列的选择和使用
异步处理可以使用 @Async,但对于高并发场景,消息队列(MQ) 是更好的选择。
常见 MQ 对比:
- Kafka:高吞吐,适合日志收集、大数据处理。
- RocketMQ:阿里巴巴出品,支持事务消息,适合金融级业务。
- RabbitMQ:灵活的路由机制,适合复杂业务场景。
- Redis:轻量级,适合简单的消息队列场景。
避坑指南:
- 不要为了用 MQ 而用 MQ。只有当同步处理无法满足性能要求,或者需要解耦系统时,才考虑使用 MQ。
- 保证消息的可靠性。使用事务消息、本地消息表等机制,确保消息不丢失。
- 处理消息重复消费。MQ 不能保证消息只被消费一次,需要业务层做幂等性处理。
第四阶段:高并发的保障——限流与降级
坑点 6:没有防护的系统是脆弱的
即使你做了缓存、异步、MQ,系统仍然可能因为突发流量而崩溃。比如,某个接口被恶意刷量,或者某个下游服务不可用。
解决方案:限流与降级
1. 限流
限制每秒/分钟的请求数量,防止系统过载。
- 令牌桶算法:以恒定速率向桶中放置令牌,请求需要消耗令牌才能通过。
- 漏桶算法:请求进入漏桶,以恒定速率流出。
- 滑动窗口:统计最近一段时间内的请求数量。
常用实现:
- Sentinel:阿里巴巴开源,功能强大,支持限流、熔断、降级。
- Resilience4j:轻量级,适合微服务场景。
- Guava RateLimiter:简单好用,但功能有限。
2. 降级
当某个服务不可用时,返回默认值或友好提示,保证核心业务可用。
@Service
public class InventoryService {
@SentinelResource(value = "deductStock", blockHandler = "handleException")
public boolean deductStock(Long productId, int quantity) {
// 核心逻辑
return inventoryDao.deductStock(productId, quantity);
}
// 降级方法
public boolean handleException(Long productId, int quantity, BlockException ex) {
log.warn("库存扣减被限流: productId={}", productId);
return false; // 或者返回默认值
}
}
避坑指南:
- 合理配置限流阈值。根据系统承载能力和业务需求,设置合理的 QPS 限制。
- 分级限流。对不同接口、不同用户设置不同的限流阈值。
- 监控和告警。实时监控限流情况,及时告警。
第五阶段:高并发的监控与运维
坑点 7:不知道系统哪里出了问题
系统崩了,你怎么办?去服务器上看日志?去找 DBA 查慢查询?去找运维看监控?
解决方案:建立完善的监控体系
1. 应用监控
- Spring Boot Actuator:提供健康检查、指标监控、环境信息等功能。
- Micrometer:指标采集和上报,支持 Prometheus、InfluxDB 等。
- Prometheus + Grafana:主流的监控和可视化方案。
2. 日志监控
- ELK Stack:Elasticsearch + Logstash + Kibana,用于日志收集、分析和可视化。
- Graylog:轻量级日志管理平台。
- Sentry:用于错误追踪和监控。
3. 链路追踪
- SkyWalking:国产开源,功能强大,支持 JVM 监控、分布式追踪。
- Zipkin:Twitter 开源,轻量级链路追踪系统。
- Jaeger:Uber 开源,适合微服务架构。
避坑指南:
- 不要等到出问题再监控。在系统上线前,就配置好监控和告警。
- 监控关键指标。CPU 使用率、内存使用率、QPS、响应时间、错误率等。
- 设置合理的告警阈值。避免告警风暴,又要能及时发现问题。
第六阶段:数据库层面的优化
坑点 8:SQL 性能低下
再好的架构,也架不住一个烂 SQL。
常见优化手段:
索引优化
- 为常用查询字段添加索引。
- 避免在索引列上进行函数运算或类型转换。
- 使用
EXPLAIN分析 SQL 执行计划。
SQL 语句优化
- 避免
SELECT *,只查询需要的字段。 - 避免在大表上进行分页查询,使用延迟关联。
- 使用
UNION ALL代替UNION。 - 批量插入、更新,减少数据库交互次数。
- 避免
分库分表
- 当单表数据量过大(如超过 1000 万)时,考虑分库分表。
- 使用 ShardingSphere 等中间件简化开发。
读写分离
- 主库写,从库读,减轻主库压力。
- 使用 MyBatis-Plus 等框架简化配置。
避坑指南:
- 不要过度优化。在性能问题没有出现前,不要过早优化。
- 测试验证。任何优化方案都要在测试环境验证效果。
- 关注慢查询日志。定期分析慢查询日志,发现性能瓶颈。
第七阶段:真实案例分享——某电商平台秒杀系统
让我给你讲一个真实的例子。
背景:某电商平台要在双十一搞秒杀活动,一款限量 100 台的高端手机,秒杀价 1 元。
最初方案:
- Spring Boot + MySQL
- 直接扣减数据库库存
- 同步发送短信通知
问题爆发:
- 活动开始前 10 分钟,系统崩溃。
- 数据库连接池耗尽,大量请求超时。
- 短信网关被打爆,通知延迟严重。
优化方案:
- Redis 预扣库存:活动开始前,将库存加载到 Redis,使用 Lua 脚本原子扣减库存,防止超卖。
- 消息队列异步处理:下单成功后,将订单信息发送到 RocketMQ,异步写入数据库和发送短信。
- 限流与降级:使用 Sentinel 对秒杀接口限流,每秒最多处理 1000 个请求;下游服务不可用时,返回排队提示。
- 前端优化:页面静态化,使用 CDN 加速,按钮点击后禁用,防止重复提交。
- 数据库优化:订单表分库分表,使用读写分离。
结果:
- 双十一当天,系统平稳运行,秒杀活动顺利完成。
- 用户排队等待,但没有崩溃。
- 短信通知延迟在可接受范围内。
第八阶段:给你的实战 checklist
最后,我给你整理了一份实战 checklist,每次搭建高并发项目前,都过一遍:
架构设计
- [ ] 是否明确了系统的 QPS 和并发量?
- [ ] 是否设计了合理的分层架构(Controller/Service/DAO)?
- [ ] 是否考虑了系统的可扩展性和可维护性?
数据库
- [ ] 是否使用了连接池?连接池参数是否合理?
- [ ] 是否对常用查询字段添加了索引?
- [ ] 是否分析了慢查询日志?
- [ ] 是否设计了分库分表方案?
缓存
- [ ] 是否识别了热点数据?
- [ ] 是否设计了缓存过期策略?
- [ ] 是否考虑了缓存穿透、击穿、雪崩问题?
- [ ] 是否监控了缓存命中率?
异步处理
- [ ] 是否将耗时操作(如发送短信、推送通知)异步化?
- [ ] 是否配置了合理的线程池?
- [ ] 是否考虑了消息的可靠性?
限流降级
- [ ] 是否对核心接口进行了限流?
- [ ] 是否配置了熔断降级策略?
- [ ] 是否测试了限流降级的效果?
监控告警
- [ ] 是否配置了应用监控(Actuator/Micrometer)?
- [ ] 是否配置了日志监控(ELK/Graylog)?
- [ ] 是否配置了链路追踪(SkyWalking/Zipkin)?
- [ ] 是否设置了合理的告警阈值?
结语
从 HelloWorld 到高并发项目,中间隔着的不是几行代码,而是对系统架构的深刻理解,对性能瓶颈的精准把控,以及对各种坑的熟练规避。
高并发项目没有银弹,只有不断的学习、实践和总结。希望这篇文章能帮你少踩一些坑,多走一些路。
记住,**代码是写给人看的,只是顺便
