在一个分布式环境中,我们习惯使用GUID做主键,来保证全局唯一,然后,GUID做主键真的合适吗?

  其实GUID做主键本身没有问题,微软的很多项目自带DB都是使用GUID做主键的,显然,这样做是没有问题的。然而,SQL Server默认会将主键设置为聚集索引,使用GUID做聚集索引就有问题了。很多时候程序员容易接受SQL Server这一默认设置,但无序GUID做聚集索引显然是低效的。

  那么,我们在项目中如何避免这一问题呢?

  主要的思路还是两方面——方案一,选择合适的列作为聚集索引;方案二,使用有序的主键。

1 方案一,选择合适的列做聚集索引

  选择原则很简单——字段值尽量唯一,字段占用字节尽量小,字段值很少修改,字段多用于查询范围数据或排序的数据。

  之所以是根据以上原则选择,主要还是基于B+树数据索引问题,这部分内容都比较基础,这里就不举例验证了,以上原则还是比较公认的,即便读者不太理解其中原理,也请记住这一选择规则。

  常见的备选项——自增列(Id)和时间列(CreateTime)。

  聚集索引的最大用处就是帮助范围查询快速定位,从而减小数据库IO的消耗来提升查询效率。对于范围查询我们更多的应用在自增列和时间列上,因为这两列本身反应了数据的创建顺序,符合多数范围查询的场景需要。

  大部分时候,我们仍然可以使用GUID做主键,只需要重新设置聚集索引就行。

2 方案二,有序的主键

  对于一个分布式环境,保证唯一和有序性,实际上有多种方法,各有利弊。

2.1 分布式数据库

  对于分布式数据库,简单使用自增主键即可,比如Tidb。

  TiDB 中,自增列只保证自增且唯一,并不保证连续分配。TiDB 目前采用批量分配 ID 的方式,所以如果在多台 TiDB 上同时插入数据,分配的自增 ID 会不连续。TiDB 实现自增 ID 的原理是每个 tidb-server 实例缓存一段 ID 值用于分配(目前会缓存 30000 个 ID),用完这段值再去取下一段。

  优点:简单好用

  缺点:不能设置ID,需要使用数据库的;ID不保证连续分配,也无法根据ID来判断数据创建的先后;负载不均匀,有数据热点问题

2.2 基于Redis等中间件的

  根据数据库分片方式不同,又有两种情形。

  方式一,取模分片

  思路:Redis初始化当前最大ID值,之后进行自增,分布式数据访问层根据取模进行路由

  优点:数据库负载比较均匀

  缺点:需要尽量保证Redis和数据库的一致性;Redis不稳定会影响系统;在增加数据库后,需要大批量移动数据,且需要成倍增加DB

  方式二,按范围分片

  思路:每台服务器负责一个号段,不够用了就增加服务器,Redis初始化当前最大ID值,之后进行自增,分布式数据访问层根据号段进行路由

  优点:增加数据库可以不迁移数据,可以一个一个的增加数据库

  缺点:需要尽量保证Redis和数据库的一致性;Redis不稳定会影响系统;数据分布严重不均匀,严重的热点问题

2.3 基于算法实现

  这里介绍下Twitter的Snowflake算法——snowflake,它把时间戳,工作机器id,序列号组合在一起,以保证在分布式系统中唯一性和自增性。

  snowflake生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,在同一毫秒内最多可以生成 1024 X 4096 = 4194304个全局唯一ID。

  优点:不依赖数据库,完全内存操作速度快

  缺点:不同服务器需要保证系统时钟一致

  snowflake的C#版本的简单实现:

  1. public class SnowflakeIdWorker
  2. {
  3. /// <summary>
  4. /// 开始时间截
  5. /// 1288834974657 是(Thu, 04 Nov 2010 01:42:54 GMT) 这一时刻到1970-01-01 00:00:00时刻所经过的毫秒数。
  6. /// 当前时刻减去1288834974657 的值刚好在2^41 里,因此占41位。
  7. /// 所以这个数是为了让时间戳占41位才特地算出来的。
  8. /// </summary>
  9. public const long Twepoch = 1288834974657L;
  10.  
  11. /// <summary>
  12. /// 工作节点Id占用5位
  13. /// </summary>
  14. const int WorkerIdBits = ;
  15.  
  16. /// <summary>
  17. /// 数据中心Id占用5位
  18. /// </summary>
  19. const int DatacenterIdBits = ;
  20.  
  21. /// <summary>
  22. /// 序列号占用12位
  23. /// </summary>
  24. const int SequenceBits = ;
  25.  
  26. /// <summary>
  27. /// 支持的最大机器Id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
  28. /// </summary>
  29. const long MaxWorkerId = -1L ^ (-1L << WorkerIdBits);
  30.  
  31. /// <summary>
  32. /// 支持的最大数据中心Id,结果是31
  33. /// </summary>
  34. const long MaxDatacenterId = -1L ^ (-1L << DatacenterIdBits);
  35.  
  36. /// <summary>
  37. /// 机器ID向左移12位
  38. /// </summary>
  39. private const int WorkerIdShift = SequenceBits;
  40.  
  41. /// <summary>
  42. /// 数据标识id向左移17位(12+5)
  43. /// </summary>
  44. private const int DatacenterIdShift = SequenceBits + WorkerIdBits;
  45.  
  46. /// <summary>
  47. /// 时间截向左移22位(5+5+12)
  48. /// </summary>
  49. public const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits;
  50.  
  51. /// <summary>
  52. /// 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
  53. /// </summary>
  54. private const long SequenceMask = -1L ^ (-1L << SequenceBits);
  55.  
  56. /// <summary>
  57. /// 毫秒内序列(0~4095)
  58. /// </summary>
  59. private long _sequence = 0L;
  60.  
  61. /// <summary>
  62. /// 上次生成Id的时间截
  63. /// </summary>
  64. private long _lastTimestamp = -1L;
  65.  
  66. /// <summary>
  67. /// 工作节点Id
  68. /// </summary>
  69. public long WorkerId { get; protected set; }
  70.  
  71. /// <summary>
  72. /// 数据中心Id
  73. /// </summary>
  74. public long DatacenterId { get; protected set; }
  75.  
  76. /// <summary>
  77. /// 构造器
  78. /// </summary>
  79. /// <param name="workerId">工作ID (0~31)</param>
  80. /// <param name="datacenterId">数据中心ID (0~31)</param>
  81. public SnowflakeIdWorker(long workerId, long datacenterId)
  82. {
  83. WorkerId = workerId;
  84. DatacenterId = datacenterId;
  85.  
  86. if (workerId > MaxWorkerId || workerId < )
  87. {
  88. throw new ArgumentException(String.Format("worker Id can't be greater than {0} or less than 0", MaxWorkerId));
  89. }
  90. if (datacenterId > MaxDatacenterId || datacenterId < )
  91. {
  92. throw new ArgumentException(String.Format("datacenter Id can't be greater than {0} or less than 0", MaxDatacenterId));
  93. }
  94. }
  95.  
  96. private static readonly object _lockObj = new Object();
  97.  
  98. /// <summary>
  99. /// 获得下一个ID (该方法是线程安全的)
  100. /// </summary>
  101. /// <returns></returns>
  102. public virtual long NextId()
  103. {
  104. lock (_lockObj)
  105. {
  106. //获取当前时间戳
  107. var timestamp = TimeGen();
  108.  
  109. //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
  110. if (timestamp < _lastTimestamp)
  111. {
  112. throw new InvalidOperationException(String.Format(
  113. "Clock moved backwards. Refusing to generate id for {0} milliseconds", _lastTimestamp - timestamp));
  114. }
  115.  
  116. //如果是同一时间生成的,则进行毫秒内序列
  117. if (_lastTimestamp == timestamp)
  118. {
  119. _sequence = (_sequence + ) & SequenceMask;
  120. //毫秒内序列溢出
  121. if (_sequence == )
  122. {
  123. //阻塞到下一个毫秒,获得新的时间戳
  124. timestamp = TilNextMillis(_lastTimestamp);
  125. }
  126. }
  127.  
  128. //时间戳改变,毫秒内序列重置
  129. else
  130. {
  131. _sequence = ;
  132. }
  133.  
  134. //上次生成ID的时间截
  135. _lastTimestamp = timestamp;
  136.  
  137. //移位并通过或运算拼到一起组成64位的ID
  138. return ((timestamp - Twepoch) << TimestampLeftShift) |
  139. (DatacenterId << DatacenterIdShift) |
  140. (WorkerId << WorkerIdShift) | _sequence;
  141. }
  142. }
  143.  
  144. /// <summary>
  145. /// 生成当前时间戳
  146. /// </summary>
  147. /// <returns>毫秒</returns>
  148. private static long GetTimestamp()
  149. {
  150. return (long)(DateTime.UtcNow - new DateTime(, , , , , , DateTimeKind.Utc)).TotalMilliseconds;
  151. }
  152.  
  153. /// <summary>
  154. /// 生成当前时间戳
  155. /// </summary>
  156. /// <returns>毫秒</returns>
  157. protected virtual long TimeGen()
  158. {
  159. return GetTimestamp();
  160. }
  161.  
  162. /// <summary>
  163. /// 阻塞到下一个毫秒,直到获得新的时间戳
  164. /// </summary>
  165. /// <param name="lastTimestamp">上次生成Id的时间截</param>
  166. /// <returns></returns>
  167. protected virtual long TilNextMillis(long lastTimestamp)
  168. {
  169. var timestamp = TimeGen();
  170. while (timestamp <= lastTimestamp)
  171. {
  172. timestamp = TimeGen();
  173. }
  174. return timestamp;
  175. }
  176. }

  测试:

  1. [TestClass]
  2. public class SnowflakeTest
  3. {
  4. [TestMethod]
  5. public void MainTest()
  6. {
  7. SnowflakeIdWorker idWorker = new SnowflakeIdWorker(, );
  8. for (int i = ; i < ; i++)
  9. {
  10. Trace.WriteLine(string.Format("{0}-{1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:ffffff"), idWorker.NextId()));
  11. }
  12. }
  13. }

  结果:

  总之,GUID能满足大部分需要,但如果想要我们的程序精益求精,也可以考虑使用本文提到的方法,感谢阅读。

GUID做主键真的合适吗的更多相关文章

  1. 使用Guid做主键和int做主键性能比较

    使用Guid做主键和int做主键性能比较 在数据库的设计中我们常常用Guid或int来做主键,根据所学的知识一直感觉int做主键效率要高,但没有做仔细的测试无法 说明道理.碰巧今天在数据库的优化过程中 ...

  2. SQLSERVER如何使用递增排序的GUID做主键

    场景: 产品表数据量较大想用Guid做表的主键,并在此字段上建立聚簇索引. 因为Guid是随机生成的,生成的值大小是不确定的,每次生成的数可能很大,也可能很小.这样会影响插入的效率 1.NEWSEQU ...

  3. int 和guid做主键的时候性能的区别

    1.在经常需要做数据迁移的系统中,建议用Guid.并且在相应的外键字段,也就是用来做连接查询的字段添加非聚集索引,对于改善性能有极大的好处.where条件的字段也可以适当添加非聚集索引. 2.在使用G ...

  4. SQL GUID和自增列做主键的优缺点

    我们公司的数据库全部是使用GUID做主键的,很多人习惯使用int做主键.所以呢,这里总结一下,将两种数据类型做主键进行一个比较. 使用INT做主键的优点: 1.需要很小的数据存储空间,仅仅需要4 by ...

  5. Guid和Sequence做主键的比较

    记得A项目组是一个物流管理系统,后台采用了Oracle数据库.在系统中的核心表托运单表中,关于主键采用何种数据类型,是 sequence 还是用GUID , 大家起了争论. 从网络搜索得到的结论看,一 ...

  6. (转)mysql中InnoDB表为什么要建议用自增列做主键

    InnoDB引擎表的特点 1.InnoDB引擎表是基于B+树的索引组织表(IOT) 关于B+树 (图片来源于网上) B+ 树的特点: (1)所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关 ...

  7. 开发反模式(GUID) - 伪键洁癖

    一.目标:整理数据 有的人有强迫症,他们会为一系列数据的断档而抓狂. 一方面,Id为3这一行确实发生过一些事情,为什么这个查询不返回Id为3的这一行?这条记录数据丢失了吗?那个Column到底是什么? ...

  8. 扩展ASP.NET Identity使用Int做主键

    当我们默认新建一个ASP.NET MVC项目的时候,使用的身份认证系统是ASP.NET Identity.但是这里的Identity使用的主键为String类型的GUID.当然这是大多数系统首先类型. ...

  9. mysql中InnoDB表为什么要建议用自增列做主键

    InnoDB引擎表的特点 1.InnoDB引擎表是基于B+树的索引组织表(IOT) 关于B+树 (图片来源于网上) B+ 树的特点: (1)所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关 ...

随机推荐

  1. 个人永久性免费-Excel催化剂功能第26波-正确的Excel密码管理之道

    Excel等文档肩负着我们日常大量的信息存储和传递工作,难免出现数据安全的问题,OFFICE自带的密码设置,在什么样的场景下才有必要使用?网上所宣称的OFFICE文档密码保护不安全,随时可被破解,究竟 ...

  2. Excel催化剂开源第15波-VSTO开发之DataTable数据导出至单元格区域

    上篇提到如何从Excel界面上拿到用户的数据,另外反方向的怎样输出给用户数据,也是关键之处. VSTO最大的优势是,这双向的过程中,全程有用户的交互操作. 而一般IT型的程序,都是脱离用户的操作,只能 ...

  3. 深入理解 JavaScript 单例模式 (Singleton Pattern)

    概念 单例模式,也叫单子模式,是一种常用的软件设计模式.在应用这个模式时,单例对象的类必须保证只有一个实例存在. 核心:确保只有一个实例,并提供全局访问. 实现思路 一个类能返回对象一个引用(永远是同 ...

  4. 关于RedirectAttributes 重定向带参数请求问题

    @RequestMapping("/delete") public String delete(String id, RedirectAttributes redirectAttr ...

  5. InstantiationException:mybatis.spring.transaction.SpringManagedTransactionFactory

    问题表现 Error creating bean with name 'sqlSessionFactory' Invocation of init method failed; nested exce ...

  6. Gin框架 - 自定义错误处理

    目录 概述 错误处理 自定义错误处理 panic 和 recover 推荐阅读 概述 很多读者在后台向我要 Gin 框架实战系列的 Demo 源码,在这里再说明一下,源码我都更新到 GitHub 上, ...

  7. linux初学者-网络管理篇

    linux初学者-网络管理篇 linux学习中,网络管理是非常重要的一个内容,本篇将会介绍一些ip.网关.DNS配置的一些基本内容. 1.ip配置 1.1.ip查询  在linux系统中一般可以使用& ...

  8. 这 3 个 Set 集合的实现有点简单,那来做个总结吧

    Set 接口是 Java Collections Framework 中的一员,它的特点是:不能包含重复的元素,允许且最多只有一个 null 元素.Java 中有三个常用的 Set 实现类: Hash ...

  9. 【MySQL】Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and ...

    线上遇到这个问题,详细信息如下: SQL state [HY000]; error code [1267]; Illegal mix of collations (utf8mb4_general_ci ...

  10. 腾讯企业邮箱 POP3/SMTP 设置

    下午魅族MX2刷完机,原先配置的公司邮箱还要重新配置.有些地方需要改,找到了篇文章,如下: 腾讯企业邮箱支持通过客户端进行邮件管理.POP3/SMTP协议收发邮件服务器地址分别如下.接收邮件服务器:p ...