每天早上七点三十,准时推送干货

看完本文,你一定会有所收获

一、摘要

在日常开发中,数据库中主键id的生成方案,主要有三种

  • 数据库自增ID

  • 采用随机数生成不重复的ID

  • 采用jdk提供的uuid

对于这三种方案,我发现在数据量少的情况下,没有特别的差异,但是当单表的数据量达到百万级以上时候,他们的性能有着显著的区别,光说理论不行,还得看实际程序测试,今天小编就带着大家一探究竟!

二、程序实例

首先,我们在本地数据库中创建三张单表tb_uuid_1tb_uuid_2tb_uuid_3,同时设置tb_uuid_1表的主键为自增长模式,脚本如下:

CREATE TABLE `tb_uuid_1` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,`name` varchar(20) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主键ID自增长';
CREATE TABLE `tb_uuid_2` (`id` bigint(20) unsigned NOT NULL,`name` varchar(20) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主键ID随机数生成';
CREATE TABLE `tb_uuid_3` (`id` varchar(50)  NOT NULL,`name` varchar(20) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主键采用uuid生成';

下面,我们采用Springboot + mybatis来实现插入测试。

2.1、数据库自增

以数据库自增为例,首先编写好各种实体、数据持久层操作,方便后续进行测试

/*** 表实体*/
public class UUID1 implements Serializable {private Long id;private String name;//省略set、get
}
/*** 数据持久层操作*/
public interface UUID1Mapper {/*** 自增长插入* @param uuid1*/@Insert("INSERT INTO tb_uuid_1(name) VALUES(#{name})")void insert(UUID1 uuid1);
}
/*** 自增ID,单元测试*/
@Test
public void testInsert1(){long start = System.currentTimeMillis();for (int i = 0; i < 1000000; i++) {uuid1Mapper.insert(new UUID1().setName("张三"));}long end = System.currentTimeMillis();System.out.println("花费时间:" +  (end - start));
}

2.2、采用随机数生成ID

这里,我们采用twitter的雪花算法来实现随机数ID的生成,工具类如下:

public class SnowflakeIdWorker {private static SnowflakeIdWorker instance = new SnowflakeIdWorker(0,0);/*** 开始时间截 (2015-01-01)*/private final long twepoch = 1420041600000L;/*** 机器id所占的位数*/private final long workerIdBits = 5L;/*** 数据标识id所占的位数*/private final long datacenterIdBits = 5L;/*** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)*/private final long maxWorkerId = -1L ^ (-1L << workerIdBits);/*** 支持的最大数据标识id,结果是31*/private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);/*** 序列在id中占的位数*/private final long sequenceBits = 12L;/*** 机器ID向左移12位*/private final long workerIdShift = sequenceBits;/*** 数据标识id向左移17位(12+5)*/private final long datacenterIdShift = sequenceBits + workerIdBits;/*** 时间截向左移22位(5+5+12)*/private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;/*** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)*/private final long sequenceMask = -1L ^ (-1L << sequenceBits);/*** 工作机器ID(0~31)*/private long workerId;/*** 数据中心ID(0~31)*/private long datacenterId;/*** 毫秒内序列(0~4095)*/private long sequence = 0L;/*** 上次生成ID的时间截*/private long lastTimestamp = -1L;/*** 构造函数* @param workerId     工作ID (0~31)* @param datacenterId 数据中心ID (0~31)*/public SnowflakeIdWorker(long workerId, long datacenterId) {if (workerId > maxWorkerId || workerId < 0) {throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));}if (datacenterId > maxDatacenterId || datacenterId < 0) {throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));}this.workerId = workerId;this.datacenterId = datacenterId;}/*** 获得下一个ID (该方法是线程安全的)* @return SnowflakeId*/public synchronized long nextId() {long timestamp = timeGen();// 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常if (timestamp < lastTimestamp) {throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));}// 如果是同一时间生成的,则进行毫秒内序列if (lastTimestamp == timestamp) {sequence = (sequence + 1) & sequenceMask;// 毫秒内序列溢出if (sequence == 0) {//阻塞到下一个毫秒,获得新的时间戳timestamp = tilNextMillis(lastTimestamp);}}// 时间戳改变,毫秒内序列重置else {sequence = 0L;}// 上次生成ID的时间截lastTimestamp = timestamp;// 移位并通过或运算拼到一起组成64位的IDreturn ((timestamp - twepoch) << timestampLeftShift) //| (datacenterId << datacenterIdShift) //| (workerId << workerIdShift) //| sequence;}/*** 阻塞到下一个毫秒,直到获得新的时间戳* @param lastTimestamp 上次生成ID的时间截* @return 当前时间戳*/protected long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}/*** 返回以毫秒为单位的当前时间* @return 当前时间(毫秒)*/protected long timeGen() {return System.currentTimeMillis();}public static SnowflakeIdWorker getInstance(){return instance;}public static void main(String[] args) throws InterruptedException {SnowflakeIdWorker idWorker = SnowflakeIdWorker.getInstance();for (int i = 0; i < 10; i++) {long id = idWorker.nextId();Thread.sleep(1);System.out.println(id);}}
}

其他的操作,与上面类似。

2.3、uuid

同样的,uuid的生成,我们事先也可以将工具类编写好:

public class UUIDGenerator {/*** 获取uuid* @return*/public static String getUUID(){return UUID.randomUUID().toString();}
}

最后的单元测试,代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest()
public class UUID1Test {private static final Integer MAX_COUNT = 1000000;@Autowiredprivate UUID1Mapper uuid1Mapper;@Autowiredprivate UUID2Mapper uuid2Mapper;@Autowiredprivate UUID3Mapper uuid3Mapper;/*** 测试自增ID耗时*/@Testpublic void testInsert1(){long start = System.currentTimeMillis();for (int i = 0; i < MAX_COUNT; i++) {uuid1Mapper.insert(new UUID1().setName("张三"));}long end = System.currentTimeMillis();System.out.println("自增ID,花费时间:" +  (end - start));}/*** 测试采用雪花算法生产的随机数ID耗时*/@Testpublic void testInsert2(){long start = System.currentTimeMillis();for (int i = 0; i < MAX_COUNT; i++) {long id = SnowflakeIdWorker.getInstance().nextId();uuid2Mapper.insert(new UUID2().setId(id).setName("张三"));}long end = System.currentTimeMillis();System.out.println("花费时间:" +  (end - start));}/*** 测试采用UUID生成的ID耗时*/@Testpublic void testInsert3(){long start = System.currentTimeMillis();for (int i = 0; i < MAX_COUNT; i++) {String id = UUIDGenerator.getUUID();uuid3Mapper.insert(new UUID3().setId(id).setName("张三"));}long end = System.currentTimeMillis();System.out.println("花费时间:" +  (end - start));}
}

三、性能测试

程序环境搭建完成之后,啥也不说了,直接撸起袖子,将单元测试跑起来!

首先测试一下,插入100万数据的情况下,三者直接的耗时结果如下:

在原有的数据量上,我们继续插入30万条数据,三者耗时结果如下:

可以看出在数据量 100W 左右的时候,uuid的插入效率垫底,随着插入的数据量增长,uuid 生成的ID插入呈直线下降!

时间占用量总体效率排名为:自增ID > 雪花算法生成的ID >> uuid生成的ID

在数据量较大的情况下,为什么uuid生成的ID远不如自增ID呢

关于这点,我们可以从 mysql 主键存储的内部结构来进行分析。

3.1、自增ID内部结构

自增的主键的值是顺序的,所以 Innodb 把每一条记录都存储在一条记录的后面。

当达到页面的最大填充因子时候(innodb默认的最大填充因子是页大小的15/16,会留出1/16的空间留作以后的修改),会进行如下操作:

  • 下一条记录就会写入新的页中,一旦数据按照这种顺序的方式加载,主键页就会近乎于顺序的记录填满,提升了页面的最大填充率,不会有页的浪费

  • 新插入的行一定会在原有的最大数据行下一行,mysql定位和寻址很快,不会为计算新行的位置而做出额外的消耗

3.2、使用uuid的索引内部结构

uuid相对顺序的自增id来说是毫无规律可言的,新行的值不一定要比之前的主键的值要大,所以innodb无法做到总是把新行插入到索引的最后,而是需要为新行寻找新的合适的位置从而来分配新的空间。

这个过程需要做很多额外的操作,数据的毫无顺序会导致数据分布散乱,将会导致以下的问题:

  • 写入的目标页很可能已经刷新到磁盘上并且从缓存上移除,或者还没有被加载到缓存中,innodb在插入之前不得不先找到并从磁盘读取目标页到内存中,这将导致大量的随机IO

  • 因为写入是乱序的,innodb不得不频繁的做页分裂操作,以便为新的行分配空间,页分裂导致移动大量的数据,一次插入最少需要修改三个页以上

  • 由于频繁的页分裂,页会变得稀疏并被不规则的填充,最终会导致数据会有碎片

在把值载入到聚簇索引(innodb默认的索引类型)以后,有时候会需要做一次OPTIMEIZE TABLE来重建表并优化页的填充,这将又需要一定的时间消耗。

因此,在选择主键ID生成方案的时候,尽可能别采用uuid的方式来生成主键ID,随着数据量越大,插入性能会越低!

四、总结

在实际使用过程中,推荐使用主键自增ID和雪花算法生成的随机ID。

但是使用自增ID也有缺点:

1、别人一旦爬取你的数据库,就可以根据数据库的自增id获取到你的业务增长信息,很容易进行数据窃取。2、其次,对于高并发的负载,innodb在按主键进行插入的时候会造成明显的锁争用,主键的上界会成为争抢的热点,因为所有的插入都发生在这里,并发插入会导致间隙锁竞争。

总结起来,如果业务量小,推荐采用自增ID,如果业务量大,推荐采用雪花算法生成的随机ID。

本篇文章主要从实际程序实例出发,讨论了三种主键ID生成方案的性能差异, 鉴于笔者才疏学浅,可能也有理解不到位的地方,欢迎网友们批评指出!

五、参考

1、方志明 - 使用雪花id或uuid作为Mysql主键,被老板怼了一顿!

最后说两句(求关注)

最近大家应该发现微信公众号信息流改版了吧,再也不是按照时间顺序展示了。这就对阿粉这样的坚持的原创小号主,可以说非常打击,阅读量直线下降,正反馈持续减弱。

所以看完文章,哥哥姐姐们给阿粉来个在看吧,让阿粉拥有更加大的动力,写出更好的文章,拒绝白嫖,来点正反馈呗~。

如果想在第一时间收到阿粉的文章,不被公号的信息流影响,那么可以给Java极客技术设为一个星标

最后感谢各位的阅读,才疏学浅,难免存在纰漏,如果你发现错误的地方,留言告诉阿粉,阿粉这么宠你们,肯定会改的~

最后谢谢大家支持~

最最后,重要的事再说一篇~

快来关注我呀~
快来关注我呀~
快来关注我呀~

【号外】Java 极客作者团队招新啦!!!扫码添加鸭血粉丝微信,加入我们,一起进步。

使用uuid作为数据库主键,被技术总监怼了一顿!相关推荐

  1. 使用uuid作为数据库主键,被技术总监怼了!

    一.前言 在日常开发中,数据库中主键id的生成方案,主要有三种 数据库自增ID 采用随机数生成不重复的ID 采用jdk提供的uuid 对于这三种方案,我发现在数据量少的情况下,没有特别的差异,但是当单 ...

  2. 使用UUID作为数据库主键产生的问题及解决方案

    序言 看了b站IT老齐的架构三百讲的其中一个短视频,有所体会并记录一下.视频中所讲的财经部门使用的UUID主键,在日终结算时出现磁盘的IO异常,导致应用出现高延迟.最后发现是UUID的问题,UUID作 ...

  3. 使用雪花id或uuid作为Mysql主键,被老板怼了一顿!

    点击关注公众号,Java干货及时送达一.mysql和程序实例1.1 要说明这个问题,我们首先来建立三张表 分别是user_auto_key,user_uuid,user_random_key,分别表示 ...

  4. 为什么不建议用字符串或者uuid做数据库主键

    UUID 好处就是本地生成,不要基于数据库来了:不好之处就是,UUID 太长了.占用空间大,作为主键性能太差了:更重要的是,UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作(连 ...

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

    数据库主键到底是用自增长(INT)好还是UUID好? 使用自增长做主键的优点: 1.很小的数据存储空间 2.性能最好 3.容易记忆 使用自增长做主键的缺点: 1.如果存在大量的数据,可能会超出自增长的 ...

  6. 再议《反驳 吕震宇的“小议数据库主键选取策略(原创)” 》

    前天发表了篇文章叫<小议数据库主键选取策略(原创)>,随即有网友提出了反驳意见<反驳 吕震宇的"小议数据库主键选取策略(原创)" >,看到后,我又做了做实验 ...

  7. 小议数据库主键选取策略(转自吕震宇老师博文)

    < DOCTYPE html PUBLIC -WCDTD XHTML TransitionalEN httpwwwworgTRxhtmlDTDxhtml-transitionaldtd> ...

  8. mysql数据库主键如何设计

    聊一个实际问题,淘宝的数据库的主键是如何设计的呢? 某些错误的离谱的答案还在网上流传着.其中一个明显的错误就是关于mysql的主键设计. 大部分人的回答如此自信就是:用8个自己的bigint做主键,而 ...

  9. 时间戳作为数据库主键的问题

    目录 数据库主键设计原则 常见的主键方案: 自增ID: UUID: 时间戳作为主键(主键冲突问题) 数据库主键设计原则 确保主键无意义 减少主键变动(应该与重构索引有关) 采用整型主键 常见的主键方案 ...

最新文章

  1. 【模型解读】历数GAN的5大基本结构
  2. linux环境安装Kafka最新版本 jdk1.8
  3. zypper 删除mysql_如何在 Linux 上安装/卸载一个文件中列出的软件包?
  4. C#解析JSON字符串总结(转载)
  5. Enterprise Library 4.1 Validation Block 快速使用图文笔记
  6. 5个值得安利的PC软件,建议收藏转发
  7. DH参数标定原理推导
  8. 省市县乡四级联动java代码,jQuery省市区街道四级联动代码
  9. 百趣生物受邀参加代谢组学及脂质组学质谱技术研讨会
  10. RFID射频识别的解读及应用
  11. java 定时发送心跳_RxJava2.0第五篇 interval 定时任务(心跳包)
  12. 各类编程语言教程合集
  13. OpenCV/C++:点线面相关计算
  14. 【JAVA】java解析HTML代码
  15. adb wifi 连接设备
  16. 【学习打卡04】可解释机器学习笔记之Grad-CAM
  17. java中 continue outer, break inner 简解
  18. 按例学construct2_滚动背景制作 (一)
  19. Python对千分位的处理
  20. 可见光通信 调制解调技术 家庭机器人 可见光通信应用 原理及硬件方案

热门文章

  1. java获取zset_Redis Set和ZSet常用命令
  2. 数字时代需要怎样的“安全基座”?首批零信任/SASE标准和评估成果即将揭晓
  3. vim删除多行注释与添加多行注释
  4. linux查看ip下mac地址命令,windows 命令方式查找指定IP的MAC地址
  5. suse linux12添加路由,suse 11 Linux 静态路由的添加方法
  6. 小米三停在android,据小米MIUI更新公告,多部新款小米手机停更
  7. 关于计算机网络协议 下面说法错误的是,关于计算机网络协议,下面说法错误的是____...
  8. 第四周作业-项目技术指标(招标文件)
  9. java automapper 使用_AutoMapper 使用总结
  10. 学习3D游戏建模需要英语很好吗?