Redis 场景题
如果发现 Redis 内存溢出了?你会怎么做?请给出排查思路和解决方案
遇到线上问题,第一时间是止损,如果发现 Redis 内存溢出,则应该立即扩容,增加 Redis 实例的内存,保证线上业务正常运行。
然后再排查内存溢出的可能原因:
- 数据过多: Redis 中存储的数据量过大,超过了可用内存。
- 数据过期策略失效: 大量的 key 没有设置过期时间,导致内存不断增长。
- 大对象或大数据结构: 某些 key 关联的数据结构(如 hash、list、set)过于庞大,占用了大量内存。
- 持久化机制影响:RDB 采用写时复制,极端情况下会占用正常两倍的内存
常见优化方案:
- 调整内存淘汰策略:根据业务需求选择合适的淘汰策略,如 allkeys-lru 或 volatile-lru,以便在内存接近上限时能够自动删除较少使用的 key。
- 设置数据过期时间: 对重要的数据设置合理的过期时间,避免无用数据长期占用内存。
- 优化数据结构: 检查大对象或大数据结构的使用情况,优化存储方案,如将大型 hash 拆分成多个小 hash。
- 垂直扩展: 升级单台 Redis 配置。
- 水平扩展: 将数据分片到多个 Redis 实例,使用集群或分布式缓存来减轻单实例的内存压力。
- 数据持久化调整: 优化持久化策略,减少额外的内存消耗,例如在低峰期触发 RDB 备份数据。
监控告警
针对线上的一些中间件,不论是Redis、MySQL、 MQ等,都需要计对实例对应机器的基础信息(CPU/内存/IO)和业务信息(key数量、表大小、消息堆积量等)进行些控,超过一定國值后告警,使得我们可以提前发现问题、解决问题,避免事故的产生。
例如云厂商(本地则需要自己搭建)就配套提供了很多告警接入(钉钉、企微、邮件等等),在对应中间件配置内即可
如果提前监控 Redis 的内存情况,例如占用 80% 就告警通知,那么就可以避免 Redis 内存满报错的问题
问题排查思路
观测一段时间 Redis 的内存使用量,如果发现从某一天开始,内存使用量开始明显增加,再结合那股时间的发版情况,复查那个版本提交的代码中涉及 Redis 操作,即可快速定位到问题
如果是代码 bug 则修复代码。
如果是正常业务功能增加导致需要缓存的数据变多,则思考是否可以精简存储 value,并考虑是否可以设置过期时间,或过期时间是否可以再短一些。
最终优化后,如果确定目前的应用就是占用这么多内存,则只能扩容。
Redis 持久化
Redis 淘汰机制
Redis 过期策略
商家想要知道自己店铺卖的最好的 top 50 商品,如何实现这个功能?
根据问题推断这个排行榜是一个动态的排行榜,店铺内商品一直在售卖,因此排行榜也会一直在变,所以如果利用数据库来排行计算效率会非常低,总不能每卖一单就利用数据库排序计算得到排行榜吧?
所以排除数据库这个方案。
应对数据库效率差的情况,我们常用的替代方案就是缓存,而 Redis 内有个zset 可以实现排行榜的功能
每个商家都对应有个 zset,score 存储商品售卖的数量,value 存储商品 ID。
每售卖一个商品,就可以利用 ZINCRBY 更新对应 score+1,时间复杂度为 O(1)
然后商家在查看 top 50 商品的时候,利用 zrevrange key 0 49 获取排行榜,底层存储用的是跳表,时间复杂度为O(logN+M),N 为总元素数,M 为返回的元素数。
线上发现 Redis 机器爆了,如何优化?
- 首先需要排查并确定根因:通过监控工具(如 Prometheus 、 Grafana 等或云上自带的一些监控)査看 Redis 的 CPU、内存、命令执行时间或带宽等指标,确定瓶颈所在
- 依据根因针对性解决问题:
- 如果是内存耗尽,可能需要增加机器的内存量(升配)即临时升配解决线上问题后。后续再走查代码和了解相关业务,判断有些数据是否有必要都存储到 Redis 中,有些数据的过期时间是否设置的过长等等来优化占用 Redis 的大小。
- 如果是 CPU 使用率飙升,可能是因为大量的读写请求,且涉及复杂度较高的命令,比如聚合、排序等等。此时可能需要重新发版或者回退版本下线有影响的业务,后续针对具体业务评估优化,是否可以将一些操作放到后端服务执行,将一些包含大量数据的大key 拆分成多个小key。也可以使用 Redis 集群分片,分散计算压力。
- 如果是带宽方面,则需要增加带宽配置,加大带宽。
除了 Redis 自身的优化之外,还可以利用本地缓存来降低 Redis 的负载。
一些热,点数据可以存储在后端服务本地,请求先打到后端服务,如果命中本地缓存,则不需要请求Redis 直接可以返回数据,这种情况就大大的减轻了 Redis 压力,解决(集群内)单机器负载高的情况。
还有,除了上述的应急处理手段之外,各组件应都要配置监控报警机制,例如内存的國值,当内存占用率法到 80%6就很警(邮件、企散、钉红、短信)等通知开发者,及时外理即将发生的问题,避免问题进一步扩大
项目用了 Redis 缓存来提升并发度,假设 Redis挂了怎么办?
主要有三点需要考虑:
- 第一点先尽量保证 Redis 自身高可用:可以通过主从复制+哨兵 (Sentimnel)或Redis Cluster,让Redis 挂掉后能自动切换主节点(比如主节点宕机,哨兵选举从节点升主),尽可能成少服务中断时间(通常秒级恢复)
- 第二点限流+熔断,防止突然的请求打垮数据库:如果高可用失效,所有请求会直接打向数据库。需用限流(如Sentinel限制QPS)和熔断(如Hystrix)
- 第三点添加兜底降级方案:除了限流和熔断外,还需通过本地缓存(如Caffeine)或返回默认数据(defalut 值)暂时支撑,尽量保证用户的请求是通畅的。
例如某电商大促 Redis 宕机处理
Redis 集群因网络故障全挂,请求直接打向数据库。
大致处理步骤:
- 哨兵尝试自动切换(失败,因网络问题)
- 触发降级:应用层启用本地缓存(缓存TOP 100商品),返回30%流量
- 开启限流:数据库入口QPS限制为xxx
- 熔断:当数据库错误率超30%,返回“商品暂不可见”
- Redis 恢复后,通过 Binlog 同步 2 小时内的订单变更,30 分钟完成缓存填充。
MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
核心思路是通过缓存淘汰策略+热点探测,动态保留访问频率高的数据
- 选对淘汰策略:Redis 中设置 maxmemory 且使用LRU(最近最少使用) 或 LFU(最不经常使用)策略,自动淘汰长时间低频率访问的数据,保证内存中留存的是“最近/最常被访问”的热点
- 热点探测:通过埋点统计访问频率,主动将高访问数据加载到Redis。比如使用开源的京东 hotkey 实现探测。在项目刚启动的时候,我们根据业务经验或历史数据预测热点,手动选取一些数据塞入缓存进行预热,避免缓存击穿。然后通过热点探测发现热点,将其塞入 Redis 中,并且利用 Redis 淘汰机制确保当前在 Redis 中的是最热的数据,相对不常用的数据就会被淘汰
项目上用了分布式锁,加锁后并发度不就降低了吗?
这其实是一个陷阱题
不一定,因为很多业务场景如果正确使用分布式锁并不会降低并发度。
看加锁的粒度,比如对一个商品进行购买,为了防止同一个用户重复购买,以商品ID为分布式锁的 key,那么对单个商品的并发度直接就变成了1。
但是如果将分布式锁的 key改为userID+商品ID,此时的分布式锁基本上不会影响并发度,因为拦载的是同一个用户的请求,对其他用户来说是无锁的,这就是一个分布式锁的粒度问题
所以需要合理的锁设计(缩小粒度、缩短时间等),就能平衡并发与安全,实际对整体并发影响可控。
如何减少锁对并发的影响?
加锁的本质实际上就是“牺牲部分并发”换取“数据正确性”,避免更严重的业务事故(如超卖导致用户投诉、资金损失)。所以很多场最确实会对并发造成影响(不加锁的话,并发肯定更高,只不过数据会有问题)
不过有一些手段可以减少锁对并发的影响:
缩小锁粒度:锁的范围越小,并发越高
- 反例:用全局锁(如锁“整个库存”),所有扣减操作都要排队,并发度=1。
- 正例:用“行锁”(如锁“商品ID=123的库存”),不同商品的扣减可并行,并发度=商品数量
缩短锁持有时间:锁的时间越短,等待越少
锁的持有时间=“业务操作耗时”+“锁释放耗时”
- 剥离非必要操作:将不需要锁保护的逻辑(如日志记录、消息通知)移到锁外。
- 异步化:锁内只做核心操作(如扣减库存),其他操作(如更新缓存、发MQ消息)用异步线程处理。
举例:原来锁内流程是“查库存一扣减一更新缓存一写日志”(耗时200ms),优化后锁内只保留“查库存一扣减”(耗时50ms),其他操作异步,锁持有时间缩短75%,并发度提升4倍。
无锁替代方案:部分场景不需要锁
如果业务允许“非强一致”或能通过其他方式保证安全,可跳过锁:
- CAS(乐观锁):用版本号或时间戳实现“先检查后更新”,例如库存表加 version字段,更新时检查version,是否匹配(update stock set count=count - 1 where id = 123 AND version = 5),匹配则成功,匹配则重试。
- 分布式事务:用TCC(Try-Confirm-Cancel)或Seata等框架,通过补偿机制替代锁。
分布式锁一般都怎样实现?
Redis 实现
基于缓存实现分布式锁性能上会有优势,可以使用 Redis SETNX(SET if Not eXists)实现分布式锁.
注意锁需要设置过期时间,防止应用程序崩溃导致锁没有释放而阻塞后面的所有操作。
获取锁:使用 jedis.set(lockKey,lockvalue,"","PX”,lockTimeout)尝试获取锁,NX 确保键不存在时才设置,PX 设置键的过期时间(毫秒)。
释放锁:使用 Lua 脚本确保只有持有锁的客户端才能删除锁。Lua 脚本会检查键的值是否等于 lockValue,如果是则删除该键。
简单示例如下,acquireLock为获取锁,releaseLock为释放锁:
public class RedisDistributedLock {
private static final String LOCK_SUCCESS = "OK";
private static final Long RELEASE_SUCCESS = 1L;
private Jedis jedis;
private String lockKey;
private String lockValue;
private int lockTimeout;
public RedisDistributedLock(Jedis jedis, String lockKey, int lockTimeout) {
this.jedis = jedis;
this.lockKey = lockKey;
this.lockTimeout = lockTimeout;
this.lockValue = UUID.randomUUID().toString();
}
public boolean acquireLock() {
String result = jedis.set(lockKey, lockValue, "NX", "PX", lockTimeout);
return LOCK_SUCCESS.equals(result);
}
public boolean releaseLock() {
String releaseScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
Object result = jedis.eval(releaseScript, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
return RELEASE_SUCCESS.equals(result);
}
public static void main(String[] args) {
// 创建一个 Jedis 连接实例
Jedis jedis = new Jedis("localhost", 6379);
// 创建分布式锁实例
RedisDistributedLock lock = new RedisDistributedLock(jedis, "my_lock", 10000);
// 尝试获取锁
if (lock.acquireLock()) {
try {
// 执行你的业务逻辑
System.out.println("Lock acquired, executing business logic...");
} finally {
// 释放锁
lock.releaseLock();
System.out.println("Lock released.");
}
} else {
System.out.println("Unable to acquire lock, exiting...");
}
// 关闭 Jedis 连接
jedis.close();
}
}注意 lockValue 需要保证唯一,防止被别的客户端释放了锁。
这里有个问题,如果业务还没执行完,则 Redis 的锁已经到期了怎么办? 因此引入“看门狗”机制,即起一个后台定时任务,不断地给锁续期,如果锁释放了或客户端实例被关闭则停止续期,Redison 提供了此功能注意:未指定超时时间的分布式锁才会续期,如果指定了超时时间则不会续期,默认 30s 超时,每 10s 续期一次,续期时长为 30s。
除了锁续期问题,还有单点故障问题,如果这台 Redis 挂了怎么办?分布锁就加不上了,业务就被阻塞了。因此需要引入Redis 主从,利用哨兵进行故障转移,但是这又会产生新的问题,如果 master挂了,锁的信息还未传给 slave节点,此时 slave 上是没加锁的,因此可能导致多个实例都成功上锁。所以 Redis 作者又提出了 RedLock 即红锁,通过引入多个主节点共同加锁来解决单点故障问题(没有哨兵和 slave 了)。
比如现在有5个Reds 节点(官方推荐至少5个),客户端获取当前时间T1,然后依次利用SETNK对5个 Reds节点加锁,如果成功3个及以上(大多数),再次获取当前时间T2,如果 T2-T1小于锁的超时时间,则加锁成功,反之则失败
如果加锁失败则向全部节点调用释放锁的操作。
但是这个 redlock 还是有缺点的,首先它比较重,需要5个实例,成本不低。
其次如果发生时钟偏移,比如5个节点中有几个节点时间偏移了,导致锁提前超时了,那么有可能出现新客户端争抢到锁的情况,但是这个属于运维层面的问题
还有一个就是 GC问题,如果客户端抢到锁之后,发生了长时的 Gc导致redis中锁都过期了,这样一来别的客户端就能得到锁了,目老客户端 GC后正常执行后续的课作,导致并发修改,数据可能就不对了,不过这个问题无法避免,任可锁都可能会这样。
关于 RedLock 可以使用 Redisson ,它提供了 RedLock。
一般业务上,如果我们要使用 Redis 分布式锁,基本上使用 Redisson 客户端。
ZooKeeper
除了 Redis 还可以使用 ZooKeeper 的临时有序节点实现分布式锁
临时 能保证超时释放,有序 能选出谁抢到了锁。
大致流程:多进程争抢创建 ZooKeeper 指定目录下的临时有序节点,创建序号最小的节点即抢到锁的进程,释放锁可以删除此节点,如果服务端挂了也会释放这个节点。
优点:如果本身已经引入了 ZooKeeper 则成本不大,实现比较简单。
缺点:相比于Redis 性能没那么好,ZooKeeper的写入只能写到主节点,然后同步到从节点。并且临时节点如果产生网络抖动,节点也会被删除,导致多个客户端抢到锁(当然有重试机制,产生的概率比较低)
可使用 curator 客户端实现的分布式锁接口。
朋友圈点赞功能如何实现,简单说说?
首先我们要理清朋友圈点赞具体需要涉及哪几个功能点:
- 存储点赞信息:需要存储哪些用户点赞了这条朋友圈,具体需要存储用户ID、点赞时间即可。
- 取消点赞:需要快速找到这名用户,将其移除点赞列表,
- 获取点赞列表:朋友圈需要展示点赞的用户头像列表信息
核心就是这么三点,其实就是增删查,那用什么来实现比较合适呢?
实现快速存储和删除,Set 就挺合适,而且还能天然去重,但是常见的 HashSet 是无序的,朋友圈的点赞列表需要按时间顺序排序。
而 TreeSet 理论上可以支持这些功能,顺序按点赞时间排序,所以在内存中利用 TreeSet 来保存朋友圈数据。
但是数据保存在 Java 内存中不合理,因为应用重启一下不就没了?
同样是内存,我们可以联想到 Redis ,将数据存放到 Redis 里即可,Redis 也支持 aof 和 rbd 持久化.
并且 Redis 恰巧有个 zset 结构,可以满足我们的需求。
zset 的 key 可以设置此条朋友圈的 id,score 为点赞时间,value 为点赞用户 ID。如果用户点赞,则将用户 id 和当前时间添加到 zset 中,时间复杂度为O(logN),N 为排序集合内总元素数
如果用户取消点赞,则从 zset 中移除此用户,时间复杂度为 O(logN*M),N 为排序集合内总元素数,M 删除的元素数.查看朋友圈点赞列表,调用 zset的 zrange key 0 -1即可,底层存储用的是跳表,时间复杂度为0(logN+M),N 为排序集合内总元素数,M 为返回的元素数。
如何设计一个点赞系统?
用户点赞请求:
点赞请求首先写入 Redis(用户状态 +视频点赞计数)返回快速响应给用户,点赞操作实时生效。
异步任务记录点赞:
点赞操作通过消息队列写入后台任务,后台任务定期将 Redis 数据同步到 MySQL,确保持久化存储.
读取点赞状态和计数:
点赞状态和计数优先从 Redis 读取,未命中时再从 MySQL 加载。
缓存与数据库一致性:
Redis 中的数据与 MySQL 通过定时任务(或触发机制)进行同步。
