redis

统计 UV 用 HyperLogLog 而不用 Set,因为 Set 要存储全部用户标识,内存占用大;HyperLogLog 不存原始数据,基于概率算法估算独立数量,内存极小,支持海量 UV 统计,缺点是结果有微小误差,拿不到明细。

Redis String,一般用来存储 token、地址缓存、实现分布式锁、存放帖子点赞数量,借助 incr 原子命令做计数。 List 可以实现简单的消息队列,但是功能简陋,可靠性不足。 Hash 适合购物车、用户信息缓存、直播间礼物统计,可以单独读写某个字段,不用操作全部数据。 Set 可以用来去重,比如帖子点赞:key 存帖子 id,集合里面存放用户 id,天然保证一个用户只能给同一个帖子点赞一次。 ZSet 可以实现排行榜,也可以按时间顺序展示帖子的点赞用户。

IO 多路复用就是,IO 线程它不是只监听一个 socket 通道,它是监听很多 socket 通道。然后它不会因为一个 socket 通道没有数据,然后就在那一直监听着,一直等待着。它是哪个 socket 通道有数据,它就处理哪个 socket 通道。

Redis 6 之前网络读写解析和命令执行都由主线程处理;Redis6 引入多 IO 线程,网络读写和解析交给多 IO 线程处理,命令执行仍然由主线程单线程串行执行。

Redis 持久化分 RDB 快照和 AOF 日志。RDB 满足条件时 fork 子进程生成二进制快照,文件小、恢复快,但可能丢一段数据;AOF 记录每一条写命令,刷盘策略可选,数据更安全,但是文件大、恢复慢。 生产上建议两者同时开启,重启优先加载 AOF;能接受少量丢数据就只用 RDB,不建议单独只用 AOF。

处理热 key 优先加本地缓存,如果压力还是很高,就通过 key 分片实现 Redis 多实例路由。原来的热 key 是 a,拆成 a1、a2、a3,存到不同 Redis 实例。查询的时候代码随机取 1、2、3 和 a 拼接,随机选一个分片 key 去查询。因为 Redis 集群里每个 key 全局唯一,拆分后的 a1、a2、a3 会落到不同节点,这样就把热 key 的读流量分散到多个实例上了。

缓存预热就是上线之前,提前把热点数据查出来放到 Redis 里,避免刚上线的时候大量请求直接打到数据库,出现缓存雪崩。

缓存预热就是上线前提前把热点数据加载进 Redis,防止刚上线大量请求直接访问数据库,把数据库打垮。大量请求会造成磁盘 IO 暴涨,同时 SQL 执行也会持续消耗 CPU,IO 和 CPU 扛不住,数据库就雪崩了。

RDB 是数据快照,可以配置多久、多少 key 修改就触发一次快照。触发的时候会 fork 子进程,fork 这个过程是同步的,真正生成快照刷盘是子进程异步去做。RDB 是二进制文件,体积比 AOF 小,恢复速度更快;缺点是宕机容易丢失两次快照之间的数据,因为它不是实时持久化。 AOF 需要手动开启,开启之后,增删改命令先写到 aof buffer,再按照 appendfsync 刷盘策略落盘。AOF 文件体积更大,恢复慢,因为恢复的时候要逐条重放命令。如果缓冲区的数据还没 fsync 刷到磁盘就宕机,就会丢失这部分数据。AOF 还有重写机制,用来消除冗余,比如同一个 key 连续 set a、set b、set c、set d,重写之后就只保留一条 set key d,精简文件。

单独用 RDB,容易丢失快照间隔内的数据;单独用 AOF,恢复速度慢。混合持久化同时开启两者,恢复时先加载 RDB 快照快速拿到基础数据,再回放快照之后的 AOF 日志补齐后面的修改,兼顾恢复速度和数据安全性。

RDB 和 AOF 组合恢复,先加载 RDB 快照,再执行 AOF 里所有命令。同一个 key,已经存在的时候修改,比 key 不存在、新建插入的耗时更短。 纯 AOF 恢复时内存是空的,所有命令都要新建 key,开销大;先加载 RDB 之后,大部分 key 已经存在,后面回放 AOF 命令大多只是修改已有 key,开销更小。 同时 RDB 快照之后新增的数据,会通过 AOF 的命令补到内存里面,这样既提升了恢复速度,又保证数据完整。

RDB、AOF 都属于 Redis 持久化方案,都能实现本机宕机后的数据恢复。 除此之外,RDB 还会在主从全量同步的时候,临时生成快照发给从节点,用来完成全量数据同步;AOF 没有这个能力,它只负责本机的持久化和恢复,不参与主从同步。

哨兵节点是特殊节点,专门用来监控 Redis 主从节点,不存储业务数据,具备监控、通知、自动故障转移的功能。

布隆过滤器底层是 bit 数组,多个 key 会共用 bit 位置。 举个例子:key1 把 1、8 号位设为 1;key2 把 2、5 号位设为 1。 现在来了一个根本不存在的 key3,哈希算出来刚好是 5 和 8 这两个位置。 这两个 bit 已经被前面两个 key 标记成 1 了,布隆就会判定这个 key 存在,但是数据库里实际没有这条数据,这就是布隆的误判。

布隆过滤器是一个 bit 数组和多个 hash 函数

第一种是先删缓存,再更新数据库。但是高并发场景下这个方案有问题,会出现旧数据提前写入缓存,造成不一致,所以衍生出延迟双删:先删缓存,更新数据库,隔一小段时间,再删一次缓存,把期间可能进来的旧缓存清掉。 第二种方案是用 Canal 监听 MySQL 的 binlog,数据库数据发生变更之后,Canal 拿到变更日志,异步去删除缓存。这个方案的好处是解耦,业务代码不用写缓存删除逻辑。

延迟双删的好处是延迟时间我们可以自己控制,缺点是会侵入业务代码;Canal 监听 binlog 的优势就是业务无侵入,不管哪里改库都能感知,但是它的延迟由 binlog 消费链路决定,我们没法自由控制。 不过这两种方案都只能做到最终一致性,没办法彻底解决并发读写带来的数据不一致问题。如果想要完全杜绝这个问题,就需要让读和写串行执行,读写共用同一把分布式锁。

我们给缓存设置过期时间当做兜底。就算主动删缓存失败,在过期之前这段时间读到的还是旧数据,等缓存过期之后,就会重新查数据库,加载最新数据。

redis和caffine同步多实例情况下

当某个实例完成数据修改之后,生产者发送缓存失效消息到 RabbitMQ 的 fanout 广播交换机。每个服务实例都有自己专属的队列,并且全部绑定到这个 fanout 交换机。消息会被分发到每一个实例的队列,所有实例监听自己的队列,收到消息之后,各自删除本机 Caffeine 里面对应的本地缓存,实现一处修改、全部实例本地缓存失效。或者设置短期过期时间进行兜底。或者延时双删单实例。过期时间兜底单实例多实例都能适用。

缓存预热就是提前查一下数据然后将数据存到缓存里面防止刚上线一下子涌来大量不同的请求导致数据库压力过大

Redis 默认的淘汰策略是 noeviction:内存满了之后,新来的写请求直接拒绝,返回报错,不删任何 key。

然后四个策略按你原话整理:

  1. allkeys-lru:在所有 key中,淘汰最近最少使用的 key
  2. volatile-lru:只在设置了过期时间的 key里面,淘汰最近最少使用的 key
  3. volatile-random:只在设置了过期时间的 key里面随机删除
  4. allkeys-random:在全部 key里面随机删除

👉 重点坑(面试很容易问到): volatile 开头的这两个,如果 redis 里面没有任何带过期时间的 key,内存满了的时候,它就和 noeviction 一样,直接拒绝写入,不会随便删没过期的 key

Redis 默认淘汰策略是 noeviction,内存满了再插入新数据会直接拒绝。 allkeys-lru:所有 key 按照 LRU 淘汰; volatile-lru:只在设置了过期时间的 key 里按 LRU 淘汰; allkeys-random:所有 key 随机删除; volatile-random:只在有过期时间的 key 里随机删除; allkeys-lfu、volatile-lfu:按照 LFU 策略淘汰。 LRU 淘汰最久没有访问的 key;LFU 淘汰访问次数最少的 key。

Redis 主线程是单线程,IO 线程是多线程。当执行 EXEC 的时候,事务队列里的命令会在主线程连续执行,这个期间别的客户端命令要排队等待,不能插队。 注意:MULTI 之后、EXEC 之前,命令只是入队,不会阻塞其他命令。

Redis 基于内存存储,能放的数据量比不上 MySQL 这类磁盘数据库,持久化的数据量同样受限;Redis 的事务功能很弱,不像 MySQL,它的事务不支持回滚;而且 Redis 没有表结构,字段没法像 MySQL 表一样规整定义,数据组织起来比较乱,原生也不支持联表查询。

超短背诵版: Redis 基于内存,存储和持久化的数据量都有限;事务不支持回滚,能力弱;没有表结构,数据不规整,不支持联表查询,所以不适合做主库。

频繁查询、追求速度、需要定时过期的数据,适合用 Redis。

ZSet 和 List 相同点:都是有序的,都支持范围查询。 不同点:ZSet 按 value(member)去重,根据 score 排序;List 按插入顺序排序,元素可以重复,有 lpush、lpop,取出的时候自动删掉,适合做简单队列。ZSet 查到数据之后,需要手动删除。

AOF 不记录读命令(get、mget 这类查询) ,只记录写命令。 但是它会记录 select 0、select 1 这种切换数据库的命令。

  1. @PostConstruct:启动过程执行,时机早,可能碰到未就绪 Bean;默认无重试,抛异常直接启动失败,catch 吞异常会静默失败。
  2. ApplicationRunner:所有 Bean 初始化完成后执行,更稳;失败规则同上,自带也没有重试。
  3. 稳妥方案:服务启动后先不接流量,通过内部接口手动预热,校验数据没问题再上线;可加定时任务兜底。
  4. 补充:Spring 托管 Bean 只存引用没事,启动阶段别直接调用;volatile 管指令重排,解决不了 Spring 时序问题。
  5. 上线前查一下,查完同步完再进行上线

缓存预热就是项目上线前提前同步缓存,把 DB 数据加载进 Redis,避免用户请求击穿数据库。 可以手动预热,也可以用@PostConstruct做启动自动预热; 但@PostConstruct执行时机较早,不能保证依赖 Bean 已经完成初始化,风险更高。 推荐实现ApplicationRunner接口完成预热,它在所有单例 Bean 加载初始化完成之后执行,更加安全。

缓存降级是业务保护手段,分为主动降级和被动降级。被动降级在缓存接口慢请求、异常过多时触发;主动降级一般在流量高峰人工开启。不再访问缓存,直接走兜底。 原因:避免慢请求占满线程、减轻 Redis 压力,牺牲非核心功能,保障核心业务正常可用,防止故障扩散。

单纯 Redis Cluster 不需要消息广播,所有服务共享集群数据; 当引入 Caffeine 本地多级缓存之后,本地缓存是 JVM 隔离的,一个实例更新 Redis,其他实例的本地缓存不会自动失效。 所以采用 RabbitMQ fanout 交换机广播变更通知,每个实例监听自己独立队列,收到消息后清除自身本地缓存,解决多实例本地缓存不一致的问题。

Redis ZSet 实现延迟队列,将消息的到期时间戳作为 score,score 越小代表越早到期;定时任务正序查询 zset,筛选 score 小于等于当前时间的消息,处理完成后删除,实现延迟消费。

利用 RabbitMQ TTL + 死信交换机实现延迟消息,消息先进入无消费者的队列,到期后转发到业务队列消费;缺点是基于队列排队机制,前面消息没过期,后面消息就算到时间也无法触发,实际执行时间会晚于设定值。设置队列级别ttl

队列 TTL 是给队列配置统一过期时间,进入该队列的所有消息过期时长相同;缺点是灵活性差,并且依然存在队首阻塞,后面消息到期也没法及时转发。

  1. Pipeline 只是打包发送、批量执行 ,不具备原子性,中间某个命令失败,前面执行成功的不会回滚(和 MULTI/EXEC 事务不一样)
  2. Redis 服务端收到这批命令,依次执行,最后一次性把所有结果返回客户端

红锁是向多个独立 Redis 主节点去加锁,半数以上实例加锁成功才算拿到锁。它主要就是为了解决普通主从切换时,锁还没同步、主宕机导致锁丢失的问题。

先用 bigkeys 排查大 key。优先拆分大 key 为多个小 key;如果已经影响性能,用 unlink 异步删除;也可以把数据压缩序列化后存储,但是压缩会消耗 CPU,所以拆分是最好的方式。

Redis 采用惰性删除 + 定期删除。惰性删除是访问 key 时才校验过期并删除;定期删除是随机抽样扫描部分过期 key。如果过期 key 一直不被访问,又没被抽样扫到,就会过期了但还在内存中。

Redis 突然变慢的原因:

大 key:读写、删除大 key,阻塞单主线程,请求排队;

内存快要占满:频繁触发内存淘汰策略,额外消耗 CPU;

网络问题:网络抖动、带宽打满,RTT 变高;

AOF 刷盘配置为 appendfsync always:每一条写命令都 fsync 刷磁盘,磁盘 IO 慢就卡住主线程;

再加几个高频考点,面试官很爱追问: 5. 执行了慢命令:keys、hgetall、smembers 这类全量遍历命令; 6. RDB fork 子进程:fork 一瞬间阻塞主线程; 7. AOF 重写:重写阶段大量 IO、占用 CPU; 8. 热 key:大量请求打到同一个 key,单实例压力顶不住; 9. 客户端输出缓冲区溢出、大量连接堆积。

Redisson 看门狗默认每 10 秒检查一次,如果当前线程还持有锁,就把锁的过期时间重置为 30s。如果手动指定过期时间就不会生效了,看门狗机制就不会生效了。3

Redisson 看门狗机制:使用 Redisson 加锁,没有手动设置锁过期时间时,会开启看门狗。默认锁有效期 30 秒,续期间隔是锁过期时间的 1/3,也就是 10 秒。定时任务会校验当前业务线程是否还持有这把锁,如果还持有,就把锁的过期时间重新重置为 30 秒。

底层流程:调用lock()之后,内部调用tryAcquireAsync。这个方法判断有没有传入 leaseTime 过期时间:

  • 如果设置了过期时间,直接返回,不开启看门狗;
  • 如果没有设置过期时间,就开启续期定时任务,同时把线程信息存入本地的EXPIRATION_RENEWAL_MAP(ExpirationEntry 条目)。 定时任务依靠这个 Map 判断线程是否还持有锁,持有就持续续期;解锁的时候会移除这个 Map 里的条目,看门狗任务就停止。
  • 然后续期的话是通过lua脚本进行续期的。

我项目里选用 Redisson 实现 Redis 分布式锁。Redisson 是对 Redis 客户端的封装,它自带看门狗自动续期机制。 首先锁 key 要根据业务设计,争抢同一资源的线程使用同一个唯一 key,比如库存扣减就用商品 id 作为标识。 通过 RLock lock = redissonClient.getLock("锁key") 获取锁。 调用 lock () 加锁时,如果没有手动指定 leaseTime,看门狗就会开启;默认锁有效期 30s,每隔 10s(1/3 的过期时间)执行续期,底层通过 Lua 脚本原子校验当前线程是否是锁持有者,持有就重置过期时间,避免业务没跑完锁提前释放导致超卖。 解锁要放在 finally 块中,执行 lock.unlock () 释放锁。解锁的时候会移除本地 EXPIRATION_RENEWAL_MAP 里的条目,看门狗定时任务就停止,不再续期。

Redisson 的 RLock 分布式锁:

  1. 调用lock(),内部执行tryAcquireAsync;如果没有手动指定 leaseTime,就开启看门狗。
  2. 本地有一个ConcurrentHashMap<String, ExpirationEntry>:key 是锁名,value 是 Entry 对象,Entry 存放线程 id、看门狗定时任务,用来管理续期任务。
  3. 定时任务调用renewExpirationAsync,通过 Lua 脚本原子校验:当前线程是不是锁持有者,是就重置锁过期时间(默认 30s,每 10s 续一次)。
  4. Redis 这边锁是 hash 结构:key = 锁名,field = 线程 id,value = 重入次数,以此实现可重入;多次 lock 计数累加,多次 unlock 计数递减,计数归零锁才释放。
  5. 解锁写在 finally 里,unlock()也是 Lua 脚本,只能自己解自己的锁;解锁时删除本地 Map 里的 Entry,看门狗任务停止,不再续期。
  6. 隐患:业务线程卡死、进程还活着,看门狗会一直续锁,其他线程拿不到锁;长时间 STW,看门狗来不及续期,锁提前释放,出现超卖。

Redisson 分布式锁底层原理: JVM 本地维护一个 ConcurrentHashMap,key 是锁名称(字符串),value 是RedissonLockEntry对象。Entry 里面保存持有锁的线程 ID、重入次数、看门狗定时任务、信号量。 加锁的时候,先判断本地 map 里这个锁 key 是否存在:

  • 如果存在,对比线程 ID:不是同一个线程,加锁失败;是同一个线程,重入次数 + 1(可重入)。
  • 如果不存在,就通过 Lua 脚本在 Redis 加锁,Redis 里用 Hash 结构存锁信息(锁 key、线程标识、重入次数),保证原子性,不是简单的 setnx+ex 分开写。

如果我们没有手动指定锁过期时间,默认锁有效期 30 秒,会启动看门狗定时任务:每隔 1/3 有效期也就是 10 秒执行一次,检查当前线程还持有锁,就用 Lua 脚本把锁过期时间重新重置为 30 秒; 如果手动传入了 leaseTime(过期时间),看门狗就不会启动,不会自动续期。 解锁的时候,同样走 Lua 脚本,重入次数减一,次数降到 0 才真正删除锁。

相关推荐
苏宸啊2 小时前
MySQL(一)
数据库·mysql
2603_969694853 小时前
JDBC 基础学习笔记:API 核心原理解析与 CRUD 代码实操
数据库·笔记·mysql·学习方法·database·sql注入
这个DBA有点耶3 小时前
库内语义构建 vs 外挂向量库:业务语义缺失,才是RAG“胡说八道”的根源
数据库·mysql·aigc
kobe_OKOK_4 小时前
mysql 开启 binlog 日志
数据库·mysql
鸽芷咕5 小时前
KingbaseES 调参先看执行计划,参数是答案不是起点
数据库
码哥字节5 小时前
Redis 分布式锁的 7 个坑:从 setnx 到 Redlock
redis·分布式锁
这个DBA有点耶5 小时前
MySQL 8.0直方图:统计信息升级了,执行计划为什么还是选错?
数据库·mysql·代码规范
code2cat6 小时前
【随笔】推理准入控制:请求进来之前,先给KV缓存留出位置
缓存·大语言模型·vllm