《MySQL实战45讲》(1-10讲)学习总结
Posted 月亮的-影子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《MySQL实战45讲》(1-10讲)学习总结相关的知识,希望对你有一定的参考价值。
注明:极客时间《mysql实战45讲》个人学习总结
目录
第一讲:基础架构
- 基础架构分为server层和存储引擎层
Server层
- 连接器
- 查询缓存
- 分析器
- 优化器
- 执行器
- 各种函数、存储过程、视图
连接器
- 负责和客户端建立连接,维护和管理连接
- 认证通过之后会取到权限表查询你的权限
- 一旦连接之后那么权限就不会修改,即使现在被管理员修改权限。
- 长连接超过时间就会断开(除非一直有发送查询请求),短连接是经过几次查询之后就断开,尽量使用长连接,因为连接非常耗费时间
长连接如何导致内存溢出?
- mysql的长连接使用的内存都放到连接对象里面,如果连接太久就会导致内存溢出
解决方案
- 可以定时断开长连接
- 如果是5.7以后的版本,可以通过mysql_reset_connection来初始化连接资源。
查询缓存
-
其实就是通过key-value方式来把语句和结果存放,下次来的时候可以时候用查询缓存
-
不推荐使用查询缓存是因为缓存经常失效。只要对表更新,那么缓存就会失效
分析器
- 其实就是对你的语法进行分析,是否符合规范
优化器
- 决定使用什么索引,和join表的顺序
- 即使是同一条查询语句,得到的结果相同,但是经过优化器的处理,join表顺序不同也会导致查询的效率不一样
执行器
- 判断是否有权限,然后执行语句
- 执行的过程
- 通过innodb接口获取一行数据,看看是否符合条件,如果是就加入结果集,不是就跳过
- 重复上面的过程
- 把结果集返回到客户端
存储引擎
- 负责数据的存取和提取,默认是innodb
问题
表T如果不存在k,那么我们查询语句是select * from T where k=1,这个错在哪里报出来?
- 在分析器中报错
第二讲:日志系统
mysql如何恢复半个月前的任意一秒状态?
- 两个重要日志模块redo log和bin log
redo log(粉板)
-
记账(磁盘)的时候肯定是使用账本的成本比使用粉板(redo log)的时候高,粉板写上去就行,记账本还要慢慢翻找
-
WAL(write-Ahead-Logging)写日志再写磁盘,也就是先写粉板
-
每次更新都会先写到redo log,每组4个,一个1GB,每次都是写完然后像一个圆圈循环写
-
每次更新之后mysql空闲就会把redo log的记录更新到磁盘
-
下面的write pos就是写记录,check point就是擦除的位置,他们之间就是可以写的位置。
-
如果mysql发生异常退出仍然可以不丢失这种就是crash-safe
binlog
为什么会有两份日志?
- MyISAM没有crash-safe能力,binlog用于归档
- innodb作为插件带上redo log新的功能完成crash-safe
两个日志的不同?
- redo log 是innodb独有,binlog是mysql的server层实现(所有引擎都能用)
- redo log物理日志(在哪个数据页改),binlog逻辑日志( 给id=2这行c字段+1),其实就是描述物理修改和语句修改
- redo log循环写+覆盖,binlog可以追加写入
update的内部流程
- 执行器从引擎中获取id=2这行,如果数据页在内存就返回,如果不在那么就去磁盘中取
- 获取数据之后,执行器给n加上1,变成n+1,写给引擎
- 引擎更新到内存,并且更新到redo log,这个时候redo log还是prepare等待执行器调用接口去修改
- 执行器生成binlog写入更新记录,并存入磁盘
- 执行器调用引擎接口修改redo log的状态为commit
总结一下实际上就是执行器收到更新的语句,那么就去跟引擎要行数据,引擎先去内存找再去磁盘找返回给执行器,执行器做完更新之后给引擎存回去,这个时候执行器先存这个操作到redo日志,然后再存到binlog日志并且调用引擎接口存入磁盘,最后再次给redo日志确认状态commit。这里的两段提交也保证了日志的一个一致性。不会后面恢复的时候出现恢复错误问题。
那么这里为什么要写两次redo log(两阶段提交)?
- 假设现在不写两次,而且期间会发生crash问题
- 先写redo log再写binlog,写完redo log之后被crash,但是binlog没有修改,恢复的时候能够根据redo log的事务来进行恢复,但是binlog漏了一次
- 先写binlog的话,crash之后binlog记录了,但是redo log没有,恢复运行的时候发现事务无效,但是binlog记录了一个错误的更改,那么下次想要归档的时候就会出现问题,和原库不同。比如现在要c=0变成c=1,但是由于crash,c=1被记录到binlog,但是没记录到redo log,恢复的时候由于redo log没记录到所以事务无效。c还是=0但是binlog记录错误了
问题
定期全量备份什么时候是一天一次比一周一次更好?
- 当你业务有这个需求的时候,0点备份,但是要恢复到昨天23点的数据
- 好处就是恢复时间短,使用的binlog少,占用内存也少
- 一周一备,最坏的情况就是需要备份7天的binlog,备份时间长,应用的内存也多。也就是说如果经常需要使用之前的binlog的时候,这个时候一天一备会更好
- 但是更频繁的全量备份需要的时间也是非常多的。非常消耗性能。
为什么不能够把redo log替代binlog
- 因为redo log只有innodb才有
- binlog能够让所有引擎都可以使用
- 而且redo log是会覆盖的。存储的容量小
第三讲:事务隔离
- 事务是在存储引擎里面进行处理的,不是每个引擎都能处理事务
隔离性
sql的隔离级别
- 读未提交,事务未提交但是修改能被其它事务看到
- 读已提交,事务提交能被其它事务看到
- 可重复读,一个事务看到的数据一定和启动事务的时候是一致的
- 串行化,对于同一行记录需要加上写锁和读锁,而且被加上锁之后后面的时候需要等待当前事务处理完毕。
(串行化和可重复读不同的地方就是比如A查询v1(加锁),那么这个时候B去修改v1,但是阻塞了。需要等待A处理完B才能够继续执行)
- 每次进入一个事务都会有一个视图,对于可重复读那么就是静态的,读未提交是没有视图概念的,因为每次都会发生更新和改变。而串行就直接加锁处理。
- 可重复读的应用场景可以是计算账单表和余额表的统计的时候,不能被干扰
视图
- 可重复读是在事务启动的时候创建,事务过程都是使用这个视图
- 读提交,视图是每个sql语句执行的时候创建
- 读未提交没有视图
- 串行直接加锁
事务隔离的实现
(可重复读为例子)
- 每个事务都会有自己的一个read-view,ABC他们的视图都是不同的
- 如果需要查询A那么就要通过当前的视图回滚到A的情况,也就是一个记录有多个版本(MVCC)
- 那么回滚日志什么时候删除,当没有事务需要使用到这些值的时候那么自然就会删除,什么时候不需要这些日志?当系统没有比回滚日志更早的read-view,那么这个回滚日志就不被需要了。
为什么不要使用长事务?
- 回滚日志和数据字典都存放到ibdata里面,就算日志被清除,但是文件还是不会变小。
怎么避免使用长事务?
从应用开发端看
- 确认是否使用set autocommit=0
- 确认是否有不必要的只读事务
- 控制每个语句的执行时间,set MAX_EXECUTION_TIME
从数据库端来看
- 监控information_schema.Innodb_trx表如果超过阈值就报警
- 输出general_log
事务的启动方式
- begin然后commit或者是rollback
- set autocommit=0变成手动提交(可能会导致长连接问题),如果是=1那么就是自动提交
第四讲:深入浅出的索引(上)
索引的定义
- 相当于就是数据库的目录
索引的数据结构
- 索引常见数据结构,b树、哈希表、有序数组
- 哈希表的结构,数组,然后通过哈希函数计算出数组下标,找到相应的位置,适用于等值查询
- 有序数组效率更高,能够直接定位,而且还能够进行批量的查询,这是哈希表做不到的地方
- 有序数组可以适用于范围和等值查询,缺点即使只能存储静态结构
- 二叉排序树,查询速度是logN
- 还有n叉树,防止树太高导致查询太慢。通过增加每个节点的可查询分支和范围来扩大每层的查询数据。
innodb索引模型
-
索引基本是b+树
-
表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6)
-
建立了主键索引(聚簇索引),也建立了非主键索引(二级索引)
-
主键索引记录了一行数据,非主键索引记录的是主键的值
那么两种索引有什么不同?
- select * from T where ID=500只需要查询主键索引
- select * from T where k=5先查询一次二级索引,然后再查询主键索引,这种叫做回表,也就是说非主键索引查询了两次树
索引维护
为什么使用自增id更好?
- B+树如果插入一个中间的数,比如上面的图中插入一个400,那么就要在300-600的数据页上面去加入新的数据。这个时候可能会导致页分裂
- 页分裂指的是如果数据页已经满了还要插入就需要把一个页分成两个页,比以前多占用了50%的空间
- 但是id自增可以直接在后面直接增加节点和数据页而不是插入产生页分裂问题
- 而且如果使用身份证号作为主键,那么非主键索引的叶子节点占用空间巨大,但是自增id可以解决这个问题,id占用的空间相对来说会比较小
重建索引的问题?
#第一种重建索引
alter table T drop index k;
alter table T add index(k);
#第二种
alter table T drop primary key;
alter table T add primary key(id);
-
重建索引的目的是为了把那些页分裂的数据页空洞补上
-
这里重建索引的过程不合理,但是重建索引是合理的。
-
重建索引会导致表重建(?)
-
最好的方法就是alter table T engine=InnoDB
第五讲:深入浅出的索引(下)
语句的执行顺序
- 先在索引树上面找k=3的记录id=300
- 然后再通过id去主键索引树里面找到对应的R3
- 接着就是k=5去到非索引树里面找到id=500
- 再次去找id=500的R4
- 然后索引k取索引树的下一个值6,不符合结束循环
select * from T where k between 3 and 5
- 上面例子的问题就是经常需要回表(回表的意思就是查询非主键索引之后又去查询主键索引树)那么要如何解决?
覆盖索引
-
如果语句改成select id from T where k between 3 and 5那么这个时候就不需要去查询主键索引树下面的R3。也就就是不需要回表了。
-
索引k如果覆盖了查询的需求,就是查询多少个字段,索引k能不能包含这些字段,如果能够包含那么就是覆盖索引,覆盖索引不需要进行回表
-
实际上引擎是读取了k=3、5、6但是对于server层来说就是只需要范围里面的3和5
问题
在一个市民信息表上,是否有必要将身份 证号和名字建立联合索引?
- 如果根据身份证来查询姓名而且是一个高频操作那么就是有必要的。这样可以减少回表。
- 如果索引只是记录了身份证(叶子节点记录数据),那么查询姓名就需要通过主键索引
那么又来一个问题,是不是每个查询都需要建立索引?
最左前缀
- 可以通过最左前缀来建立联合索引(a,b),那么就不需要去为a特别地建立一个索引
- 而且为了保证单独来查b的时候那么就可以给b来单独建立索引(选择占用空间小的那个来单独建立),因为b必须要在a相同的时候才能够利用索引来进行查询
索引下推
select * from tuser where name like ‘张 %’ and age=10 and ismale=1;
- 上面语句直接会先查询到’张’开头的所有数据
- 5.6之前张后面的数据找到IDX都需要进行回表来在主索引上面去查询其他数据
- mysql5.6之后能够先对比条件先,过滤不符合条件的那些对象,然后再进行回表来进行对比找到数据行。比如这里的age一定是=10的时候才会进入回表
- 这种的意思其实就是在索引内部先判断完,过滤条件之后剩下的那些主键才能够去回表
总结
- 建立联合索引的好处就是能够快速定位一部分的数据,不至于造成全表扫描的问题。
- 有了最左前缀原则,能够减少一些索引的创建,最左边的几个字段是不需要再次建立索引的。
第六讲:全局锁和表锁
分类
- 全局锁
- 表锁
- 行锁
全局锁
- 锁定整个数据库 flush tables with lock(FTWRL)
- 在做全局逻辑备份的时候需要使用到这个锁
- 不能够进行更新和插入
- 对于一些事务存储引擎是可以使用single_transaction,意思就是可以使用一致性视图来保证备份的一致性。数据可以更新,这个时候可以通过mvcc来获取需要备份的数据,因为对于这样的一个全局备份也是可以通过mvcc来找到需要备份的版本就不需要锁了。
应用场景
- 全库逻辑备份
- 但是备份期间停止业务,而且不能够接收主库同步过来的binlog,导致主从延迟
问题是如果不加锁会怎么样?
- 下面图的意思其实就是现在先备份余额,但是course没有备份,而现在备份的余额是还没有购买之前的,接着用户还能够去购买这个书,购买之后余额是101,然后才开始备份course,course备份过去就是已经购买了这本书,问题就是备份的数据是余额200和买了一本书,正常来说买了一本书不是应该是101余额吗,如果数据库需要恢复到这个状态,那么图书软件就亏钱了。如果反过来备份顺序那么就是用户亏了。
- 不加锁就会导致这样的一个多线程问题,备份和购买同时进行,但是备份并不是一个原子操作,可能会导致被购买的一些操作交错进行,导致的数据备份错误问题
- 其实也是也是一个逻辑备份的时间点出错问题,由于MVCC的原因,备份的视图不是同一个逻辑视图。由于MVCC的存在那么数据就能够持续更新,备份的视图不加上锁就会导致备份的视图出现误差。
不是可以通过可重复读来解决视图不一致的问题吗?
- 但是有的存储引擎不支持事务,那么每次备份的时候都会视图更新,数据发生变化。
- single-transaction只适用于支持事务的引擎
那么为什么不使用set global readonly =true只读模式?
- 有的系统根据readonly来决定数据库是主库还是从库
- 而且如果中间发生与客户端断开连接,锁是可以自动释放的,但是readonly是不会自动变成false的,导致数据库有一段时间不能够写。锁可以处理异常但是readonly不能
表级锁
- 表级锁和元数据锁(meta data lock)
语法
- lock tables 。。。 write/read
- 上了写锁其它线程不能对他读写,上了读锁就不能够对他写(自己也不能够修改,只能读)。
MDL
- 系统默认加上,防止两个线程一个线程在读,另一个线程在修改表的结构,导致读的线程的数据出现错误
- 每次读写的时候都会默认加上mdl锁,防止中途被修改
- 主要就是为了保护读写表的时候表的数据结构不会发生变化,每次对表增删改的时候需要加上MDL读锁,这是一个表级的锁,如果要修改结构那么就使用一个写锁
如果给小表加上字段会怎么样?
- 下图sessionA需要MDL读锁,B也是需要读锁,但是C需要写锁,所以C被阻塞了,而且要等待sessionA和B完成查询完成,但是sessionC请求的是写锁,那么就会导致等待在sessionC后面的所有读操作都会被阻塞,万一前面的读写很长时间,而且查询非常频繁就会导致查询线程越来越多,特别是尝试读超时之后开启新的session来读的时候
那么怎么安全给小表加上字段?
- 解决长事务,就是去到innodb_trx表上面看看有没有占用了mdl写锁的长事务,把它清除
- 如果真的要改了,那么给等待读锁的这个alter操作设置超时时间,防止阻塞后面的业务
如果在备份期间主库binlog传来一个DDL语句会怎么样?
这里分类4个时刻
- 开始备份事务,并且拿到一致性视图
- 时刻1设置了保存点
- 时刻2拿到表的结构
- 时刻3拿到了数据
- 时刻4回滚并且释放MDL锁(用于修改每一行的结构)
- 如果是时刻1到来那么是没有关系的,因为时刻2才会改变
- 如果是时刻2到来,表的结构被改变,自然数据也就是不对的,所以这里的备份将会被终止
- 如果是时刻3到来,数据查询的期间是占据这个MDL锁的,所以修改binlog被阻塞,造成主从的binlog延迟。
- 时刻4释放之后,那么就可以正常修改。但是备份拿到的是DDL之前的。
这里对每个时刻到来都会有不同的影响,原因就是是否拿到MDL锁,表结构修改之前有没有已经备份好数据和表结构。如果没有那么就要停止备份,如果是有那么就要阻塞binlog的到达,迟一点再进行修改。关键其实就是能不能够在拿到数据和表结构之前改变表的结构,如果能那么那么就要停止备份防止备份出现问题,如果不能那么就要阻塞binlog的更新。
Q1:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Q2:START TRANSACTION WITH CONSISTENT SNAPSHOT;
/* other tables */
Q3:SAVEPOINT sp;
/* 时刻 1 */
Q4:show create table `t1`;
/* 时刻 2 */
Q5:SELECT * FROM `t1`;
/* 时刻 3 */
Q6:ROLLBACK TO SAVEPOINT sp;
/* 时刻 4 */
/* other tables */
第七讲:行锁功过
两段阶段锁协议
- 行锁在需要的时候会加上,事务结束之后才会释放
知道这个规则的作用?
- 如果顾客A在电影院B在这购买电影票,那么更新的有三个东西
- A的余额
- B的购买票记录
- 记录
- 由于B是最容易发生频繁插入的,所以最好把2放到最后,因为2加上行锁之后就必须等到事务结束之后释放,那么原则就是让事务持有B的行锁的时间尽可能少。你放语句2到最后那么持有行锁之后就能够立即释放了,如果是放到最前面,还需要等其它语句执行完毕才能够释放
死锁和死锁检测
- A拿了id=1的行,B拿了id=2的行,那么B要等A的id=1的资源,A要等B的id=2的资源造成了死锁
解决策略
- 设置等待超时时间,这种的问题就是太长导致响应差,太短可能误伤,innodb_lock_wait_timeout
- 回滚造成死锁的事务,让另一个事务能够继续执行,死锁主动检测,问题就是每次线程执行事务的时候都要去看看资源有没有被其它线程锁住。如果是1000个进来更新同一行,那么就要循环遍历100w多次。innodb_deadlock_detect->on开启检测功能
那么怎么解决死锁检测压力大的问题?
- 可以限制查询的线程,也就是限制并发度
- 另一种方法就是可以把记录进行拆分,拆成多条记录,那么修改的时候就能够并发度更高
- 减少死锁的方向就是减少并发度和冲突率
问题
删除表里10000行数据,以下三种方法
- delete from T limit 10000;
- 在一个连接中循环20次delete from T limit 500;
- 20个连接中delete from T limit 500;
- 对于第一个如果并发度太高,问题就是阻塞时间非常长,导致用户体验很差。
- 对于第二个能够稍微缓解,因为每次删除的量都改变,并且释放行锁给其它业务
- 第三种问题多个连接同时取更改同一行最后导致的问题就是人为的锁问题
总结
- 利用两阶段协议原则,解决锁等待问题,把使用锁频率高的语句放到事务的最后
- 解决死锁问题要保证检测的时间不能太短也不能太长,控制并发量。防止误判导致CPU利用率不高,经常需要解除某些事务。
- 死锁检测消耗的性能主要是在检测一个事务的时候顺带检测其它所有事务获取锁的情况,导致其实有时候没有死锁的正常等待都被误判,而且检测花费大量CPU的时间。
- 死锁检测三种策略
- 超时时间设置
- 死锁检测
- 控制并发度,减少死锁的产生。
第八讲:事务到底是隔离还是不隔离的
问题是这里的A输出是1而B是3这是为什么?
先说说概念
- MVCC有两个视图,针对RC(读提交)和RR(可重复读),视图的目标就是定义事务到底能看到什么数据
- 关于快照视图,这里并不是把所有的数据拷贝下来,而是通过记录活跃事务的id列表,最大和最小的活跃事务的id,还有当前事务的一个id。来决定当前事务到底能够看到版本链上面的哪个版本的数据,就是利用这个特点来快速创建视图快照
- 但是查询和更新在MVCC的工作机制中是有区别的
mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL,
`k` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2)
快照如何在MVCC中工作?
-
首先是每个事务都有自己的一个id,而且是严格遵循自增的,这些id会赋值给当前事务看到的数据的事务id。相当于就是给事务看到的read-view(数据版本)赋值自己的事务id
-
这里的更新的回滚日志就是u1、u2、u3,但是v1、v2、v3数据版本并不是物理存在的,而是需要通过undo log也就是回滚日志的操作来返回到这个值
快照是如何定义的
- 事务启动的时候会根据自己的赋值的id找到对应的版本
- innodb会创建一个事务数组记录活跃事务,就是启动但是没有提交的事务
- 低水位就是数组的最小事务id,高水位就是数组事务id最大+1
- 一致性视图就是事务数组+高水位组成
- 数据的可见性规则是根据事务id和事务数组来对比找到的
下面的分析必须基于事务id进行分析,同时还要基于行锁
- 绿色部分肯定就是可见的,事务启动之间其它事务提交了,那么数据已经更改对于当前事务是可见的,或者是自己生成和修改的。包括了提交的和自己的
- 红色部分都没有开始肯定是不可见的
- 黄色这里是最特殊的,当前事务就是在这个集合里面,里面记录了各种数据版本,这里的row trx_id不仅仅包括活跃的事务数组中的id,包括了已经提交事务的id。
- 如果事务id在这个数组中,说明这个版本的数据是不可见的,也就是未提交的数据
- 但是事务id如果不是在活跃事务数组中,那么说明这个数据版本是在当前事务执行过程中,另一个事务提交并修改数据
比如,上面事务的低水位是18,那么自然就要去到已提交事务上面的数据版本寻找,那么就是17了。这里的规则要记住,事务id是递增的,低水位和高水位+活跃事务数组是每个事务都会有的,这个是innodb设置的。
- 这里可以这么区分,每次启动事务之前都会创建当前事务的一个数组,而且对应的数据版本在undo log中(包括事务id),每次要知道数据是否可见,那么就要根据自己的那个事务数组去到undo log来查找,如果同样是在同一个活跃数组的id那么这些数据就是不可见的,如果是和当前id相同或者是比低水位低的就是可见的,如果是比高水位还高就是还未执行的事务,是不可见的。
回到上面的问题,结合高水位低水位和活跃事务数组来分析一下A
99是一开始就存在的活跃事务
- 对于A来说这里的活跃事务id数组是[99,100]
- 这个时候C首先去修改了k,并且binlog记录下这个数据版本和事务id
- 接着就是B去更新,记录下最新的版本(1,3)和事务id是101(这里可以先忽略,下面更新再谈为什么)
最后A查询的结果是(1,1)也就是90的版本这是为什么?
- 首先你看看A的活跃数组是[99,100]那么A去查询数据版本,低水位是99,高水位是100,并且拿出自己的id去对应发现,101当前版本大于自己的id=100,那么就是101就属于上面的红色部分(没有提交,而且已经超出A的高水位100了),不可见。
- 那么再往上查102,还是超出高水位,不可见
- 最后是90,很明显就是比低水位还低,说明这个事务肯定就是已经提交了。(分析这里已经要去参照上面的逻辑思维,事务是遵循id递增的,如果比低水位还低说明事务不是活跃事务)
情况分类
- 没有提交,不可见
- 提交但是在视图(就是那个活跃数组)创建之后,不可见
- 视图创建之前提交,可见
更新逻辑
- B这里为什么是3呢?原因就是更新的时候会执行一次当前读,也就是会读当前的版本102,所以就算是102是在B建立视图之后提交的,但是还是会被B读取,并且防止更新覆盖
如果事务C还没有提交呢?
- 那么两阶段锁协议就生效了,在事务没有完成之前,C是一直会锁住id=1这一行,也就是原本B可以通过当前读来获取当前的数据版本,但是由于C获取了写锁,导致B不能够读取数据。等待C释放之后才可以获取数据。防止如果c进行了回滚导致最后B修改的数据被覆盖了。
- 所以这里就把一致性读(A) ,当前读(B),行锁都联系起来了,还有就是两段锁协议
事务可重复读的能力是怎么实现的?
- 一致性读+当前读+锁机制
那么读提交和可重复读的区别?
-
可重复读是事务启动前创建视图
-
但是对于度提交来说就是每行语句开始前都需要创建视图
-
这里很明显就是创建视图的时机不同,A创建视图的时机是在B和C执行之后,但是B未提交,C已经提交,所以能够看到C的,但是看不到B的。
问题
update set c=0 where id=c设计一个场景让c最后无法修改?
- 这种其实就是两个事务,事务B把c给改了提交,那么A更新当前读自然就id=c不是正确的。
- 下面这种很明显就是sessionB是在A之前的一个事务,但是A最后还是能够看到B的一个修改,原因还是因为更新的时候需要当前读,根据以前的规则B就是在黄色区域里面没有提交但是做了修改,应该是对A是不可见的。
事务启动拍的快照是什么?
- 就是启动前的数据的快照
- 之所以能够保证快照并不是很大,就是因为他只是保存了当前的事务和活跃事务的数组,并且根据数据数组和自己的transactionid来找到可见和不可见的数据,其中低水位和高水位是非常重要的概念。
事务如何实现MVCC
- 每个事务都有transactionid(递增)
- 而且事务开启之前会有一个up_limit_id也就是最大的已经提交事务
- 更新的时候,会把之前数据和事务id的存入undo log,更改这条记录的事务id给当前行赋值
- 再定一个规矩就是查看数据一定是up_limit_id<trasaction_id的时候才能够去undo log找,如果up_limit_id>=transaction_id那么就是可见的。
从这个层面去理解也可以说是
- RR可以实现重复读的原因就是因为不会每个语句都去更新up_limit_id但是RC每次都会进行更新,也就是更新read-view这两个时机和概念非常相似。
- RR不仅仅只是有行锁,而且还有对视图的一个锁定和静态作用,但是RC没有对视图进行一个固定。
还有最后一个就是行锁的概念
- 就是上面的如果update没有提交,那么下一个使用要update那么就只能够进行等待别的事务把行锁释放。、
补充MVCC的理解
- 实际上就是一条undo log的链,来把各种版本的数据存储到了一起。
- 对于一个undo log 数据是否可读关键在于当前数据的事务id所在的位置。关于黄色区域这种的两种情况解释
- 第一种就是事务id存在于当前事务的活跃事务id中,那么就是不可见的,因为还没有提交
- 第二种就是事务id不在这活跃数组里面,说明其它事务已经修改数据并且在本事务开启之前提交。那么也是能够可见的(也就是在当前事务启动之前,创建了当前事务数组,然后数组里面的事务刚好在这个时候修改提交)
为什么表结构不能够使用可重复读?
- 表结构没有行数据,索引和记录都是分开的。而且没有trx_id,思考一下可重复读的关键是什么?关键就是通过一致性视图,然后对应每一行数据的一个版本判断是否可读,如果记录连这个trx_id都没有那么就无法进行判断。这其实也说明一点为什么表结构并不支持事务。从支持表级的引擎的记录结构来看,表级并不能使用mvcc。而事务的使用就是需要一致性视图+版本链。
第九讲:普通索引和唯一索引
第九讲:普通索引和唯一索引
下面语句适合是用唯一索引还是普通索引?
查询过程
- select name from CUser where id_card = ‘xxxxxxxyyyyyyzzzzz’
- 对于普通索引来说查找到值之后就会一直往后面找直到不符合条件
- 对于唯一索引来说找到之后就不会再搜素了
- 性能差距不大,引擎是把数据页读取到内存来进行查找的。而不是一条记录来查找的。所以可能会出现记录刚好在最后的一个位置,导致查询下一条记录的时候需读取下一个数据页
更新过程
change buffer的作用
普通索引
- 更新如果数据页不在内存,那么先把更新操作存放到change buffer,等待下一次查询数据的时候就会把数据页取出来然后更新。如果在内存就直接更新了。change buffer的作用就是下次查询的时候把加载进内存的数据页进行更新保证数据的一致性。而且能够不那么频繁地IO操作。(缓存更新操作)因为每次更新都需要把数据页读入内存,占用内存很大的空间,change buffer就可以减少这种内存的占用。
- 而且还能在数据库关闭之前有后台线程帮助把未更新的操作全部merge执行
什么条件下可以使用change buffer
- 对于唯一索引来说需要知道更新数据是不是唯一,那么就需要取出数据来进行对比。所以唯一索引是没有必要使用change buffer的。
- 但是普通索引没有这样的限制所以可以使用
两个场景
- 第一个是数据在内存的时候
- 对于唯一索引对比一下是否唯一,然后插入
- 对于普通索引插入
- 第二个就是数据不在内存的时候
- 对于唯一索引由于需要对比是否唯一,那么先取出数据页,然后再判断是否插入
- 对于普通索引还是直接插入操作记录就好了
所以在第二种场景的唯一索引change buffer已经没有必要了。大小限定是innodb_change_buffer_max_size
change buffer什么时候都可以使用吗
- 如果更新之后查询频繁,那么change buffer需要维护成本,反而是一个累赘
- 如果是写多读少的情况是非常适合的,比如日志、账单等。
- 通常是普通索引+change buffer的一个应用
change buffer和redo log 的比较
从目的上来说change buffer解决读IO的时候的更新,redo log解决的是写IO的时候的更新。意思就是change buffer关键是解决查询的时候对数据页的更新,但是对于redo log更多是处理系统崩溃之后对数据页的一个恢复,如果数据页已经更新到磁盘并且事务已经提交,那么这个redo log也可以被覆盖了。
- change buffer记录的是更新操作
- redo log记录的是每一条更新的记录到内存,然后再同步到磁盘。
- binlog记录的是server层也就是逻辑语句,但是redo log记录的是数据页上面的修改
- page1直接更新内存
- 接着就是在cg里面插入我要更新page2
- 上面两个动作都放到了redo log
- 虚线是后台操作
- 下面其实就可以看出redo log记录了change buffer的变化,主要就是用于系统崩溃后恢复,但是change buffer就是处理查询数据页的时候在内存更新数据页,
- 等到数据页2加载进来的时候redo log已经没有什么跟change buffer有所关联了。而且change buffer减少随机读IO的意思其实就是,每次修改不同的记录可能会在不同数据页,那么每次都需要去取数据页,数据页之间可能间隔很远导致随机IO,随机IO是非常慢的。但是有了change buffer就可以暂时先不取数据页更新,而是等待需要查询使用的时候再进行更新
问题
change buffer如果系统断电之后怎么办?
- 保存到change buffer这个动作其实会保存到redo log,所以最后并不会出现这样的问题。
总结
- 唯一索引和change buffer不能使用的场景是查询特别多的时候,因为唯一索引需要验证数据页里面没有相同的记录。导致最后的change buffer失效
第十讲:MySQL为什么有时候会选错索引?
问题这个如果先插入10w条数据,然后B 把数据删除之后再次插入,那么索引会不会错选?
- 答案就是没有使用到索引a。
- 而出现异常的原因就是因为开启了sessionA的事务
那么到底是怎么回事?
-
mysql错选一定是判断出错,这部分是优化器做的。选择是否使用索引包括查询的行数、临时表、是否排序
-
原因就是mysql认为扫描10w行主键索引直接获取数据比扫描普通索引3w行还要回表的速度更快,但实际上就是普通索引更快。
-
analyze table t可以通过这个语句来进行修复
- 很明显这两个语句,第一个语句是没有问题的,但是对于第二个语句来说,他比上面同样用了索引a的搜索rows就差很多。导致最后mysql出现错误
- 下面是基数表
select * from t where a between 10000 and 20000; /*Q1*/
select * from t force index(a) where a between 10000 and 20000;/*Q2*/
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`),
KEY `b` (`b`)
) ENGINE=InnoDB;
- 对于下面语句先b的原因就是因为认为b需要排序,如果使用a那么就会产生文件排序,导致最后的性能很差。但是实际上在这个地方a索引会更快,因为b扫描的行数和回表的成本比仅仅b的文件排序成本大的多。
- 对于下面语句为什么选a的原因就是因为现在a和b都需要排序,所以选择a扫描的行数更少。而且文件排序的成本更小。
对于下面的语句又应该选谁?
- 我一开始的想法是选b,但是如果考虑到回表问题,那么就应该选a,a可以快速定位这10000个,然后b可以通过主键索引直接去扫描表速度比回表更快
select * from t where (a between 1 and 1000) and (b between 50000 and 100000) o
索引选择异常和处理
- 强行选择一个index,force index,但是变更不及时
- 重新analize table。让它统计索引的查询行的信息。
问题
sessionA没有提交为什么会造成mysql的错误判断?
- 因为A没有提交,导致上次的数据并没有真的被删除,那么索引就会把那些数据也计算上导致最后计算出来的状态错误。数据多了也就是索引扫描的列变多了。
- 但是主键也没被删除啊,为什么主键没被计算上去?因为计算主键的时候只会去计算表的行数。但是被删除了已经标记上所以并不会使用
以上是关于《MySQL实战45讲》(1-10讲)学习总结的主要内容,如果未能解决你的问题,请参考以下文章
MySQL实战45讲 第一课总结&My SQL VS SQL Server