黄石专业网站建设推广,重庆做网站建设企业,怎么给网站做背景,wordpress 固定链接 打不开分布式锁
分布式锁是一种用于在分布式系统中实现互斥访问的机制#xff0c;它可以确保在多个节点、或进程同时访问共享资源。如果没有适当的锁机制#xff0c;就可能导致数据不一致或并发冲突的问题。
分布式锁需要的介质
需要一个多个微服务节点都能访问的存储介质#…分布式锁
分布式锁是一种用于在分布式系统中实现互斥访问的机制它可以确保在多个节点、或进程同时访问共享资源。如果没有适当的锁机制就可能导致数据不一致或并发冲突的问题。
分布式锁需要的介质
需要一个多个微服务节点都能访问的存储介质需要能保存锁信息。该工具上锁时要能保证原子操作能处理并发且能对结果进行感知。节点具有强一致性不论几个节点客户端最终结果一致。老生常谈的高可用高性能。
分布式锁需要实现的功能
锁的基本要求锁最大的要求就是互斥和可重入。 不同的对象不能拿到同一个锁同一个对象可以再次访问该锁。需要避免死锁死锁四大条件里面只要破坏一个就可以避免死锁。 互斥条件资源是排他的一个资源一次只能被一个对象获取。请求与保持条件当前对象持有这个资源的是时候会请求其他的资源并且不会放弃持有当前资源。不可剥夺条件不可以强行从一个对象手中剥夺资源。循环等待条件当前对象需要去等待其他对象的资源其他对象也要等待当前对象的资源。解决方案 超时退出最简单。锁对象独占能拿到锁能校验锁也能解除锁保证锁的独占性。尝试获取时间 / 超时自动释放一个是尝试获取锁时多久超时失败。一个是拿到锁后多久自动释放。高并发高可用除了锁介质需要满足这些实现锁的方案上也有满足。
分布式锁的特点
互斥性在任何时刻只有一个客户端能够持有锁。这是分布式锁最基本的要求确保了数据的一致性和安全性。锁超时释放锁应该有一个超时时间在超时后自动释放以防止客户端因异常情况无法释放锁而造成死锁。可重入性同一个客户端在持有锁的情况下可以重复获取锁防止客户端因再次请求锁而陷入死锁。公平性锁的获取应该尽可能公平避免某些客户端长时间等待或饿死。容错性在分布式系统中网络或节点故障是常见问题。一个健壮的分布式锁应该能够在部分故障的情况下仍然保持其功能。高可用性和高性能分布式锁应该能够在高并发和高负载的情况下稳定运行且性能损失尽可能小。具有较低的延迟和高吞吐量以确保对共享资源的访问不会成为系统性能的瓶颈。支持多种锁模式如共享锁读锁和排他锁写锁以适应不同的业务场景。锁的状态同步在分布式环境中锁的状态需要在不同的节点间同步确保一致性。锁的粒度控制可以根据需要设置锁的粒度如对整个资源加锁或只对资源的一部分加锁。锁的阻塞和非阻塞获取支持客户端选择阻塞或非阻塞方式获取锁以满足不同的业务需求。安全性分布式锁应具备一定的安全性措施如防止恶意客户端获取或释放锁。可靠性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的方案主要异同点
性能Redis基于内存读写性能高适合高并发。ZK/Etcd相对弱一些。运维成本Redis更常用、是基础组件运维也更简单。ZK/Etcd都是分布式系统运维相对复杂一些。易用性都有较成熟的客户端封装差别不大。高可用均支持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;
elseredis.call(del, KEYS[1]); // 否则则直接删除锁锁释放redis.call(publish, KEYS[2], ARGV[1]); // 广播锁释放消息唤醒等待的线程return 1;
end; return nil;分布式锁过期的业务处理
首先在设置过期时间时要结合业务场景去考虑尽量设置一个比较合理的值就是理论上正常处理的话在这个过期时间内是一定能处理完毕的。接着考虑对这个问题进行兜底设计。关于这个问题目前常见的解决方法有两种 守护线程“续命”额外起一个线程定期检查线程是否还持有锁如果有则延长过期时间。Redisson 里面就实现了这个方案使用“看门狗”定期检查每1/3的锁时间检查1次如果线程还持有锁则刷新过期时间。 超时回滚当解锁时发现锁已经被其他线程获取了说明此时我们执行的操作已经是“不安全”的了此时需要进行回滚并返回失败。
同时需要进行告警人为介入验证数据的正确性然后找出超时原因是否需要对超时时间进行优化等等。
守护线程“续命”
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 算法的基本步骤
获取当前时间戳所有Redis实例使用相同的时间源例如NTP获取当前时间戳。尝试在多个Redis实例上获取锁在每个Redis实例上尝试使用SET命令获取锁设置一个带有唯一标识符的键设置的键名应该是全局唯一的以避免与其他锁冲突。计算获取锁所花费的时间计算从第一步获取时间戳到成功获取锁所花费的时间记为elapsed_time。判断锁是否获取成功如果获取锁的时间elapsed_time小于设定的锁超时时间并且大多数例如大于一半的Redis实例成功获取了锁那么认为锁获取成功。释放锁在所有成功获取锁的Redis实例上执行释放锁的操作使用DEL命令删除对应的键。 关于Redlock还有这么一段趣事 Redis 作者把这个方案一经提出就马上受到业界著名的分布式系统专家Martin的质疑。Martin指出了分布式系统的三类异常场景NPC NNetwork Delay网络延迟 PProcess Pause进程暂停GC CClock Drift时钟漂移 Martin 用一个进程暂停GC的例子指出了 Redlock 安全性问题由于GC问题会导致分布式锁的正确性出现问题如下图所示 之后Redis 作者毫不客气地进行了回怼Redis 作者同意对方关于时钟跳跃对Redlock的影响但认为通过运维手段是可以避免的。Redlock中有超时时间判断的机制可以有效避免NPC问题但是如果Redlock 步骤3成功拿到锁之后发生GC不止是 Redlock 有问题其它分布式锁服务同样也有问题所以不在讨论范畴内。 Redisson 对分布式锁的改进
首先Redis能做分布式锁主要是因为是单线程执行的所以如果能在一个指令里面操作完获取以及设置锁的话就不会有并发问题。
比如setnx指令以及新版的set支持相关的参数。当然也可以用lua脚本来保证多个指令的原子性。
但是基于简单的一条指令去做锁的话是一个不可重入锁另外一个锁过期时间不好把控可能会出现业务没有执行完锁过期。
所以redisson主要解决这2个问题当然还有一些比如读写锁、联锁等等。
可重入的话基于luahash去实现可重入锁。然后时间把控如果你不知道锁多久过期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个单独的RedisRedlock的实现步骤如下
向5个Redis master节点请求加锁。根据设置的超时时间来判断加锁的总耗时要小于锁设置的过期时间是不是要跳过该master节点。如果大于等于3个节点加锁成功并且使用的时间小于锁的有效期即可认定加锁成功。释放锁向全部节点发起释放锁请求。
Redis 脑裂问题
Redis 脑裂问题是指在 Redis 哨兵模式或集群模式中由于网络原因导致主节点Master与哨兵Sentinel和从节点Slave的通讯中断此时哨兵就会误以为主节点已宕机就会在从节点中选举出一个新的主节点此时 Redis 的集群中就出现了两个主节点的问题就是 Redis 脑裂问题。
脑裂问题影响
Redis 脑裂问题会导致数据丢失。 而最后一步当旧的 Master 变为 Slave 之后它的执行流程如下
Slave旧 Master会向 Master新申请全量数据。Master 会通过 bgsave 的方式生成当前 RDB 快照并将 RDB 发送给 Slave。Slave 拿到 RDB 之后先进行 flush 清空当前数据此时第四步旧客户端给他的发送的数据就丢失了。之后再加载 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 的脑裂问题可以得到一定程度的缓解但并不能完全解决。原因如下 网络分区当网络发生分区时主节点可能会与部分从节点失去连接但仍然能够接收客户端的写入请求。如果剩余的从节点数量满足 min-slaves-to-write 的要求主节点将继续处理写入操作而这些写入可能不会同步到被网络分区隔离的从节点从而导致数据不一致。 假故障如果主节点或从节点发生临时故障如进程暂停、网络延迟等在故障恢复之前主节点可能会拒绝写入这会影响系统的可用性。 配置复杂性正确配置这两个参数需要深入理解系统的性能和网络状况配置不当可能会导致不必要的写入拒绝或数据不一致。 Zookeeper 也会面临脑裂问题但它通过以下机制来解决 Zab 协议Zookeeper 使用 ZabZookeeper Atomic Broadcast协议来保证分布式系统的一致性。该协议确保了即使在发生网络分区时也只有一个领导节点能够处理写操作。 过半机制Zookeeper 集群中的每个操作都需要得到超过半数节点的同意才能执行。这确保了在网络分区时至少有一半的节点与领导节点相连从而避免了脑裂问题。 领导选举当领导节点发生故障或网络分区时Zookeeper 集群会自动进行领导选举选举出一个新的领导节点来处理写操作。 通过这些机制Zookeeper 在保证一致性的同时也提高了系统的可用性和容错性。 限流算法与实现原理
高并发系统有三大特征限流、缓存和熔断。 限流是指在各种应用场景中通过技术和策略手段对数据流量、请求频率或资源消耗进行有计划的限制以避免系统负载过高、性能下降甚至崩溃的情况发生。限流的目标在于维护系统的稳定性和可用性并确保服务质量。 使用限流有以下几个好处
保护系统稳定性过多的并发请求可能导致服务器内存耗尽、CPU 使用率饱和从而引发系统响应慢、无法正常服务的问题。防止资源滥用确保有限的服务资源被合理公平地分配给所有用户防止个别用户或恶意程序过度消耗资源。优化用户体验对于网站和应用程序而言如果任由高并发导致响应速度变慢会影响所有用户的正常使用体验。保障安全在网络层面限流有助于防范 DoS/DDoS 攻击降低系统遭受恶意攻击的风险。运维成本控制合理的限流措施可以帮助企业减少不必要的硬件投入节省运营成本。
限流常见算法
计数器算法将时间周期划分为固定大小的窗口如每分钟、每小时并在每个窗口内统计请求的数量。当窗口内的请求数达到预设的阈值时后续请求将被限制。时间窗口结束后计数器清零。
优点实现简单易于理解。缺点在窗口切换时刻可能会有突刺流量问题即在窗口结束时会有短暂的大量请求被允许通过。
滑动窗口算法改进了计算器算法固定窗口算法的突刺在短时间内突然出现的流量高峰或数据量激增的现象问题将时间窗口划分为多个小的时间段桶每个小时间段有自己的计数器。随着时间流逝窗口像滑块一样平移过期的小时间段的计数会被丢弃新时间段加入计数。所有小时间段的计数之和不能超过设定的阈值。
优点更平滑地处理流量避免了突刺问题。缺点实现相对复杂需要维护多个计数器。
漏桶算法想象一个固定容量的桶水请求以恒定速率流入桶中同时桶底部有小孔让水以恒定速率流出。当桶满时新来的水请求会被丢弃。此算法主要用来平滑网络流量防止瞬时流量过大。
优点可以平滑突发流量保证下游系统的稳定。缺点无法处理突发流量高峰多余的请求会被直接丢弃。
令牌桶算法与漏桶相反有一个固定速率填充令牌的桶令牌代表请求许可。当请求到达时需要从桶中取出一个令牌如果桶中有令牌则允许请求通过否则拒绝。桶的容量是有限的多余的令牌会被丢弃。
优点既能平滑流量又能处理一定程度的突发流量因为令牌可以累积。缺点需要精确控制令牌生成速度实现较漏桶复杂。
使用Redis实现限流
使用 Redis 也可以实现简单的限流它的常见限流方法有以下几种实现
基于计数器和过期时间实现的计数器算法使用一个计数器存储当前请求量每次使用 incr 方法相加并设置一个过期时间计数器在一定时间内自动清零。计数器未到达限流值就可以继续运行反之则不能继续运行。基于有序集合ZSet实现的滑动窗口算法将请求都存入到 ZSet 集合中在分数score中存储当前请求时间。然后再使用 ZSet 提供的 range 方法轻易的获取到 2 个时间戳内的所有请求通过获取的请求数和限流数进行比较并判断从而实现限流。基于列表List实现的令牌桶算法在程序中使用定时任务给 Redis 中的 List 添加令牌程序通过 List 提供的 leftPop 来获取令牌得到令牌继续执行否则就是限流不能继续运行。
计数器算法
使用 Redis 的计数器保存当前请求的数量。设置一个过期时间使得计数器在一定时间内自动清零。每次收到请求时检查计数器当前值如果未达到限流阈值则增加计数器的值否则拒绝请求。
滑动窗口算法
使用有序集合ZSet来存储每个时间窗口内的请求时间戳成员member表示请求的唯一标识分数score表示请求的时间戳。每次收到请求时将请求的时间戳作为成员当前时间戳作为分数加入到有序集合中。根据有序集合的时间范围和滑动窗口的设置判断当前时间窗口内的请求数量是否超过限流阈值。
具体实现代码如下
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);// 查询当前时间窗口内的请求数量SetTuple 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可能会引入单点故障的风险。简化架构有些微服务架构可能会选择更简单的限流方案如使用本地计数器或基于时间的令牌桶算法以减少对外部系统的依赖。性能考虑对于一些对性能要求极高的微服务可能会选择在服务内部实现限流逻辑以避免网络通信的开销。