事务四大特性(ACID):

原子(Atomicity):

事务不可分割,事务中的SQL语句一荣俱荣一损俱损。

一致性(Consistency):

业务约束,事务操作中或者完成后满足一定业务需求约束。

隔离性(Isolation):

事务之间互相不受影响。

持久性(Durability):

事务执行完成后数据将会持久化、不会无故丢失或变更。

事务五大状态

active --> partially committed --> committed

|

--> aborted --> failed

偷个大佬的图

开启事务

START TRANSACTION READ ONLY/WRITE ONLY/READ WRITE, WITH CONSISTENT SNAPSHOT;
ROLLBACK
SAVEPOINT point_name;
RELEASE point_name;
ROLLBACK TO point_name;
COMMIT
SET autocommit = ON/OFF # 自动提交

事务并发会产生什么问题?

事务并发会产生三大问题:

  • 脏读:某并发事务读到其他处于未提交事务修改的数据。

  • 不可重复读:某并发事务读取同一条记录发现前后数据不一致。

  • 幻读:某并发事务前后读取记录条数不一致。

MySQL Innodb事务隔离级别分别是什么?

Innodb 事务隔离级别有四种:

  • 读未更新:什么也没解决,但并发性能最好。

  • 读已提交:解决脏读。

  • 可重复度 (MySQL 默认级别):解决脏读、不可重复读、部分解决幻读。

  • 串行:解决脏读、不可重复度、幻读.事务串行,操作数据串行且独占互斥,并发性能最低。

MySQL原子性如何实现的?

通过事务来保障的,InnoDB 执行事务失败后会根据undolog 回滚到事务前的数据。

MySQL一致性如何实现的?

这是一个概念,一致性,指的是最终数据满足实际一些列约束,那么算是达成了一致性。通过事务原子性,和事务隔离经过一定过程后实现的一致性。

MySQL隔离性如何实现的?

通过隔离设定各隔离级别,实现的,通过MVCC 快照即使得各事务在预设隔离级别下实现一定程度的互不干扰

MySQL持久性是如何实现的?

通过undolog、redolog、后台刷盘线程实现的数据安全持久化

读已提交和可重复读有什么区别?

  • 读已提交解决了脏读问题,但未解决幻读问题。其并发性能是高于可重复读的。再性能要求比较苛刻是,可再程序业务上引入数据业务悲观锁或者业务乐观锁来避免幻读的发生。

  • 可重复读,加入间隙锁和next-key锁 解决了大部分幻读问题。和读已提交有以下区别:

  • [1] 读已提交为了保证较高的并发性能对应数据行的快照产生每一个查询语句都会产生一个新的快照视图。但对于可重复读,对应的数据快照(read view)只在事务开始是产生一次快照,之后的事务里一直复用这个快照(read view)直到事务结束。

READ VIEW (数据快照)在MVCC 里是如何工作的?

上图是Review 的大致结构,大致分为四个字段:

  • Creator_trx_id(创建快照的事务ID):创建该数据快照的事务ID

  • m_ids: 对于这行记录,在活跃状态未提交的事务ID 列表

  • min_trx_id: m_ids 活跃列表中的最小事务ID

  • max_trx_id: 用于操作这行记录的下一个全局事务ID = max_trx_id + 1

如何工作的? How work?

开始介绍如何工作前,先假设数据库中有这么一条记录, [id,name,blance] = value(1,zklmiao,1000000), 其在独立空间的 InnoDB 数据文件 .idb 存储结构是这样的后后半部分是这样的

  • trx_id : last transcation id, 这条语句最后一条DML事务的ID,查询事务,事务ID为0,不记录到trx_id中。

  • roll_pointer : 指向这个日志 undo log 最新一行历史版本数据,每次对这行记录旧版本记录到undolog中,通过这个指针就可以找到这行记录的历史变更

在执行 read commited/ read repeatable 事务时会创建一个快照Read view, 我们可以把read view 划分为三个部分:

偷个小林哥的图

当在上面这两个隔离级别发生并发事务时,事务需要根据快照确定最后可见版本,基于次版本的记录做对应的事务具体见下面例子:

transcation 1

# trx_id 4
begain
select id, name, blacnce from account;
update account set blance=blance - 23000 where id = 1;
。。。
commit

transcation 2

trx_id 6
begain
select id, name, blacnce from account;
。。。
select id, name, blacnce from account;
。。。

example 1 [read commit]:

transcation 1 和 transcation 2 并发执行

  • [transcation1] trx_id = 4

1.select创建read view:create_trx_id = 4 | m_ids = [4,6] | min_trx_id = 4 | max_trx_id = 6 + 1

2.拿当前trx_id = 4, 和min_trx_id 比较,发现 trx_id <= min_trx_id -- 说明当前记录可能存在其他事务在操作, 查询m_ids, 发现在 m_ids 中,说明 trx_id = 4版本 的记录行不可用于当前事务

3.找到roll_point 沿着指针移动到下一条undolog 历史记录,读取trx_id 比较

4.undolog trx_id = 3, trx_id = 4 > trx_id = 3, 3 不在m_ids 里,可读

5.返回undolog trx_id = 3 的数据{1,zklmiao, 900000}


  • [transcation2] trx_id = 6

1.select 创建read view:create_trx_id = 6 | m_ids = [4,6] | min_trx_id = 4 | max_trx_id = 6 + 1

2.读change buffer account 表,id = 1, 的最新记录行数据 [1,zklmiao,1000000,trx_id=4]

3.拿当前trx_id = 6, 和min_trx_id 比较,发现 trx_id > min_trx_id -- 说明当前记录可能存在其他事务在操作, 查询m_ids, 发现在 m_ids 中,说明 trx_id = 4版本 的记录行不可用于当前事务

4.同上在undolog中沿版本链找到第一个比当前trx_id=6 小的行记录trx_id=3,读取数据返回[1,zklmiao,900000,trx_id=3]。########## ----> 此时 trx_id = 4 提交事务

5.trx_id=6 重复1步骤,create_trx_id = 6 | m_ids = [6] | min_trx_id = 6 | max_trx_id = 6 + 1

6.重复步骤2,最新记录行数据 [1,zklmiao,1000000,trx_id=4]

7.重复步骤3, 发现trxid = 6 > trx_id = 4 查询发现 trx_id =4 不在活跃事务列表中[6] 说明此记录用,返回行记录 [1,zklmiao,1000000,trx_id=4]

********************************** 幻读发生

example 1 [read repeatable]:

transcation 1 和 transcation 2 并发执行

  • [transcation1] trx_id = 4

1.select 创建read view:create_trx_id = 4 | m_ids = [4,6] | min_trx_id = 4 | max_trx_id = 6 + 1

2.拿当前trx_id = 4, 和min_trx_id 比较,发现 trx_id <= min_trx_id -- 说明当前记录可能存在其他事务在操作, 查询m_ids, 发现在 m_ids 中,说明 trx_id = 4版本 的记录行不可用于当前事务

3.找到roll_point 沿着指针移动到下一条undolog 历史记录,读取trx_id 比较

4.undolog trx_id = 3, trx_id = 4 > trx_id = 3, 3 不在m_ids 里,可读

5.返回undolog trx_id = 3 的数据{1,zklmiao, 900000}


  • [transcation2] trx_id = 6

1.select 创建read view:create_trx_id = 6 | m_ids = [4,6] | min_trx_id = 4 | max_trx_id = 6 + 1

2.读change buffer account 表,id = 1, 的最新记录行数据 [1,zklmiao,1000000,trx_id=4]

3.拿当前trx_id = 6, 和min_trx_id 比较,发现 trx_id > min_trx_id -- 说明当前记录可能存在其他事务在操作, 查询m_ids, 发现在 m_ids 中,说明 trx_id = 4版本 的记录行不可用于当前事务

4.同上在undolog中沿版本链找到第一个比当前trx_id=6 小的行记录trx_id=3,读取数据返回[1,zklmiao,900000,trx_id=3]。########## ----> 此时 trx_id = 4 提交事务

5.重复步骤3, 发现trxid = 6 > trx_id = 3 查询发现 trx_id =3 不在活跃事务列表中[6] 说明此记录用,返回行记录 [1,zklmiao,900000,trx_id=3]

*********************************没有发生幻读

总结

Read commited 和 Read repeatable 最大区别在于 Read view 创建上的不同,前者只要是查询语句都会创建一次Read view, 后者则真个事务第一次查询会创建一次Read view 之后整事务都会复用这个Read view, 直到事务结束,释放Read view.

怎么实现的MVCC

同步Read view + undolog 实现的并发访问,通过字段 trx_id 和 m_ids 实现的数据版本控制 -- 即什么隔离级别能读到什么数据。

MVCC是什么?解决了什么问题?实现原理?

  • MVCC:中文全称是多版本并发控制,是一种为了数据库保证数据竞争的并发控制方法。

  • MySQL 中Innodb 中引入了undo log 实现了MVCC,通过数据版本快照的方式解决了在高并发读写场景下数据竞争问题,在特定事务级别下做到了有读写冲突的情况下做到了不解锁-非阻塞的并发读写。

  • 实现原理:数据结构-MySQL INNODB 数据项中 加入了三个隐藏项实现的保本控制,分别是:DB_ROW_ID(隐藏主键,表中没有主键项时默认为该表主键)DB_TRX_ID(最后事务修改事件戳ID,last-modify time)

可重复读隔离级别彻底解决幻读了吗?

没有,只是解决了大部分。具体见下面例子

example 1:

偷个小林哥的图

  • 在事务B插入之后commit

  • 事务A 更新事务B相同 ID的记录 -- 此时undolog buffer 插入一条事务ID 是事务A的历史数据

  • 事务A查询沿着undolog 版本链读取发现第一条可读数据时刚才更新后的版本 -- 发生幻读。

example 2:

后面的当前都会给该函数据加next key lock(record lock + 间隙锁),读取.idb中最新的行数据,所以导致幻觉。

可重复度为什么不能完全避免幻读?什么情况下出现幻读?

  • 因为当前读可以破坏了RR 精心设计MVCC 隔离特性,在innoDB增删改是当前读的,update 会修改当前记录行的trx_id,这点如果被利用的话会卡出幻读bug.还有就是for update 会将select 读取数据为当前数据文件中的数据,不是read view 中的数据,利用这个特性,也可以绕过read view 触发bug. 另外,查询语句select 除非 后面专门加 for update, 否则会给记录加的是 S 共享读锁,这就给其他写事务侵入当前事务操作持有资源区间的机会。

CASE A

一条事务正在查询否个范围的数据,发现没有,这时候另一个事务插进来一条符合条件的数据并提交,事务更新了刚才事务另一个插入事务插入的数据,此时这条数据的事务ID 被改为当前执行更新事务ID的id,再次查询,由于刚才update 的操作,导致 新进来的数据行的数据trx_id 变为了自身!这时候这套数据可见,因此导致了幻读,这个属于骚操作系列了!因为产生幻读的事务毫无理由的更新似乎有点不太符合逻辑。

TRX A

TRX B

Select * from user where age > 20;

Insert into user(id,name,age) value(5,22,'ergou');

Update table user set age = 22 where name = 'ergou';

Select * from user where age >20;

第二次查询时出现幻读

CASE B

一个事务第一次范围查询发现没有符合条件的数据,正打算使用当前度查询数据,此时另一个并发事务插入一条数据并提交,查询事务执行当前读越过了当前事务创建的read view,发现了符合条件的数据行,导致幻读。

TRX A

TRX B

Select * from user where age > 20;

Insert into user(id,name,age) values(5,'ergou',20);

Select * from use where age 20 for update;

针对上面的列子如何解决?

每次select 前加锁,select ... for update;

shi