我们用一个示例来讲解这种情况,及在并发事务进怎样避免这种情况。
CREATE TABLE `t1` (
`a` int(11) NOT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
session1 session2
begin
select * from t1 where a > 50;
+----+-------+
| a | b |
+----+-------+
| 51 | 3000 |
| 52 | 10000 |
| 53 | 5000 |
| 54 | 3000 |
+----+-------+
4 rows in set (0.00 sec)
set autocommit = on;
insert into t1 values (55, 3000);
Query OK, 1 row affected (0.00 sec)
select * from t1 where a > 50;
+----+-------+
| a | b |
+----+-------+
| 51 | 3000 |
| 52 | 10000 |
| 53 | 5000 |
| 54 | 3000 |
+----+-------+
4 rows in set (0.00 sec)
#这里读不到55这条记录,这就是在"REPEATABLE-READ"级别下可以避免"不可重复读"的现象,如果是在"READ-COMMITTED"级别下是可以读到55这条记录的
update t1 set b = 3000 where a = 55;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1 Changed: 0 Warnings: 0
select * from t1 where a > 50;
+----+-------+
| a | b |
+----+-------+
| 51 | 3000 |
| 52 | 10000 |
| 53 | 3000 |
| 54 | 3000 |
| 55 | 3000 |
+----+-------+
5 rows in set (0.00 sec)
#55这条记录被读到了,这就是"幻读现象",这只是多读取了一条记录,影响还不大,但是在下面的情况下影响就大了
commit;
begin;
select * from t1 where a = 55;
+----+------+
| a | b |
+----+------+
| 55 | 3000 |
+----+------+
1 row in set (0.00 sec)
update t1 set b = 8000 where a = 55;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
select * from t1 where a = 55;
+----+------+
| a | b |
+----+------+
| 55 | 3000 |
+----+------+
1 row in set (0.00 sec)
update t1 set b = b + 3000 where a = 55;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
select * from t1 where a = 55;
+----+-------+
| a | b |
+----+-------+
| 55 | 11000 |
+----+-------+
1 row in set (0.00 sec)
#在这个事务中,理所当然认为b的值应该是3000+3000=6000,但是实际上是11000,"灵异幻读"现象出现了
commit;
如果b是一个账户钱袋,在session1的事务开始后,sessino2的事务中还可以给钱袋加钱,这种情况必须避免,不然如果session1事务去更新钱袋,如:update t1 set b = 5000 where a= 55,那么session2事务中的8000就被冲掉了,"后果很严重"
下面看看怎样避免这种情况。
begin;
select * from t1 where a = 55 LOCK IN SHARE MODE; #加一把共享锁,这样另一事务不能修改这条记录
+----+-------+
| a | b |
+----+-------+
| 55 | 11000 |
+----+-------+
1 row in set (0.01 sec)
select * from t1 where a = 55;
+----+-------+
| a | b |
+----+-------+
| 55 | 11000 |
+----+-------+
1 row in set (0.00 sec)
select * from t1 where a = 55 LOCK IN SHARE MODE;
+----+-------+
| a | b |
+----+-------+
| 55 | 11000 |
+----+-------+
1 row in set (0.00 sec)
#可以读
update t1 set b = b + 3000 where a = 55;
#wait ....但是不能修改
commit;
Query OK, 0 rows affected (0.00 sec)
Query OK, 1 row affected (12.23 sec)
Rows matched: 1 Changed: 1 Warnings: 0
#直到事务1提交后,这个修改才被提交
select * from t1 where a = 55;
+----+-------+
| a | b |
+----+-------+
| 55 | 14000 |
+----+-------+
1 row in set (0.00 sec)
死锁由此产生:
begin;
select * from t1 where a = 55 LOCK IN SHARE MODE; #加一把共享锁,这样另一事务不能修改这条记录
+----+-------+
| a | b |
+----+-------+
| 55 | 11000 |
+----+-------+
1 row in set (0.01 sec)
begin;
select * from t1 where a = 55 LOCK IN SHARE MODE; #也加一所共享锁
+----+-------+
| a | b |
+----+-------+
| 55 | 11000 |
+----+-------+
1 row in set (0.00 sec)
#可以读
update t1 set b = b + 3000 where a = 55;
#wait ....但是不能修改,因为会话一中在加锁
update t1 set b = b + 3000 where a = 55;
#ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Query OK, 1 row affected (12.23 sec)
Rows matched: 1 Changed: 1 Warnings: 0
#直到事务死锁退出后,这个修改才被提交
所以在这种两个事务一开始都加SHARE LOCK的情况,由于不阻碍另一事件的读写,会导致后续的更新语句产生,可以加一互斥锁来避免,如下:
事务1开始后,事务2还能读到,也不太好,干脆让事务2读都读不出来
begin;
select * from t1 where a = 55 FOR UPDATE;
+----+-------+
| a | b |
+----+-------+
| 55 | 14000 |
+----+-------+
1 row in set (0.00 sec)
select * from t1 where a = 55;
+----+-------+
| a | b |
+----+-------+
| 55 | 14000 |
+----+-------+
1 row in set (29.70 sec)
#普通select语句还是可以读
select * from t1 where a = 55 LOCK IN SHARE MODE;
#wait ....但是加了共享锁的select语句读不到
commit;
+----+-------+
| a | b |
+----+-------+
| 55 | 14000 |
+----+-------+
1 row in set (29.70 sec)
#直到事务1提交后才能查询到结果
SELECT语句在InnoDB引擎默认下是不加锁的,一个事务即使FOR UPDATE锁定了一条记录,另一个事务中默认select语句还是可以读的,为了避免这种情况就要在select语句中显示的加锁LOCK IN SHARE MODE or FOR UPDATE,这样很麻烦对吧,那就将事务的隔离级别设置为"SERIALIZABLE","SERIALIZABLE"与"REPEATABLE-READ"的区别就是,前者在事务中所有的SELECT语句上都会默认的加上一个共享锁。
"SERIALIZABLE"实际上两个事务就完全串行了,"会影响并发性能",要结合具体业务来具体处理,大部分情况下不需要这么严格的。