分库分表之后如何设计主键ID(分布式ID)?

发布于:2024-06-30 ⋅ 阅读:(17) ⋅ 点赞:(0)


在进行数据库的分库分表操作后,必然要面临的一个问题就是主键id如何生成,一定是需要一个全局的id来支持,所以分库分表之后,首当其冲的问题就是设计一个适合的主键id方案。简单的说就是分库分表之后所谓的分布式ID如何生成

当然主键id方案也是五花八门,我个人的看法是没有最好的方案,只有最适合的方案

1、数据库的自增序列+步长方案

前提:假如分表的数量不是很多或者比较固定
在数据库的自增序列(Auto-increment)中,步长(Step Value)是指每次自增操作后ID增加的数值。步长的设置可以用于控制ID的增长速度和分配方式

比如最常见的就是单步长,步长设置为1。每次插入新记录时,自增字段的值都会比上一个记录的值大1

那么假设当前我们分了三张表,可以将步长设置为3,表1的id就会是1,4,7 表2是2,5,8 表3是3,6,9

这个方案的好处是不需要其它操作,实现起来比较简单,也能达到性能的目标,而且又可以使用数据库本身的自增序列的优点长处

但是缺点也很明显,首先分表的数量必须是固定的,步长也是固定的,将来如果数据量又上来了就不好继续拆分表。而且在实际业务中,大部分需要分表的情况数据量都是比较大的,所以这个方案只适合极少部分场景

2、分表键结合自增序列

将业务相关的键(如用户名、时间戳等)与自增序列结合生成唯一ID,比如用户名+ID
或者每个分表有个唯一标识,比如表A分三张表A1、A2、A3,那么分别的ID就是A1_1, A2_1。。。等

这个方案的优点也很明显,生成的ID与业务逻辑紧密相关,便于理解和查询,且业务键和自增序列的组合使得ID生成逻辑相对简单,易于维护,后续的扩展增加分表等也相对简单

缺点:
当需要进行跨表操作时,由于并不知道数据在哪张表(分表标识的场景,如果是业务键信息+自增ID,可能需要额外的转换或关联操作)中,联合查询就需要额外的逻辑来处理。并且如果需要对分表进行合并或拆分,由于ID的结构,数据迁移可能会比较复杂。而且说到底其实这个并没有很好的解决高并发场景

3、UUID

每次聊到分布式主键或者分库分表,UUID总是离不开话题,我见过大多数人的第一反应也是说UUID。UUID也有几种不同的算法可以获得,这里就不过多赘述。作为主键,它几乎可以保证全局唯一性,而且确实是非常方便。
但是他的缺点就是长度较长,占用的空间又大,性能确实很一般。而且最最最重要的是它不具有有序性,这个是很致命的问题,会导致B+树索引在写的时候有过多的随机写操作,这里详细解释一下
UUID导致的随机写操作,作为主键在写入操作时通常会导致B+树的插入(Insert)操作而不是有序的追加(Append)操作,这会将整个B+树的节点读到内存里面,然后在插入这个节点后,再将整个节点回写到磁盘,这个操作在记录数据占用空间较大的情况下会显著降低性能

  1. 随机性:UUID是随机生成的,这意味着每次生成的UUID值在整个数据空间内是均匀分布的
  2. 索引页分裂:当UUID作为主键索引时,由于其随机性,新插入的记录很可能会落在不同的索引页上,导致索引页频繁分裂和合并
  3. 写入放大:随机写操作可能导致写入放大,因为每次写入都可能需要写入新的索引页,而不是简单地在现有页上追加
  4. 磁盘I/O增加:随机写操作会增加磁盘I/O操作,因为磁盘头需要移动到不同的位置进行写入,这比顺序写入效率低
  5. 缓存失效:由于UUID的随机性,数据库缓存可能频繁失效,因为缓存页可能很快被新的随机UUID值替换

但是呢,自增id就不会有这个问题

  1. 顺序性:自增ID是顺序生成的,每次插入新记录时,ID值都是前一个记录ID值的下一个整数
  2. 顺序写入:顺序生成的ID保证了新记录可以顺序地写入索引树,减少了索引页的分裂和合并
  3. 写入效率:顺序写入可以提高写入效率,因为新记录通常可以追加到现有的索引页上,而不是写入新的索引页
  4. 磁盘I/O减少:顺序写入减少了磁盘I/O操作,因为磁盘头不需要频繁移动到不同的位置。
  5. 缓存效率:顺序写入提高了缓存效率,因为新记录可以被顺序地写入缓存页,减少了缓存失效的可能性
  6. 索引优化:由于自增ID的顺序性,数据库可以更有效地优化索引结构,例如通过延迟索引页的分裂
  7. 数据局部性:自增ID有助于保持数据的局部性,新记录更有可能与现有记录存储在相邻的磁盘块上,这有助于提高查询性能
    所以UUID的效率大打折扣

4、雪花算法

说到uuid,那么肯定就会想到大名鼎鼎的雪花算法(Twitter的Snowflake算法),这实际是一种分布式ID生成器。它将时间戳、数据中心id或者机器id、序列号、位运算等结合来生成一个全局唯一的ID,

通常,一个雪花算法生成的ID可以分为以下几部分:

  • 第1位:未使用,固定为0。 第2到第41位:时间戳,41位可以提供69年的时间(从2016年开始)
  • 第42到第51位:数据中心ID,10位可以提供1024个数据中心。这个可根据实际业务调整
  • 第52到第61位:机器ID,10位可以提供1024个机器。这个可根据实际业务调整
  • 第62到第64位:序列号,12位可以提供4096个序列号。在每个时间戳内,序列号从0开始自增,直到达到最大值(通常是4095),然后等待下一个时间戳的到来。如果时间戳发生回拨,算法会等待直到时间戳再次递增,以避免生成重复的ID

优点:

  1. 全局唯一性:雪花算法生成的ID是全局唯一的,适用于分布式系统
  2. 高性能:算法简单,生成ID速度快,对性能影响小
  3. 趋势递增:由于ID中包含了时间戳,生成的ID是递增的,这有助于优化数据库索引和缓存
  4. 信息丰富:ID中包含了时间戳、数据中心ID、机器ID和序列号,可以提供丰富的信息
  5. 避免热点:由于序列号是在一个机器上独立生成的,避免了对中心ID生成服务的依赖,减少了热点问题
  6. 可定制性:可以根据需要调整数据中心ID和机器ID的位数,以适应不同的分布式规模。
  7. 容错性:算法可以容忍一定程度的机器ID重复或回拨,具有一定的容错性

缺点:

  1. 依赖机器时钟:算法依赖于机器的时钟,如果时钟回拨,可能会导致ID重复或生成负数。
  2. 时钟回拨问题:如果服务器的时钟发生回拨,可能会生成重复的ID。
  3. 序列号限制:序列号在一个时间片内是有限的,如果生成速度非常快,可能会耗尽序列号。
  4. 数据中心和机器ID分配:需要预先分配数据中心ID和机器ID,并确保它们不会重复。
  5. ID解析复杂:解析ID以获取时间戳、数据中心ID、机器ID等信息相对复杂。
  6. ID长度固定:由于ID由多个部分组成,其长度是固定的,可能不如某些其他方法灵活。
  7. ID生成间隔限制:由于依赖时间戳,如果系统时间变化过快,可能会在短时间内生成相同的ID。
  8. 时间戳精度限制:时间戳的精度限制了ID生成的频率,如果需要更高频率的ID生成,可能需要优化算法

通过上面可以看到雪花算法其实也比较长,但是它是递增的,相对来说很友好

但是要注意,每毫秒每个机器最多4096个序列,虽然这个并发绝大部分业务来说已经够用,如果有极端场景可能需要关注一下

其次就是时钟回拨问题,比如分布式不同的机器有时间同步,如果哪一天同步校正时间的时候将时间往前回拨了,这个时候要么会产生重复的ID,要么等待时间戳重新到达历史记录点然后再生成,所以也需要重点关注一下

5、redis的incr方案

Redis 的 INCR 命令用于将键的整数值原子性地递增。使用 Redis 作为分布式ID生成器时,INCR 命令可以提供一种简单的方式来生成唯一的递增ID,我个人是比较推崇这个方案的

优点比较明确,简单易用,性能稳定性都很不错。INCR操作是原子的,我们不需要再去考虑并发的冲突,redis帮我们解决了问题,并且redis速度很快,也很灵活和易于监控

缺点呢就是业务ID的生成强依赖于redis的服务,如果redis服务挂了宕机了之类的,将会导致业务也不可用,并且在高并发下压力就会给到redis这边,因为所有的ID生成请求都会发送到redis
所以这个方案最重要的就是保护redis的安全,虽然实际redis很少宕机,但是如果遇到了还是非常头疼的问题,所以redis的持久化机制、高可用方案等也是很重要的

总结

总结一下,方案很多,还是得根据实际业务来,脱离业务谈方案其实有点耍流氓的意思,每个方案都有优缺点。不过要是非要真的想一劳永逸的解决这个问题,而且业务体量确实有这么大,雪花算法确实也许是最优的解决办法


网站公告

今日签到

点亮在社区的每一天
去签到