跳到主要内容

设计写密集型系统

问题

如何设计一个写多读少的高吞吐数据库架构?

答案

一、场景分析

场景写入量级特点
日志收集百万 QPS只追加,几乎不改
埋点数据十万 QPS高吞吐,可接受延迟
IoT 传感器百万 QPS时序数据,定期聚合
秒杀库存扣减万级 QPS热点行,强一致
消息系统十万 QPS顺序写入,持久化

二、写优化策略总览

三、批量写入

// ❌ 逐条插入
for (const log of logs) {
await db.execute('INSERT INTO logs (msg, ts) VALUES (?, ?)', [log.msg, log.ts]);
}
// 1000 条 = 1000 次网络往返

// ✅ 批量插入
await db.execute(
'INSERT INTO logs (msg, ts) VALUES ' +
logs.map(() => '(?, ?)').join(', '),
logs.flatMap(log => [log.msg, log.ts])
);
// 1000 条 = 1 次网络往返
-- MySQL 批量插入优化
SET autocommit = 0;
INSERT INTO logs (msg, ts) VALUES
('msg1', NOW()), ('msg2', NOW()), ..., ('msg1000', NOW());
COMMIT;
-- 单事务提交,减少 fsync 次数

四、异步写入(消息队列削峰)

class LogWriter {
private buffer: LogEntry[] = [];
private readonly BATCH_SIZE = 1000;
private readonly FLUSH_INTERVAL = 1000; // ms

async add(entry: LogEntry) {
this.buffer.push(entry);
if (this.buffer.length >= this.BATCH_SIZE) {
await this.flush();
}
}

// 定时刷盘
@Interval(1000)
async flush() {
if (this.buffer.length === 0) return;
const batch = this.buffer.splice(0, this.BATCH_SIZE);
await this.db.batchInsert('logs', batch);
}
}

五、分库分表

当单库写入成为瓶颈时,进行水平分片:

分片策略:user_id % 4

user_id=1 → db_01.users
user_id=2 → db_02.users
user_id=3 → db_03.users
user_id=4 → db_00.users
function getShardDb(userId: number, totalShards: number): string {
const shardIndex = userId % totalShards;
return `db_${String(shardIndex).padStart(2, '0')}`;
}

分库分表带来的问题

问题解决方案
跨分片查询聚合层查询 / ES 全局索引
跨分片事务分布式事务(Seata)/ 最终一致
全局排序各分片排序后归并
全局 ID雪花算法 / 号段模式
扩容一致性哈希 / 成倍扩容

六、写入优化清单

优化项说明效果
减少索引写入时需维护所有索引每减一个索引提升 10~20%
批量提交多条合并为一次 COMMIT减少 fsync,提升 10x+
异步写入消息队列削峰平滑写入峰值
预分配空间InnoDB innodb_autoextend_increment减少文件扩展
SSD固态硬盘替换机械盘IOPS 提升 100x
WAL 合并innodb_flush_log_at_trx_commit=2牺牲少量持久性换吞吐
innodb_flush_log_at_trx_commit
  • = 1(默认):每次事务提交都 fsync,最安全
  • = 2:每秒 fsync,宕机可能丢 1 秒数据
  • = 0:每秒 fsync + 写日志到 OS 缓存
  • 写密集场景可设为 2,但需要评估数据丢失风险

常见面试问题

Q1: 设计一个日志收集系统的数据库方案

答案

架构:应用 → Kafka → Flink 聚合 → ClickHouse

1. 应用异步发送日志到 Kafka(毫秒级延迟可接受)
2. Flink 实时消费,按时间窗口聚合
3. 批量写入 ClickHouse(列式存储,写入吞吐高)
4. 原始日志同时归档到 S3/OSS(冷数据)
5. ClickHouse 按日分区,自动过期清理

不用 MySQL 的原因:
- 日志数据量巨大,MySQL 单表亿级写入困难
- 日志主要做分析查询,列式存储更适合
- 不需要事务保证

Q2: 秒杀场景下如何优化库存扣减的写性能?

答案

1. 预热:将库存加载到 Redis
SET stock:item_001 1000

2. 扣减:Redis Lua 原子扣减
if redis.call('get', key) > 0 then
redis.call('decr', key)
return 1
end
return 0

3. 异步落库:Kafka 消费生成订单 + 更新 MySQL 库存
4. 兜底:MySQL 乐观锁
UPDATE items SET stock = stock - 1
WHERE id = ? AND stock > 0

Q3: 分库分表后如何做全局分页查询?

答案

方案对比:

方案原理适合场景
禁止深分页只允许「上一页/下一页」信息流
全局 ES 索引所有分片数据同步到 ES搜索场景
归并排序各分片查 N 条后归并数据量可控
分页游标基于上一页最后一条的排序值通用

相关链接