Mysql——吃透事务以及隔离级别

目录

什么是事务

为什么要有事务

事务的属性

事务的操作

单sql事务

事务的原子性

事务的隔离性

串行化

可重复读(Mysql默认)

读已提交

读未提交

快照读和当前读

幻读脏读不可重复读

查询和修改隔离级别

后记

RR和RC隔离原理(MVCC)

Mysql表的隐藏列

undo日志

ReadView

RR原理

RC原理

回滚原理


什么是事务

简单来讲,事务是一组不可分割的数据库操作序列(由若干个sql语句组成)


为什么要有事务

一个完整的业务操作,往往需要执行多条SQL语句。事务的核心意义,就是把多条独立的数据库操作变成一个整体单元(这意味着,一个事务要么不执行要么全部执行)。列举两个例子来说明事务的必要性:

  1. A要给B转账,包含两个操作:A账户减少5元、B账户增加5元。但是在刚刚执行了"A账户减少5元"操作后断电了,造成:A账户的钱平白无故减少的严重错误(即单个业务操作执行一半)。
  2. 还是A给B转账。在****刚刚执行了"A账户减少5元"操作后,AB同时查询各自余额发现A的钱少了B的钱却没增加,造成逻辑不自洽的错误(即多个业务操作之间相互影响)。

事务的属性

  • 原子性(Atomicity):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。
  • **一致性(Consistency):**在事务开始之前和事务结束以后,数据库的内容始终是符合预期的。
  • 隔离性(Isolation):数据库允许多个并发事务执行,隔离性就是指并发过程中一个事务对另一个事务的可见性。
  • 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失

事务的这四个属性可以简称为ACID。其中,只要保证了原子性、隔离性、持久性,自然就满足一致性了。正是因为事务有这些属性,业务操作才能被正确无误的执行


事务的操作

相关命令:

bash 复制代码
begin //开启一个事务

start transaction  //开启一个事务

savepoint name; //设置一个保存点,它的名字是name

rollback to name; //回滚到保存点的位置

rollback //回滚在事务中做的所有操作

commit //提交事务,提交之后代表整个事务的结束

实操:


单sql事务

实际上,我们平时在mysql中执行的单sql语句也会被封装成事务,只不过他们是自动提交的,下面举个例子:


事务的原子性

在'事务的操作'一节中的第二个例子可以看到,mysql实现了事务的原子性,即一但事务执行期间发生意外会自动回滚。


事务的隔离性

Mysql中分为四种隔离级别,不同的隔离隔离级别也就代表着一个事务对另一个事务的可见度:

  • 串行化(serializable):这是事务的最高隔离级别,它通过强制事务串行,让事务与事务之间完全不可见,也就不可能发生事务并发错误。
  • 可重复读(REPEATABLE READ):这是第二严格的级别,事务对另一个事务的可见性松动(可能见到了另一个活跃事务的修改,也可能没有,这取决于事务执行的时机),但是它保证同一个事务内不会出现多次查表数据不一致的情况。
  • 读已提交(READ COMMITTED):在这个级别下,事务对另一个事务的可见性进一步加大,一个事务能看到其他事务的提交了的修改,也不保证事务执行过程中多次查表的一致性。
  • 读未提交(READ UNCOMMITTED):这个级别下,两个事务之间完全不隔离,一个事务对另一个事务所做的动作完全可见。

串行化

串行化**,是最严格的隔离级别,事务之间不会相互冲突。核心就是:让多个事务在逻辑上串行执行。**它保证事务串行的规则大致如下:

  • 事务A查询了一张表的部分内容,那么他就会给这部分内容加锁。其他事务如果想更新或者在这部分内容中插入新数据,就会被阻塞,直到事务A提交。
  • 事务A更新了一张表的某行数据,那么他就会给这行数据加锁。如果其他事务的查询会包含这行数据,或者直接想更新这行数据,就会被阻塞,直到事务A提交。
  • 事务A在一张表中插入一行数据,那么他就会给这行数据加锁。如果其他事务的查询会包含这行数据,或者直接想更新这行数据,就会被阻塞,直到事务A提交。

上面的规则其实意味着:

  • 事务A如果查询了一张表的部分内容,那么其他事务在这部分内容之外查询或者更新或者插入都不会被阻塞。
  • 事务A如果更新了一行数据,那么其他事务只要不去碰这个行数据就不会被阻塞。
  • 事务A如果插入了一行数据,那么其他事务只要不去碰这个行数据就不会被阻塞。
  • 如果两个事务都是读,那么不会阻塞。

总而言之,串行化并不是事务之间严格互斥执行,而是在保证串行效果的同时,尽量让事务之间可以并发提高效率。为了理解这个,我来打个比方:

  • 如果两个事务修改的部分根本就不一样,那么即使他两严格串行,得到的结果和并发也是一样的,还不如并发。
  • 如果两个事务都是读,那么就更没有阻塞的必要了。

注:这个级别下是可能出现死锁的情况,比如:事务A修改了第三行数据,接着事务B修改了第二行数据,接着事务B想访问第三行数据但是被阻塞了,然后事务A访问第二行数据也被阻塞了,结果就是死锁互相等待,Mysql此时会Kill一个事务。

可重复读(Mysql默认)

可重复读的隔离级别略低于串行化,它的核心是:不保证一定不读到其他事务的更新,但保证同一个事务内,多次查表得到的结果都是一致的,不会出现在同一个事务内前后查询数据不一致的情况

可重复读级别保证了事务查表结果一致,但其他事务在此期间也可以更新数据。不难想到,事务A和事务B访问的一定不是同一块数据,而是分别访问该数据的旧版本和新版本,否则不会出现一条数据两种内容。也正是因为访问的不是同一块数据,所以在可重复读级别下两个事务读写同一行数据可以并发执行,效率比串行化好一点。

之所以说它隔离级别更低,是因为在可重复读级别下事务看到的数据可能不是最新的,这就有可能造成一些问题。比如说,有个应用的功能是实时性的获取用户的状态,在获取用户状态这个事务执行过程中运行完成一个修改状态的事务,但是我们还是返回旧的状态。当然,这种隔离级别是否有问题是和业务之间挂钩的,如果业务对这方面不敏感,那么最佳选择就是可重复读级别,因为效率更高

实操:

读已提交

读已提交的隔离性又低于可重复读,它的核心是:一个事务能看到其他事务的提交了的修改,同时不保证事务执行过程中多次查表的结果一致

实操:

读未提交

这个隔离级别很简单,读者可以认为根本没有任何读写控制,即一个事务对数据修改后另一个事务立马可以看到(不需要提交后才能看到)

实操:

快照读和当前读

我们在上面介绍某些隔离级别的时候,涉及到了版本的概念。实际上对数据库中有两种"读"的方式,这两种方式走的流程不一样,因此会产生不同的效果

  • 快照读:进行版本读取,有可能读到的是旧版本,也有可能是新版本。
  • 当前读:直接读取数据库不管什么版本,读取到的一定是此时此刻最新的数据。

注:"读"的概念很宽泛,包括增删改查。对数据库的增删改操作一定是当前读,因为逻辑上历史是不能改变的,只能改变最新的数据

幻读脏读不可重复读

  • 脏读:看到了别人没有提交的数据(读未提交级别会出现这种问题)
  • 不可重复读:在同一个事务内多次查询结果不一样(读已提交,读未提交会出现这种问题)
  • 幻读:一个事务插入数据,另一个事务查询并发现新插入的数据。幻读属于不可重复读的一种。(读已提交,读未提交会出现这种问题)。

查询和修改隔离级别

bash 复制代码
 SELECT @@global.transaction_isolation;   --查看全局隔级别

 SELECT @@transaction_isolation;   --查看本会话隔级别

//每次登录mysql会话隔离级别都会被设置成全局隔离级别

//设置会话/全局隔离级别
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED | READ 
COMMITTED | REPEATABLE READ | SERIALIZABLE]

后记

在Mysql中,有三个基本的原则,无论哪种隔离级别都遵守:

  • 两个事务对同一块数据修改,其中一个事务会阻塞直到另一个事务提交或回滚。保证不出错。
  • 两个事务对同一块数据进行读,那么这两个事务都不会阻塞而是并发。提高效率
  • 两个事务分别读写不同数据,那么这两个事务都不会阻塞而是并发。提高效率

RR和RC隔离原理(MVCC)

Mysql中每一个事务都是第一个类对象,都有自己的事务ID,事务ID越大表示该事务启动的越晚。

Mysql表的隐藏列

  • DB_TRX_ID :6 byte,记录创建这条记录/最后一次修改该记录的事****务的ID
  • DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本
  • DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键就会有这个隐藏列(InnoDB 会自动以****DB_ROW_ID 产生一个聚簇索引)。

假设我们创建了一张表:

sql 复制代码
create table if not exists student(
name varchar(11) not null,
age int not null
);

实际上这个表的一条记录会有这几个字段:

undo日志

这个可以看做是Msyqld使用的一个较大缓冲区,会存放某条记录的历史版本,DB_ROLL_PTR记录的就是历史版本在undo日志中的地址。为了理解这个,接下来举个例子:

假设现在有一个ID为10的事务,它包含的sql语句是:

sql 复制代码
begin;
insert into student values ("张三",28);
update student set age=20 where name="张三";
commit;

事务执行结束得到的结果就是:

假设现在又来了一个ID为11的事务,它包含的sql语句是:

sql 复制代码
begin;
update student set age=80 where name="张三";
commit;

执行事务的结果就是:

不过实际上undo日志的内容总会销毁的,毕竟undo日志大小有限,一般来说一个事务提交,该事务所形成的版本链就会销毁,不过这也不一定,万一旧版本有人在用呢。说了这么多其实就是想说,一个事务在修改数据之前会把旧版本保存下来,并用链表连接

思考一下,如果一个事务是新插入数据,那么它的旧版本怎么保存?undo日志当然会保存一些东西来标志一下,这里暂且不做讨论。

之前说的当前读就是读undo日志之外的最新内容,而快照读读的可能就是undo日志中的旧版本内容。

ReadView

cpp 复制代码
class ReadView {
public:
// 省略...
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;

    // 省略...
}
  • m_ids:一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
  • up_limit_id:记录m_ids列表中事务ID最小的ID(没有写错)
  • low_limit_id:ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的****最大值+1(也没有写错)
  • creator_trx_id:创建该ReadView的事务ID

创建这个类对象的同时,他会把创建这个对象这一刻正在活跃的所有事务的ID(m_ids),以及还在活跃着的最早的事务的ID(up_limit_id),以及未来创建事务首先分配的ID(low_limit_id),以及创建这个对象的事务ID都记录下来。相当于给创建ReadView这一刻的事务模块的状态进行拍照记录

RR原理

RR级别下,事务A第一次进行快照读(默认就是快照读)的时候,就会先创建一个ReadView对象,保存下此刻的整个事务模块的状态:

包括本次查询在内,事务A此后每次查询操作,对每行记录都按照下面的规则进行判断:

  1. 如果这条记录的 (DB_TRX_ID == creator_trx_id)|| (DB_TRX_ID<up_limit_id),那说明该记录是被事务A自己修改的,或者修改它的事务已经提交了,此时事务A应该看到这条记录,显示这个记录。
  2. 如果这条记录的(DB_TRX_ID >=low_limit_id),说明这是新启动的事务修改的,事务A不应该看到,此时我们沿着undo日志中的版本链找到上一版本的记录并对该记录也做判断。
  3. 如果这条记录的(up_limit_id>DB_TRX_ID >=low_limit_id)&& (DB_TRX_ID在m_ids中 ),说明这个记录是正在活跃的事务修改的,事务A不应该看到,此时我们沿着undo日志中的版本链找到上一版本的记录并对该记录也做判断。
  4. **如果这条记录的(**up_limit_id>DB_TRX_ID >=low_limit_id)&& (DB_TRX_ID不在m_ids中 ),说明这是已提交的事务,,此时事务A应该看到这条记录,显示这个记录。

由于事务A多次查询都用的是第一次查询时创建的ReadView:

  • 创建ReadView之后启动的事务ID都大于low_limit_id,所以新事务的修改都看不到。
  • 创建ReadView时活跃,之后提交/提交了的事务在ReadView中的状态依旧是正在活跃而不是已提交,所以它的修改也看不到。
  • 创建ReadView时已经提交了的事务在ReadView的状态依旧是已提交,所做的修改仍然可以看到。

这就是为什么RR能维持整个事务多次查询一致性的原理。本质上就是给利用ReadView记录下了第一次查询时的哪些事务已经提交以及什么事务此时还没创建

我上面介绍的时候说,RR级别下,可能看到旧版本而不是一定。那是因为,如果事务B在事务A第一次查询之前就提交了,那么事务A就一定可以看到事务B修改的内容(ReadView标记了它的状态是提交)。

RR解决幻读:如果说事务只进行快照读,那多次查询是看不到新插入的数据的,因为ReadView的状态已经不变了,所以RR级别下快照读是没有幻读问题的。但是如果当前读的话可能会幻读,因此RR级别下如果是当前读会加间隙锁来防止其他事务的插入。

RC原理

RC的原理和RR一模一样。只不过,RC级别下,每次查询之前都会更新一下ReadView,让ReadView不是记录旧的事务模块状态,而是新的。这样一来一但有事务提交,在ReadView中的状态就是提交,那么按照RR中的记录判断逻辑,这个记录就会被显示出来。

所以RC才表现出:一个事务可以看到其他事务已经提交内容。

回滚原理

有了上面的基础,回滚其实就是用undo日志中的历史版本覆盖到数据库。

相关推荐
折哥的程序人生 · 物流技术专研2 小时前
《Java面试85题图解版(三)》上篇:高阶架构设计篇
java·开发语言·后端·面试·职场和发展
爱码小白3 小时前
MySQL易忘知识点梳理
数据库·mysql
战南诚3 小时前
mysql - 行列数据转换技巧
数据库·mysql
身如柳絮随风扬3 小时前
MySQL 中优雅统计“只算周一到周五”的到访数据
数据库·mysql
YuanDaima20483 小时前
WSL2 核心中间件部署实战:MySQL、Redis 与 RocketMQ
java·数据库·人工智能·redis·python·mysql·rocketmq
tongluowan00712 小时前
MySQL中列数量及长度
数据库·mysql
Cosolar14 小时前
大模型应用开发面试 • 每日三题|Day 003|多Agent系统中的通信协议、冲突解决和一致性保障
人工智能·后端·面试
前进的李工14 小时前
MySQL慢查询日志优化实战
数据库·mysql·性能优化
张元清16 小时前
React Observer Hooks:7 种监听 DOM 而不写样板代码的方式
前端·javascript·面试