依据数据库的第二范式,数据库中每一个表中都需要有一个唯一的主键,其他数据元素和主键一一对应。
那么关于主键的选择就成为一个关键点了,一般有如下方案:
使用业务字段作为主键
比如说对于用户表来说,可以使用手机号,email或者身份证号作为主键。对大部分场景,这并不适用,像评论表,你很难找到一个业务字段主键。而对于用户表,考虑的是业务字段是否能够唯一标识人,一人可有多个email和手机号,一旦出现变更email或手机号,就需要变更所有引用的外键信息,所以使用email或者手机作为主键不行。
身份证号码确实是用户唯一标识,但由于过于私密,并非用户系统的必须属性,你的系统如果没有要求做实名认证,肯定不会要求用户填写身份证号。
而且已有身份证号码也会变化,比如1999年时身份证号从15位变为18位,但主键一变更,以该主键为外键的表也都要随之变更,影响很大。
使用生成的唯一ID作为主键
因此,更推荐使用生成的ID作为数据库主键。不仅是因为其唯一性,且一旦生成就不会变更,可随意引用。
1 数据库自增id
提供一个专门用于生成主键的库,这样服务每次接收请求都
- 先往单点库的某表里插入一条没啥业务含义的数据
- 然后获取一个数据库自增id
- 取得id后,再写入对应的分库分表
优点
简单,是个人都会
缺点
因为是单库生成自增id,所以若是高并发场景,有性能瓶颈。
若硬是要改进,那就专门开个服务:
- 该服务每次就拿到当前id最大值
- 然后自己递增几个id,一次性返回一批id
- 然后再把当前最大id值修改成递增几个id之后的一个值
但无论怎么说都只是基于单库。
适用场景
分库分表原因其实就俩:
- 单库的并发负载过高
- 单库的数据量过大
除非并发不高,但数据量太大导致的分库分表扩容,可用该方案,因为可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。
并发很低,几百/s,但是数据量大,几十亿的数据,所以需要靠分库分表来存放海量数据。
当数据库分库分表后,使用自增字段就无法保证 ID 的全局唯一性了吗?
1.使用数据库的自增,设置起始值和步长不一样,不是一样可以实现吗?
2.预估每天的数据量,预先生成ID存入缓存(比如Redis)里面,然后去取,这种方法也简单?
但是这其实很难预估数据量,某一天有活动咋办?不同的起始值也可,只是增加人工成本,增加了库表咋办?忘了设置咋办?
2 UUID(Universally Unique Identifier,通用唯一标识码)
2.1 优点
本地生成,不依赖任何第三方系统,所以在性能和可用性上都比较好。
2.2 缺点
2.2.1 无序
生成的ID做好具有单调递增性,即有序。
为什么ID要有序呢?
因为在系统设计时,ID可能成为排序字段。
比如实现评论系统,一般会设计两个表:
-
评论表
存储评论的详细信息,其中有ID字段,有评论的内容,还有评论人ID,被评论内容的ID等等,以ID字段作为分区键
-
评论列表
存储着内容ID和评论ID的对应关系,以内容ID为分区键
获取内容的评论列表时,需按照时间序倒排,因为ID时间上有序,所以可按评论ID倒序排列。
若评论ID不在时间上有序,就得在评论列表中再冗余createTime列以排序,假设内容ID、评论ID和时间都8字节,就要多出50%存储空间存储时间字段,浪费存储空间。
ID有序会提升数据的写性能
MySQL InnoDB主键也是一种索引。索引数据在B+树中有序排列。当插入的下一条记录ID递增时,DV只需将其追加到后面。
但若插入数据无序,则DB查找数据应该插入的位置,再挪动该数据后面的数据,造成多余数据移动开销。
导致 B+ 树索引写时有着过多的随机写操作,而机械磁盘:
- 随机写时,需先“寻道”找到要写入位置,即让磁头找到对应磁道,很耗时
- 顺序写就无需寻道,大大提升索引写性能
写时不能产生有顺序的 append 操作,而需要 insert,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操作在记录占用空间较大情况下,性能下降明显
2.2.2 过长
由32个16进制数字组成的字符串,若作为DB主键使用,较耗费空间。
2.2.3 不具备业务含义
现实使用的ID中都包含有一些有意义数据,这些数据会出现在ID的固定位置。
如身份证:
- 前6位地区编号
-
7~14生日
不同城市电话号码的区号不同,前三位即可看出所属运营商。
而若生成的ID可被反解,则从反解出的信息中即可验证ID,从而知道该ID生成时间、从哪个机房发号器生成、为哪个业务服务,这都有助问题排查。
Snowflake算法则可完美弥补UUID缺点。
随机生成文件名、编号等,生成Request ID标记单次请求。
3 系统时间
获取当前时间即可。但问题是高并发时,会有重复,这肯定不合适啊,而且还可能修改系统时间!
若用该方案,一般将当前时间跟很多其他的业务字段拼接起来,作为一个id。若业务上你可以接受,那也行。
你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号,比如订单编号:
时间戳 + 用户id + 业务含义编码
。