不推荐uuid等无序序列作为主键
- innodb存储数据的内存是按页管理,每个页16K,区是由连续页组成的空间,大小都是1MB;
- 磁盘预读:为了保证区中页的连续性, InnoDB存储引擎一次从磁盘申请4~5个区,,将数据页的相邻的其他的数据页也加载到缓存中, 优先命中缓存再查磁盘。(局部性原理: 在空间上、时间上的相邻存储位置在未来被多次引用)
- 缓冲池使用LRU (Latest Recent Used,最长时间没有使用)算法来管理已经读取到未发生变动的页。最频繁使用的页在前端,而最少使用的页在尾端。当缓冲池不能存放新读取到的页时,将首先释放LRU列表中尾端的页。实际上是LRU的变种[冷热数据区innodb_old_blocks_pct + 移动队首间隔时间innodb_old_blocks_time]的方式来降低非热点页带来的负面影响。
- 聚集索引的存储并不是物理连续,只是逻辑连续;由于实际的数据页只能按照一棵B+树进行排序,因此每张表只能拥有一个聚集索引。主键索引是聚集索引,它的叶子节点是按主键排序的行全部记录,即为整张表的行记录数据,也称为数据页!
UUID是一个128位的字符串,长度过长,会占用大量的存储空间,增加存储成本,同时也会降低查询效率(eg. 原来1页可以存20个,现在只能存10个,那不得再读下一页嘛)。
无序序列将导致无法做到总是把新行主键插入到主键索引的最后,此时需要为新行寻找新的合适的位置从而来分配新的空间,这个过程需要做更多额外的操作。
新增时索引的分裂操作会更无序,影响索引页范围不可控,也将导致数据页记录的频繁乱序移动。
同时,页会变得稀疏并被不规则的填充,最终会导致数据会有碎片。
这在存储性能、查询效率上都是损耗!
雪花算法 snowflake
在分片规则配置模块可配置每个表的主键生成策略,默认使用雪花算法生成 64bit 的长整型数据。
64bit = 1bit 弃用(默认为0, 符号位) + 41bit 时间戳 (当前时间 - 约定的固定开始时间) + 10bit 的机器Id (5bit dataCenterId + 5bit workId) + 12bit 递增sequence
在末尾生成不碰撞序列时: 如果同一个时间戳毫秒级里sequence溢出了则阻塞至下一个毫秒,开始序列seq重置。
它保证了趋势递增,同时它也有缺陷:
1、在单机上是递增的,但由于涉及到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况。
该缺点可以认为无所谓,一般分布式ID只要求趋势递增, 并不会要求严格递增。
2、服务器时钟回拨会导致产生重复序列。
解决方案:
- 生成器需要记录上一次生成序列的时间,配置一个容忍阈值
- 如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;
- 如果在可容忍的范围内,默认分布式主键生成器会等待时钟同步到最后一次主键生成的时间lastTimestamp后再继续创建。
- 每次发生时间回拨就切换一次workId。
3、workId(中间10位)耗尽。
要规划好workId的分发及回收的管理策略!
- 有解决方案是: 向前 41bit时间戳 借用几位,来扩张workId的位数,延缓它的耗尽。
- 对于分配: 单机:直接默认一个值;分布式:
- 数据库表Auto_increment一个值、或 redis递增一个数,然后对 2048 - 1 = 2047 取模。(对N取模取决于要用多少位的机器码)
- 将workId值配置在机器系统环境变量里。最好能实现容器化,在各种自动化分配资源时(eg. 自动扩容、滚动发布、故障转移)自动分配业务相关的全局唯一值;
美团Leaf的解决方案
Tddl - 递增分布式 Sequence
- 在数据库表里定义业务Sequence序列。相同业务的不同服务实例定义不同的服务序列号(从0开始递增1)。它们每次都从该序列里拿一批ids缓存到本地来递增分配,并更新表内seq序列;
- 每个服务的beginVal都是不一样的,通过计算保证每个服务拿到的id都在它隶属的间隔数值区间内按步长递增: eg. step:1000, A: [0,1000) -> [3000,4000)... , B: [1000,2000) -> [4000,5000)... , C: [2000,3000) -> [5000, 6000)... 。
newBeginVal = oldBeginVal + 服务数*步长, 若oldBeginVal < 数据库序列值,优先将oldBeginVal 调整到该序列值后最近一个它的区间开始值。