String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value不仅可以是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M
。
long
型来表示,那么字符串对象会将整数值保存在字符串对象结构中的ptr
属性,并将编码设置为int
。如果字符串对象保存的是一个字符串,并且这个字符申的长度小于等于 44 字节(redis 5+版本),那么字符串对象将使用简单动态字符串(SDS)来保存这个,并将对象的编码设置为embstr
, embstr
编码是专门用于保存短字符串的一种优化编码方式:
一次内存分配函数,释放也只需要一次!
如果字符串对象保存的是一个字符串,并且这个字符串的长度大于 44 字节(redis 5+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为raw
:
两次内存分配函数,释放也需要两次!
//基本操作
set name ccz
get name
exists name
strlen name
del name
//批量操作
mset k1 v1 k2 v2
mget k1 k2
//计数器操作
set num 0
incr num
incrby num 10
decr num
decrby num 10
//过期操作
expire name 10
ttl name
setex name 60 ccz
//分布式锁
setnx key value
缓存对象;
SET user:1 '{"name":"xiaolin", "age":18}'
。常规计数
因为 Redis 处理命令是单线程,所以执行命令的过程是原子的。(计算访问次数、点赞数、转发数、库存数)
分布式锁
SET 命令有个 NX 参数可以实现「key不存在才插入」,可以用它来实现分布式锁:
一般而言,还会对分布式锁加上过期时间,分布式锁的命令如下:set key unique_value nx ex 100
共享 Session 信息
通常我们在开发后台管理系统时,会使用 Session 来保存用户的会话(登录)状态,这些 Session 信息会被保存在服务器端,但这只适用于单系统应用,如果是分布式系统此模式将不再适用。
我们需要借助 Redis 对这些 Session 信息进行统一的存储和管理,这样无论请求发送到那台服务器,服务器都会去同一个 Redis 获取相关的 Session 信息,这样就解决了分布式系统下 Session 存储的问题。
List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。
在 Redis 3.2 版本之前,List类型的底层数据结构是由双向链表或者压缩列表实现的;在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist
实现了,替代了双向链表和压缩列表。
其实 quicklist 就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表。
typedef struct quicklist {
quicklistNode *head; //quicklist的链表头
quicklistNode *tail; //quicklist的链表尾
unsigned long count; //所有压缩列表中的总元素个数
unsigned long len; //quicklistNodes的个数
...
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev; //前一个quicklistNode
struct quicklistNode *next; //后一个quicklistNode
unsigned char *zl; //quicklistNode指向的压缩列表
unsigned int sz; //压缩列表的的字节大小
unsigned int count : 16; //压缩列表(ziplist)中的元素个数
....
} quicklistNode;
在向quicklist
添加一个元素的时候,首先检查插入的位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到quicklistNode
结构里的压缩列表,如果不能容纳,才会新建一个quicklistNode
结构。
LPUSH key value [value...] //将一个或者多个value插入到key列表的表头(左边),最后的值在最前面
RPUSH key value [value...] //将一个或者多个value插入到key列表的表尾(右边)
LPOP key //移除并返回key列表的头元素
RPOP key //移除并返回key列表的尾元素
LRANGE key start stop //返回列表key中指定区间内的元素,区间以偏移量start和stop指定,从0开始
BLPOP key [key...] timeout //从key列表表 头 弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BRPOP key [key...] timeout //从key列表表 尾 弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
消息队列:满足三个需求(消息保序、处理重复消息、保证消息可靠性)
消息保序: List 本身就是按先进先出的顺序对数据进行存取的,所以作为队列来讲本身就满足这个需求;不过在消费者读取数据时,又一个潜在的性能风险点,即生产者往List 中写入数据时,并不会主动通知消费者有新消息写入,因此就需要消费者在程序中不停的调用RPOP
命令(比如使用一个while(1)循环),这就会导致消费者程序的 CPU 一直消耗在执行 RPOP
命令上,带来不必要的性能损失。
为了解决这个问题,Redis提供了 BRPOP 命令。BRPOP命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。
处理重复消息:
保证消息可靠性: 当消费者程序从 List 中读取一条消息后,List 就不会再留存这条消息了。所以,如果消费者程序在处理消息的过程出现了故障或宕机,就会导致消息没有处理完成,那么,消费者程序再次启动后,就没法再次从 List 中读取消息了。
为了留存消息,List 类型提供了 BRPOPLPUSH
命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。
Hash 是一个键值对(key - value)集合,其中 value 的形式如: value=[{field1,value1},...{fieldN,valueN}]
。Hash 特别适合用于存储对象。
Hash 类型的底层数据结构是由压缩列表或哈希表实现的,在redis7版本以后,不再使用压缩列表结构来实现,而是交由listpack
数据结构来实现。
listpack
如压缩列表一样,用一块连续的内存空间来紧凑地保存数据,并且为了节省内存的开销,listpack 节点会采用不同的编码方式保存不同大小的数据。
listpack 头包含两个属性,分别记录了 listpack 总字节数和元素数量,然后 listpack 末尾也有个结尾标识。
每个 listpack 节点结构主要包含三个方面内容:
HSET key field value // 存储一个哈希表key的键值
HGET key field //获取哈希表对应的field键值
HMSET key field value [field value...] // 在一个哈希表key中存储多个键值对
HMGET key field [field ...]
HDEL key field [field ...]
HLEN key
HGETALL key // 返回哈希表key中所有的键值
HINCRBY key field n // 为哈希表key中field键的值加上增量n
缓存对象: Hash 类型的 (key,field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
在介绍 String 类型的应用场景时有所介绍,String + Json也是存储对象的一种方式,那么存储对象时,到底用 String + json 还是用 Hash 呢?
一般对象用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。
购物车模块: 以用户 id 为 key,商品 id 为 field,商品数量为 value,恰好构成了购物车的3个要素。
涉及的命令如下:
HSET cart:{用户id} {商品id} 1
HINCRBY cart:{用户id} {商品id} 1
HLEN cart:{用户id}
HDEL cart:{用户id} {商品id}
HGETALL cart:{用户id}
当前仅仅是将商品ID存储到了Redis 中,在回显商品具体信息的时候,还需要拿着商品 id 查询一次数据库,获取完整的商品的信息。
Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。(无序、不重复、支持交兵差等操作)
Set 类型的底层数据结构是由哈希表或整数集合实现的:
512
(默认值,set-maxintset-entries
配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;SADD key member [member...]
SREM key member [member...]
SMEMBERS key // 获取集合key中所有元素
SCARD key // 获取集合key中的元素个数
SISMEMBER key member // 判断member元素是否存在于集合key中
SRANDMEMBER key [count] // 从集合key中随机选出count个元素,元素不从key中删除
SPOP key [count] // 从集合key中随机选出count个元素,元素从key中删除
SINTER key [key...] // 交集运算
SINTERSTORE destination key [key ...] // 将交集结果存入新集合destination中
SUNION key [key...] // 并集运算
SUNIONSTORE key [key...] // 将并集结果存入新集合destination中
SDIFF key [key...] // 差集运算
SDIFFSTORE destination key [key...] // 将差集结果存入新集合destination中
点赞: Set 类型可以保证一个用户只能点一个赞,这里举例子一个场景,key 是文章id,value 是用户id。
SADD article:1 uid:1 uid:2 uid:3 // uid:1、2、3用户对文章 article:1 点赞
SREM article:1 uid:1 // uid:1 取消了对 article:1 文章点赞
SMEMBERS article:1 // 获取 article:1 文章所有点赞用户
SCARD article:1 // 获取 article:1 文章的点赞用户数量
SISMEMBER article:1 uid:1 // 判断用户 uid:1 是否对文章 article:1 点赞了
共同关注: 用来计算共同关注的好友、公众号等。key 可以是用户id,value 则是已关注的公众号的id。
抽奖活动: key为抽奖活动名,value为员工名称,把所有员工名称放入抽奖箱。
Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。(元素不可以重复,但分值可以重复)
Zset 类型的底层数据结构是由压缩列表或跳表实现的:
128
个,并且每个元素的值小于 64
字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
zset 结构体里有两个数据结构:一个是跳表,一个是哈希表。这样的好处是既能进行高效的范围查询,也能进行高效单点查询。
Zset 对象在执行数据插入或是数据更新的过程中,会依次在跳表和哈希表中插入或更新相应的数据,从而保证了跳表和哈希表中记录的信息一致。
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zskiplistNode {
//Zset 对象的元素值
sds ele;
//元素权重值
double score;
//后向指针
struct zskiplistNode *backward;
//节点的level数组,保存每层上的前向指针和跨度
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
Zset 对象要同时保存「元素」和「元素的权重」,对应到跳表节点结构里就是 sds 类型的 ele 变量和 double 类型的 score 变量。每个跳表节点都有一个后向指针(struct zskiplistNode *backward),指向前一个节点,目的是为了方便从跳表的尾节点开始访问节点,这样倒序查找时很方便。
跳表是一个带有层级关系的链表,而且每一层级可以包含多个节点,每一个节点通过指针连接起来,实现这一特性就是靠跳表节点结构体中的zskiplistLevel 结构体类型的 level 数组。
ZADD key score member [[score member]...]
ZREM key member [member...]
ZCARD key // 返回有序集合key中元素个数
ZINCRBY key increment member // 为有序集合key中元素member的分值加上increment
ZRANGE key start stop [WITHSCORES] // 正序获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES]
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] // 返回有序集合中指定分数区间内的成员,分数由低到高排序
ZRANGEBYLEX key min max [LIMIT offset count] // 返回指定成员区间内的成员,按字典正序排列, 分数必须相同
ZREVRANGEBYLEX key max min [LIMIT offset count]
主要用于存储地理位置信息,并对存储的信息进行操作。
GEO 本身并没有设计新的底层数据结构,而是直接使用了 ZSet 集合类型。GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。
# 存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。
GEOADD key longitude latitude member [longitude latitude member ...]
# 从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。
GEOPOS key member [member ...]
# 返回两个给定位置之间的距离。
GEODIST key member1 member2 [m|km|ft|mi]
# 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。
简单来说 HyperLogLog 提供不精确的去重计数。
# 添加指定元素到 HyperLogLog 中
PFADD key element [element ...]
# 返回给定 HyperLogLog 的基数估算值。
PFCOUNT key [key ...]
# 将多个 HyperLogLog 合并为一个 HyperLogLog
PFMERGE destkey sourcekey [sourcekey ...]
百万级网页UV(unique visitor)计数:在统计 UV 时,你可以用 PFADD 命令(用于向 HyperLogLog 中添加新元素)把访问页面的每个用户都添加到 HyperLogLog 中(PFADD page1:uv user1 user2 user3 user4 user5
)
再用 PFCOUNT 命令直接获得 page1 的 UV 值了,这个命令的作用就是返回 HyperLogLog 的统计结果(PFCOUNT page1:uv)。
即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1
的设置,表示某个元素的值或者状态,时间复杂度为O(1)。
由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。
Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。
String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。
SETBIT key offset value // 设置值,其中value只能是0和1
GETBIT key offset // 获取值
BITCOUNT key start end // 获取制定范围内值为1的个数
BITPOS [key] [value] // 返回指定key中第一次出现指定value(0/1)的位置
GETBIT
判断对应的用户是否在线。BITCOUNT
统计 bit = 1 的个数便得到了连续打卡 7 天的用户总数了。在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:
Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。
XADD:插入消息,保证有序,可以自动生成全局唯一 ID;
XLEN :查询消息长度;
XREAD:用于读取消息,可以按 ID 读取数据;
XDEL : 根据消息 ID 删除消息;
DEL :删除整个 Stream;
XRANGE :读取区间消息
XREADGROUP:按消费组形式读取消息;
XPENDING 和 XACK:
XPENDING 命令可以用来查询每个消费组内所有消费者「已读取、但尚未确认」的消息;
XACK 命令用于向消息队列确认消息处理已完成;
针对 Redis 是否适合做消息队列,关键看你的业务场景:
Redis 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里,然后重启 Redis 的时候,先去读取这个文件里的命令,并且执行它,这不就相当于恢复了缓存数据了吗?这种写操作命令到日志的持久化方式,就是redis里的AOF(append only file)功能,读操作命令不会记录。re di s的AOF持久化功能默认不开启,需要到redis.conf文件里手动修改!
【注意】Redis 是先执行写操作命令后,才将该命令记录到 AOF 日志里的,这么做其实有两个好处:
server.aof_buf
缓冲区;Redis 提供了 3 种写回硬盘的策略,控制的就是上面说的第三步的过程。
这 3 种写回策略都无法能完美解决「主进程阻塞」和「减少数据丢失」的问题:
AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小会越来越大;这时会带来性能问题,比如重启 Redis 后,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个恢复的过程就会很慢;因此提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。
AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。
AOF日志重写的操作不能放在主进程里!
Redis 的重写 AOF 过程是由后台子进程 *bgrewriteaof* 来完成的,这个期间主进程可以继续处理命令请求。
fork
系统调用生成 bgrewriteaof 子进程,复制一份页表给子进程,页表记录着虚拟地址和物理地址的映射关系,不会复制物理内存。页表项的权限属性为只读,因此当父进程或子进程向内存发起写操作时会引起缺页中断,然后操作系统会在「缺页异常处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为「写时复制(*Copy On Write*)」。写时复制顾名思义,在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。
但是还有两个阶段可能会导致父进程阻塞:
这里有个问题: 重写 AOF 日志过程中,如果主进程修改了已经存在 key-value,此时这个 key-value 数据在子进程的内存数据就跟主进程的内存数据不一致了,这时要怎么办呢?
解决办法:Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用;在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。
也就是说,在 bgrewriteaof 子进程执行 AOF 重写期间,主进程需要执行以下三个工作:
当子进程完成 AOF 重写工作后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。
主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:
信号函数执行完后,主进程就可以继续像往常一样处理命令了。
RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。
因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。
Redis 提供了两个命令来生成 RDB 文件,分别是 save
和 bgsave
,他们的区别就在于是否在「主线程」里执行:
因此redis一般使用bgsave命令来生成RDB文件,相应的配置如下:
save 900 1 // 900 秒之内,对数据库进行了至少 1 次修改
save 300 10 // 300 秒之内,对数据库进行了至少 10 次修改
save 60 10000 // 60 秒之内,对数据库进行了至少 10000 次修改
缺点: 在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多,因为 RDB 快照是全量快照的方式,因此执行的频率不能太频繁,否则会影响 Redis 性能,而 AOF 日志可以以秒级的方式记录操作命令,所以丢失的数据就相对更少。
执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的。(通过写时复制)
这里和AOF日志不同的是,Redis 在使用 bgsave 快照过程中,如果主线程修改了内存数据,不管是否是共享的内存数据,RDB 快照都无法写入主线程刚修改的数据,因为此时主线程(父进程)的内存数据和子进程的内存数据已经分离了,子进程写入到 RDB 文件的内存数据只能是原本的内存数据。如果系统恰好在 RDB 快照文件创建完毕后崩溃了,那么 Redis 将会丢失主线程在快照期间修改的数据。
混合持久化工作在 AOF 日志重写过程。
当开启了混合持久化时,在 AOF 重写日志时,fork
出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
如果AOF 写回策略配置了 Always 策略,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。
AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 fork()
函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程):
客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
**定义:**Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。
定时删除: 在设置key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。
惰性删除: 不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
定期删除: 每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
「惰性删除+定期删除」 配合使用!
当 Redis 的运行内存已经超过 Redis 设置的最大内存之后,则会使用内存淘汰策略删除符合条件的 key,以此来保障 Redis 高效的运行。
全称是Least Recently Used,最近最久未使用,传统做法是:于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可
Redis 是如何实现 LRU 算法的?
在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。
全称是 Least Frequently Used,最近最少次使用,LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。
Redis 是如何实现 LFU 算法的?
在 LFU 算法中,Redis对象头的 24 bits 的 lru 字段被分成两段来存储,高 16bit 存储 ldt(Last Decrement Time),低 8bit 存储 logc(Logistic Counter)。
Redis 在访问 key 时,对于 logc 是这样变化的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oRIrL9BL-1667194793046)(/Users/bytedance/Desktop/自己总结八股pdf/截图/内存淘汰策略.webp)]
定义: 当用户访问的数据既不在内存也不在数据库的时候,导致缓存失效,进而大量的后续相同请求直接打到数据库上。
应对方案:
定义: 如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,导致缓存失效,直接访问数据库。
应对方案:
定义: 当大量缓存在同一时间过期或者Redis宕机故障时,此时大量的请求无法在redis中处理,会直接访问到数据库,导致数据库压力骤增,严重的话会造成数据库宕机,造成整个系统崩溃。
应对方案:
但是以上两种方案都会存在并发一致性问题,即当两个请求并发更新同一条数据的时候,可能会出现缓存和数据库中不一致的现象。
解决办法: 更新数据库并删除缓存:在更新数据时,不更新缓存,而是删除缓存中的数据。然后,到读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。(旁路缓存策略)
**兜底策略:**给缓存加上过期时间
写策略的步骤:
读策略的步骤:
这里还存在一个问题:更新数据库和删除缓存是两个操作,有可能出现第二个操作失败的情况,会导致缓存中的数据是旧值,而数据库中的数据是新值。
如何保证两个操作都能执行成功:1、重试机制 2、订阅MySQL binlog,在操作缓存。
**问题:**我们知道主服务器是可以有多个从服务器的,如果从服务器数量过多,而且都与主服务器进行全量同步的话,会带来两个问题:
**解决:**设置经理服务器角色
主从服务器在完成第一次同步后,就会基于长连接进行命令传播。如果因为网络故障断开连接后,又恢复链接,那么就要进行增量复制!
主要有三个步骤:
其中标记主从服务器复制位置的区域有两个:
【注意】在主服务器进行命令传播时,不仅会将写命令发送给从服务器,还会将写命令写入到 repl_backlog_buffer 缓冲区里,因此 这个缓冲区里会保存着最近传播的写命令。
主服务器根据自己的 master_repl_offset 和 slave_repl_offset 之间的差距,然后来决定对从服务器执行哪种同步操作:
【注意】因为缓冲物是环形的,里面数据容易被覆盖;所以对于主服务器写入速度远超从服务器的情况下,可以适当调高repl_backlog_buffer的大小,减少被覆盖的概率。
主从复制总共有三种模式:全量复制、基长链接命令传播、增量复制
主从服务器第一次同步的时候,就是采用全量复制,此时主服务器会两个耗时的地方,分别是生成 RDB 文件和传输 RDB 文件。为了避免过多的从服务器和主服务器进行全量复制,引用经理服务器角色分摊主服务器的压力。
第一次同步完成后,主从服务器都会维护着一个长连接,主服务器在接收到写操作命令后,就会通过这个连接将写命令传播给从服务器,来保证主从服务器的数据一致性。
主从服务器网络恢复时,就会发生增量复制,不过也要看repl_backlog_buffer的大小;有可能发生「从服务器」想读的数据已经被覆盖了,那么这时就会导致主服务器采用全量复制的方式。
在Redis的主从架构中,采用读写分离的主从模式;那么如果主节点挂了的话,就要选择一个从节点切换为主节点,然后让其它从节点指向新的主节点,同时通知上游连接Redis主节点的客户端,将其配置中的主节点IP更换为新主节点的IP地址。
哨兵(Sentinel)机制的作用就是实现主从节点故障转移,即上面的步骤。
哨兵节点主要负责三件事情:监控、选主、通知。
通过哨兵每隔一段时间(1秒)给所有主从节点的PING命令以及应答中,判断是否存在【主观下线】节点;然后和多个哨兵节点一起判断该【主观下线】节点是否是真的故障了,而不是由于系统压力过大或者网络用塞导致没在规定时间内响应,
哨兵节点之间是通过 Redis 的发布者/订阅者机制来相互发现的。在主从集群中,主节点上有一个名为__sentinel__:hello
的频道,哨兵 A 把自己的 IP 地址和端口的信息发布到__sentinel__:hello
频道上,哨兵 B 和 C 订阅了该频道。那么此时,哨兵 B 和 C 就可以从这个频道直接获取哨兵 A 的 IP 地址和端口号。然后,哨兵 B、C 可以和哨兵 A 互相建立网络连接。
主节点知道所有「从节点」的信息,所以哨兵会每 10 秒一次的频率向主节点发送 INFO 命令来获取所有「从节点」的信息,再与从节点建立连接,并进行监控;哨兵BC也是如此。
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程的,如下所示
随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 IO 的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度。Redis采用多个 IO 线程来处理网络IO请求,提高网络请求处理的并行度;对于读写命令的执行,Redis 仍然使用单线程来处理。
阶段一:服务端和客户端建立 Socket 连接,并分配处理线程
阶段二:IO 线程读取并解析请求
阶段三:主线程执行请求操作
阶段四:IO 线程回写 Socket 和主线程清空全局队列
定义: Redis因为是一主多从的架构模式,有可能因为网络原因导致主节点和从节点数据同步暂停,但是主节点和客户端通信正常,还再向主节点读写数据,但是这些数据此时无法同步给从节点;与此同时哨兵机制选举出了一个新主节点,这时集群就出现了两个主节点—脑裂现象。
问题:如果网络突然好了,哨兵机制因为选举出了新主节点,会把原来的旧主节点降级为从节点,然后从节点会向新的主节点请求数据同步;因为第一次同步会使用全量同步的方式,从节点会先把自己的数据清空,在执行同步,所以之前客户端在这个从节点写入的数据就会丢失。
解决方案: 当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。
在 Redis 的配置文件中有两个参数我们可以设置: