分库分表后如何设计索引?全局索引、二级索引

在分布式数据库架构下,主键设计需选择全局唯一的键,如有序UUID,避免自增主键,以确保线性可扩展性。索引设计应考虑查询效率,避免全表扫描,可以通过保存分片信息在索引中或使用全局唯一索引。对于非分片键的唯一索引,也需采用全局唯一设计。全局表可在每个分片中冗余存储,便于查询。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

b81105a749b550224d398388db7f412e.jpeg

程序员的成长之路

互联网/程序员/技术/资料共享 

关注

阅读本文大概需要 6 分钟。

来自:网络,侵删

分布式数据库架构下,索引的设计也需要做调整,否则无法充分发挥分布式架构线性可扩展的优势。今天我们就来聊聊 “在分布式数据库架构下,如何正确的设计索引?”

主键选择

对主键来说,要保证在所有分片中都唯一,它本质上就是一个全局唯一的索引。如果用大部分同学喜欢的自增作为主键,就会发现存在很大的问题。

因为自增并不能在插入前就获得值,而是要通过填 NULL 值,然后再通过函数 last_insert_id()获得自增的值。所以,如果在每个分片上通过自增去实现主键,可能会出现同样的自增值存在于不同的分片上。

比如,对于电商的订单表 orders,其表结构如下(分片键是o_custkey,表的主键是o_orderkey):

CREATE TABLE `orders` (
  `O_ORDERKEY` int NOT NULL auto_increment,
  `O_CUSTKEY` int NOT NULL,
  `O_ORDERSTATUS` char(1) NOT NULL,
  `O_TOTALPRICE` decimal(15,2) NOT NULL,
  `O_ORDERDATE` date NOT NULL,
  `O_ORDERPRIORITY` char(15) NOT NULL,
  `O_CLERK` char(15) NOT NULL,
  `O_SHIPPRIORITY` int NOT NULL,
  `O_COMMENT` varchar(79) NOT NULL,
  PRIMARY KEY (`O_ORDERKEY`),
  KEY (`O_CUSTKEY`)
  ......
) ENGINE=InnoDB

如果把 o_orderkey 设计成上图所示的自增,那么很可能 o_orderkey 同为 1 的记录在不同的分片出现,如下图所示:

cedeaf4947d86c26d9f59daefd597048.png

所以,在分布式数据库架构下,尽量不要用自增作为表的主键:自增性能很差、安全性不高、不适用于分布式架构。

讲到这儿,我们已经说明白了“自增主键”的所有问题,那么该如何设计主键呢?依然还是用全局唯一的键作为主键,比如 MySQL 自动生成的有序 UUID;业务生成的全局唯一键(比如发号器);或者是开源的 UUID 生成算法,比如雪花算法(但是存在时间回溯的问题)。

总之,用有序的全局唯一替代自增,是这个时代数据库主键的主流设计标准,如果你还停留在用自增做主键,或许代表你已经落后于时代发展了。

索引设计

通过分片键可以把 SQL 查询路由到指定的分片,但是在现实的生产环境中,业务还要通过其他的索引访问表。

还是以前面的表 orders 为例,如果业务还要根据 o_orderkey 字段进行查询,比如查询订单 ID 为 1 的订单详情:

SELECT * FROM orders WHERE o_orderkey = 1

我们可以看到,由于分片规则不是分片键,所以需要查询 4 个分片才能得到最终的结果,如果下面有 1000 个分片,那么就需要执行 1000 次这样的 SQL,这时性能就比较差了。

但是,我们知道 o_orderkey 是主键,应该只有一条返回记录,也就是说,o_orderkey 只存在于一个分片中。这时,可以有以下两种设计:

  • 同一份数据,表 orders 根据 o_orderkey 为分片键,再做一个分库分表的实现;

  • 在索引中额外添加分片键的信息。

这两种设计的本质都是通过冗余实现空间换时间的效果,否则就需要扫描所有的分片,当分片数据非常多,效率就会变得极差。

而第一种做法通过对表进行冗余,对于 o_orderkey 的查询,只需要在 o_orderkey = 1 的分片中直接查询就行,效率最高,但是设计的缺点又在于冗余数据量太大。

所以,改进的做法之一是实现一个索引表,表中只包含 o_orderkey 和分片键 o_custkey,如:

CREATE TABLE idx_orderkey_custkey (
  o_orderkey INT
  o_custkey INT,
  PRIMARY KEY (o_orderkey)
)

如果这张索引表很大,也可以将其分库分表,但是它的分片键是 o_orderkey,如果这时再根据字段 o_orderkey 进行查询,可以进行类似二级索引的回表实现:先通过查询索引表得到记录 o_orderkey = 1 对应的分片键 o_custkey 的值,接着再根据 o_custkey 进行查询,最终定位到想要的数据,如:

SELECT * FROM orders WHERE o_orderkey = 1

=>

# step 1
SELECT o_custkey FROM idx_orderkey_custkey 
WHERE o_orderkey = 1

# step 2
SELECT * FROM orders 
WHERE o_custkey = ? AND o_orderkey = 1

这个例子是将一条 SQL 语句拆分成 2 条 SQL 语句,但是拆分后的 2 条 SQL 都可以通过分片键进行查询,这样能保证只需要在单个分片中完成查询操作。不论有多少个分片,也只需要查询 2个分片的信息,这样 SQL 的查询性能可以得到极大的提升。

通过索引表的方式,虽然存储上较冗余全表容量小了很多,但是要根据另一个分片键进行数据的存储,依然显得不够优雅。

因此,最优的设计,不是创建一个索引表,而是将分片键的信息保存在想要查询的列中,这样通过查询的列就能直接知道所在的分片信息。

如果我们将订单表 orders 的主键设计为一个字符串,这个字符串中最后一部分包含分片键的信息,如:

o_orderkey = string(o_orderkey + o_custkey)

那么这时如果根据 o_orderkey 进行查询:

SELECT * FROM Orders
WHERE o_orderkey = '1000-1';

由于字段 o_orderkey 的设计中直接包含了分片键信息,所以我们可以直接知道这个订单在分片1 中,直接查询分片 1 就行。

同样地,在插入时,由于可以知道插入时 o_custkey 对应的值,所以只要在业务层做一次字符的拼接,然后再插入数据库就行了。

这样的实现方式较冗余表和索引表的设计来说,效率更高,查询可以提前知道数据对应的分片信息,只需 1 次查询就能获取想要的结果。

这样实现的缺点是,主键值会变大一些,存储也会相应变大。但只要主键值是有序的,插入的性能就不会变差。而通过在主键值中保存分片信息,却可以大大提升后续的查询效率,这样空间换时间的设计,总体上看是非常值得的。

当然,这里我们谈的设计都是针对于唯一索引的设计,如果是非唯一的二级索引查询,那么非常可惜,依然需要扫描所有的分片才能得到最终的结果,如:

SELECT * FROM Orders
WHERE o_orderate >= ? o_orderdate < ?

因此,再次提醒你,分布式数据库架构设计的要求是业务的绝大部分请求能够根据分片键定位到 1 个分片上。

如果业务大部分请求都需要扫描所有分片信息才能获得最终结果,那么就不适合进行分布式架构的改造或设计。

最后,我们再来回顾下淘宝用户订单表的设计:

9d5644b8baa9525d0fc181563d5fb4a7.png

上图是我的淘宝订单信息,可以看到,订单号的最后 6 位都是 308113,所以可以大概率推测出:

  • 淘宝订单表的分片键是用户 ID;

  • 淘宝订单表,订单表的主键包含用户 ID,也就是分片信息。这样通过订单号进行查询,可以获得分片信息,从而查询 1 个分片就能得到最终的结果。

全局表

在分布式数据库中,有时会有一些无法提供分片键的表,但这些表又非常小,一般用于保存一些全局信息,平时更新也较少,绝大多数场景仅用于查询操作。

例如 tpch 库中的表 nation,用于存储国家信息,但是在我们前面的 SQL 关联查询中,又经常会使用到这张表,对于这种全局表,可以在每个分片中存储,这样就不用跨分片地进行查询了。如下面的设计:

40c42670744be7499aac8ce54af11cc9.png

唯一索引

最后我们来谈谈唯一索引的设计,与主键一样,如果只是通过数据库表本身唯一约束创建的索引,则无法保证在所有分片中都是唯一的。

所以,在分布式数据库中,唯一索引一样要通过类似主键的 UUID 的机制实现,用全局唯一去替代局部唯一,但实际上,即便是单机的 MySQL 数据库架构,我们也推荐使用全局唯一的设计。因为你不知道,什么时候,你的业务就会升级到全局唯一的要求了。

总结

今天介绍了非常重要的分布式数据库索引设计,内容非常干货,是分布式架构设计的重中之重,建议反复阅读,抓住本文的重点,总结来说:

  • 分布式数据库主键设计使用有序 UUID,全局唯一;

  • 分布式数据库唯一索引设计使用 UUID 的全局唯一设计,避免局部索引导致的唯一问题;

  • 分布式数据库唯一索引若不是分片键,则可以在设计时保存分片信息,这样查询直接路由到一个分片即可;

  • 对于分布式数据库中的全局表,可以采用冗余机制,在每个分片上进行保存。这样能避免查询时跨分片的查询。

<END>

推荐阅读:

微信真能查到别人的聊天记录?

大公司为什么禁止在SpringBoot项目中使用@Autowired注解?

互联网初中高级大厂面试题(9个G)
内容包含Java基础、JavaWeb、MySQL性能优化、JVM、锁、百万并发、消息队列、高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper......等技术栈!
⬇戳阅读原文领取!                                  朕已阅
<think>好的,用户现在问的是“为什么要做分库分表?怎么做?”,之前他们已经问过分布式ID生成方式,现在这个问题可能是在进一步探讨分布式系统中的数据存储问题。我需要先明确用户的需求,他们可能是在设计一个高并发的系统,遇到了数据库性能瓶颈,或者想要提前了解分库分表的知识来规划架构。 首先,回答“为什么要做分库分表”时,应该从性能瓶颈、数据量过大、高并发访问、可用性等方面入手。用户可能已经遇到了单库单表的性能问题,或者预期数据增长很快,需要提前规划。这里需要强调单库单表的限制,比如连接数不够、磁盘IO压力大、查询速度变慢等。 然后是“怎么做”,这部分需要结构清晰,分步骤讲解。可能需要包括分库分表的方式(垂直和水平拆分)、分片策略(范围、哈希、一致性哈希等)、中间件选择(如ShardingSphere、MyCat)、数据迁移和扩容问题,以及带来的挑战比如分布式事务、跨库查询等。 用户可能还关心实际操作中的注意事项,比如如何选择分片键,如何避免热点问题,扩容时如何平滑迁移数据。此外,他们可能想知道有没有什么工具或框架可以简化分库分表的工作,或者是否有成功案例可以参考。 需要注意的是,用户之前的问题是关于分布式ID生成的,所以分库分表后ID生成可能会更复杂,可能需要结合之前提到的Snowflake或号段模式来确保ID的唯一性,这点可以适当联系起来,帮助用户形成完整的知识体系。 另外,用户可能对分库分表的优缺点不太清楚,需要指出虽然分库分表能解决扩展性问题,但也会带来复杂性,比如跨库join困难,事务管理复杂,维护成本增加等。需要权衡利弊,是否真的有必要进行分库分表,或者是否有其他优化手段如读写分离、缓存等可以先尝试。 最后,可能需要给出一些实际的应用场景,比如电商平台的订单表分库分表,用户量大的社交平台分库分表,帮助用户更好地理解何时以及如何实施分库分表。</think>以下是关于分库分表的详细介绍,从核心原因到实施方法,结合场景和案例进行说明: --- ### **一、为什么要做分库分表?** #### **1. 解决单机数据库的性能瓶颈** - **存储瓶颈**:单机磁盘容量有限(如MySQL单表数据量超过5000万行,性能明显下降)。 - **计算瓶颈**:高并发查询导致CPU过载(如电商大促时每秒数十万请求)。 - **连接数瓶颈**:单机数据库最大连接数有限(如MySQL默认151连接,超出后拒绝服务)。 #### **2. 提升系统扩展性** - 单机数据库无法通过无限升级硬件来应对数据量增长。 - 分库分表允许通过增加节点横向扩展,支持业务持续增长。 #### **3. 提高可用性与容灾能力** - 单机故障会导致全库不可用,分库分表后故障影响范围缩小(如仅部分分片受影响)。 - 天然支持多机房部署,实现异地容灾。 #### **4. 优化数据管理效率** - 按业务拆分后,不同库表可独立维护(如订单库和用户库分开备份、升级)。 --- ### **二、分库分表的核心策略** #### **1. 垂直拆分** - **分库**:按业务模块拆分(如订单库、用户库、商品库)。 **场景**:不同业务团队独立管理数据库,减少跨库事务。 - **分表**:将宽表拆分为多个字段较少的表(如订单表拆分为订单基础表 + 订单详情表)。 **场景**:减少单行数据大小,提升查询效率。 #### **2. 水平拆分** - **分库分表**:将同一表的数据按规则分散到多个库/表中。 **分片键选择**: - **用户ID**:适合用户维度查询(如用户订单)。 - **订单ID**:适合全局唯一ID查询(如订单详情页)。 - **时间**:适合按时间范围查询(如日志表按月分片)。 --- ### **三、分库分表的具体实现方法** #### **1. 分片策略** | **策略** | **原理** | **优缺点** | **场景** | |----------------|-----------------------------------------|----------------------------------------------|----------------------------| | **范围分片** | 按分片键范围划分(如1~100万在DB1,100万+在DB2) | 易扩容,但数据分布不均(可能热点集中在最新分片) | 时间序列数据(日志、订单按年分片) | | **哈希取模** | 对分片键哈希取模(如 `user_id % 4`) | 数据分布均匀,但扩容需迁移大量数据 | 用户ID、订单ID等离散值分片 | | **一致性哈希** | 哈希环动态分配节点,减少扩容时数据迁移量 | 扩容灵活,但实现复杂 | 动态调整分片数量的场景 | #### **2. 分库分表中间件** - **客户端SDK**: - **ShardingSphere**(Apache开源):通过JDBC驱动或Proxy代理实现分片。 - **TDDL**(阿里开源):基于JDBC的轻量级分库分表框架。 - **代理层**: - **MyCat**:通过独立代理服务路由SQL(需维护中间件高可用)。 - **Vitess**(YouTube开源):针对MySQL的集群管理方案,支持Kubernetes部署。 #### **3. 分库分表示例(以订单表为例)** - **分片规则**:按 `order_id` 哈希取模分4个库,每个库分8张表。 - **路由计算**: ```python db_index = (hash(order_id) % 4) # 确定库编号(DB0~DB3) table_index = (hash(order_id) // 4) % 8 # 确定表编号(table0~table7) ``` - **真实表名**:`order_db_{db_index}.order_table_{table_index}` --- ### **四、分库分表的挑战与解决方案** #### **1. 分布式事务** - **问题**:跨库操作无法保证ACID(如扣库存和生成订单需跨库)。 - **解决方案**: - **最终一致性**:通过消息队列异步补偿(如库存服务扣减后发MQ通知订单服务)。 - **Seata**(阿里开源):基于两阶段提交(2PC)的分布式事务框架。 #### **2. 跨分片查询** - **问题**:分页、排序、聚合计算需合并多个分片结果(如查询“用户最近3月订单”)。 - **解决方案**: - **冗余数据**:建立全局二级索引表(如ES维护用户ID与订单ID的映射)。 - **并行查询**:中间件向所有分片广播查询请求,内存中合并结果(性能较差,慎用)。 #### **3. 扩容与数据迁移** - **扩容流程**: 1. 双写旧库和新库,确保新旧数据同步。 2. 迁移历史数据到新库(使用工具如`pt-online-schema-change`)。 3. 切换流量到新库,停用旧库。 - **工具支持**: - **ShardingSphere-Scaling**:自动化分片扩容工具。 - **数据管道**:通过Kafka同步增量数据。 #### **4. 全局唯一ID** - **问题**:分库分表后无法依赖数据库自增ID。 - **解决方案**:使用分布式ID生成方案(如Snowflake、Leaf)。 --- ### **五、分库分表的适用场景** | **场景** | **拆分方式** | **示例** | |---------------------------|---------------------|---------------------------------------| | 单表数据量超过5000万行 | 水平分表 | 用户表按 `user_id` 哈希分64张表 | | 业务模块耦合度高 | 垂直分库 | 电商系统拆分为订单库、商品库、支付库 | | 高并发读写 | 分库分担连接数压力 | 社交App用户动态表按 `user_id` 分16个库 | | 数据冷热分离 | 按时间范围分库分表 | 日志表按月分片,旧数据归档到廉价存储 | --- ### **六、分库分表的最佳实践** 1. **避免过度拆分**: - 单库不超过500张表,单表数据量控制在千万级。 - 拆分前优先优化索引、缓存、SQL性能。 2. **分片键设计原则**: - 选择高频查询字段(如`user_id`)。 - 避免热点问题(如按时间分片导致新库压力大)。 3. **灰度与监控**: - 逐步迁移数据,观察慢查询和连接数变化。 - 监控分片负载均衡(如Prometheus + Grafana)。 --- ### **总结** 分库分表是应对海量数据和高并发的终极手段,但其复杂度极高。**建议优先尝试读写分离、缓存优化、数据库参数调优等方案**,仅在单机无法支撑时再考虑分库分表。拆分后需配套解决分布式ID、事务、查询等问题,可借助成熟中间件降低开发成本。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值