MySQL事务及原理

什么是事务

    一个最小的不可再分的工作单元,定义一组要么同时执行成功,要么同时执行失败的SQL语句。

四大特征(ACID)

  • 原子性(A):事务是最小单位,不可再分
  • 一致性(C):事务要求所有的DML语句操作的时候,必须同时成功或者同时失败
  • 隔离性(I):事务A和事务B之间具有隔离性
  • 持久性(D):是事务的保证,事务终结的标志(内存的数据持久到硬盘文件中)

在MySQL中,默认情况下,事务是自动提交的,也就是说,只要执行一条DML语句就开启了事物,并且提交了事务,自动提交机制可以关闭

--查看当前会话自动提交模式
show session variables like 'autocommit';
--全局
show global variables like 'autocommit';
--关闭自动提交
--会话
set session autocommit=0;
--全局
set global autocommit=0;
--显示开启事务
start transaction;
……  --一条或多条sql语句
commit;

原子性

    原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。

实现原理

以下原理都是基于MySQL的InnoDB引擎,因为MYISAM引擎不支持事务

    InnoDB存储引擎提供了两种事务日志:redo log(重做日志)和undo log(回滚日志)。其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。

    在事务中对数据库进行修改时,InnoDB会生成对应的undo log,其实就是与操作相反的SQL,如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log将数据回滚到修改之前的样子

    undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。

持久性

    事务一旦提交,它对数据库的改变就应该是永久性的。

实现原理

    InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。

    Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。

    于是,redo log被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。

    既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:

  1. 刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
  2. 刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。

redo log与binlog

    在MySQL中还存在binlog(二进制日志)也可以记录写操作并用于数据的恢复,听起来跟redo log很类似,但二者是有着根本的不同的:

  1. 作用不同:redo log是用于crash recovery的,保证MySQL宕机也不会影响持久性;binlog是用于point-in-time recovery的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。
  2. 层次不同:redo log是InnoDB存储引擎实现的,而binlog是MySQL的服务器层实现的,同时支持InnoDB和其他存储引擎。
  3. 内容不同:redo log是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于数据本身或者二者的混合。
  4. 写入时机不同:binlog在事务提交时写入;redo log的写入时机相对多元:

redo log与binlog之间还有个二阶段提交策略

隔离性

    与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。

    隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。

事务隔离级别

    SQL-92标准定义了4种隔离级别,包括了一些具体规则,用来限定事务与事务之间,哪些改变是可见的,哪些是不可见的。

Read uncommitted(读未提交)

    事务可以看到其他事务未提交的数据,未提交意味着这些数据可能会回滚,读到了最终不存在的数据也被称之为脏读(Dirty Read)。

Read committed(读提交)

    这是大多数数据库系统的默认隔离级别(MySQL除外)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。在同一事务内,不同的时刻读到的同一批数据却是不一样的,称之为不可重复读,因为可能会受到其他事务的影响,比如其他事务改了这批数据并提交了,通常针对数据更新(UPDATE)和删除(DELETE)操作。

不可重复读算不算是问题要看具体的场景

Repeatable read(可重复读取)

    在一个事务内,多次读同一个数据,在这个事务还没结束时,读到的数据是一样的(MySQL的默认事务隔离级别)。

    但是又会出现另一个问题,幻读,针对数据插入(INSERT)操作来说,当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。

MySQL 在可重复读级别解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的。

Serializable(串行化)

    最高的隔离级别,它通过强制事务排序,顺序执行,事务不能并发解决了更新丢失、脏读、不可重复读、幻读,同时代价也是最高的,性能很低,一般很少使用,在该级别下,可能导致大量的超时现象和锁竞争。

--读未提交READ-UNCOMMITTED
--读已提交READ-COMMITTED
--可重复读REPEATABLE-READ
--串行化SERIALIZABLE
--global 全局,session 会话

mysql> set @@global.tx_isolation='READ-UNCOMMITTED';
Query OK, 0 rows affected (0.00 sec)

mysql> set @@session.tx_isolation='READ-UNCOMMITTED';
Query OK, 0 rows affected (0.00 sec)

实现原理

    隔离性追求的是并发情形下事务之间互不干扰。简单起见,我们主要考虑最简单的读操作和写操作(加锁读等特殊读操作会特殊说明),那么隔离性的探讨,主要可以分为两个方面:

  • (一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
  • (一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性

    首先来看两个事务的写操作之间的相互影响。隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB通过锁机制来保证这一点。

    锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

    从锁粒度上说,InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。

  1. InnoDB引擎支持行级锁和表级锁,只有在通过索引条件检索数据的时候,才使用行级锁,否就使用表级锁,由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。
  2. 行级锁开销大,加锁慢。锁定粒度最小,发生锁冲突概率最低,支持的并发度最高。
--查看锁的情况
select * from information_schema.innodb_locks; #锁的概况
show engine innodb status; #InnoDB整体状态,其中包括锁的情况

行级锁

    分为共享锁和排它锁,

  1. 排它锁又叫写锁 (X) ,如果事务T对A加上排它锁,则其它事务都不能对A加任何类型的锁。获取排它锁的事务既能读数据,也能写数据。如: SELECT … FOR UPDATE,UPDATE
  2. 共享锁又叫读锁 (S) ,如果事务T对A加上共享锁,则其它事务只能对A再加共享锁,不能加其它锁。获取共享锁的事务只能读数据,不能写数据。 如:SELECT … LOCK IN SHARE MODE;

这里注意一点,并不是加上排它锁后其他事务就不能读了,普通的读是不需要加任何锁的。只有锁定读的情况下才需要加锁。

间隙锁

    用范围条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁,对于在条件范围内但并不存在的记录,叫做“间隙(GAP),InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。    

在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内的并发插入,这可能会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量避免使用范围条件。

目的:

  1. 防止幻读
  2. 满足恢复机制要求: Binlog 是按照事务提交的先后顺序记录, 恢复也是按这个顺序进行的 (MySQL主从复制其实就是从节点不断做基于 BINLOG 的恢复 )。因此,在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读。

一致性非锁定读和锁定读

一个事务对另一个事务读的影响

  • 利用MVCC实现一致性非锁定读,保证在同一个事务中多次读取相同的数据返回的结果是一样的,解决了不可重复读的问题
  • 利用Gap Locks和Next-Key可以阻止其它事务在锁定区间内插入数据,因此解决了幻读问题

一致性非锁定读

  • InnoDB用多版本来提供查询数据库在某个时间点的快照。如果隔离级别是REPEATABLE READ,那么在同一个事务中的所有一致性读都读的是事务中第一次读到的快照;如果是READ COMMITTED,那么一个事务中的每一个一致性读都会刷新快照版本
  • 一致性读不会给它锁访问的表加任何形式的锁,因此其他事务可以同时并发的修改它们

锁定读

在一个事务中,标准的SELECT语句不会加锁,但是有两种情况例外。SELECT ... LOCK IN SHARE MODE 和 SELECT ... FOR UPDATE。

  • SELECT ... LOCK IN SHARE MODE:给索引加上共享锁,这样其他事务只能读不能修改,直到当前事务提交
  • SELECT ... FOR UPDATE:给索引记录加锁,这种情况跟UPDATE的加锁情况是一样的

MVVC (多版本并发控制)

    大多数数据库实现事务隔离都不是简单的通过锁去实现,因为这样性能太差了,MVCC的全称是“多版本并发控制”,可以通过不适用锁来实现事务之间的隔离,大大提升了并发性能,而且可以查询另一个事务更新前和更新后的记录。

    不同的数据库对MVCC有不同的实现,在InnoDB引擎表中,会给数据库中的每一行增加三个字段,它们分别是DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID。可以简单理解为,每个记录中有两个隐藏列的概念:

    一个是行的创建时间,一个是行的过期时间。其实存储的是系统版本号,不是真实的时间。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

SELECT

根据以下两个条件检查每行记录:

  1. innodb只查找版本号早于当前事务版本的数据行,<=当前事务版本号,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的
  2. 行的删除版本要么未定义,要么大于当前的事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。

INSERT

新插入的每一行保存当前系统版本号作为行版本号

DELETE

删除的每一行保存当前系统版本号作为行删除标识

UPDATE

插入一行新纪录,保存当前系统版本号为行版本号,同时保存当前系统版本号到原来的行作为行删除标识

MVCC只在可重复读和读已提交两个隔离级别下工作。主要的区别就是在快照的创建上,可重复读仅在事务开始是创建一次,而读已提交每次执行语句的时候都要重新创建一次。

一致性

一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。

实现一致性的措施包括:

  • 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证
  • 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
  • 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致

参考

《高性能MySQL》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值