需要查看更多的框架相关的知识?点击这里
文章目录
Redis 介绍
Redis(Remote Dictionary Server
)是一个开源(BSD许可)的,内存中的数据结构存储系统(key-value的存储系统),它可以用作数据库、缓存和消息中间件。
为什么要使用Redis
Redis 本质上是一个 Key-Value 类型的内存数据库, 整个数据库加载在内存当中进行操作,定期通过异步操作把数据库数据 flush 到硬盘上进行保存。
因为是纯内存操作, Redis 的性能非常出色, 每秒可以处理超过 10 万次读写操作, 是已知性能最快的 Key-Value DB。
Redis 的出色之处不仅仅是性能, Redis 最大的魅力是支持保存多种数据结构, 此外单个 value 的最大限制是 1GB, 不像 memcached 只能保存 1MB 的数据, 因此 Redis 可以用来实现很多有用的功能,比方说用他的 List 来做 FIFO 双向链表,实现一个轻量级的高性能消息队列服务, 用他的 Set 可以做高性能的 tag 系统等等。
另外 Redis 也可以对存入的Key-Value 设置 expire 时间, 因此也可以被当作一个功能加强版的 memcached 来用。Redis 的主要缺点是数据库容量受到物理内存的限制, 不能用作海量数据的高性能读写, 因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。
Key命名要求
- key不要太长,最好不要超过1024个字节,这不仅会消耗内存还会降低查找效率
- key不要太短,如果太短会降低key的可读性
- 在项目中,key最好有一个统一的命名规范(根据企业的需求)
- 【强制】以英文字母开头,命名中只能出现小写字母、数字、英文点号(
.
)和英文半角冒号(:
); - 【强制】不要包含特殊字符,如下划线、空格、换行、单双引号以及其他转义字符;
为什么快?
redis的速度⾮常的快,单机的redis就可以⽀撑每秒10⼏万的并发,相对于mysql来说,性能是mysql的⼏⼗倍。速度快的原因主要有⼏点:
-
完全基于内存操作
-
C语⾔实现,优化过的数据结构,基于⼏种基础的数据结构,redis做了⼤量的优化,性能极⾼
-
使⽤单线程,⽆上下⽂的切换成本
为什么Redis6.0之后⼜改⽤多线程呢?
redis使⽤多线程并⾮是完全摒弃单线程,redis还是使⽤单线程模型来处理客户端的请求,只是使⽤多线程来处理数据的读写和协议解析,执⾏命令还是使⽤单线程。另外在持久化备份存储以及与slave同步时也是用的多线程
这样做的⽬的是因为redis的性能瓶颈在于⽹络IO⽽⾮CPU,使⽤多线程能提升IO读写的效率,从⽽整体提⾼redis的性能。
-
基于⾮阻塞的IO多路复⽤机制
利用多路复用机制,用一个线程监听以下操作(它们都是文件描述符)
- 客户端的新连接
- 客户端执行的命名
这里所谓的连接应答处理器,就是监听连接的文件描述符所绑定的函数 acceptHandler。所谓的命令请求处理器,就是监听客户端命令(读事件)的文件描述符绑定的函数 readQueryFromClient。
所谓的命令回复处理器,就是监听客户端响应(写事件)的文件描述符绑定的函数 sendReplyToClient。
这种一个负责响应 IO 事件,一个负责交给相应的事件处理器去处理,就叫做 Reactor 模式。
Redis 正是基于 Reactor 模式开发了自己的文件事件处理器,实现了高性能的网络通信模型,并且保持了 Redis 内部单线程设计的简单性。
key、value大小限制
- 虽然Key的大小上限为
512M
,但是一般建议key的大小不要超过1KB
,这样既可以节约存储空间,又有利于Redis进行检索。 - value的最大值也是
512M
。对于String类型的value值上限为512M
,而集合、链表、哈希等key类型,单个元素的value上限也为512M
。
基本命令
登录:redis-cli -h host -p port -a password
Info:查看当前redis信息,其中#Keyspace代表当前的0-15个db的数据量
select 0:切换到database 0(默认)
flushdb:清空当前的db
flushAll:清空所有的db
dbsize:查看当前redis存储键数量
save:保存当前所有操作
quit:退出当前连接
keys *:查看当前所有键值
rename a b:将键a重命名为b,如果当前db中有b键,那么旧的b键对应的值会被覆盖
renamenx a b:将键a重命名为b,若b已存在,则无法生效,nx结尾的都会做一些判断!
monitor:进入监视模式,对redis的操作都会在控制台显示日志
Value支持的五种数据类型
String
常用命令
set a a:创建一个String键值
setex a 100 a:设置一个String键值,过期时间为100秒
psetex d 10000 d:设置一个String键值,过期时间为10000毫秒,也就是10s
exists a:查看是否有a键
ttl a:time to leave查看当前key的剩余时间,单位秒,-1永久,-2不存在此key
expire a 10:设置a的过期时间为10秒
type a:查看键a的类型
randomkey:随机查看当前db中的key
mset a1 a1 b1 b1 c1 c1:同时设置3对key-value对
mget a1 b1 c1:同时获得3个键对应的值
setnx a a:当且只有a键不存在的时候才能成功创建,否则失败
msetnx a a b b:只有设置的所有键都在db中不存在时才能成功,要么都成功,要么都失败
set <lock.key> <lock.value> nx ex <expireTime> 版本>2.6.12时,可支持nx操作同时设置expire时间
getrange a 0 2:获得a键对应的String值的0-2位
getset a aa:获得a对应的值后再把a对应的值设置为aa,即返回原本的值
strlen a:返回a对应的值的长度
incr a:给a对应的值+1,要求其value得是数值Integer,否则会失败
incrby a 100:指定步长,给a对应的值+100,要求其value得是数值Integer,否则失败
decr a:给a对应的值-1,要求其value得是数值Integer,否则会失败
decrby a 100:指定步长,给a对应的值-100,要求其value得是数值Integer,否则失败
append a hahaha:给a对应的值追加hahaha串
实现方式(SDS)
String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr,decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。
底层编码类型
- int:8 个字节的长整型
- embstr:小于等于 39 字节的字符串
- raw:大于 39 字节的字符串
- embstr 只申请了一次内存,而 raw 需要申请两次,因此节约了一次申请内存的消耗
- 释放 embstr 只需要释放一次内存,而 raw 需要两次,因此节约了一次释放内存的消耗
- embstr 的 redisObject 和 sdshdr 放在一块连续的内存里,因此更能利用 缓存 带来的优势
使用场景
常规key-value缓存应用。常规计数: 微博数, 粉丝数。
Hash
Redis中的Hash类型可以看成具有String Key和String Value的map容器。所以该类型非常适合用于存储值对象的信息。如Username、Password和Age等。如果Hash中包含很少的字段,那么该类型的数据也将仅占用很少的磁盘空间。每一个Hash可以存储4294967295个键值对。
常用命令
hset map name tom:创建一个hash键值,键为map,key为name,对应值为tom
hexists map name:查看是否含有hash键值map的name
hget map name:获得map键的key=name对应的值
hgetall map:获得map里面含有的所有属性和属性对应的值
hkeys map:获得map里面所有的key
hvals map:获得map里面所有key对应的值
hlen map:查看key的个数
hmget map name age:同时获得map里面name和age对应的值
hmset map key1 value1 key2 value2:同时给map里面设置key1,key2键值对
hdel map key1 key2:将map里面的key1、key2键值对删除
hsetnx map a b:带有判断地给map创建key=a对应的值为b,若key=a存在则失败
实现方式(压缩列表/哈希表)
Redis Hash对应Value内部实际就是一个HashMap,会有2种不同实现,当Hash的成员比较少时,Redis为了节省内存会采用类似一维数组的方式来紧凑存储(压缩列表),而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为hash table。
- ziplist:元素个数小于 512,且所有值都小于 64 字节
- hashtable:除上述条件外
使用场景
通常用来存储对象,如用户信息等,也可作为限流器(setnx)使用。
List
常用命令
lpush list 1 2 3 4:给list添加1 2 3 4共4个值,从头插入,最后队头为4
llen list:查看当前list的长度
lrange list 0 2:取得list从0到2的范围,共返回3个数
lset list 0 100:将list第0个元素设置为100
lindex list 2:获得索引为2的元素
lpop list:将list的第一个元素弹出来
rpop list:将list的最后一个元素弹出来
BRPOP / BLPOP :阻塞式弹出元素,如果list中没有元素此操作会阻塞住,支持设置超时
实现方式(压缩列表/双向链表)
Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。
Redis的list是每个子元素都是String类型的双向链表,可以通过push和pop操作从列表的头部或者尾部添加或者删除元素,这样List即可以作为栈,也可以作为队列。
其底层编码类型/实现根据下述情况会有所不同:
- ziplist:元素个数小于 512,且所有值都小于 64 字节
- linkedlist:除上述条件外
使用场景
消息队列,朋友圈的点赞列表、评论列表等
Set
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
常用命令
sadd set a b c d:给set插入a b c d共四个值
scard set:查看集合中元素的数量
smembers set:查看set里面的所有元素,不保证顺序
sdiff set1 set2:取差集,查看set1里面的不存在于set2中的元素
sinter set1 set2:取交集,查看set1与set2共同存在的元素
sunion set1 set2:取并集,查看set1与set2的所有元素,共同拥有的只算一次
srandmember set1 2:返回set1中的随机两个元素
sismember set1 a:查看a是否为set1中的元素
srem set1 a b:移除set1里面 a b 两个元素
spop set1:随机移除set1里面的一个元素,并返回这个元素
实现方式(整数集合/哈希表)
set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。
集合这种数据类型用来存储一组不重复的数据。这种数据类型也有两种实现方法,一种是基于有序数组,另一种是基于散列表。
- intset:元素个数小于 512,且所有值都是整数
- hashtable:除上述条件外
使用场景
取交集、并集、差集、判断是否存在等
Zset / Sorted Set
常用命令
zadd key 2 a 4 b 6 c:创建一个有序集合set,第一个元素a分数为2,第二个……
zcard set:查看集合中元素的数量
zscore set a:查看集合中a元素的分数
zcount set 0 5:查看集合中分数在0-5范围的值的数量
zrank set a:查看a在set里面的排名/索引,按照分数从低到高返回
zrevrank set a:查看a在set里面的排名/索引,按照分数从高到低返回
zincrby set 10 a :给set里面的a的分数增加10
zrange set 0 10:获得排名/索引0到10的元素,共11个
zrange set 0 10 withscores:获得排名/索引0到10的元素,并带有分数值,从低到高,
zrevrange key 0 10 [withscores] 获得排名/索引0到10的元素,并带有分数值,从高到低
zrangebyscore key min max [WITHSCORES] [LIMIT offset count] 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。默认情况下,区间是闭区间,在 min/max 前面加个(,可实现闭区间
实现方式(压缩列表/跳表)
- ziplist:元素个数小于 128,且所有值都小于 64 字节
- skiplist(hashtable):除上述条件外
HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
应用场景
Redis五大数据类型应用场景:
扩展阅读:https://mp.weixin.qq.com/s/FE6we1KPKGwZL3NLLTefnw
扩展阅读:https://mp.weixin.qq.com/s/_XjK2NhGgxuyZCwbvq-Hlw
Redis数据结构
扩展阅读:https://mp.weixin.qq.com/s/SK-MCwEeLKoehICQzHktOg
Redis 是使用一个「哈希表」保存所有键值对,哈希表的最大好处就是让我们可以用 O(1) 的时间复杂度来快速查找到键值对。哈希表其实就是一个数组,数组中的元素叫做哈希桶。
哈希桶存放的是指向键值对数据的指针(dictEntry*),这样通过指针就能找到键值对数据,然后因为键值对的值可以保存字符串对象和集合数据类型的对象,所以键值对的数据结构中并不是直接保存值本身,而是保存了 void * key 和 void * value 指针,分别指向了实际的键对象和值对象,这样一来,即使值是集合数据,也可以通过 void * value 指针找到。
void * key 和 void * value 指针指向的是 Redis 对象,Redis 中的每个对象都由 redisObject 结构表示,如下图:
对象结构里包含的成员变量:
-
type,标识该对象是什么类型的对象(String 对象、 List 对象、Hash 对象、Set 对象和 Zset 对象);
-
encoding,标识该对象使用了哪种编码类型(对应底层的数据结构);
- 细分为:raw、int、embstr、linkedlist、ziplist、hashtable、intset、skiplist 共8种
-
ptr,指向底层数据结构的指针。
Redis键值对全景图
-
字符串-String:Redis没有直接使⽤C语⾔传统的字符串表示,⽽是⾃⼰实现的叫做简单动态字符串SDS的抽象类型。C语⾔的字符串不记录⾃身的⻓度信息,⽽SDS则保存了⻓度信息,这样将获取字符串⻓度的时间由O(N)降低到了O(1),支持存储"\0"字符,同时可以避免缓冲区溢出和减少修改字符串⻓度时所需的内存重分配次数。
对于String类型的,根据其encoding,可以细分为以下几种类型:
- int:8 个字节的长整型
- embstr:小于等于 39 字节的字符串
- raw:大于 39 字节的字符串
-
链表-Linkedlist:Redis链表是⼀个双向⽆环链表结构,很多发布订阅、慢查询、监视器功能都是使⽤到了链表来实现,每个链表的节点由⼀个ListNode结构来表示,每个节点都有指向前置节点和后置节点的指针,同时表头节点的前置和后置节点都指向NULL。
-
字典-Hashtable:⽤于保存键值对的抽象数据结构。Redis使⽤hash表作为底层实现,每个字典带有两个hash表,供平时使⽤和rehash时使⽤,hash表使⽤链地址法来解决键冲突,被分配到同⼀个索引位置的多个键值对会形成⼀个单向链表,在对hash表进⾏扩容或者缩容的时候,为了服务的可⽤性,rehash的过程不是⼀次性完成的,⽽是渐进式的。
注意,Redis本身存储Key、Value(各种类型)的结构时也是用的这个Hash表,也有rehash的操作。表现为dictht是容量为2的数组。
-
整数集合-Intset:⽤于保存整数值的集合抽象数据结构,不会出现重复元素,底层实现为数组(连续内存空间),当新存入的整数比较大时,支持数组升级。
-
压缩列表-Ziplist:压缩列表是为节约内存⽽开发的顺序性数据结构,他可以包含多个节点,每个节点可以保存⼀个字节数组或者整数值,由于存在以下缺点,一般只会用于保存节点数量不多的场景。
- 不能保存过多的元素,否则查询效率就会降低;
- 新增或修改某个元素时,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新(在特殊情况下产生连续多次空间扩展操作)的问题。
-
跳跃表-Skiplist:跳跃表是有序集合的底层实现之⼀,redis中在实现有序集合键和集群节点的内部结构中都是⽤到了跳跃表(由跳表与哈希表数据结构实现)。redis跳跃表由zskiplist和zskiplistNode组成,zskiplist⽤于保存跳跃表信息(表头、表尾节点、⻓度等),zskiplistNode⽤于表示跳跃表节点,每个跳跃表的层⾼都是1- 32的随机数,在同⼀个跳跃表中,多个节点可以包含相同的分值,但是每个节点的成员对象必须是唯⼀的,节点按照分值⼤⼩排序,如果分值相同,则按照成员对象的⼤⼩排序。
跳跃表的 level 是如何定义的?
跳跃表 level 层级完全是随机的。一般来说,层级越多,访问节点的速度越快。
基于上面这些基础的数据结构,Redis封装了⾃⼰的对象系统,包含字符串对象String、列表对象List、哈希对象Hash、集合对象Set、有序集合对象Zset/Sorted Set,每种对象都⽤到了⾄少⼀种基础的数据结构。
Redis通过encoding属性设置对象的编码形式来提升灵活性和效率,基于不同的场景Redis会⾃动做出优化。不同对象的编码如下:
- 字符串对象String:SDS,int整数、embstr编码的简单动态字符串、raw简单动态字符串
- 列表对象List:ziplist、linkedlist
- 哈希对象Hash:ziplist、hashtable
- 集合对象Set:intset、hashtable
- 有序集合对象Zset / Sorted set:ziplist、skiplist
Redis持久化方案
Redis的所有数据都是保存到内存中的。
RDB
Redis DataBase,快照形式,定期把内存中当前时刻的数据保存到磁盘。是Redis默认的持久化方案。RDB⽂件是⼀个压缩的⼆进制⽂件,通过它可以还原某个时刻数据库的状态。由于RDB⽂件是保存在硬盘上的,所以即使redis崩溃或者退出,只要RDB⽂件存在,就可以⽤它来恢复还原数据库的状态。
可以通过SAVE或者BGSAVE来⽣成RDB⽂件。
SAVE命令会阻塞redis进程,直到RDB⽂件⽣成完毕,在进程阻塞期间,redis不能处理任何命令请求, 这显然是不合适的。
save m n:m秒内数据集存在n次修改时,自动触发bgsave
BGSAVE则是会fork出⼀个⼦进程,然后由⼦进程去负责⽣成RDB⽂件,⽗进程还可以继续处理命令请求,不会阻塞进程。
底层采用写时复制技术(COW),在执行快照的同时,能正常处理写操作。
执行完操作后,在指定目录下会生成一个dump.rdb文件,Redis 重启的时候,通过加载dump.rdb文件来恢复数据
AOF
Append Only File,把所有对redis数据库操作的命令,增删改操作的命令。保存到日志文件中(执行完命令后将操作追加到AOF文件的末尾)。数据库恢复时把所有的命令执行一遍即可。
- always:同步写回,每个子命令执行完,都立即将日志写回磁盘。
- everysec:每秒同步,每个命令执行完,只是先把日志写到AOF内存缓冲区,每隔一秒同步到磁盘。
- no:只是先把日志写到AOF内存缓冲区,有操作系统去决定何时写入磁盘。
Redis 中**默认采用 AOF_FSYNC_EVERYSEC(每秒同步)**的策略,因为这种策略的性能很不错,而且一旦出现故障,最多只会丢失一秒的数据。
缺点
- 会降低效率(每秒刷盘),可能会阻塞下一个操作,并且不能支持太大的数据量
- 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大,数据恢复也比较慢。
AOF重写机制(后台子进程):随着时间推移,AOF文件会有一些冗余的命令如:无效命令、过期数据的命令等等,AOF重写机制就是把它们合并为一个命令(类似批处理命令),从而达到精简压缩空间的目的。
在redis.conf配置文件中配置如下:
Rdb
Aof
两种持久化方案同时开启则优先使用AOF文件来恢复数据库。
RDB、AOF混合
Redis从4.0版开始支持RDB与AOF的混合持久化方案。首先由RDB定期完成内存快照的备份,然后再由AOF完成两次RDB之间的数据备份,由这两部分共同构成持久化文件。
优点:充分利用了RDB加载快、备份文件小及AOF尽可能不丢数据的特性。缺点:兼容性差,一旦开启了混合持久化,在4.0之前的版本都不识别该持久化文件,同时由于前部分是RDB格式,阅读性较低。
数据恢复加载过程就是先按照RDB进行加载,然后把AOF命令追加写入。
持久化方案的建议
- 如果Redis只是用来做缓存服务器,比如数据库查询数据后缓存,那可以不用考虑持久化,因为缓存服务失效还能再从数据库获取恢复。
- 如果你要想提供很高的数据保障性,那么建议你同时使用两种持久化方式。如果你可以接受灾难带来的几分钟的数据丢失,那么可以仅使用RDB。
- 通常的设计思路是利用主从复制机制来弥补持久化时性能上的影响。即Master上RDB、AOF都不做,保证Master的读写性能,而Slave上则同时开启RDB和AOF(或4.0以上版本的混合持久化方式)来进行持久化,保证数据的安全性。
Redis集群
https://www.zhihu.com/question/21419897
主从复制
Sentinel 哨兵模式
基于主从复制进一步优化,相当于自动版的主从复制,解决了master挂机后无法自动恢复选举slave作为master的问题。通常会将哨兵与Redis节点的数量配置为单数。
哨兵的能力
-
监控:持续监控 master、slave、其他哨兵 是否处于预期工作状态。
哨兵之间采用Gossip通信协议,假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象称为主观下线。
哨兵1向其他实例哨兵发送
is-master-down-by-addr
命令。其他哨兵会根据自己和主库的连接情况,回应Y
或N
(Y 表示赞成,N表示反对票)。如果这个哨兵获取得足够多的赞成票数(quorum
配置),主库会被标记为客观下线。随后进入切换主库流程。 -
自动切换主库:当 Master 运行故障(客观下线),哨兵启动自动故障恢复流程(用Raft选出一个主哨兵),从 slave 中选择一台作为新 master。
哨兵Leader选举:选举主哨兵用来执行主从切换(其流程有点像Raft选举)
- 需要拿到
num(sentinels)/2+1
的赞成票。 - 并且拿到的票数需要大于等于哨兵配置文件中的
quorum
值。
故障转移:选举新的redis主节点方式(有点类似ZAB协议,都是选举投票算法,并且要考虑数据是否是最新的)
- 按照slave优先级(可配)
slave_repl_offset
与master_repl_offset
进度差距(越新的越好)- 当优先级和复制进度都相同的情况下,ID号最小的从库会被选为新主库
- 需要拿到
-
通知:让其他 slave 执行 replicaof ,与新的 master 同步;并且通知客户端与新 master 建立连接。
哨兵集群原理
为了避免单个哨兵故障后无法进行主从切换,以及为了减少误判率,又引入了哨兵集群;哨兵集群又需要有一些机制来支撑它的正常运行:
- 基于 pub/sub 机制实现哨兵集群之间的通信;
- 基于 INFO 命令获取 slave 列表,帮助 哨兵与 slave 建立连接;
- 通过哨兵的 pub/sub,实现了与客户端和哨兵之间的事件通知。
主从切换,并不是随意选择一个哨兵就可以执行,而是通过投票仲裁(Raft算法),选择一个 Leader,由这个 Leader 哨兵负责主从切换。
Sharding 模式(非官方)
客户端sharding技术-一致性hash算法
一致性Hash算法对 2^32 - 1取模的原因是IP地址是32位的
优点:加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。
缺点
-
加减节点会造成哈希环中部分数据无法命中(例如一个key增减节点前映射到第n2个节点,因此它的数据是保存在第n2个节点上的;当我们增加一个节点后被映射到n5节点上了,此时我们去n5节点上去找这个key对应的值是找不到的),需要手动处理或者忽略这部分数据,因此一致性哈希常用于缓存场景。
-
当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案。
-
普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。
Cluster 模式
官方-服务器sharding技术-哈希槽-切片集群
Redis Cluster方案采用哈希槽(Hash Slot
),来处理数据和实例之间的映射关系。
一个切片集群被分为16384
个slot(槽),每个进入Redis的键值对,根据key进行散列,分配到这16384插槽中的一个。使用的哈希映射也比较简单,用CRC16
算法计算出一个16bit
的值,再对16384
取模。数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点都可以处理这16384个槽。
Redis节点之间需要通过ping-pong机制交换数据信息,交换的的信息中,最占空间的是消息头里面的bitmap数组
myslots[CLUSTER_SLOT/8]
,其为发送节点负责的槽信息
,如果把槽的数量设置的太多,每个节点负责的槽位就越多,那么上述的心跳包就会更大,浪费带宽。
同时如果集群内的节点数量太多,ping-pong消息也会越多,但是实践上一般节点数量不会太多(官方推荐最大节点数为1000),因此16384完全够用了。
特点
- 解耦数据和节点之间的关系,简化了节点扩容和收缩难度。
- 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。
- 支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景。
集群伸缩
当增加服务器主节点时,每个节点会把一部分槽和数据迁移到新的节点6385,每个节点负责的槽和数据相比之前变少了从而达到了集群扩容的目的。
当减少服务器主节点时,会判断当前节点是否持有槽,有的话就迁移槽到其他节点,迁移完通知其他节点,并下线当前节点。
重定向机制
客户端给一个Redis实例发送数据读写操作时,如果这个实例上并没有相应的数据,会使用MOVED重定向和ASK重定向来找到对应的节点
在Redis cluster模式下,节点对请求的处理过程如下:
- 通过哈希槽映射,检查当前Redis key是否存在当前节点
- 若哈希槽不是由自身节点负责,就返回MOVED重定向
- 若哈希槽确实由自身负责,且key在slot中,则返回该key对应结果
- 若Redis key不存在此哈希槽中,检查该哈希槽是否正在迁出(MIGRATING)?
- 若Redis key正在迁出,返回ASK错误重定向客户端到迁移的目的服务器上
- 若哈希槽未迁出,检查哈希槽是否导入中?
- 若哈希槽导入中且有ASKING标记,则直接操作,否则返回MOVED重定向
MOVE重定向
客户端给一个Redis实例发送数据读写操作时,如果计算出来的槽不是在该节点上,这时候它会返回MOVED重定向错误,MOVED重定向错误中,会将哈希槽所在的新实例的IP和port端口带回去。这就是Redis Cluster的MOVED重定向机制。流程图如下:
ASK重定向
Ask重定向一般发生于集群伸缩的时候。集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据已经可能已经迁移到了目标节点,使用Ask重定向可以解决此种情况。
Redis主从同步
Redis主从同步包括三个阶段。
第一阶段:主从库间建立连接、协商同步。
- 从库向主库发送
psync
命令,告诉它要进行数据同步。- 主库收到
psync
命令后,响应FULLRESYNC
命令(它表示第一次复制采用的是全量复制),并带上主库runID
和主库目前的复制进度offset
。
第二阶段:主库把数据同步到从库,从库收到数据后,完成本地加载。
- 主库执行
bgsave
命令,生成RDB
文件,接着将文件发给从库。从库接收到RDB
文件后,会先清空当前数据库,然后加载 RDB 文件。- 主库把数据同步到从库的过程中,新来的写操作,会记录到
replication buffer
。
第三阶段,主库把新写的命令,发送到从库。
- 主库完成RDB发送后,会把
replication buffer
中的修改操作发给从库,从库再重新执行这些操作。这样主从库就实现同步啦。
一主多重全量复制时主库压力大的解决方案
如果是一主多从模式,从库很多的时候,如果每个从库都要和主库进行全量复制的话,主库的压力是很大的。因为主库fork进程生成RDB,这个fork的过程是会阻塞主线程处理正常请求的。同时,传输大的RDB文件也会占用主库的网络宽带。
可以使用主-从-从模式解决。什么是主从从模式呢?其实就是部署主从集群时,选择硬件网络配置比较好的一个从库,让它跟部分从库再建立主从关系(有点类似Mysql主从同步的多级复制架构)。如图:
Redis过期策略与内存淘汰策略
过期策略
我们在set key的时候,可以给它设置一个过期时间,比如expire key 60。指定这key60s后过期,60s后,redis是如何处理的嘛?我们先来介绍几种过期策略哈:
一般有定时过期、惰性过期、定期过期三种。
- 定时过期
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
- 惰性过期
只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
- 定期过期
每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。
Redis中同时使用了惰性过期和定期过期两种过期策略。
- 假设Redis当前存放30万个key,并且都设置了过期时间,如果你每隔100ms就去检查这全部的key,CPU负载会特别高,最后可能会挂掉。
- 因此,redis采取的是定期过期,每隔100ms就随机抽取一定数量的key来检查和删除的。
- 但是呢,最后可能会有很多已经过期的key没被删除。这时候,redis采用惰性删除。在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间并且已经过期了,此时就会删除。
但是呀,如果定期删除漏掉了很多过期的key,然后也没走惰性删除。就会有很多过期key积在内存内存,直接会导致内存爆的。或者有些时候,业务量大起来了,redis的key被大量使用,内存直接不够了,运维小哥哥也忘记加大内存了。难道redis直接这样挂掉?不会的!Redis用8种内存淘汰策略保护自己~
内存淘汰策略
- volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰;
- allkeys-lru:当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰。
- volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU(最少访问算法)进行删除key。
- allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰;
- volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数据;。
- allkeys-random:当内存不足以容纳新写入数据时,从所有key中随机淘汰数据。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰;
- noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。
Redis使用场景
缓存
全局ID
计数器
消息队列
普通消息队列
我们知道,对于专业的消息队列中间件,如Kafka和RabbitMQ,消费者在消费消息之前要进行一系列的繁琐过程。
如RabbitMQ发消息之前要创建 Exchange,再创建 Queue,还要将 Queue 和 Exchange 通过某种规则绑定起来,发消息的时候要指定 routingkey,还要控制头部信息
但是绝大多数情况下,虽然我们的消息队列只有一组消费者,但还是需要经历上面一些过程。
有了 Redis,对于那些只有一组消费者的消息队列,使用 Redis 就可以非常轻松的搞定。Redis 的消息队列不是专业的消息队列,它没有非常多的高级特性, 没有 ack 保证,如果对消息的可靠性有着极致的追求,那么它就不适合使用。
使用List来实现队列,存在以下问题:
- 不支持重复消费:消费者拉取消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费,即不支持多个消费者消费同一批数据
- 消息丢失:消费者拉取到消息后,如果发生异常宕机,那这条消息就丢失了
- 占用内存:当消息积压时,会导致这个链表很长,最直接的影响就是,Redis 内存会持续增长,直到消费者把所有数据都从链表中取出。
- 不支持根据特定类型进行消费
Redis 的 发布/订阅(Pub/Sub)模型(推模型)
Redis 提供了 PUBLISH / SUBSCRIBE 命令,来完成发布、订阅的操作。
Pub/Sub 最大的优势就是,支持多组生产者、消费者处理消息。
由于Redis实现的发布订阅模式,在整个过程中,没有任何的数据存储(Pub/Sub 的相关操作,不会写入到 RDB 和 AOF 中),一切都是实时转发的。因此会有丢数据的问题,如存在以下场景,就会丢数据:
消费者下线
Redis 宕机
消息堆积,缓冲区溢出,消费者会被强制踢下线,数据丢失
每个消费者订阅一个队列时,Redis 都会在 Server 上给这个消费者在分配一个「缓冲区」,这个缓冲区其实就是一块内存。
当生产者发布消息时,Redis 先把消息写到对应消费者的缓冲区中。之后,消费者不断地从缓冲区读取消息,处理消息。
如果超过了缓冲区配置的上限,此时,Redis 就会「强制」把这个消费者踢下线。这时消费者就会消费失败,也会丢失数据。
生产者先发消息后消费者才过来订阅(此条消息丢失)
List 其实是属于「拉」模型,而 Pub/Sub 其实属于「推」模型。
List 中的数据可以一直积压在内存中,消费者什么时候来「拉」都可以。
但 Pub/Sub 是把消息先「推」到消费者在 Redis Server 上的缓冲区中,然后等消费者再来取。
目前只有哨兵集群和 Redis 实例通信时,采用了 Pub/Sub 的方案,因为哨兵正好符合即时通讯的业务场景。
在 Redis 5.0 版本新增了一种Stream类型,可以更好地支持队列模型了~
延时队列(zset)
-
将消息序列化成字符串作为 zset 的 value,到期处理时间(消息生产时间戳 + 消息处理延迟时间戳 )为 score
-
多个线程轮询 zset 获取到期任务进行处理。多线程保障可用性,并发争抢,不会被多次执行
利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务即可。也可以通过
zrangebyscore key min max withscores limit 0 1
查询最早的一条任务,来进行消费
缺点:当需要实现精准的延时执行时,由于通常我们是需要本地有Timer线程去调用redis查看哪些任务的score到期了的,因此会有无法精确获取到任务的问题,此种情况下可以使用Redis的过期回调机制(把消息执行的时间定义为key
过期的时间,当key
触发了过期回调,那说明该消息可执行了)
分布式锁
限流器
通常限流有四种方式:漏桶、令牌桶、固定窗口、滑动窗口。
而Redis作为限流器有以下几种实现方式
1. 基于setnx + expire 【限流-固定窗口】
假如我们需要在10秒内限定某个IP 20个请求,那么可以通过以下的方式进行配置
- key:特定前缀 + IP
- value:1 (在10秒内有新请求来的时候给它incr自增)
- expire:10s
通过以上的方式,在请求刚来时,通过setnx + expire 10s用当前请求设置键值,在10s内如果请求又来了,判断value < 10 就持续给此key incr自增,最终判断达到10了就限频。
缺点:
- 当统计1-10秒的时候,无法统计2-11秒之内,如果需要统计N秒内的M个请求,那么我们的Redis中需要保持N个key等等问题
- 当请求在第一秒来一个,第9秒来了19个,随后第10秒来19个,实际上在9秒、10秒的区间来了38个请求,超出我们的限制
- 判断key值<10,随后执行incr的动作不是原子性的,在并发量较高时,会导致部分请求越过此限频。
2. 基于zset【限流-滑动窗口】
我们可以将请求打造成一个zset数组,当每一次请求进来的时候,value保持唯一,可以用UUID生成,而score可以用当前时间戳表示,因为score我们可以用来计算当前时间戳之内有多少的请求数量。而zset数据结构也提供了zcount/range方法让我们可以很轻易的获取到2个时间戳内有多少请求
假如我们需要在10秒内限定某个IP 20个请求,那么可以通过以下的方式进行配置
- Key:特定前缀 + IP(一个特定标识的用户一个Key)
- Score:当前请求的时间戳
- Value:无所谓
限流流程:在请求来时,执行:zcount key 10秒前的时间 当前时间
,获取10秒内的用户请求数量,若判断到>20了,则限制当前请求,否则zset添加当前请求并放行请求。
缺点:
- 如果没有定时清除旧值的机制,zset数组会越来越大,导致redis效率降低,内存不足。
- 不适用于大的限频策略,由于它要记录时间窗口内所有的行为记录,如果这个量很大,比如限定 60s 内操作不得超过 100w 次这样的参数,它是不适合做这样的限流的,会消耗大量的存储空间。
- 判断当前请求数量,随后执行添加当前时间分数的动作在应用层通常不是原子性的,在并发量较高时,会导致部分请求越过此限频。通常建议使用原子性的lua脚本来实现滑动窗口的限频。
3. 基于List【限流-令牌桶】
令牌桶实际上就是有一个令牌生成器,匀速地往桶里放入令牌,如果桶满了,生成器就不放了,每次请求都尝试从桶里拿一个令牌,拿到令牌就执行,拿不到令牌(桶空了)就阻塞或失败
基于上述的特征,我们可以通过Redis的 List 结构来很方便的实现令牌桶限流
- Key:如果是全局限流,可以随机命名,如果是特定请求的限流,可以为 特定前缀 + IP。
- Value:无所谓
假如我们要实现一个每秒10个令牌的全局限流令牌桶,其限流流程如下
- 定时线程(令牌生成器):每0.1秒判断当前桶有没有满,执行
llen list
查看当前list的长度- 满了:跳过
- 没满:往桶里放入一个令牌,执行
lpush list xx
- 用户请求:尝试去令牌桶拿令牌,执行
lpop/rpop list
- 拿到令牌:正常执行请求
- 没拿到:被限流
缺点:
- 需要有一个定时线程来持续生成令牌,需要考虑该线程的准时调度、可用性以及并发性(到点时此线程只能被触发一次)
排行榜
Redis事务机制
redis通过MULTI、EXEC、WATCH等命令来实现事务机制,事务执⾏过程将⼀系列多个命令按照顺序⼀次性执⾏,并且在执⾏期间,事务不会被中断,也不会去执⾏客户端的其他请求,直到所有命令执⾏完毕。事务的执⾏过程如下:
- 服务端收到客户端请求,事务以MULTI开始
- 如果客户端正处于事务状态,则会把事务放⼊队列同时返回给客户端QUEUED,反之则直接执⾏这个命令
- 当收到客户端EXEC命令时,WATCH命令监视整个事务中的key是否有被修改,如果有则返回空回复到客户端表示失败,否则redis会遍历整个事务队列,执⾏队列中保存的所有命令,最后返回结果给客户端
WATCH的机制本身是⼀个CAS的机制,被监视的key会被保存到⼀个链表中,如果某个key被修改,那么 REDIS_DIRTY_CAS 标志将会被打开,这时服务器会拒绝执⾏事务。
Java客户端
Jedis
Jedis 是 Redis 官方推荐的面向 Java 的操作 Redis 的客户端,其是直连 redis server 的。
其支持以下几种调用方式
-
普通同步调用
-
事务
-
管道:Jedis的管道实际上是使用了 Redis 的管道机制,其特点是通过批量操作机制(假异步),减少网络的调用次数,降低原本多次操作所需要耗费的多个RTT(Round Trip Time - 往返时间),从而有效提高的多个命令执行的速度。
开启 Redis pipeline 之后,再执行 Redis 的其他命令,命令将不会发送给服务端,而是先暂存在客户端,等到所有命令都执行完,然后再统一发送给服务端。服务端会根据发送过来的命令的顺序,依次运行计算。服务端会将结果暂存服务端,等到命令都执行完毕之后,统一返回给客户端。
Redis pipeline 命令的实现,其实需要客户端与服务端同时支持,并且实际执行过程中,Redis pipeline 会根据需要发送命令数据量大小进行拆分,拆分成多个数据包进行发送。这么做主要原因是因为,如果一次组装 pipeline 数据量过大,一方面会增加客户端的等待时间,而另一方面会造成一定的网络阻塞。
不同 Redis 客户端 pipeline 发送的最大字节数不太相同,比如 jedis-pipeline 每次最大发送字节数为8192。
一旦 Redis 客户端将部分 pipeline 中执行命令的发送给 Redis 服务端,服务端就会立即运行这些命令,然后返回给客户端。
但是此时客户端并不会去读取,所以返回的响应数据将会暂存在客户端的 Socket 接收缓冲区中。
如果响应数据比较大,填满缓冲区,此时客户端会通过 TCP 流量控制机制,ACK 返回 WIN=0(接收窗口)来控制服务端不要再发送数据。
这时这些响应数据将会一直暂存在 Redis 服务端输出缓存中,如果数据比较多,将会占用很多内存。
所以使用 Redis Pipeline 机制一定注意返回的数据量,如果数据很多,建议将包含大量命令的 pipeline 拆分成多次较小的 pipeline 来完成。
-
分布式
优点:
- 简单易理解
- 全面的Redis操作API
缺点:
-
使用阻塞的 I/O,且其方法调用都是同步的,程序流需要等到 sockets 处理完 I/O 才能执行,不支持异步;
-
Jedis 客户端实例不是线程安全的,所以需要通过连接池(
apache.commons.pool2
)来使用 Jedis(通过线程池可实现线程安全)。jedis本身不是多线程安全的,这并不是jedis的bug,而是jedis的设计与redis本身就是单线程相关,jedis实例抽象的是发送命令相关,一个jedis实例使用一个线程与使用100个线程去发送命令没有本质上的区别,所以没必要设置为线程安全的。但是如果需要用多线程方式访问redis服务器怎么做呢?那就使用多个jedis实例,每个线程对应一个jedis实例,而不是一个jedis实例多个线程共享。一个jedis关联一个Client,相当于一个客户端,Client继承了Connection,Connection维护了Socket连接,对于Socket这种昂贵的连接,一般都会做池化,jedis提供了JedisPool。
Lettuce
Lettuce
基于Netty的连接实例(StatefulRedisConnection),可以在多个线程间并发访问,且线程安全,满足多线程环境下的并发访问,同时它是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。Lettuce
还支持异步连接方式,提高网络等待和磁盘IO效率。
优点:
- Lettuce 的 API 是线程安全的,如果不是执行阻塞和事务操作,如BLPOP和MULTI/EXEC,多个线程就可以共享一个连接。
- 基于Netty框架的事件驱动通信,支持同步、异步、响应式调用。
- 适用于分布式缓存。
缺点:
- 学习成本高,上手相对复杂
RedisTemplate
Jedis 是 Redis 官方推荐的面向 Java 的操作 Redis 的客户端,而 RedisTemplate 是 SpringDataRedis 中对 JedisApi 的高度封装(高版本的Spring底层换成Lettuce了)。
StringRedisTemplate 继承于 RedisTemplate,两者的数据是不相通的(这两种方式不能混用于同一个Redis key,如通过RedisTemplate存,而又通过StringRedisTemplate取,会导致取不到数据)。
StringRedisTemplate 默认采用的是 string 的序列化策略,RedisTemplate 默认采用的是 JDK 的序列化策略。
RedisTemplate 使用的序列类在在操作数据的时候,比如说存入数据会将数据先序列化成字节数组,然后再存入 Redis 数据库,这个时候打开 Redis 查看的时候,你会看到你的数据不是以可读的形式展现的,而是以字节数组显示。当然从Redis获取数据的时候也会默认将数据当做字节数组转化再去Redis里面查。
当Redis当中的数据值是以可读的形式显示出来的时候,只能使用StringRedisTemplate才能获取到里面的数据。
当你的 redis 数据库里面本来存的是字符串数据或者你要存取的数据就是字符串类型数据的时候,那么你就使用 StringRedisTemplate 即可,但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从 Redis 里面取出一个对象,那么使用RedisTemplate 是更好的选择。
Redission
Java驻内存数据网格(In-Memory Data Grid)
Redission充分地利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。
Redisson在基于NIO的 Netty 框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。
简单地说,作为 Java 开发人员,我们若想在程序中集成 Redis,必须使用 Redis 的第三方库。而 Redisson 就是用于在 Java 程序中操作 Redis 的库,它使得我们可以在程序中轻松地使用 Redis。Redisson 在 java.util
中常用接口的基础上,为我们提供了一系列具有分布式特性的工具类
https://cloudpai.gitee.io/2018/04/21/2018-04-21-6/ 使用文档
相比 memcached 有哪些优势?
-
redis支持更丰富的数据类型(支持更复杂的应用场景):
Redis不仅仅支持简单的 k/v 类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
memcache仅支持简单的 k/v 数据类型,String,如果需要存储复杂的数据类型,需要客户端自己转化为字符串然后进行存储。
-
Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memcache把数据全部存在内存之中(不支持持久化)。
-
集群模式:
memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;
但是 redis 目前是原生支持 cluster 模式的.
-
Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。