业务增长与数据库压力激增
随着业务规模持续扩张,数据库数据量呈指数级增长,导致数据库负载压力显著增大
在业务高峰期,数据库性能出现明显下降。为保障系统稳定性, 急需通过技术手段分担数据库压力
主从复制架构虽能通过读写分离缓解读压力,但当写负载集中于主库时:
- 硬件升级存在瓶颈:CPU核心数限制与成本效益失衡
- 单一主库成为性能瓶颈,需通过数据库拆分实现负载分散
读写分离:缓解读负载压力
通过主从复制架构,将主数据库数据同步至多个从数据库节点,实现读写分离:
- 核心机制:主库处理写操作,从库处理读请求,分散读负载压力
- 短期效果:初期可有效提升性能,缓解单一主库的读压力
- 局限性:
- 仅解决读负载瓶颈,无法应对写压力增长
- 硬件升级(如增加CPU)存在物理上限,无法长期支撑业务扩张
技术细节:MySQL主从复制通过二进制日志(binlog)实现数据同步
-- 主库配置示例
CHANGE MASTER TO
MASTER_HOST='primary_host',
MASTER_USER='replica_user',
MASTER_PASSWORD='password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=154;
通过主从复制架构,将读请求分流到多个从库:
- 实现方式:主库同步数据到从库,应用层通过中间件分离读写流量
- 局限性:仅解决读负载,主库写压力未缓解
- 代码示例(NestJS 读写分离配置):
// datasource.config.ts @Injectable() export class DataSourceConfig { createTypeOrmOptions(): TypeOrmModuleOptions { return { type: 'mysql', replication: { // 读写分离配置 master: { host: 'master.db.com', port: 3306 }, slaves: [ { host: 'slave1.db.com', port: 3306 }, { host: 'slave2.db.com', port: 3306 } ] } }; } }
关键架构对比
| 方案 | 适用场景 | 优势 | 挑战 |
|---|---|---|---|
| 读写分离 | 读多写少 | 简单易用,降低读负载 | 无法解决写负载 |
| 垂直拆分(分库) | 多库压力均衡 | 逻辑清晰,隔离性强 | 跨库事务复杂 |
| 垂直拆分(分表) | 单库内表压力不均 | 精准分散热点表负载 | 单表过大时无效 |
| 水平拆分 | 单表数据/写请求过大 | 无限扩展能力 | 跨分片查询复杂 |
垂直拆分:按业务维度分库
当主库写压力成为瓶颈时,采用垂直分库策略:
1 ) 拆分逻辑:
- 将单节点中的多个逻辑数据库(如订单库、用户库、促销库)迁移至独立物理节点。
- 示例:原MySQL节点拆分为三个独立节点:
- 节点1:仅存储订单库
- 节点2:仅存储用户库
- 节点3:仅存储促销库
2 ) 优势:
- 实现简单,无跨库查询需求时仅需调整数据源配置
- 分散写压力至不同节点
3 )挑战:
- 跨库查询需业务层处理
- 若热点数据集中(如订单库),该节点仍可能成为瓶颈
4 ) 缺陷:
- 若写压力集中于单一库(如订单库),节点1仍可能成为瓶颈
// 动态数据源配置示例 @EntityRepository(Order) export class OrderRepository extends Repository<Order> { @InjectDataSource('order_db') // 指向订单库数据源 private readonly dataSource: DataSource; }
5 )SQL 示例(分库后查询)
/* 订单服务专属连接 */
SELECT * FROM order_db.orders WHERE user_id = 1001;
/* 用户服务专属连接 */
SELECT * FROM user_db.users WHERE id = 1001;
垂直分表:按主题拆分表
当单库写压力仍过高时,进一步采用垂直分表:
- 拆分逻辑:
- 将同一数据库中的表按业务主题拆分至不同节点。
- 示例:订单库中的表拆分为:
- 节点A:订单表(
orders)、订单详情表(order_details) - 节点B:商品表(
products)、购物车表(carts)
- 节点A:订单表(
- 核心价值:
- 将原单库的写压力分散至多个节点,实现负载二次分摊。
- 瓶颈:
- 若单主题表数据量过大(如订单表日均百万级),节点A仍可能成为性能瓶颈。
原生SQL分表示例:
-- 创建分库分表路由(订单表拆分至节点A)
CREATE TABLE order_db.orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
amount DECIMAL(10,2)
) ENGINE=InnoDB;
-- 商品表拆分至节点B
CREATE TABLE product_db.products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100)
);
NestJS 多数据源配置:
// order.datasource.ts
@EntityRepository(Order)
export class OrderRepository extends Repository<Order> {
@Inject('ORDER_DATASOURCE')
private dataSource: DataSource
}
// cart.datasource.ts
@EntityRepository(Cart)
export class CartRepository extends Repository<Cart> {
@Inject('CART_DATASOURCE')
private dataSource: DataSource
}
水平拆分:终极解决方案
当垂直拆分无法满足需求时,采用水平分表:
- 核心思想:
- 将单表数据按分片键(如用户ID、时间)分散到多个物理节点
- 拆分策略:
- 范围分片:按时间区间(如每月一张表)
- 哈希分片:通过哈希算法均匀分布数据
- 关键技术:
- 分片算法:哈希取模、范围分片、一致性哈希
- 路由中间件:MyCat、ShardingSphere
- 技术实现:
- 以订单表为例,按用户ID哈希分片:
- 节点A:存储
user_id % 2 = 0的订单 - 节点B:存储
user_id % 2 = 1的订单
- 节点A:存储
- 以订单表为例,按用户ID哈希分片:
对单表进行数据分片(Sharding),按分片键(如用户ID)分散存储:
-- 原始订单表(1亿条)
CREATE TABLE orders (id BIGINT, user_id INT, amount DECIMAL);
-- 水平拆分后(按user_id分4片)
CREATE TABLE orders_0 (CHECK (user_id % 4 = 0));
CREATE TABLE orders_1 (CHECK (user_id % 4 = 1));
CREATE TABLE orders_2 (CHECK (user_id % 4 = 2));
CREATE TABLE orders_3 (CHECK (user_id % 4 = 3));
- 核心逻辑:
- 分片键选择:优先高频查询字段(如
user_id); - 数据路由:通过中间件或应用层路由到对应分片;
- 全局ID生成:需分布式ID方案(雪花算法、UUID)。
- 分片键选择:优先高频查询字段(如
SQL 示例(分片路由逻辑):
/* 按 user_id 分片:user_id % 4 决定分片位置 */
INSERT INTO orders_${shard_id} (order_id, user_id, amount)
VALUES (10001, 3005, 99.9); -- 写入 orders_1 分片(3005 % 4 = 1)
NestJS + ShardingSphere 集成
// sharding.config.ts
const shardingRule = new ShardingRule({
tables: [{
logicTable: 'orders',
actualDataNodes: 'ds${0..3}.orders_${0..3}',
databaseStrategy: 'standard',
shardingColumn: 'user_id',
shardingAlgorithm: 'mod4'
}],
shardingAlgorithms: {
mod4: { type: 'MOD', props: { shardingCount: 4 } }
}
});
NestJS分片路由逻辑
// 分片数据源选择器
getDataSource(shardKey: number): DataSource {
const shardIndex = shardKey % 2;
return shardIndex === 0 ? dataSourceA : dataSourceB;
}
// 查询分片表示例
async findOrdersByUserId(userId: number) {
const ds = this.getDataSource(userId);
return ds.query(`SELECT * FROM orders WHERE user_id = ${userId}`);
}
再看下面的分片示例
// order.entity.ts
@Entity()
@EntityRepository(Order)
export class Order {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
userId: number;
@Column()
amount: number;
}
// order.service.ts
@Injectable()
export class OrderService {
async getOrder(userId: number, orderId: string) {
const shardId = userId % 4; // 计算分片ID
const dataSource = this.getShardDataSource(shardId);
return dataSource.getRepository(Order).findOne({ where: { id: orderId } });
}
private getShardDataSource(shardId: number) {
// 动态选择分片数据源(需预配置)
return dataSources[`order_${shardId}`];
}
}
分库分表衍生问题解决方案
| 问题 | 解决策略 |
|---|---|
| 跨分片查询 | 并行查询+结果聚合(如MyCAT、ShardingSphere) |
| 事务一致性 | 分布式事务(XA、Seata)或最终一致性 |
| 全局唯一ID | Snowflake、Redis自增、UUID |
| 数据倾斜 | 一致性哈希算法动态调整分片 |
关键技术补充
- 分片键选择原则:
- 高离散性(避免数据倾斜)
- 高频查询字段(如用户ID、订单时间)
- 分库分表中间件:
- 推荐方案:ShardingSphere、Vitess
- 功能:SQL解析、路由、结果归并
- 跨分片查询处理:
- 并行查询+结果聚合(如
SUM()、MAX()) - 避免全表扫描(强制指定分片键)
- 并行查询+结果聚合(如
跨分片聚合SQL示例:
-- 统计所有节点订单总额
SELECT SUM(amount) FROM (
SELECT amount FROM orders_node0
UNION ALL
SELECT amount FROM orders_node1
) AS total_orders;
技术架构演进对比
| 阶段 | 适用场景 | 优势 | 挑战 |
|---|---|---|---|
| 读写分离 | 读多写少 | 快速提升读性能 | 主库写压力未解决 |
| 垂直分库 | 多业务模块耦合 | 业务解耦,分散写负载 | 跨库事务复杂 |
| 垂直分表 | 单库内表访问热度不均 | 优化热点表性能 | 需重构数据访问层 |
| 水平分表 | 海量数据高并发场景 | 无限扩展能力 | 分片策略设计复杂 |
核心原则:优先读写分离 → 次选垂直拆分 → 最终水平扩展。水平分表需结合分片键选择、全局ID生成(雪花算法)、分布式事务(Seata/TCC)综合设计
架构选择决策树
总结
- 数据库分库分表是应对高并发的核心手段,需分阶段实施:
-
- 读写分离 → 2. 垂直分库 → 3. 垂直分表 → 4. 水平分表
-
- 关键点:
- 分库分表本质是牺牲架构复杂度换取扩展性,需评估业务增长预期
- 水平分表是终极方案,但需配套解决分布式事务、查询聚合等难题
- 优先尝试读写分离+垂直拆分,避免过早引入分片复杂性
注意事项
- 读写分离是基础:通过主从复制分担读负载,但无法解决写瓶颈
- 垂直拆分先于水平拆分:优先按业务模块分库或分表,降低实现复杂度
- 水平拆分需谨慎设计:
- 分片键选择:避免数据倾斜(如按时间分片导致冷热不均)
- 全局ID生成:使用Snowflake或UUID避免主键冲突
- 跨分片事务:通过Saga模式或二阶段提交(2PC)保证一致性
- 中间件支持:借助ShardingSphere、Vitess等工具简化路由逻辑
核心原则:先读写分离,再垂直拆分,最后水平拆分——层层递进解决数据库扩展问题
每阶段需结合业务特点选择拆分维度,重点关注数据分布均衡性与查询路由效率
水平分表作为终极方案,需依托中间件与严谨的分片逻辑设计,确保系统的线性扩展能力
139

被折叠的 条评论
为什么被折叠?



