Redis是如何进行内存管理的?缓存中有哪些常见问题?如何实现分布式锁?

Redis内存管理

Redis的内存用完了会怎样?

如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回)。

也可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。

Redis如何做内存优化?

可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面。

过期key的删除策略?

详情请查看:过期删除策略

  1. 惰性删除。在访问key时,如果发现key已经过期,那么会将key删除。
  2. 定时删除。定时清理key,每次清理会依次遍历所有DB,从db随机取出20个key,如果过期就删除,如果其中有5个key过期,那么就继续对这个db进行清理,否则开始清理下一个db。

定期删除策略原理?

Redis内部维护一个定时任务,默认每秒进行10次(也就是每隔100毫秒一次)过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。

  1. 从过期字典中随机取出20个key
  2. 删除这 20 个 key 中已经过期的 key;
  3. 如果这20个key中过期key的比例超过了25%,则重复步骤1

为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。

为什么定期删除不是把所有过期 key 都删除呢?

这样会对性能造成太大的影响。如果我们 key 数量非常庞大的话,挨个遍历检查是非常耗时的,会严重影响性能。Redis 设计这种策略的目的是为了平衡内存和性能。

过期key对持久化的影响

RDB:

  • 生成rdb文件:生成时,程序会对key进行检查,过期key不放入rdb文件。
  • 载入rdb文件:载入时,如果以主服务器模式运行,程序会对文件中保存的key进行检查,未过期的key会被载入到数据库中,而过期key则会忽略;如果以从服务器模式运行,无论键过期与否,均会载入数据库中,过期key会通过与主服务器同步而删除。

AOF:

  • 当服务器以AOF持久化模式运行时,如果数据库中的某个key已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期key而产生任何影响
  • 当过期key被惰性删除或者定期删除之后,程序会向AOF文件追加一条DEL命令,来显示的记录被该key已经发被删除
  • 在执行AOF重写的过程中,程序会对数据库中的key进行检查,已过期的key不会被保存到重写后的AOF文件中

主从复制:当服务器运行在复制模式下时,从服务器的过期删除动作由主服务器控制:

  • 主服务器在删除一个过期key后,会显式地向所有从服务器发送一个del命令,告知从服务器删除这个过期key;
  • 从服务器在执行客户端发送的读命令时,即使碰到过期key也不会将过期key删除,而是继续像处理未过期的key一样来处理过期key;
  • 从服务器只有在接到主服务器发来的del命令后,才会删除过期key。

内存淘汰策略有哪些?

当Redis的内存超过最大允许的内存之后,Redis 会触发内存淘汰策略,删除一些不常用的数据,以保证Redis服务器正常运行。

  • volatile-lru:针对设置了过期时间的key,使用lru算法进行淘汰。
  • allkeys-lru:针对所有key使用lru算法进行淘汰。
  • volatile-lfu:针对设置了过期时间的key,使用lfu算法进行淘汰。
  • allkeys-lfu:针对所有key使用lfu算法进行淘汰。
  • volatile-random:针对设置了过期时间的key中使用随机淘汰的方式进行淘汰。
  • allkeys-random:针对所有key使用随机淘汰机制进行淘汰。
  • volatile-ttl:针对设置了过期时间的key,越早过期的越先被淘汰。

Redis对随机淘汰和LRU策略进行的更精细化的实现,支持将淘汰目标范围细分为全部数据和设有过期时间的数据,这种策略相对更为合理一些。因为一般设置了过期时间的数据,本身就具备可删除性,将其直接淘汰对业务不会有逻辑上的影响;而没有设置过期时间的数据,通常是要求常驻内存的,往往是一些配置数据或者是一些需要当做白名单含义使用的数据(比如用户信息,如果用户信息不在缓存里,则说明用户不存在),这种如果强行将其删除,可能会造成业务层面的一些逻辑异常。

内存淘汰策略可以通过配置文件来修改 ,相应的配置项是maxmemory-policy,默认配置是noeviction

缓存常见问题

Redis 中如何保证缓存与数据库的数据一致性?

  1. 先更新缓存,再更新数据库
  2. 先更新数据库存,再更新缓存
  3. 先删除缓存,再更新数据库,后续等查询把数据库的数据回种到缓存中
  4. 先更新数据库,再删除缓存,后续等查询把数据库的数据回种到缓存中
  5. 缓存双删策略。更新数据库之前,删除一次缓存;更新完数据库后,再进行一次延迟删除
  6. 使用 Binlog 异步更新缓存,监听数据库的 Binlog 变化,通过异步方式更新 Redis 缓存

以上就是实现数据库与缓存一致性的六种方式,这里前面三种都不太推荐使用,后面三种需要根据实际场景选择:

  • 如果是要考虑实时一致性的话,先写 MySQL,再删除 Redis 应该是较为优的方案,虽然短期内数据可能不一致,不过其能尽量保证数据的一致性。
  • 如果考虑最终一致性的话,推荐的是使用 binlog + 消息队列的方式,这个方案其有重试和顺序消费,能够最大限度地保证缓存与数据库的最终一致性:。

详情可以看这篇文章:缓存和数据库一致性问题

缓存雪崩

在使用缓存时,通常会对缓存设置过期时间,一方面目的是保持缓存与数据库数据的一致性,另一方面是减少冷缓存占用过多的内存空间。

那么,当大量缓存数据在短时间集体失效 或者 Redis 故障宕机 时,请求全部转发到数据库,从而导致数据库压力骤增,甚至宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。

所以,发生缓存雪崩的场景通常有两个:

  • 大量热点key同时过期;
  • 缓存服务故障;

大量热点key同时过期

针对大量数据同时过期而引发的缓存雪崩问题,常见的应对方法有下面这几种:

  1. 均匀失效 (建议):将key的过期时间后面加上一个随机数(比如随机1-5分钟)
    • 如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。就可以给这些数据的过期时间加上一个随机数,这样就保证数据不会在同一时间过期。
  2. 考虑用队列或者互斥锁的方式,保证缓存单线程写,但这种方案可能会影响并发量,不建议
    • 当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里。也就不会有大量的热点数据需要从数据库读取数据了),即保证缓存单线程写。
    • 实现互斥锁的时候,最好设置超时时间,防止线程出现意外一直阻塞导致其它请求也无法获取锁。
  3. 可以让热点数据永久有效(不推荐,一般都会要求必须设置过期时间),后台异步更新缓存,适用于不严格要求缓存一致性的场景。
    • 事实上即使数据永久有效,数据也不一定会一直在内存中,这是因为 Redis的内存淘汰策略 ,当系统内存紧张的时候,有些缓存数据会被"淘汰"。如果此时用户读取的是淘汰数据,那就有可能会返回空值,误以为数据丢失了。解决方式:
    • 后台频繁地检测缓存是否有效,如果检测到缓存失效了,那就从数据库读取数据,并更新到缓存。但这个频繁 的时间不好掌握,总会有时间间隔,间隔时间内就有可能导致空值的返回;
    • 用户读取数据时,发现数据不在Redis中,则通知后台线程更新缓存。后台线程收到消息后,发现数据不存在就读取数据库数据,并将数据加载到缓存。
  4. 双key策略,主key设置过期时间,备key不设置过期时间,当主key失效时,直接返回备key值。
    • 这个只是 key 不一样,但是 value 值是一样的,相当于给缓存数据做了个副本。但是在更新缓存时,需要同时更新「主 key 」和「备 key 」的数据。
    • 双 key 策略的好处是,当主 key 过期了,有大量请求获取缓存数据的时候,直接返回备 key 的数据,这样可以快速响应请求。而不用因为 key 失效而导致大量请求被锁阻塞住(采用了互斥锁,仅一个请求来构建缓存),后续再通知后台线程,重新构建主 key 的数据。
    • 但是需要同时存储两份数据,增大了内存开销

缓存服务故障

  1. 构建缓存高可用集群(针对缓存服务故障情况)。
    • 如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题
  2. 当缓存雪崩发生时,可以实行服务熔断、限流、降级 等措施进行保障。
    • 服务熔断机制,就是暂停业务应用对缓存服务的访问,直接返回错误,也就不用再继续访问数据库,从而降低对数据库的访问压力,保证数据库系统的正常运行,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。这种方式就是暂停了业务访问
    • 请求限流机制,就是只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制。

缓存击穿

缓存雪崩是指大量热点key 同时失效的情况,如果是单个热点key,一直都有着大并发访问,那么在这个key失效的瞬间,这个大并发请求就会击破缓存,直接请求到数据库,好像蛮力击穿一样。这种情况就是缓存击穿(Cache Breakdown)。

缓存击穿和前面提到的缓存雪崩产生的原因其实很相似。区别点在于:

  • 缓存雪崩是大面积的缓存失效导致大量请求涌入数据库。
  • 缓存击穿是少量缓存失效 的时候恰好失效的数据遭遇大并发量的请求,导致这些请求全部涌入数据库中。

因此可以将缓存击穿视为缓存雪崩的子集,应对方案也是缓存雪崩说到的方案。

解决方案:

  1. 过期时间续期:比如每次请求的时候自动将过期时间续期一下

  2. 使用互斥锁(Mutex Key):当缓存不可用时,仅持锁的线程负责从数据库中查询数据并写入缓存中,其余请求重试时先尝试从缓存中获取数据,避免所有的并发请求全部同时打到数据库上。步骤如下:

    1. 没有命中缓存的时候,先请求获取分布式锁,获取到分布式锁的线程,执行DB查询操作,然后将查询结果写入到缓存中;

    2. 没有抢到分布式锁的请求,原地自旋等待一定时间后进行再次重试;

    3. 未抢到锁的线程,再次重试的时候,先尝试去缓存中获取下是否能获取到数据,如果可以获取到数据,则直接取缓存已有的数据并返回;否则重复上述123步骤。

  3. 逻辑过期:热点数据不设置过期时间,后台异步更新缓存,适用于不严格要求缓存一致性 的场景。

对于业务中最常使用的旁路型缓存而言,通常会先读取缓存,如果不存在则去数据库查询,并将查询到的数据添加到缓存中,这样就可以使得后面的请求继续命中缓存。

但是这种常规操作存在个"漏洞",因为大部分缓存容量有限制,且很多场景会基于LRU策略进行内存中热点数据的淘汰,假如有个恶意程序(比如爬虫)一直在刷历史数据,容易将内存中的热点数据变为历史数据,导致真正的用户请求被打到数据库层。

针对这种场景,在缓存的设计时,需要考虑到对这种冷数据的加热机制进行一些额外处理,如设定一个门槛,如果指定时间段内对一个冷数据的访问次数达到阈值,则将冷数据加热,添加到热点数据缓存中,并设定一个独立的过期时间,来解决此类问题。

比如可以约定同一秒内对某条冷数据的请求超过10次,则将此条冷数据加热作为临时热点数据存入缓存,设定缓存过期时间为30天。通过这样的机制,来解决冷数据的突然窜热对系统带来的不稳定影响。

缓存穿透

缓存穿透(cache penetration)是用户访问的数据既不在缓存当中,也不在数据库中。出于容错的考虑,如果从底层数据库查询不到数据,则不写入缓存。这就导致每次请求都会到底层数据库进行查询,缓存也失去了意义。当高并发或有人利用不存在的Key频繁攻击时,数据库的压力骤增,甚至崩溃,这就是缓存穿透问题。

缓存穿透与缓存击穿同样非常相似,区别点在于缓存穿透 的实际请求数据在数据库中也没有,而缓存击穿是仅仅在缓存中没命中,但是在数据库中其实是存在对应数据的。

发生场景:

  • 原来数据是存在的,但由于某些原因(误删除、主动清理等)在缓存和数据库层面被删除了,但前端或前置的应用程序依旧保有这些数据;
  • 黑客恶意攻击,外部爬虫,故意大量访问某些读取不存在数据的业务;

缓存穿透解决方案:

  1. 缓存空值(null)或默认值
    • 分析业务请求,如果是正常业务请求时发生缓存穿透现象,可针对相应的业务数据,在数据库查询不存在时,将其缓存为空值(null)或默认值,但是需要注意的是,针对空值的缓存失效时间不宜过长,一般设置为5分钟之内。当数据库被写入或更新该key的新数据时,缓存必须同时被刷新,避免数据不一致。
  2. 业务逻辑前置校验
    • 在业务请求的入口处进行数据合法性校验,检查请求参数是否合理、是否包含非法值、是否恶意请求等,提前有效阻断非法请求。比如,根据年龄查询时,请求的年龄为-10岁,这显然是不合法的请求参数,直接在参数校验时进行判断返回。
  3. 使用 布隆过滤器快速判断数据不存在(推荐)
    • 在写入数据时,使用布隆过滤器进行标记(相当于设置白名单),业务请求发现缓存中无对应数据时,可先通过查询布隆过滤器判断数据是否在白名单内(布隆过滤器可以判断数据一定不存在),如果不在白名单内,则直接返回空或失败。
  4. 用户黑名单限制:当发生异常情况时,实时监控访问的对象和数据,分析用户行为,针对故意请求、爬虫或攻击者,进行特定用户的限制;
  5. 添加反爬策略:比如添加请求签名校验机制、比如添加IP访问限制策略等等

缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

解决方案:

  1. 直接写个缓存刷新页面,上线时手工操作一下;
  2. 数据量不大,可以在项目启动的时候自动进行加载;
  3. 定时刷新缓存;

缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
  4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

分布式锁

详情请查看:Redis实现分布式锁

Redis 中如何实现分布式锁?

在 Redis 中实现分布式锁的常见方法是通过 setnx 命令 + lua 脚本组合使用。确保多个客户端不会获得同一个资源锁的同时,也保证了安全解锁和意外情况下锁的自动释放,

LUA脚本

Redis 通过 LUA 脚本创建具有原子性的命令: 当lua脚本命令正在运行的时候,不会有其他脚本或 Redis 命令被执行,实现组合命令的原子操作。

在Redis中执行Lua脚本有两种方法:evalevalshaeval命令使用内置的 Lua 解释器,对 Lua 脚本进行求值。

java 复制代码
//第一个参数是lua脚本,第二个参数是键名参数个数,剩下的是键名参数和附加参数
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

lua脚本作用

  1. Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
  2. Lua脚本可以将多条命令一次性打包,有效地减少网络开销。

使用Redis + lua方式可能存在的问题?

  1. 不可重入性。同一个线程无法多次获取同一把锁
  2. 不可重试。获取锁只尝试一次就返回false,没有重试机制
  3. 超时释放。锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁的释放,存在安全隐患
  4. 主从一致性。如果Redis是主从集群,主从同步存在延迟,当主机宕机时,从成为了主,但可能存在从此时还未完成同步,因此从上就没有锁标识,此时会出现线程安全问题。

分布式锁在未完成逻辑前过期怎么办?

若锁在未完成逻辑前就过期,此时可能会产生数据不一致的问题,因为锁过期了,此时如果再出现一个客户端争抢锁,即可拿到锁然后同时进行业务操作,这等于锁失效了。

此时可以在逻辑执行过程中定期续期锁,确保锁在处理过程中不会过期。

业界出了一个看门狗机制来防止这种情况的产生。理论很简单,在抢到锁之后,后台会有一个任务,定时向redis进行锁的续期、比如锁的过期时间是 305,可以每过三分之一时长(30/3)10s后就去redis 重新设置过期时间为 30s

在锁被释放的时候,就移除这个定时任务。

什么是Reddsion?

RLock是Redisson分布式锁的最核心接口,继承了concurrent包的Lock接口和自己的RLockAsync接口,RLockAsync的返回值都是RFuture,是Redisson执行异步实现的核心逻辑,也是Netty发挥的主要阵地。

redisson 是一个类库,封装了很多 redis 操作,便于我们使用。其实现的分布式锁就引入了看门狗机制,具体原理和上面所述的一致,并且 redisson 支持可重入锁,即同一个线程可以多次获取同一个分布式锁,而不会导致死锁。

实现方法如下:在获取锁时,检查当前锁的唯一标识是否已经属于当前线程。

  • 如果是,则增加一个重入计数器。
  • 释放锁时,减少重入计数器,只有当计数器为0时才真正释放锁。

说说 Redisson 分布式锁的原理?

Redison 是基于 Redis实现的分布式锁,实际上是使用 Redis 的原子操作来确保多线程、多进程或多节点系统中,只有一个线程能获得锁,避免并发操作导致的数据不一致问题

  1. 锁的获取:Redisson 使用Lua 脚本,利用 exists+ hexists+ hincrby 命令来保证只有一个线程能成功设置键(表示获得锁)。同时,Redisson 会通过 pexpire命令为锁设置过期时间,防止因宕机等原因导致锁无法释放(即死锁问题)。
  2. 锁的续期:为了防止锁在持有过程中过期导致其他线程抢占锁,Redisson 实现了锁自动续期的功能(看门狗机制)。持有锁的线程会定期续期,即更新锁的过期时间,确保任务没有完成时锁不会失效。
  3. 锁的释放:锁释放时,Redisson也是通过 Lua 脚本保证释放操作的原子性。利用 hexists+del 确保只有持有锁的线程才能释放锁,防止误释放锁的情况。Lua 脚本同时利用 publish 命令,广播唤醒其它等待的线程。
  4. 可重入锁:Redison 支持可重入锁,持有锁的线程可以多次获取同一把锁而不会被阻塞。具体是利用 Redis 中的哈希结构,哈希中的 key 为线程ID,如果重入则 value + 1,如果释放则 value - 1 ,减到0说明锁被释放了,则 del 锁。

Redisson 看门狗(watch dog)机制了解吗?

Redisson 的看门狗(watchdog)主要用来避免 Redis 中的锁在超时后业务逻辑还未执行完毕,锁却被自动释放的情况。它通过定期刷新锁的过期时间来实现自动续期

主要原理:

  1. 定时刷新:如果当前分布式锁未设置过期时间,Redisson基于 Netty 时间轮启动一个定时任务,定期向 Redis 发送命令更新锁的过期时间,默认每 10s发送一次请求,每次续期 30s.
  2. 释放锁:当客户端主动释放锁时,Redison 会取消看门狗刷新操作。如果客户端宕机了,定时任务自然也就无法执行了,此时等超时时间到了,锁也会自动释放。

什么是RedLock?

Red Lock,又称为红锁,是一种分布式锁的实现方案,旨在解决在分布式环境中使用 Redis 实现分布式锁时的安全性问题。一般情况下,我们在生产环境会使用主从+哨兵方式来部署 Redis。如果我们正在使用 redis 分布式锁,此时发生了主从切换,但从节点上不一定已经同步了主节点的锁信息,所以新的主节点上可能没有锁的信息。此时另一个业务去加锁,一看锁还没被占,于是抢到了锁,开始执行业务逻辑。此时就发生了两个竞争者同时进入临界区操作临界资源的情况,可能就会发生数据不一致的问题。所以 Redis 官方推出了红锁,避免这种状况产生,它主要解决的问题就是当部分节点发生故障也不会影响锁的使用和数据问题的产生,。

相关推荐
IAtlantiscsdn2 小时前
Redis Stack扩展功能
java·数据库·redis
没有bug.的程序员2 小时前
Redis 大 Key 与热 Key:生产环境的风险与解决方案
java·数据库·redis·缓存·热key·大key
wuyunhang1234562 小时前
Redis----缓存策略和注意事项
redis·缓存·mybatis
零雲3 小时前
除了缓存,我们还可以用redis做什么?
数据库·redis·缓存
java搬砖工-苤-初心不变3 小时前
OpenResty 限流方案对比:lua_shared_dict vs Redis
redis·lua·openresty
winfield8214 小时前
Redis 线上问题排查简版手册
redis
lifallen6 小时前
字节跳动Redis变种Abase:无主多写架构如何解决高可用难题
数据结构·redis·分布式·算法·缓存
纪元A梦6 小时前
Redis最佳实践——安全与稳定性保障之高可用架构详解
redis·安全·架构
无敌的神原秋人18 小时前
关于Redis不同序列化压缩性能的对比
java·redis·缓存