锁(Lock): 在介绍悲观锁和乐观锁之前,让我们看一下锁。锁,在我们生活中随处可见,我们的门上有锁,我们存钱的保险柜上有锁,是用来保护我们财产安全的。程序中也有锁,当多个线程修改共享变量时,我们可以给修改操作上锁(syncronized)。当多个用户修改表中同一数据时,我们可以给该行数据上锁(行锁)。因此,锁其实是在并发下控制多个操作的顺序执行,以此来保证数据安全的变动。 并且,锁是一种保证数据安全的机制和手段,而并不是特定于某项技术的。悲观锁和乐观锁亦是如此。
悲观锁(Pessimistic Lock)
顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

乐观锁(Optimistic Lock)
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。本质上,数据库的乐观锁做法和悲观锁做法主要就是解决下面假设的场景,避免丢失更新问题:
经过上面的学习,我们知道悲观锁和乐观锁是用来控制并发下数据的顺序变动问题的。那么我们就模拟一个需要加锁的场景,来看不加锁会出什么问题,并且怎么利用悲观锁和乐观锁去解决。
场景:A和B用户最近都想吃猪肉脯,于是他们打开了购物网站,并且找到了同一家卖猪肉脯的>店铺。下面是这个店铺的商品表goods结构和表中的数据
id | name | num |
|---|---|---|
1 | 猪肉脯 | 1 |
2 | 牛肉干 | 1 |
从表中可以看到猪肉脯目前的数量只有1个了。在不加锁的情况下,如果A,B同时下单,就会报错。
select num from goods where id = 1 for update;
下面是悲观锁的加锁图解
我们通过开启mysql的两个会话,也就是两个命令行来演示。 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看到后就知道被买走了,就会放弃购买。 通过悲观锁,我们解决了猪肉脯购买的问题。

我们还是通过开启mysql的两个会话,也就是两个命令行来演示。 1 事务A执行查询命令,事务B执行查询命令,因为两者查询的结果相同,所以下面我只列出一个截图。

此时A和B均获取到相同的数据 2 事务A进行购买更新数据,然后再查询更新后的数据。

我们可以看到事务A成功更新了数据和版本号。 事务B再进行购买更新数据,然后我们看影响行数和更新后的数据

可以看到最终修改行数为0,数据没有改变。此时就需要我们告知用户重新处理。
下面我们介绍下乐观锁和悲观锁的优缺点以便我们分析他们的应用场景,这里我只分析最重要的优缺点,也是我们要记住的。