悲观锁
本文并未全部原创,感觉网络上的知识比较混乱,故自己整理了一下。
乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制采用的技术手段,是由人们定义出来的概念。可以认为是一种思想。
针对不同的业务情景,应该选用不同的并发控制方式。所以,不要把乐观锁和悲观锁狭义的理解为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值是否发生过变化)。
相关阅读
对于苹果ID锁很多人了解都不是很清楚,而且很多人认为用爱思助手刷机可以刷掉ID锁,如果你的手机出现ID锁后,那你就必须输入Apple ID账
无锁队列的实现 | 酷壳 - CoolShell.cn无锁队列的实现
文章分享了经济型连锁酒店行业的相关数据的分析方法,希望能够对你有所帮助。随着“大旅游”、“大住宿”的概念兴起,酒店行业作为其
概述汽车差速器可以使同一驱动轴的左、右车轮实现以不同的转速转动,当汽车转弯行驶或在不平路面上行驶时,保证两侧驱动车轮作纯滚动
专题相关文章:从内存可见性看Volatile、原子变量和CAS算法多线程并发之CountDownLatch(闭锁)使用详解多线程并发之显示锁Lock与其