1 事务的概念
简单的说,事务就是保证一组数据库操作,要么全部成功,要么全部失败。
MySQL中,事务是由引擎层来实现的。但并不是所有的引擎都支持事务,如原生的MyISAM引擎不支持事务,导致InnoDB逐渐取代MyISAM。
由于数据库一般都是并发执行多个事务,当多个事务并发的增删查改同一批数据时,就会出现脏写、脏读、不可重复读、幻读等问题。这些问题的本质都是数据库的多事务并发造成的,为了解决多事务并发,Mysql数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制等等。
多事务并发操作,不考虑隔离性会产生哪些问题:
- 脏写或更新丢失(Lost Update)
解析:A、B事物同时对一行数据基于原始数据进行更新,事物B的更新对事物A的更新造成了覆盖。带来的问题:数据不准确。 - 脏读.
解析:A事物读到B(其它)事物已修改但尚未提交的数据。带来的问题:
1. 给用户带来数据混乱的感觉
2. 让用户看到根本不存在的数据
- 不可重复读
解析:事物A查询某个数据后,再次进行查询得到不同的结果。带来的问题:
1. 基于查询到的数据直接操作,结果不准确 - 幻读
解析:A事物内的前后2次查询时,后次查询读取到其他事物新增的数据。带来的问题:
1. 读取到其他事物新增的事物
2 隔离性与隔离级别
事务的特性:ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性)
- Atomicity 原子性。事务是一个原子操作单元,其对数据的修改要么全部执行,要么全不执行。(重点落在操作上)
- Consistency 一致性。在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据都必须
- Isolation 隔离性。数据库提供隔离机制。事务间可以保证相对独立。
- Durability 持久性。事务完成后,它对数据的修改时永久性的,即使系统故障也能够恢复。
2.1 mysql各种锁的关系
当事务的隔离级别越严格,mysql存储引擎的执行效率就越低。因此我们在设置隔离级别时,需要在效率与隔离级别间寻找平衡点。乐观锁、悲观锁、表锁、行锁、写锁、读锁等。
- 读锁(所有session都能查,但所有事务都不能有其他操作)
- 写锁(只有当前session能操作数据,其他session都不能操作)
2.2 各隔离级别下操作与锁粒度关系
InnoDB的隔离界界别可以分为4类:
- 读未提交(read uncommitted)
A事务还没提交时,它对数据操作的影响就能被B(其他)事务看到。
- 读已提交(read committed)
A事务提交后,对数据操作的影响结果才能被B(其他)事务看到
- 可重复读(repeatable read)
A事务在执行过程中读到的数据,从获取到视图都结束始终一致。(MVCC机制控制)
- 串行化(serialzable)
锁机制最严格,事务只能串行进行。即使是读数据也会加行锁。
2.3 设置隔离级别
set [ global | session ] transaction isolation level Read uncommitted | Read committed | Repeatable | Serializable;
golbal级别的事务级别无法在session中设置,会报错没有权限。需要在配置项中修改。session中可以设置会话的隔离级别,如下。
#查看目前的事务隔离级别
show variables like '%isolation%';
#设置为读未提交
set session transaction isolation level read uncommitted;
#设置为读已提交
set session transaction isolation level read committed;
#设置为可重复度
set session transaction isolation level repeatable read;
#设置为串行化
set session transaction isolation level serializable ;
#查询所有数据
select * from test;
#查询单行数据
select * from test where id='1';
#插入单行数据
insert into test (id,name) values (5,'zhangsan');
#删除单行数据
delete from test where id='5';
#修改单行数据
update test set name='' where id='1';
结论:各事务隔离级别下,增删改查的锁情况如下。具体说,只有select时(除串行化)不加锁,其余操作都加行锁。
2.4 各隔离级别下操作数据产生的问题
2.5 间隙锁(Gap lock)
mysql的innodb引擎下,默认使用RR(Repeatable-Read)隔离级别。看上去可重复读的数据也会发生幻读。当其他事务新增数据后,在本事务中新增数据,再次查询所有数据,就能查询到幻读数据。为了解决幻读问题,mysql又设计了间隙锁,用于解决RR下的幻读。何谓间隙锁?
如上图,当间隙锁锁住id={3.14]间的数据时,其他事务创建id={3,14]间的数据,会被阻塞。
但我们在实际生产中也会发现这种问题。在对表中不存在的数据进行删除,此时无疑会删除失败,但同时也会带来数据前后范围的间隙锁。来看下面的案例:
库中共计4条数据,开启A、B两个事务,A事务删除id=10的数据,此时无法找到数据,加锁范围扩大为间隙锁。锁住{9,+∞]的数据。
上述描述的是主键索引时的间隙锁,此时库中有该数据的操作加行锁,无该数据的操作加间隙锁。
当操作的数据列不是索引时,即使是select查询也会加入间隙锁。
2.5 临建锁(Next-key-lock)
临键锁是行锁与间隙锁的组合。mysql锁是落在索引上的。当我们对非索引数据进行更新,会导致表锁。(个人理解:mysql无法通过索引对某行进行加行锁找到对应数据,只能加表锁以确定找到该值。)
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为 表锁 。
3 MVCC
在RR级别下,mysql可以做到其他事务提交完成后,我们取到的数据依然不变,这说明无疑我们读取的数据是带有版本的。这其实就是通过undo log的版本链来实现的MVCC(Multi-Version Concurrency Control)。
3.1 分配事务trx_id
mysql在为了并发控制版本,当事务进行增删改查时会为当前事务分配trx_id。我们通过下面实例佐证:
1)我们通过mysql的客户端打开2个session,使用test表并开始事务。此时INNODB_TRX表中空空如也。
2)我们分别通过查询语句、更新语句(增、删语句同理)来查询数据,并且不要提交事务。发现INNODB_TRX表生成了事务的信息,最重要的是生成了事务号xtr_id。
3)当提交事务后,INNODB_TRX表中的数据也将被删除。
3.2 控制视图readview
表中的数据就像长江中滚滚的流水,只要有事务在不断操作数据,大河中的水就在不停流动。那作为A事务,需要精准获取此时河水中的到底有多少鱼怎么办?我先拍张照,看看有多少渔民正在捕鱼,排出掉他们的干扰,剩下的数据就精准了。
read-view数据:
1)当前尚未提交事务的最小值和最大值组成[100,200](数据从本文3.1节中获取)
2)当前已创建的中的最大trx_id,300(为了更好的了解版本比对过程,忽略SessionD的事务trx_id:400)
3.3 回滚日志查询记录过程
- 获取read-view视图 ---[100,200],300
- 查看回滚日志记录
- 比对回滚日志版本链
1)如果回滚日志日志中row的trx_id<trx_active_min_id (简言之:行事务id<活跃事务id的最小值)
当前行的事务已经提交,该行数据可以读到。
2)回滚日志日志中row的trx_id>trx_max_id (简言之:行事务id>活跃事务id的最大值)
当前行的事务尚未开始。
3)前面2条都不符合,那么row的范围区间trx_active_min_id <= trx_id <= trx_max_id 。此时:
a)trx_id在read-view数组[100,150,200]中(假设活跃事务中还有个事务trx_id150,帮助理解)
那么当前事务未提交,不可见。
b)trx_id不在read-view数组[100,150,200]中,那么说明这个事务已经提交,可见。
分析:当读取回滚日志时,首先拿到第一行id=1,name=zwx,trx_id=300的row比对视图,此时属于3.b的范围(100 <= 300<= 300),且trx_id=300,不在read-view数组中[100,150,200],那么此事务已完成,数据可见。读到的结果是该行
3.4 RR级别下的可重复度实现
对上述案例中的事务A添加2个update操作,再次读取数据。由于视图再当前事务的
分析:
读第一行:trx_id=100,属于trx_active_min_id <= trx_id <= trx_max_id,并且trx_id=100,在[100,150,200]数组中,说明这是尚未提交的事务。读下一行。
读第二行:同理。读下一行。
读第三行:trx_id,属于trx_active_min_id <= trx_id <= trx_max_id,并且trx_id=300,不在[100,150,200]数组中,说明事务已经提交。读取到name=zwx。
4 innodb引擎与Buffer pool缓存机制
- 执行器调用InnoDB引擎接口(增、删、改、查)
- 查看buffer pool中是否有对应的row信息
- 存在在跳至第5步,不存在进行第4步
- 从磁盘文件中,加载包含row信息的数据页
- 增删改操作写回滚日志,查操作读回滚日志并结束。
- 更新Buffer pool内存中的值。
- 记账模式写入redo log Buffer
- 写入redolog日志,并将该行状态修改为prepare(第一段提交)
- 准备写入Bin log落盘。
- binlog写入成功,提交redolog状态为commit(第二段提交)