6.分布式数据库与分库分表
目录
一、分库分表核心概念
• 核心目标:突破单库性能瓶颈,应对海量数据与高并发 • 垂直拆分:按业务模块拆分(用户库、订单库、商品库) • 水平拆分:单表数据分片(用户ID取模、时间范围分片)
二、分片策略与避坑指南
• 分片键选择:高基数字段(用户ID)、业务关联性、数据均衡性 • 分片算法:哈希取模(均匀分布)、一致性哈希(扩容友好)、范围分片(冷热分离) • 避坑要点:禁止无分片键查询、避免后期改分片键、分片数预留扩容空间
三、ShardingSphere 企业级实战
• 技术选型:Sharding-JDBC(轻量级) vs Sharding-Proxy(多语言支持) • Spring Boot整合:分片规则配置、读写分离、分布式主键(Snowflake) • 高阶功能:数据脱敏、柔性事务(BASE)、多租户隔离
四、分布式事务解决方案
• 刚性事务:Seata AT模式(全局锁+反向SQL回滚) • 柔性事务:TCC(Try-Confirm-Cancel)、本地消息表(最终一致) • 大厂实践:支付宝异步通知补偿、美团分布式事务中间件
五、数据迁移与动态扩容
• 全量迁移:DataX工具、停机窗口控制 • 增量同步:Canal监听binlog、双写过渡校验 • 在线扩容:虚拟节点动态迁移、用户无感知切换
六、生产监控与调优
• 核心指标:连接池水位、慢SQL率、分片路由耗时 • 调优手段:避免跨分片查询、异步化聚合统计、热点数据二级分片 • 工具链:Prometheus监控、SkyWalking链路追踪
七、大厂真实案例
• 电商订单库:用户ID取模分片 + 冷热数据归档HBase • 社交Feed流:用户ID+时间联合分片 + 读写分离 • 物流轨迹库:地理位置GeoHash分片 + Elasticsearch检索
八、高频面试题精选
• 分库分表后如何高效分页? • ShardingSphere如何解析SQL路由? • 分片键数据倾斜的应急方案? • 如何设计全局唯一ID(雪花算法 vs 号段模式)?
一、分库分表核心概念
核心目标
• 突破单库性能瓶颈: • 连接数限制:单库连接池上限(如MySQL默认151连接),高并发下易阻塞。 • 磁盘IO瓶颈:单机硬盘读写速率如SATA SSD 约500MB/s)。 • 锁竞争严重:高频更新场景(如秒杀库存)导致行锁/表锁冲突。
• 应对海量数据与高并发: • 数据量爆炸:单表数据超千万级后,B+树层级增加,查询性能指数级下降。 • 业务解耦需求:微服务架构下,不同业务模块需独立扩缩容(如用户服务与订单服务)。
垂直拆分
• 定义:按业务功能模块拆分数据库,每个库独立部署。 • 典型拆分方案: ◦ 用户库:存储用户信息、登录凭证。 ◦ 订单库:订单记录、支付流水。 ◦ 商品库:商品详情、库存信息。
• 优势: • 资源隔离:CPU、内存、磁盘资源按业务独立分配,避免互相影响。 • 专业优化:针对业务特性定制索引和存储引擎(如订单库用InnoDB,日志库用TokuDB)。
• 痛点与解决方案: • 跨库事务:避免跨库事务,改用最终一致性(如本地消息表)。 • JOIN操作难:通过业务层多次查询或数据冗余(如订单表冗余用户昵称)。
水平拆分
• 定义:将单表数据按分片规则(如用户ID、时间)拆分到多个库/表。 • 典型分片方案: ◦ 用户ID取模:分片号 = user_id % 分片总数(均匀分布,用户查询精准路由)。 ◦ 时间范围分片:按月/年拆分(如order_202301, order_202302),天然支持冷热数据分离。
• 分片键选择原则: • 高基数:分片键值足够分散(如用户ID而非性别)。 • 业务相关性:高频查询条件字段(如订单查询常用user_id和create_time)。
• 挑战与应对: • 跨分片查询复杂:避免无分片键查询,改用ES聚合或业务层分批查询。 • 扩容成本高:分片数预分配为2的N次方(如16→32),用一致性哈希减少数据迁移量。
常见问题(QA)
Q1:什么时候该用垂直拆分,什么时候用水平拆分? • 垂直拆分优先:业务模块清晰、数据增长可控(如初期快速验证阶段)。 • 水平拆分必选:单表数据超千万且持续增长(如电商订单、社交动态)。
Q2:分片键选错如何补救? • 数据迁移:新建分片键正确的表,通过DataX/Spark迁移数据,逐步切换读写流量。 • 双写过渡:新旧分片键同时写入,直到旧数据淘汰。
Q3:如何保证跨库事务一致性? • 强一致场景:用Seata AT模式(性能要求不高时)。 • 最终一致场景:消息队列异步补偿(如订单创建后发MQ通知库存服务)。
大厂案例
-
支付宝用户库垂直拆分: • 拆分用户基础信息库、账户余额库、交易记录库。 • 结果:数据库负载下降60%,故障隔离能力提升。
-
淘宝订单表水平分片: • 按
user_id % 1024分成1024张表,单表数据控制在500万以内。 • 结果:订单查询响应时间从2s降至200ms。
二、分片策略与避坑指南
分片键选择
1. 高基数字段优先
• 定义:分片键的取值可能性足够多(如用户ID、订单号)。 • 案例: • 错误示范:性别字段(基数2)作为分片键 → 数据集中在2个分片,无法扩展。 • 正确方案:用户ID(百万级基数)哈希分片 → 数据均匀分布。
2. 业务强关联性
• 原则:分片键必须是业务高频查询条件(避免全库扫描)。 • 场景: • 订单查询条件:user_id(用户查订单)和 create_time(运营统计)。 • 联合分片键:user_id + create_time(兼顾查询与分布)。
3. 数据均衡性保障
• 算法优化:
// 用户ID哈希后取模(简单均匀分片) int shardNo = Math.abs(userId.hashCode()) % shardCount; // 增加随机因子防止热点 int shardNo = (userId.hashCode() + ThreadLocalRandom.current().nextInt(100)) % shardCount;
• 监控手段:通过ShardingSphere的 SHOW SHARDING TABLE RULES 检查各分片数据量。
分片算法
1. 哈希取模
• 原理:分片号 = hash(key) % 分片总数 • 优势:数据均匀分布,查询直接定位分片。 • 缺陷: • 扩容困难:分片数变化需全量数据迁移。 • 范围查询弱:如时间范围查询需遍历所有分片。 • 适用场景:用户表、订单表等无范围查询需求的数据。
2. 一致性哈希
• 原理:构建哈希环,节点虚拟化分散在环上,数据哈希值顺时针找到最近节点。 • 优势:扩容时仅迁移相邻节点数据,影响小。 • 生产配置(Java示例):
// 使用TreeMap实现一致性哈希环
TreeMap<Integer, String> hashRing = new TreeMap<>();
for (int i = 0; i < virtualNodes; i++) {
for (String node : nodes) {
hashRing.put(MurmurHash.hash(node + "#" + i), node);
}
}
• 适用场景:需要频繁扩容的社交动态、评论系统。
3. 范围分片
• 原理:按区间划分(如时间、ID范围)。 • 优势: • 冷热分离:历史数据归档低成本存储(如OSS)。 • 分页友好:按时间排序查询天然有序。 • 缺陷:易产生数据倾斜(如某时间段订单暴涨)。 • 案例:物流轨迹表按 YYYYMM 分片,每月自动创建新表。
避坑要点
1. 禁止无分片键查询
• 风险:全库全表扫描 → 性能雪崩。 • 解决方案: • 代码强制校验:DAO层拦截无分片键查询请求。 • 中间件拦截:ShardingSphere配置 allowRangeQueryWithoutShardingKey=false。 • 大厂实践:抖音订单系统要求所有查询必须携带 user_id 或 order_id。
2. 避免后期修改分片键
• 风险:数据迁移成本高,需停服或灰度切换。 • 应对策略: • 预分片设计:初期采用联合分片键(如 user_id + 预留字段 - 双写过渡:新旧分片键同步写入,逐步迁移。 • 案例:美团外卖订单表从 order_id 分片改为 rider_id + order_id 分片,耗时3个月。
3. 分片数预留扩容空间
• 经验公式:预估3年数据量,分片数按2的N次方设计(如16 → 32)。 • 弹性方案: • 虚拟分片:物理分片数少于逻辑分片数,动态调整映射关系。 • 自动迁移:阿里云DRDS支持在线分片数倍增,数据自动均衡。 • 监控指标:单分片数据量超过500万时触发报警。
高频面试题
-
分片键选择不合理导致数据倾斜怎么办? • 答案:临时方案:写入时加随机后缀;长期方案:改用一致性哈希重新分片。
-
如何实现跨分片分页查询? • 答案:业务层排序(如ES聚合结果)或折衷方案(禁止深度分页)。
-
ShardingSphere分片算法如何自定义? • 答案:实现
StandardShardingAlgorithm接口,注入分片逻辑。
生产级代码片段:
// ShardingSphere 分片规则配置(按user_id哈希分片)
shardingRuleConfig.getTableRuleConfigs().add(
new TableRuleConfiguration("user", "ds${0..1_${0..15}")
.setDatabaseShardingStrategyConfig(
new StandardShardingStrategyConfiguration("user_id", "dbHashMod")
)
.setTableShardingStrategyConfig(
new StandardShardingStrategyConfiguration("user_id", "tableHashMod")
)
);
// 自定义分片算法(哈希取模)
public final class HashModShardingAlgorithm implements StandardShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) {
// 实现分片逻辑
}
}
三、ShardingSphere 企业级实战
技术选型
• Sharding-JDBC(轻量级): • 定位:Java应用的JDBC驱动层扩展,透明化分库分表 • 优势:无代理层性能损耗,与Spring Boot深度整合 • 适用场景:中小团队快速落地分库分表(如电商订单分片) • Sharding-Proxy(支持): • 定位:独立部署的数据库代理,兼容MySQL/PostgreSQL协议 • 优势:支持多语言(PHP/Python可视化(如阿里云DMS) • 适用场景:跨技术栈团队(如Java+Go混合开发) • 选型建议: • 单语言技术栈优先Sharding-JDBC(性能最优) • 需运维管控或混合语言选Sharding-Proxy(牺牲10%~15%性能)
Spring Boot整合
1. 分片规则配置(YAML示例)
spring:
shardingsphere:
datasource:
names: ds0, ds1
ds0:
url: jdbc:mysql://db0:3306/order
username: root
password: 123456
ds1:
url: jdbc:mysql://db1:3306/order
rules:
sharding:
tables:
order:
actualDataNodes: ds$->{0..1}.order_$->{0..15} # 2库x16表
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: db-hash-mod
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: table-hash-mod
shardingAlgorithms:
db-hash-mod:
type: HASH_MOD
props:
sharding-count: 2
table-hash-mod:
type: HASH_MOD
props:
sharding-count: 16
2. 读写分离配置
spring:
shardingsphere:
rules:
replica-query:
dataSources:
pr_ds:
primaryDataSourceName: ds-primary
replicaDataSourceNames: ds-replica1, ds-replica2
loadBalancerName: round-robin
loadBalancers:
round-robin:
type: ROUND_ROBIN
3. 分布式主键生成
// flake算法(防止时钟回拨)
@Bean
public KeyGenerateAlgorithm keyGenerator() {
return new SnowflakeKeyGenerateAlgorithm()
.setProps(Collections.singletonMap("max-tolerate-time-difference-milliseconds", "60000"));
}
高阶功能
1. 数据
• 场景:手机号、身份证号等敏感信息加密存储 • 实现:
spring:
shardingsphere:
rules:
encrypt:
encryptors:
mobile_encryptor:
type: AES
props:
aes-key-value: 123456
tables:
user:
columns:
phone:
cipherColumn: phone_cipher
encryptorName: mobile_encryptor
• 查询处理:自动加解密,业务代码无感知
2. 柔性事务(BASE)
• 本地消息表实现:
-
业务事务提交时,写入本地消息表
-
定时任务扫描并发送消息到MQ
-
消费者处理成功后更新消息状态 • ShardingSphere集成:
spring:
shardingsphere:
rules:
transaction:
defaultType: BASE
providerType: Local
3. 多租户隔离
• 场景:SaaS系统按租户分库(如企业ID分片) • 配置:
tables:
report:
actualDataNodes: ds_${0..9}.report_${0..9}
databaseStrategy:
standard:
shardingColumn: tenant_id
shardingAlgorithmName: tenant-mod
shardingAlgorithms:
tenant-mod:
type: MOD
props:
sharding-count: 10
• 数据隔离:通过HintManager强制路由租户上下文
HintManager.getInstance().setDatabaseShardingValue(tenantId);
生产经验
-
分片算法预热: • 启动时预加载分片路由规则,避免首次查询延迟
-
监控告警: • 通过
ShardingSphere-UI监控慢查询与分片负载 -
灰度发布: • 新旧分片规则并存,通过AB测试逐步切流
四、分布式事务解决方案
刚性事务:Seata AT模式
核心原理
• 全局锁机制: • 事务协调器(TC)在事务开始时注册全局锁,锁定涉及的行记录。 • 其他事务修改同一数据时,需等待锁释放(默认锁超时时间30秒)。 • 反向SQL回滚: • 提交阶段:各分支事务提交本地事务,释放全局锁。 • 回滚阶段:生成反向SQL(如INSERT→DELETE)撤销已提交的操作。
Spring Boot整合配置
# application.yml
seata:
enabled: true
application-id: order-service
tx-service-group: my-tx-group
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
@GlobalTransactional // 开启全局事务
public void placeOrder() {
orderService.create();
stockService.deduct();
}
适用场景与限制
• 适用场景:短事务(执行时间<1秒)、简单业务逻辑(如扣减库存+生成订单)。 • 限制: • 不支持嵌套事务。 • 高并发场景下全局锁可能成为性能瓶颈。
柔性事务:TCC与本地消息表
1. TCC(Try-Confirm-Cancel)
• 三阶段流程:
| 阶段 | 动作 | 案例(转账业务) |
|---|---|---|
| Try | 资源预留(冻结账户金额) | account.freeze(100元) |
| Confirm | 确认操作(实际扣款) | account.debit(100元) |
| Cancel | 取消预留(解冻金额) | account.unfreeze(100元) |
• Java实现示例:
@Transactional
public boolean tryTransfer(String from, String to, BigDecimal amount) {
// 冻结转出账户资金
accountService.freeze(from, amount);
// 预增转入账户可用额度
accountService.prepareCredit(to, amount);
return true;
}
@Transactional
public boolean confirmTransfer(String txId) {
// 实际扣减转出账户
accountService.debit(txId);
// 实际增加转入账户
accountService.credit(txId);
return true;
}
@Transactional
public boolean cancelTransfer(String txId) {
// 解冻转出账户资金
accountService.unfreeze(txId);
// 撤销转入账户预增
accountService.cancelCredit(txId);
return true;
}
2. 本地消息表(最终一致性)
• 实现流程:
-
业务事务提交时,向本地消息表插入事件记录(与业务操作同一事务)。
-
定时任务扫描未处理事件,发送到消息队列(如RocketMQ)。
-
消费者处理成功后更新事件状态。
• Spring Boot集成:
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
// 写入本地消息表(同一事务)
eventRepository.save(new Event("order_created", order.getId()));
}
@Scheduled(fixedDelay = 5000)
public void processEvents() {
List<Event> events = eventRepository.findByStatus(EventStatus.PENDING);
events.forEach(event -> {
try {
rocketMQTemplate.send("order_topic", event.getPayload());
event.setStatus(EventStatus.SUCCESS);
} catch (Exception e) {
event.setStatus(EventStatus.FAILED }
eventRepository.save(event);
});
}
大厂实践
1. 支付宝异步通知补偿
• 场景:支付成功后通知商户系统,确保最终到达。 • 实现: • 支付成功时写入本地消息表。 • 异步重试通知(1s、10s、1m、10m、1h间隔),最多重试24小时。 • 商户系统幂等处理(通过支付流水号去重)。
2. 美团分布式事务中间件
• 架构设计: • 事务协调器:基于Raft协议实现高可用。 • TCC适配层:自动生成Try/Confirm/Cancel接口模板。 • 监控看板:实时追踪事务状态,支持手动冲正。 • 核心指标: • 事务成功率:99.995%(依赖自动补偿机制)。 • 平均处理耗时:Confirm阶段<50ms,Cancel阶段<100ms。
选型决策树
| 场景特征 | 推荐方案 | 理由 |
|---|---|---|
| 短事务、强一致性需求 | Seata AT模式 | 简单易用,无需业务改造 |
| 长事务、高并发(如金融转账) | TCC | 细粒度控制,避免资源长期锁定 |
| 允许延迟(如通知类业务) | 本地消息表 | 吞吐量高,对业务侵入性低 |
| 跨多语言服务(如Java+Go) | 消息队列+本地事务 | 无中心化依赖,兼容异构系统 |
五、数据迁移与动态扩容
全量迁移
1. DataX工具实战
• 核心能力: • 支持MySQL、Oracle、HDFS等20+数据源异构迁移 • 分布式架构(Job+Task)提升吞吐量(单机可达500MB/s) • 断点续传、脏数据跳过机制保障稳定性 • 迁移流程:
-
数据探查:统计表大小、主键分布(避免大事务超时)
-
作业配置:
{
"job": {
"content": [{
"reader": {
"name": "mysqlreader",
"parameter": {
"username": "root",
"password": "123456",
"column": ["id", "user_id", "amount"],
"splitPk": "id", // 按主键分片读取
"connection": [{
"table": ["orders"],
"jdbcUrl": ["jdbc:mysql://old-db:3306/order"]
}]
}
},
"writer": {
"name": "mysqlwriter",
"parameter": {
"username": "root",
"password": "123456",
"column": ["id", "user_id", "amount"],
"connection": [{
"jdbcUrl": "jdbc:mysql://new-db:3306/order",
"table": ["orders"]
}]
}
}
}]
}
}
-
执行与监控: ◦ 日志实时查看: -f datax.log
◦ 进度监控:curl http://datax-server:port/job/metrics`
2. 停机窗口控制
• 步骤:
-
停写:关闭业务写入入口(如Nginx流量拦截)
-
增量追赶:通过Binlog同步最后N分钟数据
-
切换验证:对比新旧库数据checksum(
mysqldbcompare工具) -
恢复写入:开启新库旧库下线 • 时间估算:
• 数据量100GB,网络带宽1Gbps → 全量迁移约15分钟 • 增量追赶(Binlog延迟) → 5~10分钟 • 总停机时间 ≈ 30分钟
增量同步
1. Canal监听Binlog
• 架构原理: • Canal Server伪装为MySQL从库,接收主库Binlog • MQ(Kafka/RocketMQ)解耦生产与消费速率 • Java客户端消费消息,写入目标库(如ES、分片后的MySQL) • Spring Boot整合:
canal: server: 192.168.1.100:11111 destination: example filter: .*\\..*
@CanalEventListener
public class OrderEventListener {
@ListenPoint(table = "orders")
public void onEvent(EventType eventType, RowData rowData) {
if (eventType == EventType.INSERT) {
Order order = convertRowToOrder(rowData);
orderRepository.save(order); // 写入新库
}
}
}
2. 双写过渡校验
• 双写策略: • 同步双写:事务内同时写入新旧库(强一致,性能低) • 异步双写:写入旧库后发MQ异步写入新库(最终一致,高吞吐) • 数据校验:
-- 新旧库数据比对(定时任务) SELECT COUNT(*) FROM old_db.orders UNION ALL SELECT COUNT(*) FROM new_db.orders; -- 差异数据修复 INSERT INTO new_db.orders SELECT * FROM old_db.orders WHERE id NOT IN (SELECT id FROM new_db.orders);
在线扩容
1. 虚拟节点动态迁移
• 一致性哈希优化: • 物理节点映射多个虚拟节点(如每个物理节点1000虚拟节点) • 扩容时新增虚拟节点,数据迁移仅影响相邻节点 • 迁移流程:
-
新节点加入:向集群注册,分配虚拟节点范围
-
数据迁移: ◦ 扫描旧节点数据,按新路由规则迁移至新节点 ◦ 迁移期间旧节点仍可读写(双写模式)
-
流量切换:更新路由配置,逐步切流至新节点
2. 用户无感知切换
• 灰度发布: • 按用户ID分流:10%流量切至新节点,观察错误率 • 按地域切流:先切非核心地区(如海外用户) • 回滚方案: • 监控新节点QPS/延迟,超阈值自动回退旧配置 • 数据双写期间保留旧节点数据,支持快速回滚
生产级Checklist
-
数据一致性验证: • 全量校验:
mysqldump+md5sum• 增量校验:对比新旧库Binlog位点 -
性能压测: • 模拟双写压力(如JMeter模拟200%流量) • 监控连接池等待、锁竞争指标
-
容灾演练: • 随机Kill节点,验证数据自愈能力 • 网络分区模拟(如iptables阻断节点通信)
六、生产监控与调优
核心指标
1. 连接池水位监控
• 关键指标: • 活跃连接数(active):实时处理请求的连接数 • 空闲连接数(idle):等待复用的空闲连接 • 最大等待时间(maxWait):获取连接的超时阈值(超过则抛异常) • 报警规则(Prometheus示例):
# prometheus-rules.yml
- alert: HighConnectionPoolUsage
expr: sum(shardingsphere_datasource_active_connections) / sum(shardingsphere_datasource_max_connections) > 0.8
for: 5m
labels:
severity: critical
annotations:
summary: "数据库连接池使用率超过80%"
• 优化手段: • 动态扩容:HikariCP的maximumPoolSize根据QPS自动调整(需配合微服务动态配置中心) • 连接泄漏检测:Druid的removeAbandoned=true + 告警通知
2. 慢SQL率分析
• 采集方式: • MySQL慢查询日志: sql SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 2; -- 超过2秒的SQL记录 • ShardingSphere全链路追踪: yaml spring: shardingsphere: props: sql-show: true # 打印逻辑SQL与真实SQL • 治理流程:
-
TOP N慢SQL定位:通过
mysqldumpslow工具分析日志 -
执行计划分析:
EXPLAIN查看索引使用情况 -
优化方案: ◦ 缺失索引 → 添加联合索引 ◦ 复杂JOIN → 冗余字段或拆分为多次查询
3. 分片路由耗时
• 监控项: • 路由计算耗时:ShardingSphere的sql_route_time指标 • 跨分片查询比例:shardingsphere_routed_sql_total{type="select", is_broadcast="false"} • 调优方案: • 强制分片键:拦截无分片键查询(ShardingSphere配置allowRangeQueryWithoutShardingKey=false) • 本地缓存路由表:预热高频查询分片位置(如用户ID与分片映射关系)
调优手段
1. 避免跨分片查询
• 分片键强制校验:
// AOP拦截无分片键查询
@Around("execution(* com.example.repository.*.*(..))")
public Object checkShardingKey(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
ShardingKeyRequired annotation = signature.getMethod().getAnnotation(ShardingKeyRequired.class);
if (annotation != null) {
Object[] args = joinPoint.getArgs();
if (!hasShardingKey(args)) {
throw new IllegalStateException("查询必须包含分片键");
}
}
return joinPoint.proceed();
}
• 冗余字段设计:订单表冗余user_id,避免关联查询用户表
2. 异步化聚合统计
• 方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| MQ+Elasticsearch | 实时性高(秒级延迟) | 数据需同步到ES |
| Flink实时计算 | 支持复杂计算(如UV统计) | 架构复杂度高 |
| 本地缓存+定时批处理 | 资源消耗低 | 实时性差(分钟级延迟) |
| • 代码示例: |
// 订单金额统计异步化
@Async("statsThreadPool")
public void asyncOrderStats(LocalDate date) {
List<Order> orders = orderRepository.findByDate(date); // 直接查分片
BigDecimal total = orders.stream().map(Order::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
statsCache.put(date, total);
}
3. 热点数据二级分片
• 动态分片键: • 场景:某直播间用户评论突增导致单分片过热 • 方案:在原分片键(直播间ID)基础上追加随机后缀(如room_id:123#slot=5) • 本地缓存:
// Caffeine缓存热点评论
Cache<String, List<Comment>> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.SECONDS) // 短时间缓存降低DB压力
.build();
public List<Comment> getHotComments(String roomId) {
return cache.get(roomId, key -> commentRepository.findHotComments(key));
}
工具链
1. Prometheus + Grafana监控
• 数据采集: • ShardingSphere Exporter:暴露shardingsphere_datasource_active_connections等指标 • 自定义指标:通过Micrometer注册业务指标(如跨分片查询次数) • 看板配置:
// Grafana面板示例(分片负载均衡)
{
"panels": [{
"type": "graph",
"title": "分片查询分布",
"targets": [{
"expr": "sum(shardingsphere_routed_sql_total) by (datasource)"
}]
}]
}
2. SkyWalking链路追踪
• 集成配置:
# skywalking-agent.config agent.service_name=order-service collector.backend_service=127.0.0.1:11800 plugin.mysql.trace_sql_parameters=true
• 链路分析: • 跨分片查询追踪:自动标记跨库查询的Span • 慢事务根因定位:分析事务链路中的慢SQL或远程调用 • 生产案例: • 美团外卖订单链路:通过SkyWalking定位到跨分片JOIN导致慢查询,优化后RT降低60%
高频面试题
-
如何快速定位慢SQL的瓶颈? • 答:SkyWalking链路追踪 +
EXPLAIN执行计划分析,优先检查索引缺失与数据倾斜。 -
分片路由耗时过高可能是什么原因? • 答:路由规则复杂(如多分片键联合计算)、未预热路由缓存、跨分片查询过多。
-
如何设计一个高可用的监控系统? • 答:Prometheus联邦架构 + Thanos长期存储,配合Grafana多数据源聚合展示。
七、大厂真实案例
电商订单库:用户ID取模分片 + 冷热数据归档HBase
背景与挑战
• 业务场景:日订单量超千万,单表数据一年破百亿,查询性能从秒级跌至分钟级。 • 核心痛点: • 用户高频查询“我的订单”接口(强依赖user_id)。 • 历史订单占用90%存储但访问频率低(冷热数据混杂)。
分库分表方案
-
水平分片: • 分片键:
user_id % 1024(1024个分片,单分片控制在500万行内)。 • 路由规则:// 根据用户ID计算分片 int shardNo = Math.abs(userId.hashCode()) % 1024; String tableName = "orders_" + shardNo;
-
冷热分离: • 热数据:近3个月订单存MySQL,索引优化(
user_id + create_time联合索引)。 • 冷数据:3个月前数据归档HBase,ROW_KEY设计为user_id|order_id(范围扫描优化)。
技术细节
• 数据同步:Canal监听MySQL Binlog,触发冷数据迁移至HBase。 • 查询优化: • 热数据查询直接走MySQL分片。 • 冷数据查询走HBase的PrefixFilter(user_id前缀匹配)。
优化效果
• 查询性能:用户订单列表响应时间从12s降至200ms。 • 存储成本:HBase压缩比提升60%,存储费用降低75%。
社交Feed流:用户ID+时间联合分片 + 读写分离
业务场景
• 用户发布动态实时推送粉丝,读QPS峰值百万级。 • 单表存储用户动态,数据量日均十亿级。
分片方案
-
联合分片键: • 主分片键:
user_id % 256(256个分片)。 • 二级分片键:create_time按月分表(如feed_202301)。 -
读写分离: • 写节点:主库处理发布请求,分片规则为
user_id。 • 读节点:从库按user_id + create_time分片,支撑粉丝Feed流拉取。
技术实现
• 动态发布:
INSERT INTO feed_{user_id%256} (content, user_id, create_time)
VALUES ('Hello World', 123456, NOW());
• Feed流读取:
-- 查询用户关注列表的动态(按时间倒序) SELECT * FROM feed_* WHERE user_id IN (SELECT followed_user_id FROM follow WHERE fan_user_id = 123) ORDER BY create_time DESC LIMIT 100;
• 优化手段: ◦ 粉丝关系缓存Redis(Sorted Set存储关注列表)。 ◦ Feed流结果缓存CDN,降低DB压力。
性能提升
• 发布吞吐量:从5K TPS提升至50K TPS。 • 读延迟:Feed流加载从3s降至800ms。
物流轨迹库:地理位置GeoHash分片 + Elasticsearch检索
业务需求
• 存储全国物流轨迹点,每日新增轨迹数据十亿级。 • 需支持两类查询: • 精确查询:根据运单号查全链路轨迹。 • 区域查询:查询某地所有待派送订单。
分片方案
-
GeoHash分片: • 原理:将经纬度编码为字符串(如
wx4g0),按前缀分片。 • 分片键:geohash.substring(0, 3)(前3位作为分片键,256个分片)。 -
Elasticsearch辅助索引: • 空间索引(geo_point):支持半径1km内的订单搜索。 • 联合查询:
运单号走MySQL分片,地理位置走Elasticsearch。
技术实现
• 写入流程:
// 计算GeoHash(纬度31.23, 经度121.47)
String geoHash = GeoHash.encode(31.23, 121.47, 5);
// 插入MySQL分片
String table = "track_" + geoHash.substring(0, 3);
jdbcTemplate.update("INSERT INTO " + table + " VALUES (?, ?, ?)", orderId, geoHash, time);
// 同步到Elasticsearch
esClient.index(new IndexRequest("track").id(orderId)
.source(JsonUtils.toMap(new TrackPoint(orderId, geoHash, time))));
• 区域查询:
GET /track/_search
{
"query": {
"geo_distance": {
"distance": "1km",
"location": "31.23,121.47"
}
}
}
优化效果
• 精确查询:运单号查询走MySQL分片,RT<50ms。 • 区域查询:Elasticsearch百公里范围检索,RT<200ms。
八、高频面试题精选
1. 分库分表后如何高效分页?
问题分析
• 传统分页失效:LIMIT 10000, 10需扫描并丢弃前10000行,跨分片时性能灾难。 • 解决方案: • 业务折衷: ◦ 禁止跳页(仅允许“下一页”按钮),用连续游标(如Search After)。 sql -- 第一页 SELECT * FROM orders WHERE user_id=123 ORDER BY id LIMIT 10; -- 第二页(使用上一页最后一条ID) SELECT * FROM orders WHERE user_id=123 AND id > last_id ORDER BY id LIMIT 10; • Elasticsearch辅助:复杂条件分页走ES,结果反查MySQL获取明细。 • 内存分页:若数据可缓存(如Redis),全量加载后内存中分页。
2. ShardingSphere如何解析SQL路由?
核心流程
-
SQL解析: • 解析引擎生成抽象语法树(AST),提取分片条件(如
user_id=123)。 -
路由计算: • 精确路由:分片键等值查询(
user_id=123)直接定位分片。 • 广播路由:无分片键的更新(如UPDATE config SET value=1)全分片执行。 -
结果归并: • 跨分片查询结果在内存中排序、聚合(如
ORDER BY time DESC)。
配置示例
rules:
- !SHARDING
tables:
orders:
actualDataNodes: ds_${0..1}.orders_${0..15}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: hash_mod
shardingAlgorithms:
hash_mod:
type: HASH_MOD
props:
sharding-count: 2
3. 分片键数据倾斜的应急方案?
临时措施
• 虚拟节点再平衡:
// 原分片:user_id % 8 // 扩容后:(user_id.hashCode() + virtual_node) % 16 int newShard = (userId.hashCode() + slot) % 16;
• 热点数据二级分片: • 例如对热点用户(如网红)的订单按user_id + order_id联合分片。
长期方案
• 分片键改造:联合业务高基字段(如user_id + city_code)。 • 动态分片:根据数据分布自动调整分片映射(如一致性哈希)。
4. 如何设计全局唯一ID(雪花算法 vs 号段模式)?
方案对比
| 维度 | 雪花算法(Snowflake) | 号段模式(Segment) |
|---|---|---|
| 唯一性 | 全局唯一(数据中心ID+机器ID+时间戳+序列号) | 依赖数据库唯一性保障(如自增主键) |
| 性能 | 本地生成,无网络开销(单机每秒百万级) | 需预取号段,DB宕机影响ID生成 |
| 缺点 | 时钟回拨导致ID重复(需处理NTP同步) | 号段耗尽时需访问DB,存在尖峰压力 |
| 适用场景 | 高并发分布式系统(如电商订单、支付流水) | 中小规模系统(如内部管理平台) |
Snowflake避坑实践
public class SnowflakeIdWorker {
private long twepoch = 1288834974657L; // 起始时间戳
private long sequence = 0L;
// 解决时钟回拨
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) { // 允许回拨5ms内等待
Thread.sleep(offset << 1);
timestamp = timeGen();
} else {
throw new RuntimeException("时钟回拨超过5ms");
}
}
// ...生成ID逻辑
}
}
总结:本章涵盖的案例与面试题均来自阿里、美团、字节等一线大厂真题,掌握这些内容可从容应对95%的分库分表相关技术挑战。
更多推荐
所有评论(0)