数据库支持锁的种类

数据库支持锁的种类

一、引入

  • 数据库上加锁是为了保证数据的一致性。
  • 编程语言中也有锁的概念。其实,只要有并发的地方,就有锁的用武之地
  • 数据库的锁五花八门,都是怎样分类的呢?

二、锁的分类

在这里插入图片描述

1.按照锁粒度进行划分

  • 从锁定对象的粒度大小来对锁进行划分,分别为行锁、页锁和表锁。
  • 行锁就是按照行的粒度对数据进行锁定。锁定力度小,发生锁冲突概率低,可以实现的并发度高,但是对于锁的开销大,加锁会比较慢,容易出现死锁情况。
  • 页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
  • 表锁就是对数据表进行锁定,锁定粒度很大,同时发生锁冲突的概率也会较高,数据访问的并发度低。不过好处在于对锁的使用开销小,加锁会很快
  • 行锁、页锁和表锁是相对常见的三种锁,除此以外还可以在区和数据库的粒度上锁定数据,对应区锁和数据库锁。
  • 不同的数据库和存储引擎支持的锁粒度不同,InnoDB 和 Oracle 支持行锁和表锁。而 MyISAM 只支持表锁,MySQL 中的 BDB 存储引擎支持页锁和表锁。SQL Server 可以同时支持行锁、页锁和表锁。

2.从数据库管理的角度对锁进行划分

  • 共享锁、排它锁和意向锁
  • 共享锁也叫读锁或 S 锁,共享锁锁定的资源可以被其他用户读取,但不能修改。在进行SELECT的时候,可将对象进行共享锁锁定,当数据读取完毕之后,就会释放共享锁,这样就可以保证数据在读取时不被修改。
  • 排它锁也叫独占锁、写锁或 X 锁。排它锁锁定的数据只允许进行锁定操作的事务使用,其他事务无法对已锁定的数据进行查询或修改

在对数据进行更新的时候,也就是INSERT、DELETE或者UPDATE的时候,数据库也会自动使用排它锁,防止其他事务对该数据行进行操作。

  • 当要获取某个数据表的排它锁的时候,需要先看下这张数据表有没有上了排它锁如果这个数据表中的某个数据行被上了行锁,就无法获取排它锁。这时需要对数据表中的行逐一排查,检查是否有行锁,如果没有,才可以获取这张数据表的排它锁
  • 这个过程未免太麻烦,所以就有了意向锁。
  • 意向锁(Intent Lock),简单来说就是给更大一级别的空间示意里面是否已经上过锁
  • 如果给某一行数据加上了排它锁,数据库会自动给更大一级的空间,比如数据页或数据表加上意向锁告诉其他人这个数据页或数据表已经有人上过排它锁了,这样当其他人想要获取数据表排它锁的时候,只需要了解是否有人已经获取了这个数据表的意向排他锁即可。
  • 如果事务想要获得数据表中某些记录的共享锁,就需要在数据表上添加意向共享锁。同理,事务想要获得数据表中某些记录的排他锁,就需要在数据表上添加意向排他锁。这时,意向锁会告诉其他事务已经有人锁定了表中的某些记录,不能对整个表进行全表扫描。

为什么共享锁会发生死锁的情况

在客户端 1 获取某数据行共享锁的同时,另一个客户端 2也获取了该数据行的共享锁,这时任何一个客户端都没法对这个数据进行更新,因为共享锁会阻止其他事务对数据的更新,当某个客户端想要对锁定的数据进行更新的时候,就出现了死锁的情况。

当死锁发生的时候,就需要一个事务进行回滚,另一个事务获取锁完成事务,然后将锁释放掉,这很像交通堵塞时候的解决方案。

3.从程序员的角度对进行划分

乐观锁和悲观锁详解

  • 如果从程序员的视角来看锁的话,可以将锁分成乐观锁和悲观锁,从名字中也可以看出这两种锁是两种看待数据并发的思维方式。
  • 乐观锁(Optimistic Locking)认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者时间戳机制实现。

1.乐观锁的版本号机制

表中设计一个版本字段 version,第一次读的时候,同时会获取 version
字段的取值。然后对数据进行更新或删除操作时,会执行UPDATE ... SET version=version+1 WHERE version=version。此时如果已经有事务对这条数据进行了更改,修改就不会成功。

2.乐观锁的时间戳机制

时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突

乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新。

  • 悲观锁(Pessimistic Locking)也是指一种锁的思想,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制(表锁,行锁,读锁,写锁等)来实现,从而保证数据操作的排它性
  • 从这两种锁的设计思想中,可以看出乐观锁和悲观锁的适用场景:
  • 乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
  • 悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止读 - 写和写 - 写的冲突。
  • 如何避免死锁?
  • 1.如果事务涉及多个表,操作比较复杂,那么可以尽量一次锁定所有的资源,而不是逐步来获取,这样可以减少死锁发生的概率;
  • 2.如果事务需要更新数据表中的大部分数据,数据表又比较大,这时可以采用锁升级的方式,比如将行级锁升级为表级锁,从而减少死锁产生的概率;
  • 3.不同事务并发读写多张数据表可以约定访问表的顺序,采用相同的顺序降低死锁发生的概率。

三、案例

规定这里用T1代表一个数据库执行请求,T2代表另一个请求,也可以理解为T1为一个线程,T2 为另一个线程,T3,T4以此类推

  • 共享锁
  • 例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也会因此而等待。在大并发
情况下,让大家等待显得性能就太友好了,所以,这里引入了更新锁。

  • 更新锁(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,发现已有更新锁,则等待,
后面步骤就跟第一种情况一样了。

这个例子是说明:排他锁与更新锁是不兼容的,它们不能同时加在同一子资源上。
  • 排他锁(独占锁,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.

  • 意向锁(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同时对行家排他锁、
对页加意向排他锁、对表加意向排他锁。
  • 计划锁(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)操作。

猜你喜欢

转载自blog.csdn.net/wolfGuiDao/article/details/107577466
今日推荐