前言
首先从概念上进行理解什么是事务,以及事务的4大属性,知道是什么还要知道为什么?
事务是如何进行操作的,最后在谈事务的隔离性、隔离级别(最重要但是也很难理解),理解隔离级别体现在哪里 (操作层面进行理解隔离性)
理论层面理解事务的实现原理
先谈场景再谈事务的概念
数据库是支持多线程下的高并发访问的,高并发的访问如果不进行确保原子性的话,非常容易进行出现数据不一致问题。
所以说数据库的CURD操作要进行处理这种问题,主要通过一下策略
1. 买票的过程得是原子的吧
2. 买票互相应该不能影响吧
3. 买完票应该要永久有效吧
4. 买前,和买后都要是确定的状态吧
CURD通过策略确保数据库的正常操作和事务有什么关系呢?
通过事务概念就可以知道这两者之间的关系了
什么是事务?
事务就是一组DML语句组成,各种mysql语句,他们之间具有一定的逻辑,就把这些具有逻辑的mysql的一批语句封装起来称为事务。
这一组DML语句要么全部成功,要么全部 失败,是一个整体。防止出现一个事务没有执行完,另一个事务执行造成数据不一致问题,MySQL提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的
为了保证事务的正常运行,拥有四大属性
- 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中 间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个 事务从来没有执行过一样。
- 一致性:进行事务操作后的结果是可以预期的,在数据库中mysql并没有通过实现策略进行确保一致性,而是通过其他三种特性来进行维持的
- 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务 并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Read uncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化 ( Serializable )
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
为什么要有事务呢?
不要只站在程序员的角度去理解:
为了保证多个sql语句在执行的时候,不会出现交差执行的问题,防止数据不一致,从而破环mysql的完整性
还要站在数据库使用者的角度去理解:
事务被MySQL编写者设计出来,本质就是为了当应用层程序进行访问数据库的时候,事务能够进行简化我们的编程模型,上层开发者不需要再进行考虑这种潜在的错误和并发问题,这些都是MySQL 已经帮我们进行避免了,因此事务的本质就是为了应用层进行服务的,而不是伴随数据库系统天生就有的。
了解事务的提交方式
事务的版本支持
并不是所有的引擎都支持事务的
查看MySQL 系统支持的所有存储引擎以及每个存储引擎的相关特性和状态。
show engines;
备注:
XA 事务是分布式事务的一部分,通常用于多个数据库系统之间的事务管理。
保存点是事务中的检查点,允许部分回滚某些操作,而不是完全回滚。
事务的提交方式
事务提交方式的种类
事务的常见提交方式有两种
- 手动提交
- 自动提交
查看事务的提交方式
show variables like 'autocommit';
value值对应的是NO表示支持自动提交,OFF 表示不支持自动提交,也就是手动提交。
更改事务的提交方式
事务的常见操作
事务正常验证与产出结论
提前准备
mysql 不仅本地的客户端可以进行连接,远端的客户端也可以进行连接
一个mysql服务器是可以通过多个客户端进行访问的
需要进行重启客户端生效
创建测试表
开始事务
start transaction;
或者直接使用
begin;
设置任务保存点
savepiont + 保存点
回滚
rollback to 保存点
回退到指定的保存点。
结束事务
回滚操作只能在事务进行提交之前进行回滚
事务异常验证与产出结论
当我们在事务中插入数据,然后客户端崩溃(此时崩溃的客户端自动提交是开启的,但是未进行手动commit 提交),我们在进行查看,发现刚刚进行插入的数据,回滚回去了 。
当我们在事务中插入数据,然后客户端崩溃(此时崩溃的客户端自动提交是开启的,进行手动commit 提交),我们在进行查看,发现刚刚进行插入的数据,无法进行回滚回去了 。
ctrl + \ 客户端直接崩溃
通过上面的现象我们可以看到,mysql中的事务保证了要么就不操作,要么就操作完,这不就是事务四大特性中的 原子性 和 持久性 吗
单条SQL和事务的关系
当未进行打开自动提交的时候,进行插入数据后(未进行手动commit 提交),客户端崩溃,数据库中的数据会进行回滚。
当打开自动提交的时候,进行插入数据后(未进行手动commit 提交),客户端崩溃,数据库中的数据会进行回滚,也就说明autocommit 在客户端崩溃之前进行了自动提交。
因为事务是主动提交的,所以我们感知不到,但是可以进行证明单SQL本质就是事务。
结论
- 只要输入begin或者start transaction,事务便必须要通过commit提交,才会持久化,与是 否设置set autocommit无关。
- 事务可以手动回滚,同时,当操作异常,MySQL会自动回滚
- 对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。(select有特殊情况,因为 MySQL 有 MVCC )
- 从上面的例子,我们能看到事务本身的原子性(回滚),持久性(commit)
什么是隔离性,什么是隔离级别,为什么要有这么多隔离级别?
如何理解隔离性?
理解事务的隔离性(Isolation)通常可以通过时间轴来帮助可视化不同事务之间的数据操作顺序和它们之间的相互影响。事务隔离性的目的是确保在多个事务并发执行时,事务之间互不干扰,并且每个事务都有一个独立的执行环境。
如何理解隔离级别?
数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别
READ UNCOMMITTED(读取未提交):最底层的隔离级别,事务可以看到其他事务的未提交数据。
READ COMMITTED(读取已提交):事务只能读取已提交的数据,不允许读取未提交的数据。
REPEATABLE READ(可重复读取):确保在同一事务中,读取的数据是一致的,即使其他事务修改了该数据,当前事务的读取结果不受影响。
SERIALIZABLE(可串行化):最高的隔离级别,所有事务按顺序执行,就像它们是串行执行的。
READ UNCOMMITTED(读取未提交):
在这个隔离级别下,事务可以读取其他事务未提交的数据(脏读)。这个级别允许事务之间的最大干扰。
- T1: 写入数据 A=10 (未提交)
- T2: 读取数据 A=10 (脏读)
- T1: 提交
- T2: 继续操作
时间轴:
-
事务
T2
读取了事务T1
中尚未提交的变更(脏读)。 -
事务
T1
提交后,T2
继续操作,但T2
的数据读取可能是无效的,因为T1
的更改最终可能被回滚。
READ COMMITTED(读取已提交):
在这个隔离级别下,事务只能读取其他事务已经提交的数据,避免了脏读,但仍然可能发生不可重复读(数据在同一事务中读取两次时不一致)。
- T1: 写入数据 A=10 (未提交)
- T2: 读取数据 A=10 (脏读)
- T1: 提交
- T2: 继续读取数据 A=20 (不可重复读)
时间轴:
-
事务
T2
最初读取了事务T1
中未提交的数据。 -
当事务
T1
提交后,事务T2
读取的数据可能发生变化,从A=10
变成A=20
,发生了不可重复读。
这个过程还经常发生幻读,幻读和不可重复读的区别如下
同一事务中,两次读到同一行内容不同 | ❌ 这是不可重复读 |
同一事务中,两次读到的行数或记录集不同 |
很多人都会对不可重复有下面这个疑问?
提交了被看到,难道不应该吗
提交了确实应该被看到,但是不能是正在运行的事务看到,这样是非常容易出现问题的,如下图所示:
当我们正在进行执行发放年终奖,发放年终奖是根据薪资来进行决定的,当tom的薪资本来在3000~4000中,已将tom进行执行到这个环节,但是呢,tom薪资在另一个事务中进行发生了更改,此时由于是不可重复读,导致呢tom这个人在4000~5000的薪资中又重新被统计了一遍,从而导致问题的出现,这也破环了事务的隔离性原则。
REPEATABLE READ(可重复读取):
在此级别下,事务读取的数据在整个事务过程中保持一致,即使其他事务修改了相同的数据。该级别避免了脏读和不可重复读,但仍然可能出现幻读(即读取的记录集在事务执行过程中发生变化)。
- T1: 读取数据 A=10 (已提交)
- T2: 写入数据 A=20 (未提交)
- T1: 继续读取数据 A=10 (可重复读)
- T2: 提交
时间轴:
-
事务
T1
在整个过程中读取的数据是一致的(不会因为其他事务的修改而改变),即使T2
修改了数据,T1
仍然读取到A=10
。 -
事务
T1
读取的数据是可重复的,不会出现数据的不一致。
SERIALIZABLE(可串行化):
在这个最高级别的隔离下,事务会像串行执行一样,完全避免了脏读、不可重复读和幻读。
- T1: 读取数据 A=10 (已提交)
- T2: 事务等待 T1 提交
- T1: 提交
- T2: 写入数据 A=20 (提交)
时间轴:
-
事务
T2
在T1
提交之前不能读取或修改T1
正在操作的数据。 -
所有事务按顺序执行,不会发生并发冲突。
事务隔离级别的设置与查看
查看全局隔离级别
select @@global.transaction_isolation;
查看会话(当前)全局隔离级别
select @@session.transaction_isolation;
注:默认当前会话都是进行拷贝的全局隔离级别进行赋值的。
设置当前会话隔离级别
set session transaction_isolation = '隔离级别';
设置全局会话隔离级别
set globle transaction_isolation = '隔离级别';
mysql提供不同隔离性的原因是
供外部用户进行选择。
事务的隔离级别-一致性的正确理解
事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务 成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中 断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一 致)的状态。因此一致性是通过原子性来保证的。
其实一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑 做支撑,也就是,一致性,是由用户决定的。
事务的一致性,其实就是通过其他三种特性进行决定的,其他三种特性是因,一致性其实就是其他三种特性的必然结果。
读提交和可重复读这种隔离性是如何做到的???
原理是怎么做到的?通过MVCC 进行做到的,那么MVCC是什么呢?
MVCC
多版本并发控制( MVCC )是一种用来解决 读-写冲突 的无锁并发控制
为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始 前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题
- 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数 据库并发读写的性能
- 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
如何理解MVCC
前置知识
- 3个记录隐藏字段
- undo 日志
- Read View
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变了
如何区分每个事务的先后问题呢
- 每个事务都要有自己的事务ID,可以根据事务的ID大小进行判断事务的先后到来顺序,事务ID越小越先被执行
- mysqld可能会面临处理多个事务的情况,事务也有自己的声明周期,mysqld要对多个事务进行管理:先描述,在组织。mysqld使用C/C++写的,mysqld中一定有对应的一个或者一套结构体对象,事务也要有自己的结构体。
name | age | DB_TRX_ID(创建该记录的事 务ID) | DB_ROW_ID(隐式 主键) | DB_ROLL_PTR(回滚 指针) |
张三 | 28 | null | 1 | null |
图片中进行描述的信息其实是表格中这样
updo 日志
写入之前先进行拷贝,再填地址,然后进行更改
历史的回滚采用的相反sql的方式
多版本控制就交于MVCC
事务的回滚操作也是因为 undo log
undo log 不用担心打满的情况,当进行commit并且灭有用户进行访问的时候会自动进行free
update 和 delete 可以形成版本链,但是insert 暂时不考虑版本链的原因
select读取最新版本还是读取历史版本呢??
Read view理论
Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一 刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被 分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照 读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的 数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据
class ReadView {// 省略...private:/** 高水位,大于等于这个ID的事务均不可见*/trx_id_t m_low_limit_id我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的
DB_TRX_ID 。
那么,我们现在手里面有的东西就有,当前快照读的 ReadView 和 版本链中的某一个记录的
DB_TRX_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;// 省略...
};
我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的 DB_TRX_ID 。 那么,我们现在手里面有的东西就有,当前快照读的 ReadView 和 版本链中的某一个记录的 DB_TRX_ID 。 所以现在的问题就是,当前快照读,应不应该读到当前版本记录。一张图,解决所有问题!
这里的版本链并不是undo log 中的 数据.只是类似的结构.
m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id; //记录m_ids列表中事务ID最小的ID(没有写错)
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的
最大值+1(也没有写错)
creator_trx_id //创建该ReadView的事务ID
左侧:
拿着我当前事务的信息,进行遍历版本链,当我们是事务的creator_trx_id 和版本链中的DB_TRX_ID相同时,就证明我现在进行产看的这个事务就是我自己进行创建的(增加、修改、删除)那么我应不应该看到呢?
当然应该看到。
当我的事务中的up_limit_id ,当我进行遍历版本链中的最新的提交的快照的 DB_TRX_ID 要是比我up_limit_id还要小,也就是说历史中运行的事务ID比我正在活跃运行的事务ID还要小,说明DB_TRX_ID早就已经结束了,早就已经提交完了,所以我应该看到.我和你这个事务的执行是串行的,没有交叉,你先跑完,我才开始跑的.
右侧:
当版本链中的DB_TRX_ID>=当我事务中的 low_limit_id 值说明是快照之后才进行,才进行提交的事务,不应该被看到
-
DB_TRX_ID >= low_limit_id
意味着该数据版本来自于一个在当前事务开始后提交的事务,因此当前事务 不能看到 这个版本的数据,因为它只应该读取 低于low_limit_id
的事务 提交的数据。 -
事务提交时会更新版本链中的快照数据,但是当前事务所使用的快照视图是在事务开始时就已经确定的,它不会受到当前正在提交的事务的影响。
view 并不是创建事务的时候就有的,而是在查询的时候就存在的. ??
RR与RC的区别
储备知识已经就绪,现在来分析一下RR与RC的本质区别
当前读和快照读在RR下的区别:
少做了一次select 为什么会出现这么大差别,说好的可重复读呢??
在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活 跃的其他事务记录起来
此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更 新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可 见;
即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事 务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下 的事务中可以看到别的事务提交的更新的原因
总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是 同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。
正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。