对MySQL数据库中的事务操作、存在的问题和相应的隔离级别等知识点进行整理,通过实例进行说明
MySQL事务主要用于处理操作量大,复杂度高的数据。比如说,在银行管理系统中,账户A给账户B转账,既需要减少账户A的余额,又需要增加账户B的余额,二者缺一不可;再比如,在人员管理系统中,你删除一个人员,既需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等这样,这些数据库操作语句就构成一个事务!
MySQL事务具有一些基本特性:
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
事务处理可以用来维护数据库的完整性,保证成批的SQL语句要么全部执行,要么全部不执行。
事务用来管理insert,update,delete语句
事务的基本要素(ACID)
原子性(Atomicity):事务开始操作后,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
事务并发问题
事务的并发操作会带来编程中多线程操作类似的问题,具体有以下几点:
脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。
幻读:事务A正在修改数据库的数据,而与此同时,事务B新增或者删除了一些数据,等A改完发现,一些数据没有被修改,好像出现了幻觉,这就叫幻读。
不可重复读:事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
注意:不可重复读和幻读经常会混淆,我的理解是,不可重复读侧重于数据在两次读取之间被修改了,导致读取的结果不一样;而幻读侧重于新增或者删除了数据。更好的理解就是对于二者的解决办法是不一样的:不可重复读只需要锁住所需要读取的数据就行了,而幻读则需要锁住整个数据表。
事务隔离级别
不同事务的隔离级别带来的操作消耗是不一样的,体现着锁的范围。不同的隔离级别对于并发问题的敏感性也是不一样。
事务隔离级别 |
脏读 |
不可重复读 |
幻读 |
未提交读(read-uncommitted) |
是 |
是 |
是 |
提交读(read-committed) |
否 |
是 |
是 |
可重复读(repeatable-read) |
否 |
否 |
是 |
串行读(serializable) |
否 |
否 |
否 |
未提交读:相当于完全没有隔离,一个事务可能会读到其他事务中未提交修改的数据,而别的事务可能还会对这个数据进行其他修改,甚至回滚操作,所以这种隔离级别下,上述并发问题均可能出现。
提交读:只能读取到其他事务已经提交的数据,是Oracle等数据库默认的级别。
可重复度:在同一个事务内的查询都是在事务开始时刻一致的,是MySQL的InnoDB引擎默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻读的可能。
串行读:事务完全串行操作,每次读都需要获得表级共享锁,读写相互都会阻塞。
实例说明
创建数据库和数据表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| mysql> CREATE DATABASE IF NOT EXISTS examples DEFAULT CHARSET utf8; Query OK, 1 row affected (0.00 sec)
mysql> use examples; Database changed
mysql> CREATE TABLE IF NOT EXISTS `students` ( -> `id` SMALLINT UNSIGNED AUTO_INCREMENT, -> `name` VARCHAR(10) NOT NULL, -> `scores` SMALLINT UNSIGNED NOT NULL, -> PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8; Query OK, 0 rows affected (0.28 sec)
mysql> INSERT INTO students (`name`, `scores`) VALUES ("xiaohua", 100); Query OK, 1 row affected (0.09 sec)
mysql> INSERT INTO students (`name`, `scores`) VALUES ("zhangsan", 90); Query OK, 1 row affected (0.02 sec)
mysql> INSERT INTO students (`name`, `scores`) VALUES ("lisi", 80); Query OK, 1 row affected (0.03 sec)
mysql> SELECT * FROM students; + | id | name | scores | + | 1 | xiaohua | 100 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
|
首先验证InnoDB的默认隔离级别
1 2 3 4 5 6 7
| mysql> SELECT @@tx_isolation; + | @@tx_isolation | + | REPEATABLE-READ | + 1 row in set (0.00 sec)
|
可以看到默认是可重复读
未提交读
先将客户端A的事务隔离级别改为未提交读
1 2 3 4 5 6 7 8 9 10
| 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 (0.00 sec)
|
这种隔离级别的改变只对输入该命令的客户端生效,而且不是永久的,在客户端断开连接之后,将恢复为默认的级别。
然后在客户端A中开始一个查询事务,且不提交:
1 2 3 4 5 6 7 8 9 10 11 12
| mysql> start transaction; Query OK, 0 rows affected (0.00 sec)
mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 100 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
|
在客户端B中开启一个事务,更改表的数据,但不提交:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| mysql> start transaction; Query OK, 0 rows affected (0.00 sec)
mysql> update students set scores=60 where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 60 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
|
而客户端A中已经能够看到更改的数据了:
1 2 3 4 5 6 7 8 9
| mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 60 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
|
如果此时客户端B因为其他原因进行rollback
,那么A此时读的数据就是脏数据。
提交读
先将客户端A的级别改为提交读,并且数据都回滚了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| mysql> set session transaction isolation level read committed; Query OK, 0 rows affected (0.01 sec)
mysql> start transaction; Query OK, 0 rows affected (0.00 sec)
mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 100 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
|
在客户端B中开启一个事务修改数据,但不提交。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| mysql> begin; Query OK, 0 rows affected (0.03 sec)
mysql> update students set scores=50 where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 50 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
|
此时在客户端A中查询,发现数据是没有变化的,已经读不到未提交的数据了。
1 2 3 4 5 6 7 8 9
| mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 100 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
|
在客户端B中利用commit
命令提交之后,在A中就能够查到修改后的数据了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 100 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 50 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
|
但是我们可以看出,客户端A在一个事务中两次相同命令查询的结果不同,这也就是不可重复读的现象。
另外还有一点,在commit
命令生效后,由于持久化的特性,rollback
是无法回滚回去的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| mysql> commit; Query OK, 0 rows affected (0.04 sec)
mysql> rollback; Query OK, 0 rows affected (0.01 sec)
mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 50 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
|
可重复读
将客户端A改为可重复读,查询数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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 (0.00 sec)
mysql> begin; Query OK, 0 rows affected (0.00 sec)
mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 50 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
|
在客户端B中修改数据并提交。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| mysql> begin; Query OK, 0 rows affected (0.00 sec)
mysql> update students set scores=30 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.05 sec)
mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 30 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.01 sec)
|
此时在客户端A中继续查询数据,数据是没有变化的,没有出现不可重复读的问题
1 2 3 4 5 6 7 8 9
| mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 50 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
|
这地方有个小问题:在事务中输入命令不要使用上键来偷懒,每一条命令都要自己输,否则容易出现事务自动提交的bug
但是这个地方如果执行数据修改,其会按照客户端B中已提交的数据进行修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 50 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
mysql> update students set scores=scores-10 where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 20 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
|
这种结果我觉得其实就是幻读的一种….但是好像不是这种叫法。另外,这种做法能够很好的保持数据的一致性。
串行化
将客户端A设置为串行化,查询数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| mysql> set session transaction isolation level serializable; Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation; + | @@tx_isolation | + | SERIALIZABLE | + 1 row in set (0.00 sec)
mysql> begin; Query OK, 0 rows affected (0.00 sec)
mysql> select * from students; + | id | name | scores | + | 1 | xiaohua | 20 | | 2 | zhangsan | 90 | | 3 | lisi | 80 | + 3 rows in set (0.00 sec)
|
将客户端B设置为串行化,然后插入数据。
1 2 3 4 5
| mysql> begin; Query OK, 0 rows affected (0.00 sec)
mysql> insert into students values(4, 'wangwu', 10); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
|
一开始并没有报错,命令执行被阻塞,但是隔了一会,会报错,执行失败,因为客户端A的事务一直没有提交。
总结
mysql中默认事务隔离级别是可重复读,但并不会锁住读取到的行,两个事务都可以修改,且修改的结果会叠加,但是一个事务中读取的结果一致。
事务隔离级别为读提交时,写数据只会锁住相应的行。
事务隔离级别为可重复读时,如果有索引(包括主键索引)的时候,以索引列为条件更新数据,会存在间隙锁间隙锁、行锁、下一键锁的问题,从而锁住一些行;如果没有索引,更新数据时会锁住整张表。
事务隔离级别为串行化时,读写数据都会锁住整张表。
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大,鱼和熊掌不可兼得啊。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。