
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 then
if redis.call(get, KEYS[1]) == ARGV[1] then
return redis.call(del, KEYS[1])
end
end
return 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优化
索引优化
避免函数、隐式转换、左模糊
这些秘诀不是孤立存在的,它们构成了高并发系统的骨架。掌握这些图解原理,你在面试中就能自信地画出架构图,解释每个组件的作用。
你在项目里踩过这个坑吗?评论区聊聊