2500 行代码!!使用gozero 开发一个分布式ID生成服务
- goleaf 分布式ID生成系统,基于 go-zero实现,项目亮点
- 多算法支持
- 同时支持 Snowflake 算法和号段模式:
- Snowflake 算法:依赖第三方库,快速生成全局唯一ID
- 号段模式:通过数据库事务更新 max_id,批量获取ID段
- 灵活适应不同业务场景的ID生成需求
- 高性能设计
- 使用本地内存缓存减少数据库查询
- SDK 预获取机制,降低延迟影响
- 通过 channel 实现高效的 ID 分发
- 技术栈
- 基于 go-zero 框架
- 集成 MySQL、Etcd、Redis 等组件
- 使用 gorm 进行数据库操作
- 源码地址:https://github.com/colinrs/goleaf
架构
- goleaf:核心服务,生成分布式id
- sdk:goleaf 服务提供sdk 获取id
- mysql:数据库,业务信息存储
- etcd:获取node id,Snowflake算法使用
- redis:缓存服务器
部署
- 前置条件
- etcd 部署
- mysql 部署
- redis 部署
数据库表创建
- id:自增主键
- biz_tag:业务标签(最多 128 个字符)
- max_id:最大 ID(默认 1)
- step:增量步长
- description:可选描述
- 创建、更新和删除的时间戳
create table leaf.leaf_alloc
(
id int auto_increment
primary key,
biz_tag varchar(128) default '' not null,
max_id bigint default 1 not null,
step int not null,
description varchar(256) default '' not null ,
created_at timestamp default current_timestamp not null,
updated_at timestamp default current_timestamp not null,
deleted_at timestamp default null
);
create index leaf_alloc_biz_tag_index
on leaf.leaf_alloc (biz_tag);
编译
- make build: 编译,二进制文件生成在 ./bin 目录下
- 服务启动:./bin/goleaf -f etc/goleaf-api.yaml
工具
- make swagger: 生成 swagger 文档
- make all: api 文档格式化和生成go代码、swagger文档
- make build:编译,二进制文件生成在 ./bin 目录下
核心代码实现
目录
.
├── bin
├── etc
├── internal
│ ├── config
│ ├── handler
│ │ ├── biztag
│ │ └── idgen
│ ├── infra
│ ├── logic
│ │ ├── biztag
│ │ └── idgen
│ ├── manager
│ ├── model
│ ├── repo
│ ├── svc
│ └── types
├── pkg
│ ├── cache
│ ├── client
│ ├── code
│ ├── codec
│ ├── gosafe
│ ├── httpc
│ ├── httpy
│ ├── response
│ ├── rest
│ │ └── clientinterceptor
│ ├── sdk
│ ├── snowflake
│ └── utils
└── swagger
goleaf 项目目录结构的详细解析:
-
bin/
- 编译后的二进制可执行文件存储目录
- 存放项目的最终运行文件
- make build 命令生成的文件位置
-
etc/
- 存放配置文件
- 包含服务启动所需的配置,如 goleaf-api.yaml
- 配置数据库连接、服务参数等
-
internal/
- 项目内部实现代码,不对外暴露
-
config/
- 定义服务配置结构
- 解析和管理服务启动配置
-
handler/
- HTTP 请求处理器
- biztag/:处理业务标签相关请求
- idgen/:处理 ID 生成相关请求
-
logic/
- 业务逻辑实现
- biztag/:业务标签相关逻辑
- idgen/:ID 生成核心逻辑
-
manager/
- 管理类组件
- 可能服务协调、资源管理等功能
- 聚合多个repo
-
model/
- 数据模型定义
- 与数据库表结构对应的 Go 结构体
- ORM 映射实体
-
repo/
- 数据访问仓库
- 数据库交互逻辑
- 封装数据库增删改查操作
-
svc/
- 服务上下文
- 依赖注入和服务组件管理
- 初始化和配置服务所需组件
-
types/
- 类型定义
- 请求/响应结构体
- 公共类型声明
-
pkg/
- 通用工具包和第三方依赖
- cache:缓存实现、本地内存缓存机制
- client:客户端工具 包含 、etcd 客户端
- code:错误码定义,统一错误处理
- codec:编解码工具,数据序列化/反序列化
- gosafe:安全并发工具,Goroutine 安全执行
- httpc:HTTP 客户端工具。网络请求封装
- httpy:HTTP 相关工具
- response:响应结构封装、统一响应格式
- rest/clientinterceptor:REST 客户端拦截器、请求/响应拦截处理
- sdk:SDK 实现、提供给外部系统调用的客户端
- snowflake:Snowflake 算法实现、ID 生成算法
- utils:通用工具函数、各种辅助方法
-
swagger:Swagger 文档、API 接口文档生成目录
分布式ID生成
两种不通形式的算法生成可以见文章:全局唯一ID生成方案详解
Snowflake算法
snowflake 使用了第三方库 github.com/bwmarrin/snowflake,没有自己再实现
import (
snowflakeExternal "github.com/bwmarrin/snowflake"
)
type Snowflake interface {
GetNodeID() int64
NextID(ctx context.Context) (int64, error)
NextIDs(ctx context.Context, n int64) ([]int64, error)
}
type snowflake struct {
nodeID int64
idGen *snowflakeExternal.Node
}
func NewSnowflake(nodeID int64) Snowflake {
idGen, err := snowflakeExternal.NewNode(nodeID)
logx.Must(err)
return &snowflake{
nodeID: nodeID,
idGen: idGen,
}
}
号段模式
- Segment: 获取号段模式下的可以使用的最大ID和最小ID,在这里会对BizTag进行校验:先在本地内存校验,如果没有,则会使用GetBizTagLoader 函数去数据库查询,将查询结果自动设置到本地内存中,
- 使用本地内存减少DB查询,本地内存通过配置LocalCacheRefreshTime控制更新频率
- GetSegmentMaxID: 更新数据库最大ID,核心SQL就是下面两条
- UPDATE
leaf_alloc
SETmax_id
=max_id + step WHERE biz_tag = ? ANDleaf_alloc
.deleted_at
IS NULL - SELECT
max_id
FROMleaf_alloc
WHERE biz_tag = ? ANDleaf_alloc
.deleted_at
IS NULL
- UPDATE
// Segment
func (l *segmentManager) Segment(req *types.SegmentRequest) (*types.SegmentResponse, error) {
leafAlloc := &model.LeafAlloc{}
//err := l.svcCtx.LocalCache.Get(l.ctx, req.BizTag, leafAlloc)
err := l.svcCtx.GetLocalCache().Load(l.ctx, l.GetBizTagLoader, req.BizTag, leafAlloc, 0)
if err != nil {
return nil, code.BizTagNotExist
}
maxID := l.idGenRepo.GetSegmentMaxID(l.db, req.BizTag)
if maxID == 0 {
return nil, code.UnknownErr
}
resp := &types.SegmentResponse{
MaxID: maxID,
MinID: maxID - leafAlloc.Step.Int64 + 1,
Step: leafAlloc.Step.Int64,
}
return resp, nil
}
// GetBizTagLoader
func (l *segmentManager) GetBizTagLoader(ctx context.Context, keys []string) ([]interface{}, error) {
if len(keys) == 0 {
return nil, code.BizTagNotExist
}
bizTag := keys[0]
leafAlloc, _ := l.bizTagRepo.GetBizTagByName(l.db, bizTag)
if leafAlloc == nil {
return nil, code.BizTagNotExist
}
return []interface{}{leafAlloc}, nil
}
// GetSegmentMaxID
func (r *idGen) GetSegmentMaxID(db *gorm.DB, bizTag string) int64 {
var maxID int64
err := db.Transaction(func(tx *gorm.DB) error {
err := tx.Model(&model.LeafAlloc{}).Where("biz_tag = ?", bizTag).
UpdateColumn("max_id", gorm.Expr("max_id + step")).Error
if err != nil {
return err
}
err = tx.Model(&model.LeafAlloc{}).Where("biz_tag = ?", bizTag).Pluck("max_id", &maxID).Error
if err != nil {
return err
}
return nil
})
if err != nil {
return 0
}
return maxID
}
本地缓存
- goleaf 本地缓存是为了校验bizTag 在数据库是否存在而构建,在 pkg/cache 中实现的本地缓存
- 在服务启动时会对本地缓存进行构建,并通过定时任务定时构建缓存,构建缓存
func (s *ServiceContext) SyncLocalCache() {
var bizTags []*model.LeafAlloc
db := s.DB.WithContext(context.Background())
err := db.Find(&bizTags).Error
logx.Must(err)
// 同步数据库到内存中
s.syncCache(bizTags)
localCron := cron.New()
entryID, err := localCron.AddFunc(fmt.Sprintf("@every %s", s.Config.LocalCacheRefreshTime), func() {
logx.Infof("sync local cache")
db = s.DB.WithContext(context.Background())
bizTags = []*model.LeafAlloc{}
err = db.Find(&bizTags).Error
if err != nil {
logx.Errorf("sync local cache err:%s", err.Error())
return
}
s.syncCache(bizTags)
})
logx.Must(err)
localCron.Start()
logx.Infof("entryID:%d", entryID)
}
func (s *ServiceContext) syncCache(bizTags []*model.LeafAlloc) {
memCache, err := newLocalCache()
if err != nil {
logx.Errorf("newLocalCache err:%s", err.Error())
return
}
count := 0
_ = memCache.Flush(context.Background())
for _, bizTag := range bizTags {
err := memCache.Set(context.Background(), bizTag.BizTag.String, bizTag, 0)
if err != nil {
logx.Errorf("bizTag:%s,sync local cache err:%s", bizTag.BizTag.String, err.Error())
continue
}
logx.Infof("bizTag:%s,sync local cache success", bizTag.BizTag.String)
count++
}
// 替换缓存
oldCache := s.LocalCache
s.LocalCacheLock.Lock()
s.LocalCache = memCache
s.LocalCacheLock.Unlock()
_ = oldCache.Flush(context.Background())
logx.Infof("sync local cache success count:%d, all:%d", count, len(bizTags))
}
SDK 实现
- goleaf 在接入方获取分布式ID时,实现了 SDK,接入方通过SDK来获取分布式ID,在SDK中实现了预获取分布式ID,减少对接入方的延迟影响
- SDK 在初始化完成之后就在内存中已经获取到了一份分布式ID列表在内存中,并且会时实去goleaf服务端获取分布式ID,保证内存中一直有1000分布式ID
idGenClient
idGenClient 时SDK的核心结构,接入方通过 NextId 获取ID,NextId 只是从 ids 这个 chan 获取
type idGenClient struct {
host string
bizTagName string
ids chan int64
httpClient httpc.Client
stat *stat
}
func NewIdGenClient(options ...Option) IdGenClient {
idGen := &idGenClient{}
o := &Options{
host: "http://127.0.0.1:8888",
}
for _, option := range options {
option(o)
}
idGen.host = o.host
idGen.bizTagName = o.bizTagName
idGen.ids = make(chan int64, 1000)
idGen.httpClient = httpc.NewClient(idGen.host)
idGen.stat = newStat()
logx.Must(idGen.sendToChannel())
logx.Must(idGen.start())
return idGen
}
func (i *idGenClient) NextId(ctx context.Context) (int64, error) {
for {
select {
case nextId := <-i.ids:
return nextId, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
}
start
start
- sendToChannelLoop: 实现一个死循环,一直调用sendToChannel 通过 getNextIds 拿到 的 id,写入到 ids 中,实现只要接入方获取了一个 id,就补充一个ID,如果接入方获取的速度>写入速度,则需要等待
- logStat: 打印id的长度和更新时间,debug 日志
- goSafe 方法实现了自动recover,避免panic导致接入方服务退出
func (i *idGenClient) start() error {
gosafe.GoSafe(context.Background(), func() {
i.sendToChannelLoop()
})
gosafe.GoSafe(context.Background(), func() {
i.logStat()
})
return nil
}
func (i *idGenClient) sendToChannelLoop() {
for {
err := i.sendToChannel()
if err != nil {
logx.Errorf("goleaf idGenClient sendToChannel error: %v", err)
}
}
}
func (i *idGenClient) sendToChannel() error {
nextIds, err := i.getNextIds()
if err != nil {
logx.Errorf("goleaf idGenClient getNextIds error: %v", err)
return err
}
i.stat.lastIdUpdate.Store(time.Now().Unix())
for _, nextId := range nextIds {
i.ids <- nextId
}
i.stat.lastIdUpdate.Store(time.Now().Unix())
return nil
}
func (i *idGenClient) logStat() {
ticker := time.NewTicker(5 * time.Second)
for {
<-ticker.C
logx.Debugf("goleaf idGenClient lens: %d, last id update: %d",
len(i.ids), i.stat.lastIdUpdate.Load())
}
}
getNextIds
实际调用 goleaf 接口获取到 分布式id函数
- getFromSegment: 如果 bizTagName 存在,则会调用接口:/api/v1/segment/get 获取到分布式ID,否则使用getFromSnowflake
- getFromSnowflake: 获取Snowflake算法生成的ID
func (i *idGenClient) getNextIds() ([]int64, error) {
if i.bizTagName == "" {
return i.getFromSnowflake()
}
return i.getFromSegment()
}
func (i *idGenClient) getFromSegment() ([]int64, error) {
resp, err := i.httpClient.Get(context.Background(), fmt.Sprintf("/api/v1/segment/get?biz_tag=%s", i.bizTagName))
if err != nil {
return nil, err
}
segmentResponse := &SegmentResponse{}
defer func() {
_ = resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if err = json.Unmarshal(body, segmentResponse); err != nil {
return nil, err
}
if segmentResponse.Code != 0 {
return nil, code.NewErr(code.WithCode(segmentResponse.Code), code.WithMsg(segmentResponse.Msg))
}
logx.Debugf("getFromSegment lens: %d, minId: %d, maxId: %d",
segmentResponse.Data.Step, segmentResponse.Data.MinID, segmentResponse.Data.MaxID)
ids := make([]int64, 0, segmentResponse.Data.Step)
for i := segmentResponse.Data.MinID; i < segmentResponse.Data.MaxID; i++ {
ids = append(ids, i)
}
return ids, nil
}
func (i *idGenClient) getFromSnowflake() ([]int64, error) {
resp, err := i.httpClient.Get(context.Background(), "/api/v1/snowflake/get?step=100")
if err != nil {
return nil, err
}
snowflakeResponse := &SnowflakeResponse{}
defer func() {
_ = resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if err = json.Unmarshal(body, snowflakeResponse); err != nil {
return nil, err
}
if snowflakeResponse.Code != 0 {
return nil, code.NewErr(code.WithCode(snowflakeResponse.Code), code.WithMsg(snowflakeResponse.Msg))
}
logx.Debugf("getFromSnowflake lens: %d,start:%d,end:%d", snowflakeResponse.Data.Total,
snowflakeResponse.Data.List[0], snowflakeResponse.Data.List[len(snowflakeResponse.Data.List)-1])
return snowflakeResponse.Data.List, nil
}
总结
核心特性
- 分布式ID生成策略
- Snowflake 算法:依赖第三方库,快速生成全局唯一ID
- 号段模式:通过数据库事务更新 max_id,批量获取ID段
- 本地缓存机制
- 定期同步数据库 bizTag 信息
- 减少数据库查询,提高系统性能
- 支持自动缓存刷新
- SDK 设计
- 预获取 ID 列表,维持 1000 个 ID 缓冲
- 异步补充 ID,保证业务持续性
- 支持平滑获取分布式 ID
技术亮点
- 使用 Go 安全并发编程
- 实现优雅的错误处理
- 提供灵活的配置选项
- 支持多种 ID 生成策略
应用场景
- 高并发分布式系统
- 需要全局唯一 ID 的业务
- 微服务架构
- 需要按业务标签生成 ID 的系统
不足与改进方向
- 可以考虑增加更多的监控和告警机制
- 可以探索更多 ID 生成算法
- 优化缓存一致性策略
- 增加更详细的性能测试和基准测试
总的来说,goleaf 是一个轻量、高效、可扩展的分布式 ID 生成服务,为复杂分布式系统提供了可靠的唯一 ID 生成方案。