高并发分布式系统中生成全局唯一ID汇总

一、概述

在分布式系统中,特别是在分库分表场景下,生成全局唯一ID是一个关键的技术挑战。单纯的生成全局ID并不困难,但生成的ID需要满足分布式系统的特定要求:

  1. 无单点故障:ID生成服务必须具备高可用性
  2. 时间有序性:ID应包含时间信息或按时间排序,便于索引优化和冷热数据分离
  3. 分片可控性:能够控制ShardingId,使相关数据位于同一分片,提高查询和修改效率
  4. 长度适中:最好为64bit,便于使用long类型操作,避免组件兼容性问题

本文将详细介绍几种主流的全局唯一ID生成方案。

二、Snowflake算法(Twitter方案)

2.1 背景

Twitter在将存储系统从MySQL迁移到Cassandra时,由于Cassandra没有内置的顺序ID生成机制,开发了Snowflake全局唯一ID生成服务。

2.2 算法结构

Snowflake生成的ID为64位,结构如下:

1
0 | 41位时间戳 | 10位机器标识 | 12位序列号

各字段说明

  • 符号位:1位,始终为0
  • 时间戳:41位,精确到毫秒,可使用69年(从自定义起始时间开始)
  • 机器标识:10位,最多支持1024个节点
  • 序列号:12位,每个节点每毫秒可生成4096个ID

2.3 核心原理

![[Snowflake算法原理图.png]]

2.4 Java实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class IdWorker {
private final long workerId;
private final static long twepoch = 1288834974657L;
private long sequence = 0L;
private final static long workerIdBits = 4L;
public final static long maxWorkerId = -1L ^ -1L << workerIdBits;
private final static long sequenceBits = 10L;
private final static long workerIdShift = sequenceBits;
private final static long timestampLeftShift = sequenceBits + workerIdBits;
public final static long sequenceMask = -1L ^ -1L << sequenceBits;
private long lastTimestamp = -1L;

public IdWorker(final long workerId) {
super();
if (workerId > this.maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format(
"worker Id can't be greater than %d or less than 0",
this.maxWorkerId));
}
this.workerId = workerId;
}

public synchronized long nextId() {
long timestamp = this.timeGen();
if (this.lastTimestamp == timestamp) {
this.sequence = (this.sequence + 1) & this.sequenceMask;
if (this.sequence == 0) {
timestamp = this.tilNextMillis(this.lastTimestamp);
}
} else {
this.sequence = 0;
}

if (timestamp < this.lastTimestamp) {
try {
throw new Exception(
String.format(
"Clock moved backwards. Refusing to generate id for %d milliseconds",
this.lastTimestamp - timestamp));
} catch (Exception e) {
e.printStackTrace();
}
}

this.lastTimestamp = timestamp;
long nextId = ((timestamp - twepoch) << timestampLeftShift)
| (this.workerId << this.workerIdShift) | (this.sequence);
return nextId;
}

private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}

private long timeGen() {
return System.currentTimeMillis();
}
}

2.5 优缺点分析

优点

  • 高性能,低延迟
  • 独立应用,不依赖外部服务
  • ID按时间有序

缺点

  • 需要独立开发和部署
  • 时钟回拨问题需要处理
  • 机器标识需要管理

三、Flickr数据库方案

3.1 方案概述

Flickr利用MySQL的自增ID特性,通过auto_incrementreplace into和MyISAM引擎实现全局ID生成。

3.2 实现步骤

创建专用表

1
2
3
4
5
6
CREATE TABLE Tickets64 (
id bigint(20) unsigned NOT NULL auto_increment,
stub char(1) NOT NULL default '',
PRIMARY KEY (id),
UNIQUE KEY stub (stub)
) ENGINE=MyISAM

生成ID操作

在事务会话中执行:

1
2
REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();

高可用配置

通过配置两台MySQL服务器,设置不同的起始值和步长来生成奇偶数ID:

1
2
3
4
5
6
7
# TicketServer1配置
auto-increment-increment = 2
auto-increment-offset = 1

# TicketServer2配置
auto-increment-increment = 2
auto-increment-offset = 2

客户端通过轮询方式获取ID。

3.3 优缺点分析

优点

  • 利用数据库自增ID机制,可靠性高
  • 生成的ID有序
  • 实现相对简单

缺点

  • 需要独立的MySQL实例,资源消耗大
  • 性能受数据库限制
  • 存在单点故障风险(需额外配置高可用)

四、UUID方案

4.1 基本概念

UUID(Universally Unique Identifier)生成的是32位16进制格式的字符串,转换为byte数组为16个字节,即128bit。

4.2 生成原理

UUID算法的核心思想是结合机器的网卡地址、当地时间和一个随机数来生成唯一标识符。

4.3 唯一性保证

理论上,如果一台机器每秒产生1000万个GUID,可以保证(概率意义上)3240年不重复。

4.4 优缺点分析

优点

  • 本地生成,无需远程调用,延迟低
  • 扩展性好,基本无性能上限
  • 全球唯一性保证

缺点

  • 无法保证趋势递增
  • 长度过长(128bit),作为主键时索引效率低
  • 常见优化方案存在局限性:
    • 转化为两个uint64整数存储
    • 折半存储(可能影响唯一性)

五、基于Redis的分布式ID生成器

5.1 实现原理

利用Redis的Lua脚本执行功能,在每个节点上通过Lua脚本生成唯一ID。

5.2 ID结构

生成的ID为64位,结构如下:

  • 41位时间戳:精确到毫秒,可使用41年
  • 12位逻辑分片ID:最大分片ID为4095
  • 10位自增长ID:每个节点每毫秒最多生成1024个ID

5.3 生成示例

假设:

  • GTM时间:Fri Mar 13 10:00:00 CST 2015
  • 毫秒数:1426212000000
  • 分片ID:53
  • 自增长序列:4

生成ID计算:

1
5981966696448054276 = 1426212000000 << 22 + 53 << 10 + 4

5.4 Redis命令使用

Redis提供TIME命令获取服务器上的秒数和微秒数。Lua脚本返回四元组:(second, microSecond, partition, seq)

客户端处理逻辑:

1
2
long id = ((second * 1000 + microSecond / 1000) << (12 + 10)) 
+ (shardId << 10) + seq;

六、MongoDB ObjectId方案

6.1 设计考虑

MongoDB的_id字段需要满足分布式环境下的全局唯一性要求,因此不能使用自增主键,而是采用ObjectId对象。

6.2 ObjectId结构

ObjectId使用12字节存储空间,结构如下:

字节位置 0-3 4-6 7-8 9-11
内容 时间戳 机器ID 进程ID 计数器

各字段说明

  • 时间戳(4字节):从标准纪元开始的秒数
  • 机器ID(3字节):服务器主机标识,通常是主机名的散列值
  • 进程ID(2字节):mongod进程标识符
  • 计数器(3字节):自动增加的计数器,每个进程独立

6.3 唯一性保证机制

  1. 时间戳:保证秒级唯一性
  2. 机器ID:考虑分布式环境,避免时钟同步问题
  3. 进程ID:保证同一服务器上多个mongod实例的唯一性
  4. 计数器:保证同一秒内的唯一性(最多16777216个)

6.4 生成位置

_id可以在服务器端生成,也可以在客户端生成。客户端生成可以降低服务器端压力。

七、方案对比

方案 优点 缺点 适用场景
Snowflake 高性能、有序、独立 需独立部署、时钟问题 大规模分布式系统
Flickr数据库 简单、可靠、有序 性能受限、资源消耗大 中小规模系统
UUID 全球唯一、无中心化 无序、存储效率低 需要全局唯一标识
Redis方案 性能好、可扩展 依赖Redis、配置复杂 Redis环境下的系统
MongoDB ObjectId 内置支持、分布式友好 MongoDB专用 MongoDB数据库系统

八、选择建议

8.1 考虑因素

  1. 系统规模:大规模系统推荐Snowflake,中小规模可考虑数据库方案
  2. 性能要求:高并发场景选择Snowflake或Redis方案
  3. 有序性需求:需要有序ID时选择Snowflake或数据库方案
  4. 技术栈:现有技术栈影响方案选择(如已使用MongoDB)

8.2 最佳实践

  1. Snowflake:适合需要高性能、有序ID的大规模分布式系统
  2. 数据库方案:适合对数据库依赖较强的系统,实现简单
  3. 混合方案:可根据业务场景组合使用不同方案

九、总结

全局唯一ID生成是分布式系统中的基础且重要的技术。不同的方案各有优劣,选择时需要综合考虑系统规模、性能要求、有序性需求和技术栈等因素。

在实际应用中,建议:

  1. 明确业务需求,选择最适合的方案
  2. 考虑系统的扩展性和维护成本
  3. 做好异常处理,特别是时钟回拨等问题
  4. 定期评估和优化ID生成策略

随着技术发展,新的ID生成方案不断涌现,但核心原则不变:在保证唯一性的前提下,追求更高的性能和更好的扩展性。


参考资源

  • Twitter Snowflake官方文档
  • Flickr技术博客
  • MongoDB官方文档
  • Redis官方文档

如何在高并发分布式系统中生成全局唯一Id - 滴答的雨 - 博客园

Excerpt

如何在高并发分布式系统中生成全局唯一Id。
1、使用数据库自增Id
2、单独开一个数据库,获取全局唯一的自增序列号或各表的MaxId
3、Sequence特性
4、通过数据库集群编号+集群内的自增类型两个字段共同组成唯一主键
5、通过设置每个集群中自增 ID 起始点
6、GU


  最近公司用到,并且在找最合适的方案,希望大家多参与讨论和提出新方案。我和我的小伙伴们也讨论了这个主题,我受益匪浅啊……

博文示例:

1.         GUID生成Int64值后是否还具有唯一性测试

2.         Random生成高唯一性随机码

今天分享的主题是:如何在高并发分布式系统中生成全局唯一Id。

但这篇博文实际上是“半分享半讨论”的博文:

1)         半分享是我将说下我所了解到的关于今天主题所涉及的几种方案。

2)         半讨论是我希望大家对各个方案都说说自己的见解,更加希望大家能提出更好的方案。(我还另外提问在此:http://q.cnblogs.com/q/53552/

我了解的方案如下……………………………………………………………………

1、  使用数据库自增Id

优势:编码简单,无需考虑记录唯一标识的问题。

缺陷:

1)         在大表做水平分表时,就不能使用自增Id,因为Insert的记录插入到哪个分表依分表规则判定决定,若是自增Id,各个分表中Id各自增长就会重复

2)         在业务上操作父、子表(即关联表)插入时,需要在插入数据库之前获取max(id)用于标识父表和子表关系,若存在并发获取max(id)的情况,max(id)会同时被别的线程获取到。

3)         DB数据记录都是可以根据ID号进行推测出来,对于一些数据敏感的场景,不建议采用

结论:适合小应用,无需分表,低并发。

2、  单独开一个数据库,获取全局唯一的自增序列号或各表的MaxId

使用MaxId表存储各表的MaxId值

专门一个数据库,记录各个表的MaxId值,建一个存储过程来取Id,逻辑大致为:开启事物,对于在表中不存在记录,直接返回一个默认值为1的键值,同时插入该条记录到table_key表中。而对于已存在的记录,key值直接在原来的key基础上加1更新到MaxId表中并返回key。(给table_key中为每个表初始化一条key为1的记录,这样就不用每次if来判断了—@辉_辉 提议)

使用此方案的问题是:每次的查询MaxId是一个性能损耗;

详细可参考:《使用MaxId表存储各表的MaxId值,以获取全局唯一Id》

                   我截取此文中的sql语法如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

第一步:创建表

create table table_key

(

       table_name   varchar(50) not null primary key,

       key_value    int         not null

)

第二步:创建存储过程来取自增ID

create procedure up_get_table_key

(

   @table_name     varchar(50),

   @key_value      int output

)

as

begin

     begin tran

         declare @key  int

         set @key=1

         if not exists(select table_name from table_key where table_name=@table_name)

            begin

              insert into table_key values(@table_name,@key)       

            end

         else   

            begin

                select @key=key_value from table_key with (nolock) where table_name=@table_name

                set @key=@key+1

                update table_key set key_value=@key where table_name=@table_name

            end

    set @key_value=@key

    commit tran

        if @@error>0

      rollback tran

end

2.         (@乐活的CodeMonkey)提醒提高获取ID时存储过程的隔离级别,避免读取到未提交事务导致并发ID重复的问题。(MSSQL事务隔离级别详解

1

2

3

4

5

6

7

eg:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

GO

BEGIN TRANSACTION;

……

GO

COMMIT TRANSACTION;

3.         (@土豆烤肉)存储过程中不使用事物,一旦使用到事物性能就急剧下滑。直接使用UPDATE获取到的更新锁,即SQL SERVER会保证UPDATE的顺序执行。(已在用户过千万的并发系统中使用)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

create procedure [dbo].[up_get_table_key]

(

   @table_name     varchar(50),

   @key_value      int output

)

as

begin

    SET NOCOUNT ON;

    DECLARE @maxId INT

    UPDATE table_key

    SET @maxId = key_value,key_value = key_value + 1

    WHERE table_name=@table_name

    SELECT @maxId

end

结论:适用中型应用,此方案解决了分表,关联表插入记录的问题。但是无法满足高并发性能要求。存在单点问题

        改进方案:时间信息 + 缓存总的maxid  (@wee616 提议)

              从redis中用lpop指令取指定key值的数据。(lpop:移除并返回列表的头元素)
              如果将指定key值的数据取完了,会触发初始化。
              初次初始化:
                  1)用for update锁表,存储最小值1和最大值50到数据库中。
                  2)将这50个数字放入redis中。
              下次初始化:
                  1)用for update锁表,存储最小值51和最大值100到数据库中。
                  2)将这50个数字放入redis中。

               数据库每天有脚本定时清理这个表,每天都将最小值归0,避免最大值过大。

结论:适合大型应用,生成Id顺序性,可读性比较好。

3、  Sequence特性

这个特性在SQL Server 2012、Oracle中可用。这个特性是数据库级别的,允许在多个表之间共享序列号。它可以解决分表在同一个数据库的情况,但倘若分表放在不同数据库,那将共享不到此序列号。(eg:Sequence使用场景:你需要在多个表之间公用一个流水号。以往的做法是额外建立一个表,然后存储流水号)

相关Sequence特性资料:

SQL Server2012中的SequenceNumber尝试

SQL Server 2012 开发新功能——序列对象(Sequence)

identity和sequence的区别

Difference between Identity and Sequence in SQL Server 2012

结论:适用中型应用,此方案不能完全解决分表问题。

4、  通过数据库集群编号+集群内的自增类型两个字段共同组成唯一主键

优点:实现简单,维护也比较简单。

缺点:关联表操作相对比较复杂,需要两个字段。并且业务逻辑必须是一开始就设计为处理复合主键的逻辑,倘若是到了后期,由单主键转为复合主键那改动成本就太大了。

结论:适合大型应用,但需要业务逻辑配合处理复合主键。

5、  通过设置每个集群中自增 ID 起始点(auto_increment_offset),将各个集群的ID进行绝对的分段来实现全局唯一。当遇到某个集群数据增长过快后,通过命令调整下一个 ID 起始位置跳过可能存在的冲突。

优点:实现简单,且比较容易根据 ID 大小直接判断出数据处在哪个集群,对应用透明。缺点:维护相对较复杂,需要高度关注各个集群 ID 增长状况。

结论:适合大型应用,但需要高度关注各个集群 ID 增长状况。

6、  GUID(Globally Unique Identifier,全局唯一标识符)

GUID通常表示成32个16进制数字(0-9,A-F)组成的字符串,如:{21EC2020-3AEA-1069-A2DD-08002B30309D},它实质上是一个128位长的二进制整数。

GUID制定的算法中使用到用户的网卡MAC地址,以保证在计算机集群中生成唯一GUID;在相同计算机上随机生成两个相同GUID的可能性是非常小的,但并不为0。所以,用于生成GUID的算法通常都加入了非随机的参数(如时间),以保证这种重复的情况不会发生。

优点:GUID是最简单的方案,跨平台,跨语言,跨业务逻辑,全局唯一的Id,数据间同步、迁移都能简单实现。

缺点:

1)         存储占了32位,且无可读性;

2)         插入时因为GUID是无需的,在聚集索引的排序规则下可能移动大量的记录。

有两位园友主推GUID,无须顺序GUID方案原因如下:

@徐少侠           GUID无序在并发下效率高,并且一个数据页内添加新行,是在B树内增加,本质没有什么数据被移动,唯一可能的,是页填充因子满了,需要拆页。而GUID方案导致的拆页比顺序ID要低太多了

@无色                我们要明白id是什么,是身份标识,标识身份是id最大的业务逻辑,不要引入什么时间,什么用户业务逻辑,那是另外一个字段干的事,使用base64(guid,uuid),是通盘考虑,完全可以更好的兼容nosql,key-value存储。

结论:适合大型应用;生成的Id不够友好;占据了32位;

改进:

1)         (@dudu告知)在SQL Server 2005中新增了NEWSEQUENTIALID函数。

详细请看:《理解newid()和newsequentialid()》

在指定计算机上创建大于先前通过该函数生成的任何 GUID 的 GUID。 newsequentialid 产生的新的值是有规律的,则索引B+树的变化是有规律的,就不会导致索引列插入时移动大量记录的问题。

但一旦服务器重新启动,其再次生成的GUID可能反而变小(但仍然保持唯一)。这在很大程度上提高了索引的性能,但并不能保证所生成的GUID一直增大。SQL的这个函数产生的GUID很简单就可以预测,因此不适合用于安全目的。

a)         只能做为数据库列的DEFAULT VALUE,不能执行类似SELECT NEWSEQUENTIALID()的语句.

b)         如何获得生成的GUID.

如果生成的GUID所在字段做为外键要被其他表使用,我们就需要得到这个生成的值。通常,PK是一个IDENTITY字段,我们可以在INSERT之后执行 SELECT SCOPE_IDENTITY()来获得新生成的ID,但是由于NEWSEQUENTIALID()不是一个INDETITY类型,这个办法是做不到了,而他本身又只能在默认值中使用,不可以事先SELECT好再插入,那么我们如何得到呢?有以下两种方法:

1

2

3

4

5

6

7

8

9

10

11

12

DECLARE @outputTable TABLE(ID uniqueidentifier)

INSERT INTO TABLE1(col1, col2)

OUTPUT INSERTED.ID INTO @outputTable

VALUES('value1', 'value2')

SELECT ID FROM @outputTable

INSERT INTO TABLE1(col1, col2)

VALUES('value1', 'value2')

SELECT ROWGUIDCOL FROM TABLE1

结论:适合大型应用,解决了GUID无序特性导致索引列插入移动大量记录的问题。但是在关联表插入时需要返回数据库中生成的GUID;生成的Id不够友好;占据了32位。

2)         “COMB”(combined guid/timestamp,意思是:组合GUID/时间截)

(感谢:@ ethan-luo ,@lcs-帅

COMB数据类型的基本设计思路是这样的:既然GUID数据因毫无规律可言造成索引效率低下,影响了系统的性能,那么能不能通过组合的方式,保留GUID的10个字节,用另6个字节表示GUID生成的时间(DateTime),这样我们将时间信息与GUID组合起来,在保留GUID的唯一性的同时增加了有序性,以此来提高索引效率。

在NHibernate中,COMB型主键的生成代码如下所示:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

/// <summary> /// Generate a new <see cref="Guid"/> using the comb algorithm.

/// </summary>

private Guid GenerateComb()

{

    byte[] guidArray = Guid.NewGuid().ToByteArray();

    DateTime baseDate = new DateTime(1900, 1, 1);

    DateTime now = DateTime.Now;

    TimeSpan days = new TimeSpan(now.Ticks - baseDate.Ticks);

    TimeSpan msecs = now.TimeOfDay;

    byte[] daysArray = BitConverter.GetBytes(days.Days);

    byte[] msecsArray = BitConverter.GetBytes((long)

      (msecs.TotalMilliseconds / 3.333333));

    Array.Reverse(daysArray);

    Array.Reverse(msecsArray);

    Array.Copy(daysArray, daysArray.Length - 2, guidArray,

      guidArray.Length - 6, 2);

    Array.Copy(msecsArray, msecsArray.Length - 4, guidArray,

      guidArray.Length - 4, 4);

    return new Guid(guidArray);

}

结论:适合大型应用。即保留GUID的唯一性的同时增加了GUID有序性,提高了索引效率;解决了关联表业务问题;生成的Id不够友好;占据了32位。

3)         长度问题,使用Base64或Ascii85编码解决。(要注意的是上述有序性方案在进行编码后也会变得无序)

如:

GUID:{3F2504E0-4F89-11D3-9A0C-0305E82C3301}

当需要使用更少的字符表示GUID时,可能会使用Base64或Ascii85编码。Base64编码的GUID有22-24个字符,如:

7QDBkvCA1+B9K/U0vrQx1A

7QDBkvCA1+B9K/U0vrQx1A==

Ascii85编码后是20个字符,如:

5:$Hj:Pf\4RLB9%kU\Lj

                   代码如:

         Guid guid = Guid.NewGuid();

         byte[] buffer = guid.ToByteArray();

         var shortGuid = Convert.ToBase64String(buffer);

                   结论:适合大型应用,缩短GUID的长度。生成的Id不够友好;

7、  GUID TO Int64

对于GUID的可读性,有园友给出如下方案:(感谢:@黑色的羽翼

即将GUID转为了19位数字,数字反馈给客户可以一定程度上缓解友好性问题。EG:

GUID: cfdab168-211d-41e6-8634-ef5ba6502a22    (不友好)

Int64: 5717212979449746068                                      (友好性还行)

不过我的小伙伴说ToInt64后就不唯一了。因此我专门写了个并发测试程序,后文将给出测试结果截图及代码简单说明。

(唯一性、业务适合性是可以权衡的,这个唯一性肯定比不过GUID的,一般程序上都会安排错误处理机制,比如异常后执行一次重插的方案……)

结论:适合大型应用,生成相对友好的Id****(纯数字)。****

8、  自己写编码规则

优点:全局唯一Id,符合业务后续长远的发展(可能具体业务需要自己的编码规则等等)。

缺陷:根据具体编码规则实现而不同;还要考虑倘若主键在业务上允许改变的,会带来外键同步的麻烦。

我这边写两个编码规则方案:(可能不唯一,只是个人方案,也请大家提出自己的编码规则)

1)         12位年月日时分秒+5位随机码+3位服务器编码  (这样就完全单机完成生成全局唯一编码)-–共20位

缺陷:因为附带随机码,所以编码缺少一定的顺序感。(生成高唯一性随机码的方案稍后给给出程序)

2)         12位年月日时分秒+5位流水码+3位服务器编码 (这样流水码就需要结合数据库和缓存)-–共20位   (将影响顺序权重大的“流水码”放前面,影响顺序权重小的服务器编码放后)

缺陷:因为使用到流水码,流水码的生成必然会遇到和MaxId、序列表、Sequence方案中类似的问题

(为什么没有毫秒?毫秒也不具备业务可读性,我改用5位随机码、流水码代替,推测1秒内应该不会下99999[五位]条语法)

结论:适合大型应用,从业务上来说,有一个规则的编码能体现产品的专业成度。

GUID生成Int64值后是否还具有唯一性测试

测试环境

clip_image002

主要测试思路:

1.         根据内核数使用多线程并发生成Guid后再转为Int64位值,放入集合A、B、…N,多少个线程就有多少个集合。

2.         再使用Dictionary字典高效查key的特性,将步骤1中生成的多个集合全部加到Dictionary中,看是否有重复值。

示例注解:测了 Dictionary<long,bool> 最大容量就在5999470左右,所以每次并发生成的唯一值总数控制在此范围内,让测试达到最有效话。

主要代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

for (int i = 0; i <= Environment.ProcessorCount - 1; i++)

{

    ThreadPool.QueueUserWorkItem(

        (list) =>

        {

            List<long> tempList = list as List<long>;

            for (int j = 1; j < listLength; j++)

            {

                byte[] buffer = Guid.NewGuid().ToByteArray();

                tempList.Add(BitConverter.ToInt64(buffer, 0));

            }

            barrier.SignalAndWait();

        }, totalList[i]);

}

测试数据截图:                                                                           

clip_image004

数据一(循环1000次,测试数:1000*5999470)

image

数据二(循环5000次,测试数:5000*5999470)--跑了一个晚上……

image

**感谢@Justany_WhiteSnow****的专业回答:(**大家分析下,我数学比较差,稍后再说自己的理解)

GUID桶数量:(2 ^ 4) ^ 32 = 2 ^ 128

Int64桶数量: 2 ^ 64

倘若每个桶的机会是均等的,则每个桶的GUID数量为:

(2 ^ 128) / (2 ^ 64) = 2 ^ 64 = 18446744073709551616

也就是说,其实重复的机会是有的,只是概率问题。

楼主测试数是29997350000,发生重复的概率是:

1 - ((1 - (1 / (2 ^ 64))) ^ 29997350000) ≈ 1 - ((1 - 1 / (2 ^ 64)) ^ (2 ^ 32)) < 1 - 1 + 1 / (2 ^ 32) = 1 / (2 ^ 32) ≈ 2.3283064e-10

(唯一性、业务适合性是可以权衡的,这个唯一性肯定比不过GUID的,一般程序上都会安排错误处理机制,比如异常后执行一次重插的方案……)

(唯一性、业务适合性是可以权衡的,这个唯一性肯定比不过GUID的,一般程序上都会安排错误处理机制,比如异常后执行一次重插的方案……)

结论:GUID转为Int64值后,也具有高唯一性,可以使用与项目中。

Random生成高唯一性随机码

我使用了五种Random生成方案,要Random生成唯一主要因素就是种子参数要唯一。

不过该测试是在单线程下的,多线程应使用不同的Random实例,所以对结果影响不会太大。

1.         使用Environment.TickCount做为Random参数(即Random的默认参数),重复性最大。

2.         使用DateTime.Now.Ticks做为Random参数,存在重复。

3.         使用unchecked((int)DateTime.Now.Ticks)做为Random参数,存在重复。

4.         使用Guid.NewGuid().GetHashCode()做为random参数,测试不存在重复(或存在性极小)。

5.         使用RNGCryptoServiceProvider做为random参数,测试不存在重复(或存在性极小)。

即:

        static int GetRandomSeed()

        {

            byte[] bytes = new byte[4];

            System.Security.Cryptography.RNGCryptoServiceProvider rng

= new System.Security.Cryptography.RNGCryptoServiceProvider();

            rng.GetBytes(bytes);

            return BitConverter.ToInt32(bytes, 0);

        }

测试结果:

clip_image007

*结论:随机码使用RNGCryptoServiceProvider***或Guid.NewGuid().GetHashCode()**生成的唯一性较高。

一些精彩评论(部分更新到原博文对应的地方)

一、

数据库文件体积只是一个参考值,可水平扩展系统性能(如nosql,缓存系统)并不和文件体积有高指数的线性相关。

如taobao/qq的系统比拼byte系统慢,关键在于索引的命中率,缓存,系统的水平扩展。

如果数据库很少,你搞这么多byte能提高性能?

如果数据库很大,你搞这么多byte不兼容索引不兼容缓存,不是害自已吗?

如果数据库要求伸缩性,你搞这么多byte,需要不断改程序,不是自找苦吗?

如果数据库要求移植性,你搞这么多byte,移植起来不如重新设计,这是不是很多公司不断加班的原因?

不依赖于数据存储系统是分层设计思想的精华,实现战略性能最大化,而不是追求战术单机性能最大化。

不要迷信数据库性能,不要迷信三范式,不要使用外键,不要使用byte,不要使用自增id,不要使用存储过程,不要使用内部函数,不要使用非标准sql,存储系统只做存储系统的事。当出现系统性能时,如此设计的数据库可以更好的实现迁移数据库(如mysql->oracle),实现nosql改造((mongodb/hadoop),实现key-value缓存(redis,memcache)。

二、

很多程序员有对性能认识有误区,如使用存储过程代替正常程序,其实使用存储过程只是追求单服务器的高性能,当需要服务器水平扩展时,存储过程中的业务逻辑就是你的噩运。(web服务器可以简单伸缩,但是数据库伸缩比较复杂)

三、

除数字日期,能用字符串存储的字段尽量使用字符串存储,不要为节省那不值钱的1个g的硬盘而使用类似字节之类的字段,进而大幅牺牲系统可伸缩性和可扩展性。

不要为了追求所谓的性能,引入byte,使用byte注定是短命和难于移植,想想为什么html,email一直流行,因为它们使用的是字符串表示法,只要有人类永远都能解析,如email把二进制转成base64存储。除了实时系统,视频外,建议使用字符串来存储数据,系统性能的关键在于分布式,在于水平扩展。

本次博文到此结束,希望大家对本次主题“如何在高并发分布式系统中生成全局唯一Id”多提出自己宝贵的意见。另外看着感觉舒服,还请多帮推荐…推荐……