本文为《分布式中间件技术实战:Java版》的笔记, 适合初学者,可以对分布式技术场景先有个初步认识,了解技术的实际应用场景,后续遇到实际问题时再去深入了解技术细节。
实战要求与建议:
沉下心、坚持、多动手、多反思、多做笔记、享受代码、享受Bug
。
采用了目前比较流行的Spring Boot微框架作为实战中间件的奠基,在整合中间件的相关依赖并实践其相关功能组件时,还介绍了Spring Boot、Spring MVC、MyBatis、Redis、RabbitMQ、ZooKeeper、Redisson和MySQL等热门技术。
代码实现:https://gitee.com/steadyjack/middleware/tree/master#
走进分布式中间件
分布式系统发展历程
1) 单点集中式Web应用
这种系统架构有一个很明显的特点就是数据库(比如MySQL)及应用的War包都是共同部署在同一台服务器上,文件的上传存储也是上传到本台机器上。单点集中式Web应用系统架构的优点是适用于小型项目,发布便捷(只需要打包成War包,并进行解压即可),对于运维的工作量也比较小。其缺点在于若是该台服务器宕机了,整个应用将无法访问。
2) 应用与文件服务及数据库单独拆分
3) 引入缓存与集群,改善系统整体性能
引入缓存:把大量用户的读请求引导至缓存(如Redis)中,而写操作仍然直接写到数据库DB中。这点性能上的优化,可以将数据库的一部分数据或者系统经常需要访问的数据(如热点数据)放入缓存中,减少数据库的访问压力,提高用户并发请求性能。
引入集群:目的在于减少单台服务器的压力。
可以通过部署多台Tomcat来减少单机带来的压力,常见手段是Nginx+Lvs,最终是多台应用服务器构成了负载均衡,减少了单机的负载压力(需要注意的是,对于用户的Session需要调整为使用Redis或者Spring-Session进行管理)。
4) 数据库读写分离,并提供反向代理及CDN加速访问服务
经过调查发现,在大多数互联网应用系统中,用户的读请求数量往往大于写请求,它们会相互竞争,在这个时候往往写操作会受到影响,导致数据库出现存储瓶颈。DB的读写分离将有效地提高数据库的存储性能,而加入CDN与反向代理将加速系统的访问速度。
5) 分布式文件系统与分布式数据库
经过统计与监测,发现系统对于某些表有大量的请求,此时为了减少DB的压力,我们会进行分库分表,即根据业务来拆分数据库。
分布式系统特性
总的来说,分布式系统具有以下5个特性:
- 网络并没有那么可靠
- 节点故障无法避免
常见的网络问题有网络延时、丢包和消息丢失等。
常见中间件介绍
- Redis简介
Redis是一个开源(BSD许可)的、基于内存存储、采用Key-Value(键值对)格式存储的内存数据库,支持多种数据类型,包括字符串、哈希表、列表、集合、有序集合和位图等。 - Redisson简介
Redisson是“架设在Redis基础上的一个Java驻内存数据网络(In-Memory Data Grid)”,可以简单地理解为Redisson是Redis的一个升级版,它充分利用了Redis键值对数据库提供的一系列优势,为使用者提供了一系列具有分布式特性的常用工具类。 - RabbitMQ简介
RabbitMQ是一款应用相当广泛并开源的消息中间件,可用于实现消息异步分发、模块解耦、接口限流等功能。特别是在处理分布式系统高并发的业务场景时,RabbitMQ能够起到很好的作用,比如接口限流,从而降低应用服务器的压力;比如消息异步分发,从而降低系统的整体响应时间。 - ZooKeeper简介
ZooKeeper是一个开源的分布式应用程序协调服务,可以为分布式应用提供一致性服务,简称ZK。其提供的功能服务包括配置维护、域名服务、分布式同步等;提供的接口则包括分布式独享锁、选举、队列等。
搭建微服务项目
目前应用比较广泛的项目搭建规范主要是“基于Maven构建多模块”的方式,这种方式搭建的每个模块各司其职,负责应用的不同功能,同时每个模块采用层级依赖的方式,最终构成一个聚合型的Maven项目。
缓存中间件Redis
Redis是一款免费、开源、遵循BSD协议的高性能结构化存储数据库,可以满足目前企业大部分应用中对于高性能数据存储的需求。同时,它也是NoSQL(Not Only SQL),即非关系型数据库的一种,内置多种丰富多彩的数据结构,如字符串String、列表List、集合Set、散列Hash等,可以高效地解决企业应用频繁读取数据库而带来的诸多问题。
由于Redis是基于内存的、采用Key-Value结构化存储的NoSQL数据库,加上其底层采用单线程和多路I/O复用模型,所以Redis的查询速度很快。
根据Redis官方提供的数据,它可以实现每秒查询的次数达到10万次,即QPS为100000+,这在某种程度上足以满足大部分的高并发请求。
概括来讲,Redis具有以下4种典型的应用场景。
- 热点数据的存储与展示
- 最近访问的数据
热点数据”可以理解为大部分用户频繁访问的数据,这些数据对于所有的用户来说,访问将得到同一个结果,比如“微博热搜”(每个用户在同一时刻的热搜是一样的),如果采用传统的“查询数据库”的方法获取热点数据,将大大增加数据库的压力,而降低数据库的读写性能。采用Redis的List作为“最近访问的足迹”的数据结构,将大大降低数据库频繁的查询请求。 - 并发访问
对于高并发访问某些数据的情况,Redis可以将这些数据预先装载在缓存中,每次高并发过来的请求则可以直接从缓存中获取,减少高并发访问给数据库带来的压力。 - 排名
采用Redis的有序集合(Sorted Set)可以很好地实现用户的排名,避免了传统的基于数据库级别的Order By及Group By查询所带来的性能问题。
除此之外,Redis还有诸多应用场景,比如消息队列、分布式锁等。Redis的使用
Spring Boot有一大特性,称为“自定义注入Bean配置”,即Java Config Bean的自定义注入配置,它可以允许特定Bean组件的属性自定义为用户指定的取值。在Spring Boot项目整合第三方依赖或主流框架时,这一特性几乎随处可见,Redis也不例外!
对于Spring Boot项目整合Redis,最主要的Bean操作组件莫过于RedisTemplate跟StringRedisTemplate,后者其实是前者的一种特殊体现。而在项目中使用Redis的过程中,一般情况下是需要自定义配置上述两个操作Bean组件的,比如指定缓存中Key与Value的序列化策略等配置。
StringRedisTemplate,顾名思义是RedisTemplate的特例,专门用于处理缓存中Value的数据类型为字符串String的数据,包括String类型的数据,和序列化后为String类型的字符串数据。单击StringRedisTemplate类查看其底层源代码,可以发现其继承了RedisTemplate类,所以StringRedisTemplate也能通过opsForValue()方法获取ValueOperations操作组件类,进而实现相关业务操作。Redis常见数据结构
- 字符串
其实,对于字符串信息的存储,在实际的项目中应用还是很广泛的,因为在Java世界里“到处都是对象”,甚至从数据库中查询出来的结果都可以封装成Java中的对象,而对象又可以通过ObjectMapper等JSON解析框架进行序列化与反序列化,从而将对象转化为JSON格式的字符串,或者将序列化后的字符串反序列化解析为对象。 - 列表
Redis的列表类型跟Java的List类型很类似,用于存储一系列具有相同类型的数据。其底层对于数据的存储和读取可以理解为一个“数据队列”,往List中添加数据时,即相当于往队列中的某个位置插入数据(比如从队尾插入数据);而从List获取数据时,即相当于从队列中的某个位置中获取数据(比如从队头获取数据)。
在Spring Boot整合Redis的项目使用List类型存储数据时,可以通过push添加、pop获取等操作存储获取的数据。在实际应用场景中,Redis的列表List类型特别适用于“排名”“排行榜”“近期访问数据列表”等业务场景,是一种很实用的存储类型。 - 集合
Redis中的集合Set存储的数据是唯一的,其底层的数据结构是通过哈希表来实现的,所以其添加、删除、查找操作的复杂度均为O(1)。
在实际互联网应用中,Redis的Set类型常用于解决重复提交、剔除重复ID等业务场景。 有序集合
Redis的有序集合SortedSet跟集合Set具有某些相同的特性,即存储的数据是不重复、无序、唯一的;而这两者的不同之处在于SortedSet可以通过底层的Score(分数/权重)值对数据进行排序,实现存储的集合数据既不重复又有序,可以说其包含了列表List、集合Set的特性。
默认情况下,SortedSet的排序类型是根据得分Score参数的取值从小到大排序,如果需要倒序排列,则可以调用reverseRange()方法即可!
在实际生产环境中,Redis的有序集合SortedSet常用于充值排行榜、积分排行榜、成绩排名等应用场景。哈希Hash存储
Redis的哈希存储跟Java的HashMap数据类型有点类似,其底层数据结构是由Key-Value组成的映射表,而其Value又是由Filed-Value对构成,特别适用于具有映射关系的数据对象的存储。
在实际互联网应用,当需要存入缓存中的对象信息具有某种共性时,为了减少缓存中Key的数量,应考虑采用Hash哈希存储。
Key失效与判断是否存在:
在某些业务场景下,缓存中的Key对应的数据信息并不需要永久保留,这个时候就需要对缓存中的这些Key进行“清理”。在Redis缓存体系结构中,Delete与Expire操作都可以用于清理缓存中的Key,这两者不同之处在于Delete操作需要人为手动触发,而Expire只需要提供一个TTL,即“过期时间”,就可以实现Key的自动失效,也就是自动被清理。
通过redisTemplate.hasKey()方法,传入一个Key的名称参数,即可获取缓存中该Key是否仍然存在。
Redis实战场景之缓存穿透
1 什么是缓存穿透
“当查询数据库时如果没有查询到数据,则直接返回Null给前端用户,流程结束”,如果前端频繁发起访问请求时,恶意提供数据库中不存在的Key,则此时数据库中查询到的数据将永远为Null。由于Null的数据是不存入缓存中的,因而每次访问请求时将查询数据库,如果此时有恶意攻击,发起“洪流”式的查询,则很有可能会对数据库造成极大的压力,甚至压垮数据库。这个过程称之为“缓存穿透”,顾名思义,就像是“永远越过了缓存而直接永远地访问数据库”。
2 缓存穿透的解决方案
即“当查询数据库时如果没有查询到数据,则将Null返回给前端用户,同时将该Null数据塞入缓存中,并对对应的Key设置一定的过期时间,流程结束”。
3 其他典型问题介绍
(1)缓存雪崩:指的是在某个时间点,缓存中的Key集体发生过期失效致使大量查询数据库的请求都落在了DB(数据库)上,导致数据库负载过高,压力暴增,甚至有可能“压垮”数据库。
所以为了更好地避免这种问题的发生,一般的做法是为这些Key设置不同的、随机的TTL(过期失效时间),从而错开缓存中Key的失效时间点,可以在某种程度上减少数据库的查询压力。
(2)缓存击穿:指缓存中某个频繁被访问的Key(可以称为“热点Key”),在不停地扛着前端的高并发请求,当这个Key突然在某个瞬间过期失效时,持续的高并发访问请求就“穿破”缓存,直接请求数据库,导致数据库压力在某一瞬间暴增。这种现象就像是“在一张薄膜上凿出了一个洞”。
在实际情况中,既然这个Key可以被当作“热点”频繁访问,那么就应该设置这个Key永不过期,这样前端的高并发请求将几乎永远不会落在数据库上。
Jmeter压力测试与高并发
Apache Jmeter是Apache组织开发的基于Java的压力测试工具。它可以通过产生来自不同类别的压力,模拟实际生产环境中高并发产生的巨大负载,从而对应用服务器、网络或对象整体性能进行测试,并对产生的测试结果进行分析和反馈。
使用Jmeter压力测试工具之前,需要前往Apache官网获取Jmeter压力测试工具包。访问 http://jmeter.apache.org/download_jmeter.cgi ,选择最新的版本单击下载即可。
下载完成之后将其解压到某个磁盘目录下,双击进入该文件夹,找到bin文件目录,然后双击jmeter.sh文件,会弹出DOS界面,此时是Jmeter在做一些初始化工作,稍微等待一定的时间,即可进入Jmeter的操作界面。
优化方案介绍
同一时刻多个并发的线程对共享资源进行了访问操作,导致最终出现数据不一致或者结果并非自己所预料的现象,而这其实就是多线程高并发时出现的并发安全问题。
在传统的单体Java应用中,为了解决多线程高并发的安全问题,最常见的做法是在核心的业务逻辑代码中加锁操作(同步控制操作),即加Synchronized关键字。
因为Synchronized关键字是跟单一服务节点所在的JVM相关联,而分布式系统架构下的服务一般是部署在不同的节点(服务器)下,从而当出现高并发请求时,Synchronized同步操作将显得“力不从心”!
分布式锁其实是一种解决方案,它的出现主要是为了解决分布式系统中高并发请求时并发访问共享资源导致并发安全的问题。目前关于分布式锁的实现有许多种,典型的包括基于数据库级别的乐观锁和悲观锁,以及基于Redis的原子操作实现分布式锁和基于ZooKeeper实现分布式锁等。
- 优化方案之Redis分布式锁
由于Redis底层架构是采用单线程进行设计的,因而它提供的这些操作也是单线程的,即其操作具备原子性。而所谓的原子性,指的是同一时刻只能有一个线程处理核心业务逻辑,当有其他线程对应的请求过来时,如果前面的线程没有处理完毕,那么当前线程将进入等待状态(堵塞),直到前面的线程处理完毕。当多个并发的线程同一时刻调用setIfAbsent()时,Redis的底层是会将线程加入“队列”排队处理的。消息中间件RabbitMQ
1 RabbitMQ简介
RabbitMQ的官网:https://www.rabbitmq.com/ 。
RabbitMQ在如今盛行的分布式系统中主要起到存储分发消息、异步通信和解耦业务模块等作用,在易用性、扩展性和高可用性等方面均表现不俗。
RabbitMQ还是一款开源并实现了高级消息队列协议(Advanced Message Queuing Protocol,AMQP)的消息中间件,既支持单一节点的部署,同时也支持多个节点的集群部署,在某种程度上完全可以满足目前互联网应用或产品高并发、大规模和高可用性的要求,其服务端是采用Erlang语言开发的,支持多种编程语言的应用系统调用其提供的API接口。
除此之外,RabbitMQ的开发团队还为其内置了人性化的后端管理控制台,或者称为“面向开发者的消息客户端”,可以用于实现RabbitMQ的队列、交换机、路由、消息和服务节点的管理等,同时也可以通过客户端管理相应的用户(主要是分配相应的操作权限和数据管理权限等)。
2 典型应用场景介绍
1.异步通信和服务解耦
2.接口限流和消息分发
以“用户注册”为实际场景,其核心的业务逻辑在于“判断用户注册信息的合法性并将信息写入数据库”,而“发送邮件”和“短信验证”服务在某种程度上并不归属于“用户注册”的核心流程,因而可以将相应的服务从其中解耦出来,并采用消息中间件如RabbitMQ进行异步通信,系统接口的整体响应时间也明显降低了许多,即实现了“低延迟”。从用户的角度上看,这将给用户带来很好的体验效果。
以“商城用户抢购商品”为例,在高并发的情况下,这些业务操作会给系统带来诸多的问题。比如,商品超卖、数据不一致、用户等待时间长、系统接口挂掉等现象。因而这种单一的处理流程只适用于同一时刻前端请求量很少的情况,而对于类似商城抢购、商品秒杀等某一时刻产生高并发请求的情况则显得力不从心。
RabbitMQ的引入主要是从以下两个方面来优化系统的整体处理流程:
(1)接口限流:当前端产生高并发请求时,并不会像“无头苍蝇”一样立即到达后端系统接口,而是像每天上班时的地铁限流一样,将这些请求按照先来后到的规则加入RabbitMQ的队列,即在某种程度上实现“接口限流”。
(2)消息异步分发:当商品库存充足时,当前抢购的用户将可以抢到该商品,之后会异步地通过发送短信、发送邮件等方式通知用户抢购成功,并告知用户尽快付款,即在某种程度上实现了“消息异步分发”。
3.业务延迟处理
比如用户抢到火车票后,由于各种原因而迟迟没有付款,过了30分钟后仍然没有支付车票的价格,导致系统自动取消该笔订单。类似这种“需要延迟一定的时间后再进行处理”的业务在实际生产环境中并不少见,传统企业级应用对于这种业务的处理,是采用一个定时器定时去获取没有付款的订单,并判断用户的下单时间距离当前的时间是否已经超过30分钟,如果是,则表示用户在30分钟内仍然没有付款,系统将自动使该笔订单失效并回收该张车票
早期的很多抢票软件每当赶上春运高峰期时,经常会出现“网站崩溃”“单击购买车票后却一直没响应”等状况,某种程度上是因为在某一时刻产生的高并发,或者定时频繁拉取数据库得到的数据量过大等状况,导致内存、CPU、网络和数据库服务等负载过高所引起的。
RabbitMQ的引入主要是替代了传统处理流程的“定时器处理逻辑”,取而代之的是采用RabbitMQ的延迟队列进行处理。延迟队列,顾名思义指的是可以延迟一定的时间再处理相应的业务逻辑。
3 RabbitMQ后端控制台介绍
由于RabbitMQ是采用Erlang语言开发的、遵循AMQP协议并建立在强大的Erlang OTP平台的消息队列,因而在安装RabbitMQ服务之前,需要先安装Erlang,之后再前往官方网站下载相应的RabbitMQ安装包即可安装。安装过程中,需要同时安装RabbitMQ的管理控制台,即RabbitMQ的后端控制台,其提供的人性化界面主要用于更好地管理消息、队列、交换机、路由、通道、消费者实例和用户等信息。
安装完成后,打开浏览器,在地址栏输入http://127.0.0.1:15672/ 并回车,输入guest/guest(即默认的用户名和密码),单击Login,即可进入RabbitMQ的后端管理控制台
4 基于Spring的事件驱动模型实战
Spring的事件驱动模型,顾名思义是通过“事件驱动”的方式实现业务模块之间的交互,交互的方式有同步和异步两种,某种程度上,“事件”也可以看作是“消息”。
在传统企业级Spring应用系统中,正是通过事件驱动模型实现信息的异步通信和业务模块的解耦,此种模型跟RabbitMQ的消息模型有几分相似之处。
Spring的事件驱动模型主要由3部分组成,包括发送消息的生产者、消息(或事件)和监听接收消息的消费者,这三者是绑定在一起的,可以说是“形影不离”。
5 Spring Boot项目整合RabbitMQ
- RabbitMQ相关词汇介绍
RabbitMQ的核心要点其实在于消息、消息模型、生产者和消费者,而RabbitMQ的“消息模型”有许多种,包括基于FanoutExchange的消息模型、基于DirectExchange的消息模型和基于TopicExchange的消息模型等,这些消息模型都有一个共性,那就是它们几乎都包含交换机、路由和队列等基础组件。
RabbitMQ在实际应用开发中涉及的这些核心基础组件。
● 生产者:用于产生、发送消息的程序。
● 消费者:用于监听、接收、消费和处理消息的程序。
● 消息:可以看作是实际的数据,如一串文字、一张图片和一篇文章等。在RabbitMQ底层系统架构中,消息是通过二进制的数据流进行传输的。
● 队列:消息的暂存区或者存储区,可以看作是一个“中转站”。
消息经过这个“中转站”后,便将消息传输到消费者手中。
● 交换机:同样也可以看作是消息的中转站点,用于首次接收和分发消息,其中包括Headers、Fanout、Direct和Topic这4种。
● 路由:相当于密钥、地址或者“第三者”,一般不单独使用,而是与交换机绑定在一起,将消息路由到指定的队列。
RabbitMQ的消息模型主要是由队列、交换机和路由三大基础组件组成。
项目中自定义注入和配置Bean相关组件:在Spring Boot项目为了能方便地使用RabbitMQ的相关操作组件并跟踪消息在发送过程中的状态,可以将需要加入自定义配置的Bean组件放到RabbitmqConfig配置类中。当RabbitMQ需要处理高并发的业务场景时,可以通过配置“多消费者实例”的方式来实现;而在正常的情况下,对消息不需要并发监听消费处理时,则只需要配置“单一消费者实例”的容器工厂即可。
RabbitMQ在实际的应用系统中,除了可以采用发送字节型(通过getBytes()方法或者序列化方法)的消息和采用@RabbitListener接收字节数组类型的消息之外,还可以通过发送、接收“对象类型”的方式实现消息的发送和接收。
如果需要发送对象类型的消息,则需要借助RabbitTemplate的convertAndSend方法,该方法通过MessagePostProcessor的实现类直接指定待发送消息的类型。
- 基于FanoutExchange的消息模型
FanoutExchange,顾名思义,是交换机的一种,具有“广播消息”的作用,即当消息进入交换机这个“中转站”时,交换机会检查哪个队列跟自己是绑定在一起的,找到相应的队列后,将消息传输到相应的绑定队列中,并最终由队列对应的消费者进行监听消费。
此种交换机具有广播式的作用,纵然为其绑定了路由,也是不起作用的。所以,严格地讲,基于FanoutExchange的模型不能称为真正的“消息模型”,但是该消息模型中仍旧含有交换机、队列和“隐形的路由”,因而在这里我们也将其当作消息模型中的一种。
基于FanoutExchage消息模型主要的核心组件是交换机和队列,一个交换机可以对应并绑定多个队列,从而对应多个消费者!
此种消息模型适用于“业务数据需要广播式传输”的场景,比如“用户操作写日志”。 - 基于DirectExchange的消息模型
DirectExchange 具有“直连传输消息”的作用,即当消息进入交换机这个“中转站”时,交换机会检查哪个路由跟自己绑定在一起,并根据生产者发送消息指定的路由进行匹配,如果能找到对应的绑定模型,则将消息直接路由传输到指定的队列,最终由队列对应的消费者进行监听消费。
需要且必须要指定特定的交换机和路由,并绑定到指定的队列中。或许是由于这种严格意义上的要求,基于DirectExchange消息模型在实际生产环境中具有很广泛的应用。
此种消息模型适用于业务数据需要直接传输并被消费的场景,比如业务服务模块之间的信息交互,一般业务服务模块之间的通信是直接、实时的,因而可以借助基于DirectExchange的消息模型进行通信。事实上,在实际应用系统中,几乎90%的业务场景中,凡是需要RabbitMQ实现消息通信的,都可以采用DirectExchange消息模型实现,这或许是它被称之为“正规军”的缘由。
- 基于TopicExchange的消息模型
TopicExchange也是RabbitMQ交换机的一种,是一种“发布-主题-订阅”式的交换机,在实际生产环境中同样具有很广泛的应用。
TopicExchange消息模型同样是由交换机、路由和队列严格绑定构成,与前面介绍的另外两种消息模型相比,最大的不同之处在于其支持“通配式”的路由,即可以通过为路由的名称指定特定的通配符“*”和“#”
,从而绑定到不同的队列中。其中,通配符“*”
表示一个特定的“单词”,而通配符“#”则可以表示任意的单词(可以是一个,也可以是多个,也可以没有)。某种程度上讲“#”通配符表示的路由范围大于等于“*”
通配符表示的路由范围,即前者可以包含后者。
如果说基于DirectExchange的消息模型在RabbitMQ诸多消息模型中属于“正规军”,那么基于TopicExchange的消息模型则可以号称是“王牌军”,可以“统治正规军”。而事实上也正是如此,这主要是因为其通配符起到的功效:当这种消息模型的路由名称包含“*”
时,由于“*”
相当于一个单词,因而此时此种消息模型将降级为“基于DirectExchange的消息模型”;当路由名称包含“#”时,由于#相当于0个或者多个单词,因而此时此种消息模型将相当于“基于FanoutExchange的消息模型”,即此时绑定的路由将不起作用了,哪怕进行了绑定,也不再起作用!
TopicExchange消息模型适用于“发布订阅主题式”的场景,在实际应用系统中,几乎所有的业务场景都适用,而且从上述代码实现中可以得知,TopicExchange消息模型包含了FanoutExchange消息模型和DirectExchange消息模型的功能特性,即凡是这两种消息模型适用的业务场景,基于TopicExchange的消息模型也是适用的,从某种程度上讲,TopicExchange模型是一种强有力的、具有普适性的消息模型。
6 RabbitMQ确认消费机制
消息高可用和确认消费
RabbitMQ会要求生产者在发送完消息之后进行“发送确认”,当确认成功时即代表消息已经成功发送出去了!如何保证RabbitMQ队列中的消息“不丢失”;RabbitMQ则是强烈建议开发者在创建队列、交换机时设置其持久化参数为true,即durable参数取值为true。
除此之外,在创建消息时,RabbitMQ要求设置消息的持久化模式为“持久化”,从而保证RabbitMQ服务器出现崩溃并执行重启操作之后,队列、交换机仍旧存在而且消息不会丢失。
如何保证消息能够被准备消费、不重复消费,RabbitMQ则是提供了“消息确认机制”,即ACK模式。RabbitMQ的消息确认机制有3种,分别是NONE(无须确认)、AUTO(自动确认)和MANUAL(手动确认),不同的确认机制,其底层的执行逻辑和实际应用场景是不相同的。
在实际生产环境中,为了提高消息的高可用、防止消息重复消费,一般都会使用消息的确认机制,只有当消息被确认消费后,消息才会从队列中被移除,这也是避免消息被重复消费的实现方式。
常见的确认消费模式介绍
RabbitMQ的消息“确认消费”模式有3种,它们定义在AcknowledgeMode枚举类中,分别是NONE、AUTO和MANUAL,这3种模式的含义、作用和应用场景是不同的。
NONE指的是“无须确认”机制,即生产者将消息发送至队列,消费者监听到该消息时,无须发送任何反馈信息给RabbitMQ服务器。
AUTO指的是“自动确认”机制,即生产者将消息发送至队列,消费者监听到该消息时,需要发送一个AUTO ACK的反馈信息给RabbitMQ服务器,之后该消息将在RabbitMQ的队列中被移除。其中,这种发送反馈信息的行为是RabbitMQ“自动触发”的,即其底层的实现逻辑是由RabbitMQ内置的相关组件实现自动发送确认反馈信息。
MANUAL消费模式。它是一种“人为手动确认消费”机制,即生产者将消息发送至队列,消费者监听到该消息时需要手动地“以代码的形式”发送一个ACK的反馈信息给RabbitMQ服务器,之后该消息将在RabbitMQ的队列中被移除,同时告知生产者,消息已经成功发送并且已经成功被消费者监听消费了。
- 基于自动确认消费模式实战
基于AUTO的自动确认消费机制其实是RabbitMQ提供的一种默认确认消费机制,由于其使用起来比较简单,因而在实际的生产环境中具有很广泛的应用。
此种消息模式其实只需要在RabbitmqConfig配置类中创建监听器容器工厂实例,并在该实例中指定消息的确认消费模式为AUTO即可,而其他的如发送消息、监听消费消息的流程则没有太大的变化。
在Spring Boot整合RabbitMQ的项目中,除了采用这种方式指定消费者的确认消费模式之外,还可以通过在配置文件application.properties中配置全局的消息确认消费模式 - 基于手动确认消费模式实战
“基于MANUAL确认消费模式”需要在执行完实际的业务逻辑之后,手动调用相关的方法进行确认消费,哪怕是在处理的过程中发生了异常,也需要执行“确认消费”,避免消息一直留在队列从而出现重复消费的现象!死信队列/延迟队列实战
死信队列简介与作用
死信队列又称之为延迟队列、延时队列,也是RabbitMQ队列中的一种,指进入该队列中的消息会被延迟消费的队列。这种队列跟普通的队列相比,最大的差异在于消息一旦进入普通队列将会立即被消费处理,而延迟队列则是会过一定的时间再进行消费。
相对于传统定时器的轮询处理方式,死信队列具有占用系统资源少(比如不需要再轮询数据库获取数据,减少DB层面资源的消耗)、人为干预很少(只需要搭建好死信队列消息模型,就可以不需要再去干预了),以及自动消费处理(当指定的延迟时间一到,消息将自动被路由到实际的队列进行处理)等优势。 - 死信队列专有词汇介绍
与普通的队列相比,死信队列同样也具有消息、交换机、路由和队列等专有名词,只不过在死信队列里增加了另外3个成员,即DLX、DLK和TTL。其中DLX跟DLK是必需的成员,而TTL则是可选、非必需的。
● DLX,即Dead Letter Exchange,中文为死信交换机,是交换机的一种类型,只是属于特殊的类型。
● DLK,即Dead Letter Routing-Key,中文为死信路由,同样也是一种特殊的路由,主要是跟DLX组合在一起构成死信队列。
● TTL,即Time To Live,指进入死信队列中的消息可以存活的时间。当TTL一到,将意味着该消息“死了”,从而进入下一个“中转站”,等待被真正的消息队列监听消费。
- 当消息在一个队列中发生以下几种情况时,才会出现“死信”的情况:
● 消息被拒绝(比如调用basic.reject或者basic.nack方法时即可实现)并且不再重新投递,即requeue参数的取值为false。
● 消息超过了指定的存活时间(比如通过调用messageProperties.setExpiration()设置TTL时间即可实现)。
● 队列达到最大长度了。
当发生上述情况时,将出现“死信”的情况,而之后消息将被重新投递(publish)到另一个Exchange,即交换机,此时该交换机就是DLX,即死信交换机。
简单地说就是没有被死信队列消费的消息将换个“地方”重新被消费,从而实现消息“延迟、延时”消费的功能,而这个“地方”就是消息的下一个中转站,即“死信交换机”。
对于RabbitMQ的死信队列,简单理解就是消息一旦进入死信队列,将会等待TTL时间,而TTL一到,消息将会进入死信交换机,然后被路由到绑定的真正队列中,最终被真正的队列对应的消费者监听消费。
值得一提的是,TTL既可以设置成为死信队列的一部分,也可以在消息中单独进行设置,当队列跟消息同时都设置了存活时间TTL时,则消息的“最大生存时间”或者“存活时间”将取两者中较短的时间。
分布式锁实战
在互联网和移动互联网时代,企业级应用系统大多数是采用集群和分布式的方式进行部署,将“业务高度集中”的传统企业级应用按照业务拆分成多个子系统,并进行独立部署。而为了应对某些业务场景下产生的高并发请求,通常一个子系统会部署多份实例,并采用某种均衡机制“分摊”处理前端用户的请求,此种方式俗称“集群”。然而,任何事情都并非十全十美的,正如服务集群、分布式系统架构一样,虽然可以给企业应用系统带来性能、质量和效率上的提升,但由此也带来了一些棘手的问题,其中比较典型的问题是高并发场景下多个线程并发访问、操作共享资源时,出现数据不一致的现象。
随着用户、数据量的增长,企业应用为了适应各种变化,不得不对传统单一的应用系统进行拆分并作分布式部署,而此种分布式系统架构部署方案的落地在带来性能和效率上提升的同时也带来了一些问题,即传统采取加锁的方式将不再起作用。这是因为集群、分布式部署的服务实例一般是部署在不同机器上的,在分布式系统架构下,此种资源共享将不再是传统的线程共享,而是跨JVM进程之间资源的共享了。因此,为了解决这种问题,我们将引入“分布式锁”的方式进行控制。
- 锁机制
在单体应用时代,传统企业级Java应用为了解决“高并发下多线程访问共享资源时出现数据不一致”的问题,通常是借助JDK自身提供的关键字或者并发工具类Synchronized、Lock和RetreenLock等加以实现,这种访问控制机制业界普遍亲切地称之为“锁”。 - 分布式锁
对于分布式锁的设计与使用,业界普遍有几点要求
● 排他性:这一点跟单体应用时代加的“锁”是一个道理,即需要保证分布式部署、服务集群部署的环境下,被共享的资源如数据或者代码块在同一时间内只能被一台机器上的一个线程执行。
● 避免死锁:指的是当前线程获取到锁之后,经过一段有限的时间(该时间一般用于执行实际的业务逻辑),一定要被释放(正常情况或者异常情况下释放)。
● 高可用:指的是获取或释放锁的机制必须高可用而且性能极佳。
● 可重入:指的是该分布式锁最好是一把可重入锁,即当前机器的当前线程在彼时如果没有获取到锁,那么在等待一定的时间后一定要保证可以再被获取到。
● 公平锁(可选):这并非硬性的要求,指的是不同机器的不同线程在获取锁时最好保证几率是一样的,即应当保证来自不同机器的并发线程可以公平获取到锁。
- 分布式锁常见的几种实现方式
● 基于数据库级别的乐观锁:主要是通过在查询、操作共享数据记录时带上一个标识字段version,通过version来控制每次对数据记录执行的更新操作。
● 基于数据库级别的悲观锁:在这里以MySQL的InnoDB引擎为例,它主要是通过在查询共享的数据记录时加上For Update字眼,表示该共享的数据记录已经被当前线程锁住了(行级别锁、表级别锁),只有当该线程操作完成并提交事务之后,才会释放该锁,从而其他线程才能获取到该数据记录。
● 基于Redis的原子操作:主要是通过Redis提供的原子操作SETNX与EXPIRE来实现。SETNX表示只有当Key在Redis不存在时才能设置成功,通常这个Key需要设计为与共享的资源有联系,用于间接地当作“锁”,并采用EXPIRE操作释放获取的锁。
● 基于ZooKeeper的互斥排它锁:这种机制主要是通过ZooKeeper在指定的标识字符串(通常这个标识字符串需要设计为跟共享资源有联系,即可以间接地当作“锁”)下维护一个临时有序的节点列表Node List,并保证同一时刻并发线程访问共享资源时只能有一个最小序号的节点(即代表获取到锁的线程),该节点对应的线程即可执行访问共享资源的操作。
基于数据库实现分布式锁
乐观锁简介
乐观锁是一种很“佛系”的实现方式,它总是认为不会产生并发的问题,因而每次从数据库中获取数据时总认为不会有其他线程对该数据进行修改,因此不会上锁,但是在更新时其会判断其他线程在这之前有没有对该数据进行修改,通常是采用“版本号version”机制进行实现。
“版本号version”机制的执行流程是这样的:当前线程取出数据记录时,会顺带把版本号字段version的值取出来,最后在更新该数据记录时,将该version的取值作为更新的条件。当更新成功之后,同时将版本号version的值加1,而其他同时获取到该数据记录的线程在更新时由于version已经不是当初获取的那个数据记录,因而将更新失败,从而避免并发多线程访问共享数据时出现数据不一致的现象。
悲观锁简介
悲观锁是一种“消极、悲观”的处理方式,它总是假设事情的发生是在最坏的情况,即每次并发线程在获取数据的时候认为其他线程会对数据进行修改,因而每次在获取数据时都会上锁,而其他线程访问该数据的时候就会发生阻塞的现象,最终只有当前线程释放了该共享资源的锁,其他线程才能获取到锁,并对共享资源进行操作。
在传统的关系型数据库中就用到了很多类似悲观锁的机制,比如行锁、表锁、读锁和写锁等,都是在进行操作之前先上锁。除此之外,Java中的Synchronized关键字和ReentrantLock工具类等底层的实现也是参照了“悲观锁”的思想。
由于乐观锁主要是通过version字段对共享数据进行跟踪和控制,其最终的一个实现步骤是带上version进行匹配、同时执行version+1的更新操作,因而当并发的多线程需要频繁“写”数据库时,是会严重影响数据库性能的。从这种角度看,“乐观锁”其实比较适合于“写少读多”的业务场景。
对于悲观锁而言,由于是建立在数据库底层搜索引擎的基础之上,并采用select…for update的方式对共享资源加“锁”,因而当产生高并发多线程请求,特别是“读”请求时,将对数据库的性能带来严重的影响,因为在同一时刻产生的多线程中将只有一个线程能获取到锁,而其他的线程将处于堵塞的状态,直到该线程释放了锁。
“悲观锁”的方式如果使用不当,将会产生“死锁”的现象(即两个或者多个线程同时处于等待获取对方的资源锁的状态),因而“悲观锁”其实更适用于“读少写多”的业务场景。
基于Redis实现分布式锁
得益于Redis的单线程机制,即不管外层应用系统并发了N多个线程,当每个线程都需要使用Redis的某个原子操作时,是需要进行“排队等待”的,即在其底层系统架构中,同一时刻、同一个部署节点中只有一个线程执行某种原子操作。
在使用SETNX操作实现分布式锁功能时,需要注意以下几点事项:
● 使用SETNX命令获取“锁”时,如果操作结果返回0(表示Key及对应的“锁”已经存在,即已经被其他线程所获取了),则获取“锁”失败,反之则获取成功。
● 为了防止并发线程在获取“锁”之后,程序出现异常情况,从而导致其他线程在调用SETNX命令时总是返回0而进入死锁状态时,需要为Key设置一个“合理”的过期时间。
● 当成功获取到“锁”并执行完成相应的操作之后,需要释放该“锁”。可以通过执行DEL命令将“锁”删除,而在删除的时候还需要保证所删除的“锁”是当时线程所获取的,从而避免出现误删除的情况!
实现流程主要由3个核心操作构成,第一个是构造一个与共享资源或核心业务相关的Key;第二个是采用Redis的操作组件调用SETNX与EXPIRE操作,获取对共享资源的“锁”;最后一个核心操作是在执行完相应的业务逻辑之后释放该“锁”。
基于ZooKeeper实现分布式锁
- ZooKeeper在实际项目中的作用
● 统一配置管理:指的是ZooKeeper可以把每个子系统都需要的配置文件抽取出来,并将其统一放置到ZooKeeper的ZNode节点中进行共享。
● 统一命名服务:指的是给存放在每个节点上的资源进行命名,各个子系统便可以通过名字获取到节点上相应的资源。
● 集群状态:通过动态地感知ZooKeeper上节点的增加、删除,从而保证集群下的相关节点主、副本数据的一致性。
● 分布式锁:是ZooKeeper提供给外部应用的一个重大功能,主要是通过创建与共享资源相关的“临时顺序节点”和动态Watcher监听机制,从而控制多线程对共享资源的并发访问,而这也可以看作是ZooKeeper拥有诸多特性的根本原因。
ZooKeeper在实际生产环境中具有广泛的应用场景,比较典型的业务场景当属ZooKeeper用于担任服务生产者和服务消费者的“注册中心”,即服务生产者将自己提供的服务注册到ZooKeeper中心,服务的消费者在进行服务调用时先在ZooKeeper中查找服务,获取到服务生产者的详细信息之后,再去调用服务生产者提供的相关接口。
访问https://www.imooc.com/learn/1096 ,作者录制的免费课程,即“2小时实战Apache顶级项目-RPC框架Dubbo分布式服务调度”,其中就有关于Dubbo框架的相关技术与ZooKeeper作为注册中心所起到的作用的讲解
- 分布式锁的实现流程与原理分析
节点:在谈及分布式的时候,通常所说的“节点”指的是组成集群的每一台机器。然而在ZooKeeper中,“节点”分为两种类型,第一种是指构成集群的机器,我们称之为机器节点;第二种则是指数据模型中的数据单元,我们称之为数据节点,即前文介绍的ZNode
在ZooKeeper中,ZNode可以分为“持久节点”和“临时节点”两种类型。
持久节点,顾名思义,指的是一旦这个ZNode被创建了,除非主动移除这个ZNode,否则它将一直保存在ZooKeeper上。而临时节点则不一样,它的生命周期是和客户端的会话绑定在一起的,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。除此之外,ZooKeeper创建的临时节点ZNode可以带上一个整型的数字,这个特性可以用来创建一系列带有顺序序号标识的临时ZNode。
Watcher监听器:指的是“事件监听器”,由于ZooKeeper允许用户在指定的节点ZNode上注册“监听”事件,因而当该节点触发一些特定的事件时,ZooKeeper服务端即Server会将事件通知到感兴趣的客户端Client上,从而让客户端做出相对应的措施。
- Spring Boot整合ZooKeeper
Windows环境下ZooKeeper的安装步骤是非常简单的,只需要下载ZooKeeper的安装包,然后解压到某个不带中文字符的磁盘目录下,最后双击bin文件夹里面的zkServer.cmd文件,如果可以弹出“ZooKeeper服务端”启动成功的对话框,则代表ZooKeeper服务已经安装成功,并已经在本地开发环境成功运行起来了
为了方便开发者访问操作ZooKeeper,ZooKeeper研发人员提供了一款高度封装后的客户端Curator框架,使用该框架实例可以很好地解决很多Zookeeper客户端非常底层的细节开发工作,包括重连、反复注册及NodeExistsException异常等问题。
综合中间件Redisson
Redisson的官方网站:https://redisson.org/ 。
Redisson是一款免费、开源的中间件。其内置了一系列的分布式对象、分布式集合、分布式锁及分布式服务等诸多功能特性,是一款基于Redis实现、拥有一系列分布式系统功能特性的工具包,可以说是实现分布式系统架构中缓存中间件的最佳选择。
Redisson的底层基础架构采用基于NIO(即Non-Blocking IO,非阻塞式的输入输出)的Netty框架实现数据的传输,具有更为高效的数据传输性能。
- Redisson的功能特性
● 多种连接方式:指作为客户端的应用系统可以拥有多种方式连接到Redisson所在的服务节点,比如同步连接的方式、异步连接的方式及异步流连接的方式等。
● 数据序列化:指对象的序列化和反序列化方式,从而实现Java对象在Redis的存储和读取功能。
● 集合数据分片:Redisson可以通过自身的分片算法,将一个大集合拆分为若干个片段,然后将拆分后的片段均匀地分布到集群里的各个节点中,以保证每个节点分配到的片段数量大体相同。
● 分布式对象:可以说大部分数据组件都是Redisson所特有的,比如布隆过滤器、BitSet、基于订阅发布式的话题功能等。
● 分布式集合:这一点和原生的缓存中间件Redis所提供的数据结构类似,包括列表List、集合Set、映射Map,以及Redisson所特有的延迟队列等数据组件。
● 分布式锁:是Redisson至关重要的组件。目前在Java应用系统中使用Redisson最多的功能特性当属分布式锁了,其提供了可重入锁、一次性锁、读写锁等组件。
● 分布式服务:指可以实现不在同一个Host(机器节点)的远程服务之间的调度。除此之外,还包括定时任务的调度等。典型应用场景之布隆过滤器与主题
传统Java应用系统一般采用JDK自身提供的HashSet进行去重,即主要是通过调用contains()方法,判断当前元素是否存在于集合Set中。如果方法调用返回的结果为true,则代表元素已存在于该集合中;如果返回结果为false,则代表元素不存在,并将该元素添加进集合中。此种方式要求在调用contains()方法之前,将数据列表加载至内存中,即HashSet的contains()方法是基于内存存储实现判断功能的。故在高并发产生大数据量的场景下,此种方式则显得“捉襟见肘”。为此Redisson分布式对象提供了一种高性能的数据组件“布隆过滤器”(Bloom Filter)。
布隆过滤器在实际项目中主要是用于判断一个元素是否存在于“大数量 ”的集合中,其底层判重的算法主要有两个核心步骤:首先是“初始化”的逻辑,即需要设计并构造K个哈希函数及容量大小为N、每个元素初始取值为0的位数组,第二个核心步骤是“判断元素是否存在”的执行逻辑。该步骤其实与第一个核心步骤有点类似,首先也是需要将当前的元素经过K个哈希函数的计算得到K个哈希值,然后判断K个哈希值(数组的下标)对应数组中的取值是否都为1。如果都为1,则代表该元素是“大概率”性存在的,否则,只要有其中一个取值不为1,则表示元素一定不存在。
之所以说是“大概率”地存在,是因为位数组中元素被赋值为1时,很有可能是在不同元素通过K个哈希函数计算出K个“哈希值”时,刚好有“下标”是重复所导致的,即所谓的“误判”现象
对于布隆过滤器而言,其优点与缺点是并存的。优点在于它不需要开辟额外的内存存储元素,从而节省了存储空间;缺点在于判断元素是否在集合中时,有一定的误判率,并且添加进布隆过滤器的元素是无法删除的,因为位数组的下标是多个元素“共享”的,如果删除的话,很有可能出现“误删”的情况。
第二个典型业务场景是“消息通信”,即Redisson也可以实现类似于“消息中间件RabbitMQ”所提供的消息队列功能,从而实现业务服务模块的异步通信与解耦。这一功能主要是通过Redisson分布式对象中的一个组件“基于发布订阅模式的主题”功能实现的,该数据组件底层在执行发布、订阅逻辑时与RabbitMQ消息队列的生产者发送消息、消费者监听消费消息很类似。
典型应用场景之延迟队列与分布式锁
在Ressison开源组件里并没有提供“死信队列”这一核心组件,取而代之的是Redisson的阻塞式队列。阻塞式队列指的是进入到Redisson队列中的消息将会发生阻塞的现象,如果消息设置了过期时间TTL(也叫存活时间),那么消息将会在该阻塞队列“暂时停留”一定的时间,直到过期时间TTL一到,即代表消息该“离开”了,将前往下一个中转站,消息将进入真正的队列,等待着被消费者监听消费。
Redisson提供了多种“分布式锁”供开发者使用,包括“可重入锁”“一次性锁”“联锁”“红锁RedLock”及“读写锁”等,每一种分布式锁实现的方式和适用的应用场景各不相同。而应用比较多的当属Redisson的“可重入锁”及“一次性锁”了。
可重入锁,顾名思义,指的是当前线程如果没有获取到对共享资源的锁,将会等待一定的时间重新获取。在过个过程中Redisson会提供一个重新获取锁的时间阈值,并在调用tryLock()方法时进行指定。
一次性锁指的是当前线程获取分布式锁时,如果成功则执行后续对共享资源的操作,否则将永远地失败下去(有点“成者为王,败者为寇”的韵味!),其主要是通过调用lock()方法获取锁。
数据结构之映射Map
基于Redis的分布式集合中的数据结构“映射Map”,是开源中间件Redisson提供给开发者的另一种高性能功能组件,在Redisson里称为RMap。RMap功能组件实现了Java对象中的java.util.concurrent.ConcurrentMap接口和java.util.Map接口,不仅拥有Map和ConcurrentMap相应的操作方法,同时自身也提供了丰富的Redisson特有的操作方法,而且几乎每种方法还提供了多种不同的操作方式
Redisson还为映射RMap这一功能组件提供了一系列具有不同功能特性的数据结构,这些数据结构按照特性主要分为3大类,即Eviction元素淘汰、LocalCache本地缓存及Sharding数据分片。
● 数据分片 :指将单一的映射结构切分为若干个小的映射,并均匀地分布在集群中各个节点的数据结构。
● 本地缓存 :指可以将部分数据保存在本地内存里,从而将数据读取的速度提高最多45倍。
● 元素淘汰 :可以通过为某个数据元素指定过期时间从而决定该数据元素的存活时效,而不需要因为所有数据元素共用的Key失效而导致所有的数据元素都丢失。
数据结构之集合Set
功能组件RSet则主要是实现了Java对象中的java.util.Set接口,该功能组件可以保证集合中每个元素的唯一性,它不仅拥有Set接口提供的相应的操作方法,同时自身也扩展提供了丰富的Redisson特有的操作。
除此之外,Redisson还为集合Set这一功能组件提供了一系列具有不同功能特性的数据结构,包括有序集合功能组件SortedSet、计分排序集合功能组件ScoredSortedSet,以及字典排序集合功能组件LexSortedSet等。
队列Queue
按照不同的特性,分布式队列Queue还可以分为双端队列Deque、阻塞队列Blocking Queue、有界阻塞队列(Bounded Blocking Queue)、阻塞双端队列(Blocking Deque)、阻塞公平队列(Blocking Fair Queue)、阻塞公平双端队列(Blocking Fair Deque)等功能组件,不同的功能组件其作用不尽相同,适用的业务场景也是不一样的。
队列Queue主要由3大核心部分组成,即用于生产和发布消息的生产者、代表着数据流的消息,以及监听消费、处理消息的消费者,这3大部分组成了最基本的消息模型(这一点跟分布式消息中间件RabbitMQ的基本消息模型类似)。
延迟队列Delayed Queue
RabbitMQ的死信队列一般在实际生产环境中适用于“消息的TTL的取值一样”的场景。
在实际的生产环境中,仍旧存在着需要处理不同TTL(即过期时间/存活时间)的业务数据的场景,为了解决此种业务场景,Redisson提供了“延迟队列”这个强大的功能组件,它可以解决RabbitMQ死信队列出现的缺陷,即不管在什么时候,消息将按照TTL从小到大的顺序先后被真正的队列监听、消费。
Redisson的延迟队列需要借助“阻塞式队列”作为“中转站”,用于充当消息的第一个暂存区(相当于RabbitMQ的死信交换机)。当TTL即存活时间一到,消息将进入真正的队列被监听消费。
分布式锁实战
Redisson的分布式锁功能组件解决了采用基于Redis的原子操作实现分布式锁的方式的缺陷。下面介绍其中一个典型的缺陷。
当采用基于Redis的原子操作实现分布式锁时,如果负责储存这个分布式锁的Redis节点发生了宕机等情况,而该“锁”又正好处于被锁住的状态,那么这个“锁”很有可能会出现被锁死的状态,即传说中的“死锁”。为了避免这种情况的发生,Redisson内部提供了一个监控锁的“看门狗”,其作用是在Redisson实例被关闭之前,不断地延长分布式锁的有效期。在默认情况下,“看门狗”检查锁的超时时间是30秒钟,当然,在实际业务场景下,也可以通过修改Config.lockWatchdogTimeout进行设置。
按照功能特性的不同,主要包含了可重入锁(Reentrant Lock)、公平锁(Fair Lock)、联锁(MultiLock)、红锁(RedLock)、读写锁(ReadWriteLock)、信号量(Semaphore)及闭锁(CountDownLatch)等功能组件。
Redisson提供的“可重入锁”功能组件有一次性与可重入两种实现方式。一次性指的是当前线程如果可以获取到分布式锁,则成功获取之,否则,将永远失败下去,即所谓“永不翻身”;可重入指的是如果当前线程不能获取到分布式锁,它并不会立即失败,而会等待一定的时间后重新获取分布式锁,这种方式也可以简单理解为“虽为咸鱼,却仍有可翻身的机会”。
- 分布式锁之一次性锁实战
分布式锁的一次性方式适用于那些在同一时刻而且是在很长的一段时间内,仍然只允许只有一个线程访问共享资源的场景,比如用户注册、重复提交、抢红包等业务场景。在开源中间件Redisson这里,主要是通过lock.lock()方法实现。 - 分布式锁之可重入锁实战
分布式锁的可重入方式适用于那些在同一时刻并发产生多线程时,虽然在某一时刻不能获取到分布式锁,但是却允许隔一定的时间重新获取到“分布式锁”并操作共享资源的场景。典型的应用场景当属商城平台高并发抢购商品的业务。而在开源中间件Redisson里,主要是通过lock.tryLock()方法来实现。