数据库中的事务是指对数据库执行一批操作,在同一个事务当中,这些操作最终要么全部执行成功,要么全部失败,不会存在部分成功的情况。
事务是一个原子操作。是一个最小执行单元。可以甶一个或多个SQL语句组成
在同一个事务当中,所有的SQL语句都成功执行时,整 个事务成功,有一个SQL语句执行失败,整个事务都执行失败。
举个例子:
比如A用户给B用户转账100操作,过程如下:
从A账户扣100
给B账户加100
如果在事务的支持下,上面最终只有2种结果:
操作成功:A账户减少100;B账户增加100
操作失败:A、B两个账户都没有发生变化
如果没有事务的支持,可能出现错:A账户减少了100,此时系统挂了,导致B账户没有加上100,而A账户凭空少了100。
原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
BEGIN 或 START TRANSACTION 显式地开启一个事务;
COMMIT 也可以使用 COMMIT WORK,不过二者是等价的。COMMIT 会提交事务,并使已对数据库进行的所有修改成为永久性的;
ROLLBACK 也可以使用 ROLLBACK WORK,不过二者是等价的。回滚会结束用户的事务,并撤销正在进行的所有未提交的修改;
SAVEPOINT identifier,SAVEPOINT 允许在事务中创建一个保存点,一个事务中可以有多个 SAVEPOINT;
RELEASE SAVEPOINT identifier 删除一个事务的保存点,当没有指定的保存点时,执行该语句会抛出一个异常;
ROLLBACK TO identifier 把事务回滚到标记点;
SET TRANSACTION 用来设置事务的隔离级别。InnoDB 存储引擎提供事务的隔离级别有READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ 和 SERIALIZABLE。
1、用 BEGIN, ROLLBACK, COMMIT 来实现
BEGIN 或 START TRANSACTION:开用于开始一个事务。
ROLLBACK 事务回滚,取消之前的更改。
COMMIT:事务确认,提交事务,使更改永久生效。
2、直接用 SET 来改变 MySQL 的自动提交模式:
SET AUTOCOMMIT=0 禁止自动提交
SET AUTOCOMMIT=1 开启自动提交
BEGIN 或 START TRANSACTION -- 用于开始一个事务:
COMMIT -- 用于提交事务,将所有的修改永久保存到数据库:
ROLLBACK -- 用于回滚事务,撤销自上次提交以来所做的所有更改:
SAVEPOINT -- 用于在事务中设置保存点,以便稍后能够回滚到该点:
ROLLBACK TO SAVEPOINT -- 用于回滚到之前设置的保存点:
-- 开始事务 START TRANSACTION; -- 执行一些SQL语句 UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; UPDATE accounts SET balance = balance + 100 WHERE user_id = 2; -- 判断是否要提交还是回滚 IF (条件) THEN COMMIT; -- 提交事务 ELSE ROLLBACK; -- 回滚事务 END IF;
-- 设置不自动提交事务 set autocommit=0; -- 执行事务操作 commit|rollback;
提交事务操作,如下:
mysql> create table test1 (a int); mysql> select * from test1; Empty set (0.00 sec) mysql> set autocommit=0; mysql> insert into test1 values(1); mysql> commit; mysql> select * from test1; +------+ | a | +------+ | 1 | +------+
示例2:回滚事务操作,如下:
mysql> set autocommit=0; mysql> insert into test1 values(2); mysql> rollback; mysql> select * from test1; +------+ | a | +------+ | 1 | +------+
-- 开启事务 start transaction; -- 执行事务操作 commit|rollback;
示例1:提交事务操作,如下:
mysql> select * from test1; +------+ | a | +------+ | 1 | +------+ mysql> start transaction; mysql> insert into test1 values (2); mysql> insert into test1 values (3); mysql> commit; mysql> select * from test1; +------+ | a | +------+ | 1 | | 2 | | 3 | +------+
示例2:回滚事务操作,如下:
mysql> select * from test1; +------+ | a | +------+ | 1 | | 2 | | 3 | +------+ mysql> start transaction; mysql> delete from test1; mysql> rollback; mysql> select * from test1; +------+ | a | +------+ | 1 | | 2 | | 3 | +------+
一个事务在执行的过程中读取到了其他事务还没有提交的数据。 这个还是比较好理解的。
两个事务同时操作同一数据,A事务对该数据进行了修改还没提交的时候,B事务访问了该条事务,并且使用了该数据,此时A事务回滚,那么B事务读到的就是脏数据。
比如事务1,修改了某个数据 事务2,刚好访问了事务1修改后的数据
此时事务1,回滚了操作 事务2,读到还是回滚前的数据
在同一事务中,多次读取同一数据返回的结果有所不同,换句话说,后续读取可以读到另一事务已提交的更新数据。相反,“可重复读” 在同一事务中多次读取数据时, 能够保证所读数据一样, 也就是后续读取不能读到另一事务已提交的更新数据。
这种情况发生 在一个事务内多次读同一数据。A事务查询某条数据,该事务未结束时,B事务也访问同一数据并进行了修改。那么在A事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。
事务1,查询某个数据 事务2,修改了某个数据,提交
事务1,再次查询这个数据
这样事务1两次查询的数据不一样,称为不可重复读
脏读、不可重复读、可重复读、幻读,其中最难理解的是幻读
1、幻读现象例子:
可重复读模式下,比如有个用户表,手机号码为主键,有两个事物进行如下操作
事务A操作如下: 1、打开事务 2、查询号码为X的记录,不存在 3、插入号码为X的数据,插入报错(为什么会报错,先向下看) 4、查询号码为X的记录,发现还是不存在(由于是可重复读,所以读取记录X还是不存在的)
事物B操作:在事务A第2步操作时插入了一条X的记录,所以会导致A中第3步插入报错(违反了唯一约束)
上面操作对A来说就像发生了幻觉一样,明明查询X(A中第二步、第四步)不存在,但却无法插入成功
幻读可以这么理解:事务中后面的操作(插入号码X)需要上面的读取操作(查询号码X的记录)提供支持,但读取操作却不能支持下面的操作时产生的错误,就像发生了幻觉一样。
2、看第二种解释:
事务A在操作一堆数据的时候,事务B插入了一条数据,A事务再次(第二次)查询,发现多了一条数据,像是幻觉。与不可重复读类似,不同的是一个是修改删除操作,一个是新增操作。
当多个事务同时进行的时候,如何确保当前事务中数据的正确性,比如A、B两个事物同时进行的时候,A是否可以看到B已提交的数据或者B未提交的数据,这个需要依靠事务的隔离级别来保证,不同的隔离级别中所产生的效果是不一样的。
隔离级别分为4种:
读未提交:READ-UNCOMMITTED
读已提交:READ-COMMITTED
可重复读:REPEATABLE-READ
串行:SERIALIZABLE
mysql> show variables like 'transaction_isolation'; +-----------------------+----------------+ | Variable_name | Value | +-----------------------+----------------+ | transaction_isolation | READ-COMMITTED | +-----------------------+----------------+
分2步骤,修改文件、重启mysql,如下:
修改mysql中的my.init文件,我们将隔离级别设置为:READ-UNCOMMITTED,如下:
# 隔离级别设置 READ-UNCOMMITTED 读未提交, READ-COMMITTED 读已提交, REPEATABLE-READ 可重复读,SERIALIZABLE 串行 transaction-isolation=READ-UNCOMMITTED
隔离级别 | 脏读可能性 | 不可重复读可能性 | 幻读可能性 |
---|---|---|---|
READ-UNCOMMITTED | 有 | 有 | 有 |
READ-COMMITTED | 无 | 有 | 有 |
REPEATABLE-READ | 无 | 无 | 有 |
SERIALIZABLE | 无 | 无 | 无 |
按时间顺序在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; |
看一下:
T2-A:无数据,T6-A:有数据,T6时刻B还未提交,此时A已经看到了B插入的数据,说明出现了脏读。
T2-A:无数据,T6-A:有数据,查询到的结果不一样,说明不可重复读。
结论:读未提交情况下,可以读取到其他事务还未提交的数据,多次读取结果不一样,出现了脏读、不可重复读、幻读
将隔离级别置为READ-COMMITTED
# 隔离级别设置 READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行 transaction-isolation=READ-COMMITTED
清空表,按时间顺序在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; mysql> select * from test1; Empty set (0.00 sec) mysql> select * from test1; Empty set (0.00 sec) mysql> select * from test1; +------+ | a | +------+ | 1 | +------+ mysql> commit;
B窗口如下:
mysql> start transaction; mysql> insert into test1 values (1); mysql> select * from test1; +------+ | a | +------+ | 1 | +------+ mysql> commit;
看一下:
T5-B:有数据,T6-A窗口:无数据,A看不到B的数据,说明没有脏读。
T6-A窗口:无数据,T8-A:看到了B插入的数据,此时B已经提交了,A看到了B已提交的数据,说明可以读取到已提交的数据。
T2-A、T6-A:无数据,T8-A:有数据,多次读取结果不一样,说明不可重复读。
结论:读已提交情况下,无法读取到其他事务还未提交的数据,可以读取到其他事务已经提交的数据,多次读取结果不一样,未出现脏读,出现了读已提交、不可重复读、幻读
# 隔离级别设置 READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行 transaction-isolation=REPEATABLE-READ
清空表,按时间顺序在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; 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; mysql> select * from test1; +------+ | a | +------+ | 1 | +------+
B窗口如下:
mysql> start transaction; mysql> insert into test1 values (1); mysql> select * from test1; +------+ | a | +------+ | 1 | +------+
看一下:
T2-A、T6-A窗口:无数据,T5-B:有数据,A看不到B的数据,说明没有脏读。
T8-A:无数据,此时B已经提交了,A看不到B已提交的数据,A中3次读的结果一样都是没有数据的,说明可重复读。
结论:可重复读情况下,未出现脏读,未读取到其他事务已提交的数据,多次读取结果一致,即可重复读。
将隔离级别置为REPEATABLE-READ,准备数据
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,'路人甲Java'),(2,'路人甲Java'); ERROR 1062 (23000): Duplicate entry '路人甲Java' ***\*for\**** key 'name' mysql> select * from t_user; Empty set (0.00 sec)
按时间顺序在2个窗口中执行下面操作:
时间 | 窗口A | 窗口B |
---|---|---|
T1 | start transaction; | |
T2 | start transaction; | |
T3 | – 插入路人甲Java insert into t_user values (1,‘路人甲Java’); | |
T4 | select * from t_user; | |
T5 | – 查看路人甲Java是否存在 select * from t_user where name=‘路人甲Java’; | |
T6 | commit; | |
T7 | – 插入路人甲Java insert into t_user values (2,‘路人甲Java’); | |
T8 | – 查看路人甲Java是否存在 select * from t_user where name=‘路人甲Java’; | |
T9 | commit; |
A窗口如下:
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from t_user where name='路人甲Java'; Empty set (0.00 sec) mysql> insert into t_user values (2,'路人甲Java'); ERROR 1062 (23000): Duplicate entry '路人甲Java' ***\*for\**** key 'name' mysql> select * from t_user where name='路人甲Java'; 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,'路人甲Java'); Query OK, 1 row affected (0.00 sec) mysql> select * from t_user; +----+---------------+ | id | name | +----+---------------+ | 1 | 路人甲Java | +----+---------------+ 1 row in set (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec)
A想插入数据路人甲Java,插入之前先查询了一下(T5时刻)该用户是否存在,发现不存在,然后在T7时刻执行插入,报错了,报数据已经存在了,因为T6时刻B已经插入了路人甲Java。
然后A有点郁闷,刚才查的时候不存在的,然后A不相信自己的眼睛,又去查一次(T8时刻),发现路人甲Java还是不存在的。
此时A心里想:数据明明不存在啊,为什么无法插入呢?这不是懵逼了么,A觉得如同发生了幻觉一样。
SERIALIZABLE会让并发的事务串行执行(多个事务之间读写、写读、写写会产生互斥,效果就是串行执行,多个事务之间的读读不会产生互斥)。事务A中先读取操作,事务B发起写入操作,事务A中的读取会导致事务B中的写入处于等待状态,直到A事务完成为止。串行的限制仅仅限制该表,对其它事务操作其它表不相关。简单的说就是如果事务操作表发生改变则后面的事务增删改查需要等待(非事务增删改查不受影响)
设置串行
# 隔离级别设置 READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行 transaction-isolation=SERIALIZABLE
清空表,按时间顺序在2个窗口中执行下面操作:
时间 | 窗口A | 窗口B |
---|---|---|
T1 | start transaction; | |
T2 | select * from test1; | |
T3 | start transaction; | |
T4 | insert into test1 values (1); | |
T5 | commit; | |
T6 | commit; |
按时间顺序运行上面的命令,会发现T4-B这样会被阻塞,直到T5-A执行完毕。
上面这个演示的是读写互斥产生的效果,大家可以自己去写一下写读、写写互斥的效果。