hello,大家好,我是千羽。
最近有同学私信到数据库分布式id设计的时候,咨询这一块是怎么设计的,所以趁着周末,总结了根据现有业务来探讨分布式ID技术与实现。
先从传统的主键自增ID开始聊起,探讨其存在的局限性以及业务系统对分布式ID的需求。
随后,我们将调研业界常见的分布式ID生成方案,包括雪花算法、号段模式、UUID等。在选择方案时,我们将采取雪花算法与段模式相结合的方式。最后,我们将深入探讨分布式ID的落地与实现,包括使用Golang实现雪花算法和段模式,并结合实际业务场景进行讨论。
在当今大数据时代,随着业务规模的不断扩大和数据量的不断增长,业务系统对于唯一标识符(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的位置。
如果是单体系统来说,主键ID可能会常用主键自动的方式进行设置,这种ID生成方法在单体项目是可行的。
但是对于在分布式系统中,可能存在多个数据库实例,每个数据库实例都有自己的自增ID生成器,这样就会造成跨库的ID不唯一问题,需要额外的处理来解决,所以这是不符合业务的。
雪花算法是Twitter开源的一种分布式ID生成算法,采用64位的整数表示,其中包含时间戳、机器ID、数据中心ID和序列号等信息,保证了ID的全局唯一性和趋势递增。
号段模式将ID的生成分成两个步骤,首先申请一个区间(号段),然后在该区间内自增生成ID。号段模式适用于高并发场景,可以减少对数据库的访问压力,但需要额外的管理和调度机制。
全球唯一标识符(UUID)是一种由128位数字表示的标准,通常以32位的十六进制数表示。UUID生成算法基于时间戳和设备唯一标识等信息,保证了全局唯一性。但由于其长度较长,不适合作为数据库的主键。
在数据库中使用自增主键生成ID,每次插入新记录时,数据库会自动分配一个唯一的ID值。这种方式简单易用,但不适用于分布式环境,可能存在单点故障和性能瓶颈。
利用Redis的原子操作和分布式锁机制,可以实现分布式ID的生成。通过维护一个递增的计数器或使用Redis的自增功能,可以生成全局唯一的ID。
此外,还有其他大厂之间的百度Uidgenerator,美团Leaf,滴滴TinyID等等。
结合当前的系统业务场景,既要进行分布式id也要进行自增和保持历史数据的现状。采取雪花算法+段模式两种模式去实现分布式id的实现。
保证了生成的ID具有全局唯一性和趋势递增性,每个ID都是递增的,并且不会出现重复的情况。
段模式在分段管理的过程中也能够保证ID的唯一性和递增性,通过对号段进行动态管理和分配,可以充分利用号段的使用效率,提高了ID的生成性能和效率。
此外,段模式还可以一眼开出这个id是谁谁谁,清晰明了。
通过一个简单的 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())
}
}
结合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())
}
}
在实际的业务上,通过设置一个分布式id的生成服务,每次涉及新增的逻辑,会先调研这个分布式服务生成id,在进行数据库插入等等。
当然在数据库层面也会设置:是否为雪花算法和段模式。
//分布式id改造
protected $distributed = true;
protected $distributedType = 1;
protected $distributedTag = "test:test:book";
protected $table = 'book';
public $timestamps = false;
当我考虑雪花算法(SnowFlake)和段模式时,我发现它们都是用于生成分布式系统中唯一ID的重要方案。但两种方案各有优劣:
雪花算法(SnowFlake)是一种简单且高效的算法。它通过利用时间戳和节点ID生成全局唯一的ID,这确保了ID的唯一性和趋势递增。这使得它在许多场景下都是一种理想的选择,特别是在需要高性能和简单实现的情况下。
另一方面,段模式则更加灵活。它允许每个节点预分配一段ID范围,并自行管理这些ID。这种方式避免了单点故障,并且可以根据需求动态调整ID范围
总的来说,我认为雪花算法(SnowFlake)适用于简单的分布式系统场景,而段模式则更适用于复杂的分布式系统场景。在选择适合自己系统的ID生成方案时,需要权衡它们的优缺点,并根据实际情况做出合适的选择。
如果你对分布式ID生成方案还有其他疑问或需要进一步讨论的地方,请随时在评论区留言哦~