分库分表是解决单库单表性能瓶颈的核心方案,但它本质是 "用复杂度换性能",会引入 7 大类核心问题,我结合实际项目经验逐一说明:
问题分类 | 具体表现 | 主流解决方案 | 推荐指数 |
|---|---|---|---|
分布式事务 | 跨库操作无法保证 ACID,数据不一致 |
| ⭐⭐⭐⭐⭐ |
跨库关联查询 | JOIN 操作无法跨库执行,多表查询困难 |
| ⭐⭐⭐⭐ |
分页排序聚合 | 跨表 LIMIT、ORDER BY、GROUP BY 结果错误 |
| ⭐⭐⭐⭐ |
分布式 ID 生成 | 自增 ID 在多表间重复,主键冲突 |
| ⭐⭐⭐⭐⭐ |
数据迁移扩容 | 扩容时数据重分布,业务停服 |
| ⭐⭐⭐⭐ |
多数据源管理 | 数据源切换复杂,连接池资源浪费 |
| ⭐⭐⭐⭐ |
读写一致性 | 主从延迟导致读写不一致 |
| ⭐⭐⭐ |

public class SnowflakeIdGenerator {
// 起始时间戳 (2020-01-01)
private static final long START_TIMESTAMP = 1577836800000L;
// 机器ID位数
private static final long WORKER_ID_BITS = 5L;
// 数据中心ID位数
private static final long DATACENTER_ID_BITS = 5L;
// 序列号位数
private static final long SEQUENCE_BITS = 12L;
// 最大值
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
// 偏移量
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
// 序列号掩码
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException("Worker ID超出范围");
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException("Datacenter ID超出范围");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 核心亮点:时钟回拨处理
if (currentTimestamp < lastTimestamp) {
long offset = lastTimestamp - currentTimestamp;
// 回拨小于5ms,等待恢复
if (offset <= 5) {
try {
wait(offset << 1);
currentTimestamp = System.currentTimeMillis();
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨严重,无法生成ID");
}
} catch (InterruptedException e) {
throw new RuntimeException("线程中断", e);
}
} else {
throw new RuntimeException("时钟回拨超过5ms,无法生成ID");
}
}
// 同一毫秒内序列号自增
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
// 序列号溢出,等待下一毫秒
if (sequence == 0) {
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
// 拼接64位ID
return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
| (datacenterId << DATACENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}synchronized保证单实例线程安全,性能优于ReentrantLock /**
* 复合分片策略:按用户ID哈希分表 + 按创建时间分库
* 优点:数据分布均匀,支持按时间快速查询历史数据
*/
public class UserOrderComplexShardingStrategy implements ComplexKeysShardingAlgorithm<Long> {
// 分库数量
private static final int DB_COUNT = 4;
// 每个库的分表数量
private static final int TABLE_COUNT_PER_DB = 8;
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Long> shardingValue) {
// 获取分片键值
Map<String, Collection<Long>> valueMap = shardingValue.getColumnNameAndShardingValuesMap();
Long userId = valueMap.get("user_id").iterator().next();
Long createTime = valueMap.get("create_time").iterator().next();
// 按时间分库:每半年一个库
LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(createTime), ZoneId.systemDefault());
int year = time.getYear() - 2020;
int month = time.getMonthValue();
int dbIndex = (year * 2 + (month > 6 ? 1 : 0)) % DB_COUNT;
// 按用户ID哈希分表
int tableIndex = Math.abs(userId.hashCode()) % TABLE_COUNT_PER_DB;
// 拼接实际表名
String actualTableName = String.format("order_db_%d.order_%d", dbIndex, tableIndex);
return Collections.singletonList(actualTableName);
}
@Override
public void init(Properties properties) {}
@Override
public String getType() {
return "USER_ORDER_COMPLEX_SHARDING";
}
}@Service
public class OrderPaginationService {
@Autowired
private OrderMapper orderMapper;
/**
* 游标分页查询(性能比offset分页高100倍以上)
* 对比:offset分页需要扫描前N条数据,游标分页直接定位到起始位置
*/
public PageResult<Order> getOrdersByCursor(Long userId, Long cursor, int pageSize) {
List<Order> orders;
if (cursor == null) {
// 第一页,没有游标
orders = orderMapper.selectFirstPage(userId, pageSize);
} else {
// 后续页,使用游标(上一页最后一条记录的ID)
orders = orderMapper.selectByCursor(userId, cursor, pageSize);
}
// 计算是否有下一页
boolean hasNext = orders.size() == pageSize;
// 获取下一页的游标
Long nextCursor = hasNext ? orders.get(orders.size() - 1).getId() : null;
return new PageResult<>(orders, hasNext, nextCursor);
}
}
// Mapper接口
public interface OrderMapper {
@Select("SELECT * FROM order WHERE user_id = #{userId} ORDER BY id DESC LIMIT #{pageSize}")
List<Order> selectFirstPage(@Param("userId") Long userId, @Param("pageSize") int pageSize);
@Select("SELECT * FROM order WHERE user_id = #{userId} AND id < #{cursor} ORDER BY id DESC LIMIT #{pageSize}")
List<Order> selectByCursor(@Param("userId") Long userId, @Param("cursor") Long cursor, @Param("pageSize") int pageSize);
}
// 分页结果类
@Data
@AllArgsConstructor
public class PageResult<T> {
private List<T> data;
private boolean hasNext;
private Long nextCursor;
}技术难点 | 根本原因 | 最优解决方案 | 技术亮点 | 避坑指南 |
|---|---|---|---|---|
数据倾斜 | 分片键选择不当,部分分片数据量远超其他 |
| 热点数据自动识别与分流,支持在线调整分片权重 | ❌ 绝对不要用状态字段(如订单状态)作为分片键 |
跨库 COUNT/SUM 聚合 | 无法直接执行 SQL 聚合,结果不准确 |
| 冷热数据分离,热数据实时聚合,冷数据预计算 | ❌ 不要在高并发场景下做跨库实时聚合 |
平滑扩容(不停服) | 数据重分布需要迁移大量数据,影响业务 |
| 增量数据同步 + 全量校验,业务无感知 | ❌ 扩容前必须做压测,避免迁移过程中数据库压力过大 |
读写一致性 | 主从延迟导致刚写入的数据读不到 |
| 基于业务场景动态选择读策略,平衡性能与一致性 | ❌ 不要盲目开启读写分离,核心交易场景必须强制读主 |
分布式事务回滚失败 | 部分分支事务失败,无法全局回滚 |
| 事务日志持久化,失败自动重试,人工干预接口 | ❌ 避免长事务,事务超时时间设置合理 |
分库分表后的运维 | 多数据源管理复杂,备份恢复困难 |
| 一键扩容、一键迁移、一键备份,减少人工操作 | ❌ 不要手动修改分片数据,所有操作通过统一平台执行 |
“咱们聊个实在的。你现在负责的订单系统,单库单表已经到瓶颈了,你决定分库分表。那你踩过哪些实打实的坑?怎么填的?一个一个说。” 😏
“好,这一拆,真是捅了马蜂窝。我先给您画个痛点的全景图,您一看就明白👇”

“这四个玩意儿,一个比一个让人头秃。我按顺序跟您讲讲当时我们流的血和泪。” 😅
“行,那先说说 ID 那块,自增主键冲突你们怎么搞的?”
“原来 AUTO_INCREMENT 爽得很,拆成 32 张表后直接傻眼。UUID 太长、索引不友好,我们毫不犹豫上了雪花算法 Snowflake 的魔改版。
机器 ID 这块,因为我们跑在 k8s 上,直接用 Pod IP 最后两个字节异或一下当 workerId,部署时自动分配,不用人工配。
核心噩梦是时钟回拨。有一次 NTP 同步把时间往回拨了 3 秒,差点没把我们搞死。😱 后来我们在 ID 生成器里加了个硬防御:如果当前时间比上次生成时间小,就自旋等待差值加倍时间;要是回拨超过 500ms,直接抛异常、告警,那台机器熔断不接流量,等时钟追平再恢复。稳得很。”
“跨分片查询呢?按 user_id 分片,运营非要按时间范围查全部,还要求分页,这你们怎么‘伺候’的?” 🤔
“这就是最隐蔽的雷!我们当时差点跟产品打起来。最后形成了三套组合拳:
WHERE id > last_id ORDER BY id LIMIT N,内存里归并取最小 N 条,这样不分页,只有上下滑动。敢点“跳转到第 100 页”就直接报错 —— 因为真实数据量下内存归并 100 页就是找死。🤯 WHERE id IN(...) 查详细。延迟秒级,够用。 我拿 B+ 树推算说服了产品:分片下 LIMIT 100,20 这种偏移量越大性能越烂,产品听完也认了。” ✌️
“说得好,那分布式事务呢?@Transactional 肯定废了,你们对一致性要求高的扣款怎么搞的?”
“我们铁律就一条:能不用强分布式事务就不用。扣款这种场景用了 TCC 模式。
比如扣库存,Try 阶段用 Redis DECR 预占,同时把冻结流水写进一张 frozen_record 表(和业务库同分片,本地事务保证)。Confirm 阶段真正扣减数据库库存并删冻结记录,Cancel 则把 Redis 库存加回去。
这块最忌讳空谈理论。我们还做了双重兜底:一是所有 TCC 接口必须幂等,靠唯一流水号防重;二是每天凌晨跑一套对账脚本,对比支付单和库存流水,差一分钱马上报警。金融业务没有对账就是裸奔。” 💰
对于非核心链路,比如发送优惠券,我们直接用本地消息表 + RocketMQ 事务消息,保证最终一致性就完事了。
“那最后说扩容吧,假如你开始是 8 库 16 表,现在要扩到 16 库 32 表,数据要重分布,怎么弄不丢数据还不长时间停机?” ⏳
“扩容绝对是终极大考,我们用的双写 + 灰度切流的方案,分四步走:
有一次迁移了 2 亿条数据,干了一整晚,好在全自动脚本稳住了,第二天早上只公告停机了 20 分钟。😌 如果不追求秒级无损,停服搬家最稳妥,不丢人。”
“总结得不错,能看出你是真刀真枪干过的。那最后一个开放点,做完这些你架构上最大的体会是什么?”
“分库分表本质是把压力分散,但把查询复杂度集中了。所以不能光拆,要同步做‘减熵’:联机库只保留最核心的主键查询和小范围扫描,把复杂分析、搜索甩给 ES/列存。代码里杜绝跨分片 join,宁肯多查一次。这样架构才不会腐化成一坨没人敢动的屎山。” 💩
“别光说方案,把你刚才提到的 雪花算法防时钟回拨、跨分片游标分页、TCC 幂等扣库存 这几个最见功底的代码,挑关键部分给我看看。再把你整个分库分表面临的技术难点,一表给我汇总。” ⌨️
“没问题,我直接上 Java 伪代码,去掉业务敏感词,保留骨骼,您一看就懂。”
public class SnowflakeIdGenerator {
private final long epoch = 1577836800000L; // 2020-01-01
private final long workerIdBits = 10L;
private final long sequenceBits = 12L;
private final long maxWorkerId = ~(-1L << workerIdBits);
private final long sequenceMask = ~(-1L << sequenceBits);
private final long workerIdShift = sequenceBits;
private final long timestampShift = sequenceBits + workerIdBits;
private long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
private final long maxBackwardMs = 500; // 允许最大回拨 500ms
public SnowflakeIdGenerator(long workerId) {
if (workerId > maxWorkerId || workerId < 0) throw new IllegalArgumentException();
this.workerId = workerId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 🧨 时钟回拨防御
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= maxBackwardMs) {
try { Thread.sleep(offset << 1); } // 等两倍差值
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) throw new RuntimeException("Clock moved backwards!");
} else {
throw new RuntimeException("Clock moved backwards too much!");
}
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) timestamp = tilNextMillis(lastTimestamp);
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - epoch) << timestampShift) | (workerId << workerIdShift) | sequence;
}
private long tilNextMillis(long lastTimestamp) {
long ts = System.currentTimeMillis();
while (ts <= lastTimestamp) ts = System.currentTimeMillis();
return ts;
}
}✨ 技术亮点:内置两级时钟回拨处理 —— 小回拨自旋等待,超限直接熔断抛异常,配合监控保证 ID 绝对递增。
public List<Order> scrollOrders(long lastId, int limit, List<Shard> shards) {
int shardCount = shards.size();
List<CompletableFuture<List<Order>>> futures = new ArrayList<>();
// 并行查询每个分片
for (Shard shard : shards) {
futures.add(CompletableFuture.supplyAsync(() ->
shard.query("SELECT * FROM order WHERE id > ? ORDER BY id LIMIT ?", lastId, limit)
));
}
// 内存归并,使用最小堆取全局最小的 N 条
PriorityQueue<Order> minHeap = new PriorityQueue<>(Comparator.comparingLong(Order::getId));
for (CompletableFuture<List<Order>> future : futures) {
List<Order> part = future.join();
if (part.size() > limit) part = part.subList(0, limit); // 防爆
minHeap.addAll(part);
}
List<Order> result = new ArrayList<>(limit);
while (!minHeap.isEmpty() && result.size() < limit) {
result.add(minHeap.poll());
}
return result;
}✨ 技术亮点:CompletableFuture 并行,PriorityQueue 归并取 Top N,强制截断单分片结果集防 OOM,天然适配瀑布流分页。
// Try 阶段
@Transactional
public boolean tryFreeze(String orderId, long skuId, int count) {
// 幂等:先查流水
if (frozenRecordDao.exists(orderId)) return true;
// Redis 预占库存,原子操作
Long remain = redis.decrBy("stock:sku:" + skuId, count);
if (remain == null || remain < 0) {
redis.incrBy("stock:sku:" + skuId, count); // 回滚 Redis
return false;
}
// 写冻结流水(和订单同库,本地事务)
frozenRecordDao.insert(new FrozenRecord(orderId, skuId, count));
return true;
}
// Confirm 阶段
@Transactional
public void confirm(String orderId) {
FrozenRecord record = frozenRecordDao.selectForUpdate(orderId);
if (record == null || record.getStatus() == CONFIRMED) return; // 幂等
// 正式扣减数据库库存
stockDao.decrease(record.getSkuId(), record.getCount());
// 标记完成
record.setStatus(CONFIRMED);
frozenRecordDao.update(record);
// Redis 不需要管,预占时已经扣了
}
// Cancel 阶段
@Transactional
public void cancel(String orderId) {
FrozenRecord record = frozenRecordDao.selectForUpdate(orderId);
if (record == null || record.getStatus() == CANCELLED) return; // 幂等
// 释放 Redis 库存
redis.incrBy("stock:sku:" + record.getSkuId(), record.getCount());
record.setStatus(CANCELLED);
frozenRecordDao.update(record);
}✨ 技术亮点:所有操作靠流水状态做幂等,Redis 和 DB 不一致时依靠每天对账脚本修复,实现最终一致。
public class ConsistentHashRouter {
private final TreeMap<Integer, Shard> ring = new TreeMap<>();
private final int virtualNodeCnt;
private final List<Shard> newShardGroup = null; // 扩容时临时持有新集群
public Shard route(long shardKey) {
int hash = hash(shardKey);
if (ring.ceilingEntry(hash) != null) return ring.ceilingEntry(hash).getValue();
return ring.firstEntry().getValue();
}
// 双写:同时写入新旧集群
public void insertWithDualWrite(Order order) {
Shard oldShard = route(order.getUserId());
oldShard.insert(order);
if (newShardGroup != null) {
Shard newShard = newConsistentHashRoute(order.getUserId());
newShard.insert(order);
}
}
}✨ 技术亮点:TreeMap 实现环,虚拟节点均匀分布;扩容期间 newShardGroup 非空时开启双写,灰度切换读流量。
技术难点 | 问题表现 | 解决方案 | 关键手段 |
|---|---|---|---|
分布式 ID 冲突 | 多库自增主键重复 | 雪花算法 + workerId 自动分配 | 时钟回拨两级防御(自旋&熔断) |
跨分片排序分页 | 全局分页需归并所有分片 | 游标分页 + 内存归并 + 异构索引 | 拒绝跳页,限制单分片结果集 |
分布式事务一致 | 跨库操作原子性丧失 | TCC + 本地消息表 + 对账 | 幂等设计,Redis 预占,每日对账 |
扩容数据迁移 | 新分片加入需数据重分布 | 一致性哈希 + 双写 + 灰度切流 | 虚拟节点,MD5 校验,停服兜底 |
分片热点倾斜 | 个别分片负载过高 | 分片键打散(如尾部加随机数)或再分片 | 监控分区 QPS,动态路由感知 |
“代码清爽,回拨处理、归并防 OOM、幂等设计都到位了,看起来是写过上万行核心代码的人。"
【别走,交个朋友】
我是Rain ,一个喜欢把复杂技术讲透、让代码落地的实践者。
如果你厌倦了四处收集碎片化的八股文,想看看一线项目里真实的架构决策和踩坑记录,欢迎来我的公众号「Rain的Java大神之路」坐坐。每天一个知识点,程序员的加油站。
在这里,我会把每一次技术复盘、每一个项目的设计源码,毫无保留地分享给你。
🔥技术这条路很酷,我们结伴同行。
🚀Keep Coding, Keep Loving —— 与所有 Java 同路人共勉。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。