[TOC]
什么是事务?
我们来看一个场景:
客户端A正在买票,买到一张票之后,但是还没来得及更新数据库,客户端B也来买了这张票,此时就出问题了。
那么如何解决这样的问题呢?这就需要规定一些原则:
- 买票的过程是原子的
- 买票这个事情互相间不能影响
- 买完票要永久有效
- 买前和买后的状态是要确定的
事务概念
要实现这些原则,就引出了“事务”:
事务就是一组DML语句组成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体(学习事务,要站在使用者的角度来看,而不是程序员的角度!)。MySQL提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。
事务就是要做的或所做的事情,主要用于处理操作量大,复杂度高的数据。假设一种场景:你毕业了,学校的教务系统后台MySQL中,不再需要你的数据,要删除你的所有信息(一般不会:) ), 那么要删除你的基本信息(姓名,电话,籍贯等)的同时,也删除和你有关的其他信息,比如:你的各科成绩,你在校表现,甚至你在论坛发过的文章等。这样,就需要多条MySQL语句构成,那么所有这些操作合起来,就构成了一个事务。
正如我们上面所说,一个MySQL数据库,可不止你一个事务在运行,同一时刻,甚至有大量的请求被包装成事务,在向MySQL服务器发起事务处理请求。而每条事务至少一条SQL,或者很多SQL,这样如果大家都访问同样的表数据,在不加保护的情况,就绝对会出现问题。甚至,因为事务由多条SQL构成,那么,也会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢?
事务四个属性(ACID)
所有,一个完整的事务,绝对不是简单的 sql 集合,还需要满足如下四个属性:
- 原子性(Atomicity)
一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。如果事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。 - 一致性(Consistency)
在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。 - 隔离性(Isolation)
数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Readuncommitted
)、读提交(read committed
)、可重复读(`repeatable read
)和串行化(Serializable
)。 - 持久性(Durability)
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
实际上MySQL并没有为特别为一致性做什么,实现原子性、隔离性、持久性之后,再加上用户的配合等等,就自然而然地实现一致性了。
为什么要有事务?
事务被MySQL编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的,而不是伴随着数据库系统天生就有的。
我们把MySQL中的一行信息称为“记录”。
事务的版本支持
并不是所有搜索引擎都支持事务的,我们在MySQL中可以输入这行代码进行查询:
mysql> show engines\G;
*************************** 1. row ***************************
Engine: ARCHIVE
Support: YES
Comment: Archive storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 2. row ***************************
Engine: BLACKHOLE
Support: YES
Comment: /dev/null storage engine (anything you write to it disappears)
Transactions: NO
XA: NO
Savepoints: NO
*************************** 3. row ***************************
Engine: MRG_MYISAM
Support: YES
Comment: Collection of identical MyISAM tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 4. row ***************************
Engine: FEDERATED
Support: NO
Comment: Federated MySQL storage engine
Transactions: NULL
XA: NULL
Savepoints: NULL
*************************** 5. row ***************************
Engine: MyISAM
Support: YES
Comment: MyISAM storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 6. row ***************************
Engine: PERFORMANCE_SCHEMA
Support: YES
Comment: Performance Schema
Transactions: NO
XA: NO
Savepoints: NO
*************************** 7. row ***************************
Engine: InnoDB
Support: DEFAULT -> 表示这是默认引擎
Comment: Supports transactions, row-level locking, and foreign keys
Transactions: YES -> 支持事务
XA: YES
Savepoints: YES -> 支持事务保存点
*************************** 8. row ***************************
Engine: MEMORY -> 内存引擎
Support: YES
Comment: Hash based, stored in memory, useful for temporary tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 9. row ***************************
Engine: CSV
Support: YES
Comment: CSV storage engine
Transactions: NO
XA: NO
Savepoints: NO
9 rows in set (0.00 sec)
能看到,InnoDB是支持事务的,MyISAM不支持。
事务的提交方式
事务有两种提交方式:自动提交和手动提交。
-
查看事务的提交方式
mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set (0.12 sec)
-
改变MySQL的自动提交模式
mysql> set autocommit=0; -- 设置为0表示关闭自动提交 Query OK, 0 rows affected (0.00 sec) mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | OFF | +---------------+-------+ 1 row in set (0.01 sec) mysql> set autocommit=1; -- 设置为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)
事务常见操作
我们平时用的mysql -uroot -p
,实际上登录的是MySQL的客户端。
AQL@VM-24-7-ubuntu:~$ ls /usr/bin/mysql
/usr/bin/mysql -> 这是我的服务器上本地的MySQL客户端
AQL@VM-24-7-ubuntu:~$ ls /usr/sbin/mysqld
/usr/sbin/mysqld -> 这是我的服务器上的MySQL服务端
用mysql -root -p
命令登录,其实是以root账号登录本地的MySQL客户端。
实际上我们的MySQL服务端开启的话,也是可以在远端主机的MySQL客户端登录的(不过需要账号)!因此我们的MySQL服务端可能被多个客户端高并发地访问。
-- 打开我的配置文件,看到mysql客户端和服务端的端口号都是设置的3306
root@VM-24-7-ubuntu:/www/server/data/# cat /etc/my.cnf
[client]
#password = your_password
port = 3306
socket = /tmp/mysql.sock
[mysqld]
port = 3306
socket = /tmp/mysql.sock
...
...
...
准备工作
为了便于展示,先将mysql的默认隔离级别设置成“读未提交”
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> create table if not exists account( -> id int primary key, -> name varchar(50) not null default '', -> blance decimal(10,2) not null default 0.0 -> )ENGINE=InnoDB DEFAULT CHARSET=UTF8; Query OK, 0 rows affected, 1 warning (0.16 sec) mysql> desc account; +--------+---------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +--------+---------------+------+-----+---------+-------+ | id | int | NO | PRI | NULL | | | name | varchar(50) | NO | | | | | blance | decimal(10,2) | NO | | 0.00 | | +--------+---------------+------+-----+---------+-------+ 3 rows in set (0.00 sec)
事务操作
演示事务的开始和回滚:
1. 演示事务全部操作
启动事务、设置保存点、回滚、提交事务
-- 1. 首先设置成自动提交 mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set (0.02 sec) -- 2. 开始一个事务 mysql> start transaction; -- 或者直接用'begin;' 也可以启动 Query OK, 0 rows affected (0.00 sec) -- 3. 设置第一个保存点 mysql> savepoint s1; Query OK, 0 rows affected (0.00 sec) -- 4. 插入一条数据 mysql> insert into account values(1, '张三', 1234.5); Query OK, 1 row affected (0.01 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | +----+--------+---------+ 1 row in set (0.00 sec) -- 5. 设置第二个保存点 mysql> savepoint s2; Query OK, 0 rows affected (0.00 sec) -- 6. 插入第二条记录 mysql> insert into account values(2, '李四', 5432.1); Query OK, 1 row affected (0.00 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 2 | 李四 | 5432.10 | +----+--------+---------+ 2 rows in set (0.00 sec) -- 9. 设置第三个保存点 mysql> savepoint s3; Query OK, 0 rows affected (0.00 sec) -- 10. 插入第三条记录 mysql> insert into account values(3, '王五', 3333.1); Query OK, 1 row affected (0.00 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 2 | 李四 | 5432.10 | | 3 | 王五 | 3333.10 | +----+--------+---------+ 3 rows in set (0.00 sec) -- 11. 此时我后悔插入第三条记录了,就回滚到s3 mysql> rollback to s3; Query OK, 0 rows affected (0.01 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 2 | 李四 | 5432.10 | +----+--------+---------+ 2 rows in set (0.00 sec) -- 12. 可以回滚到任何一个保存点 mysql> rollback to s1; Query OK, 0 rows affected (0.00 sec) mysql> select * from account; Empty set (0.00 sec) -- 13. 提交事务 mysql> commit; Query OK, 0 rows affected (0.00 sec)
2. 不设保存点,然后回滚
如果没有手动创建任何的保存点,直接输
rollback;
,就会把事务里的所有操作都舍弃!回到启动事务之前的时候。
3. 提交事务后不能再回滚
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into account values(1, '张三', 1234.5); Query OK, 1 row affected (0.00 sec) mysql> insert into account values(2, '李四', 5432.1); Query OK, 1 row affected (0.00 sec) mysql> insert into account values(3, '王五', 3333.1); Query OK, 1 row affected (0.00 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 2 | 李四 | 5432.10 | | 3 | 王五 | 3333.10 | +----+--------+---------+ 3 rows in set (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.01 sec) mysql> rollback; Query OK, 0 rows affected (0.00 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 2 | 李四 | 5432.10 | | 3 | 王五 | 3333.10 | +----+--------+---------+ 3 rows in set (0.00 sec)
提交了事务之后再回滚,是没有用的!事务一旦提交,便永久保存下来了,没法再回滚。
4. 不手动提交事务,若出异常会自动回滚
不手动提交
commit
,然后让客户端崩溃退出->结果会自动回滚(隔离级别为“读未提交”)-- 1. 自动提交打开 mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set (0.00 sec) -- 2. 开启事务 mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 2 | 李四 | 5432.10 | | 3 | 王五 | 3333.10 | +----+--------+---------+ 3 rows in set (0.00 sec) -- 3. 插入一条记录 mysql> insert into account values(4, '赵六', 4444.4); Query OK, 1 row affected (0.00 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 2 | 李四 | 5432.10 | | 3 | 王五 | 3333.10 | | 4 | 赵六 | 4444.40 | +----+--------+---------+ 4 rows in set (0.00 sec) -- 4. ctrl + \ 让MySQL客户端直接崩溃 mysql> Aborted -- 5. 再查看,发现回滚了 mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 2 | 李四 | 5432.10 | | 3 | 王五 | 3333.10 | +----+--------+---------+ 3 rows in set (0.00 sec)
如果出现异常,那么事务就自动回滚到事务启动前!
不仅仅是Mysql客户端崩溃,甚至直接把ssh连接给关闭了,结果也是一样!
可以看出,事务是原子性的,没有说进行到一半就保存了。结果要么是回滚到事务开启之前,要么是事务提交了就彻底保存!
5. 自动提交不影响手动开启的事务
把自动提交关闭,结果和上边也是一样的!
-- 1. 把自动提交关了 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) -- 2. 开启事务 mysql> begin; Query OK, 0 rows affected (0.00 sec) -- 3. 插入记录 mysql> insert into account values(5, '田七', 7777.7); Query OK, 1 row affected (0.00 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 2 | 李四 | 5432.10 | | 3 | 王五 | 3333.10 | | 5 | 田七 | 7777.70 | +----+--------+---------+ 4 rows in set (0.00 sec) -- 4. ctrl + \ 让MySQL客户端直接崩溃 mysql> Aborted -- 5. 再查看,发现又回滚了 mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 2 | 李四 | 5432.10 | | 3 | 王五 | 3333.10 | +----+--------+---------+ 3 rows in set (0.00 sec)
同样的,如果在崩溃前已经
commit
了,那么结果就是田七的记录永久保存了。这就表明,自动提交对于事务的异常回滚没有影响。手动
begin
或start transaction
开启的事务,就必须手动commit
来提交,才能永久保存,是否开启自动提交对这个事务没有任何影响。
6. 单条SQL语句和事务的关系
对比实验:探究
autocommit
的作用
1)自动提交关闭->出异常会回滚-- 1. 自动提交关闭 mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | OFF | +---------------+-------+ 1 row in set (0.00 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 2 | 李四 | 5432.10 | | 3 | 王五 | 3333.10 | +----+--------+---------+ 3 rows in set (0.00 sec) -- 2. 不开启事务,直接删除记录 mysql> delete from account where id=2; Query OK, 1 row affected (0.00 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 3 | 王五 | 3333.10 | +----+--------+---------+ 2 rows in set (0.00 sec) -- 3. 让MySQL客户端崩溃 mysql> Aborted -- 4. 发现回滚了! mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 2 | 李四 | 5432.10 | | 3 | 王五 | 3333.10 | +----+--------+---------+ 3 rows in set (0.00 sec)
2)自动提交打开->出异常不会回滚
-- 1. 自动提交打开 mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set (0.00 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 2 | 李四 | 5432.10 | | 3 | 王五 | 3333.10 | +----+--------+---------+ 3 rows in set (0.00 sec) -- 2. 不开启事务,直接删除记录 mysql> delete from account where id=2; Query OK, 1 row affected (0.01 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 3 | 王五 | 3333.10 | +----+--------+---------+ 2 rows in set (0.00 sec) -- 3. 让客户端崩溃 mysql> Aborted -- 4. 发现数据真的删除了! mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 3 | 王五 | 3333.10 | +----+--------+---------+ 2 rows in set (0.00 sec)
3)自动提交关闭,然后手动提交->出异常不会回滚
-- 1. 关闭自动提交 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> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | | 3 | 王五 | 3333.10 | +----+--------+---------+ 2 rows in set (0.00 sec) -- 2. 单SQL语句删除一条记录 mysql> delete from account where id=3; Query OK, 1 row affected (0.00 sec) mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | +----+--------+---------+ 1 row in set (0.00 sec) -- 3. 手动提交 mysql> commit; Query OK, 0 rows affected (0.01 sec) -- 4. 让客户端崩溃 mysql> Aborted -- 5. 发现永久删除掉了! mysql> select * from account; +----+--------+---------+ | id | name | blance | +----+--------+---------+ | 1 | 张三 | 1234.50 | +----+--------+---------+ 1 row in set (0.00 sec)
现象:
单条SQL语句其实是被MySQL自动变成一个事务来进行的;
自动提交影响的是单条SQL语句(不影响手动开启的事务)
结论
- 只要输入
begin
或者start transaction
,事务便必须要通过输入commit
提交,才会持久化,与是否设置autocommit
无关。 - 事务可以手动回滚,同时,当操作异常,MySQL会自动回滚。
- 对于InnoDB,每一条SQL语言都默认封装成事务,自动提交,除非把
autocommit
设置为0
了(select
有特殊情况,因为MySQL有MVCC
)。 - 如果没有设置保存点,也可以回滚,但只能回滚到事务的开始。直接使用
rollback
(前提是事务还没有提交) - 如果一个事务被提交了(
commit
),则不可以回退(rollback
) - 可以选择回退到哪个保存点(手动设置的)
- InnoDB支持事务,MyISAM不支持事务
事务的隔离级别
理解隔离性
MySQL服务可能同时被多个客户端进程(线程)访问,访问的方式都是事务!
一个事务可能由多条SQL语句构成,那么任何一个事务都有执行前、执行中、执行后三种状态。原子性就是让用户层只看到执行前和执行后,看不到执行中!
但是多个事务同时执行多个SQL的时候,可能会相互影响,比如多个事务同时访问、修改同一张表。
在数据库中,保证事务的执行过程尽量不受到干扰,就是一个重要的特征:隔离性。
允许事务收到不同程度的干扰,就有了:隔离级别。
四种隔离级别
- 读未提交(Read Uncommitted)
所有事务都可以看到其他事务尚未提交的执行结果,没有任何的隔离性。(不提交就能互相看到)
会有很多并发问题:脏读、幻读、不可重复读等,实际生产中不可能使用这种隔离级别(上面的代码为了方便,就用的是读未提交)。 - 读提交(Read Committed)
这是大多数数据库默认的隔离级别(不过不是MySQL的)。
它满足了隔离的简单定义:一个事务只能看到其他已经提交的事务所做的改变。(提交了才能相互看到)
这种隔离级别会引起不可重复读的问题(一个事务执行时,如果多次select
,可能得到不同的结果)。 - 可重复读(Repeatable Read)
这是MySQL默认的隔离级别。
它确保同一个事务在执行中,多次读取操作数据时,会看到同样的数据行。(对面事务提交了我看不到,只有我自己也提交了,下次才能看到)
但是会有幻读的问题。 - 串行化(Serializable)
这是最高的隔离级别。
它通过强制事务排序,在每个读的数据行上面加上共享锁(读是可以并发的,但是修改不可以)。使事务之间不可能相互冲突,解决了幻读的问题。但是可能导致超时和锁竞争的问题。
(这种隔离级别太过极端,实际生产基本不使用)
实现隔离,基本都是通过锁来实现的,不同的隔离级别用不同的锁:表锁、行锁、读锁、写锁、间隙锁(GAP)、Next-Key锁(GAP+行锁)等等。
查看&设置隔离级别
-
查看隔离级别
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; -- 和session的一样 +-------------------------+ | @@transaction_isolation | +-------------------------+ | READ-UNCOMMITTED | +-------------------------+ 1 row in set (0.00 sec)
global是全局的隔离级别,session是会话的隔离级别,它是拷贝global的。(session的生命周期到这个客户端退出)
-
设置隔离级别
语法:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
-- 设置session的隔离级别为READ COMMITTED mysql> set session transaction isolation level read committed; Query OK, 0 rows affected (0.00 sec) mysql> select @@session.transaction_isolation; +---------------------------------+ | @@session.transaction_isolation | +---------------------------------+ | READ-COMMITTED | +---------------------------------+ 1 row in set (0.00 sec)
修改session的隔离级别,不影响别的会话的隔离级别,不影响global的隔离级别;
修改global的隔离级别,所有会话的global的隔离级别会跟着改变,但是已存在的session的隔离级别不变,只有会话重新启动或者新启动的会话才会变。一般不要更改隔离级别。
读未提交(Read Uncommitted)
所有事务都可以看到其他事务尚未提交的执行结果,没有任何的隔离性。(不提交就能互相看到)
会有很多并发问题:脏读、幻读、不可重复读等,实际生产中不可能使用这种隔离级别(上面的代码为了方便,就用的是读未提交)。
读未提交几乎没有加锁,虽然效率高,但是问题太多,非常不建议采用。
-- 终端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, 1 warning (0.00 sec)
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> begin; -- 开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> update account set blance=123.0 where id=1; -- 更新指定行
Query OK, 1 row affected (0.05 sec)
Rows matched: 1 Changed: 1 Warnings: 0
-- 终端A的事务没有commit!
-- 切换到终端B
mysql> begin;
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- 读到终端A更新但是未commit的数据[insert,delete同样]
==脏读==:一个事务在执行中,读到另一个执行中的事务的更新(或其他操作)但是未commit的数据。
读提交(Read Committed)
这是大多数数据库默认的隔离级别(不过不是MySQL的)。
它满足了隔离的简单定义:一个事务只能看到其他已经提交的事务所做的改变。(提交了才能相互看到)
这种隔离级别会引起不可重复读的问题(一个事务执行时,如果多次select
,可能得到不同的结果)。
-- 终端A
mysql> set global transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
-- 重启客户端
mysql> select * from account; -- 查看当前数据
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> begin; -- 手动开启事务,同步的开始终端B的事务
Query OK, 0 rows affected (0.00 sec)
-- 切换到终端B,手动开启事务,和终端A一前一后
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
-- 再切回终端A
mysql> update account set blance=321.0 where id=1; -- 更新张三数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit; -- commit提交!
Query OK, 0 rows affected (0.01 sec)
-- 切换到终端B,再次查看数据
mysql> select * from account; -- 终端A commit之前,查看不到
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 | -- 老的值
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- 切到终端A commit之后
-- 再切到终端B,就能看到了
mysql> select *from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 | -- 新的值
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- but,此时终端B还在当前事务中,并未commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段(依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)!
==不可重复读==:同一个事务内,同样的读取,在不同的时间段(依旧还在事务操作中),读取到了不同的值。
可重复读(Repeatable Read)
这是MySQL默认的隔离级别。
它确保同一个事务在执行中,多次读取操作数据时,会看到同样的数据行。(对面事务提交了我看不到,只有我自己也提交了,下次才能看到)
但是会有幻读的问题。
-- 终端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, 1 warning (0.00 sec)
mysql> select *from account; -- 查看当前数据
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> begin; -- 开启事务,同时,终端B也开始事务
Query OK, 0 rows affected (0.00 sec)
-- 终端B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
-- 切回终端A
mysql> update account set blance=4321.0 where id=1; -- 更新数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
-- 切换到终端B,查看另一个事务是否能看到->看不到
mysql> select * from account; -- 终端A中事务 commit之前,查看当前表中数据,数据未更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- 回到终端A提交
mysql> commit; -- 提交事务
-- 切换终端到终端B,查看数据->还是看不到
mysql> select * from account; -- 终端A中事务 commit 之后,查看当前表中数据,数据未更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- 可以看到,在终端B中,事务无论什么时候进行查找,看到的结果都是一致的,这叫做可重复读!
-- 终端B结束事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; -- 终端B新事务中再次查看,看到最新的更新数据
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
----------------------------------------------------------------
-- 如果将上面的终端A中的update操作,改成insert操作,会有什么问题??
-- 终端A
mysql> select *from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> begin; -- 开启事务,终端B同步开启
Query OK, 0 rows affected (0.00 sec)
-- 终端A
mysql> insert into account (id,name,blance) values(3, '王五', 5432.0);
Query OK, 1 row affected (0.00 sec)
-- 切换到终端B,查看另一个事务是否能看到
mysql> select * from account; -- 终端A commit前 查看不到
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> commit; -- 提交事务
Query OK, 0 rows affected (0.00 sec)
-- 切换终端到终端B,查看数据。
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
mysql> select * from account; -- 终端A commit后 能看到
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> commit; -- 结束终端B的事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; -- 依旧能看到更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
多次查看,发现在MySQL中终端A在对应事务中insert的数据,在终端B的事务周期中,也没有什么影响,也符合可重复的特点。
但是,其他的数据库在可重复读情况的时候,可能无法屏蔽其他事务insert的数据(因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做==幻读==(phantom read)。
很明显,MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用Next-Key锁(GAP+行锁)解决的。这块比较难,有兴趣可以了解一下)。
串行化(Serializable)
这是最高的隔离级别。
它通过强制事务排序,在每个读的数据行上面加上共享锁(读是可以并发的,但是修改不可以)。使事务之间不可能相互冲突,解决了幻读的问题。但是可能导致超时和锁竞争的问题。
(这种隔离级别太过极端,实际生产基本不使用)
-- 对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用
-- 终端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, 1 warning (0.00 sec)
mysql> begin; -- 开启事务,终端B同步开启
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; -- 两个读取不会串行化,共享锁
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
mysql> update account set blance=1.00 where id=1; -- 终端A中有更新或者其他操作,会阻塞。直到终端B事务提交。
Query OK, 1 row affected (18.19 sec)
Rows matched: 1 Changed: 1 Warnings: 0
-- 终端B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; -- 两个读取不会串行化
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
mysql> commit; -- 提交之后,终端A中的update才会提交。
Query OK, 0 rows affected (0.00 sec)
总结:
- 隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平
衡点。 - 不可重复读的重点是修改和删除:同样的条件,你读取过的数据,再次读取出来发现值不一样了;
幻读的重点在于新增:同样的条件, 第1次和第2次读出来的记录数不一样。 - mysql默认的隔离级别是可重复读,一般情况下不要修改。
隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
---|---|---|---|---|
读未提交 | 有 | 有 | 有 | 不加锁 |
读已提交 | 无 | 有 | 有 | 不加锁 |
可重复读 | 无 | 无 | 无 | 不加锁 |
可串行化 | 无 | 无 | 无 | 加锁 |
一致性
- 事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。
- 一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑做支撑,也就是,一致性,是由用户决定的。
- 技术上,通过原子性、隔离性、持续性来保证一致性。
实际上,update,insert,delete之间是会有加锁现象的,但是select和这些操作是不冲突的。这就
是通过读写锁(锁有行锁或者表锁)+MVCC完成隔离性。写写是有加锁的。
数据库并发
数据库的并发场景有三种:
- 读-读:不存在任何问题,不需要并发控制
- 读-写:有线程安全问题,可能造成事务隔离性的问题,脏读、幻读、不可重复读
- 写-写:有线程安全问题,可能造成更新丢失问题,第一类更新丢失、第二类更新丢失
读-写并发
两个结论:
- 每个事务都有自己的事务ID,从小到大代表了事务到来的先后顺序。
- mysqld可能面临同时处理多个事务的情况,事务也有自己的生命周期,mysqld要对多个事务进行管理,“先描述,再组织”。所以mysqld中一定有一套对应的结构体对象/类对象。
多版本并发控制(MVCC)
MVCC是一种用来解决“读-写冲突”的无锁并发控制。
理解MVCC需要知道三个前提知识:
- 4个记录隐藏字段
undo
日志Read View
4个记录隐藏字段
-
DB_TRX_ID
:6 byte,最近修改的事务的ID,记录创建这条记录/最后一次修改该记录的事务ID -
DB_ROLL_PTR
:7 byte,回滚指针,指向这条记录的上一个版本(指向历史版本,一般保存在undo log
中) -
DB_ROW_ID
:6 byte,隐藏的自增ID(隐藏主键),如果数据表没有创建主键, 那么InnoDB
会自动以它产生一个聚簇索引(主键)。 -
还有一个删除
flag
隐藏字段,记录数据被更新或删除,并不是真正的把数据全部删掉,通过这个flag可以知道这些数据是有效数据还是脏数据。
undo日志
MySQL是以服务进程的方式,在内存中运行,之前讲的索引、事务、隔离性、日志等等,都是在内存中完成的,即在MySQL内部的相关缓冲区中,保存相关数据,完成各种判断操作,然后再合适的时候将相关数据刷新到磁盘中。
undo log
简单来说就是MySQL中的一个内存缓冲区,用来保存日志数据。
模拟MVCC的过程
假设当前有一个记录是这样的(说明:实际上隐藏字段看不到,这里为了方便说明就写出来了):
+--------+-----+-------------+-------------+-------------+
| name | age | DB_TRX_ID | DB_ROW_ID | DB_ROLL_PTR |
+--------+-----+-------------+-------------+-------------+
| 张三 | 28 | null | 1 | null |
+--------+-----+-------------+-------------+-------------+
以修改记录为例,MVCC的整个逻辑如下:
-
事务10对
student
表中的记录进行修改,将name('张三')改为name('李四')-
首先事务10因为要修改记录,会先给该记录加行锁
-
修改之前,将该行记录拷贝到
undo log
中(写时拷贝),此时undo log
中就有了一行副本数据 -
接着修改原始记录中的name,并且修改原始记录的隐藏字段
DB_TRX_ID
为事务10的ID,DB_ROLL_PTR
中写入undo log
中的副本数据的地址,表示该行副本记录是当前记录的上个历史版本 -
事务10提交,释放锁
-
-
事务11对
student
表中的记录进行修改,将age(28)改成age(38)- 事务11要修改记录,先给该记录加锁
- 修改之前,将该行记录拷贝到
undo log
中(写时拷贝),此时undo log
中又多有了一行副本数据,采用头插的方式插入undo log
表中 - 修改原始记录,
DB_TRX_ID
改为事务11的ID,DB_ROLL_PTR
写入undo log
中新插入的那行记录的地址 - 事务11提交,释放锁
这样,undo log
中有多个记录,新一条的记录的DB_ROLL_PTR
指向旧一条的记录,得到了一个基于链表记录的历史版本链。而所谓的回滚,无非是用历史数据覆盖当前数据。
上边的一个一个版本,可以称为快照。
MVCC控制读写的总结:
-
写
上边的步骤是update,其实delete也一样,因为delete不是把数据删除了,而是把flag字段设置为0,其实就相当于update。但是insert就不一样了,因为在insert之前,是没有相关的数据的,也就是没有历史版本,那怎么保存到
undo log
中呢?其实回滚的时候只需要执行一下delete操作就好了。当事务commit之后,undo log
中insert的记录就可以清空了,update和delete不一定,因为insert的历史版本是没有的,在并发的时候不会其他事务正在访问insert的历史版本,而有可能正在访问update和delete的历史版本! -
读
select不会修改数据,所以为select维护多版本是没有意义的。但此时的问题在于,select读的时候,是该读哪个版本呢?(实际上读哪个版本,是由隔离性来决定的->也就是说读不同的版本会导致不同的隔离性,所以隔离性和回滚一样,都是由MVCC多版本来控制的!)
读有两种方式:
- 当前读:读取最新的记录,就是当前读。增删改,都是当前读,select也有可能当前读,比如:selectlock in share mode(共享锁),select for update。
- 快照读:读取历史版本(一般而言),就叫做快照读。
在多个事务同时进行的时候,删、改、查都是当前读,是要加锁的。此时若有读,当前读也要加锁,这是串行化;快照读不需要加锁,可以与删改查并行执行。这样提高了效率。
而隔离级别正是决定了采用哪种读的方式!
我们知道事务是从begin->CURD->commit的,是有一个过程的;同时多个事务之间也是有先后的。那么某一时刻会有很多个事务进行CURD的操作,为了保证事务是原子的(保证有先有后),就要让不同的事务看到它该看到的内容,先进行的事务,就不应该看到后进行的事务所做的修改。这就是隔离性与隔离级别的目的。
Read View
Read View是事务进行快照读
操作的时候产生的读视图。
在该事务执行快照读的时候,会生成数据库系统当前的一个快照,记录并维护当前活跃的事务的ID(每个事务启动时,都会被分配一个ID,这个ID是递增的,所以最新的事务的ID最大)
Read View在MySQL源码中其实就是一个类,它本质上是用来进行可见性判断的:当某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件,用来判断当前事务能看到哪个版本的数据。
下边是Read View结构的简化版本:
class ReadView {
// ...
private:
// 高水位,>=这个ID的事务均不可见
trx_id_t m_low_limit_id;
// 低水位,<这个ID的事务均可见
trx_id_t m_up_limit_id;
// 创建该Read View的事务的ID
trx_id_t m_creator_trx_id;
// 创建读视图时正在活跃的事务ID列表
ids_t m_ids;
// 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
// 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG
trx_id_t m_low_limit_no;
// 标记视图是否被关闭
bool m_closed;
//...
}
关注其中四个即可:
m_ids; // 一张列表,维护Read View生成时刻,系统正在活跃的事务的ID
up_limit_id; // 记录m_ids列表中事务ID最小的
low_limit_id; // Read View生成时刻系统尚未分配的下一个事务ID,即目前所有事务ID的最大值+1
creator_trx_id; // 创建该Read View的事务的ID
我们读取数据版本链的时候,是能读到每一个版本对应的事务ID的,即当前记录的DB_TRX_ID
,那么现在就拿到了当前快照读的Read View和版本链中的某一个记录的DB_TRX_ID
。
在我有了一个read view快照读之后,当我再拿到某一个事务时,它的ID如果小于up_limit_id
,说明这个事务在这个时刻之前就已经跑完了;它的ID如果大于或等于low_limit_id
,说明这个事务在这个时刻还没有开始呢。
Read View是事务可见性的一个类,但并不是事务被创建出来时就有Read View了,而是在对这个(已经存在的)事务进行快照读的时候,MySQL才会形成Read View!
事务中对快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后续快照读结果的能力,delete同样如此。
RC与RR的本质区别
- 正是Read View生成时机的不同,从而造成了RC/RR级别下快照读的结果不同。
- 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来。此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见。即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见。
- 在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因
- 也正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题
- 总结:在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View