吃透MySQL(六):事务详细介绍

一,什么是事物

数据库中的事务是指对数据库执行一批操作,这些操作最终要么全部执行成功,要么全部失败,不会存在部分成功的情况。

举个例子

比如A用户给B用户转账100操作,过程如下:

  1. 从A账户扣100
  2. 给B账户加100

如果在事务的支持下,上面最终只有2种结果:

  1. 操作成功:A账户减少100;B账户增加100。
  2. 操作失败:A、B两个账户都没有发生变化。

如果没有事务的支持,可能出现错:A账户减少了100,此时系统挂了,导致B账户没有加上100,而A账户凭空少了100。

二,事物的几个特性(ACID)

  • 原子性(Atomicity):事务的整个过程如原子操作一样,最终要么全部成功,或者全部失败,这个原子性是从最终结果来看的,从最终结果来看这个过程是不可分割的。
  • 一致性(Consistency):事务开始之前、执行中、执行完毕,这些时间点,多个人去观察事务操作的数据的时候,看到的数据都是一致的,比如在事务操作过程中,A连接看到的是100,那么B此时也去看的时候也是100,不会说AB看到的数据不一样,他们在某个时间点看到的数据是一致的。
  • 隔离性(Isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持久性(Durability):一个事务一旦提交,他对数据库中数据的改变就应该是永久性的。当事务提交之后,数据会持久化到硬盘,修改是永久性的。

三,事物操作

mysql中事务默认是隐式事务,执行insert、update、delete操作的时候,数据库自动开启事务、提交或回滚事务。

是否开启隐式事务是由变量autocommit控制的。

所以事务分为隐式事务显式事务

1,隐式事务

事务自动开启、提交或回滚,比如insert、update、delete语句,事务的开启、提交或回滚由mysql内部自动控制的。

查看变量autocommit是否开启了自动提交:

mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)

autocommit为ON表示开启了自动提交。

2,显式事物

事务需要手动开启、提交或回滚,由开发者自己控制。

2种方式手动控制事务:

2.1,方式1

  1. 设置不自动提交事务 set autocommit=0;
  2. 手动执行事务操作 commit|rollback;

提交事物如下:

mysql> create database test_trans;
Query OK, 1 row affected (0.07 sec)

mysql> use test_trans;
Database changed
mysql> create table test1(id int not null);
Query OK, 0 rows affected (0.23 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.05 sec)

mysql> select * from test1;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

mysql> 

示例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.04 sec)

mysql> select * from test1;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

mysql> 

可以看到上面数据回滚了。

我们把autocommit还原回去,还设置成自动提交:

mysql> set autocommit=1;
Query OK, 0 rows affected (0.00 sec)

2.2,方式2

语法:

# 开启事物
start transaction;
# 执行事物操作
commit | rollback;

示例1:提交事务操作,如下:

mysql> select * from test1;
+----+
| id |
+----+
|  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.05 sec)

mysql> select * from test1;
+----+
| id |
+----+
|  1 |
|  2 |
|  3 |
+----+
3 rows in set (0.00 sec)

上面成功插入了2条数据。

示例2:回滚事务操作,如下:

mysql> select * from test1;
+----+
| id |
+----+
|  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.02 sec)

mysql> select * from test1;
+----+
| id |
+----+
|  1 |
|  2 |
|  3 |
+----+
3 rows in set (0.00 sec)

上面事务中我们删除了test1的数据,显示删除了3行,最后回滚了事务。

3,savepoint关键字

在事务中我们执行了一大批操作,可能我们只想回滚部分数据,怎么做呢?

我们可以将一大批操作分为几个部分,然后指定回滚某个部分。可以使用savepoin来实现,效果如下:

mysql> delete from test1;
Query OK, 3 rows affected (0.09 sec)

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 point1;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test1 values(2);
Query OK, 1 row affected (0.01 sec)

# 回退到安全点
mysql> rollback point1;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'point1' at line 1
mysql> rollback to point1;
Query OK, 0 rows affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.10 sec)

mysql> select * from test1;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

mysql> 

从上面可以看出,执行了2次插入操作,最后只插入了1条数据。

savepoint需要结合rollback to sp1一起使用,可以将保存点sp1rollback to之间的操作回滚掉。

4,只读事物

表示在事务中执行的是一些只读操作,如查询,但是不会做insert、update、delete操作,数据库内部对只读事务可能会有一些性能上的优化。

用法如下:

start transaction read only;

示例:

mysql> start transaction read only;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test1;
+----+
| id |
+----+
|  1 |
+----+
1 row 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;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

只读事务中执行delete会报错。

四,事物中的一些问题

这些问题主要是基于数据在多个事务中的可见性来说的。

  • 脏读:一个事务在执行的过程中读取到了其他事务还没有提交的数据。
  • 读已提交:即一个事务操作过程中可以读取到其他事务已经提交的数据。
  • 可重复读:一个事务操作中对于一个读取操作不管多少次,读取到的结果都是一样的。
  • 幻读:幻读在可重复读的模式下才会出现,其他隔离级别中不会出现

幻读现象例子:

可重复读模式下,比如有个用户表,手机号码为主键,有两个事物进行如下操作:

事务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未提交的数据,这个需要依靠事务的隔离级别来保证,不同的隔离级别中所产生的效果是不一样的。

事务隔离级别主要是解决了上面多个事务之间数据可见性及数据正确性的问题。

1,隔离级别分为4种:

  1. 读未提交:READ-UNCOMMITTED
  2. 读已提交:READ-COMMITTED
  3. 可重复读:REPEATABLE-READ
  4. 串行:SERIALIZABLE

上面4中隔离级别越来越强,会导致数据库的并发性也越来越低。

2,查看隔离级别

mysql> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.01 sec)

默认隔离级别为:REPEATABLE-READ

3,隔离级别的设置

set session transaction isolation level read committed;
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'transaction_isolation';
+-----------------------+------------------+
| Variable_name         | Value            |
+-----------------------+------------------+
| transaction_isolation | READ-UNCOMMITTED |
+-----------------------+------------------+
1 row in set (0.00 sec)

4,各种隔离级别中会出现的问题

隔离级别 脏读 不可重复读 幻读
READ-UNCOMMITTED
READ-COMMITTED
REPEATABLE-READ
SERIALIZABLE

幻读只会在可重复读级别中才会出现,其他级别下不存在。

下面我们来演示一下,各种隔离级别中可见性的问题,开启两个窗口,叫做A、B窗口,两个窗口中登录mysql。

4.1,READ-UNCOMMITTED:读未提交

将隔离级别置为READ-UNCOMMITTED

mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.01 sec)

mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED        |
+-------------------------+
1 row in set (0.00 sec)

按时间顺序在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;
+----+
| id |
+----+
|  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;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.05 sec)

看一下:

T2-A:无数据,T6-A:有数据,T6时刻B还未提交,此时A已经看到了B插入的数据,说明出现了脏读

T2-A:无数据,T6-A:有数据,查询到的结果不一样,说明不可重复读

结论:读未提交情况下,可以读取到其他事务还未提交的数据,多次读取结果不一样,出现了脏读、不可重复读

4.2,READ-COMMITTED:读已提交

将隔离级别置为READ-COMMITTED

mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-COMMITTED          |
+-------------------------+
1 row in set (0.00 sec)

先清空test1表数据:

mysql> delete from test1;
Query OK, 1 row affected (0.06 sec)

mysql> select * from test1;
Empty set (0.00 sec)

按时间顺序在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;
Empty set (0.00 sec)

mysql> select * from test1;
+----+
| id |
+----+
|  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;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.07 sec)

看一下:

T5-B:有数据,T6-A窗口:无数据,A看不到B的数据,说明没有脏读

T6-A窗口:无数据,T8-A:看到了B插入的数据,此时B已经提交了,A看到了B已提交的数据,说明可以读取到已提交的数据

T2-A、T6-A:无数据,T8-A:有数据,多次读取结果不一样,说明不可重复读

结论:读已提交情况下,无法读取到其他事务还未提交的数据,可以读取到其他事务已经提交的数据,多次读取结果不一样,未出现脏读,出现了读已提交、不可重复读。

4.3,REPEATABLE-READ:可重复读

将隔离级别置为REPEATABLE-READ

mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.00 sec)

先清空test1表数据:

mysql> delete from test1;
Query OK, 1 row affected (0.06 sec)

mysql> select * from test1;
Empty set (0.00 sec)

按时间顺序在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;
+----+
| id |
+----+
|  1 |
+----+
1 row 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;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.05 sec)

看一下:

T2-A、T6-A窗口:无数据,T5-B:有数据,A看不到B的数据,说明没有脏读

T8-A:无数据,此时B已经提交了,A看不到B已提交的数据,A中3次读的结果一样都是没有数据的,说明可重复读

结论:可重复读情况下,未出现脏读,未读取到其他事务已提交的数据,多次读取结果一致,即可重复读。

幻读演示

幻读只会在REPEATABLE-READ(可重复读)级别下出现,需要先把隔离级别改为可重复读。

将隔离级别置为REPEATABLE-READ

mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.00 sec)

准备数据:

我们创建user表,name添加了唯一约束,表示name不能重复,否则报错。

mysql> create table user(id int primary key,name varchar(20) unique key);
Query OK, 0 rows affected (0.81 sec)

mysql> insert into user values(1,'bobo'),(2,'bobo');
ERROR 1062 (23000): Duplicate entry 'bobo' for key 'user.name'

mysql> select * from user;
Empty set (0.00 sec)

按时间顺序在2个窗口中执行下面操作:

时间 窗口A 窗口B
T1 start transaction;
T2 start transaction;
T3 insert into t_user values (1,‘bobo’);
T4 select * from t_user;
T5 select * from t_user where name=‘bobo’;
T6 commit;
T7 insert into t_user values (2,‘bobo’);
T8 select * from t_user where name=‘bobo’;
T9 commit;

A窗口如下:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user;
Empty set (0.00 sec)

mysql> insert into user values(2,'bobo');
ERROR 1062 (23000): Duplicate entry 'bobo' for key 'user.name'
mysql> select * from user;
Empty set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.07 sec)

B窗口如下:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into user values(1,'bobo');
Query OK, 1 row affected (0.00 sec)

mysql> select * from user;
+----+------+
| id | name |
+----+------+
|  1 | bobo |
+----+------+
1 row in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.06 sec)

看一下:

A想插入数据bobo,插入之前先查询了一下(T5时刻)该用户是否存在,发现不存在,然后在T7时刻执行插入,报错了,报数据已经存在了,因为T6时刻B已经插入了bobo

然后A有点郁闷,刚才查的时候不存在的,然后A不相信自己的眼睛,又去查一次(T8时刻),发现bobo还是不存在的。

此时A心里想:数据明明不存在啊,为什么无法插入呢?这不是懵逼了么,A觉得如同发生了幻觉一样。

4.4,SERIALIZABLE:串行

SERIALIZABLE会让并发的事务串行执行。

将隔离级别置为SERIALIZABLE

mysql> set session 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)

先清空test1表数据:

mysql> delete from test1;
Query OK, 1 row affected (0.06 sec)

mysql> select * from test1;
Empty set (0.00 sec)

按时间顺序在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执行完毕。

可以看出来,事务只能串行执行了。串行情况下不存在脏读、不可重复读、幻读的问题了。

5,关于隔离级别的选择

  1. 需要对各种隔离级别产生的现象非常了解,然后选择的时候才能游刃有余
  2. 隔离级别越高,并发性也低,比如最高级别SERIALIZABLE会让事物串行执行,并发操作变成串行了,会导致系统性能直接降低。
  3. 具体选择哪种需要结合具体的业务来选择。
  4. 读已提交(REPEATABLE-READ)通常用的比较多。

六,总结

  1. 理解事务的4个特性:原子性、一致性、隔离性、持久性
  2. 掌握事务操作常见命令的介绍
  3. set autocommit可以设置是否开启自动提交事务
  4. start transaction:开启事务
  5. start transaction read only:开启只读事物
  6. commit:提交事务
  7. rollback:回滚事务
  8. savepoint:设置保存点
  9. rollback to 保存点:可以回滚到某个保存点
  10. 掌握4种隔离级别及了解其特点
  11. 了解脏读、不可重复读、幻读

猜你喜欢

转载自blog.csdn.net/u013277209/article/details/113585022