悲观锁与乐观锁的认识

188 阅读4分钟

一个多线程应用,尤其是分布式系统,在运行过程中往往需要保证数据访问的排他性。

例如在最常见的车站售票系统上,在对系统中车票“剩余量”的更新处理中,我们希望在针对某个时间点的数据进行更新操作时(这可能是一个极短的时间间隔,例如几秒或几毫秒,甚至是几纳秒,在计算机科学的有些应用场景中,几纳秒可能也算不上太短的时间间隔),数据不会因为其他人或系统的操作再次发生变化。也就是说,车站的售票员在卖票的过程中,必须要保证在自己的操作过程中,其他售票员不会同时也在出售这个车次的车票。

为保证上面这个场景的正常运作,一种可能的做法或许是这样,车站某售票窗口的售票员突然向其他售票员大喊一声:“现在你们不要出售杭州到北京的 XXX 次车票!”然后当他售票完毕后,再次通知大家:“该车次已经可以售票啦!”

当然在现实生活中,不会依靠这么原始的人工方式来实现数据访问的排他性,但这个例子给我们的启发是:在并发环境中,我们需要通过一些机制来保证这些数据在某个操作过程中不会被外界修改,我们称这样的机制为“锁”。在数据库技术中,通常提到的“悲观锁”和“乐观锁”就是这种机制的典型实现。

悲观锁,又被称作悲观并发控制(Pessimistic Concurrency Control, PCC),是数据库中一种非常典型且非常严格的并发控制策略。悲观锁具有强烈的独占和排他特性,能够有效地避免不同事务对同一数据并发更新而造成的数据一致性问题。在悲观锁的实现原理中,如果一个事务(假定事务A)正在对数据进行处理,那么在整个处理过程中,都会将数据处于锁定状态,在这期间,其他事务将无法对这个数据进行更新操作,直到事务A完成对该数据的处理,释放了对应的锁之后,其他事务才能够重新竞争来对数据进行更新操作。也就是说,对于一份独立的数据,系统只分配了一把唯一的钥匙,谁获得了这把钥匙,谁就有权力更新这份数据。一般我们认为,在实际生产应用中,悲观锁策略适合解决那些对于数据更新竞争十分激烈的场景——在这类场景中,通常采用简单粗暴的悲观锁机制来解决并发控制问题。

乐观锁,又被称作乐观并发控制(Optimistic Concurrency Control, OCC),也是一种常见的并发控制策略。相对于悲观锁而言,乐观锁机制显得更加宽松与友好。从上面对悲观锁的讲解中我们可以看到,悲观锁假定不同事务之间的处理一定会出现互相干扰,从而需要在一个事务从头到尾的过程中都对数据进行加锁处理。而乐观锁则正好相反,它假定多个事务在处理过程中不会彼此影响,因此在事务处理的绝大部分时间里不需要进行加锁处理。当然, 既然有并发, 就一定会存在数据更新冲突的可能。在乐观锁机制中,在更新请求提交之前,每个事务都会首先检查当前事务读取数据后,是否有其他事务对该数据进行了修改。如果其他事务有更新的话,那么正在提交的事务就需要回滚。乐观锁通常适合使用在数据并发竞争不大、事务冲突较少的应用场景中。

从上面的讲解中,我们其实可以把一个乐观锁控制的事务分成如下三个阶段:数据读取、写入校验和数据写入,其中写入校验阶段是整个乐观锁控制的关键所在。在写入校验阶段,事务会检查数据在读取阶段后是否有其他事务对数据进行过更新,以确保数据更新的一致性。那么,如何来进行写入校验呢?我们首先可以来看下 JDK 中最典型的乐观锁实现——CAS。简单地讲就是“对于值 V, 每次更新前都会比对其值是否是预期值 A,只有符合预期,才会将 V 原子化地更新到新值 B”,其中是否符合预期便是乐观锁中的“写入校验”阶段。