记录些Redis题集(3)

news2024/9/25 19:24:27

分布式锁

分布式锁是一种用于在分布式系统中实现互斥访问的机制,它可以确保在多个节点、或进程同时访问共享资源。如果没有适当的锁机制,就可能导致数据不一致或并发冲突的问题。

分布式锁需要的介质

  • 需要一个多个微服务节点都能访问的存储介质,需要能保存锁信息。
  • 该工具上锁时要能保证原子操作,能处理并发,且能对结果进行感知。
  • 节点具有强一致性,不论几个节点,客户端最终结果一致。
  • 老生常谈的高可用,高性能。

分布式锁需要实现的功能

  • 锁的基本要求:锁最大的要求就是互斥和可重入。
    • 不同的对象不能拿到同一个锁,同一个对象可以再次访问该锁。
  • 需要避免死锁:死锁四大条件里面,只要破坏一个就可以避免死锁。
    • 互斥条件:资源是排他的,一个资源一次只能被一个对象获取。
    • 请求与保持条件:当前对象持有这个资源的是时候会请求其他的资源,并且不会放弃持有当前资源。
    • 不可剥夺条件:不可以强行从一个对象手中剥夺资源。
    • 循环等待条件:当前对象需要去等待其他对象的资源,其他对象也要等待当前对象的资源。
    • 解决方案 : 超时退出最简单。
  • 锁对象独占:能拿到锁,能校验锁,也能解除锁,保证锁的独占性。
  • 尝试获取时间 / 超时自动释放:一个是尝试获取锁时,多久超时失败。一个是拿到锁后,多久自动释放。
  • 高并发,高可用:除了锁介质需要满足这些,实现锁的方案上也有满足。

分布式锁的特点

  1. 互斥性:在任何时刻,只有一个客户端能够持有锁。这是分布式锁最基本的要求,确保了数据的一致性和安全性。
  2. 锁超时释放:锁应该有一个超时时间,在超时后自动释放,以防止客户端因异常情况无法释放锁而造成死锁。
  3. 可重入性:同一个客户端在持有锁的情况下可以重复获取锁,防止客户端因再次请求锁而陷入死锁。
  4. 公平性:锁的获取应该尽可能公平,避免某些客户端长时间等待或饿死。
  5. 容错性:在分布式系统中,网络或节点故障是常见问题。一个健壮的分布式锁应该能够在部分故障的情况下仍然保持其功能。
  6. 高可用性和高性能:分布式锁应该能够在高并发和高负载的情况下稳定运行,且性能损失尽可能小。具有较低的延迟和高吞吐量,以确保对共享资源的访问不会成为系统性能的瓶颈。
  7. 支持多种锁模式:如共享锁(读锁)和排他锁(写锁),以适应不同的业务场景。
  8. 锁的状态同步:在分布式环境中,锁的状态需要在不同的节点间同步,确保一致性。
  9. 锁的粒度控制:可以根据需要设置锁的粒度,如对整个资源加锁或只对资源的一部分加锁。
  10. 锁的阻塞和非阻塞获取:支持客户端选择阻塞或非阻塞方式获取锁,以满足不同的业务需求。
  11. 安全性:分布式锁应具备一定的安全性措施,如防止恶意客户端获取或释放锁。
  12. 可靠性(Reliability):分布式锁应该能够在各种异常情况下保持正确的行为,包括:网络分区、节点故障、客户端崩溃等情况。即使某个客户端持有锁的过程中发生异常,也应该确保锁最终能够被释放,以避免死锁等问题。

分布式锁的解决方案

Redis:高性能简单的分布式锁方案

  • 本身的单线程方式保证了操作的并发性。
  • 通过 EVAL 命令可以保证操作的原子性。
  • 虽然没有达到强一致,但是多节点时可以保证最终一致性。

基于 ZooKeeper Etcd 实现

ZooKeeper或Etcd实现分布式锁的优势在于:

1、两者都是分布式的,天然具备高可用能力。

2、都有临时节点的能力,能够很好支持锁超时释放等机制的实现。

3、基于顺序节点、Revision 机制等,更方便实现可重入、公平性等特性。

4、基于Watch、Revision 机制更容易避免分布式锁争抢中的「惊群效应」问题(抢占分布式锁时被频繁唤醒和重新休眠,造成浪费)。解决方案为:分布式锁释放后,只唤醒满足条件的下一个节点。

图片

ZK实现分布式锁的基本原理是:以某个资源为目录,然后这个目录下面的节点就是需要获取锁的客户端,未获取到锁的客户端注册需要注册Watcher到上一个客户端。如下图所示:

图片

  • 临时节点事件监听机制,创建临时有序节点,判断是否是最小从而获取到锁。
  • 通过事件监听等待锁的释放。
  • 解锁则删除节点。

MySQL:性能较低

  • MySQL的事务机制和锁机制足够满足上述的基本需求。
  • 阻塞等机制我们可以借助MySQL的事务和锁机制(select...for update)。
  • 性能较低,不适合高并发场景,实现比较繁琐,很少使用MySQL去实现分布式锁。

Redis与ZK/Etcd的方案主要异同点:

  1. 性能:Redis基于内存,读写性能高,适合高并发。ZK/Etcd相对弱一些。
  2. 运维成本:Redis更常用、是基础组件,运维也更简单。ZK/Etcd都是分布式系统,运维相对复杂一些。
  3. 易用性:都有较成熟的客户端封装,差别不大。
  4. 高可用:均支持,Redis采用Redlock方案,ZK/Etcd本身就是高可用的。

图片

以上的几种实现方式里面,用的最多的还是 Redis 。

  • ZK 需要额外的部署,有些项目并没有使用 ZK 的场景 。
  • ZK 在性能上比 Redis 要差。
  • 对一致性的要求没想象那么高小概率事件,Redis 基本上可以满足。
  • 非要强一致,Redis 也有替代的方案 , 比如 RedissonRedLock。
  • 在锁的处理上,数据库算是性能最差的,占用资源最多。
  • 通常用上分布式锁的时候,系统已经比较大了,这个时候大概率已经分库分表,增加了复杂度。
  • 对于一些复杂的功能,数据库实现不了(解锁,判断锁)。
  • 用数据库做分布式锁还不如让它作为乐观锁。

分布式锁的使用场景

Martin Kleppmann是英国剑桥大学的分布式系统的研究员,之前和Redis之父 Antirez 进行过关于RedLock(红锁)是否安全有过激烈讨论。正如Martin所说,我们使用分布式锁一般有两个场景:正确性和效率。

1、保证数据正确性,比如抢红包、秒杀下单等场景,需要保障不会出现超卖等问题。

因为红包、秒杀商品等场景下,只能被先到达的人才能抢到,所以要顺序执行库存扣减等操作,这样就需要限制同一时间只能有一个线程或进程对资源进行访问和修改。分布式锁可以确保在多个副本部署服务或高并发的情况下,同一时间只有一个线程或进程能够执行相应的业务代码,从而避免数据不一致的问题。

2、避免重复执行某些操作,浪费资源。比如多个客户端可能都执行发送短信通知,但是需要保证这个通知只被发送一次。这些操作可能是非幂等性的,即执行多次会产生不同的结果。为了避免重复执行这些操作,可以使用分布式锁来确保同一时间只有一个客户端能够执行该操作。比如,业务逻辑可能包括以下步骤:

(1)先获取分布式锁;

(2)获取到锁后,先查询是否已发送短信通知,

(3)之后如果查询到未发送状态后才发送,

(4)发送成功后更新发送状态到数据库中。

这样只有第一次执行该逻辑时才能成功发送,此处应用分布式锁能避免因并发操作导致的重复发送问题。

类比单个服务中,操作全局共享变量会先加锁避免并发修改资源造成错误,分布式锁是用于分布式环境下避免并发修改资源导致破坏数据正确性,用于控制某个资源在同一时刻只能被一个应用所使用。如下图所示:

图片

 分布式锁的业务场景

场景一 :限制资源写入

资源访问限制是一个很宽泛的领域,来细化一下就是 API 的访问数据库的访问等等场景都可以通过分布式锁来控制。而往业务场景去偏移,包括超卖问题 ,重复消费问题,等等也都在分布式锁的解决范围之内。同时可以在一定程度上避免数据库级别的锁竞争问题。避免同时的数据写入和修改。

场景二 : 限制资源的产生

这种最常见的场景在于缓存过期的问题上,当并发到来的时候,如果缓存服务器即将过期,可能会基于缓存的特性限制缓存的重复读取和写入。 避免查询重复的数据。再就例如分布式ID的场景下,也会通过分布式锁类似的方式,来获取一个粗粒度的 ID 范围,用于后续ID的细分。

场景三 : 限制触发的频率

这种体现在 Job 定时任务的执行上。不过如果使用的是类似于 XXL-JOB 这类外部的 Job 组件,可能这个特性就用不上。但是如果是单个服务内置的 Job 组件,微服务之间没有互相通信,那么就需要分布式锁来限制任务触发的频率。对应的还包括 API 的访问频率,也可以在分布式锁的基础上进行扩展(主要就是要求原子性的计数)。

场景四 : 维护资源的一致性

由于分布式场景的特性,可能在单机上面被视为原子对象的资源,在分布式场景下就变成了多个资源。分布式锁并不能改变这种状态,但是可以增强一致性 ,维护他们的统一状态。常见的场景包括分布式事务。

分布式锁的实现思路

关于锁的实现要点

  • 要实现锁的等待,首先要有个明确的等待时间,然后在业务代码里面等待(比如自旋,Java的锁)。
  • 锁的主键:一般情况下实现的时候都是通过 类 + 方法 + 参数 + 值
  • 锁的重入:使用 redisson 的情况下 ,它是通过线程ID来实现的重入(如果同一个应用线程相同,就可能存在问题)。

分布式锁的实现方式

  • Zookeeper 有提供完整功能的第三方包,例如 Curator。
  • Redis 使用更加简单。

基于 Redis 的方案:

  • 基于 LUA 脚本自定义分布式锁。
  • 基于 redisson 的分布式锁 (其实本质上还是 LUA 脚本)。

图片

解锁原理

// 如果锁不存在,则直接返回
 if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then 
     return nil;
end;

// 若锁存在,且唯一标识(线程ID)匹配:则先将锁重入计数减1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then   // 如果锁的持有数还是大于 0 ,则不可以删除锁,只是设置时间
    redis.call('pexpire', KEYS[1], ARGV[2]);  
    return 0; 
else
    redis.call('del', KEYS[1]);  // 否则则直接删除锁,锁释放
    redis.call('publish', KEYS[2], ARGV[1]);  // 广播锁释放消息,唤醒等待的线程
    return 1;
end; 

return nil;

分布式锁过期的业务处理

首先,在设置过期时间时要结合业务场景去考虑,尽量设置一个比较合理的值,就是理论上正常处理的话,在这个过期时间内是一定能处理完毕的。接着考虑对这个问题进行兜底设计。关于这个问题,目前常见的解决方法有两种:

  1. 守护线程“续命”:额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间。

  2. 超时回滚:当解锁时发现锁已经被其他线程获取了,说明此时我们执行的操作已经是“不安全”的了,此时需要进行回滚,并返回失败。

同时,需要进行告警,人为介入验证数据的正确性,然后找出超时原因,是否需要对超时时间进行优化等等。

守护线程“续命”

Redisson 的 RLock 对象会自动处理锁的续期。当一个线程获取了锁,Redisson 会在后台启动一个定时任务(看门狗),用于在锁即将过期时自动续期。 详细流程步骤:

  • 获取锁:当调用 lock.lock() 时,Redisson 会尝试在 Redis 中创建一个具有过期时间的锁。
  • 锁的自动续期:Redisson 会启动一个后台线程(看门狗),它会在锁的过期时间的一半时检查锁是否仍然被当前线程持有。
  • 续期锁:如果锁仍然被持有,看门狗会延长锁的过期时间。这确保了即使业务逻辑执行时间较长,锁也不会过期。
  • 执行业务逻辑:在锁的保护下,执行业务逻辑。
  • 释放锁:当业务逻辑执行完毕后,调用 lock.unlock() 释放锁。如果当前线程是最后一个持有锁的线程,Redisson 会从 Redis 中删除锁。
  • 异常处理:如果在执行业务逻辑时发生异常,finally 块中的 unlock() 调用确保了锁能够被释放,防止死锁。
  • 看门狗线程终止:一旦锁被释放,看门狗线程会停止续期操作,并结束。

通过这种方式,Redisson 提供了一个简单而强大的机制来处理分布式锁的自动续期,从而减少了锁过期导致的问题。

超时回滚

使用超时回滚机制处理 Redis 分布式锁过期的情况,是指当一个线程因为执行时间过长导致持有的分布式锁过期,而其他线程又获取了同一把锁时,原线程需要能够检测到这一情况并执行业务逻辑的回滚操作。无论业务逻辑是否成功执行,都需要在 finally 块中释放锁,以避免死锁。在释放锁之后,如果业务逻辑执行失败,可能需要通知用户或者记录日志,以便进一步处理。

常见的 Redis 锁实现

基于SETNX + EXPIRE命令

SETNX是"SET if Not eXists"的缩写,命令格式:SETNX key value。

  • 获取锁:使用SETNX命令尝试设置唯一锁标识符。返回1表示成功创建锁,返回0表示锁已被占用。
  • 设置过期时间:成功获取锁后,用EXPIRE命令为锁设定超时时间,防止客户端崩溃导致锁无法释放。
  • 释放锁:完成任务后,使用DEL命令删除锁标识符,释放锁。

需要注意的是,在获取锁后,执行业务逻辑时应设定合理的超时时间,以避免锁被长时间占用。这种方式实现的锁存在一定的缺陷,当 Redis 服务器故障或者出现网络分区时,可能会导致锁无法正常释放,从而导致死锁的问题。SETNX 和 EXPIRE 是两个命令,不是原子操作,如果执行完 SETNX 后宕机,这个锁就会一直存在。

先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?

set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来使用。

基于 SET 扩展命令

命令格式:SET key value [EX seconds | PX milliseconds]  [NX]。

  • 获取锁并设置过期时间:使用 SET $lock_key $val EX $second NX 命令保证加锁原子性,并为锁设置过期时间。NX 表示只有在锁不存在的情况下才设置锁。
  • 释放锁:完成任务后,使用DEL命令删除锁标识符,释放锁。

缺陷:一个客户端可能会误删除别的客户端的锁。比如,客户端A获得分布式锁,之后执行业务操作过久,导致分布式锁已经过期了。此时客户端B成功获得分布式锁,之后A完成业务操作,就把客户端B的锁操作删除了。

基于 SET 命令 + LUA脚本

基于SET扩展命令 + SET 随机 value 以便删除时校验 + LUA脚本保证删除时对比数据和DEL操作的原子性。

1、获取锁并设置过期时间

使用 SET $lock_key $unique_val EX $second NX 命令保证加锁原子性,并为锁设置过期时间。

多个客户端使用同一个lock_key,但是各个客户端应该使用自己唯一的unique_val,以便删除的时候进行校验,防止自己的锁被别的客户端误操作删除。

2、释放锁

完成任务后,获取当前lock_key的 value 与 unique_val 是否相同,相同则使用DEL命令删除锁标识符,释放锁。为保证释放锁为原子操作,需使用lua脚本完成这两步操作。

RedLock 实现锁

RedLock 锁它是由Redis的作者Salvatore Sanfilippo提出的,旨在提供一个可靠的分布式锁方案。

以下是RedLock 算法的基本步骤:

  1. 获取当前时间戳:所有Redis实例使用相同的时间源,例如:NTP获取当前时间戳。
  2. 尝试在多个Redis实例上获取锁:在每个Redis实例上尝试使用SET命令获取锁,设置一个带有唯一标识符的键,设置的键名应该是全局唯一的,以避免与其他锁冲突。
  3. 计算获取锁所花费的时间:计算从第一步获取时间戳到成功获取锁所花费的时间,记为elapsed_time
  4. 判断锁是否获取成功:如果获取锁的时间elapsed_time小于设定的锁超时时间,并且大多数(例如大于一半)的Redis实例成功获取了锁,那么认为锁获取成功。
  5. 释放锁:在所有成功获取锁的Redis实例上执行释放锁的操作,使用DEL命令删除对应的键。

关于Redlock还有这么一段趣事:

Redis 作者把这个方案一经提出,就马上受到业界著名的分布式系统专家Martin的质疑。Martin指出了分布式系统的三类异常场景(NPC):

N:Network Delay,网络延迟

P:Process Pause,进程暂停(GC)

C:Clock Drift,时钟漂移

Martin 用一个进程暂停(GC)的例子,指出了 Redlock 安全性问题,由于GC问题会导致分布式锁的正确性出现问题,如下图所示:

图片

之后Redis 作者毫不客气地进行了回怼,Redis 作者同意对方关于时钟跳跃对Redlock的影响,但认为通过运维手段是可以避免的。Redlock中有超时时间判断的机制,可以有效避免NPC问题,但是如果Redlock 步骤3(成功拿到锁)之后发生GC,不止是 Redlock 有问题,其它分布式锁服务同样也有问题,所以不在讨论范畴内。

Redisson 对分布式锁的改进

首先,Redis能做分布式锁,主要是因为是单线程执行的,所以如果能在一个指令里面操作完获取以及设置锁的话,就不会有并发问题。

比如setnx指令,以及新版的set支持相关的参数。当然也可以用lua脚本来保证多个指令的原子性。

但是基于简单的一条指令去做锁的话,是一个不可重入锁,另外一个锁过期时间不好把控,可能会出现业务没有执行完锁过期。

所以redisson主要解决这2个问题,当然还有一些比如读写锁、联锁等等。

可重入的话基于lua+hash去实现可重入锁。然后时间把控,如果你不知道锁多久过期,redisson会基于时间轮+递归来时间锁续期。也就是我们说的看门狗。

时间轮+递归实现续期

时间轮就是hashWheelTimer,是一个netty包下的类,它主要实现的功能是延时执行任务。

它的实现逻辑是会启动一个线程去轮询数组,然后任务根据延时多久添加到对应数组,如果轮询到了的话,通过多线程去执行相关的任务。

看门狗,就是用hashWheelTimer 去根据设置的看门狗时间/3 去延时判断key是否存在,如果存在,就续期,并且递归,做到只要key还在就一直续期。如果key不存在,就不再递归。

Redisson 联锁

Redisson 的联锁(Redisson MultiLock)是 Redisson 分布式锁的一种实现,用于在多个 Redis 节点上同时加锁。它允许多个资源(例如不同的Redis键)同时被锁定,只有当所有的资源都成功锁定时,联锁才会认为锁定成功。这种机制非常适合于需要同时锁定多个资源以执行某些操作的场景。

联锁的目的是因为redis是属于AP模式的中间件,会存在数据丢失,那么锁就会失效。

所以联锁主要做的一件事情就是尽可能的去保证数据不丢失,加锁会加在不同的独立集群机器。当满足一半成功就成功。其实主要思想就是把鸡蛋分散到不同的篮子。降低风险。只要不是超过一半的失败,就是成功的。

实现Redis分布式锁的高可用

首先想到的还是Redis的单点故障问题,如果Redis挂了,分布式锁就不能正常工作,因此可以引入Redis主从模式。

但是,如果在Redis集群的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。但是此时分布式锁也已经失效了。

怎么解决这个问题呢?为此,Redis 的作者提出一种解决方案,就是经常听到的 Redlock(红锁)。为此,需要部署5个单独的Redis,Redlock的实现步骤如下:

  1. 向5个Redis master节点请求加锁。
  2. 根据设置的超时时间来判断(加锁的总耗时要小于锁设置的过期时间),是不是要跳过该master节点。
  3. 如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功。
  4. 释放锁,向全部节点发起释放锁请求。

Redis 脑裂问题

Redis 脑裂问题是指,在 Redis 哨兵模式或集群模式中,由于网络原因,导致主节点(Master)与哨兵(Sentinel)和从节点(Slave)的通讯中断,此时哨兵就会误以为主节点已宕机,就会在从节点中选举出一个新的主节点,此时 Redis 的集群中就出现了两个主节点的问题,就是 Redis 脑裂问题。

脑裂问题影响

Redis 脑裂问题会导致数据丢失。

图片

而最后一步,当旧的 Master 变为 Slave 之后,它的执行流程如下:

  1. Slave(旧 Master)会向 Master(新)申请全量数据。
  2. Master 会通过 bgsave 的方式生成当前 RDB 快照,并将 RDB 发送给 Slave。
  3. Slave 拿到 RDB 之后,先进行 flush 清空当前数据(此时第四步旧客户端给他的发送的数据就丢失了)。
  4. 之后再加载 RDB 数据,初始化自己当前的数据。

从以上过程中可以看出,在执行到第三步的时候,原客户端在旧 Master 写入的数据就丢失了,这就是数据丢失的问题。

脑裂问题解决方法

脑裂问题只需要在旧 Master 恢复网络之后,切换身份为 Slave 期间,不接收客户端的数据写入即可,那怎么解决这个问题呢?

Redis 为我们提供了以下两个配置,通过以下两个配置可以尽可能的避免数据丢失的问题:

  • min-slaves-to-write:与主节点通信的从节点数量必须大于等于该值主节点,否则主节点拒绝写入。
  • min-slaves-max-lag:主节点与从节点通信的 ACK 消息延迟必须小于该值,否则主节点拒绝写入。

这两个配置项必须同时满足,不然主节点拒绝写入。

在假故障期间满足 min-slaves-to-write 和 min-slaves-max-lag 的要求,那么主节点就会被禁止写入,脑裂造成的数据丢失情况自然也就解决了。

设置了参数之后,Redis 脑裂问题能完全被解决吗?Zookeeper 是如何解决脑裂问题的?

设置了 min-slaves-to-write 和 min-slaves-max-lag 参数后,Redis 的脑裂问题可以得到一定程度的缓解,但并不能完全解决。原因如下:

  1. 网络分区:当网络发生分区时,主节点可能会与部分从节点失去连接,但仍然能够接收客户端的写入请求。如果剩余的从节点数量满足 min-slaves-to-write 的要求,主节点将继续处理写入操作,而这些写入可能不会同步到被网络分区隔离的从节点,从而导致数据不一致。

  2. 假故障:如果主节点或从节点发生临时故障(如进程暂停、网络延迟等),在故障恢复之前,主节点可能会拒绝写入,这会影响系统的可用性。

  3. 配置复杂性:正确配置这两个参数需要深入理解系统的性能和网络状况,配置不当可能会导致不必要的写入拒绝或数据不一致。

Zookeeper 也会面临脑裂问题,但它通过以下机制来解决:

  1. Zab 协议:Zookeeper 使用 Zab(Zookeeper Atomic Broadcast)协议来保证分布式系统的一致性。该协议确保了即使在发生网络分区时,也只有一个领导节点能够处理写操作。

  2. 过半机制:Zookeeper 集群中的每个操作都需要得到超过半数节点的同意才能执行。这确保了在网络分区时,至少有一半的节点与领导节点相连,从而避免了脑裂问题。

  3. 领导选举:当领导节点发生故障或网络分区时,Zookeeper 集群会自动进行领导选举,选举出一个新的领导节点来处理写操作。

通过这些机制,Zookeeper 在保证一致性的同时,也提高了系统的可用性和容错性。

限流算法与实现原理

高并发系统有三大特征:限流、缓存和熔断。

限流是指在各种应用场景中,通过技术和策略手段对数据流量、请求频率或资源消耗进行有计划的限制,以避免系统负载过高、性能下降甚至崩溃的情况发生。限流的目标在于维护系统的稳定性和可用性,并确保服务质量。

使用限流有以下几个好处:

  • 保护系统稳定性:过多的并发请求可能导致服务器内存耗尽、CPU 使用率饱和,从而引发系统响应慢、无法正常服务的问题。
  • 防止资源滥用:确保有限的服务资源被合理公平地分配给所有用户,防止个别用户或恶意程序过度消耗资源。
  • 优化用户体验:对于网站和应用程序而言,如果任由高并发导致响应速度变慢,会影响所有用户的正常使用体验。
  • 保障安全:在网络层面,限流有助于防范 DoS/DDoS 攻击,降低系统遭受恶意攻击的风险。
  • 运维成本控制:合理的限流措施可以帮助企业减少不必要的硬件投入,节省运营成本。

限流常见算法

计数器算法:将时间周期划分为固定大小的窗口(如每分钟、每小时),并在每个窗口内统计请求的数量。当窗口内的请求数达到预设的阈值时,后续请求将被限制。时间窗口结束后,计数器清零。

  • 优点:实现简单,易于理解。
  • 缺点:在窗口切换时刻可能会有突刺流量问题,即在窗口结束时会有短暂的大量请求被允许通过。

滑动窗口算法:改进了计算器算法(固定窗口算法)的突刺(在短时间内突然出现的流量高峰或数据量激增的现象)问题,将时间窗口划分为多个小的时间段(桶),每个小时间段有自己的计数器。随着时间流逝,窗口像滑块一样平移,过期的小时间段的计数会被丢弃,新时间段加入计数。所有小时间段的计数之和不能超过设定的阈值。

  • 优点:更平滑地处理流量,避免了突刺问题。
  • 缺点:实现相对复杂,需要维护多个计数器。

漏桶算法:想象一个固定容量的桶,水(请求)以恒定速率流入桶中,同时桶底部有小孔让水以恒定速率流出。当桶满时,新来的水(请求)会被丢弃。此算法主要用来平滑网络流量,防止瞬时流量过大。

  • 优点:可以平滑突发流量,保证下游系统的稳定。
  • 缺点:无法处理突发流量高峰,多余的请求会被直接丢弃。

令牌桶算法:与漏桶相反,有一个固定速率填充令牌的桶,令牌代表请求许可。当请求到达时,需要从桶中取出一个令牌,如果桶中有令牌则允许请求通过,否则拒绝。桶的容量是有限的,多余的令牌会被丢弃。

  • 优点:既能平滑流量,又能处理一定程度的突发流量(因为令牌可以累积)。
  • 缺点:需要精确控制令牌生成速度,实现较漏桶复杂。

使用Redis实现限流

使用 Redis 也可以实现简单的限流,它的常见限流方法有以下几种实现:

  1. 基于计数器和过期时间实现的计数器算法:使用一个计数器存储当前请求量(每次使用 incr 方法相加),并设置一个过期时间,计数器在一定时间内自动清零。计数器未到达限流值就可以继续运行,反之则不能继续运行。
  2. 基于有序集合(ZSet)实现的滑动窗口算法:将请求都存入到 ZSet 集合中,在分数(score)中存储当前请求时间。然后再使用 ZSet 提供的 range 方法轻易的获取到 2 个时间戳内的所有请求,通过获取的请求数和限流数进行比较并判断,从而实现限流。
  3. 基于列表(List)实现的令牌桶算法:在程序中使用定时任务给 Redis 中的 List 添加令牌,程序通过 List 提供的 leftPop 来获取令牌,得到令牌继续执行,否则就是限流不能继续运行。
计数器算法
  1. 使用 Redis 的计数器保存当前请求的数量。
  2. 设置一个过期时间,使得计数器在一定时间内自动清零。
  3. 每次收到请求时,检查计数器当前值,如果未达到限流阈值,则增加计数器的值,否则拒绝请求。
滑动窗口算法
  1. 使用有序集合(ZSet)来存储每个时间窗口内的请求时间戳,成员(member)表示请求的唯一标识,分数(score)表示请求的时间戳。
  2. 每次收到请求时,将请求的时间戳作为成员,当前时间戳作为分数加入到有序集合中。
  3. 根据有序集合的时间范围和滑动窗口的设置,判断当前时间窗口内的请求数量是否超过限流阈值。

具体实现代码如下:

public class RedisSlidingWindowRateLimiter {

    private static final String ZSET_KEY = "request_timestamps";
    private static final int WINDOW_SIZE = 60; // 时间窗口大小(单位:秒)
    private static final int REQUEST_LIMIT = 100; // 限流阈值

    public boolean allowRequest() {
        Jedis jedis = new Jedis("localhost");
        long currentTimestamp = System.currentTimeMillis() / 1000;

        // 添加当前请求的时间戳到有序集合
        jedis.zadd(ZSET_KEY, currentTimestamp, String.valueOf(currentTimestamp));

        // 移除过期的请求时间戳,保持时间窗口内的请求
        long start = currentTimestamp - WINDOW_SIZE;
        long end = currentTimestamp;
        jedis.zremrangeByScore(ZSET_KEY, 0, start);

        // 查询当前时间窗口内的请求数量
        Set<Tuple> requestTimestamps = jedis.zrangeByScoreWithScores(ZSET_KEY, start, end);
        long requestCount = requestTimestamps.size();

        jedis.close();

        // 判断请求数量是否超过限流阈值
        return requestCount <= REQUEST_LIMIT;
    }
}
令牌桶算法

① 添加令牌

在 Spring Boot 项目中,通过定时任务给 Redis 中的 List 每秒中添加一个令牌(当然也可以通过修改定时任务的执行时间来控制令牌的发放速度),具体实现代码如下:

redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString());

② 获取令牌

令牌的获取代码如下:

Object result = redisTemplate.opsForList().leftPop("limit_list");

在上述代码中,每次访问 allowRequest() 方法时,会尝试从 Redis 中获取一个令牌,如果拿到令牌了,那就说明没超出限制,可以继续执行,反之则不能执行。

使用 Redis 实现限流有什么优缺点?为什么微服务中不会使用 Redis 实现限流?

优点

  • 高性能:基于内存的键值存储系统,读写速度非常快,适合用于需要高性能的限流场景。
  • 分布式环境友好:支持分布式部署,可以在多个服务器之间共享计数器和状态。
  • 灵活的限流策略:数据结构多,可以实现各种复杂的限流策略,如固定窗口、滑动窗口等。
  • 原子操作:如 INCR、DECR 等,可以保证在并发场景下的数据一致性。
  • 易于集成:Redis 在多种编程语言中都有客户端库,易于和各种应用系统集成。

缺点

  • 依赖外部系统:依赖于 Redis 服务器,如果服务器出现问题,会影响限流功能的正常运行。
  • 网络延迟:虽然 Redis 性能高,但网络请求比本地计算要慢,可能会引入额外的延迟。
  • 资源消耗:虽然 Redis 是基于内存的,但在大规模和高并发的场景下,Redis 仍然可能消耗大量的内存和 CPU 资源。

至于为什么微服务中不会使用 Redis 实现限流,这个说法可能有些绝对。实际上,Redis 在微服务架构中经常被用于限流,尤其是在需要跨服务限流或全局限流的场景下。然而,微服务架构中不使用 Redis 进行限流的原因可能包括:

  • 避免单点故障:微服务架构强调去中心化和容错性,过度依赖外部系统(如 Redis)可能会引入单点故障的风险。
  • 简化架构:有些微服务架构可能会选择更简单的限流方案,如使用本地计数器或基于时间的令牌桶算法,以减少对外部系统的依赖。
  • 性能考虑:对于一些对性能要求极高的微服务,可能会选择在服务内部实现限流逻辑,以避免网络通信的开销。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1923947.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【操作系统】进程管理——死锁(个人笔记)

学习日期&#xff1a;2024.7.13 内容摘要&#xff1a;死锁的概念和三大处理策略 目录 死锁 死锁的概念 死锁、饥饿和死循环的区别 死锁产生的必要条件 死锁的处理策略&#xff1a;预防、避免和解除 预防死锁 破坏互斥条件 破坏不剥夺条件 破坏请求和保持条件 破坏循…

UWB定位系统在智能制造行业中的主要功能

定位系统全套源码&#xff0c;UWB定位系统 UWB&#xff08;超宽带&#xff09;定位系统在智能制造行业赋能可以实现多种功能&#xff0c;这些功能不仅提升了企业的管理水平和生产效率&#xff0c;还增强了生产现场的安全性和灵活性。 开发语言&#xff1a;JAVA 开发工具&…

【产品应用】一体化步进伺服电机在板材封边机中的应用

随着现代工业技术的快速发展&#xff0c;封边机作为木材加工行业的重要设备&#xff0c;其精度、效率和稳定性已成为衡量设备性能的重要指标。 近年来&#xff0c;一体化步进伺服电机因其高精度、高效率和强稳定性等特点&#xff0c;在封边机中得到了广泛应用。 本文将详细介绍…

医疗器械FDA |FDA网络安全测试具体内容

医疗器械FDA网络安全测试的具体内容涵盖了多个方面&#xff0c;以确保医疗器械在网络环境中的安全性和合规性。以下是根据权威来源归纳的FDA网络安全测试的具体内容&#xff1a; 一、技术文件审查 网络安全计划&#xff1a;制造商需要提交网络安全计划&#xff0c;详细描述产…

MySQL:库操作

1. 创建数据库 create database [if not exists] name [create_specification], [create_specification]... []内为可选的选项 create_specification: character set charset_name -- 指定数据库采用的字符集 -- 数据库未来存储数据 collate collation_name -- 指定数据库字符…

【vue教程】二. Vue特性原理详解

目录 回顾本章涵盖知识点Vue 实例和选项创建 Vue 实例Vue 实例的选项 Vue 模板语法插值表达式指令v-bindv-modelv-on 自定义指令创建自定义指令在模板中使用自定义指令自定义指令的钩子函数自定义指令的实例演示 指令注册局部注册指令过滤器 数据绑定和响应式原理响应式数据绑定…

开源PHP论坛HadSky本地部署与配置公网地址实现远程访问

文章目录 前言1. 网站搭建1.1 网页下载和安装1.2 网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3 Cpolar稳定隧道&#xff08;本地设置&#xff09;2.4 公网访问测试 总结 前言 今天和大家分享…

[Vulnhub] Simple CuteNews-CMS+Kernel权限提升

信息收集 IP AddressOpening Ports192.168.8.104TCP:80 $ nmap -p- 192.168.8.104 --min-rate 1000 -sC -sV PORT STATE SERVICE VERSION 80/tcp open http Apache httpd 2.4.7 ((Ubuntu)) |_http-title: Please Login / CuteNews |_http-server-header: Apache/2.4.7…

2024辽宁省大学数学建模竞赛试题思路

A题 (1) 建立模型分析低空顺风风切变对起飞和降落的影响 模型假设 飞机被视为质点&#xff0c;忽略其尺寸和形状对风阻的影响。风切变仅考虑顺风方向的变化&#xff0c;忽略其他方向的风切变。飞机的飞行速度、高度和姿态&#xff08;如迎角、俯仰角&#xff09;是变化的&am…

Shopee马来西亚站点八月开始强制规定使用电子发票

马来西亚国内税收局 (IRBM) 宣布&#xff0c;从 2024 年 8 月 1 日起&#xff0c;将强制采用电子发票。 所有卖家必须在 2024 年 7 月 31 日之前提交强制性信息/文件&#xff0c;以遵守电子发票要求。 此项电子发票的实施意味着卖家们在平台上的买卖的透明性将会增强&#xff…

将swagger注解导入apifox的IDEA配置

在使用IDEA开发中&#xff0c;经常需要将后端接口导出到Apifox&#xff0c;以便于测试。将swagger注解内容导出到Apifox中&#xff0c;需要进行以下设置: file->settting打开对话框&#xff0c;选择Other Settings -> Apifox Help&#xff0c;如下图&#xff1a; 2.选…

国产精品ORM框架-SqlSugar详解 SqlSugar初识 专题一

国产精品ORM框架-SqlSugar详解 1、SqlSugar初识 2、开始实操 3、增删改操作 4、进阶功能 5、集成整合 6、脚手架应用 sqlsugar 官网-CSDN博客 国产精品ORM框架-SqlSugar详解 SqlSugar初识 专题二-CSDN博客 1、SqlSugar初识 1.1 基本概念和历史 SqlSugar 是一款 老牌 …

eNsp公司管理的网络NAT策略搭建

实验拓扑图 实验需求&#xff1a; 7&#xff0c;办公区设备可以通过电信链路和移动链路上网(多对多的NAT&#xff0c;并且需要保留一个公网IP不能用来转换) 8&#xff0c;分公司设备可以通过总公司的移动链路和电信链路访问到Dmz区的http服务器 9&#xff0c;多出口环境基于带…

Start11 V2(Windows 开始菜单美化工具 )值得购买吗?

去年底 Stardock 正式推出了 Start11 V2&#xff0c;与第一代 Start11 相比&#xff0c;新版带来了超过 50 项的新特性&#xff0c;其中包括&#xff1a; 全新的开始菜单样式、应用图标分组 / 换色、固定任意程序 / 文件 / 文件夹到任务栏、配置备份还原…… 开始菜单的新样…

Vue 最新动态!!!

大家好,我是CodeQi! 一位热衷于技术分享的码仔。 当Vue 3.4在六个月前发布时,整个前端开发社区都为之振奋。这次更新不仅带来了许多新特性,还解决了许多开发过程中遇到的痛点。 然而,时间飞逝,随着我在项目中不断应用这些新特性,逐渐积累了很多宝贵的经验和心得。 今…

Python-PLAXIS自动化建模技术与典型岩土工程案例

有限单元法在岩土工程问题中应用非常广泛&#xff0c;很多软件都采用有限单元解法。在使用各大软件进行数值模拟建模的过程中&#xff0c;岩土工程中的各种问题&#xff08;塑性、渗流、固结、动力、稳定安全、热力TM&#xff09;&#xff0c;一步一步地搭建自己的Plaxis模型&a…

《昇思25天学习打卡营第17天 | CycleGAN图像风格迁移互换》

《昇思25天学习打卡营第17天 | CycleGAN图像风格迁移互换》 目录 《昇思25天学习打卡营第17天 | CycleGAN图像风格迁移互换》模型介绍模型简介模型结构 数据集数据集下载数据集加载可视化 构建生成器构建判别器优化器和损失函数前向计算计算梯度和反向传播模型训练模型推理参考…

大模型-基于大模型的数据标注

方法来自于这篇论文&#xff1a; Can Generalist Foundation Models Outcompete Special-Purpose Tuning? Case Study in Medicine。 一.背景 假设&#xff0c;存在一批标注好的数据D_labeled&#xff0c;其包含m个标注样本(x, y)。 目标是&#xff0c;基于D_labeled&#…

Redis集群和高可用

文章目录 一、Redis主从复制redis主从复制架构主从复制实现主从复制故障恢复主从复制优化主从复制过程 主从同步优化配置 二、哨兵模式 (Sentinel)redis集群介绍哨兵 (Sentinel)工作原理实现哨兵主从复制哨兵配置文件 三、Redis cluster架构工作原理Redis cluster架构实现集群 …

电表及销售统计Python应用及win程序

暑假每天都要填表算账很烦躁&#xff0c;就整了个小程序来减轻压力 程序可以做到记录输入的每一条数据&#xff0c;并用新数据减去旧数据算新增的量&#xff0c;同时记录填写时间 Python代码 import json import os # 导入os模块 from datetime import datetime from tkint…