这是一篇介绍悲观锁和乐观锁的入门文章。旨在让不懂悲观锁和乐观锁的人,明白什么是悲观锁,什么是乐观锁。与其他文章不同的是,本文会附上相应的图表,方便大家理解。通过本文,您将学到以下知识。1.锁(Lock)在介绍悲观锁和乐观锁之前,我们先来看看什么是锁。在我们的生活中,锁随处可见。在我们存钱的地方,我们的门和保险箱上有锁。它们用于保护我们的财产安全。程序中也有锁。当多个线程修改共享变量时,我们可以锁定(同步)修改操作。当多个用户修改表中的相同数据时,我们可以锁定行数据(行锁)。因此,锁实际上控制了并发下多个操作的顺序执行,以保证数据安全变化。而且,锁是一种保证数据安全的机制和手段,并不特定于某种技术。悲观锁和乐观锁也是如此。本文介绍的悲观锁和乐观锁都是基于数据库级别的。2.悲观锁悲观并发控制(PessimisticConcurrencyControl),第一眼看到,相信大家都会认为这是悲观锁。没错,就是悲观锁。那么这种悲观情绪体现在哪里呢?悲观是我们人类的一种消极情绪,对应锁的悲观情绪。悲观锁认为被它保护的数据是极不安全的,每时每刻都可能发生变化。A事务获得悲观锁后(可以理解为用户),其他事务不能修改数据,只能等待锁释放后执行。数据库中的行锁、表锁、读锁、写锁以及synchronized实现的锁都是悲观锁。这里再介绍一下数据库中什么是表锁和行锁,以免后面有同学不理解悲观锁的实现。我们经常使用的数据库是mysql。mysql中最常用的引擎是Innodb。Innodb默认使用行锁。行锁是基于索引的,所以如果要加行锁,加锁的时候一定要激活索引,否则会用到表锁。3.乐观锁对应悲观。乐观对我们人类来说是一种积极的情绪。OptimisticConcurrencyControl的“乐观”体现在它认为数据变化不会太频繁。因此,它允许多个事务同时对数据进行更改。但是,乐观不代表不负责任,那么如何负责多个事务顺序修改数据呢?乐观锁通常是通过给表加一个版本(version)或者时间戳(timestamp)来实现的,其中版本是最常用的。当事务从数据库中获取数据时,它也会获取数据的版本(v1)。当事务更改完数据要更新到表中时,会将之前取到的版本v1与数据合并**与*的版本v2相比,如果v1=v2,则表示在数据更改期间,没有其他事务修改数据。这时候允许事务修改表中的数据,修改的时候version会加1,这样就说明数据已经改变了。如果v1不等于v2,说明在数据更改期间,数据被其他事务更改了。这个时候不允许数据更新到表中。一般的解决办法是通知用户重新操作。与悲观锁不同,乐观锁是人为控制的。4.如何实现经过上面的学习,我们知道悲观锁和乐观锁是用来控制并发下数据的顺序变化的。然后我们模拟一个需要加锁的场景,看看不加锁会出现什么问题,以及如何使用悲观锁和乐观锁来解决。场景:用户A和B最近都想吃肉干,打开购物网站发现同一家卖肉干的店。下面是本店的product表goods结构和表中的数据。从表中可以看出,目前猪肉干的数量只有1个,在没有锁的情况下,如果A和B同时下单,可能会导致超卖。悲观锁解决方案使用悲观锁的思路是我们认为数据修改冲突的概率比较高,所以在更新之前,我们先给要修改的记录加锁,直到修改完成后才释放锁。在锁定期间,只有你可以读写,其他事务只能读不能写。A下单前,给肉干数据(id=1)这一行加悲观锁(行锁)。此时这行数据只能由A操作,即只有A可以购买。B要买就得一直等。A买了之后,当B再想买的时候,发现数量已经是0了,B看到后就放弃购买了。那么如何给猪肉干的数据加悲观锁,即id=1呢?我们可以通过如下语句selectnumfromgoodswhereid=1forupdate;对id=1的数据行加悲观锁;下面是一个悲观锁图,我们通过打开mysql的两个session,也就是两个命令行进行Demonstrate。1、事务A执行命令悲观锁住id=1的数据,准备更新数据。之所以以begin开头是因为mysql是自提交的,所以事务必须以begin开头,否则所有的改动都会被mysql自动提交。2、事务B也对id=1的数据进行悲观锁定,更新数据。我们可以看到事务B等待A释放锁。如果A长时间不释放锁,那么最终事务B会报错,有兴趣的可以试试。3、然后我们让事务A执行修改数据的命令,将猪肉干的数量减一,然后检查修改后的数据,***commit,结束事务。我们可以看到,此时最后一块猪肉干被A买了,只剩下0块了。4、事务A执行完步骤3,我们再看事务B发生了什么,我们看到因为事务A释放了锁,事务B结束等待,拿到了锁,但是此时数据变成了0。那么B看到就知道已经买了,就会放弃购买。通过悲观锁,我们解决了购买猪肉干的问题。乐观锁解决方案下面,我们使用乐观锁来解决这个问题。在上面乐观锁的介绍中,我们提到了乐观锁是通过版本号version来实现的。因此,我们需要在goods表中增加一个version字段。改表后的结构如下:使用乐观锁的解决方案是我们认为数据修改冲突的概率不高,修改数据前应该检测多个事务。版本号,修改时以当前版本号作为修改条件,只有一个事务可以修改成功,其他事务都会失败。A和B同时查了猪肉干的数据(id=1下面说是id=1),然后A先买了,A以id=1和version=0为条件更新了数据,即数量-1,+1版本号。此时版本号变为1。A现在已经完成了产品的购买。***B开始买,B也以id=1,version=0为条件更新数据,但是更新后发现更新的数据行数为0,说明有人改了数据,此时应提示用户查看***数据购买。下面是乐观锁的锁图。我们还是通过打开mysql的两个session,也就是两条命令行来进行演示。1、事务A执行查询命令,事务B执行查询命令。因为两者的查询结果是一样的,下面我只列一张截图。此时A和B都获得了相同的数据。2.事务A购买并更新数据,然后查询更新后的数据。我们可以看到事务A成功更新了数据和版本号。然后事务B购买并更新了数据,然后我们查看受影响的行数和更新后的数据,可以看到最后修改的行数为0,数据没有变化。这时候我们需要通知用户重新处理。5.优缺点下面我们分别介绍一下乐观锁和悲观锁的优缺点,以便分析它们的应用场景。这里我只分析最重要的优点和缺点,这些也是我们需要记住的。悲观锁优点:悲观锁利用数据库中的锁机制实现数据变化的顺序执行,是最有效的方法缺点:一个事务用悲观锁锁定数据后,其他事务将无法删除被锁定的数据data对于查询以外的所有操作,如果事务的执行时间很长,其他事务就会一直等待下去,这势必会影响到我们系统的吞吐量。乐观锁的优点:乐观锁不锁数据库,任何事务都可以对数据进行操作,只有更新时才进行校验,避免了悲观锁降低吞吐量的缺点。缺点:乐观锁因为是我们人为实现的,所以只适用于我们自己的业务。如果插入了国外交易,可能会出现错误。6、应用场景悲观锁:因为悲观锁会影响系统吞吐量的性能,适合应用在写次数最多的场景。乐观锁:因为乐观锁是为了避免悲观锁的缺点,所以适用于读操作最多的场景的应用。参考文献https://chenzhou123520.iteye.com/blog/1860954https://baike.baidu.com/item/optimisticlock/7146502【本文为专栏作者霍利斯原创文章,作者微信公众号霍利斯(ID:hollishuang)】点此阅读该作者更多好文
