赞
踩
1、页和扇区大小如图2.20
2、磁盘内部结构如图2.53
磁头、扇区、磁道
硬盘的容量:存储容量=磁头数×磁道(柱面)数×每道扇区数×每扇区字节数
3、影响硬盘性能的因素
磁盘完成一个I/O请求所花费的时间,它由寻道时间、旋转延迟和数据传输时间三部分构成。
Tseek 即将读写磁头移动至正确的磁道上所需要的时间。avg在3-15ms
Trotation 即盘片旋转将请求数据所在的扇区移动到读写磁盘下方所需要的时间。
旋转延迟取决于磁盘转速,通常用磁盘旋转一周所需时间的1/2表示。比如:7200rpm的磁盘平均旋转延迟大约为60*1000/7200/2 = 4.17ms
Transfer即完成传输所请求的数据所需要的时间,它取决于数据传输率,其值等于数据大小除以数据传输率。
SATA II可达到300MB/s的接口数据传输率,数据传输时间通常远小于前两部分消耗时间,可忽略。
4、衡量性能的指标
这主要是因为磁头移动到正确的磁道上需要时间,随机读写时,磁头需要不停的移动,时间都浪费在了磁头寻址上
1)IOPS
IOPS(Input/Output Per Second)即每秒的输入输出量(或读写次数),即指每秒内系统能处理的I/O请求数量。
随机读写频繁的应用,如小文件存储等,关注随机读写性能,IOPS是关键衡量指标。可以推算出磁盘的IOPS = 1000ms / (Tseek + Trotation + Transfer),如果忽略数据传输时间,理论上可以计算出随机读写最大的IOPS。常见磁盘的随机读写最大IOPS为
7200rpm的磁盘 IOPS = 76 IOPS
10000rpm的磁盘IOPS = 111 IOPS
15000rpm的磁盘IOPS = 166 IOPS
2)吞吐量
吞吐量(Throughput),指单位时间内可以成功传输的数据数量。
顺序读写频繁的应用,如视频点播,关注连续读写性能、数据吞吐量是关键衡量指标。
它主要取决于磁盘阵列的架构、通道的大小、磁盘的个数。
Compact是一种基于固定长度存储的方式,存储的额外信息较少,具有较高的存储效率
行结构整体如图2.34
字段为name varchar(32)时,实际存储数据’mjp’,没用到32字符。这时候就需要记录下变长字段的真实长度
记录头整体结构如图2.35所示
1)delete_mark
2)record_type
3)heap_no
作用:表示当前记录在本页中的位置
内容:插入数据时,图2.35中四条数据,heap_no从2开始算起,依次为2、3、4、5
0:最小记录的位置,对应record_type = 2
1:最大记录的位置,对应record_type = 3,
它俩的位置最靠前
4)next_record
作用:从当前记录的数据到下一条记录的数据的地址偏移量,即单链表
删除操作
如图2.26所示
正常列的数据
事务id,6字节
回滚指针,7字节
行id,6字节非空且唯一:如果用户未声明主键id,InnoDB会使用row_id作为primary key。
如果表中有主键id了,则不会存在row_id
mysql会根据行的大小自动选择不同的存储方式进行存储,以实现更好的查询性能和空间利用率
1、和Compact的区别
发生行溢出时,dynamic是将该字段的全不值都存到其它页中
行溢出时,处理方式和dynamic相同,在此基础上使用zlib算法进行压缩处理
16kb
数据库I/O操作的最小单位是页,即使修改了一条数据,从内存回刷磁盘,IO也是页维度
mysql中的存储空间如图2.32
最大的就是表空间,即对立表空间.ibd文件,存储了索引和数据
如图2.33
描述各种页的通用信息(如页的编号、其上一页、下一页是谁等)
1、offset
每一个页都有一个单独的页号,就跟你的身份证号码一样,InnoDB通过页号可以唯一定位一个页。
2、type
代表当前页的类型,页类型常用如下
3、prev和next
4、checksum校验和
1)代表当前页面的校验和
2)校验和定义
3)作用
5、lsn
Log Sequence Number:页面被最后修改时对应的日志序列位置
作用结合校验和一起校验页的完整性的
检验和 + lsn,同File Header一起检验页的完整性
按照比如Compact行格式一条一条,相互之间形成单链表,参考行结构
Compact行格式中的记录头信息中的heap_no和record_type
在页中,记录是以单向链表的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高
页目录通过二分查找法的方式快速定位到具体的某行数据,提升效率。
页目录结构如图2.36和2.37
1、定义
2、作用
当确认数据在某一页中,则从目录页中按照二分法,查找所有槽位slot的值,快速定位到具体在哪个组中
3、查找实操
1、slots:页槽的个数,方便二分查找
2、还未使用空间的最小地址:当插入新数据时,可以快速获取要插入此页中的哪儿
3、页数据个数
4、最后插入记录的位置
5、max_trx_id:二级索引中,修改当前页的最大事务的id
6、页的在B+数中的层级
7、当前页属于哪个索引
简略如图2.27
1、区定义
一个区是1M,可以存放64个连续的页
2、区作用
顺序IO,因为页之间可能不是物理连续的存储,而是使用的双向链表进行逻辑上连续。为了顺序IO查找更快,将64个物理上连续的页放在一个区中。这样where c between 10 and 300,很有可能就在一个区的连续页中,这样顺序IO一次就可以将这些页都捞取出来。
3、段定义
如果区1、区2都放叶子节点,区3、区4都放目录页即非叶子节点,那么区1、2放在一个段中(数据段),这样更方便查询。区3、4放在一个段中(索引段)
4、表空间定义
InnoDB的最高层的逻辑结构,数据和索引都存放于此。
独立表空间:每张表都有一个单独的表空间,便于空间管理
大小:新表创建时.ibd文件默认96kb(6页),mysql8.0之后.frm和.ibd都和为.ibd了,默认112kb(7页)
主键索引的叶子节点存的是整行数据。在InnoDB里,主键索引也被称为聚簇索引(clustered index)。
非主键索引的叶子节点内容是主键的值。在InnoDB里,非主键索引也被称为二级索引(secondary index)。
叶子节点:装的是每一行的数据,对应的record_type = 0表示普通记录
非页面节点:record_type = 1表示目录项记录,页30中
数据即索引,索引即数据
Innodb的索引和数据存放在.ibd文件中,因为数据本身就是要一个索引(数据结构)存储的,索引的叶子节点又是数据。
聚簇表示:叶子节点存放数据(聚簇索引),非聚簇表示叶子节点不存放数据(二级索引,叶子节点只存放主键id,不存放数据,所有才会有根据主键id再回表这一说)
以InnoDB的一个整数字段索引为例,这个N差不多是1200。这棵树高是4的时候,就可以存1200的3次方个值,这已经17亿了。
考虑到树根【叶子节点】的数据块总是在内存中的,一个10亿行的表上一个整数字段的索引,查找一个值最多只需要访问3次磁盘。
在机械硬盘时代,从磁盘随机读一个数据块需要10 ms左右的寻址时间。也就是说,对于一个10亿级别行的表,使用1200叉树来存储,单独访问一个行需要3个10 ms的时间
如图2.29
这样在存入(id=9,c2=1)的数据时,就可以在页3中,先根据c2=1判断,再根据id = 9判断,可推断出存入页5中
如图2.30,以c2、c3两字段组成联合索引
为字符串创建前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本
alter table SUser add index index1(email);
alter table SUser add index index2(email(6));
select id,name,email from SUser where email='zhangssxyz@xxx.com';
查询步骤如图2.4(回表1次)
前缀索引查询步骤如图2.5(回表4次)
在这个过程中,要回主键索引取4次数据,也就是扫描了4行。
1)优点:
对于这个查询语句来说,如果你定义的index2不是email(6)而是email(7),也就是说取email字段的前7个字节来构建索引的话,即满足前缀’zhangss’的记录只有一个,也能够直接查到ID2,只扫描一行就结束了。
2)缺点:
有可能额外增加回表的次数
使用前缀索引就用不上覆盖索引对查询性能的优化了,必须回到ID索引再去判断email字段的值
前缀索引无法做orderBy、groupBy(可以在代码中使用map的groupBy)
3)确定前缀的长度
select count(distinct email) as L from user;//假设有1000个不同的值
select count(distinct left(email,4) as L4, //只有200个不同的值,损失了太多区分度
select count(distinct left(email,5) as L5, //有500个不同值,仍损失一半区分度
select count(distinct left(email,6) as L6, //有800个不同值
select count(distinct left(email,7) as L7, //有950个不同值,损失比例5%,可以接受。
left(email,7)既节省空间,又不用额外增加太多的查询成本(损失区分度导致的多次回表)
4)如果前缀索引很难区分,则其他优化方式:
倒序存储
如果你存储身份证号的时候把它倒过来存,每次查询的时候,可以这么写
select x,x,x from t where id_card = reverse('xxxxx');
由于身份证号的最后6位没有地址码这样的重复逻辑,所以最后这6位很可能就提供了足够的区分度。
5)使用limit时候,不能使用前缀索引
alter table user add index index2(email(6));
select id,name,email from user where name = 'mjp' order by email limit 3;
在order by的时候,可能前3个数据都一样,无法支持使用索引排序,只能用文件排序
1、索引字段禁止为NULL,请为字段设置默认值
2、创建联合索引时区分度高的字段在前
select count(distinct(a)) / count(*) from t;
发展角度
表中字段poi_id和sale_date,已知poi_id总共就只会有<100种不同的值,而sale_date每个月就是30种不同的值,而且随着日期不断推迟,sale_date字段的区分度只会越来越高。这种情况下,联合索引的顺序建议为:
sale_date,poi_id
3、对于区分度很低的字段,没必要放在联合索引中。
4、ctime字段要加上索引(便于后续数据归档)
1、单张表中索引数量不超过6个, 单个索引中的字段数不超过5个
2、建议使用联合索引,而非单个索引
3、如果必须建立(a,b)和b两个索引,注意索引字段大小。eg:name字段是比age字段大的 ,那我就建议你创建一个(name,age)的联合索引和一个(age)的单字段索引。
4、sale_date日期字段作为索引时,建议使用int(11)而非varchar
5、对频繁更新的字段,尽量不要创建索引
要想设计一个结构合理的表,必须满足一定的范式(标准)
球员表(player) :id、球员编号 、 姓名 、 身份证号 、 年龄 、 球队编号
球队表(team) :id、球队编号 、主教练 球队所在地
主键 :默认为自增主键id
外键 :球员表中的球队编号。
主属性 :能够唯一确定一条数据的字段。在球员表中,主属性是id、球员编号、身份证号
非主属性 :其他的属性(姓名) (年龄)(球队编号)都是非主属性。
1、定义:每一列原子不可再分
2、举例:address列:上海市杨浦区大桥街道1000弄
可以拆分为:
省 | 市 | 区 | 详细地址 |
---|---|---|---|
上海 | 上海 | 杨浦 | 大桥街道1000弄 |
3、原子性
是主观的,主要是根据业务是否会用于搜素,统计等诉求。加入要统计上海市-杨浦区的用户有多少人,则address字段就需要拆分为原子的更小粒度。如果没有这种诉求,则直接作为user_info一起存储也可。
1、定义
非主键必须完全依赖主键,不能有部分依赖
2、举例
商品表: skuId是主键,skuName、品类id、商品价格等都是非主键。都完全依赖skuId
skuId已知,则skuName就知
skuId已经,则对应的品类id就知
skuId已知,则对应的价格就知
学生id | 老师id | 学生name | 老师name |
---|---|---|---|
10001 | 2001 | 张三 | 李丽 |
10002 | 2002 | 李四 | 韩理 |
10003 | 2003 | 王五 | 齐其 |
主键:学生id + 老师id
部分依赖:学生name字段,只依赖主键中学生id,即部分依赖。同理老师name
问题:数据冗余
3、解决:多对多
id | 学生id | 老师id |
---|---|---|
1 | 10001 | 2001 |
2 | 10002 | 2002 |
3 | 10003 | 2003 |
4 | 10004 | 2003 |
5 | 10004 | 2004 |
学生1:老师N
10004学生的数学老师是2003,语文老师是2004
老师1:学生N
2003数学老师,既教10003同学,又教10004同学
1、定义:非主键必须直接依赖主键,不能传递依赖
本质:两个非主键之间不能有依赖关系
2、解决
产生依赖传递的两个非主键,单独抽出来一张表。
3、优点和缺点
优点:冗余低
缺点:需要各种join,查询效率低
1、背景
范式的缺点就是需要各种join,查询效率低。我司甚至不允许复杂sql查询,比如join、子查询等。所以,这时候就需要反范式
2、举例
品类名称 --> 品类id --> sku_id,不满足3NF。但业务一般展示sku的品类id同时也会展示这个品类名称。如果为了满足3NF拆开,则需要用join查询,这样业务访问量大的时候,反而影响查询性能。所以,在性能和标准之间,我们也需要考量
3、问题
如果我们为了满足性能,采用了反范式,可能存在以下问题
品类名称 --> 品类id --> sku_id
4、思考和感悟
create table `my_table`( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `order_no` varchar(64) COLLATE utf8mb4_bin NOT NULL COMMENT '单号', `supply_code` int(11) DEFAULT 0 COMMENT '供应商code', `sku_category_level` tinyint(4) NOT NULL DEFAULT 0 COMMENT '品类级别', `sku_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'sku名', `extra_info` json DEFAULT NULL COMMENT 'sku额外信息', `price` decimal(26,6) DEFAULT NULL COMMENT 'sku价格', `trigger_date` date NOT NULL COMMENT '触发日期2024-01-01', `trigger_time` time NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '触发时间10:00:00', `apply_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间', `approve_time` datetime DEFAULT NULL COMMENT '审批时间', `ctime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `utime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `valid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否有效0无效,1有效', PRIMARY KEY(`id`), UNIQUE KEY uniq_order_no(`order_no`), KEY idx_ctime(`ctime`), KEY idx_code_level(`supply_code`,`sku_category_level`), ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT '自定义表'
补充:order_no使用了utf8mb4_bin大小写敏感(区分大小写)
如果你创建的表没有主键,那么InnoDB会自己生成一个长度为6字节的rowid来作为主键。
对比 | 自增整型 | UUID |
---|---|---|
大小 | 8字节 | mysql是用字符串存储占36字节(使用二进制方式存储则占16字节),浪费存储空间 |
比较大小 | 字符串比大小慢 | 整型快 |
插入 | 随机插入,如果4k页已满,则需要移动分页 | 顺序插入,不需要移动已经数据,效率高 |
查询 | 慢 | 连续存储,查询是一段范围,更快 |
自增UUID:mysql8.0中对UUID作为优化,uuid可以单调递增(将秒-分-时,改为时-分-秒);同时存储占用16字节
(uuid_to_bin(UUID(), true));
综上,顺序io不需要再额外的寻道,只需要第一次寻址完成后,磁头顺序读取数据即可。
所以顺序I/O一般比随机I/O快几千倍
商品id:不可以,这个id可能会注销,给别的商品用
手机号::不可以,用户可能会注销,运营商再给别人用
身份证号:不可以,因为主键id不能为空,但是并不是所有用户都愿意将隐私信息输入。可能会影响业务推进
单据号:可以。TG|HT + 01|02|03|04 + 231231 + 10位随机数(一个业务类型,一天最多有10位数的单据生成,可以使用雪花算法生成、也可以使用leaf生成、也可以使用redis自增生成)
推荐:生成单据号
1、背景
唯一键冲突导致id自增+1后,无法恢复到错误发生之前的值(事务的回滚也会产生类似的现象)
2、描述
表中name字段是唯一键,假如表中有了一条数据(1,mjp,18),此时再insert(null,mjp,28)
3、执行流程如下
Innodb发现用户没有传入自增id值,会去获取当前表的自增值应为2
将传入的内容改为(2,mjp,18)
再insert之前会先修改自增至为3,然后再插入
报唯一键冲突。语句返回
此时id=2的数据没有插入成功,但是id此时已经为3了不会被改回去。下次再插入数据之前会获取到id=3
所以没有id=2这行数据
补充:批次一200、批次二200、批次三200,批量插
public void batchInsert(List<TestTableDO> DOList) {
if (CollectionUtils.isEmpty(DOList)) {
return;
}
Lists.partition(DOList, 200).forEach(partitionData -> {
this.testTableAutoGeneratorMapper.batchInsert(partitionData);
});
}
同一个批次,插入多条数据时,是原子性的,一条失败,这个批次200条都失败,不会写入表中
不同批次之间,没有原子性,报错的批次 以及 其后面的批次,都不会写入。报错之前的批次数据可正常插入
eg1:批次一失败,批次一中200条数据都不会写入。且后续批次二、三都不会执行
eg2:批次一成功,会插入200条数据,批次二中有失败,则批次二全部写入失败,批次三不会执行
eg3:原本id = 1,批次一中因为唯一键冲突导致插入失败,则批次2再插入时,下一条数据的id = 1 + batchSize即id从201开始
4、优化: 可以使用alter table A engine=InnoDB语句来优化空洞 。
varchar(100) 就是指 100 个字符
最大65535,但是只能Varchar(65532),行结构中变长字段长度2字符、NULL值列表1字符(如果声明了Not Null,则不需要),业务中不建议超过5000,超过了5000可选择将此字段拆出来text(65535)放在另外一张表或使用Json
都是可变的字符串
使用二者保存较短的字符串’hello world’时,占用的空间都是一样的
当20<字符长度<255,varchar(20)会报错,varchar(255)会正常插入
字符串特别长
字符长度>255,可以选择更大的值,VARCHAR(M)定义的列长度为可变长字符串,M取值可以为0~65535(64K)
故
当我们定义一个varchar(255)的字段时,其实它真实使用的空间是256(255+1)字节
当我们定义一个一个varchar(256)字段时,它真实使用的空间是258(256+2)字节
字符集是一套符号和编码,用于比较字符的一套规则。
总结:general_ci 更快,unicode_ci 更准确、utf8mb4_bin对字符大小写敏感。(补充:现在的CPU来说,它远远不足以成为考虑性能的因素,索引涉及、SQL设计才是。使用者更应该关心字符集与排序规则在db里需要统一)
1、概念
ci即case insensitive,不区分大小写即大小写不敏感。"A"和"a"在排序和比较的时候是一视同仁。
selection * from test where cloumn="a"同样可以把cloumn为"A"的值选出来。对于mysql来说,'a'和‘A’没有区别
2、实战
使用单据号作为唯一键: 数据001A落表的时候会变成001a,因为mysql默认是不区分大小写的。
这样,当表里面有001a的时候 & 其是唯一键,再存001A就会导致唯一键冲突。
3、情景复现
CREATE TABLE `user` (
`name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名字',
UNIQ_KEY `name` (`name`) COMMENT '唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT=''
这里的建表语句,使用的是COLLATE=utf8mb4_unicode_ci,这种方式创建的表中的字符串字段,都是不区分大小写的。
表里面已经有name = "aaaa"的数据
name字段是唯一键
name为字符串,且为ci格式
再次插入数据name = 'AAAA’时,就会报错。唯一键冲突
复现
4、解决
utf8mb4_unicode_ci或utf8mb4_general_ci ==> utf8mb4_bin
ALTER TABLE my_table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
方式二:也可以针对某一字符的字段属性,utf8mb4_bin(推荐)
alter table table modify column `name` varchar(128) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '姓名';
5、sop
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='xxxx'
需要大小写区分:
如:用于存用户登陆的账号、密码。密码就必须大小写敏感
当业务需要某个字段,需要区分大小写。即大小写不同有不同的含义时,则需要将字段设置为大小写敏感,即utf8mb4_bin
一般用于记录单据对应的明细(即1:N场景)
{
"单据某个属性1":{
"属性1的name、描述":"",
"属性1的类型type":"",
"属性1的值value":""
"属性1的额外extra信息":""
}
}
实战中,单行json字段列最大存储65535(64kb)
`xxx` json default null comment ''
8.0.13版本之前,json类型字段不允许设置默认值。之后则可以default(‘’)
INSERT INTO table (id, data) VALUES (1, '{"name": "John", "age": 30}');
UPDATE table SET data = JSON_SET(data, '$.age', 31) WHERE id = 1;
删除JSON列中的数据
UPDATE my_table SET data = JSON_REMOVE(data, '$.age') WHERE id = 1;
1、GsonUti
@UtilityClass @Slf4j public class GsonUtils { private static final Gson GSON = new Gson(); public static String toJsonStr(Object object) { return GSON.toJson(object); } public static String toJsonStr(Object obj, Type type) { return GSON.toJson(obj, type); } public static <T> T fromJson(String json, Class<T> classOfT) { return GSON.fromJson(json, classOfT); } public static <T> T fromJson(String json, Type typeOfT) { return GSON.fromJson(json, typeOfT); } public static String toJson(Object object) { try { return GSON.toJson(object); } catch (Exception e) { log.error("序列化失败", e); } return StringUtils.EMPTY; } }
2、map->json
map.put("0915", "1");
map.put("0916", "2");
String jsonStr = GsonUtils.toJsonStr(map);
3、date -> json
如果类中有Date类型的属性字段,则需要在创建gson的时候设置一下日期格式
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd")
.create();
System.out.println(gson.toJson(new Date()));
1、{} -> 对象
String str = "{\"status\":{\"code\":0,\"__isset_bit_vector\":[1]},\"poiId\":1}";
TResponse tResponseA = GsonUtils.fromJson(str, TResponse.class);
2、json->map
Map<String,String> json2Map = GsonUtils.fromJson(jsonStr,new TypeToken<HashMap<String,String>>(){}.getType());
3、json->list
List<SkuDTO> list = GsonUtils.fromJson(jsonStr,new TypeToken<List<SkuDTO>>(){}.getType());
List<Long> list2 = GsonUtils.fromJson(Lists.newArrayList(1L,2L), new TypeToken<List<Long>>() {}.getType());
public void t() throws JSONException { String s1 = "{\"skuId\":1,\"skuName\":\"test\",\"temp\":2,\"address\":\"bj\"}"; String s2 = "{\"skuId\":1,\"skuName\":\"test\",\"temp\":3,\"author\":\"mjp\"}"; // 方式一: JSONCompareResult result = JSONCompare.compareJSON(s1, s2, JSONCompareMode.STRICT); <dependency> <groupId>org.skyscreamer</groupId> <artifactId>jsonassert</artifactId> <version>1.5.1</version> <scope>test</scope> </dependency> // 1中有2中没有 System.out.println(result.isMissingOnField()); List<FieldComparisonFailure> fieldMissing = result.getFieldMissing(); for (FieldComparisonFailure fieldComparisonFailure : fieldMissing) { System.out.println(fieldComparisonFailure.getExpected());//address } // 2中有1中没有 System.out.println(result.isUnexpectedOnField()); List<FieldComparisonFailure> fieldUnexpected = result.getFieldUnexpected(); for (FieldComparisonFailure fieldComparisonFailure : fieldUnexpected) { System.out.println(fieldComparisonFailure.getActual());//author } // 1中2中都有,但是val值不一样 System.out.println(result.isFailureOnField()); List<FieldComparisonFailure> list = result.getFieldFailures(); for (FieldComparisonFailure fieldComparisonFailure : list) { System.out.println(fieldComparisonFailure.getField());//temp System.out.println(fieldComparisonFailure.getActual());//3 System.out.println(fieldComparisonFailure.getExpected());//2 } // 方式二: HashMap<String, Object> diffMap = Maps.newHashMap(); Gson gson = new Gson(); Map<String,Object> json1Map = gson.fromJson(s1,new TypeToken<HashMap<String,Object>>(){}.getType()); Map<String,Object> json2Map = gson.fromJson(s2,new TypeToken<HashMap<String,Object>>(){}.getType()); for (Map.Entry<String, Object> entry : json1Map.entrySet()) { String k1 = entry.getKey(); Object v1 = entry.getValue(); Object v2 = json2Map.get(k1); // 1中有2中没有 if (v2 == null) { diffMap.put(k1, v1); continue; } // 1中2中都有,但是不一样 if (!Objects.equals(v1, v2)){ diffMap.put(k1, "expect:" + v1 + ", actual:" + v2); } } json2Map.forEach((k2, v2) -> { Object v1 = json1Map.get(k2); // 2中有1中没有 if (v1 == null) { diffMap.put(k2, v2); } }); System.out.println(diffMap);//{temp=expect:2.0, actual:3.0, address=bj, author=mjp} }
使用RSA加密实现如下
public class TestRSA { /** * RSA最大加密明文大小 */ private static final int MAX_ENCRYPT_BLOCK = 117; /** * RSA最大解密密文大小 */ private static final int MAX_DECRYPT_BLOCK = 128; /** * 获取密钥对 * * @return 密钥对 */ public static KeyPair getKeyPair() throws Exception { KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(1024); return generator.generateKeyPair(); } /** * 获取私钥 * * @param privateKey 私钥字符串 * @return */ public static PrivateKey getPrivateKey(String privateKey) throws Exception { KeyFactory keyFactory = KeyFactory.getInstance("RSA"); byte[] decodedKey = Base64.decodeBase64(privateKey.getBytes()); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey); return keyFactory.generatePrivate(keySpec); } /** * 获取公钥 * * @param publicKey 公钥字符串 * @return */ public static PublicKey getPublicKey(String publicKey) throws Exception { KeyFactory keyFactory = KeyFactory.getInstance("RSA"); byte[] decodedKey = Base64.decodeBase64(publicKey.getBytes()); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey); return keyFactory.generatePublic(keySpec); } /** * RSA加密 * * @param data 待加密数据 * @param publicKey 公钥 * @return */ public static String encrypt(String data, PublicKey publicKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); int inputLen = data.getBytes().length; ByteArrayOutputStream out = new ByteArrayOutputStream(); int offset = 0; byte[] cache; int i = 0; // 对数据分段加密 while (inputLen - offset > 0) { if (inputLen - offset > MAX_ENCRYPT_BLOCK) { cache = cipher.doFinal(data.getBytes(), offset, MAX_ENCRYPT_BLOCK); } else { cache = cipher.doFinal(data.getBytes(), offset, inputLen - offset); } out.write(cache, 0, cache.length); i++; offset = i * MAX_ENCRYPT_BLOCK; } byte[] encryptedData = out.toByteArray(); out.close(); // 获取加密内容使用base64进行编码,并以UTF-8为标准转化成字符串 // 加密后的字符串 return new String(Base64.encodeBase64String(encryptedData)); } /** * RSA解密 * * @param data 待解密数据 * @param privateKey 私钥 * @return */ public static String decrypt(String data, PrivateKey privateKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] dataBytes = Base64.decodeBase64(data); int inputLen = dataBytes.length; ByteArrayOutputStream out = new ByteArrayOutputStream(); int offset = 0; byte[] cache; int i = 0; // 对数据分段解密 while (inputLen - offset > 0) { if (inputLen - offset > MAX_DECRYPT_BLOCK) { cache = cipher.doFinal(dataBytes, offset, MAX_DECRYPT_BLOCK); } else { cache = cipher.doFinal(dataBytes, offset, inputLen - offset); } out.write(cache, 0, cache.length); i++; offset = i * MAX_DECRYPT_BLOCK; } byte[] decryptedData = out.toByteArray(); out.close(); // 解密后的内容 return new String(decryptedData, "UTF-8"); } /** * 签名 * * @param data 待签名数据 * @param privateKey 私钥 * @return 签名 */ public static String sign(String data, PrivateKey privateKey) throws Exception { byte[] keyBytes = privateKey.getEncoded(); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey key = keyFactory.generatePrivate(keySpec); Signature signature = Signature.getInstance("MD5withRSA"); signature.initSign(key); signature.update(data.getBytes()); return new String(Base64.encodeBase64(signature.sign())); } /** * 验签 * * @param srcData 原始字符串 * @param publicKey 公钥 * @param sign 签名 * @return 是否验签通过 */ public static boolean verify(String srcData, PublicKey publicKey, String sign) throws Exception { byte[] keyBytes = publicKey.getEncoded(); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PublicKey key = keyFactory.generatePublic(keySpec); Signature signature = Signature.getInstance("MD5withRSA"); signature.initVerify(key); signature.update(srcData.getBytes()); return signature.verify(Base64.decodeBase64(sign.getBytes())); } public static void main(String[] args) { try { // 生成密钥对 KeyPair keyPair = getKeyPair(); String privateKey = new String(Base64.encodeBase64(keyPair.getPrivate().getEncoded())); String publicKey = new String(Base64.encodeBase64(keyPair.getPublic().getEncoded())); System.out.println("私钥:" + privateKey); System.out.println("公钥:" + publicKey); // RSA加密 String data = "待加密的文字内容"; String encryptData = encrypt(data, getPublicKey(publicKey)); System.out.println("加密后内容:" + encryptData); // RSA解密 String decryptData = decrypt(encryptData, getPrivateKey(privateKey)); System.out.println("解密后内容:" + decryptData); // RSA签名 String sign = sign(data, getPrivateKey(privateKey)); System.out.println("加签后:"+sign); // RSA验签 boolean result = verify(data, getPublicKey(publicKey), sign); System.out.print("验签结果:" + result); } catch (Exception e) { e.printStackTrace(); System.out.print("加解密异常"); } } }
如图1.1
两块:一块是Server层,它主要做的是MySQL功能层面的事情
一块是引擎层,负责存储相关的具体事宜。
1、连接器-建立连接
2、分析器-解析sql
3、优化器优化
总代代价= IO代价 + CPU代价+ 内存代价 + 远程代价
1)其中IO代价
2)CPU代价
3)内存代价
创建临时表,默认代价值为20。在内存中创建临时表还好点,磁盘中代价太大
4、执行器执行
5、调用Innodb的API接口查询数据并返回
1、不要使用负向查询,如:NOT IN/NOT LIKE
2、严禁 like ‘%abc’、like ‘%abc%(如果非要,则走es等搜索引擎)’、not in 、!= 、not exists、<>等语法。推荐like ‘北京%’
3、禁止使用select for update
4、禁止使用select子查询,使用join替代
select * from t where t.id in(
select f.id from table where table.id in(10,20,30)
);
// 使用join替换
select * from t inner join table on t.id = table.id where table.id in(10,20,30);
1、不建议使用前项通配符查找,例如 “%foo”,查询参数有一个前项通配符的情况无法使用已有索引。
2、对于连续的数值,能用 BETWEEN 就不要用 IN 了
select id from t where num in(1,2,3,4);
3、conut(任意)统计某列非空个数,一般都是全表扫描,尽量减少使用(有NULL的行不算)。
count查询where条件要能走索引
4、新增的查询SQL一定要执行过执行计划,访问类型type:至少要达到 range 级别,要求是 ref 级别,如果可以是 consts 最好。禁止all和index
5、ORDER BY、GROUP BY、DISTINCT(UNION)的字段,充分利用索引先天顺序避免排序,否则会消耗大量磁盘IO
6、对于select, in 操作建议控制在200个之内。最好在100内
7、索引中断问题
使用了函数
对字段做了函数计算,就用不上索引
对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。
8.0之后的版本,a_b_c联合索引,当where条件是b = and c = 也会使用到联合索引
8、not in、not exists建议使用left join on where xxx is null替代
9、如果要使用where (col1 = a and col2 = b) or (col1 = c and col2 =d)
where (col1,col2) in (
(a,b),
(c,d)
)
@Select
("<script"+
"SELECT * FROM fulfill_data where (sale_dt, rdc_id) in " +
"<foreach collection='pairs' item = 'pair' open='(' separator= ',' close=')'>" +
"(#{pair.saleDate}, #{pair.rdcId})" +
"</foreach> " +
" limit 0,200 " +
"</script>")
List<XxxDO> select(@Param("pairs") List<ListKey> pairs);
SELECT * FROM t WHERE sale_dt in('20240115', '20240116') and rdc_id in (111,777)
原因:但是使用上述方式查询,可能会产生笛卡尔乘积结果
sale_dt | rdc_id |
---|---|
20240115 | 111 |
20240116 | 111 |
20240115 | 777 |
20240116 | 777 |
但实际我们查询的是(sale_da = 20240115 and rdc_id = 111) or (sale_da = 20240116 and rdc_id = 777) 应该只返回2条数据才对
1、对结果中某一列去重
select * from 表名 where id in(select max(id) from 表名 group by 要去重的字段 )
2、去重
select distinct(rdc_id) from table where sale_date = '2024-01-22';
使用distinct时,去重字段最好作为联合索引的一部分,因为索引本身就是排序好的,这样找where满足条件的数据后,本身就是排序好的去重即可。否则会using temp
按照效率排序的话,count(普通字段)<count(主键id)<count(索引字段)<count(1)≈count(*)
(
CASE table1.sku_temperature_zone
WHEN 5 THEN '常温'
WHEN 2 THEN '冷藏'
WHEN 1 THEN '冷冻'
END
) as `温层`
1、yyyymmdd 转 yyyy-mm-dd
where
sale_date = concat(
substr('20220517', 1, 4),
'-',
substr('20220517', 5, 2),
'-',
substr('20220517', 7, 2)
);
2、yyyy-mm-dd 转yyyymmdd
concat(substr('2022-05-18',1,4),substr('2022-05-18',6,2),substr('2022-05-18',9,2));
方式2
sale_date = replace('2021-11-03','-','');
1、索引覆盖
假设,我们有一个主键列为ID的表,表中有字段k,并且在k上有索引。
表中R1~R5的(ID,k)值分别为(100,1)、(200,2)、(300,3)、(500,5)和(600,6),两棵树的示例示意图如下2.1
2、回表
select * from T where k between 3 and 5
在这个过程中,回到主键索引树搜索的过程,我们称为回表(回表本质是随机I/O比较耗时)。可以看到,这个查询过程读了k索引树的3条记录(步骤1、3和5),回表了两次(步骤2和4)。
回表属于随机IO
原因:二级索引叶子节点字段1 + id1 、字段2 + id2,其中字段1、2是顺序排序的在一个页或一个区中,但是id1和id2可能在不同的页不同的区,所以回表本质上是随机IO
联合索引(name, age),现在需要检索出表中“名字第一个字是张,而且年龄是10岁的所有男孩
select * from tuser where name like '张%' and age=10 and ismale=1;
like后面,索引失效。索引联合索引只能走到第一个name字段
在MySQL 5.6之前,只能从ID3开始一个个回表。到主键索引上找出数据行,再对比字段值。
此时不关注age字段,只关注name字段,(name,age)二级索引找到叶子节点id,然后回表聚簇索引,查询id对应的这行数据。然后再根据age值与这行数据中的age值对比,一致才留下。因此,需要回表4次。
而MySQL 5.6 引入的优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
在(name,age)索引内部就判断了age是否等于10,对于不等于10的记录,直接判断并跳过。只需要对ID4、ID5这两条记录回表取数据判断,就只需要回表2次。
优点:减少回表次数(所以无需回表的查询,都不会触发索引下推)
索引下推ICP触发的条件
name_age是联合索引,其中age在联合索引字段中
8.0优化:
8.0之后的版本,where name like ‘张%’ and age=10 and ismale=1,联合索引为name_age,不需索引下推直接正常走索引
1、查询
唯一索引查找到数据在某一页中的某一条数据时,则可以结束。因为数据是唯一的
这里以单字段二级索引为例,当查到是某一页的某一条数据时,还需要向后继续判断,因为可能name ='mjp’的有多条数据
2、更新
由于需要提前将数据页读取到内存中,用于判断插入的数据是否满足唯一性,故无法使用change_buffer(用于缓存更新语句的内容,类似于懒加载,缓存多条更新语句,等某条语句需要查询最新的内容了,再去刷盘(merge操作)(也会有后台线程定期的merge)。这种懒加载避免了,每次更新一条语句都要直接读取页,刷页,刷盘,减少读盘操作)
3、结论
1、功能:获取左表的所有记录,即使右表没有对应的匹配记录,用null填充
2、建议:使用小表作为驱动表
1)本质
双层for循环
2)小表作驱动表的原因
假设:驱动表全表扫描、被驱动表走索引
被驱动表行数M,每次查询一条数据,需要先搜索索引,索引是B+树结构,且需要回表,综上复杂度2(回表+二级索引搜索) * Log2M(索引查询M条数据)
驱动表行数N,没索引全表扫描N行,且每一行都要到被驱动表上去匹配一次
双层For循环复杂度 N * 2 * Log2M
N扩大1000倍,整体复杂度就扩大1000倍,所以一定要让N小的做驱动表(即“小表”作驱动表的原理)
3)“小表”定义
不是表本身数据量的多少,而是
两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”(正常情况下数据量小的表一般是小表)
3、强制
被驱动表的查询条件,必须是索引字段。理论上驱动表的匹配字段也最好是索引字段。
匹配的字段类型一定要一样,要么都是整型,要么都是字符串,否则会使用函数转换,索引失效
select * from t1 left join t2 on t1.name = t2.user_name;
1、功能:获取两个表中字段匹配关系记录,最终留下的每行数据中,左表和右表对应的列一定有数据
2、建议:使用inner join 即简写join时,mysql会选择小表作为驱动表
3、优化器
straight_join代替join
on:用于生产临时表的条件
where:在临时表产生后,再对临时表进行过滤,返回最终的数据
t1
id | sku_id |
---|---|
1 | 111 |
2 | 222 |
3 | 333 |
t2
sku_id | sale_date |
---|---|
111 | 2023-01-01 |
222 | 2023-06-30 |
222 | 2023-12-31 |
select * from t1 left join t2 on t1.sku_id = t2.sku_id where t2.sale_date= '2023-06-30';
1、步骤一:将t1表中每行数据,用on条件(t1.sku_id = t2.sku_id)去获取匹配到的数据
t1.id | t1.sku_id | t2.sku_id | t2.sale_date |
---|---|---|---|
1 | 111 | 111 | 2023-01-01 |
2 | 222 | 222 | 2023-06-30 |
2 | 222 | 222 | 2023-12-31 |
2、t1未匹配到的行数据,也保留下来(这一行仅仅除了t1有正常数据,t2的数据字段列均为null)
t1.id | t1.sku_id | t2.sku_id | t2.sale_date |
---|---|---|---|
3 | 333 | null | null |
3、经过on条件后,生成的临时表temp
t1.id | t1.sku_id | t2.sku_id | t2.sale_date |
---|---|---|---|
1 | 111 | 111 | 2023-01-01 |
2 | 222 | 222 | 2023-06-30 |
2 | 222 | 222 | 2023-12-31 |
3 | 333 | null | null |
select * from t1 left join t2 on t1.sku_id = t2.sku_id and.sale_date= '2023-06-30';
1、将t1表中每行数据,用on条件(t1.sku_id = t2.sku_id and.sale_date= ‘2023-06-30’)去获取匹配到的数据
t1.id | t1.sku_id | t2.sku_id | t2.sale_date |
---|---|---|---|
2 | 222 | 222 | 2023-06-30 |
2、t1未匹配到的行数据,也保留下来(这一行仅仅除了t1有正常数据,t2的数据字段列均为null)
t1.id | t1.sku_id | t2.sku_id | t2.sale_date |
---|---|---|---|
1 | 111 | null | null |
3 | 333 | null | null |
3、经过on条件后,生成的临时表temp
t1.id | t1.sku_id | t2.sku_id | t2.sale_date |
---|---|---|---|
2 | 222 | 222 | 2023-06-30 |
1 | 111 | null | null |
3 | 333 | null | null |
如图2.38
select * from a left join b on a.key = b.key where b.key is null
结合上述on + where的执行过程,先生成临时表,然后在临时表的基础上进行b.key is null过滤。明显是过滤掉a中存在b中不存在的数据。所以场景4的图如上
自动忽略null值
需要先分组、再聚合(avg、sum),否则整张表就是一个分组
使用group by 的场景:当出现每种、每个的时候,后续的内容就是需要分组的字段
select gender, max(math_score) from user group by gender;
select depart_id, count(*) from emp group by depart_id;
查询部门员工数 > 100的部门id
步骤一:每个部门的员工数
步骤二: 人数> 100
select depart_id, count(*) from emp group by depart_id
having count(*) > 100;
每种工种最高工资 > 1w的工种id和最高工资数
步骤一:每种工种的最高工资
步骤二:最高工资 > 1w
select job_id, max(salary) from emp group by job_id
having max(salary) > 10000;
查询最低工资 > 10号部门最低工资的 部门id和其对应的最低工资
步骤一:每个部门的最低工资
步骤二:10号部门的最低工资
步骤三:每个部门的最低工资 > 10号部门的最低工资
select depart_id, min(salary) from emp group by depart_id
having min(salary) > (
select min(salary) from emp where depart_id = 10
);
建议改成join连接查询
联合索引a_b_c
1、简单的临时表
select a,b,c from t where a = 1 group by c;
group by未使用到索引,会产生临时表和排序using filesort、using temporary
2、复杂临时表
索引为a
select id % 10 as m , count(*) as c from t where a = 1 group by m;
产生带临时表,内含2字段m(主键)和 c
扫描表t的索引a,依次取出其叶子节点上的id值
计算id % 10 =》 x
此时查看临时表中是否有值为x的行(1 % 10 = 1),第一次没有,在临时表对应的m、c两列中插入一条记录(x, 1)
m | c |
---|---|
1 | 1 |
如果后续有了值为x的行(11 % 10 = 1),则m列值为x的这行,对应的c列值+1即(x,2)
m | c |
---|---|
1 | 2 |
遍历完索引a后,对临时表按照主键m进行正排序,结果返回给客户端
3、不产生临时表、排序
select * from t where a = 1 and b = 2 group by c;
select * from t where a = 1 group by b;
1、union:选取表1和表2不同的值
2、union all:选取表1和表2所有的值,允许重复
3、表1order by UNION ALL 表2 order by
(select * from t1 where xxx order by xxx limit 100)
UNION ALL
(select * from t2 where xxx order by xxx limit 100)
如果在UNION中使用order by,那么必须使用()来包含查询,参考综合查询中
4、如果不强制要求数据不可重复,建议使用union all
因为union会给临时表加上distinct,用上了临时表主键id的唯一约束。唯一性校验代价很高
如果非要用,可以在java程序内存中进行去重。
如图2.19,union执行去重过程
查询城市是“杭州”的所有人名字,并且按照姓名排序返回前1000个人的姓名、年龄。
索引city
select city,name,age from t where city='杭州' order by name limit 1000 ;
Extra这个字段中的“Using filesort”表示的就是需要排序,MySQL会给每个线程分配一块内存用于排序,称为sort_buffer。
Extra字段显示Using temporary,表示的是需要使用临时表
二者都出现,需要临时表,并且需要在临时表上排序
如图2.6
1)初始化sort_buffer,确定放入name、city、age这三个字段;
2)从索引city找到第一个满足city='杭州’条件的主键id,也就是图中的ID_X;
3)到主键id索引取出整行,取name、city、age三个字段的值,存入sort_buffer中;
4)从索引city取下一个记录的主键id;
5)重复步骤3、4直到city的值不满足查询条件为止,对应的主键id也就是图中的ID_Y;
6)对sort_buffer中的数据按照字段name做快速排序;
7)按照排序结果取前1000行返回给客户端。
如果排序数据量太大超过sort_buffer_size的大小(默认1M)
内存放不下,则不得不利用磁盘临时文件辅助排序。外部排序一般使用归并排序算法,MySQL将需要排序的数据分成12份,每一份单独排序后存在这些临时文件中。然后把这number_of_tmp_files = 12个有序文件再合并成一个有序的大文件。
max_length_for_sort_data,是mysql中专门用于控制排序的行数据的长度参数(默认1024)
如果select的字段加起来长度超过这个值,则会从全字段排序换为rowId排序
1)初始化sort_buffer,确定放入两个字段name和id
2)从索引city中找到满足city="杭州"的第一个主键id
3)然后使用id回表,取name和id字段值放入sort_buffer
4)从索引city字段取下一个满足的id,同时使用id再回表,然后再存值,直到不满足city=“杭州”
5)对sort_buffer中的数据按照name排序
6)对排序后的结果取limit 1000
7)然后再使用这1000个id去回表,依次拿出city、name、age三个字段返回给客户端
总结
1、如果无法避免filesort
2、避免filesort
where查询条件可以使用到索引即可
select city,name,age from t where city='杭州' order by name limit 1000 ;
3、发生了filesort但性能很好
select * from t where name = 'm' and age > 10 order by address;
1)会使用filesort,但是性能比不使用filesort还好
2)只使用到了联合索引的前半部分name_age,进行过滤后,发现数据量不大1w级别这种,可以直接在内存中排序反而更快
4、limit起作用
全表扫描 + filesort
select * from t where age = 18 order by name;
走索引 + using where
select * from t where age = 18 order by name limit 10
原因是:优化器认为先按照name进行order by然后再where比对 age = 18的取10条,认为走索引更快
参考我的另一篇文章
Order By Limit一起使用时可能产生不确定性
不要直接limit 10000, 200的写法正例:select table_name from table where id > 10000 limit 200
实战
List<DO> result = Lists.newArrayList(); boolean loop = true; long id = 0L; do { XxxDOExample example = new XxxDOExample(); example.limit(200); example.setOrderByClause("id asc"); XxxDOExample.Criteria criteria = example.createCriteria(); criteria1.andIdGreaterThan(id); List<DO> selectByExample = myMapper.selectByExample(example); if (CollectionUtils.isNotEmpty(selectByExample)) { result.addAll(selectByExample); int size = selectByExample.size(); if (size < 200) { loop = false; } else { id = selectByExample.get(size - 1).getId(); } } } while (loop);
1、背景json字段net_pois_json数据内容如下
{ "test1":{ "poiId":323, "skuId2Qty":{ "1":1, "2":2 }, "skuId2Date":{ "3":"2023-01-01", "4":"2024-01-01" } }, "test2":{ "poiId":10005977, "skuId2Qty":{ "5":5, "6":6 }, "skuId2Date":{ "7":"2027-01-01", "8":"2028-01-01" } }, "test3":10, "test4":["mjp","wxx"] }
2、Map<String, Object> map 转为jsonString
map的key是String,第test1、test2的v是个对象,test3的v是Integer、test4的v是List<Stirng>
Gson g = new Gson();
String netPpoisJson = g.toJson(map);
json_extract():从json中返回想要的字段
json_contains():json格式数据是否在字段中包含特定对象
json_object():将一个键值对列表转换成json对象
json_array():创建json数组
1、k-v是一级
select * from t where 联合索引字段查询 and json_contains(net_pois_json, json_object("test3", 10));
select * from t where 联合索引字段查询 and net_pois_json -> '$.test3' = 10;
这一行的数据
ref + 使用联合索引 + using where
2、k-v是二级
"test1":{
"poiId":323,
"skuId2Qty":{
"1":1,
"2":2
},
"skuId2Date":{
"3":"2023-01-01",
"4":"2024-01-01"
}
},
select * from t where id = 1 and json_contains(
json_extract(net_pois_json, '$.test1'), json_object("poiId", 323)
);
即判断
{
"poiId":323,
"skuId2Qty":{
"1":1,
"2":2
},
"skuId2Date":{
"3":"2023-01-01",
"4":"2024-01-01"
}
}
中是否有指定的对象"poiId", 323
3、k-v是三级
select * from t where id = 1 and json_contains(
json_extract(net_pois_json, '$.test1."skuId2Date"'), json_object("3", "2023-01-01")
);
4、k-v作为范围查询条件
select * from t where id = 1 and json_contains(net_pois_json -> '$.test4', json_array("mjp"));
1、k是一级
select id, json_extract(net_pois_json, '$.test1') as v from t where id = 1;
select id, net_pois_json -> '$.test1' as v from t where id = 1;
id, test1对应的v
{
"poiId":323,
"skuId2Qty":{
"1":1,
"2":2
},
"skuId2Date":{
"3":"2023-01-01",
"4":"2024-01-01"
}
}
2、k是二级
select id, json_extract(net_pois_json, '$.test1."skuId2Qty"') as v from t where id = 1;
3、k是三级
select id, json_extract(net_pois_json, '$.test1."skuId2Qty"."3"') as v from t where id = 1;
select id, json_keys(net_pois_json) from t where id = 1 ;
结果:[“test1”,“test2”,“test3”,“test4”]
1、作用:查询包含v的数据
2、前提:v必须是字符串
net_pois_json列{“id”:1, “name”:“苹果”, “price”:0.5}
select * from t where json_serarch(net_pois_json, "all", "苹果") is not null;
{“id”:1, “price”:0.5} -> {“id”:1, “name”:“苹果”, “price”:0.5}
update t set net_pois_json = json_insert('{"id":1, "price":0.5}', '$.name', '苹果');
{“id”:1, “name”:“苹果”, “price”:0.5} -> {“id”:1, “name”:“香蕉”, “price”:0.5}
update t set net_pois_json = json_replace('{"id":1, "name":"香蕉", "price":0.5}', '$.name', '香蕉') where id = 1;
补充
update t set net_pois_json = json_replace( '"test1":{ "poiId":323, "skuId2Qty":{ "1":1, "2":2 }, "skuId2Date":{ "3":"2023-01-01", "4":"2024-01-01" } }', '$.test1."skuId2Date"."3"', '1994-11-23' ) where id = 1 ;
参考文档
1、sale_log as result: 主表,大部分字段都是取自这个表
2、sale_num as sale:需要从这个表获取真实销量sale_num字段
3、schedule as snap: 需要从这个表获取最终售最大售卖量total_stock字段
(sale.sale_num - result.origin_max_sale) as `降低|带来多货|提高销量PCS`;
(sale.sale_num - result.origin_max_sale) * result.sku_price as `销售额提高`;
如图2.18
1、场景1
result表 left join sale 表,并按照 where条件形成场景1-降多货
2、场景2
result 表 left join sale 表
on条件result和sale二者中一一对应的关系字段
where条件形成场景2-提升售卖量即提升GMV
3、场景1 UNION ALL场景2,组成临时表t
4、result表left join snap表,组成临时表m
on条件result和snap二者中一一对应的关系字段
where条件是日期范围
5、临时表t INNER JOIN 临时表m,形成最终的表
on条件t和m二者中一一对应的关系字段
where条件是网店id
6、概述sql
select
t.主要字段,t.真实销量, m.最终最大售卖量
from t
inner join
m
on
t和m一一对应关系
where
xxx
select distinct t.`商品SKUID`, t.`网店ID`, t.`网店名称`, t.`销售日期`, t.`角色`, t.`修改前数值`, t.`修改后数值`, t.`真实销量`, m.total_stock as `实际修改量`, t.`OR值`, t.`降低|带来多货|提高销量`, t.`降低|带来多货|提高销量PCS`, t.`销售价`, t.`销售价` * t.`真实销量` as `GMV` from( ( select result.sku_id as `商品SKUID`, result.poi_id as `网店ID`, sale.poi_name as `网店名称`, result.sale_date as `销售日期`, (CASE result.role WHEN 1 THEN '商家' WHEN 2 THEN '普通用户' END) as `角色`, result.origin_max_sale as `修改前数值`, result.update_max_sale as `修改后数值`, sale.sale_num as `真实销量`, result.or_qty as `OR值`, (CASE 1 WHEN 1 THEN '降低多货' END) as `降低|带来多货|提高销量`, (result.update_max_sale - result.origin_max_sale) as `降低|带来多货|提高销量PCS`, result.sku_price as `销售价` from sale_log as result Left join sale_num as sale on result.sku_id = sale.sku_id and result.poi_id = sale.poi_id and replace(result.sale_date,'_','') = sale.sale_date where result.sale_date between '$$begindate' and '$$enddate' and result.update_status = 1 and result.update_type = 1 and sale.sale_num < result.update_max_sale Order by result.operate_time Desc Limit 5000000 ) Union All ( select result.sku_id as `商品SKUID`, result.poi_id as `网店ID`, sale.poi_name as `网店名称`, result.sale_date as `销售日期`, (CASE result.role WHEN 1 THEN '商家' WHEN 2 THEN '普通用户' END) as `角色`, result.origin_max_sale as `修改前数值`, result.update_max_sale as `修改后数值`, sale.sale_num as `真实销量`, result.or_qty as `OR值`, (CASE 1 WHEN 1 THEN '提高销量' END) as `降低|带来多货|提高销量`, (result.update_max_sale - result.origin_max_sale) as `降低|带来多货|提高销量PCS`, result.sku_price as `销售价` from sale_log as result Left join sale_num as sale on result.sku_id = sale.sku_id and result.poi_id = sale.poi_id and replace(result.sale_date,'_','') = sale.sale_date where result.sale_date between '$$begindate' and '$$enddate' and result.update_status = 0 and result.update_type = 2 and sale.sale_num <= result.origin_max_sale Order by result.operate_time Desc Limit 5000000 ) ) as t Inner join( select snap.total_stock as total_stock, snap.base_sku_id as base_sku_id, snap.poi_id as poi_id,snap.schedule_date as schedule_date from sale_log as result Left join schedule as snap on result.sku_id = snap.base_sku_id and result.poi_id = sanp.poi_id and result.sale_date = snap.schedule_date and snap.dt = replace(snap.schedule_date,'-','') where result.sale_date between '$$begindate' and '$$enddate' ) as m On t.`商品SKUID` = m.base_sku_id and t.`网店ID` = m.poi_id and t.`销售日期` = m.schedule_date where t.`网店ID` in($poiId)
注意
1、查看最近执行的sql语句,读取了几页数据
SHOW STATUS LIKE 'last_query_cost';
2、是否开启慢查询日志
SHOW variables LIKE '%slow_query_log';
默认是off的,因为开启ON时,可能会影响性能,当需要慢查询分析时,可以开启慢查询日志
3、开启慢查询日志
set global slow_query_log = on;
4、慢查询时间为10s
show variables like '%long_query_time%';
5、慢查询时间定义为0.1s
set global long_query_time = 0.1;#全局
set long_query_time= 0.1;#session级别
2条语句都要执行,否则只执行global只会对新增的表查询生效
6、慢查询日志
名称:主机名-slow.log
查看:日志地址
show variables like '%slow_query_log_file';//opt/tmp/mysql.slow
7、慢查询定义
当一条sql的执行时间超过了设定的long_query_time时间 && 扫描的记录数(数据条数) > min_examined_row_limit
show variables like '%min_examined_row_limit'; // 默认为0,表示扫描过的最小记录数
这里加入将min_examined_row_limit值设置为10w,即使sql执行时间 > 慢查询定义0.1s,但是sql扫描的记录数不足10w,那这条sql也不算慢查询
分析步骤
1、找到slow.sql文件地址
2、常用查询
具体参数如下:
-a: 不将数字抽象成N,字符串抽象成S
-s: 是表示按照何种方式排序:
-t: 即为返回前面多少条的数据;
-g: 后边搭配一个正则匹配模式,大小写不敏感的
#得到返回记录集最多的10个SQL
mysqldumpslow -a -s r -t 10 /var/lib/mysql/slow.log
#得到访问次数最多的10个SQL
mysqldumpslow -a -s c -t 10 地址
#得到按照时间排序的前10条里面含有左连接的查询语句
mysqldumpslow -a -s t -t 10 -g "left join" 地址
#另外建议在使用这些命令时结合 | 和more 使用 ,否则有可能出现爆屏情况
mysqldumpslow -a -s r -t 10 地址 | more
select a.id, b.name from a left join b on a.id = b.id where a.date >= '2024-01-23';
all < index < rang < index_merge < ref < eq_ref < const < system
select * from table where id = 1;
join查询,on对应的驱动表table2通过主键、唯一索引、或者二级索引的等值匹配
select * from table1 inner join table2 on table2.id = table1.id;//或table2.age = 18二级索引等值匹配
使用二级索引列,进行常量等值匹配查询
联合索引name_age
select * from table where name = 'm' and age = 10;
index_merge
索引1 name
索引2 remark
select * from table where name = 'mjp' or remard = 'sorry';
如果or前后字段有不是索引的字段,则type直接变为all全表扫描
使用到了范围查找 > < between and like in or等
联合索引name_address_age
select id,name,address,age from table where age = 18;//key使用了name_address_age
key:使用到了name_address_age联合索引,且查询的列被联合索引覆盖。
但没有通过此索引进行过滤数据,需要扫描此联合索引的全部数据,本质属于“全表”扫描
全表扫描
联合索引name_address_age
select id,name,age,sex from table where age = 18;
等价
select * from table where age = 18;
全表扫描且查询返回大量的数据(几十万条数据),并不会把MySQL的内存打爆,但是会打爆Java内存
因为MySQL查询数据后把数据返回给客户端,流程如下
1)获取一行数据,写到net_buffer(默认16k,由net_buffer_length决定)中
2)再次获取一行数据,写到net_buffer中,当写满时,调用网络接口发送出去
3)发送成功后,清空net_buffer,然后重复1、2步骤
理论上,rows值越小越好。表示此次查询总共扫描了多少行,是一个预估值
查询用到了唯一索引,rows = 1
其他二级索引,只要回主键索引取一次数据,系统就会对row = row + 1。
using index
覆盖索引查询( 使用到了二级覆盖索引查询,且索引内容覆盖了要获取的字段 )
联合索引name_age
select id, name, age from table where name = 'mjp';
加上order by id则降为using where ;using index
using index condition
索引下推查询(参考上文-索引下推,减少回表次数)
联合索引name_age
select * from table where name like '张%' and age = 10;
加上order by id则降为using index condition;using filesort
using where ;using index
使用索引访问数据,并达到索引覆盖,但是 where 子句中有属于索引一部分(可以是联合索引第一个字段或第二个字段)但无法使用索引的条件(比如 like ‘%abc’ 左侧字符不确定)时:
联合索引 name_age_address
select id, name, age from table where name like 'm%' and age > 10;
select id, name, age from table where age = 10;
select id, name, age from table where age > 10;
加上order by id则降为using where
using where
场景1:使用全表扫描type = all
联合索引 name_address_age
select * from table where address = '铁岭';
select * from table where address like '铁岭%';
加上order by id仍为using where
场景2:type = ref
联合索引name_age
select * from table where name = 'm' and age = 10 and valid = 1;
查询条件中有除了索引包含的列之外的其它列查询
思考:我们设计表的时,是否需要valid字段:1表示有效、0表示无效,用于逻辑删除。这样的话查询语句都要加上where xxx and valid = 1
using filesort
order by、group by时,无法使用到联合索引。只能在内存中排序
using temporary
场景1:order by、group by时,无法使用到联合索引。而且内存大小不够排序,需要io创建临时文件用于排序
场景2:
select distinct(name) from t where id = 1;
name字段非索引字段,则需要使用临时表在内存中去重
性能分析
using index = null > using index condition > using index; using where > using where
order by| group by排序时:using filesort > using temporary。其他性能同上
1、单表查询
查询条件过滤数据的百分比越接近100越好。
结合rows一起理解。rows值为999表示预估扫描行数999,filtered = 100表示rows扫描的999行全部都是我们需要的。说明索引的过滤性很好
如果rows = 5000,filtered = 50,说明扫描了5000行,最终符合的数据只有2500行,说明查询条件的过滤性不好,白白的扫描了页中的数据
2、join
filtered指标显示被驱动表要执行的次数
select * from t1 join t2 on t2.key = t1.key where t1.name = 'mjp';
table | rows | filtered |
---|---|---|
t1 | 9000 | 10 |
t2 | 1 | 100 |
真正使用到的索引
强制走索引语句
select * from t force index(idx_name) where xxx;
索引的长度
这个字段可以确认联合索引是否所有字段都被用上查询了,还是只是用到了最左部分。比如联合索引name_age_address是全部用上了,还是只用到了name(联合索引中的字段被使用的越多即key_length越大越好,这样能更好的使用联合索引进行数据过滤)
eg:
select * from t where name = 'mjp' and address = 'cn';
不满足最左前缀匹配原则,即使查询用到了name_age_address索引,但是从ken_length结果可以看到只用到了索引的最前部分name字段
总结:
eg: 联合索引a_b_c
where a = 'm' and b ='j' and c='p'
优于
where a = 'm' and b = 'j' //等效where a = 'm' and b like 'j%' and c = 'p';
优于
where a = 'm' //等效 where a = 'm' and c = 'p';
计算规则如下:
结合type理解:当索引列使用等值查询时,ref内容即与索引列进行等值匹配的对象的信息
1、如果type类型是ref
select * from table where id = 1;
则ref是const,表示与索引列(这里是id主键索引)进行等值匹配的对象信息(这里与id进行等值匹配的对象是1,是个常量const)
2、场景2
联合索引name_address
select * from table where name = 'm' and address = '铁岭';
则ref指标会显示: const,const(与联合索引进行等值匹配的对象是2个常量)
3、场景3
select * from table1 inner join table2 on table2.id = table1.id;
这里type是eq_ref,ref是table1.id,表示与索引列table2.id进行等值查询的对象的信息为:table1.id
4、场景4
select * from table where id = abs(18);
ref: func,表示与索引列id,进行等值匹配的对象信息(是个函数)
背景:
那么我们怎么查看真正执行的sql语句是啥样子的呢,步骤如下
步骤一:explain select * from t where id in(1)
步骤二: SHOW WARNINGS
message中会有上述sql真正的执行内容
类似dump文件,一定不要在业务高峰期执行,收集信息时会影响性能,影响业务
#1. 查询冗余索引
select * from sys.schema_redundant_indexes;
// eg:有了联合索引name_age,又创建了name单值索引属于冗余
#2. 查询未使用过的索引
select * from sys.schema_unused_indexes;
#3. 查询索引的使用情况
select index_name,rows_selected,rows_inserted,rows_updated,rows_deleted
from sys.schema_index_statistics where table_schema='dbname' ;
# 1. 查询表的访问量
select table_schema,table_name,sum(io_read_requests+io_write_requests) as io from
sys.schema_table_statistics group by table_schema,table_name order by io desc;
# 2. 查看表的全表扫描情况
select * from sys.statements_with_full_table_scans where db='dbname';
#1. 监控SQL执行的频率
select db,exec_count,query from sys.statement_analysis
order by exec_count desc;
#2. 监控使用了临时表或者磁盘临时表的SQL
select db,exec_count,tmp_tables,tmp_disk_tables,query
from sys.statement_analysis where tmp_tables>0 or tmp_disk_tables >0
order by (tmp_tables+tmp_disk_tables) desc;
#1. 行锁阻塞情况
select * from sys.innodb_lock_waits;
事务就是要保证事物内的一组 操作单元(CRUD),要么全部成功,要么全部失败
1、Atomicity:原子性-全部提交成功/全部失败回滚
用户A给用户B转 50块,要么就是转成功A-50、B+50,要么就是转失败,二者账户都不增不减。
2、Consistency:一致性-数据从一个合法状态 变换到另外一个合法状态
3、Isolation:隔离性-隔离级别决定
4、Durability:持久性-一旦事物提交则修改会永久保存到数据库
总结:
A是基础、C是约束条件、I是手段、D是目的
事务并发时,各个事务之间不能互相干扰(类似于多线程并发操作共享数据,多线程可以通过加锁解决),并发事务可以通过隔离级别解决
总述如图2.17
含义:一个事务还没提交时,它做的变更就能被别的事务看到
问题:读未提交级别下可能会有脏读、不可重复读、幻读。这里简单介绍下脏读:
读取到一条不存在的数据(读取到的数据,别人回滚了)
解决:读已提交
含义:一个事务开始时,只能读到已经提交的事务所做的修改。即:一个事务从开始到提交,所做的任何修改,对其他事务都是不可见的
问题:可能会有不可重复读、幻读,这里简单介绍下不可重复读
两次执行相同的sql查询,可能得到不一样的结果,如图2.9
不可重复读的重点是修改!!!update操作
解决:可重复读
含义: 在一个事务中,从开始到结束的任意一瞬间读取到的数据应该都是一致的。
存在问题:幻读如图2.10幻读
幻读的重点在于新增或者删除操作
解决:
可串行化-不推荐
快照读(普通读)中的幻读使用的MVCC解决的
当前读select for update,中的幻读采用next-key lock解决
select * from table where id = 5 for update;
假如id在1,3,6有值
在可重读读隔离级别下,select for update会对数据加锁
如果id = 5值不存在,则从record lock 降级为next-lock间隙锁,锁住范围(1,6),这样就确保id=5无法插入,即图中步骤3无法执行。这样两次读取到的数据一样,不会出现幻读
含义:在读取的每一行数据都加锁。这样,就不会出现第一次和第二次读中间,事务B插入一条数据了。因为id = 5这行被加锁了
问题:大量的超时和锁竞争【几乎不用】
场景:除非非常需要数据一致性 且 没有并发
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。
delete大量数据,一次会占满整个事务日志,锁住很多数据,会阻塞重要的查询。
建议在业务低峰期,批量且有时间间隔的删除1w、1w这种方式
@Transactional(rollbackFor = Exception.class) public void main() { //1. insert A //2. insert B 、C try { insertBC(); } catch (Exception e) { } //3.其他业务逻辑 } @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED_NEW) public void insertBC() { // insert b //insert c }
注意点:
1、结论
本地写事务和rpc写操作(最好)不要放在一个事物中
2、原因
因为若本地写事务成功了,rpc写操作ReadTimeout执行失败(实际上此rpc写已经在远程成功了),会导致,本地写操作回滚。
造成,本地未写,远程写成功了
3、特殊
如果本地写和rpc写不要求那么强的一致性,而且rpc写失败了又可以重拾+幂等。那么允许放在一起
在保证幂等的情况下,job执行失败会有重试。同样能达到重试的机制
4、解决
@Transaction{
// 1、本地写
// 2、使用最大努力重试机制,保证rpc或者多个rpc一定成功
@最大努力重试{
rpc1
rpc2
}
}
1、踩坑-Methods should not call same-class methods with incompatible “@Transactional” values问题
方法和调用事务的方法,在同一个类中
问题描述
同一个类中无事务方法a()内部调用有事务方法b(),b方法上的事务不会生效
public class Demo{
public void funcA(){
funcB();
}
@Transactional(rollbackFor = Exception.class)
public void funcB() {
}
}
解决
((类名) AopContext.currentProxy()).funcB()
@Transactional(rollbackFor = Exception.class)
public void processBill() {
}
public class Demo{
@Transactional(rollbackFor = Exception.class)
public void funcA(){
funcB();
}
@Transactional(rollbackFor = Exception.class)
public void funcB() {
}
}
3、其他@Transactional不生效的场景
问题描述
NoUniqueBeanDefinitionException: No qualifying bean of type ‘org.springframework.transaction.PlatformTransactionManager’ available: expected single matching bean but found 2: default
原因
多数据源的时,某一数据源未配置事务name,导致事务失效
第一个数据源,事务默认名称为default,第二个数据源事务名称为sale_smart
当第一个数据源,使用事务,但是,没有指定事务处理器,就会报这个错误。
因为每个数据源都有自己的事务配置,单纯地用@Transactional 没法确定是哪个事务处理
解决
在多数据源配置文件中,指定name
数据源1 transactionName = sale_smart
数据源2 transactionName = other
在使用注解时,指定对应的数据源名称
@Transactional(value= "sale_smart", rollbackFor = Exception.class)
事务1方法
@Transactional(value= "other", rollbackFor = Exception.class)
事务2方法
update table set name = 'mjp' where id = 2;
update table set name = 'wxx' where id = 3;
如图2.22
1)将id = 2数据从磁盘中读取到内存,放入data_buffer中
准备更新内存数据前,先将id=2的旧值,写入undo log中便于回滚
将data_buffer中的id = 2数据进行值修改
将id=2的更改动作实时记录到内存的redo log buffer中
2)将id = 3的更新语句执行步骤1
3)当这2条更新语句对应的事务提交commit时!,将redo log buffer中的更新记录会按照一定的策略写到磁盘中的redo log file中
再进行半异步方式-主从复制(bin log 和 中继日志,参考后文主从复制过程)
主从复制中继日志写成功后,发动ACK,主库确认ACk后返回客户端事务处理成功
4)再将内存data_buffer中的更新结果以一定的时间间隔频率刷盘data中
InnoDB引擎特有
内容 | 物理日志,记录的是“在某个数据页上做了什么DML操作” |
---|---|
作用 | 保障事务的持久性。即如何将更新的数据从内存中可靠的刷到磁盘中 |
区别 | 一个事务中10条更新语句,redo log是不间断的顺序记录的 |
特点 | 循环写,空间固定会用完,会覆盖。固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB从头开始写,写到末尾就又回到开头循环写。write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件(因为随着内存data中数据刷盘,完成刷盘部分的数据就没必要再存在于redo log中了,就可以被覆盖了)。 |
循环写过程,如图2.42
其中checkpoint -> write pos范围内的数据,是内存data_buffer还未完成刷盘的数据,redo log必须要存好
write pos -> checkpoint范围内的数据,是内存data_buffer已完成刷盘的数据,redo log就没必要存储了。这部分就可以被循环写
1、先将内存中的redo log buffe刷到文件系统缓存page cacahe中
2、至于什么时候将page cache中内容写入redo log file中,完全交于操作系统自己决定
3、Innodb提供了参数innodb_flush_log_at_trx_commit,可以调整redo log刷盘的策略
提示:两阶段提交过程,是写redo log、写bin log与事务commit提交动作,先后顺序关系的最终解释版
1、背景
2、解决:redo log两阶段提交
两个阶段如图2.50
3、两阶段如何解决上述主从数据不一致问题
如图2.51
重启服务器时:
1、定义
对于同一份数据,多个事务之间可以并发读取,相互之间不阻塞
2、加锁
select * from t whrer id = 1 lock in share mode;
即事务1为上述sql、事务2也为上述sql,二者都加的共享锁,则读-读之间互不影响
3、生效范围
表、行
1、定义
当前写操作没有完成之间,会阻塞其他的写、读操作。
确保只有一个事务可以写,其它事务不能写,并且其它用户不能读取正在写的同一资源
2、加锁
select * from t whrer id = 1 for update;
即事务1为上述sql,事务2也为上述sql或事务2为select * from t whrer id = 1 lock in share mode。事务1未提交之前,事务2都会被阻塞
3、生效范围
表、行
优点
缺点
行锁优缺点相反
1、定义
当我们主键id auto increment时,而且我们batchInsert时或insert的内容是select的结果时,我们不知道要插入多少条数据,这个时候,一个事务持有表级锁-自增锁时,其他事务无法执行插入操作
2、特殊
我们普通的mybatis的单条数据的insert,知道要插入数据的条数。
只需使用metux轻量级锁,在分配id值期间保持即可,无需像上述批量插入,需要保持自增锁到插入语句结束为止
CRUD时,对表加MDL读锁;Alter table时,加MDL写锁(更改字段属性、表会锁全表)。读-写、写-读、写-写都是阻塞的
Alter table操作导致的MDL-写锁,必须要在业务低峰期进行,否则可能阻塞CRUD-MDL读锁
原因如图2.8
select * from table where id = 5;
select * from table where id = 5 for update;
如果查询条件字段不是索引字段,会降级为表锁
1、背景
顾客A要在影院B购买电影票
2、解析
这个交易,需要update两条记录,并insert一条记录
为了保证交易的原子性,把这三个操作放在一个事务中
如果同时有另外一个顾客C要在影院B买票,那么这两个事务冲突的部分就是语句2了。因为它们要更新同一个影院账户的余额,需要修改同一行数据。
3、建议
4、总结
如果你的事务中需要锁多个行,在不影响业务的情况下,把最可能造成锁冲突、最可能影响并发度的锁尽量往后放
一旦某个加锁操作没有使用到索引,则该锁就会退化为表锁
场景1:排他锁-读
SELECT * FROM table WHERE id = 5 FOR UPDATE;//锁住id = 5单行
select * from table where id = 5 share in mode;
场景2:排他锁-写
update table set name = 'mjp' WHERE id = 5 ;//锁住id = 5单行
select * from table where id = 5 share in mode;
SELECT * FROM table WHERE age = 5 FOR UPDATE;
SELECT * FROM table WHERE id >1 and id < 10 FOR UPDATE;
补充
1、当前读情况下,可以通过加此锁,解决可重复的读时的幻读问题
2、间隙锁,不区分共享间隙锁和排他间隙锁,即以下两种查询会加间隙锁,锁住(1,10)
SELECT * FROM table WHERE age = 5 FOR UPDATE;
SELECT * FROM table WHERE age = 5 share in mode;
3、 如果是between 1 and 10,则会锁住[1,10]。同理如果是id >=1 and id <= 10,也是锁定[1,10]
本质:Record lock + Gap lock
每个数据行上的,非唯一索引字段列上,都会存在一把临键锁
如图2.11
(0,10]
(10,24]
(24,32]
(32,45]
(45,无穷]
则
SELECT * FROM table WHERE age = 24 FOR UPDATE;
锁定的范围是:左gap lock + record lock+ 右gap lock =》 (10,24) + 24 + (24,32) =>(10,32)
作用:解决事务隔离级别为可重复读时,当前读(for update)可能存在的幻读问题
举例:事务A在写数据id=24时,会先查db有无id=24这条数据,有则不插入。无则插入
加锁的基本单位是next-key lock ,锁定范围( ]
查找过程中访问到的对象才会加锁
索引上的等值查询(where id = 5),给唯一索引加锁的时候(id字段),next-key lock退化为record lock,只锁id=5这一行的数据(前提是id=5这一行有数据)。
索引上的等值=查询(where age = 5为二级索引),向右遍历时且最后一个值不满足等值条件的时候(age = 6),next-key lock退化为间隙锁Gap lock,即锁定范围不是(1,6],还是间隙锁(1,6)
唯一索引上的范围查询(id=7这一行无数据)会访问到不满足条件的第一个值为止。
CREATE TABLE table (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into table values(0,0,0),(5,5,5), (10,10,10),(15,15,15),(20,20,20),(25,25,25);
1、等值查询从record lock降级为间隙锁场景
SELECT * FROM table WHERE id = 7 FOR UPDATE;//没有id=7的数据
2、据此再分析图2.11的sql
SELECT * FROM table WHERE age = 24 FOR UPDATE;
认为在更细数据时,总有别的线程会更新我们这份数据,所以会加锁,如表锁、行锁、Java锁关键字
不加锁,通过代码层面如CAS,version版本号的方式,解决潜在的资源可能别修改问题。适用于读操作多的场景
其中版本号操作如下:
进程A和进程B举例
在一段时间内,资源被A占用。若此时B来请求此资源,那么B只能等待
A获到的资源在其未使用结束前,B不能强行夺走此资源。B只能等待A主动释放
B提出资源获取请求,但是此资源正在被A占有,此时B被阻塞
B被阻塞的过程中,B占有的资源也保持持有,不释放
即阻塞申请资源的同时,也不释放自己占有的资源
A持有a资源,B持有b资源,A访问b资源,B访问a资源。形成一个循环等待
即避免产生死锁的四个条件
当发生死锁时,会主动检测,将死锁链条中持有最少行排他锁的事务进行回滚。这样便打破了死锁产生的条件,让其他事务得以继续执行
背景:Mybatis的upsert方法
插入数据时,本质为:
insert into xxx on duplicate key update;
1)初始化数据
insert into table (code, other) values(1,1),(3,3),(5,5);
2)事务1
insert into table (code, other) values(3,3) on duplicate key update
由于(3,3)这条数据已经存在,所以会在(1,3)范围内加间隙锁
3)事务2
insert into table (code, other) values(5,5) on duplicate key update
由于(5,5)这条数据已经存在,所以会在(3,5)范围内加间隙锁
4)产生死锁
总结过程如图2.44
5)建议
在并发事务执行的insert语句中,最好不要使用,可能会造成死锁
如果要用,那么inset的batch大小要小一些,这样单个事务获取的next-key范围就会变小,减少死锁发生的概率
下单操作,需要锁定订单汇总多个商品的库存(手机、鞋子、衣服)
先拿到所有商品的锁
依次进行扣减库存
释放锁
为了避免死锁,可以在对商品加锁的时候,可以通过对购物车中的商品先进行排序来实现顺序的加锁。这样就能有效避免产生死锁的循环等待这一条件
public void createOrder(List orderList) { //定义存放锁的集合 List lockList = Lists.newArrayList(); // 1、对所有购物车中待下单的商品进行排序 Collections.sort(orderList); // 2、对排序好的商品进行依次加锁 try { for (Object order : orderList) { if (order.lock.tryLock(10, TimeUnit.SECONDS)) { //加锁成功 lockList.add(order.lock); } } //3、依次扣减库存 orderList.forEach(order -> order.remaining--); } catch (Exception e) { } finally { lockList.forEach(ReentrantLock::unlock); } }
锁结构如图2.45
1、锁所在的事务信息
不论是表锁还是行锁,哪个事务生成了这个锁结构 ,这里就记录这个事务的id。
2、 索引信息
对于行锁来说,需要记录一下加锁的记录是属于哪个索引的
3、表锁信息
对哪个表加的锁
4、行锁信息
5、type_mode
32位的数,被分成了 lock_mode 、 lock_type 和 rec_lock_type 三部分,如图2.46
1)锁的模式( lock_mode )
2)锁的类型( lock_type )
3)行锁的具体类型( rec_lock_type )
6、一堆比特位
show status like 'innodb_row_lock%';
如果这两个参数值比较大,即等待次数较多,且等待的时间avg较大,则有可能是事务语句可能有问题
SELECT * FROM information_schema.INNODB_LOCKS;
指标1:trx_query:等待锁的sql语句
指标2:trx_rows_locked:锁定的行数
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
指标1: REQUESTING_ENGINE_TRANSACTION_ID: 13845 #被阻塞的事务ID
指标2:BLOCKING_ENGINE_TRANSACTION_ID: 13844 #正在执行的事务ID,阻塞了13845
每行数据有两个隐藏的字段trx_id、roll_pointer
trx_id
就是最近一次更新这条数据的事务id,它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。
roll_pointer,指向了你更新这个事务之前生成的链。隐藏字段组成的undo log版本链如图2.12
1、概念
2、特点
是逻辑上的回滚,不是物流上的回滚
3、作用
4、存储结构
5、回滚段rollback segement中数据类型分类
随时可能作为回滚的恢复内容,所以不能被其他事务的数据覆盖
已提交
mvcc作用:可能有其他事务需要undo log内容来读取之前的行版本信息。所以不能被立即删除,由purge线程来决定什么时候删除
超过了undo retention参数指定的时间,属于过期数据,当回滚段满了之后,会优先覆盖这部分的数据
6、undo 类型
commit时,可直接删除
update
事务在进行 update 或 delete 时产生的 undo log, 不仅在事务回滚时需要(作用1),在Mvcc机制中(作用2)也需要。所以不能随便删除,等待 purge 线程统一清除。
所以,delete语句不要有大事务,会长时间的占满整个事务日志
长时间:不能及时的删除日志
占满:undo log需要记录待删除的数据全部内容,内容很多
7、一条语句的更新过程(redo + undo log)
如何2.43(基本和图2.22一致)
帮我们解决行的可见性问题。分析如图2.13
规则1
当前事务内的更新,能读到
即被访问的trx_id = 视图中的creator_trx_id,说明是当前事务(生成视图的事务)内的更新,能读到
规则2
其他事务已提交,且在视图创建前提交,能读到
- 即被访问的trx_id(其他事务) < 视图中的(当前事务)min_trx_id,表明被访问的事务,在生成该视图之前就提交了(因为不是活跃事务了),所以被访问的事务可以被当前事务读到
- 对应图中的绿色部分
规则3
其他事务已提交,但在视图创建后提交,不能读到
即被访问的trx_id(其他事务) >= 视图中的(当前事务)max_trx_id,表明被访问的事务,在生成该视图之后才开启的,所以被访问的事务不能被当前事务读到
对应图中的红色部分
规则4
被访问的事务min_trx_id =< trx_id < max_trx_id
trx_id ∈m_ids集合中,说明是活跃的事务,还未提交,不可被读取
不在m_ids集合中,说明不是活跃的事务,说明已提交,可被读取
有两个事务并发过来执行,事务A(id=45),事务B(id=59),事务A要去读取这行数据,事务B要去修改这行数据,事务A开启一个ReadView如图2.14
事务A第一次查询(快照读即普通读非当前读)这行数据时,如图2.15
判断被访问的这行数据的trx_id是否小于ReadView中的min_trx_id
事务B开始修改这行数据
事务A第二次查询
被访问的事务trx_id=59,min =<trx_id < max
且trx_id ∈m_ids[45,59],根据规则4,读取不到
此时事务A会根据隐藏字段roll pointer顺着undo log版本链查询之前的版本,于是就会查到trx_id=32的数据,trx_id=32是小于ReadView里min_trx_id=45的,可以查到值仍为A
事务A第一次读 和 第二次读,值都为A,即可重复读。即通过MVCC实现MySQL的可重复读(快照读情况下)事务隔离级别
它们生成ReadView的时机不同
Server层所有引擎都可以使用
伪sql内容如图2.52
主从复制、数据恢复
同redo log一样,也不是直接刷盘。而是采用默认策略(sync_binlog = 0):
如图2.49
因为所有DDL、DML操作事件都有记录(备份)在bin log中。所有可以据此进行数据恢复
执行步骤:
1、flush logs:生成一个新的bin log2专门记录本次数据恢复事件动作,不要影响原有的正常bin log1
2、方式一:通过读取bin log1中的pos位置,来恢复[pos,pos]之间的事件
方式二:通过读取bin log1中的指定世间段内的事件
找打对应事件的pos或时间,准备恢复
3、恢复读取到的内容
/usr/bin/mysqlbinlog --start-position=1 --stop-position=100 --database = 你的db名 /bilog的全路径 | /usr/bin/mysql -uroot -p密码 -v 你的db名;
--start-datetime="2024-01-01 12:00:01" --stop-datetime="2024-01-02 12:00:01"
作用:读写分离、数据备份、高可用性。
1、事务提交后,redo log刷盘完成后,主库把更新操作先记录到bin log中,状态为Prepare
2、从库将主库上的bin log复制到自己的中继日志relay log中,如图2.23
3、从库会读取其中继日志relay log内容,重放执行sql,存于从库数据中
如图2.24
1、散列hash取模
hash算法取模
可解决数据热点问题,但是无法扩容和数据迁移
一旦扩容 %值变了,导致% 值结果也变了,原本6%2 =0,去table0查询数据,现在6%4 = 2需要去table2查询数据,查不到数据
shardingsphere实战
根据order_id取模,order_id%2
= 0,插入表1
= 1,插入表2
2、Range增量
3、group分组(hash+range)
1)hash解决热点数据
如图2.39,3个db、10张table
eg:
2)range解决不易扩容
如图2.40
3)组-库-表结构
如图2.41
按照业务分(订单、库存等)
1)异步复制
主commit后,不 care从库的复制过程和复制结果,直接返回客户端成功。
2)半异步复制
过程如前文图2.22
3)组复制
MySQL5.7.17版本基于Paxos协议推出的MGR复制方案
1、停止向master中写入
2、让slave追上master
3、找到可以成为mater的slaver
4、其他slaver指向新的master,开启新的master写
其他slaver如何指向master,指向的reader_master_log_pos的值如何确定,如图2.48
5、通过pos,slaver确认指向master的pos
参考附件
show index from sellout_warn_sku;
查看索引的区分度,一个索引上不同的值的个数我们称之为“基数”(cardinality)。也就是说,这个基数越大,索引的区分度越好。
“采样统计”。
采样统计的时候,InnoDB默认会选择N个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。
而数据表是会持续更新的,索引统计信息也不会固定不变。所以,当变更的数据行数超过1/M的时候,会自动触发重新做一次索引统计。
在MySQL中,有两种存储索引统计的方式,可以通过设置参数innodb_stats_persistent的值来选择:
force index(idx_a)
如果使用索引a,每次从索引a上拿到一个值,都要回到主键索引上查出整行数据,这个代价优化器也要算进去的。直接在主键索引上扫描的,没有额外的代价。
优化器会估算这两个选择的代价,如果优化器认为直接扫描主键索引更快,就不会走索引a。当然,从执行时间看来,这个选择并不是最优的。所以,可以强制其走索引a
alter table A engine=InnoDB
重建表,主键索引更紧凑,数据页的利用率也更高。这个方案在重建表的过程中,允许对表A做增删改操作
SET max_length_for_sort_data = 16;
如果单行的长度超过这个值,MySQL就认为单行太大,要换一个算法。
long_query_time = 0
捕获所有的查询。几乎没有额外的IO开销
分析工具:pt-query-digest
show table status like ‘表名’ : 可以查看表的基本信息
innodb_change_buffer_max_size : 值表示占用buffer_size整体大小的百分比。更新频繁的表此值可以设置的大一点,减少读盘次数
SHOW variables LIKE ‘log_err%’ 查询错误日志的地址,默认是开启的
sql_safe_updates :执行update和delete语句必须要有where条件
本文主要参考了书籍《高性能MySQL》、 林晓斌老师 的《MySQL实战45讲》、尚硅谷康师傅的《MySQL高级教程》以及网上资料并结合自己工作经验总结而出。理解有误的地方,欢迎批评指正,感谢!
2024-01-31 22:00:00 mjp
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。