👋 嗨,你好!

记录技术心得与生活点滴,见证成长的足迹。

唯一索引这玩意儿,真得好好掂量掂量

在大厂(就是那种用户多、数据量大、需求快速迭代的地方),如果不是对账那种一分钱不能错的业务,想着靠数据库的 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 痛苦和业务逻辑的死板限制。 ...

五月 16, 2025 · 1 分钟 · Zhiya

羊了个羊科技通关攻略

最近羊了个羊比较火,但是难度非常高,打了几天几十盘都通不过,所以犯了职业病,想看看有没有科技手段,实践有效后整理出来方便大家科技通关。 原理 科技通关的原理比较简单,游戏每天都有两幅地图,第一个地图是练手的可以无脑通过,第二个是难度爆表的版本,所以只要能够把第二个地图改成第一个,就可以实现通关。 准备工作 实现科技通关需要一个 Web 调试代理 App,在 iOS 上可以用 HTTP Catcher(需要内购),Storm Sniffer(三天试用),Android 上也可以找类似的软件。以 HTTP Catcher 为例,安装好之后需要安装并启用 Root 证书,以实现 HTTPS 解密。 步骤 首先需要启动 HTTP Catcher,打开羊了个羊进入游戏开始挑战,然后返回 HTTP Catcher,筛选 JSON 类型的请求,找到包含 map_info_ex 的请求。 点进这个请求里的 Response,可以看到返回内容里有个 map_md5 的列表,里面有两个 md5 值,分别对应第一个地图和第二个地图,我们要做的就是把返回值里第二个地图的 md5 替换成第一个的。 接下来返回上一个界面,左滑选择更多,新建重写,在弹出的界面中新增规则。 按下图的选择 Response 和 Body,将第二张地图的 md5(可以提前复制好)填入 Find,将第二张地图的 md5 填入 Replace,然后一路保存。 接下来重新启动 HTTP Catcher,回到羊了个羊重新开始游戏,第二关就变成和第一关一样简单的地图了。

九月 20, 2022 · 1 分钟 · Zhiya

基于Clean Architecture的Go项目架构实践

经过这些年的发展,Go 语言已经成为一门被广泛使用在各个领域的编程语言。从 k8s、docker 等基础组件,到业务领域的微服务,都可以用 Go 构建。在构建这些 Go 项目时,采用哪种架构模式和代码布局,是一个仁者见仁智者见智的事情。有 Java Spring 经验的可能会采用 MVC 模式,有 Python Flask 经验的可能会采用 MTV 模式。加上 Go 语言领域并没有出现主流的企业级开发框架,很多项目甚至没有明确的架构模式。 Clean Architecture Clean Architecture 是 Uncle Bob 提出的适用于复杂业务系统的架构模式,其核心思想是将业务复杂度与技术复杂度解藕,相比于 MVC、MTV 等模式,Clean Architecture 除了进行分层,还通过约定依赖原则,明确了与外部依赖的交互方式,以及外部依赖与业务逻辑的边界。感兴趣的朋友可以直接阅读作者原文https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html。 由于 Clean Architecture 具有脱离语言和框架的灵活性,作者在提出时也没有规定实现细节,给 Clean Architecture 的落地带来了困难,接下来以一个例子来说明如何在 Go 项目中应用 Clean Architecture 的思想。 布局 作为一个 Go 项目,不管用哪种架构模式,建议都建立 app 和 scripts 这两个路径。app 存放启动 Go 项目的入口文件,通常是 main.go。而 scripts 可以放一些构建和部署时候用到的脚本。 clean_architecture_demo ├── README.md ├── app │ └── main.go ├── scripts │ ├── build.sh │ └── run.sh ├── go.mod ├── go.sum └── usecases 接下来是代码部分,分为 entities、usecases、adapters 三个部分。 ...

十二月 12, 2021 · 1 分钟 · Zhiya

Python跨服务传递作用域的坑

背景 在一个古老的系统中,有这样一段代码: scope = dict(globals(), **locals()) exec( """ global_a = 123 def func_a(): print(global_a) """ , scope) exec("func_a()", scope) 第一段用户代码定义了函数,第二段用户代码执行函数(不要问为什么这么做,因为用户永远是正确的)。第一个代码段执行后,func_a 和 global_a 都会被加入作用域 scope,由于第二个代码段也使用同一个 scope,所以第二个代码段调用 func_a 是可以正确输出 123 的。 但是使用 exec 执行用户代码毕竟不优雅,也很危险,于是把 exec 函数封装在了一个 Python 沙箱环境中(简单理解就是另一个 Python 服务,将 code 和 scope 传给这个服务后,服务会在沙箱环境调用 exec(code,scope)执行代码),相当于每一次对 exec 调用都替换成了对沙箱服务的 RPC 请求。 于是代码变成了这个样子: scope = dict(globals(), **locals()) scope = call_sandbox( """ global_a = 123 def func_a(): print(global_a) """ , scope) call_sandbox("func_a()", scope) 作用域跨服务传递问题 由于多次 RPC 调用需要使用同一个作用域,所以沙箱服务返回了新的 scope,以保证下次调用时作用域不会丢失。但是执行代码会发现第二次 call_sandbox 调用时候,会返回错误: ...

十一月 6, 2021 · 2 分钟 · Zhiya

利用AWS Lambda和iOS捷径实现手机一键开小区门禁

我住的小区使用了一个叫守望领域的智能门禁系统,可以通过手机 App 开小区门禁和单元门,但是用 App 开门需要经过四五步:打开 App→ 进入开门界面 → 找到需要开的门 → 点击开门。 加上戴口罩时候解锁手机需要输入密码,导致整个流程非常耗时,经常需要站在小区门口和单元门口操作半天,有一段时间我甚至养成了携带实体门禁卡的习惯,实体门禁卡开门要快很多。 最近又开始忘带门禁卡,苦恼之余发现 iOS 在锁屏界面右划可以免解锁直接进入 spotlight 界面,这个界面可以添加捷径,如果能写一个捷径去调用守望领域 App 的 API 开门,就可以实现手机免解锁一键开门。 查找 API 首先需要通过 Charles 之类的软件查找 App 调用的 API,配置 Charles 查看 App 请求的方式不再赘述,Google 一下可以看到很多教程。直接看结果 Charles 的结果,可以看到 api.lookdoor.cn 是这个软件所请求的 API 域名。 打开软件发的请求非常多,经过操作和请求的对比可以看到,发送开门指令调用的 API 是:/func/hjapp/house/v1/pushOpenDoorBySn.json?equipmentId=xxxxxx 这个路径。 详细查看这个请求可以发现,equipmentId 指的就是小区门的 Id,接口使用 cookie 做认证,只要将 cookie 带上就可以模拟开门指令。 第一次尝试 打开 iOS 捷径 App,创建一个新捷径,App 调用 API 使用了 POST 请求,搜索 Get contents of 这个动作来实现发送 POST 请求。 ...

十月 19, 2021 · 2 分钟 · Zhiya