分布式缓存
缓存预热
redis 启动时可以把一些数据放入 redis 中,结合 spring bean 的初始化回调方案是可行的实现方式
- 实现的
InitializingBean
接口的afterPropertiesSet()
方法 - 使用
@PostConstruct
注解标注方法
内存回收
随着时间推移,redis 放入的数据越多,使用的内存越大,不能无休止使用内存,所以要做一些处理来释放内存
redis 内存回收策略一般两种方式:给 key 设置过期时间、内存自动淘汰的策略
过期策略
给 key 设置过期时间,到达时间后该 key 就释放
到达时间缓存就会立即删除吗?答案是否定的,给每个 key 设置一个定时器代价太大了,key 比较少影响还不大,当 key 过多的时候性能影响就不能接受了。redis 使用两种方式搭配完成内 key 过期的处理:惰性删除、周期删除
惰性删除
- 每次访问缓存时,检查 key 的存活时间(ttl),如果到期,删除 key
- 当一个 key 已经到达过期时间,但是很长时间不会再次访问,这个 key 就不会被删除,这是惰性删除的弊端
周期删除:故名思意,是一个定时任务,周期性的抽样检查部分 key,当达到 ttl 的 key 就删除,两种模式 SLOW、FAST
内存淘汰策略
当 redis 使用的内存达到阈值时(当内存充足就不会淘汰),执行每个命令前会淘汰一些数据,淘汰策略如下
- noeviction:不淘汰任何 key,当内存不足时拒绝写入新数据,这是默认策略
- volatile-ttl:对设置了 ttl 的 key,比较 ttl 的值,ttl 值越小越先被淘汰
- allkeys-random:对所有的 key 进行随机淘汰
- volatile-random:对设置了 ttl 的 key 进行随机淘汰
- allkeys-lru:对所有的 key 进行 LRU 算法淘汰
- volatile-lru:对设置了 ttl 的 key 进行 LRU 算法淘汰
- allkeys-lfu:对所有的 key 进行 LFU 算法淘汰
- volatile-lfu:对设置了 ttl 的 key 进行 LFU 算法淘汰
LRU(least recently used):最近最少使用。当前时间减去最近访问时间,这个值越大越优先被淘汰
LFU(least frequently used):最少频率使用。redis 会统计每个 key 的访问频率,最越少越优先被淘汰
数据同步
- 设置有效期:给缓存设置 ttl,到期后自动删除,下一次查询时,缓存为空,再把数据库的数据加入缓存
- 优点:简单方便
- 缺点:时效性差,当缓存过期前数据库发生了更新,导致数据库和缓存不一致
- 同步双写:在有效期的前提下,如果修改数据库,同步修改缓存(写进一个事务中,要么都成功,要么都失败)
- 优点:时效性强,缓存和数据库一致
- 缺点:代码侵入
- 异步通知:在有效期的前提下,当数据库发生更新,不是同步修改缓存,而是发送一个消息,通知缓存修改,只管发消息,具体什么时候修改缓存不用管
- 优点:保证了数据一致性,代码侵入也降低了
- 缺点:时效性一般,因为不清楚什么时候消费者接收到消息,最终一致,但是不是实时一致
- 使用中间件,比如 canel,canel 会监听 mysql 的 binlog ,当发现数据变化可以做自定义的业务处理
- 基于 mysql 的主从同步,canel 伪装成一个从节点,读取主节点的 binlog 从而具有知晓数据变化的能力
- 前提必须 mysql 是主从模式,如果 mysql 是单台服务就不能使用 canel 了
BigKey
-
怎么定义是大 key?
-
key 本身数据量过大,比如 String 的 key,value 达到 10k
-
key 中成员数过多,比如 list 或 set,成员数超过 1000 个
-
key 的成员数据量过大,比如 hash 的 key,可能成员数不大,但是所有的字段的 value 总和达到 10m
-
-
怎么查找大 key?
MEMORY USAGE key
查看一个 key 占用了多少字节(但是这个不推荐使用,占用 cpu 会比较高)- 如果是字符串,可以直接使用
STRLEN key
查看 value 的长度(推荐,但只能查一个) - 如果是集合,可以使用
LLEN key
查看集合的成员个数(推荐,但只能查一个) redis-cli --bigkeys
命令查找每种数据类型占用内存最大的一个 key(不推荐,只能查每种类型占用最大的一个)SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
(推荐方式)- 利用第三方工具,比如 Redis_Rdb_Tools 分析 RDB 快照文件(推荐,不会影响 redis)
-
BigKey 带来什么问题?
- 网络阻塞:比如一个 key 达到了 10m,只需要少量的 QPS 就可能导致网络带宽被占满
- 数据倾斜:集群环境下,数据分片不均匀,多个节点,同为 1000 个 key,可能某个节点的 key 全都是 BigKey
- Redis 阻塞:当一个集合的成员过多,要进行排序等运算,这是很耗时的
- CPU 压力:对 key 的读取和写入要进行序列化,当大 key 过多时,导致 CPU 飙升
-
怎么处理大 Key?
目的是要删除大 key,重新组成多个小 的 key,不能直接使用 del 删除,当集合出现大 key 时,del 同步删除会阻塞 redis
- 如果是 4.0 及之上的版本,可以使用 unlink 命令异步删除大 key
- 如果是 4.0 以下版本,一个成员一个成员的删除
缓存穿透(数据本就不存在)
数据本身就不存在,比如查询 id = -1 的用户,缓存中肯定没有,数据库中同样没有,每次查询还是会落在数据库上(黑客恶意攻击)
解决方案:1,接口层处理,不合理的数据直接拦截,不到数据库;2,没有的数据也加入缓存,ttl 设置短点
缓存雪崩(数据库存在、缓存不存在)
还有个概念时缓存击穿,缓存击穿和缓存雪崩都是 数据库存在但是缓存不存在 的场景,大量请求最终查询数据库把数据库搞崩了
原因可能是热点数据过期(缓存击穿)、大批量缓存同时过期(缓存雪崩)
解决方案:1,如果是缓存击穿,可以设置热点数据永不过期;2,如果时缓存雪崩,过期时间不要太集中,在 ttl 加一点随机时间来分散过期
数据一致性
上面数据同步略带提过,单纯的同步双写在多线程场景中也是有问题的,
a 线程同步双写的操作有三步:更新数据库、删除缓存、添加缓存
b 线程在 a 线程删除缓存时,也更新了缓存,流程结束。这是 a 线程添加缓存,覆盖了 b 线程的数据
b 线程后来,b 线程是最新的数据,但是 b 比 a 先走完,所以 a 的值覆盖了 b 的值
解决方案:延时删除,所有线程更新数据库后,删除缓存,不添加缓存,隔几秒再次删除缓存。等下一次查询的时候再加入缓存
为什么要隔几秒?就是等业务做完再次删除,保证每次业务完都删除缓存,这样就能在下一次查询的时候加入缓存时的数据是最新的
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库