redis
常见问题
缓存穿透是指缓存和数据库中都没有的数据
-
接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截
-
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击(缓存空对象)
-
让运维大大对单个IP每秒访问次数超出阈值的IP都拉黑
-
布隆过滤器
- 原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机
-
缓存数据的过期时间设置随机
-
如果Redis是集群部署,将热点数据均匀分布在不同的Redis库
-
设置热点数据永远不过期
-
主要思路
-
缓存高可用,就是集群
-
使用本地缓存
-
请求db限流
- 可以使用 Guava RateLimiter、Sentinel、Hystrix 实现限流的功能
-
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期指并发查同一条数据,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库
-
设置热点数据永远不过期(逻辑过期)
- 也要加互斥锁进行缓存重建
-
加互斥锁
- 使用分布式锁,保证有且只有一个线程去查询 DB ,并更新到缓存
- 然后缓存重建
综合解决方案
- 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃
- 事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免MySQL被打死
- 事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据
多核cpu浪费,可以开启多个redis实例
如果有大量的 key 需要设置同一时间过期,一般需要注意什么
-
一般需要在时间上加一个随机值,使得过期时间分散一些
-
调大 hz 参数,每次过期的 key 更多,从而最终达到避免一次过期过多。
- 这个定期的频率,由配置文件中的 hz 参数决定,代表了一秒钟内,后台任务期望被调用的次数。Redis 3.0.0 中的默认值是 10 ,代表每秒钟调用 10 次后台任务。hz 调大将会提高 Redis 主动淘汰的频率,如果你的 Redis 存储中包含很多冷数据占用内存过大的话,可以考虑将这个值调大
为什么快?基于内存,数据结构简单,采用单线程,避免了不必要的上下文切换和竞争条件,使用多路I/O复用模型,非阻塞IO,底层模型不同,Redis直接自己构建了VM 机制
概念:Redis采用的是基于内存的采用的是单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS
数据结构
string
- 共享用户Session
list
- 文章列表或者数据分页展示的应用
set
- 微博热搜榜
Sorted set(zset)
-
底层数据结构是跳表和hash表,但是编码格式只显示为跳表,组合是为了满足,键值存储,键唯一,可排序三个特性
-
SkipList
- 可排序
-
HT(Dict)
- 键值存储,键唯一
-
SkipList和HT组合太耗内存,所以如果满足两个条件采用zipList结构
- 元素数量小于zset_max_ziplist_entries默认128
- 每个元素都小于zset_max_ziplist_value字节,默认值64
- ziplist本身没有排序,也没有键值对的概念,需要zset编码实现
Hash
- 类似 Map 的一种结构
- 底层与Zset基本一致,只需要把排序的SkipList去掉即可
- 默认采用ZipList编码,数据量较大时,会转为HT编码,也就是Dict
Bitmaps
- 比如用户签到功能
HyperLogLogs
-
PV和UV
- 有一定的误差
Stream
线程模型
多个 Socket
IO 多路复用程序
文件事件分派器
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
Pub/Sub
持久化
默认RDB
- 优势:RDB 允许大数据集更快地重新启动
- 劣势:间隔保存,丢失的数据量比较大,RDB 经常需要 fork ()耗时,数据集很大可能导致redis服务宕机
- 原理:fork和cow,适合做冷备
AOF
- 更持久(丢失数据更少),AOF是一秒一次去通过一个后台的线程fsync操作,那最多丢这一秒的数据,仅限于附加的日志,因此如果发生断电,就不会出现查找或损坏问题
- 劣势:对于相同的数据集,AOF 文件通常比等效的 RDB 文件大,根据具体的 fsync 策略,AOF 可能比 RDB 慢一秒,RDB 仍然能够提供关于最大延迟的更多保证
- 原理:通AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,适合做热备
集群
主从同步
- 为啥要用主从这样的架构模式,前面提到了单机QPS是有上限的,而且Redis的特性就是必须支撑读高并发的
- 启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave
哨兵Sentinal 着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务
-
必须用三个实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用。
- 监视
- 消息通知
- 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上
- 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址
-
选举机制
-
主观下线
- 一个节点在down-after-milliseconds时间内没有回复Sentinel节点的心跳包,则该redis节点被该Sentinel节点主观下线。
-
客观下线
- 如果Sentinel集群中超过quorum数量的Sentinel节点认为该redis节点主观下线,则该redis客观下线。如果客观下线的redis节点为主节点,则开始故障转移,从从节点中选举一个节点升级为主节点。
-
Sentinel集群选举Leader
- 如果一个Sentinel节点获得的选举票数达到Leader最低票数(quorum和Sentinel节点数/2+1的最大值),则该Sentinel节点选举为Leader,否则重新进行选举。
-
Sentinel Leader决定新主节点
-
由Sentinel Leader从redis从节点中选择一个redis节点作为主节点
- 过滤故障的节点
- 选择优先级slave-priority最大的从节点作为主节点,如不存在则继续
- 选择复制偏移量最大的从节点作为主节点,如不存在则继续
- 选择runid(redis每次启动的时候生成随机的runid作为redis的标识)最小的从节点作为主节点
-
-
为什么Sentinel集群至少3节点
- 一个Sentinel节选举成为Leader的最低票数为quorum和Sentinel节点数/2+1的最大值,如果Sentinel集群只有2个Sentinel节点,则Sentinel节点数/2 + 1=2
-
Cluster 着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
- Redis cluster 支撑 N 个 Redis master node,每个master node都可以挂载多个 slave node
java常用客户端
Jedis
lettuce
Redisson
淘汰策略
定期删除
-
默认100ms就随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了,一般过期key不超过10%,就不会再执行
- 定时任务serverCron,按照server.hz频率,hz默认10,1秒钟执行10次,模式为SLOW,默认的,默认10次,每次不超过25ms
- 每个事件循环前会调用beforeSleep()函数,执行过期Key,模式为FAST,执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
惰性删除
- 查询过期没,过期就删了不返回
淘汰机制
-
noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
-
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放
-
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放
-
allkeys-random: 回收随机的键使得新添加的数据有空间存放
-
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键
-
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放
-
如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。
-
LRU(Least Recently Used)
-
最少最近使用
- 当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高
-
以秒为单位记录最近一次访问时间,长度24bit
-
-
LFU(Least Frequently Used)
-
最少频率使用
- 统计每个key的访问频率,值越小淘汰优先级越高
-
高16位以分钟为单位记录最近一次访问时间,低8位记录逻辑访问次数,逻辑访问次数不是真实访问次数,可以增加也可以衰减
-
经典读写模式:读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。更新的时候,先更新数据库,然后再删除缓存,删除缓存,而不是更新缓存,就是一个 Lazy 计算的思想
常用命令
缓存更新策略
内存淘汰
- 不用自己维护,利用内部淘汰策略
超时剔除
- 添加TTL时间,到期后自动删除缓存
主动更新
-
Cache Aside Pattern旁路缓存模式,更新数据库的同时更新缓存
-
删除缓存还是,更新缓存?一般是删缓存,更新很多时候无效
-
缓存操作与数据库操作放在一个事务,分布式事务要利用分布式事务
-
先操作缓存,还是先操作数据库
-
先删除缓存,再操作数据库
- 失败概率高
-
先操作数据库,再删除缓存
- 失败概率更低一些
-
-
-
Read/Write Through Pattern(读写穿透)
- 先更新缓存,再更新数据库,是同步更新
-
Write Behind Pattern(异步缓存写入)
- 与Read/Write方式不同的是, Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。