一、Redis 与数据库的双一致性问题
描述: 在某些特定环境下,无论是先更新Redis
还是更新数据库,两者的数据都有可能不一致。
解决方案1 双写模式
解决方案2 失效模式
最终解决方案
无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事,怎么办?
- 如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
- 如果是菜单,商品介绍等基础数据,也可以去使用
canal
订阅binlog
的方式 - 缓存数据+过期时间也足够解决大部分业务对于缓存的要求
- 通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);
总结:
- 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可;
- 我们不应该过度设计,增加系统的复杂性
- 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
Canal - Binlog的增量订阅和消费组件(了解)
二、缓存穿透
描述: 指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null
写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
风险:利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃。
解决方案:
- 接口层增加校验,如用户鉴权校验,或者对
id
做基础校验,id<=0
的直接拦截; - 从缓存取不到的数据,在数据库中也没有取到,这时也可以将
value
存为null
,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用),这样可以防止攻击用户反复用同一个id
暴力攻击。
三、缓存击穿
描述:
- 对于一些设置了过期时间的
key
,如果这些key
可能会在某些时间点被超高并发地访问,是一种非常热点的数据 - 如果这个
key
在大量请求同时进来前正好失效,那么所有对这个key
的数据查询都落到db
,我们称为缓存击穿。
解决方案:
- 设置热点数据永远不过期。
- 加分布式锁,让未获取到分布式锁的线程自旋操作,缓解数据库的压力。分布式锁的实现方案参考:
https://hucheng.blog.csdn.net/article/details/106020884
四、缓存雪崩
描述:
缓存雪崩是指在我们设置缓存时key
采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB
,DB
瞬时压力过重雪崩。
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中
- 设置热点数据永远不过期
五、堆外内存溢出异常:OutOfDirectMemoryError
造成异常的原因: SpringBoot 2.0
以后默认使用lettuce
作为操作redis
的客户端,它使用netty
进行网络通信;lettuce
的bug
导致netty
堆外内存溢出,netty
如果没有指定堆外内存,默认使用-Xmx
参数指定的内存;可以通过-Dio.netty.maxDirectMemory
进行设置。
解决方案:不能只使用-Dio.netty.maxDirectMemory
去调大堆外内存,这样只会延缓异常出现的时间。
升级 lettuce 客户端,或使用 jedis 客户端。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>