一、事务的acid特性
- 原子性:一个事务必须被视为一个不可分割的最小工作单元,事务中所有操作要么全部操作成功,要么全部失败回滚;
- 一致性:数据库总是从一个一致性的状态转换到另一个一致性的状态;
- 隔离性:通常来说,一个事务所做的修改在其提交之前,对其他事务是不可见的;
- 持久性:一旦事务提交,所做的修改就会永久保存早数据库中,即使此时系统崩溃,修改部分也不会丢失。
在sql标准中定义了四种隔离级别,每一种级别都规定了一个事务中所做的修改在另一个事务中是可见的还是不可见的。较低级别的隔离通常可以执行更高级别的并发,系统的开销也更低。
二、隔离级别
- read uncommitted(未提交读)
在read uncommitted级别,事务中的修改,即使还没有提交,在其他事务中也是可见的,也称作“脏读”,实际中很少使用。先看看数据库当前隔离级别并更改为read uncommitted级别:
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)
然后同时开启事务1和事务2。在事务1中先查看某条数据,然后在事务2中修改该条数据,暂不提交,这时在事务1中再次查看这条数据,发现数据变化了的,这时就发生“脏读”了。
#事务1
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
#事务2
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from qh_user;
+----+------+--------+----------+
| id | name | phone | address |
+----+------+--------+----------+
| 1 | jack | 111111 | new york |
+----+------+--------+----------+
1 row in set (0.00 sec)
mysql> update qh_user set phone = '222222' where id = 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from qh_user;
+----+------+--------+----------+
| id | name | phone | address |
+----+------+--------+----------+
| 1 | jack | 222222 | new york |
+----+------+--------+----------+
1 row in set (0.00 sec)
- read committed(提交读/不可重复读)
在read committed级别,一个事务开始后,只能看见其他已经提交事务所做的修改。所以在一个事务中某条数据前后两次查看可能是不同的,原因就在于这两次查看之间有其他的事务对该条数据作了修改并提交了。
先还是将事务设置成read committed级别:
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set, 1 warning (0.00 sec)
此处也是同时开启两个事务,先在事务1中查看某条数据,在事务2中修改该条数据,然后事务1中再查看该条数据,这时再提交事务2,最后再第三次在事务1中查看数据,会发现第二次查看和第三次查看数据是不一样的,也就是不可重复读。
#事务1
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
#事务2
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from qh_user;
+----+------+--------+----------+
| id | name | phone | address |
+----+------+--------+----------+
| 1 | jack | 111111 | new york |
+----+------+--------+----------+
1 row in set (0.00 sec)
mysql> update qh_user set phone = '333333' where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from qh_user;
+----+------+--------+----------+
| id | name | phone | address |
+----+------+--------+----------+
| 1 | jack | 111111 | new york |
+----+------+--------+----------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from qh_user;
+----+------+--------+----------+
| id | name | phone | address |
+----+------+--------+----------+
| 1 | jack | 333333 | new york |
+----+------+--------+----------+
1 row in set (0.00 sec)
- repeatable read(可重复读)
在repeatable read级别,在一个事务中读某条数据,不管在另一个事务中是修改(update)还是删除(delete),在原事务中都是不可见的,也就是说多次读取的结果都是一致的,部分解决了不可重复读的问题。还是先设置隔离级别:
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
还是同时开启四个事务1、2、3、4,先在事务1中查看某条数据,在事务2中修改并提交,再查看事务1中数据,应该是没有变化,此时在事务3中删除这条记录,再查看事务1中数据,应该也是没变化,最后再在事务4中插入一条记录,此时理论上在事务1中应该会看到多出一条数据,也就出现了“幻读”,但是innodb存储引擎已通过mvcc解决了“幻读”问题,所以实际上还是没变化。
#事务1
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from qh_user;
+----+------+--------+----------+
| id | name | phone | address |
+----+------+--------+----------+
| 1 | jack | 333333 | new york |
+----+------+--------+----------+
1 row in set (0.00 sec)
#事务2
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update qh_user set phone = '1111' 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.00 sec)
mysql> select * from qh_user;
+----+------+--------+----------+
| id | name | phone | address |
+----+------+--------+----------+
| 1 | jack | 333333 | new york |
+----+------+--------+----------+
1 row in set (0.00 sec)
#事务3
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> delete from qh_user where id = 1;
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from qh_user;
+----+------+--------+----------+
| id | name | phone | address |
+----+------+--------+----------+
| 1 | jack | 333333 | new york |
+----+------+--------+----------+
1 row in set (0.00 sec)
#事务4
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into qh_user(name, phone, address)
-> values ('rose', '55555', 'beijing');
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from qh_user;
+----+------+--------+----------+
| id | name | phone | address |
+----+------+--------+----------+
| 1 | jack | 333333 | new york |
+----+------+--------+----------+
1 row in set (0.00 sec)
- serializable(可串行化)
其实在repeatable read级别,只是部分解决了“幻读”问题,虽然在事务1中没看到新增的记录,但实际上是可以修改这条记录的。
#事务1
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from qh_user;
+----+------+--------+----------+
| id | name | phone | address |
+----+------+--------+----------+
| 1 | jack | 333333 | new york |
+----+------+--------+----------+
1 row in set (0.00 sec)
#事务2
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into qh_user(name, phone, address)
-> values ('david', '8888', 'toronto');
Query OK, 1 row affected (0.00 sec)
mysql> select * from qh_user;
+----+-------+--------+----------+
| id | name | phone | address |
+----+-------+--------+----------+
| 1 | jack | 333333 | new york |
| 2 | david | 8888 | toronto |
+----+-------+--------+----------+
2 rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from qh_user;
+----+------+--------+----------+
| id | name | phone | address |
+----+------+--------+----------+
| 1 | jack | 333333 | new york |
+----+------+--------+----------+
1 row in set (0.00 sec)
mysql> update qh_user set address = 'california' where id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
从上述实验结果可以看到,虽然在事务1中select没有id=2的记录,但是直接修改这条数据,数据库返回的依然是ok的,所以说mvcc并没有完全解决幻读问题。而在serializable级别每读取一行数据都会加锁,是最高的隔离级别,这样就可以彻底解决“幻读”问题。
#事务1
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from qh_user;
+----+-------+--------+------------+
| id | name | phone | address |
+----+-------+--------+------------+
| 1 | jack | 333333 | new york |
| 2 | david | 8888 | california |
+----+-------+--------+------------+
2 rows in set (0.00 sec)
#事务2
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into qh_user(name, phone, address)
-> values ('floyd', '55555', 'beijing');
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
Query OK, 1 row affected (25.69 sec)
由上述实验结果可以看到,在设置serializable级别后,在事务2中插入一条新纪录后,并没有马上显示insert成功,而是阻塞在那里,直到事务1执行commit后,事务2才提示query ok。
由此可见,“幻读”问题是解决了,但是这极可能会导致大量超时和锁竞争问题,实际中也很少应用。只有在必须确保数据一致性并且可以接受没有并发情况下才会考虑使用该级别。