golang实现全局唯一id snowflake算法

TangYiMo · · 2756 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

> 在应用程序中,经常需要全局唯一的ID作为数据库主键。在一台节点容易全局唯一,那在多台节点呢? ### 有两个思路: - 1使用散列函数,如sha256,加上时间戳、mac地址、cpu负荷、随机数等组成,id足够长,引入多个不确定因素,以至于碰撞几率非常小,可以认为是全局唯一。例如uuid就是这种。但是uuid是字符串的形式,对于DB来说,占用的空间至少大一倍,DB的索引是需要存储和对比的,因此在存储空间和查询时间上面都比整形要低,这种情况在DB的数据条数越多时越明显。 - 2使用分割法。每个节点都保证自己生成的所有id在本机唯一,每个节点都有一个人为分配的不重复节点编号,插入id中,这样所有节点的id都是全局唯一的。 - 3利用DB自带的主键唯一性来确保id唯一。但是db的自增id是需要等到事务提交后,ID才算是有效的。有些双向引用的数据,不得不插入后再做一次更新,比较麻烦。 > 第二种方式是类似Twitter的Snowflake算法,它给每台机器分配一个唯一标识,然后通过时间戳+标识+自增实现全局唯一ID。这种方式好处在于ID生成算法完全是一个无状态机,无网络调用,高效可靠。缺点是如果唯一标识有重复,会造成ID冲突。 > Snowflake算法采用41bit毫秒时间戳,加上10bit机器ID(最多支持1024台id服务器),加上12bit序列号,理论上最多支持1024台机器每秒生成4096000个序列号。409万个id每秒,在任何交易平台目前都是够用的。 推特的id构成(从最高位往最低位方向): - 1位 ,不用。固定是0 - 41位 ,毫秒时间戳 - 5位 ,数据中心ID (用于对数据中心进行编码) - 5位 ,WORKERID (用于对工作进程进行编码) - 12位 ,序列号。用于同一毫秒产生ID的序列 (自增id) 下面是用golang实现的uuid方法(uuid类型为整形): ```go package main import ( "fmt" idworker "github.com/gitstliu/go-id-worker" ) func main() { currWoker := &idworker.IdWorker{} currWoker.InitIdWorker(1000, 1) newID, err := currWoker.NextId() if err == nil { fmt.Println(newID) } } ```` 下载库 ```shell go get github.com/gitstliu/go-id-worker ``` 下面是在vscode中的调试结果 ```shell API server listening at: 127.0.0.1:4442 4917572028174794752 Process exiting with code: 0 ``` ## 用mysql自带的自增id生成全局唯一id ```go package main import ( "database/sql" "errors" "log" "time" "fmt" _ "github.com/go-sql-driver/mysql" ) type logger interface { Error(error) } // Logger Log接口,如果设置了Logger,就使用Logger打印日志,如果没有设置,就使用内置库log打印日志 var Logger logger // ErrTimeOut 获取uid超时错误 var ErrTimeOut = errors.New("get uid timeout") type Uid struct { db *sql.DB // 数据库连接 businessId string // 业务id ch chan int64 // id缓冲池 min, max int64 // id段最小值,最大值 } // NewUid 创建一个Uid;len:缓冲池大小() // db:数据库连接 // businessId:业务id // len:缓冲池大小(长度可控制缓存中剩下多少id时,去DB中加载) func NewUid(db *sql.DB, businessId string, len int) (*Uid, error) { lid := Uid{ db: db, businessId: businessId, ch: make(chan int64, len), } go lid.productId() return &lid, nil } // Get 获取自增id,当发生超时,返回错误,避免大量请求阻塞,服务器崩溃 func (u *Uid) Get() (int64, error) { select { case <-time.After(1 * time.Second): return 0, ErrTimeOut case uid := <-u.ch: return uid, nil } } // productId 生产id,当ch达到最大容量时,这个方法会阻塞,直到ch中的id被消费 func (u *Uid) productId() { u.reLoad() for { if u.min >= u.max { u.reLoad() } u.min++ u.ch <- u.min } } // reLoad 在数据库获取id段,如果失败,会每隔一秒尝试一次 func (u *Uid) reLoad() error { var err error for { err = u.getFromDB() if err == nil { return nil } // 数据库发生异常,等待一秒之后再次进行尝试 if Logger != nil { Logger.Error(err) } else { log.Println(err) } time.Sleep(time.Second) } } // getFromDB 从数据库获取id段 func (u *Uid) getFromDB() error { var ( maxId int64 step int64 ) row := u.db.QueryRow("SELECT max_id, step FROM uid;") //row = u.db.QueryRow("SELECT max_id, step FROM uid WHERE business_id = ? FOR UPDATE",1) if err :=row.Scan(&maxId, &step); err != nil{ fmt.Printf("scan failed, err:%v",err) return err } _, err := u.db.Exec("UPDATE uid SET max_id = ?", maxId+step) if err != nil { return err } u.min = maxId u.max = maxId + step return nil } const ( DeviceIdBusinessId = "device_id" // 设备id ) var( DeviceIdUid *Uid ) func InitUID(db *sql.DB) { var err error DeviceIdUid, err = NewUid(db, DeviceIdBusinessId, 5) if err != nil { panic(err) } } func check(err error) { if err != nil { panic(err) } } const ( USER_NAME = "root" PASS_WORD = "123456" HOST = "localhost" PORT = "3306" DATABASE = "test" CHARSET = "utf8" ) // func main() { // http init // http api goroutine url := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s", USER_NAME, PASS_WORD, HOST, PORT, DATABASE, CHARSET) db, err := sql.Open("mysql", url) if err != nil { panic(err) } InitUID(db) for i:=0; i<20; i++ { id ,err := DeviceIdUid.Get() if err == nil { fmt.Println("id=", id) } else { fmt.Println(err) } } } ``` 测试结果: ```shell [root@bogon uid]# go build main.go [root@bogon uid]# ./main id= 1706 id= 1707 id= 1708 id= 1709 id= 1710 id= 1711 id= 1712 id= 1713 id= 1714 id= 1715 id= 1716 id= 1717 id= 1718 id= 1719 id= 1720 id= 1721 id= 1722 id= 1723 id= 1724 id= 1725 [root@bogon uid]# ``` >改进:上面的是单机版本的uid生成器。单机的流量会有瓶颈。因此是实际的商业部署期间。如果一台不能满足uid,则设置一个单独的http server go程,其它业务节点从本节点获取一段uid(step需要调整,例如从5变成1w),其它节点请求一次,则返回1w个uid,不管这1w个uid有没有用完,都不再分配给其它节点,这样保证了全局唯一性。 以下为uid服务器的架构图: ![在这里插入图片描述](https://img-blog.csdnimg.cn/2020030613245113.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2phY2t5MTI4MjU2,size_16,color_FFFFFF,t_70)

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2756 次点击  ∙  2 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传