百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

一篇解释很好的数据库的乐观锁与悲观锁

wptr33 2025-05-21 16:53 30 浏览

数据库并发的三种场景

  • 数据库并发场景有三种,分别为:
    • 读-读:不存在任何问题,也不需要并发控制
    • 读-写:有隔离性问题,可能遇到脏读,幻读,不可重复读
    • 写-写:可能存更新丢失问题,比如第一类更新丢失,第二类更新丢失

乐观锁和悲观锁的澄清

  • 无论是悲观锁还是乐观锁,他们本质上不是数据库中具体的锁概念,而是我们定义出来,用来描述两种类别的锁的思想。所以有了设计的分类,我们就可以通过这个分类去对数据库中具体的锁进行分门别类;
  • 不过数据库中的乐观锁更倾向叫乐观并发控制(OCC),悲观锁叫悲观并发控制(PCC),还有区别于乐观悲观锁的一种控制叫MVCC,多版本并发控制
  • 也不要把乐观锁和悲观锁与数据库中的行锁,表锁,排他锁,共享锁混为一谈,他们并不是一个维度的东西;前者是一个锁思想,可以将后者根据是否进行趋近于乐观或悲观锁的思想进行分类
  • 乐观锁和悲观锁的概念不仅仅存在于数据库领域,可以说存在线程安全,存在并发的场景几乎都有乐观锁和悲观锁的适用场景,比如Java中也有乐观锁和悲观锁思想的具体实现;但不同领域的乐观和悲观锁的具体实现都不尽相同,要解决的问题也可能有所不一样

乐观锁和悲观锁是一种锁的设计思想,它不是一种具体的锁,它可以有很多具体的实现类。

悲观锁

在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法; 悲观锁指的是采用一种持悲观消极的态度,默认数据被外界访问时,必然会产生冲突,所以在数据处理的整个过程中都采用加锁的状态,保证同一时间,只有一个线程可以访问到数据,实现数据的排他性;通常,数据库的悲观锁是利用数据库本身提供的锁机制去实现的。

数据库的悲观并发控制可以解决读-写冲突和写-写冲突,指在用加锁的方式去解决。

悲观锁的实现

常情况下,数据库的悲观锁就是利用数据库本身提供的锁去实现的

  • 外界要访问某条数据,那它就要首先向数据库申请该数据的锁(某种锁)
  • 如果获得成功,那它就可以操作该数据,在它操作期间,其他客户端就无法再操作该数据了
  • 如果获得失败,则代表同一时间已有其他客户端获得了该锁,那就必须等待其他客户端释放锁

当然数据库提供了非常多的锁,每种数据库提供的锁也不尽然相同,所以具体情况就要看是什么锁了,比如行锁,表锁等

优点与缺点

优点:
适合在写多读少的并发环境中使用,虽然无法维持非常高的性能,但是在乐观锁无法提更好的性能前提下,可以做到数据的安全性
缺点:
加锁会增加系统开销,虽然能保证数据的安全,但数据处理吞吐量低,不适合在读书写少的场合下使用

悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数

乐观锁

在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法;乐观锁( Optimistic Locking ) 是相对悲观锁而言,乐观锁是假设认为即使在并发环境中,外界对数据的操作一般是不会造成冲突,所以并不会去加锁(所以乐观锁不是一把锁),而是在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回冲突信息,让用户决定如何去做下一步,比如说重试,直至成功为止;数据库的乐观锁,并不是利用数据库本身的锁去实现的,可能是利用某种实现逻辑去实现做到乐观锁的思想

数据库的乐观并发控制要解决的是数据库并发场景下的写-写冲突,指在用无锁的方式去解决

CAS思想

其实数据库乐观锁的具体实现几乎就跟Java中乐观锁采用的CAS算法思想是一致,所以我们可以从CAS算法中学习到数据库乐观锁的设计:

CAS指令全称为Compare and Swap,它是系统的指令集,整个CAS操作是一个原子操作,是不可分割的。从具体的描述上,我们可以这么看CAS操作:

CAS指令需要3个操作数,分别是内存位置V,旧的预期值A,和新值B。CAS指令执行时,当我们读取的内置位置V的现值等于旧预期值A时,处理器才会将新值B去更新内置位置V的值。否则它就不执行更新,但无论是否更新V的值,都会返回V的旧值。

我们通俗的放到代码层次上去理解i = 2; i++,就是说:

  • 首先线程1从内存位置V中读取到了值,保存并作为旧预期值A. (v = 2 ,a = 2)
  • 然后在因为i要进行++操作,系统会比较内存位置V的现值跟旧预期值A进行比较,既V =? A。
  • 如果相等,B = i++ = 3 ,新值B就会对内存位置V进行更新,所以内存位置V的值就变成了B的值,3
  • 如果不相等,则说明有其他的线程修改过了内存位置V的值,比如线程2在线程1修改i的值前就更新了i的值。,所以线程1会更新变量i失败。但线程不会挂起,而是返回失败状态,等待调用线程决定是否重试或其他操作。(通常会重试直到成功)

数据库层的乐观锁实现也类似代码层面的实现

数据库中乐观锁的实现

通常乐观锁的实现有两种,但它们的内在都是CAS思想的设计:

方式一:

使用数据版本(version)实现

这是乐观锁最常用的一种实现方式。什么是数据版本呢?就是在表中增添一个字段作为该记录的版本标识,比如叫version,每次对该记录的写操作都会让 version+ 1。

所以当我们读取了数据(包括version),做出更新,要提交的时候,就会拿取得的version去跟数据库中的version比较是否一致,如果一致则代表这个时间段,并没有其他的线程的也修改过这个数据,给予更新,同时version + 1;如果不一致,则代表在这个时间段,该记录以及被其他线程修改过了, 认为是过期数据,返回冲突信息,让用户决定下一步动作,比如重试(重新读取最新数据,再过更新)

update table set num = num + 1 , version = version + 1 where version = #{version} and id = #{id}

方式二:

使用时间戳(timestamp)实现

表中增加一个字段,名称无所谓,比如叫update_time, 字段类型使用时间戳(timestamp)

原理和方式一一致,也是在更新提交的时检查当前数据库中数据的时间戳和自己更新前取到的时间戳是否一致,如果一致则代表此刻没有冲突,可以提交更新,同时时间戳更新为当前时间,否则就是该时间段有其他线程也更新提交过,返回冲突信息,等待用户下一步动作。

update table set num = num + 1 ,update_time = unix_timestamp(now()) where id = #{id} and update_time = #{updateTime}


但是我们要注意的是,要实现乐观锁的思想的同时,我们必须要要保证CAS多个操作的原子性,即获取数据库数据的版本,拿数据库的数据版本与之前拿到的版本的比较,以及更新数据等这几个操作的执行必须是连贯执行,具有复合操作的原子性;所以如果是数据库的SQL,那么我们就要保证多个SQL操作处于同一个事务中。

优点与缺点

优点:

在读多写少的并发场景下,可以避免数据库加锁的开销,提高Dao层的响应性能

其实很多情况下,我们orm工具都有带有乐观锁的实现,所以这些方法不一定需要我们人为的去实现

缺点:

在写多读少的并发场景下,即在写操作竞争激烈的情况下,会导致CAS多次重试,冲突频率过高,导致开销比悲观锁更高

什么是MVCC?

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
mvcc - @百度百科

MVCCMySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。

什么是当前读和快照读?

什么是MySQL InnoDB下的当前读和快照读?

当前读

像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

快照读

像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

说白了快照读就是MVCC思想在MySQL的具体非阻塞读功能实现,整个MVCC多并发控制的目的就是为了实现读-写冲突不加锁,提高并发读写性能,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现。

总结

乐观锁和悲观锁的抉择

对乐观锁和悲观锁的抉择主要体现在写-写

在悲观锁和乐观锁的抉择中,我们可以从下面三个因素来考虑:

  • 响应速度: 如果Dao层需要非常高的响应速度,尤其是读多写少的场景下,那我们就可以采用乐观锁方案,降低数据库锁的开销,提供并发量
  • 冲突频率: 如果冲突频率非常高,那么我们就可以采用悲观锁,保证成功率;毕竟如果冲突频率大,乐观锁会需要多次重试才能成功,代价可能会大大增加
  • 重试代价: 如果重试代价大,比如说重试过程的代码执行非常耗时,那么此时我就不建议使用乐观锁了,还不如直接上悲观锁来了爽快

所以我们知道:

  • 在读多写少,CAS竞争没这么激烈的时候,我们可以采用乐观锁策略,降低数据库加锁的开销,提高数据库并发响应
  • 在写多读少的场景下,因为会产生大量的CAS竞争,且重试成本比较高的情况下,我们就不建议再采用乐观锁策略了,还是直接使用悲观锁的数据库加锁吧。

OCC,PCC,MVCC三者的关系

  • 悲观并发控制(PCC)是一种用来解决读-写冲突和写-写冲突的的加锁并发控制, 为每个操作都加锁,同一时间下,只有获得该锁的事务才能有权利对该数据进行操作,没有获得锁的事务只能等待其他事务释放锁;所以可以解决脏读,幻读,不可重复读,第一类更新丢失,第二类更新丢失的问题。
  • 乐观并发控制(OCC)是一种用来解决写-写冲突的无锁并发控制,认为事务间争用没有那么多,所以先进行修改,在提交事务前,检查一下事务开始后,有没有新提交改变,如果没有就提交,如果有就放弃并重试。乐观并发控制类似自旋锁。乐观并发控制适用于低数据争用,写冲突比较少的环境;无法解决脏读,幻读,不可重复读,但是可以解决更新丢失问题。
  • 多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 这样在读操作时就不用阻塞写操作,写操作也不用阻塞读操作;不仅可以提高并发性能,还可以解决脏读,幻读,不可重复读等事务问题。更新丢失问题除外。

总的来说,MVCC的出现就是数据库不满用悲观锁去解决读-写冲突问题,因性能不高而提出的解决方案,所以在数据库中,我们可以形成两个组合:

  • MVCC + 悲观锁

MVCC解决读写冲突,悲观锁解决写写冲突

  • MVCC + 乐观锁

MVCC解决读写冲突,乐观锁解决写写冲突

这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题。

引自:
https://blog.csdn.net/SnailMann/article/details/88388829

作者:SnailMann

相关推荐

oracle数据导入导出_oracle数据导入导出工具

关于oracle的数据导入导出,这个功能的使用场景,一般是换服务环境,把原先的oracle数据导入到另外一台oracle数据库,或者导出备份使用。只不过oracle的导入导出命令不好记忆,稍稍有点复杂...

继续学习Python中的while true/break语句

上次讲到if语句的用法,大家在微信公众号问了小编很多问题,那么小编在这几种解决一下,1.else和elif是子模块,不能单独使用2.一个if语句中可以包括很多个elif语句,但结尾只能有一个else解...

python continue和break的区别_python中break语句和continue语句的区别

python中循环语句经常会使用continue和break,那么这2者的区别是?continue是跳出本次循环,进行下一次循环;break是跳出整个循环;例如:...

简单学Python——关键字6——break和continue

Python退出循环,有break语句和continue语句两种实现方式。break语句和continue语句的区别:break语句作用是终止循环。continue语句作用是跳出本轮循环,继续下一次循...

2-1,0基础学Python之 break退出循环、 continue继续循环 多重循

用for循环或者while循环时,如果要在循环体内直接退出循环,可以使用break语句。比如计算1至100的整数和,我们用while来实现:sum=0x=1whileTrue...

Python 中 break 和 continue 傻傻分不清

大家好啊,我是大田。今天分享一下break和continue在代码中的执行效果是什么,进一步区分出二者的区别。一、continue例1:当小明3岁时不打印年龄,其余年龄正常循环打印。可以看...

python中的流程控制语句:continue、break 和 return使用方法

Python中,continue、break和return是控制流程的关键语句,用于在循环或函数中提前退出或跳过某些操作。它们的用途和区别如下:1.continue(跳过当前循环的剩余部分,进...

L017:continue和break - 教程文案

continue和break在Python中,continue和break是用于控制循环(如for和while)执行流程的关键字,它们的作用如下:1.continue:跳过当前迭代,...

作为前端开发者,你都经历过怎样的面试?

已经裸辞1个月了,最近开始投简历找工作,遇到各种各样的面试,今天分享一下。其实在职的时候也做过面试官,面试官时,感觉自己问的问题很难区分候选人的能力,最好的办法就是看看候选人的github上的代码仓库...

面试被问 const 是否不可变?这样回答才显功底

作为前端开发者,我在学习ES6特性时,总被const的"善变"搞得一头雾水——为什么用const声明的数组还能push元素?为什么基本类型赋值就会报错?直到翻遍MDN文档、对着内存图反...

2023金九银十必看前端面试题!2w字精品!

导文2023金九银十必看前端面试题!金九银十黄金期来了想要跳槽的小伙伴快来看啊CSS1.请解释CSS的盒模型是什么,并描述其组成部分。答案:CSS的盒模型是用于布局和定位元素的概念。它由内容区域...

前端面试总结_前端面试题整理

记得当时大二的时候,看到实验室的学长学姐忙于各种春招,有些收获了大厂offer,有些还在苦苦面试,其实那时候的心里还蛮忐忑的,不知道自己大三的时候会是什么样的一个水平,所以从19年的寒假放完,大二下学...

由浅入深,66条JavaScript面试知识点(七)

作者:JakeZhang转发链接:https://juejin.im/post/5ef8377f6fb9a07e693a6061目录由浅入深,66条JavaScript面试知识点(一)由浅入深,66...

2024前端面试真题之—VUE篇_前端面试题vue2020及答案

添加图片注释,不超过140字(可选)1.vue的生命周期有哪些及每个生命周期做了什么?beforeCreate是newVue()之后触发的第一个钩子,在当前阶段data、methods、com...

今年最常见的前端面试题,你会做几道?

在面试或招聘前端开发人员时,期望、现实和需求之间总是存在着巨大差距。面试其实是一个交流想法的地方,挑战人们的思考方式,并客观地分析给定的问题。可以通过面试了解人们如何做出决策,了解一个人对技术和解决问...