mysql悲观锁原理详解

mysql中的锁概念

mysql已经成为大家日常数据存储的最常用平台,但随着业务量和访问量的上涨,会出现并发访问等场景,如果处理不好并发问题的话会带来严重困扰。下面介绍一下如何通过mysql的悲观锁来解决因并发访问出现的种种数据不一致问题。

 

为什么需要锁

      当我们并发访问或更新数据库时,有可能会出现脏读(Dirty Read)、不可重复读(Unrepeatable Read)、幻读(Phantom Read)、更新丢失(Lost update)等数据不一致情况,为了解决这些问题,mysql引入了多种锁的概念。

      我们在项目开发过程中,可以通过sql语句或DB配置快速使用mysql为我们提供的这些底层通用锁服务。

 

锁类型

       MySQL InnoDB对数据行的锁定类型一共有四种:共享锁(读锁,S锁)、排他锁(写锁,X锁)、意向共享锁(IS锁)和意向排他锁(IX锁)。

       我们可以根据不同的业务场景和并发特点使用不同的锁。

 

锁定方式

    mysql支持三种锁定方式:

  • 行锁(Record Lock):锁直接加在索引记录上面。
  • 间隙锁(Gap Lock):锁加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间。
  • Next-Key Lock:行锁与间隙锁组合起来用就叫做Next-Key Lock。

       默认情况下,InnoDB工作在可重复读隔离级别下,并且以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock是行锁与间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。当一个间隙被事务T加了锁,其它事务是不能在这个间隙插入记录的。

 

锁的实现

  • 在可重复读级别下,InnoDB以Next-Key Lock的方式对索引加锁;在读已提交级别下,InnoDB以Index-Record Lock的方式对索引加锁。
  • 被加锁的索引如果不是聚族索引,那被锁的索引所指向的聚族索引以及其它指向相同聚族索引的索引也会被加锁。
  • SELECT * FROM ... LOCK IN SHARE MODE对索引加共享锁;SELECT * FROM ... FOR UPDATE对索引加排他锁。
  • SELECT * FROM ... 是非阻塞式读,(除Serializable级别)不会对索引加锁。在读已提交级别下,总是查询记录的最新、有效的版本;在可重复读级别下,会记住第一次查询时的版本,之后的查询会基于该版本。例外的情况是在串行化级别,这时会以Next-Key Lock的方式对索引加共享锁。
  • UPDATE ... WHERE 与DELETE ... WHERE对索引加排他锁。
  • INSERT INTO ... 以Index-Record Lock的方式对索引加排他锁

悲观锁原理详解

什么是悲观锁 

     对于悲观锁的概念解释主要有两种,但本质上悲观锁主要用于数据库访问的并发控制上。

解释一 写道
悲观锁是指对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态,在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。

 

解释二 写道
在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作都某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。

 

悲观锁处理流程

  1. 在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)
  2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常
  3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了
  4. 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常

mysql悲观锁实现

 在使用mysql悲观锁之前,我们需要关闭mysql数据库中的“自动提交”属性(set autocommit=0;),因为在mysql中默认使用的是"autocommit模式"。在默认模式下,执行完一个数据库更新操作后,mysql会立即将结果进行提交。

 

悲观锁使用的示例代码如下:

  1. 开始事务。begin;/begin work;/start transaction; (三者选一即可)
  2. 查询商品信息。select status from item where id=1 for update;
  3. 插入订单数据。insert into order (id,item_id) values (null,1);
  4. 修改商品状态。update item set status=2;
  5. 事务提交。commit;/commit work;(二选一即可)

select…for update

       上面的查询语句中,我们使用了select…for update的方式,这样就通过开启排他锁的方式实现了悲观锁。此时在item表中,id为1的那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。     

       使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键/索引,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。

 

悲观锁优点

  • 悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。
  • 悲观锁基于DB层面实现,对业务代码无入侵,使用方便

悲观锁缺点

  • 悲观锁适用于可靠的持续性连接,诸如C/S应用。 对于Web应用的HTTP连接,先天不适用
  • 锁的使用意味着性能的损耗,在高并发、锁定持续时间长的情况下,尤其严重。 Web应用的性能瓶颈多在数据库处,使用悲观锁,进一步收紧了瓶颈
  • 非正常中止情况下的解锁机制,设计和实现起来很麻烦,成本还很高
  • 不够严谨的设计下,可能产生莫名其妙的,不易被发现的死锁问题
  • 悲观的缺陷是不论是页锁还是行锁,加锁的时间可能会很长,这样可能会长时间的限制其他用户的访问,也就是说悲观锁的并发访问性不好

结语

悲观锁在特定业务场景下有其独特的使用优势,但在大多数互联网场景中性能问题会严重制约我们技术方案的选择,后面再介绍一种新的数据库并发问题解决方案——乐观锁。

 

猜你喜欢

转载自nuistcc.iteye.com/blog/2412042