热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:面试官问:说说悲观锁乐观锁分布式锁?都在什么场景下使用?有什么技巧?

篇首语:本文由编程笔记#小编为大家整理,主要介绍了面试官问:说说悲观锁乐观锁分布式锁?都在什么场景下使用?有什么技巧?相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了面试官问:说说悲观锁乐观锁分布式锁?都在什么场景下使用?有什么技巧?相关的知识,希望对你有一定的参考价值。







来源 | www.cnblogs.com/jackyfei/p/12142840.html


如何确保一个方法,或者一块代码在高并发情况下,同一时间只能被一个线程执行,单体应用可以使用并发处理相关的 API 进行控制,但单体应用架构演变为分布式微服务架构后,跨进程的实例部署,显然就没办法通过应用层锁的机制来控制并发了。


那么锁都有哪些类型,为什么要使用锁,锁的使用场景有哪些?


锁类别


不同的应用场景对锁的要求各不相同,我们先来看下锁都有哪些类别,这些锁之间有什么区别。


  • 悲观锁(synchronize)

    • Java 中的重量级锁 synchronize

    • 数据库行锁


  • 乐观锁

    • Java 中的轻量级锁 volatile 和 CAS

    • 数据库版本号


  • 分布式锁(Redis锁)


乐观锁


就好比说是你是一个生活态度乐观积极向上的人,总是往最好的情况去想,比如你每次去获取共享数据的时候会认为别人不会修改,所以不会上锁,但是在更新的时候你会判断这期间有没有人去更新这个数据。


乐观锁使用在前,判断在后。我们看下伪代码:


reduce()
{
    select total_amount from table_1
    if(total_amount < amount ){
          return failed.  
    }  
    //其他业务逻辑
    update total_amount &#61; total_amount - amount where total_amount > amount; }

  • 数据库的版本号属于乐观锁&#xff1b;

  • 通过CAS算法实现的类属于乐观锁。


悲观锁


悲观锁是怎么理解呢&#xff1f;相对乐观锁刚好反过来&#xff0c;总是假设最坏的情况&#xff0c;假设你每次拿数据的时候会被其他人修改&#xff0c;所以你在每次共享数据的时候会对他加一把锁&#xff0c;等你使用完了再释放锁&#xff0c;再给别人使用数据。


悲观锁判断在前&#xff0c;使用在后。我们也看下伪代码&#xff1a;


reduce()
{
    //其他业务逻辑
    int num &#61; update total_amount &#61; total_amount - amount where total_amount > amount; 
   if(num &#61;&#61;1 ){
          //业务逻辑.  
    } 
}

  • Java中的的synchronize是重量级锁 &#xff0c;属于悲观锁&#xff1b;

  • 数据库行锁属于悲观锁&#xff1b;


扣减操作案例


这里举一个非常常见的例子&#xff0c;在高并发情况下余额扣减&#xff0c;或者类似商品库存扣减&#xff0c;也可以是资金账户的余额扣减。扣减操作会发生什么问题呢&#xff1f;很容易可以看到&#xff0c;可能会发生的问题是扣减导致的超卖&#xff0c;也就是扣减成了负数。


举个例子&#xff0c;比如我的库存数据只有100个。并发情况下第1笔请求卖出100个&#xff0c;第2批卖出100元&#xff0c;导致当前的库存数量为负数。遇到这种场景应该如何破解呢&#xff1f;这里列举四种方案。


方案1&#xff1a;同步排它锁


这时候很容易想到最简单的方案&#xff1a;同步排它锁(synchronize)。但是排他锁的缺点很明显&#xff1a;


  • 其中一个缺点是&#xff0c;线程串行导致的性能问题&#xff0c;性能消耗比较大。

  • 另一个缺点是无法解决分布式部署情况下跨进程问题&#xff1b;


方案2&#xff1a;数据库行锁


第二我们可能会想到&#xff0c;那用数据库行锁来锁住这条数据&#xff0c;这种方案相比排它锁解决了跨进程的问题&#xff0c;但是依然有缺点。


  • 其中一个缺点就是性能问题&#xff0c;在数据库层面会一直阻塞&#xff0c;直到事务提交&#xff0c;这里也是串行执行&#xff1b;

  • 第二个需要注意设置事务的隔离级别是Read Committed&#xff0c;否则并发情况下&#xff0c;另外的事务无法看到提交的数据&#xff0c;依然会导致超卖问题&#xff1b;

  • 缺点三是容易打满数据库连接&#xff0c;如果事务中有第三方接口交互(存在超时的可能性)&#xff0c;会导致这个事务的连接一直阻塞&#xff0c;打满数据库连接。

  • 最后一个缺点&#xff0c;容易产生交叉死锁&#xff0c;如果多个业务的加锁控制不好&#xff0c;就会发生AB两条记录的交叉死锁。


方案3&#xff1a;redis分布式锁


前面的方案本质上是把数据库当作分布式锁来使用&#xff0c;所以同样的道理&#xff0c;redis&#xff0c;zookeeper都相当于数据库的一种锁&#xff0c;其实当遇到加锁问题&#xff0c;代码本身无论是synchronize或者各种lock使用起来都比较复杂&#xff0c;所以思路是把代码处理一致性的问难题交给一个能够帮助你处理一致性的问题的专业组件&#xff0c;比如数据库&#xff0c;比如redis&#xff0c;比如zookeeper等。


这里我们分析下分布式锁的优缺点&#xff1a;


  • 优点&#xff1a;

    • 可以避免大量对数据库排他锁的征用&#xff0c;提高系统的响应能力&#xff1b;


  • 缺点&#xff1a;

    • 设置锁和设置超时时间的原子性&#xff1b;

    • 不设置超时时间的缺点&#xff1b;

    • 服务宕机或线程阻塞超时的情况&#xff1b;

    • 超时时间设置不合理的情况&#xff1b;



加锁和过期设置的原子性


redis加锁的命令setnx&#xff0c;设置锁的过期时间是expire&#xff0c;解锁的命令是del&#xff0c;但是2.6.12之前的版本中&#xff0c;加锁和设置锁过期命令是两个操作&#xff0c;不具备原子性。如果setnx设置完key-value之后&#xff0c;还没有来得及使用expire来设置过期时间&#xff0c;当前线程挂掉了或者线程阻塞&#xff0c;会导致当前线程设置的key一直有效&#xff0c;后续的线程无法正常使用setnx获取锁&#xff0c;导致死锁。


针对这个问题&#xff0c;redis2.6.12以上的版本增加了可选的参数&#xff0c;可以在加锁的同时设置key的过期时间&#xff0c;保证了加锁和过期操作原子性的。


但是&#xff0c;即使解决了原子性的问题&#xff0c;业务上同样会遇到一些极端的问题&#xff0c;比如分布式环境下&#xff0c;A获取到了锁之后&#xff0c;因为线程A的业务代码耗时过长&#xff0c;导致锁的超时时间&#xff0c;锁自动失效。后续线程B就意外的持有了锁&#xff0c;之后线程A再次恢复执行&#xff0c;直接用del命令释放锁&#xff0c;这样就错误的将线程B同样Key的锁误删除了。代码耗时过长还是比较常见的场景&#xff0c;假如你的代码中有外部通讯接口调用&#xff0c;就容易产生这样的场景。


设置合理的时长


刚才讲到的线程超时阻塞的情况&#xff0c;那么如果不设置时长呢&#xff0c;当然也不行&#xff0c;如果线程持有锁的过程中突然服务宕机了&#xff0c;这样锁就永远无法失效了。同样的也存在锁超时时间设置是否合理的问题&#xff0c;如果设置所持有时间过长会影响性能&#xff0c;如果设置时间过短&#xff0c;有可能业务阻塞没有处理完成&#xff0c;是否可以合理的设置锁的时间?


续命锁


这是一个很不容易解决的问题&#xff0c;不过有一个办法能解决这个问题&#xff0c;那就是续命锁&#xff0c;我们可以先给锁设置一个超时时间&#xff0c;然后启动一个守护线程&#xff0c;让守护线程在一段时间之后重新去设置这个锁的超时时间&#xff0c;续命锁的实现过程就是写一个守护线程&#xff0c;然后去判断对象锁的情况&#xff0c;快失效的时候&#xff0c;再次进行重新加锁&#xff0c;但是一定要判断锁的对象是同一个&#xff0c;不能乱续。


同样&#xff0c;主线程业务执行完了&#xff0c;守护线程也需要销毁&#xff0c;避免资源浪费&#xff0c;使用续命锁的方案相对比较而言更复杂&#xff0c;所以如果业务比较简单&#xff0c;可以根据经验类比&#xff0c;合理的设置锁的超时时间就行。


方案4&#xff1a;数据库乐观锁


数据库乐观锁加锁的一个原则就是尽量想办法减少锁的范围。锁的范围越大&#xff0c;性能越差&#xff0c;数据库的锁就是把锁的范围减小到了最小。我们看下面的伪代码


reduce()
{
    select total_amount from table_1
    if(total_amount < amount ){
          return failed.  
    }  
    //其他业务逻辑
    update total_amount &#61; total_amount - amount;  
}

我们可以看到修改前的代码是没有where条件的。修改后&#xff0c;再加where条件判断&#xff1a;总库存大于将被扣减的库存。


update total_amount &#61; total_amount - amount where total_amount > amount

如果更新条数返回0&#xff0c;说明在执行过程中被其他线程抢先执行扣减&#xff0c;并且避免了扣减为负数。


但是这种方案还会涉及一个问题&#xff0c;如果在之前的update代码中&#xff0c;以及其他的业务逻辑中还有一些其他的数据库写操作的话&#xff0c;那这部分数据如何回滚呢&#xff1f;


我的建议是这样的&#xff0c;你可以选择下面这两种写法&#xff1a;


  • 利用事务回滚写法&#xff1a;


我们先给业务方法增加事务&#xff0c;方法在扣减库存影响条数为零的时候扔出一个异常&#xff0c;这样对他之前的业务代码也会回滚。


reduce()
{
    select total_amount from table_1
    if(total_amount < amount ){
          return failed.  
    }  
    //其他业务逻辑
    int num &#61; update total_amount &#61; total_amount - amount where total_amount > amount;   if(num&#61;&#61;0) throw Exception;}

  • 第二种写法


reduce()
{
    //其他业务逻辑
    int num &#61; update total_amount &#61; total_amount - amount where total_amount > amount;    if(num &#61;&#61;1 ){
          //业务逻辑.  
    }  else{    throw Exception;  }
}

首先执行update业务逻辑&#xff0c;如果执行成功了再去执行逻辑操作&#xff0c;这种方案是我相对比较建议的方案。在并发情况下对共享资源扣减操作可以使用这种方法&#xff0c;但是这里需要引出一个问题&#xff0c;比如说万一其他业务逻辑中的业务&#xff0c;因为特殊原因失败了该怎么办呢&#xff1f;比如说在扣减过程中服务OOM了怎么办&#xff1f;


我只能说这些非常极端的情况&#xff0c;比如突然宕机中间数据都丢了&#xff0c;这种极少数的情况下只能人工介入&#xff0c;如果所有的极端情况都考虑到&#xff0c;也不现实。我们讨论的重点是并发情况下&#xff0c;共享资源的操作如何加锁的问题。


总结


最后我来给你总结一下&#xff0c;如果你可以非常熟练的解决这类问题&#xff0c;第一时间肯定想到的是&#xff1a;数据库版本号解决方案或者分布式锁的解决方案&#xff1b;但是如果你是一个初学者&#xff0c;相信你一定会第一时间考虑到Java中提供的同步锁或者数据库行锁。


END


免费领取 1000&#43; 道面试资料&#xff01;&#xff01;小编这里有一份面试宝典《Java 核心知识点.pdf》&#xff0c;覆盖了 JVM&#xff0c;锁、高并发、Spring原理、微服务、数据库、Zookeep人、数据结构等等知识点&#xff0c;包含 Java 后端知识点 1000&#43; 个&#xff0c;部分如下&#xff1a;




如何获取&#xff1f;加小编微信&#xff0c;回复【1024】






推荐阅读
  • 秒建一个后台管理系统?用这5个开源免费的Java项目就够了
    秒建一个后台管理系统?用这5个开源免费的Java项目就够了 ... [详细]
  • 从0到1搭建大数据平台
    从0到1搭建大数据平台 ... [详细]
  • Python 数据可视化实战指南
    本文详细介绍如何使用 Python 进行数据可视化,涵盖从环境搭建到具体实例的全过程。 ... [详细]
  • ### 优化后的摘要本学习指南旨在帮助读者全面掌握 Bootstrap 前端框架的核心知识点与实战技巧。内容涵盖基础入门、核心功能和高级应用。第一章通过一个简单的“Hello World”示例,介绍 Bootstrap 的基本用法和快速上手方法。第二章深入探讨 Bootstrap 与 JSP 集成的细节,揭示两者结合的优势和应用场景。第三章则进一步讲解 Bootstrap 的高级特性,如响应式设计和组件定制,为开发者提供全方位的技术支持。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 在当今的软件开发领域,分布式技术已成为程序员不可或缺的核心技能之一,尤其在面试中更是考察的重点。无论是小微企业还是大型企业,掌握分布式技术对于提升工作效率和解决实际问题都至关重要。本周的Java架构师实战训练营中,我们深入探讨了Kafka这一高效的分布式消息系统,它不仅支持发布订阅模式,还能在高并发场景下保持高性能和高可靠性。通过实际案例和代码演练,学员们对Kafka的应用有了更加深刻的理解。 ... [详细]
  • 本文详细介绍了Java代码分层的基本概念和常见分层模式,特别是MVC模式。同时探讨了不同项目需求下的分层策略,帮助读者更好地理解和应用Java分层思想。 ... [详细]
  • 本文详细介绍了Java反射机制的基本概念、获取Class对象的方法、反射的主要功能及其在实际开发中的应用。通过具体示例,帮助读者更好地理解和使用Java反射。 ... [详细]
  • 本文将带你快速了解 SpringMVC 框架的基本使用方法,通过实现一个简单的 Controller 并在浏览器中访问,展示 SpringMVC 的强大与简便。 ... [详细]
  • 阿里巴巴终面技术挑战:如何利用 UDP 实现 TCP 功能?
    在阿里巴巴的技术面试中,技术总监曾提出一道关于如何利用 UDP 实现 TCP 功能的问题。当时回答得不够理想,因此事后进行了详细总结。通过与总监的进一步交流,了解到这是一道常见的阿里面试题。面试官的主要目的是考察应聘者对 UDP 和 TCP 在原理上的差异的理解,以及如何通过 UDP 实现类似 TCP 的可靠传输机制。 ... [详细]
  • 2021年Java开发实战:当前时间戳转换方法详解与实用网址推荐
    在当前的就业市场中,金九银十过后,金三银四也即将到来。本文将分享一些实用的面试技巧和题目,特别是针对正在寻找新工作机会的Java开发者。作者在准备字节跳动的面试过程中积累了丰富的经验,并成功获得了Offer。文中详细介绍了如何将当前时间戳进行转换的方法,并推荐了一些实用的在线资源,帮助读者更好地应对技术面试。 ... [详细]
  • 观察 | 求职体验:收到录用通知的公司通常不深究技术细节,而那些详细追问的公司往往没有后续进展
    观察 | 求职体验:收到录用通知的公司通常不深究技术细节,而那些详细追问的公司往往没有后续进展 ... [详细]
  • DAO(Data Access Object)模式是一种用于抽象和封装所有对数据库或其他持久化机制访问的方法,它通过提供一个统一的接口来隐藏底层数据访问的复杂性。 ... [详细]
  • IOS Run loop详解
    为什么80%的码农都做不了架构师?转自http:blog.csdn.netztp800201articledetails9240913感谢作者分享Objecti ... [详细]
  • 本文总结了一些开发中常见的问题及其解决方案,包括特性过滤器的使用、NuGet程序集版本冲突、线程存储、溢出检查、ThreadPool的最大线程数设置、Redis使用中的问题以及Task.Result和Task.GetAwaiter().GetResult()的区别。 ... [详细]
author-avatar
无正道
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有