浅谈数据库主键策略

廖雪峰
资深软件开发工程师,业余马拉松选手。

数据库表的主键很多童鞋都非常熟悉了,主键就是Primary Key,简称PK。

数据库主键的作用是唯一标识一条记录,所以在同一张表中,任意一条记录的主键都是唯一的,不然,数据库系统就无法根据主键直接定位记录。

虽然数据库系统本身对主键没有特别的要求,但是,写程序的时候,要考虑清楚使用什么类型的主键。正确地使用主键是存储数据成功的一半,错误地使用主键会让一个应用逐渐走向崩溃。

主键不可修改

对于数据库来说,主键其实是可以修改的,只要不和其他主键冲突就可以。但是,对于应用来说,如果一条记录要修改主键,那就会出大问题。

因为主键的第二个作用是让其他表的外键引用自己,从而实现关系结构。一旦某个表的主键发生了变化,就会导致所有引用了该表的数据必须全部修改外键。很多Web应用的数据库并不是强约束(仅仅引用主键但并没有设置外键约束),修改主键会导致数据完整性直接被破坏。

业务字段不可用于主键

所有涉及到业务的字段,无论它看上去是否唯一,都决不能用作主键。例如,用户表的Email字段是唯一的,但是,如果用它作主键,就会导致其他表到处引用Email字段,从而泄露用户信息。

此外,修改Email实际上是一个业务操作,这个操作就直接违反了上一条原则。

那么,主键应该使用哪个字段呢?

主键必须使用单独的,完全没有业务含义的字段,也就是主键本身除了唯一标识和不可修改这两个责任外,主键没有任何业务含义。

类似的,看上去唯一的用户名、身份证号等,也不能用作主键。对这些唯一字段,应该加上unique索引约束。

主键应该用什么类型

主键应该使用整数还是字符串?(用浮点数的请自觉充值智商)

我强烈建议使用字符串。

为什么?

我们先看使用整数的问题。

使用整数有两个选择:数据库自增和自己生成。

自己生成其实也是自增,无非就是把上次使用的值保存到某个地方,下次使用的时候继续自增。常见的做法是用一个单独的表存储上次用的最大值。这种方式实现复杂,可靠性低,还不如数据库自增。

数据库自增最大的问题还不在于数据库单点造成无法水平切分,因为绝大部分公司还撑不到业务需要分库的情况就倒闭了。

自增主键最大的问题是把公司业务的关键运营数据完全暴露给了竞争对手和VC。举个例子,用户表采用自增主键,只需要每周一早上去注册一个用户,把上周注册的ID和本周注册的ID一比,立刻就知道了该公司一周的新增用户数量。如果网站声称新增了10万用户,但ID却只增加了1千,就只能呵呵了。

因为主键的本质是保证唯一记录,并不要求主键是连续的。实际上不连续的更好,这样既避免了运营数据泄露,也给黑客预测ID制造了障碍,具有更高的安全性。

用字符串主键就不存在这个问题。如果我们用一个UUID作为主键,即varchar(32),除了占用的存储空间较多外,字符串主键具有不可预测性。

有人觉得UUID完全随机,主键本身没有按时间递增,不利于直接主键排序。其实解决这个问题很简单。

方法一,直接用时间戳+UUID构造一个主键,时间戳注意补0,这样生成的主键就是按时间排序的。这个方法简单粗暴,缺点是主键更长了。

方法二,自定义一个算法,时间戳放高位,序列号放低位,还可以保留机器位,然后用base32编码,可以把长度控制在20个字符内。

有人会问,根据方法二,构造包含时间戳和序列号的64位整数作为主键是否可行?

理论上来说是可行的,因为时间戳0xffffffff可以表示到2100年。但是剩下的位不是ffffffff而是只有fffff,如果给机器分配ff作为标识,那么每秒只能最多生成0xfff+1=4096个主键,对一些大型应用不太够用。

为啥64位整数除掉时间戳只能用后面的fffff位呢?这是因为JavaScript的Number类型是56位精度,它能表示的最大整数是0x1fffffffffffff,而我们迟早会用REST跟JavaScript打交道,所以要把64位整数的范围限制在0x1fffffffffffff内,否则与JavaScript交互就会出错。

虽然理论上64位整数做时间戳+序列号的主键是没问题的,但是实践中是没法绕开与JavaScript交互的,综合考虑,字符串主键最可靠。



Comments

Loading comments...