你真的了解为什么禁用外键吗?

2,249 阅读7分钟

妈妈说:学而不思则罔,思而不学则殆。 妈妈说:知其然,知其所以然。

看到这个观点,先思考一下.... 但思考太烧脑,我还是先必应一下:

“互联网行业应用不推荐使用外键: 用户量大,并发度高,为此数据库服务器很容易成为性能瓶颈,且不能轻易地水平扩展;把数据一致性的控制放到程序代码中,也即让应用服务器承担此部分的压力,而应用服务器一般都是可以做到轻松地水平的伸缩”

“为何说外键有性能问题:1.数据库需要维护外键的内部管理;2.外键等于把数据的一致性事务实现,全部交给数据库服务器完成;3.有了外键,当做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,而不得不消耗资源;4.外键还会因为需要请求对其他表内部加锁而容易出现死锁情况;”

相信大家去搜索这个问题,看到的结果和上面摘抄的文字大致相同。

妈妈说:尽信书不如无书。 妈妈说:不可人云亦云。 妈妈说:不可迷信权威。

继续思考,这次必须烧脑了,因为搜索到的结果基本上就上面一种声音,没有其它声音。

数据库的瓶颈可能存在于CPU、内存、IO上(一般指磁盘IO,网络IO其实也可能成为瓶颈)

磁盘IO都一样,都需要将对应的数据加载进入内存;

内存不用深究,内存白菜价,而且搞上T的内存都可以。

外键方案占用CPU多? 外键方案将数据读入内存,加几个锁,做一个逻辑判断;程序判断方案,需要将内存的数据按特定的格式通过网络发送给程序。那个多还真说不准。但是CPU现在有128核的是不是?普通PC服务器支持2个CPU是不是?无论如何CPU也不会成为数据库瓶颈的。

网络IO,程序判断网络IO多。

从硬件层面来说。使用了外键的数据库不会爆炸。那是锁的问题?

死锁不考虑,这个可以有一定的机制来处理:顺序加锁能规避掉绝大部分死锁;数据库配置减小锁等待时间让事务回滚,偶尔来一个失败操作就完美解释为偶发网络波动问题。 但是外键会不会引来大规模的锁等待从而降低数据库吞吐量是我们关注的要点。

等等,用程序检测就没有锁了吗? 如果需要强一致性,程序检测需要引入分布式锁!!!才能保证数据绝对一致。

脑袋里跳出了一个小黑人:

“我是互联网,只要高并发,数据有点点不一致没啥大问题,天塌下来还有人肉运营加人肉运维顶着呢!所以,程序校验我不上分布式锁。

“我是互联网,只要高并发,数据有点点不一致没啥大问题,天塌下来还有人肉运营加人肉运维顶着呢!所以,程序校验我不读数据库我读缓存

“你看我程序校验数据库是不是压力小了不少?”

脑袋里的小白人一时语塞。。。。 这个这个,你高并发,早晚分库分表,你数据库不就水平扩展了。。 我们还是来看看锁会不会让数据库并发大跌

我们先假设一个场景: 商品表(product) 和商品分类表(product_catgory), product有一个字段category_id外键引用product_catgory表。

product.category_id --> product_cagory

product有这些字段: id(主键), name, category_id(索引),deleted(删除标志)

product_catgory有这些字段: id(主键), name,deleted(删除标志)

外键约束起作用的场景:

  • 场景1:insert product (id,name,category_id) values ('1','product_1','1');

  • 场景2:update product set category_id='1' where id='1';

  • 场景3: update product_category set name='other name' where id='1';

  • 场景4:delete product_category where id='1';

我们先来分析一下外键关系的数据完整性检查如何做。

数据库外键约束原理

如果用外键约束来保证数据完整性。官方文档如是说。 dev.mysql.com/doc/refman/…

If a FOREIGN KEY constraint is defined on a table, any insert, update, or delete that requires the constraint condition to be checked sets shared record-level locks on the records that it looks at to check the constraint.

翻译: 如果一个表定义了外键。任何需要做外键约束检查的insert, update 或者delete操作,都在外键约束要查看的记录上设置“行级共享锁”,借此来做完整性检查。

这一句话说的太简略,冥思苦想了半天不得要领,做了若干是实验结论如下:

  • 场景1:insert product (id,name,category_id) values ('1','product_1','1'); 会在product_category表上id=1的记录上添加共享锁。
  • 场景2:update product set category_id='1' where id='1'; 会在product_category表上id=1的记录上添加共享锁。(如果返回的更改行数不是为0)

上面两个场景使用共享锁,锁住product_category上一条记录,这条记录上不能做更新操作。这两个场景外键上的锁没啥大问题。虽然product_category上的记录被极大几率锁住,但是,product_category只有运营以极低的频率操作。不是啥大问题。

但是如果运营人员修改了product_category表,会怎么样?

  • 场景3: update product_category set name='other name' where id='1'; 这会在product_category表上id=1的记录上添加排他锁。然后将场景1、场景2的操作锁住。 这里有点问题,但是这个问题并不是无可救药。“set autocommit=1;”来一下。这个事务会很快的被提交。假设这个SQL语句执行了1ms(很长很长了),product的插入性能最大降低了 1ms/1S=0.1%。(假设全部插入的这个category的数据,这个时间内啥都干不了。) 降低了0.1%是不是毛毛雨? 这个关键是要降低操作product_category表的事务长度。

  • 场景4:delete product_category where id='1'; 这会在product_category表上id=1的记录上添加排他锁,并且在product表的category_id索引上添加一个Gap锁(看这个:dba.stackexchange.com/questions/2… "......., Gap locking is only used for foreign-key constraint checking and duplicate-key checking")。和上面一样,场景1、场景2的操作锁住被锁住。只要事务时间短,都不是问题。此外,还有一些其他语句会被锁住,但是高并发一般不会允许这样的业务,例如:

    • update product set deleted=1 where category_id=1; -- 也会在product表的category_id索引上添加一个Gap锁,这种批量修改数据在高并发场景下应该会被DBA打死。

所以,外键引入的锁看起来问题也不大。

总结

不用外键的原因,恐怕不是数据库服务器硬件性能不够。更多考虑的是锁的问题。使用外键得当,锁引起的吞吐量下降几乎可以忽略。

亲,互联网高并发要分库分表哦,分库分表还咋用外键。。。。 (如果product和product_category没有在两个微服务里,放一个数据库还是可以外键,product_category数据量应该不会超过2000万吧)

不会是因为外键必然使用索引,索引太多影响数据库插入性能?有一些道理,但是分库分表后单库1W tps应该应该高不可攀了吧? 可是mysql完全可以hold住啊。来看mysql性能测试:www.mysql.com/why-mysql/b…

但是但是,如果有个无证程序员写了这样的代码: 更新category表没有使用autocommit为true,且更新category的事务里有远程调用。好家伙,那个远程调用,它超时了!!!总不能杀了无证程序员祭天吧? 架构师咬咬牙说:新来的给我背熟了,不准使用外键!不准使用外键!不准使用外键!重要的事情说3遍。

水平有限,请各位轻一些打脸。