SQLServer事务管理和锁的底层机制浅谈
环境准备
-
创建数据库及表
create database student; use student; create table campus_card ( studcardid char(8), balance decimal(10,2) ); create table icbc_card ( studcardid char(8), icbcid char(10), balance decimal(10,2) );
-
插入数据
insert into campus_card values ('20150031',30), ('20150032',50), ('20150033',70); insert into icbc_card values ('20150031','2015003101',1000), ('20150032','2015003201',1000), ('20150033','2015003301',1000);
针对数据库STUDENT,按要求完成以下内容
-
编写一个事务处理实现如下操作:
学号为20150032的学生需要从银行卡中转帐200元到自己的校园卡中,若过程中出现错误则回滚
set transaction isolation level repeatable read Begin transaction use STUDENT go declare @x decimal(10,2) select @x=balance from icbc_card where studcardid='20150032' set @x=@x-200 if(@x>=0) begin update icbc_card set balance=@x where studcardid='20150032' update campus_card set balance=balance+200 where studcardid='20150032' commit tran end else begin print '余额不足,不能转账!' rollback tran end --隔离级别为REPEATABLE READ
执行
select * from campus_card select * from icbc_card --默认的隔离级别为READ COMMITTED --且这两个是隐式的事务定义,每一句都是一个隔离级别为READ COMMITTED的事务 --但是这个和上面的那个事务是顺序执行的,没有啥影响
执行结果
-
基于当前数据库STUDENT,设计样例展示四种数据不一致问题:丢失修改、读脏数据、不可重复读和幻读(删除和插入)
-
丢失修改
--由于上面更改过默认级别,所以先执行 set transaction isolation level READ COMMITTED --重置所有打开会话的默认隔离级别为READ COMMITTED
--事务1 begin tran declare @balance decimal(10,2) select @balance=balance from campus_card where studcardid='20150033' waitfor delay '00:00:05' set @balance=@balance-10 update campus_card set balance=@balance where studcardid='20150033' commit tran go select balance from campus_card where studcardid='20150033'
--事务2 begin tran declare @balance1 decimal(10,2) select @balance1=balance from campus_card where studcardid='20150033' waitfor delay '00:00:05' set @balance1=@balance1-20 update campus_card set balance=@balance1 where studcardid='20150033' commit tran go select balance from campus_card where studcardid='20150033'
在窗口1中执行事务1,很快切换到第二个窗口执行事务2
执行结果:
事务1:
事务2:
过程分析:
这两个事务的隔离级别都是READ COMMITTED,都会在读取数据时申请共享锁
而先执行的那个事务(即事务1)会先申请到共享锁,然后读取完之后释放共享锁,读取到的数据为70,然后延迟5秒,事务2申请到该数据的共享锁,然后读取,然后释放共享锁,读取到的数据为70,两个共享锁可以叠加(但是手速不够)
然后事务1申请到排他锁,进行数据的更新,将balance更新为60,然后释放,释放后事务2才能申请到排他锁,然后更新,将balance更新为50,然后释放。
此时发生了更新的覆盖,即50覆盖掉了60
我第一次验证这个实验的时候,没有修改默认的隔离界别,所以当时两个事务的隔离级别为REPEATABLE READ,发生了死锁
分析:
事务1申请到了共享锁,事务2也申请到了共享锁,这两个共享锁都是要持续到事务结束才能释放,所以事务1的更新语句要等事务2的共享锁释放后才能加排他锁,而事务2的更新语句要等事务1的共享锁释放后才能加排他锁,所以造成了死锁
-
读脏数据(dirty read):
--事务1 set transaction isolation level read uncommitted begin tran declare @balance decimal(10,2) select @balance=balance from campus_card where studcardid='20150032' update campus_card set balance=@balance+100 where studcardid='20150032' waitfor delay '00:00:05' rollback tran go select balance from campus_card where studcardid='20150032'
--事务2 set transaction isolation level read uncommitted begin tran declare @balance decimal(10,2) select @balance=balance from campus_card where studcardid='20150032' update campus_card set balance=@balance+50 where studcardid='20150032' commit tran go select balance from campus_card where studcardid='20150032'
先在一个窗口执行事务1,然后很快在另一个窗口执行事务2
执行结果
事务1:
事务2:
过程分析:
事务1和事务2的隔离级别都是READ UNCOMMITTED
所以事务1和事务2读取数据时都不会申请共享锁
事务1读取数据后,进行更新数据,此时给数据加上了排他锁,回滚之前将balance更新为350。
但是事务2此时还是能读取该数据,在此基础上加50成为400,事务2提交了事务,该数据写入数据库。
而事务1回滚事务,相当于没有操作,但是此时balance已经变成了400
-
不可重复读(NonRepeatable read):
--事务1 set transaction isolation level READ COMMITTED use STUDENT go begin tran select balance from campus_card where studcardid='20150031' waitfor delay '00:00:10' select balance from campus_card where studcardid='20150031' commit tran
--事务2 set transaction isolation level READ COMMITTED use STUDENT go begin tran update campus_card set balance=balance+200 where studcardid='20150031' commit tran
先在一个窗口执行事务1,然后很快在另一个窗口执行事务2
执行结果
事务1:
过程分析:
两个事务的隔离级别都是READ COMMITTED
事务1第一次读取时,加了共享锁,读取完之后释放。在等待期间,数据是没有加锁的,此时事务2加上排他锁进行数据的更新,实行完成提交事务。然后事务1的第二次读取,加上共享锁,读取完成后释放。但是这两次读取到的数据已经不一样了
-
幻读(Phantom Read):
-
插入
--事务1 set transaction isolation level repeatable read begin tran use STUDENT go select balance from campus_card where studcardid='20150031' waitfor delay '00:00:05' select balance from campus_card where studcardid='20150031' commit tran
--事务2 set transaction isolation level repeatable read begin tran insert into campus_card values('20150031',30) commit tran
先在一个窗口执行事务1,然后很快在另一个窗口执行事务2
执行结果
事务1:
过程分析:
事务1和事务2的隔离级别都是REPEATABLE READ
则事务1第一次读取时,在数据上面加上了共享锁,知道事务1结束才释放
但是事务1只是在studcardid='20150031’上面加的共享锁,其它不符合where条件的数据没有加锁,所以事务2能够进行插入操作,所以出现了幻读现象
-
删除
--事务1 set transaction isolation level read committed begin tran use STUDENT go select balance from campus_card where studcardid='20150031' waitfor delay '00:00:10' select balance from campus_card where studcardid='20150031' commit tran
set transaction isolation level repeatable read begin tran delete from campus_card where studcardid='20150031' commit tran
先在一个窗口执行事务1,然后很快在另一个窗口执行事务2
执行结果
事务1:
过程分析:
这里将事务1的隔离级别换成了READ COMMITTED
如果还是REPEATABLE READ则不能成功验证,因为事务2的delete操作涉及到studcardid='20150031’的数据,事务1结束之前该数据上是有事务1加的共享锁,所以得等到事务1执行完。
事务1的隔离级别是READ COMMITTED
则事务1的读取加的共享锁会在读取操作完成之后就释放,那么等待期间,这些数据上没有锁,则事务2就可以删除这些数据,所以事务1第二次读取的时候,数据已经被删除了
-
-
-
利用锁机、数据库的隔离级别等知识,设计方案分别解决上述丢失修改、读脏数据和不可重复读、幻读的数据不一致问题,可以用sp_lock过程查看当前锁的状态。
修改隔离级别以确保数据的正确性:
-
丢失修改,在SQL语句前加已提交读``
分析:
如果隔离级别是
REPEATABLE READ
和SERIALIZABLE
则会发生死锁,原因前面已经分析
本实验中丢失修改不发生的关键是让事务1和事务2最好能够顺序执行,我的一个想法是可以在事务1最前面加上一句修改语句,那么事务1开始执行时就会给该数据加上排他锁直到事务1结束。事务2就只能等到事务1结束后才能获取到共享锁和排他锁
执行结果:
事务1:
事务2:
-
读脏数据,将事务2的隔离级别设置成
READ COMMITTED
,事务1的隔离级别随意则此时,事务1读取在修改数据期间,在数据上面加上了排他锁,则事务2在事务1确认提交之前不能得到该数据的共享锁,所以不能读取该数据,等待事务1回滚后,该数据上的排他锁就释放了,事务2就可以读取数据进行更新
我觉得这个事务2修改为
READ COMMITED
隔离级别之后,还是存在着读脏数据的风险,因为事务1有个数据的读取阶段,也就是说事务1在执行时是先将该数据加上共享锁(或者不加锁),但是共享锁允许其它事务在该数据上再加共享锁,所以如果事务1和事务2执行的间隔够短,则事务2就能够再事务1加上排他锁之前读取到数据,还是会发生读取脏数据执行结果:
-
不可重复读,将事务1的隔离级别设置为
REPEATABLE READ
,事务2的隔离级别至少设置为READ COMMITTED
(要申请共享锁)此时事务1执行,第一次读取数据时,在数据上加上了共享锁,且共享锁一直持续到事务结束才释放,所以在事务1结束之前,事务2不能够获得事务的排他锁进行更新。
所以事务1两次读取到的数据是一致的
执行结果:
事务1:
-
幻影读
- 插入:设置事务1的隔离级别为
serializable
,事务2的隔离级别随意
此时事务1执行期间,是不允许其它事务插入数据的,所以不会发生幻影读
执行结果:
事务1:
- 删除:设置事务1的隔离级别为
REPEATABLE READ
,事务2的隔离级别随意
因为事务2是删除操作,这个排他锁是要加的数据是符合事务1的where条件的数据
所以事务1只需在事务1结束之前,给这些数据上加上共享锁即可
执行结果:
事务1:
- 插入:设置事务1的隔离级别为
-
-
构造一个出现死锁的情况
set transaction isolation level read committed
--事务1 begin tran declare @read int select @read=grade from sc where sno='95003' waitfor delay '00:00:10' update sc set grade=@read-1 where sno='95003'
--事务2 begin tran declare @read int select @read=grade from sc where sno='95003' update sc set grade=@read-1 where sno='95003'
将两个事务的隔离级别都设置成为
REPEATABLE READ
即可发生死锁这个和我前面验证那个修改丢失很类似
即事务1先获取到共享锁,知道事务1结束后才释放
事务2执行时同样也获得了该数据的共享锁,直到事务2结束后才释放
则此时事务1的修改语句要加排他锁,但是得先等事务2释放共享锁
同理,事务2想加排他锁得先等事务1释放共享锁,就发生了死锁