我正在参加「掘金·启航计划」”
本文不介绍任何基本用法
对redis无了解,参考如下:\
\
从几个点,了解redis的深入部分
1、redis的线程模型
2、redis的数据模型\
3、redis的过期策略
4、redis的主从复制\主从切换
5、redis的哨兵机制(Redis Sentinel)
6、redis的集群机制(Redis Cluster)
7、redis数据库一致性问题
\
memcached英雄的迟暮
多线程模型的memcached本来混的不差,
单线程模型的redis,随着分布式集群的流行顶掉了这个老大哥
\
为什么能顶掉?
\
redis有原生的集群模式支持,并且单线程不需要上下文的切换,还能享受局部性原理的热缓存(cache局部性原理)\
\
memcached的数据结构比redis少
复杂结构的数据要在客户端处理后,才能进缓存
处理后,会额外增加IO次数与数据体积\
\
而redis原生支持多数据结构
\
所以,如果缓存需要复杂的结构,用redis\
\
集群方面,redis原生就有一种集群模式(redis cluster)自带数据分片能力
memcached集群需要在客户端里面进行分片写入
不提供原生自带的
\
什么是数据分片?
\
按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中,以达到提升性能以及可用性的效果
\
但memcached不是很差,
多核对于处理大数据量是有性能优势的
\
\
单线程模型的内在
从几个点阐述一下
\
1、IO多路复用程序\
"多路复用程序"监听所有"socket"(socket是指长连接)
每个"socket"会产生事件
"多路复用程序"会把产生事件的"socket"推送到队列排队(消息队列依次执行)
监听socket的操作是系统调用,所以性能很快
\
IO多路复用系统调用详细介绍:
\
(socket排队与产生事件)
\
"AE_READABLE"事件产生原因:socket变得可读、或者出现(客户端对redis进行write和close、或者connect操作)\
"AE_WRITABLE"事件产生原因:socket变得可写(客户端对redis进行read操作)\
"IO多路复用程序"同时监听这俩,对于并发情况优先处理"AE_READABLE"\
\
在队列里的事件,会由"IO多路复用程序"转发给"文件事件分发器"\
\
2、文件事件分发器与事件处理器\
强相关,就一起讲了\
(文件事件分配的过程)
\
"IO多路复用程序"每次会从队列取一个"socket"
丢给"文件事件分派器"
由分派器来根据"socket"的事件,来指定对应的"文件事件处理器"
处理完再取下一个
\
直到队列处理完,才开始重新监听"socket"
\
如何分派"socket"对应的事件?
有个规则
\
如果客户端是要连接redis,为socket关联连接应答处理器\
如果客户端是写数据至redis,为socket关联命令请求处理器\
如果客户端是从redis读数据,为socket关联命令回复处理器\
\
举个例子:
当客户端连接至redis,产生"AE_READABLE事件","连接应答处理器"跟客户端建立socket,随后把此socket与"命令请求处理器"关联
\
当客户端向redis发送请求,触发"AE_READABLE事件","命令请求处理器"从socket里面读取 请求和相关 的数据,redis根据请求准备数据
\
当redis准备好数据后,会关联socket的"AE_WRITABLE事件"与"命令回复处理器",当客户端准备好读取数据的时候,会触发"AE_WRITABLE事件","命令回复处理器"便会写入数据至socket
\
单线程快,不仅仅因为纯内存操作,也与非阻塞的IO多路复用强相关,并且单线程避免的上下文切换问题
(温馨提示:redis的单线程是并不涉及持久化模块的,下文会讲解持久化模块)\
\
redis数据类型
关于数据类型的实际运用,不涉及命令教学
详细讲解命令使用,请参见:\
\
\
hash
K_V的存储方式
常用于结构化数据存储、修改、增加
(比如简单的对象)
\
(list结构数据样例)\
不能嵌套结构化数据
\
\
list
存储列表型数据,并且支持分页(使用irange命令,从某个元素开始读取到某个元素,高性能分页)
\
也可以模拟消息队列使用
\
set\
集合,元素不重复\
\
可利用求并集的API,实现共同好友的业务\
或者利用唯一性,记录登录者的IP
也可以根据用户标签来求交集,做共同兴趣推荐
\
sorted set\
有序set,元素会根据权重排序\
\
可作于实时游戏排名,或作于带权重的消息队列
\
像食品一样,数据也是会过期的\
设置数据的时候,redis顺便可以设置数据的失活时间
\
什么是失活?在数据过期的时候,redis会删除数据
(过期模块这里着重的是删除部分的操作,因为redis不会全内存搜索过期数据然后删除,这样导致性能会很慢)
\
如何删除?
\
别以为redis会扫描是否过期,redis的解决方案是:
惰性删除+定期随机删除\
\
惰性删除:
对Key为A的数据进行搜索,检查A对应的数据是否过期,若过期则删除
\
定期随机删除:
随机抽取设置了过期时间的Key,检查是否过期,若过期则删除
\
如果,这两种方案依旧导致大量的已过期数据未被删除
则会进行内存淘汰机制
\
内存淘汰机制:
针对内存达到限制值redis执行的策略
1.noeviction:返回错误,不删除任何键值
2.allkeys-lru:尝试回收最少使用的键值(LRU算法)
3.volatile-lru:尝试回收最少使用的键值,但仅限于在过期键值集合中
4.allkeys-random:回收随机的键值
5.volatile-random:回收随机的键值,但仅限于在过期键值集合中
6.volatile-ttl:回收过期集合的键值,并优先回收存活时间较短的键值
7.allkeys-lfu:回收最少使用频次的键值(LFU算法)
8.volatile-lfu:回收最少使用频次的键值(LFU算法),但仅限于在过期键值集合中
这里有两个重要的算法:
LRU:最近最少被使用。访问时间"越早"越容易被淘汰
LFU:最近最少使用频次。使用频次"越少"越容易被淘汰
\
可以借助JDK内置的LinkedHashMap类来实现一个简易版本的LRU
class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int CACHE_SIZE;
// 这里就是传递进来最多能缓存多少数据 public LRUCache(int cacheSize) { // 这块就是设置一个hashmap的初始大小, // 同时最后一个true指的是让linkedhashmap按照访问顺序来进行排序, // 最近访问的放在头,最老访问的就在尾 super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true); CACHE_SIZE = cacheSize; }
@Override protected boolean removeEldestEntry(Map.Entry eldest) { // 这个意思就是说当map中的数据量大于指定的缓存个数的时候, // 就自动删除最老的数据 return size() > CACHE_SIZE; }
}
\
Redis的LRU算法采用一种近似LRU的方式来实现,对少量keys(maxmemory-samples,默认为5)进行取样,然后回收其中一个最好的key(被访问时间最早的)。
\
Redis3.0算法已改进为回收键的候选池子
1.第一次随机选取的key都放入一个pool中(pool大小为16),pool中的key是按照访问时间大小排序;
2.接下来每次随机选取的key都必须小于pool中最小的key,若pool未填满,则重复步骤1
3.放满后,若有新的key需要进入,将pool中lru最大的一个key取出
4.淘汰的时候,直接从pool中取出一个lru最小的一个key然后淘汰
\
\
Redis有了LRU之后,为什么还需要LFU呢?因为Redis作者发现就算提高采样数量或者pool的大小,也无法再提高缓存命中率,而LFU算法能起到更好的效果。
LFU近似于LRU,它使用一个概率计数器(morris counter),用来估计访问频率;counter的计数有两个特点,1.随着访问次数的增加,counter的计数会越来越缓慢(counter最大值为255),2.随着时间的流逝,counter会逐渐衰减。淘汰时也会有一个pool,也采取与LRU类似的方式,但是排序是按照计数从大到小排列(越靠后越容易被淘汰)
\
LRU与LFU详解:
\
\
实际通过redis的高并发高可用,是如何实现的?
\
如何使用读写分离承载高请求数量?
\
使用多个redis集群,主节点负责写以及同步其他子节点,子节点提供读取服务
\
(redis读写分离架构)
\
值得注意的是,主节点需要做持久化策略,防止主节点崩溃重启后,将空数据同步复制给其他的子节点(即使采用了高可用,选举父节点的策略,也需要对主节点做持久化策略,防止突然重启)\
\
同步复制的操作
它被称为redis replication
\
(redis同步复制)
\
从2.8版本开始,slave node会周期性轮询自己复制的数据量\
\
举个同步复制(redis replication)的例子:\
当启动一个slave node的时候,会发送"PSYNC"命令给配置文件里使用者规定的"master node IP"\
\
启动情况:
若 slave node是重新连接,则master node只复制缺少的数据
若slave node是第一次启动,则master node会启动全量复制(full resynchronization)\
\
简略介绍,全量复制:
当启动全量复制的时候,master会fork一个新进程生成快照文件(RDB持久化),然后将快照发送给slave node
\
slave node收到快照后先写入本地磁盘,随后写入内存
\
master不仅仅同步快照,
\
也会将内存里面正在处理的写入命令,发送给slave,由slave进行同步
\
对于多个slave发送"PSYNC"命令索要数据的情况,
\
master只会使用一个RDB进程用于服务所有slave
\
全量复制里面提供无磁盘配置\
repl-diskless-sync设置为true
\
master会在内存里面直接创建rdb快照文件发送给slave,不会保存在本地磁盘
\
此配置,让使用机械硬盘的服务器能大幅度提升性能
\
(关于持久化,RDB镜像与AOF镜像区别:juejin.cn/post/684490…
下文会深入解析 )\
\
对于过期的key,slave不会过期key,是由master节点发送del指令来删除slave里面已经过期的key来实现的
\
分析全量复制的细节:
\
首先来讲讲backlog(复制积压缓冲区)\
\
(backlog与offset偏移)
\
它是一个环形缓冲区,用来存储主节点向从节点传递的命令,大小是固定的。
\
用于全量复制被中断后,采用的"增量复制机制"(redis2.8版本之后,修改了全量复制的逻辑,若在复制过程中,网络连接断开了。会提供"增量复制机制",用于恢复上次传输的进度。)
\
\
offset(偏移量)
不管slave节点还是master节点都会维护一个offset,用于宕机恢复数据
\
master会在自身不断累加offset,slave也会在自身不断累加offset
slave每秒都会上报自己的offset给master,同时master也会保存每个slave的offset
\
谁是master节点?
slave节点,其实仅仅用 "ip + port" 来定位master节点是不准确的
\
(run_id)\
\
准确的其实是用run_id来进行区分slave节点,若发现新master run_id 不一致则重新全量复制(run_id不一致只能说明节点被重启了)\
\
(全量复制与增量复制)
\
举两个例子:
全量复制流程
master 本地生成 "rdb快照文件"
->
master 生成 "rdb快照文件" 之际,会把当前的写命令缓存在内存中,待slave接受完快照后,再把写命令发送给它
->
master把rdb快照文件发送给 slave
1、如果传输时间超过配置文件"repl-timeout"字段规定的大小,slave会认为复制失败
2、"client-output-buffer-limit"字段控制复制的最大缓存量,若复制的rdb一次性超过设定的值,则直接复制失败\
->\
slave接收到rdb后,加载rdb到内存中,加载的过程中是使用旧数据来提供服务\
如果slave开启了AOF,则会进行"重写AOF操作"
\
增量复制流程(redis2.8版本之后)\
触发条件,全量复制过程中网络断开,slave重新连接master会触发增量复制
->\
master 从backlog获取部分丢失的数据,并重新发送给master\
->
具体如何判断部分丢失?其实是master节点根据发送过来的"PSYNC"命令里含的偏移量(offset),来根据偏移量从backlog这个队列里面取偏移的数据\
\
关于主从复制的详细介绍:\
\
*什么是高可用,什么是不可用?
*
(系统崩溃了)
\
可能出现上面情况的原因:\
1、断电了
2、JVM堆内存太小,溢出了(oom)
3、CPU满了,被阻塞了
4、磁盘IO满了,不工作了
\
这就是不可用,服务崩溃\
\
即使系统崩了,短时间内也能恢复上来,此时为高可用\
\
redis的高可用是在于对master节点的备份——这就引出了主备切换\
\
由上文可知,高性能的读写分离架构使得redis采用主从体系(master节点负责写入操作并且提供数据给slave节点,slave节点负责读)\
\
如果master节点崩溃了呢?\
\
这时主备切换就起作用了,在主备切换的架构中,通过选举重新确定一个新的主节点(master),用于提供数据给从节点(slave)\
\
从而实现redis的高可用
\
分析主备切换的细节:
\
讲到主备切换,不得不提一下哨兵机制
\
哨兵是 Redis 的一种运行模式,它专注于对 Redis 实例(主节点、从节点)运行状态的监控 (监控功能),并能够在主节点发生故障时通过一系列的机制实现选主及主从切换(自动切换主库功能),并且进行主从节点数据同步(通知功能),确保整个 Redis 系统的可用性
\
(主备切换)
\
如何防止主节点的突然重启?\
\
采用了主观下线和客观下线两种方案
\
若宕机的是slave节点,直接标注下线,这被称为主观下线
\
若master节点被宕机,会先进行"客观下线",然后再进行投票进行新的master节点的选举,下图是选举过程
\
(选举过程)
\
选举master的时候也涉及了对节点的打分,规则如下:
\
- slave 优先级,通过 slave-priority 配置项,给不同的从库设置不同优先级,优先级高的直接晋级为新 master
- slave_repl_offset与 master_repl_offset进度差距,若一致,那就继续下一个规则。其实就是比较 slave 与旧 master 复制进度的差距;
- slave runID,在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。(根据 runID 的创建时间来判断,时间早的上位);
\
*什么是客观下线?
*
\
判断master是否下线并不是一个哨兵说了算, "主观下线"是"哨兵集群"过半的哨兵认为master已经下线,这才能被称为客观下线\
\
(主观下线与客观下线)
\
简单来说,主观下线是哨兵自己认为节点宕机,而客观下线是不但哨兵自己认为节点宕机,而且该哨兵与其他哨兵沟通后,达到一定数量的哨兵都认为master挂了。(所以最低都需要3台redis实例才能进行主备切换)
\
上文提过哨兵集群,那么它是什么呢?\
\
多实例组成的集群模式进行部署,这就是哨兵集群
\
多哨兵同时出现网络波动的情况比较少,所以稳定性较高\
\
*哨兵本质
*
\
哨兵之间是如何知道彼此的?如何知道slave节点并且进行监控,哪个哨兵进行主从切换呢?
\
pub/sub 消息发布订阅模式
\
哨兵与 master 建立通信,利用 master 提供发布/订阅机制发布自己的信息\
\
master 有一个 sentinel:hello 的专用通道,用于哨兵之间发布和订阅消息。这就好比是 sentinel:hello 微信群,哨兵利用 master 建立的微信群发布自己的消息,同时关注其他哨兵发布的消息
\
哨兵如何通过命令"INFO"来对其他哨兵进行监控?\
\
首先,master有个哨兵列表,里面存放了在线的哨兵信息,其他哨兵可以向master发送"INFO"命令来获取哨兵列表
\
哨兵根据 master 响应的 slave 名单信息与每一个 slave 建立连接,并且根据这个连接持续监控哨兵
\
任何一个哨兵判断 master "主观下线"后,就会给其他哨兵发送 is-master-down-by-addr 命令,其他哨兵根据自己跟 master 之间的连接状况分别响应 Y 或者 N ,Y 表示赞成票, N 就是反对。
\
如果某个哨兵获得了大多数哨兵的“赞成票”之后,就可以标记 master 为"客观下线",赞成票数是通过哨兵配置文件中的 quorum 配置项设定。
\
sentinel monitor <master-name> <ip> <redis-port> <quorum>
\
举个例子 :\
比如一共 3 个哨兵组成集群,那么 quorum 就可以配置成 2,当一个哨兵获得了 2 张赞成票,就可以标记 master “客观下线”,当然这个票包含自己的那一票
\
获得多数赞成票的哨兵可以向其他哨兵发送命令,申明自己想要执行主从切换。并让其他哨兵进行投票,投票过程就叫做 "Leader 选举"。
\
想要成为 “Leader”没那么简单,得有两把刷子。需要满足以下条件:
- 获得其他哨兵过半的赞成票;
- 赞成票的数量还要大于等于配置文件的 quorum 的值。
\
如果哨兵集群有 2 个实例,此时,一个哨兵要想成为 Leader,必须获得 2 票,而不是 1 票。
\
所以,如果有个哨兵挂掉了,那么,此时的集群是无法进行主从库切换的。
\
因此,通常我们至少会配置 3 个哨兵实例。
\
这也是为啥哨兵集群部署成单数的原因,双数的话多余浪费。
\
如何备份数据,关于redis的持久化?
如果单单只把数据放到内存里,断电就会数据丢失
\
可以备份到磁盘、第三方对象存储服务,这样能有效保存redis内存中的数据\
\
redis的持久化策略有两种,
RDB,AOF,他们之间的区别和特点\
*
*
什么是RDB?
\
\
(RDB的流程)
\
RDB持久化是一种周期性的持久,\
\
它会生成多个数据文件,每个数据文件代表某时间段里redis存放的数据\
\
可以用于冷备份,把数据文件放到远程的安全存储\
\
RDB在持久化的时候对redis性能影响小,本质是fork一个新进程来做持久化操作,新进程来进行IO磁盘持久化操作
\
对于宕机的节点,RDB方案的恢复速度比AOF方案更快速\
\
缺点也有,
在fork新进程来保存数据的过程中,需要产生的数据文件过大会导致服务被暂停数秒(把内存拷贝给新进程来进行IO操作,本身也是耗时的操作)\
\
(RDB机制没有及时备份)\
\
\
定时备份会导致 分钟长度 以上的数据丢失
*
*
什么是AOF?
*
*
(AOF流程)
\
AOF机制会对每条写命令做持久化操作,以只追加(append-only)的方式写入一个日志文件中去,无磁盘寻址的开销,性能高\
\
产生的数据文件由于这个原因,相对来说不易损坏,易修复\
\
当AOF产生的原有数据文件过大,redis会根据当前数据产生新的数据文件\
(因为内存数据可能被删除了,而AOF只记录指令操作,所以可能会出现大量冗余的指令操作)\
\
对于灾难性的误删除(使用了flushall指令),这时AOF策略能极大发挥自身优势
\
需要在重新生成数据文件前(生成数据文件不受flushall影响,而是取决于当前设置的数据文件大小上限),备份并且修改当前AOF产生的数据文件
\
把最后一条指令(也就是flushall指令)删除,然后再恢复,数据就回来了
\
*AOF策略的缺点
*
\
AOF的数据文件因为是指令保存,比起RDB模式下生成的来说还是过于庞大\
\
并且开启AOF后,每个写操作都需要执行"fsync"命令(备份操作,默认1s备份一次),会损耗性能(当然也不会损耗多少)\
\
fsync是一个系统调用,详解请见:
zhuanlan.zhihu.com/p/140417823
\
所以,实际运用中单一策略不足以应对所有场景
\
应当结合使用,AOF和RDB两种持久化机制
\
用AOF来保证数据不丢失,作为数据恢复的第一选择;
用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复
\
redis cluster的集群模型
前面讲了主从复制、哨兵机制,
\
针对海量数据的需求,redis提出了一种近似于Hadoop数据分片的架构\
\
现在来介绍,redis cluster\
\
首先从存储数据说起,
1、自动对数据进行分片,每个master上放若干数据\
2、提供高可用架构支持,master有对应备份的slave节点
\
数据如何分布到这些节点上去呢?这里就涉及了hash slot算法\
\
讲个引子,用求模算法存储数据
\
比如,现在有3个节点,我存储"hello"这个字符串,然后取hello的hash值(类似MD5算法),用hash值去对3取模(结果一定是0、1、2),通过结果随机选择一台对应的节点 \
\
在应对其中一台节点崩溃的状态下,老土的求模hash算法是不够解决问题的
(一旦一个节点崩溃,所有的新请求会按照"对2取模"的方式去取数据,但存放数据是按照"对3取模"进行存储的,这样"缓存全失效"了)
\
(hash slot 哈希槽)
\
redis中hash提出槽的概念,把多个redis组成的"整体"抽象为"多个槽"
(预先分配16384(2^14)个卡槽,所有的键根据哈希函数映射到 0 ~ 16383整数槽内,每一个分区内的节点负责维护一部分槽以及槽所映射的键值数据)
\
每个节点,有多个卡槽,每个卡槽都能存放一些数据,卡槽 到 key映射算法:对每个key计算CRC16值,然后对16384取模
\
计算公式:slot = CRC16(key) & 16383
\
在redis cluster架构下,每个redis要放开两个端口号,比如一个是6379,另外一个就是加10000的端口号,此端口号是集群节点来进行通信的(被一种称为 "cluster bus集群总线" 的机制所使用)
\
cluster bus, 使用了一种"二进制协议"(binary protocol)进行集群内点对点(node-to-node)通讯,
包括节点失效检测, 配置更新, 故障转移(failover)认证等等
\
hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去
\
*这些master节点是如何进行内部通信?
*
\
(gossip协议传播过程)
\
\
redis采用了gossip协议作于内部通信,这是一种消息冗余的P2P网络分布式通信
\
Gossip 过程是由种子节点发起,当一个种子节点有状态需要更新到网络中的其他节点时,它会随机的选择周围几个节点散播消息,收到消息的节点也会重复该过程,直至最终网络中所有的节点都收到了消息。
\
这个过程可能需要一定的时间,由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。
\
这里选几个代表性的指令介绍:\
meet指令: 某个节点发送"meet"给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信
\
ping指令:每个节点都会频繁向其他发送ping指令,用于交换元数据(自身状态等),防止ping指令频繁导致网络通信拥塞,可以设置cluster_node_timeout参数的大小来调节ping指令发送间隔
如果数据交换时延过长,会导致整个集群的元数据不一致,所以不能设置的过长
\
fail指令:某个节点判断另一个节点"fail"之后,就发送fail给其他master节点,通知其他节点有人宕机了
\
*redis的客户端jedis,因集群架构带来了一些改变
*
*
*
\
\
客户端挑选随机一个节点发送命令, 此节点计算hash slot,判断是否指向自身,是则执行命令,不是则回复客户端moved(含数据对应节点的ip+port),客户端自动重定向重新发送命令\
\
此过程中:
\
1、节点间通过gossip协议进行数据交换,就知道每个hash slot在哪个节点\
\
2、用hash tag可以手动指定key对应的slot,减少重定向的出现
\
hash tag作用是将某一固定特征数据存储到一台实例上,避免逐个查询集群中实例,但会导致数据集中在一个实例中,需要合理使用
\
\
当客户端向某个节点发送命令,节点向客户端返回moved异常,告诉客户端数据对应的槽的节点信息
\
客户端再向正确的节点发送命令时,如果此时正在进行集群扩展或者缩空(减少)操作,槽及槽中数据已经被迁移到别的节点了,就会返回ask,这就是ask重定向机制\
\
smart机制的出现就是为了解决多次重定向的问题,网络IO过多损耗性能
\
在本地维护一份hash slot 与 节点的映射表,减少重定向出现
\
当JedisCluster进行初始化的时候,随机选择一个节点,初始化映射表,并且为每一个节点创建连接池
\
当访问目标节点返回moved重定向,客户端会根据moved里面携带的数据更新本地映射表
顺带一提,ask的重定向不会导致本地的映射表更新,因为它发生在 节点 与 节点 的迁移过程中
\
如果重试超过5次,客户端本地会抛出一个异常
JedisClusterMaxRedirectionException
重试可以根据配置文件进行调整
\
上述说的 "节点" 其实指的是master节点
\
在redis集群机制里面,默认整合了主从机制
\
每一个master节点都有若干slave节点,具体机制可参考上文里讲解的主从机制
\
缓存雪崩和穿透的场景
\
什么是缓存穿透\
(当redis宕机后发生了什么)
\
黑客也会使用这种行为,发送特殊的请求进行攻击,击穿缓存(这种请求一般是不可能会有结果的请求)\
(黑客进行doss攻击)
\
DB涌入大量请求,导致DB崩溃,这就是缓存穿透的危害
\
解决方案:
1、缓存空结果
当Redis 及 DB 中都不存在该资源,就缓存空结果一段时间,一定要设置一个较短的缓存失效时间,防止数据库数据更新后,请求依然返回缓存的情况。\
\
2、用户合法性校验
对用户的请求合法性进行校验,拦截恶意重复请求。
\
3、布隆过滤器
详解:
\
4、热点数据不过期
热榜等相关的信息,用脚本定时刷新进redis内存(防止缓存过期),避免多个请求同时访问数据库
\
*什么是缓存雪崩
*
\
Redis集群中的 热点数据 在某一时刻同时失效,海量请求直接请求 DB,DB可能在瞬间就爆了
\
可以使用随机过期时间防止同时过期
\
如何解决redis与数据库的一致性问题\
\
假如,缓存与数据库进行存储双写
\
那么,一定会出现一致性问题
\
Cache Aside Pattern模式,
读的时候先读缓存,如果缓存没有就进行数据库查询,返回结果并且结果加入缓存
\
写的时候先删除缓存,然后修改数据库
\
*为什么是删除缓存而不是修改缓存?
*
\
修改缓存的代价高,是相对的来说
\
因为大部分的数据会热写入,冷访问
\
特别是如果缓存数据的产生涉及了复杂的计算,更需要减少缓存更新的次数
\
并且先删除缓存能解决缓存不一致问题
(一致性问题其一)\
\
举例1:先修改数据库,再删除缓存,如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致
\
(一致性问题其二)
\
举例2:数据发生了变更,删除了缓存,然后去修改数据库,此时还没修改
\
一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中
\
并发的发生,在更新一个库存的时候,同时也在读取这个库存(对于高并发的场景很常见此问题)
\
缓存与数据库不一致的问题复现了
\
解决方案:\
\
更新数据的时候,根据数据的唯一标识,将操作加入JVM的一个队列中
\
读取数据的时候,若发现数据不在缓存,也将操作加入JVM的一个队列中(也是根据数据的唯一标识)
\
工作队列拿到操作之后,然后一条一条的执行
\
这样,如果有一个更新数据的操作和读取数据的操作同步的进行,也会放入此队列,队列确保了他们的操作是原子性的
\
并且在此队列里面,能使用过滤,来整合一些重复操作
(比如,多次更新缓存操作,只保留最后一个操作即可)
\
高并发的特殊情况
\
对于海量请求的环境下,队列里面可能积压了很多写操作,写操作在队列会超时并转化称为数据库请求,然后数据库崩溃\
\
这里需要进行水平拆分、加机器,让每个机器上面的服务实例处理更少的数据
\
当然,根据"28法则",写操作出现大量的情况还是比较少见的
\
如果是海量的读请求,基本上是走的缓存,若出现数据更新的情况,也可以使用队列进行过滤
\
海量读请求,只进行一次更新缓存操作,缓存更新之后,其他的读请求就可以读取缓存里面的数据