在介绍悲观锁和乐观锁之前,我们先看一下什么是锁。
锁
生活中:锁在我们身边无处不在,比如我出门玩去了需要把门锁上,比如我需要把钱放到保险柜里面,必须上锁以保证我财产的安全。
代码中:比如多个线程需要同时操作修改共享变量,这时需要给变量上把锁(syncronized),保证变量值是对的。
数据库表:当多个用户修改表中同一数据时,我们可以给该行数据上锁(行锁)。
悲观锁(悲观并发控制)
当我们要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发的发生。
为什么叫做悲观锁呢?因为这是一种对数据的修改抱有悲观态度的并发控制方式。我们一般认为数据被并发修改的概率比较大,所以需要在修改之前先加锁。
数据库中的行锁,表锁,读锁,写锁,以及 syncronized 实现的锁均为悲观锁。
乐观锁
乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁,只有到数据提交的时候才通过一种机制来验证数据是否存在冲突。
乐观锁通常是通过在表中增加一个版本(version)或时间戳(timestamp)来实现,其中,版本最为常用。
乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。
如何实现
我们知道悲观锁和乐观锁是用来控制并发下数据的顺序变动问题的。那么我们就模拟一个需要加锁的场景,来看不加锁会出什么问题,并且怎么利用悲观锁和乐观锁去解决。
我们以商品为例,现在 线程A 和线程 B 都想吃红薯,但是红薯数量只有 1 个了。在不加锁的情况下,如果A,B同时下单,就有可能导致超卖。
悲观锁实现
利用悲观锁的解决思路是,我们认为数据修改产生冲突的概率比较大,所以在更新之前,我们显示的对要修改的记录进行加锁,直到自己修改完再释放锁。加锁期间只有自己可以进行读写,其他事务只能读不能写。
此时线程 A 下单前先给红薯这行数据(id=C001)加上悲观锁(行锁)。此时这行数据只能 A 来操作,也就是只有 A 能买。B 想买就必须一直等待。当 A 买好后,B 再想去买的时候会发现库存数量已经为 0,那么 B 看到后就会放弃购买。
那怎么样给这行数据加上悲观锁呢?当然是在select给这行数据加上锁,如下所示:
select num from commodity where id = C001 for update
悲观锁图解:
乐观锁解实现
下面我们利用乐观锁来解决该问题。上面乐观锁的介绍中,我们提到了,乐观锁是通过版本号 version 来实现的。所以,我们需要给 commodity 表加上 version 字段。
我们认为数据修改产生冲突的概率并不大,多个线程在修改数据的之前先查出版本号,在修改时把当前版本号作为修改条件,只会有一个线程可以修改成功,其他线程则会失败。
A 和 B 同时将红薯(id=C001)的数据查出来,然后 A 先买,A 将 id=C001 和 version=0 作为条件进行数据更新,即将数量 -1,并且将版本号+1。
此时版本号变为 1。A 此时就完成了商品的购买。最后 B 开始买,B 也将 id=C001 和 version=0 作为条件进行数据更新,但是更新完后,发现更新的数据的库存为 0,此时就说明已经有人修改过数据,此时就应该提示用户重新查看最新数据购买。
乐观锁图解:
如何选择
- 乐观锁适用于读多写少的场景,可以省去频繁加锁、释放锁的开销,提高吞吐量
- 在写比较多的场景下,乐观锁会因为版本不一致,不断重试更新,产生大量自旋,消耗 CPU,影响性能。这种情况下,适合悲观锁