├── uid.go └── README.md /uid.go: -------------------------------------------------------------------------------- 1 | package uid 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "log" 7 | "time" 8 | ) 9 | 10 | type logger interface { 11 | Error(error) 12 | } 13 | 14 | // Logger Log接口,如果设置了Logger,就使用Logger打印日志,如果没有设置,就使用内置库log打印日志 15 | var Logger logger 16 | 17 | // ErrTimeOut 获取uid超时错误 18 | var ErrTimeOut = errors.New("get uid timeout") 19 | 20 | type Uid struct { 21 | db *sql.DB // 数据库连接 22 | businessId string // 业务id 23 | ch chan int64 // id缓冲池 24 | min, max int64 // id段最小值,最大值 25 | } 26 | 27 | // NewUid 创建一个Uid;len:缓冲池大小() 28 | // db:数据库连接 29 | // businessId:业务id 30 | // len:缓冲池大小(长度可控制缓存中剩下多少id时,去DB中加载) 31 | func NewUid(db *sql.DB, businessId string, len int) (*Uid, error) { 32 | lid := Uid{ 33 | db: db, 34 | businessId: businessId, 35 | ch: make(chan int64, len), 36 | } 37 | go lid.productId() 38 | return &lid, nil 39 | } 40 | 41 | // Get 获取自增id,当发生超时,返回错误,避免大量请求阻塞,服务器崩溃 42 | func (u *Uid) Get() (int64, error) { 43 | select { 44 | case <-time.After(1 * time.Second): 45 | return 0, ErrTimeOut 46 | case uid := <-u.ch: 47 | return uid, nil 48 | } 49 | } 50 | 51 | // productId 生产id,当ch达到最大容量时,这个方法会阻塞,直到ch中的id被消费 52 | func (u *Uid) productId() { 53 | u.reLoad() 54 | 55 | for { 56 | if u.min >= u.max { 57 | u.reLoad() 58 | } 59 | 60 | u.min++ 61 | u.ch <- u.min 62 | } 63 | } 64 | 65 | // reLoad 在数据库获取id段,如果失败,会每隔一秒尝试一次 66 | func (u *Uid) reLoad() error { 67 | var err error 68 | for { 69 | err = u.getFromDB() 70 | if err == nil { 71 | return nil 72 | } 73 | 74 | // 数据库发生异常,等待一秒之后再次进行尝试 75 | if Logger != nil { 76 | Logger.Error(err) 77 | } else { 78 | log.Println(err) 79 | } 80 | time.Sleep(time.Second) 81 | } 82 | } 83 | 84 | // getFromDB 从数据库获取id段 85 | func (u *Uid) getFromDB() error { 86 | var ( 87 | maxId int64 88 | step int64 89 | ) 90 | 91 | tx, err := u.db.Begin() 92 | if err != nil { 93 | return err 94 | } 95 | defer tx.Rollback() 96 | 97 | row := tx.QueryRow("SELECT max_id,step FROM uid WHERE business_id = ? FOR UPDATE", u.businessId) 98 | err = row.Scan(&maxId, &step) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | _, err = tx.Exec("UPDATE uid SET max_id = ? WHERE business_id = ?", maxId+step, u.businessId) 104 | if err != nil { 105 | return err 106 | } 107 | err = tx.Commit() 108 | if err != nil { 109 | return err 110 | } 111 | 112 | u.min = maxId 113 | u.max = maxId + step 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 高性能分布式自增id生成器lid 2 | 先看下测试结果: 3 | 4 | ``` 5 | goos: darwin 6 | goarch: amd64 7 | pkg: goim/public/lib/lid 8 | BenchmarkLeafKey-4 2000000 1081 ns/op 9 | PASS 10 | ``` 11 | 12 | 步长设置为1000.缓冲池大小设为1000,每秒可以达到近百万次的生成量,其思想借鉴了[Leaf——美团点评分布式ID生成系统](https://tech.meituan.com/MT_Leaf.html)的Leaf-segment数据库双buffer优化方案,其实他的核心思想是,每次从数据库拿取一个号段,用完了,再去数据库拿,当用尽去数据库拿的时候,会有一小会的阻塞,对这一情况做了一些优化。 13 | 14 | 刚开始实现的时候,和美团的方案一样,利用两个buffer,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。 15 | 16 | 最后想了想,其实没必要这么复杂,用一个channal,一边起一个goroutine,先从数据库拿取一个号段,然后生成id放到channel里面,如果号段用尽,再从数据库里面取,如此往复,当channel里面满时,goroutine会阻塞。一边用的时候从里面拿就行。 17 | 18 | 贴出代码: 19 | 20 | ```go 21 | package uid 22 | 23 | import ( 24 | "database/sql" 25 | "errors" 26 | "log" 27 | "time" 28 | ) 29 | 30 | type logger interface { 31 | Error(error) 32 | } 33 | 34 | // Logger Log接口,如果设置了Logger,就使用Logger打印日志,如果没有设置,就使用内置库log打印日志 35 | var Logger logger 36 | 37 | // ErrTimeOut 获取uid超时错误 38 | var ErrTimeOut = errors.New("get uid timeout") 39 | 40 | type Uid struct { 41 | db *sql.DB // 数据库连接 42 | businessId string // 业务id 43 | ch chan int64 // id缓冲池 44 | min, max int64 // id段最小值,最大值 45 | } 46 | 47 | // NewUid 创建一个Uid;len:缓冲池大小() 48 | // db:数据库连接 49 | // businessId:业务id 50 | // len:缓冲池大小(长度可控制缓存中剩下多少id时,去DB中加载) 51 | func NewUid(db *sql.DB, businessId string, len int) (*Uid, error) { 52 | lid := Uid{ 53 | db: db, 54 | businessId: businessId, 55 | ch: make(chan int64, len), 56 | } 57 | go lid.productId() 58 | return &lid, nil 59 | } 60 | 61 | // Get 获取自增id,当发生超时,返回错误,避免大量请求阻塞,服务器崩溃 62 | func (u *Uid) Get() (int64, error) { 63 | select { 64 | case <-time.After(1 * time.Second): 65 | return 0, ErrTimeOut 66 | case uid := <-u.ch: 67 | return uid, nil 68 | } 69 | } 70 | 71 | // productId 生产id,当ch达到最大容量时,这个方法会阻塞,直到ch中的id被消费 72 | func (u *Uid) productId() { 73 | u.reLoad() 74 | 75 | for { 76 | if u.min >= u.max { 77 | u.reLoad() 78 | } 79 | 80 | u.min++ 81 | u.ch <- u.min 82 | } 83 | } 84 | 85 | // reLoad 在数据库获取id段,如果失败,会每隔一秒尝试一次 86 | func (u *Uid) reLoad() error { 87 | var err error 88 | for { 89 | err = u.getFromDB() 90 | if err == nil { 91 | return nil 92 | } 93 | 94 | // 数据库发生异常,等待一秒之后再次进行尝试 95 | if Logger != nil { 96 | Logger.Error(err) 97 | } else { 98 | log.Println(err) 99 | } 100 | time.Sleep(time.Second) 101 | } 102 | } 103 | 104 | // getFromDB 从数据库获取id段 105 | func (u *Uid) getFromDB() error { 106 | var ( 107 | maxId int64 108 | step int64 109 | ) 110 | 111 | tx, err := u.db.Begin() 112 | if err != nil { 113 | return err 114 | } 115 | defer tx.Rollback() 116 | 117 | row := tx.QueryRow("SELECT max_id,step FROM uid WHERE business_id = ? FOR UPDATE", u.businessId) 118 | err = row.Scan(&maxId, &step) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | _, err = tx.Exec("UPDATE uid SET max_id = ? WHERE business_id = ?", maxId+step, u.businessId) 124 | if err != nil { 125 | return err 126 | } 127 | err = tx.Commit() 128 | if err != nil { 129 | return err 130 | } 131 | 132 | u.min = maxId 133 | u.max = maxId + step 134 | return nil 135 | } 136 | ``` 137 | sql语句 138 | ```sql 139 | CREATE TABLE `uid` ( 140 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', 141 | `business_id` varchar(128) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '业务id', 142 | `max_id` bigint(20) unsigned DEFAULT NULL COMMENT '最大id', 143 | `step` int(10) unsigned DEFAULT NULL COMMENT '步长', 144 | `description` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '描述', 145 | `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 146 | `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 147 | PRIMARY KEY (`id`), 148 | UNIQUE KEY `uk_business_id` (`business_id`) 149 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='分布式自增主键'; 150 | ``` 151 | 152 | 153 | 154 | --------------------------------------------------------------------------------