数据库主键的设计

九种分布式ID生成方式

一、数据库主键的设计原则

主键和外键是把多个表组织为一个有效的关系数据库的粘合剂。主键和外键的设计对物理数据库的性能和可用性都有着决定性的影响。主键和外键的结构是将数据库模式从理论上的逻辑设计转换为实际的物理设计。一旦将所设计的数据库用于了生产环境,就很难对这些键进行修改,所以在开发阶段就设计好主键和外键就是非常必要和值得的。

主键:关系数据库依赖于主键—它是数据库物理模式的基石。它确定了关系数据库的实体完整性约束,主键在物理层面上只有两个用途:

  1. 惟一标识一行。
  2. 作为一个可以被外键有效引用的对象。

基于以上这两个用途,设计物理层面的主键要遵循以下原则:

1️⃣主键应当是对用户没有意义的:
如果用户看到了一个表示多对多关系的连接表中的数据,并抱怨它没有什么用处,那就证明它的主键设计地很好。

2️⃣主键应该是单列的,以便提高连接和筛选操作的效率:
使用复合键的人通常有两个理由为自己开脱,而这两个理由都是错误的。其一是主键应当具有实际意义,然而,让主键具有意义只不过是给人为地破坏数据库提供了方便;其二是利用这种方法可以在描述多对多关系的连接表中使用两个外部键来作为主键,但是复合主键常常导致不良的外键,即当连接表成为另一个从表的主表,而依据上面的第二种方法成为这个表主键的一部分,然,这个表又有可能再成为其它从表的主表,其主键又有可能成了其它从表主键的一部分,如此传递下去,越靠后的从表,其主键将会包含越多的列了。

3️⃣永远也不要更新主键:
因为主键除了惟一标识一行之外,再没有其他的用途了,所以也就没有理由去对它更新。如果主键需要更新,则说明主键应对用户无意义的原则被违反了。

注:这项原则对于那些经常需要在数据转换或多数据库合并时进行数据整理的数据并不适用。

4️⃣主键不应包含动态变化的数据,如时间戳、创建时间列、修改时间列等。

5️⃣主键应当由计算机自动生成:
如果由人来对主键的创建进行干预,就会使它带有除了惟一标识一行以外的意义。一旦越过这个界限,就可能产生认为修改主键的动机,这样,这种系统用来链接记录行、管理记录行的关键手段就会落入不了解数据库设计的人的手中。

二、数据库主键的数据类型的选择

1️⃣自增主键,在 MySQL 中应用最广泛。
如果是微型系统,而且肯定数据库建好了就不会动了,数据也不会去动,那么数据库主键就用自增 int 就够了。

优点:

  1. 需要很小的数据存储空间,仅仅需要 4 byte。(bigint类型,是8 byte)
  2. insert 和 update 操作时使用 int 的性能比 UUID 好,所以使用 int 将会提高应用程序的性能。
  3. index 和 Join 操作,int 的性能最好。
  4. 容易记忆。

缺点:

  1. 如果经常有合并表的操作,就可能会出现主键重复的情况。
  2. 使用 int 数据范围有限制。如果存在大量的数据,可能会超出 int 的取值范围。
  3. 很难处理分布式存储的数据表。

2️⃣UUID
如果是比较中大型的系统,比如电商网站,erp,crm之类的系统,主键最好用 UUID,可以保证数据的唯一性,而且数据迁移数据合并分库分表的时候会更省心,如果是 int 主键,数据迁移和合并会很麻烦,如果涉及到外键关联,更要考虑主键 ID 重复性的问题,所以宁肯牺牲一点性能使用 UUID 做主键,长远考虑带来的好处远超过 int 带来的这点性能优势。

优点:

  1. 能够保证独立性,程序可以在不同的数据库间迁移,效果不受影响。
  2. 保证生成的 ID 不仅是表独立的,而且是库独立的,这点在想切分数据库的时候尤为重要。

缺点:

  1. 比较占地方,和 int 类型相比,存储一个 UUID 要花费更多的空间。
  2. 使用 UUID 后,URL 显得冗长,不够友好。
  3. 没有内置的函数获取最新产生的 UUID 主键。
  4. 很难记忆。Join 操作性能比 int 要低。
  5. UUID 做主键将会添加到表上的其他索引中,因此会降低性能。

三、MySQL 的自增 ID 用完了,会如何?

当继续 insert 时,使用的自增 ID 还是该字段类型的最大值(2147483647),报主键冲突的错误。

四、面试:分库分表之后,主键 id 如何处理?(唯一性,排序等)

分库分表必然要面对的一个问题,就是 id 如何生成?因为分成多个表之后,每个表肯定不能都从 1 开始累加;还有排序问题等。比较典型的,如下:

1️⃣主键冲突问题
分库分表的环境中,数据分布在不同的分片上,不能再借助数据库自增长特性直接生成,否则会造成不同分片上的数据表主键会重复。

2️⃣事务问题
在执行分库分表之后,由于数据存储到了不同的库上,数据库事务管理出现了困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。

3️⃣跨库跨表的join问题
在执行了分库分表之后,难以避免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上。此时,表的关联操作将受到限制,无法 join 位于不同分库的表,也无法 join 分表粒度不同的表,结果原本一次查询能够完成的业务,可能需要多次查询才能完成。

解决方案

1️⃣数据库自增 id
系统往一个库的一个表里插入一条没什么业务意义的数据,然后获取一个数据库自增的一个 id。拿到这个 id 之后再往对应的分库分表里去写入。

优点方便简单,谁都会用;缺点就是单库生成自增 id,要是高并发的话,就会有瓶颈。如果硬是要改进一下,那么就专门开一个服务出来,这个服务每次就拿到当前 id 最大值,然后递增几个 id,一次性返回一批 id,然后再把当前最大 id 值修改成递增几个 id 之后的一个值;但是无论如何都是基于单个数据库。

2️⃣UUID
优点就是本地生成,不需要依赖数据库。缺点就是,UUID 太长了,作为主键性能太差了,另外 UUID 不具有有序性,会造成 B+ 树索引在写的时候有过多的随机写操作,频繁修改树结构,从而导致性能下降。

适合的场景:如果要随机生成文件名、编号之类的,适合用 UUID。
UUID.randomUUID().toString().replaceAll("-","");

3️⃣获取系统当前时间
这个就是获取当前时间即可,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的。

适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个 id,如果业务上可以接受,那么也是可以的。可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号。

4️⃣snowflake 算法

你可能感兴趣的:(DataBase,数据库,sqlserver,sql)