搞懂Java分布式锁实现看这篇文章就对了

前言:

随着微处理机技术的发展,人们只需花几百美元就能买到一个CPU芯片,这个芯片每秒钟执行的指令比80年代最大的大型机的处理机每秒钟所执行的指令还多。如果你愿意付出两倍的价钱,将得到同样的CPU,但它却以更高的时钟速率运行。因此,最节约成本的办法通常是在一个系统中使用集中在一起的大量的廉价CPU。所以,倾向于分布式系统的主要原因是它可以潜在地得到比单个的大型集中式系统好得多的性价比。实际上,分布式系统是通过较低廉的价格来实现相似的性能的。

随着互联网的兴起,越来越多的人使用者互联网产品。一般互联网系统都是分布式部署的,分布式部署确实能带来性能和效率上的提升,提升效率的同事,我们还需要注意,保证一个分布式环境下数据一致性的问题。

分布式锁简述

在单机时代,虽然不存在分布式锁,但也会面临资源互斥的情况,只不过在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就需要对这个资源进行加锁,当使用完资源之后,再解锁,其它线程就可以接着使用了。例如,在JAVA中,甚至专门提供了一些处理锁机制的一些API(synchronize/Lock等)。

但是到了分布式系统的时代,这种线程之间的锁机制,就没作用了,系统可能会有多份并且部署在不同的机器上,这些资源已经不是在线程之间共享了,而是属于进程之间共享的资源。因此,为了解决这个问题,「分布式锁」就强势登场了。

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。

分布式锁要满足哪些要求呢?

  • 排他性: 在同一时间只会有一个客户端能获取到锁,其它客户端无法同时获取
  • 避免死锁: 这把锁在一段有限的时间之后,一定会被释放(正常释放或异常释放)
  • 高可用: 获取或释放锁的机制必须高可用且性能佳
  • ...

目前相对主流的有三种,从实现的复杂度上来看,从上往下难度依次增加:

  • 数据库(MySQL)
  • Redis
  • ZooKeeper

v 基于数据库实现

基于数据库来做分布式锁的话,通常有两种做法:

  • 基于数据库的乐观锁
  • 基于数据库的悲观锁

乐观锁

乐观锁的特点先进行业务操作,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。

乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。当我们要从数据库中读取数据的时候,同时把这个version字段也读出来,如果要对读出来的数据进行更新后写回数据库,则需要将version加1,同时将新的数据与新的version更新到数据表中,且必须在更新的时候同时检查目前数据库里version值是不是之前的那个version,如果是,则正常更新。如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。

看图叙事。模拟实战场景。

如上图,故事男主人公(以下简称男主)打算去ATM机取3000元,故事女主人公(以下简称女主)则要在某宝买买买,买个包需要3000元,账户的余额是5000元。如果没有采用锁的话,在两人同时取款和买买买,可能会出现合计消费了6000,导致账户余额异常。所以需要用到锁的机制,当男主女主甚至更多小主同时消费时,除了读取到6000的账户余额外,还需要读取到当前的版本号version=1,等先行消费成功的主人公(无论谁先消费)去出发修改账户余额的同时,会触发version=version+1,即version=2。那么其他人使用未更新的version(1)去更新账户余额时就会发现版本号不对,就会导致本次更新失败,就得重新去读取最新账户余额以及版本号。

乐观锁遵循的两点法则:

  • 锁服务要有递增的版本号version
  • 每次更新数据的时候都必须先判断版本号对不对,然后再写入新的版本号

悲观锁

悲观锁的特点是先获取锁,再进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。

通常所说的“一锁二查三更新”即指的是使用悲观锁。通常来讲在数据库上的悲观锁需要数据库本身提供支持,即通过常用的 select ... for update 操作来实现悲观锁。当数据库执行 select for update 时会获取被 select 中的数据行的行锁,因此其他并发执行的 select for update 如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。 select for update 获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。

示例:

 
/** * 消费以后更新银行余额 * @param bankId 银行卡号 * @param cost 消费金额 * @return */ public boolean consume(Long bankId, Integer cost){ //先锁定银行账户 BankAccount product = query("SELECT * FROM bank_account WHERE bank_id=#{bankId} FOR UPDATE", bankId); if (product.getNumber() > 0) { int updateCnt = update("UPDATE tb_product_stock SET number=#{cost} WHERE product_id=#{productId}", cost, bankId); if(updateCnt > 0){ //更新库存成功 return true; } } return false; } 

乐观锁与悲观锁的区别

乐观锁的思路一般是表中增加版本字段,更新时where语句中增加版本的判断,算是一种CAS(Compare And Swep)操作,银行消费场景中version起到了版本控制的作用( AND version=#{version} )。

悲观锁之所以是悲观,在于他认为本次操作会发生并发冲突,所以一开始就对银行账户加上锁( SELECT ... FOR UPDATE ),然后就可以安心的做判断和更新,因为这时候不会有别人更新账户余额。

v 基于Redis实现

基于Redis实现的锁机制,主要是依赖redis自身的原子操作,例如:

SET user_key user_value NX PX 100

redis从2.6.12版本开始,SET命令才支持这些参数:

NX:只在在键不存在时,才对键进行设置操作, SET key value NX 效果等同于 SETNX key value

PX millisecond:设置键的过期时间为millisecond毫秒,当超过这个时间后,设置的键会自动失效

上述代码示例是指,当redis中不存在user_key这个键的时候,才会去设置一个user_key键,并且给这个键的值设置为 user_value,且这个键的存活时间为100ms

为什么这个命令可以帮我们实现锁机制呢?

因为这个命令是只有在某个key不存在的时候,才会执行成功。那么当多个进程同时并发的去设置同一个key的时候,就永远只会有一个进程成功。当某个进程设置成功之后,就可以去执行业务逻辑了,等业务逻辑执行完毕之后,再去进行解锁。

解锁很简单,只需要删除这个key就可以了,不过删除之前需要判断,这个key对应的value是当初自己设置的那个。

另外,针对redis集群模式的分布式锁,可以采用redis的 Redlock (可能会被墙)机制。

v 基于ZooKeeper实现

其实基于ZooKeeper,就是使用它的临时有序节点来实现的分布式锁。

原理

当某客户端要进行逻辑的加锁时,就在zookeeper上的某个指定节点的目录下,去生成一个唯一的临时有序节点, 然后判断自己是否是这些有序节点中序号最小的一个,如果是,则算是获取了锁。如果不是,则说明没有获取到锁,那么就需要在序列中找到比自己小的那个节点,并对其调用 exist() 方法,对其注册事件监听,当监听到这个节点被删除了,那就再去判断一次自己当初创建的节点是否变成了序列中最小的。如果是,则获取锁,如果不是,则重复上述步骤。

当释放锁的时候,只需将这个临时节点删除即可。

如上图,locker是一个持久节点, node_1/node_2/.../node_n 就是上面说的临时节点,由客户端client去创建的。

client_1/client_2/.../clien_n 都是想去获取锁的客户端。以client_1为例,它想去获取分布式锁,则需要跑到locker下面去创建临时节点(假如是node_1)创建完毕后,看一下自己的节点序号是否是locker下面最小的,如果是,则获取了锁。如果不是,则去找到比自己小的那个节点(假如是node_2),找到后,就监听node_2,直到node_2被删除,那么就开始再次判断自己的node_1是不是序列中最小的,如果是,则获取锁,如果还不是,则继续找一下一个节点。

总结:

分布式锁有很多种,开篇说的"相对主流的有三种"只是针对我所遇到的。分布式锁未来肯定是千变万化的,无论你身处一个什么样的公司,最开始的工作可能都得尽可能的从简单的做起。希望大家能根据所在公司业务场景,选择适合所在项目的方案。

顺便在此给大家推荐一个Java架构方面的交流学习群:698581634,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系,主要针对Java开发人员提升自己,突破瓶颈,相信你来学习,会有提升和收获。在这个群里会有你需要的内容  朋友们请抓紧时间加入进来吧。

原文地址:https://www.cnblogs.com/gp666/p/9710452.html

时间: 2024-10-10 12:00:54

搞懂Java分布式锁实现看这篇文章就对了的相关文章

Java分布式锁,搞懂分布式锁实现看这篇文章就对了

随着微处理机技术的发展,人们只需花几百美元就能买到一个CPU芯片,这个芯片每秒钟执行的指令比80年代最大的大型机的处理机每秒钟所执行的指令还多.如果你愿意付出两倍的价钱,将得到同样的CPU,但它却以更高的时钟速率运行.因此,最节约成本的办法通常是在一个系统中使用集中在一起的大量的廉价CPU.所以,倾向于分布式系统的主要原因是它可以潜在地得到比单个的大型集中式系统好得多的性价比.实际上,分布式系统是通过较低廉的价格来实现相似的性能的. 随着互联网的兴起,越来越多的人使用者互联网产品.一般互联网系统

搞懂Xamarin.Forms布局,看这篇应该就够了吧

Xamarin.Forms 布局介绍 什么是布局?可以简单的理解为,我们通过将布局元素有效的组织起来,让屏幕变成我们想要的样子! 我们通过画图的方式来描述一下Xamarin.Forms的布局. 小节锚点: 布局控件之StackLayout Xamarin.Forms 中可以C#代码进行布局 Xamarin.Forms 的布局方向 边距和填充 八种布局选项 布局控件之Grid 布局控件之AbsoluteLayout 布局控件之ScrollView 布局控件之RelativeLayout 布局控件之

java 分布式锁方案

第一步,自身的业务场景: 在我日常做的项目中,目前涉及了以下这些业务场景: 场景一: 比如分配任务场景.在这个场景中,由于是公司的业务后台系统,主要是用于审核人员的审核工作,并发量并不是很高,而且任务的分配规则设计成了通过审核人员每次主动的请求拉取,然后服务端从任务池中随机的选取任务进行分配.这个场景看到这里你会觉得比较单一,但是实际的分配过程中,由于涉及到了按用户聚类的问题,所以要比我描述的复杂,但是这里为了说明问题,大家可以把问题简单化理解.那么在使用过程中,主要是为了避免同一个任务同时被两

java 分布式锁 -图解- 秒懂

目录 写在前面 1.1. 分布式锁 简介 1.1.1. 图解:公平锁和可重入锁 模型 1.1.2. 图解: zookeeper分布式锁的原理 1.1.3. 分布式锁的基本流程 1.1.4. 加锁的实现 1.1.5. 释放锁的实现 1.1.1. 分布式锁的应用场景 写在最后 疯狂创客圈 亿级流量 高并发IM 实战 系列 疯狂创客圈 Java 分布式聊天室[ 亿级流量]实战系列之 -26[ 博客园 总入口 ] 写在前面 ? 大家好,我是作者尼恩.目前和几个小伙伴一起,组织了一个高并发的实战社群[疯狂

【转载】如果有人问你数据库的原理,叫他看这篇文章

原文:如果有人问你数据库的原理,叫他看这篇文章 本文由 伯乐在线 - Panblack 翻译,黄利民 校稿.未经许可,禁止转载!英文出处:Christophe Kalenzaga.欢迎加入翻译组. 一提到关系型数据库,我禁不住想:有些东西被忽视了.关系型数据库无处不在,而且种类繁多,从小巧实用的 SQLite 到强大的 Teradata .但很少有文章讲解数据库是如何工作的.你可以自己谷歌/百度一下『关系型数据库原理』,看看结果多么的稀少[译者注:百度为您找到相关结果约1,850,000个…] 

推荐:想了解一个项目完整测试流程,看这篇文章就OK了

推荐:想了解一个项目完整测试流程,看这篇文章就OK了 写在前面:本文来自真实企业测试人员的工作总结,用一个项目的进行流程为线索,记录每个阶段测试包含的内容及关注点. <ignore_js_op> 项目的测试流程大只包含的几个阶段:立项.需求评审.用例评审.测试执行.测试报告文档 一.立项后测试需要拿到的文档 1.需求说明书 2.原型图(及UI图) 3.接口文档 4.数据库字典(表的数量.缓存机制) 二.需求评审 参加人员:开发.测试及需求人员,由需求人员主持讲解. 为了会议的有效举行,测试及开

[ZZ]如果有人问你数据库的原理,叫他看这篇文章

如果有人问你数据库的原理,叫他看这篇文章 http://blog.jobbole.com/100349/ 文章把知识链都给串起来,对数据库做一个概述. 合并排序 阵列.树和哈希表 B+树索引概述 数据库的全局概述 基于成本的优化概述,特别专注了联接运算 缓冲池管理概述 事务管理概述

CSS如何设置列表样式属性,看这篇文章就够用了

原文:CSS如何设置列表样式属性,看这篇文章就够用了 列表样式属性# 在HTML中有2种列表.无序列表和有序列表,在工作中无序列表比较常用,无序列表就是ul标签和li标签组合成的称之为无序列表,那什么是有序列表呢?就是ol标签和li标签组合成的称之为有序列表,列表的基础知识就简单说明下,本章内容主要说明的是如何给列表设置样式,若有不懂列表是什么的园友笔者建议去W3school官网进行学习. 列表样式常用的属性有4种如:list-style-type.list-style-position.lis

CSS中如何使用背景样式属性,看这篇文章就够用了

原文:CSS中如何使用背景样式属性,看这篇文章就够用了 css背景样式属性介绍# 背景样式就是自定义HTML标签的背景颜色或背景图像. 背景属性说明表 属性名 属性值 描述 background-color #f00.red.rgb(255,0,0) 设置背景颜色. background-image url(背景图片路径) 设置背景图像. background-repeat repeat.repeat-x.repeat-y.no-repeat 设置背景图片是否平铺和平铺方向. backgroun