1. MySQL索引篇:为什么是B+树?
1.1 从磁盘I/O角度理解数据结构选择
当面试官问及MySQL索引结构时,80%的候选人能说出"B+树"这个名词,但只有20%能讲清楚背后的硬件原理。让我们从计算机组成原理的视角重新审视这个问题。
机械硬盘的随机访问延迟通常在10ms左右,而SSD约为0.1ms。这意味着:
- 一次随机磁盘I/O相当于约50万次CPU指令周期
- 传统B树每个节点存储数据会导致节点容量减小
- B+树非叶子节点仅存储键值,使得单个16KB页能容纳更多索引项
具体来看,假设:
- 主键为bigint(8字节)
- 指针占用6字节
- 行数据约1KB
在B树结构中,每个节点最多存储:
(16KB)/(8+6+1024) ≈ 15个条目
而B+树非叶子节点可存储:
(16KB)/(8+6) ≈ 1142个条目
这意味着B+树的扇出(Fan-out)是B树的76倍!对于1亿条记录:
- B+树仅需log₁₁₄₂(100,000,000)≈3层
- B树需要log₁₅(100,000,000)≈7层
1.2 范围查询的链表优化
实际业务中,分页查询和范围扫描占比超过60%。B+树的双向链表设计使这类查询效率提升显著:
sql复制-- 普通范围查询
SELECT * FROM orders WHERE create_time BETWEEN '2023-01-01' AND '2023-01-31';
-- 分页查询
SELECT * FROM products ORDER BY price DESC LIMIT 10000, 20;
B树执行过程:
- 从根节点开始查找下限值
- 中序遍历到右子树
- 反复在非叶子节点间跳跃
- 涉及大量随机I/O
B+树执行过程:
- 定位到叶子节点的起始位置
- 沿链表顺序扫描
- 完全避免非叶子节点访问
- 产生连续的顺序I/O
实测对比:在1000万条记录的表中,B+树范围查询速度比B树快8-12倍
1.3 实战中的索引优化建议
- 前缀索引技巧:
sql复制-- 对长字符串列优化
ALTER TABLE users ADD INDEX idx_name_email (name(10), email(6));
- 覆盖索引陷阱:
sql复制-- 看似使用了覆盖索引
EXPLAIN SELECT user_id FROM orders WHERE status = 'paid';
-- 实际可能失效的情况
EXPLAIN SELECT user_id, order_time FROM orders WHERE status = 'paid';
- 索引合并的代价:
sql复制-- 索引合并可能不如复合索引高效
ALTER TABLE products ADD INDEX idx_cate_status (category_id, status);
2. Java并发篇:ThreadLocal深度解析
2.1 线程隔离的实现机制
现代JDK中ThreadLocal的实现堪称精妙:
java复制public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals;
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
}
}
关键设计点:
- 每个Thread持有自己的ThreadLocalMap
- Map的Key是ThreadLocal实例的弱引用
- Value是用户设置的强引用对象
2.2 内存泄漏的完整链条
典型泄漏场景:
- 线程池中的worker线程长期存活
- 业务代码设置ThreadLocal后未remove
- ThreadLocal外部强引用消失(如方法结束)
- GC回收Key的弱引用,留下null键值对
- 线程复用导致Map不断积累垃圾条目
java复制// 典型错误示例
ExecutorService pool = Executors.newFixedThreadPool(5);
pool.execute(() -> {
ThreadLocal<User> local = new ThreadLocal<>();
local.set(new User()); // 泄漏开始
// 忘记local.remove()
});
2.3 高级应用与最佳实践
- InheritableThreadLocal的陷阱:
java复制// 父线程传递值给子线程
InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
itl.set("parent_value");
new Thread(() -> {
// 获取到父线程值
System.out.println(itl.get());
}).start();
// 线程池场景下会得到错误的上次任务值
- FastThreadLocal优化:
Netty提供的替代方案,使用数组而非Map:
- 通过atomic变量分配index
- 线程本地存储使用Object[]而非HashMap
- 访问速度提升3-5倍
- 防御性编程建议:
java复制try {
threadLocal.set(resource);
// ...业务逻辑
} finally {
threadLocal.remove(); // 必须确保执行
}
3. Redis缓存异常处理方案
3.1 穿透 vs 击穿 vs 雪崩对比
| 问题类型 | QPS特征 | 持续时间 | 影响范围 | 典型解决方案 |
|---|---|---|---|---|
| 穿透 | 持续中等流量 | 长期 | 单个Key | 布隆过滤器、空值缓存 |
| 击穿 | 瞬时高峰 | 短暂 | 热点Key | 互斥锁、逻辑过期 |
| 雪崩 | 系统级高峰 | 持续较久 | 大量Key | 随机TTL、集群高可用 |
3.2 布隆过滤器的实现细节
Guava的实现示例:
java复制BloomFilter<String> filter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000, // 预期元素数量
0.01 // 误判率
);
// 预热数据
for (String id : existingIds) {
filter.put(id);
}
// 查询拦截
if (!filter.mightContain(queryId)) {
return null; // 直接拦截
}
注意事项:
- 误判率与内存占用成反比
- 不支持删除操作(Counting Bloom Filter可解决)
- 需要定期重建(数据变更时)
3.3 分布式锁的进阶实现
Redisson方案示例:
java复制RLock lock = redisson.getLock("product_" + productId);
try {
// 尝试加锁,最多等待100ms,锁自动释放时间30s
if (lock.tryLock(100, 30000, TimeUnit.MILLISECONDS)) {
// 查询数据库
Product product = getFromDB(productId);
// 更新缓存
redis.setex(cacheKey, 3600, product);
return product;
}
} finally {
lock.unlock();
}
关键改进点:
- 锁自动续期机制(watchdog)
- 避免锁永久占用
- 支持可重入特性
4. 高并发库存系统设计实战
4.1 分层防御体系构建
完整架构图:
code复制用户层 -> 浏览器限流 -> CDN缓存 -> 网关限流 ->
服务层 -> 本地缓存 -> Redis集群 -> 消息队列 ->
数据层 -> 数据库分片 -> 备份容灾
4.2 Redis原子操作优化
Lua脚本实现库存扣减:
lua复制-- KEYS[1]: 库存key
-- ARGV[1]: 扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
return redis.call('DECRBY', KEYS[1], ARGV[1])
else
return -1
end
优势:
- 单次网络开销
- 原子性执行
- 避免客户端竞争
4.3 最终一致性方案
补偿事务设计:
java复制// 定时任务检查悬挂事务
@Scheduled(fixedRate = 60000)
public void checkInventoryTx() {
List<InventoryLog> pendings = dao.queryPendingLogs();
for (InventoryLog log : pendings) {
if (log.expireTime < System.currentTimeMillis()) {
// 恢复库存
redis.incrBy(log.productId, log.amount);
// 更新状态
log.status = "ROLLBACK";
dao.update(log);
}
}
}
4.4 压测数据参考
模拟10万并发测试结果:
| 方案 | QPS | 平均延迟 | 超卖率 |
|---|---|---|---|
| 纯数据库事务 | 1,200 | 850ms | 0% |
| Redis预扣减 | 45,000 | 35ms | 0.2% |
| Redis+MQ+异步确认 | 68,000 | 22ms | 0% |
关键发现:
- 纯数据库方案完全不可行
- 异步方案吞吐量提升50倍
- 需要根据业务容忍度选择方案
5. 面试应答技巧精要
5.1 STAR法则应用
情景(Situation):
"在我们电商系统的618大促中..."
任务(Task):
"需要保证100万件商品库存的准确扣减..."
行动(Action):
"设计了三级缓存体系,先用Redis原子操作..."
结果(Result):
"最终实现零超卖,系统扛住了5万QPS..."
5.2 技术选型对比表
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 悲观锁 | 强一致性 | 性能差 | 金融交易 |
| 乐观锁 | 高吞吐 | 重试成本高 | 商品库存 |
| 分布式锁 | 实现简单 | 单点风险 | 临时方案 |
| 消息队列 | 削峰填谷 | 延迟较高 | 可容忍延迟的场景 |
5.3 常见问题应答模板
当被问及"为什么选择X而不是Y"时:
"在A场景下X确实更合适,因为它能解决B问题。不过我们也考虑过Y方案,经过压测发现当C条件时会出现D问题。最终选择X是因为..."
举例:
"选择Redis而不是数据库直接扣减,是因为Redis的单线程模型能保证原子性,实测QPS可以从2000提升到50000。当然我们也考虑了Zookeeper的方案,但发现写入延迟太高..."
6. 系统设计原则总结
6.1 空间换时间典型案例
- 索引结构:B+树多消耗30%空间,换取查询性能提升10倍
- 缓存体系:用20%内存空间缓存热点数据,降低80%数据库负载
- 冗余存储:订单详情反范式化设计,避免多表关联查询
6.2 时间换空间优化策略
- 压缩算法:对冷数据采用ZSTD压缩,节省60%存储空间
- 延迟加载:用户信息分片获取,减少单次传输数据量
- 计算卸载:将排序操作转移到客户端处理
6.3 一致性权衡实践
-
最终一致性:
- 订单状态异步更新
- 允许短暂查询不一致
- 通过对账任务修复
-
强一致性:
- 支付核心流程
- 采用分布式事务
- 性能牺牲在可接受范围
-
读写分离:
- 主库写入
- 从库读取
- 延迟监控关键指标