MySQL事务

什么是事务

事务是指将一系列数据操作整体进行统一管理,这些数据操作要么同时成功,要么同时失败;
MySQL只有InnoDB存储引擎支持事务;

事务特性(ACID)

1.原子性(atomicity):事务必须被视为一个不可分割的最小单元,整个事务中的所有操作要么全部提交成功,要么全部失败
2.一致性(consistency):一致性是指事务将数据库从一种一致性转换到另外一种一致性状态,在事务开始之前和事务结束之后数据库中数据的完整性没有被破坏;
3.隔离性(isolation): 一个事务的执行不能被其他事务干扰,并发执行的各个事务之间互相隔离;
4.持久性(durability):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,已经提交的修改数据也不会丢失;

事务并发引发的问题

1.脏读:同一个事务读取到了另外一个事务修改但未提交的数据,被称为脏读;
2.不可重复读:同一个事务内相同的记录被检索两次,且两次得到的结果不一致;
3.幻读:在事务执行过程,另一个事务将新记录添加到正在读取的事务中时,导致同一个事务中第二次读取到了之前读取没有获取到的记录

MySQL中的隔离级别

READ UNCOMMITTED:未提交读 — 有脏读,不可重复读,幻读
READ COMMITTED:已提交读 — 有不可重复读,幻读
REPEATABLE READ:可重复读 — 有幻读(MySQL并没有完全解决)
SERIALIZABLE:可串行化

MySQL保存点

关闭掉MySQL自动提交,可以在MySQL执行语句的过程中设置保存点,然后回滚到保存点;

MySQL隐式提交

即使MySQL关掉了自动提交,在事务执行过程中如果执行了DDL语句(如:创建表,添加字段,删除表),或者再次执行开启事务的语句(begain, start transaction会提交上一个事务)都会发生隐式提交;

MVCC(Multi-Version Concurrency Control - 多版本并发控制)

利用记录的版本链来控制并发事务访问相同记录的行为,这种机制就被称之为多版本并发控制(Mulit-Version Concurrency Control MVCC)。
版本链:
首先需要明确几个概念:
rowId:(没有主键或者唯一键时MySQL会创建的隐藏主键);
trxid:事务ID;
roll_pointer:版本链中指向旧版本undo_log记录的指针;
每次对某行数据进行改动,都会记录一条undo日志,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。每个版本中还包含生成该版本时对应的事务id。

ReadView(读视图)

主要包含4个重要的概念:
m_ids:在生成ReadView时对应当前数据活跃的事务id列表(活跃的事务id列表中记录的未提交的事务id);
min_trx_id:在生成ReadView时对应当前数据活跃的事务中最小的事务id,也就是m_ids中的最小值
max_trx_id:生成ReadView时应该分配给下一个事务的id值;
creator_trx_id:表示生成该ReadView的事务的事务id,执行INSERT、DELETE、UPDATE这些语句时才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0;

不同事务隔离级别下MySQL是如何解决脏读,不可重复读的

不同事务隔离级别下Select语句获取到的ReadView的版本不同;

  1. READ UNCOMMITTED:由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了(所以就会出现脏读、不可重复读、幻读);
  2. SERIALIZABLE:InnoDB使用加锁的方式来访问记录(也就是所有的事务都是串行的,当然不会出现脏读、不可重复读、幻读)
  3. READ COMMITTED:每次查询开始时都会生成一个独立的ReadView,生成该ReadView的版本位当前最小活跃事务id的前一个版本,也就历史正真提交了的版本;(当前活跃的事务id对应的版本都是未提交的)
  4. REPEATABLE READ:同一个事务中,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成;(保证了前后两次查询数据一致,解决了不可重复读);

REPEATABLE READ在大部分情况下是解决了幻读问题的,只有在一些特殊情况下才会出现幻读问题,例如:A事务查询id=1的数据,此时为空(此时连版本链都没有,ReadView是空的),但是此时事务B添加了一条id=1的数据并提交,事务A修改id=1等于的这条数据,然后事务A在读取id=1的数据,因为这时已经存在版本链,所以ReadView不为空,会产生幻读;

redo log

确保事务的持久性
在事务执行之前将操作写入redo log,这样当发生掉电之类的情况时系统可以保证事务的持久性;

undo log

确保事务的原子性
undo log称为撤销日志,当事务变更执行到一半无法完成时,可以根据撤销日志恢复到变更之间的状态来保证事务的原子性

binlog

binlog以事件形式记录了所有的DDL和除了查询语句select外的DML语句的日志,还包含语句执行所消耗的时间;

MySQL崩溃后的恢复

在服务器不挂的情况下,redo日志简直就是个大累赘,不仅没用,反而让性能变得更差。但是万一数据库挂了,就可以在重启时根据redo日志中的记录就可以将页面恢复到系统崩溃前的状态。

崩溃后的恢复为什么不用binlog

  1. binlog会记录表所有更改操作,更改表结构等操作,但redo_log还记录MySQl崩溃前没有提交的事务操作;
  2. redo log是InnoDB引擎特有的,binlog是MySQL的Server层实现的,所有引擎都可以使用;
  3. redo log是物理日志,记录的是”发生在具体数据页上的修改”,直接锁定了数据位置,恢复的速度更快;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如”给ID=2这的c字段加1”,恢复工程需要转化所以速度要慢一些;
  4. redo log是”循环写”的日志文件,redo log只会记录未刷盘的日志,已经刷入磁盘的数据都会从redo log 这个有限大小的日志文件里删除.binlog是追加日志,保存的是全量的日志.如果想要未刷盘的部分数据到内存时,binlog 是无法恢复的,虽然binlog拥有全量的日志,但没有一个标志让innoD 判断哪些数据已经写入磁盘,哪些数据还没有;

MySQL主从复制原理

首先需要明确一下要点:

  1. 我们在搭建MySQL主从复制架构时,首先要保证的就slave节点的IO Thread和SQL Thread必须同时正常启动,不然主从复制的搭建就是有问题的,这两个线程在主从复制过程中起着重要的作用;
  2. MySQL会以事件形式记录了所有的DDL和除了select查询语句外的DML语句的binlog日志;
    然后就是主从复制的具体实现原理:
  3. slave节点会在一定时间间隔内对master节点的binlog进行探测其是否发生改变,如果发生改变,IO Thread会请求master节点binlog中新添加的二进制事件;
  4. 同时master节点为每个IO Thread启动一个dump线程,向从节点发送二进制事件,并保存至从节点的中继日志中(Relay log)
  5. 从节点使用SQL Thread从中继日志中读取二进制事件,在本地进行重放,从而使得从节点与主节点的数据保持一致,
    执行完毕后,IO Thread和SQL Thread进入睡眠状态,等待下一次被唤醒;

主从复制延迟问题的产生

MySQL主库写binlog采用的是追加日志的方式,这里产生的是顺序IO,但是SQL Thread将relay_log中的数据读取出来同步到从机的数据文件中去,这里发生的是随机IO,需要先查找具体操作数据的内存地址,然后再进行修改,这个过程可能比较耗时;且整个过程是单线程执行的;
也就是主要问题点在于:1. 随机IO; 2. 单线程执行;
当然也会有其他一些架构上和外部原因:1. 从库所在的机器性能要比主库所在的机器性能差; 2. 从库读压力过大,查询消耗了大量CPU资源从而影响了同步数据的速度; 3. 网路带宽不是很好,导致同步数据延时;

如果担心binlog落盘的效率,可以将硬盘换成固态;

主库的写操作是顺序写binlog,从库单线程去主库顺序读binlog,从库取到binlog之后在本地执行。mysql的主从复制都是单线程的操作,但是由于主库是顺序写,所以效率很高,而从库也是顺序读取主库的日志,此时的效率受网络因素的影响,但是当数据拉取回来之后变成了随机的操作,而不是顺序的,所以此时成本会提高。

解决主从复制延迟问题

  1. 业务的持久化层的实现采用分库架构,让不同的业务请求分散到不同的数据库服务上,分散单台机器的压力;
  2. 在读多写少的情况下,对于热点数据,可以在业务层和mysql之间加入缓存层,来减少MySQL读的压力;
  3. 使用更好的硬件设备,比如使用更好的CPU或者将硬盘换成固态;

使用GTID来解决主从复制问题

想要从根本上解决MySQL主从复制问题,就需要MySQL主从复制设计上存在的问题出发;
主要存在两个问题:SQL Thread从中继日志中读取二进制事件,然后在本地进行重放;这里主要存在两个问题:1. 随机IO; 2. 单线程执行;
在MySQL5.6版本之后,可以给MySQL加上GTID的配置:
gtid_mode=on
enforce-gtid-consistency=true
当开启gtid配置后,每一个操作语句执行前会添加一个gtid事件,记录当前全局事务id,MySQL会采用多线程分组提交的方式来提高SQL Thread重放的速度,组提交信息被保存在gtid事件中;

MySQL中的锁

MySQL中的锁主要用于解决并发事务问题;

独占锁:排斥其他独占锁和共享锁;
共享锁:排斥其他独占锁,兼容其他共享锁;
意向锁:分为意向独占锁,意向共享锁,只能加在表级别,它的作用是用于标记表中某行是否加了独占锁或者共享锁,而不需要一行一行去做检查;
当事务准备在某条记录上加共享锁时,需要先在表级别加一个意向共享锁
当事务准备在某条记录上加独占锁时,需要先在表级别加一个意向独占锁

MySQL中只有InnoDB存储引擎支持行锁,其他存储引擎都是表锁;

行锁和表锁

行锁:也称为记录锁,顾名思义就是在记录上加的锁。但是要注意,这个记录指的是通过给索引上的索引项加锁。
表锁:对整张表记录进行加锁;

间隙锁

间隙锁(Gap Lock):
1)间隙锁锁定的是索引BTree+叶子节点的前后next指针,这就导致了加锁范围为当前叶子节点的所有索引项;
2)间隙锁可以在一定程度上解决可重复读事务隔离级别中的幻读问题。