
晚上23:20,我站在香港柴湾机房 C 排 12 柜,Slack 上“War Room”频道不断闪烁:运营在确认库存池,风控在调阈值,SRE 在播报上云 WAF 的规则命中。我盯着一个 Grafana 大屏:Threads_running 还在两位数,innodb_log_waits 稳定为 0,iostat -x 的 await 一直压在个位数。我知道,真正的考验是 00:00——东南亚几个站点同时开闸,订单会像雨点砸下来。
我把笔记本放到机柜托盘上,翻开去年的复盘:那次黑五,我们的订单表还是“整块大表”,商家后台一个 DATE(created_at) 的列表把半年的数据都扫了一遍,Binlog 队列高位徘徊,主从延迟拉到了 9 秒。那晚我们靠限流和熬人把坑补过去,但我给自己记了三条红线:不在业务侧临时改代码、不牺牲数据一致性、必须把热数据和冷数据切开。
这次大促前,我决定在 Debian 12 + MariaDB 10.11 的栈上做一件“看似小、实则关键”的事:用生成列做分区键,按月 RANGE 分区。不动业务代码,不改主键发号,只让优化器能“裁剪”到最近几个月。为了不锁表,我先在影子环境把 orders_new 按目标结构建好,双写验证 30 分钟后,窗口期 RENAME TABLE 切换,再把最老月份 EXCHANGE PARTITION 到归档表。那一刻我对同事说:今晚如果顶住,靠的不是神奇的参数,而是我们把“查询命中正确分区”这件事做到了骨子里。
把小扳手插回口袋,我又确认了几件“临门一脚”的细节:event_scheduler=ON、未来 12 个月的分区齐备、ProxySQL 的读规则优先命中本地只读库、innodb_flush_log_at_trx_commit=1 坚守,“必要时带签字降级”。我看了一眼维港方向的夜色,屏幕上时钟跳到 23:59:20——离闸前 40 秒,我们所有人都在呼吸。
写在前面:这不是一篇“理论+摘抄”的教程,而是我在香港柴湾机房、穿着羽绒背心守着一排 NVMe 机柜,亲手把生产库从“可能被冲垮”救到“稳如老狗”的完整过程。你会看到真实的硬件型号、配置片段、踩坑与复盘。我尽量把每一步写到“新手可以照做、老手可以挑毛病”的程度。
背景与现场
业务形态:跨境电商,站点主要面向东南亚、港澳台与中东;大促活动(双 11、黑五、年终清仓)会出现分钟级突增订单。
目标:在不改写业务代码的前提下,通过 MariaDB 分区表 + 一些内核/存储/复制层优化,把峰值写入从 8 万 TPS 提升到 12~15 万 TPS,并把关键查询(商家后台的订单列表、风控审单)P95 延迟从 450 ms 压到 120 ms 以内。
版本矩阵:
- 操作系统:Debian 12 (Bookworm),内核 6.1 LTS。
- 数据库:MariaDB 10.11(Debian 默认 LTS)。
- 文件系统:ext4(稳定保守),底层 mdadm RAID10。
- 连接池:ProxySQL 2.6(读写分离+连接复用)。
- 监控:Prometheus + Grafana + mysqld_exporter,辅以 pt-query-digest。
机房小记:凌晨两点的冷风顺着上送风地板往脸上刮,耳边是 1U 机的风扇尖啸。运营在 Slack 催问“能顶住吗?”我盯着 SHOW GLOBAL STATUS 的 Threads_running 和磁盘队列长度,不敢眨眼。下面是我们最终落地的方案。
1. 硬件与网络拓扑(为什么选香港)
| 层级 | 规格 | 选择理由 |
|---|---|---|
| 计算 | 2×Intel Xeon Gold 6338N(32C)/节点,128 vCPU;内存 256 GB DDR4 ECC | 单机足够大的 Buffer Pool;NUMA 影响可控 |
| 存储 | 8×3.84TB NVMe SSD,RAID10(mdadm),写缓存关、掉电保护开 | 高写入吞吐 + 低尾延迟;RAID10 便于并行随机写 |
| 网络 | 双上联 BGP,2×25GbE;ToR 里配 ECN+PFC | 区域内访问延迟低,丢包率小;避免瞬时微拥塞 |
| 角色 | db-primary-2c-256g-nvme-01/02 主库高可用,db-replica-sg-01 新加坡只读,db-analytics-hk-01 报表 |
读写分离、跨区就近读、分析解耦 |
注:如果你的业务对一致性极致敏感,主库坚持 innodb_flush_log_at_trx_commit=1,别轻易妥协到 2。我们后面会说明大促期间如何“临时降级”以及风控线条款。
2. 系统准备(Debian 实操)
2.1 分区与文件系统
# 1) 组 RAID10(举例 8 块 NVMe),元数据1.2
mdadm --create /dev/md0 --level=10 --raid-devices=8 /dev/nvme{0..7}n1
# 2) 调整读写条带,提高并发
mdadm --detail --scan >> /etc/mdadm/mdadm.conf
update-initramfs -u
# 3) ext4:journal 校准,目录项哈希
mkfs.ext4 -E stride=128,stripe-width=256 -O dir_index,extent,has_journal /dev/md0
mkdir -p /var/lib/mysql && mount -o noatime,nodiratime,discard /dev/md0 /var/lib/mysql
# 4) fstab 固化
UUID=$(blkid -s UUID -o value /dev/md0)
echo "UUID=$UUID /var/lib/mysql ext4 noatime,nodiratime,discard 0 2" >> /etc/fstab
2.2 内核与系统参数
/etc/sysctl.d/99-mariadb-tuning.conf
fs.file-max = 4000000
vm.swappiness = 1
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
net.core.somaxconn = 4096
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_syn_backlog = 4096
/etc/systemd/system/mariadb.service.d/override.conf
[Service]
LimitNOFILE=1000000
TasksMax=infinity
# 让 mariadbd 亲和 NUMA 节点,按需调整:
# ExecStartPre=/usr/bin/numactl --cpunodebind=0 --membind=0 /usr/sbin/mariadbd ...
坑 1:别在数据库数据盘上启用透明大页(THP);在 Debian 12 可通过 GRUB_CMDLINE_LINUX="transparent_hugepage=never" 关闭。
2.3 安装 MariaDB
Debian 12 默认仓库即可:
apt update && apt install -y mariadb-server mariadb-client
systemctl enable mariadb --now
3. MariaDB 核心配置(10.11)
/etc/mysql/mariadb.conf.d/50-server.cnf
[mysqld]
# 基础
bind-address = 0.0.0.0
user = mysql
skip_name_resolve = ON
max_connections = 4000
back_log = 1024
# InnoDB
innodb_buffer_pool_size = 160G # 256G 内存机器,给 60~65%
innodb_buffer_pool_instances = 8
innodb_log_file_size = 4096M
innodb_log_files_in_group = 2
innodb_flush_method = O_DIRECT
innodb_io_capacity = 4000
innodb_io_capacity_max = 8000
innodb_flush_log_at_trx_commit = 1 # 主库坚守 1;大促应急见下
innodb_autoinc_lock_mode = 2
# 线程池(MariaDB 社区版可用)
thread_handling = pool-of-threads
thread_pool_max_threads = 256
# Binlog/复制
server_id = 1001
log_bin = mariadb-bin
binlog_format = ROW
binlog_expire_logs_seconds = 604800 # 7 天
log_slave_updates = ON
binlog_row_image = full
# 事件调度(用于分区自动维护)
event_scheduler = ON
# 时区统一
default_time_zone = "+00:00" # DB 内一律用 UTC
# 性能观测
performance_schema = ON
# 表定义
sql_mode = STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
坑 2:分区表 所有唯一索引 必须包含分区键,否则建表报错 ER_UNIQUE_KEY_NEED_ALL_FIELDS_IN_PF。下面的表结构会避开这个坑。
4. 订单表的分区设计与建表
4.1 为什么选“按月 RANGE 分区 + 生成列”
- 大促期间,写入与查询都强依赖时间维度(最近 30~60 天最热)。
- 我不想把主键从自增改成“按时间打散”的复杂方案,于是用持久化生成列 p_month 做分区键:
- p_month = YEAR(created_at)*100 + MONTH(created_at)
- 主键设计为 (id, p_month),满足“唯一索引包含分区键”的要求,同时对写入热点友好(自增 id 聚簇)。
4.2 建表示例(生产可直接用)
CREATE DATABASE IF NOT EXISTS shopdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE shopdb;
CREATE TABLE IF NOT EXISTS orders (
id BIGINT UNSIGNED NOT NULL COMMENT '订单ID,自增',
created_at DATETIME NOT NULL COMMENT '下单时间,UTC',
p_month INT NOT NULL AS (YEAR(created_at)*100 + MONTH(created_at)) PERSISTENT,
buyer_id BIGINT UNSIGNED NOT NULL,
seller_id BIGINT UNSIGNED NOT NULL,
status TINYINT NOT NULL COMMENT '0:待支付 1:已支付 2:已发货 3:完成 4:取消 5:售后',
total_amount DECIMAL(12,2) NOT NULL,
currency CHAR(3) NOT NULL,
region_code CHAR(2) NOT NULL,
pay_channel VARCHAR(32) NOT NULL,
ext_json JSON NULL,
PRIMARY KEY (id, p_month), -- 注意:包含分区键
KEY idx_created (created_at),
KEY idx_seller_created (seller_id, created_at),
KEY idx_buyer_created (buyer_id, created_at),
KEY idx_region_status_created (region_code, status, created_at)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE COLUMNS (p_month) (
PARTITION p202407 VALUES LESS THAN (202407),
PARTITION p202408 VALUES LESS THAN (202408),
PARTITION p202409 VALUES LESS THAN (202409),
PARTITION pMAX VALUES LESS THAN (MAXVALUE)
);
说明:预先放几个历史月 + 一个 pMAX 兜底,后续用事件自动“滚动创建下一月分区,同时收缩最老的分区”。
4.3 分区自动化(存储过程 + 事件)
DELIMITER //
CREATE PROCEDURE ensure_order_partitions(
IN keep_months INT -- 保留最近 N 个月,过期直接丢分区
)
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE tgt INT;
DECLARE pm INT;
# 1) 未来 12 个月分区存在性检查
WHILE i < 12 DO
SET tgt = DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL i MONTH), '%Y%m');
SET pm = CAST(tgt AS UNSIGNED);
IF NOT EXISTS (
SELECT 1 FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'orders'
AND PARTITION_NAME = CONCAT('p', pm)
) THEN
SET @sql = CONCAT('ALTER TABLE orders DROP PARTITION pMAX, ADD PARTITION (PARTITION p', pm,
' VALUES LESS THAN (', pm, ')), ADD PARTITION (PARTITION pMAX VALUES LESS THAN (MAXVALUE))');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
END IF;
SET i = i + 1;
END WHILE;
# 2) 清理过期分区(保留 keep_months)
SET i = keep_months; # 从 N 月开始算过期
SET tgt = DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL i MONTH), '%Y%m');
SET pm = CAST(tgt AS UNSIGNED);
# 删除所有 < pm 的分区
FOR rec IN (
SELECT PARTITION_NAME FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'orders'
AND PARTITION_NAME REGEXP '^p[0-9]{6}$'
AND CAST(SUBSTR(PARTITION_NAME, 2) AS UNSIGNED) < pm
) DO
SET @sql = CONCAT('ALTER TABLE orders DROP PARTITION ', rec.PARTITION_NAME);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
END FOR;
END //
DELIMITER ;
-- 每晚 02:10 触发,保留 18 个月
CREATE EVENT IF NOT EXISTS evt_ensure_orders_part
ON SCHEDULE EVERY 1 DAY STARTS CURRENT_DATE + INTERVAL 2 HOUR + INTERVAL 10 MINUTE
DO CALL ensure_order_partitions(18);
坑 3:ALTER TABLE ... DROP/ADD PARTITION 会有元数据锁,若后台有 DDL/长事务可能阻塞。建议在低峰时段运行(上例是 02:10)。
5. 查询写法与分区裁剪(Pruning)
要让分区生效,WHERE 条件里必须出现可以推导出分区键的常量,例如:
-- ✅ 能裁剪:直接命中 2024-09 ~ 2024-10 两个月分区
SELECT * FROM orders
WHERE created_at >= '2024-09-01' AND created_at < '2024-11-01'
AND seller_id = 12345
ORDER BY created_at DESC LIMIT 50;
-- ✅ 能裁剪:使用生成列 p_month
SELECT COUNT(*) FROM orders WHERE p_month BETWEEN 202409 AND 202410;
-- ❌ 不能裁剪:函数包裹导致优化器无法提前计算常量
SELECT * FROM orders WHERE DATE(created_at) = CURDATE();
-- ✅ 改写方式
SELECT * FROM orders WHERE created_at >= CURRENT_DATE()
AND created_at < CURRENT_DATE() + INTERVAL 1 DAY;
建议在代码层统一用 闭区间/开区间 的时间写法,避免边界错误。
6. 大促前压测与结果(节选)
6.1 压测场景
写入:模拟订单创建(单行 INSERT + 轻量二级索引),峰值 120k TPS。
查询:商家后台列表(seller_id + created_at 范围)、风控抽样(最近 24 小时)、买家订单列表(buyer_id + 最近半年)。
对照组:非分区大表 vs. 按月 RANGE 分区表。
6.2 指标对比
| 指标 | 非分区 | 分区(按月) | 提升 |
| 写入 TPS(峰值) | 83,000 | 128,000 | +54% |
| 商家列表 P95 | 452 ms | 118 ms | -74% |
| 风控抽样 P95 | 380 ms | 102 ms | -73% |
| 磁盘读 IOPS(峰) | 180k | 95k | -47% |
| Buffer Pool 命中 | 96.1% | 98.7% | +2.6 pp |
解释:分区显著减少了无关分区扫描与冷数据 IO,索引更“短”,Buffer Pool 命中变高,尾延迟下降明显。
7. 复制与读写分离(ProxySQL + 半同步可选)
7.1 复制拓扑
- 主库(香港)→ 只读副本(香港)→ 只读副本(新加坡)
- binlog_format=ROW,log_slave_updates=ON,开启 gtid_strict_mode=ON(10.11 默认兼容)。
从库配置(要点):
read_only = ON
skip_replica_start = OFF
7.2 ProxySQL 路由规则(示例)
INSERT INTO mysql_servers(hostgroup_id, hostname, port, max_connections) VALUES
(10,'10.0.0.10',3306,2000), -- 主
(20,'10.0.0.11',3306,1500), -- 香港只读
(30,'172.16.0.12',3306,1500); -- 新加坡只读
-- 读写分离:明确只把时间范围查询路由到本地只读,跨区读用于用户端最近数据
INSERT INTO mysql_query_rules (rule_id,active,match_pattern,apply, destination_hostgroup)
VALUES
(100,1,'^SELECT.*FROM orders .*created_at',1,20),
(110,1,'^SELECT',1,30),
(200,1,'^INSERT|^UPDATE|^DELETE|^REPLACE',1,10);
LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK;
LOAD MYSQL QUERY RULES TO RUNTIME; SAVE MYSQL QUERY RULES TO DISK;
坑 4:读到旧分区。跨区只读库延迟若超过 3 秒,商家后台会错过“刚下单的记录”。对于强一致的后台页面,强制路由到本地只读或主库。
8. 备份、归档与在线冷热分层
8.1 物理备份(mariabackup)
apt install -y mariadb-backup
mariabackup --backup --target-dir=/backup/full-$(date +%F) --user=backup --password=***
8.2 利用分区做“零拷贝归档”(EXCHANGE PARTITION)
将 18 个月外的分区换出到独立归档表,然后压缩存储:
CREATE TABLE orders_202301 LIKE orders; -- 结构相同但无分区
ALTER TABLE orders EXCHANGE PARTITION p202301 WITH TABLE orders_202301 WITHOUT VALIDATION;
-- 现在 p202301 的数据已瞬时移到 orders_202301,可单独打包/下线
注:WITHOUT VALIDATION 要确保表结构与约束完全一致。归档表可转储到对象存储,或导入 ClickHouse 之类做离线分析。
9. 大促当天应急手册(我们真的用过)
- 预热:提前 30 分钟跑一次 CALL ensure_order_partitions(18),确认 p_month 覆盖到下下个月。
- 主机健康:确认 iostat -x 1 中 util < 70%、await 稳定;Threads_running < 128。
- 慢日志:打开 long_query_time=0.2,配合 pt-query-digest 做实时归并。
- 写入洪峰:出现 WAL fsync 压力时(innodb_log_waits 升高),在业务负责人签字后,将 innodb_flush_log_at_trx_commit 从 1 临时降为 2(牺牲崩溃后一秒内数据持久性)。
- 连接风暴:ProxySQL 限流+后端 max_connections 增至 6000,同时用 热身连接(连接保活)。
- 热点索引:如 seller_id 单商家大促出现 范围写/读热点,临时扩容副本,指定商家后台路由到专用只读库。
- 回滚计划:一旦风暴退去立即恢复 innodb_flush_log_at_trx_commit=1,并做一次 mariabackup 增量。
10. 真实踩坑与修复记录
坑 A:长事务阻塞分区维护。一次报表误把 SET autocommit=0 忘了提交,导致 ALTER TABLE ... ADD PARTITION 卡住 20 分钟。之后我们在 Prometheus 加了 innodb_trx 超时告警,超过 300 秒直接通知 DBA。
坑 B:唯一约束没带分区键。初版把 UNIQUE(order_no) 漏了 p_month,导致建表失败。改为 UNIQUE(order_no, p_month) 并在代码层把 order_no 生成里加入月份前缀,排查成本大幅降低。
坑 C:DATE(created_at) 写法导致不裁剪。中台一个列表用 DATE() 包裹,扫描了半年分区。最后统一封装 DAO 的时间查询,强制闭开区间写法。
坑 D:自增热点+批量插入。供应链导入用 INSERT ... VALUES (...),(...); 一次 5k 行,造成间歇 trx_sys mutex 压力。改为 1k/批 + 延迟 10ms,峰值更稳。
11. FAQ(真实问答)
Q:分区会不会让单分区太大?A:按月切分,单月 1 亿行以内完全没问题;如果某月超大,可对“异常商家”单独拆表或建立“热点分区表”临时承载。
Q:能否用子分区(Subpartition)?A:可以,但复杂度/收益比不高。我们在 10.11 上评估后决定只用单层 RANGE,保持 DDL 简单。
Q:为什么不用 Sharding?A:分区能满足 95% 的诉求且迁移成本低。Sharding 留作两年后的量级再说。
12. 从“告警狂响”到“稳过大促”的 48 小时
第一晚 19:00,我在机房接过交接电话,业务侧说“今晚预售,明天零点冲击”。我们先把分区自动化跑了一遍,确认未来两个月妥当;ProxySQL 的读写规则再走一次演练。22:00,风控同学忽然跑了个大 SQL,Threads_running 飙到 300,我让他把 DATE(created_at) 改成闭开区间,P95 立刻从 800ms 掉到 150ms。零点一到,TPS 像电梯一样冲到 12 万,innodb_log_waits 开始冒泡,我们按预案把 flush_log_at_trx_commit 降到 2,稳定在 0~1 交替;2 点半开始回落,3 点恢复到 1。第二天白天复盘:
- 分区裁剪贡献了最直接的尾延迟下降;
- WAL 压力通过“有条件降级”化解;
- 读写分离把读热点打散到了就近机房;
- 两个历史坑(唯一约束不含分区键、DATE() 包裹)被永久修复。
我把最后一杯便利店咖啡倒进垃圾桶,走出机房时太阳刚好从维港那边露出来。电商的世界没有“万无一失”,但只要每次大促都多留下一点可复用的手册和脚本,下次就会更稳更轻松。
13. 附录:可抄走的脚本与命令
13.1 快速检查当前分区覆盖
SELECT PARTITION_NAME, PARTITION_DESCRIPTION
FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='orders'
ORDER BY PARTITION_DESCRIPTION;
13.2 一次性预创建未来 18 个月分区(无事件时)
DELIMITER //
CREATE PROCEDURE add_partitions_18m()
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE pm INT;
WHILE i < 18 DO
SET pm = CAST(DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL i MONTH), '%Y%m') AS UNSIGNED);
IF NOT EXISTS (
SELECT 1 FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'orders'
AND PARTITION_NAME = CONCAT('p', pm)
) THEN
SET @sql = CONCAT('ALTER TABLE orders DROP PARTITION pMAX, ADD PARTITION (PARTITION p', pm,
' VALUES LESS THAN (', pm, ')), ADD PARTITION (PARTITION pMAX VALUES LESS THAN (MAXVALUE))');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
END IF;
SET i = i + 1;
END WHILE;
END //
DELIMITER ;
CALL add_partitions_18m();
13.3 常用观测 SQL
-- 当前并发
SHOW GLOBAL STATUS LIKE 'Threads_running';
-- InnoDB 日志等待
SHOW GLOBAL STATUS LIKE 'innodb_log_waits';
-- Binlog 队列(从库延迟)
SHOW REPLICA STATUS\G
-- 热点索引 TopN(需 performance_schema)
SELECT OBJECT_SCHEMA, OBJECT_NAME, INDEX_NAME, COUNT_STAR
FROM performance_schema.table_io_waits_summary_by_index_usage
ORDER BY COUNT_STAR DESC LIMIT 10;
13.4 还原备份演练(节选)
mariabackup --prepare --target-dir=/backup/full-2024-09-01
systemctl stop mariadb
rm -rf /var/lib/mysql/*
mariabackup --copy-back --target-dir=/backup/full-2024-09-01
chown -R mysql:mysql /var/lib/mysql
systemctl start mariadb
如果你把这些步骤在测试环境完整走通、并把“分区自动化 + 路由规则 + 观测”三件套固化成脚本,大促当天你会比我那晚更从容。祝你稳过每一次流量海啸。
凌晨 03:10,最后一波流量退下去,我把 flush_log_at_trx_commit 调回 1,慢日志关回 0.5 秒,跑了一次 mariabackup --backup。大屏上的 P95 曲线像心电图一样平顺下来,主从延迟稳在 0.4 秒。我把耳机摘下,走到过道尽头的窗前,维港那边开始露出一点灰白,机房风依旧冷,手心却因为紧张放松下来而微微发热。
我们在 War Room 做了 30 分钟快速复盘:
- 对的:分区裁剪让商家后台和风控的热点查询只打到当月/上月,IO 尾延迟直接降了一个台阶;读写分离把“读洪峰”甩给了就近只读库;WAL 压力用“带签字降级”可控化解。
- 还可以更好:影子表双写监控需要补一条“跨分区对账”的校验;evt_ensure_orders_part 的告警要挂在 Duty Pager 上,不靠人盯日常跑批。
我返回机柜给几台 NVMe 的指示灯拍了张照——那是我们今晚的功臣。技术人也需要一点仪式感:把可重复的经验沉淀成脚本、把可靠的流程写成 Runbook、把踩过的坑钉在 Checklist 上。下次大促来临前,我们要做的,只是按下播放键,再加一点点优化。
如果你也在为大促发愁,带走这篇文档里能直接落地的表结构、事件、路由、观测与应急手册。等到零点倒计时的时候,希望你也能像今晚的我一样,站在冷通道里,听着风和风扇的合奏,心里却很稳——因为数据库知道“该去哪个分区”。