数据库主键到底是用自增长(INT)好还是UUID好?

2022-07-06 13:37:19

数据库主键到底是用自增长(INT)好还是UUID好?

使用自增长做主键的优点
1、很小的数据存储空间
2、性能最好
3、容易记忆
使用自增长做主键的缺点
1、如果存在大量的数据,可能会超出自增长的取值范围
2、很难(并不是不能)处理分布式存储的数据表,尤其是需要合并表的情况下
3、安全性低,因为是有规律的,容易被非法获取数据
使用UUID做主键的优点
1、它是独一无二的,出现重复的机会少
2、适合大量数据中的插入和更新操作,尤其是在高并发和分布式环境下
3、跨服务器数据合并非常方便
4、安全性较高
使用UUID做主键的缺点
1、存储空间大(16 byte),因此它将会占用更多的磁盘空间
2、会降低性能
3、很难记忆

那么一般情况下是如何选择的呢?

1、项目是单机版的,并且数据量比较大(百万级)时,用自增长的,此时最好能考虑下安全性,做些安全措施。
2、项目是单机版的,并且数据量没那么大,对速度和存储要求不高时,用UUID。
3、项目是分布式的,那么首选UUID,分布式一般对速度和存储要求不高。
4、项目是分布式的,并且数据量达到千万级别可更高时,对速度和存储有要求时,可以用自增长。

总体对比来看自增长相对来说在较大的项目中是首选,那么有没有一更优于自增长的方法呢?答案在↓边!

SnowFlake是一种介于自增长和UUID之间的一种主键(存储空间小、速度快、分布式、时间序列)它有如下优点

1.所有生成的id按时间趋势递增
2.整个分布式系统内不会产生ID碰撞(重复id,因为有datacenterId和workerId来做区分)
3.id生成的效率高

SnowFlake算法生成id的结果是一个64bit大小的整数,一般用户分布式环境的id生成下面我们来看一下SnowFlake生成的id的结构

第一段占1位

固定值0。二进制中最高位为1的都是负数,但是我们生成的id一般都使用整数,所以这个最高位固定是0

第二段占41位

用来记录时间戳(毫秒)。41位可以表示$2^{41}-1$个数字,
如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 $2^{41}-1$,减1是因为可表示的数值范围是从0开始算的,而不是1。
也就是说41位可以表示$2^{41}-1$个毫秒的值,转化成单位年则是$(2^{41}-1) / (1000 * 60 * 60 * 24 * 365) = 69$年

第三段占10位

用来记录工作机器id。

可以部署在$2^{10} = 1024$个节点,包括5位datacenterId和5位workerId
5位(bit)可以表示的最大正整数是$2^{5}-1 = 31$,即可以用0、1、2、3、....31这32个数字,来表示不同的datecenterId或workerId

第四段占12位

序列号,用来记录同毫秒内产生的不同id。

12位(bit)可以表示的最大正整数是$2^{12}-1 = 4095$,即可以用0、1、2、3、....4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。

下图为SnowFlake算法生成解析

由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id就是long来存储的,下面附上java版源码。

  1. /**

  2. * SnowFlake

  3. */

  4. public class SnowflakeIdWorker {

  5. /**

  6. * 开始时间截 (1971-01-01 08:00:00)

  7. * 时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数

  8. * */

  9. private final long twepoch = 31536000L;

  10. /** 机器id所占的位数 */

  11. private final long workerIdBits = 5L;

  12. /** 数据标识id所占的位数 */

  13. private final long datacenterIdBits = 5L;

  14. /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */

  15. private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

  16. /** 支持的最大数据标识id,结果是31 */

  17. private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

  18. /** 序列在id中占的位数 */

  19. private final long sequenceBits = 12L;

  20. /** 机器ID向左移12位 */

  21. private final long workerIdShift = sequenceBits;

  22. /** 数据标识id向左移17位(12+5) */

  23. private final long datacenterIdShift = sequenceBits + workerIdBits;

  24. /** 时间截向左移22位(5+5+12) */

  25. private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

  26. /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */

  27. private final long sequenceMask = -1L ^ (-1L << sequenceBits);

  28. /** 工作机器ID(0~31) */

  29. private long workerId;

  30. /** 数据中心ID(0~31) */

  31. private long datacenterId;

  32. /** 毫秒内序列(0~4095) */

  33. private long sequence = 0L;

  34. /** 上次生成ID的时间截 */

  35. private long lastTimestamp = -1L;

  36. /**

  37. * 构造函数

  38. * @param workerId 工作ID (0~31)

  39. * @param datacenterId 数据中心ID (0~31)

  40. */

  41. public SnowflakeIdWorker(long workerId, long datacenterId) {

  42. if (workerId > maxWorkerId || workerId < 0) {

  43. throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));

  44. }

  45. if (datacenterId > maxDatacenterId || datacenterId < 0) {

  46. throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));

  47. }

  48. this.workerId = workerId;

  49. this.datacenterId = datacenterId;

  50. }

  51. /**

  52. * 获得下一个ID (该方法是线程安全的)

  53. * @return SnowflakeId

  54. */

  55. public synchronized long nextId() {

  56. long timestamp = timeGen();

  57. //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常

  58. if (timestamp < lastTimestamp) {

  59. throw new RuntimeException(

  60. String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));

  61. }

  62. //如果是同一时间生成的,则进行毫秒内序列

  63. if (lastTimestamp == timestamp) {

  64. sequence = (sequence + 1) & sequenceMask;

  65. //毫秒内序列溢出

  66. if (sequence == 0) {

  67. //阻塞到下一个毫秒,获得新的时间戳

  68. timestamp = tilNextMillis(lastTimestamp);

  69. }

  70. }

  71. //时间戳改变,毫秒内序列重置

  72. else {

  73. sequence = 0L;

  74. }

  75. //上次生成ID的时间截

  76. lastTimestamp = timestamp;

  77. //移位并通过或运算拼到一起组成64位的ID

  78. return ((timestamp - twepoch) << timestampLeftShift) //

  79. | (datacenterId << datacenterIdShift) //

  80. | (workerId << workerIdShift) //

  81. | sequence;

  82. }

  83. /**

  84. * 阻塞到下一个毫秒,直到获得新的时间戳

  85. * @param lastTimestamp 上次生成ID的时间截

  86. * @return 当前时间戳

  87. */

  88. protected long tilNextMillis(long lastTimestamp) {

  89. long timestamp = timeGen();

  90. while (timestamp <= lastTimestamp) {

  91. timestamp = timeGen();

  92. }

  93. return timestamp;

  94. }

  95. /**

  96. * 返回以毫秒为单位的当前时间

  97. * @return 当前时间(毫秒)

  98. */

  99. protected long timeGen() {

  100. return System.currentTimeMillis();

  101. }

  102. /** 测试 */

  103. public static void main(String[] args) {

  104. SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0 , 0);

  105. long start = System.currentTimeMillis();

  106. for (int i = 0; i < 1000000; i++) {

  107. long id = idWorker.nextId();

  108. System.out.println(id);

  109. }

  110. long end = System.currentTimeMillis();

  111. System.out.println("生成1000000id用时:" + (end-start) + "ms" );

  112. }

  113. }

小落i5低压U生成1000000个id用时8122ms 可见其速度之快。

  • 作者:liuxing201122013
  • 原文链接:https://blog.csdn.net/liuxing201122013/article/details/109224252
    更新时间:2022-07-06 13:37:19