关于锁的一系列

前言

并发控制: 事务和锁的存在都是为了更好的解决并发访问造成的数据不一致性的的问题

一、乐观锁与悲观锁

** 乐观锁和悲观锁都是为了解决并发控制问题, 乐观锁可以认为是一种在最后提交的时候检测冲突的手段,而悲观锁则是一种避免冲突的手段。 **

1.1乐观锁

1.什么是乐观锁
是应用系统层面和数据的业务逻辑层次上的(实际上并没有加锁,只不过大家一直这样叫而已),利用程序处理并发**,它假定当某一个用户去读取某一个数据的时候,其他的用户不会来访问修改这个数据,**但是在最后进行事务的提交的时候会进行版本的检查,以判断在该用户的操作过程中,没有其他用户修改了这个数据。开销比较小

2.实现方式
乐观锁的实现大部分都是基于版本控制实现的, 除此之外,还可以通过时间戳的方式,通过提前读取,事后对比的方式实现 。譬如mysql里添加版本version字段来控制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u6pKteOM-1617104572859)(8.png)]

3.乐观锁的优势和劣势

1.优势:如果数据库记录始终处于悲观锁加锁状态,可以想见,如果面对几百上千个并发,那么要不断的加锁减锁,而且用户等待的时间会非常的长, 乐观锁机制避免了长事务中的数据库加锁解锁开销,大大提升了大并发量下的系统整体性能表现所以如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以建议就要选择乐观锁定的方法, 而如果并发量不大,完全可以使用悲观锁定的方法。乐观锁也适合于读比较多的场景。
2.劣势: 但是乐观锁也存在着问题,只能在提交数据时才发现业务事务将要失败,如果系统的冲突非常的多,而且一旦冲突就要因为重新计算提交而造成较大的代价的话,乐观锁也会带来很大的问题,在某些情况下,发现失败太迟的代价会非常的大。而且乐观锁也无法解决脏读的问题

1.2 悲观锁

1.什么是悲观锁?
完全依赖于数据库锁的机制实现的,在数据库中可以使用Repeatable Read的隔离级别(可重复读)来实现悲观锁。它认为当某一用户读取某一数据的时候,其他用户也会对该数据进行访问,所以在读取的时候就对数据进行加锁, 在该用户读取数据的期间,其他任何用户都不能来修改该数据,但是其他用户是可以读取该数据的, 只有当自己读取完毕才释放锁。

2.悲观锁的优势和劣势
劣势:开销较大,而且加锁时间较长,对于并发的访问性支持不好。
优势 : 能避免冲突的发生

1.3 如何实现乐观锁/悲观锁

我们经常会在访问数据库的时候用到锁,怎么实现乐观锁和悲观锁呢?
以hibernate为例,可以通过为记录添加版本时间戳字段来实现乐观锁,一旦发现出现冲突了,修改失败就要通过事务进行回滚操作。可以用session.Lock()锁定对象来实现悲观锁(本质上就是执行了SELECT * FROM t FOR UPDATE语句)

1.4 乐观锁和悲观锁选择标准

乐观锁和悲观所各有优缺点,在乐观锁和悲观锁之间进行选择的标准是

  • 发生冲突的频率与严重性。
  • 如果冲突很少,或者冲突的后果不会很严重,那么通常情况下应该选择乐观锁,因为它能得到更好的并发性,而且更容易实现。
  • 如果冲突太多或者冲突的结果对于用户来说痛苦的,那么就需要使用悲观锁,它能避免冲突的发生。 如果要求能够支持高并发,那么乐观锁。
    其实使用乐观锁 高并发==高冲突, 看看你怎么衡量了。

但是现在大多数源代码开发者更倾向于使用乐观锁策略

二、共享锁和排它锁

共享锁和排它锁是具体的锁,是数据库机制上的锁

2.1 共享锁(读锁)

共享锁(读锁) 在同一个时间段内,多个用户可以读取同一个资源,读取的过程中数据不会发生任何变化。读锁之间是相互不阻塞的, 多个用户可以同时读,但是不能允许有人修改, 任何事务都不允许获得数据上的排它锁,直到数据上释放掉所有的共享锁

2.2 排它锁(写锁)

排它锁(写锁) 在任何时候只能有一个用户写入资源,当进行写锁时会阻塞其他的读锁或者写锁操作,只能由这一个用户来写,其他用户既不能读也不能写。

三、加锁的粒度

加锁会有粒度问题,从粒度上从大到小可以划分为

3.1 表锁

开销较小,一旦有用户访问这个表就会加锁,其他用户就不能对这个表操作了,应用程序的访问请求遇到锁等待的可能性比较高。

3.2 页锁

是MySQL中比较独特的一种锁定级别,锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。

3.3 行锁

开销较大,能具体的锁定到表中的某一行数据,但是能更好的支持并发处理, 会发生死锁

四、加锁协议

5.1 一次封锁协议

因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。

5.2 两段锁协议

将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)

1.加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁(只有当前数据无共享锁,无排它锁之后才能获得),其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
2.解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作
事务提交时(commit) 和事务回滚时(rollback)会自动的同时释放该事务所加的insert、update、delete对应的锁。
这种方式虽然无法避免死锁,但是两段锁协议可以保证事务的并发调度是串行化(串行化很重要,尤其是在数据恢复和备份的时候)的。

五、死锁

5.1 出现死锁的原因:
  1. 系统资源不足

  2. 进程运行推进的顺序不当

  3. 资源分配不当

    产生死锁的四个必要条件

    • 互斥条件: 一个资源只能被一个进程使用
    • 请求和保持条件:进行获得一定资源,又对其他资源发起了请求,但是其他资源被其他线程占用,请求阻塞,但是也不会释放自己占用的资源。
    • 不可剥夺条件: 指进程所获得的资源,不可能被其他进程剥夺,只能自己释放
    • 环路等待条件: 进程发生死锁,必然存在着进程-资源之间的环形链
      处理死锁的方法: 预防,避免,检查,解除死锁

数据库也会发生死锁的现象,数据库系统实现了各种死锁检测和死锁超时机制来解除死锁,锁监视器进行死锁检测,MySQL的InnoDB处理死锁的方式是 将持有最少行级排它锁的事务进行回滚,相对比较简单的死锁回滚办法

5.2 如何避免死锁?

避免死锁的核心思想是:

系统对进程发出每一个资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配.这是一种保证系统不进入不安全或者死锁状态的动态策略。 什么是不安全的状态?系统能按某种进程推进顺序( P1, P2, …, Pn),为每个进程Pi分配其所需资源,直至满足每个进程对资源的最大需求,使每个进程都可顺序地完成。此时称 P1, P2, …, Pn 为安全序列。如果系统无法找到一个安全序列,则称系统处于不安全状态。
其实第一和第二是预防死锁的方式,分别对应着的是破坏循环等待条件,和破坏不可剥夺条件。

第一: 加锁顺序: 对所有的资源加上序号,确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生,比如有资源 A, B,规定所有的线程只能按照A–B的方式获取资源, 这样就不会发生 线程1持有A,请求B,线程2持有B请求A的死锁情况发生了
第二: 获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求,同时放弃掉自己已经成功获得的所有资源的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行。
第三:死锁的提前检测, 很出名的就是银行家算法。 每当一个线程获得了锁,会存储在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中,当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。

银行家算法:

思想: 当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程已占用的资源数本次申请的资源数之和是否超过了该进程对资源的最大需求量。若超过则拒绝分配资源,若没有超过则再测试系统现存的资源能否满足该进程尚需的最大资源量,若能满足则按当前的申请量分配资源,否则也要推迟分配

5.3 如何预防死锁?

主要是通过设置某些外部条件去破坏死锁产生的四个必要条件中的一个或者几个

  • 破坏互斥条件,一般不采用,因为资源的互斥性这个特性有时候是我们所需要的;
  • 破坏请求和保持条件:可以一次性为一个进程或者线程分配它所需要的全部资源,这样在后面就不会发起请求资源的情况,但是这样资源的效率利用率很低;
  • 破坏不可剥夺条件: 当一个已保持了某些不可剥夺资源的进程,请求新的资源而得不到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请,但是释放已获得的资源可能造成前一阶段工作的失效,反复地申请和释放资源会增加系统开销,降低系统吞吐量
  • 破坏循环等待条件: 可釆用顺序资源分配法。首先给系统中的资源编号,规定每个进程,必须按编号递增的顺序请求资源,同类资源一次申请完。也就是说,只要进程提出申请分配资源Ri,则该进程在以后的资源申请中,只能申请编号大于Ri的资源。
    但是这样的话,编号必须相对稳定,这就限制了新类型设备的增加;尽管在为资源编号时已考虑到大多数作业实际使用这些资源的顺序,但也经常会发生作业使甩资源的顺序与系统规定顺序不同的情况,造成资源的浪费;此外,这种按规定次序申请资源的方法,也必然会给用户的编程带来麻烦
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lr-fcc

你的鼓励是我的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值