乐观锁与悲观锁

前言

  • 最近学习遇到了”锁”这个有趣的知识,以前也只是听过各种”锁”,感觉很多而且杂乱,现在就将他们进行整理学习。

  • 乐观锁和悲观锁本质上只是一种思想,在实现的时候悲观锁还是利用了数据库提供的”锁”机制(也只有数据库层提供的”锁”机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加”锁”机制,也无法保证外部系统不会修改数据),乐观锁则会记录和检查数据版本。这两种”锁”多用于关系数据库的并发控制。

基础

  • 概念

    • 在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。(就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block,直到它拿到锁)

      • 悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
    • 在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。(就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做)

      • 乐观并发控制多数用于数据争用不大、冲突较少的环境中,这种环境中,偶尔回滚事务的成本会低于读取数据时锁定数据的成本,因此可以获得比其他并发控制方法更高的吞吐量。
  • 流程

    • 悲观锁

      • 在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。

      • 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际需要决定。

      • 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。

      • 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。

    • 乐观锁

      • 数据版本,为数据增加的一个版本标识(实现数据版本有两种方式,第一种是使用版本号,第二种是使用时间戳)。

      • 当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。

      • 当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。

为什么需要”锁”

  • 在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。

  • 典型的冲突有:

    • 丢失更新:一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户A把值从6改为2后,用户B又把值从2改为6,则用户A丢失了他的更新。

    • 脏读:当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是6,用户B把值改为2的过程中,用户A读到的值仍为6。

失效情况

  • 悲观锁

    • 时间戳的精度如果不够,如毫秒级别,那么在高并发,或者非常凑巧情况下,有失效的可能。(如果使用高精度时间戳的话,成本又太高)
  • 乐观锁

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

      • 应用采用自己的策略管理主键ID。如,常见的取当前ID字段的最大值+1作为新ID。

      • 版本号字段 ver 默认值为 0 。

      • 用户A读取了某个记录准备修改它。该记录正好是ID最大的记录,且之前没被修改过, ver 为默认值 0。

      • 在用户A读取完成后,用户B恰好删除了该记录。之后,用户C又插入了一个新记录。

      • 此时,阴差阳错的,新插入的记录的ID与用户A读取的记录的ID是一致的, 而版本号两者又都是默认值 0。

      • 用户A在用户C操作完成后,修改完成记录并保存。由于ID、ver均可以匹配上, 因此用户A成功保存。但是,却把用户C插入的记录覆盖掉了。

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

    • 时间戳的精度如果不够,如毫秒级别,那么在高并发,或者非常凑巧情况下,有失效的可能。(如果使用高精度时间戳的话,成本又太高)

优缺点

  • 悲观锁

    • 悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。

    • 悲观锁适用于可靠的持续性连接,诸如C/S应用。 对于Web应用的HTTP连接,先天不适用。

    • 锁的使用意味着性能的损耗,在高并发、锁定持续时间长的情况下,尤其严重。 Web应用的性能瓶颈多在数据库处,使用悲观锁,进一步收紧了瓶颈。

    • 非正常中止情况下的解锁机制,设计和实现起来很麻烦,成本还很高。

    • 不够严谨的设计下,可能产生莫名其妙的,不易被发现的,让人头疼到想把键盘一巴掌碎的死锁问题。

  • 乐观锁

    • 乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁

    • 但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就会出现”脏”数据的问题。

参考及推荐

-------------本文结束感谢您的阅读-------------
谢谢你请我吃糖果!
0%