5个秘诀图解原理:后端高并发避坑指南
面试时被问“为什么你的接口在高并发下挂了”,结果只能支支吾吾说“可能是负载高”,这种尴尬谁没经历过?很多后端工程师背了无数八股文,一到实战就露怯,根本搞不清底层图解原理。
别急,今天不整虚的。结合我在大厂踩过的坑,拆解5个核心秘诀,用代码和图表把原理讲透,让你下次面试能直接甩出方案。
秘诀一:连接池不是万能的,得懂“池化”边界
很多人以为配置了HikariCP或Druid就万事大吉,其实不然。连接池的核心不是“复用”,而是资源隔离。
图解原理: 想象一个停车场(数据库),车(连接)有数量限制。如果所有车都堵在门口排队(等待获取连接),整个停车场就瘫痪了。
// HikariCP 配置示例:关键参数解析
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/db");
config.setMaximumPoolSize(10); // 秘诀:不是越大越好,通常 CPU核数 * 2 + 磁盘数
config.setConnectionTimeout(3000); // 获取连接超时,避免线程无限阻塞
config.setIdleTimeout(600000); // 空闲连接回收时间
避坑点:
在CSDN上搜索“HikariCP调优”,你会发现大量案例显示,maximumPoolSize 设置过大反而导致数据库上下文切换开销激增。建议通过 show processlist 监控活跃连接数,动态调整。
秘诀二:缓存穿透与雪崩的“双层防御”
Redis缓存是性能提升的关键,但一旦失效,流量直接打到数据库,瞬间压垮。
核心差异对比:
| 故障类型 | 现象 | 根本原因 | 解决方案 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 缓存和DB都没有 | 布隆过滤器/空值缓存 |
| 缓存击穿 | 热点Key过期 | 高并发同时查同一个Key | 互斥锁/逻辑过期 |
| 缓存雪崩 | 大量Key同时过期 | 统一TTL策略 | 随机TTL/多级缓存 |
代码实战:逻辑过期法
// 使用互斥锁解决缓存击穿
public String getHotData(String key) {String data = redis.get(key);if (data == null) {// 秘诀:双重检查锁,防止并发穿透RLock lock = redissonClient.getLock("lock:" + key);if (lock.tryLock()) {try {// 再次检查缓存,防止其他线程已填充data = redis.get(key);if (data == null) {data = db.query(key); // 查库redis.set(key, data, 30 + random(10), TimeUnit.MINUTES); // 随机TTL}} finally {lock.unlock();}}}return data;
}
注意: 随机TTL是防止雪崩的秘诀,但别用固定值加随机数,要用 base + random(0, delta) 的方式,避免分布不均。
秘诀三:线程池拒绝策略的“生死抉择”
线程池满了怎么办?默认是 AbortPolicy,直接抛异常,业务中断。但在高并发场景,我们需要更优雅的降级。
图解原理: 线程池像是一个有固定工位(corePoolSize)和临时工(maxPoolSize)的工厂。任务来了,先给正式工,再给临时工,再进队列(queue)。队列满了,就触发拒绝策略。
适用场景对比:
| 策略 | 行为 | 适用场景 | 风险 |
|---|---|---|---|
| AbortPolicy | 抛异常 | 关键业务,必须知道失败 | 中断流程 |
| CallerRunsPolicy | 调用线程执行 | 非关键业务,需保证不丢 | 主线程阻塞 |
| DiscardOldestPolicy | 丢弃队列头部 | 日志、监控等非实时任务 | 数据丢失 |
| DiscardPolicy | 静默丢弃 | 极低价值任务 | 无感知丢失 |
代码示例:自定义降级策略
// 秘诀:自定义RejectionHandler,记录日志并降级
RejectedExecutionHandler handler = (r, executor) -> {log.warn("线程池已满,任务{}被拒绝,执行降级", r.toString());// 这里可以调用备用逻辑,比如返回默认值或写入延迟队列fallbackService.handle(r);
};ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 20, 60L, TimeUnit.SECONDS,new LinkedBlockingQueue<>(100),new ThreadFactoryBuilder().setNameFormat("biz-pool-%d").build(),handler
);
避坑点:
千万不要用 Executors.newFixedThreadPool(),它使用无界队列,容易OOM。务必显式指定队列容量。
秘诀四:分布式锁的“误删”陷阱
Redis分布式锁看似简单,但DEL命令可能误删其他节点的锁。
核心问题:
节点A加锁,未过期但GC暂停,锁过期。节点B加锁。节点A恢复,执行DEL key,删掉的是节点B的锁。
解决方案:Lua脚本保证原子性
-- 秘诀:将判断和删除放在同一个Lua脚本中
local script = [[if redis.call("exists", KEYS[1]) == 1 thenif redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1])endendreturn 0
]]// Java调用
Boolean result = redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),Collections.singletonList(lockKey),requestId // 唯一标识,防止误删
);
进阶技巧:
使用Redisson的RLock,它内部实现了看门狗(Watchdog)机制,自动续期,避免手动管理锁过期时间。
秘诀五:SQL慢查询的“索引失效”盲区
90%的慢查询都是因为索引失效。
常见失效场景:
- 对索引列使用函数:
WHERE YEAR(create_time) = 2023 - 隐式类型转换:
WHERE phone = 13800138000(phone是varchar) LIKE '%xxx':左模糊查询OR连接非索引列
图解原理: B+树索引是有序的。当使用函数或左模糊时,数据库无法利用有序性,只能全表扫描。
优化案例:
-- 错误写法
SELECT * FROM orders WHERE YEAR(create_time) = 2023;-- 正确写法:范围查询
SELECT * FROM orders WHERE create_time >= '2023-01-01' AND create_time < '2024-01-01';
工具推荐:
使用 EXPLAIN 分析执行计划,关注 type 字段。ALL 表示全表扫描,ref 或 range 才是高效访问。
总结与选型建议
| 场景 | 推荐方案 | 关键参数/技巧 |
|---|---|---|
| 高并发读 | Redis + 本地缓存 | 逻辑过期,随机TTL |
| 数据库连接 | HikariCP | 池大小 = CPU * 2 + 磁盘 |
| 线程池 | ThreadPoolExecutor | 自定义拒绝策略,有界队列 |
| 分布式锁 | Redisson | 看门狗机制,Lua脚本 |
| SQL优化 | 索引优化 | 避免函数、隐式转换、左模糊 |
这些秘诀不是孤立存在的,它们构成了高并发系统的骨架。掌握这些图解原理,你在面试中就能自信地画出架构图,解释每个组件的作用。
你在项目里踩过这个坑吗?评论区聊聊