首页 > 编程知识 正文

郑州市java培训机构,java软件技术培训机构

时间:2023-05-04 18:28:57 阅读:54861 作者:427

前言分布式是程序员必备的技能之一,是面试中必备的课堂,在工作中更是常用。 Kafka是一个基于发布订阅的分布式消息队列,目前其吸引力无穷,Kafka的奥秘有待深入探究。

要说对Kafka有多熟悉,还是因为我相信蚂蚁积极的铃铛是最有发言权的,所以今天分享的内容是Alibaba内部提供的“限定笔记本”。 关于Kafka的精髓全部写在这里。 不愧是Alibaba的技术官啊。 真的很佩服。

一方面,背景我们日常在电商网站购物时,经常会遇到一些高并发的场景,比如电商网站APP上经常出现的秒杀活动、限量优惠券抢购,以及我们去哪儿网抢火车票的系统。 这些场景有一个共同的特征,就是访问量的激增。 系统设计时采用限流、异步、排队等方法进行优化,整体并发度比平时多数倍以上,为用户提供避免并发问题,防止库存超标

在单个进程并发方案中,可以使用编程语言和相应类库提供的锁定(如Java同步语法和ReentrantLock类)来避免并发问题。

在分布式方案中,要实现不同客户端线程对代码和资源的同步访问,并确保多线程处理共享数据的安全,需要分布式锁定技术。

那么什么是分布式锁定呢? 分布式锁是控制分布式系统或不同系统之间对共享资源的公共访问的锁的实现。如果不同系统或同一系统的不同主机之间共享资源,则为了防止相互干扰并保持一致性,通常需要互斥

比较安全的分布式锁,一般需要以下特征。

互相排他性。 互斥锁的基本特征,在同一时间只能有一个线程执行关键节。 超时释放。 超时释放有助于避免死锁,避免等待不必要的线程和浪费资源。 类似于MySQL innodb引擎中的innodblockwait_timeout参数配置。 可再入性。 如果线程保持锁定,则可以再次请求锁定,以免线程在执行关键节之前解锁。 高性能和高可用性。 锁定和解锁过程的性能开销必须尽可能低,同时确保高可用性,以防止分布式锁定意外禁用。 可见,要实现分布式锁定,锁定资源并不是一件好事,还需要满足一些附加特征,以避免死锁和锁定失效等问题。

二、分布式锁定的实现方式目前有很多实现分布式锁定的方式,常见的主要有:

使用33558www.Sina.com/memcached的添加命令。 此命令是原子操作,表示只有在key不存在时add才成功,线程已锁定。

利用33558www.Sina.com/zookeeper的顺序临时节点实现分布式锁定和排队。 ZooKeeper是分布式APP应用程序的专用框架,提供了非常好的功能,如ephemeral类型的znode自动删除功能。 ZooKeeper还提供了一种watch机制,使分布式锁像本地锁一样在客户端上使用。 锁定失败后进行阻止,直到获取锁定。

Memcached 分布式锁Google提供的粗粒度分布式锁定服务与ZooKeeper相似,但存在许多差异。 Chubby通过sequencer机制解决了由于延迟请求而导致的锁定禁用问题。

以3358www.Sina.com/Redis单体实现的分布式锁。 其方式与Memcached的实现方式相似,利用Redis的SETNX命令,该命令也是原子操作,只有在不存在key的情况下set才会成功。 基于Redis多机的分布式锁定Redlock是Redis作者antirez为规范Redis分布式锁定的实现而提出的一种更加安全有效的实现机制。

本文主要探讨分析基于Redis的分布式锁的几种实现方式和存在的问题。

三、Redis分布式锁使用Redis作为分布式锁,本质上要实现的目标是一个流程在Redis中占有唯一的“厕所”,另一个流程也要占有坑的时候,已经发现有人蹲在那里,放弃了

目前,基于Redis实现分布式锁定主要有两大类。 一个基于独立,另一个基于Redis多机。 这两种实现方法都需要实现三种分布式锁定的核心要素:锁定、解锁和锁定超时。

Zookeeper 分布式锁

Chubby

最简单的锁定方法是直接使用Redis的SETNX命令。 仅当key不存在时,此命令才将key的值设置为value;如果key已经存在,则不执行任何SETNX命令。 key是锁定的唯一标识符,可以根据业务上需要锁定的资源进行命名。

例如,在某商城秒杀事件即商品上锁时,key可以设置为lock_resource_id,value可以设置为任意值,资源使用完成后,使用DEL删除该key释放密钥。 整个过程如下。

很明显,获得这样的钥匙是因为

方式很简单,但也存在一个问题,就是我们上面提到的分布式锁三个核心要素之一的锁超时问题,即如果获得锁的进程在业务逻辑处理过程中出现了异常,可能会导致 DEL 指令一直无法执行,导致锁无法释放,该资源将会永远被锁住。

所以,在使用 SETNX 拿到锁以后,必须给 key 设置一个过期时间,以保证即使没有被显式释放,在获取锁达到一定时间后也要自动释放,防止资源被长时间独占。由于 SETNX 不支持设置过期时间,所以需要额外的 EXPIRE 指令,整个过程如下:

这样实现的分布式锁仍然存在一个严重的问题,由于 SETNX 和 EXPIRE 这两个操作是非原子性的, 如果进程在执行 SETNX 和 EXPIRE 之间发生异常,SETNX 执行成功,但 EXPIRE 没有执行,导致这把锁变得“长生不老”,这种情况就可能出现前文提到的锁超时问题,其他进程无法正常获取锁。

2)使用 SET 扩展指令

为了解决 SETNX 和 EXPIRE 两个操作非原子性的问题,可以使用 Redis 的 SET 指令的扩展参数,使得 SETNX 和 EXPIRE 这两个操作可以原子执行,整个过程如下:

在这个 SET 指令中:

NX 表示只有当 lock_resource_id 对应的 key 值不存在的时候才能 SET 成功。保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。EX 10 表示这个锁10秒钟后会自动过期,业务可以根据实际情况设置这个时间的大小。

但是这种方式仍然不能彻底解决分布式锁超时问题:

锁被提前释放。假如线程 A 在加锁和释放锁之间的逻辑执行的时间过长(或者线程 A 执行过程中被堵塞),以至于超出了锁的过期时间后进行了释放,但线程 A 在临界区的逻辑还没有执行完,那么这时候线程 B 就可以提前重新获取这把锁,导致临界区代码不能严格的串行执行。锁被误删。假如以上情形中的线程A执行完后,它并不知道此时的锁持有者是线程 B,线程A会继续执行 DEL 指令来释放锁,如果线程 B 在临界区的逻辑还没有执行完,线程 A 实际上释放了线程 B 的锁。

为了避免以上情况,建议不要在执行时间过长的场景中使用 Redis 分布式锁,同时一个比较安全的做法是在执行 DEL 释放锁之前对锁进行判断,验证当前锁的持有者是否是自己。

具体实现就是在加锁时将 value 设置为一个唯一的随机数(或者线程 ID ),释放锁时先判断随机数是否一致,然后再执行释放操作,确保不会错误地释放其它线程持有的锁,除非是锁过期了被服务器自动释放,整个过程如下:

但判断 value 和删除 key 是两个独立的操作,并不是原子性的,所以这个地方需要使用 Lua 脚本进行处理,因为 Lua 脚本可以保证连续多个指令的原子性执行。

基于 Redis 单节点的分布式锁基本完成了,但是这并不是一个完美的方案,只是相对完全一点,因为它并没有完全解决当前线程执行超时锁被提前释放后,其它线程乘虚而入的问题。

3)使用 Redisson 的分布式锁

怎么能解决锁被提前释放这个问题呢?

可以利用锁的可重入特性,让获得锁的线程开启一个定时器的守护线程,每 expireTime/3 执行一次,去检查该线程的锁是否存在,如果存在则对锁的过期时间重新设置为 expireTime,即利用守护线程对锁进行“续命”,防止锁由于过期提前释放。

当然业务要实现这个守护进程的逻辑还是比较复杂的,可能还会出现一些未知的问题。

目前互联网公司在生产环境用的比较广泛的开源框架 Redisson 很好地解决了这个问题,非常的简便易用,且支持 Redis 单实例、Redis M-S、Redis Sentinel、Redis Cluster 等多种部署架构。

其实现原理如图所示(图中以 Redis 集群为例):

2、基于Redis多机实现的分布式锁Redlock

以上几种基于 Redis 单机实现的分布式锁其实都存在一个问题,就是加锁时只作用在一个 Redis 节点上,即使 Redis 通过 Sentinel 保证了高可用,但由于 Redis 的复制是异步的,Master 节点获取到锁后在未完成数据同步的情况下发生故障转移,此时其他客户端上的线程依然可以获取到锁,因此会丧失锁的安全性。

整个过程如下:

客户端 A 从 Master 节点获取锁。Master 节点出现故障,主从复制过程中,锁对应的 key 没有同步到 Slave 节点。Slave升 级为 Master 节点,但此时的 Master 中没有锁数据。客户端 B 请求新的 Master 节点,并获取到了对应同一个资源的锁。出现多个客户端同时持有同一个资源的锁,不满足锁的互斥性。

正因为如此,在 Redis 的分布式环境中,Redis 的作者 antirez 提供了 RedLock 的算法来实现一个分布式锁,该算法大概是这样的:

假设有 N(N>=5)个 Redis 节点,这些节点完全互相独立,不存在主从复制或者其他集群协调机制,确保在这N个节点上使用与在 Redis 单实例下相同的方法获取和释放锁。

获取锁的过程,客户端应执行如下操作:

获取当前 Unix 时间,以毫秒为单位。按顺序依次尝试从5个实例使用相同的 key 和具有唯一性的 value(例如 UUID)获取锁。当向 Redis 请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端 Redis 已经挂掉的情况下,客户端还在一直等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁。客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的 Redis 节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。如果取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。如果因为某些原因,获取锁失败(没有在至少N/2+1个 Redis 实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(使用 Redis Lua 脚本)。

释放锁的过程相对比较简单:客户端向所有 Redis 节点发起释放锁的操作,包括加锁失败的节点,也需要执行释放锁的操作,antirez 在算法描述中特别强调这一点,这是为什么呢?

原因是可能存在某个节点加锁成功后返回客户端的响应包丢失了,这种情况在异步通信模型中是有可能发生的:客户端向服务器通信是正常的,但反方向却是有问题的。虽然对客户端而言,由于响应超时导致加锁失败,但是对 Redis节点而言,SET 指令执行成功,意味着加锁成功。因此,释放锁的时候,客户端也应该对当时获取锁失败的那些 Redis 节点同样发起请求。

除此之外,为了避免 Redis 节点发生崩溃重启后造成锁丢失,从而影响锁的安全性,antirez 还提出了延时重启的概念,即一个节点崩溃后不要立即重启,而是等待一段时间后再进行重启,这段时间应该大于锁的有效时间。

最后

分享一些系统的面试题,大家可以拿去刷一刷,准备面试涨薪。

请点赞后,戳这里,免费获取!

这些面试题相对应的技术点:

JVMMySQLMybatisMongoDBRedisSpringSpring bootSpring cloudKafkaRabbitMQNginx…

大类就是:

Java基础数据结构与算法并发编程数据库设计模式微服务消息中间件

…(img-SHYWYaet-1622094483023)]

[外链图片转存中…(img-hjT77aE1-1622094483023)]

[外链图片转存中…(img-EIsvjT9i-1622094483024)]

[外链图片转存中…(img-iMWxbW8Z-1622094483025)]

[外链图片转存中…(img-JLITCMTk-1622094483026)]

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。