目录1. 事务基础概念1.1 什么是事务?1.2 为什么要使用事务?1.3 事务四大ACID特性2. MySQL事务实操语法2.1 支持事务的存储引擎2.2 事务基础语法2.3 事务保存点SAVEPOINT3. 事务隔离性与隔离级别3.1 并发与隔离性通俗理解3.2 四大隔离级别是什么?3.3 隔离级别并发问题read uncommittedread committed(读已提交)repeatable read(可重复读)串行化 serializable3.4 性能与安全取舍前言数据库事务ACID是Java后端、软件测开面试核心考点本文结合转账实操案例通俗拆解事务原理、实操代码、并发隔离问题。1. 事务基础概念1.1 什么是事务?事物我们可以理解为将一个东西打包成一个快递箱子对应到MySQL语句中:事物把一组SQL语句打包成一个整体,在这一组SQL语句要么全部执行成功要么全部不执行⬅️ 返回目录1.2 为什么要使用事务?既然我们在知道什么是事物的基础上,那么为什么我们要使用事物呢?举一个简单的例子:两个滑稽老铁A,B进行转账操作,A手机中余额:3000,B手机中余额:1000滑稽A给滑稽B转账1000元,正常情况下:A中余额减少1000减少成2000,B中余额增加1000,增加成2000;总额并没有发生改变,即使转账了加起来任然是4000;意外情况:假设没有事物的情况之下;一天滑稽老铁A给B进行转账转账1000元,突然滑稽老铁B手机坏了;此时转账的程序还没跑完, 滑稽老铁A已经将钱给转过去了,钱已经在A中扣除了,但是滑稽老铁B由于手机问题,B没有收到资金转账;此时就造成了一定的损失;此时总额就变成了:3000所以:在一些特定的的情况之下,事物是非常重要的,事物要么发生,要么不发生起到了非常重要的作用此时候我们不得不使用事物来防止这样的事物发生;⬅️ 返回目录1.3 事务四大ACID特性事物的四大ACID特性包括:Atomicity (原子性)一个事物中所有的操作,一定是要么发生,或者失败这两种情况,不会出现一个发生了,一个不发生这种情况,如果事物在执行过程中发生错误,就会触发roll back(回滚),回滚到事物开始前的状态;正如日常生活中的转账,这一个过程要么发生,要么失败;若 A 扣款后程序崩溃会自动回滚A、B 余额不变Consistency (一致性)事物开始时与事物开始后,数据库的完整性不会被破坏,写入的数据必须完全符合预设的规则,包括精度,关联性,以及事物执行过程中服务器崩溃后如何恢复;原子性是手段一致性是最终目标依靠原子性、隔离性、持久性共同保障一致性。就像我们刚才举例:转账时滑稽老铁A扣除了金钱,而滑稽老铁B因为手机问题没有收到钱这样一种情况;(不一样的数据)Isolation (隔离性)数据库允许多个并发事物对数据进行读写和修改,隔离性可以防止多个事物在并发执行时由于多个交叉执行而导致数据不一致;事物可以指定不用的隔离级别,在不同场景之下分别有利弊;隔离级别脏读不可重复读幻读读未提交 Read Uncommitted允许允许允许读已提交 Read CommittedOracle 默认禁止允许允许可重复读 Repeatable ReadMySQL InnoDB 默认禁止禁止允许串行化 Serializable禁止禁止禁止隔离性可以防止多个事物的干扰;日常中很简单的一个例子:两一个隔音好房间中分别有两个工人,同一个房间中的工人吐槽另一个房间中的工人,另一房间中的工人不会听见,也就相当于工人的吐槽并没有对另一个工人进行任何的干扰;具体例子会在下文讲解4.Durability (持久性)事物在结束的时候一旦commit提交成功,对数据库的数据修改永久生效即使数据库宕机、断电、服务器崩溃数据也不会丢失。依靠redo log重做日志提交时先写入 redo 日志再刷盘宕机重启后通过 redo 日志恢复已提交事务的数据。简单的例子:我们创建一个记事本,我们在写完日记的时候,保存完之后重启电脑,我们重新上线的时候,记事本中的日记依旧存在,只要你不删除,可以保存到硬件报废;⬅️ 返回目录2. MySQL事务实操语法2.1 支持事务的存储引擎InnoDB主流MySQL 5.5 及之后默认存储引擎完整支持事务、MVCC、行锁、外键满足 ACID 四大特性。绝大多数业务系统电商、支付、后台管理均使用该引擎。MyISAM不支持事务无事务、无崩溃安全恢复机制仅支持表锁并发性能差。优势查询速度快适用于静态只读场景日志归档、纯统计报表。其他引擎Memory内存表不支持事务重启数据丢失CSV/Archive仅特定场景使用无事务支持。⬅️ 返回目录2.2 事务基础语法开启事物:starttransaction或者begin提交事物:commit;emsp;emsp;执行后所有修改持久化到磁盘其他事务可见本次修改回滚事物:sqlrollback;执行后放弃当前事务内所有 DML 操作数据恢复至事务开启前状态。我们创建一个A表来进行举例子:createtableifnotexistsA(idintprimarykeyauto_increment,namevarchar(20)notnull,scoreint)当前我使用的MySQL版本是8.0以上的版本,默认存储引擎就是:InnoDB开启我们的事物-我们要插入(张三,成绩是80分的数据);starttransaction;insertintoAvalues(null,张三,80);select*fromA;此时查询的数据和我们想要插入的数据相同,但是此时我们并没有提交事物;rollback进行返回;starttransaction;insertintoAvalues(null,张三,80);select*fromA;rollback;select*fromA;此时我们就能够观察到,我们写入的数据不见了,此时这一个操作就像我们QQ或者微信中的撤回;我们再次重新插入:(张三,成绩是80分的数据)starttransaction;insertintoAvalues(null,张三,80);select*fromA;commit;查询数据:在我们提交事物之后再次进行查询;starttransaction;insertintoAvalues(null,张三,80);select*fromA;commit;select*fromA;我们发现此时的结果是一模一样的;执行后所有修改持久化到磁盘其他事务可见本次修改。⬅️ 返回目录2.3 事务保存点SAVEPOINT语法 :savepoint保存点名字我们再一次创建一张一模一样的B表来进行讲解:createtableifnotexistsB(idintprimarykeyauto_increment,namevarchar(20)notnull,scoreint)示例:此时我们想要插入(张三,80分),(李四,90),(王五,95);并在每一个中间设定事物保存点;starttransaction;insertintoB(name,score)values(张三,80);savepointpos1;insertintoB(name,score)values(李四,90);savepointpos2;insertintoB(name,score)values(王五,95);select*fromB;查询结果;此时我们指定返回到pos2位置进行查询;rollbacktopos2;select*fromB;我们可以看见结果少了王五;我们再一次进行回滚到pos1位置;rollbacktopos1;select*fromB;结果显而易见;最后回滚完;rollback;select*fromB;结果就是没有任何的数据;⬅️ 返回目录3. 事务隔离性与隔离级别3.1 并发与隔离性通俗理解并发多个客户端同时读写同一张表 / 同一行数据会出现脏读、不可重复读、幻读问题并发:通俗理解 就是多个人同时操作一份数据我们可以理解为:两个人同时去取同一张银行卡里面的钱;银行卡余额 1000 元用户 A 在 ATM 取钱同时用户 B 用手机银行转账两个人同时读取余额都看到余额是 1000各自扣 500最后账户本该剩 0结果变成 500钱凭空多出来 —— 这就是并发带来的数据错乱。隔离性 : 事务四大特性 ACID 中的 IIsolation通过事务隔离级别控制并发冲突平衡并发性能与数据安全读未提交、读已提交、可重复读MySQL 默认、串行化隔离级别越高并发性能越差级别越低并发问题越多业务需按需取舍设置和查看隔离级别:查看隔离级别:-- 全局隔离级别selectGLOBAL.transaction_isolation;-- 当前会话隔离级别仅当前窗口生效selectSESSION.transaction_isolation;-- 简写selecttransaction_isolation;设置隔离级别:方式 1临时设置重启数据库失效-- 设置全局新连接生效setglobaltransaction_isolation隔离级别名字;SETSESSIONtransaction_isolationREPEATABLE-READ-- 设置当前会话立即生效仅当前窗口setsessiontransaction_isolation隔离级别名字;SETSESSION.transaction_isolationREPEATABLE-READ;注意:设置隔离级别的时候,隔离级别中间要使用 - 来隔开,否则会报错;方式 2永久生效修改配置文件 my.cnf/my.ini[mysqld]transaction-isolationREPEATABLE-READ第二种设置隔离级别的方式是我们以后工作中经常使用的方式;global 全局改完之后新开的数据库连接才会使用新隔离级别已经打开的窗口不受影session 会话只改当前你正在操作的这个查询窗口关闭窗口就失效绝大多数业务使用默认 repeatable-read;支付、金融类业务可按需调整为 read-committed; (默认的隔离级别)几乎不会使用 read-uncommitted, 性能极低场景才考虑报表、强一致性场景偶尔使用 serializable, 并发性能损耗大。⬅️ 返回目录3.2 四大隔离级别是什么?隔离级别脏读不可重复读幻读读未提交 read uncommitted允许允许允许读已提交 read committedOracle 默认禁止允许允许可重复读 repeatable readMySQL InnoDB 默认禁止禁止允许串行化 serializable禁止禁止禁止⬅️ 返回目录3.3 隔离级别并发问题read uncommitted这是数据库事物中隔离级别最低的级别;当前隔离级别会出现脏读的现象;什么是脏读?脏读:我们可以理解为,一个事务可以读取到另一个事务未提交的修改数据还是以去银行取钱的例子为例:一天滑稽老铁A和滑稽老铁B同时去了银行但是去的不是同一家银行此时我们认为滑稽老铁A和B是家属关系卡里有1000块钱,滑稽老铁A取出了500元,此时账户的余额变成了500;而此时滑稽老铁B去银行查看卡里还有多少钱?一查询发现卡了还有500元;而此时滑稽老铁A又不想要这500元了,于是就把钱给重新存回去,然后滑稽老铁B又重新查询了一遍账户余额,发现账号余额多了500元变成了1000元;此时我们就能认为滑稽老铁B查询到的500元是无效数据,此时我们就认为这是脏读;用代码来进行表示:代码前置要求:创建一张滑稽老铁的银行卡;createtableifnotexistsA(idintprimarykey,namevarchar(20)default匿名,amountdecimal(10,2));insertintoAvalues(1,滑稽老铁的银行卡,1000.00);select*fromA;此时我们我们讲隔离级别修改为:read uncommittedselectSESSION.transaction_isolation;此时两个客户端的隔离级别都是相同的;事物A中开启事物;事物Astarttransaction;updateAsetamount500.00whereid1;select*fromA;此时在客户端B中开启事物;starttransaction;select*fromA;此时客户端2查询到的结果为:500;执行完成之后,在A中进行回滚;starttransaction;updateAsetamount500whereid1;select*fromA;rollback;select*fromA;我们再一次在另一个客户端进行查询;starttransaction;select*fromA;select*fromA;commit;我们此时就能看见在B中,同一个事物内,出现了两次不同的结果;一个事务读取到另一个事务未提交、最终回滚作废的数据;⬅️ 返回目录read committed:(读已提交)为了解决脏读问题,我们可以把隔离级别设置为read committed此时事物只能读到别人已提交过的事物,与此同时也会出现不可重复读的问题此时,我们可以再一次重复脏读的流程,把隔离性修改成read committedSELECTGLOBAL.transaction_isolation;SETGLOBAL.transaction_isolationread-committed;什么是不可重复读?不可重复读同一个事务内多次读取同一行数据中途被其他事务提交修改 / 更新导致前后两次查询结果不一致。脏读读到未提交、回滚作废的数据不可重复读读到其他事务已经正常提交生效的修改数据还是以转账为例子:小 A事务 1自己查银行卡余额RC 隔离小 B事务 2小 A 家人转账操作小 A 打开银行 APP 第一次查余额账户显示 1000 元第一次读取不关闭页面暂时停留在余额页面没有结束本次查询家人小 B 另一边手机给这张卡转入 500 元点击确认转账事务提交完成小 A 在自己页面刷新第二次查余额变成 1500 元我们可以理解为:一个事物中,看到了两次不一样的结果我们就把这个就叫做不可重复读;初始数据:小A开启事物并查询starttransaction;select*fromA;此时小B开启事物,给银行卡转账500元;starttransaction;updateAsetamountamount500whereid1;commit;select*fromA;然后小A继续查询数据starttransaction;-- 第一次查询select*fromA;-- 第二次查询select*fromA;同一个事务小 A 这次打开 APP 查看账户的操作两次读取同一笔余额中间没有自己修改任何数据只是别人提交了更新前后数值不一样这就是不可重复读。⬅️ 返回目录repeatable read(可重复读)为了解决不可重复读,我们把隔离性等级调整到repeatable readuseboke;SETGLOBAL.transaction_isolationrepeatable-read;SELECTGLOBAL.transaction_isolation;再次进行我们上述不可重复的操作;repeatable read隔离级别引入了快照这一个机制来帮我们解决不可重复读的问题,我们可以理解为提前拍了一张照片,后续相关操作时与照片进行比较;当然repeatable read也会出现问题,会出现幻读的一种问题,当然MySQL中repeatable read也解决出了一部分幻读的问题;什么是幻读?同一个事务内多次执行相同范围查询中间被其他事务插入 / 删除了符合条件的数据导致前后两次查询结果行数不一样像出现了幻觉这就是幻读。此时我们重新来创建一张表:createtableifnotexistsstudent(idintprimarykey,namevarchar(20)default匿名,scoreint);我们此时在A客户端中学生表中插入数据,然后开启事物:insertintostudentvalue(1,张三,90);starttransaction;select*fromstudent;此时在去B中开启事物并查询;starttransaction;select*fromstudent;此时在客户端A中插入数据;insertintostudentvalue(1,张三,90);starttransaction;select*fromstudent;insertintostudentvalue(2,李四,90);select*fromstudent;插入完成之后,再去客户端B中查询:starttransaction;-- 第一次查询select*fromstudent;-- 第二次查询select*fromstudent;此时我们还是能看见,并没有出现幻读,我们刚才了解到repeatable read 解决了一定的幻读行为此时我们可以这么去看:我们在客户端A中开启事物并插入一条数据,然后客户端B中也开启事物,进行查询;然后此时我们在客户端B中插入一条和客户端A一模一样的数据;我们此时如果让客户端A中进行rollback,那么客户端B就会插入成功;如果我们此时客户端A进行了commit客户端B中就会报错;我们此时就能认为当前repeatable 隔离级别发生了幻读;⬅️ 返回目录串行化 serializable核心特性:MySQL 事务最高隔离级别四大隔离问题全部解决无脏读、不可重复读、幻读;所有普通 SELECT 语句自动隐式加共享读锁等价于 SELECT … LOCK IN SHARE MODE读写冲突、写写冲突都会阻塞事务只能串行执行并发性能最差锁机制普通查询自动加共享锁其他事务无法修改 / 插入区间数据范围查询会加临键锁彻底封锁间隙杜绝新增数据产生幻读只要事务未提交锁定的数据 / 区间其他事务无法写入。优点数据一致性最强不会出现任何事务并发异常适合金融、对账等强一致性场景缺点并发极低大量锁等待极易触发锁超时、死锁线上业务极少使用(执行速度非常慢);我们讲隔离级别修改成serializable;SETGLOBAL.transaction_isolationserializable;SELECTGLOBAL.transaction_isolation;还是以我们转账为例子:createtableifnotexistsA(idintprimarykey,namevarchar(20),accountint);客户端A中开启事物,并插入一条数据;然后再B中开启事物,修改金额为1500;starttransaction;updateAsetaccount1500whereid1;此时我们客户端B中在进行等待,要让客户端A进行提交之后才会执行下一步操作;如果客户端A长时间不操作,客户端B就会失败,超时了只有当客户端A完成操作时,客户端B才会进行下一步操作;⬅️ 返回目录3.4 性能与安全取舍隔离级别越高数据一致性安全越强但并发性能越差二者反向权衡。读未提交 RU性能最高安全最差脏读、不可重复读、幻读全存在线上不用。读已提交 RC性能较高仅解决脏读无间隙锁会幻读适合日志、埋点等弱一致业务。可重复读 RRMySQL 默认性能与安全均衡杜绝脏读、不可重复读加锁读可消除幻读绝大多数业务首选。串行化 Serializable安全最强无任何并发异常性能最差读写互堵、死锁多仅低并发金融对账使用。不升级隔离、兼顾性能与安全的优化手段控制短事务缩短锁持有时间保证索引精准避免锁范围扩大查询分两种普通快照读不加锁范围更新手动加悲观锁高并发更新使用乐观锁version 版本号减少锁竞争大事务拆分批量提交降低锁冲突。问为什么不直接用串行化保证数据安全答串行化所有读自动加共享锁读写互相阻塞系统吞吐量暴跌锁等待、死锁大幅增多并发业务无法承受业务层面可通过 RR 手动加锁实现同等一致性性能损耗更小。⬅️ 返回目录