Redis面试总结

定义

一个使用C语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库。

关系型和非关系型数据库特点

关系型数据库

关系型数据库的特点:
1、以表格的形式,基于行存储数据,是一个二维的模式;
2、存储的是结构化的数据,数据存储有固定的模式(schema),数据需要适应表结构;
3、表与表之间存在关联;
4、大部分关系型数据库都支持SQL(结构化查询语言)的操作,支持复杂的关联查询;
5、通过支持事务(ACID)来提供严格或者实时的数据一致性。

关系型数据库限制:
1、要实现扩容的话,只能向上(垂直)扩展,比如磁盘限制了数据存储,就要扩大磁盘容量,通过堆硬件的方式,不支持动态的扩缩容。水平扩容需要复杂的技术来实现,比如分库分表;
2、表结构修改困难,因此存储的数据格式也受到限制;
3、在高并发和高数据量的情况下,关系型数据库通常会把数据持久化到磁盘,基于磁盘的读写压力比较大。

非关系型数据库

非关系型数据库(NoSQL)的特点:
1、存储非结构化的数据,比如文本、图片、音频、视屏;
2、表与表之间没有关联,可扩展性强;
3、保证数据的最终一致性。
4、支持海量数据存储和高并发的高效读写;
5、支持分布式,能够对数据进行分片存储,扩缩容简单。

常见非关系型数据库

1、文档数据库,比较常见的有MongoDB。
2、KV 存储,用 Key Value 的形式来存储数据。比较常见的有Redis和Memcached。
3、图形存储,这个图(Graph)是数据结构,不是文件格式。比较常见的有Neo4j。
4、宽列存储,比较常见的有HBase。

Redis特点

1、更丰富的数据类型;
2、进程内与进程;单机与分布式;
3、功能丰富:持久化机制、过期策略;
4、支持多种编程语言;
5、高可用,集群。

Redis为什么这么快

1)、完全基于内存(ns级的访问),非常快速;
2)、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,也不存在加锁释放操作,没有因为可能出现死锁而导致的性能消耗;
3)、使用多路I/O复用模型,非阻塞IO;
4)、Redis中的数据结构是采用类似于java中的HashMap的数据结构Dict,底层采用数组加链表实现的哈希表,可以实现查找和操作O(1)时间复杂度。

Redis单线程如何处理那么多的并发客户端连接

Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。

Redis是单线程还是多线程

redis4.0之前,完全是单线程;
redis4.0时,引入了多线程,但是不针对核心流程,额外的线程仅用于后台处理;(所以说redis4.0有些人说是单线程,是因为他们指的是核心流程是单线程的)
redis6.0中,又一次引入了多线程概念,多线程主要用于网络I/O阶段,也就是接收命令和写回结果阶段,而在执行命令阶段,还是由单线程串行执行。由于执行时还是串行,因此无需考虑并发安全问题。
注:核心流程指的是redis正常处理客户端请求的流程,通常包括:接收命令、解析命令、执行命令、返回结果等。

Redis单线程原因

因为Redis是基于内存操作的,通常情况下内存不会是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。
既然CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了,因为如果使用多线程的话会更复杂,同时需要引入上下文切换、加锁等等,会带来额外的性能消耗。
而随着近些年互联网的不断发展,大家对于缓存的性能要求也越来越高了,因此redis也开始在逐渐往多线程方向发展。
最近的6.0版本就对核心流程引入了多线程,主要用于解决redis在网络I/O上的性能瓶颈。而对于核心的命令执行阶段,目前还是单线程的。

Redis数据类型

String字符串

可存储字符串、整数、浮点数

操作命令

set key value 存入字符串键值对
mset key value [key value…] 批量存储字符串键值对
setnx key value 存入一个不存在的字符串键值对
get key 获取一个字符串值
mget key [key…] 批量获取字符串值
del key [key…] 删除一个键值对
expire key seconds 设置一个键的过期时间(秒)
incr key 将key对应的value数字值加1
decr key 将key对应的value数字值减1
incrby key increment 将key对应的value数字值加increment
decrby key decrement 将key所对应的value数字值减decrement

应用场景

1)、单值缓存
2)、对象缓存
set user:1 value(json格式数据)
mset user:1:name aaa user:1:age 18
mget user:1:name user:1:age
3)、分布式锁
set lock true ex 10 nx
4)、计数器(某篇文章的阅读量、微博点赞数)
incr article:readcount:{文章id}
get article:readcount:{文章id}
5)、Web集群session共享
spring session + redis实现session共享
6)、分布式系统全局序列号
incrby orderId 10000 Redis批量生成序列号提升性能

存储实现原理

键值对形式,存储在dictEntry(key,value,及next(dictEntry))中
key:字符串,存储在SDS中
value:字符串,不是直接做为字符串存储,也不是直接存储在SDS中,实际存储在redisObject中

SDS(Simple Dynamic String,简单动态字符串):Redis 中自定义字符串的实现。SDS有多种结构,sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同长度的字符串,分别代表的是32byte(2^5byte),256byte,64KB,4GB。

数据模型图:
数据模型图
为什么使用SDS存储字符串:
1)、C语言中没有字符串类型,若要实现只能使用char[]实现,但使用字符数组必须分配足够的空间,否则会溢出;
2)、如果想获得字符串长度,必须遍历字符数组,时间复杂度是O(n);
3)、C语言中字符串的长度变更需要对字符数组的内存进行重新分配;
4)、C语言字符串是以’ \0’ 结尾的,因此如果存储图片、音频等多媒体文件的时候,存在二进制不安全问题。

SDS具备的特点:
1)、 SDS需要时可以自己进行扩容,解决了内存溢出的问题;
2)、SDS中定义了len属性,获取字符串长度时间复杂度为 O(1);
3)、通过“空间预分配”和“惰性空间释放”,防止多次重分配内存;
4)、判断是否结束的标志是 len 属性,但是还是以’ \0’ 结尾的(可以使用C语言库操作字符串)。

字符串类型的三种内部编码:
1)、int,存储8个字节的长整型(2^63-1);
2)、embstr,代表embstr格式的SDS,存储小于44个字节的字符串;
3)、raw,存储大于44个字节的字符串。

hash哈希

操作命令

hset key field value 存储一个哈希表key的键值
hsetnx key field value 存储一个不存在的哈希表key的键值
hmset key field value [field value…] 在一个hash表key中存储多个键值对
hget key field 获取哈希表key对应的field键值
hmget key field [field…] 批量获取哈希表key中多个field键值
hdel key field [field…] 删除哈希表key的field键值
hlen key 返回哈希表key中field的数量
hgetall key 返回哈希表key中所有的键值
hincrby key field increment 为哈希表key中的field键的值加上increment

优点:
1)、同类数据归类整合存储,方便数据管理;
2)、相比string操作消耗内存与cpu更小;
3)、相比string存储更节省空间。
缺点:
1)、过期功能不能使用在field上,只能使用在key上;
2)、Redis集群架构下不适合大规模使用。

应用场景

1)、对象缓存
hmset user {userId}:name aaa {userId}:age 18
hmget user 1:name 1:age
2)、电商购物车
以用户id为key,商品id为field,商品数量为value
添加商品:hset cart:001 10001 1
增加数量:hincrby cart:001 1001 1
商品总数:hlen cart:001
删除商品:hdel cart:001 1001
获取购物车所有商品:hgetall cart:001

存储实现原理

Redis在hash数据类型中使用了两种数据机构实现:ziplist(压缩列表)和hashtable(字典,dict)

ziplist:是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能来换取高效的内存空间利用率,是一种时间换空间的思想。只适用于字段个数少和字段值小的场景里面。
ziplist内部组织结构
使用到ziplist需要满足以下两个条件:
1)、所有的键值对的健和值的字符串长度都小于等于64byte;
2)、哈希对象保存的键值对数量小于512个。
(在redis.conf中对应的参数:hash-max-ziplist-value 64;hash-max-ziplist-entries 512)

hashtable(dict):hashtable被称为字典(dictionary),它是一个数组+链表的结构。
从最底层到最高层:dictEntry——dictht——dict

一个哈希对象超过配置的阈值(键和值的长度有>64byte,键值对个数>512 个)时,会转换成哈希表(hashtable)

list 列表

操作命令

lpush key value [value…] 将一个或多个value值插入到key列表的表头(最左边)
rpush key vlaue [value…] 将一个或多个value值插入到key列表的表尾(最右边)
lpop key 移除并返回列表头元素
rpop key 移除并返回列表尾元素
lrange key start stop 返回列表key中指定区间内的元素,区间以偏移量start和stop指定
blpop key [key…] timeout 从key列表表头弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待
brpop key [key…] timeout 从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待

应用场景

1)、常用数据结构
Stack(栈)= lpush + lpop
Queue(队列)= lpush + rpop
Blocking MQ(阻塞队列) = lpush + brpop
2)、微博和公众号消息流
张三微博关注了李四及王五
李四发微博,消息id为1001:lpush msg:{张三-id} 1001
王五发微博,消息id为1002:lpush msg:{张三-id} 1002
张三查看最新微博消息:lrange msg:{张三-id} 0 4

存储实现原理

3.2版本之前:采用ziplist(压缩列表)和linkedlist(双向链表)两种数据结构作为底层实现。

ziplist转为linkedlist的条件:
1)、新添加的字符串值长度超过默认值为64;
2)、ziplist包含的节点超过默认值为512。
(在redis.conf中对应的参数:list_max_ziplist_value 64;list_max_ziplist_entries 512)

3.2版本之后:采用quicklist数据结构作为底层实现。
quickList是一个ziplist组成的双向链表。每个节点使用ziplist来保存数据。(相当于ziplist 和 linkedlist 的结合体)

set 集合

操作命令

sadd key member [member …] 往集合key中存入元素,元素存在则忽略,若key不存在则新建
srem key member [member …] 从集合key中删除元素
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 destination key [key …] 将并集结果存入新集合destination中
sdiff key [key …] 差集运算
sdiffstore destination key [key …] 将差集结果存入新集合destination中

应用场景

1)、微信抽奖小程序
点击参与抽奖加入集合:sadd key {userId}
查看参与抽奖所有用户:smembers key
抽取count名中奖者:srandmember key [count] / spop key [count]
2)、微信微博点赞、收藏、标签
点赞:sadd like:{消息id} {用户id}
取消点赞:srem like:{消息id} {用户id}
检查用户是否点过赞:sismember like:{消息id} {用户id}
获取点赞的用户列表:smembers like:{消息id}
获取点赞用户数:scard like:{消息id}
3)、集合操作
sinter set1 set2 set3:取交集
sunion set1 set2 set3:取并集
sdiff set1 set2 set3:取差集
4)、集合操作实现微博微信关注模型
我(张三)关注的人: zhangsanSet-> {wangwu, zhaoliu}
李四关注的人:lisiSet–> {zhangsan, sunqi, wangwu, zhaoliu}
王五关注的人: wangwuSet-> {zhangsan, lisi, sunqi, zhaoliu, zhouba)
我(张三)和李四共同关注:SINTER zhangsanSet lisiSet–> {wangwu, zhouba}
我(张三)关注的人也关注他(李四): SISMEMBER wangwuSet lisi SISMEMBER zhaoliuSet lisi
我(张三)可能认识的人: SDIFF lisiSet zhangsanSet->(zhangsan, sunqi}
5)、集合操作实现电商商品筛选
SADD brand:huawei P50
SADD brand:xiaomi mi-12
SADD brand:iPhone iphone13
SADD os:android P50 mi-12
SADD cpu:brand:intel P50 mi-12
SADD ram:8G P50 mi-12 iphone13
SINTER os:android cpu:brand:intel ram:8G -> {P50,mi-12}

存储实现原理

set采用intset和hashtable两种数据结构作为底层实现。

intset有INTSET_ENC_INT16、INSET_ENC_INT32和INSET_ENC_INT64三种编码格式,分别对应不同的范围。Redis为了尽可能地节省内存,会根据插入数据的大小选择不一样的类型来进行存储。(根据插入整数数据大小进行升级,一旦升级了就不能降级)

元素都是整数类型,就用inset存储;不是整数类型或元素个数超过512个,就用hashtable存储。

zset 有序集合

操作命令

zadd key score member [[score member]…] 往有序集合key中加入带分值元素
zrem key member [member …] 从有序集合key中删除元素
zscore key member 返回有序集合key中元素member的分值
zincrby key increment member 为有序集合key中元素member的分值加上increment
zcard key 返回有序集合key中元素个数
zrange key start stop [WITHSCORES] 正序获取有序集合key从start下标到stop下标的元素
zreverange key start stop [WITHSCORES] 倒序获取有序集合key从start下标到stop下标的元素
zunionstore destkey numkeys key [key …] 并集计算
zinterstore destkey numkeys key [key …] 交集计算

应用场景

1)、Zset集合操作实现排行榜
点击新闻:zincrby hotNews:20220206 1 中国女足夺得亚洲杯冠军
展示当日排行前十:zreverange hotNews:20220206 0 9 WITHSCORES
七日搜索榜单计算:zunionstore hotNews:20220207-20220213 7 hotNews:20220207 hotNews:20220208… hotNews:20220213
展示七日排行前十:zreverange hotNews:20220207-20220213 0 9 WITHSCORES

存储实现原理

zset采用ziplist(压缩列表)和skiplist(跳表)+dict(字典)两种数据结构作为底层实现。

使用ziplist条件:
1)、元素数量小于128个;
2)、所有元素长度小于64byte。
(在redis.conf中对应的参数:zset-max-ziplist-entries 128;zset-max-ziplist-value 64)

超过上面的阈值采用skiplist(跳表)+dict(字典)数据结构存储。

Redis其它命令

dbsize:获取键总数。
exists:查看键是否存在。例:exists keyName
rename:重命名键。例:rename oldKeyName newKeyName
type:查看类型。例:type keyName
keys:全量遍历键,用来列出所有满足特定正则字符串规则的key,当redis数据量比较大时,
性能比较差,要避免使用。例:keys *
scan:渐进式遍历键,SCAN cursor [MATCH pattern] [COUNT count],三个参数,第一个是 cursor 整数值(hash桶的索引值),第二个是 key 的正则模式,第三个是一次遍历的key的数量(参考值,底层遍历的数量不一定),并不是符合条件的结果数量。第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。scan缺点是并不能保证完整的遍历出来所有的键(如果在scan的过程中如果有键的变化(增加、 删除、 修改),新增的键可能没有遍历到,遍历出了重复的键等情况)。例:scan 0 match key* count 1000
Info:查看redis服务运行信息。

Redis过期策略

Redis处理过期数据:采用惰性删除和定期删除两种策略来删除过期的键。

惰性删除:指Redis服务器不主动删除过期的键值,而是当访问键值时,再检查当前键值是否过期,如果过期则执行删除并返回null给客户端;如果没有过期则正常返回信息给客户端。

优点:简单,不需要对过期数据做额外处理,只有在每次访问时才检查值是否过期。
缺点:删除过期键不及时,造成一定的空间浪费。

定期删除:指Redis服务器每隔一段时间会检查一下数据库,看看是否有过期键可以被清除。(默认定期检查频率是每秒扫描10次,用于定期清除过期键;redis.conf中的“hz 10”可以调整;)

Redis服务器采用的是随机抽取形式,每次从过期字典中,取出20个键进行过期检测,过期字典中存储的是所有设置了过期时间的键值对。如果这批随机检查的数据中有25%的比例过期,那么会再抽取20个 随机键值进行检测和删除,并且会循环执行这个流程,直到抽取的这批数据中过期键值小于25%,此次检测才算完成。Redis服务器为了保证过期删除策略不会导致线程卡死,会给过期扫描增加了最大执行时间为25ms,及每次扫描不会超过25ms。

Redis淘汰策略

当内存不够用时Redis如何处理:当Redis的内存超过最大允许的内存之后,Redis会触发内存淘汰策略,删除一些不常用的数据,以保证Redis服务器的顺利运行。

在4.0版本之前Redis的内存淘汰策略有以下6种:
noeviction:不淘汰任何数据,当内存不足时,执行缓存新增操作会报错,它是Redis默认内存淘汰策略;
allkeys-lru:淘汰整个键值中最久未使用的键值(系统一般使用的);
allkeys-random:随机淘汰任意键值;
volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值;
volatile-random:随机淘汰设置了过期时间的任意键值;
volatile-ttl:优先淘汰更早过期的键值。

而在Redis4.0版本中又新增了2种淘汰策略:
volatile-lfu:淘汰所有设置了过期时间的键值中最少使用的键值;
allkeys-lfu:淘汰整个键值中最少使用的键值。

注:内存淘汰策略可以通过配置文件来修改,redis.conf 对应的配置项是“maxmemory-policy noeviction”,只需要把它修改成我们需要设置的类型即可。

Redis持久化

Redis持久化:指把内存数据写到磁盘中,防止服务器宕机了内存数据丢失。

RDB方式

RDB(Redis DataBase):按照一定时间将内存的数据以快照形式保存到硬盘中,对应产生的数据文件为dump.rdb(默认持久化方式)
redis.conf中save m n设置,意思是在m秒内至少有n个键被改动,则dump内存快照到dump.rdb文件中
手动生成rbd快照:
1)、save:生成快照的时候会阻塞当前Redis服务器, Redis不能处理其他命令。如果内存中的数据比较多,会造成Redis长时间的阻塞。生产环境不建议使用这个命令。
2)、bgsave(写时复制机制):执行 bgsave时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。
在这里插入图片描述
自动生成rdb快照:1)、文件配置的save m n触发;2)、shutdown触发,保证服务器正常关闭;3)、flushall

注:lastsave查看最近一次生成快照的时间。

优点:
1)、RDB是一个非常紧凑的文件,它保存了redis在某个时间点上的数据集;这种文件非常适合用于进行备份和灾难恢复;
2)、生成RDB文件时,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作;
3)、RDB在恢复大数据集时的速度比AOF的要快。

缺点:
1)、RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,频繁执行成本过高;
2)、在一定间隔时间做一次备份,所以redis意外down掉的话,就会丢失一次快照之后的所有修改(数据丢失)。

AOF方式

AOF(Append-Only File):AOF采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改Redis数据的命令时,就会把命令写入到AOF中。
redis.conf中appendonly yes开启AOF持久化方式,appendfilename “appendonly.aof”设置保存的具体文件,AOF的持久化策略,默认everysec,“appendfsync everysec”配置(no表示不执行fsync;always表示每次写入都执行fsync;everysec表示每秒执行一次fsync,兼顾了安全和效率,最多只会丢1秒的数据。)

AOF重写机制:当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件内容压缩,只保留可以恢复数据的最小指令集。(重写时会fork出一个子进程去做,不会对Redis正常命令处理有太多影响)
手动重写:客户端执行bgrewriteaof命令进行重写。
设置重写频率的参数:
auto-aof-rewrite-percentage 100 //aof文件自上一次重写后文件大小增长了100%则再次触发重写
auto-aof-rewrite-min-size 64mb //aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就
很快,重写的意义不大

优点:
AOF持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis最多也只会丢失1秒数据而已。

缺点:
1)、对于相同数据的Redis,AOF文件通常会比RDF文件更大;
2)、AOF 在恢复大数据集时的速度比RDB的恢复速度要慢。

混合持久化方式

混合持久化(4.x):指进行AOF重写时子进程将当前时间点的数据快照保存为RDB文件格式,而后将父进程累积命令保存为AOF格式

在redis.conf中通过“aof-use-rdb-preamble yes”配置来开启混合持久化

如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将 重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一 起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改 名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。 于是在Redis重启的时候,可以先加载RDB的内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,因此重启效率大幅得到提升。
在这里插入图片描述

Redis集群

为什么需要集群?
1、性能;(并发量非常高的情况下,单个redis性能不足以支撑,需要多个redis服务来完成)
2、扩展;(方便横向扩展)
3、可用性。(基于可用性和安全性考虑,若硬件发生故障,单机数据无法恢复,带来影响也是灾难性的)

Redis主从架构

Redis主从工作原理:
slave启动连接master并发送一个PSYNC命令请求复制数据。master收到PSYNC命令后,会在后台进行数据持久化通过bgsave生成最新的rdb快照文件,持久化期间,master会继续接收客户端的请求,它会把那些可能修改数据集的命令缓存在内存中。当持久化进行完毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的快照文件加载到内存中。然后,master再将之前缓存在内存中的命令发送给slave并执行。此后,master每次执行一个写命令都会同步发送给slave,保持master与slave之间数据的一致性。
当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多 个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送给多个并发连接的slave。

如果从节点有一段时间断开了与主节点的连接是不是要重新全量复制一遍? 如果可以增量复制,怎么知道上次复制到哪里?
不需要全量复制;通过断点续传,slave与master能够在网络连接断开重连后只进行部分数据复制。 (通过master-repl-offset记录的偏移量)

优点:
读写分离,master自动将数据同步到slave;
架构缺点:
1)、RDB文件过大时,同步非常耗时;
2)、在一主一从或一主多从的情况下,如果主服务器挂了,对外提供的服务就不可用了,单点问题没有得到解决;
3)、不支持在线扩容,Redis的容量受限于单机配置。

哨兵高可用架构(Sentinel,保证可用性)

原理:通过运行监控服务器来保证服务的可用性
功能:
1)、监控,监控主服务器和从服务器是否正常运行(给主从库发送ping命令判断);
2)、故障转移,主服务器出现故障时自动将从服务器转换为主服务器。

服务下线 :
Sentinel默认以每秒钟1次的频率向Redis服务节点发送PING命令。如果在down-after-milliseconds内都没有收到有效回复,Sentinel会将该服务器标记为下线 (主观下线)。
这个时候Sentinel节点会继续询问其他的Sentinel节点,确认这个节点是否下线,如果多数Sentinel节点都认为master下线,master才真正确认被下线(客观下线)。
此时候就需要重新选举master。
故障转移:如果master被标记为下线,就会开始故障转移流程。
既然有这么多的Sentinel节点,由谁来做故障转移的事情呢?故障转移流程的第一步就是在Sentinel集群选择一个Leader,由Leader完成故障转移流程。Sentinle通过Raft算法(Raft的核心思想:先到先得,少数服从多数),实现Sentinel选举。

Sentinel集群选举Leader流程:
发现master下线的哨兵节点(我们称他为A)向每个哨兵发送命令,要求对方选自己为领头哨兵;
如果目标哨兵节点没有选过其他人,则会同意选举A为领头哨兵;
如果有超过一半的哨兵同意选举A为领头,则A当选;
如果有多个哨兵节点同时参选领头,此时有可能存在一轮投票无竞选者胜出,此时每个参选的节点等待一个随机时间后再次发起参选请求,进行下一轮投票竞选,直至选举出领头哨兵。

哨兵Leader开始对系统进行故障恢复,从出现故障的master的从数据库中挑选一个来当选新的master,选择规则如下:
所有在线的slave中选择优先级最高的,优先级可以通过slave-priority配置;
如果有多个最高优先级的slave,则选取复制偏移量最大(即复制越完整)的当选;
如果以上条件都一样,选取id最小的slave。

挑选出需要继任的slave后,领头哨兵向该数据库发送命令使其升格为master,然后再向其他slave发送命令接受新的master,最后更新数据。将已经停止的旧的master更新为新的master的从数据库,使其恢复服务后以slave的身份继续运行。

为什么存在”主观下线”和“客观下线”两种下线状态:
因为单机哨兵很容易产生误判,误判后主从切换会产生一系列的额定开销,为了缩小误判,防止这些不必要的开销,采纳哨兵集群,引入多个哨兵实例一起来判断,就能够防止单个哨兵因为本身网络情况不好,而误判主库下线的状况,基于多数遵从少数准则, 当有N个哨兵实例时,最好要有N/2 + 1个实例判断主库为“主观下线”,才能最终断定主库为“客观下线” (能够自定义设置阙值)。

哨兵之间是如何相互通信:
哨兵集群中哨兵实例之间能够互相通信,是基于Redis提供的发布 / 订阅机制(pub/sub 机制),哨兵集群中各个实现通信后,就能够断定主库是否已主观下线。

优点:高可用,主从具有的优点都有
缺点:
1)、主从切换的过程中会丢失数据,因为只有一个master;
2)、没有解决水平扩容的问题。

Redis集群(Redis-cluster)

Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供。且并没有使用一致性hash,而是采用slot(槽)的概念,将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行将所有数据划分为16384个slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。 当Redis Cluster的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地。这 样当客户端要查找某个key时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不 一致的情况,还需要纠正机制来实现槽位信息的校验调整。

槽位定位算法Cluster默认会对key值使用 crc16 算法进行hash得到一个整数值,然后用这个整数值对16384进行取模 来得到具体槽位。HASH_SLOT = CRC16(key) mod 16384

优点:
1)、无中心架构。
2)、数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布。
3)、可扩展性,可线性扩展到1000个节点(官方推荐不超过1000个),节点可动态添加或删除。
4)、高可用性,部分节点不可用时,集群仍可用。通过增加Slave做standby数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升。
5)、降低运维成本,提高系统的扩展性和可用性。
缺点:
1)、Client实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。
2)、节点会因为某些原因发生阻塞(阻塞时间大于clutser-node-timeout),被判断下线,这种failover是没有必要的。
3)、数据通过异步复制,不保证数据的强一致性。
4)、多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。

Redis集群为什么至少需要三个master节点,并且推荐节点数为奇数?
因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。
奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。

Redis集群脑裂问题

当一个集群中的master恰好网络故障,导致与sentinal通信不上了,sentinal会认为master下线,且sentinal选举出一个slave作为新的master,此时就存在两个master了。此时,可能存在client还没来得及切换到新的master,还持续写向旧master的数据,当master再次复原的时候,会被作为一个slave挂到新的master下来,本人的数据将会清空,从新从新的master复制数据,这样就会导致数据缺失。

解决:通过配置参数min-slaves-to-write m和min-slaves-max-lag n解决,第一个参数表示最少的salve节点为m个,第二个参数表示数据复制和同步的延迟不能超过n秒
配置了这两个参数,如果发生脑裂,原master会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失。

缓存问题

缓存穿透

大量的请求访问一个不存在的key,由于缓冲数据不存在,则会穿透到数据库,这个时候,大量请求同时访问数据库,容易造成数据库崩溃,从而使系统对外不可用,这就是缓存穿透。
解决方案:
1)、查询这些不存在的数据时,将结果返回null放到缓存中,并设置一个短暂的过期时间;
2)、使用布隆过滤器,记录全量数据,访问数据时通过布隆过滤器判断key是否存在,如果不存在直接返回。

布隆过滤器的特点:
从容器的角度来说:
1)、如果布隆过滤器判断元素在集合中存在,不一定存在
2)、如果布隆过滤器判断不存在,一定不存在
从元素的角度来说:
1)、如果元素实际存在,布隆过滤器一定判断存在
2)、如果元素实际不存在,布隆过滤器可能判断存在

缓存击穿

指缓存中没有数据但数据库中有数据,一般是缓存时间过期,恰巧这个时间点对这个key有大量的并发请求过来,这时候大并发的请求会打向数据库,导致其崩溃。
解决方案:
1)、设置热点数据永不过期;(内存压力增大)
2)、访问时加互斥锁。(首先从缓存中读取数据,如果存在,直接返回值;如果不存在,设置互斥锁,设置成功后去数据库取数据并更新到缓存同时释放锁,如果获取锁失败,则暂停短暂时间后重新递归执行此方法)

public String get(String key) throws InterruptedException {
	String value = jedis.get(key);// 根据传入的key到缓存中获取对应的值
	if (value == null) {// 若值为空,则代表缓存值过期
		String mutexLock = "key_mutex_lock";
		if (jedis.set(mutexLock, "true", "nx", "ex", 3) == "1") {// 设置一个redis互斥锁,且设置成功
			//TODO:value = db.get(key);// 从数据库中获取值
	    	jedis.set(key, value);// 将值放入缓存一份
	    	jedis.del(mutexLock);// 删除互斥锁
		} else {// 设置互斥锁失败,代表有其它线程已经在操作db获取值
			Thread.sleep(500);// 等待0.5秒
			value = get(key);  // 重试
		}
		return value;
	} else {// 若值不为空,直接返回
    	return value;      
	}
}

缓存雪崩

指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。
解决方案:
1)、缓存数据的过期时间设置随机,防止同一时间大量数据过期的现象;
2)、设置部分热点数据永不过期。

缓存和数据库的一致性问题

Cache Aside Pattern(边路缓存模式):(尽可能地解决缓存与数据库的数据不一致问题)
1)、读数据时,先读缓存,缓存没有,再读数据库,然后取出来放入缓存,同时返回响应;
2)、更新的时候,先删除缓存,再更新数据库。

先更新数据库,再删除缓存
异常情况:若更新数据库成功,删除缓存失败。数据库中是新数据,缓存中是旧数据,则发生了数据双写不一致的情况。
解决:异步更新缓存,监听binlog日志的变化,然后在客户端完成删除 key 的操作。如果删除失败的话,再发送到消息队列。总之,对于后删除缓存失败的情况,我们的做法是不断地重试删除,直到成功。

先删除缓存,再更新数据库
异常情况:并发情况下,线程A删除缓存准备更新数据库时,线程B再A更新数据库之前先进行了查询操作,把数据库中未更新的值放入了缓存,然后线程A执行更新操作,数据库值被修改,就会发生数据双写不一致的情况。
解决:采用延时双删策略,在写入数据之后,再删除一次缓存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值