MySQL事务原理

一、事务

目的

事务将数据库从一种一致性状态转换为另一种一致性状态;

组成

事务可由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成;单条语句默认有事务。

特征

在数据库提交事务时,可以确保要么所有修改都已经保存,要么所有修改都不保存;

事务是访问并更新数据库各种数据项的一个程序执行单元。

在 MySQL innodb 下,每一条语句都是事务;可以通过 set autocommit = 0,设置当前会话手动提交,这样只每个SQL语句或者语句块所在的事务都需要显式"commit"才能提交事务;(autocommit默认是1)

事务控制语句

-- 显示开启事务
START TRANSACTION | BEGIN     # 会挂起自动提交模式
-- 提交事务,并使得已对数据库做的所有修改持久化
COMMIT
-- 回滚事务,结束用户的事务,并撤销正在进行的所有未提交的修改
ROLLBACK
-- 创建一个保存点,一个事务可以有多个保存点
SAVEPOINT identifier
-- 删除一个保存点
RELEASE SAVEPOINT identifier
-- 事务回滚到保存点
ROLLBACK TO [SAVEPOINT] identifier

【】在事务中加锁,会在提交或回滚后释放。

二、ACID特性

每个数据库都要分析其ACID特性。

原子性(A)

事务操作要么都做(提交),要么都不做(回滚);事务是访问并更新数据库各种数据项的一个程 序执行单元,是不可分割的工作单位;通过 undolog 来实现回滚操作。undolog 记录的是事务每 步具体操作,当回滚时,回放事务具体操作的逆运算。

同时, undo log 也是 MVCC(多版本并发控制)实现的关键。undo log存储在共享空间。

隔离性(I)

事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,并发事务之间不会相互 影响,设定了不同程度的隔离级别,通过适度破环一致性,得以提高性能;通过 MVCC 和 锁来实 现;MVCC 时多版本并发控制,主要解决一致性非锁定读,通过记录和获取行版本,而不是使用 锁来限制读操作,从而实现高效并发读性能。锁用来处理并发 DML 操作;数据库中提供粒度锁的 策略,针对表(聚集索引B+树)、页(聚集索引B+树叶子节点)、行(叶子节点当中某一段记录 行)三种粒度加锁;

持久性(D)

事务提交后,事务DML操作将会持久化(写入 redolog 磁盘文件 哪一个页 页偏移值 具体数 据);即使发生宕机等故障,数据库也能将数据恢复。redolog 记录的是物理日志。

redo log解决更新磁盘数据的性能问题(顺序IO和随机IO)。

一致性(C)

一致性指事务将数据库从一种一致性状态转变为下一种一致性的状态,在事务执行前后,数据库完 整性约束没有被破坏;一个事务单元需要提交之后才会被其他事务可见。例如:一个表的姓名是唯 一键,如果一个事务对姓名进行修改,但是在事务提交或事务回滚后,表中的姓名变得不唯一了, 这样就破坏了一致性;一致性由原子性、隔离性以及持久性共同来维护的。

三、隔离级别

ISO 和 ANIS SQL 标准制定了四种事务隔离级别的标准,各数据库厂商在正确性和性能之间做了妥 协,并没有严格遵循这些标准;MySQL innodb默认支持的隔离级别是 REPEATABLE READ。SQL Server和Oracle默认是READ COMMITTED。

隔离级别越高,并发性能越低,隔离级别越低,并发性能越高。

READ UNCOMMITTED

读未提交;该级别下读不加锁,写加排他锁,写锁在事务提交或回滚后释放锁。其他事务可以在本事务未提交时读取到本事务做出的修改。

READ COMMITTED

读已提交(RC);从该级别后支持 MVCC (多版本并发控制),也就是提供一致性非锁定读;此时 读取操作读取历史快照数据;该隔离级别下读取历史版本的最新数据,所以读取的是已提交的数据;

REPEATABLE READ

可重复读(RR);该级别下也支持 MVCC,此时读取操作读取事务开始时的版本数据;

SERIALIZABLE

可串行化;该级别下给读加了共享锁;所以事务都是串行化的执行;此时隔离级别最严苛;

命令

-- 设置隔离级别
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 或者采用下面的方式设置隔离级别
SET @@tx_isolation = 'REPEATABLE READ';
SET @@global.tx_isolation = 'REPEATABLE READ';
-- 查看全局隔离级别
SELECT @@global.tx_isolation;
-- 查看当前会话隔离级别
SELECT @@session.tx_isolation;
SELECT @@tx_isolation;
-- 手动给读加 S 锁
SELECT ... LOCK IN SHARE MODE;
-- 手动给读加 X 锁
SELECT ... FOR UPDATE;
-- 查看当前锁信息
SELECT * FROM information_schema.innodb_locks;

四、redo log 和 undo log

redo log

redo 日志用来实现事务的持久性;内存中包含 redo log buffer,磁盘中包含 redo log file;

当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的commit操 作完成才完成了事务的提交;

redo log 顺序写,记录的是对每个页的修改(页、页偏移量、以及修改的内容);在数据库运行 时不需要对 redo log 的文件进行读取操作;只有发生宕机的时候,才会拿redo log进行恢复;

undo log

undo 日志用来帮助事务回滚以及 MVCC 的功能;存储在共享表空间中;undo 是逻辑日志,回滚 时将数据库逻辑地恢复到原来的样子,根据 undo log 的记录,做之前的逆运算;比如事务中有 insert 操作,那么执行 delete 操作;对于 update 操作执行相反的 update 操作;

同时 undo 日志记录行的版本信息,用于处理 MVCC 功能;

五、MVCC

多版本并发控制;用来实现一致性的非锁定读;非锁定读是指不需要等待访问的行上X锁的释放;

在 read committed 和 repeatable read下,innodb使用MVCC;然后对于快照数据的定义不同:

  • 在 read committed 隔离级别下,对于快照数据总是读取被锁定行的最新一份快照数据
  • 而在 repeatable read 隔离级别下,对于快照数据总是读取当前事务开始时的行数据版本;

MVCC提高了读并发性能。

【思考】为什么快照读不需要上锁?

因为没有事务需要对历史的数据进行修改操作;

六、锁

锁机制用于管理对共享资源的并发访问;用来实现事务的隔离级别 ;

锁类型

共享锁和排他锁都是行级锁;MySQL当中事务采用的是粒度锁;针对表(B+树)、页(B+树叶子节点)、行(B+树叶子节点当中某一段记录行)三种粒度加锁。

意向共享锁和意向排他锁都是表级别的锁。

 

 

共享锁(S)

事务读操作加的锁;对某一行加锁。

在SERIALIZABLE隔离级别下,默认帮读操作加共享锁;

在REPEATABLE READ隔离级别下,需手动加共享锁(此时其他事务无法修改删除数据),可一定程度上解决幻读问题;

在READ COMMITTED隔离级别下,没必要加共享锁,采用的是MVCC;

在READ UNCOMMITTED隔离级别下,既没有加锁也没有使用MVCC;

排它锁(X)

事务删除或更新加的锁;对某一行加锁;

在四种隔离级别下,都添加了排它锁,事务提交或事物回滚后释放锁;

意向共享锁(IS)

对一张表中某一行加的共享锁

意向排它锁(IX)

对一张表中某几行加的排它锁

AUTO-INC Lock(AI锁)

自增锁,是一种特殊的表级锁,发生在 AUTO_INCREMENT 约束下的插入操作;采用的一种特殊 的表锁机制;完成对自增长值插入的SQL语句后立即释放;在大数据量的插入会影响插入性能,因 为另一个事务中的插入会被阻塞;从MySQL 5.1.22开始提供一种轻量级互斥量的自增长实现机 制,该机制提高了自增长值插入的性能;

AI锁因为较低概率造成B+树的分裂,所以性能高。

锁的兼容性

行锁只会和行锁冲突。

意向锁之间不会产生冲突,也不会和AUTO_INC表锁冲突,它只会阻塞表级读锁或表级写锁。

意向锁的作用是告诉其他事务此表正在被访问操作,它的作用是排除全表读写锁。

下标列出了表级粒度的锁 的兼容性:

 

 

由于InnoDB支持的是行级别的锁,意向锁不会阻塞除了全表扫描以外的任何请求。

意向锁之间是相互兼容的;

IS只对表级排它锁不兼容;

当想为某一行添加S锁,先自动为所在的页和表添加意向锁IS,再为该行添加S锁;

当想为某一行添加X锁,先自动为所在的页和表添加意向锁IX,再为该行添加X锁;

当事务试图读或写某一条记录时,会先在表上加上意向锁,然后才在要操作的记录上加上读锁或写 锁。这样判断表中是否有记录加锁就很简单了,只要看下表上是否有意向锁就行了。

锁算法

Record Lock

记录锁,单个行记录上的锁;

Gap Lock

间隙锁,锁定一个范围,但不包含记录本身;全开区间;REPEATABLE READ级别及以上支持间隙锁。

//如此时有行索引数据 1 5 11
//在RR级别下,如果要更新2,因为索引未命中会添加间隙锁,锁范围为(1,5)
-- 查看是否支持间隙锁,默认支持,也就是 innodb_locks_unsafe_for_binlog = 0(OFF);
SELECT @@innodb_locks_unsafe_for_binlog;

如果 REPEATABLE READ 修改 innodb_locks_unsafe_for_binlog = 0 ,那么隔离级别相当于退化为 READ COMMITTED。

但是,这一设置变更并不影响外键和唯一索引(含主键)对gap进行加锁的需要。

Next-Key Lock

记录锁+间隙锁,锁定一个范围,并且锁住记录本身;左开右闭区间

Insert Intention Lock

插入意向锁,insert操作的时候产生;在多事务同时写入不同数据至同一索引间隙的时候,并不需 要等待其他事务完成,不会发生锁等待。

//假设有一个记录索引包含键值4和7,两个不同的事务分别插入5和6,
//每个事务都会产生一个加在4-7之间的插入意向锁,并获取在插入行上的排它锁,
//但是不会被互相锁住,因为数据行并不冲突。

锁兼容

 

 

 横向:表示已经持有的锁;纵向:表示正在请求的锁;

一个事物已经持有了插入意向锁,对其他事物是没有任何影响的;

一个事务想要获取插入意向锁,如果有其他事务已经加了 gap lock 或 Next-key lock 则会阻塞; 这个是重点,死锁之源;

锁对象

行级锁是针对表的索引加锁;索引包括聚集索引和辅助索引;

表级锁是针对页或表进行加锁;

重点考虑 InnoDB 在 read committed 和 repeatable read 级别下锁的情况;

如下图 students 表作为实例,其中 id 为主键,no(学号)为二级唯一索引,name(姓名)和 age(年龄)为二级非唯一索引,score(学分)无索引。

 

 

分别讨论 

  • 聚集索引,查询命中:UPDATE students SET score = 100 WHERE id = 15;

  • 聚集索引,查询未命中:UPDATE students SET score = 100 WHERE id = 16;

  • 辅组唯一索引,查询命中:UPDATE students SET score = 100 WHERE no = 'S0003';

  • 辅组唯一索引,查询未命中:UPDATE students SET score = 100 WHERE no = 'S0008';

  • 辅助非唯一索引,查询命中:UPDATE students SET score = 100 WHERE name = 'Tom';

  • 辅助非唯一索引,查询未命中:UPDATE students SET score = 100 WHERE name = 'John';

  • 无索引:UPDATE students SET score = 100 WHERE score = 22;

  • 聚集索引,范围查询:UPDATE students SET score = 100 WHERE id <= 20;

 

 

 在RR级别下,上述情况中,会出现一个特殊情况,有时候会添加(20,30]的锁(未知原因,无法归纳)

  • 辅助索引,范围查询:UPDATE students SET score = 100 WHERE age <= 23;

  • 修改索引值:UPDATE students SET name = 'John' WHERE id = 15;

 

 

在上述修改索引值的情况下,修改了辅助索引,会对聚集索引和辅助索引加锁 ,然后使用change buffer对辅助索引B+树异步刷新。

我们不能直接根据sql语句判断走的什么索引,一定要执行explain查看优化器选择走什么索引,再分析加锁情况。

七、并发异常

并发异常分读异常和死锁,主要由行锁和并发造成的。

MyISAM只有表锁,所以MyISAM不会出现并发读异常和并发死锁。

7.1 并发读异常

脏读

事务(A)可以读到另外一个事务(B)中未提交的数据;也就是事务A读到脏数据;

在读写分离的 场景下,可以将slave节点设置为 READ UNCOMMITTED;此时脏读不影响,在slave上查询并不 需要特别精准的返回值。

 

 

 不可重复读

事务(A) 可以读到另外一个事务(B)中提交的数据;通常发生在一个事务中两次读到的数据是不 一样的情况;不可重复读在隔离级别 READ COMMITTED 存在。一般而言,不可重复读的问题是 可以接受的,因为读到已经提交的数据,一般不会带来很大的问题,所以很多厂商(如Oracle、 SQL Server)默认隔离级别就是READ COMMITTED;

 

 

幻读

事务中一次读操作不能支撑接下来的业务逻辑;

通常发生在一个事务中一次读判断接下来写操作失 败的情况;

例如:以 name 为唯一键的表,一个事务中查询 select * from t where name = 'mark'; 不存在,接下来 insert into t(name) values ('mark'); 出现错误,此时另外一个 事务也执行了 insert 操作;

幻读在隔离级别 REPEATABLE READ 及以下存在;但是可以在 REPEATABLE READ 级别下通过读加锁(使用next-key locking)解决;

 

 解决幻读

 

 丢失更新

脏读、不可重复读、幻读都是一个事务写,一个事务读,由于一个事务的写导致另一个事务读到了 不该读的数据;丢失更新是两个事务都是写;

丢失更新分为提交覆盖和回滚覆盖;

回滚覆盖数据库会拒绝,不可能产生,重点关注提交覆盖;

 

解决提交覆盖

在事务一开始添加独占锁。

 区别

脏读和不可重复读的区别在于,脏读是读取了另一个事务未提交的数据,而不可重复读是读取了另一个事务提交后的数据。;本质上都是其他事务的修改影响了本事务的读取。

不可重复读和幻读比较类比;不可重复读是两次读取同一条记录,得到不同的结果;而幻读是两次读取同一个范围内的记录得到的结果集不一样(可能不同个数,也可能相同个数内容不一样,比 如删除一行后又添加新行);不可重复读是因为其他事务进行了update操作,幻读是因为其他事务进行了insert或者delete操作。

隔离级别下的并发读异常

 

 7.2 并发死锁

死锁:两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象;

MySQL 中采用 wait-for graph (等待图-采用非递归深度优先的图算法实现)的方式来进行死锁检 测;

异常报错:deadlock found when trying to get lock

相反加锁顺序死锁

不同表的加锁顺序相反或者相同表不同行加锁顺序相反造成死锁;

其中相同表不同行加锁顺序相反 造成死锁有很多变种,

其中容易忽略的是给辅助索引行加锁的时候,同时会给聚集索引行加锁;

同 时还可能出现在外键索引时,给父表加锁,同时隐含给子表加锁;

触发器同样如此,这些都需要视 情况分析;

解决:调整加锁顺序

锁冲突死锁

innodb 在 RR 隔离级别下,最常见的是插入意向锁与 gap 锁冲突造成死锁;主要原理为:一个事 务想要获取插入意向锁,如果有其他事务已经加了 gap lock 或 Next-key lock 则会阻塞;

案例

https://www.cnblogs.com/unrealCat/p/16071165.html

查看死锁

线下看日志,线上用命令。

系统表

-- 开启标准监控
CREATE TABLE innodb_monitor (a INT) ENGINE=INNODB;
-- 关闭标准监控
DROP TABLE innodb_monitor;
-- 开启锁监控
CREATE TABLE innodb_lock_monitor (a INT) ENGINE=INNODB;
-- 关闭锁监控
DROP TABLE innodb_lock_monitor

系统参数

-- 开启标准监控
set GLOBAL innodb_status_output=ON;
-- 关闭标准监控
set GLOBAL innodb_status_output=OFF;
-- 开启锁监控
set GLOBAL innodb_status_output_locks=ON;
-- 关闭锁监控
set GLOBAL innodb_status_output_locks=OFF;
-- 将死锁信息记录在错误日志中
set GLOBAL innodb_print_all_deadlocks=ON;

命令

-- 查看事务
select * from information_schema.INNODB_TRX;
-- 查看锁
select * from information_schema.INNODB_LOCKS;
-- 查看锁等待
select * from information_schema.INNODB_LOCK_WAITS;

7.3 死锁解决

对于顺序相反型,调整执行顺序;

对于锁冲突型,更换语句或者降低隔离级别;

7.4 如何避免死锁

  • 尽可能以相同顺序来访问索引记录和表;
  • 如果能确定幻读和不可重复读对应用影响不大,考虑将隔离级别降低为RC;
  • 添加合理的索引,不走索引将会为每一行记录加锁,死锁概率非常大;
  • 尽量在一个事务中锁定所需要的所有资源,减小死锁概率;
  • 避免大事务,将大事务分拆成多个小事务;大事务占用资源多,耗时长,冲突概率变高;
  • 避免同一时间点运行多个对同一表进行读写的概率;
posted @ 2022-03-29 13:02  幻cat  阅读(125)  评论(0编辑  收藏  举报