数据库锁与事物

事务并发控制

我们从另外一个方向来说说,如果不对事务进行并发控制,我们看看数据库并发操作是会有那些异常情形,有些使我们可以接受的,有些是不能接受的,注意这里的异常就是特定语境下的,并不一定就是错误什么的。假设有一个order表,有个字段叫count,作为计数用,当前值为100

  • 第一类丢失更新(Update Lost):此种更新丢失是因为回滚的原因,所以也叫回滚丢失。此时两个事务同时更新count,两个事务都读取到100,事务一更新成功并提交,count=100+1=101,事务二出于某种原因更新失败了,然后回滚,事务二就把count还原为它一开始读到的100,此时事务一的更新就这样丢失了。

  • 脏读(Dirty Read):此种异常时因为一个事务读取了另一个事务修改了但是未提交的数据。举个例子,事务一更新了count=101,但是没有提交,事务二此时读取count,值为101而不是100,然后事务一出于某种原因回滚了,然后第二个事务读取的这个值就是噩梦的开始。

  • 不可重复读(Not Repeatable Read):此种异常是一个事务对同一行数据执行了两次或更多次查询,但是却得到了不同的结果,也就是在一个事务里面你不能重复(即多次)读取一行数据,如果你这么做了,不能保证每次读取的结果是一样的,有可能一样有可能不一样。造成这个结果是在两次查询之间有别的事务对该行数据做了更新操作。举个例子,事务一先查询了count,值为100,此时事务二更新了count=101,事务一再次读取count,值就会变成101,两次读取结果不一样。

  • 第二类丢失更新(Second Update Lost):此种更新丢失是因为更新被其他事务给覆盖了,也可以叫覆盖丢失。举个例子,两个事务同时更新count,都读取100这个初始值,事务一先更新成功并提交,count=100+1=101,事务二后更新成功并提交,count=100+1=101,由于事务二count还是从100开始增加,事务一的更新就这样丢失了。

  • 幻读(Phantom Read):幻读和不可重复读有点像,只是针对的不是数据的值而是数据的数量。此种异常是一个事务在两次查询的过程中数据的数量不同,让人以为发生幻觉,幻读大概就是这么得来的吧。举个例子,事务一查询order表有多少条记录,事务二新增了一条记录,然后事务一查了一下order表有多少记录,发现和第一次不一样,这就是幻读。

数据库锁分类

一般可以分为两类,一个是悲观锁,一个是乐观锁,悲观锁一般就是我们通常说的数据库锁机制,乐观锁一般是指用户自己实现的一种锁机制,比如hibernate实现的乐观锁甚至编程语言也有乐观锁的思想的应用。

悲观锁:顾名思义,就是很悲观,它对于数据被外界修改持保守态度,认为数据随时会修改,所以整个数据处理中需要将数据加锁。悲观锁一般都是依靠关系数据库提供的锁机制,事实上关系数据库中的行锁,表锁不论是读写锁都是悲观锁。

悲观锁按照使用性质划分:
  • 共享锁(Share locks简记为S锁):也称读锁,事务A对对象T加s锁,其他事务也只能对T加S,多个事务可以同时读,但不能有写操作,直到A释放S锁。

  • 排它锁(Exclusivelocks简记为X锁):也称写锁,事务A对对象T加X锁以后,其他事务不能对T加任何锁,只有事务A可以读写对象T直到A释放X锁。

  • 更新锁(简记为U锁):用来预定要对此对象施加X锁,它允许其他事务读,但不允许再施加U锁或X锁;当被读取的对象将要被更新时,则升级为X锁,主要是用来防止死锁的。因为使用共享锁时,修改数据的操作分为两步,首先获得一个共享锁,读取数据,然后将共享锁升级为排它锁,然后再执行修改操作。这样如果同时有两个或多个事务同时对一个对象申请了共享锁,在修改数据的时候,这些事务都要将共享锁升级为排它锁。这些事务都不会释放共享锁而是一直等待对方释放,这样就造成了死锁。如果一个数据在修改前直接申请更新锁,在数据修改的时候再升级为排它锁,就可以避免死锁。

补充:

  1. 共享锁(Shared lock)。
    例1:
    ----------------------------------------
    T1:    select * from table (请想象它需要执行1个小时之久,后面的sql语句请都这么想象)
    T2:    update table set column1='hello'
    
    过程:
    
    T1运行 (加共享锁)
    T2运行
    If T1 还没执行完
        T2等......
    else
        锁被释放
        T2执行
    endif
    
    T2之所以要等,是因为T2在执行update前,试图对table表加一个排他锁,
    而数据库规定同一资源上不能同时共存共享锁和排他锁。所以T2必须等T1
    执行完,释放了共享锁,才能加上排他锁,然后才能开始执行update语句。
    
    例2:
    ----------------------------------------
    T1:    select * from table
    T2:    select * from table
    
    这里T2不用等待T1执行完,而是可以马上执行。
    
    分析:
    T1运行,则table被加锁,比如叫lockA
    T2运行,再对table加一个共享锁,比如叫lockB。
    
    两个锁是可以同时存在于同一资源上的(比如同一个表上)。这被称为共
    享锁与共享锁兼容。这意味着共享锁不阻止其它session同时读资源,但阻
    止其它session update
    
    例3:
    ----------------------------------------
    T1:    select * from table
    T2:    select * from table
    T3:    update table set column1='hello'
    
    这次,T2不用等T1运行完就能运行,T3却要等T1和T2都运行完才能运行。
    因为T3必须等T1和T2的共享锁全部释放才能进行加排他锁然后执行update
    操作。
    
    例4:(死锁的发生)
    ----------------------------------------
    T1:
    begin tran
    select * from table (holdlock) (holdlock意思是加共享锁,直到事物结束才释放)
    update table set column1='hello'
    
    T2:
    begin tran
    select * from table(holdlock)
    update table set column1='world'
    
    假设T1和T2同时达到select,T1对table加共享锁,T2也对加共享锁,当
    T1的select执行完,准备执行update时,根据锁机制,T1的共享锁需要升
    级到排他锁才能执行接下来的update.在升级排他锁前,必须等table上的
    其它共享锁释放,但因为holdlock这样的共享锁只有等事务结束后才释放,
    所以因为T2的共享锁不释放而导致T1等(等T2释放共享锁,自己好升级成排
    他锁),同理,也因为T1的共享锁不释放而导致T2等。死锁产生了。
    
    例5:
    ----------------------------------------
    T1:
    begin tran
    update table set column1='hello' where id=10
    
    T2:
    begin tran
    update table set column1='world' where id=20
    
    这种语句虽然最为常见,很多人觉得它有机会产生死锁,但实际上要看情
    况,如果id是主键上面有索引,那么T1会一下子找到该条记录(id=10的记
    录),然后对该条记录加排他锁,T2,同样,一下子通过索引定位到记录,
    然后对id=20的记录加排他锁,这样T1和T2各更新各的,互不影响。T2也不
    需要等。
    
    但如果id是普通的一列,没有索引。那么当T1对id=10这一行加排他锁后,
    T2为了找到id=20,需要对全表扫描,那么就会预先对表加上共享锁或更新
    锁或排他锁(依赖于数据库执行策略和方式,比如第一次执行和第二次执行
    数据库执行策略就会不同)。但因为T1已经为一条记录加了排他锁,导致
    T2的全表扫描进行不下去,就导致T2等待。
    
    死锁怎么解决呢?一种办法是,如下:
    例6:
    ----------------------------------------
    T1:
    begin tran
    select * from table(xlock) (xlock意思是直接对表加排他锁)
    update table set column1='hello'
    
    T2:
    begin tran
    select * from table(xlock)
    update table set column1='world'
    
    这样,当T1的select 执行时,直接对表加上了排他锁,T2在执行select时,就需要等T1事物完全执行完才能执行。排除了死锁发生。
    但当第三个user过来想执行一个查询语句时,也因为排他锁的存在而不得不等待,第四个、第五个user也会因此而等待。在大并发
    情况下,让大家等待显得性能就太友好了,所以,这里引入了更新锁。
    
  2. 更新锁(Update lock)
    为解决死锁,引入更新锁。
    
    例7:
    ----------------------------------------
    T1:
    begin tran
    select * from table(updlock) (加更新锁)
    update table set column1='hello'
    T2:
    begin tran
    select * from table(updlock)
    update table set column1='world'
    
    更新锁的意思是:“我现在只想读,你们别人也可以读,但我将来可能会做更新操作,我已经获取了从共享锁(用来读)到排他锁
    (用来更新)的资格”。一个事物只能有一个更新锁获此资格。
    
    T1执行select,加更新锁。
    T2运行,准备加更新锁,但发现已经有一个更新锁在那儿了,只好等。
    
    当后来有user3、user4...需要查询table表中的数据时,并不会因为T1的select在执行就被阻塞,照样能查询,相比起例6,这提高
    了效率。
    
    例8:
    ----------------------------------------
    T1:    select * from table(updlock)    (加更新锁)
    T2:    select * from table(updlock)    (等待,直到T1释放更新锁,因为同一时间不能在同一资源上有两个更新锁)
    T3:    select * from table (加共享锁,但不用等updlock释放,就可以读)
    
    这个例子是说明:共享锁和更新锁可以同时在同一个资源上。这被称为共享锁和更新锁是兼容的。
    
    例9:
    ----------------------------------------
    T1:
    begin
    select * from table(updlock)      (加更新锁)
    update table set column1='hello'  (重点:这里T1做update时,不需要等T2释放什么,而是直接把更新锁升级为排他锁,然后执行update)
    T2:
    begin
    select * from table               (T1加的更新锁不影响T2读取)
    update table set column1='world'  (T2的update需要等T1的update做完才能执行)
    
    我们以这个例子来加深更新锁的理解,
    
    第一种情况:T1先达,T2紧接到达;在这种情况中,T1先对表加更新锁,T2对表加共享锁,假设T2的select先执行完,准备执行update,
    发现已有更新锁存在,T2等。T1执行这时才执行完select,准备执行update,更新锁升级为排他锁,然后执行update,执行完成,事务
    结束,释放锁,T2才轮到执行update。
    
    第二种情况:T2先达,T1紧接达;在这种情况,T2先对表加共享锁,T1达后,T1对表加更新锁,假设T2 select先结束,准备
    update,发现已有更新锁,则等待,后面步骤就跟第一种情况一样了。
    
    这个例子是说明:排他锁与更新锁是不兼容的,它们不能同时加在同一子资源上。
    
    
  3. 排他锁(独占锁,Exclusive Locks)
    这个简单,即其它事务既不能读,又不能改排他锁锁定的资源。
    例10
    T1:    update table set column1='hello' where id<1000
    T2:    update table set column1='world' where id>1000
    
    假设T1先达,T2随后至,这个过程中T1会对id<1000的记录施加排他锁.但不会阻塞T2的update。
    
    例11 (假设id都是自增长且连续的)
    T1:    update table set column1='hello' where id<1000
    T2:    update table set column1='world' where id>900
    
    如同例10,T1先达,T2立刻也到,T1加的排他锁会阻塞T2的update.
    
  4. 意向锁(Intent Locks)
    意向锁就是说在屋(比如代表一个表)门口设置一个标识,说明屋子里有人(比如代表某些记录)被锁住了。另一个人想知道屋子
    里是否有人被锁,不用进屋子里一个一个的去查,直接看门口标识就行了。
    
    当一个表中的某一行被加上排他锁后,该表就不能再被加表锁。数据库程序如何知道该表不能被加表锁?一种方式是逐条的判断该
    表的每一条记录是否已经有排他锁,另一种方式是直接在表这一层级检查表本身是否有意向锁,不需要逐条判断。显然后者效率高。
    
    例12:
    ----------------------------------------
    T1:    begin tran
           select * from table (xlock) where id=10  --意思是对id=10这一行强加排他锁
    T2:    begin tran
           select * from table (tablock)     --意思是要加表级锁
           
    假设T1先执行,T2后执行,T2执行时,欲加表锁,为判断是否可以加表锁,数据库系统要逐条判断table表每行记录是否已有排他锁,
    如果发现其中一行已经有排他锁了,就不允许再加表锁了。只是这样逐条判断效率太低了。
    
    实际上,数据库系统不是这样工作的。当T1的select执行时,系统对表table的id=10的这一行加了排他锁,还同时悄悄的对整个表
    加了意向排他锁(IX),当T2执行表锁时,只需要看到这个表已经有意向排他锁存在,就直接等待,而不需要逐条检查资源了。
    
    例13:
    ----------------------------------------
    T1:    begin tran
           update table set column1='hello' where id=1
    T2:    begin tran
           update table set column1='world' where id=1
    
    这个例子和上面的例子实际效果相同,T1执行,系统对table同时对行家排他锁、对页加意向排他锁、对表加意向排他锁。
    
  5. 计划锁(Schema Locks)
    例14:
    ----------------------------------------
    alter table .... (加schema locks,称之为Schema modification (Sch-M) locks
    
    DDL语句都会加Sch-M锁
    该锁不允许任何其它session连接该表。连都连不了这个表了,当然更不用说想对该表执行什么sql语句了。
    
    例15:
    ----------------------------------------
    用jdbc向数据库发送了一条新的sql语句,数据库要先对之进行编译,在编译期间,也会加锁,称之为:Schema stability (Sch-S) locks
    
    select * from tableA
    
    编译这条语句过程中,其它session可以对表tableA做任何操作(update,delete,加排他锁等等),但不能做DDL(比如alter table)操作。

悲观锁按照作用范围划分:
  • 行锁:锁的作用范围是行级别,数据库能够确定那些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。举个例子,一个用户表user,有主键id和用户生日birthday当你使用update … where id=?这样的语句数据库明确知道会影响哪一行,它就会使用行锁,当你使用update … where birthday=?这样的的语句的时候因为事先不知道会影响哪些行就可能会使用表锁。
  • 表锁:锁的作用范围是整张表。

乐观锁:顾名思义,就是很乐观,每次自己操作数据的时候认为没有人回来修改它,所以不去加锁,但是在更新的时候会去判断在此期间数据有没有被修改,需要用户自己去实现。既然都有数据库提供的悲观锁可以方便使用为什么要使用乐观锁呢?对于读操作远多于写操作的时候,大多数都是读取,这时候一个更新操作加锁会阻塞所有读取,降低了吞吐量。最后还要释放锁,锁是需要一些开销的,我们只要想办法解决极少量的更新操作的同步问题。换句话说,如果是读写比例差距不是非常大或者你的系统没有响应不及时,吞吐量瓶颈问题,那就不要去使用乐观锁,它增加了复杂度,也带来了额外的风险。

乐观锁实现方式:
  • 版本号(记为version):就是给数据增加一个版本标识,在数据库上就是表中增加一个version字段,每次更新把这个字段加1,读取数据的时候把version读出来,更新的时候比较version,如果还是开始读取的version就可以更新了,如果现在的version比老的version大,说明有其他事务更新了该数据,并增加了版本号,这时候得到一个无法更新的通知,用户自行根据这个通知来决定怎么处理,比如重新开始一遍。这里的关键是判断version和更新两个动作需要作为一个原子单元执行,否则在你判断可以更新以后正式更新之前有别的事务修改了version,这个时候你再去更新就可能会覆盖前一个事务做的更新,造成第二类丢失更新,所以你可以使用update … where … and version=”old version”这样的语句,根据返回结果是0还是非0来得到通知,如果是0说明更新没有成功,因为version被改了,如果返回非0说明更新成功。
  • 时间戳(timestamp):和版本号基本一样,只是通过时间戳来判断而已,注意时间戳要使用数据库服务器的时间戳不能是业务系统的时间。
  • 待更新字段:和版本号方式相似,只是不增加额外字段,直接使用有效数据字段做版本控制信息,因为有时候我们可能无法改变旧系统的数据库表结构。假设有个待更新字段叫count,先去读取这个count,更新的时候去比较数据库中count的值是不是我期望的值(即开始读的值),如果是就把我修改的count的值更新到该字段,否则更新失败。java的基本类型的原子类型对象如AtomicInteger就是这种思想。
  • 所有字段:和待更新字段类似,只是使用所有字段做版本控制信息,只有所有字段都没变化才会执行更新。

    乐观锁几种方式的区别:

    新系统设计可以使用version方式和timestamp方式,需要增加字段,应用范围是整条数据,不论那个字段修改都会更新version,也就是说两个事务更新同一条记录的两个不相关字段也是互斥的,不能同步进行。旧系统不能修改数据库表结构的时候使用数据字段作为版本控制信息,不需要新增字段,待更新字段方式只要其他事务修改的字段和当前事务修改的字段没有重叠就可以同步进行,并发性更高。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值