MySQL——事务

发布于:2025-06-03 ⋅ 阅读:(23) ⋅ 点赞:(0)

目录

问题

什么是事务 

为什么会有事务

事务版本支持

事务提交方式 

事务常见操作

事务隔离级别

查看与设置隔离性

事务四种隔离级别 

读未提交

读提交 

不可重复读

串行化

一致性 

理解隔离性

4个隐藏字段

undo日志

MVCC

Read View

RR 与 RC 本质区别


问题

        通过前面的MySQL文章,我们已经掌握了基本的CRUD操作,能够编写日常的sql语句进行使用,但是光有sql语句操作还不行,如果不对操作加以控制,就会出现各种问题:如在买票中两个客户端同时并发进行买票可能出现:明明票只剩一站却卖出去了两张?

        那要满足什么才能解决上面的问题呢?这就需要我们来学习MySQL中的事务

什么是事务 

        事务有一组DML语句所组成一个整体,这些语句在逻辑上具有相关性且有一个特性:要么全都执行成功,要么全都不执行(原子性);这些是MySQL所提供的一种机制来让用户使用时达到某种效果,也就是在内部为创建出一个一个事务对象并通过自己的方式管理起来,这在使用者角度是不知情的;比如在学校的MySQL数据系统中,如果一个人毕业了,学校管理人员就要对该同学的相关信息作删除,通过多条sql语句也就是事务进行数据库信息的更新

        此外,事务还要满足四种属性,简称为 A(Atomicity)  D(Durability) C(Consistency) I(Isolation)

原子性 一个事务要么全都执行完成了,要么都不执行:也就是在执行过程进行回滚到原来的状态
持久性 事务执行完成后,修改的数据一定是持久化的,不随系统原因而出现错误
一致性 事务之前与之后的数据具有完整性不能被破坏,这就意味着读入的数据具有可预期性
隔离性 MySQL允许数据被并发读/写,可以保证数据在被并发交叉访问时不会存在数据的不一致性

为什么会有事务

        有了事务,从当前我们所能认识到的就是:在并发访问时能够保证数据的一致性;但这仅仅是事务所存在的价值;MySQL本身出现时是没有事务的,在时间的发展中程序员开发使用时发现了数据库当中存在的各种问题,有了问题后,作为维护MySQL的开发者就在内部帮我们实现出事务,通过事务解决当前存在的问题,提高程序员开发效率;也就是说事务本质上是为了应用层而服务的,而不是伴随着数据库的出现而出现的

事务版本支持

        在MySQL的各种引擎中,只有 InnoDB 支持事务

事务提交方式 

        常见的方式有两种:(默认)自动提交 与 手动提交

 

事务常见操作

        先设置隔离级别

         正式进行操作:需要启动两个mysql进行观察,启动一个事务begin,之后的sql语句就都是事务;使用 savepoint 定位,后面后悔了可以回滚到定位处

        也可以不指名直接进行回滚,但这样就回滚到最开始设置节点的时候

        启动事务begin 后完成一些操作,配合提交commit 把上面的事务进行持久化

        如果此时设置 autocommit 为 OFF:也就是关闭的状态,会怎么样呢?

        可以看到:两者的结果是完全一样的,说明 autocommit 不起作用 

        如果设置 autocommit ON,不使用begin,直接插入数据,后面意外退出了,会怎么样呢? 

        可以看到我们的之前sql语句都进行回滚了

        难道是 autocommit干的吗?我们可以来验证下:mysql服务会回滚,那么最后加上commit后不会滚就是是它干的

       结论:auotcommit 开启不会影响我们设置beign之后的事务情况,而是在我们正常编写sql语句时如果异常退出了,会该我们进行回滚(相反 autocommit 关闭后则无现象)这样保证了sql语句的原子性;而设置 begin 之后的sql语句需要 commit 把事务进行提交后才能对数据进行持久化,这又体现出事务的持久性;为什么 autocommit 会对编写一条一条sql语句有影响?  因为一条sql语句本质上也是一个事务,只是我们站在应用层的角度没感觉而已!

事务隔离级别

         事务有了原子性,保证事务要么不执行,要么执行完成,不能在执行过程中受到干扰;谁来保证中间过程不受干扰呢? 就是事务特性中的——隔离性;隔离性就好比:你父母和你出生的时间(事务开始)不同,你父母比你先出生所经历的场面就更多一点,但你出生后,你们所看到场面就在时间线上重叠了,往后看到的场面就相同了;在你出生之后能不能看到在你出生之前你父母所经历的场景呢?(假设父母对你是保密的) 一定是看不到的!因为这段时间是处于隔离的;当父母走到生命结束后(事务结束),在家里面就找到了这段你所不知情的经历(事务CRUD操作)

        根据隔离级别的不同,共有四种隔离级别

读未提交 事务之间没有如何隔离性可言
读提交 只有其中一方的事务结束了,另一方的事务才能看到
可重复度 其中一方的事务结束了还不够,还要另一方的事务结束了才能看到
串行化 加共享锁后事务按照顺序进行线性执行(写写场景)

查看与设置隔离性

        有三个隔离性:全局隔离,会话隔离,默认隔离

        没修改前

        修改会话隔离,只会影响当前会话隔离与普通隔离变量,不会影响全局隔离与其它用户

        而修改了全局隔离,当前所有mysql用户的全局隔离都发送了变化,退出后另起mysql也一样

事务四种隔离级别 

读未提交

        读未提交隔离表现在:一方执行的事务在读数据时读到了另一方执行的事务未 commit 的CRUD操作,这种现象叫做脏读(不合理),与事务原子性相违背

读提交 

        在执行事务的一方进行CRUD操作时只有commit后才会被另一个执行事务的一方读到,称为读提交

        读提交也称为不可重复读:也就是读数据时可能得到的数据不一致(被并发的事务提交后更新过了),这有问题吗? 

        这就好像在公司里,发奖品时要对员工的薪资范围给指定的奖品,一个程序员小张在数据库中开启事务将什么薪资范围内的人给找出来时,由于小王业绩突出,公司给它临时加薪了,薪资跳到了上一个等级,这时就又有一个程序员小李开启事务并发执行中修改小王的信息;由于小张在查到之前小王所在薪资范围中小李还没来得及修改,查到小王就把它归到旧薪资范围中,这时再次查时下一个薪资范围内的人之前小李就把小王的薪资修改后提交了,此时小张就又查到了小王在新的薪资范围中,但由于小张是一个叫粗心的程序员,没有注意后就把名单交给了管理人员,此时小张就被破口大骂:小王怎么可能出现在两个薪资范围内啊,这就不知道按照名单派发奖品的话叫公司的人怎么看我啊!所以说读提交存在一定的问题

不可重复读

        只有一个在执行的事务提交后,另一个在执行的事务也提交完成,才能看到被修改后的数据;这种隔离性比较推荐,也是mysql默认设置的隔离性

        但这并不是所有的数据库都能做到这样,在有些低版本的数据库,如果一个执行的事务插入数据还每commit时,另一个执行的事务是有可能查到新增加的数据的,这种情况称为幻读:本来是重复读了怎么它新增加的数据我还能看到啊!这种情况是由于低版本的数据库无法解决的,在比较新的数据库通过用Next-Key锁(GAP+行锁)解决了

串行化

        两个事务并发执行时在查数据时不阻塞,如果一个事务在进行CURD操作后将被阻塞直到另一个事务commit后阻塞才取消;或者一个事务执行先进行CURD操作,另一个事务不管执行什么操作都将被阻塞,直到先执行操作的事务commit后阻塞才取消

        阻塞是mysql在内部进行加锁来实现的 

一致性 

        到这里事务的前三种特性:原子性,隔离性,持久性,我们就能理解了,但一致性一直没说,现在就来谈谈一致性:事务执行的结果,数据是要从一种状态转到另一种状态保证事务的原子性,但如果事务在执行过程之中没有commit异常退出了,mysql为了保证事务的原子性和根据事务是否设置来判断当前的状态,不可能出现之前和之后两种不一致状态,这是mysql从技术角度上来帮助我们确保事务的原子性,而一致性恰恰就是通过原子性来保证的(事务只存在一种状态),但还是有点片面:如果一个二刀子程序员在编写事务时故意将本来应该修改的数据(如转账后对方账号上增加金额)不进行修改后就把commit了,出现投诉就把锅甩到mysql身上,这就不应该了;所以说保证事务一致性不仅是要从mysql技术上支持(AID保证C),也要程序员本身的配合

理解隔离性

        数据库并发下共有三种场景:

  • 读-读:不存在任何问题,不需要进行控制;
  • 读-写:有线程安全问题,可能会造成事务隔离性问题,遇到:脏读,不可重复读,幻读
  • 写-写:有线程安全问题,可能出现数据更新丢失问题

        mysql对事务的管理:每个事务都有自己的事务id,通过事务id(id依次增大)来进行事务先后顺序;一个事务先到,一个事务后到,在mysql内部一定存在的很多事务,怎么对事务进行管理? 先描述,后组织,把事务设计成一个一个的事务对象,通过“链表”的方式进行链接起来,对事务的管理转化成对链表的增删查改;为了对事务有一定的理解后才方便展开接下来的内容

        事务之间 读-写 出现的无锁并发问题mysql通过多版本控制(MVCC)来解决,而理解MVCC我们要来认识三个相关周边知识

4个隐藏字段

  • DB_TRX_ID:最近一次的事务进行插入/删除/修改当前行数据的事务id
  • DB_ROLL_PTR:回滚指针,指向上一次未被修改前的当前行数据
  • DB_ROW_ID:隐藏的自增主键,也就是之前我们说的B+树没有主键时通过默认主键构成
  • flag:标记删除字段,当前行数据要被删除时不是真的被删除了,而是进行标记,等后续数据进行刷盘删除

        创建一个测试表 

        你以为只有一行两个字段,实际上

name age DB_TRX_ID DB_ROW_ID DB_ROLL_PTR
张三 28 null 1 null

undo日志

        MySQL 将来是以服务进程的方式,在内存中运行;我们之前所讲的所有机制:索引,事务,隔离性,日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的;所以,我们这里理解undo log,简单理解成,就是 MySQL 中的一段内存缓冲区,用来保存日志历史数据即可

MVCC

        理解MVCC我们要有场景来进行模拟:

        当前有事务10正在对张三的数据进行修改:mysql先要把原始数据拷贝到undo log中,接着要加行锁保证该数据只有事务10可以修改,修改完成后的数据要把DB_TRX_ID修改成10,DB_ROLL_PTR填充原始数据地址方便下次后悔时进行回滚,最后事务10提交后释放锁;

         后来有新的事务11来了,此时它看到了被事务10修改后的数据,它要对该数据进行修改也是参照以上的上面的步骤,加锁,字段修改,提交,释放锁

        

        对于保存在undo log中的数据,我们称为历史版本链,各自用回滚指针链接起来;里面一个一个的数据我们称为快照,而当前最新的数据我们称为当前读;

问题:以上更新都是基于原来的数据上进行对数据的改操作,那么如果是增删操作呢?undo log怎么保存?

        对于删的数据,我们一定要记得不是原来意义上的删除,只是在对应删除的字段的flag进行标记后保存在undo 日志中,而对应增操作,新增的数据也保存在历史版本链中,同时mysql对应历史版本的数据还保存了修改指令的相反操作,也就说:你insert的语句它保存了delete语句,当你回滚时它就调用该语句来执行不就进行还原了吗!

        undo 日志我们把它认为是在内存中的一块临时缓冲区,随着历史版本数据的增多,总有满的时候吧?

        undo log中设置了自动清理机制,根据事务需要进行保存与清理,不会有数据的堆积问题

select只会对数据查询,不会对数据进行进行修改操作,因此就没必要为select设置多版本,但是:select怎么知道读的数据是历史版本,还是更新后的版本?

        读取历史版本的数据,我们称为快照读;读取更新后的数据,我们称为当前读;对于多个事务并发执行CRUD操作,select如果要进行当前读,是需要加锁的(为保证事务的原子性),这称为串行化;select如果是快照读,是不需要加锁的:因为select读的是历史数据,事务修改的是最新数据,两者互不影响的;而且不加锁用能够并发执行,效率是很快的(可重复读),这也是MVCC的意义所在!所以说:select读的是哪个版本的数据完全是取决于事务当前的隔离级别!

        一个事务虽然在概念上来说是原子性的,但一个事务实际在执行过程中有:begin -> middle -> commit ,并发执行时可以不仅只有你一个事务在进行CRUD操作哦,执行是事务执行过程是交织在一起,那么先来的事务应不应该看到后来的事务修改的数据呢? 原子性的存在当然是不应该看到的,但mysql要怎么实现才能保证不同的事务在执行过程中看到不同的内容?也就是隔离级别mysql是怎么做到的?

Read View

        Read View:是事务执行 快照读 操作时产生的读视图,读视图中保存着当前活跃(未提交)的事务id(id越大,表面该事务越晚执行)(通过mysql维护的一张全局事务系统获取到的),在mysql中读视图是一个类,类中处理事务id列表,还有其它成员变量,值得我们来研究

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;

    // 省略...
};

        快照读到的历史版本应不应该被我(正在快照读的事务)读到,取决于读视图中的成员变量: 当 DB_TRX_ID 比 m_up_limit_id(当前活跃的最小事务id)还要小,说明当前修改历史版本的事务已经提交了,已经成为了历史,该被我看到,或者是 DB_TRX_ID 等于 m_creator_trx_id,这不是我之前提交的吗,理所应当被我看到;当 DB_TRX_ID 比 m_down_limit_id(当前活跃的最大事务id)还要大时,说明这个事务比我晚执行但是比我先CRUD,不应该被我看到(看到了的话就不符合事务的原子性了);如果以上情况都不满足,就来 m_ids 中看看有没有 DB_TRX_ID,没有说明CRUD的事务提交了,我可以看到;有就说明当前事务正在和我进行并发执行,不应该被我看到

 

        实现策略的相关代码

RR 与 RC 本质区别

        准备工作:启用两个终端充当两个事务,创建一张测试表并插入数据方便观察

        场景1 

事务A 事务B
begin; begin;
select *from student;(快照读无影响) select *from student;(快照读无影响)
update student set age=28 where name='张三';
commit;
select *from student;(之前快照读一样)
select *from student lock in share mode;(修改后的数据)

        结果倒也正常,因为当前隔离级别是 可重复读,正常select 查询不是修改后的数据 

        场景2

事务A 事务B
begin; begin;
select *from student;(快照读无影响)
update student set age=28 where name='张三';
commit;
select *from student;
select *from student lock in share mode;(修改后的数据)

        此时我们发现:当事务A修改完后事务B还没退出呢,进行查询居然看到了事务A修改的数据  

        原因:在场景1中事务B在事务A修改前就进行了快照读操作,生成了读视图:里面保存着事务Aid,当事务A提交后,事务B进行快照读时发现历史版本的 DB_TRX_ID 在活跃的事务id列表中,以为事务A在并发执行(实际上是不在的),此时是不能进行读取的,所以事务B读到的就还是原来没修改前的版本;在场景2由于是在事务A提交后才进行快照读,此时生成的读视图中事务id列表中就没有了事务Aid,不在列表说明不是与我一同并发执行(实际上是并发执行的),历史版本已经成为了过去式,事务B理所应当能够看到!

        总结:在RR 隔离级别下快照读后生成的读视图之后是不在进行更新的,而 RC 隔离级别下每次快照读(读视图生成的情况下要)更新读视图,所以在并发执行的事务退出后读视图的事务列表中不存在了事务id,所以在没提交之前能被看到,才有了不可重复读问题;这两者隔离级别本质上是读视图要不要更新的问题

好文推荐:正确的理解MySQL的MVCC及实现原理 详细分析MySQL事务日志 InnoDB 如何避免脏读和不可重复读


网站公告

今日签到

点亮在社区的每一天
去签到