十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL 事务、四大隔离级别、MVCC完整原理笔记|案例+问题演示

MySQL 事务、四大隔离级别、MVCC完整原理笔记|案例+问题演示 1、为什么需要事务1.1 并发更新问题案例售票系统1业务场景车票表ticketsid10上海→北京剩余票数12并发流程客户端A判断票数0 → 执行票数‑1更新客户端BA还没更新数据库B查到票数同样大于0也执行票数‑1更新3问题同一张票被卖出两次并发下直接修改CURD会出现数据错乱。1.2 事务定义1事务一组DML语句逻辑上强相关作为不可分割整体。要么全部执行成功要么全部执行失败回滚。2典型场景删除用户需要同时删除用户基础信息、帖子、评论、关联数据整套操作属于一个事务。1.3 事务四大特性 ACID1原子性(Atomicity)不可分割。事务内部操作要么全成功要么全失败回滚到事务开始不会执行一半。底层依靠undo日志实现。2一致性(Consistency)事务执行前后数据库完整性约束不被破坏。数据从一种合法状态变为另一种合法状态。一致性是最终目标由原子性隔离性持久性业务代码共同保障。注意MySQL只提供技术支撑最终一致性需要程序员业务逻辑保证。3隔离性(Isolation)多个事务并发读写同一份数据事务之间互相隔离避免并发冲突带来数据异常。由隔离级别控制依靠锁 MVCC实现。4持久性(Durability)事务commit提交之后对数据的修改永久写入磁盘就算数据库宕机、系统故障修改不会丢失。依靠redo日志实现。1.4 引擎支持事务说明1InnoDB默认引擎完整支持事务、行锁、外键show engines; --查看所有存储引擎 show engines \G --竖版显示InnoDBTransactions: YES 支持事务、支持保存点MyISAMTransactions: NO 不支持事务MEMORY内存引擎不支持事务2结论只有InnoDB引擎的表才可以使用事务。2、事务基础语法与提交方式2.1 事务两种提交模式1自动提交autocommitON单条DML执行完立刻自动commit2手动提交autocommitOFF或者手动begin开启事务必须手动执行commit才持久化。2.2 查询/修改自动提交状态--查看自动提交开关 show variables like autocommit; --关闭自动提交 SET AUTOCOMMIT0; --开启自动提交 SET AUTOCOMMIT1;重点结论只要执行begin; / start transaction;开启事务就必须手动commit不受autocommit开关影响。2.3 完整事务基础指令begin; --开启事务 等价 start transaction; savepoint save1; --设置保存点save1 insert into account values(1,张三,100); savepoint save2; --设置保存点save2 insert into account values(2,李四,10000); rollback to save2; --回滚到指定保存点只撤销save2之后操作 rollback; --直接回滚撤销整个事务所有操作回到事务最开始 commit; --提交事务数据永久落盘回滚失效2.4 4组异常宕机实验前提说明客户端崩溃 直接关闭navicat/命令行窗口不是执行rollback手动回滚模拟程序异常宕机、断连。MySQL服务端本身正常运行只是客户端连接断开。1实验1开启事务beginDML写完未commit客户端直接崩溃操作步骤1. 执行 begin; 手动开启事务2. 执行 update / insert / delete 修改数据DML语句3. 不执行 commit;直接关掉当前查询窗口客户端崩溃断开连接现象MySQL检测到客户端连接异常断开自动执行rollback回滚所有本次事务内的修改全部撤销数据库数据完全没有变化。底层原理事务只是在内存undo日志中记录变更还没有把变更持久落到磁盘数据页。连接断开 → MySQL判定事务异常终止触发自动回滚丢弃本次所有修改。结论只要事务没有commit客户端宕机修改一定消失不会保存。2实验2事务已经commit客户端崩溃操作步骤1. begin;2. 执行DML修改数据3. 手动执行 commit;事务提交完成4. 关闭客户端窗口现象commit执行完成时数据变更已经通过redo日志持久化写入磁盘。之后客户端就算崩溃、断开连接完全不会影响已经提交成功的数据修改永久保留。底层原理commit成功 事务修改已经刷入磁盘事务生命周期已经彻底结束。客户端只是“使用者”提交之后数据属于数据库和客户端存活无关。结论commit一旦执行成功数据永久落地客户端崩溃不会丢数据。3实验3autocommit关闭不写begin直接执行DMLautocommit0 关闭自动提交很多人误区不写begin就没有事务实际不是操作步骤1. 设置 set autocommit 0;2. 不执行 begin;直接执行 update / insert3. 不手动commit直接关闭窗口客户端崩溃现象整条DML语句自动被包裹在一个隐式事务当中客户端断开 → MySQL自动rollback本次修改全部撤销。底层原理autocommit关闭状态下只要执行DML自动开启隐式事务必须手动commit才会保存不显式写begin也存在事务。连接消失隐式事务未提交自动回滚。结论autocommit0即使省略beginDML依然处于事务宕机直接回滚。4实验4autocommit开启不写begin直接执行DMLMySQL默认配置 autocommit 1开启操作步骤1. set autocommit 1;默认状态2. 不写begin直接执行单条update / insert / delete3. SQL执行完毕立刻关闭客户端现象单条DML执行结束之后MySQL立刻自动执行一次隐式commit这条修改直接持久化磁盘就算马上客户端崩溃数据不会回滚修改保留。底层原理自动提交开启每一条DML语句自成一个独立事务SQL执行完成瞬间自动提交事务瞬间结束。只有手动begin开启事务之后才会把多条SQL合并成一个大事务。结论默认模式下单条增删改执行完直接提交宕机不会丢失这条修改。4组实验对比总结场景是否commit客户端崩溃结果begin DML未commit❌未提交自动回滚数据消失begin DML commit✅已提交数据永久保存autocommit0直接DML❌隐式事务未提交自动回滚数据消失autocommit1直接DML✅单句自动提交数据永久保存面试提问标准答案Q客户端突然宕机没执行commit数据会保存吗A1. 如果是显式begin开启事务没有commit客户端断开MySQL自动回滚修改全部失效。2. 如果autocommit开启没有写begin单条DML执行完自动提交宕机不影响。3. autocommit关闭即使不写beginDML属于隐式事务宕机同样自动回滚。坑点提醒实操易错1. 只是关闭SQL查询标签页客户端断开不等于服务端崩溃2. autocommit是会话级别新开窗口会恢复默认值13. 隐式事务和显式事务回滚逻辑完全一致只是不需要手写begin4. 只有commit成功刷盘数据才真正安全中途断电/宕机都会丢失未提交修改5. begin / start transaction 开启事务必须commit才保存崩溃自动回滚6. 不开beginautocommitON →每条SQL自动提交autocommitOFF →DML等待手动commit7. commit之后无法再rollback。3、事务隔离级别3.1 隔离性产生背景1数据库会大量客户端、多线程并发访问同一张表。2一个事务执行分为执行前 →执行中 →执行完成。事务执行中途其他事务读写数据会产生互相干扰出现脏读、不可重复读、幻读。3隔离级别给事务设置不同隔离强度控制事务之间互相干扰程度。隔离越强并发性能越低需要权衡取舍。3.2 4种隔离级别定义从低→高1READ UNCOMMITTED 读未提交一个事务可以读到另一个事务还没有commit的数据。问题脏读、不可重复读、幻读全部存在。生产几乎不用不加锁速度最快。2READ COMMITTED(RC) 读已提交事务只能读取其他事务已经commit之后的数据。可以解决脏读不可重复读、幻读仍然存在。Oracle默认隔离级别。3REPEATABLE READ(RR) 可重复读取MySQL InnoDB默认隔离级别。同一个事务内多次select读取同一条数据读取结果永远一致不受外部事务update修改影响。解决脏读、不可重复读存在幻读。补充MySQL InnoDB通过Next‑Key Lock临键锁在RR级别业务层面消除幻读现象不是SQL标准定义消除4SERIALIZABLE 串行化最高隔离级别。所有读写全部加锁事务串行排队执行完全杜绝并发问题。无脏读、无不可重复读、无幻读并发极差超时锁冲突严重几乎不用于生产。3.3 并发三大异常现象详解1脏读(Dirty Read)事务A读到事务A未提交的修改数据一旦A回滚读到的数据就是无效脏数据。仅【读未提交】出现。2不可重复读(Non‑Repeatable Read)同一个事务内两次读取同一行数据中间被其他事务updatecommit两次读取数值不一样。重点update/delete修改单行发生于读未提交、读已提交3幻读(Phantom Read)同一个事务多次按条件查询一批数据其他事务执行insert新增符合条件的数据再次查询行数变多像幻觉一样多出来一行。重点insert新增记录。RC级别一定会出现幻读标准RR理论存在幻读MySQL InnoDB RR通过临键锁屏蔽幻读。3.4 查看设置隔离级别SQL--查看全局隔离级别 SELECT global.transaction_isolation; --查看当前会话隔离级别 SELECT session.transaction_isolation; --简写查看当前会话 SELECT transaction_isolation; --设置【当前会话】隔离级别仅当前窗口生效 set session transaction isolation level repeatable read; --设置【全局】隔离级别 set global transaction isolation level read uncommitted;⚠修改全局隔离级别后必须关闭客户端重新打开终端连接MySQL设置才完全生效。补充说明1. set global 修改后已经打开的会话不变新连接才生效重启数据库会复原2. 想要永久生效去 my.cnf/my.ini 添加配置transaction-isolation REPEATABLE-READ3. 四种隔离级别可选READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READMySQL默认SERIALIZABLE3.5 隔离级别现象对照表隔离级别脏读不可重复读幻读加锁情况READ‑UNCOMMITTED✅发生✅发生✅发生不加锁READ‑COMMITTED(RC)❌不会✅发生✅发生不加锁REPEATABLE‑READ(RRMySQL默认)❌不会❌不会理论存在InnoDB锁解决不加锁快照读SERIALIZABLE串行化❌不会❌不会❌不会全部加共享锁重点区分不可重复读单行数据内容被修改幻读返回结果集行数变多/变少新增删除4、MVCC 多版本并发控制4.1 MVCC是什么1全称Multi‑Version Concurrency Control 多版本并发控制2作用无锁并发控制方案解决读写冲突写操作update/delete/insert加锁普通select【快照读不加锁】直接读取历史快照版本读写互不阻塞大幅提升数据库并发性能。3前提仅InnoDB支持只作用于RC、RR隔离级别普通select快照读两种读模式区分• 快照读普通selectselect * from table;不加锁读取历史版本快照MVCC生效• 当前读加锁读select * from user lock in share mode; --共享锁 当前读 select * from user for update; --排他锁 当前读 update / delete / insert --本身就是当前读一定加锁当前读不走MVCC直接读取磁盘最新数据会加锁阻塞其他写事务。4.2 InnoDB每行隐藏3个系统字段MVCC底层每张InnoDB表自带3个隐藏列用户不可直接select查询1DB_TRX_ID6字节最后修改这一行数据的事务ID每次update/delete赋值当前事务ID事务ID全局自增越大事务越晚执行。2DB_ROLL_PTR7字节回滚指针。指针指向undo log日志中上一个历史版本数据地址形成版本链表。3DB_ROW_ID6字节隐藏自增主键。如果建表没有设置主键InnoDB自动使用该字段作为聚簇索引。补充delete不会直接删除行底层只是修改隐藏flag标记为删除旧版本存入undo log。4.3 Undo Log回滚日志1概念一块内存缓冲区保存数据修改之前旧版本副本2流程演示版本链形成①原始数据name张三 age28DB_TRX_IDnull回滚指针null②事务id10执行update set name李四修改前先把原始【张三】完整拷贝存入undo log修改磁盘行name李四DB_TRX_ID10DB_ROLL_PTR指向undo log内张三那条旧记录③事务id11执行update set age38修改前把【李四 age28】拷贝存入undo log修改磁盘行age38DB_TRX_ID11回滚指针指向undo log李四那条记录undo内部多条历史记录依靠回滚指针串联 →版本链表版本链事务rollback回滚顺着DB_ROLL_PTR指针读取undo旧数据覆盖回去。事务commit之后对应的undo日志后续会被后台线程purge清理释放。insert说明插入没有历史版本undo只保存用于回滚的记录事务提交之后undo直接清空不会加入版本链。4.4 ReadView 读视图快照可见性判断核心1ReadView执行快照读普通select一瞬间生成的快照视图用来判断当前事务能不能看见这条版本链上某一条历史记录。❗关键区别RC/RR• RC级别每一次普通select查询都会生成全新ReadView →每次读取最新视图产生不可重复读• RR级别同一个事务内【第一条普通select】的时候只生成一次ReadView后面所有select复用同一个ReadView →全程快照不变实现可重复读。2ReadView内部4个核心成员变量class ReadView{ private: trx_id_t m_low_limit_id; //高水位尚未分配的下一个事务ID这个ID事务快照之后开启不可见 trx_id_t m_up_limit_id; //低水位活跃事务ID列表里面最小id小于该id事务一定已经提交可见 ids_t m_ids; //快照【瞬间】所有**正在运行、未commit**活跃事务ID集合 trx_id_t m_creator_trx_id; //创建这个ReadView自身的事务ID }3可见性判断逻辑拿行数据DB_TRX_ID 和ReadView对比给定一条版本链记录的DB_TRX_ID① DB_TRX_ID m_up_limit_id →事务在快照创建前已经提交 ✅可以看见② DB_TRX_ID m_low_limit_id →快照之后才开启的事务 ❌看不见③ m_up_limit_id DB_TRX_ID m_low_limit_id判断DB_TRX_ID 是否存在于m_ids活跃事务集合- 在m_ids事务还没提交 ❌看不见- 不在m_ids事务提前提交 ✅可以看见如果当前版本不可见顺着DB_ROLL_PTR往undo版本链往前找循环判断上一个历史版本直到找到第一条符合可见条件的版本返回给用户。找不到返回空。4.5 RC与RR本质核心区别1RC读已提交每执行一次select快照读就新建一个ReadView。每次查询视图都是最新能够看到别的事务刚刚提交的数据 →出现不可重复读2RR可重复读MySQL默认同一个事务第一条普通select才生成ReadView后续所有select全程复用同一个ReadView快照视图固定不变无论外部事务怎么update提交快照读取的数据永远一致 →实现可重复读4.6 MVCC完整执行流程总结1执行普通select快照读2RC每次新建ReadViewRR仅事务第一次select新建ReadView3读取聚簇索引当前最新行取出DB_TRX_ID4使用ReadView规则判断该行版本是否可见5不可见沿着回滚指针DB_ROLL_PTR遍历undo log版本链表逐个版本做可见性校验6找到第一条可见历史版本组装数据返回无可见版本返回空结果集5、隔离级别实操实验5.1 实验前置准备建测试账户表create table if not exists account( id int primary key, name varchar(50) not null default , blance decimal(10,2) not null default 0.0 )ENGINEInnoDB DEFAULT CHARSETutf8;开启两个MySQL终端【终端A、终端B】模拟并发事务。实验1READ‑UNCOMMITTED读未提交脏读演示1全局设置隔离级别set global transaction isolation level read uncommitted;重启两个终端查询隔离级别确认生效2终端Abegin; update account set blance123 where id1; --⚠ 没有执行commit事务没提交3终端Bbegin; select * from account;✅现象终端B直接读到A未提交 blance123 →脏读一旦A执行rollback回滚B读到的数据完全虚假无效。实验2READ‑COMMITTEDRC读已提交不可重复读演示1设置全局隔离级别为read committed重启客户端2终端Abegin; update account set blance321 where id1; --未commit3终端Bbegin; select * from account; --看不到321读取旧值1234终端A执行commit;提交事务5终端B再次执行完全一样select现象第二次查询读到blance321✅同一个事务B两次相同查询返回结果不一样 →不可重复读原因RC每次select生成全新ReadView可以看到刚刚提交的数据。实验3REPEATABLE‑READRR可重复读MySQL默认1设置隔离级别 RR重启客户端2终端Abegin; update account set blance4321 where id1; commit;3终端Bbegin; select * from account; --第一次查询生成唯一ReadView读取旧值321 --此时A已经commit更新数据 select * from account; --再次查询复用同一个ReadView仍然读取旧值321✅现象事务B无论查询多少次只要事务不结束数值永远是第一次快照的值不会看到A提交后的新值。可重复读只有B执行commit结束事务新开事务查询才可以看到4321。幻读补充实验insert新增1终端A begin; insert一条id3王五数据 commit2终端B RR隔离 begin第一次select看不到王五A已经commitB再select依旧看不到新增王五原因RR固定ReadView快照看不到快照之后insert提交的数据屏蔽幻读现象。实验4SERIALIZABLE串行化1设置隔离级别串行化2终端A begin; update修改id13终端B begin; select * from account;✅现象B的select直接阻塞排队直到A执行commit之后B查询才返回结果。所有读写操作串行排队执行完全没有并发性能极差。6、面试高频问答总结1.Q事务ACID靠什么实现A原子性undo log回滚日志持久性redo log重做日志隔离性锁 MVCC一致性是最终目标由原子性、持久性、隔离性 业务代码共同保证2.QRC和RR下MVCC的ReadView区别ARC读已提交每执行一次select就重新生成ReadView可以读到别的事务已提交的数据RR可重复读事务内第一条select时生成1次ReadView整个事务复用全程看到同一套快照3.Q快照读、当前读区别A快照读普通select不加锁走MVCC读取历史快照版本当前读update / delete / insert / select ... lock in share mode / for update加锁读取磁盘最新行数据不走MVCC快照4.QMySQL RR级别为什么看不到幻读ASQL标准里RR隔离级别本身不能解决幻读InnoDB实现上1. MVCC快照读看不到别的事务新插入的数据2. 临键锁 Next‑Key Lock记录锁间隙锁锁住间隙阻止其他事务插入新数据双重机制业务层面消除幻读现象5.Qundo log、redo log作用区分Aundo log保存数据修改前旧版本用途事务回滚、构建MVCC版本链redo log保存数据修改操作记录用途崩溃宕机之后恢复数据保证事务持久性7、关键易错笔记1MyISAM完全不支持事务不要用MyISAM做转账、订单业务。2begin开启事务之后autocommit开关失效必须手动commit。3隔离级别越高并发吞吐量越低互联网业务绝大多数使用RC或者RR。4MVCC只解决读写冲突写写冲突依旧依靠锁机制解决。5隔离级别全局修改必须重连MySQL客户端设置才完全生效已经打开窗口不会改变。
返回列表