数据库的锁机制

来源:互联网 发布:python可以建网站吗 编辑:程序博客网 时间:2024/05/19 00:54

为什么需要锁机制

事务是并发控制的基本单位,保证事务ACID原则是事务处理的重要任务,但是当多个事务对数据库进行并发操作时,就有可能破坏事务的ACID特性。

为了保证事务的隔离性与一致性,就有了数据库的锁机制。

在数据库中,存在着很多种类的锁:共享锁、排他锁、悲观锁、乐观锁、行级锁、表级锁等。

基本的锁类型

锁有两种基本的类型:共享锁、排他锁

共享锁(Share Locks,简称S锁,也叫读锁)

若事务T对数据对象A加上S锁,则事务T只能读取A而不能修改A,其他事务也只能对该数据对象A加上S锁,而不能加上X锁,直到事务T释放A上的S锁。

即是说,共享锁只允许多个事务读取数据,而不允许修改数据。

排他锁(Exclusive Locks,简称X锁,也叫写锁)

若事务T对数据对象A加上X锁,则只允许事务T对A进行读取和修改,其他事务都不能对A加上任何类型的锁,直到事务T释放A上的X锁。

即是说,排他锁只允许一个事务读取和修改被锁定的数据。

锁会带来什么问题

给数据加锁可能会引起活锁和死锁的问题。

活锁

如果事务T1封锁了数据对象R,事务T2也请求封锁R,于是T2等待。T3也请求封锁R,当T1释放了R上的锁后系统首先批准了T3的请求,于是T2仍然等待。然后T4也请求封锁R,当T3释放了R上的锁后系统又批准了T4的请求,于是T2仍然等待……

在这个过程中,T2可能会永远在等待,这就是活锁。

避免活锁的简单方法就是采用先来先到的策略,当多个事务请求封锁同一数据对象的时候,封锁子系统按请求封锁的先后次序对事务进行排队,数据对象上的锁一旦释放,就批准申请队列中第一个事务获得锁。

死锁

如果事务T1封锁了数据R1,事务T2封锁了数据R2,然后T1请求封锁R2,由于R2已经被T2封锁,所以T1等待T2释放R2上的锁;接着T2也请求封锁R1,由于R1已经被T1封锁,所以T2等待T1释放R1上的锁。

这样就出现了两个事务互相等待的局面,这两个事务永远也不能结束,形成了死锁。

数据库一般允许发生死锁,并采用一定手段定期诊断系统中有无死锁,若有则解除之。一般采用超时法或事务等待图法来诊断死锁。

  • 超时法

如果一个事务等待的时间超过了规定的时限,就认为发生了死锁。超时法实现简单,但有可能发生误判死锁,事务因为其他原因使等待时间超过了时限导致被系统误认为发生了死锁;如果时限设置得太长,也可能无法及时发现死锁。

  • 事务等待图法

事物等待图是一个有向图G=(T,U)。T为结点的集合,每个结点表示正运行的事务;U为边的集合,每条边表示事务等待的情况。若T1等待T2,则T1、T2之间画一条有向边,从T1指向T2。

事务等待图动态地反映了所有事务的等待情况,并发控制子系统周期性地生成事务等待图,并进行检测。如果发现图中存在回路,则表示系统中发生了死锁。

死锁的情况可以多种多样,如下图所示,在大回路中又有小回路。

事物等待图

如何解除死锁

当DBMS检测到系统中存在死锁,就要设法解除。通常采用的做法是选择一个处理死锁代价最小的事务,将其撤销,释放此事务持有的锁,使其他事务得以运行下去。解除死锁之后,被撤销的事务所执行的数据操作必须加以恢复。

锁的粒度

锁的粒度就是锁的作用范围,一般分为行级锁、表级锁。行级锁锁定记录行,表级锁锁定整个表。

行级锁

系统开销大,加锁慢,锁定粒度最小,会发生死锁,但是发生冲突的概率最低,并发性最高

表级锁

系统开销小,加锁快,锁定粒度最大,不会发生死锁,但是发生冲突的概率最高,并发性最低

总结

  • 共享锁只用于表级,排它锁用于行级或表级。
  • 加了共享锁的对象,可以继续加共享锁,不能再加排他锁。
  • 加了排他锁的对象,不能再加任何锁。

悲观锁(Pessimistic Lock)

顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。

悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

乐观锁(Optimistic Lock)

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

乐观锁适用于多读的应用类型,即冲突真的很少发生的时候,这样可以提高吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒会降低性能,所以这种情况下用悲观锁就比较合适。

乐观锁与悲观锁不同的是,它是一种逻辑上的锁,而不需要数据库提供锁机制来支持,它需要我们自己在程序中实现。乐观锁的实现,一般是通过数据版本version或者时间戳来实现。

数据版本version(版本戳)

在需要乐观锁的表中添加一个version字段,每修改一次数据就将version+1。每次对数据进行操作时先读出当前的数据版本,如果要对数据进行修改就先判断此时的version是否与之前查询到的version一致,如果version一样说明在这期间没有其他人修改这条数据,则可以执行此次更新操作并更新版本号;如果version不一致,则意味着冲突,不执行此次更新。

代码实现例子:

查询当前商品信息:
select name,num,version from products where id = #{id};

执行更新商品操作:
update products set num = num - 1, version = version + 1 where id = #{id} and version = #{version};

时间戳timestamp

和version类似,每操作一次数据就修改时间戳;在进行更新操作时需要先判断当前时间戳是否与之前查询到的时间戳一致。

总结

通常情况下,写操作较少时,使用乐观锁,写操作较多时,使用悲观锁。除了自己手动实现乐观锁之外,有的持久层框架已经封装好了乐观锁的实现。比如Hibernate就提供了以数据版本实现的乐观锁机制。