MySQL原理之UUID主键分析,插入或更新语法分析

1 MySQL不能用UUID做主键

1.1 前言

mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复雪花id(long形且唯一,单机递增),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究竟有什么坏处?

1.2 mysql和程序实例

1.2.1 准备工作

要说明这个问题,我们首先来建立三张表
分别是user_auto_key,user_uuid,user_random_key分别表示自动增长的主键,uuid作为主键,随机key作为主键,其它我们完全保持不变.
根据控制变量法,我们只把每个表的主键使用不同的策略生成,而其他的字段完全一样,然后测试一下表的插入速度和查询速度:

注:这里的随机key其实是指用雪花算法算出来的前后不连续不重复无规律的id,一串18位长度的long值
id自动生成表:

create table user_key_auto(
	id int UNSIGNED not null auto_increment,
	user_id BIGINT(64) not NULL DEFAULT 0,
	user_name VARCHAR(64) not NULL DEFAULT '',
	sex int(2) not NULL,
	address VARCHAR(255) not null DEFAULT '',
	city VARCHAR(64) not NULL DEFAULT '',
	email VARCHAR(64) not nulT DEFAULT '',
	state int(6) not NULL DEFAULT 0,
	PRIMARY KEY(id),
	key user_name_key(user_name)
)ENGINE=INNODB

用户uuid表

create table user_uuid(
	id VARCHAR(36) not null,
	user_id BIGINT(64) not NULL DEFAULT 0,
	user_name VARCHAR(64) not NULL DEFAULT '',
	sex int(2) not NULL,
	address VARCHAR(255) not null DEFAULT '',
	city VARCHAR(64) not NULL DEFAULT '',
	email VARCHAR(64) not null DEFAULT '',
	state int(6) not NULL DEFAULT 0,
	PRIMARY KEY(id),
	key user_name_key(user_name)
)ENGINE=INNODB

随机主键表:

create table user_random_key(
	id BIGINT(64) not null DEFAULT 0,
	user_id BIGINT(64) not NULL DEFAULT 0,
	user_name VARCHAR(64) not NULL DEFAULT '',
	sex int(2) not NULL,
	address VARCHAR(255) not null DEFAULT '',
	city VARCHAR(64) not NULL DEFAULT '',
	email VARCHAR(64) not null DEFAULT '',
	state int(6) not NULL DEFAULT 0,
	PRIMARY KEY(id),
	key user_name_key(user_name)
)ENGINE=INNODB

1.2.2 开始测试

光有理论不行,直接上程序,使用spring的jdbcTemplate来实现增查测试:
技术框架:springboot+jdbcTemplate+junit+hutool,程序的原理就是连接自己的测试数据库,然后在相同的环境下写入同等数量的数据,来分析一下insert插入的时间来进行综合其效率,为了做到最真实的效果,所有的数据采用随机生成,比如名字、邮箱、地址都是随机生成。

import cn.hutool.core.collection.CollectionUtil;
import com.wyq.mysqldemo.databaseobject.UserKeyAuto;
import com.wyq.mysqldemo.databaseobject.UserKeyRandom;
import com.wyq.mysqldemo.databaseobject.UserKeyUUID;
import com.wyq.mysqldemo.diffkeytest.AutoKeyTableService;
import com.wyq.mysqldemo.diffkeytest.RandomKeyTableService;
import com.wyq.mysqldemo.diffkeytest.UUIDKeyTableService;
import com.wyq.mysqldemo.util.JdbcTemplateService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.StopWatch;
import java.util.List;
@SpringBootTest
class MysqlDemoApplicationTests {

    @Autowired
    private JdbcTemplateService jdbcTemplateService;

    @Autowired
    private AutoKeyTableService autoKeyTableService;

    @Autowired
    private UUIDKeyTableService uuidKeyTableService;

    @Autowired
    private RandomKeyTableService randomKeyTableService;


    @Test
    void testDBTime() {

        StopWatch stopwatch = new StopWatch("执行sql时间消耗");


        /**
         * auto_increment key任务
         */
        final String insertSql = "INSERT INTO user_key_auto(user_id,user_name,sex,address,city,email,state) VALUES(?,?,?,?,?,?,?)";

        List<UserKeyAuto> insertData = autoKeyTableService.getInsertData();
        stopwatch.start("自动生成key表任务开始");
        long start1 = System.currentTimeMillis();
        if (CollectionUtil.isNotEmpty(insertData)) {
            boolean insertResult = jdbcTemplateService.insert(insertSql, insertData, false);
            System.out.println(insertResult);
        }
        long end1 = System.currentTimeMillis();
        System.out.println("auto key消耗的时间:" + (end1 - start1));

        stopwatch.stop();


        /**
         * uudID的key
         */
        final String insertSql2 = "INSERT INTO user_uuid(id,user_id,user_name,sex,address,city,email,state) VALUES(?,?,?,?,?,?,?,?)";

        List<UserKeyUUID> insertData2 = uuidKeyTableService.getInsertData();
        stopwatch.start("UUID的key表任务开始");
        long begin = System.currentTimeMillis();
        if (CollectionUtil.isNotEmpty(insertData)) {
            boolean insertResult = jdbcTemplateService.insert(insertSql2, insertData2, true);
            System.out.println(insertResult);
        }
        long over = System.currentTimeMillis();
        System.out.println("UUID key消耗的时间:" + (over - begin));

        stopwatch.stop();


        /**
         * 随机的long值key
         */
        final String insertSql3 = "INSERT INTO user_random_key(id,user_id,user_name,sex,address,city,email,state) VALUES(?,?,?,?,?,?,?,?)";
        List<UserKeyRandom> insertData3 = randomKeyTableService.getInsertData();
        stopwatch.start("随机的long值key表任务开始");
        Long start = System.currentTimeMillis();
        if (CollectionUtil.isNotEmpty(insertData)) {
            boolean insertResult = jdbcTemplateService.insert(insertSql3, insertData3, true);
            System.out.println(insertResult);
        }
        Long end = System.currentTimeMillis();
        System.out.println("随机key任务消耗时间:" + (end - start));
        stopwatch.stop();


        String result = stopwatch.prettyPrint();
        System.out.println(result);
    }

1.2.3 程序写入结果

user_key_auto写入结果:
在这里插入图片描述

user_random_key写入结果:
在这里插入图片描述

user_uuid表写入结果:
在这里插入图片描述

1.2.4 效率测试结果

在这里插入图片描述

在已有数据量为130W的时候:我们再来测试一下插入10w数据,看看会有什么结果:
在这里插入图片描述

可以看出在数据量100W左右的时候,uuid的插入效率垫底,并且在后序增加了130W的数据,uuid的时间又直线下降

时间占用量总体可以打出的效率排名为:auto_key>random_key>uuid,uuid的效率最低,在数据量较大的情况下,效率直线下滑。那么为什么会出现这样的现象呢?带着疑问,我们来探讨一下这个问题:

1.3 使用uuid和自增id的索引结构对比

1.3.1 自增id

使用自增id的内部结构
在这里插入图片描述
自增的主键的值是顺序的,所以Innodb把每一条记录都存储在一条记录的后面。当达到页面的最大填充因子时候(innodb默认的最大填充因子是页大小的15/16,会留出1/16的空间留作以后的修改):

  • 下一条记录就会写入新的页中,一旦数据按照这种顺序的方式加载,主键页就会近乎于顺序的记录填满,提升了页面的最大填充率,不会有页的浪费
  • 新插入的行一定会在原有的最大数据行下一行,mysql定位和寻址很快,不会为计算新行的位置而做出额外的消耗
  • 减少了页分裂和碎片的产生

1.3.2 uuid

使用uuid的索引内部结构
在这里插入图片描述

因为uuid相对顺序的自增id来说是毫无规律可言的,新行的值不一定要比之前的主键的值要大,所以innodb无法做到总是把新行插入到索引的最后,而是需要为新行寻找新的合适的位置从而来分配新的空间。
这个过程需要做很多额外的操作,数据的毫无顺序会导致数据分布散乱,将会导致以下的问题:

  • 写入的目标页很可能已经刷新到磁盘上并且从缓存上移除,或者还没有被加载到缓存中,innodb在插入之前不得不先找到并从磁盘读取目标页到内存中,这将导致大量的随机IO
  • 因为写入是乱序的,innodb不得不频繁的做页分裂操作,以便为新的行分配空间,页分裂导致移动大量的数据,一次插入最少需要修改三个页以上
  • 由于频繁的页分裂,页会变得稀疏并被不规则的填充,最终会导致数据会有碎片
  • 在把随机值(uuid和雪花id)载入到聚簇索引(innodb默认的索引类型)以后,有时候会需要做一次OPTIMEIZE TABLE来重建表并优化页的填充,这将又需要一定的时间消耗。

结论:使用innodb应该尽可能的按主键的自增顺序插入,并且尽可能使用单调的增加的聚簇键的值来插入新行

1.4 自增id缺点

那么使用自增id就完全没有坏处了吗?并不是,自增id也会存在以下几点问题:

  • 别人一旦爬取你的数据库,就可以根据数据库的自增id获取到你的业务增长信息,很容易分析出你的经营情况
  • 对于高并发的负载,innodb在按主键进行插入的时候会造成明显的锁争用,主键的上界会成为争抢的热点,因为所有的插入都发生在这里,并发插入会导致间隙锁竞争
  • Auto_Increment锁机制会造成自增锁的抢夺,有一定的性能损失
    Auto_increment的锁争抢问题,如果要改善,需要调优innodb_autoinc_lock_mode的配置

点击了解MySQL自增锁机制

参考连接:https://mp.weixin.qq.com/s/px-vRWikt8xp7b3ZCr27uw

1.5 雪花算法

点击了解MySQL分布式环境下生成全局自增有序ID(雪花算法Snowflake)

2 插入或更新

MySQL中假如想要在插入时没有就插入有则更新,可以用MyISAM引擎的merge或者InnoDB引擎的on duplicate key,现在主要是用InnoDB,下面主要介绍on duplicate key

2.1 on duplicate key

2.1.1 定义

如果在INSERT语句末尾指定了 on duplicate key update,并且插入行后会导致在一个UNIQUE索引或PRIMARY KEY中出现重复值,则在出现重复值的行执行UPDATE;如果不会导致唯一值列重复的问题,则插入新行。

2.1.2 values函数

values函数使用说明:在一个 INSERT … ON DUPLICATE KEY UPDATE … 语句中,可以在 UPDATE 子句中使用 VALUES(col_name ) 函数,用来访问来自该语句的 INSERT 部分的列值。换言之,UPDATE 子句中的 VALUES(col_name ) 访问需要被插入的 col_name 的值 , 并不会发生重复键冲突。这个函数在多行插入中特别有用。

values(col_name)函数只是取当前插入语句中的插入值,并没有累加功能。 如:count = values(count) 取前面 insert into 中的 count 值,并更新。
当有多条记录冲突,需要插入时,前面的更新值都被最后一条记录覆盖,所以呈现出取最后一条更新的现象。如:count = count + values(count) 依然取前面 insert into 中的 count 值,并与原记录值相加后更新回数据库,这样,当多条记录冲突需要插入时,就实现了不断累加更新的现象
注意VALUES() 函数只在 INSERT ... UPDATE 语句中有意义,而在其它情况下只会返回 NULL

2.1.3 注意事项

注意事项:

  • insert into ... on duplicate key update ... values() 这个语句,尽管在冲突时执行了更新,并没有插入,但是发现依然会占用 id 序号(自增),出现很多丢失的 id
  • 因为这是个插入语句,所以不能加where条件
  • 如果是插入操作,受到影响行的值为1;如果更新操作,受到影响的行的值为2;如果更新的数据和已有的数据比对一样(就相当于没变,所有值保持不变),受到影响的行的值为0
  • 更新的内容中unique key或者primary key最好按照一个来判断是否重复,不然不能保证语句执行正确(有任意一个unique key重复就会走更新);尽量不对存在多个唯一键的talbe使用该语句,避免可能导致数据错乱
  • 在有可能有并发事物执行的insert语句下不要使用该语句,可能导致产生dead lock
  • 如果数据表id是自动递增的不建议使用该语句;id不连续,如果前面更新的比较多,新增的下一条会相应跳跃的更大
  • 该语句是mysql独有的语法,如果可能涉及到其他数据库语言要慎重使用

2.1.4 操作分析

创建案例表 word_count(单词计数表)
  use test;
  CREATE TABLE IF NOT EXISTS word_count (
  	id int(11) NOT NULL AUTO_INCREMENT,
  	word varchar(64) NOT NULL,
  	count int(11) DEFAULT 0,
  	date date NOT NULL,
  	PRIMARY KEY (id),
  	UNIQUE KEY word (word, date)  // (word,date) 两字段组合唯一
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
  

执行第一次:(首次数据库表中没有数据,正常插入)
insert into word_count (word, count, date) values 
('a',5,curdate()) 
on duplicate key update count=values(count);
  # 结果显示:
  id   word    count    date 
  1    a       5        2023-03-11
  
执行第二次:(与第一次的唯一(word,date)冲突,执行更新)
insert into word_count (word, count, date) values 
('a',6,curdate()) 
on duplicate key update count=values(count);
  # 结果显示:
  id   word    count    date 
  1    a       6        2023-03-11  (更新)
  
执行第三次:
insert into word_count (word, count, date) values 
('a',6,curdate()-1),    // 取前一天,不会冲突
('a',7,curdate()) // 冲突
on duplicate key update count=values(count);
  # 结果显示:
  id   word    count    date 
  1    a       7        2023-03-11  (更新)
  3    a       6        2023-03-10  (新插入)
  
执行第四次:(更新冲突的最后一条插入值)
insert into word_count (word, count, date) values 
('a',2,curdate()),  // 冲突
('a',1,curdate())  // 冲突
on duplicate key update count=values(count);
  # 结果显示:
  id   word    count    date 
  1    a       1        2023-03-11  (更新最后一条插入值)
  3    a       6        2023-03-10  (不变)
  
执行第五次:(更新冲突的累加插入值)
insert into word_count (word, count, date) values 
('a',2,curdate()),
('a',1,curdate()) 
on duplicate key update count=count+values(count); // 实现每行累加
  # 结果显示:
  id   word    count    date 
  1    a       4        2023-03-11
  3    a       6        2023-03-10
 
执行第六次:(无冲突插入,观察 id 键值,出现了很多丢失,id 直接跳到了 9)
insert into word_count (word, count, date) values 
('b',2,curdate())
on duplicate key update count=count+values(count);
  # 结果显示:
  id   word    count    date 
  1    a       4        2019-06-26
  3    a       6        2019-06-25
  9    b       2        2019-06-26

2.2 replace info

2.2.1 定义

咱们在运用数据库时能够会常常遇到这种状况。若是一个表在一个字段上建立了索引,当咱们再向这个表中运用现已存在的键值插进一条记载,那将会抛出一个主键抵触的过错。当然,咱们能够想用新记载的值来掩盖本来的记载值。若是运用传统的做法,有必要先运用 DELETE 句子删去原先的记载,然后再运用 INSERT 插进新的记载。而在MySQL中为咱们供给了一种新的解决方案,这即是 REPLACE 句子。
运用REPLACE插进一条记载时,若是不重复,REPLACE就和INSERT的功用相同,若是有重复记载,REPLACE就运用新记载的值来更换本来的记载值。

运用REPLACE的最大优点即是能够将DELETEINSERT合二为一,构成一个原子操作。这样就能够不用思考在一起运用DELETEINSERT时增加业务等杂乱操作了。
在运用REPLACE时,表中有必要有索引,并且这个索引地点的字段不能答应空值,不然REPLACE就和INSERT彻底相同的

在履行REPLACE后,体系回来了所影响的行数,若是回来1,阐明在表中并没有重复的记载,若是回来2,阐明有一条重复记载,体系主动先调用了DELETE删去这条记载,然后再记载用INSERT来刺进这条记载。若是回来的值大于2,那阐明有多个仅有索引,有多条记载被删去和刺进。

replace具备替换拥有唯一索引或者主键索引重复数据的能力,也就是如果使用replace into插入的数据的唯一索引或者主键索引与之前的数据有重复的情况,将会删除原先的数据,然后再进行添加。
语法:replace into table( col1, col2, col3 ) values ( val1, val2, val3 )
语义:向table表中col1, col2, col3列replace数据val1,val2,val3

2.2.2 操作

先查询:select * from word_count
# 结果显示:
1	a	9	2023-03-11
3	a	7	2023-03-10
12	c	4	2023-03-11
13	b	1	2023-03-11

执行replace into 语句:
replace into word_count (word, count, date) values ('b',1,curdate());

再次查询:select * from word_count
# 结果显示:
1	a	9	2023-03-11
3	a	7	2023-03-10
12	c	4	2023-03-11
14	b	1	2023-03-11

2.2.3 MySQL中特殊插入语法

MySQL中的INSERT句子和规范的INSERT不太相同,在规范的SQL句子中,一次刺进一条记载的INSERT句子只要一种办法。

INSERT INTO tablename(列名…) VALUES(列值);

而在MySQL中还有别的一种办法,如下的方法把列名和列值成对呈现和运用,如下面的句子将产生中样的作用

INSERT INTO tablename SET column_name1 = value1, column_name2 =
value2,…;

2.3 INSERT IGNORE

MySQL中,可以使用INSERT IGNORE语句来忽略重复的记录。如果尝试插入的数据会导致重复键错误,那么INSERT IGNORE会跳过这条记录,而不是报错。

例如:

INSERT IGNORE INTO table (column1, column2) VALUES ('value1', 'value2');

在这个例子中,如果('value1', 'value2')会导致重复键错误,那么这条插入语句就会被忽略,而不会插入数据。同时,这条SQL语句也不会报错,而是会继续执行下一条SQL语句(如果有的话)。

需要注意的是,INSERT IGNORE不仅会忽略重复键错误,还会忽略其他一些错误(例如数据溢出)。因此,应当确保要插入的数据确实是想要插入的,并且确实希望在出错时跳过插入,而不是报错。

posted @ 2024-09-11 21:17  上善若泪  阅读(52)  评论(0编辑  收藏  举报