要想生成单调递增的 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 一列显示乱码,需要切换到 “原始数据模式” 才能正常显示。
步骤:点击菜单栏上的 查看 -> 显示 -> 原始数据模式。如下图所示: