MySql事务隔离级别锁机制实际案例深入分析

1      什么是隔离级别?锁?

首先我在此不强调定义,而详细的讨论实际开发中,什么时的情况下会用到隔离级别,什么情况下又会用到锁呢?在我面试过的开发人员中似乎很多人都没有关注过这些,甚至有人听不懂“隔离级别”为何物,的确很多情况下你跟本不用考虑这些。这么说不是没用?在什么情况下会产生影响?会造成那些不一致的情况?带着这些问题,让我们用一个简单的实例来说明,你将恍然大悟。

2      实例场景说明

在那些场景下会用到呢?不访先举出一些常见的实例,如网上订票、公司内部会议室预定等,这些案例都有一个特点,即都会有很多用户竞争获取这些资源,而从程序的角度看,就是同时有很多事务对数据库中的记录进行更新,如某个用户订到会议室了,则除了会记录登记会议室的地点、使用时间、预订者之外,还会把这段时间会议室标记为不可再申请,即该资源已分配了。

 

3      实例详解

3.1    创建表、记录

以上述的会议室预订为例,这里为了简单,即不考虑会议室的位置、时间等业务,只有一个标识ID字段和表示会议室个数的字段,申请人预定时提供一个数字表示要预订会议室的数量,创建表和数据的脚本如下:

-- 创建会议室表
DROP TABLE IF EXISTS `Office`;
CREATE TABLE `Office` (
  `Id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `Cnt` int(11) NOT NULL COMMENT '会议室数量',
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB AUTO_INCREMENT=53 DEFAULT CHARSET=utf8;

truncate table Office;

-- 初始100个会议室
insert into Office values(1,100);

 

3.2    模拟分析各种情况下剩余会议室数量

同样为了方便说明问题,我们用两个查询窗口(实际上通过网站中调用数据库效果一样),来模拟两个或多个事务同时请求情况,这里打开两个窗口,本例我们采用的是Navicate for Mysql工具完成模拟,其中两个事务的执行顺序请按下方的行号来执行(由于事务可以是完全随机发生的,所以这里给出的很多不同的执行顺序)。

 

l  正常的更新

 

事务A

事务B

备注

set  autocommit=0;

 

模拟事务,让其不立即提交,下同。

 

set  autocommit=0;

 

update  office set Cnt=Cnt-50 where id=1;

 

可以执行成功

 

update  office set Cnt=Cnt-50 where id=1;

由于事务A更新了该行记录,未提交,即A上了排它锁,此时将等待下去

commit;

 

提交了事务,这时锁释放了,事务B将自动执行

 

Commit;

事务

select  Cnt from office where id=1;

 

查询一下结果

 

 

剩作会议室有两种情况,一种是在事务A提交前事务B超时,这时事务B失败回滚,即不影响数据,那么该值为50,另一种是事务B提交也成功了,则结果为0了。

 

l  数据产生了不一致情况

上面的业务在订会议室前,没有考虑会议室的数量,就去减,这样很可能会减出负数,一般程序会先判断一下是否有足够的会议室再去减操作,如下:

 

事务A

事务B

备注

set  autocommit=0;

 

模拟事务,让其不立即提交,下同。

 

set  autocommit=0;

 

select  Cnt from office where id=1;

 

事务A先查出来看看数量够不够

update  office set Cnt=Cnt-50 where id=1;

 

事务A占用了50

 

select  Cnt from office where id=1;

事务B也查出来看看数量够不够,此时仍有100个未占用,这里就产生的判断失误了,失际上只有50个了,这是因为Mysql的默认隔离级别为可重复读

 

update  office set Cnt=Cnt-60 where id=1;

事务B占用了60

commit;

 

提交了事务,这时锁释放了,事务B将自动执行

 

Commit;

事务

select  Cnt from office where id=1;

 

查询一下结果

 

此时的结果,如超时则同上述一样,正常则仍为-10,这个结果是一致性的,但不是业务上预期的,即不能把会议室订到剩下-10个,这是由于事务B的中间的一个判断引起的,即可重复读的隔离级别下,数据在整个事务期间读取是一致的,不会受到外部事务的影响。

 

l  死锁情况

为了避免上述情况,我们在读取时上共享锁,由于更新是独占锁,它们不相容,这样可以避免上述数量判断失误的情况,因为不等两个共享锁释放它们都无法更新,但带来的新的问题,即两个都在无限期的等待对方释方,引起了死锁,如下:

 

事务A

事务B

备注

set  autocommit=0;

 

模拟事务,让其不立即提交,下同。

 

set  autocommit=0;

 

select  Cnt from office where id=1 lock in share mode;

 

事务A先查出来看看数量够不够并上共享锁,防止其它事务更新

 

select  Cnt from office where id=1 lock in share mode;

事务B也查出来看看数量够不够并上共享锁,防止其它事务更新

update  office set Cnt=Cnt-50 where id=1;

 

事务A等待中

 

update  office set Cnt=Cnt-50 where id=1;

事务B也提交更新,此时系统检测到死锁,返回了Deadlock found when trying to get lock; try  restarting transaction,即事务B失败回滚,这时事务A自动成功执行

commit;

 

 

select  Cnt from office where id=1;

 

查询一下结果

 

此时的结果为50,预期中的结果,但美中不足是事务B失败了,成了牺牲品,这种结果在事务上是一致性的,数据结果符合预期,但是用户体验不好,没有都成功。

 

l  完善的解决方案

 

事务A

事务B

备注

set  autocommit=0;

 

模拟事务,让其不立即提交,下同。

 

set  autocommit=0;

 

select  Cnt from office where id=1 for update;

 

事务A先查出来看看数量够不够并上更新锁(独占),防止其它事务更新

 

select  Cnt from office where id=1 for update;

事务B也查出来看看数量够不够并上更新锁(独占),防止其它事务更新,但此时不能再加这个锁,等待中

update  office set Cnt=Cnt-50 where id=1;

 

事务A成功执行

commit;

 

事务A提交了,此时事务B自动执行上意向独占锁,并向下执行

 

update  office set Cnt=Cnt-50 where id=1;

成功

select  Cnt from office where id=1;

 

查询一下结果

 

此时结果完全符合预期,并且中间的数量验证也符合业务规则,数据可以保证一致性。

通过以上分析,我想你对事务的并发执行、锁机制有一个了解了吧,下面我们,再把焦点转向事务隔离级别。

 

3.3    事务隔离级别

ISO/ANSISQL92规范中就提出了几种不同的事务隔离级别,当然隔离程度是逐步加强的,从最低的级别允许脏读到最高的可串行化,总体理解来说,隔离级别越高,事务间的并发性越是差,即可能因为两个事务有用到相关的数据,会造成一个事务要完全等另一个执行完成才能开始,造成并发和性能大打折扣,所以主流的数据库如Sql server     oracle都把隔离级别设为已提交读(当然它们已提交读的策略上仍有差异),mysql则为重复读,下面表格为不同的级别下读取情况(注各数据库实现上可能存在差异):

 

隔离级别

脏读

非重复读

幻像读

read uncommitted

允许

允许

允许

read committed

 

允许

允许

repeatable  read

 

 

允许

serializable

 

 

 

 

这么看,可能没有感觉,下面同样用一个例子来说明,如下:

 

事务A

事务B

当前步骤说明

SELECT  @@GLOBAL.tx_isolation, @@tx_isolation;

 

则可以看到全局和当前会话所使用的隔离级别为:

REPEATABLE-READ即重复读

 

SELECT  @@GLOBAL.tx_isolation, @@tx_isolation;

则可以看到全局和当前会话所使用的隔离级别为:

REPEATABLE-READ即重复读

SET  SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

SELECT  @@GLOBAL.tx_isolation, @@tx_isolation;

 

把当前会话设置为已提交读

 

SET  SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

SELECT  @@GLOBAL.tx_isolation, @@tx_isolation;

把当前会话设置为已提交读

set  autocommit=0;

 

模拟事务,让其不立即提交,下同。

 

set  autocommit=0;

 

update  office set Cnt=Cnt-50 where id=1;

 

 

commit;

 

 

 

select  Cnt from office where id=1 ;

此时读取的是预期值50

 

update  office set Cnt=Cnt-50 where id=1;

成功

 

 

 

select  Cnt from office where id=1;

 

查询一下结果

 

 

 

3.4    延伸&拓展

l  本实例使用的所有源码,见附件:

SELECT @@GLOBAL.tx_isolation, @@tx_isolation;

-- 运行设置和不设置已提交读情况
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@GLOBAL.tx_isolation, @@tx_isolation;

set autocommit=0; 

-- 分别运行:没有任何执行时和下方的任意一种
select RunCount from unit where id=53; -- 常规时,互视对方不存在
select RunCount from unit where id=53 lock in share mode; -- 两者都可以加,但下方更新时会存在死锁情况
select RunCount from unit where id=53 for update; -- 意向更新锁,此时另一个不能再加这个锁

update unit set RunCount=RunCount-6 where id=53;

select RunCount from unit where id=53;

commit;

 

你完全可以按照你希望的执行顺序,随意的调整并查看执行结果,从而更深入的体会到事务、锁、隔离级别的奇妙之处。

 

l  本文保持100%原创,不复制任何其它文章中的内容,当然如果你希望进一步的了解更全面的内容,可访问以下相关网页:

http://www.cnblogs.com/ggjucheng/archive/2012/11/14/2770445.html

http://www.cnblogs.com/ggjucheng/archive/2012/11/14/2770445.html

http://www.blogjava.net/neverend/archive/2012/03/31/373127.html

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RocChenKing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值