彻底搞定欢乐锁与悲观锁

本文并未全部原创,感觉网络上的知识比较混乱,故自己整理了一下。

乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制采用的技术手段,是由人们定义出来的概念。可以认为是一种思想。

针对不同的业务情景,应该选用不同的并发控制方式。所以,不要把乐观锁和悲观锁狭义的理解为DBMS(数据库管理)中的概念,更不要与数据库中提供的锁机制(行锁、表锁、共享锁、排他锁)混为一谈。

首先了解下数据库锁的概念,才能更好的理解乐观锁与悲观锁。

数据库锁的概念

共享锁(S锁)

如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁,获取共享锁的事务只能读取数据,不能修改数据。

排他锁(X锁)

如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的封锁。获取排他锁的事务既能读数据,又能修改数据。

 

例1:

T1: select * from table (执行1小时之久);

T2: update table set column1 =’hello’;

过程:

T1: 运行,加共享锁

T2: 运行

只有T1运行完毕释放锁之后T2才能运行

T2之所以需要等,是因为T2在执行update钱,试图对table表加了一个排他锁,而数据库规定同一资源上不能同时存在共享锁和排他锁。所以T2必须等待T1释放了共享锁,才能加上排他锁,然后执行Update语句

 

2:

T1: select * from table1

T2: select * from table1

过程:

在这里,T2不用等待T1执行完毕,而是马上执行。

分析:

两个共享锁都是同时存在同一个资源上,这被称之为共享锁与共享锁兼容。这意味着共享锁不组织其它session同时读取资源。但组织其它session update;

 

3:

T1: select * from table1

T2: select * from table1

T3: update table set column1 =’hello’;

 

分析:

这里T2不需要等待T1完成之后运行,而T3需要等待T1、T2都完成之后才能运行,因为T3必须等T1、T2释放共享锁才能进行加排他锁执行update。

 

死锁的产生

T1:begin tran

Select * from table (holdlock)--共享锁,直到事务结束后才释放

Update table set column1=’hello’

T2:begin tran

Select * from table (holdlock)

Update table set column1=’hello’

分析:

假设T1,T2同时到达select,T1对table加共享锁,T2也加共享锁,当T1的select执行完,准备执行update时候,由于T2的共享锁还没有释放,必须等table上的其他共享锁释放之后才能进行update,但因为holdlock这样的共享锁只有等事务结束后才能释放,所以T2的共享锁不释放,而导致T1一直在等。这样,死锁就产生了。

 

5:

T1:

Begin tran

Update table set column1 =’hello’ where id = 10

 

T2:

Begin tran

Update table set column1 =’hello’ where id = 20

 

分析:

这种情况也会产生死锁,但是既要看情况。如果id是主键上面有索引,那么T1一下就找到id=10的这条记录,人后对该条记录加排他锁。T2同样,也是一下子通过索引定位到记录id=20的这条记录,对该条记录加排他锁,那么T1和T2之间个更新各的,互不影响。

如果id是普通的一列,没有索引,那么当T1对id=10这条加排他锁之后,T2为了找到id=20,需要对全表扫面,那么久会对预先对表加上了共享锁或者更新锁或者排他锁(依赖于数据库执行策略和方式,比如第一次执行和第二次执行,数据库的执行策略就不通)。但是因为T1已经为一挑记录加了排他锁,导致T2的全表扫描进行不下去了,就导致T2一直等待。

 

死锁如何解决呢?

6:

T1:begin tran

Select * from table (xlock)--直接对表加排他锁

Update table set column1=’hello’

T2:begin tran

Select * from table (xlock)

Update table set column1=’world’

 

分析:

因为排他锁既可以查询也可以更新,所以T1运行后,T2开始运行,发现table表已经被T1加上了排他锁,就需要等待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、4......需要查询table表中的数据时,并不会因为T1的select在执行就被阻塞,正常查询。

 

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

排他锁与更新锁是不兼容的,它们不能同时加在同一子资源上。

 

意向锁

比如一个屋子里,门口有一个标识,标识说明了屋子里有人被锁住了。另一个人想知道屋子里有没有人被锁,不用进屋里来看,直接看门口标识就行了。

当一个表中的某一行被加上排他锁后,该表就不能被加表锁,数据库如何判断该表能不能加表锁?一种方式是逐条判断,是否加上排他锁,另一种方式是直接检查表本身时候有意向锁。

12:

T1: begin tran

   select * from table (xlock) where id=10  --意思是对id=10这一行强加排他锁

T2: begin tran

   select * from table (tablock)     --意思是要加表级锁

假设T1先执行,当T2执行时,欲加表锁,为了判断时候可以加锁,数据库系统要逐条判断是否有排他锁,如果发现其中有排他锁了,就不允许加表锁了。

实际上数据库不是这样操作的,当T1执行时候,系统对表id=10这一样加了排他锁,同时还偷偷的为整个表加了意向排他锁,当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)

DDL语句都会加Sch-M锁

DDl:数据定义语言的缩写,就是对数据库内部的对象进行创建、删除、修改等操作的语言。它和DML语句的最大区别是DML只是对表内部数据操作,而不涉及表的定义、结构的修改,更不会涉及其他对象。DDL语句更多地由数据库管理员(DBA)使用。

该锁不允许任何其它session连接该表。连都连不了这个表了,当然更不用说想对该表执行什么sql语句了。

15:

jdbc向数据库发送了一条新的sql语句,数据库要先对之进行编译,在编译期间,也会加锁,称之为:Schema stability (Sch-S) locks

select * from tableA

编译这条语句过程中,其它session可以对表tableA做任何操作(update,delete,加排他锁等等),但不能做DDL(比如alter table)操作。

 

何时加锁

可以通过hint手工强行指定,但大多数由数据库系统自动决定。

16:

T1: begin tran

       update table set column1='hello' where id=1

T2: select * from table where id=1 --为指定隔离级别,则使用系统默认隔离级别,它不允许脏读

 

如果事物级别不设为脏读,则:

1) T1执行,数据库自动加排他锁

2) T2执行,数据库发现事物隔离级别不允许脏读,便准备为此次select过程加共享锁,但发现加不上,因为已经有排他锁了,所以就等啊等。直到T1执行完,释放了排他锁,T2才加上了共享锁,然后开始读....

 

锁的粒度

锁的粒度就是指锁的生效范围,如:行锁、页锁、整表锁。锁的粒度同样可以有数据库管理,也可以通过hint来管理。

17:

T1: select * from table (paglock)

T2: update table set column1='hello' where id>10

T1执行后,对第一页加锁,读完第一页之后释放锁在对第二页加锁,假设10记录签好是第一页最后一条,那么,T1执行第一页查询时,并不会阻塞T2更新。

 

18:

T1: select * from table (rowlock)

T2: update table set column1='hello' where id=10

T1执行时,对每行加共享锁,读取,然后释放,再对下一行加锁;T2执行时,会对id=10的那一行试图加锁,只要该行没有被T1加上行锁,T2就可以顺利执行update操作。

 

19:

T1:    select * from table (tablock)

T2:    update table set column1='hello' where id = 10

T1执行,对整个表加共享锁. T1必须完全查询完,T2才可以允许加锁,并开始更新。

锁与事务隔离级别的优先级

手工指定的锁优先。

20:

T1: GO

   SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

   GO

   BEGIN TRANSACTION

   SELECT * FROM table (NOLOCK)

   GO

T2: update table set column1='hello' where id=10

T1是事物隔离级别为最高级,串行锁,数据库系统本应对后面的select语句自动加表级锁,但因为手工指定了NOLOCK,所以该select语句不会加任何锁,所以T2也就不会有任何阻塞。

 

锁的超时等待

26:

SET LOCK_TIMEOUT 4000 用来设置锁等待时间,单位是毫秒,4000意味着等待

4秒可以用select @@LOCK_TIMEOUT查看当前session的锁超时设置。-1 意味着

永远等待。

T1: begin tran

    udpate table set column1='hello' where id = 10

T2: set lock_timeout 4000

select * from table wehre id = 10

T2执行时,会等待T1释放排他锁,等了4分钟,如果T1还没有释放,T2就会抛出异常:Lock request time out period exceeded.

 

 

悲观锁

在整个事务过程中,将数据处于锁定状态。只有当这个事务把锁释放,其他事务才能执行与该锁冲突的操作。

悲观锁的流程:

在对于任意记录进行修改前,都尝试为该条记录加上排他锁,如果加锁失败,说明该记录正在被修改,需要等待或者抛出异常,具体有由开发者根据实际需要决定。

如果成功,那么就可以对记录修改,事务完毕后就会解锁,期间如果有其他对该记录的修改或加排他锁的操作,都会等待解锁或抛出异常。

对于Mysql innoDB中使用悲观锁

使用悲观锁,必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当执行一个更新操作后,Mysql会立即将结果提交。set autocommit 0;

使用场景

商品goods表中有一个字段status,status为1代表商品没有被下单,status为2代表商品已经被下单,如果我们对某个商品下单时,必须确保商品status为1才可以下单。假设商品id为1

如果不采用锁,那么操作方法如下:

--1.查出商品信息

Select status from t_goods where id = 1;

--2根据商品信息生成订单

Inset into t_orders(id,goods_id) values(null,1);

--3.修改商品status为2

Update t_goods set status =2

上面的这种场景在高并发访问的情况下很有可能出现问题。

前面说,只有goods的status为1才能对该商品下单。在第一步操作中,查出商品status为1,但是当我们执行第三步update操作的时候,有可能出现其他人先一步把商品status修改为2了,但是我们并不知道数据已经被修改了,这样就导致同一个商品被下单2次,导致数据不一致,这种方式是不安全的。

使用悲观锁来实现

使用悲观锁的原理就是当我们查询出goods信息的时候就把当前数据加锁,直到我们修改完毕后再释放锁,那么在这个过程中,因为goods被锁定了,就不会出现第三者对其进行修改。

首先,设置autocommit = 0;

--开启事务

Begin;/begin work;/start transaction;(三者选一即可)

--查出商品信息;

Select status from t_goods where id = 1 for update;

--根据商品信息生成订单

Insert into t_orders(id,goods_id)values(null,1);

--修改商品status为2

Update t_goods set status = 2;

--提交事务

Commit;/commit work;

注意:上面的begin/commit为事务的开始和结束,因为在之前我们关闭了mysql的autocommit,所以需要手动控制事务提交。

与普通查询不同的是,我们使用了select ...for update的方式,这样就通过数据库实现了悲观锁。这时在t_goods表中。Id =1的那条记录就被锁定,其他事务必须等本次事务提交之后才能执行。这样我们就可以保证之前的数据不会被其他事务修改。

在事务中,只用SELECT ... FOR UPDATE(加排他锁)或SELECT ... LOCK IN SHARE MODE(加共享锁)操作同一组数据时会等待其他事务结束后才执行。对于一般的select...不收影响。比如:select status form goods where id = 1 for update;后,在另一个事务中如果再次执行select status from goods where id =1 for update 则第二个事务会一直等待第一个事务提交。此时第二个查询处于阻塞的状态,但如果在第二个事务中执行的是select status from goods where id = 1,则能正常查询数据,不受第一个事务的影响。

补充:mysql的select for update的row lock 与 table lock

上面说,使用select ... for update 会把数据给锁住,不过我们需要注意一些锁的级别,MySql innoDB 默认 Row-Level lock,所以只有明确的指定主键/索引,Mysql才会执行 Row lock(锁住被选取的数据),否则Mysql 会执行 Table Lock (将整个表给锁住)。

优点与不足:悲观锁实际上是“先加锁在访问”的保守策略,为数据处理的安全提供了保证,但是在效率方面,处理加锁机制会让数据库产生额外的开销,并且增加了死锁的可能性。另外,在只读型事务中没必要使用锁,这样只能增加系统的负载,降低了并发性。

 

乐观锁

相对于悲观锁而言,乐观锁假设认为一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突进行检测,如果发现冲突了,返回错误信息,让用户去处理。实现乐观锁有一下两种方式:

1. 使用数据库版本(version)记录的机制来实现,何为数据库版本?及为增加一个版本标识,一般是通过数据库表增加一个version字段来实现,当读取数据时,将version一并带出,数据每更新一次就对version+1,当我们提交更新的时候,判断version是否是与取出来的version值一致,一致则予以更新,不一致则认为过期数据。

2. 使用时间戳来标志版本,跟version类似,也是在更新的时候,判断时间戳是否与读出来的时间戳是否一致,一致则予以更新,否则版本冲突

使用举例:

--查询出商品信息

Select status version from goods where id = #id;

--根据商品信息生成订单

--修改商品status为2

Update goods set sttatus = 2 version = version +1 where id = #id and version = #version;

优点与不足

乐观锁假设认为不会造成冲突,只有在提交的时候才去锁定,所以不会产生任何锁和死锁。但是如果直接这么做,还是有可能遇到不同预期的结果,例如aba问题(aba:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化)。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值