@

最近在项目中用了UUID的方式生成主键,一开始只是想把这种UUID的方式生成主键记录下来,在查阅资料的过程中,又有了一些新的认识和思考。

主键定义

唯一标识表中每行的一个列(或一组列)称为主键。主键用来表示一个特定的行。

主键设计和应用原则

除了满足MySQL强制实施的规则(主键不可重复;一行中主键不可为空)之外,主键的设计和应用应当还遵守以下公认的原则:

  • 不更新主键列中的值;
  • 不重用主键列的值;
  • 不在主键列中使用可能会更改的值。(例如,如果使用一个

    名字作为主键以标识某个供应商,当该供应商合并和更改其

    名字时,必须更改这个主键。)

主键生成策略

自增ID

使用数据库的自动增长(auto_increment),是比较简单和常见的ID生成方案,数据库内部可以确保生成id的唯一性。

优点:

1、数据库自动编号,速度快,而且是增量增长,聚集型主键按顺序存放,对于检索非常有利。

2、 数字型,占用空间小,易排序,在程序中传递方便。

缺点:

1、不支持水平分片架构,水平分片的设计当中,这种方法显然不能保证全局唯一。

2、对数据库有依赖,每种数据库可能实现不一样,数据库切换时候,涉及到代码的修改,不利于扩展

结论:

自增id做主键适用于非分布式架构。

UUID

UUID:通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息数目的一个128位标识符,还有相关的术语:全局唯一标识符(GUID)。
根据标准方法生成,不依赖中央机构的注册和分配,UUID具有唯一性,这与其他大多数编号方案不同。重复UUID码概率接近零,可以忽略不计。UUID是由一组32位数的16进制数字所构成,标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的32个字符。示例:

550e8400-e29b-41d4-a716-446655440000

到目前为止业界一共有5种方式生成UUID,详情可见IETF发布的UUID规范A Universally Unique IDentifier (UUID) URN Namespace

优点:

性能非常高:本地生成,没有网络消耗。

缺点:

1、不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。

2、信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

3、ID作为主键时在特定的环境会存在一些问题,比如需要排序的时候——UUID是无序的。

4、MySQL官方有明确的建议主键要尽量越短越好,36个字符长度的UUID不符合要求。

5、对MySQL索引不利:作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。

关于MySQL 使用自增ID主键和UUID 作为主键的性能比较可以查看参考【8】。

结论:

1、uuid做主键适用于小规模分布式架构用。

2、在使用uuid作为主键的时候,最好设计createtime(创建时间)列和modifytime(修改时间)列以应付可能的排序等场景。

自建的id生成器

Twitter的snowflake算法

Twitter的snowflake算法的核心把时间戳,工作机器id,序列号组合在一起。



除了最高位bit标记为不可用以外,其余三组bit占位均可浮动,看具体的业务需求而定。默认情况下41bit的时间戳可以支持该算法使用到2082年,10bit的工作机器id可以支持1023台机器,序列号支持1毫秒产生4095个自增序列id。

具体可以查看:https://github.com/twitter-archive/snowflake.git (但是最近一次的提交是6年前,显示已经停止了对初始版snowflake的支持)

源码如下:

  1. package com.yjd.comm.util;/**
  2. * Created by pc on 2017/8/16 0016.
  3. */
  4. /**
  5. * Twitter_Snowflake<br>
  6. * SnowFlake的结构如下(每部分用-分开):<br>
  7. * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
  8. * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
  9. * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
  10. * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
  11. * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
  12. * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
  13. * 加起来刚好64位,为一个Long型。<br>
  14. * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
  15. */
  16. public class SnowflakeIdWorker {
  17. // ==============================Fields===========================================
  18. /**
  19. * 开始时间截 (2015-01-01)
  20. */
  21. private final long twepoch = 1420041600000L;
  22. /**
  23. * 机器id所占的位数
  24. */
  25. private final long workerIdBits = 5L;
  26. /**
  27. * 数据标识id所占的位数
  28. */
  29. private final long datacenterIdBits = 5L;
  30. /**
  31. * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
  32. */
  33. private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
  34. /**
  35. * 支持的最大数据标识id,结果是31
  36. */
  37. private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
  38. /**
  39. * 序列在id中占的位数
  40. */
  41. private final long sequenceBits = 12L;
  42. /**
  43. * 机器ID向左移12位
  44. */
  45. private final long workerIdShift = sequenceBits;
  46. /**
  47. * 数据标识id向左移17位(12+5)
  48. */
  49. private final long datacenterIdShift = sequenceBits + workerIdBits;
  50. /**
  51. * 时间截向左移22位(5+5+12)
  52. */
  53. private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
  54. /**
  55. * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
  56. */
  57. private final long sequenceMask = -1L ^ (-1L << sequenceBits);
  58. /**
  59. * 工作机器ID(0~31)
  60. */
  61. private long workerId;
  62. /**
  63. * 数据中心ID(0~31)
  64. */
  65. private long datacenterId;
  66. /**
  67. * 毫秒内序列(0~4095)
  68. */
  69. private long sequence = 0L;
  70. /**
  71. * 上次生成ID的时间截
  72. */
  73. private long lastTimestamp = -1L;
  74. //==============================Constructors=====================================
  75. /**
  76. * 构造函数
  77. *
  78. * @param workerId 工作ID (0~31)
  79. * @param datacenterId 数据中心ID (0~31)
  80. */
  81. public SnowflakeIdWorker(long workerId, long datacenterId) {
  82. if (workerId > maxWorkerId || workerId < 0) {
  83. throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
  84. }
  85. if (datacenterId > maxDatacenterId || datacenterId < 0) {
  86. throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
  87. }
  88. this.workerId = workerId;
  89. this.datacenterId = datacenterId;
  90. }
  91. // ==============================Methods==========================================
  92. /**
  93. * 获得下一个ID (该方法是线程安全的)
  94. *
  95. * @return SnowflakeId
  96. */
  97. public synchronized long nextId() {
  98. long timestamp = timeGen();
  99. //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
  100. if (timestamp < lastTimestamp) {
  101. throw new RuntimeException(
  102. String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
  103. }
  104. //如果是同一时间生成的,则进行毫秒内序列
  105. if (lastTimestamp == timestamp) {
  106. sequence = (sequence + 1) & sequenceMask;
  107. //毫秒内序列溢出
  108. if (sequence == 0) {
  109. //阻塞到下一个毫秒,获得新的时间戳
  110. timestamp = tilNextMillis(lastTimestamp);
  111. }
  112. }
  113. //时间戳改变,毫秒内序列重置
  114. else {
  115. sequence = 0L;
  116. }
  117. //上次生成ID的时间截
  118. lastTimestamp = timestamp;
  119. //移位并通过或运算拼到一起组成64位的ID
  120. return ((timestamp - twepoch) << timestampLeftShift) //
  121. | (datacenterId << datacenterIdShift) //
  122. | (workerId << workerIdShift) //
  123. | sequence;
  124. }
  125. /**
  126. * 阻塞到下一个毫秒,直到获得新的时间戳
  127. *
  128. * @param lastTimestamp 上次生成ID的时间截
  129. * @return 当前时间戳
  130. */
  131. protected long tilNextMillis(long lastTimestamp) {
  132. long timestamp = timeGen();
  133. while (timestamp <= lastTimestamp) {
  134. timestamp = timeGen();
  135. }
  136. return timestamp;
  137. }
  138. /**
  139. * 返回以毫秒为单位的当前时间
  140. *
  141. * @return 当前时间(毫秒)
  142. */
  143. protected long timeGen() {
  144. return System.currentTimeMillis();
  145. }
  146. //==============================Test=============================================
  147. /**
  148. * 测试
  149. */
  150. public static void main(String[] args) {
  151. SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1);
  152. long startime = System.currentTimeMillis();
  153. for (int i = 0; i < 4000000; i++) {
  154. long id = idWorker.nextId();
  155. // System.out.println(Long.toBinaryString(id));
  156. // System.out.println(id);
  157. }
  158. System.out.println(System.currentTimeMillis() - startime);
  159. }
  160. }

优点:

1、毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。

2、 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。

3、可以根据自身业务特性分配bit位,非常灵活。

缺点:

强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

结论:

用自建的id生成器做主键适用于大规模分布式架构

参考:

【1】:红心李 :MySQL主键设计

【2】:Uncle Nucky :MySQL数据库主键设计原则

【3】:ellis:设计套路:Mysql主键的选取

【4】:路人甲Java:分布式系统生成唯一id常见方案

【5】:《MySQL必知必会》

【6】:美团技术团队:Leaf——美团点评分布式ID生成系统

【7】:UUID performance in MySQL?

【8】:alex.shu:MySQL 使用自增ID主键和UUID 作为主键的优劣比较详细过程(从百万到千万表记录测试)

【9】:咖啡拿铁:如果再有人问你分布式ID,这篇文章丢给他

【10】:漫漫路:Twitter-Snowflake,64位自增ID算法详解

MySQL主键设计盘点的更多相关文章

  1. MySQL主键设计

    [TOC] 在项目过程中遇到一个看似极为基础的问题,但是在深入思考后还是引出了不少问题,觉得有必要把这一学习过程进行记录. MySQL主键设计原则 MySQL主键应当是对用户没有意义的. MySQL主 ...

  2. mysql主键问题

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/qq_22314145/article/details/80824660 MySQL主键 一. MyS ...

  3. MySQL中innodb表主键设计原则

    主键设计的原则:1. 一定要显式定义主键2. 采用与业务无关的单独列3. 采用自增列4. 数据类型采用int,并尽可能小,能用tinyint就不用int,能用int就不用bigint5. 将主键放在表 ...

  4. 【转载】mysql主键的缺少导致备库hang

    最近线上频繁的出现slave延时的情况,经排查发现为用户在删除数据的时候,由于表主键的主键的缺少,同时删除条件没有索引,或或者删除的条件过滤性极差,导致slave出现hang住,严重的影响了生产环境的 ...

  5. mysql主键,外键,索引

    主键 唯一而非空,只能有一个 作用: 1.唯一的标识一行  2.作为一个可以被外键有效引用的对象  3.保证数据完整性 设计原则: 1. 主键应当是对用户没有意义的.如果用户看到了一个表示多对多关系的 ...

  6. mysql主键的缺少导致备库hang

    最近线上频繁的出现slave延时的情况,经排查发现为用户在删除数据的时候,由于表主键的主键的缺少,同时删除条件没有索引,或或者删除的条件过滤性极差,导致slave出现hang住,严重的影响了生产环境的 ...

  7. PowerDesigner 15设置mysql主键自动增长及基数

    PowerDesigner 15设置mysql主键自动增长及基数 1.双击标示图,打开table properties->columns,  如图点击图标Customize Columns an ...

  8. MYSQL主键自动增加的配置及auto_increment注意事项

    文章一 原文地址: http://ej38.com/showinfo/mysql-202971.html 文章二:   点击转入第二篇文章 在数据库应用,我们经常要用到唯一编号.在MySQL中可通过字 ...

  9. 获得自动增长的MySQL主键

    下面的脚本教您如何获得自动增长的MySQL主键,如果您对MySQL主键方面感兴趣的话,不妨一看,相信对您学习MySQL主键方面会有所启迪. import java.sql.Connection; im ...

随机推荐

  1. Windows2008R2 一键安全优化脚本

      ::author vim ::QQ 82996821 ::filename Windows2008R2_safe_auto_set.bat   :start @echo off color 0a ...

  2. centos7 字体库。vim乱码

    centos7 字体库.vim乱码 windows上传文件到centos,需要先使用dos2unix命令进行格式转换 先查看/usr/share下有没有这两个文件 没有的话yum -y install ...

  3. vue 父子通信

    节制地使用 $parent 和 $children - 它们的主要目的是作为访问组件的应急方法.更推荐用 props 和 events 实现父子组件通信

  4. cf959E

    题意简述:一个包含n个点的完全图,点的编号从0开始,两个点之间的权值等于两个点编号的异或值,求这个图的最小生成树 规律是 ∑ i from 0 to n-1 (i&-i) #include & ...

  5. 逻辑卷管理(LVM)-迁移

    逻辑卷管理(LVM)-迁移 更换卷组中逻辑卷中的一块硬盘流程:1确保卷组剩余空间大于需要更换的空间(缩减或添加添加新空间)-2迁移-3从卷组删除-4删除物理卷 #移除sdc1 1.查看卷组可用空间是否 ...

  6. Ubuntu P40显卡配置CUDA 10.1,CUDNN 7.6,Conda 5.2.0, Tensorflow-gpu 1.8

    1. 安装CUDA 禁用nouveau vim /etc/modprobe.d/blacklist.conf 最后两行加入 blacklist nouveau options nouveau mode ...

  7. EasyUI笔记(五)表单

    本系列只列出一些常用的属性.事件或方法,具体完整知识请查看API文档 Form(表单) 创建一个简单的HTML表单.构建一个包含id.action和method值的表单元素. <form id= ...

  8. P1478 陶陶摘苹果(升级版)(sort(),时间优化,priority_queue)

    题目描述 又是一年秋季时,陶陶家的苹果树结了 n 个果子.陶陶又跑去摘苹果,这次他有一个 a 公分的椅子.当他手够不着时,他会站到椅子上再试试. 这次与 NOIp2005 普及组第一题不同的是:陶陶之 ...

  9. 【Android】java中调用JS的方法

    最近因为学校换了新的教务系统,想做一个模拟登陆功能,发现登陆的账号和密码有一个js脚本来进行加密 整理了一下java中执行JS的方法 智强教务 账号 密码 加密方法 var keyStr = &quo ...

  10. 监控自己的电脑浏览器访问记录并生成csv格式

    #!usr/bin/env python #-*- coding:utf-8 _*- """ @author:lenovo @file: 获取浏览器历史记录.py @ti ...