拓冰建站拓冰建站
首页 / 资讯中心 / 正文

分库分表后索引设计:主键、二级索引与全局唯一性

1. 分库分表之后索引问题为什么突然变复杂了先交代一下背景。我最早接触分库分表是在一个电商订单系统里日均订单量从几十万涨到几百万单库单表在高峰期明显吃不住慢查询一个接一个。当时团队的第一反应很直接既然单表扛不住了那就拆呗。按订单归属的用户ID做水平拆分拆成16个库、每个库64张表。方案评审阶段一切看起来很美好数据量被打散到1024张表里单表数据量直接降了两三个数量级理论上慢查询应该彻底消失。但等真把数据拆完、业务代码改完、开始联调的时候问题才浮出水面所有不按分片键走的查询全都变成了全分片扫描。查询条件是订单号不知道它在哪个分片查询条件是手机号不知道它在哪个分片后台管理端想按商户查交易流水商户ID并不是分片键同样不知道去哪个分片。最夸张的一个接口本来单表毫秒级返回分片之后要并发请求1024张表再在内存里做归并接口直接超时。那时候我才意识到分库分表表面上拆分的是数据实际上拆分的是原来的索引结构。单库单表时代索引天然是全局的主键、唯一键、普通二级索引都在同一份数据文件里随便你怎么查。但数据一旦散到多个物理节点上索引也跟着散了原来“一条SQL搞定”的事情现在要重新设计。这个问题在面试和实际架构评审里被反复问到背后的核心其实就是三个分布式环境下的主键索引怎么设计才能保证全局唯一且不依赖自增二级索引怎么配才能让非分片键查询也能精确路由而不是全表扫描全局唯一索引在跨库场景下到底还能不能做用什么方案做才不牺牲性能和一致性这三个问题想清楚了分库分表的索引设计基本就过关了。这篇文章就围绕这三个点展开结合我自己在订单、支付、用户中心几个系统里的实际落地经验和踩坑记录把主键、二级索引、全局索引的设计思路和取舍逻辑完整梳理一遍。无论你是在做技术选型还是已经被分库分表之后的慢查询搞得焦头烂额这篇应该都能给你一个相对完整的答案。2. 分片键决定物理位置索引决定访问路径先厘清这对关系2.1 分片键的本质是“数据定位符”不是索引很多人对分片键和索引的关系是模糊的。分片键最本质的作用只有一个决定一条数据落在哪个物理分片上。它承担的是路由职责而不是查询加速职责。比如订单表按user_id分片那么任何一条订单记录的物理位置都由user_id哈希或取模后的结果唯一确定。这是数据写入时的固定规则和你在订单表上建了几个索引没有关系。但正因为分片键承担了路由职责它反而成了分布式环境下最重要的一条“索引”。为什么这么说因为只要查询条件里带上了分片键系统就能直接定位到目标分片一次网络往返就能拿到结果。查询条件里没带分片键系统就只能把所有分片都问一遍这就是“全分片扫描”开销放大几十倍甚至上百倍。这就引出了分库分表后的第一个设计原则分片键的选择本质上是在选择大多数业务查询的访问路径。你选user_id做分片键就等于宣布“以用户为中心的查询”是最高频路径你选order_id做分片键就等于宣布“以订单为中心的查询”是最高频路径。没有哪个分片键能覆盖所有查询场景所以分片键选完之后剩下的查询场景必须靠二级索引方案去补救这就是后面几个章节要解决的问题。2.2 索引语义在分片前后的变化从“全局可用”到“分片内可用”单库单表时代索引的语义非常简单索引是全局的无论是主键索引还是二级索引查询优化器可以在全表范围内通过索引快速定位数据。但分库分表之后索引的语义发生了根本变化。我用一个例子来说明。假设订单表在单库里有三个索引主键索引(order_id)、唯一索引(order_no)、普通索引(user_id)。某个查询条件是order_no单库场景下走唯一索引直接命中一行数据非常快。分片之后呢我们把表拆到了多个分片每个分片上的表结构仍然保留了这三个索引但每个分片上的索引只对自己分片内的数据有效。也就是说order_no上的唯一索引在每个分片内是唯一的但跨分片不唯一——分片1里可以有一个order_noABC001的订单分片2里也可以有一个order_noABC001的订单两个分片各自认为自己的数据没问题但全局来看重复了。这就是分布式环境下索引设计最棘手的点你熟悉的那些索引约束在分片之后都变成了“局部约束”不再具备全局语义。主键索引不再能保证全局唯一唯一索引不再能保证全局唯一普通二级索引如果不包含分片键连“快速定位数据”这个基本功能都实现不了只能退化成全分片扫描的辅助工具。把这两层关系理清楚之后后面所有方案就都围绕一个核心问题展开如何在分片的物理现实之上重建全局的索引语义。下文逐一拆解。3. 主键索引设计分布式全局ID的选型和实现细节3.1 为什么数据库自增主键在分库分表后不能用了分库分表之后第一个需要推翻的就是数据库自增主键。单库最简单的id BIGINT AUTO_INCREMENT在单库里有一个全局的计数器保证不重复但拆成多个分片之后每个分片的自增计数器是独立的分片1生成id1分片2同样生成id1主键冲突就这样产生了而且这种冲突是必然的不是概率问题。有人会说那我给每个分片设置不同的自增起始值不就行了比如分片1从1开始、每次加16分片2从2开始、每次加16。这种方式确实能让每个分片生成的ID互不重复但存在几个现实问题。第一个问题是扩容麻烦刚开始分了16个分片步长定的是16后面数据量涨了要扩到32个分片老的步长配置和新分片的分配规则就对不上了要么做数据迁移要么重新规划发号规则代价很大。第二个问题是ID有规律可循别人看到一个订单号就能推算出一段时间内的订单量这在电商里属于商业数据泄露。第三个问题是这种方案没办法保证ID的全局单调趋势如果业务方对ID有排序需求这种带分片前缀的递增ID在不同的分片区间里是乱序的。所以在分布式环境下主键设计的第一原则是不能让数据库自己生成ID必须由发号器统一生成全局唯一ID。而发号器选型的核心指标就三个全局唯一、趋势递增、高可用低延迟。3.2 主流的分布式ID方案对比UUID、雪花、号段、Redis先看UUID。UUID在分布式环境下保证全局唯一是没问题的生成也不依赖任何中心节点但它有致命弱点长度过长36个字符作为主键存储在MySQL的InnoDB引擎下会导致主键索引变得又大又慢完全随机没有时间顺序插入时B树节点会频繁分裂随机IO严重写入性能直线下降。我的建议很明确UUID可以用于一些不需要索引的内部标识字段但绝对不要拿来当分布式环境下的主键。再看雪花算法Snowflake。雪花算法生成的ID是一个64位的long型整数结构上分为四段1位符号位固定为0、41位时间戳毫秒级当前时间减去自定义纪元、10位机器ID可以拆成机房ID机器ID、12位序列号。这个设计精妙的地方在于同一毫秒内同一台机器最多能生成2^124096个ID超过4096就等到下一毫秒不同机器之间用机器ID做区分。算一下总量41位时间戳大约能用69年10位机器ID支持1024台机器每台机器每毫秒4096个ID整体上足够绝大多数业务使用。雪花算法是分布式下主键的“默认方案”但它有几个实现层面的坑一是时钟回拨问题如果服务器的系统时间往回跳了可能会导致生成的ID比之前小甚至和之前的ID冲突业界常见的处理方式是记录上次生成ID的时间戳发现时间回拨就拒绝生成或等待追平二是机器ID的分配管理上线初期可以用配置文件硬编码节点多了之后最好有统一的注册和管理方式避免机器ID配重了出问题。我在实际项目中是把雪花算法的ID生成做成了独立的基础服务由专门的发号服务统一提供业务方通过RPC或本地缓存批量获取ID段这样既避免了每个业务系统各自实现雪花算法的重复劳动也方便统一做时钟回拨的保护处理。号段模式Segment也是一种很实用的方案。它的思路是数据库里维护一张发号表记录当前最大的ID值和步长发号服务每次从DB申请一段ID比如1000个缓存到本地内存业务需要ID时直接从本地内存发出去内存用完了再向DB申请下一段。这种模式的好处是DB的压力很小每秒只需要访问几次数据库性能上限很高而且ID是趋势递增的对数据库索引友好。缺点是如果发号服务重启会丢失一部分未发完的ID段但ID已经发放过的部分不会重复整体不影响业务正确性。对ID没有“严格单调递增”要求、只需要“趋势递增且全局唯一”的场景号段模式是最容易落地、运维成本最低的方案之一。另外Redis的INCR命令也可以生成全局递增ID但在持久化策略和集群故障切换时需要额外注意ID重复或跳号的问题适合对ID要求不苛刻的内部场景。3.3 业务主键和代理主键一张表该用哪个做主键索引最后一个值得展开的点是业务主键和代理主键的分工。很多团队在分库分表后容易陷入一个误区就是用一个自然业务字段比如订单号、手机号、身份证号直接做主键。这里隐藏的问题很多业务字段长度通常不可控手机号是11位数字还好订单号带了各种业务前缀就很容易超长更关键的是业务字段可能有变更需求比如手机号可以注销再注册而主键一旦变更所有引用它的表和外键关系都要跟着动代价非常大。所以我的建议非常明确每一张分片表都应该有一个独立的代理主键surrogate key也就是我们上面用分布式ID生成的全局唯一ID用它来作为主键索引业务上的唯一标识订单号、业务流水号作为业务唯一键单独建立索引去约束。这样做有三个好处主键索引保持短小精悍InnoDB的聚簇索引性能最优业务字段的调整不影响物理表结构全局唯一ID天然适合作为跨分片的数据关联依据后面做数据合并、异步同步、消息对账时都能用上。这里顺带说一下主键索引本身的B树行为。InnoDB是聚簇索引表主键的顺序直接决定了数据在磁盘上的物理排列使用趋势递增的雪花ID或号段ID作为主键新插入的数据总是追加到B树尾部写入性能非常稳定反之如果用随机UUID作为主键每次插入都可能在B树的中间某处引发节点分裂和页面移位在写入密集的分布式场景下性能衰减非常明显。这算是“顺序写”和“随机写”之争在索引设计里的一个直接体现。4. 二级索引设计三种落地方案关键看你怎么查4.1 为什么非分片键查询会全表扫描主键问题解决之后紧接着就是二级索引。这里说的二级索引指的是主键之外的普通索引和唯一索引以及业务上最常见的“按非分片键查询”的需求。比如订单表按user_id分片但你经常要通过order_no查订单详情order_no不是分片键数据库不知道这条订单在哪个分片只能把查询请求广播到所有分片每个分片在自己的索引里查一遍最后汇总结果返回。我之前在一个支付系统里就踩过这个坑。交易流水表按用户ID分片但风控部门和客服后台经常按商户订单号查流水一开始没做二级索引方案查询直接走全分片扫描。当时分了64个分片每次查询要并发打到64个库加上超时重试最坏情况一个接口要等好几秒。后来专门给这类查询做了改造才把P99耗时降回50毫秒以内。要解决这类问题核心思路是“把非分片键查询转化为分片键查询”或者“提前建立非分片键到分片键的映射关系”。这一步做得好不好直接决定系统的查询能力上限。下面要讲的三种方案就是这个思路的具体落地。4.2 方案一索引冗余法适合读多写少索引冗余法的思路是把非分片键的所有字段复制一份到根据该字段分片的“冗余表”里查询时先查冗余表再反查主表。举个例子订单主表按user_id分片同时建立一张订单冗余表按order_no分片冗余表里只存必要的字段比如order_no、user_id、order_amount、create_time。按order_no查询时先去冗余表查到user_id再用user_id去主表查完整数据。这个方案的优点是查询路径最短两次精确路由没有全分片扫描性能非常可控缺点也很明显——写入时要把数据同步写到多张表写放大且存在数据一致性问题。如果主表和冗余表在同一个库同一个事务里可以用本地事务解决但分库分表场景下通常是不同分片甚至不同库本地事务管不了跨库的写入这时候就需要引入分布式事务或异步对账机制复杂度会显著上升。所以我的结论是索引冗余法比较适合读多写少、数据一致性要求不那么苛刻能接受秒级延迟的场景比如订单查询、交易明细查询这种“后台列表页”类型的业务。项目初期可以直接用数据量大了之后再考虑更复杂的方案。而且冗余表本质上和后面要讲的“索引表法”有相似之处区别只是冗余表可以多存一些字段索引表通常只存映射关系。4.3 方案二基因法适合需要精确路由又不牺牲写入性能基因法是我个人最喜欢的方案它非常巧妙地解决了非分片键无法路由的问题同时又不需要额外的同步机制。核心思路是在生成业务ID的时候把分片键的某些位“嵌入”到这个业务ID里这样以后拿到业务ID就能直接从中提取出分片键信息从而完成分片路由。举个例子还是订单表按user_id分片。假设user_id100123456分片规则是user_id对64取模那我可以取user_id最后6位或者最后N位具体位数取决于总分片数能覆盖的模数范围作为“分片基因”当生成order_no时把这段基因拼接进order_no的固定位置。以后按order_no查询时先解析order_no里的基因位就能直接算出它在哪个分片一步到位不需要全分片扫描也不需要查映射表。用二进制算起来更直观。如果总分片数是64那么分片基因需要log2(64)6位二进制。把user_id的二进制表示的最后6位提取出来嵌入到order_no中。查询order_no时解析出那6位就是分片号。如果总分片数是1024基因位就需要10位二进制用户ID能覆盖的基因空间足够基本不用担心碰撞。在实际实现中我用过两种方式。第一种是拼接法order_no 时间戳部分 分片基因 序列号部分简单粗暴第二种是按位运算嵌入比如order_no占64位其中6位固定用来放分片基因其他位放其他信息这种更省空间但代码里要做移位运算可读性稍差。无论哪种方式关键是分片基因的位数必须能覆盖总的分片数。如果总共有128个分片但你只嵌了4位基因那么最多只能区分16个分片剩下的分片会被路由到错误的位置。基因法最核心的优点是写入主表时不需要同步任何冗余数据因为订单ID本身就包含了路由信息按order_no的查询天然能精确路由。缺点同样明显基因位会占用业务ID的一部分空间如果分片键本身很短比如某些业务用户ID只有个位数提取基因可能不够用另外基因法的路由规则和分片规则强绑定一旦分片规则改了比如从64分片扩到128分片基因位数要相应增加历史数据的ID是重新生成的还是兼容旧规则都要提前规划。4.4 方案三映射表法适合多条件关联查询映射表法本质上是把“非分片键到分片键”的映射关系单独建一张全局表来维护。比如订单表按user_id分片但业务经常按phone查订单。可以建一张phone_user索引表以phone为分片键存储phone和user_id的映射关系。按phone查订单时先在这张索引表里查到对应的user_id再用user_id去订单主表分区查。这个方案比索引冗余法更轻量因为索引表里通常只存映射关系的两三个字段数据量小很多但它有一个绕不开的问题写入时如何保证映射表和主表的数据一致性。因为两张表可能在不同的分片或不同的库里不可能用同一个本地事务原子地插入。常见的做法是先插入主表再插入映射表如果映射表写入失败就需要用消息队列做异步补偿或者在查询的时候做兜底校验。这背后其实就是分布式事务和最终一致性的问题和热词里的“分布式事务一致性”“分布式事务解决方案”直接相关。我实际落地映射表法时还踩过一个坑直接用了同步双写结果高峰期消息队列消费积压映射表数据暂时缺失部分查询暂时查不到数据客服投诉增多。后来改成本地消息表加定时任务补偿的方式才算稳定下来。我的经验是映射表方案不等于简单的双写你必须提前想清楚数据补偿机制和查询兜底策略不然最终一致性会变成最终不一致。那么在三个方案里怎么选我的建议是这样的如果查询条件相对固定、能接受写入时多写一份数据选冗余法如果查询条件主要是通过业务ID订单号、流水号反查主表选基因法它的性能最好、实现也最干净如果查询条件非常灵活、涉及多个非分片键字段的关联组合选映射表法把每个查询维度都做成一张映射表虽然写放大更严重但查询路径清晰可控。这里给出一个直观的场景对照表方案查询精确路由写入放大一致性要求典型场景索引冗余法是高可接受秒级延迟后台列表、订单查询基因法是无无需额外同步业务ID反查主表映射表法是中需要补偿机制多字段关联查询5. 全局唯一索引性能、一致性和成本的取舍5.1 全局唯一索引为什么“做不了”标题里提到的“全局索引”是分库分表场景下一个特别容易被高估的概念。很多人理解的是我能不能在分库分表之后依然在某个非分片键字段上建立一个全局唯一的索引让数据库帮我保证这个字段在全局范围内不重复答案很直接传统关系型数据库的索引是单机数据结构在跨分片的物理架构下没有一个内置的全局索引机制。你可以在每个分片上建唯一索引但那个唯一索引只约束当前分片内的数据管不到其他分片。想要全局唯一必须自己在上层做校验而跨分片做唯一性校验本质上是把“一次单点判断”变成“分布式一致性协议”复杂度很高性能损耗也很大。更关键的是全局唯一校验和分布式系统的可用性天然冲突。要保证严格全局唯一最简单粗暴的办法是引入一把全局锁比如之前热词提到的Redis分布式锁让所有写入请求先抢锁抢到锁之后再判断全局是否有重复然后写入最后释放锁。这个方案在数据量小的时候还行但订单表每秒几千上万次写入的时候分布式锁的并发瓶颈立刻就会成为系统的天花板而且锁本身的过期时间、误删、重入等问题都会带来额外的风险。所以我相信一句话“分布式环境下没有免费的全局唯一索引所有看起来像的其实都是一致性协议的变种。”5.2 实际工程里的四种妥协方案既然严格意义的技术方案不可行就得在性能和一致性之间做取舍。下面是我在实践中验证过可用的四种妥协方案。第一种业务前置校验加同步兜底。在写入前先去一个中心化的存储比如Redis或者另一张去重表里检查这个唯一键是否已存在存在就拒绝不存在就放行。这个方案要求校验和写入必须在同一个事务或非常接近的时序内完成否则并发场景下依然可能重复。为了降低风险可以在数据库层面再加一层约束分片内的唯一索引两边同时兜底把重复概率降到极低。它的性能上限取决于中心化的存储QPS但实现成本低是很多系统的默认选择。第二种异步校验加最终一致性。不管控写入路径上的实时校验而是先写入再由异步任务对账发现重复后由业务系统做补偿处理比如拒绝后单、发告警、人工介入。这个方案牺牲了实时唯一性换来了很高的写入性能适合唯一性要求不那么刚性的场景比如一些非核心的运营类数据。但如果是支付流水、用户手机号这类强唯一字段我不建议用方案二兜底因为重复写入的错误一旦发生后续修复成本和业务影响都很大。第三种把唯一性字段也变成分片键。如果某个字段在全局必须唯一而且这个字段本身就是一个非常高优级的查询入口不妨认真考虑直接把它作为分片键。比如用户中心里手机号是天然唯一且高频查询的字段就可以用手机号分片在手机号分片上建唯一索引全局唯一就有了物理保证。这个方案不是做不到全局唯一而是通过调整分片维度让“唯一键”和“分片键”重合从而在分片内就能完成约束。缺点是需要为多个唯一字段设计多个分片副本或者接受主分片键之外的查询都要走前面的二级索引方案。第四种放弃数据库唯一索引在服务层做幂等控制。从业务角度想我们要的往往不是“数据库里不能有重复记录”而是“同一业务请求不能被处理两次”。如果真的把关注点放在幂等上就可以通过唯一的幂等ID加Redis或数据库锁来实现请求处理前先判断这个幂等ID是否已经存在存在就返回第一次的处理结果不存在就处理并写入幂等记录。这种方案既解决了业务层面的重复问题又不依赖数据库的全局唯一索引扛住了更高的并发。到这里“全局唯一索引”的实现路径已经比较清晰了如果你的场景对强实时一致性和强唯一性要求都非常高那就考虑把该字段从“非分片键”变成“分片键”或者引入分布式事务来保证跨分片校验如果允许最终一致就用异步校验加补偿如果只是防重复请求用幂等控制就足够了。没有银弹只有取舍。6. 索引落到查询链路路由、分页与聚合的几个坑6.1 精确查询好办范围查询和分页才是真痛点说完索引本身的设计还得把镜头拉到完整查询链路上。分库分表之后即使你做好了二级索引映射能让单个查询精确路由到目标分片一旦查询语义是范围查询、排序、分页、关联聚合索引的作用就会大打折扣。先说范围查询。比如按订单创建时间查某段时间的订单如果时间字段不是分片键即使你在时间字段上建了二级索引数据库也不知道该去哪个分片查最终还是全分片扫描然后对每个分片的查询结果按时间排序合并。这个过程中二级索引在每个分片内是有用的可以减少每个分片的扫描量但跨分片的归并排序仍然在应用层完成时间复杂度是O(NlogN)N是所有分片满足条件的数据总量。数据量大了之后仅内存开销就很可观。分页更是如此。单表分页limit 0,10和limit 10000,10性能上的差距其实没有那么夸张索引存在的前提下但分库分表之后limit 10000,10意味着每个分片都要查出前10010条数据然后在内存里做全局排序取前10条这不仅是慢的问题更可怕的是随着offset增大每个分片要扫描的数据量线性增长整体查询耗时无限逼近不可用状态。我处理过的方案主要有两种。一种是“禁止深分页”也就是说只允许翻前N页或者改成“下一页”形式的时间游标把limit offset替换成where create_time ? order by create_time desc limit 10利用上一页最后一条记录的时间做游标这样每一页的查询都能借助索引精确定位不会出现offset膨胀。另一种是“全局翻页预计算”在离线或异步任务里把全局排序结果提前算好缓存到Redis或ES里线上只查缓存。不管哪种方案都意味着产品交互设计需要妥协技术上的索引优化解决不了深分页的算法复杂度问题必须从产品层面规避。6.2 跨分片聚合count、sum、去重的正确姿势聚合查询同样会被分库分表放大。单表的count(*)走主键覆盖就是一次索引扫描分片之后要把所有分片的count结果加起来查询时间和分片数量成正比。sum、avg同理。distinct这种去重聚合更麻烦因为同一个值可能出现在多个分片里必须在应用层维护一个全局的HashSet去重后再统计。线上做这个事要分两个场景看。如果是对实时性要求不高的运营看板类统计完全可以走异步ETL把数据同步到数仓或分析型数据库里做OLAP分片表根本不参与统计查询如果确实需要在线展示实时计数那就要做预聚合每次写入时同步更新一个计数器记录查询直接读计数器的值。我的经验是不要指望分库分表之后还能用SQL做复杂的在线聚合统计那是分析型数据库的活这种架构本身就是一种索引缺失的现实妥协。但如果你业务形态需要ES就是一个非常适合做跨分片聚合的组件可以把它作为分片数据库之上的聚合查询层把索引和数据检索的需求交给它把事务性写入和强一致读取留给分片数据库。6.3 一次故障复盘分页参数导致的跨分片查询风暴这部分值得单独拿出来说因为它的教训太典型了。当时我们有一个商户交易流水的后台查询页面按商户号查流水。分词键是用户ID商户号不是分片键我们按商户号建立了一张映射表。看起来一切正常。但某个版本为了支持“跳转到指定页”的功能前端把页码参数从10页改成了可输入300页。正常情况下300页并不是一个很大的页码但在分库分表机构里limit 9000,20意味着每个分片都把前9020条记录全部捞出来再做20条的全局切片。那一整天后台系统连续报警数据库连接池被打满。我排查后确认了问题不是某一台数据库CPU跑满而是每个分片都要深度扫描并回传大量数据网络IO和内存占用同步飙升。而且因为请求是并发打到几十个分片上的只要其中一个分片的响应稍微慢一点整个请求就要等最慢的那个分片用户体验就是从“响应很快”直接变成“超时转圈”。这个案例反映的本质问题是在分库分表环境里分页和排序的成本不是线性的而是乘法级的。你每翻一页背后都是全分片数据的一次集中归并。技术上可以通过加索引覆盖条件、限制最大分页深度、加缓存等方式缓解但根治方案还是前面说的游标分页或者禁止深分页。这也是我在后面所有系统设计里的一条硬性规范所有面向用户的分页接口一律禁止深分页所有按非分片键查询的接口都必须先走映射表或索引表精确路由。这两条规范直接避免了一大批潜在的查询故障。7. 设计实践总结一次分库分表索引方案的完整决策路径上面把主键、二级索引、全局索引三个主题都讲完了最后结合一个完整的业务场景把整个设计决策路径串起来方便你直接参考。假设你现在要做的是一个电商订单系统的分库分表改造订单量增长快需要水平拆分。我建议按下面的顺序走。第一步选分片键。先分析最核心的高频查询路径。如果订单查询绝大多数是用户进来查自己的订单就选user_id做分片键。如果订单中心经常要按订单号直接查单比如支付回调、物流查询也可以考虑order_id做分片键。没有完美的分片键关键是符合你80%以上的查询场景。第二步设计主键。用分布式ID生成器生成全局唯一、趋势递增的代理主键order_id。推荐雪花算法或号段模式具体选型看团队对时钟回拨的容忍度和运维成本。订单号业务标识比如业务上的order_no作为独立的业务唯一列单独建索引不要和主键混用。第三步处理非分片键查询。优先使用基因法在order_id生成时嵌入user_id的分片基因这样任何持有order_id的查询都能精确路由。如果还有按手机号的查询建一张phone_user映射表。如果还要按商户号查询交易流水建一张merchant_order映射表。每张映射表的建立都要同步设计写入补偿机制推荐本地消息表加定时对账。第四步全局唯一约束。凡是需要全局唯一的业务字段先看能不能把它变成分片键或映射表的主键不能的话用Redis预校验加数据库分片内唯一索引兜底如果允许最终一致用异步对账。不要把全局唯一约束完全寄托在数据库上。第五步定义查询规范。分页一律用游标分页禁止深分页所有非分片键查询必须走映射表和索引表所有聚合统计查询走异步OLAP或ES。把这些规范沉淀到代码评审清单里避免后面有人绕过架构设计直接写SQL。最后再啰嗦一句索引设计没有标准答案。不同业务、不同数据量、不同的一致性要求推导出的方案可能完全不同但只要把握住“分片键决定物理位置索引决定访问路径”这条主线沿着“先把80%的查询路由到正确的分片再用映射表或基因法覆盖剩余场景最后用幂等和补偿机制兜底”的思路去设计基本不会出大方向的问题。我在多个系统里验证过这套路径整体是稳定可靠的希望能帮你少走一些弯路。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门