唯一索引这玩意儿,真得好好掂量掂量
文章目录
在大厂(就是那种用户多、数据量大、需求快速迭代的地方),如果不是对账那种一分钱不能错的业务,想着靠数据库的 UNIQUE INDEX
(唯一索引)来拦重复数据,说实话,效果不一定好,伺候它的成本还很高。更好的办法是把去重的主要活儿放应用层,数据库那个唯一索引,能不用就先别用,或者想清楚了再用。
一、 为啥我开始琢磨唯一索引这事儿?因为坑踩了
数据库唯一索引,听着挺靠谱,对吧?保证数据不重复的最后一道防线。以前我也是这么想的,表里哪个字段不让重,随手就给它来个唯一索引。
直到被现实狠狠摩擦了。
很久之前,当我头发还很茂密的时候,给一个千万级的表加个组合唯一索引(比如 tenant_id
和 is_deleted
这俩字段不能重复)。听着简单吧?结果呢,整个变更从头到尾折腾了几天!这期间,主从延迟跟过山车似的,时不时还得担心线上服务会不会抖一下。事儿完了我在想,就为了数据库层面这个“唯一”,搭进去这么多工夫、担这么大风险,值吗?
还有个事儿也挺别扭。 业务上,我们都知道 [email protected]
和 [email protected]
其实是一个邮箱,你注册的时候,应用代码肯定也会把它们统一处理成小写再判断有没有重复。结果呢,数据库里的唯一索引(区分大小写)不认这个账。有时候因为一些历史数据或者旁路数据同步没做好规范化,数据库里就存了两种大小写格式的“同一个”邮箱。这时候,唯一索引它要么“眼瞎”发现不了这种业务上的重复,要么在你修数据的时候,因为它那死板的规矩,反而碍手碍脚。
更别提业务迭代了。 比如,以前光是“邮箱唯一”就行,现在要改成“租户 ID+邮箱唯一”。好家伙,应用代码得改吧?数据库的唯一索引也得跟着 DROP
旧的 CREATE
新的吧?这两拨操作怎么配合?谁先谁后?万一中间出岔子怎么办?在大表上搞这种操作,每次都跟拆炸弹似的,提心吊胆。
就这些事儿搞得我不得不琢磨:数据量大、并发高、需求又变得快,唯一索引这一套,是不是该重新掂量掂量了?它带来的麻烦,是不是已经比它的好处多了?
这篇文章,就是想跟大家伙儿聊聊我的反思。
二、 UNIQUE INDEX
:我们为啥那么信它?
在吐槽之前,咱也得公平点,说说唯一索引为啥那么招人待见,它确实有几个看上去不错的点:
- 数据不跑偏的最后保险: 防止数据重复的最后一道关卡。
- 上手简单: 建表的时候或者后来加个 DDL,几行 SQL 就搞定了。
- 表结构一看就懂: Schema 里标着呢,这字段不能重。
- 顺便还能快点查: 反正也是个索引,按这个键查数据能快点。
这些好处,在小项目或者数据量不大、业务不复杂的时候,确实挺香。但一到大数据量+快速迭代“修罗场”,情况就变了。
三、 大厂滤镜下的UNIQUE INDEX
:那些“好处”还好使吗?
接下来咱们挨个盘盘上面说的那些“好处”,看看在大厂这环境下,它们是不是还那么“美”。
“最后保险”?这保险靠谱吗?保的是啥险?
业务上的“重复”它认不全啊!就像前面说的邮箱大小写,还有手机号带不带
+86
,用户名清不清除特殊字符……这些业务上才认的“一样”,数据库那简单粗暴的“字节必须一样”的唯一索引根本管不过来。它防不住业务层面的“逻辑重复”。应用层反正要干活。 既然这些复杂的“一样不一样”都得在应用代码里判断(总不能直接把数据库报错丢给用户吧?),那应用层才是真正保证“业务数据不重复”的主力军。数据库那个唯一索引,充其量是个标准可能还跟业务不一致的“辅警”。
分布式系统里它就是个“本地保镖”。在分布式场景下一旦分表,表内的唯一索引,管不了全局唯一性。全局唯一还得靠 ID 生成服务或者应用层的全局校验。这时候,数据库本地那点“保险”作用就更小了。
这个“最后保险”既可能保不到点子上,覆盖面也有限,全指望它优点悬。
“上手简单”?一次上线,一周折腾
新表新加个唯一索引,确实就一条 SQL。但更多的时候是给已经跑了好久、数据堆成山的旧表改规则。你想给千万行的表改个唯一索引(比如从一个字段唯一改成俩字段组合唯一),可能就是几分钟的锁表!在线 DDL 工具,也只是让你不用停服务,但整个过程照样漫长、耗资源、有风险。
敏捷?快不起来啊!在快速迭代+多区域同步+合规要求的场景下。数据库这儿一个唯一索引的变更就要卡你好几天,啥敏捷都白搭。
所以开始那一下“简单”,跟后来改起来的“要老命”比,简直是钓鱼。
“表结构一看就懂”?懂的可能跟实际要的不一样啊!
唯一索引在表结构里写着,是,算是一种“技术文档”。可是“文档”可能误导人,如果这个唯一索引定义的“唯一”跟业务上实际的、更复杂的唯一规则对不上(比如大小写问题),那这份“文档”不光没用,还可能误导后来的开发。 如果改这份“文档”(就是改唯一索引)就要经历九九八十一难,那我们为啥不把业务规则好好写在真正的设计文档、Wiki 或者代码注释里呢?那些地方改起来可方便多了。
“顺便还能快点查”?为了这点醋,才包的这顿饺子?
这是个很常见的误解,或者说是一个被过分强调的“附加值”。如果你只是想让某个字段或某几个字段的查询快一点,你完全可以给它们建一个普普通通的、非唯一的索引(Non-Unique Index)啊!非唯一索引照样能嗖嗖地提高查询速度,而且它还没有唯一性约束带来的那些写入开销、DDL 痛苦和业务逻辑的死板限制。
主从索引不一致,复制立马“瘫痪”:
我见过好几次,主库的唯一索引配置更新了(比如加了个字段,或者改了约束),从库的索引没同步改。那主库那边数据一有变化(比如插了条在从库看来会重复的数据,或者主库能写进去但在从库因为有那个错误的/未更新的索引而写不进去),binlog 传到从库,咣当一下,
Slave_SQL_Running: No
,复制直接挂掉!这种事儿一出,数据延迟,读写分离受影响,甚至影响故障切换,你说闹不闹心?
四、 把活儿交给应用层,专业!
既然数据库唯一索引这么多毛病,数据唯一性这事儿,主要还得靠咱应用层自己来扛。
应用层搞唯一性的好处,那可太多了:
- 灵活、精准: 业务说啥样算重,咱就按啥样写逻辑,大小写、格式化、各种复杂条件,想咋玩咋玩。
- 用户体验好: 用户输错了,咱能给出明确提示,比如“这手机号注册过了,要不要直接登录?” 而不是冷冰冰的数据库错误。
- 效率高,先拦住: 在数据还没到数据库呢,在服务接口那层甚至网关层就给它拦下来,省得白跑一趟数据库。
- 接口幂等性: 这是防止重复操作的大杀器。用户手抖点了两下提交,网络不好重试了一下,应用层做好幂等,数据就不会重。这事儿唯一索引可管不了。
总结
只有当唯一索引带来的那点好处(通常是极端情况下的数据兜底),明确地、大大地超过它在大数据量+快速迭代的复杂环境里带来的各种麻烦(影响敏捷、运维痛苦)时,再考虑用它。优先把应用层的唯一性保障机制(前置校验、异步处理、幂等性、全局 ID 等等)做扎实。数据库那个唯一索引,能省则省,实在要用,也得想清楚了,当成个“特殊工具”来对待,别当成“标配”。