1 事务的概念

简单的说,事务就是保证一组数据库操作,要么全部成功,要么全部失败。

MySQL中,事务是由引擎层来实现的。但并不是所有的引擎都支持事务,如原生的MyISAM引擎不支持事务,导致InnoDB逐渐取代MyISAM。

由于数据库一般都是并发执行多个事务,当多个事务并发的增删查改同一批数据时,就会出现脏写、脏读、不可重复读、幻读等问题。这些问题的本质都是数据库的多事务并发造成的,为了解决多事务并发,Mysql数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制等等。

多事务并发操作,不考虑隔离性会产生哪些问题:

  • 脏写或更新丢失(Lost Update)
    解析:A、B事物同时对一行数据基于原始数据进行更新,事物B的更新对事物A的更新造成了覆盖。带来的问题:数据不准确。
  • 脏读.
    解析:A事物读到B(其它)事物已修改但尚未提交的数据。带来的问题:
    1. 给用户带来数据混乱的感觉
    2. 让用户看到根本不存在的数据

mysql 事务传播行为 mysql 事务传播机制_数据

  • 不可重复读
    解析:事物A查询某个数据后,再次进行查询得到不同的结果。带来的问题:
    1. 基于查询到的数据直接操作,结果不准确
  • 幻读
    解析:A事物内的前后2次查询时,后次查询读取到其他事物新增的数据。带来的问题:
    1. 读取到其他事物新增的事物

2 隔离性与隔离级别

事务的特性:ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性)

  • Atomicity 原子性。事务是一个原子操作单元,其对数据的修改要么全部执行,要么全不执行。(重点落在操作上)
  • Consistency 一致性。在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据都必须
  • Isolation 隔离性。数据库提供隔离机制。事务间可以保证相对独立。
  • Durability 持久性。事务完成后,它对数据的修改时永久性的,即使系统故障也能够恢复。

2.1 mysql各种锁的关系

当事务的隔离级别越严格,mysql存储引擎的执行效率就越低。因此我们在设置隔离级别时,需要在效率与隔离级别间寻找平衡点。乐观锁、悲观锁、表锁、行锁、写锁、读锁等。

mysql 事务传播行为 mysql 事务传播机制_数据_02

  • 读锁(所有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时(除串行化)不加锁,其余操作都加行锁。

mysql 事务传播行为 mysql 事务传播机制_数据库_03

  2.4 各隔离级别下操作数据产生的问题

mysql 事务传播行为 mysql 事务传播机制_数据_04

  2.5 间隙锁(Gap lock)

mysql的innodb引擎下,默认使用RR(Repeatable-Read)隔离级别。看上去可重复读的数据也会发生幻读。当其他事务新增数据后,在本事务中新增数据,再次查询所有数据,就能查询到幻读数据。为了解决幻读问题,mysql又设计了间隙锁,用于解决RR下的幻读。何谓间隙锁?

mysql 事务传播行为 mysql 事务传播机制_数据库_05

如上图,当间隙锁锁住id={3.14]间的数据时,其他事务创建id={3,14]间的数据,会被阻塞。

但我们在实际生产中也会发现这种问题。在对表中不存在的数据进行删除,此时无疑会删除失败,但同时也会带来数据前后范围的间隙锁。来看下面的案例:

库中共计4条数据,开启A、B两个事务,A事务删除id=10的数据,此时无法找到数据,加锁范围扩大为间隙锁。锁住{9,+∞]的数据。

mysql 事务传播行为 mysql 事务传播机制_数据库_06

上述描述的是主键索引时的间隙锁,此时库中有该数据的操作加行锁,无该数据的操作加间隙锁。

当操作的数据列不是索引时,即使是select查询也会加入间隙锁。

 2.5 临建锁(Next-key-lock)

临键锁是行锁与间隙锁的组合。mysql锁是落在索引上的。当我们对非索引数据进行更新,会导致表锁。(个人理解:mysql无法通过索引对某行进行加行锁找到对应数据,只能加表锁以确定找到该值。)


InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为 表锁


mysql 事务传播行为 mysql 事务传播机制_数据库_07

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表中空空如也。

mysql 事务传播行为 mysql 事务传播机制_mysql_08

 2)我们分别通过查询语句、更新语句(增、删语句同理)来查询数据,并且不要提交事务。发现INNODB_TRX表生成了事务的信息,最重要的是生成了事务号xtr_id。

mysql 事务传播行为 mysql 事务传播机制_java_09

 3)当提交事务后,INNODB_TRX表中的数据也将被删除。

mysql 事务传播行为 mysql 事务传播机制_数据库_10

 3.2 控制视图readview

表中的数据就像长江中滚滚的流水,只要有事务在不断操作数据,大河中的水就在不停流动。那作为A事务,需要精准获取此时河水中的到底有多少鱼怎么办?我先拍张照,看看有多少渔民正在捕鱼,排出掉他们的干扰,剩下的数据就精准了。

read-view数据:

1)当前尚未提交事务的最小值和最大值组成[100,200](数据从本文3.1节中获取)

2)当前已创建的中的最大trx_id,300(为了更好的了解版本比对过程,忽略SessionD的事务trx_id:400)

mysql 事务传播行为 mysql 事务传播机制_数据库_11

  3.3 回滚日志查询记录过程

  • 获取read-view视图      ---[100,200],300
  • 查看回滚日志记录

mysql 事务传播行为 mysql 事务传播机制_数据库_12

  • 比对回滚日志版本链

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级别下的可重复度实现

mysql 事务传播行为 mysql 事务传播机制_数据_13

 对上述案例中的事务A添加2个update操作,再次读取数据。由于视图再当前事务的

mysql 事务传播行为 mysql 事务传播机制_mysql_14

 分析:

读第一行: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缓存机制

  1. 执行器调用InnoDB引擎接口(增、删、改、查)
  2. 查看buffer pool中是否有对应的row信息
  3. 存在在跳至第5步,不存在进行第4步
  4. 从磁盘文件中,加载包含row信息的数据页
  5. 增删改操作写回滚日志,查操作读回滚日志并结束。
  6. 更新Buffer pool内存中的值。
  7. 记账模式写入redo log Buffer
  8. 写入redolog日志,并将该行状态修改为prepare(第一段提交)
  9. 准备写入Bin log落盘。
  10. binlog写入成功,提交redolog状态为commit(第二段提交)

mysql 事务传播行为 mysql 事务传播机制_java_15