结合业务探讨分布式ID技术与实现

hello,大家好,我是千羽。
最近有同学私信到数据库分布式id设计的时候,咨询这一块是怎么设计的,所以趁着周末,总结了根据现有业务来探讨分布式ID技术与实现。
先从传统的主键自增ID开始聊起,探讨其存在的局限性以及业务系统对分布式ID的需求。
随后,我们将调研业界常见的分布式ID生成方案,包括雪花算法、号段模式、UUID等。在选择方案时,我们将采取雪花算法与段模式相结合的方式。最后,我们将深入探讨分布式ID的落地与实现,包括使用Golang实现雪花算法和段模式,并结合实际业务场景进行讨论。
引言:
在当今大数据时代,随着业务规模的不断扩大和数据量的不断增长,业务系统对于唯一标识符(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生成方案还有其他疑问或需要进一步讨论的地方,请随时在评论区留言哦~

声明:文中观点不代表本站立场。本文传送门:http://eyangzhen.com/414866.html

(0)
联系我们
联系我们
分享本页
返回顶部