【MySQL】-- 事务
文章目录
- 1. 什么是事务
- 2. 事务的ACID特性(重要!!!)
- 3. 为什么要使用事务
- 4. 如何使用事务
- 4.1 查看支持事务的存储引擎
- 4.2 自动/手动提交事务
- 4.3 语法
- 4.4 开启一个事务,执行修改后回滚
- 4.5 开启一个事务,执行修改后提交
- 4.6 保存点
- 5. 事务的隔离性和隔离级别
- 5.1 什么是隔离性
- 5.2 隔离级别
- 5.3 查看和设置隔离级别
- 5.4 不同隔离级别存在的问题
- 5.4.1 READ UNCOMMITTED -- 读未提交与脏读
- 5.4.2 READ COMMITTED -- 读已提交与不可重复读
- 5.4.3 REPEATABLE READ -- 可重复读
- 5.4.4 SERIALIZABLE--串行化
- 5.5 不同隔离级别的性能与安全
1. 什么是事务
事务把一组SQL语句打包成为一个整体,在这组SQL的执行过程中,要么全部成功,要么全部失败。这组SQL语句可以是一条也可以是多条。来看一个转账的例子,如图:
转账之前和转账之后张三和李四的总额都是2000
mysql> create table bank_account(-> id bigint primary key auto_increment,-> name varchar(255) not null,-> balance decimal(10, 2) not null-> );
Query OK, 0 rows affected (0.06 sec)mysql> insert into bank_account(name, balance) values ('张三', 1000), ('李四', 1000);
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
如果在转账的过程中张三的余额减少100之后服务器崩溃了,李四的余额没有加100,最终两个人的余额之和是900 + 1000 = 1900
如果转账成功,应该有以下结果
- 张三的账户余额减少100,变成900,李四的账户余额增加了100,变成1100,不能出现张三的余额减少而李四的余额没有增加的情况。
- 张三和李四在发生转账前后的总额不变,也就是说转账前张三和李四的余额总数为1000+1000=2000,转账后他们的余额总数为900+1100=2000。
- 转账后的余额结果应当保存到存储介质中,以便以后读取。
- 还有一点需要注意,在转账的处理过程中张三和李四的余额不能因其他的转账事件而受到干扰。
需要把不同的操作隔离开,比如,两个客户端同时执行
update bank_account set baance = alance - 100 where name = '张三';
这个语句。
- 客户端1从数据库中把1000读出来。
- 客户端1把张三的余额减100
- 客户端2从数据库中把1000读出来
- 客户端2把张三的余额减100
- 客户端1把修改后的余额存回数据库
- 客户端2把修改后的余额存回数据库
客户端1和客户端2同时执行update操作,也就是说张三的余额原定是要减200的,但是由于两个客户端是同时发生的执行更新语句,导致最终结果只减了100,这是一个错误的结果,这个现象就是两个客户端之间进行了干扰。
以上这四点在事务的整个执行过程中必须要得到保证,这也就是事务的ACID特性。
2. 事务的ACID特性(重要!!!)
事务的ACID特性指的是Atomicity(原子性),Consistency(一致性),Isolation(隔离性)和DurabiLity(持久性)。
- **Atomicity(原子性):**一个事务中的所有操作,要么全部成功,要么全部失败,不会出现只执行了一半的情况,如果事务在执行过程中发生错误,会回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
支持事务的数据库中最基本的一个特性,一组sql要么全部成功,要么全部失败。
事务如果没有出现错误则提交,数据进行落盘,永久保存。
事务如果在执行中出现错误,则回滚到事务开始之前的状态。
- **Consistency(一致性):**在事务开始之前和事务结束以后,数据库的完整性不会被破坏。这表示写入的数据必须完全符合所有的预设规则,包括数据的精度、关联性以及关于事务执行过程中服务器崩溃后如何恢复。
事务执行完成之和,需要保证数据的正确并且复合预期。
- **Isolation(隔离性):**数据库允许多个并发事务同时对数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务可以指定不同的隔离级别,以权衡在不同的应用场景下数据库性能和安全。
多个事务之间不能相互影响。
- **Durability(持久性):**事务处理结束后,对数据的修改将永久的写入存储介质,即便系统故障也不会丢失。
事务一旦提交,就需要永久的保存到存储介质中,不论数据库软件或电脑和操作系统是否崩溃,都不会影响数据的安全。
3. 为什么要使用事务
事务具备的ACID特性,是我们使用事务的原因,在我们日常的业务场景中有大量的需求要用事务来保证。支持事务的数据库能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题,在使用事务过程中,要么提交,要么回滚,不用去考虑网络异常,服务器岩机等其他因素,因此我们经常接触的事务本质上是数据库对ACID模型的一个实现,是为应用层服务的。
如果数据库不保证事务,那么业务系统也要去保证事务的特性。
4. 如何使用事务
4.1 查看支持事务的存储引擎
要使用事务,那么数据库就要支持事务,在MySQL中支持事务的存储引擎是InnoDB,可以通过show engines;
语句查看:
mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine | Support | Comment | Transactions | XA | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
| MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
| CSV | YES | CSV storage engine | NO | NO | NO |
| FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL |
| PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
| MyISAM | YES | MyISAM storage engine | NO | NO | NO |
| InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES |
| ndbinfo | NO | MySQL Cluster system information storage engine | NULL | NULL | NULL |
| BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
| ARCHIVE | YES | Archive storage engine | NO | NO | NO |
| ndbcluster | NO | Clustered, fault-tolerant tables | NULL | NULL | NULL |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
11 rows in set (0.01 sec)
InnoDB是一个常用的存储引擎。
4.2 自动/手动提交事务
- 默认情况下,MySQL是自动提交事务的,也就是说我们执行的每个修改操作,比如插入、更新和删除,都会自动开启一个事务并在语句执行完成之和自动提交,发生异常时自动回滚。
查看当前事务是否自动提交:
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set, 1 warning (0.03 sec)
on表示开启了事务的自动提交
off表示关闭了事务的自动提交
关闭当前事务自动提交:
mysql> set autocommit = off;
Query OK, 0 rows affected (0.01 sec)mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set, 1 warning (0.00 sec)
事务手动提交的模式下,不用手动开启事务,执行完sql之后,手动commit或rollback。
4.3 语法
开启一个新的事务
start transaction;
-- 或
begin
提交当前事务,并对更改持久化保存
commit;
回滚当前事务,取消其更改
rollback;
- 无论提交还是回滚,事务都会关闭.
- 开启一个事务之后,在事务提交或回滚之前写的所有SQL都应具有ASID特性。
4.4 开启一个事务,执行修改后回滚
开启事务
mysql> start transaction;
Query OK, 0 rows affected (0.01 sec)
执行更新语句
mysql> update bank_account set balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1000.00 |
+----+------+---------+
2 rows in set (0.00 sec)mysql> update bank_account set balance = balance + 100 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0mysql> 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.01 sec)mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 1000.00 |
| 2 | 李四 | 1000.00 |
+----+------+---------+
2 rows in set (0.00 sec)
事务回滚之后,事务中所做的修改全部取消,回到事务开启之前的状态,同时会关闭事务。
4.5 开启一个事务,执行修改后提交
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)mysql> update bank_account set balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1000.00 |
+----+------+---------+
2 rows in set (0.00 sec)mysql> update bank_account set balance = balance + 100 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0mysql> 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)
事务提交之后,把事务中对数据的修改保存到磁盘,同时关闭事务。事务一旦提交就不能再回滚了。
注意:
- 只要使用
start transaction
或begin
开启事务,必须要通过commit
提交才会持久化,与是否设置set autocommit
无关。 - 手动提交模式下,不用显示开启事务,执行修改操作后,提交或回滚事务时直接使用
commit
或rollback
。 - 已提交的事务不能回滚。
4.6 保存点
保存点就相当于一个阶段性的快照。
-- 开启事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)-- 查询bank_account表最初的数据
mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)-- 执行更新操作
mysql> update bank_account set balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0mysql> update bank_account set 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)-- 设置记忆点1
mysql> savepoint savepoint1;
Query OK, 0 rows affected (0.00 sec)-- 再次更新数据
mysql> update bank_account set balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0mysql> update bank_account set balance = balance + 100 where name = '李四';
Query OK, 1 row affected (0.01 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)-- 设置保存点2
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.03 sec)-- 回滚到记忆点2
mysql> rollback to savepoint2;
Query OK, 0 rows affected (0.03 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.00 sec)-- 回到事务开始前的原始状态
mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
5. 事务的隔离性和隔离级别
5.1 什么是隔离性
MySQL服务可以同时被多个客户端访问,每个客户端执行的DML语句以事务为基本单位,那么不同的客户端在对同一张表中的同一条数据进行修改的时候就可能出现相互影响的情况,为了保证不同的事务之间在执行的过程中不受影响,那么事务之间就需要要相互隔离,这种特性就是隔离性。
5.2 隔离级别
事务具有隔离性,那么如何实现事务之间的隔离?隔离到什么程度?如何保证数据安全的同时也要兼顾性能?
事务间不同程度的隔离,称为事务的隔离级别;不同的隔离级别在性能和安全方面做了取舍,有的隔离级别注重并发性,有的注重安全性,有的则是并发和安全适中;在MySQL的InnoDB引I擎中事务的隔离级别有四种,分别是:
read uncommitted
:读未提交read committed
:读已提交repeatable read
:可重复读serializable
:串行化
从上往下隔离程度依次升高。
从上往下性能(并发性)依次降低。
5.3 查看和设置隔离级别
- 事务的隔离级别分为全局作用域和会话作用域,查看不同作用域事务的隔离级别,可以使用以下的方式:
-- 查看全局作用域的事务隔离级别
mysql> select @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ | -- 可重复读
+--------------------------------+
1 row in set (0.00 sec)-- 查看会话作用域的事务隔离级别
mysql> select @@SESSION.transaction_isolation;
+---------------------------------+
| @@SESSION.transaction_isolation |
+---------------------------------+
| REPEATABLE-READ | -- 可重复读
+---------------------------------+
1 row in set (0.00 sec)
5.4 不同隔离级别存在的问题
5.4.1 READ UNCOMMITTED – 读未提交与脏读
老师布置的作业,我写完之后放在我的桌子上,我的同桌看到之后拿去参考了,但是我发现我作业有些地方写错了,自己又写了一遍,最后交作业的时候同桌发现我俩最终的作业内容不太一样。
对应到事务中:事务A对数据进行了修改,事务B访问了事务A还没有提交的数据,这个情况叫“脏读”。
- 在一个客户端A中先设置全局事务隔离级别为
read uncommitted
读未提交
-- 设置隔离级别为读未提交
mysql> set global transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.03 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)
- 在不同的客户端中执行事务
- 客户端1
-- 开启事务
mysql> start transaction;
Query OK, 0 rows affected (0.03 sec)-- 插入一条数据
mysql> insert into bank_account values (null, '王五', 2000);
Query OK, 1 row affected (0.00 sec)-- 查询
mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
| 5 | 王五 | 2000.00 |
+----+------+---------+
3 rows in set (0.00 sec)
- 客户端2
-- 创建事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)-- 查询结果(发现查询到了客户端1没有提交的数据)
mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
| 5 | 王五 | 2000.00 |
+----+------+---------+
3 rows in set (0.03 sec)
- 客户端1
-- 回滚
mysql> rollback;
Query OK, 0 rows affected (0.03 sec)-- 查询结果,数据正常回滚
mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
- 客户端2
-- 再次查询结果,刚刚‘王五’这条记录不存在了
mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
5.4.2 READ COMMITTED – 读已提交与不可重复读
还有一种情况,我写完作业可能会直接交给组长(相当于提交的过程),我同桌在参考我已经交的作业的同时,我对作业进行了修改,然后又重新交了一份。我的同桌第一次去参考我作业的内容和第二次去参考我作业的内容,发现两次的作业内容不一样。
对应到事务中:事务A第一次查询某条数据,此时事务B对记录进行了修改并且提交事务,当事务A再次去查询这条记录时,发现与第一次查询的结果不一样,这个现象叫“不可重复读”。
- 在客户端1中先设置全局事务隔离级别
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)
- 打开客户端2并确认隔离级别
mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| READ-COMMITTED |
+--------------------------------+
1 row in set (0.00 sec)
- 客户端2
mysql> select * from bank_account;
Empty set (0.00 sec)mysql> insert into bank_account values (null, '王五', 2000);
Query OK, 1 row affected (0.01 sec)mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 6 | 王五 | 2000.00 |
+----+------+---------+
1 row in set (0.00 sec)
- 客户端1
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)mysql> update bank_account set balance = 1000 where name = '王五';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 6 | 王五 | 1000.00 |
+----+------+---------+
1 row in set (0.00 sec)mysql> commit;
Query OK, 0 rows affected (0.01 sec)
- 客户端2
mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 6 | 王五 | 1000.00 |
+----+------+---------+
1 row in set (0.00 sec)
5.4.3 REPEATABLE READ – 可重复读
我写完所有科的作业之后,还是把所有作业交给了组长,我的同桌拿到我的所有作业,留下了一科作业来进行参考,同桌在参考我的作业的时候,联系我说:“桌桌,我在抄你的作业,你先别改哈”。我说:“好的呢😀”。这时候,就相当于给这个作业加了一把锁,但是我可以重新提交和修改我的其他作业。
但是当我同桌再次拿到我所有科的作业的时候会发现两次拿到的作业不一致。
事务A第一次查询到的结果集与第二次以相同条件查询到的结果集不一致,这个现象叫“幻读”。
在InnoDB存储引擎中,使用了next_key锁,锁住了目标行和之前的间隙,解决了部分的幻读问题。
5.4.4 SERIALIZABLE–串行化
串行化可以解决所有的数据安全问题,所有的事务一个接一个的执行,下一个事务必须要等到上一个事务执行完成之后才可以继续执行。
5.5 不同隔离级别的性能与安全
InnoDB存储引擎事务隔离性以及相关的隔离级别是由锁和MVCC机制配合实现的。