--- title: "19-事务与并发" created: 2025-11-25 tags: - 项目筑基 --- # 事务与并发 数据库是可供多个用户使用的共享资源 (可创多个登入名 一登入名下又有多个用户) 为了充分利用数据库资源 应该允许多个用户并行地存储数据库 但这种并发操作若不加以控制就可能会破坏数据库的一致性 所以 数据库管理系统必须提供并发控制机制 ===> Sql sever以事务为单位 通常使用锁来实现并发控制 ## 概念 类似于存储过程 由一系列T-SQL语句组成 就是作为单个逻辑工作单元执行的一系列操作 这一系列操作要么都被执行要么都不被执行(一个事务就是一个完整的业务逻辑) 比如转账 a给b转10000 a账户减10000(update) b账户加10000 (update) 这些操作是一个最小的工作单元 要么同时成功要么同时失败 不可再分 才能保证资金正确 只有DML语句才有事务这一说(insert、update、delete) 涉及增删改就一定要考虑安全问题 数据安全第一位 如果所有业务只要一条DML语句就能完成 就不需要事务 说到底 事务就是因为单条DML无法实现 需要多条协同 它们必须是整体 同时成功/失败 ### 四个原则(acdi) 原子性:(原子不可再分)一个事务里的内容要么都被做要么都不被做 (使得即使系统崩溃也能保证数据一致性 发生崩溃时 系统会记录当时该事务所处的状态 如果没做完就撤销该事务已做的所有操作 做完了就将修改的操作成功提交) 一致性:事务设置的目的就是让数据库中数据保持一致性 要求事务执行完成后 将数据库从一个一致状态转变到另一个一致状态 (所有规则都必须应用于事务的修改 以保证数据的完整性) 隔离性:对用户来讲 各自只能看见各自的视图 指并行事务的修改必须与其他并行事务的修改相互独立 保证事务查看数据时数据所处的状态 只能说另一并发事务修改它之前的状态或者是修改它之后的状态 而不能是中间状态的数据 看上去每个成功事务就像按串行执行一样(像线程安全问题) 持久性:一旦执行对数据库中数据的变化是不可逆的 而撤销操作又是一个新事务 在事务提交完成后 就对系统产生持久的影响 即事务的操作将写入数据库中,无论发生何种机器和系统故障都不应该对其有任何影响 #### \*隔离性 a和b教室中间有一道墙 这道墙可以很薄也可以很厚 这就是事务的隔离级别 墙越厚 隔离级别越高 一般有4个级别 读未提交:read uncommitted(没有提交就读到了) 读已提交:read committed(提交才读得到) 可重复读:repeatable read(提交了也读不到) 序列化/串行化:serializable 这几个级别就像是原码反码补码 后一种解决前一种的问题 ##### 读未提交: 事务a可以读取到事务b未提交的数据(隔墙有耳) 这种隔离级别操作的问题就是脏读现象(Dirty Read) 大多数的数据库都不用这种隔离级别 可以说它是存在于理论上 问题:脏读 (读取未提交数据) A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。就好像原本的数据比较干净、纯粹,此时由于B事务更改了它,这个数据变得不再纯粹。这个时候A事务立即读取了这个脏数据,但事务B良心发现,又用回滚把数据恢复成原来干净、纯粹的样子,而事务A却什么都不知道,最终结果就是事务A读取了此次的脏数据,称为脏读。 这种情况常发生于转账与取款操作中 ![[image-4918ea3d.png]] ##### 读已提交: 事务a只可以读到b提交后的数据 解决了脏读现象 但问题在于不可重复读取数据 这种隔离级别是比较真实的数据 每一次读到的数据都是绝对的真实 oracle、SQL Server就默认隔离级别为read committed但mysql不是 问题:不可重复读 (前后多次读取,数据内容不一致) 是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。 事务A在执行读取操作,由于整个事务A比较大,前后读取同一条数据需要经历很长的时间 。而在事务A第一次读取数据,比如此时读取了小明的年龄为20岁,事务B执行更改操作,将小明的年龄更改为30岁,此时事务A第二次读取到小明的年龄时,发现其年龄是30岁,和之前的数据不一样了 ![[image-fedf38b4.png]] 1.在事务1中,Mary 读取了自己的工资为1000,操作并没有完成 2.在事务2中,这时财务人员修改了Mary的工资为2000,并提交了事务. 3.在事务1中,Mary 再次读取自己的工资时,工资变为了2000 无法再在a中读到a事务原有的内容 它的内容随着b的提交也修改了 其实这应该是我们想要的结果 比较真实 但某些特殊情况下 它也是一种问题 原有的a中数据被改了 无法再读到 这就是不可重复读 解决办法:如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题 ##### 可重复读 事务a开启之后 不管是多久 每一次在事务a中读取到的数据都是一致的 即使数据b将数已经修改 并且提交了 事务a读到的数据还是没有改变 解决了不可重复读取数据问题 但存在一个问题 幻影读(幻读)每一次读取到的数据都是幻像 不够真实 读取的永远是刚开启事务时的数据 在这种隔离级别下,所有事务前后多次的读取到的数据内容是不变的。 也就是某个事务在执行的过程中,不允许其他事务进行update操作,但允许其他事务进行add操作,造成某个事务前后多次读取到的数据总量不一致的现象,从而产生幻读 mysql以它为默认隔离级别 幻读 (前后多次读取,数据总量不一致) 即一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行 事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,称为幻读。 ![[image-c1c87548.png]] 不可重复读的重点是修改: 同样的条件, 你读取过的数据,再次读取出来发现值不一样了 幻读的重点在于新增或者删除 同样的条件, 第 1 次和第 2 次读出来的记录数不一样 ##### 序列化/串行化 效率最低 解决的所有问题 事务要进行排队不能并发 你执行完了 我才执行 在这种隔离级别下,所有的事务顺序执行,所以他们之间不存在冲突,从而能有效地解决脏读、不可重复读和幻读的现象。 但是安全和效率不能兼得,这样事务隔离级别,会导致大量的操作超时和锁竞争,从而大大降低数据库的性能,一般不使用这样事务隔离级别。 ![[image-0e8f6161.png]] ## 隔离级别实验 ### 查看/设置隔离级别 ![[image-031be4fc.png]] mysql 8.0之后 查看会话级的当前隔离级别——使用select @@transaction\_isolation; ![[image-03efa27e.png]] 查看全局级的当前隔离级别——使用select @@global.transaction\_isolation; ![[image-ac7d573c.png]] 或者使用 show global variables like '%isolation%'; ![[image-c015f0c1.png]] 可见mysql默认的隔离级别是三级 可重复读 设置全局隔离级别——set global transaction isolation level …… ### 测试第一级别 read uncommitted 设置全局隔离级别为读未提交 set global transaction isolation level read uncommitted ![[image-542a18b1.png]] 修改完后先exit退出 再重新进入 再查询 ![[image-933b30c5.png]] 发现修改成功 开启两个mysql窗口 分别作为事务a和事务b 使用text数据库中的空表t\_use | | | | --- | --- | | 事务A | 事务B | | use text; ![[image-b8a17619.png]] | | | | use text; ![[image-299834f6.png]] | | start transaction; ![[image-66abf415.png]] | | | select \* from t\_use; ![[image-25ebb12e.png]] | | | | start transaction; ![[image-3ddcfbdf.png]] | | | insert into t\_use values('1') ![[image-2c01c41c.png]] | | select \* from t\_use; ![[image-4bd5b493.png]] | | 在事务B中并没有commit提交 却能在事务A中查到它所做的修改 如果在事务B中rollback 那么再到事务A中查 | | | | --- | --- | | 事务A | 事务B | | | rollback; ![[image-48182502.png]] | | select \* from t\_use; ![[image-031a11eb.png]] | | 又变回空了 这就是—— ![[image-0a0176f7.png]] 如果在事务B中做了一系列修改 最后都回滚了 事务A中就做了很多无用功 读取了一堆没用的东西 这就是脏读 ### 测试第二级别 read committed 设置全局隔离级别为读已提交 set global transaction isolation level read committed; exit; 重新登入 ![[image-d5b378e5.png]] | | | | --- | --- | | 事务A | 事务B | | use text; ![[image-3228e661.png]] | | | | use text; ![[image-3228e661.png]] | | start transaction; ![[image-21067f7c.png]] | | | | start transaction; ![[image-380b9bf0.png]] | | select \* from t\_use; ![[image-147bc419.png]] | | | | insert into t\_use values('1'); ![[image-a26b7336.png]] | | select \* from t\_use; ![[image-952cfcb6.png]] | | | | commit; ![[image-db109dd7.png]] | | select \* from t\_use; ![[image-250b68cf.png]] | | 只有当事务B提交commit了 事务A中才能查询到对应修改 解决了脏读问题 反过来也一样 | | | | --- | --- | | 事务A | 事务B | | delete from t\_use where no='1'; ![[image-9f192e29.png]] | | | | select \* from t\_use; ![[image-2f263467.png]] | | commit; ![[image-ec593977.png]] | | | | select \* from t\_use; ![[image-e0361634.png]] | 但是如果单纯对于一个事务来说 前后两次查询的结果是不一样的 在当前事务中并没有做出任何修改 但原有的数据消失了 这就是不可重复读(数据会被其他事务修改 无法读到该事务原本的数据) 其实这也是比较真实的 是我们想要的情况 ### 测试第三级别 repeatable read 设置全局隔离级别为可重复读(默认) set global transaction isolation level repeatable read; exit; 重新登入 ![[image-5c5a02f1.png]] | | | | --- | --- | | 事务A | 事务B | | use text; | | | | use text; | | start transaction; | | | | start transaction; | | select \* from t\_use; ![[image-288663be.png]] | | | | insert into t\_use values ('1'); ![[image-d3fa46b0.png]] | | | insert into t\_use values ('2'); ![[image-76b30577.png]] | | | commit; ![[image-a3deb16d.png]] | | select \* from t\_use; ![[image-56b7efe1.png]] | | | | select \* from t\_use; ![[image-24f697b1.png]] | | select \* from t\_use for update; ![[image-a7a2455b.png]] | | 使用select ……for update就能查到更新后的结果 涉及到新概念—— **当前读和快照读** 快照读:读取快照中的数据,不需要进行加锁 MVCC 作用于读取已提交和可重复读(默认)这两个隔离级别,这俩隔离级别下的普通 select 操作就是快照读 简单来说 就是无论其他事务做什么修改 都不会对当前事务产生影响 事务B插入100条数据 事务A中仍是原来的情况 也就是在这个例子中 使用select \*from t\_use仍是空的原因 当前读:读取的是最新版本的数据, 并且对读取的记录加锁,阻塞其他事务同时改动相同记录,避免出现安全问题 select \* from t\_use for update 在事务A中查到了B中的更新 除了读已提交和可重复读这俩隔离级别下的普通 select 操作,其余操作都是当前读。 ![[image-956525de.png]] 开启两个事务 ![[image-7217023b.png]] 先给事务A来个快照读——select \* from user; ![[image-ecbca54a.png]] 很正常 事务B修改age=99; 提交 对事务A快照读 ![[image-fe9bc602.png]] 还是原本数据 对事务A当前读(得到最新数据) ![[image-e5060990.png]] 所谓幻读,即一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。 为什么一模一样的 SQL 语句,第一次查询是10条数据,第二次查询是12条数据?难道刚才出现幻觉了?这就是「幻读」这个名词的由来 (和不可重复读有点像 但侧重点不同) 不可重复读的重点是修改: 同样的条件,你读取过的数据,再次读取出来发现值不一样了 幻读的重点在于新增或者删除 同样的条件, 第 1 次和第 2 次读出来的记录数不一样 在可重复读隔离级别下,普通的查询是快照读,当前事务是不会看到别的事务插入的数据的。因此,幻读问题在 “当前读” 下才会出现。 https://baijiahao.baidu.com/s?id=1717832879121532896&wfr=spider&for=pc 解决? ### 捋一捋 对于读未提交就是事务间实时更新 如果回滚会产生很多脏数据(做无用功) 所以为了解决脏读 有了二级的隔离级别——读已提交 就是只有真正确定修改了 其他事务中也会同步对数据的修改 这是一般情况下我们希望的样子 可能sql sever和oracle默认二级也是因为这个 但对于这种情况来说 b事务修改了 a事务也跟着修改 那么 在a事务中就丢失了原有的样子 无法查询到之前的状态 这叫做 不可重复读 为了解决不可重复的读问题 就有了三级——可重复读 无论其他事务(b,c……)怎么修改 我在事务a中使用快照读 永远都得到原有的结果 但在使用当前读的话 事务a中就会做出相应的更新 这好像回到了不可重复读的问题 它有个名字叫幻读 但它们侧重点不同 不可重复读侧重于数据的修改方面 b中修改了数据 a里面找不到原来的样子 幻读存在于数据的条目数 在b中添加了很多数据 在a里使用相同的查询语句 查到的结果条目数居然不同 就好像出现幻觉 这叫做幻读 一个侧重修改 一个侧重增删 至于如何解决 应该要涉及到锁的一系列概念 ### 测试第四级别 serializable 设置全局隔离级别为可重复读(默认) set global transaction isolation level serializable; exit; 重新登入 ![[image-a5ebf063.png]] | | | | --- | --- | | 事务A | 事务B | | use text; | | | | use text; | | start transaction; | | | | start transaction; | | select \* from t\_use; ![[image-303432bd.png]] | | | delete from t\_use where no='1'; ![[image-bd669c8e.png]] | | | | select \* from t\_use; ![[image-7b69b951.png]] | | commit; | ![[image-f974e646.png]] | 当事务A还没提交时 在事务B中进行操作 光标会卡在下一行的位置 并告诉需要等待 一旦事务A提交commit了 事务B中立刻得到结果 事务间要进行排队 你执行完了 我才执行 ## 分类 ### 系统事务 在执行某些语句时 一条语句就是一个事务 系统提供的事务语句:CREATE ALTER DROP INSERT DELETE ELECT UPDATE等 比如说 `create table student` `( id char(10),` `name char(6),` `sex char(2)` `)` 这条语句就构成了一个事务 要么创建全部成功 要么全部失败 ### 用户定义事务 实际运用中 大多数事务处理采用用户定义的事务 使用BEGIN TRANSACTION语句来定于事务的开始 必须要有明确的结束语句 如果没有 系统会把从事务开始到用户关闭连接之间的所有操作都作为一个事务对待 事务结束语句 不是end 有两种 一、COMMIT 提交语句 将事务操作全部提交到数据库 二、ROLLBACK 取消(回滚)语句 将事务操作全部取消 表示操作失败 或者按运行模式 分为 **显示事务 隐式事务 自动提交事务 批处理级事务** 1、自动提交事务: 每条单独的T-SQL语句都是一个事务 之前使用的每一条语句都可以叫做一个自动提交事务 无需commit去进行提交操作 2、显示事务: 每个事务均以begin transaction语句、commit transaction或者rollback transaction语句明确地定义事务的开始与结束 3、隐式事务: 前一个事务完成是 新事务隐式启动 但每个事务仍需commit transaction或者rollback transaction显式结束 (无开始有结束) 4、批处理级事务: sql sever 2005新增功能 只能应用于多个活动结果集(MARS) 在MARS会话中启动的T-SQL显式或隐式事务变成批处理级事务 ## 事务怎么工作 事务是怎么做到多条DML语句同时成功和同时失败的呢 之前InnoDB存储引擎有提过 它提供一组用来记录事务性活动的日志文件 事务开始了: insert…… insert…… insert…… delete…… update…… update…… …… 事务结束了 在事务的执行过程中 每一条DML的操作都会记录在事务性活动的日志文件中 可以提交事务也可以回滚事务(两种事务结束方式) 提交事务:清空事务性活动的日志文件 将数据全部彻底持久化到数据库表中 回滚事务:将所有的DML操作全部撤销 并清空事务性活动的日志文件 可能sqlsever也是使用InnoDB存储引擎 因为我们在创建的时候就有两个文件 一个mdf主数据文件 一个ldf日志文件 这个ldf可能就是事务日志 另外再补充一个概念 写的语句保存下来会是.sql形式 这个叫做sql脚本文件 sql脚本文件中就编写了大量的sql语句 用source命令执行sql脚本文件时 里面的所有语句会全部执行 实际的工作中 第一天到公司 就会收到一个.sql的文件 让自己电脑上有对应数据库数据 ## 提交/回滚 事务:transaction 提交事务:commit;语句 回滚事务:rollback;语句 但是在之前的操作中都没碰到这种问题 哪有碰到要输入commit和rollback的情况 都是执行一条DML语句 然后数据表里的数据就进行的相应修改 这是因为 在mysql中默认的事务行为是自动提交(每执行一条DML语句就提交一次) insert…… 执行 查询一下 多了一条记录 rollback 再查询 记录仍在 这是怎么回事? ——回滚只能回滚到上一次的提交点 执行了就是默认commit了 那个文件里的东西都清空了 rollback自然无效 那回滚根本起不到作用 怎么办 能不能把自动提交关了 当然可以 `start transaction ;` 实验一下 对于空表user2插入数据 `insert into user2 values (1);` 查看一下 `sselect * from user2;` ![[image-1bc71d11.png]] 虽然有 但是 还没有提交 `rollback ;` 再查询 `select * from user2;` ![[image-a48bede9.png]] 操作被撤销了 ![[image-3284938e.png]] `start transaction ;` 是非常重要的 因为默认提交是不符合开发要求的 一种操作往往需要很多DML语句共同完成 必须选择手动提交 像转账 如果默认提交 a的钱扣了 提交了 如果出现问题转账失败 a的钱是无法回滚的 因为 回滚只能回滚到上次提交后面的一系列操作 ```sql start transaction ; insert into user2 values (1); insert into user2 values (2); insert into user2 values (3); delete from user2 where no=2; select * from user2; rollback; select * from user2; ``` 取消所有操作(三条insert一条delete) ```sql start transaction ; insert into user2 values (1); insert into user2 values (2); insert into user2 values (3); delete from user2 where no=2; commit ; select * from user2; rollback ; select * from user2; ``` 执行所有操作 已经提交 回滚无效 不用`start transaction ;` 就成了 `insert into user2 values (1); commit ;` `insert into user2 values (2); commit ;` `insert into user2 values (3); commit ;` `delete from user2 where no=2; commit ;` 次次都提交 比如玩一个游戏 中途存过档 后面又玩了一会 可能死了 也可能没存档离开了 下次进入游戏就会回滚到上次存档点 如果从玩的时候就没存过档 那么死了就直接从头开始了 存档就是一次提交commit 死亡或者退出 后续继续游戏 就是一次回滚rollback 举例: ``` Begin Transaction 读账户甲的余额Balance Balance=Balance - Amount If(Balance < 0) Then { 打印’余额不足’; RollBack; } Else { 读账户乙的余额Balance1; Balance1=Balance1+Amount 写回Balance; Commit; } ``` --- ⬅️ [[18-存储过程|18-存储过程]] 🏠 [[00-数据库|00-数据库]] ➡️ [[20-索引|20-索引]]