有两个思路:
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类型为整形):
package mainimport ("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)}}
下载库
go get github.com/gitstliu/go-id-worker
下面是在vscode中的调试结果
API server listening at: 127.0.0.1:4442
4917572028174794752
Process exiting with code: 0
用mysql自带的自增id生成全局唯一id
package mainimport ("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 // 业务idch 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, ErrTimeOutcase 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 errorfor {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 int64step 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 = maxIdu.max = maxId + stepreturn nil}const (DeviceIdBusinessId = "device_id" // 设备id)var(DeviceIdUid *Uid)func InitUID(db *sql.DB) {var err errorDeviceIdUid, 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 goroutineurl := 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)}}}
测试结果:
[root@bogon uid]# go build main.go[root@bogon uid]# ./mainid= 1706id= 1707id= 1708id= 1709id= 1710id= 1711id= 1712id= 1713id= 1714id= 1715id= 1716id= 1717id= 1718id= 1719id= 1720id= 1721id= 1722id= 1723id= 1724id= 1725[root@bogon uid]#
以下为uid服务器的架构图:
原文链接:https://blog.csdn.net/jacky128256/java/article/details/104687772
