(十四)并发控制

  之前在(十三)事务处理中简单的介绍了锁系统,并且介绍了由于并发而产生的种种问题,例如脏读、幻读等,因此这里对如何解决这些问题再进行一下补充。


1、悲观锁

  在关系型数据库管理系统里,悲观并发控制是一种并发控制的方法,它可以阻止一个事务以影响其他事务的方式来修改数据。悲观锁需要使用数据库的锁机制,可以从字面理解为这种并发方式就是很悲观,每次调用数据的时候都认为同时会有其他事务在修改数据,因此每次在调用数据前都会先上锁,这样可以防止其他事务读取或修改表中的数据。

  例如:



  此处之所以会出现死锁,就是因为执行这条语句时已经使用了锁,

UPDATE tbl_name SET col_name = newValue WHERE id = x;

  因此互相调用相同的字段造成了死锁。

  悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。悲观并发控制实际上是”先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,并且会增加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载,还会降低并行性,因为一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以进行处理。


2、乐观锁

  乐观锁相对于悲观锁而言,通常会假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自负责的数据。在提交数据更新之前,每个事务会先检查在读取数据后,有无其他事务再次修改了该数据。如果有,那么当前正在提交的事务会进行回滚。可以从字面理解为这种并发方式就是很乐观,每次调用数据的时候都认为其他事务不会在同时修改数据,因此不会上锁。

  在处理数据时,乐观锁并不会使用数据库提供的锁机制,通常乐观锁的实现方式是记录数据版本,即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加1。当提交更新的时候,判断当前version值是否与之前读出的version值一致,如果相同,则予以更新,否则认为是过期数据。

  例如:
  假设用户A和用户B对同一张数据表的同一个字段记录值进行修改,此时用户A和用户B从该表中读取的version值为2,用户A对记录修改结束后,version值增加1,此时该表的version字段的值是3,而用户B依然按照version值为2进行操作,不满足“提交版本必须大于记录当前版本才能执行更新”的乐观锁策略,因此,用户B的提交被驳回。这样,就避免了用户B用基于version值为2的旧数据修改的结果覆盖用户A的操作结果的可能。

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


3、MVCC

  多版本并发控制(Multi-Version Concurrency Control)是为了实现数据库的并发控制而设计的一种机制。大多数的关系型数据库都支持MVCC,其突出特点是:读不加锁,读写不冲突。

  在MVCC中,读操作可以分成两类,快照读和当前读:

  • 快照读,读取的是记录的可见版本(可能是历史版本,即最新的数据可能正在被当前执行的事务并发修改),不会对返回的记录加锁;
  • 当前读,读取的是记录的最新版本,并且会对返回的记录加锁,保证其他事务不会并发修改这条记录。

  在MySQL InnoDB中,基本的SELECT操作,如

SELECT * FROM tbl_name WHERE xxxx;

  都属于快照读;而属于当前读的包含以下操作:

SELECT * FROM tbl_name WHERE xxxx LOCK IN SHARE MODE;(共享锁)
SELECT * FROM tbl_name WHERE xxxx FOR UPDATE;(排他锁)
INSERT,UPDATE,DELETE操作(排他锁)

  可以将MVCC理解为行级锁的一种妥协,它在许多情况下避免了使用锁,同时可以提供更小的开销。根据实现的不同,它可以允许非阻塞式读,在写操作进行时只锁定必要的记录。

  各个存储引擎对于MVCC的实现各不相同,下面将通过一个简化的InnoDB版本的行为来展示MVCC工作原理:
简单来说,通过为每一行记录添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。但是InnoDB并不存储这些事件发生时的实际时间,相反它只存储这些事件发生时的系统版本号。这是一个随着事务的创建而不断增长的数字。每个事务在事务开始时会记录它自己的系统版本号。每个查询必须去检查每行数据的版本号与事务的版本号是否相同。

  以下是在默认隔离级别REPEATABLE READ下,MVCC具体是怎样实现的:

  • SELECT:
      InnoDB只查找版本早于(包含等于)当前事务版本的数据行。这保证了不管是事务开始之前,或者事务创建时,或者修改了这行数据的时候,这行数据是存在的;这行数据的删除版本必须是未定义的或者比事务版本要大,这可以保证在事务开始之前这行数据没有被删除。
      符合这两个条件的行可能会被当作查询结果而返回。

  • INSERT:
    InnoDB为这个新行记录当前的系统版本号。

  • DELETE:
    InnoDB将当前的系统版本号设置为这一行的删除ID。

  • UPDATE:
    InnoDB会写一个这行数据的新拷贝,这个拷贝的版本为当前的系统版本号。它同时也会将这个版本号写到旧行的删除版本里。


4、MVCC与乐观锁的区别

  在了解了MVCC的实现机制后可能会感觉与乐观锁中使用版本号加锁有相似之处,实际上MVCC可以保证不阻塞地读到一致的数据。但是,MVCC并没有对实现细节做约束,在InnoDB引擎下是只对读无锁,写操作仍是上锁的悲观并发控制,这也意味着,InnoDB中只能见到因死锁和不变性约束而回滚,而不会出现因为写冲突而回滚的现象;MVCC对数据表中的每行数据只保留一份,在更新数据时上行级锁,同时将旧版数据写入undo log;数据表和undo log中行数据都记录着事务ID,在检索时,只读取来自当前已提交的事务的行数据。这种额外的记录所带来的结果就是对于大多数查询来说根本就不需要获得一个锁。MVCC只是简单地以最快的速度来读取数据,确保只选择符合条件的行。但其缺点也正是存储引擎必须为每一行存储更多的数据,做更多的检查工作,处理更多的善后操作。


版权声明:欢迎转载,欢迎扩散,但转载时请标明作者以及原文出处,谢谢合作!             ↓↓↓

推荐阅读更多精彩内容