MySQL事务隔离级别为"REPEATABLE-READ"下的"幻读"现象

我们用一个示例来讲解这种情况,及在并发事务进怎样避免这种情况。


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"实际上两个事务就完全串行了,"会影响并发性能",要结合具体业务来具体处理,大部分情况下不需要这么严格的。


你可能感兴趣的:(数据库,MYSQL基础)