2500 行代码!!使用gozero 开发一个分布式ID生成服务

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 SET max_id=max_id + step WHERE biz_tag = ? AND leaf_alloc.deleted_at IS NULL
    • SELECT max_id FROM leaf_alloc WHERE biz_tag = ? AND leaf_alloc.deleted_at IS NULL
// 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
}

总结

核心特性

  1. 分布式ID生成策略
  • Snowflake 算法:依赖第三方库,快速生成全局唯一ID
  • 号段模式:通过数据库事务更新 max_id,批量获取ID段
  1. 本地缓存机制
  • 定期同步数据库 bizTag 信息
  • 减少数据库查询,提高系统性能
  • 支持自动缓存刷新
  1. SDK 设计
  • 预获取 ID 列表,维持 1000 个 ID 缓冲
  • 异步补充 ID,保证业务持续性
  • 支持平滑获取分布式 ID

技术亮点

  • 使用 Go 安全并发编程
  • 实现优雅的错误处理
  • 提供灵活的配置选项
  • 支持多种 ID 生成策略

应用场景

  • 高并发分布式系统
  • 需要全局唯一 ID 的业务
  • 微服务架构
  • 需要按业务标签生成 ID 的系统

不足与改进方向

  • 可以考虑增加更多的监控和告警机制
  • 可以探索更多 ID 生成算法
  • 优化缓存一致性策略
  • 增加更详细的性能测试和基准测试

总的来说,goleaf 是一个轻量、高效、可扩展的分布式 ID 生成服务,为复杂分布式系统提供了可靠的唯一 ID 生成方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Go语言小鸟编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值