您好,欢迎访问一九零五行业门户网

MySQL InnoDB四个事务级别 与 脏读、不重复读、幻读_MySQL

mysql innodb事务隔离级别脏读、可重复读、幻读
mysql innodb事务的隔离级别有四级,默认是“可重复读”(repeatable read)。
·        未提交读(readuncommitted)。另一个事务修改了数据,但尚未提交,而本事务中的select会读到这些未被提交的数据(脏读)。
·        提交读(readcommitted)。本事务读取到的是最新的数据(其他事务提交后的)。问题是,在同一个事务里,前后两次相同的select会读到不同的结果(不重复读)。
·        可重复读(repeatableread)。在同一个事务里,select的结果是事务开始时时间点的状态,因此,同样的select操作读到的结果会是一致的。但是,会有幻读现象(稍后解释)。
·        串行化(serializable)。读操作会隐式获取共享锁,可以保证不同事务间的互斥。
四个级别逐渐增强,每个级别解决一个问题。
·        脏读,最容易理解。另一个事务修改了数据,但尚未提交,而本事务中的select会读到这些未被提交的数据。
·        不重复读。解决了脏读后,会遇到,同一个事务执行过程中,另外一个事务提交了新数据,因此本事务先后两次读到的数据结果会不一致。
·        幻读。解决了不重复读,保证了同一个事务里,查询的结果都是事务开始时的状态(一致性)。但是,如果另一个事务同时提交了新数据,本事务再更新时,就会“惊奇的”发现了这些新数据,貌似之前读到的数据是“鬼影”一样的幻觉。
createtable `t` (
  `a` int(11) not null primary key
) engine=innodbdefault charset=utf8;
insertinto t(a)values(1),(2),(3);
上面的文字,读起来并不是那么容易让人理解,以下用几个实验对innodb的四个事务隔离级别做详细的解释,希望通过实验来加深大家对innodb的事务隔离级别理解。
实验一:解释脏读、可重复读问题
更新事务
事务a read-uncommitted
事务b read-committed,
事务c-1 repeatable-read
事务c-2 repeatable-read
事务d serializable
set autocommit =0;
start transaction ;
start transaction;
insert into t(a)values(4);
select * from t;
1,2,3,4(脏读:读取到了未提交的事务中的数据)
select * from t;
1,2,3(解决脏读)
select * from t;
1,2,3
select * from t;
1,2,3
select * from t;
1,2,3
commit;
select * from t:
1,2,3,4
select * from t:
1,2,3,4
select * from t:
1,2,3,4 (与上面的不在一个事务中,所以读到为事务提交后最新的,所以可读到4)
select * from t:
1,2,3(重复读:由于与上面的在一个事务中,所以只读到事务开始事务的数据,也就是重复读)
select * from t:
1,2,3,4
commit(提交事务,下面的就是一个新的事务,所以可以读到事务提交以后的最新数据)
select * from t:
1,2,3,4
read-uncommitted 会产生脏读,基本很少适用于实际场景,所以基本不使用。
实验二:测试read-committed与repeatable-read
事务a
事务b read-committed
事务c repeatable-read
set autocommit =0;
start transaction ;
start transaction;
start transaction;
insert into t(a)values(4);
select * from t;
1,2,3
select * from t;
1,2,3
commit;
select * from t:
1,2,3,4
select * from t:
1,2,3(重复读:由于与上面的在一个事务中,所以只读到事务开始事务的数据,也就是重复读)
commit(提交事务,下面的就是一个新的事务,所以可以读到事务提交以后的最新数据)
select * from t:
1,2,3,4
repeatable-read可以确保一个事务中读取的数据是可重复的,也就是相同的读取(第一次读取以后,即使其他事务已经提交新的数据,同一个事务中再次select也并不会被读取)。
read-committed只是确保读取最新事务已经提交的数据。
当然数据的可见性都是对不同事务来说的,同一个事务,都是可以读到此事务中最新数据的。
starttransaction;
insertinto t(a)values(4);
select *from t;
1,2,3,4;
insertinto t(a)values(5);
select *from t;
1,2,3,4,5;
实验三:测试serializable事务对其他的影响
事务a serializable
事务b read-uncommitted
事务c read-committed,
事务d repeatable-read
事务e serializable
set autocommit =0;
start transaction ;
start transaction;
select a from t union all select sleep(1000) from dual;
insert into t(a)values(5);
insert into t(a)values(5);
insert into t(a)values(5);
insert into t(a)values(5);
error 1205 (hy000): lock wait timeout exceeded; try restarting transaction
error 1205 (hy000): lock wait timeout exceeded; try restarting transaction
error 1205 (hy000): lock wait timeout exceeded; try restarting transaction
error 1205 (hy000): lock wait timeout exceeded; try restarting transaction
serializable 串行化执行,导致所有其他事务不得不等待事务a结束才行可以执行,这里特意使用了sleep函数,直接导致事务b,c,d,e等待事务a持有释放的锁。由于我sleep了1000秒,而innodb_lock_wait_timeout为120s。所以120s到了就报错hy000错误。
serializable是相当严格的串行化执行模式,不管是读还是写,都会影响其他读取相同的表的事务。是严格的表级读写排他锁。也就失去了innodb引擎的优点。实际应用很少。
实验四:幻读
一些文章写到innodb的可重复读避免了“幻读”(phantom read),这个说法并不准确。
做个试验:(以下所有试验要注意存储引擎和隔离级别)
mysql>show create table t_bitfly/g;
create table `t_bitfly` (
`id` bigint(20) not null default '0',
`value` varchar(32) default null,
primary key (`id`)
) engine=innodb default charset=gbk
mysql>select @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation  |
+-----------------------+-----------------+
| repeatable-read       | repeatable-read |
+-----------------------+-----------------+
试验4-1:
tsessiona                                                     session b
|
| start transaction;                           start transaction;
|
| select * from t_bitfly;
| empty set
|                                insert into t_bitfly values (1, 'a');
|                             
|
| select * from t_bitfly;
| empty set
|                                                                      commit;
|
| select * from t_bitfly;
| empty set
|
| insert into t_bitfly values (1, 'a');
| error 1062 (23000):
| duplicate entry '1' for key 1
v (shit,刚刚明明告诉我没有这条记录的)
如此就出现了幻读,以为表里没有数据,其实数据已经存在了,傻乎乎的提交后,才发现数据冲突了。
试验4-2:
tsessiona               session b
|
| start transaction;           start transaction;
|
| select * from t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 |a     |
| +------+-------+
|                                       insert into t_bitfly values (2, 'b');
|                            
|
| select * from t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 |a     |
| +------+-------+
|                               commit;
|
| select * from t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 |a     |
| +------+-------+
|
| update t_bitfly set value='z';
| rows matched: 2  changed:2  warnings: 0
| (怎么多出来一行)
|
| select * from t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 |z     |
| |    2 |z     |
| +------+-------+
|
v
本事务中第一次读取出一行,做了一次更新后,另一个事务里提交的数据就出现了。也可以看做是一种幻读。
------
那么,innodb指出的可以避免幻读是怎么回事呢?
http://dev.mysql.com/doc/refman/5.0/en/innodb-record-level-locks.html
by default, innodb operatesin repeatable read transaction isolation level and with the innodb_locks_unsafe_for_binlogsystem variable disabled. in this case, innodb uses next-key locks for searchesand index scans, which prevents phantom rows (see section 13.6.8.5, “avoidingthe phantom problem using next-key locking”).
准备的理解是,当隔离级别是可重复读,且禁用innodb_locks_unsafe_for_binlog的情况下,在搜索和扫描index的时候使用的next-keylocks可以避免幻读。
关键点在于,是innodb默认对一个普通的查询也会加next-key locks,还是说需要应用自己来加锁呢?如果单看这一句,可能会以为innodb对普通的查询也加了锁,如果是,那和序列化(serializable)的区别又在哪里呢?
mysql manual里还有一段:
13.2.8.5. avoiding the phantomproblem using next-key locking (http://dev.mysql.com/doc/refman/5.0/en/innodb-next-key-locking.html)
toprevent phantoms, innodb usesan algorithm called next-key locking that combinesindex-row locking with gap locking.
youcan use next-key locking to implement a uniqueness check in your application:if you read your data in share mode and do not see a duplicate for a row youare going to insert, then you can safely insert your row and know that thenext-key lock set on the successor of your row during the read prevents anyonemeanwhile inserting a duplicate for your row. thus, the next-key lockingenables you to “lock” the nonexistence of something in your table.
我的理解是说,innodb提供了next-key locks,但需要应用程序自己去加锁。manual里提供一个例子:
select * from child where id> 100 for update;
这样,innodb会给id大于100的行(假如child表里有一行id为102),以及100-102,102+的gap都加上锁。
可以使用showinnodb status来查看是否给表加上了锁。
再看一个实验,要注意,表t_bitfly里的id为主键字段。
实验4-3:
t sessiona             session b
|
| start transaction;                start transaction;
|
| select * from t_bitfly
| where id<=1
| for update;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 |a     |
| +------+-------+
|                                      insert into t_bitfly   values (2, 'b');
|                            query ok, 1 row affected
|
| select * from t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 |a     |
| +------+-------+
|                              insert into t_bitfly
|                              values (0, '0');
|                                  (waiting for lock ...
|                               then timeout)
|                                   error 1205 (hy000):
|                                  lock wait timeout exceeded;
|                                   try restarting transaction
|
| select * from t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 |a     |
| +------+-------+
|                           commit;
|
| select * from t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 |a     |
| +------+-------+
v
可以看到,用id
mysql manual里对可重复读里的锁的详细解释:
http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
forlocking reads (select with forupdate or lock in share mode),update, and delete statements, lockingdepends on whether the statement uses a unique index with a unique searchcondition, or a range-type search condition. for a unique index with a uniquesearch condition, innodb locksonly the index record found, not the gap before it. for other searchconditions, innodb locksthe index range scanned, using gap locks or next-key (gap plus index-record)locks to block insertions by other sessions into the gaps covered by the range.
------
一致性读和提交读,先看实验,
实验4-4:
tsessiona              session b
|
| starttransaction;             start transaction;
|
| select * from t_bitfly;
| +----+-------+
| | id | value |
| +----+-------+
| |  1 |a     |
| +----+-------+
|                              insert into t_bitfly   values (2, 'b');
|                              
|                              commit;
|
| select * from t_bitfly;
| +----+-------+
| | id | value |
| +----+-------+
| |  1 |a     |
| +----+-------+
|
| select * from t_bitfly lock in share mode;
| +----+-------+
| | id | value |
| +----+-------+
| |  1 |a     |
| |  2 |b     |
| +----+-------+
|
| select * from t_bitfly for update;
| +----+-------+
| | id | value |
| +----+-------+
| |  1 |a     |
| |  2 |b     |
| +----+-------+
|
| select * from t_bitfly;
| +----+-------+
| | id | value |
| +----+-------+
| |  1 |a     |
| +----+-------+
v
如果使用普通的读,会得到一致性的结果,如果使用了加锁的读,就会读到“最新的”“提交”读的结果。
本身,可重复读和提交读是矛盾的。在同一个事务里,如果保证了可重复读,就会看不到其他事务的提交,违背了提交读;如果保证了提交读,就会导致前后两次读到的结果不一致,违背了可重复读。
可以这么讲,innodb提供了这样的机制,在默认的可重复读的隔离级别里,可以使用加锁读去查询最新的数据。
http://dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html
ifyou want to see the “freshest” state of the database, you should use either theread committed isolation level or a locking read:
select * from t_bitfly lock in share mode;
------
结论:mysqlinnodb的可重复读并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁度使用到的机制就是next-keylocks。
====================结尾====================
文章幻读部分直接转载了bitfly的文章: http://blog.bitfly.cn/post/mysql-innodb-phantom-read/ 
转载请说明出处,包括参考文章出处。
其它类似信息

推荐信息