Golang 生成分布式单调递增 UUID

要想生成单调递增的 UUID,唯有使用带时间戳的 UUID V1 版。

众所周知,UUID 的前 4 个字节存放的是时间戳低位,而时间戳低位是随着时间的变化而时刻不断地进行 “随机” 变化的,并非单调递增。如果直接将 UUID 作为主键插入势必会产生离散 IO,导致数据库在每次插入 / 查询数据时需要扫描更多的记录数,从而产生性能瓶颈。那么,如何让随机 UUID 按索引的顺序密集存储以减少离散IO的产生呢?

解决方案:互换标准 UUID 的时间戳高低位。如下图所示:

一、自定义一个 NewIncUUID 函数(参照:https://github.com/satori/go.uuid)。为了节省存储空间,本例将原本占用36个字节的字符串 UUID 换成16个字节的二进制(binary)进行存储。

注:存放时间戳时须使用大端字节序(Big-Endian)

// 新生一个单调递增的 UUID
func (g *generator) NewIncUUID() uuid {
	u := uuid{}

	timeNow, clockSeq, hardwareAddr := g.getStorage()

	// 时间戳高位
	u[0] = byte(uint16(timeNow >> 48))
	// 时间戳中位
	binary.BigEndian.PutUint16(u[1:], uint16(timeNow>>32))
	// 时间戳低位第1~3个字节
	u[3] = byte(uint32(timeNow) >> 24)
	u[4] = byte(uint32(timeNow) >> 16)
	u[5] = byte(uint32(timeNow) >> 8)
	// UUID版本
	u[6] = byte(uint16(timeNow>>48) >> 8)
	// 时间戳低位第4个字节
	u[7] = byte(uint32(timeNow))
	// 时钟序列
	binary.BigEndian.PutUint16(u[8:], clockSeq)
	// MAC地址
	copy(u[10:], hardwareAddr)

	// 设置版本号位
	u[6] = (u[6] & 0x0f) | (v1 << 4)
	// 设置变体号位
	u[8] = (u[8]&(0xff>>2) | (0x02 << 6))

	return u
}

二、使用 GORM 做单元测试

package tests

import (
	"testing"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"

    "tests/uuid"
)

var db *gorm.DB

type User struct {
	ID       []byte `gorm:"type:binary(16);primaryKey"`
	Name     string
	Age      uint8
	Sex      byte `gorm:"check:sex_checker,sex = 'M' OR sex = 'F'"`
	Birthday *time.Time
}

func init() {
	var err error
	db, err = gorm.Open(mysql.New(mysql.Config{
		DSN: "boge:123456@tcp(192.168.23.110:3306)/test?charset=utf8mb4&parseTime=True&loc=Local",
		DefaultStringSize:         256,
		DisableDatetimePrecision:  true,
		SkipInitializeWithVersion: false,
	}), &gorm.Config{
		Logger:                                   logger.Default.LogMode(logger.Info),
		NamingStrategy:                           schema.NamingStrategy{SingularTable: true},
		DisableForeignKeyConstraintWhenMigrating: true,
	})

	if err != nil {
		panic(err)
	}
}

func GetUser(name string) *User {
	var (
		birthday = time.Now().AddDate(-18, 0, 0).Round(time.Second)
		user     = User{
			Name:     name,
			Age:      18,
			Sex:      'M',
			Birthday: &birthday,
		}
	)

	return &user
}

func (s *User) BeforeCreate(tx *gorm.DB) (err error) {
	s.ID = uuid.NewIncUUID().Bytes()
	return
}

func TestIncUUID(t *testing.T) {
	db.AutoMigrate(&User{})
	
    var user = *GetUser("create")

	if results := db.Create(&user); results.Error != nil {
		t.Fatalf("errors happened when create: %v", results.Error)
	} else if results.RowsAffected != 1 {
		t.Fatalf("rows affected expects: %v, got %v", 1, results.RowsAffected)
	}

	t.Run("First", func(t *testing.T) {
		var first User
        // ECA2D942DB4B11C28FA6005056C00001 为上面 BeforeCreate 插入的 UUID
		if err := db.Where("id = UNHEX(?)", "ECA2D942DB4B11C28FA6005056C00001").First(&first).Error; err != nil {
			t.Errorf("errors happened when query first: %v", err)
		}
	})
}

三、Navicat 查看原始数据

因为 UUID 是以 binary 类型进行存储的,默认情况下当在 Navicat 查看数据时,id 一列显示乱码,需要切换到 “原始数据模式” 才能正常显示。

步骤:点击菜单栏上的 查看 -> 显示 -> 原始数据模式。如下图所示:

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值