悲观锁

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

详细过程

  • 在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
  • 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际需要决定。
  • 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
  • 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。

乐观锁

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于 write_condition 机制的其实都是提供的乐观锁。

乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

更新过程

具体来讲,使用乐观锁之后的更新过程,就是这么一个流程:

  • 读取要更新的记录。
  • 对记录按照用户的意愿进行修改。当然,这个时候不会修改 ver 字段。 这个字段对用户是没意义的。
  • 在保存记录前,再次读取这个记录的 ver 字段,与之前读取的值进行比对。
  • 如果 ver 不同,说明在用户修改过程中,这个记录被别人改动过了。那么, 我们要给出提示。
  • 如果 ver 相同,说明这个记录未被修改过。那么,对 ver +1, 并保存这个记录。这样子就完成了记录的更新。同时,该记录的版本号也加了1。

乐观锁失效

乐观锁存在失效的情况,属小概率事件,需要多个条件共同配合才会出现。如:

  • 应用采用自己的策略管理主键 ID。如,常见的取当前 ID 字段的最大值+1作为新 ID。
  • 版本号字段 ver 默认值为 0 。
  • 用户 A 读取了某个记录准备修改它。该记录正好是 ID 最大的记录,且之前没被修改过, ver 为默认值 0。
  • 在用户 A 读取完成后,用户 B 恰好删除了该记录。之后,用户 C 又插入了一个新记录。
  • 此时,阴差阳错的,新插入的记录的 ID 与用户 A 读取的记录的 ID 是一致的, 而版本号两者又都是默认值 0。
  • 用户 A 在用户 C 操作完成后,修改完成记录并保存。由于 ID、ver 均可以匹配上, 因此用户 A 成功保存。但是,却把用户 C 插入的记录覆盖掉了。

乐观锁此时的失效,根本原因在于应用所使用的主键 ID 管理策略, 正好与乐观锁存在极小程度上的不兼容。

对此,也有一些意见提出来,使用时间戳作为版本号字段,就可以避免这个问题。 但是,时间戳的话,如果精度不够,如毫秒级别,那么在高并发,或者非常凑巧情况下, 仍有失效的可能。而如果使用高精度时间戳的话,成本又太高。

使用时间戳,可靠性并不比使用整型好。问题还是要回到使用严谨的主键成生策略上来。

【参考资料】

  1. http://www.digpage.com/lock.html
  2. https://zh.wikipedia.org/wiki/%E4%B9%90%E8%A7%82%E5%B9%B6%E5%8F%91%E6%8E%A7%E5%88%B6
  3. https://zh.wikipedia.org/wiki/%E6%82%B2%E8%A7%82%E5%B9%B6%E5%8F%91%E6%8E%A7%E5%88%B6

—EOF—