关于数据库设计:数据库主键适合用UUID吗

48次阅读

共计 3809 个字符,预计需要花费 10 分钟才能阅读完成。

动机

为什么想到用 UUID 做数据库主键呢?思考如此场景,有多个生产环境各自运行,有时要从一个环境导数据到另一个环境,因而要求主键不抵触,自增整数不能满足要求。
搞个地方发号器服务如何?很多互联网公司都这么做了。但对于以上场景,那就须要各个生产环境依赖同一个发号器服务了,难以跨数据中心乃至跨地区。
其实这须要一种去中心化的发号策略,无论哪个服务器都能够发号,而且这些号互不抵触。

有一种现成的去中心化发号的实现,这就是 UUID!UUID 叫做通用惟一标识符(Universally Unique Identifier),是 128bit 的整数,用算法计算失去。在分布式系统中的任一服务器只有依规范创立 UUID 值,就能保障在全局范畴不反复。

例如 Java 的 UUID.randomUUID()是用 SecureRandom 实现的齐全随机数,这是一种在密码学意义上平安的随机数,随机位越多越平安(猜不到法则,而且不容易反复)。因为 128bit 全是随机位,实践上认为在地球上是不会反复的。

考查

就不多介绍了,来讲讲它能不能作为数据库主键吧,看看优缺点:

长处

  • 去核心化生成 -> 无单点危险
  • 去核心化生成 -> 无需特意设计就能并行生成(自增主键是串行生成的,比较慢)
  • 无状态生成 -> 数据记录在存入数据库之前就能领有主键(自增主键要在数据记录存入数据库后能力获取),编程更容易

毛病

  • 无序 -> 不能用主键排序代替工夫排序,以防止加载数据记录(个别实现能有序,但 Java 内置实现齐全无序)
  • 无序 -> 升高数据库写入性能
  • 无序 -> 升高数据库读取性能

长处不必多说了,来探讨这些毛病吧。

首先弄清楚关系型数据库系统是怎么工作的。相似于文件系统,关系型数据库系统以页为单位来存放数据,一页(个别为 4KB、8KB 或 16KB 大小)能寄存一批数据记录。读写时也是整页地读取或写入。对于有主键列的表,默认提供主键索引,索引项蕴含了主键值并且以主键排序。
MySQL 的主键索引采纳聚簇索引,索引与数据融为一体,数据记录依照主键程序来存储。
PostgreSQL 的主键索引采纳非聚簇索引,索引与数据各存一份(索引相当于 ” 主键值 -> 数据寄存地址 ” 的映射表),有专门的索引页和数据页。

(1) 不能用主键排序代替工夫排序,以防止加载数据记录。
当一个查问只须要返回主键但须要按工夫排序时,如果主键是工夫有序的,就能够对主键排序。这时只需拜访索引而无需拜访数据记录就能实现查问,因为索引就蕴含了主键值。

(2) 升高数据库写入性能
用 INSERT 语句增加数据记录时,要更新主键索引。UUID 主键的随机性使得 MySQL 的数据页、PostgreSQL 的索引页被随机写入,很可能一页只增加一行,因而须要读写很多页(从磁盘读入内存,批改再写回,命中缓存时不必读磁盘但仍要写回)。有序的主键更有可能把多行增加到同一页,因而能够写更少的页(数据库系统不会每写一行就刷盘,而会略微缓冲一小会,让一页有可能多收到几行)。简而言之,随机主键不能利用数据的空间局部性。
PostgreSQL 只是索引页受影响,比 MySQL 数据页受影响要好些。但 PostgreSQL WAL 的 full_page_writes 个性引起的写入放大使它也受不小的影响。

(3) 升高数据库读取性能
相邻工夫增加的数据记录会随机散布在 MySQL 的很多个数据页。新建的数据记录可能比拟热门,然而它们随机放在很多个数据页,没有哪一页是热门的。而且一些范畴查问,例如按创立工夫查问,须要读取很多个数据页能力拿到所有匹配的数据记录。
同样,PostgreSQL 只是索引页受影响,比 MySQL 数据页受影响要好些。

还有一个问题是 UUID(128bit)比 BIGINT(64bit)大,若用 char(32)来保留 UUID 则须要 256bit,存储和计算的开销都更大。

这篇是 Percona 首席架构师的文章,探讨 MySQL 应用 UUID 主键的性能。
https://www.percona.com/blog/…
这两篇是一位 PostgreSQL 专家的文章,探讨 PostgreSQL 应用 UUID 主键的性能,以及 full_page_writes 遇到的写入放大问题。
https://www.2ndquadrant.com/e…
https://www.2ndquadrant.com/e…

URL 编码

再思考 URL 编码的问题。主键须要在 REST 格调 URL 中用来标识资源,例如 /users/{id},id 能够是任一主键值。此时主键要被编码为字符串,UUID 的字符串模式至多也要 32 字节(若保留 ’-‘ 分隔符,如 Java UUID.toString(),则为 36 字节)。这个字符串模式理论是 UUID 的 16 进制写法。

如果对 UUID 做 Base64 编码,能够压缩到 22 字节。请留神要应用 URL Safe Base64 编码(Java 提供这一编码模式),因为规范的 Base64 含有 URL 保留字符,使得 id 字符串须要被本义。

有一些版本的 UUID 是有序的,其字符串模式满足 ASCII 程序,Base64 编码使其不恪守 ASCII 程序。可应用 Firebase-style Base64 编码,参见 https://firebase.googleblog.c…。

有序 ID

能够同时取得工夫有序性和平安随机性的益处吗?能够。

一种工夫有序的去中心化惟一 ID 实现:

  1. 生成一个齐全随机的 UUID,长度为 128bit
  2. 以以后 unix time 作前缀,这样总长度为 192bit
  3. Firebase-style Base64 编码,这样总长度为 32 字节,相当于一般 UUID string

一种更玲珑的实现是 unix time 配上 64bit 随机值,和 UUID 一样有 32 字节,Firebase-style Base64 编码后长度为 22 字节。然而可能对碰撞率有影响,须要实践证实。
还有一种实现是 48bit unix time 配上 80bit 随机数,编码前 char(32),编码后 char(22),这一类算法有 Firebase 在生产环境用了,举荐。
Cassandra 应用 UUID v1,依赖微秒工夫和 MAC 地址。

有序 ID 的编码

Firebase-style Base64 有一个问题:以后的 ID 总是以 ’-‘ 开始。用户体验不好,因为用户很容易疏忽这个字符,认为它不是 ID 的成分。

因而我从新设计了编码,还赠送一份 Java 实现代码给读者(48bit unix time 配上 80bit 随机数,满足 ASCII 程序的 URL Safe Base64 编码)。

/**
 * (22 chars) 48bit milliseconds + 80bit random value (ASCII-ordered URL-safe Base64-encoded)
 */
public final class UniqueIdUtil {private static final SecureRandom secureRandom = new SecureRandom();
  private static final byte[] remapper = new byte[128];
  static {byte[] oldCodes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".getBytes(StandardCharsets.US_ASCII);
    byte[] newCodes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~".getBytes(StandardCharsets.US_ASCII);
    for (int i = 0; i < oldCodes.length; i++) {remapper[oldCodes[i]] = newCodes[i];
    }
  }

  private UniqueIdUtil() {}

  public static String newId() {ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[18]);
    byteBuffer.putLong(System.currentTimeMillis());
    byte[] randomBytes = new byte[10];
    secureRandom.nextBytes(randomBytes);
    byteBuffer.put(randomBytes);

    return newId(byteBuffer);
  }

  static String newId(ByteBuffer byteBuffer) {byte[] original = Arrays.copyOfRange(byteBuffer.array(), 2, 18);
    byte[] encoded = Base64.getUrlEncoder().withoutPadding().encode(original);
    for (int i = 0; i < encoded.length; i++) {encoded[i] = remapper[encoded[i]];
    }
    return new String(encoded, StandardCharsets.US_ASCII);
  }
}

来比拟两种 ID 编码:
Firebase-style Base64 编码:-MPZFw-83QdUZ_vQ6UAMdF
自制 Base64 编码:0NQ_LnK8m~Cv5uYuAOTzUG

是不是成果更好?

正文完
 0