雨落在了我的手上个人主页个人仓库Gitee仓库❄️个人专栏JaveSe C语言 C语言数据结构Java数据结构MySQL数据库基础 人生格言无人扶我青云志我自踏雪至山巅博主简介目录1. 本节目标2. 什么是事务3. 事务的ACID特性4. 为什么要使用事务5. 如何使用事务5.1 查看支持事务的存储引擎5.2 语法5.3 开启一个事务执行修改后回滚5.4 开启一个事务执行修改后提交5.5 保存点5.6 自动/手动提交事务6. 事务的隔离性和隔离级别6.1 什么是隔离性6.2 隔离级别6.3 查看和设置隔离级别6.4 不同隔离级别存在的问题6.4.1 READ UNCOMMITTED - 读未提交与脏读6.4.1.1 存在问题6.4.1.2 问题重现6.4.2 READ COMMITTED - 读已提交与不可重复读6.4.2.1 存在问题6.4.2.2 问题重现6.4.3 REPEATABLE READ - 可重复读与幻读6.4.3.1 存在问题6.4.3.2 问题重现6.4.4 SERIALIZABLE - 串行化6.5 不同隔离级别的性能与安全内容大纲本文系统讲解数据库事务的ACID特性原子性、一致性、隔离性、持久性通过转账案例阐明事务的重要性。详细介绍了MySQL中事务的开启、提交、回滚及保存点操作并演示了四种隔离级别读未提交、读已提交、可重复读、串行化及其对应的问题脏读、不可重复读、幻读。结合实际场景分析了不同隔离级别的性能与安全性权衡强调在高并发环境下合理设置隔离级别以保障数据一致性的关键作用。1. 本节目标了解事务的概念与使用场景掌握事务的ACID特性掌握如何使用事务掌握事务的隔离性与隔离级别2. 什么是事务事务把一组SQL语句打包成为一个整体在这组SQL的执行过程中要么全部成功要么全部失败。这组SQL语句可以是一条也可以是多条。来看一个转账的例子如图在这个例子中涉及了两条更新语句# 账户表 CREATE TABLE bank_account ( id bigint PRIMARY KEY AUTO_INCREMENT, name varchar(255) NOT NULL, # 姓名 balance decimal(10, 2) NOT NULL # 余额 ); INSERT INTO bank_account(name, balance) VALUES(张三, 1000); INSERT INTO bank_account(name, balance) VALUES(李四, 1000);# 更新操作 # 张三余额减少100 UPDATE bank_account set balance balance - 100 where name 张三; # 李四余额增加100 UPDATE bank_account set balance balance 100 where name 李四;如果转账成功应该有以下结果张三的账户余额减少100 变成900 李四的账户余额增加了100 变成1100 不能出现张三的余额减少而李四的余额没有增加的情况张三和李四在发生转账前后的总额不变也就是说转账前张三和李四的余额总数为100010002000 转账后他们的余额总数为90011002000 转账后的余额结果应当保存到存储介质中以便以后读取还有一点需要要注意在转账的处理过程中张三和李四的余额不能因其他的转账事件而受到干扰以上这四点在事务的整个执行过程中必须要得到保证这也就是事务的ACID 特性3. 事务的ACID特性事务的ACID特性指的是Atomicity (原子性)Consistency (一致性)Isolation (隔离性)和Durability (持久性)。Atomicity (原子性)一个事务中的所有操作要么全部成功要么全部失败不会出现只执行了一半的情况如果事务在执行过程中发生错误会回滚Rollback 到事务开始前的状态就像这个事务从来没有执行过一样Consistency (一致性)在事务开始之前和事务结束以后数据库的完整性不会被破坏。这表示写入的数据必须完全符合所有的预设规则包括数据的精度、关联性以及关于事务执行过程中服务器崩溃后如何恢复Isolation (隔离性)数据库允许多个并发事务同时对数据进行读写和修改隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务可以指定不同的隔离级别以权衡在不同的应用场景下数据库性能和安全Durability (持久性)事务处理结束后对数据的修改将永久的写入存储介质即便系统故障也不会丢失。4. 为什么要使用事务事务具备的ACID特性是我们使用事务的原因在我们日常的业务场景中有大量的需求要用事务来保证。支持事务的数据库能够简化我们的编程模型, 不需要我们去考虑各种各样的潜在错误和并发问题在使用事务过程中要么提交要么回滚不用去考虑网络异常服务器宕机等其他因素因此我们经常接触的事务本质上是数据库对ACID 模型的一个实现是为应用层服务的。5. 如何使用事务5.1 查看支持事务的存储引擎要使用事务那么数据库就要支持事务在MySQL中支持事务的存储引擎是InnoDB可以通过show engines; 语句查看5.2 语法通过以下语句可以完成对事务的控制# 开始一个新的事务 START TRANSACTION; # 或 BEGIN # 提交当前事务并对更改持久化保存 COMMIT # 回滚当前事务取消其更改 ROLLBACKSTART TRANSACTION 或BEGIN 开始一个新的事务COMMIT 提交当前事务并对更改持久化保存ROLLBACK 回滚当前事务取消其更改无论提交还是回滚事务都会关闭5.3 开启一个事务执行修改后回滚# 开启事务 mysql START TRANSACTION; Query OK, 0 rows affected (0.00 sec)# 在修改之前查看表中的数据 mysql select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 1000.00 | | 2 | 李四 | 1000.00 | ------------------- 2 rows in set (0.00 sec) # 张三余额减少100 mysql UPDATE bank_accountset balance balance - 100 where name 张三; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 # 李四余额增加100 mysql UPDATE bank_accountset balance balance 100 where name 李四; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 # 在修改之后提交之前查看表中的数据余额已经被修改 mysql select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 900.00 | | 2 | 李四 | 1100.00 | ------------------- 2 rows in set (0.00 sec) # 回滚事务 mysql ROLLBACK; Query OK, 0 rows affected (0.00 sec) # 再查询发现修改没有生效 mysql select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 1000.00 | | 2 | 李四 | 1000.00 | ------------------- 2 rows in set (0.00 sec)5.4 开启一个事务执行修改后提交# 开启事务 mysql BEGIN; Query OK, 0 rows affected (0.00 sec) # 在修改之前查看表中的数据 mysql select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 1000.00 | | 2 | 李四 | 1000.00 | ------------------- 2 rows in set (0.00 sec) # 张三余额减少100 mysql UPDATE bank_accountset balance balance - 100 where name 张三; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 # 李四余额增加100 mysql UPDATE bank_accountset balance balance 100 where name 李四; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 # 在修改之后提交之前查看表中的数据余额已经被修改 mysql select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 900.00 | | 2 | 李四 | 1100.00 | ------------------- 2 rows in set (0.00 sec) # 提交事务 mysql COMMIT; Query OK, 0 rows affected (0.01 sec) # 再查询发现数据已被修改说明数据已经持久化到磁盘 mysql select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 900.00 | | 2 | 李四 | 1100.00 | ------------------- 2 rows in set (0.00 sec)5.5 保存点在事务执行的过程中设置保存点回滚时指定保存点可以把数据恢复到保存点的状态# 开启事务 mysql START TRANSACTION; Query OK, 0 rows affected (0.00 sec) # 在修改之前查看表中的数据 mysql select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 900.00 | | 2 | 李四 | 1100.00 | ------------------- 2 rows in set (0.00 sec) # 张三余额减少100 mysql UPDATE bank_accountset balance balance - 100 where name 张三; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 # 李四余额增加100 mysql UPDATE bank_accountset balance balance 100 where name 李四; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 # 余额已经被修改 mysql select * from bank_account; --------------------- | id | name | balance | --------------------- | 1 | 张三 | 800.00 | | 2 | 李四 | 1200.00 | --------------------- 2 rows in set (0.00 sec) # 设置保存点 mysql SAVEPOINT savepoint1; Query OK, 0 rows affected (0.01 sec) # 再次执行张三余额减少100 mysql UPDATE bank_accountset balance balance - 100 where name 张三; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 # 再次执行李四余额增加100 mysql UPDATE bank_accountset balance balance 100 where name 李四; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 # 余额已经被修改 mysql select * from bank_account; --------------------- | id | name | balance | --------------------- | 1 | 张三 | 700.00 | | 2 | 李四 | 1300.00 | --------------------- 2 rows in set (0.00 sec) # 设置第二个保存点 mysql SAVEPOINT savepoint2; Query OK, 0 rows affected (0.00 sec) # 插入一条新记录 mysql insert into bank_account values (null, 王五, 1000); Query OK, 1 row affected (0.01 sec) # 查询结果新记录写入成功 mysql select * from bank_account; --------------------- | id | name | balance | --------------------- | 1 | 张三 | 700.00 | | 2 | 李四 | 1300.00 | | 3 | 王五 | 1000.00 | --------------------- 3 rows in set (0.00 sec) # 回滚到第二个保存点 mysql ROLLBACK TO savepoint2; Query OK, 0 rows affected (0.00 sec) # 回滚成功 mysql select * from bank_account; --------------------- | id | name | balance | --------------------- | 1 | 张三 | 700.00 | | 2 | 李四 | 1300.00 | --------------------- 2 rows in set (0.00 sec) # 回滚到第一个保存点 mysql ROLLBACK TO savepoint1; Query OK, 0 rows affected (0.00 sec) # 回滚成功 mysql select * from bank_account; --------------------- | id | name | balance | --------------------- | 1 | 张三 | 800.00 | | 2 | 李四 | 1200.00 | --------------------- 2 rows in set (0.00 sec) # 回滚时不指定保存点直接回滚到事务开始时的原始状态事务关闭 mysql ROLLBACK; Query OK, 0 rows affected (0.01 sec) # 原始状态 mysql select * from bank_account; --------------------- | id | name | balance | --------------------- | 1 | 张三 | 900.00 | | 2 | 李四 | 1100.00 | --------------------- 2 rows in set (0.00 sec)5.6 自动/手动提交事务默认情况下MySQL是自动提交事务的也就是说我们执行的每个修改操作比如插入、更新和删除都会自动开启一个事务并在语句执行完成之后自动提交发生异常时自动回滚。查看当前事务是否自动提交可以使用以下语句mysql show variables like autocommit; ---------------------- | Variable_name | Value | ---------------------- | autocommit | ON | # ON 表示自动提交开启 ---------------------- 1 row in set, 1 warning (0.04 sec)可以通过以下语句设置事务为自动或手动提交# 设置事务自动提交 mysql SET AUTOCOMMIT1; # 方式一 mysql SET AUTOCOMMITON; # 方式二 # 设置事务手动提交 mysql SET AUTOCOMMIT0; # 方式一 mysql SET AUTOCOMMITOFF; # 方式二注意只要使用START TRANSACTION 或BEGIN 开启事务必须要通过COMMIT 提交才会持久化与是否设置SET autocommit 无关。手动提交模式下不用显示开启事务执行修改操作后提交或回滚事务时直接使用commit或rollback已提交的事务不能回滚6. 事务的隔离性和隔离级别6.1 什么是隔离性MySQL服务可以同时被多个客户端访问每个客户端执行的DML语句以事务为基本单位那么不同的客户端在对同一张表中的同一条数据进行修改的时候就可能出现相互影响的情况为了保证不同的事务之间在执行的过程中不受影响那么事务之间就需要要相互隔离这种特性就是隔离性。6.2 隔离级别事务具有隔离性那么如何实现事务之间的隔离隔离到什么程度如何保证数据安全的同时也要兼顾性能这都是要思考的问题。事务间不同程度的隔离称为事务的隔离级别不同的隔离级别在性能和安全方面做了取舍有的隔离级别注重并发性有的注重安全性有的则是并发和安全适中在MySQL的InnoDB引擎中事务的隔离级别有四种分别是READ UNCOMMITTED 读未提交READ COMMITTED 读已提交REPEATABLE READ 可重复读(默认)SERIALIZABLE 串行化6.3 查看和设置隔离级别事务的隔离级别分为全局作用域和会话作用域查看不同作用域事务的隔离级别可以使用以下的方式# 全局作用域 SELECT GLOBAL.transaction_isolation; # 会话作用域 SELECT SESSION.transaction_isolation; # 可以看到默认的事务隔离级别是REPEATABLE-READ(可重复读) --------------------------------- | SESSION.transaction_isolation | --------------------------------- | REPEATABLE-READ | # 默认是可重复读 --------------------------------- 1 row in set (0.00 sec)设置事务的隔离级别和访问模式可以使用以下语法# 通过GLOBAL|SESSION分别指定不同作用域的事务隔离级别 SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level|access_mode; # 隔离级别 level: { REPEATABLE READ # 可重复读 | READ COMMITTED # 读已提交 | READ UNCOMMITTED # 读未提交 | SERIALIZABLE # 串行化 } # 访问模式 access_mode: { READ WRITE # 表示事务可以对数据进行读写 | READ ONLY # 表示事务是只读不能对数据进行读写 } # 示例 # 设置全局事务隔离级别为串行化后续所有事务生效不影响当前事务 SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE; # 设置会话事务隔离级别为串行化当前会话后续的所有事务生效不影响当前事务可以在任何时候执行 SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; # 如果不指定任何作用域设置只针对下一个事务随后的事务恢复之前的隔离级别 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; # 方式一 SET GLOBAL transaction_isolation SERIALIZABLE; # 注意使用SET语法时有空格要用-代替 SET SESSION transaction_isolation REPEATABLE-READ; # 方式二 SET GLOBAL.transaction_isolationSERIALIZABLE; # 注意使用SET语法时有空格要用-代替 SET SESSION.transaction_isolationREPEATABLE-READ;6.4 不同隔离级别存在的问题6.4.1 READ UNCOMMITTED - 读未提交与脏读6.4.1.1 存在问题出现在事务的READ UNCOMMITTED 隔离级别下由于在读取数据时不做任何限制所以并发性能很高但是会出现大量的数据安全问题比如在事务A中执行了一条INSERT 语句在没有执行COMMIT 的情况下会在事务B中被读取到此时如果事务A执行回滚操作那么事务B中读取到事务A写入的数据将没有意义我们把这个理象叫做脏读 。6.4.1.2 问题重现在一个客户端A中先设置全局事务隔离级别为READ UNCOMMITTED 读未提交:# 设置隔离级别为READ UNCOMMITTED读未提交 mysql SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; Query OK, 0 rows affected (0.00 sec) # 查看设置是否生效 mysql SELECT GLOBAL.transaction_isolation; -------------------------------- | GLOBAL.transaction_isolation | -------------------------------- | READ-UNCOMMITTED | # 已生效 -------------------------------- 1 row in set (0.00 sec)打开另一个客户端B并确认隔离级别# 查看设置是否生效 mysql SELECT GLOBAL.transaction_isolation; -------------------------------- | GLOBAL.transaction_isolation | -------------------------------- | READ-UNCOMMITTED | # 已生效 -------------------------------- 1 row in set (0.00 sec)在不同的客户端中执行事务会话A开启事务A会话B开启事务B# 选择数据库 use test_db; # 开启事务 START TRANSACTION;# 写入一条新数据 insert into bank_account values (null, 王五, 2000);# 查询结果 select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 800.00 | | 2 | 李四 | 1100.00 | | 5 | 王五 | 2000.00 | ------------------- 3 rows in set (0.00 sec) # 新记录已写入但是此时事务A并没有提交# 选择数据库 use test_db; # 开启事务 START TRANSACTION;# 查询结果 select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 800.00 | | 2 | 李四 | 1100.00 | | 5 | 王五 | 2000.00 | ------------------- 3 rows in set (0.00 sec) # 发现查到了事务A没有提交的数据# 回滚 rollback; # 查询结果数据正常回滚 select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 800.00 | | 2 | 李四 | 1100.00 | ------------------- 2 rows in set (0.00 sec)# 再次查询刚才“王五”这条记录不存在了 select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 800.00 | | 2 | 李四 | 1100.00 | ------------------- 2 rows in set (0.00 sec)由于READ UNCOMMITTED 读未提交会出现脏读现象在正常的业务中出现这种问题会产生非常危重后果所以正常情况下应该避免使用READ UNCOMMITTED 读未提交这种的隔离级别。6.4.2 READ COMMITTED - 读已提交与不可重复读6.4.2.1 存在问题为了解决脏读问题可以把事务的隔离级别设置为READ COMMITTED 这时事务只能读到了其他事务提交之后的数据但会出现不可重复读的问题比如事务A先对某条数据进行了查询之后事务B对这条数据进行了修改并且提交( COMMIT )事务事务A再对这条数据进行查询时得到了事务B修改之后的结果这导致了事务A在同一个事务中以相同的条件查询得到了不同的值这个现象要不可重复读。6.4.2.2 问题重现在一个客户端A中先设置全局事务隔离级别为READ COMMITTED 读已提交:# 设置隔离级别为READ COMMITTED读未提交 mysql SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED; Query OK, 0 rows affected (0.00 sec) # 查看设置是否生效 mysql SELECT GLOBAL.transaction_isolation; -------------------------------- | GLOBAL.transaction_isolation | -------------------------------- | READ-COMMITTED | # 已生效 -------------------------------- 1 row in set (0.00 sec)打开另一个客户端B并确认隔离级别# 查看设置是否生效 mysql SELECT GLOBAL.transaction_isolation; -------------------------------- | GLOBAL.transaction_isolation | -------------------------------- | READ-COMMITTED | # 已生效 -------------------------------- 1 row in set (0.00 sec)在不同的客户端中执行事务会话A开启事务A会话B开启事务B# 选择数据库 use test_db; # 写入一条新测试数据 insert into bank_account values (null, 王五, 2000); Query OK, 1 row affected (0.00 sec)# 开启事务 START TRANSACTION; # 查询王五的记录余额是2000 select * from bank_account where name王五; ------------------- | id | name | balance | ------------------- | 6 | 王五 | 2000.00 | ------------------- 1 row in set (0.01 sec)# 选择数据库 use test_db; # 开启事务 START TRANSACTION;# 修改王五的余额为1000 update bank_account set balance1000 where name 王五; Query OK, 1 row affected (0.00 sec)# 提交事务 commit;# 此时事务并没有提交或回滚 # 再次查询王五的记录发现余额变成了 # 与上一个查询结果不一致 select * from bank_account where name王五; ------------------- | id | name | balance | ------------------- | 6 | 王五 | 1000.00 | # 出现问题 ------------------- 1 row in set (0.00 sec)6.4.3 REPEATABLE READ - 可重复读与幻读6.4.3.1 存在问题为了解决不可重复读问题可以把事务的隔离级别设置为REPEATABLE READ 这时同一个事务中读取的数据在任何时候都是相同的结果但还会出现一个问题事务A查询了一个区间的记录得到结果集A事务B向这个区间的间隙中写入了一条记录并提交事务A再查询这个区间的结果集时会查到事务B新写入的记录得到结果集B两次查询的结果集不一致这个现象就是幻读。MySQL的InnoDB存储引擎使用了Next-Key锁解决了大部分幻读问题6.4.3.2 问题重现由于REPEATABLE READ 隔离级别默认使用了Next-Key 锁为了重现幻读问量我们把隔离级回退到更新时只加了排他锁的READ COMMITTED .# 设置隔离级别为READ COMMITTED读未提交 mysql SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED; Query OK, 0 rows affected (0.00 sec)# 查看设置是否生效 mysql SELECT GLOBAL.transaction_isolation; -------------------------------- | GLOBAL.transaction_isolation | -------------------------------- | READ-COMMITTED | # 已生效 -------------------------------- 1 row in set (0.00 sec)在不同的客户端中执行事务会话A开启事务A会话B开启事务B# 选择数据库 use test_db; # 开启事务 START TRANSACTION;# 更新五五的余额使该记录加排他锁 update account set bank_account2000 where name王五; Query OK, 1 row affected (0.01 sec)# 查询结果集更新成功 select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 800.00 | | 2 | 李四 | 1100.00 | | 6 | 王五 | 2000.00 | # 更新成功 ------------------- 3 rows in set (0.00 sec)# 选择数据库 use test_db; # 开启事务 START TRANSACTION;# 在李“四与”和“王五”之间的间隙写入 # 一条数据“赵六” insert into bank_account values (3, 赵六, 5000);# 查询结果集写入成功 select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 800.00 | | 2 | 李四 | 1100.00 | | 3 | 赵六 | 5000.00 | | 6 | 王五 | 1000.00 | ------------------- 4 rows in set (0.01 sec)# 提交事务 commit;# 查询结果集 # 发现比上一次查询的结果集 # 多出了其他事务写入的数据 select * from bank_account; ------------------- | id | name | balance | ------------------- | 1 | 张三 | 800.00 | | 2 | 李四 | 1100.00 | | 3 | 赵六 | 5000.00 | # 幻像行 | 6 | 王五 | 2000.00 | ------------------- 4 rows in set (0.01 sec)# 提交事务 commit;把隔离级别设置为REPEATABLE-READ后在ID的间隙中插入新数据观察现象比如插入ID 4的记录6.4.4 SERIALIZABLE - 串行化进一步提升事务的隔离级别到SERIALIZABLE 此时所有事务串行执行可以解决所有并发中的安全问题。6.5 不同隔离级别的性能与安全InnoDB存储引擎事务隔离性以及相关的隔离级别是由锁和MVCC机制配合实现的关于锁与MVCC的相关内容和原子性持久性一致性的相关实现原理可以参阅MySQL进阶中事务章节