这两种类型的锁都是为了解决更新丢失的问题。
1、什么是丢失更新?
考虑这个场景:在一个web应用中,用户1检索某表的数据行R到编辑页面,用户2也检索数据R到编辑页面,用户1修改了R的字段A1,然后提交修改(注意,这里的提交是将用户编辑页面的所有字段都更新到数据库,哪怕该字段没有变化),用户2这时修改了R的字段A2,然后执行了类似的全量更新提交。不难发现,这种情况下,用户1对于字段A1的修改会被用户2的操作覆盖掉。这时,我们称丢失了修改。
有人会认为,由于我们在提交修改时,也会将没有变化的字段也更新,如果只是更新变化的字段就不会出现这个问题。实际上,这里的问题在于用户2在编辑页面修改数据时,所面对的数据已经不是当前数据库中最新的版本,可能有其它用户已经将某些数据做了修改。考虑在一个电商系统中,对于某个刚支付完成的订单1,客户检索该订单到编辑页面,库管检索该订单到编辑页面,他们看到的该订单都是已支付的状态。然后,顾客将订单的状态置为取消,然后提交修改(这里只提交对状态字段的修改),而库管看到已支付的订单,然后就将发货,并将订单状态置为了已发货。这也是一个丢失修改的例子。
这里问题的关键在于后面的用户在修改时,不知道从他查出数据到提交更新两个操作期间,可能有其它用户对数据做了修改。这期间的更新没有被用户看到,这种,情况称为丢失更新。
2、悲观锁和乐观锁
为了解决这个问题,需要用到数据库的锁机制,大致两种思路:悲观锁(pessimistic locking)和乐观锁(optimistic locking)。
2.1 悲观锁
悲观锁就是在尝试更新某记录之前,先给该记录加锁。在上面的例子场景中,就是在将记录检索到编辑页面时,对该记录加锁。之所以叫悲观锁,就是因为这个思路的出发点比较悲观,认为坏的事情(在数据检索到编辑页面之后,修改提交之前,会有其他用户也修改页面上的数据)会发生。
其实,加锁很简单。比如之前在将记录载入编辑页面时,用的查询语句是select cols from table where condition1,那么加锁后的语句就是select cols from table where condition1 for update nowait。其中,for update代表加锁,nowait表示如果待加锁对象上已经有锁,导致没法加锁时,立即返回“资源正忙,无法加锁”的信息,并执行结束。如果没有指定nowait,则会一直等着待加锁对象上当前有的锁释放,然后加锁成功时,才会执行结束。
这样加锁之后,在对该对象持有锁期间,对其进行更新,并提交,就能够保证没有其他用户能够修改加锁的对象。这就是悲观锁。
2.2 乐观锁
相反的,乐观锁的出发点就是认为坏的事情不会发生。这样,在用户将记录载入编辑页面时不会进行提前加锁。转而在更新提交时采用一些技巧,来避免丢失更新。
可以想象的技巧,大概如下:
(1)给记录添加版本号标识,每次更新时对该标识加一,只有版本号大于数据库中记录原来的版本号时,记录才会被成功更新。
(2)在更新条件中,添加旧值筛选到where条件,这样,如果期间有其它用户更新,会导致筛选不出记录,更新失败。比如,上面电商的例子中,库管发货的更新脚本可以写为update table set status=“已发货” where condition and status=“已支付”。这样,如果有其它用户将订单状态改为“取消”,就会更新0行,从而避免丢失更新。
(3)在提交更新前采用select for update(也就是锁)检查。这里不是在记录检索到修改页面的时候加锁(这时悲观锁的做法),而是在用户提交修改的时候,通过加锁来检查该记录这期间是否被其他用户修改。如果该记录没有变化,就提交修改;如果有变化,就提示用户记录状态有改变。
有人可能会认为最后一种方式实际是悲观锁的方式,但实际上,因为没有提前加锁,只是更新提交时加锁检查,所以,也是乐观锁。
3、适用场景
两种锁机制的适用场景,从它们的名字就能看出来。悲观锁适用于冲突出现概率较高的场景,悲观锁应用于对象的时间较长,容易导致阻塞。所以,web应用中,用户读取数据后,可能很久才会提交更新,这样会导致锁长时间占用,影响效率。所以web应用中,不适合采用悲观锁。而乐观锁适用于冲突出现概率较低的场景。