MySQL事务实现与锁

  • Post author:
  • Post category:mysql


在讲MySQL的事务之前,我们需要明白什么是事务,为什么会用到事务。

事务:是指一组操作要么同时成功,要么同时失败,失败后数据内容恢复初始状态,成功后数据内容持久化。

为什么用到事务呢? 在项目研发中,基于对对象的抽象会产生不同的对象实体,这些实体映射到底层数据库表的时候会有多张表存在。一组业务操作可能需要对这多张表同时处理,为了保障这多张表数据同时成功或者同时失败,就需要借助数据库事务来保障。

1. MySQL事务

1.1 事务的特性

  • 原子性:指单个事务本身涉及到的数据库操作,要么全部成功,要么全部失败,不存在完成事务中一部分操作的可能;
  • 隔离性:指多个事务之间,没有相互影响干扰。
  • 一致性:指事务执行前后的状态要一致,没有脏数据。
  • 持久性:数据库一旦完成事务的提交之后,那么这个事务的状态就会持久在数据库中。

1.2 事务的隔离级别

MySQL标准定义了4类隔离级别,用来限定事务内外的哪些改变是可见的,哪些是不可见的。隔离级别由低到高:Read Uncommitted < Read Committed < Repeatable Read < Serializable。对应隔离级别产生的差异有:

=========================================================================

隔离级别               脏读(Dirty Read)          不可重复读(NonRepeatable Read)     幻读(Phantom Read)

=========================================================================

未提交读(Read uncommitted)        可能                            可能                       可能

已提交读(Read committed)          不可能                          可能                        可能

可重复读(Repeatable read)          不可能                          不可能                     可能

可串行化(Serializable )                不可能                          不可能                     不可能

=========================================================================

脏读:当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个实物访问了这个数据,并且使用了这个数据。

不可重复读:在一个事务内,对一个数据进行了多次的读取;这个事务还没有结束的时候,另一个事务对数据进行了修改,导致第一个事务前后读取到的数据不一致,产生不可重复读。

幻读:第一个事务对表中的数据进行了修改,这种修改涉及到表的全部数据;第二个事务也修改了表中的数据,这种修改主要是向表中插入一条记录,导致操作第一个事务的用户发现表中还有没有修改的数据行,像是幻觉一样。

–查看全局事务隔离级别

SELECT @@global.tx_isolation;

— 查看session隔离级别

SELECT @@session.tx_isolation;

— 查看当前事务隔离级别

SELECT @@tx_isolation;

//设置read uncommitted级别: set session transaction isolation level read uncommitted;

//设置read committed级别: set session transaction isolation level read committed;

//设置repeatable read级别: set session transaction isolation level repeatable read;

//设置serializable级别: set session transaction isolation level serializable;

2. 事务的分类

  • 扁平事务:最简单也是实际使用最频繁的一种事务,由 begin 开始,commit work 或者 rollback 结束,期间的操作都是原子性操作,要么执行,要么回滚
  • 带有保存点的扁平事务:

    • 除了支持扁平事务支持的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态
    • 保存点用来通知系统应该记住事务当前的状态,一旦事务过程中发生错误,事务能回到保存点当时的状态
  • 链事务

    • 可视为带有保存点的扁平事务的变种,当发生系统崩溃时,所有保存点都将消失,因为保存点是易失的而非持久的
    • 当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行
  • 嵌套事务:由一个顶层事务控制着各个层次的事务,顶层事务之下嵌套的事务称为子事务,其控制着每个局部的变换
  • 分布式事务:通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在的位置访问网络中的不同节点

3. 事务实现机制

3.1 基于锁的事务隔离

数据库的锁是为了解决事务的隔离性问题,为了让事务之间相互不影响,每个事务进行操作的时候都会对数据加上一把特有的锁,防止其他事务同时操作数据。数据库里有的锁有很多种,为了方面理解,对锁进行了一个分类,分别如下

  • 基于锁的属性分类:共享锁、排他锁。
  • 基于锁的粒度分类:表锁、行锁、记录锁、间隙锁。
  • 基于锁的状态分类:意向共享锁、意向排它锁。

3.1.1 锁的属性分类


共享锁(Share Lock)

共享锁又称读锁,简称S锁;当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。


排他锁(Exclusive Lock)

排他锁又称写锁,简称X锁;当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。

3.1.2 锁的力度分类


表锁

引擎 MyISAM, 直接锁定整张表,在锁定期间,其它进程无法对该表进行写操作;如果是读锁,则其它进程读也不允许。特点: 粒度大,加锁简单,容易冲突;


页锁

引擎 BDB。页级锁介于表级锁和行级锁之间,一次锁定相邻的一组记录


行锁

引擎 INNODB, 锁住的是表的某一行或多行记录,其它进程还是可以对同一个表中的其它记录进行操作。 特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高;


记录锁(Record Lock)

记录锁也属于行锁中的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录。


间隙锁(Gap Lock)

间隙锁属于行锁中的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成一个区间,遵循左开右闭原则。

间隙锁作用

:防止幻读问题。


临键锁(Next-Key Lock)

临键锁也属于行锁的一种,是INNODB的行锁默认算法,它是记录锁和间隙锁的组合,加锁的时候会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住。

临键锁的作用:

临键锁避免了在范围查询时出现脏读、重复读、幻读问题。加了临键锁之后,在范围区间内数据不允许被修改和插入。

3.1.3 状态锁

为了允许表级锁和行级锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。

  • 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
  • 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。

共享锁(S)

排他锁(X)

意向共享锁(IS)

意向排他锁(IX)

共享锁(S)

兼容

冲突

兼容

冲突

排他锁(X)

冲突

冲突

冲突

冲突

意向共享锁(IS)

兼容

冲突

兼容

兼容

意向排他锁(IX)

冲突

冲突

兼容

兼容

注意:意向锁是InnoDB自动加的,不需要用户干预;对于insert、update、delete,InnoDB会自动给涉及的数据加排他锁(X)。


3.1.2 MVCC

MVCC:多版本并发控制(MVCC,Multiversion Currency Control),实现了非阻塞的读操作(解决了幻读、不可重复读的问题),但保存了两个额外的系统版本号,占用额外的存储空间。实现原理:innodb存储的最基本row中包含一些额外的存储信息 DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BIT。

  • DATA_TRX_ID,6字节, 标记了最新更新这条行记录的transaction id,每处理一个事务,其值自动+1
  • DATA_ROLL_PTR:7字节,指向当前记录项的rollback segment的undo log记录,找之前版本的数据就是通过这个指针
  • DB_ROW_ID:6字节,当由innodb自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值,否则聚集索引中不包括这个值.,这个用于索引当中
  • DELETE BIT:位用于标识该记录是否被删除,这里是标志出来的删除。真正意义的删除是在commit的时候

在默认隔离级别RR下,MVCC的工作方式:

  • Select:

    • InnoDB只查找版本早于当前事务版本的数据行,保证事务读取的行, 要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
    • 行的删除版本定义,要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。
  • INSERT:InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
  • DELETE:InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
  • UPDATE:InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。

MVCC在mysql中的实现依赖的是undo log与read view

  • undo log :undo log 中记录某行数据的多个版本的数据。
  • read view :用来判断当前版本数据的可见性

3.2 redo log 和undo log, binlog


3.2.1 redo log

redo log叫做重做日志,是InnoDB存储引擎层的日志,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。redo log日志的大小是固定的,即记录满了以后就从头循环写。

redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。

为了确保每次日志都能写入到事务日志文件中,在每次将log buffer中的日志写入日志文件的过程中都会调用一次操作系统的fsync操作(即fsync()系统调用)。MySQL支持用户自定义在commit时如何将log buffer中的日志刷log file中。这种控制通过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有3种值:0、1、2,默认为1。但注意,这个变量只是控制commit动作是否刷新log buffer到磁盘。

  • 当设置为1的时候,事务每次提交都会将log buffer中的日志写入os buffer并调用fsync()刷到log file on disk中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。
  • 当设置为0的时候,事务提交时不会将log buffer中日志写入到os buffer,而是每秒写入os buffer并调用fsync()写入到log file on disk中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
  • 当设置为2的时候,每次提交都仅写入到os buffer,然后是每秒调用fsync()将os buffer中的日志写入到log file on disk。


3.2.2 undo log

undo log 叫做回滚日志,记录事务修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以根据undo log的信息来进行回滚到没被修改前的状态。

undo log有两个作用:提供回滚和多个行版本控制(MVCC)。undo log和redo log记录物理日志不一样,它是逻辑日志。当执行rollback时,可以从undo log中的逻辑记录读取到相应的内容并进行回滚。

undo log是采用段(segment)


的方式来记录的,每个undo


操作在记录的时候占用一个undo log segment




另外,

undo log


也会产生redo log


,因为undo log


也要实现持久性保护。

undo log 格式:

  • insert undo log:insert 操作产生的 undo log ,在事务提交后就删除
  • update undo log:delete 和 update 操作产生的 undo log,该 undo log 可能需要提供 MVCC 机制,因此不能在事务提交时就删除


3.2.3 binlog 二进制日志

mysql-binlog是MySQL数据库的二进制日志,用于记录用户对数据库操作的SQL语句((除了数据查询语句)信息。binlog的格式也有三种:STATEMENT、ROW、MIXED 。


3.2.3 binlog 和事务日志的先后顺序

二进制日志是MySQL的上层日志,先于存储引擎的事务日志被写入。在MySQL5.6以前,当事务提交(即发出commit指令)后,MySQL接收到该信号进入commit prepare阶段;进入prepare阶段后,立即写内存中的二进制日志,写完内存中的二进制日志后就相当于确定了commit操作;然后开始写内存中的事务日志;最后将二进制日志和事务日志刷盘,它们如何刷盘,分别由变量 sync_binlog 和 innodb_flush_log_at_trx_commit 控制。

但因为要保证二进制日志和事务日志的一致性,在提交后的prepare阶段会启用一个

prepare_commit_mutex

锁来保证它们的顺序性和一致性。但这样会导致开启二进制日志后group commmit失效,特别是在主从复制结构中,几乎都会开启二进制日志。在MySQL5.6中进行了改进。提交事务时,在存储引擎层的上一层结构中会将事务按序放入一个队列,队列中的第一个事务称为leader,其他事务称为follower,leader控制着follower的行为。虽然顺序还是一样先刷二进制,再刷事务日志,但是机制完全改变了:删除了原来的prepare_commit_mutex行为,也能保证即使开启了二进制日志,group commit也是有效的。

MySQL5.6中分为3个步骤:

flush阶段、sync阶段、commit阶段。

  • flush阶段:向内存中写入每个事务的二进制日志。
  • sync阶段:将内存中的二进制日志刷盘。若队列中有多个事务,那么仅一次fsync操作就完成了二进制日志的刷盘操作。这在MySQL5.6中称为BLGC(binary log group commit)。
  • commit阶段:leader根据顺序调用存储引擎层事务的提交,由于innodb本就支持group commit,所以解决了因为锁 prepare_commit_mutex 而导致的group commit失效问题。


3.2.4 group commit

对于InnoDB来说,事务提交的两个阶段

  • 修改内存中事务对应的信息,并且将日志写入重做日志缓冲
  • 调用 fsync 将确保日志都从重做日志缓冲写入磁盘

若事务为非只读事务,则每次事务提交时需要进行一次 fsync 操作,以此保证重做日志都已经写入磁盘,为了提高磁盘 fsync 的效率,当前数据库提供了 group commit 的功能,即一次 fsync 可以刷新确保多个事务日志被写入文件

3.3 事务实现


3.3.1 undo log实现原子性

每条数据变更(insert/update/delete)操作都伴随一条undo log的生成,并且回滚日志必须先于数据持久化到磁盘上。


3.3.2 redo log实现持久性

事务开始之后就产生redo log,redo log的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo log文件中。


3.3.3 隔离级别实现

通过MySQL的锁和MVCC实现事务隔离级别;


3.3.4 一致性实现

通过回滚,以及恢复,和在并发环境下的隔离做到一致性。

参考文献:


MySQL事务实现 – 搜索结果 – 知乎


详细分析MySQL事务日志(redo log和undo log) – 海东潮 – 博客园


MySQL的Binlog原理_击水三千里的专栏-CSDN博客_mysqlbinlog



版权声明:本文为chenwiehuang原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。