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

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

针对不同的业务情景,应该选用不同的并发控制方式。所以,不要把乐观锁和悲观锁狭义的理解为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值是否发生过变化)。

彻底搞定欢乐锁与悲观锁相关推荐

  1. 一文搞懂 mysql 中的共享锁、排他锁、悲观锁、乐观锁及使用场景

    目录 一.常见锁类型 二.Mysql引擎介绍 三.常用引擎间的区别 四.共享锁与排他锁 五.排他锁的实际应用 六.共享锁的实际应用 七.死锁的发生 八.另一种发生死锁的情景 九.死锁的解决方式 十.意 ...

  2. **Java有哪些悲观锁的实现_面试4连问:乐观锁与悲观锁的概念、实现方式、场景、优缺点?...

    推荐阅读: 数据库面试4连问:分库分表,中间件,优缺点,如何拆分? 终极手撕之架构大全:分布式+框架+微服务+性能优化,够不够? 消息队列面试,你能顶得住面试官这波10大连环炮的攻势吗? 01 乐观锁 ...

  3. 一篇文章带你解析,乐观锁与悲观锁的优缺点

    乐观锁与悲观锁 概述 乐观锁 总是假设最好的情况,每次去读数据的时候都认为别人不会修改,所以不会上锁, 但是在更新的时候会判断一下在此期间有没有其他线程更新该数据, 可以使用版本号机制和CAS算法实现 ...

  4. 并发编程中常见的锁机制:乐观锁、悲观锁、CAS、自旋锁、互斥锁、读写锁

    文章目录 乐观锁 VS 悲观锁 悲观锁 乐观锁 CAS CAS机制 ABA问题 CAS的优缺点 互斥锁 VS 自旋锁 互斥锁 自旋锁 对比及应用场景 读写锁 实现方式 读写锁 VS 互斥锁 乐观锁 V ...

  5. 独占锁、共享锁、更新锁,乐观锁、悲观锁

    转载自   独占锁.共享锁.更新锁,乐观锁.悲观锁 1.锁的两种分类方式 (1)从数据库系统的角度来看,锁分为以下三种类型: 独占锁(Exclusive Lock)       独占锁锁定的资源只允许 ...

  6. 悲观锁和乐观锁_面试必备之乐观锁与悲观锁

    何谓悲观锁与乐观锁 乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展.这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人. 大家可以点 ...

  7. mysql原子性和乐观锁_乐观锁 VS 悲观锁

    1.乐观锁 VS 悲观锁 乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度.在Java和数据库中都有此概念对应的实际应用. 1.1 概念悲观锁:对于同一个数据的并发操作,悲观锁认为自己在 ...

  8. 乐观锁和悲观锁的含义-实现方式-应用场景

    何谓悲观锁与乐观锁 乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展.这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人. 悲观锁 总 ...

  9. 阿里面试,问了我乐观锁、悲观锁、AQS、sync和Lock,这个回答让我拿了offer

    前言 关于线程安全一提到可能就是加锁,在面试中也是面试官百问不厌的考察点,往往能看出面试者的基本功和是否对线程安全有自己的思考. 那锁本身是怎么去实现的呢?又有哪些加锁的方式呢? 我今天就简单聊一下乐 ...

最新文章

  1. 如何在centos7下tomcat中安装https
  2. JDK1.8 十大新特性详解
  3. redis服务的部署
  4. SAP Commerce Cloud Storefront 框架选型:Accelerator 还是 Spartacus?
  5. 标自然段的序号格式_你可能还不会基本的公文格式
  6. C++primer 9.5.5节练习
  7. python+freetype+opencv 图片中文(汉字)显示 详细图文教程和项目完整源代码
  8. HDU2083 简易版之最短距离【最值】
  9. Eclipse中查看JDK类库的源代码
  10. 于仕琪老师的人脸检测库
  11. 瑞星序列号更换器———可更换瑞星2007、2008的杀毒软件和防火墙序列号
  12. 10种常用数据分析方法
  13. (附源码)spring boot小型仪器公司生产管理系统 毕业设计 031853
  14. 我的世界漆黑一片 看不见明天
  15. 公有云安全修炼之路,郭靖和周伯通带你走
  16. 33、Python第三方库安装和使用
  17. 微信小程序开发教程(破解版IDE 无内测资格也可使用)
  18. 数字图像处理报告:实验3 同态滤波、频域滤波、傅里叶变换性质、DCT变换性质
  19. 微信联盟链接不到服务器怎么,LOL微信绑定方法及无法登录处理方案推荐
  20. python快速入门系列_十五分钟快速入门系列:Python基础

热门文章

  1. OJ 1471 小鱼买水果
  2. Python人体肤色检测
  3. 作为一名 程序员,怎样写出一份漂亮的简历?让招聘者眼前一亮,充满激动?...
  4. 5-10 列车调度 (25分)
  5. Linux下电骡aMule Kademlia网络构建分析2
  6. 商品交易系统之---集合竞价与连续竞价区别
  7. matlab画图时无法显示中文(显示为框框)
  8. 图解各种悬挂系统优缺点
  9. 破解百度网盘 -- 使下载速度增加20倍
  10. 提逆否命题——小学生都会