并发访问在许多地方都被使用,用以提高对资源的利用率,例如操作系统中多道程序访问,提高CPU和内存的利用率。与并发紧密相关的就是共享资源的安全访问,通常利用加锁或者限制访问数量的机制来保证数据结构的一致性和操作结果的正确性。
数据库中引入事务隔离来保证在一定程度上的正确性,之所以说是一定程度上,因为在并发访问中使用隔离策略,其实是对访问顺序的一个限制,是在向串行化方向的靠拢。所以给出的几种隔离策略只是提供了在串行和并发之间的不同程度的折中选择,根据具体的使用特性来选择合适的策略,实际上每个数据库除了这几种标准隔离策略外,也都会提供符合自己国情的隔离策略。
标准的隔离策略有:
1.未提交读(read uncommitted)
作为限制最低的隔离级别,几乎不提供访问限制,只要不是读取操作一半的错误的物理结构就行(这是操作系统的限制了)。一个事务在事务之中可以读取到另一个事务修改的数据的中间状态。即还没有提交修改事务,数据就被别的事务读取了,也就是“脏读”问题。
产生原因也就是没有对写操作事务加锁。
2.提交读(read committed)
对修改事务加锁,即写操作事务加锁,这样就保证了不会使得修改事务的中间状态“逃逸”,中间状态对别的事务不可见。但是提交读对读操作是不在事务加锁的,即select之后就释放读锁,然后就可能被修改,下次再读就可能发生了修改,也就是“不可重复读”。
产生原因也就是没有对读操作事务加锁。
3.可重复读(repeatable read)
听名字就知道是对读操作事务和写操作事务都加锁,以此保证可重复读。中间状态不可见,不会有别的事务发生“脏读”;读事务加锁,读事务内数据不会被别的事务修改,不会“不可重复读”。
但是仍然有问题,因为不存在“范围锁”,或者说加锁范围不是使用的整张表或者用到的所有表,只是部分数据。关于InnoDB这里有点不同,后面会提。
4.序列化读(serializable)
序列化读,对用到的所有表加锁,串行化操作,对并发的极大不支持。
以上所说的及下面也会提到的读锁、写锁,其实都是InnoDB的行锁。具体见后面一篇文章。
事务隔离演示
建表
mysql> create table t(
-> id varchar(10))
-> engine=innodb
Query OK, 0 rows affected (0.15 sec)
1.未提交读
mysql> set session tx_isolation='read-uncommitted';
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set (0.00 sec)
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED |
+------------------------+
1 row in set (0.00 sec)
设置session 1和session 2会话事务隔离级别为未提交读,其中设置语句标准为:
set [session|global] transaction isolation level {read uncommitted|read committed|repeatable read|serializable},此处直接使用set tx_isolation方式设置会话事务隔离
session 1 执行插入语句
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t values(1);
Query OK, 1 row affected (0.02 sec)
session 2 在session 1 commit之前就可以直接访问
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t;
+------+
| id |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
接着session 1 执行了更改或者其他修改操作,session 2 读取的则是脏数据
mysql> update t set id=2 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.03 sec)
2.提交读
设置session 1和session 2隔离级别
mysql> set tx_isolation='read-committed';
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t;
+------+
| id |
+------+
| 2 |
+------+
1 row in set (0.00 sec)
session 1 执行insert语句(或者update语句都行),获得行写锁(此时session 2不能对该行进行修改)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t values(3);
Query OK, 1 row affected (0.02 sec)
mysql> select * from t;
+------+
| id |
+------+
| 2 |
| 3 |
+------+
2 rows in set (0.00 sec)
session 1 的修改操作在未提交事务前,对session 2不可见,即session 2不会读到脏数据
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t;
+------+
| id |
+------+
| 2 |
+------+
1 row in set (0.00 sec)
session 1执行更新语句,并提交事务
mysql> update t set id=4 where id=2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.02 sec)
session 2读到的数据由 id=2 变成 id=4,即“不可重复读”
mysql> select * from t;
+------+
| id |
+------+
| 4 |
| 3 |
+------+
2 rows in set (0.00 sec)
此时看到的 id=3 是在session 1 提交之后看到的(其实就是“幻读”),所以读取不存在“脏读”问题
3.可重复读
设置session 1和session2 隔离级别
mysql> set tx_isolation='repeatable-read';
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t;
+------+
| id |
+------+
| 4 |
| 3 |
+------+
2 rows in set (0.00 sec)
session 2 开启事务并读取
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t;
+------+
| id |
+------+
| 4 |
| 3 |
+------+
2 rows in set (0.00 sec)
session 1 修改并提交事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update t set id=5 where id=4;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from t;
+------+
| id |
+------+
| 5 |
| 3 |
+------+
2 rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.02 sec)
session 2 中读取的数据仍然不变,即不存在“不可重复读”,可重复读
mysql> select * from t;
+------+
| id |
+------+
| 4 |
| 3 |
+------+
2 rows in set (0.00 sec)
这里存在一个问题,即InnoDB存储引擎在可重复读隔离级别,可以防止“幻读”。
session 2 在当前隔离级别,开启事务,并查询
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t;
+------+
| id |
+------+
| 5 |
| 3 |
+------+
2 rows in set (0.00 sec)
session 1 在当前隔离级别,开启事务并插入语句
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t values(6);
Query OK, 1 row affected (0.00 sec)
mysql> select * from t;
+------+
| id |
+------+
| 5 |
| 3 |
| 6 |
+------+
3 rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.03 sec)
session 2 中的查询结果依然不变,即不存在“幻读”
mysql> select * from t;
+------+
| id |
+------+
| 5 |
| 3 |
+------+
2 rows in set (0.00 sec)
4.序列化读
序列化读就是串行操作,设置隔离级别
mysql> set tx_isolation='serializable';
Query OK, 0 rows affected (0.00 sec)
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| SERIALIZABLE |
+------------------------+
1 row in set (0.00 sec)
session 2 开启事务,加读锁
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t;
+------+
| id |
+------+
| 5 |
| 3 |
| 6 |
+------+
3 rows in set (0.00 sec)
session 1 开启事务,insert操作阻塞
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t values(7);
session 2 提交事务之后,session 1 才能完成insert操作,即串行化执行,不存在“幻读”
Serializable隔离级别下,读事务之间可以并行,读写串行执行。
结论
数据库提供的四种隔离级别只是作为一个选择,数据库可以根据自身特性选择使用某一种,或者自身提供隔离级别供使用。