一.什么是事务
事务把一组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 = '李四';如果转账成功,会出现以下结果:
1.张三的账户余额减少 100,变成 900,李四的账户余额增加了 100,变成 1100,不能出现张三的余额减少而李四的余额没有增加的情况;
2.张三和李四在发生转账前后的总额不变,也就是说转账前张三和李四的余额总额为1000+1000=2000,转账后他们的余额总数为 900+1100=2000;
3.转账后的余额结果应当保存到存储介质中,以便以后读取;
4.还有一点需要注意,在转账的处理过程中张三和李四的余额不能因其他的转账事件而受到干扰;
以上这四点在事务的整个执行过程中必须要得到保证,这也就是事务的 ACID 特性
二.事务的ACID特性
1.Atomicity (原子性):一个事务中的所有操作,要么全部成功,要么全部失败,不会出现只执行了一半的情况,如果事务在执行过程中发生错误,会回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样;
2.Consistency (一致性):在事务开始之前和事务结束以后,数据库的完整性不会被破坏。这表示写入的数据必须完全符合所有的预设规则,包括数据的精度、关联性以及关于事务执行过程中服务器崩溃后如何恢复;
3.Isolation (隔离性):数据库允许多个并发事务同时对数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务可以指定不同的隔离级别,以权衡在不同的应用场景下数据库性能和安全;
4.Durability (持久性):事务处理结束后,对数据的修改将永久的写入存储介质,即便系统故障也不会丢失。
三.为什么要使用事务
事务具备的 ACID 特性,是我们使用事务的原因
在我们日常的业务场景中有大量的需求要用事务来保证。
支持事务的数据库能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题
在使用事务过程中,要么提交,要么回滚,不用去考虑网络异常,服务器宕机等其他因素
因此我们经常接触的事务本质上是数据库对 ACID 模型的一个实现,是为应用层服务的。
四.使用事务
1.查看支持使用事务的存储引擎
可以看到MySQL中支持事务的存储引擎是InnoDB
2.语法:
开启事务之后,只要执行了rollback操作或者commit操作,事务都会关闭!!!
3.开启一个事务后执行修改后回滚
还是使用一开始的转账例子
先创建一个表,里面有张三和李四的记录
create table if not exists bank_account( id int primary key auto_increment, name varchar(50), balance decimal(20,2) ); insert into bank_account(name,balance) values('张三',1000); insert into bank_account(name,balance) values('李四',1000);然后查看bank_account
开启事务
start transaction;然后执行转账操作
update bank_account set balance = balance - 100 where name = '张三'; select * from bank_account; update bank_account set balance = balance + 100 where name = '李四'; select * from bank_account;先将张三减去100
再给李四加100
可以看到此时我们的要求已经达成了,但是我们开启了事务,还没有提交
此时我们回滚事务
然后再去查看bank_account,发现之前的修改都没有生效
4.开启一个事务后执行修改后提交
还是刚刚那个例子
先开启事务,然后查看当前表中内容
执行转账操作
update bank_account set balance = balance - 100 where name = '张三'; update bank_account set balance = balance + 100 where name = '李四';再次查看表中数据
然后提交事务
最后查看表中数据,发现之前的修改已经生效
5.保存点
概念:在执行事务的过程中可以设置保存点,ROLLBACK时可以把数据恢复到保存点的状态
语法:savepoint 保存点名
继续使用转账例子
开启一个新事务,分别在三处设置保存点,看看实际效果
分别在给张三-100前,给张三-100后,给李四+100后设置了一个保存点,此时只开启事务,没有提交
查看当前bank_account表
可以看到修改已经成功
试着回滚到保存点point003,预计应该没有任何变化,因为在设置point003之后没有对任何数据进行修改
可以看到回滚之后数据没有发生变化,符合预期
试着回滚到point002,预计原本在point002之后执行的MySQL语句,李四的balance+100会被回滚
符合预期
最后回滚到point001,数据预计回到最初的状态
6.自动/手动提交事务
默认情况下,MySQL 是自动提交事务的
也就是说我们执行的每个修改操作,比如插入、更新和删除,都会自动开启一个事务并在语句执行完成之后自动提交,发生异常时自动回滚。
可以通过以下语句设置MySQL是否自动提交
如果将事务设置成手动提交,在执行完一系列SQL语句之后,如果忘记提交,直接断开连接的话,再次登入数据库,之前的SQL都会回滚到事务开始之前.
五.事务的隔离性和隔离级别
1.什么是隔离性
MySQL 服务可以同时被多个客户端访问,每个客户端执行的 DML 语句以事务为基本单位,那么不同的客户端在对同一张表中的同一条数据进行修改的时候就可能出现相互影响的情况
为了保证不同的事务之间在执行的过程中不受影响,那么事务之间就需要要相互隔离,这种特性就是隔离性。
2.隔离级别
事务具有隔离性,那么如何实现事务之间的隔离?隔离到什么程度?如何保证数据安全的同时也要兼顾性能?这都是要思考的问题。
事务间不同程度的隔离,称为事务的隔离级别
不同的隔离级别在性能和安全方面做了取舍,有的隔离级别注重并发性,有的注重安全性,有的则是并发和安全适中
在 MySQL 的 InnoDB 引擎中事务的隔离级别有四种,分别是:
3.查看和设置隔离级别
查看隔离级别语法:
设置隔离级别语法:
各个隔离级别具体什么样子,我会在下面进行实际演示,先看怎么设置隔离级别
示例:设置全局事务隔离级别为串行化,后续所有事务生效,不影响当前事务
tip:全局事务是只有下次连接数据库才会生效,对当前连接不会影响
set global transaction isolation level serializable;示例:设置会话事务隔离级别为串行化,当前会话后续所有事务生效,不影响当前事务,可以再任何时候执行
tip:会话事务是只有在当前连接生效,当你断开再次连接数据库的时候就会回到默认的隔离级别
set session transaction isolation level serializable;如果不指定作用域,设置只针对下一个事务,之后的事务又会回到默认的隔离级别
set transaction isolation level serializable;其他设置隔离级别的方法:
六.不同隔离级别之间存在的问题
这里我们使用cmd,打开两个客户端,方便问题重现,在重现问题之前需要确定autocommit = 0
1.READUNCOMMITTED-读未提交与脏读
存在问题:
出现在事务的 READ UNCOMMITTED 隔离级别下
由于在读取数据时不做任何限制,所以并发性能很高,但是会出现大量的数据安全问题
比如在事务 A 中执行了一条 INSERT 语句,在没有执行 COMMIT 的情况下,会在事务 B 中被读取到,此时如果事务 A 执行回滚操作,那么事务 B 中读取到事务 A 写入的数据将没有意义,我们把这个现象叫做 “脏读”。
问题重现:
先打开客户端A设置隔离级别为读未提交
查看是否生效
再打开客户端B确认隔离级别,可以看出已经生效
然后再客户端A执行一系列MySQL语句,往bank_account里面插入一条新数据,但是并未提交
客户端A中可以查看
然后再打开客户端B进行查看,可以看出在客户端A没有提交的情况下,客户端B依然可以查看到客户端A修改的内容
此时在客户端A进行回滚操作
再去客户端B查看,发现刚刚插入的王五那条数据已经不在了,这种现象就叫做脏读
tip:由于 READ UNCOMMITTED 读未提交易会出现 "脏读" 现象,在正常的业务中出现这种问题会产生非常危重后果,所以正常情况下应该避免使用 READ UNCOMMITTED 读未提交这种的隔离级别。
2.READCOMMITTED-读已提交与不可重复读
存在问题:
为了解决脏读问题,可以把事务的隔离级别设置为 READ COMMITTED
这时事务只能读到了其他事务提交之后的数据,但会出现不可重复读的问题
比如事务 A 先对某条数据进行了查询,之后事务 B 对这条数据进行了修改,并且提交 (COMMIT) 事务,事务 A 再对这条数据进行查询时,得到了事务 B 修改之后的结果,这导致了事务 A 在同一个事务中以相同的条件查询得到了不同的值,这个现象要 “不可重复读”。
问题重现:
现在客户端A中设置全局事务为读已提交,查看已经生效
再打开客户端B查看隔离级别,也已经生效
然后先打开客户端A,新增一条王五数据,查看表中数据
插入数据之后开启事务
再打开客户端B,开启事务,
修改表中王五数据,将balance改为10000,查看表中数据,成功修改,此时客户端B并未提交,在客户端A中是查不到的
查看客户端A中的bank_account,发现还是2000,这就解决了之前的脏读问题
然后提交客户端B中的事务
再去客户端A中查看,发现已经是10000了,这就是读已提交
3.REPEATABLEREAD-可重复读与幻读
存在问题:
为了解决不可重复读问题,可以把事务的隔离级别设置为 REPEATABLE READ
这时同一个事务中读取的数据在任何时候都是相同的结果
但还会出现一个问题,事务 A 查询了一个区间的记录得到结果集 A,事务 B 向这个区间的间隙中写入了一条记录并提交,事务 A 再查询这个区间的结果集时会查到事务 B 新写入的记录得到结果集 B,两次查询的结果集不一致,这个现象就是 “幻读”。
MySQL 的 InnoDB 存储引擎使用了 Next-Key 锁解决了大部分幻读问题
由于 REPEATABLE READ 隔离级别默认使用了 Next-Key 锁,为了重现幻读问题,我们把隔离级回退到更新时只加了排他锁的 READ COMMITTED。
问题重现:
先设置为read committed
打开客户端A,开启事务,往李四和王五中间插入一条数据
再打开客户端B,查看bank_account表,查不到这个测试幻读这个数据,是因为客户端A没有提交,
客户端A提交事务
再去客户端B中查看,可以看到多了一条数据
4.SERIALIZABLE-串行化
进⼀步提升事务的隔离级别到SERIALIZABLE ,此时所有事务串行执行,可以解决所有并发中的安全问题。