悲观锁和乐观锁

数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。


事务的概念

事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。


事务的例子

假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。


事务的详细说明

在介绍并发控制前,首先需要了解事务。数据库提供了增删改查等几种基础操作,用户可以灵活地组合这几种操作,实现复杂的语义。在很多场景下,用户希望一组操作可以做为一个整体一起生效,这就是事务。事务是数据库状态变更的基本单元,包含一个或多个操作(例如多条SQL语句)。经典的转账事务,就包括三个操作:(1)检查A账户余额是否足够。(2)如果足够,从A扣减100块。(3)B账户增加100块。

事务有个基本特性:这一组操作要么一起生效,要么都不生效,事务执行过程中如遇错误,已经执行的操作要全部撤回,这就是事务的原子性。如果失败发生后,部分生效的事务无法撤回,那数据库就进入了不一致状态,与真实世界的事实相左。例如转账事务从A账户扣款100块后失败了,B账户还未增加款项,如果A账户扣款操作未撤回,这个世界就莫名奇妙丢失了100块。原子性可以通过记日志(更改前的值)来实现,还有一些数据库将事务操作缓存在本地,如遇失败,直接丢弃缓存里的操作。

事务只要提交了,它的结果就不能改变了,即使遇到系统宕机,重启后数据库的状态与宕机前一致,这就是事务的持久性。数据只要存储非易失存储介质,宕机就不会导致数据丢失。因此数据库可以采用以下方法来保证持久性:(1)事务完成前,所有的更改都保证存储到磁盘上了。或(2)提交完成前,事务的更改信息,以日志的形式存储在磁盘,重启过程根据日志恢复出数据库系统的内存状态。一般而言,数据库会选择方法(2),原因留给读者思考。

数据库为了提高资源利用率和事务执行效率、降低响应时间,允许事务并发执行。但是多个事务同时操作同一对象,必然存在冲突,事务的中间状态可能暴露给其它事务,导致一些事务依据其它事务中间状态,把错误的值写到数据库里。需要提供一种机制,保证事务执行不受并发事务的影响,让用户感觉,当前仿佛只有自己发起的事务在执行,这就是隔离性。隔离性让用户可以专注于单个事务的逻辑,不用考虑并发执行的影响。数据库通过并发控制机制保证隔离性。由于隔离性对事务的执行顺序要求较高,很多数据库提供了不同选项,用户可以牺牲一部分隔离性,提升系统性能。这些不同的选项就是事务隔离级别。 – 事务隔离级别的由来

数据库反映的是真实世界,真实世界有很多限制,例如:账户之间无论怎么转账,总额不会变等现实约束;年龄不能为负值,性别最多只能有男、女、跨性别者三种选项等完整性约束。事务执行,不能打破这些约束,保证事务从一个正确的状态转移到另一个正确的状态,这就是一致性。不同与前三种性质完全由数据库实现保证,一致性既依赖于数据库实现(原子性、持久性、隔离性也是为了保证一致性),也依赖于应用端编写的事务逻辑。


事务的隔离级别

事务隔离是数据库处理的基础之一,隔离级别在多个事务同时进行更改和执行查询时,对性能与结果的可靠性、一致性和可再现性之间的平衡进行调整,InnoDB利用不同的锁策略支持不同隔离级别。MySQL中有四种隔离级别,分别是读未提交(READ UNCOMMITTED),读已提交(READ COMMITTED),可重复读(REPEATABLE READ)以及串行化(SERIALIZABLE)。mysql的默认隔离级别是可重复读。

在这里插入图片描述


事务的并发问题

  • 脏读:事务A读取了事务B未提交的数据。
  • 不可重复读:事务A多次读取同一份数据,事务B在此过程中对数据修改并提交,导致事务A多次读取同一份数据的结果不一致。
  • 幻读:事务A读取数据的同时,事务B插入了一条或者多条数据,当事务A提交后发现有新数据被提交,产生了幻觉。

不可重复读侧重于update或者delete操作,幻读侧重于insert。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。可以用行锁解决不可重复读,用间隙锁解决幻读,这里的幻读指的是当前读。


共享锁和排他锁

从锁的类别上来讲,有共享锁和排他锁

共享锁

共享锁,又称为读锁,可以查看但无法修改和删除的一种数据锁。

共享锁(S锁)又称为读锁,若事务T对数据对象A加上S锁,则事务T只能读A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了事务T和其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改


排他锁

排他锁,又称为写锁、独占锁,是一种基本的锁类型。

排他锁(Exclusive Locks,简称X锁),又称为写锁、独占锁,在数据库管理上,是锁的基本类型之一。若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。


区别

  1. 共享锁(S锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。
  2. 排他锁(X锁):如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
  3. 共享锁下其它用户可以并发读取,查询数据。但不能修改,增加,删除数据,资源共享。

事务的加锁机制

未提交读

加锁机制:在写事务时加行级共享锁,读事务时不加锁

例子:事务A修改了一条记录的内容,但是并没有提交(commit),在隔离级别为未提交读的情况下,事务B可以读取到A修改后并未提交的记录内容。(由于是共享锁,写事务未提交前其他事务仍然能读)

有何影响呢?一旦A执行回滚操作(没有commit之前都是可以回滚的),B之前所读取的记录内容则为脏数据,这就造成了脏读[不仅脏读无法避免,不可重复读(下面2.已提交读中介绍)、幻读(下面3.可重复读中介绍)也无法避免]


已提交读

加锁机制:写事务时加行级排他锁,事务结束才释放;读事务时加行级共享锁,读完立即释放锁(不等到整个事务结束后才释放)。(确保写一行时其他事务无法读写此行,只有commit以后才可读写,读一行时其他事务只能读,无法写这一行)

例子:事务A查询一条记录后,并没有结束事务A,在隔离级别为已提交读的情况下,接着事务B修改了A刚才查询的那条记录(既然是已提交读,那B修改内容后需要commit)。

有何影响呢?当A又再次查询这条记录时,发现与之前查询的记录不同(因为事务A没结束的时候B修改了内容,导致事务A两次读取不一致)。前后查询的记录不一就造成了不可重复读[不仅不可重复读无法避免,幻读(下面3.可重复读中介绍)也无法避免]


可重复读

加锁机制:写事务时加行级排他锁,读事务时加行级共享锁,都持续到事务结束才释放。(确保整个事务中写就是写,读就是读,整个事务读完之后才可以写)

例子:事务A根据条件查询一组记录,之后事务B在A查询条件的记录范围内插入一条记录。

有何影响呢?当A又使用相同的方式再次对表进行检索时,却发现了一条新纪录。这个新记录对A来说就像突然出现的一样,这就造成了幻读。(读写事务只是加了行级锁,其他事务虽然不能修改这些行,但是能添加新行,因此出现幻读现象)


可串行化

加锁机制:读事务时加表级共享锁,写事务时加表级排他锁。(避免其他事务对该表的操作)


悲观锁

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制。

正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。


乐观锁

乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过 version 的方式来进行锁定。如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户重新操作。实现方式:乐观锁一般会使用版本号机制或CAS算法实现。

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。

而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

核心SQL代码:update table set x=x+1, version=version+1 where id=#{id} and version=#{version};

CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

CAS(V,E,N)

  • V表示要更新的变量(主内存)
  • E表示预期值(本地内存)
  • N表示新值

操作机制:【(本地内存值==主内存预期值)?新值更新:迭代循环等待(也叫自旋)】
tips: CAS是通过自旋实现的,但不能说CAS就是自旋锁。


两种锁的使用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种, 像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候, 这样可以省去了锁的开销,加大了系统的整个吞吐量。 但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行 retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值