Kevin's blog Kevin's blog
首页
  • AI基础
  • RAG技术
  • 提示词工程
  • Wireshark抓包
  • 常见问题
  • 数据库
  • 代码技巧
  • 浏览器
  • 手册教程
  • 技术应用
  • 流程规范
  • github技巧
  • git笔记
  • vpn笔记
  • 知识概念
  • 学习笔记
  • 环境搭建
  • linux&运维
  • 微服务
  • 经验技巧
  • 实用手册
  • arthas常用
  • spring应用
  • javaAgent技术
  • 网站
友情链接
  • 分类
  • 标签
  • 归档

Kevin

你可以迷茫,但不可以虚度
首页
  • AI基础
  • RAG技术
  • 提示词工程
  • Wireshark抓包
  • 常见问题
  • 数据库
  • 代码技巧
  • 浏览器
  • 手册教程
  • 技术应用
  • 流程规范
  • github技巧
  • git笔记
  • vpn笔记
  • 知识概念
  • 学习笔记
  • 环境搭建
  • linux&运维
  • 微服务
  • 经验技巧
  • 实用手册
  • arthas常用
  • spring应用
  • javaAgent技术
  • 网站
友情链接
  • 分类
  • 标签
  • 归档
  • JVM性能调优

  • 并发编程

  • MySql

  • spring

  • redis

  • zookeeper

  • rabbitMQ

  • 架构

  • 锁

  • 分库分表

    • 为何需要分库分表
    • 分库分表的 21 条法则
    • SpringBoot 2 种方式快速实现分库分表
    • 分片管理
    • 分片算法
    • 分库分表 3种自定义的分片算法
    • 分库分表 6种分布式主键生成策略
      • 为什么用分布式主键ID
      • 内置算法
        • UUID
        • NanoID
        • 定制雪花算法
        • CosId
        • CosId-Snowflake
      • 自定义分布式主键
        • 实现接口
        • SPI 注册
        • 配置使用
      • 总结
  • 学习笔记
  • 分库分表
kevin
2024-07-10
目录

分库分表 6种分布式主键生成策略

# 为什么用分布式主键ID

在传统的单库单表结构时,通常可以使用自增主键来保证数据的唯一性。但在分库分表的情况下,每个表的默认自增步长为1,这导致了各个库、表之间可能存在重叠的主键范围,从而使得主键字段失去了其唯一性的意义。

为了解决这一问题,我们需要引入专门的分布式 ID 生成器来生成全局唯一的ID,并将其作为每条记录的主键,以确保全局唯一性。通过这种方式,我们能够有效地避免数据冲突和重复插入的问题,从而保障系统的正常运行。

除了满足唯一性的基本要求外,作为主键 ID,我们还需要关注主键字段的数据类型、长度对性能的影响。因为主键字段的数据类型、长度直接影响着数据库的查询效率和整体系统性能表现,这一点也是我们在选方案时需要考虑的因素。

# 内置算法

在ShardingSphere 5.X版本后进一步丰富了其框架内部的主键生成策略方案。此前仅提供了UUID和Snowflake两种策略,现在又陆续提供了NanoID、CosId、CosId-Snowflake三种策略。下面我们将逐个的过一下。

注意:SQL中不要主动拼接主键字段(包括持久化工具自动拼接的)否则一律走默认的Snowflake策略!!!

ShardingSphere中为分片表设置主键生成策略后,执行插入操作时,会自动在SQL中拼接配置的主键字段和生成的分布式ID值。所以,在创建分片表时主键字段无需再设置 自增 AUTO_INCREMENT。同时,在插入数据时应避免为主键字段赋值,否则会覆盖主键策略生成的ID。

CREATE TABLE `t_order` (
  `id` bigint NOT NULL,
  `order_id` bigint NOT NULL,
  `user_id` bigint NOT NULL,
  `order_number` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
  `customer_id` bigint NOT NULL,
  `order_date` datetime DEFAULT NULL,
  `interval_value` varchar(125) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `total_amount` decimal(10,2) NOT NULL,
  PRIMARY KEY (`order_id`) USING BTREE
) ;
1
2
3
4
5
6
7
8
9
10
11

# UUID

想要获得一个具有唯一性的ID,大概率会先想到UUID,因为它不仅具有全球唯一的特性使用还简单。但并不推荐将其作为主键ID。

  • • UUID的无序性。在插入新行数据后,InnoDB无法像插入有序数据那样直接将新行追加到表尾,而是需要为新行寻找合适的位置来分配空间。由于ID无序,页分裂操作变得不可避免,导致大量数据的移动。频繁的页分裂会导致数据碎片化(即数据在物理存储上分散分布)。这种随机的ID分配过程需要大量的额外操作,导致频繁的对数据进行无序的访问,导致磁盘寻道时间增加。数据的无序性进一步加剧了数据碎片化,降低了数据访问效率。
  • • UUID字符串类型。字符串比数字类型占用更多的存储空间,对存储和查询性能造成较大的消耗;字符串类型的长度可变,可变长度的数据行会破坏索引的连续性,导致索引查找性能下降。

算法类型:UUID

spring:
  shardingsphere:
    rules:
      sharding:
        key-generators:  # 分布式序列算法配置
          # UUID生成算法
          uu-id-gen:
            type: UUID
        tables:
          t_order:  # 逻辑表名称
            actual-data-nodes: db$->{0..1}.t_order_${0..2} # 数据节点:数据库.分片表
            database-strategy:  # 分库策略
              standard:
                sharding-column: order_id
                sharding-algorithm-name: t_order_database_mod
            table-strategy: # 分表策略
              standard:
                sharding-column: order_id
                sharding-algorithm-name: t_order_table_mod
            key-generate-strategy: # 分布式主键生成策略
              column: id
              keyGeneratorName: uu-id-gen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# NanoID

或许很多人都不熟悉 NanoID,它是一款用类似 UUID 生成唯一标识符的轻量级库。不过,与 UUID 不同的是 NanoID 生成的字符串ID长度较短,仅为21位。但仍然不推荐将它作为主键ID,理由和UUID一样。

算法类型:NANOID

spring:
  shardingsphere:
    rules:
      sharding:
        key-generators:  # 分布式序列算法配置
          # nanoid生成算法
          nanoid-gen:
            type: NANOID
        tables:
          t_order:  # 逻辑表名称
            actual-data-nodes: db$->{0..1}.t_order_${0..2} # 数据节点:数据库.分片表
            key-generate-strategy: # 分布式主键生成策略
              column: id
              keyGeneratorName: nanoid-gen
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 定制雪花算法

雪花算法是比较主流的分布式ID生成方案,在 ShardingSphere 中的Snowflake算法生成的是 Long 类型的 ID,通常作为默认的主键生成策略使用。

内置的雪花算法生成的ID主要由时间戳、工作机器IDworkId、序列号sequence三部分组成。

@Override
  public synchronized Long generateKey() {
      ..........
      return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
  }
1
2
3
4
5

定制 Snowflake 算法有三个可配置的属性:

worker-id:工作机器唯一标识,单机模式下会直接取此属性值计算ID,默认是0;集群模式下则由系统自动生成,此属性无效

max-vibration-offset:最大抖动上限值,范围[0, 4096),默认是1。那么如何理解这个属性呢?这个属性是用来控制上边生成雪花ID中的sequence。通过限制抖动范围,同一毫秒内生成的ID中引入微小的变化,让数据更均匀地分散到不同的分片上。

private void vibrateSequenceOffset() {
    sequenceOffset = sequenceOffset >= maxVibrationOffset ? 0 : sequenceOffset + 1;
}
1
2
3

若使用此算法生成值作分片值,建议配置此属性。此算法在不同毫秒内所生成的 key 取模 2^n (2^n一般为分库或分表数) 之后结果总为 0 或 1。为防止上述分片问题,建议将此属性值配置为 (2^n)-1

max-tolerate-time-difference-milliseconds:最大容忍时钟回退时间(毫秒)。服务器在校对时间时可能会发生时钟回拨的情况(当前时间回退),由于根据时间戳参与计算ID,这可能导致生成相同的ID,而这对系统来说是不可接受的。

ShardingSphere 雪花算法针对时钟回拨场景进行了处理,记录最后一次生成ID的时间 lastMilliseconds,并与回拨后的当前时间 currentMilliseconds 进行比对。如果时间差超过了设置的最大容忍时钟回退时间,系统将直接抛出异常;如果未超过,则系统会休眠等待两者时间差的时长,核心原则确保不会发放重复的ID。

@SneakyThrows(InterruptedException.class)
private boolean waitTolerateTimeDifferenceIfNeed(final long currentMilliseconds) {
    if (lastMilliseconds <= currentMilliseconds) {
        return false;
    }
    long timeDifferenceMilliseconds = lastMilliseconds - currentMilliseconds;
    Preconditions.checkState(timeDifferenceMilliseconds < maxTolerateTimeDifferenceMilliseconds,
            "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastMilliseconds, currentMilliseconds);
    Thread.sleep(timeDifferenceMilliseconds);
    return true;
}
1
2
3
4
5
6
7
8
9
10
11

算法类型:SNOWFLAKE

spring:
  shardingsphere:
    rules:
      sharding:
        key-generators:  # 分布式序列算法配置
          # 雪花ID生成算法
          snowflake-gen:
            type: SNOWFLAKE
            props:
              worker-id: # 工作机器唯一标识
              max-vibration-offset: 1024 # 最大抖动上限值,范围[0, 4096)。注:若使用此算法生成值作分片值,建议配置此属性。此算法在不同毫秒内所生成的 key 取模 2^n (2^n一般为分库或分表数) 之后结果总为 0 或 1。为防止上述分片问题,建议将此属性值配置为 (2^n)-1
              max-tolerate-time-difference-milliseconds: 10 # 最大容忍时钟回退时间,单位:毫秒
        tables:
          t_order:  # 逻辑表名称
            actual-data-nodes: db$->{0..1}.t_order_${0..2} # 数据节点:数据库.分片表
            key-generate-strategy: # 分布式主键生成策略
              column: id
              keyGeneratorName: snowflake-gen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# CosId

CosId 是一个高性能的分布式ID生成器框架,Shardingsphere 将其引入到自身的框架内,只简单的使用了 CosId 算法。但目前亲测 5.2.0版本该算法处于不可用状态!!!我已经给官方提了issue,看看他们咋回复吧。

CosId 框架内提供了 3 种算法:

  • • SnowflakeId: 单机 TPS 性能:409W/s , 主要解决时钟回拨问题 、机器号分配问题并且提供更加友好、灵活的使用体验。
  • • SegmentId: 每次获取一段 (Step) ID,来降低号段分发器的网络IO请求频次提升性能,提供多种存储后端:关系型数据库、Redis、Zookeeper 供用户选择。
  • • SegmentChainId(推荐): SegmentChainId (lock-free) 是对 SegmentId 的增强。性能可达到近似 AtomicLong 的 TPS 性能 12743W+/s。

该算法使用对外提供了两个属性:

  • • id-name:ID 生成器名称。
  • • as-string:是否生成字符串类型ID,将 long 类型 ID 转换成 62 进制 String 类型(Long.MAX_VALUE 最大字符串长度11位),并保证字符串 ID 有序性。

算法类型:COSID

spring:
  shardingsphere:
    rules:
      sharding:
        key-generators:  # 分布式序列算法配置
          # COSID生成算法
          cosId-gen:
            type: COSID
            props:
              id-name: share
              as-string: false
        tables:
          t_order:  # 逻辑表名称
            actual-data-nodes: db$->{0..1}.t_order_${0..2} # 数据节点:数据库.分片表
            key-generate-strategy: # 分布式主键生成策略
              column: id
              keyGeneratorName: cosId-gen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# CosId-Snowflake

CosId-Snowflake 是 CosId 框架内提供的 Snowflake 算法,它的实现原理和上边的定制版雪花算法类似,ID主要也是由时间戳、工作机器ID、序列号sequence三部分组成。同样处理了时钟回拨等问题。

public synchronized long generate() {
    long currentTimestamp = this.getCurrentTime();
    if (currentTimestamp < this.lastTimestamp) {
        throw new ClockBackwardsException(this.lastTimestamp, currentTimestamp);
    } else {
        if (currentTimestamp > this.lastTimestamp && this.sequence >= this.sequenceResetThreshold) {
            this.sequence = 0L;
        }

        this.sequence = this.sequence + 1L & this.maxSequence;
        if (this.sequence == 0L) {
            currentTimestamp = this.nextTime();
        }

        this.lastTimestamp = currentTimestamp;
        long diffTimestamp = currentTimestamp - this.epoch;
        if (diffTimestamp > this.maxTimestamp) {
            throw new TimestampOverflowException(this.epoch, diffTimestamp, this.maxTimestamp);
        } else {
            return diffTimestamp << (int)this.timestampLeft | this.machineId << (int)this.machineLeft | this.sequence;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这个算法提供了两个属性:

  • • epoch:固定的起始时间点,雪花ID算法的 epoch 变量值,默认值:1477929600000。用它的目的提高生成的ID的时间戳部分的可读性、稳定性和范围限制,使得生成的ID更加可靠和易于管理。
  • • as-string:是否生成字符串类型ID,将 long 类型 ID 转换成 62 进制 String 类型(Long.MAX_VALUE 最大字符串长度11位),并保证字符串 ID 有序性。

算法类型:COSID_SNOWFLAKE

spring:
  shardingsphere:
    rules:
      sharding:
        key-generators:  # 分布式序列算法配置
          # cosId-snowflake生成算法
          cosId-snowflake-gen:
            type: COSID_SNOWFLAKE
            props:
              epoch: 1477929600000
              as-string: false
        tables:
          t_order:  # 逻辑表名称
            actual-data-nodes: db$->{0..1}.t_order_${0..2} # 数据节点:数据库.分片表
            key-generate-strategy: # 分布式主键生成策略
              column: id
              keyGeneratorName: cosId-snowflake-gen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 自定义分布式主键

上边咱们介绍了 ShardingSphere 内提供的 5 种生成主键的ID算法,这些算法基本可以满足大部分的业务场景。不过,在某些情况下,我们可能会要求生成的ID具有特殊的含义或遵循特定的规则。ShardingSphere 也支持我们自定义生成主键ID,来满足定制的业务需求。

# 实现接口

要实现自定义的主键生成算法,首先需要实现 KeyGenerateAlgorithm 接口,并实现内部 4 个方法, 其中有两个方法比较关键:

  • • getType():我们自定义的算法类型,方便配置使用;
  • • generateKey():处理主键生成的核心逻辑,我们可以根据业务需求选择合适的主键生成算法,比如美团的 Leaf、滴滴的 TinyId 等。
@Data
@Slf4j
public class SequenceAlgorithms implements KeyGenerateAlgorithm {

    // 这个方法用于指定我们自定义的算法的类型。它会返回一个字符串,表示所使用算法的类型,方便在配置和识别时使用。
    @Override
    public String getType() {
        // 返回算法类型表示
        return "custom";
    }

    // 这是生成主键的核心逻辑所在。在这个方法内部,我们可以根据业务需求选择合适的主键生成算法,比如美团的Leaf、滴滴的TinyId等。这个方法的具体实现会根据所选算法的特点和要求来设计
    @Override
    public Comparable<?> generateKey() {
        return null;
    }

    @Override
    public Properties getProps() {
        return null;
    }
    // 这个方法用于初始化主键生成算法所需的资源或配置
    @Override
    public void init(Properties properties) {
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

在引入外部的分布式ID生成器时,应尽量遵循以下原则:

  • • 全局唯一:必须保证ID是全局性唯一的,基本要求
  • • 高性能:高可用低延时,ID生成响应要块,否则反倒会成为业务瓶颈
  • • 高可用:100%的可用性是骗人的,但是也要无限接近于100%的可用性
  • • 好接入:要秉着拿来即用的设计原则,在系统设计和实现上要尽可能的简单

# SPI 注册

通过 SPI 方式加载我们自定义的主键算法,需要在 resource/META-INF/services 目录下创建一个文件,文件名为 org.apache.shardingsphere.sharding.spi.KeyGenerateAlgorithm,并将我们自定义的主键算法的完整类路径放入文件内,每行一个。在系统启动时会自动加载到这个文件,读取其中的类路径,然后通过反射机制实例化对应的类,完成主键算法的注册和加载。

resource
    |_META-INF
        |_services
           |_org.apache.shardingsphere.sharding.spi.KeyGenerateAlgorithm
1
2
3
4

# 配置使用

上边完成了自定义算法的逻辑,使用上与其他的算法一致。只需将我们刚刚定义的算法类型 custom 配置上即可。

spring:
  shardingsphere:
    rules:
      sharding:
        key-generators:  # 分布式序列算法配置
          # 自定义ID生成策略
          xiaofu-id-gen:
            type: custom
        tables:
          t_order:  # 逻辑表名称
            actual-data-nodes: db$->{0..1}.t_order_${0..2} # 数据节点:数据库.分片表
            key-generate-strategy: # 分布式主键生成策略
              column: id
              keyGeneratorName: xiaofu-id-gen
1
2
3
4
5
6
7
8
9
10
11
12
13
14

当执行插入操作时,debug 看已经进入到了定义的主键算法内了。

图片

# 总结

我们介绍了 ShardingSphere 的几种内置主键生成策略以及如何自定义主键生成策略,市面上还有许多优秀的分布式ID框架都可以整合进来,但具体选择何种策略还是要取决于自身的业务需求。

上次更新: 2025/01/13, 10:51:06
分库分表 3种自定义的分片算法

← 分库分表 3种自定义的分片算法

最近更新
01
AI是如何学习的
06-05
02
chatGpt提示原则
06-05
03
提示词工程实践指南
06-05
更多文章>
| Copyright © 2022-2025 Kevin | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式