本篇内容
- 什么是事务,它有什么用?
- 事务的几个特性
- 事务常见操作指令详解
- 事务的隔离级别详解
- 脏读、不可重复读、可重复读、幻读详解
- 演示各种隔离级别产生的现象
- 关于隔离级别的选择
什么是事务?
数据库中的事务是指对数据库执行一批操作,这些操作最终要么全部执行成功,要么全部失败;
举个例子
比如A用户给B用户转账100操作,过程如下:
1.从A账户扣100
2.给B账户加100
如果在事务的支持下,上面最终只有2种结果:
- 操作成功:A账户减少100;B账户增加100
- 操作失败:A、B两个账户都没有发生变化
如果没有事务的支持,可能出现错:A账户减少了100,此时系统挂了,导致B账户没有加上100,而A账户凭空少了100。
事务的几个特性(ACID)
原子性(Atomicity)
事务的整个过程如原子操作一样,最终要么全部成功,或者全部失败,这个原子性是从最终结果来看的,从最终结果来看这个过程是不可分割的。
一致性(Consistency)
事务开始之前、执行中、执行完毕,这些时间点,多个人去观察事务操作的数据的时候,看到的数据都是一致的;
隔离性(Isolation)
一个事务的执行不能被其他事务干扰。每个事务的执行过程是相对独立的;
持久性(Durability)
一个事务一旦提交,他对数据库中数据的改变就应该是永久性的。当事务提交之后,数据会持久化到硬盘;
Mysql中事务操作(隐式事务 显式事务)
mysql中事务默认是隐式事务,执行insert、update、delete操作的时候,数据库自动开启事务、提交、回滚事务。
是否开启隐式事务是由变量autocommit
控制的。
隐式事务
insert、update、delete语句,事务的开启、提交或回滚由mysql内部自动控制的。
查看变量autocommit
是否开启了自动提交
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set, 1 warning (0.00 sec)
显式事务
事务需要手动开启、提交或回滚,由开发者自己控制。
2种方式手动控制事务:
方式1:
语法:
//关闭隐式事务
set autocommit=0;
//执行事务操作
commit|rollback;
示例1:提交事务操作,如下:
mysql> create table test1 (a int);
Query OK, 0 rows affected (0.01 sec)
mysql> select * from test1;
Empty set (0.00 sec)
mysql> set autocommit=0; #关闭隐式事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1 values(1);
Query OK, 1 row affected (0.00 sec)
mysql> commit; #提交事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
示例2:回滚事务操作,如下:
mysql> set autocommit=0; #关闭隐式事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1 values(2);
Query OK, 1 row affected (0.00 sec)
mysql> rollback; #回滚事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
我们把autocommit
还原回去:
mysql> set autocommit=1; #开启隐式事务
Query OK, 0 rows affected (0.00 sec)
方式2:
语法:
start transaction; #开启显式事务
commit|rollback; #提交|回滚
示例1:提交事务操作,如下:
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
mysql> start transaction; #开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1 values (2);
Query OK, 1 row affected (0.00 sec)
mysql> insert into test1 values (3);
Query OK, 1 row affected (0.00 sec)
mysql> commit; #提交事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
| 2 |
| 3 |
+------+
3 rows in set (0.00 sec)
上面成功插入了2条数据。
示例2:回滚事务操作,如下:
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
| 2 |
| 3 |
+------+
3 rows in set (0.00 sec)
mysql> start transaction; #开启显式事务
Query OK, 0 rows affected (0.00 sec)
mysql> delete from test1;
Query OK, 3 rows affected (0.00 sec)
mysql> rollback; #回滚事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
| 2 |
| 3 |
+------+
3 rows in set (0.00 sec)
上面事务中我们删除了
test1
的数据,显示删除了3行,最后回滚了事务。
savepoint关键字
在事务中我们执行了一大批操作,可能我们只想回滚部分数据,可以使用savepoin
来实现,效果如下:
清除test1
表数据:
mysql> delete from test1;
Query OK, 3 rows affected (0.00 sec)
mysql> select * from test1;
Empty set (0.00 sec)
演示savepoint
效果:
mysql> start transaction; #开启显式事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1 values (1);
Query OK, 1 row affected (0.00 sec)
mysql> savepoint part1; #设置一个保存点
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1 values (2);
Query OK, 1 row affected (0.00 sec)
mysql> rollback to part1;#回滚到保存点part1
Query OK, 0 rows affected (0.00 sec)
mysql> commit;#提交事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
从上面可以看出,执行了2次插入操作,最后只插入了1条数据。
savepoint
需要结合rollback to sp1
一起使用,可以将保存点sp1
到rollback to
之间的操作回滚掉。
只读事务
事务执行的操作是一些查询操作,性能上可能会有优化;
用法如下:
start transaction read only;
示例:
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction read only;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
| 1 |
+------+
2 rows in set (0.00 sec)
mysql> delete from test1;
ERROR 1792 (25006): Cannot execute statement in a READ ONLY transaction.
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
| 1 |
+------+
2 rows in set (0.00 sec)
只读事务中执行增删改的时候会报错;
事务中的一些问题
脏读
一个事务在执行的过程中读取到了其他事务还没有提交的数据。
读已提交
一个事务操作过程中可以读取到其他事务已经提交的数据。
事务中的每次读取操作,读取到的都是数据库中其他事务已提交的最新的数据
可重复读
一个事务操作中对于一个读取操作不管多少次,读取到的结果都是一样的。
幻读
以mysql为例:
幻读在可重复读的模式下才会出现,其他隔离级别中不会出现
幻读现象例子:
可重复读模式下,比如有个用户表,手机号码为主键,有两个事物进行如下操作
事务A操作如下:
1、打开事务
2、查询号码为X的记录,不存在
3、插入号码为X的数据,插入报错(为什么会报错,先向下看)
4、查询号码为X的记录,发现还是不存在(由于是可重复读,所以读取记录X还是不存在的)
事物B操作:在事务A第2步操作时插入了一条X的记录,所以会导致A中第3步插入报错(违反了唯一约束)
上面操作对A来说就像发生了幻觉一样,明明查询X(A中第二步、第四步)不存在,但却无法插入成功
幻读可以这么理解:事务中后面的操作(插入号码X)需要上面的读取操作(查询号码X的记录)提供支持,但读取操作却不能支持下面的操作时产生的错误,就像发生了幻觉一样。
事务的隔离级别
当多个事务同时进行的时候,如何确保当前事务中数据的正确性,比如A、B两个事物同时进行的时候,A是否可以看到B已提交的数据或者B未提交的数据,这个需要依靠事务的隔离级别来保证,不同的隔离级别中所产生的效果是不一样的。
事务隔离级别主要是解决了上面多个事务之间数据可见性及数据正确性的问题。
隔离级别分为4种:
- 读未提交:READ-UNCOMMITTED
- 读已提交:READ-COMMITTED
- 可重复读:REPEATABLE-READ
- 串行:SERIALIZABLE
上面4中隔离级别越来越强,会导致数据库的并发性也越来越低。
查看隔离级别
mysql> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
1 row in set, 1 warning (0.00 sec)
隔离级别的设置
分2步骤,修改文件、重启mysql,如下:
修改mysql中的my.init文件,我们将隔离级别设置为:READ-UNCOMMITTED,如下:
# 隔离级别设置,READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行
transaction-isolation=READ-UNCOMMITTED
以管理员身份打开cmd窗口,重启mysql,如下:
C:\Windows\system32>net stop mysql
mysql 服务正在停止..
mysql 服务已成功停止。
C:\Windows\system32>net start mysql
mysql 服务正在启动 .
mysql 服务已经启动成功。
各种隔离级别中会出现的问题
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED | 有 | 有 | 无 |
READ-COMMITTED | 无 | 有 | 无 |
REPEATABLE-READ | 无 | 无 | 有 |
SERIALIZABLE | 无 | 无 | 无 |
下面我们来演示一下,各种隔离级别中可见性的问题,开启两个窗口,叫做A、B窗口,两个窗口中登录mysql。
READ-UNCOMMITTED:读未提交
将隔离级别置为READ-UNCOMMITTED
:
重启mysql:
C:\Windows\system32>net stop mysql
mysql 服务正在停止..
mysql 服务已成功停止。
C:\Windows\system32>net start mysql
mysql 服务正在启动 .
mysql 服务已经启动成功。
查看隔离级别:
mysql> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-UNCOMMITTED |
+-----------------------+----------------+
1 row in set, 1 warning (0.00 sec)
先清空test1表数据:
delete from test1;
select * from test1;
按时间顺序在2个窗口中执行下面操作:
时间 | 窗口A | 窗口B |
---|---|---|
T1 | start transaction; | |
T2 | select * from test1; | |
T3 | start transaction; | |
T4 | insert into test1 values (1); | |
T5 | select * from test1; | |
T6 | select * from test1; | |
T7 | commit; | |
T8 | commit; |
A窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
Empty set (0.00 sec)
mysql> select * from test1; #这里我们是能够看到b事务未提交的数据的
+------+
| a |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
B窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1 values (1);
Query OK, 1 row affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
结论:读未提交情况下,可以读取到其他事务还未提交的数据,多次读取结果不一样,出现了脏读、不可重复读
READ-COMMITTED:读已提交
将隔离级别置为READ-COMMITTED
重启mysql:
C:\Windows\system32>net stop mysql
mysql 服务正在停止..
mysql 服务已成功停止。
C:\Windows\system32>net start mysql
mysql 服务正在启动 .
mysql 服务已经启动成功。
查看隔离级别:
mysql> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
1 row in set, 1 warning (0.00 sec)
先清空test1表数据:
delete from test1;
select * from test1;
按时间顺序在2个窗口中执行下面操作:
时间 | 窗口A | 窗口B |
---|---|---|
T1 | start transaction; | |
T2 | select * from test1; | |
T3 | start transaction; | |
T4 | insert into test1 values (1); | |
T5 | select * from test1; | |
T6 | select * from test1; | |
T7 | commit; | |
T8 | select * from test1; | |
T9 | commit; |
A窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
Empty set (0.00 sec)
mysql> select * from test1; #b事务没有提交的数据是看不到;
Empty set (0.00 sec)
mysql> select * from test1; #这里读取到的是b事务已经提交的数据
+------+
| a |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
B窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1 values (1);
Query OK, 1 row affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
结论:读已提交情况下,无法读取到其他事务还未提交的数据,可以读取到其他事务已经提交的数据,多次读取结果不一样,未出现脏读,出现了读已提交、不可重复读。
REPEATABLE-READ:可重复读
将隔离级别置为REPEATABLE-READ
重启mysql:
C:\Windows\system32>net stop mysql
mysql 服务正在停止..
mysql 服务已成功停止。
C:\Windows\system32>net start mysql
mysql 服务正在启动 .
mysql 服务已经启动成功。
查看隔离级别:
mysql> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+----------------+
1 row in set, 1 warning (0.00 sec)
先清空test1表数据:
delete from test1;
select * from test1;
按时间顺序在2个窗口中执行下面操作:
时间 | 窗口A | 窗口B |
---|---|---|
T1 | start transaction; | |
T2 | select * from test1; | |
T3 | start transaction; | |
T4 | insert into test1 values (1); | |
T5 | select * from test1; | |
T6 | select * from test1; | |
T7 | commit; | |
T8 | select * from test1; | |
T9 | commit; | |
T10 | select * from test1; |
A窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
Empty set (0.00 sec)
mysql> select * from test1;
Empty set (0.00 sec)
mysql> select * from test1;
Empty set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
| 1 |
+------+
2 rows in set (0.00 sec)
B窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1 values (1);
Query OK, 1 row affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
| 1 |
+------+
2 rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
结论:可重复读情况下,未出现脏读,未读取到其他事务已提交的数据,多次读取结果一致,即可重复读。
幻读演示
幻读只会在REPEATABLE-READ
(可重复读)级别下出现,需要先把隔离级别改为可重复读。
将隔离级别置为REPEATABLE-READ
重启mysql:
C:\Windows\system32>net stop mysql
mysql 服务正在停止..
mysql 服务已成功停止。
C:\Windows\system32>net start mysql
mysql 服务正在启动 .
mysql 服务已经启动成功。
查看隔离级别:
mysql> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+----------------+
1 row in set, 1 warning (0.00 sec)
准备数据:
mysql> create table t_user(id int primary key,name varchar(16) unique key);
Query OK, 0 rows affected (0.01 sec)
mysql> insert into t_user values (1,'mysql'),(2,'mysql');
ERROR 1062 (23000): Duplicate entry 'mysql' for key 'name'
mysql> select * from t_user;
Empty set (0.00 sec)
上面我们创建t_user表,name添加了唯一约束,表示name不能重复,否则报错。
按时间顺序在2个窗口中执行下面操作:
时间 | 窗口A | 窗口B |
---|---|---|
T1 | start transaction; | |
T2 | start transaction; | |
T3 | – 插入 insert into t_user values (1,‘mysql’); | |
T4 | select * from t_user; | |
T5 | – 查看是否存在 select * from t_user where name=‘mysql’; | |
T6 | commit; | |
T7 | – 插入 insert into t_user values (2,‘mysql’); | |
T8 | – 查看是否存在 select * from t_user where name=‘mysql’; | |
T9 | commit; |
A窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_user where name='mysql';
Empty set (0.00 sec)
mysql> insert into t_user values (2,'mysql');
ERROR 1062 (23000): Duplicate entry 'mysql' for key 'name'
mysql> select * from t_user where name='mysql';
Empty set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
B窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t_user values (1,'mysql');
Query OK, 1 row affected (0.00 sec)
mysql> select * from t_user;
+----+---------------+
| id | name |
+----+---------------+
| 1 | mysql |
+----+---------------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
SERIALIZABLE:串行
SERIALIZABLE会让并发的事务串行执行。
看效果:
将隔离级别置为SERIALIZABLE
# 隔离级别设置,READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行
transaction-isolation=SERIALIZABLE
重启mysql:
C:\Windows\system32>net stop mysql
mysql 服务正在停止..
mysql 服务已成功停止。
C:\Windows\system32>net start mysql
mysql 服务正在启动 .
mysql 服务已经启动成功。
查看隔离级别:
mysql> show variables like 'transaction_isolation';
+-----------------------+--------------+
| Variable_name | Value |
+-----------------------+--------------+
| transaction_isolation | SERIALIZABLE |
+-----------------------+--------------+
1 row in set, 1 warning (0.00 sec)
先清空test1表数据:
delete from test1;
select * from test1;
按时间顺序在2个窗口中执行下面操作:
时间 | 窗口A | 窗口B |
---|---|---|
T1 | start transaction; | |
T2 | select * from test1; | |
T3 | start transaction; | |
T4 | insert into test1 values (1); | |
T5 | select * from test1; | |
T6 | commit; | |
T7 | commit; |
按时间顺序运行上面的命令,会发现T4-B这样会被阻塞,直到T6-A执行完毕。
可以看出来,事务只能串行执行了。串行情况下不存在脏读、不可重复读、幻读的问题了。
关于隔离级别的选择
- 隔离级别越高,并发性也低,比如最高级别
SERIALIZABLE
会让事物串行执行,并发操作变成串行了,会导致系统性能直接降低。 - 具体选择哪种需要结合具体的业务来选择。
- 读已提交(READ-COMMITTED)通常用的比较多。
总结
- 理解事务的4个特性:原子性、一致性、隔离性、持久性
- 掌握事务操作常见命令的介绍
set autocommit
可以设置是否开启自动提交事务- start transaction:开启事务
- start transaction read only:开启只读事物
- commit:提交事务
- rollback:回滚事务
- savepoint:设置保存点
- rollback to 保存点:可以回滚到某个保存点
- 掌握4种隔离级别及了解其特点
- 了解脏读、不可重复读、幻读