包含:风险现象 → 原因 → 解决方案 → 预防措施 → 代码/配置示例,可用于架构评审、上线检查、故障应急。
1. 无分片键查询 → 全库表广播,数据库雪崩
现象
- 接口 RT 突增几百毫秒~几秒
- 所有分库 CPU 飙升、IO 飙升
- 连接数瞬间打满,应用假死
根本原因
查询条件不携带分片键,Sharding 必须向所有库、所有表发送 SQL,然后内存聚合。
解决方案
- 强制接口必传分片键(user_id、order_id 等)
- 后台管理、运营查询走 ES/ClickHouse/Doris
- 对非核心查询做限流、熔断
- 对必须全表查询的场景做离线同步
预防措施
yaml
# ShardingSphere 开启全路由告警
spring.shardingsphere.props:
sql-show: false
check-all-route-enabled: true # 开启广播表检测
java
// 代码层强制校验分片键(AOP 实现)
@Before("execution(* com.xxx.mapper.*.select*(..))")
public void checkShardingColumn() {
Object param = getParamValue();
if (param == null || ShardingUtil.getShardingValue(param) == null) {
throw new RuntimeException("禁止无分片键查询");
}
}
2. 跨库 JOIN → 应用 OOM / 极慢查询
现象
- 简单关联查询执行几秒
- 应用频繁 FullGC、OOM 宕机
- 数据库线程堆积
根本原因
Sharding 不支持分布式 join,只能:
各库查 → 网络传输 → 应用内存聚合 → 性能灾难
解决方案
- 绝对禁止跨库 join
- 拆分为多次单表查询,业务层聚合
- 构建宽表,冗余字段避免关联
- 报表类走数仓
预防措施
- CodeReview 拦截 XML 里的 JOIN
- SQL 审计平台自动拦截
- 固定使用单表查询规范
3. 数据倾斜(热分片)→ 分库分表失效
现象
- 部分表数据占比 70%+
- 热点分片查询极慢
- 其他分片空闲浪费
根本原因
- 分片键选择不合理(地区、商家、活动)
- 哈希算法不均匀
- 超级商家/大用户存在
解决方案
- 热点数据单独分片、单独缓存
- 使用 复合分片键(user_id + order_id)
- 对超级商家做业务层打散
预防措施
上线前执行数据分布统计:
sql
select user_id%2 as db_idx, count(*) from t_order group by db_idx;
任何分片占比 >30% 必须整改。
4. 分布式事务不一致 → 部分成功部分失败
现象
- 订单创建成功,库存没扣
- 库存扣了,订单没生成
- 资金与业务数据不一致
根本原因
分库后跨数据源,Spring 本地事务失效。
解决方案
- 尽量避免跨库写(同用户数据路由到同库)
- 引入 Seata AT 模式 分布式事务
- 核心场景使用 消息最终一致性(RocketMQ 事务消息)
预防措施
yaml
# Seata 整合示例
spring:
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group
java
@GlobalTransactional(rollbackFor = Exception.class)
public void createOrder() {
orderMapper.insert();
stockMapper.deduct();
}
5. 双写不一致 → 新旧库数据对不上
现象
- 旧表有数据,分片表无
- 分片表多数据、重复数据
- 金额/状态不一致
根本原因
- 双写异步失败未重试
- 异常被吞
- 事务未统一
解决方案
- 双写失败自动重试(3 次)
- 定时对账任务,不一致自动修复
- 关键路径同步双写
预防措施
java
// 重试模板
RetryTemplate.execute(retryContext -> {
shardingOrderMapper.insert(order);
return true;
});
sql
-- 每日自动对账
SELECT count(*) FROM old_table
WHERE id NOT IN (SELECT id FROM sharding_table);
6. 数据库连接数爆炸 → 数据库拒绝连接
现象
- 应用报 CannotGetJdbcConnection
- 数据库 show processlist 爆满
- 新请求进不来
根本原因
分 4 库 × 20 连接 × 10 实例 = 800 连接,MySQL 默认只有 151~3000。
解决方案
- 每个库连接数设为 8~16
- 合理评估总连接数
- 开启连接池监控告警
预防措施
yaml
spring.shardingsphere.datasource.ds0.hikari:
maximum-pool-size: 12
minimum-idle: 3
7. 深度分页 limit m,n → OOM / 极慢
现象
- limit 100000,10 直接卡死
- 应用内存飙升
- 所有分片参与查询排序
解决方案
- 禁止深度分页
- 使用 游标分页(cursor-based)
- 后台导出走离线任务
预防措施
java
// 游标分页示例
lambdaQuery()
.gt(Order::getId, lastId)
.orderByAsc(Order::getId)
.limit(20);
8. 分片键设计错误 → 架构级事故
现象
- 上线后发现查询无法带分片键
- 无法扩容
- 无法做统计
根本原因
- 用时间、地区、商家 ID 做分片
- 未评估业务查询模型
解决方案
- 优先选择 用户 ID / 订单 ID
- 哈希分片,均匀分布
- 复合分片提高兼容性
预防措施
架构评审必须回答三问:
- 90% 查询是否带它?
- 是否均匀?
- 是否稳定不变?
9. DDL 风险 → 锁表、漏执行、主从延迟
现象
- 加字段导致线上阻塞
- 部分表加了,部分没加
- 主从延迟巨大
解决方案
- 使用 gh-ost / pt-online-schema-change
- 低峰执行
- 批量自动执行,禁止手动
预防措施
编写批量执行 DDL 脚本:
bash
for db in order_db_0 order_db_1; do
for table in t_order_0 t_order_1 t_order_2 t_order_3; do
mysql -e "ALTER TABLE $db.$table ADD COLUMN ..."
done
done
10. 扩容困难(取模分片无法在线扩)
现象
user_id%4 → 想扩成 8 张表
→ 必须全量迁移、停服、改代码
解决方案
- 初始规划 2库16表 / 4库32表,一步到位
- 使用一致性哈希分片
- 使用 ShardingSphere 自动扩容能力
预防措施
yaml
# 使用 MOD 避免简单取模
sharding-algorithms:
table-sharding:
type: MOD
props:
sharding-count: 16
二、生产上线前必查 10 条红线
- ❌ 禁止无分片键查询
- ❌ 禁止跨库 JOIN
- ❌ 禁止深度分页
- ❌ 禁止使用自增 ID
- ❌ 禁止手动分表 DDL
- ❌ 禁止无监控上线
- ❌ 禁止无双写/无回滚直接切
- ❌ 禁止数据倾斜
- ❌ 禁止无对账任务
- ❌ 禁止非哈希分片
三、故障应急 3 步法
- 切回旧数据源(双写还在,1 分钟恢复)
- 关闭非核心查询,限流
- 排查全路由 SQL / 慢 SQL / 热点分片