← 返回题目列表

分库分表后如何保证唯一约束?

高频 中等 第 5 / 27 题 更新于 2026/07/28
唯一约束全局唯一分布式ID唯一索引

简化版

分库分表后,单表唯一索引只能保证单个物理表内唯一,不能天然保证全局唯一。常见做法是使用全局 ID、唯一索引表、分片键内唯一、业务号段分配或把唯一字段纳入路由规则,并配合幂等和冲突处理。

详细版

解决方式按场景选择:

  1. 主键唯一:用雪花算法、号段模式、UUID 等全局 ID;
  2. 分片内唯一即可:唯一索引包含分片键,如 (user_id, coupon_id)
  3. 全局业务唯一:建唯一索引表,如手机号、订单号、支付单号;
  4. 预生成唯一号:订单号生成时保证全局唯一并包含路由信息;
  5. 低频管理类数据:可集中到单独配置库或主数据服务。

重点是不要误以为每个分表都有 unique(phone) 就能保证全局手机号唯一。不同分片上可以插入相同手机号,必须通过全局协调或统一路由解决。

完整版教学

一、唯一索引的能力范围变小了

在单表里,数据库唯一索引可以保证某个字段不重复。例如 user.phone 上建唯一索引,两个相同手机号无法同时插入。

但水平分表后,可能存在 user_00user_01user_02。每张表都有自己的唯一索引,它们彼此不知道对方的数据。user_00 里有手机号 A,不影响 user_01 再插入手机号 A。数据库只能保证“本表唯一”,不能保证“全局唯一”。

分库后更明显,不同数据库实例之间没有一个统一的唯一索引结构。因此全局唯一必须重新设计。

二、主键唯一通常交给分布式 ID

如果只是要保证主键 ID 唯一,可以使用雪花算法、号段模式、UUID、数据库号段服务等方案。它们的目标是让每条记录生成时就拿到全局不重复的 ID。

雪花算法通过时间戳、机器号、序列号组合生成趋势递增 ID;号段模式从数据库批量申请一段 ID,本地内存分配;UUID 简单但较长且不利于 B+Tree 局部性。选型要结合有序性、性能、可读性、时钟依赖和运维复杂度。

主键唯一相对好解决,因为 ID 由系统生成。更麻烦的是手机号、用户名、身份证号、订单号这类业务唯一字段。

三、业务唯一可以用唯一索引表

唯一索引表的思路是把需要全局唯一的字段集中登记。例如注册用户时,先向 unique_phone 表插入手机号:

insert into unique_phone(phone, user_id) values (?, ?)

这张表对 phone 建唯一索引。如果插入成功,再创建用户;如果插入失败,说明手机号已存在。它可以是单独的库,也可以按唯一字段本身分片,但同一个唯一值必须稳定路由到同一个位置。

难点是业务表和唯一索引表之间的一致性。索引占用成功但用户创建失败,需要释放;用户创建成功但索引写失败,需要补偿。可以通过本地事务、状态机、幂等重试和定时清理处理中记录来处理。

四、把唯一字段纳入分片规则也很常见

如果用户表按手机号 Hash 分片,那么相同手机号一定落到同一个分片,本地唯一索引就能保证全局唯一。但这要求主要查询也适合按手机号路由。如果系统更多按 user_id 查用户,按手机号分片可能会牺牲主链路效率。

另一种做法是唯一索引包含分片键。例如优惠券领取记录按 user_id 分片,要求同一用户同一券只能领一次,可以建唯一索引 (user_id, coupon_id)。因为相同 user_id 一定在同一分片,这个唯一约束就是有效的。

这说明“全局唯一”不一定总是全站唯一。有些业务只要求某个范围内唯一,比如同一用户、同一商家、同一租户内唯一。范围内唯一优先通过分片键组合解决,成本更低。

五、幂等和唯一约束经常一起出现

在分布式系统里,请求可能重试。创建订单、领取优惠券、支付回调都可能被调用多次。唯一约束常被用作幂等保护:同一个业务请求 ID 只能插入一次。

例如支付回调表可以对 pay_channel + pay_no 建唯一索引。第一次处理成功,第二次重复回调插入失败或查到已有记录,就直接返回成功。分库分表后,这个唯一键必须能路由到同一分片,否则幂等会失效。

六、唯一约束要和业务状态一起设计

全局唯一字段经常不是简单插入一次就结束。以手机号注册为例,可能存在注册中、注册成功、注销、换绑、冻结等状态。如果唯一索引表只存一个手机号是否存在,就很难处理注册失败释放、注销后能否复用、换绑过程中的并发冲突。

更稳的做法是让唯一索引表带状态和业务归属,例如 phone、user_id、status、expire_time、version。注册时先占用手机号,状态为处理中;用户创建成功后改为已绑定;如果注册流程超时失败,通过过期时间或补偿任务释放。换绑时要保证旧手机号释放和新手机号占用的顺序,避免短时间内被其他账号抢占。

唯一约束还要服务幂等。比如同一个请求 ID 重试时,应该识别为同一次业务操作,而不是报唯一冲突。唯一索引表可以保存业务请求号,冲突时先判断是不是同一请求重复提交。

七、唯一性范围要说清楚,否则方案会过度设计

不是所有唯一都要求全站唯一。用户名可能全站唯一,店铺内商品编码可能只要求同一店铺唯一,用户优惠券领取只要求同一用户同一券唯一。范围越小,方案越简单。能用 (tenant_id, code)(user_id, coupon_id) 这类包含分片键的本地唯一索引解决,就不要上全局唯一服务。

对真正全局唯一的字段,要明确写入顺序和失败补偿。同步唯一索引强一致性更好,但会增加主流程延迟;异步索引吞吐更高,但短暂窗口内可能查不到或需要处理中状态。业务越核心,越倾向同步占位加补偿;查询类索引可以接受异步最终一致。

面试追问时可以主动补充:分布式 ID 解决主键唯一,不等于解决手机号、订单号、外部流水号等业务唯一;本地唯一索引只在同一个物理表内有效;唯一字段如果不能稳定路由到同一位置,就必须引入全局协调。

八、常见误区与追问

这道题要紧扣「分库分表唯一约束」本身回答,不能把它混成泛泛的分库分表套话。面试官通常不是只听定义,而是看你能不能把适用场景、关键流程、失败边界和工程取舍串起来,尤其要说明拆分边界、路由规则、扩容迁移、跨库查询和一致性兜底。

回答层次要讲清的内容容易漏掉的边界
核心结论分库分表后单库唯一索引只能保证本分片唯一,全局唯一要靠全局 ID、唯一表、业务键路由或中心化校验不要停在名词解释
流程机制识别唯一字段 -> 判断是否含分片键 -> 选择全局校验方案 -> 先占位再写业务表 -> 失败回滚或释放 -> 对账修复异常占位要说清触发点、状态变化、确认点和失败兜底
工程取舍手机号唯一如果用户按 user_id 分片,直接在各分片建 phone 唯一索引只能保证每个分片不重复,不能保证全局不重复分库分表能扩展容量和吞吐,但会带来路由、事务、Join、唯一约束、扩容和运维复杂度
分库分表唯一约束 面试拆解:
1. 识别唯一字段
2. 判断是否含分片键
3. 选择全局校验方案
4. 先占位再写业务表
5. 失败回滚或释放
6. 对账修复异常占位

记忆钩子:先给结论,再拆流程,再讲数字例子和失败边界;回答「分库分表唯一约束」时要围绕题目问法收束,不要把相邻概念堆成一段没有重点的名词清单。

  • 误区:每个分片建唯一索引就能全局唯一。 唯一索引只在单个物理库表内生效。
  • 误区:全局唯一表没有代价。 它会成为中心化写点,需要高可用、幂等和清理机制。
  • 误区:先写业务表再检查唯一也可以。 并发下可能已经写入重复数据,补救更麻烦。
  • 追问:手机号唯一怎么做? 用手机号路由到固定分片,或写全局唯一表先占位。
  • 追问:唯一表写成功业务写失败怎么办? 释放占位、标记过期或用事务消息补偿。
  • 追问:全局 ID 和唯一约束一样吗? 全局 ID 保证主键唯一,不自动保证手机号、邮箱等业务字段唯一。

九、加强记忆

分库分表后的唯一约束要先问“唯一范围在哪里”。主键全局唯一用分布式 ID;分片内唯一把分片键放进唯一索引;业务字段全局唯一用唯一索引表或让相同唯一值路由到同一分片。不要相信分散在多张表上的本地唯一索引能自动保证全局唯一。