大厂面试官问我:Redis缓存如果扛不住,该怎么办?【后端八股文十一:Redis缓存八股文合集(1)】

 本文为【Redis分布式锁八股文合集(2)】初版,后续还会进行优化更新,欢迎大家关注交流~

hello hello~ ,这里是绝命Coding——老白~💖💖 ,欢迎大家点赞🥳🥳关注💥💥收藏🌹🌹🌹
19d95742d45b4220ad0ae0359ffcba93.png

💥个人主页绝命Coding-CSDN博客
💥 所属专栏后端技术分享
这里将会不定期更新有关后端、前端的内容,希望大家多多点赞关注收藏💖

    往期内容(篇幅过多,不一一列出,感兴趣的小伙伴可以查看专栏):

大厂面试官问我:Redis处理点赞,如果瞬时涌入大量用户点赞(千万级),应当如何进行处理?【后端八股文一:Redis点赞八股文合集】-CSDN博客

大厂面试官问我:布隆过滤器有不能扩容和删除的缺陷,有没有可以替代的数据结构呢?【后端八股文二:布隆过滤器八股文合集】-CSDN博客

大厂面试官问我:Redis持久化RDB有没有可能阻塞?阻塞点在哪里?【后端八股文三:Redis持久化八股文合集】-CSDN博客

Redis缓存

为什么Redis做缓存?

(高并发,高性能——内存)
(1)高并发
承受的请求  大  ( 直接缓存   数据库)
(单台设备的 Redis 的 QPS(每秒处理完请求的次数)是Mysql的10倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w)
(2)高性能
第一次访问,从硬盘;
缓存,直接内存,速度快

redis作为缓存的使用场景?/ 哪些数据适合放入缓存

即时性、数据线要求不高
访问量大且更新频率不高的数据(读多,写少)

比如:电商商品

什么是缓存预热

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

如果不进行预热,那么Redis初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。

缓存预热解决方案:

数据量不大的时候,工程启动的时候进行加载缓存动作;

数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;

数据量太大的时候,优先保证热点数据进行提前加载到缓存。

假设一种高热的数据它突然之间过期了,怎么预防这样的情况?你就那比如说恰好这一秒钟我过期了,正好这一秒钟有一两万个QPS

  1. 分布式缓存: 考虑使用分布式缓存方案,如Redis集群,将热数据分散存储在多个节点上,减少单一节点的过期负担。
  2. 过期时间随机化: 在设置过期时间时,可以引入一些随机化因素,以减少在相同时刻过期的可能性。
  3. 限制过期数据量: 设置过期数据的限制,确保在同一时刻过期的数据数量有一个上限,从而避免过多的数据一起过期。
  4. 增加缓存节点: 如果预计在某个时间段内会有大量数据过期,可以临时增加缓存节点数量,以扩展缓存容量和处理能力。
  5. 过期前刷新: 对于热数据,可以在过期之前进行刷新,延长数据的有效期。这样可以避免数据突然过期造成的负载冲击。
  6. 流量控制: 在高并发情况下,可以实施流量控制机制,限制同时访问过期数据的请求数量,减轻过期时的压力。
  7. 淘汰策略: 使用合适的淘汰策略,如LRU(最近最少使用)等,使得热数据能够保持在缓存中,减少过期的影响。

缓存雪崩,缓存穿透,缓存击穿 / 常见问题解决方案

(缓存穿透——不存在、缓存击穿——访问量大、缓存雪崩——过期时间)

缓存雪崩
指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。

解决:
缓存数据的过期时间设置随机(防止同一时间大量数据过期现象发生)
加锁排队:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待;
数据预热:可以通过缓存 reload 机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀;

  1. 均匀设置过期时间:设置不同的过期时间,让缓存失效的时间点尽量均匀。通常可以为有效期增加随机值
  2. 互斥锁:同一时间只让一个线程构建缓存,其他线程阻塞排队。
  3. 双 key 策略:
    主缓存:有效期按照经验值设置,设置为主读取的缓存,主缓存失效后从数据库加载最新值
    备份缓存:有效期长获取锁失败时读取的缓存主缓存更新时需要同步更新备份缓存
    (限流、提前缓存)

缓存击穿
一份热点数据访问量过大,在缓存失效,请求直达存储层     服务器崩溃
(可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。)
解决:
永不过期,不给热点数据设置过期时间
加互斥锁

缓存穿透
客户端查询不存在的数据     请求直达存储层    负载过大(既不在缓存中,也不在数据库中
原因:业务层误将缓存和库中数据删除
有人恶意访问库中不存在的
解决:
缓存空对象,并加入短暂过期时间(存储层未命中,仍将空值缓存)
布隆过滤器(数据存入过滤器,访问缓存前拦截,若请求数据不存在则返回空值)
非法请求的限制

缓存击穿的互斥锁怎么实现的?

使用setnx作为Redis中的锁。

Redis中查询缓存

  • 存在且不为空值,直接返回
  • 为空值(比如“”、0等特殊值),返回失败结果
  • 不存在,获取锁

获取锁失败,等待重试

获取成功,查找MySQL

  • 不存在,Redis存入空值
  • 存在,写入Redis

释放锁,返回结果

public Result queryShopById(Long id) {

	// 用String形式存储JSON
	String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

	// 如果查询结果不为null,直接返回
	if (StrUtil.isNotBlank(shopJson)) {
		Shop shop = JSONUtil.toBean(shopJson, Shop.class);
		return Result.ok(shop);
	}

	// 否则Redis中查询结果为空,判断是否为“”
	if (shopJson != null) {
		return Result.fail("店铺不存在,请确认id是否正确");
	}

	// 尝试获取锁,
	// 如果没有得到锁,Sleep一段时间
	if (!tryLock(LOCK_SHOP_KEY + id)) {
		try {
			Thread.sleep(50);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		// 从开始重试
		return queryShopById(id);
	}

	// 获得了锁,从MySQl中查找
	Shop shop = this.getById(id);
	// 模拟重建的延时
	try {
		Thread.sleep(200);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	// 不在MySQL中
	if (shop == null) {
		// 将空值写入Redis
		stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
		// 释放锁
		unLock(LOCK_SHOP_KEY + id);
		return Result.fail("店铺不存在,请确认id是否正确");
	}
	else {
		// 在MySQL中,存入redis
		stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
		// 释放锁
		unLock(LOCK_SHOP_KEY + id);
		return Result.ok(shop);
	}
}

public boolean tryLock(String key) {
	// 尝试获取锁,set成功返回true,否则返回false
	Boolean getLock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
	// 避免getLock为null,使用工具类
	return BooleanUtil.isTrue(getLock);
}

public void unLock(String key) {
	stringRedisTemplate.delete(key);
}

一般我们在更新数据库数据时,需要同步redis中缓存的数据,所以存在两种方法:
第一种方案:先执行update操作,再执行缓存清除。
第二种方案:先执行缓存清除,再执行update操作。
这两种方案的弊端是当存在并发请求时,很容易出现以下问题:
第一种方案:当请求1执行update操作后,还未来得及进行缓存清除,此时请求2查询到并使用了redis中的旧数据。
第二种方案:当请求1执行清除缓存后,还未进行update操作,此时请求2进行查询到了旧数据并写入了redis。

所以此时我们需要使用第三种方案:先进行缓存清除,再执行update,最后(延迟N秒)再执行缓存清除。

上述中(延迟N秒)的时间要大于一次写操作的时间,一般为3-5秒。
        原因:如果延迟时间小于写入redis的时间,会导致请求1清除了缓存,但是请求2缓存还未写入的尴尬。。。
        ps:一般写入的时间会远小于5秒

先更新db再删缓存,如果删失败了怎么办?

事务,回退数据库
延迟删除

如果Redis缓存扛不住怎么办?

使用本地缓存(在面对大部分并发场景或者一些中小型公司流量没有那么高的情况,使用redis基本都能解决了。但是在流量较高的情况下可能得使用到本地缓存了)
比如guava的LoadingCache和快手开源的ReloadableCache。

如果单节点qps达到了千级别就要解决单点问题了(即使redis号称能抗住十万级别的qps)

本地缓存

在一些场景下可能单纯使用Redis类的远程缓存已经不够了,还需要进一步配合本地缓存使用,例如Guava cache或Caffeine,从而再次提升程序的响应速度与服务性能。于是,就产生了使用本地缓存作为一级缓存,再加上远程缓存作为二级缓存的两级缓存架构。

为什么要使用本地缓存
  • 本地缓存基于本地环境的内存,访问速度非常快,对于一些变更频率低、实时性要求低的数据,可以放在本地缓存中,提升访问速度
  • 使用本地缓存能够减少和Redis类的远程缓存间的数据交互,减少网络I/O开销,降低这一过程中在网络通信上的耗时

本地缓存问题及解决
1. 缓存一致性

两级缓存与数据库的数据要保持一致,一旦数据发生了修改,在修改数据库的同时,本地缓存、远程缓存应该同步更新。
解决方案1: MQ

一般现在部署都是集群部署,有多个不同节点的本地缓存; 可以使用MQ的广播模式,当数据修改时向MQ发送消息,节点监听并消费消息,删除本地缓存,达到最终一致性;

解决方案2:Canal + MQ

如果你不想在你的业务代码发送MQ消息,还可以适用近几年比较流行的方法:订阅数据库变更日志,再操作缓存。Canal 订阅Mysql的 Binlog日志,当发生变化时向MQ发送消息,进而也实现数据一致性。

redis优化缓存结构

(1)使用Redis和Cache 多级缓存;

(2)Caffine+Redis+ES 做三级缓存;

(3)使用 Redis 实现分布式缓存,另外配合 Nginx 缓存 + 本地 Caffeine 缓存 + CDN 缓存 + 浏览器缓存共同实现多级缓存来保证系统的性能。

(4)使用mq转发(来自京东技术:营销权益平台春晚技术探究):

本地缓存的构建的两种方式:

  • 一级缓存:本地+mysql+推模式更新。
  • 两级缓存:本地+r2m+mysql+推模式更新。

     后期新的八股文合集文章会继续分享,感兴趣的小伙伴可以点个关注~

 更多精彩内容以及免费资料请关注公众号:绝命Coding

914cbb12b2c3492aaa31232a11aa9c64.png

  • 46
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值