事务
事务是由 MySQL 的引擎来实现,常见的 InnoDB 引擎它是支持事务的 并不是所有的引擎都能支持事务,比如 MySQL 原生的 MyISAM 引擎就不支持事务
特性
具体特性
- 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节;事务在执行过程中发生错误,会被回滚到事务开始前的状态
- 一致性:事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态
- 隔离性:允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失
如何保证特性?
- 持久性是通过 redo log (重做日志)来保证的
- 原子性是通过 undo log(回滚日志) 来保证的
- 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的
- 一致性则是通过持久性+原子性+隔离性来保证
隔离级别
- MySQL服务端允许多个客户端连接,意味着 MySQL 会出现同时处理多个事务的情况
- 问题:
- 脏读:如果一个事务「读到」了另一个「未提交事务修改过的数据」
- 事务 A 是还没提交事务的,也就是它随时可能发生回滚操作,被读
- 不可重复读:一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况
- 事务A两次读取过程中,事务B进行修改且提交
- 幻读:一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况
- 事务A两次读取过程中,事务B进行修改且提交
- 脏读:如果一个事务「读到」了另一个「未提交事务修改过的数据」
- 隔离级别
- 读未提交:一个事务还没提交时,它做的变更就能被其他事务看到
- 读提交:一个事务提交之后,它做的变更才能被其他事务看到
- 可重复读:一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别
- 串行化:会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行
- 「可重复读」但是很大程度上避免幻读现象
- 针对快照读(普通 select 语句),通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的
- 针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那这个插入语句就会被阻塞,无法成功插入
- 四种隔级别实现
- 「读未提交」可以读到未提交事务修改的数据,所以直接读取最新的数据就好
- 「串行化」通过加读写锁的方式来避免并行访问;
- 「读提交」和「可重复读」通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View
- 执行「开始事务」begin/start transaction 命令
- 执行了 begin/start transaction 命令后,并不代表事务启动了
- 只有在执行这个命令后,执行了第一条 select 语句,才是事务真正启动的时机
Read View在MVCC工作
- Read View 有四个重要的字段:
- m_ids :在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务
- min_trx_id :在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值
- max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,就是全局事务中最大的事务 id 值 + 1
- creator_trx_id :创建该 Read View 的事务的事务 id
- InnoDB 存储引擎的数据库表,聚簇索引记录中包含下面两个隐藏列:
- trx_id:一个事务对某条聚簇索引记录进行改动时,会把该事务的事务 id 记录在 trx_id 隐藏列里
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录
- 通过「版本链」控制并发事务访问同一个记录时的行为叫 MVCC(多版本并发控制)
- Read View 有四个重要的字段:
可重复读和读提交如何工作
- 可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View
- 读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View
日志
undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC
redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复
binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制
undoLog
- 执行执行一条“增删改”语句的时候,虽然没有输入 begin 开启事务和 commit 提交事务,但 MySQL 会隐式开启事务来执行“增删改”语句的,执行完就自动提交事务
- 事务执行过程中,都记录下回滚时需要的信息到一个日志里,若事务执行中途发生MySQL 崩溃后,就不用担心无法回滚到事务之前的数据,可通过这个日志回滚到事务之前的数据
- undo log 是一种用于撤销回退的日志,在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚
- 在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好
- 在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好
- delete操作实际上不会立即直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作是purge线程完成的
- 在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好
- update的列是否是主键列。
- 不是主键列,在undo log中直接反向记录是如何update的。即update是直接进行的。
- 是主键列,update分两部执行:先删除该行,再插入一行目标行
- update的列是否是主键列。
- undo log 两大作用:
- 实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态
- 实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录
Buffer Pool
MySQL数据都是存在磁盘中,那么要更新一条记录时候,得先从磁盘读取该记录,然后在内存中修改这条记录。那修改完这条记录是选择直接写回到磁盘,还是选择缓存起来呢?
- 作用?
- 当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取
- 当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页,为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘
- 缓存内容:
- InnoDB 会把存储的数据划分为若干个「页」,以页作为磁盘和内存交互的基本单位,一个页的默认大小为 16KB。Buffer Pool 同样需要按「页」来划分
- MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的
16KB的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页 - Buffer Pool 除了缓存「索引页」和「数据页」,还包括了 Undo 页,插入缓存、自适应哈希索引、锁信息等等
- 作用?
redoLog
- Buffer Pool 基于内存,而内存不可靠,万一断电重启,没来及落盘脏页数据就会丢失, InnoDB 引擎会在适当时候,由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里,这就 WAL (Write-Ahead Logging)技术
- WAL 技术: MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上
- redo log 是物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条或者多条物理日志
- redo log 和 undo log 区别
- redo log 记录此次事务「修改后」的数据状态,记录的是更新之后的值,主要用于事务崩溃恢复,保证事务的持久性。
- undo log 记录此次事务「修改前」的数据状态,记录的是更新之前的值,主要用于事务回滚,保证事务的原子性
- redo log 要写到磁盘,数据也要写磁盘,超麻烦
- 写入 redo log 的方式使用了追加操作, 所以磁盘操作是顺序写
- 写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写
- redo log 也有自己缓存—— redo log buffer,每当产生一条 redo log 时,会先写入到 redo log buffer,后续在持久化到磁盘
- 作用 :
- 实现事务的持久性,让 MySQL 有 crash-safe 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;
- 将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能
- redoLog刷盘时间(也就是从redoLogBuffer到磁盘)
- MySQL 正常关闭时;
- 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;
- InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。
- 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘(这个策略由 innodb_flush_log_at_trx_commit 参数控制)
- 参数
innodb_flush_log_at_trx_commit参数控制,可取的值有:0、1、2,默认值为 1,这三个值分别代表的策略如下:- 当设置该参数为 0 时,表示每次事务提交时 ,还是将 redo log 留在 redo log buffer 中 ,该模式下在事务提交时不会主动触发写入磁盘的操作。
- 当设置该参数为 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不会丢失。
- 当设置该参数为 2 时,表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache,Page Cache 是专门用来缓存文件数据的,所以写入「 redo log文件」意味着写入到了操作系统的文件缓存
- redoLog文件写满怎么办?
- 默认情况下, InnoDB 存储引擎有 1 个重做日志文件组( redo log Group),「重做日志文件组」由有 2 个 redo log 文件组成
- 重做日志文件组是以循环写的方式工作的,从头开始写,写到末尾就又回到开头,相当于一个环形
binLog
- MySQL 完成一条更新操作后,Server 层会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写入 binlog 文件
- binlog 文件记录所有数据库表结构变更和表数据修改的日志,用于备份恢复、主从复制
锁
全局锁
要使用全局锁,执行后,整个数据库就处于只读状态了,命令:
flush tables with read lock释放全局锁,则要执行这条命令:
unlock tables全局锁主应用于全库逻辑备份,这样备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样
加上全局锁,意味着整个数据库都是只读状态
表级锁
表锁
//表级别的共享锁,也就是读锁; //允许当前会话读取被锁定的表,但阻止其他会话对这些表进行写操作。 lock tables t_student read; //表级别的独占锁,也就是写锁; //允许当前会话对表进行读写操作,但阻止其他会话对这些表进行任何操作(读或写)。 lock tables t_stuent write;释放表锁,可以使用下面这条命令,会释放当前会话的所有表锁:
unlock tables元数据锁(MDL)
不需要显示的使用 MDL,当对数据库表进行操作时,会自动给这个表加上 MDL
- 对一张表进行 CRUD 操作时,加的 MDL 读锁;
- 当有线程在执行 select 语句( 加 MDL 读锁)的期间,如果有其他线程要更改该表的结构( 申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句( 释放 MDL 读锁)
- 对一张表做结构变更操作的时候,加的MDL 写锁
- 当有线程对表结构进行变更( 加 MDL 写锁)的期间,如果有其他线程执行了 CRUD 操作( 申请 MDL 读锁),那么就会被阻塞,直到表结构变更完成( 释放 MDL 写锁)
- 对一张表进行 CRUD 操作时,加的 MDL 读锁;
意向锁
目的是为了快速判断表里是否有记录被加锁
- 在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;
- 在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(*lock tables … read*)和独占表锁(*lock tables … write*)发生冲突
AUTO-INC 锁
表里的主键通常都会设置成自增,通过对主键字段声明
AUTO_INCREMENT属性实现AUTO-INC 锁是特殊的表锁机制,锁不是再一个事务提交后才释放,而是再执行完插入语句后就会立即释放
行级锁
InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁
普通的 select 语句是不会对记录加锁的,因为它属于快照读
要在查询时对记录加行锁,可以使用下面这两个方式,这种查询加锁的语句称为锁定读
//对读取的记录加共享锁 select ... lock in share mode; //对读取的记录加独占锁 select ... for update;上面这两条语句共同点,必须在一个事务中,因为当事务提交了,锁就会被释放,所以在使用这两条语句的时候,要加上 start transaction,COMMIT;
-- 自动提交模式下,锁立即释放(无意义) SELECT * FROM table WHERE id = 1 FOR UPDATE; -- 执行完立即释放锁若需锁生效,必须显式开启事务
START TRANSACTION; SELECT * FROM table WHERE id = 1 FOR UPDATE; -- 锁持续到事务结束 -- 执行其他操作... COMMIT; -- 提交时释放锁行级锁的类型:
- Record Lock:记录锁,就是仅仅把一条记录锁上;
- Gap Lock:间隙锁,锁定一个范围,但是不包含记录本身;
- Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身
Record Lock
Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的:
当一个事务执行了下面这条语句:
mysql > begin; mysql > select * from t_test where id = 1 for update;对 t_test 表中主键 id 为 1 这条记录加上 X 型的记录锁,这样其他事务就无法对这条记录进行修改
当事务执行 commit 后,事务过程中生成的锁都会被释放
Gap Lock
- Gap Lock 称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象
- 间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的
Next-Key Lock
- Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身
- Next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的