业务唯一键应该如何设计?唯一索引和幂等号有什么区别?
简化版
业务唯一键用于保证某个业务维度不重复,比如订单号、手机号、租户内项目编码。它通常通过唯一索引兜底;幂等号则用于识别一次请求或一次业务操作,防止重复提交。两者都和“唯一”有关,但业务含义不同,不能混用。
详细版
业务唯一键关注“业务对象是否重复”,幂等号关注“同一次操作是否重复执行”。比如 order_no 是订单业务唯一键,request_id 是创建订单请求的幂等号。
设计原则:
- 唯一约束尽量交给数据库唯一索引兜底。
- 多租户系统唯一键要带
tenant_id。 - 软删除场景要考虑删除后是否允许重名。
- 幂等号要有请求维度、业务维度和过期策略。
- 不要只靠代码先查再插,存在并发重复风险。
面试里可以用“创建订单”举例:订单号唯一防止两个订单同号,请求幂等号防止同一次提交创建两笔订单。
完整版教学
一、业务唯一键和主键不是一回事
主键是数据库内部标识一行的稳定 ID,业务唯一键是业务上不能重复的字段。比如用户表的 id 是主键,手机号可能是业务唯一键;订单表的 id 是主键,订单号 order_no 是业务唯一键。
业务唯一键常常会对外展示、参与查询或跨系统传递。它更贴近业务语义,但不一定适合作为主键,因为它可能更长,生成规则也可能变化。
id: 数据库内部主键
order_no: 业务唯一订单号
request_id: 请求幂等号
记忆钩子:主键标识“一行”,业务唯一键标识“一个业务对象”,幂等号标识“一次操作”。
二、唯一性要由数据库唯一索引兜底
很多重复数据来自并发。两个请求同时检查“手机号是否存在”,都发现不存在,然后同时插入。如果没有唯一索引,重复数据就进库了。
所以业务唯一性不能只靠代码判断,必须有数据库唯一约束。
create unique index uk_user_phone on users(phone);
插入时如果撞唯一索引,数据库会报错或返回冲突,业务再转成“手机号已存在”的提示。这个兜底比先查再插可靠。
三、多租户和作用域决定唯一索引字段
唯一不是总是全局唯一。SaaS 系统里,项目编码可能只要求租户内唯一。A 租户可以有 CRM 项目,B 租户也可以有 CRM 项目。
这时唯一索引应该是:
unique (tenant_id, project_code)
如果误设计成 unique(project_code),不同租户会互相影响。如果误以为代码判断即可,又可能在并发下重复。
唯一键设计前要明确作用域:全局、租户内、用户内、店铺内,还是某个业务周期内。
四、软删除会影响唯一键设计
软删除场景经常遇到一个问题:删除后的名称是否允许再次使用?比如项目 alpha 被软删除后,用户能不能重新创建同名项目。
如果不允许重名,唯一索引可以继续是 (tenant_id, code)。如果允许重名,就要把删除标记或删除时间纳入设计。
方案 A:不允许复用 -> unique(tenant_id, code)
方案 B:允许复用 -> 需要结合 deleted_at 或归档策略
不同数据库对部分唯一索引支持不同。PostgreSQL 可以用部分唯一索引;MySQL 里通常要用额外字段或业务归档方案处理。
五、幂等号防的是重复操作,不是业务对象重复
幂等号常用于防止重复提交。比如用户点了两次“提交订单”,请求可能到达两次,但应该只创建一笔订单。
幂等表或业务表可以保存 request_id:
unique (user_id, request_id)
它和订单号不同。订单号是订单对象的业务编号;请求幂等号是一次创建动作的唯一标识。一次请求成功后,重复请求应该返回同一个结果,而不是再创建一笔。
六、唯一冲突处理要设计用户体验和重试逻辑
唯一索引冲突不是系统崩溃,而是业务分支。注册手机号重复要提示用户;订单号生成冲突可以重试生成;幂等号冲突要返回已有处理结果。
假设订单号生成算法每 100 万次有 1 次冲突,插入失败后重试 3 次通常足够。如果连续失败,就应该告警,而不是无限循环。
插入订单 -> 唯一冲突
业务唯一冲突:提示已存在
生成号冲突:重新生成并有限重试
幂等冲突:查询原请求结果返回
七、常见误区与追问
- 误区:先查再插就能保证唯一。 并发下两个请求可能都查不到,必须用唯一索引兜底。
- 误区:业务唯一键可以直接当主键。 业务键可能较长、规则变化或对外暴露,内部主键和业务键通常分离更稳。
- 误区:幂等号和订单号是一回事。 订单号标识业务对象,幂等号标识请求或操作。
- 追问:租户内唯一怎么设计? 把租户维度放进唯一索引,例如
(tenant_id, code)。 - 追问:软删除后允许重名怎么办? 根据数据库能力使用部分唯一索引、删除版本字段或归档表。
- 追问:唯一索引冲突怎么处理? 按冲突类型处理:提示已存在、有限重试或返回幂等结果。
八、面试中可以这样落地
以创建订单为例:订单表有内部主键 id,业务唯一号 order_no,请求幂等号 request_id。order_no 全局唯一,request_id 在用户维度唯一。
create unique index uk_order_no on orders(order_no);
create unique index uk_order_request on orders(user_id, request_id);
重复提交时,如果 request_id 冲突,就查询已有订单并返回;如果 order_no 冲突,说明生成号碰撞,可以重新生成并有限重试。
九、加强记忆
业务唯一键设计记住“三个唯一”:主键唯一一行,业务键唯一一个对象,幂等号唯一一次操作。唯一性要靠数据库唯一索引兜底,并根据租户、用户、软删除等作用域组合字段。面试时把“先查再插不可靠”讲出来,基本就抓住重点了。