问题

首先来看看问题。数据表ATable结构如下

CREATE TABLE `FUN_Product` (
  `ID` int(11) NOT NULL COMMENT '主键',
  `Value` int(11) DEFAULT '0' COMMENT '对应产品引用ID' 

现在用户UserA和UserB要同时更新表中的同一条记录,可能会出现如下的流程:

在相同的时间段内,UserA和UserB之间的更新是相互冲突的,那该怎么解决冲突,是合并两者的结果还是分先后?

悲观锁

悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)[1]。

以MySQL InnoDB为例:

//0.开始事务
begin;/begin work;/start transaction; (三者选一就可以)
//1.查询信息
select value from Atable where ID=1 for update;
//2.修改value
update Atable set value=2;
//4.提交事务
commit;/commit work;

SQL中使用了select…for update的方式,就实现了悲观锁,在ATable表中ID为1的列就被锁定,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。
需要注意的是,在事务中,只有SELECT ... FOR UPDATE 或LOCK IN SHARE MODE 同一笔数据时会等待其它事务结束后才执行,一般SELECT ... 则不受此影响。
使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。
这样在上述的问题,A必须等待B的事务结束之后,才能操作表,也就解决了可能出现的冲突。

乐观锁

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。那么我们如何实现乐观锁呢,一般来说有以下2种方式:

  • 使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
  • 乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

乐观锁在更新数据时的流程会变成如下的模型:

方案选择

悲观锁 乐观锁
独占数据,其他线程需要等待,不会出现修改的冲突,能够保证数据的一致性 其他线程可以同时读取数据,会出现冲突修改,需要上层逻辑解决冲突。
依赖于数据库的实现 但在出现冲突时,需要上层逻辑处理,一般进行重试,或者抛弃修改。
会降低效率,在线程较多时出现等待 能够保证高并发下的读取,效率较高
避免冲突 解决冲突

两种锁的选择并没有绝对的优劣,实际应用中根据项目需求判断;一般而言,对于读取频率很高而修改频率较少的需求可以采用乐观锁;数据很敏感且读取频率较低的可以采用悲观锁的方式。

Reference

1.百度百科
2.mysql乐观锁总结和实践
3.mysql悲观锁总结和实践
4.Optimistic or pessimistic locking - Which one should you pick?
5.Transactions and Optimistic Locking