搞定了 6 种分布式id,分库分表哪个适合做主键? | java 技术论坛-380玩彩网官网入口
大家好,我是小富~
本文是《shardingsphere5.x分库分表原理与实战》系列的第七篇,目前系列的前几篇制作成了pdf,需要的可以在文末获取下载方式,持续更新中。今天咱们继续一起来探究下,分布式id在分库分表中起到的作用以及如何使用,shardingsphere-jdbc
中已经为我们提供了多种分布式主键id生成策略。接下来将分别介绍这些策略的优缺点,看看它们在实际应用中的场景和效果。
小富的技术站:https://xiaofucode.com
为什么用分布式主键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) ;
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
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
定制雪花算法
雪花算法是比较主流的分布式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; }
定制 snowflake 算法有三个可配置的属性:
worker-id
:工作机器唯一标识,单机模式下会直接取此属性值计算id,默认是0;集群模式下则由系统自动生成,此属性无效
max-vibration-offset
:最大抖动上限值,范围[0, 4096)
,默认是1。那么如何理解这个属性呢? 这个属性是用来控制上边生成雪花id中的sequence
。通过限制抖动范围,同一毫秒内生成的id中引入微小的变化,让数据更均匀地分散到不同的分片上。
private void vibratesequenceoffset() { sequenceoffset = sequenceoffset >= maxvibrationoffset ? 0 : sequenceoffset 1;}
若使用此算法生成值作分片值,建议配置此属性。此算法在不同毫秒内所生成的 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;}
算法类型: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
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
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; } }}
这个算法提供了两个属性:
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
自定义分布式主键
上边咱们介绍了 shardingsphere 内提供的 5 种生成主键的id算法,这些算法基本可以满足大部分的业务场景。不过,在某些情况下,我们可能会要求生成的id具有特殊的含义或遵循特定的规则。shardingsphere 也支持我们自定义生成主键id,来满足定制的业务需求。
实现接口
要实现自定义的主键生成算法,首先需要实现 keygeneratealgorithm
接口,并实现内部 4 个方法, 其中有两个方法比较关键:
gettype()
:我们自定义的算法类型,方便配置使用;generatekey()
:处理主键生成的核心逻辑,我们可以根据业务需求选择合适的主键生成算法,比如美团的 leaf、滴滴的 tinyid 等。
@data@slf4jpublic 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) { }}
在引入外部的分布式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
配置使用
上边完成了自定义算法的逻辑,使用上与其他的算法一致。只需将我们刚刚定义的算法类型 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
当执行插入操作时,debug 看已经进入到了定义的主键算法内了。
总结
我们介绍了 shardingsphere 的几种内置主键生成策略以及如何自定义主键生成策略,市面上还有许多优秀的分布式id框架都可以整合进来,但具体选择何种策略还是要取决于自身的业务需求。关于分布式 id 生成器,我曾经撰写过一篇 ,详细介绍了多种生成器的优缺点,大家可以作为参考。
案例github地址:
本作品采用《cc 协议》,转载必须注明作者和本文链接