声明:本人能力有限,Java开发经验较少,如分享的内容有不对或者不恰当的地方,欢迎通过各种途径指正,不胜感激。另外,这篇文章保证绝对原创,在发布到CSDN之前,我也在公司内部刊物上进行了刊发申请,各位如果有修改意见提出,我会及时进行更正。
1. 背景
本人所在单位是传统企业,借着数字化转型的东风,我也加入到了转型大军,从传统领域转到了IT领域。经过了1年的培训后,我光荣的成为了一个数字化领域的底层农民工。
之前公司的很多项目都是外包团队开发,但这几年开始组建了自己的开发团队。最近接触到了很多使用UUID作为数据库的主键和业务ID的项目,这对于读了多遍阿里Java开发手册的我来说,总觉得有点别扭。所以有了写这个分享的想法。
上述现象分解一下,其实是2个独立的问题:①使用UUID作为数据库主键;②使用UUID作为了业务id。接下来我将针对这2个问题进行逐一分析。
在分析之前,我们先看看阿里Java开发手册在这方面的规范。如下图所示:
链接地址:链接地址:嵩山版Java开发手册-阿里云开发者社区
我们要避免重复造轮子,也要学会站在巨人的肩膀上。为了让这些互联网大厂的和公司的规范能在我们的项目中落地,我想从多个角度分析一下,为什么会有这样的要求。前面已经提到,UUID是目前大部分项目采取的主键策略,所以接下来主要是把UUID和自增主键进行对比。
其实这方面的文章很多,对于UUID的优缺点也很容易能查询到结果,但我想从MySQL官方文档的角度,分析一下这个问题,因为很多文章都提到“MySQL官方建议使用自增主键”,但都没有举证,我也是想给这种说法进行一下举证。
2 MySQL UUID主键和自增主键的对比分析
2.1 官方文档对于主键的相关说明
考虑到目前公司的生产环境都是MySQL 5版本为主,所以接下来的所有举证,都是查询的该版本的官方文档。首先,在主键的描述中,MySQL推荐使用合成值(synthetic key)而不是自然值(natural key)。
所谓自然值(natural key)就是有一些现实意义的值,比如纳税人id,其实我觉得就是业务id。这些值有可能也是唯一的,但是不适合作为主键使用。官方文档释义如下(这里边提到主键推荐使用合成值,比如:自增):
对合成值(synthetic key)的说明如下。这里边进一步说明了使用合成值的意义,同时,也提到了,通常使用自增列作为主键列。
另外,MySQL的默认存储引擎是InnoDB,该引擎的主键是聚簇索引。从InnoDB的相关说明里,可找到如下图所示内容:如果使用顺序插入(自增主键属于顺序插入),索引页的利用率可以达到15/16,而使用随机插入(UUID无序,所以它作为主键属于随机插入),索引页的利用率最低时只有1/2。 (这又引入一个新的概念:页。你可以把它简单理解数据从磁盘加载到内存的最小单位,MySQL默认是16K,如果page利用率高,那查询一个数据的IO次数就更少,速度自然也就更快。显然除了有被浪费的空间外,UUID也比自增要长几倍,综合算下来,一个索引页里容纳的索引数量UUID会低很多)
此外,MySQL的官方文档也提到,使用自增主键,有利于查询优化器的工作,方便提升查询性能。相关信息如下图所示:
基于以上分析,我觉得可以得出结论,MySQL的官方是推荐使用自增主键的(请注意,这里只是提到主键,这里暂时不要把主键和业务id混在一起。)。
2.2 实验验证两种主键对数据插入性能的影响
表结构:
-- 为避免其他因素影响,2个表都是只有2列,1列是bigint类型,1列是varchar类型
-- Table structure for long_test
CREATE TABLE `long_test` (
`id` bigint NOT NULL AUTO_INCREMENT,
`data` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 0 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
-- Table structure for uuid_test
CREATE TABLE `uuid_test` (
`id` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`data` bigint NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
2.2.1 实验一:表数据量足够大时,单条数据插入的对比
- 方法:数据量是500万的表中,每次插入1条数据,循环插入500次,对比UUID主键和自增主键的耗时
- 说明:下图横坐标是第几次、纵坐标是耗时(单位ms)。
- 结论:数据量是500万时,使用UUID和自增主键,单条数据的插入时间差别不大。(这里只举证了插入的效率,实际上其他操作也一样,对于简单查询来说,UUID对查询性能的实际影响并不大;另外,由于篇幅所限,我并没测试在高并发的情况下的性能对比)
2.2.2 实验二:随着表数据量的增加,批量插入数据的耗时对比
- 方法:分别使用UUID和自增ID做主键,循环插入500次,每次插入10000条数据,耗时变化
- 说明:下图横轴是数据表中已有数据量,纵轴是耗时(ms)。可以看出,使用UUID作为主键,当表中数据量达到100万时,批量插入10000条数据的耗时会直线增加,数据量达到500万左右时,单次批量插入的耗时为5.5秒;而使用自增主键,同样的数据量,耗时比较稳定,500万数据量时,约为0.3秒。
- 结论:自增主键和UUID主键,批量插入数据时,随着数据量的增大,UUID性能会急剧下降。
2.3 实验验证两种主键对存储空间的影响
2.3.1 实验一:使用上述表结构建立新表,随着数量的增加,对比数据文件大小占用
- 方法:循环插入500次,每次插入10000条数据,对比磁盘占用空间的变化
- 说明:从统计图可以看出,随着数据量的增加,数据文件的大小都是线性增加的。当数量为500万是,UUID做主键的表,最终大小是556MB,自增主键的表,最终大小是344MB。
- 结论:存储完全相同的内容,使用UUID作为主键,数据文件大小是自增主键的1.62倍(这个算的比较粗糙,主要是为了说明UUID浪费了空间,要想深入追究浪费了多少,得具体情况具体分析)。
2.4 其他影响
- UUID对索引的影响:InnoDB引擎的非主键索引,叶子节点存储的是主键ID,如果使用UUID作为主键,非主键索引也会占用更多空间,进而影响查询效率;
- UUID作为主键,数据不是按照插入顺序进行默认排序,极大降低数据库内容的可读性;
- UUID作为主键,在分库分表、数据迁移时,会带来麻烦。之前做了一个项目,需要把SQLServer数据库中2千万条数据同步到MySQL中去,数据表结构发生了变化,且每天还有上万条增量,SQLServer只有UUID主键,导致很难识别增量数据,给迁库带来了极大的不便。
3. UUID作为业务ID的缺点分析
- 不能做到见名知义:无法区分是哪个业务ID,也不能区分时间、书序等信息,可读性很差;
- 浪费了不必要的存储空间:UUID可以做到全局唯一性,但需要36个字符的空间,浪费了不必要的存储空间;尤其是业务数据量不大的时候,使用36位的id,显得过于臃肿;
- 性能影响:业务ID一般也会设置成唯一索引,其在主键方面的各种劣势也同样存在。
4. 推荐的主键及业务ID策略
- 必须使用自增作为主键(毕竟这个是公司规范的要求),且不要在任何情况下将主键公开、或使用主键去做相关的业务操作;
- 如果工作需要,在主键之外定义额外的业务id,根据业务量的大小,选择合适的业务id生成策略(可参考开源的分布式id策略:SnowFlake、TinyID、Uidgenerator、Leaf等)。
5. 写在最后
写了这么多,可能有些人会有疑问:从实验结论来看,单次的插入使用UUID并没有带来太多性能上的损耗。而且日常工作中,我的业务都是ToB的,很多业务表在整个生命周期内最多也就只有几千条数据,使用UUID作为主键也没有浪费多少空间,所带来的性能问题也可忽略不计。
对于这种疑问,我想从另外一个角度说明一下。比如有一天,你面临入岗入级、或者想换个更好的平台,当面试的人请你说说你这几年的工作中使用的主键或ID策略时,你要怎么回答呢?何不在现有工作中,把一些东西一一实现一下呢,到时候说的也更有底气。