SQLServer事务管理和锁底层机制浅谈

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 READSERIALIZABLE

    则会发生死锁,原因前面已经分析

    本实验中丢失修改不发生的关键是让事务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:

  • 构造一个出现死锁的情况

    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释放共享锁,就发生了死锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

lzukomorebi

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

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

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

打赏作者

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

抵扣说明:

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

余额充值