面试必备的数据库悲观锁与乐观锁

前言

在上一个章节5分钟带你读懂事务隔离性与隔离级别 的最后,其实咱们已经提到了锁的概念。本章节接下来将主要介绍如下数据库悲观锁与乐观锁的相关知识。若有错误还请你们及时指出~mysql

本文已同步至 GitHub/Gitee/公众号,感兴趣的同窗帮忙点波关注~git

问题:github

  • 为何须要锁?
  • 什么是悲观锁?
  • 什么是乐观锁?
  • 悲观锁与乐观锁区别与联系?
  • 悲观锁与乐观锁的使用场景?

为何须要锁?

在并发环境下,若是多个客户端访问同一条数据,此时就会产生数据不一致的问题,如何解决,经过加锁的机制,常见的有两种锁,乐观锁和悲观锁,能够在必定程度上解决并发访问。sql

1. 悲观锁(Pessimistic Lock)

1.1 定义

百度百科:数据库

悲观锁,正如其名,具备强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其余事务,以及来自外部系统的事务处理)修改持保守态度,所以,在整个数据处理过程当中,将数据处于锁定状态。悲观锁的实现,每每依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,不然,即便在本系统中实现了加锁机制,也没法保证外部系统不会修改数据)。编程

其余知识点bash

悲观锁主要是共享锁排他锁 共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据能够共享一把锁,都能访问到数据,可是只能读不能修改。 排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其余所并存,如一个事务获取了一个数据行的排他锁,其余事务就不能再获取该行的其余锁,包括共享锁和排他锁,可是获取排他锁的事务是能够对数据就行读取和修改。微信

1.2 案例分析

使用场景举例:以MySQL InnoDB为例并发

做为演示,咱们继续使用以前的数据库表:product表学习

productId productName productPrice productCount
1 小米 1999 100
2 魅族 1999 100

首先咱们须要set autocommit=0,即不容许自动提交

有看过上一篇文章5分钟带你读懂事务隔离性与隔离级别 的同窗,能够看到最后咱们使用事务隔离级别时,所引伸出来的根本问题就是能够经过锁机制解决。

问题

在并发状况下回致使数据一致性的问题: 若是有A、B两个用户须要抢productId =1的小米手机,A、B用户都查询小米手机数量是100,A购买后修改商品的数量为99,B购买后修改数量为99。

用法

每次获取小米手机时,对该商品加排他锁。也就是在用户A获取获取 id=1 的小米手机信息时对该行记录加锁,期间其余用户阻塞等待访问该记录。代码以下:

start transaction;
 
select p.productCount from product p where p.productId = 1 for update;
 
update product p set p.productCount=p.productCount-1 where p.productId=1 ;
 
commit;

复制代码

操做

下面同时打开两个窗口模拟2个用户并发访问数据库

时间轴 事务A 事务B
T1 start transaction;
T2 select p.productCount from product p where p.productId = 1 for update;
T3 start transaction;
T4 select p.productCount from product p where p.productId = 1 for update;(等待中...)

流程说明

  1. 用户A start transaction开启一个事物。前一步咱们关闭了mysql的autocommit,因此须要手动控制事务的提交。
  2. 在得到小米手机信息(productId = 1 )时,进行数据加锁操做(for update)。与普通查询方式不一样,咱们使用了select…for update的方式,这样就经过数据库实现了悲观锁。在这个update事务提交以前其余外界是不能修改这条数据的,可是这种处理方式效率比较低,通常不推荐使用。
  3. 用户B start transaction开启一个事物。
  4. 用户B 也进行查询操做,此时处于等待中(阻塞状态)。ps:须要等待用户A事务提交后,才会执行。

注意:在事务中,只有select…for update(排他锁) 或lock in share mode(共享锁) 操做同一个数据时才会等待其它事务结束后才执行,通常select... 则不受此影响。例如在 T3中执行select p.productCount from product p where p.productId = 1;则能正常查询出数据,不会受第一个事务的影响。

2. 乐观锁(Optimistic Lock)

2.1 定义

百度百科:

乐观锁机制采起了更加宽松的加锁机制。乐观锁是相对悲观锁而言,也是为了不数据库幻读、业务处理时间过长等缘由引发数据处理错误的一种机制,但乐观锁不会刻意使用数据库自己的锁机制,而是依据数据自己来保证数据的正确性。

其余知识点

实现乐观锁通常来讲有如下2种方式:

  • 使用版本号 使用数据版本(Version)记录机制实现,这是乐观锁最经常使用的一种实现方式。何谓数据版本?即为数据增长一个版本标识,通常是经过为数据库表增长一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当咱们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,若是数据库表当前版本号与第一次取出来的version值相等,则予以更新,不然认为是过时数据。

  • 使用时间戳 乐观锁定的第二种实现方式和第一种差很少,一样是在须要乐观锁控制的table中增长一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version相似,也是在更新提交的时候检查当前数据库中数据的时间戳和本身更新前取到的时间戳进行对比,若是一致则OK,不然就是版本冲突。

2.2 案例分析

使用场景举例:以MySQL InnoDB为例

做为演示,咱们继续使用以前的数据库表:product表

productId productName productPrice productCount version
1 小米 1999 100 1
2 魅族 1999 100 2

咱们以版本号实现的方式进行说明。

操做

查询当前事务隔离级别:

SELECT @@tx_isolation;


结果:
REPEATABLE-READ

复制代码

下面同时打开两个窗口模拟2个用户并发访问数据库

第一种测试

时间轴 用户A 用户B
T1 start transaction;
T2 select * from product p where p.productId = 1;(productCount=100)
T3 update product p set p.productCount = 99,version=version+1 where p.productId = 1 and version = 1;(受影响的行: 1)
T4 start transaction;
T5 select * from product p where p.productId = 1;(productCount=100)
T6 update product p set p.productCount = 99,version=version+1 where p.productId = 1 and version = 1;(等待中...)
T7 commit;
T8 T6执行(受影响的行: 0)
T9 commit;

流程说明

  1. 事务A开启事务。
  2. 事务A查询当前小米手机数量为100。
  3. 事务A购买小米手机,小米手机数量更新为99。(此时并未提交事务)。
  4. 事务B开启事务。
  5. 事务B查询当前小米手机数量为100。
  6. 事务B购买小米手机,小米手机数量更新为99。注意:此时处于阻塞状态。
  7. 事务A提交事务。
  8. 此时第六步执行完毕,但并未成功(受影响的行: 0)。
  9. 事务B提交事务。

第二种测试

时间轴 用户A 用户B
T1 select * from product p where p.productId = 1;(productCount=100)
T2 update product p set p.productCount = 99,version=version+1 where p.productId = 1 and version = 1;(受影响的行: 1)
T3 select * from product p where p.productId = 1;(productCount=100)
T4 update product p set p.productCount = 99,version=version+1 where p.productId = 1 and version = 1;(受影响的行: 0)

乐观锁小结

  • 用户B修改数据的时候,受影响行数为0,对业务来讲,及更新失败。这时候咱们只须要告诉用户购买失败,从新查询一遍便可。
  • 对比第一种和第二种测试,咱们会发现第一种测试,将update语句放入事务中会出现阻塞的状况,而第二种测试不会出现阻塞状况。这是为何呢?update其实在不在事务中都无所谓,在内部是这样的:update是单线程的,及若是一个线程对一条数据进行update操做,会得到锁,其余线程若是要对同一条数据操做会阻塞,直到这个线程update成功后释放锁。

乐观锁不须要数据库底层的支持!

3. 适用场景

悲观锁

比较适合写入操做比较频繁的场景,若是出现大量的读取操做,每次读取的时候都会进行加锁,这样会增长大量的锁的开销,下降了系统的吞吐量。

乐观锁

比较适合读取操做比较频繁的场景,若是出现大量的写入操做,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层须要不断的从新获取数据,这样会增长大量的查询操做,下降了系统的吞吐量。

文末

本章节主要简单介绍了数据库中乐观锁与悲观锁的相关知识,后续咱们将会继续介绍数据库中的其余锁以及相关知识。例如行锁、表锁、死锁、

欢迎关注公众号:Coder编程 获取最新原创技术文章和相关免费学习资料,随时随地学习技术知识!

参考文章:

chenzhou123520.iteye.com/blog/186095…

chenzhou123520.iteye.com/blog/186340…

微信公众号

推荐阅读

带你了解数据库中JOIN的用法

带你了解数据库中事务的ACID特性

5分钟带你读懂事务隔离性与隔离级别

相关文章
相关标签/搜索