浅谈 UUID


通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息的一个128位标识符。

UUID按照标准方法生成时,在实际应用中具有唯一性,且不依赖中央机构的注册和分配。UUID重复的概率接近零,可以忽略不计[1][2]。

因此,所有人都可以自行建立和使用UUID,而且几乎可以确定其不会与既有的标识符重复。也因为如此,在不同地方产生的UUID可以使用于同一个数据库或同一个频道中,而且几乎不可能重复。

UUID的应用相当普遍,许多计算平台都提供了对于生成和解析UUID的支持。

1990年代, UUID 原本是用于阿波罗电脑的网络计算系统,后被用于开放软件基金会的分布式运算环境。分布式运算环境UUID的初始设计基于网络计算系统UUID,其设计受 Domain/OS 中定义和使用的(64位)唯一标识符的启发,这是一个也由 阿波罗电脑 设计的操作系统。后来,微软视窗平台采用分布式运算环境设计作为全局唯一标识符(GUID)。

2005年7月,RFC 4122 为 UUID 注册了一个 URN 命名空间,并制定了早期的规范。当 RFC 4122 作为互联网工程任务组标准发布时,国际电信联盟基于先前的标准和 RFC 4122 早期版本标准化了 UUID。

uuid格式

UUID由16个字节组成,一共是128位,转换成16进制表示后共有32位,其定义如下:

const Size = 16
type UUID [Size]byte

一般会使用-来连接其中的各个部分,因此,常见的为36个字符:

784b99c1-2a77-11ec-8421-0800270e658d
              |    |
             版本  变体

在一个UUID中,可以通过上述两个位置得知UUID生成器的版本、以及该版本下的变体版本。UUID现有5个版本,每个版本定义了不同的生成逻辑,因此其安全性和适用场景也有所区别。

version generation rule
Version 1 date-time and MAC address
Version 2 date-time and MAC address, DCE security version (removed)
Version 3 based on MD5 hashing of a named value
Version 4 random
Version 5 based on SHA-1 hashing of a named value

github.com/gofrs/uuid库中的定义如下,之所以没有v2是因为v2版本已经弃用了。

type Generator interface {
	NewV1() (UUID, error)
	NewV3(ns UUID, name string) UUID
	NewV4() (UUID, error)
	NewV5(ns UUID, name string) UUID
}

uuid:v1

func (g *Gen) NewV1() (UUID, error) {
	u := UUID{}

	timeNow, clockSeq, err := g.getClockSequence()
	if err != nil {
		return Nil, err
	}
	binary.BigEndian.PutUint32(u[0:], uint32(timeNow))
	binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>32))
	binary.BigEndian.PutUint16(u[6:], uint16(timeNow>>48))
	binary.BigEndian.PutUint16(u[8:], clockSeq)

	hardwareAddr, err := g.getHardwareAddr()
	if err != nil {
		return Nil, err
	}
	copy(u[10:], hardwareAddr)

	u.SetVersion(V1)
	u.SetVariant(VariantRFC4122)

	return u, nil
}

从源码可以看出,V1版本的UUID生成基于时间戳和网卡MAC地址,同时设置两个标志位(版本和变体版本)。多次生成后如下:

f781c598-2afc-11ec-a8f6-0800270e658d
f6cf8996-2afc-11ec-8afb-0800270e658d  第10位开始后面mac部分相同

因此,V1版本可以基于uuid反向推出mac网卡地址以及时间戳,并以此推测出后续生成的uuid,所以它并不安全。

uuid.v3

为了解决安全性问题,v3版本采用了MD5的方式计算uuid。通过一个已生成的uuid,结合一个名称进行哈希,最后再设置版本和变体版本。

func main() {
	u1, err := uuid.NewV1()
	if err != nil {
		log.Fatalf("failed to generate UUID: %v", err)
	}
	u3 := uuid.NewV3(u1, "uuid.v3")
	fmt.Println(u3, err)
}

最后生成的效果如下:

b31741ed-47ee-34f1-bdd6-a73e7be61f10
66dc196b-ad5e-3e52-ad8d-dd55d5f180bb  除了版本号和变体版本号之外,其他均不相同

因此,v3版本通过MD5哈希后相当于给了现有一个uuid再套一层。当然uuid对象本质是一个字节数组,我们也可以自定义一个字节数组作为参数传递。如:

func main() {
	u3 := uuid.NewV3(uuid.UUID([16]byte{1: 1, 3: 3}), "uuid.v3")
	fmt.Println(u3)
}

相同参数下生成的uuid一定是一致的,这也是v3版本的一个特点。其源码比较简单,如下:

func (g *Gen) NewV3(ns UUID, name string) UUID {
    // 直接计算 md5(ns + name)
	u := newFromHash(md5.New(), ns, name)
	u.SetVersion(V3)
	u.SetVariant(VariantRFC4122)
	return u
}

uuid.v4

v4版本的生成依赖随机数,其源码如下:

func (g *Gen) NewV4() (UUID, error) {
	u := UUID{}
	if _, err := io.ReadFull(g.rand, u[:]); err != nil {
		return Nil, err
	}
	u.SetVersion(V4)
	u.SetVariant(VariantRFC4122)

	return u, nil
}

因此,它和平台、硬件、时间都没关系,它也是使用最多的一种uuid生成方式。

uuid.v5

v5版本和v3版本是一样的,只不过v5采用了SHA1算法来计算哈希,而v3采用的是MD5。

func (g *Gen) NewV5(ns UUID, name string) UUID {
	u := newFromHash(sha1.New(), ns, name)
	u.SetVersion(V5)
	u.SetVariant(VariantRFC4122)

	return u
}

如何选择

  • v1基于mac地址和时间戳,可反向推导和预测,因此不推荐。
  • v3和v5能够实现相同参数产生相同的uuid,因此,在需要这种功能的场景下可以使用。
  • v4版本依赖随机数,更适合于大多数业务场景。
  • v2版本已经弃用,相比v1,它除了加入mac,还加入了标识符(GID/UID),其暴露的用户信息更多。

文章作者: sfc9982
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 sfc9982 !
  目录