[TOC]
标题:结合业务探讨分布式ID技术与实现
引言:
在当今大数据时代,随着业务规模的不断扩大和数据量的不断增长,业务系统对于唯一标识符(ID)的需求越来越迫切。特别是在分布式系统中,生成唯一ID成为了一项挑战。
本文将深入探讨为什么需要分布式ID,业务系统对分布式ID的要求,以及业界几种常见的分布式ID生成方案。结合部门的实际的业务案例,将详细介绍如何根据业务需求选择合适的分布式ID技术,并通过段模式和雪花模式重构部门数据库,实现更高效的数据管理。
一、聊聊传统的主键自增ID
传统的MySQL主键ID模式通常采用自增主键的方式来生成唯一标识符。
在这种模式下,数据库表通常会定义一个名为”id”的列,将其设置为主键,并启用自动递增功能。每当向表中插入一条新记录时,MySQL都会自动为该记录分配一个唯一的ID值,并且这个ID值会自动递增,确保每个记录都具有不同的ID。
比如这张表而言
具体的表设计
CREATE TABLE `book` (
`bookid` int NOT NULL AUTO_INCREMENT COMMENT '图书ID',
`bookname` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '图书名称',
`price` decimal(6,2) NOT NULL COMMENT '价格',
`author` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作者',
`publisher` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '出版社',
`tid` int NOT NULL COMMENT '类别ID',
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态:0-未上架,1-已上架',
`del` tinyint(1) NOT NULL DEFAULT 0 COMMENT '删除标志:0-未删除,1-已删除',
`comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '评论',
PRIMARY KEY (`bookid`),
FOREIGN KEY (`tid`) REFERENCES `category`(`categoryid`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;
我们可以来分析一下,最后一行 ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;
ENGINE=InnoDB
:指定了使用的存储引擎为InnoDB。InnoDB是MySQL的一种常用存储引擎,提供了事务支持和行级锁等特性。AUTO_INCREMENT=9
:指定了表的自增主键从值9开始递增。这意味着当向表中插入新记录时,自增主键的初始值为9,并且每次插入新记录时,该主键值会自动递增1。DEFAULT CHARSET=utf8mb3
:指定了表的默认字符集为utf8mb3。utf8mb3是UTF-8的一种实现方式,支持最多3个字节表示一个字符,适用于大部分的中文和英文字符。ROW_FORMAT=DYNAMIC
:指定了行的格式为动态行格式。动态行格式是InnoDB存储引擎的一种行存储格式。在动态行格式中,每行的列不固定,根据实际数据大小进行灵活存储,可以节省存储空间并提高性能。
AUTO_INCREMENT=9,表示该表自增到9的位置。
1.1 主键ID自增存在的局限
如果是单体系统来说,主键ID可能会常用主键自动的方式进行设置,这种ID生成方法在单体项目是可行的。
但是对于在分布式系统中,可能存在多个数据库实例,每个数据库实例都有自己的自增ID生成器,这样就会造成跨库的ID不唯一问题,需要额外的处理来解决,所以这是不符合业务的。
1.2 业务系统对分布式ID的要求
- 全局唯一性:生成的ID必须在全局范围内是唯一的,不同的节点和不同的系统都不能生成相同的ID。
- 趋势递增:趋势递增,这对于MySQL等使用聚集索引的数据库来说尤为重要,可提高写入效率。
- 单调递增:保证下一个ID大于上一个ID,这种情况可以保证事务版本号,排序等特殊需求实现
- 可扩展性:ID生成方案需要具备良好的扩展性,能够适应系统规模的持续增长。无论是增加节点数量还是增加系统负载,ID生成器都能够轻松应对,不会成为系统的瓶颈。
- 可预测性:生成的ID应具有一定的可预测性,即在一定范围内,可以预测下一个生成的ID值是多少。(段模式)
二、调研业界常见的分布式ID生成方案
2.1 雪花算法(SnowFlake):
雪花算法是Twitter开源的一种分布式ID生成算法,采用64位的整数表示,其中包含时间戳、机器ID、数据中心ID和序列号等信息,保证了ID的全局唯一性和趋势递增。
优点:
高效性能:雪花算法通过位运算和时间戳生成ID,性能高效,适用于高并发场景。
全局唯一性:雪花算法生成的ID具有全局唯一性,不会产生重复。
缺点:
- 时钟回拨问题:如果系统时钟发生回拨,可能会导致生成的ID不唯一或不连续。
- 依赖时间戳:雪花算法的ID生成依赖于时间戳,如果时间戳不稳定,可能会影响ID的唯一性。
2.2 号段模式
号段模式将ID的生成分成两个步骤,首先申请一个区间(号段),然后在该区间内自增生成ID。号段模式适用于高并发场景,可以减少对数据库的访问压力,但需要额外的管理和调度机制。
- 优点:
- 分段管理:号段模式可以将ID生成过程分成两个阶段,提高了并发能力和性能。
- 适用性广泛:号段模式适用于各种分布式系统,并且可以灵活调整号段的大小和生成频率。
- 缺点:
- 管理复杂:需要额外的管理和调度机制来管理号段的分配和使用。
- 可能存在重复:如果号段生成不当,可能会导致ID的重复或碰撞。
2.3 UUID:
全球唯一标识符(UUID)是一种由128位数字表示的标准,通常以32位的十六进制数表示。UUID生成算法基于时间戳和设备唯一标识等信息,保证了全局唯一性。但由于其长度较长,不适合作为数据库的主键。
- 优点:
- 全局唯一性:UUID是全球唯一标识符,保证了生成的ID在全球范围内的唯一性。
- 无序性:UUID是随机生成的,不受顺序限制,适合于分布式系统。
- 缺点:
- 长度较长:UUID通常为128位,较长的长度可能会占用较大的存储空间。
- 不易读性:由于UUID是一串数字和字母的组合,不易于人类识别和记忆。
2.4 数据库自增
在数据库中使用自增主键生成ID,每次插入新记录时,数据库会自动分配一个唯一的ID值。这种方式简单易用,但不适用于分布式环境,可能存在单点故障和性能瓶颈。
- 优点:
- 简单易用:使用数据库自增主键生成ID非常简单,不需要额外的代码实现。
- 递增性:自增主键生成的ID是递增的,有助于提高查询效率。
- 缺点:
- 单点故障:在分布式系统中,数据库自增主键可能存在单点故障和性能瓶颈。
- 不适合分布式:数据库自增主键无法满足分布式系统的需求,不适合于跨数据库实例的应用。
2.5 Redis实现
利用Redis的原子操作和分布式锁机制,可以实现分布式ID的生成。通过维护一个递增的计数器或使用Redis的自增功能,可以生成全局唯一的ID。
- 优点:
- 高性能:Redis具有高效的原子操作和分布式锁机制,可以实现高性能的分布式ID生成。
- 可扩展性:Redis支持集群模式,可以轻松扩展到多个节点,适用于大规模分布式系统。
- 缺点:
- 单点故障:Redis作为单点服务可能存在单点故障的风险。
- 数据丢失:由于Redis是内存数据库,数据可能会丢失或不稳定。
此外,还有其他大厂之间的百度Uidgenerator,美团Leaf,滴滴TinyID等等。
三、方案选择:采取雪花算法+段模式
结合当前的系统业务场景,既要进行分布式id也要进行自增和保持历史数据的现状。采取雪花算法+段模式两种模式去实现分布式id的实现。
3.1 雪花算法(SnowFlake)
保证了生成的ID具有全局唯一性和趋势递增性,每个ID都是递增的,并且不会出现重复的情况。
3.2 段模式
段模式在分段管理的过程中也能够保证ID的唯一性和递增性,通过对号段进行动态管理和分配,可以充分利用号段的使用效率,提高了ID的生成性能和效率。
此外,段模式还可以一眼开出这个id是谁谁谁,清晰明了。
四、分布式ID落地与实现
4.1 golang实现雪花算法
通过一个简单的 SnowFlake 结构体,其中包含了生成唯一ID所需的参数和方法。通过调用 NextID() 方法,可以生成基于雪花算法的唯一ID
package main
import (
"fmt"
"sync"
"time"
)
// SnowFlake 结构体定义
type SnowFlake struct {
mu sync.Mutex
startTime int64 // 起始时间戳,单位为毫秒
datacenterID int64 // 数据中心ID
workerID int64 // 工作节点ID
sequence int64 // 序列号
lastStamp int64 // 上次生成ID的时间戳
}
// NewSnowFlake 函数用于创建一个新的 SnowFlake 对象
func NewSnowFlake(datacenterID, workerID int64) *SnowFlake {
return &SnowFlake{
startTime: 1609459200000, // 2021-01-01 00:00:00 的毫秒级时间戳
datacenterID: datacenterID,
workerID: workerID,
sequence: 0,
lastStamp: -1,
}
}
// NextID 方法用于生成下一个唯一ID
func (sf *SnowFlake) NextID() int64 {
sf.mu.Lock()
defer sf.mu.Unlock()
// 获取当前时间戳,单位为毫秒
now := time.Now().UnixNano() / 1e6
// 如果当前时间小于上次生成ID的时间戳,则等待
if now < sf.lastStamp {
for now <= sf.lastStamp {
now = time.Now().UnixNano() / 1e6
}
}
// 如果当前时间与上次生成ID的时间戳相同,则递增序列号
if now == sf.lastStamp {
sf.sequence = (sf.sequence + 1) & 4095 // 序列号取值范围为 0-4095
if sf.sequence == 0 {
now = sf.waitNextMillis(now)
}
} else {
sf.sequence = 0
}
// 更新上次生成ID的时间戳
sf.lastStamp = now
// 生成ID
id := ((now - sf.startTime) << 22) | (sf.datacenterID << 17) | (sf.workerID << 12) | sf.sequence
return id
}
// waitNextMillis 方法用于等待下一个毫秒
func (sf *SnowFlake) waitNextMillis(lastStamp int64) int64 {
now := time.Now().UnixNano() / 1e6
for now <= lastStamp {
now = time.Now().UnixNano() / 1e6
}
return now
}
func main() {
// 创建一个 SnowFlake 对象
sf := NewSnowFlake(1, 1) // 设置数据中心ID和工作节点ID
// 生成并打印 10 个唯一ID
for i := 0; i < 10; i++ {
fmt.Println("ID:", sf.NextID())
}
}
4.2 golang实现段模式
结合Segment 结构体,其中包含了生成唯一ID所需的参数和方法。通过调用 NextID() 方法,可以生成基于段模式的唯一ID
package main
import (
"fmt"
"sync"
)
// Segment 结构体定义
type Segment struct {
mu sync.Mutex
start int64 // 起始值
step int64 // 步长
current int64 // 当前值
}
// NewSegment 函数用于创建一个新的 Segment 对象
func NewSegment(start, step int64) *Segment {
return &Segment{
start: start,
step: step,
current: start,
}
}
// NextID 方法用于生成下一个唯一ID
func (s *Segment) NextID() int64 {
s.mu.Lock()
defer s.mu.Unlock()
id := s.current
s.current += s.step
return id
}
func main() {
// 创建一个 Segment 对象
segment := NewSegment(1000, 1) // 设置起始值和步长
// 生成并打印 10 个唯一ID
for i := 0; i < 10; i++ {
fmt.Println("ID:", segment.NextID())
}
}
4.3 实际的业务
在实际的业务上,通过设置一个分布式id的生成服务,每次涉及新增的逻辑,会先调研这个分布式服务生成id,在进行数据库插入等等。
当然在数据库层面也会设置:是否为雪花算法和段模式。
//分布式id改造
protected $distributed = true;
protected $distributedType = 1;
protected $distributedTag = "test:test:book";
protected $table = 'book';
public $timestamps = false;
- $distributed:这个变量表示是否启用分布式ID。如果设置为true,则表示启用分布式ID,否则表示不启用。在这段代码中,设置为true,即启用分布式ID。
- $distributedType:这个变量表示分布式ID的类型。在这里,设置为1,指定了雪花算法分布式ID生成算法或方案的类型。2是段模式。
- $distributedTag:这个变量表示分布式ID的标签或命名空间。在分布式系统中,通常会使用命名空间来区分不同的业务模块或数据表。
- $table:这个变量表示数据库表的名称。在这段代码中,设置为’book’,表示该模型对应的数据库表名称是’wx_label_v2’。
- $timestamps:这个变量表示是否启用模型的自动维护时间戳。在这段代码中,设置为false,表示不启用模型的自动维护时间戳,即不会自动生成created_at和updated_at字段。
五、总结
当我考虑雪花算法(SnowFlake)和段模式时,我发现它们都是用于生成分布式系统中唯一ID的重要方案。这两种方案各有优劣,下面是我的总结:
雪花算法(SnowFlake)是一种简单且高效的算法。它通过利用时间戳和节点ID生成全局唯一的ID,这确保了ID的唯一性和趋势递增。这使得它在许多场景下都是一种理想的选择,特别是在需要高性能和简单实现的情况下。
另一方面,段模式则更加灵活。它允许每个节点预分配一段ID范围,并自行管理这些ID。这种方式避免了单点故障,并且可以根据需求动态调整ID范围
总的来说,我认为雪花算法(SnowFlake)适用于简单的分布式系统场景,而段模式则更适用于复杂的分布式系统场景。在选择适合自己系统的ID生成方案时,需要权衡它们的优缺点,并根据实际情况做出合适的选择。
如果你对分布式ID生成方案还有其他疑问或需要进一步讨论的地方,请随时在评论区留言哦~