数据库事务隔离级别

在并发的场景中,为了保证数据的一致性我们会在数据库中使用事务。然而在强一致性与性能上则需要根据具体业务来取舍,所以一般数据库提供了四种事务隔离级别:

  1. 读未提交(Read Uncommitted)
  2. 读提交(Read Committed)
  3. 可重复读(Repeatable Read)
  4. 序列化(Serializable)

由于日常工作中使用事务比较频繁,遂在此作一下总结

在了解这四种事务隔离级别之前,需要了解如下概念:

更新丢失(Lost Update):
两个事务同时修改一行数据,其中一个事务的更新被另外一个事务的更新覆盖了

脏读(Dirty Reads):
一个事务在修改(未提交)数据时,被另一个事务读到了修改后的数据

不可重复读(Non-repeatable Reads):
一个事务先读取了某个数据,然后另外一个事务修改(并提交)了数据后,这个事务再一次读取之前的数据时,得到的是不一样的值

幻读(Phantom Reads):
一个事务预先读取到或者未读取到某条数据,然后另外一个事务删除或新增这条数据(并提交)后,这个事务再一次读取之前的数据时未读到或读到数据。

以上概念对应在四种隔离级别中:

读未提交(Read Uncommitted)
在该级别下的事务允许脏读,但不允许更新丢失:当一个事务开始修改某个值时,其他的事务可以读这个值,但是无法修改这个值。

以mysql为例,将事务隔离级别设置为Read Uncommitted

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

测试表格式:

mysql> desc user;
+---------+------------------+------+-----+---------+----------------+
| Field   | Type             | Null | Key | Default | Extra          |
+---------+------------------+------+-----+---------+----------------+
| id      | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| name    | varchar(255)     | NO   |     | NULL    |                |
| balance | int(10) unsigned | NO   |     | 0       |                |
+---------+------------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

测试数据:

mysql> select * from user;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | eslizn |     100 |
+----+--------+---------+
1 row in set (0.01 sec)

打开一个新的会话:

mysql> show processlist;
+----+------+-----------+------+---------+------+-------+------------------+
| Id | User | Host      | db   | Command | Time | State | Info             |
+----+------+-----------+------+---------+------+-------+------------------+
|  1 | root | localhost | test | Sleep   |   41 |       | NULL             |
|  2 | root | localhost | NULL | Query   |    0 | init  | show processlist |
+----+------+-----------+------+---------+------+-------+------------------+
2 rows in set (0.00 sec)

在会话1中使用事务修改数据,但不提交:

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

mysql> update user set balance = balance - 5 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

在会话2中查询(脏读):

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

在会话2中修改(不会更新丢失):

mysql> update user set balance = balance + 5 where id = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

因为会话1对该条数据的操作还未结束,所以无法得到写锁。

读提交(Read Committed)
在该级别下允许不可重复读,但不允许脏读。

回滚之前会话1中的操作,设置事务隔离级别为读提交:

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

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

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

在会话1中使用事务修改数据,但不提交:

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

mysql> update user set balance = balance - 5 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

在会话2中修改事务隔离级别为读提交并读取数据(不允许脏读):

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

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

回滚会话1,新建事务读取内容:

mysql> rollback;
Query OK, 0 rows affected (0.01 sec)

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

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

在会话2中修改这个数据:

mysql> update user set balance = balance - 5 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

在会话1中再次读取(不可重复读):

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

读取到了与之前不一样的值。

可重复读(Repeatable Read)
禁止不可重复读,但可能出现幻读。

回滚会话1,设置隔离级别为可重复读:

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

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

在会话1的事务中读取数据:

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

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

在会话2中修改事务隔离级别,修改此数据:

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

mysql> update user set balance = balance - 5 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

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

在会话1中查询(可重复读):

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

在会话2中新增一条数据:

mysql> insert into user (name, balance) values ('root', 100);
Query OK, 1 row affected (0.00 sec)

mysql> select * from user;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | eslizn |      90 |
|  2 | root   |     100 |
+----+--------+---------+
2 rows in set (0.00 sec)

在会话1中查询(幻读):

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

序列化(Serializable)
在此级别下,修改数据的操作必须是位于事务队列的最前端(所以个人觉得叫做队列化更合适),事务化级别最高,但是带来的资源竞争也更多。

在会话1中设置事务级别并打开会话查询数据:

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

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

mysql> select * from user;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | eslizn |      90 |
|  2 | root   |     100 |
+----+--------+---------+
2 rows in set (0.00 sec)

在会话2中删除一条数据:

mysql> delete from user where id = 2;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

因为无法获得锁而操作失败,故不会造成幻读。

标签: mysql, transaction

添加新评论