Redis缓存问题:穿透,击穿,雪崩等
在高并发场景下,数据库往往是最薄弱的环节,我们通常选择使用redis
来进行缓存,以起到缓冲作用,来降低数据库的压力,但是一旦缓存出现问题,也会导致数据库瞬间压力过大甚至崩溃,从而导致整个系统崩溃.今天就聊聊常见的redis
缓存问题.
缓存击穿
缓存击穿一般指redis中的一个热点数据过期,导致大量请求直接访问数据库的情况,导致数据库瞬间压力过大甚至崩溃.
解决方案:
- 设置热点数据永不过期,这是一个不错的方案(要考虑业务特性,体量以及成本),前提是热点数据不能频繁发生改变,否则就会出现缓存污染.最好是根据一定的策略进行定时更新
- 重要接口限流,做好熔断和降级的准备,sentinel是个不错的选择
- 使用互斥锁,保证同一时刻只有一个线程可以访问数据库,这何尝不是一种限流呢
缓存穿透
缓存穿透指缓存和数据库中都没有的数据,用户不断发起请求.这种情况最可能就是有人试图恶意攻击系统
解决方案:
- 加校验:拦截非法请求,用户鉴权等
- redis缓存一个无效值,以防止对同一个key在数据库中的多次查询,但redis中可能会出现大量无效值,导致缓存污染,所以要将有效时间设置得短一些
- 添加布隆过滤器,在对数据库进行查询前,先通过布隆过滤器判断是否存在
一般来说这三种方案是同时使用的,第一层一般是校验,拦截部分非法用户和不合理请求(拦截不可能全部拦截而且如果攻击者通过某些方式掌握了大量合法用户呢),第二层是布隆过滤器,尽量避免对数据库的直接访问,但仍然有误判的可能性,第三层再缓存一个无效值,做到尽可能降低风险
缓存雪崩
缓存雪崩一般指reids中大批量数据在极短时间内(同时)过期,导致大量的查询数据库
解决方案:
- 在存储数据时,设置过期时间为一个随机值(也可以理解成给固定的过期时间加上一个随机值,类似密码学中的加盐),尽量保证不会有大量数据在同一时间过期
- 将热点数据尽量均匀地分布在不同的数据库中
- 多级缓存
- 设置热点数据永不过期(同缓存击穿中的)
缓存污染
缓存污染指的是缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间,也会在一定程度上影响redis的性能
redis缓存的maxmemory应该设置多大,这是一个关乎性能和成本的问题,需要根据实际情况进行权衡,但普遍推荐的是设置为总数据量的15%-30%(其他博客都这么写,而且范围还挺大,应该没什么问题🤔)
缓存淘汰策略
官方文档写了8种,如下图:
-
noeviction(不驱逐,即不淘汰)
默认策略,当缓存达到maxmemory时,redis会拒绝所有写请求,并返回错误信息,此时redis已经进入只读模式,无法再进行写操作,但仍然可以进行读操作
-
allkeys-lru
所有key采用LRU算法进行淘汰,即优先删除最近最少使用的key -
allkeys-lfu
所有key采用LFU算法进行淘汰,即优先删除最不常用的key -
volatile-lru
只淘汰设置了过期时间的key,采用LRU算法进行淘汰 -
volatile-lfu
只淘汰设置了过期时间的key,采用LFU算法进行淘汰 -
allkeys-random
所有key采用随机删除 -
volatile-random
只淘汰设置了过期时间的key,采用随机删除 -
volatile-ttl
删除过期字段设置为true和剩余最短生存时间(TTL)值的密钥。
缓存和数据库一致性
不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写数据库,都有可能出现数据不一致的情况。举一个例子:如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
更新缓存有四种设计模式: Cache aside, Read through, Write through, Write behind caching
Cache Aside
- 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
- 命中:应用程序从cache中取数据,取到后返回。
- 更新:先把数据存到数据库中,成功后,再让缓存失效
这样就不会出现上面所说的问题了吗,并不是:一个读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放入缓存,所以,会造成脏数据.但这种情况发生的概率非常之低
Read Through
Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候,Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对应用方是透明的。
Write Through
Write Through 套路和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)
Write Behind Caching
在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。但是,其带来的问题是,数据不是强一致性的,而且可能会丢失
队列+重试机制
- 更新数据库数据;
- 缓存因为种种问题删除失败
- 将需要删除的key发送至消息队列
- 自己消费消息,获得需要删除的key
- 继续重试删除操作,直到成功
该方案有一个缺点,会对业务线代码造成大量的侵入。
基于订阅binlog的同步机制
本方案启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。关键是使用canal框架订阅binlog
- 要开启mysql的binlog,需要设置binlog_format为ROW模式,并且设置server_id,保证唯一性。修改my.cnf配置文件,重启mysql服务。
[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
- 查看是否修改 Binlog 成功。
# 查看 binlog 日志是否开启
show variables like 'log_%';
- MySQL 执行 SQL 语句创建 canal 单独使用的账号,用来进行 Binlog 的同步和监听
CREATE USER canal IDENTIFIED BY 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
Reference:
- 缓存更新的套路
- Java全栈知识体系
欢迎访问我的个人博客www.levitategu.cn