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

一、前言

在日常开发中,数据库中主键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主键,被老板怼了一顿!


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544812.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android设置透明、半透明等效果

首先说明一点&#xff0c;关于透明的 Android 控件 background 问题&#xff0c;从转载来的文章看到最主要的一句有用的代码是&#xff1a; v.getBackground().setAlpha(100);//0~255透明度值 这里的 Alpha 值&#xff0c;实际上是 0-1 的取值范围。 以下内容转自&#xff…

Java Long类shortValue()方法与示例

长类shortValue()方法 (Long class shortValue() method) shortValue() method is available in java.lang package. shortValue()方法在java.lang包中可用。 shortValue() method is used to return the value denoted by this Long object converted to type short (by casti…

ThreadLocal不好用?那是你没用对!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在 Java 中&#xff0c;如果要问哪个类使用简单&#xff0c;但用好最不简单&#xff1f;我想你的脑海中一定会浮现出一次词—…

记一则js替换字符串的问题

2019独角兽企业重金招聘Python工程师标准>>> 软件的一处功能用到EasyUI的表单提交&#xff0c;返回一串字符串&#xff0c;这串字符串里有一段HTML代码&#xff0c;正常的情况下这段HTML代码里的双引号“ 是用 \ 转义过的。在IE中没问题&#xff0c;但是在Firefox和…

『图解Java并发』面试必问的CAS原理你会了吗?

在并发编程中我们都知道i操作是非线程安全的&#xff0c;这是因为 i操作不是原子操作。如何保证原子性呢&#xff1f;常用的方法就是加锁。在Java语言中可以使用 Synchronized和CAS实现加锁效果。Synchronized是悲观锁&#xff0c;线程开始执行第一步就是获取锁&#xff0c;一旦…

美易官方:“圣诞老人行情”美股能否延续近期涨势?

圣诞老人行情”有望上演&#xff0c;美股能否延续近期涨势&#xff1f; 随着圣诞节的临近&#xff0c;市场开始期待所谓的“圣诞老人行情”能够上演。在过去的几年里&#xff0c;这个时期往往会出现一波上涨行情&#xff0c;给投资者带来一些安慰和喜悦。然而&#xff0c;今年的…

Linux高并发应用类型对系统内核的优化

Linux操作系统内核参数优化net.ipv4.tcp_max_tw_buckets 6000 net.ipv4.ip_local_port_range 1024 65000 net.ipv4.tcp_tw_recycle 1 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_syncookies 1 net.core.somaxconn 262144 net.core.netdev_max_backlog 262144 net.ipv4.tcp_m…

Java LinkedHashMap forEach()方法与示例

LinkedHashMap类的forEach()方法 (LinkedHashMap Class forEach() method) forEach() method is available in java.util package. java.util包中提供了forEach()方法 。 forEach() method is used to perform the specified action for every entry (key-value) pairs in the …

SimpleDateFormat线程不安全的5种解决方案!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;1.什么是线程不安全&#xff1f;线程不安全也叫非线程安全&#xff0c;是指多线程执行中&#xff0c;程序的执行结果和预期的…

Java LineNumberReader mark()方法与示例

LineNumberReader类mark()方法 (LineNumberReader Class mark() method) mark() method is available in java.io package. mark()方法在java.io包中可用。 mark() method is used to set the current position in this LineNumberReader stream and whenever we call to reset…

mac地址漂移flapping的前因后果

一、什么是mac地址flapping?mac地址漂移是指&#xff1a;在同一个vlan内&#xff0c;mac地址表项的出接口出现变更。如图&#xff1a;二、产生的原因1、因为环路或VRRP切换&#xff0c;导致的MAC地址漂移告警。&#xff08;不予关注&#xff09;2、因为无线用户漫游&#xff0…

时间转换竟多出1年!Java开发中的20个坑你遇到过几个?

前言最近看了极客时间的《Java业务开发常见错误100例》&#xff0c;再结合平时踩的一些代码坑&#xff0c;写写总结&#xff0c;希望对大家有帮助&#xff0c;感谢阅读~1. 六类典型空指针问题包装类型的空指针问题级联调用的空指针问题Equals方法左边的空指针问题ConcurrentHas…

android去掉顶部标题栏

在AndroidManifest.xml中实现&#xff1a; 注册Activity时加上如下的一句配置就可以实现。 <activity android:name".Activity"android:theme"android:style/Theme.NoTitleBar"></activity>

Oracle RAC Failover 详解

2019独角兽企业重金招聘Python工程师标准>>> Oracle RAC 同时具备HA(High Availiablity) 和LB(LoadBalance). 而其高可用性的基础就是Failover(故障转移). 它指集群中任何一个节点的故障都不会影响用户的使用&#xff0c;连接到故障节点的用户会被自动转移到健康节…

超级详细的Spring Boot 注解总结

日常编程中我相信大家肯定都用过spring&#xff0c;也用过spring的注解&#xff0c;哪怕面试的时候也经常会被问到一些spring和spring boot注解的作用和含义等&#xff0c;那么这篇就带大家来看看超级详细的Spring Boot 注解总结&#xff01;搞起!我们先来看看本篇会讲到的注解…

Android下 布局加边框 指定背景色 半透明

文章转自&#xff1a;http://www.cnblogs.com/bavariama/archive/2013/09/25/3338375.html 背景设置为自定义的shape文件&#xff1a; <!-- <?xml version"1.0" encoding"utf-8"?> <shape xmlns:android"http://schemas.android.com/a…

inputstream示例_Java InputStream available()方法与示例

inputstream示例InputStream类的available()方法 (InputStream Class available() method) available() method is available in java.io package. available()方法在java.io包中可用。 available() method is used to return the number of available bytes left for reading …

json前后台传值

谈到JSON,简单的说就是一种数据交换格式。近年来&#xff0c;其在服务器之间交换数据的应用越来越广&#xff0c;相比XML其格式更简单、编解码更容易、扩展性更好&#xff0c;所以深受开发人员的喜爱。 下面简单的写一下在项目中前后台json传值的一个小例子&#xff0c;供大家参…

一个ThreadLocal和面试官大战30个回合

开场杭州某商务楼里&#xff0c;正发生着一起求职者和面试官的battle。面试官&#xff1a;你先自我介绍一下。安琪拉&#xff1a;面试官你好&#xff0c;我是草丛三婊&#xff0c;最强中单&#xff08;妲己不服&#xff09;&#xff0c;草地摩托车车手&#xff0c;第21套广播体…

ASP.NET 网站项目 EF 的简单操作例子

ASP.NET 网站项目 EF 的简单操作例子&#xff1a;操作代码&#xff1a;using EFTest.Models; using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Web; using System.Web.Mvc;namespace EFTest.Controllers {public class D…