Mysql——吃透事务以及隔离级别
发布时间:2026/8/19 14:46:40 作者:尧图编辑部 阅读量:1,286

目录什么是事务为什么要有事务事务的属性事务的局限性事务的操作单sql事务事务的原子性事务的隔离性串行化serializable可重复读REPEATABLE READ读已提交READ COMMITTED读未提交READ UNCOMMITTED快照读和当前读幻读脏读不可重复读查询和修改隔离级别后记RR和RC隔离原理MVCCMysql表的隐藏列undo日志ReadViewRR原理RC原理回滚原理什么是事务简单来讲事务是一组不可分割的数据库操作序列由若干个sql语句组成。为什么要有事务一个完整的业务操作往往需要执行多条SQL语句。数据库通过事务来保证多条sql语句打包执行的数据一致性。如果没有事务会出现以下情况A要给B转账包含两个操作A账户减少5元、B账户增加5元。但是在刚刚执行了“A账户减少5元”操作后断电了造成A账户的钱平白无故减少的严重错误即单个业务操作执行一半。A事务有两次查询操作第一次查询后B事务修改了数据然后A查询又得到了更新后的值造成同一事务内查询数据不一致即多个业务操作之间相互影响。事务的属性原子性Atomicity一个事务transaction中的所有操作要么全部完成要么全部不完成不会结束在中间某个环节。隔离性Isolation数据库允许多个并发事务执行隔离性就是指并发过程中一个事务对另一个事务的可见性。这个特性使得多事务并发不会相互影响而是模拟出串行的效果。持久性Durability事务处理结束后对数据的修改就是永久的即便系统故障也不会丢失一致性Consistency在事务开始之前和事务结束以后数据库的内容始终是符合预期的。事务的这四个属性可以简称为ACID。其中只要保证了原子性、隔离性、持久性自然就满足一致性了。事务的局限性数据一致性又分为业务层面和数据库层面。业务层面的数据一致性是与业务强相关的比如秒杀活动不能让库存变成负数。而数据库层面的数据一致性则是通用的它只保证一个事务的执行是原子的、多个事务并发执行的结果和这些事务按照某种顺序串行执行的效果是一致的。还是秒杀活动的例子在该业务层面我们必须保证库存不能为负数那么难道数据库层面也要这样保证吗如果使用数据库的业务不是秒杀业务而是一个库存可以为负数的业务呢业务是多种多样的数据库不应该也无法预测业务是什么需求所以它只保证最基本数据一致性即使这会导致业务错误业务正确性由业务自己通过数据库加锁或者其他什么方法来保证。事务的操作相关命令begin //开启一个事务 start transaction //开启一个事务 savepoint name; //设置一个保存点它的名字是name rollback to name; //回滚到保存点的位置 rollback //回滚在事务中做的所有操作 commit //提交事务,提交之后代表整个事务的结束实操单sql事务实际上我们平时在mysql中执行的单sql语句也会被封装成事务只不过他们是自动提交的下面举个例子事务的原子性在‘事务的操作’一节中的两个个例子可以看到mysql实现了事务的原子性即一但事务执行期间发生意外会自动回滚。事务的隔离性Mysql中分为四种隔离级别不同的隔离隔离级别也就代表着一个事务对另一个事务的可见度串行化serializable这是事务的最高隔离级别它通过强制事务串行让事务与事务之间完全不可见也就不可能发生事务并发错误。可重复读REPEATABLE READ这是第二严格的级别事务对另一个事务的可见性松动可能见到了另一个活跃事务的修改也可能没有这取决于事务执行的时机但是它保证同一个事务内不会出现多次查表数据不一致的情况。读已提交READ COMMITTED在这个级别下事务对另一个事务的可见性进一步加大一个事务能看到其他事务的提交了的修改也不保证事务执行过程中多次查表的一致性。读未提交READ UNCOMMITTED这个级别下两个事务之间完全不隔离一个事务对另一个事务所做的动作完全可见。需要注意的是隔离级别主要针对读写来设置因为两个事务写同一行数据是一定要串行的而读同一行数据是定能并发的。串行化serializable串行化是最严格的隔离级别事务之间不会相互冲突。核心就是让多个事务在逻辑上串行执行。它保证事务串行的规则大致如下事务A查询了一张表的部分内容那么他就会给这部分内容加锁。其他事务如果想更新或者在这部分内容中插入新数据就会被阻塞直到事务A提交。事务A更新了一张表的某行数据那么他就会给这行数据加锁。如果其他事务的查询会包含这行数据或者直接想更新这行数据就会被阻塞直到事务A提交。事务A在一张表中插入一行数据那么他就会给这行数据加锁。如果其他事务的查询会包含这行数据或者直接想更新这行数据就会被阻塞直到事务A提交。上面的规则其实意味着事务A如果查询了一张表的部分内容那么其他事务在这部分内容之外查询或者更新或者插入都不会被阻塞。事务A如果更新了一行数据那么其他事务只要不去碰这个行数据就不会被阻塞。事务A如果插入了一行数据那么其他事务只要不去碰这个行数据就不会被阻塞。如果两个事务都是读那么不会阻塞。总而言之串行化并不是事务之间严格互斥执行而是在保证串行效果的同时尽量让事务之间可以并发提高效率。为了理解这个我来打个比方如果两个事务修改的部分根本就不一样那么即使他两严格串行得到的结果和并发也是一样的还不如并发。如果两个事务都是读那么就更没有阻塞的必要了。注这个级别下是可能出现死锁的情况比如事务A修改了第三行数据接着事务B修改了第二行数据接着事务B想访问第三行数据但是被阻塞了然后事务A访问第二行数据也被阻塞了结果就是死锁互相等待Mysql此时会Kill一个事务。可重复读REPEATABLE READ可重复读的隔离级别略低于串行化它的核心是保证同一个事务内多次查表得到的结果都是一致的不会出现在同一个事务内前后查询数据不一致的情况。但是不保证串行。可重复读级别保证了事务查表结果一致但其他事务在此期间也可以更新数据。不难想到事务A和事务B访问的一定不是同一块数据而是分别访问该数据的旧版本和新版本否则不会出现一条数据两种内容。也正是因为访问的不是同一块数据所以在可重复读级别下两个事务读写同一行数据可以并发执行效率比串行化好一点。之所以说它隔离级别更低是因为在可重复读级别下事务看到的数据可能不是最新的这就有可能造成一些问题。比如说有个应用的功能是实时性的获取用户的状态在获取用户状态这个事务执行过程中运行完成一个修改状态的事务但是我们还是返回旧的状态。当然这种隔离级别是否有问题是和业务之间挂钩的如果业务对这方面不敏感那么最佳选择就是可重复读级别因为效率更高。可重复读是mysql的默认隔离级别因为他是安全性与效率之间的最佳权衡。实操读已提交READ COMMITTED读已提交的隔离性又低于可重复读它的核心是一个事务能看到其他事务的提交了的修改同时不保证事务执行过程中多次查表的结果一致。实操读未提交READ UNCOMMITTED这个隔离级别很简单读者可以认为根本没有任何读写控制即一个事务对数据修改后另一个事务立马可以看到不需要提交后才能看到。实操快照读和当前读我们在上面介绍某些隔离级别的时候涉及到了版本的概念。实际上对数据库中有两种“读”的方式这两种方式走的流程不一样因此会产生不同的效果快照读进行版本读取有可能读到的是旧版本也有可能是新版本。当前读直接读取数据库不管什么版本读取到的一定是此时此刻最新的数据。注“读”的概念很宽泛包括增删改查。对数据库的增删改操作一定是当前读因为逻辑上历史是不能改变的只能改变最新的数据。幻读脏读不可重复读脏读看到了别人没有提交的数据(读未提交级别会出现这种问题)不可重复读在同一个事务内多次查询结果不一样(读已提交读未提交会出现这种问题)幻读一个事务插入数据另一个事务查询并发现新插入的数据。幻读属于不可重复读的一种。读已提交读未提交会出现这种问题。查询和修改隔离级别SELECT global.transaction_isolation; --查看全局隔级别 SELECT transaction_isolation; --查看本会话隔级别 //每次登录mysql会话隔离级别都会被设置成全局隔离级别 //设置会话/全局隔离级别 SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE]后记在Mysql中有三个基本的原则无论哪种隔离级别都遵守两个事务对同一块数据修改其中一个事务会阻塞直到另一个事务提交或回滚。保证不出错。两个事务对同一块数据进行读那么这两个事务都不会阻塞而是并发。提高效率两个事务分别读写不同数据那么这两个事务都不会阻塞而是并发。提高效率RR和RC隔离原理MVCCMysql中每一个事务都是一个类对象都有自己的事务ID事务ID越大表示该事务启动的越晚。Mysql表的隐藏列DB_TRX_ID 6 byte记录创建这条记录/最后一次修改该记录的事务的IDDB_ROLL_PTR : 7 byte回滚指针指向这条记录的上一个版本DB_ROW_ID : 6 byte隐含的自增ID隐藏主键如果数据表没有主键就会有这个隐藏列InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引。假设我们创建了一张表create table if not exists student( name varchar(11) not null, age int not null );实际上这个表的一条记录会有这几个字段undo日志这个可以看做是Msyqld使用的一个较大缓冲区会存放某条记录的历史版本DB_ROLL_PTR记录的就是历史版本在undo日志中的地址。为了理解这个接下来举个例子假设现在有一个ID为10的事务它包含的sql语句是begin; insert into student values (张三,28); update student set age20 where name张三; commit;事务执行结束得到的结果就是假设现在又来了一个ID为11的事务它包含的sql语句是begin; update student set age80 where name张三; commit;执行事务的结果就是不过实际上undo日志的内容总会销毁的毕竟undo日志大小有限一般来说一个事务提交该事务所形成的版本链就会销毁不过这也不一定万一旧版本有人在用呢。说了这么多其实就是想说一个事务在修改数据之前会把旧版本保存下来并用链表连接。思考一下如果一个事务是新插入数据那么它的旧版本怎么保存undo日志当然会保存一些东西来标志一下这里暂且不做讨论。之前说的当前读就是读undo日志之外的最新内容而快照读读的可能就是undo日志中的旧版本内容。ReadViewclass 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生成时刻系统正活跃的事务IDup_limit_id记录m_ids列表中事务ID最小的ID(没有写错)low_limit_idReadView生成时刻系统尚未分配的下一个事务ID也就是目前已出现过的事务ID的最大值1(也没有写错)creator_trx_id创建该ReadView的事务ID创建这个类对象的同时他会把创建这个对象这一刻正在活跃的所有事务的IDm_ids以及还在活跃着的最早的事务的ID(up_limit_id)以及未来创建事务首先分配的ID(low_limit_id)以及创建这个对象的事务ID都记录下来。相当于给创建ReadView这一刻的事务模块的状态进行拍照记录。RR原理RR级别下事务A第一次进行快照读默认就是快照读的时候就会先创建一个ReadView对象保存下此刻的整个事务模块的状态包括本次查询在内事务A此后每次查询操作对每行记录都按照下面的规则进行判断如果这条记录的 DB_TRX_ID creator_trx_id|| DB_TRX_IDup_limit_id,那说明该记录是被事务A自己修改的或者修改它的事务已经提交了此时事务A应该看到这条记录显示这个记录。如果这条记录的DB_TRX_ID low_limit_id说明这是新启动的事务修改的事务A不应该看到此时我们沿着undo日志中的版本链找到上一版本的记录并对该记录也做判断。如果这条记录的up_limit_idDB_TRX_ID low_limit_id DB_TRX_ID在m_ids中说明这个记录是正在活跃的事务修改的事务A不应该看到此时我们沿着undo日志中的版本链找到上一版本的记录并对该记录也做判断。如果这条记录的up_limit_idDB_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日志中的历史版本覆盖到数据库。