序言

看了b站IT老齐的架构三百讲的其中一个短视频,有所体会并记录一下。视频中所讲的财经部门使用的UUID主键,在日终结算时出现磁盘的IO异常,导致应用出现高延迟。最后发现是UUID的问题,UUID作为主键在数据进行插入的时候对于系统的压力是非常大的。

UUID(Universally Unique Identifier)介绍

uuid当索引 页分裂 uuid作为索引_主键

 UUID结构如上图所示,比如7bf13c38-00a1-484e-b1e2-80c1ab8e754c,作为唯一标识符,类似网卡上的MAC地址。

UUID的生成方式主要有下面几种:

  1. 基于时间的UUID:利用时间戳和网卡的MAC地址生成唯一的UUID,超高并发的系统在同一设备上生成的UUID可能重复。所以基本上不怎么使用。
  2. 基于DCE(Distributed ComPuting Environment)安全的UUID:基于身份验证和安全服务生成的UUID,使用用户的某些特征如账号邮箱等信息,涉及侵犯用户隐私,无法大规模推广。
  3. 基于名字的UUID(MD5):加密算法使用MD5生成的UUID。
  4. 基于名字的UUID(SH1):加密算法使用SH1生成的UUID,在相同的命名空间下可能会出现UUID冲突。故3和4也不常用。
  5. 随机生成的UUID:最常用的UUID生成算法,完全随机生成,存在极小概率重复的情况;与外部环境无关,不涉及环境信息,生成内容无序无规律。

Java提供了生成UUID的方法,如下

public class UUIDGenerator {
    public static void main(String[] args) {
        UUID uuid = UUID.randomUUID();
        System.out.println("SecureRandom生成的UUID为:" + uuid);
    }
}

 UUID主键(UUID Primary Key)

数据库主键,指的是一个列或多列的组合,其值能唯一地标识表中的每一行,通过它可强制表的实体完整性。主键主要是用与其他表的外键关联,以及文本记录的修改与删除。

UUID作为主键在大数据量的情况下为什么会产生问题,先看一下UUID模式,

  • 全局唯一性
  • 信息安全
  • 非趋势递增
  • 影响索引效率(InnoDB引擎)

底层数据结构B+Tree,在叶子结点上以有序的方式进行存储,InnoDB的索引也叫簇集索引(索引值和数据是紧密联系的),处理效率较高。如果存储的是UUID,那么存储的数据是无序的。


UUID是无序的,当UUID可能在索引中间某一页插入数据时,新增记录所在的数据页已满,数据库需要申请一个新的数据页存储数据,这种现象被称为“页分裂”。

页分裂确保后一个数据页中的所有的ID值一定比数据页中的ID值大。在大并发环境下增加了磁盘IO的压力,无序ID才是罪魁祸首。

解决办法:改为有序的数字主键生成策略就可以。如美团Leaf/推特SnowFlake。

SnowFlake

分布式自增ID算法snowFlake之前有同事也用到了这个,它的结构如下,

uuid当索引 页分裂 uuid作为索引_big data_02

  • 1位标识部分:由于long的最高位是符号位,正数是0,负数是1,一般生成的ID为正数,所以为0。
  • 41位时间戳部分:毫秒级的时间,一般实现上不会存储当前的时间戳,而是时间戳的差值(当前时间-固定的开始时间),这样可以使产生的ID从更小值开始;41位的时间戳可以使用69年。
  • 10位节点部分:Twitter实现中使用前5位作为数据中心标识,后5位作为机器标识,可以部署1024个节点。
  • 12位序列号部分:支持同一毫秒内同一个节点可以生成4096个ID。

我的那位同事出现的问题是使用雪花算法生成的主键ID长度增强导致插入数据库失败,由于雪花算法41位是根据时间戳生成的,随着时间毫秒数增长,正好在某个节点生成的ID长度增加一位导致数据库插入失败。所以在使用雪花算法时需要关注41位时间戳部分,何时会产生变化。