目录
- [1. 基本概念](#1. 基本概念)
- [2. 事务常见操作方式](#2. 事务常见操作方式)
- [3. 事务的隔离级别](#3. 事务的隔离级别)
1. 基本概念
什么是事务?
事务是由一条/多条SQL构成的集合体,为了完成某种任务
为什么要有事务?
事务的存在,是为了解决 "多步数据库操作因异常导致的中间态数据问题",保证数据的完整性和一致性。
事务的属性?
- 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不丢失
- 隔离性:数据库允许多个并发事务同时对数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
- 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏
事务隔离级别?
- 读未提交(read uncommitted)
- 读提交(read committed)
- 可重复读(repeatable read)
- 串行化(Serializable)
事务的版本支持?
在MySQL中只有使用了innodb数据库引擎的数据库/表才支持事务
bash
mysql> show engines \G #查看数据库引擎
---
Engine: InnoDB #引擎名称
Support: DEFAULT #默认引擎
Comment: Supports transactions, row-level locking, and foreign keys #描述
Transactions: YES #支持事务
XA: YES
Savepoints: YES #支持事务保存点
---
事务的提交方式?
- 自动提交
- 手动提交
bash
#查看事务提交方式
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
#用set来改变MySQL的自动提交模式
mysql> set autocommit=0; #禁止自动提交
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.00 sec)
mysql> set autocommit=1; #开启自动提交
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
2. 事务常见操作方式
- 创建测试表
bash
mysql> create table account(
-> id int primary key,
-> name varchar(50) not null default '',
-> sal decimal(10,2) not null default 0.0
-> )engine=InnoDB default charset=utf8;
- 演示:证明事务的开始和回滚
bash
mysql> start transaction; #开始一个事务begin也行
Query OK, 0 rows affected (0.00 sec)
mysql> savepoint save1; #创建一个保存点save1
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values (1,'张三',1256.0); #插入一条记录
Query OK, 1 row affected (0.00 sec)
mysql> savepoint save2; #创建一个保存点save2
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(2,'李四',2456); #再插入一条记录
Query OK, 1 row affected (0.00 sec)
mysql> select * from account; #2条记录都存在了
+----+--------+---------+
| id | name | sal |
+----+--------+---------+
| 1 | 张三 | 1256.00 |
| 2 | 李四 | 2456.00 |
+----+--------+---------+
2 rows in set (0.01 sec)
mysql> rollback to save2; #回滚到保存点save2
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; #只剩一条记录了
+----+--------+---------+
| id | name | sal |
+----+--------+---------+
| 1 | 张三 | 1256.00 |
+----+--------+---------+
1 row in set (0.00 sec)
mysql> rollback; #直接rollback,回滚到最开始
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; #所有的记录都没有了
Empty set (0.00 sec)
- 演示:证明未commit,客户端崩溃,MySQL会自动回滚(隔离级别设置为读未提交)
bash
# 终端A
mysql> select * from account; # 当前表内无数据
Empty set (0.00 sec)
mysql> show variables like 'autocommit'; # 依旧自动提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
mysql> begin; #开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values (1, '张三', 100); # 插入记录
Query OK, 1 row affected (0.00 sec)
mysql> select * from account; #数据已经存在,但没有commit,此时同时查看终端B
+----+--------+--------+
| id | name | sal |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
mysql> Aborted # ctrl + \ 异常终止MySQL
#终端B
mysql> select * from account; #终端A崩溃前
+----+--------+--------+
| id | name | sal |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
mysql> select * from account; #数据自动回滚
Empty set (0.00 sec)
- 演示:证明commit了,客户端崩溃,MySQL数据不会再受到影响,已经持久化
bash
# 终端 A
mysql> show variables like 'autocommit'; # 依旧自动提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
mysql> select * from account; # 当前表内无数据
Empty set (0.00 sec)
mysql> begin; # 开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values (1, '张三', 100); # 插入记录
Query OK, 1 row affected (0.00 sec)
mysql> commit; # 提交事务
Query OK, 0 rows affected (0.04 sec)
mysql> Aborted -- ctrl + \ 异常终止MySQL
--终端 B
mysql> select * from account; # 数据存在了,所以commit的作用是将数据持久化到MySQL中
+----+--------+--------+
| id | name | sal |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
- 演示:证明单条SQL与事务的关系
bash
# 实验一
# 终端A
mysql> select * from account;
+----+--------+--------+
| id | name | sal |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
mysql> set autocommit=0; # 关闭自动提交
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values (2, '李四', 10000); # 插入记录
Query OK, 1 row affected (0.00 sec)
mysql> select *from account; # 查看结果,已经插入。此时可以在查看终端B
+----+--------+----------+
| id | name | sal |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> ^DBye # ctrl + \ or ctrl + d,终止终端
# 终端B
mysql> select * from account; # 终端A崩溃前
+----+--------+----------+
| id | name | sal |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select * from account; # 终端A崩溃后
+----+--------+--------+
| id | name | sal |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
# 实验二
# 终端A
mysql> show variables like 'autocommit'; # 开启默认提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
mysql> select * from account;
+----+--------+--------+
| id | name | sal |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
mysql> insert into account values (2, '李四', 10000);
Query OK, 1 row affected (0.01 sec)
mysql> select *from account; # 数据已经插入
+----+--------+----------+
| id | name | sal |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> Aborted # 异常终止
# 终端B
mysql> select * from account; # 终端A崩溃前
+----+--------+----------+
| id | name | sal |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select * from account; # 终端A崩溃后,并不影响,已经持久化。autocommit起作用
+----+--------+----------+
| id | name | sal |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
总结:
1.只要输入了begin / start transaction ,事务便必须要通过commit 提交,才会持久化,与是否设置set autocommit无关
2.事务可以手动回滚,同时,当操作异常,MySQL会自动回滚
3.对于InnoDB每一条SQL语言都封装成事务,自动提交
3. 事务的隔离级别
数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性
数据库中,允许事务受到不同程度的干扰,就有了一种重要特征:隔离级别
隔离级别
- 读未提交(read uncommitted):在该隔离级别,所有事务都可以看到其他事务没有提交的执行结果(实际生产中不可能使用这种隔离级别)
- 读提交(read committed):该隔离级别是大多数数据库的默认隔离级别(不是MySQL默认的)。一个事务只能看到其他已经提交的事务所做的改变。这种隔离级别会引起不可重复读------一个事务执行时,如果多次select,可能得到不同的结果
- 可重复读(repeatable read):这是MySQL默认的隔离级别,它确保同一个事务,在执行中,允许读取操作数据时,会看到同样的数据行
- 串行化(serializable):这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突。它在每个读的数据行上面加上共享锁。但是可能会导致超时和锁竞争(实际生产中基本不使用)
隔离级别如何实现?
隔离,基本都是通过锁实现的。不同的隔离级别,锁的使用是不同的
查看与设置隔离性
bash
# 查看
mysql> select @@global.transaction_isolation; #查看全局隔离级别
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| READ-UNCOMMITTED |
+--------------------------------+
1 row in set (0.00 sec)
mysql> select @@session.transaction_isolation; #查看会话(当前)全局隔离级别
+---------------------------------+
| @@session.transaction_isolation |
+---------------------------------+
| READ-UNCOMMITTED |
+---------------------------------+
1 row in set (0.00 sec)
mysql> select @@transaction_isolation; #查看当前全局隔离级别
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED |
+-------------------------+
1 row in set (0.00 sec)
#设置
#设置当前会话 or 全局隔离级别
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ
COMMITTED | REPEATABLE READ | SERIALIZABLE}
#设置当前会话隔离性
mysql> set session transaction isolation level serializable;
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)
mysql> select @@session.transaction_isolation; #会话隔离性成为串行化
+---------------------------------+
| @@session.transaction_isolation |
+---------------------------------+
| SERIALIZABLE |
+---------------------------------+
1 row in set (0.00 sec)
mysql> select @@transaction_isolation; #同上
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| SERIALIZABLE |
+-------------------------+
1 row in set (0.00 sec)
-- 注意如果没有现象,关闭MySQL客户端,重新连接
读未提交(read uncommitted) ------会出现脏读
bash
# 几乎没有加锁,虽然效率高,但是问题多
#终端A
#设置隔离级别为 读未提交
mysql> set global transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
# 重启客户端
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED |
+-------------------------+
1 row in set (0.00 sec)
mysql> select * from account;
+----+--------+---------+
| id | name | sal |
+----+--------+---------+
| 1 | 张三 | 2345.00 |
| 2 | 李四 | 2463.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
mysql> begin; #开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> update account set sal=10000 where id=1; #更新指定行
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 此时没有commit!!!
# 终端B
mysql> begin;
mysql> select * from account;
+----+--------+----------+
| id | name | sal |
+----+--------+----------+
| 1 | 张三 | 10000.00 | #读到终端A更新但未commit的数据
| 2 | 李四 | 2463.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
# 脏读------一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据
读提交(read committed)
bash
# 终端 A
mysql> set global transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
# 重启客户端
mysql> select * from account;
+----+--------+---------+
| id | name | sal |
+----+--------+---------+
| 1 | 张三 | 2345.00 |
| 2 | 李四 | 2463.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set sal=10000 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 此时没有commit
#切换到客户端B,查看数据
# commit提交了
mysql> commit
-> ;
Query OK, 0 rows affected (0.01 sec)
# 切换到终端B 再次查看
mysql>begin; #手动开启事务,和终端A一前一后
mysql> select * from account;
+----+--------+---------+
| id | name | sal |
+----+--------+---------+
| 1 | 张三 | 2345.00 | #终端A commit之前,旧的数据
| 2 | 李四 | 2463.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
mysql> select * from account; #commit之后,看到了
+----+--------+----------+
| id | name | sal |
+----+--------+----------+
| 1 | 张三 | 10000.00 | #新的值
| 2 | 李四 | 2463.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
# 但是此时还在事务中,并未commit,那么就造成了,同一事务内,同样的读取,在不同时间段(依旧还在事务操作中),读取到了不同的值,这种现象叫做不可重复读!!
可重复读(repeatable read)
bash
# 终端A
mysql> set global transaction isolation level repeatable read; --设置全局隔离级别RR
Query OK, 0 rows affected (0.01 sec)
# 关闭终端重启
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ | #隔离级别RR
+-------------------------+
1 row in set (0.00 sec)
mysql> select* from account; #查看当前数据
+----+--------+----------+
| id | name | sal |
+----+--------+----------+
| 1 | 张三 | 10000.00 |
| 2 | 李四 | 2463.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> begin; #开启事务,同步的,终端B也开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> update account set sal=5200 where id=1; #更新数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 切换到终端B,查看另一个事务是否能看到
mysql> insert into account values(3,'王五',4500); #插入记录
Query OK, 1 row affected (0.00 sec)
# 切换到终端B,查看另一个事务是否能看到
mysql> commit; #提交事务
Query OK, 0 rows affected (0.01 sec)
# 切换到终端B,查看另一个事务是否能看到
#终端B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select* from account; #终端A中事务,commit之前,查看表中数据,没有更新
+----+--------+----------+
| id | name | sal |
+----+--------+----------+
| 1 | 张三 | 10000.00 |
| 2 | 李四 | 2463.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select* from account; #终端A中事务,commit之前,查看表中数据,没有更新
+----+--------+----------+
| id | name | sal |
+----+--------+----------+
| 1 | 张三 | 10000.00 |
| 2 | 李四 | 2463.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select* from account; #终端A中事务,commit之后,查看表中数据,没有更新
+----+--------+----------+
| id | name | sal |
+----+--------+----------+
| 1 | 张三 | 10000.00 |
| 2 | 李四 | 2463.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> commit; #结束事务
Query OK, 0 rows affected (0.00 sec)
mysql> select* from account; #再次查看,看到了最新的更新数据
+----+--------+---------+
| id | name | sal |
+----+--------+---------+
| 1 | 张三 | 5200.00 |
| 2 | 李四 | 2463.00 |
| 3 | 王五 | 4500.00 |
+----+--------+---------+
3 rows in set (0.00 sec)
串行化(serializable)
bash
# 对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用
#终端A
mysql> set global transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| SERIALIZABLE |
+-------------------------+
1 row in set (0.00 sec)
mysql> begin; # 开启事务,终端B同步开启
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; #两个读取不会串行化,共享锁
+----+--------+---------+
| id | name | sal |
+----+--------+---------+
| 1 | 张三 | 5200.00 |
| 2 | 李四 | 2500.00 |
| 3 | 王五 | 4500.00 |
+----+--------+---------+
3 rows in set (0.00 sec)
mysql> insert into account values(9,'田七',987); #终端A中有插入,会阻塞,直到终端B事务提交
Query OK, 1 row affected (8.14 sec)
# 终端B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; #2个读取不会串行化
+----+--------+---------+
| id | name | sal |
+----+--------+---------+
| 1 | 张三 | 5200.00 |
| 2 | 李四 | 2500.00 |
| 3 | 王五 | 4500.00 |
+----+--------+---------+
3 rows in set (0.00 sec)
mysql> commit; # 提交之后,终端A中的insert才会提交
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+--------+---------+
| id | name | sal |
+----+--------+---------+
| 1 | 张三 | 5200.00 |
| 2 | 李四 | 2500.00 |
| 3 | 王五 | 4500.00 |
| 9 | 田七 | 987.00 |
+----+--------+---------+
4 rows in set (0.00 sec)
总结:
- 隔离级别越严格,安全性越高,但数据库并发性也就越低
- MySQL默认的隔离级别是可重复读,一般情况下不要修改
- 不可重复读的重点是修改和删除:同样的条件,你读取过的数据,再次读取出来发现值不一样了
- 幻读的重点在于新增:同样的条件,第一次和第二次读出来的记录数不一样
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
|---|---|---|---|---|
| 读未提交 | √ | √ | √ | 不加锁 |
| 读提交 | × | √ | √ | 不加锁 |
| 可重复读 | × | × | × | 不加锁 |
| 串行化 | × | × | × | 加锁 |