文章目录
一、概念简述
为引出今天的主题,我们来看下面这个场景:

假设这是一个售票程序,车票相关信息被存在数据库中,·nums·表示剩余票数。如果A,B两个客户同时买票访问数据库时(假设程序如上图1到5依次执行),剩余票数是1,所以都进入if条件语句,导致·nums·被减为-1,在程序上他们都抢到票了,但实际上只有一个座位。
那么如果100个人去抢一个票呢,那将是一场灾难。
这个例子类似一个线程安全问题,两个执行流同时访问一个临界资源。
因此这个程序起码要满足下面需求:
- 买票的过程得是原子的
- 客户买票互相不能影响
- 买完票了得永久有效
- 买票前和买票后要确定状态
即:原子+隔离+持久+一致
什么是事务?
事务是数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成,这些操作要么全部成功执行,要么全部不执行,从而保证数据从一种一致性状态转换到另一种一致性状态。
它是解决并发问题的。
事务的属性:
- 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
- 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
注:
事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的,而不是伴随着数据库系统天生就有的。
二、事务操作
版本支持
注意:在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。使用指令show engines查看存储引擎是否支持事务:

查看当前所有数据库连接和线程状态:
sql
show processlist;
事务流程
-
启动事务
start transaction;
#或
begin; -
SQL操作...
-
创建保存点(可选):
savepoint 名称(自定义);
-
定向回滚(可选)
rollback to 保存点名称;
-
提交事务
commit;
注意:
- 在创建保存点 和回滚操作前后可执行多个SQL操作
- 创建保存点并定义名称是为了方便回滚到指定的保存点,如果没有保存点,不能进行定向回滚而是把这个事务过程都丢弃掉。
- 进行
commit提交事务后不再在回滚。 - 如果没有执行commit指令,系统也没有自动提交事务(如
ctrl+c、终端直接关掉、异常退出等),会自动回滚到整个事务。

事务提交方式
在MySQL中有自动事务提交机制,autocommit=1表示默认事务提交是打开的。
autocommit=1时的SQL操作:
- start开启事务:需要手动commit提交SQL操作,不手动提交则不会被保存。
- 不开启事务:使用系统的自动提交机制,每做完一条SQL语句自动提交,相当于通过SQL指令就是一个事务。
autocommit=0时的SQL操作:
- start开启事务:同上,需要手动commit提交SQL操作。
- 不开启事务:所做的SQL操作,只能在当前会话被看到,而且退出回话操作将不会被保存,所以需要手动commit。
查看autocommit状态:
sql
select @@autocommit
设置autocommit:
sql
set autocommit = 0;
#或
set autocommit = 1;
三、事务的隔离级别
在事务的执行中,执行流之间允许不同程度的干扰,这就是事务隔离级别的核心逻辑。
每个事务都看最新的事务是不合理的(如果事务没有commit),不同事务对应看到它该看到的,才知道怎么改,才能改对。
根据不同的影响程度,分为这些隔离级别:
-
读未提交(Read Uncommitted):所有事务都可以看到其他未提交事务的执行结果。可能导致脏读、幻读、不可重复读等问题,实际生产中基本不使用。
-
读提交(Read Committed):多数数据库的默认隔离级别(非MySQL默认)。一个事务只能看到其他已提交事务的结果。可能引发不可重复读问题。
-
可重复读(Repeatable Read):MySQL默认隔离级别。确保同一事务中多次读取数据时结果一致。可能产生幻读问题。
-
串行化(Serializable):最高隔离级别,通过强制事务排序避免冲突,解决幻读问题。但可能导致超时和锁竞争,实际生产中很少使用。
隔离级别的实现:主要通过锁机制实现,包括表锁、行锁、读锁、写锁、间隙锁(GAP)、Next-Key锁(GAP+行锁)等。
隔离级别的查看:
sql
select @@tx_isolation;
- 读未提交:相当与没有做隔离是存在安全问题的。
- 读提交:只能读到别人commit后的数据,而且只要别人commit后就一定能读到。
- 可重复读:自己在事务中,在此区间有人对数据更新,那么它读不到,只能读到它刚好进入事务时的数据信息。保证了事务的一致性。
- 串行化:访问完一个,另一个才能访问。隔离级别最高。无法并发访问,效率低下。
对数据库的操作分为以下这些:
- 只写:必须是串行化级别。
- 只读:可以无隔离,即读未提交级别。
- 可读可写:使用读提交或可重复读。
隔离级别查看:
sql
##当前会话隔离级别
select @@transaction_isolation;
## 或
select @@session.transaction_isolation;
## 或(旧版本MySQL)
select @@tx_isolation;
##或
select @@session.tx_isolation;
sql
##全局会话隔离级别
select @@global.transaction_isolation;
#或(旧版本MySQL)
select @@global.tx_isolation;
隔离级别的设置:
格式:
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL level;
可选项:
GLOBAL:设置全局的隔离级别(即作用在所有会话的所有库)SESSION:设置当前的隔离级别(即作用在当前会话的所有库)
其中 level 为:
READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE
四、隔离验证
读未提交


读提交
同样的方法设置隔离级别进行验证:

可重复读

串行化

注意:验证完后把隔离级别改回原先的级别。
五、多版本并发控制(MVCC)
MVCC技术它通过保存数据的多个历史版本,让数据库的读操作和写操作可以同时进行而互不干扰,从而极大地提升了并发性能。
在表中除了我们构建的字段以外,还有一些隐藏的字段,是 MVCC 的"基础设施"和"数据载体",如下:
DB_TRX_ID:6 byte 最近修改(修改/插入)事务ID,记录创建这条记录/最后一次修改该记录的事务ID
DB_ROLL_PTR:7 byte 回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在undo log中)
DB_ROW_ID:6 byte 隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
- 事务ID:在MySQL并发访问中,为了区分事务先后,为每个事务分配事务ID,ID越小来的事务越早。
回滚指针指向哪?
undo 日志是 MySQL 在存储引擎层维护的一块内存(及磁盘)空间。在更改数据前会做一次写时拷贝,拷贝到undo日志,并且新的数据DB_ROLL_PTR列会储存被拷贝到的undo的老数据的地址。其中insert 也要放在undo log中,insert、update、delete能形成版本链。
隔离本质:在版本上进行隔离,隔离级别决定应该看到那个版本。
ReadView
Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也可能是该行记录在 undo log 中的某个历史版本。
ReadView部分成员:
cpp
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生成时刻,系统正活跃的事务IDup_limit_id:记录m_ids列表中事务ID最小的IDlow_limit_id:ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1(也没有写错)creator_trx_id:创建该ReadView的事务ID
在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的DB_TRX_ID 。那么,我们现在手里面有的东西就有,当前快照读的 ReadView 和 版本链中的某一个记录的DB_TRX_ID 。所以现在的问题就是,当前快照读,应不应该读到当前版本记录。
如下图示解析:

注:
- RC模式 :每次
select都生成新的ReadView。 - RR模式 :第一次
select生成,后续复用。
非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!
