唯一索引这玩意儿,真得好好掂量掂量
在大厂(就是那种用户多、数据量大、需求快速迭代的地方),如果不是对账那种一分钱不能错的业务,想着靠数据库的 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 痛苦和业务逻辑的死板限制。 ...
羊了个羊科技通关攻略
最近羊了个羊比较火,但是难度非常高,打了几天几十盘都通不过,所以犯了职业病,想看看有没有科技手段,实践有效后整理出来方便大家科技通关。 原理 科技通关的原理比较简单,游戏每天都有两幅地图,第一个地图是练手的可以无脑通过,第二个是难度爆表的版本,所以只要能够把第二个地图改成第一个,就可以实现通关。 准备工作 实现科技通关需要一个 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,回到羊了个羊重新开始游戏,第二关就变成和第一关一样简单的地图了。
基于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 三个部分。 ...
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 调用时候,会返回错误: ...
利用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 请求。 ...