动机
为什么想到用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实现:
- 生成一个齐全随机的UUID,长度为128bit
- 以以后unix time作前缀,这样总长度为192bit
- 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
是不是成果更好?