目录
🌈前言🌈
📁 事务概念
📁 事务操作
📁 事务提交方式
📁 隔离级别
📁 MVCC
📂 3个隐藏列字段
📂 undo日志
📂 Read View视图
📁 RR和RC的本质区别
📁 总结
🌈前言🌈
本期【MySQL】主要介绍MySQL中事务的概念,事务是据库管理系统中一个重要的概念,确保数据库操作可靠性的重要机制。
本期内容主要介绍事务的概念,如何操作失误,隔离级别以及事务的底层机制。
📁 事务概念
事务是一组DML语句,具有某种逻辑关系,完成特定操作。这些操作要么全部成功,要么全部失败,不能有其他状态。
事务四大特性:
-
原子性(Atomicity):
事务中的所有操作要么全部执行成功,要么全部不执行。即使在执行过程中发生错误,系统也会回滚到事务开始之前的状态。 -
一致性(Consistency):
事务必须使数据库从一个一致性状态转换到另一个一致性状态。在事务执行前后,数据库的完整性约束必须得到满足。 -
隔离性(Isolation):
并发执行的事务之间是相互隔离的,一个事务的执行不应受到其他事务的影响。各个事务的中间状态对其他事务是不可见的。 -
持久性(Durability):
一旦事务提交,其对数据库的修改就会永久保存,即使系统崩溃也不会丢失。
我们来设想一下事务场景,例如抢火车票。
当我们使用上层程序,访问数据库抢票时,我们是否需要保证过程的原子性操作,确保不会有安全问题。那么是否需要程序员来保证数据库访问的安全问题呢?事务就可以解决这个问题。
事务的出现,简化了编程模型,不需要我们考虑各种潜在的错误和迸发问题,当我们使用事务时,事务就确保了原子性。因此,事务本质上是为了应用层服务的。
在MySQL中只有使用了InnoDB数据库引擎的数据库或表才支持事务,MyISAM不支持。
show engines;
📁 事务操作
1. 开启事务
begin
//start transaction
2. 提交事务
commit
3. 回滚
rollback savepoint //回滚到保存点
rollback //回滚到最开始
4. 设置保存点
savepoint [保存点名字]
📁 事务提交方式
事务提交方式分为:手动提交 自动提交
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
//使用set改变提交模式
mysql> SET AUTOCOMMIT=0; #SET AUTOCOMMIT=0 禁止自动提交
Query OK, 0 rows affected (0.00 sec)
事务的提交方式只影响单SQL语句,在MySQL的InnoDB中每一条SQL语句被封装成一个事务,自动提交。
我们也可以进行手动提交,设置成手动提交后,执行完每一条SQL语句后,必须commit。
我们下面通过一个例子进行讲解:
//终端A
mysql> select * from student;
+--------+--------+
| stu_id | name |
+--------+--------+
| 1 | 张三 |
| 2 | 李四 |
| 3 | 王五 |
| 4 | 赵六 |
+--------+--------+
mysql> set autocommit=0;
Query OK, 0 rows affected (0.01 sec)
mysql> insert into student values (5,'田七');
Query OK, 1 row affected (0.00 sec)
mysql> select * from student;
+--------+--------+
| stu_id | name |
+--------+--------+
| 1 | 张三 |
| 2 | 李四 |
| 3 | 王五 |
| 4 | 赵六 |
| 5 | 田七 |
+--------+--------+
5 rows in set (0.00 sec)
//使用ctrl + d 终止进程
//终端B
mysql> select * from student;
+--------+--------+
| stu_id | name |
+--------+--------+
| 1 | 张三 |
| 2 | 李四 |
| 3 | 王五 |
| 4 | 赵六 |
+--------+--------+
4 rows in set (0.00 sec)
我们可以看到关闭自动提交后,如果你没有commit,MySQL不会将数据持久化到磁盘,而是回滚到最开始,体现了事务的原子性和一致性。
总结一下,对于设置提交方式,只会影响一条SQL语句(封装成事务),对于begin开始的事务,事务必须通过commit提交,才会持久化,与set autocommit无关。
此外,事务可以手动回滚,选择保存点。如果操作异常,MySQL会自动回滚,只能回滚到事务的最开始。如果commit了,就不能回滚了。
所谓的原子性,就是通过回滚保证,事务要么执行成功,成功数据保存在磁盘,进行持久化操作;要么操作失败,回滚到最开始状态。 进行保证了一致性。
一致性是指,数据库必须从一种状态切换到另一种状态,例如事务A成功完成操作,将数据写入数据库,数据库只包含正确完成事务结果,这就是一致状态。事务B因为操作错误,未能完成,但是未完成的事务将数据修改并保存到数据库,此时数据库就是不一致状态。
因此一致性是通过原子性来保证的。
📁 隔离级别
上面介绍了原子性,一致性,现在我们来介绍隔离性。在事务中,隔离性尤为总要。
多事务执行过程中,执行单个或多个SQLy语句,其中如果有事务更新了表,例如插入了新的记录或修改了记录,可能会影响到另一个事务。
为了保证事务在执行过程中尽量不受影响,就有了:隔离性。
允许事务受不同程度的影响,就有了:隔离级别。
隔离级别:
读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。
读提交【Read Committed】 :该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。
可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。
串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,。但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)
//查看和设置隔离性
mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ |
+--------------------------------+
1 row in set (0.00 sec)
mysql> select @@session.transaction_isolation;
+---------------------------------+
| @@session.transaction_isolation |
+---------------------------------+
| REPEATABLE-READ |
+---------------------------------+
1 row in set (0.00 sec)
mysql> set global transaction isolation level [隔离级别]
mysql> set session transaction isolation level [隔离级别]
不可重复读,事务A查询表,事务B更新或修改表,事务A再次查表的时候,两次读取值不一样。
幻读是指,事务A查询表,事务B插入新的记录,事务A再次查表的时候,两次读取记录数不一样。
mysql 默认的隔离级别是可重复读,一般情况下不要修改。其中隔离级别越严格,安全性越高,但数据库的并发性能也就越低。
📁 MVCC
上面我们了解了隔离级别,接下来我们从业务角度理解隔离级别,并且区分RR和RC的本质区别。
通过3个隐藏字段,和undo日志,我们来了解MySQL如何做到进行看到不同版本(通过版本链),解决脏读问题。
学习Read View,进而了解到RR和RC的本质区别,如何解决不可重复度问题。
📂 3个隐藏列字段
DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID。
DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)。
DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引。
补充:实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了。
📂 undo日志
undo log就是MySQL内存中一段内存空间,用来保存日志数据。
MySQL以守护进程的形式,运行在内存,会在内存中开辟一段空间,所有的机制:索引,日志等都是在该内存空间中完成,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作,在合适的时机刷新到硬盘。
当我们执行updata等指令更新记录值时,先给该记录加锁,修改前,将该行记录拷贝到undo log中,所以undo log中就有了副本,再修改原始记录,通过回滚指针指向副本,这样就形成了版本链。
其中MySQL中将每一条SQL语句封装成了事务,具有事务id。每一行记录都有一个隐藏列字段,其中有一列DB_TRX_ID记录被哪个事务修改。
事务提交后,释放锁。我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据
上面的一个一个版本,我们可以称之为一个一个的快照。
上述是updata,delete就是将删除flag字段设置,因此也可以加入版本链;insert之前没有数据,可以加入版本链吗?MySQL为了回滚操作方便,因此也将insert加入版本链。
但是select呢?select不需要对数据进行查改插入,加入版本链也没有任何意义,但是版本链对select读取非常有意义。
在多个事务同时删改查的时候,都是当前读,是要加锁的。那同时有select过来,如果也要读取最新版(当前读),那么也就需要加锁,这就是串行化。但如果是快照读,读取历史版本的话,是不受加锁限制的。也就是可以并行执行!换言之,提高了效率,即MVCC的意义所在。
select究竟是读取哪个版本的记录呢?这就涉及Read View和隔离级别了。
undo log清除:
事务提交:
- 当一个事务成功提交后,该事务相关的 undo log 就可以被清除。这是因为一旦事务提交,所有的修改都是永久性的,不再需要撤销这些操作。
回滚:
- 如果事务由于某种原因被回滚,相关的 undo log 也会被清除,因为它们不再需要保留。
空间回收:
- 在特定情况下,InnoDB 会进行空间回收,以释放不再需要的 undo log。当系统检测到某些 undo log 不再被任何活动事务引用时,这些日志就会被清除。
InnoDB 的内部机制:
- InnoDB 会定期检查和清理未使用的 undo log,尤其是在执行
CHECKPOINT
操作时,系统会尝试清理旧的 undo log,以控制日志文件的大小并优化性能。配置参数:
- 用户可以通过配置参数(如
innodb_undo_logs
和innodb_max_undo_log_size
)来影响 undo log 的管理,但具体的清除行为仍然依赖于 InnoDB 的内部管理机制。
📂 Read View视图
Read View就是事务进行快照读操作的时候生产的读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。即当进行快照读的时候,创建read view进行判断,select读取现版本,还是某个历史版本。
每次创建read view对象后,创建出来后read view不能再被修改。
class ReadView {
// 省略...
private:
/** 高水位,大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id
/** 低水位:小于这个ID的事务均可见 */
trx_id_t m_up_limit_id;
/** 创建该 Read View 的事务ID*/
trx_id_t m_creator_trx_id;
/** 创建视图时的活跃事务id列表*/
ids_t m_ids;
/* 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
trx_id_t m_low_limit_no;
/** 标记视图是否被关闭*/
bool m_closed;
// 省略...
};
m_ids: 一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id: 记录m_ids列表中事务ID最小的ID(没有写错)
low_limit_id: ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1(也没有写错)
creator_trx_id: 创建该ReadView的事务ID
以上就是进行快照读时,read view的作用,就是用来进行判断可视化的,该快照对哪些事务是可见的,对哪些事务是不可见的。
select * from user lock in share mode ,以加共享锁方式进行读取,对应的就是当前读。
Read View会记录下此时其他所有活动事务的快照,这些事务的修改对于当前事务是不可见的,而早于read view创建的事务所做的修改是可见的,晚于read view创建的修改是不可见的。
📁 RR和RC的本质区别
因为read view不可修改,因此read view的创建时机非常重要,事务A可能上一秒进行快照读,不能读取事务B更新的结果,但是下一秒就能看到。
事务A操作 | 事务A描述 | 事务B描述 | 事务B操作 |
begin | 开启事务 | 开启事务 | begin |
select * fromuser | 快照读(无影响)查询 | 快照读查询 | select * from user |
update user setage=18 where id=1; | 更新age=18 | - | - |
commit | 提交事务 | - | - |
select 快照读 ,没有读到age=18 | select * from user | ||
select lock in sharemode当前读 , 读到age=18 | select * from userlock in share mode |
事务A操作 | 事务A描述 | 事务B描述 | 事务B操作 |
begin | 开启事务 | 开启事务 | begin |
select * fromuser | 快照读,查到age=18 | - | - |
update user setage=28 where id=1; | 更新age=28 | - | - |
commit | 提交事务 | - | - |
select 快照读 age=28 | select * from user | ||
select lock in sharemode当前读 age=28 | select * from userlock in share mode |
通过上述例子,我们可以看出事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后续快照读结果的能力。
正是read view生成时机不同,才造就了RC,RR级别下快照读的结果的不同。
RR 与 RC的本质区别:
在RC隔离级别下,每次进行快照读的时候都会创建新的read view;在RR级别下,再试同一个事物的第一个快照才会创建read view,此后同一个事物的快照都采用同一个read view。
📁 总结
以上,就是本期【MySQL必知会】,重点就是事务的概念,隔离级别,以及MVCC。其中,版本链,read view对事务可见性的影响,RC和RR的区别以及如何实现也是尤为重要。
如果感觉本期内容对你有帮助,欢迎点赞,关注,收藏Thanks♪(・ω・)ノ