Redis
0. 声明
写这篇博客的目的主要是为了自己复习使用,参考如下博客,如有侵权,可以联系删除,在此特别鸣谢。同时,该文章的内容对于一些细节,并没有说明的很清楚,具体可以参考《Redis设计与实现》,或者欢迎共同探讨。并且,很多涉及到生产环境的知识及应用,本人还是学生一枚,并不能解答类似生产环境的知识,欢迎各位大佬指正、指导。
具体参考博文链接如下:
https://www.cnblogs.com/barrywxx/p/8570821.html
https://blog.youkuaiyun.com/qq_35190492/article/details/102958250
https://thinkwon.blog.youkuaiyun.com/article/details/103522351
1. 概述
1.1 Redis是什么?
由C
语言编写的,一个单线程,高性能的(key/value
)内存数据库,基于内存运行并支持持久化的NoSQL
数据库。
1.2 Redis优缺点
优点
- 读写性能优异, 基于内存进行读写,而不是从磁盘进行读写。
- 支持数据持久化,支持
AOF
和RDB
两种持久化方式。 - 支持事务,
Redis
的所有操作都是原子性的,同时Redis
还支持对几个操作合并后的原子性执行,比如在存储键值对的时候,同时设置时间,这也通常被用来实现分布式锁。 - 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
- 支持哨兵模式,搭建集群时,可用性较高
缺点
- 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此
Redis
适合的场景主要局限在较小数据量的高性能操作和运算上。 - 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
- 主机宕机时,在哨兵进行选举时,会导致前端部分读写请求失败。
1.3 为什么要用 Redis /为什么要用缓存?
高性能
我们经常需要访问数据库,而数据库的数据又是存储在磁盘。当第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。数据需要从内核态拷贝到用户态,修改数据后,又需要从用户态拷贝到内核态,所以非常影响性能。而如果采用 Redis 的话,数据是已经被加载到内存中,不再有磁盘的读写过程,所以速度很快。
高并发
Redis
进行读的速度是110000
次/s,写的速度是81000
次/s,所以可以支持大量请求进行读写操作。
高可用
Redis
支持主从同步,提供 Cluster
集群部署模式,通过 Sentinel
哨兵来监控 Redis
主服务器的状态。当主挂掉时,在从节点中根据一定策略选出新主,并调整其他从 slaveof
到新主。
1.4 Redis为什么这么快?
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
- 数据结构简单,对数据操作也简单,
Redis
中的数据结构是专门进行设计的; - 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗
CPU
,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗; - 使用多路
I/O
复用模型,非阻塞IO
;
2. 常用数据结构
Redis
里面的每个键值对(key - value pair
)都是由对象(Object
)组成的。
- 键总是一个字符串对象(
String Object
) - 值可以是 字符串对象(String)、列表对象(List)、哈希对象(hash)、集合对象(set)、有序集合对象(zset) 这五种对象中的一种。
2.1 String
String
类型是 Redis 中最常使用的类型,内部的实现是通过 SDS
(简单动态字符串)来存储的(底层并不完全使用SDS实现,如一个数字,那么其是直接通过 int
表示)。
SDS
的数据结构如下:
struct sdshdr{
// 记录 buf 数组 中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
}
String
并没有直接采用 C
字符串,而是构建了上述的数据结构。但是依然遵循 C
字符串以空字符结尾的惯例,保存空字符串的1
字节不计算在 SDS
的 len
数组里面,并且为空字符串分配额外的1
个字节空间,以及添加空字符到字符串尾等操作,都是由 SDS
函数自动完成的。
SDS的好处:
- 常数复杂度获取字符串长度
- 缓冲区不会溢出
- 减少字符串修改所带来的内存重新分配次数
- 二进制安全
- 兼容部分
C
字符串函数
应用场景
- 缓存功能:做简单的键值对缓存
- 计数器:使用
Redis
作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。 - 共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存
Cookie
,但是可以利用Redis
将用户的Session
集中管理,在这种模式只需要保证Redis
的高可用,每次用户Session
的更新和获取都可以快速完成。大大提高效率。同时,如果项目被发布到多台服务器后,可以通过Redis
存储Session
,而不用将每台服务器中的Session
信息进行同步。
2.2 List
List
是有序列表,其底层是通过链表和压缩列表实现的。
链表和链表节点的实现:
typedef struct listNode{
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void * value;
}
typedef struct list{
// 表头结点
listNode *head;
// 表尾结点
listNode *tail;
// 链表所包含结点的数量
unsigned long len;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void *(*free)(void *ptr);
// 节点值对比函数
int (*match)(void *pre,void *key);
}
Redis
链表的特性:
- 双向无环链表
- 带表头指针和表尾指针
- 带链表长度计数器
- 多态:节点使用
void *
指针来保存值,所有可以用于保存各种不同类型的值
压缩列表的构成:
压缩列表(ziplist
)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较段的字符串,那么就使用压缩列表最为列表键底层实现。
压缩列表目的是为了节约内存,是由一系列特殊编码的连续内存块组成的顺序型(sequential
)数据结构。
应用场景
- 可以通过
List
存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。 - 可以通过
lrange
命令,读取某个闭区间内的元素,可以基于List
实现分页查询,这个是很棒的一个功能,基于Redis
实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。 - 消息队列:
Redis
的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush
命令从左边插入数据,多个数据消费者,可以使用BRpop
命令阻塞的“抢”列表尾部的数据。
2.3 Set
Set
是无序集合,会自动去重。其底层实现是通过整数集合或者字典。
整数集合的实现:
当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis
就是使用整数做为集合键的底层实现。
typedef struct intset{
// 编码方式
uint_32 encoding;
// 集合中包含的元素数量
uint_32 length;
// 保存元素的数组
int8_t contents[];
}inset;
字典的实现:
字典,又称为符号表、关联数组或映射,是一种用于保存键值对(key - value pair
)的抽象数据结构。
Redis
的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点保存了字典中的一个键值对。
1、哈希表
typedef struct dictht{
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
}dictht;
2、哈希表节点
typedef struct dictEntry{
// 键
void * key;
// 值
union{
void *val;
uint64_t u64;
int64_t s64;
}v;
// 执行下个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
3、字典
typedef struct dict {
// 函数管理
dictType *type;//函数管理
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当不再进行 rehash 时,值为 -1
long rehashidx;
} dict;
一般情况下,字典只使用 ht[0]
哈希表,ht[1]
哈希表只会在对 ht[0]
哈希表进行 rehash
时使用。
应用场景
- 对一些数据进行快速的全局去重
- 交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,看看俩人的共同好友是谁
2.4 Zset
Zset
是排序的 Set
,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。Zet
的底层是通过压缩表和(跳表+字典)实现的。
压缩表的结构前文有说明,这里不再说明。第一个节点保存元素的成员(member
),而第二个元素则保存元素的分值( score
)。
跳表的实现:
跳表是一种有序数据结构,通过在每个节点中维持多个执行其他节点的指针,从而实现快速访问节点的目的。(即空间换时间)
跳表平均查找复杂度(log(N)
),最坏(O(N)
)
如果一个有序集合包含的元素数量比较多,或者其成员是比较长度字符串时,Redis
就是使用跳表作为底层实现。
应用场景
- 排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
- 用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
2.5 Hash
Redis
hash
是一个键值对集合。 Redis
hash
是一个String
类型的field
和value
的映射表,hash
特别适合用于存储对象。其底层实现是通过压缩表或者字典。
应用场景
- 结构化的数据,比如一个对象(但是不支持对象内部嵌套对象)。
3. 持久化
4. 过期键的删除策略
我们可以给键值对设置超时时间,那么这些键值对是否是超时时间一到便自动被删除呢?答案并不是这样的,如果这样做的话,会极大的影响 Redis
的效率。那么过期键的删除有哪些策略呢?
4.1 定时删除
每当有过期键时,便立即删除。这样做的优点时,能够最大程度的的节约内存,但是后台运行大量定时任务,消耗CPU
。
4.2 惰性删除
当使用到某一个键时,如果发现这个键已经超时,那么此时在进行删除。对 CPU
友好,降低 CPU
的压力,但是浪费太多内存。
4.3 定期删除
指的是 Redis
默认是每隔100ms
就随机抽取一些设置了过期时间的key
,检查其是否过期,如果过期就删除。可以看作是对前两种方式的整合和折中。
当一个键过期时,Redis 使用的是惰性删除和定期删除两种策略,进行删除。
5. 内存淘汰策略
在上面我们介绍了过期键的删除策略,删除过期键可以在一定程度上节约内存,但是如果我们没有给键值对设置过期时间。那或者如果大量过期key
堆积在内存里,Redis
所占的内存满了,此时还可以继续存储吗?此时会走 Redis
的内存淘汰策略。
5.1 noeviction
当内存不足以容纳新写入数据时,新写入操作会报错。
5.2 allkeys-lru
当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key
。
5.3 allkeys-random
当内存不足以容纳新写入数据时,在键空间中,随机移除某个key
。
5.4 volatile-lru
当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key
。
5.5 volatile-random
当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
。
5.6 volatile-ttl
当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,选择淘汰将要过期的key
。
6. Redis 线程模型
Redis
基于 Reactor
模式开发了自己的网络事件处理器: 这个处理器被称为文件事件处理器(file event handler
)。
- 文件事件处理器使用
I/O
多路复用(multiplexing
)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。 - 当被监听的套接字准备好执行连接应答(
accept
)、读取(read
)、写入(write
)、关闭(close
)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O
多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 Redis
服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis
内部单线程设计的简单性。
6.1 文件事件处理器的构成
文件事件处理器由四个组成部分, 它们分别是套接字、I/O
多路复用程序、 文件事件分派器(dispatcher
)、 以及事件处理器。
文件事件是对套接字操作的抽象, 每当一个套接字准备好执行连接应答(accept
)、写入、读取、关闭等操作时, 就会产生一个文件事件。 因为一个服务器通常会连接多个套接字, 所以多个文件事件有可能会并发地出现。
I/O
多路复用程序负责监听多个套接字, 并向文件事件分派器传送那些产生了事件的套接字。
尽管多个文件事件可能会并发地出现, 但I/O
多路复用程序总是会将所有产生事件的套接字都入队到一个队列里面, 然后通过这个队列, 以有序(sequentially
)、同步(synchronously
)、每次一个套接字的方式向文件事件分派器传送套接字: 当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕), I/O
多路复用程序才会继续向文件事件分派器传送下一个套接字 。
6.2 IO 多路复用的实现
6.3 文件事件的处理器
Redis 为文件事件编写了多个处理器, 这些事件处理器分别用于实现不同的网络通讯需求, 比如说:
- 为了对连接服务器的各个客户端进行应答, 服务器要为监听套接字关联连接应答处理器。
- 为了接收客户端传来的命令请求, 服务器要为客户端套接字关联命令请求处理器。
- 为了向客户端返回命令的执行结果, 服务器要为客户端套接字关联命令回复处理器。
- 当主服务器和从服务器进行复制操作时, 主从服务器都需要关联特别为复制功能编写的复制处理器。
- 等等。
在这些事件处理器里面, 服务器最常用的要数与客户端进行通信的连接应答处理器、 命令请求处理器和命令回复处理器。
6.3.1 连接应答处理器
该处理器用于对连接服务器监听套接字的客户端进行应答。
当 Redis
服务器进行初始化的时候, 程序会将这个连接应答处理器和服务器监听套接字的 AE_READABLE
事件关联起来, 当有客户端用sys/socket.h/connect
函数连接服务器监听套接字的时候, 套接字就会产生 AE_READABLE
事件, 引发连接应答处理器执行, 并执行相应的套接字应答操作。
6.3.2 命令请求处理器
该处理器负责从套接字中读入客户端发送的命令请求内容。
当一个客户端通过连接应答处理器成功连接到服务器之后, 服务器会将客户端套接字的 AE_READABLE
事件和命令请求处理器关联起来, 当客户端向服务器发送命令请求的时候, 套接字就会产生 AE_READABLE
事件, 引发命令请求处理器执行, 并执行相应的套接字读入操作。
6.3.3 命令回复处理器
该处理器负责将服务器执行命令后得到的命令回复通过套接字返回给客户端。
当服务器有命令回复需要传送给客户端的时候, 服务器会将客户端套接字的 AE_WRITABLE
事件和命令回复处理器关联起来, 当客户端准备好接收服务器传回的命令回复时, 就会产生 AE_WRITABLE
事件, 引发命令回复处理器执行, 并执行相应的套接字写入操作。
6.3.4 一次完整的客户端与服务器连接事件示例
让我们来追踪一次 Redis
客户端与服务器进行连接并发送命令的整个过程, 看看在过程中会产生什么事件, 而这些事件又是如何被处理的。
假设一个 Redis
服务器正在运作, 那么这个服务器的监听套接字的 AE_READABLE
事件应该正处于监听状态之下, 而该事件所对应的处理器为连接应答处理器。
如果这时有一个 Redis
客户端向服务器发起连接, 那么监听套接字将产生 AE_READABLE
事件, 触发连接应答处理器执行: 处理器会对客户端的连接请求进行应答, 然后创建客户端套接字, 以及客户端状态, 并将客户端套接字的 AE_READABLE
事件与命令请求处理器进行关联, 使得客户端可以向主服务器发送命令请求。
之后, 假设客户端向主服务器发送一个命令请求, 那么客户端套接字将产生 AE_READABLE
事件, 引发命令请求处理器执行, 处理器读取客户端的命令内容, 然后传给相关程序去执行。
执行命令将产生相应的命令回复, 为了将这些命令回复传送回客户端, 服务器会将客户端套接字的AE_WRITABLE
事件与命令回复处理器进行关联: 当客户端尝试读取命令回复的时候, 客户端套接字将产生 AE_WRITABLE
事件, 触发命令回复处理器执行, 当命令回复处理器将命令回复全部写入到套接字之后, 服务器就会解除客户端套接字的AE_WRITABLE
事件与命令回复处理器之间的关联。
6. 事务
6.1 什么是事务?
事务指一系列操作可以被看作一个原子操作,如果有一个操作失败,则之前的操作也失败。
6.2 Redis事务的概念
Redis 事务的本质是通过MULTI
、EXEC
、WATCH
等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
6.3 Redis事务的三个阶段
- 事务开始
MULTI
- 命令入队
- 事务执行
EXEC
事务执行过程中,如果服务端收到有EXEC
、DISCARD
、WATCH
、MULTI
之外的请求,将会把请求放入队列中排队。
6.4 Redis事务相关命令
Redis
事务功能是通过MULTI
、EXEC
、DISCARD
和WATCH
四个原语实现的。
Redis
会将一个事务中的所有命令序列化,然后按顺序执行。
Redis
不支持回滚,“Redis
在事务失败时不进行回滚,而是继续执行余下的命令”, 所以Redis
的内部可以保持简单且快速。- 如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
- 如果在一个事务中出现运行错误,那么正确的命令会被执行。
注意:命令出现错误和运行出现错误是两个不同的概念。
WATCH
命令是一个乐观锁,可以为Redis
事务提供check-and-set
(CAS
)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC
命令。MULTI
命令用于开启一个事务,它总是返回OK
。MULTI
执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC
命令被调用时,所有队列中的命令才会被执行。EXEC
:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值nil
。- 通过调用
DISCARD
,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。 UNWATCH
命令可以取消watch
对所有key
的监控。
6.5 ACID 概述
- 原子性(
Atomicity
)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性(
Consistency
)
事务前后数据的完整性必须保持一致。
- 隔离性(
Isolation
)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
- 持久性(
Durability
)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
7. Redis 集群演变过程
7.1 单机版
单机版指只开启一台Redis的实例,如果当前实例挂掉后,则不能进行读写操作可用性较差;同时,如果有大量请求进行访问时,也不能够立即处理,负载能力较弱;并且存储能力受物理内存的限制。
7.2 主从复制架构
7.3 哨兵架构
7.4 集群架构
Redis的集群架构演变过程中,可用性不断增强,负载能力、存储能力也在不断的增强中。
7. 分布式
7.1 Redis 分布式锁的实现
7.2 一致性哈希算法
8. 缓存异常
8.1 缓存雪崩
缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案
- 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力
- 给缓存设置随机值,不要让大量缓存在同一时刻失效
8.2 缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
- 设置热点数据永远不过期。
- 加互斥锁,互斥锁
8.3 缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决方案
-
直接写个缓存刷新页面,上线时手工操作一下;
-
数据量不大,可以在项目启动的时候自动进行加载;
-
定时刷新缓存;
8.4 缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
-
一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
-
警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
-
错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
-
严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
8.5 热点数据和冷数据
热点数据,缓存才有价值
对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存
对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。
数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。
缓存热点key
缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
8.5.1 缓存热点key
缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案
对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询
9. Redis 数据安全
10. 其他问题
- Redis 是单线程的,如何发挥当前计算机多核的优势?
一台的服务器启动多个 Redis 实例。
- 如何保证缓存与数据库双写时的数据一致性?
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库
偶尔有不一致的情况。最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情
况串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。
3. 假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?
使用keys指令可以扫出指定模式的key列表。
对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。