掌握Redis

概述

redis是什么?

引用官方,redis.io

Redis是一个开源的,内存数据结构存储,作为数据库,缓存,消息队列使用。
redis提供了String,Hash,List,Set,Zset多种数据结构,并针对这些数据结构支持范围查询,bitmap,hyperloglogs,geospatial 索引和流。
Redis内置了replication、Lua脚本、LRU eviction、事务和不同级别的磁盘持久性,并通过Redis 哨兵和Redis Cluster自动分区提供高可用性。

数据结构

数据结构通常是指value的数据结构,key的数据结构是String。

String

命令

单个操作,set,get

127.0.0.1:6379> set language1 Java
OK
127.0.0.1:6379> get language1
"Java"
127.0.0.1:6379>

批量操作

127.0.0.1:6379> mset language1 Java language2 c++ language3 python
OK
127.0.0.1:6379> mget language1 language2 language3
1) "Java"
2) "c++"
3) "python"
127.0.0.1:6379> 

setnx,key不存在时setkey,可用作分布式锁,后面会提到。

127.0.0.1:6379> setnx language1 Java
(integer) 0
127.0.0.1:6379> setnx language4 php
(integer) 1
127.0.0.1:6379> get language4
"php

超时操作,expire为key设置超时时间,setEx设置超时时间并set key

127.0.0.1:6379> expire language1 3 #设置三秒后超时,删除key
(integer) 1
127.0.0.1:6379> get language1
(nil)
127.0.0.1:6379> setex language 100 Java #set lauguange1的value为Java,并且设置超时时间为100秒
OK
127.0.0.1:6379> get language
"Java"

自增,当key为数字时,可以使用incr或者incrby自增

127.0.0.1:6379> set count 100
OK
127.0.0.1:6379> incr count
(integer) 101
127.0.0.1:6379> INCRBY count 100
(integer) 201

实现

字符串类型,底层使用SDS(简单动态字符串)实现。

SDS字符串
Simple Dynamic String,其结构体中定义了字符串长度len,未使用数组长度free,用来保存数据的c字符串buf。

SDS预分配和惰性分配
预分配,创建sds时,或者扩展sds长度时,预先分配空间,字符串小于1MB时,预分配字符串长度的空间,字符串大于1MB时,分配1MB空间。
惰性分配,字符串缩短时,不回收被缩短的内存部分,而是加入到free中,等待重用
对比c字符串char[]

优点
1,长度计算变为O1
2,防止内存溢出
3,减少内存分配次数
缺点
浪费内存空间

key-value中key是字符串,value可以是字符串,list,set中的元素也可能是SDS

Hash

hash结构,底层使用hash表,每一个redis数据库是一个大的hash表

命令

单个操作hget hset

127.0.0.1:6379> hset languages frontend html
(integer) 0
127.0.0.1:6379> hset languages backend java
(integer) 0
127.0.0.1:6379> hget languages frontend
"html"
127.0.0.1:6379> hget languages backend
"java"
127.0.0.1:6379>

批量操作hmget hmset

127.0.0.1:6379> hmset languages frontend javascript backend nodejs
OK
127.0.0.1:6379> hmget languages frontend backend
1) "javascript"
2) "nodejs"
127.0.0.1:6379> 

其他,hincr,hlen,hgetall,hdel

127.0.0.1:6379> hset languages count 2
(integer) 1
127.0.0.1:6379> HINCRBY languages count 1
(integer) 3
127.0.0.1:6379> hgetall languages
1) "frontend"
2) "javascript"
3) "backend"
4) "nodejs"
5) "count"
6) "3"
127.0.0.1:6379> hlen languages
(integer) 3
127.0.0.1:6379> hdel languages count
(integer) 1
127.0.0.1:6379> hgetall languages
1) "frontend"
2) "javascript"
3) "backend"
4) "nodejs"

实现

底层使用dic(hash表)实现,类似Java低版本HashTable。
Hash表
结构中先是数组,数组元素后面跟链表。

  • 查找时,先对key计算hash值,对应到数组的位置,逐一对比数组元素对应的链表的元素key,如果相等返回,否则返回空。
  • 新增时,先根据key查找位置,如果key存在更新,否则存储在该位置对应链表的最后面。
  • 更新时,先根据key查找,找到key,对key的value进行更新
  • 删除时,同样先根据key查找,找到key的位置,删除数据。

新增删除操作,会触发到hash表的rehash操作。

何时触发rehash操作?
负载因子=used数组元素/数组长度
1,负载因子大于等于1,且没有bgsave和reWriteAof操作时
2,负载因子大于等于5时
3,负载因子小于0.1是,开始收缩

rehash过程
hash表中维护两个子hash表h0和h1,同时仅有一个保存key value数据。当扩展h0时,根据元素数量,初始化子hash表h1长度,copy h0的元素到h1,完成后h0清空,准备作为下次rehash使用。

rehash时,新数组h1的长度
扩展时,长度为大于等于h0.used*2的2的最小次幂,如h0.used=5,则扩展后数组长度为16
收缩时,长度为大于等于h0.used的2的最小次幂,如长度为5,扩展后长度为8

渐进式rehash
原因:hash表非常庞大时,集中hash会导致服务不可用,所以redis使用渐进式的hash
从0开始,对h0表对应的数组元素及对应链表元素,逐一rehash到h1中
渐进式rehash过程中,h0表不添加,只在h1表添加,但是更新,删除,查找操作在h0,h1都要执行

Set

命令

sadd,新增集合元素
sdiff,两个集合的差集
scard,集合的元素数量
sismember,集合是否包含某元素

127.0.0.1:6379> sadd team1 xiaoming lily frank james lilei
(integer) 5
127.0.0.1:6379> sadd team2 xiaoli zhangsan lilei wangwu zhaosi
(integer) 5
127.0.0.1:6379> SDIFF team1 team2
1) "lily"
2) "frank"
3) "james"
4) "xiaoming"
127.0.0.1:6379> SISMEMBER team1 xiaoming
(integer) 1
127.0.0.1:6379> scard team1
(integer) 5

实现

redis set刷数据结构底层使用inset或者hash表数据结构存储数据

1. 存储的元素为整数
2. 元素个数不超过512个

是,采用inset存储,否则采用hash表存储,hash表这里不在赘述。
inset
inset数据结构类似于数组,inset数据结构包含三个属性:

  • encoding,编码方式,包括16位,32位或者64位整数
  • length,set中的元素个数,即content中的元素个数
  • content[],按照顺序存储整数

如下图,是一个inset结构:
在这里插入图片描述
下面通过新增、查找、删除的过程来了解inset
新增
元素a的新增过程

  1. 新增元素a,检测a的类型,和inset的encoding比较
  2. 如果需要字节数小于等于encoding,直接插入元素a,插入后a后面的元素向后移动,length加1
  3. 如果需要的字节数大于encoding,修改encoding,对contents重新进行内存分配
  4. 调整contents中的元素在内存中的排列方式,从旧编码调整到新编码
  5. 将元素a插入到contents对应位置,a后面的元素向后移动,length加1
    如下图,新增65537后,inset结构变为:
    在这里插入图片描述

查找
content中的元素是按照顺序存储的,则可以通过二分查找可以很快的找到元素,复杂度O(logn)

删除
查找待删除元素a的位置,删除a元素,a后面的元素向前移动,len-1

zset

命令

zset新增元素,zrem删除元素,zscore查看元素分值

127.0.0.1:6379> zadd scores 72 jackson
(integer) 1
127.0.0.1:6379> zrem scores angurla
(integer) 1
127.0.0.1:6379> zscore scores jackson
"72"

排序,zrange按照顺序指定区间查询元素,zrevrange按照倒序指定区间查询元素,zrank查询元素的排名,zrangebyscore,根据分数区间查询元素

127.0.0.1:6379> zrange scores 0 10
1) "jackson"
2) "lily"
3) "xiaoming"
4) "james"
127.0.0.1:6379> zrevrange scores 0 10
1) "james"
2) "xiaoming"
3) "lily"
4) "jackson"
127.0.0.1:6379> zrank scores xiaoming
(integer) 2
127.0.0.1:6379> zrangebyscore scores 90 100
1) "xiaoming"
2) "james"

查询zset中元素的数目

127.0.0.1:6379> zcard scores
(integer) 4

实现

使用skiplist或者ziplist实现

1,zset的元素小于128时
2,且zset的元素大小都小于64kb时,
时,使用ziplist,对于ziplist,见list数据结构的实现,其中元素个数和元素大小可通过redis.conf中的配置指定:
zset-max-ziplist-entries
zset-max-ziplist-value
不满足这两个条件时使用skiplist
在使用skiplist时,还需要一个hash表dic来辅助,即:

typedef struct zset{
     //跳跃表
     zskiplist *zsl;
     //字典
     dict *dice;
} zset;

skiplist,是多个双向链表组成的平衡查找数据结构,skiplist中的对象,包括分值和元素
hash表,key是zset的元素,value是zset元素对应的分值。

同时使用hash表和skiplist的原因是,
对于根据元素查询分值的场景,使用hash表,提供O(1)复杂度的查询
对于排序相关区间、排名查询场景,使用有序的跳跃表直接查询,复杂度O(log n)

skiplist
跳跃表是一个具有快速查找功能的链表数据结构,可以比拟二叉查找树,比bst,avl树,红黑树简单,跳跃表的结构如下图:
在这里插入图片描述

跳跃表包含:

  • 水平level,一个双向链表,数据存储在S0,其他水平leve辅助快速查找
  • 垂直tower,包括相同元素的连续列表,辅助查找
  • 位置,跳跃表中的每一个节点称为一个位置

每个位置,又有四个指针

  • after,同一水平level上的后一个位置,没有返回null
  • before,同一个水平level上的前一个位置,没有返回null
  • above,同一个垂直tower上的上一个位置,没有返回null
  • below,同一个垂直tower上的下一个位置,没有返回null

查找
从Header开始出发,依次after和below进行比较,比如查找27
1,首先找到S3的负无穷位置,和S3负无穷的after 31进行比较,小于31
2,到S3负无穷的below S2负无穷,和S2负无穷的after 18进行比较,大于18
3,和S2 18的after 31比较,小于31
4,到S1 18,和S1 18的after 31比较,小于31
5,到$0 18, 和S0 18的after 27比较,等于27,返回。
插入
如插入25节点
1,查找小于25的最大节点,找到18(根据查找流程)
2,打开指针,在S0插入25
3,random取随机值,如果random值为0,插入结束
4,random值为1,去25的before位置的above位置,即为P,打开P和P的after节点指针,插入25
5,再取随机值,递归step3和step4
插入25后结构变为:
在这里插入图片描述

删除
如删除31位置
1,查找31位置,删除31位置,设置31位置after和before的指针
2,查看31是否有above,above记为P,删除P
3,递归P
删除31后,跳跃表结构变为:
在这里插入图片描述

List

列表类型,底层使用双向链表数据结构实现。

命令

插入:rpush:右侧插入, lpush:左侧插入,llen:list长度

127.0.0.1:6379> lpush languages Java
(integer) 1
127.0.0.1:6379> rpush languages python
(integer) 2
127.0.0.1:6379> llen languages
(integer) 2

lpop key,左侧第一个出队列,rpop key:右侧第一个出队列

127.0.0.1:6379> lpop languages
"Java"
127.0.0.1:6379> rpop languages 
"python"
127.0.0.1:6379> llen languages
(integer) 0

lindex key index:遍历链表,找到第index个value,不删除元素
lrange key start end:拿start到end之间的,不删除元素
ltrim key start end: 和lrange类型,不过会删除没在范围内的value
左侧从0开始计数,右侧从-1开始计数

127.0.0.1:6379> llen languages
(integer) 5
127.0.0.1:6379> lindex languages 1
"python"
127.0.0.1:6379> lindex languages -1
"shell"
127.0.0.1:6379> lrange languages 0 5
1) "java"
2) "python"
3) "php"
4) "nodejs"
5) "shell"
127.0.0.1:6379> ltrim languages 0 3
OK
127.0.0.1:6379> lrange languages 0 5
1) "java"
2) "python"
3) "php"
4) "nodejs"

没有lrange对应的

实现

redis3.2之后,list底层使用quicklist实现,quicklist是一个双向链表,链表的节点是一个ziplist。
ziplist
一系列特殊编码的连续内存块组成的顺序存储结构,类似于数组,与数组不同的是,元素的长度是可变的。采用连续的空间避免了内存碎片,采用可变长度、特殊编码压缩了数据占用的空间。
ziplist结构如下图:
在这里插入图片描述
ziplist中普通的entry结构如下图,分为四部分
在这里插入图片描述

上图第一部分值恒为254,当上一节点的长度小于254时,省去prevlength字段,长度存在第一部分中,这时entry的结构变为:
在这里插入图片描述
encoding的长度为8位,其中高2位用来区分整数节点和字符串节点(高2位为11时是整数节点,高两位为01,10,00为字符串节点),低6位用来区分整数节点的类型(如32位整数,64位整数等),当节点为字符串时,encoding中包含字符串的长度,最多时5个字节,4个字节用来表示data字符串长度。

这样就可以计算出entry的长度,知道起始节点,本entry的字节长度,上一个entry的字节长度,从而可以像双向链表一样遍历ziplist中的节点了。

优点:提高内存利用率
缺点:新增删除时,连续空间扩展或缩减内存,损失性能。

quicklist
简单来说,quicklist是节点都是ziplist的双向链表。结构如图:
在这里插入图片描述
其中,quicklist中:

  • head,指向头部节点quicklistNode
  • tail,执行尾部节点quicklistNode
  • count,所有ziplist中entry总数
  • len,ziplist总数
  • fill,ziplist的大小,在redis.conf中通过
  • list-max-ziplist-size指定 compress,ziplist前后跳过压缩的entry个数

quicklistNode中:

  • prev,指向前一个quicklistNode,维护双向链表结构
  • next,执行下一个quicklistNode,维护双向链表结构
  • zl,zip未压缩时指向ziplist,ziplist压缩时指向quicklistLFZ对象
  • sz,zl对应ziplist未压缩的大小,字节数
  • count,ziplist的entry总数
  • encoding,压缩算法,0未压缩,1采用LFZ算法压缩

quicklistLFZ的结构:
sz,压缩后的ziplist大小,字节数
compress,压缩后的ziplist对应的字节数组
list实现

  • 双向链表存储效率不高,需要额外存储两个指针,且数据量大时,因为不是连续的空间,所以会产生大量的内存碎片,内存利用率不高。
  • ziplist可以提高内存利用率,但是当新增删除节点时,需要动态的调整连续内存空间的大小,而且涉及到数据的复制,且当数据量大时,会产生小的内存区域,导致不够去分配新的ziplist。

所以redis使用quicklist和ziplist折中的方式,先是quickList双向链表,链表的节点是ziplist,兼顾内存利用率和读写效率。
这时,在redis.conf中指定如下参数:

  • list-max-ziplist-size,list中ziplist的大小,大于0时表示ziplist的元素个数,小于0表示ziplist占用空间,如下
    -5: max size: 64 Kb,每个quicklist中的ziplist的空间不超过64Kb,一般业务中不建议
    -4: max size: 32 Kb,每个quicklist中的ziplist的空间不超过32Kb,一般业务中不建议
    -3: max size: 16 Kb,每个quicklist中的ziplist的空间不超过16Kb,一般业务中不建议
    -2: max size: 8 Kb,每个quicklist中的ziplist的空间不超过8Kb,一般业务中适用
    -1: max size: 4 Kb,每个quicklist中的ziplist的空间不超过4Kb,一般业务中适用
  • list-compress-depth,quicklist压缩深度,指每一个quicklist的ziplist的前后几个节点不压缩,从而支持快速的读写。0:全部不压缩,1:前后各一个元素不压缩,2:前后各两个元素不压缩

高级数据结构

bitmap

bitmap就是用最小的二进制bit位来表示状态,如1bit可以取值0或1,来表示一个元素的两个状态
8bit,可以表示8个元素的两个状态
32bit,可以表示32个元素的两个状态

bitmap可以以很少的内存空间标识大量的元素的状态

redis支持bitmap,如:
设置key的offset位为0或1,setbit key offset value
获取key的offset位的值,getbit key offset

127.0.0.1:6379>  setbit activelist 10 1
(integer) 0
127.0.0.1:6379> getbit activelist 10
(integer) 1

bitcount,计算bitmap中bit位为1的位数,bitop,对指定key的值进行位运算

127.0.0.1:6379> bitcount activelist
(integer) 2
127.0.0.1:6379> setbit expirelist 23 1
(integer) 0
127.0.0.1:6379> setbit expirelist 2 1
(integer) 0
127.0.0.1:6379> setbit expirelist 4 1
(integer) 0
127.0.0.1:6379> BITOP or activelist expirelist
(integer) 3

HyperLogLog

计算集合的基数,即集合不重复的元素个数,近似计算。
HyperLogLog数据结构命令包括:

PADD 集合中添加元素
PFMERGE 合并指定key中的元素
PFCOUNT 计算指定key对应集合的不重复元素个数

如:

127.0.0.1:6379> pfadd fruit apple banana orange menlon
(integer) 1
127.0.0.1:6379> pfcount fruit
(integer) 4
127.0.0.1:6379> pfadd fruit1 tomato pear apple banana
(integer) 1
127.0.0.1:6379> pfcount fruit1
(integer) 4
127.0.0.1:6379> PFMERGE fruit fruit1
OK
127.0.0.1:6379> 
127.0.0.1:6379> 
127.0.0.1:6379> pfcount fruit
(integer) 6

Bloom Filter

布隆过滤器,近似的判断元素在大量的集合元素中是否存在。

需要安装redis插件,然后使用。

GeoHash

计算距离,使用zset实现,
命令:

geoadd,添加经纬度和名称
zrem,删除名称以及名称对应经纬度
geodist,计算两个名称对应之间的距离
geopos, 获取名称对应经纬度
127.0.0.1:6379> geoadd geos 118.12131 23.1234 xian
(integer) 1
127.0.0.1:6379> geoadd geos 120.22345 24.22332 beijing
(integer) 1
127.0.0.1:6379> geoadd geos 118.87321 21.32434 guangzhou
(integer) 1
127.0.0.1:6379> geodist geos xian beijing km
"246.6149"
127.0.0.1:6379> zrem geos guangzhou
(integer) 1
127.0.0.1:6379> GEOPOS geos xian
1) 1) "118.12131196260452271"
    2) "23.12340059881565679"

georadiusbymember,计算zset中,指定地点,指定距离范围内的经纬度位置,如计算xian经纬度100km范围内的位置,按照距离倒序排序,前5条数据

127.0.0.1:6379> GEORADIUSBYMEMBER geos xian 100 km withcoord withdist withhash count 5 desc
1) 1) "zhengzhou"
   2) "24.0758"
   3) (integer) 4048697130231704
   4) 1) "118.32323938608169556"
      2) "23.01231897872878562"
2) 1) "xianyang"
   2) "1.2569"
   3) (integer) 4047232217341926
   4) 1) "118.12121003866195679"
      2) "23.11210081188732346"
3) 1) "xian"
   2) "0.0000"
   3) (integer) 4047232234370576
   4) 1) "118.12131196260452271"
      2) "23.12340059881565679"

georadius,和georadiusbymember类似,georadius可以自定义经纬度,如计算指定经纬度119.9999 23.3333,150km范围内,按照距离倒序排序,前10条数据

127.0.0.1:6379> GEORADIUS geos 119.9999 23.3333 150 km withcoord withdist withhash count 10 desc
1) 1) "beijing"
   2) "101.5749"
   3) (integer) 4049655868691915
   4) 1) "120.22345036268234253"
      2) "24.22332003342035733"

stream

redis 5.0新增数据结构,用于消息队列,提供了消息的持久化和主备复制功能,消息有顺序,并且记录客户端的访问位置。

redis stream底层是一个消息链表,按照顺序记录消息,stream是一个只能在后面追加的数据结构(append only)

stream在命令方面和list类似,比list强大复杂。

写消息
XADD,添加消息,消息是一个包含多个field和value的结构,类似hash,如:

127.0.0.1:6379> xadd message * a 1 b 2
"1622034850208-0"
127.0.0.1:6379> xadd message * fff fff
"1622034862928-0"

读消息
xrange
可以使用:

xrange读消息列表,指定两个id区间,查询id区间内的消息,-表示最小id,+表示最大id,在消息id为自动生成的消息ID时,也可指定为
xrevrange倒序查看消息列表,
xlen查看消息数目。
127.0.0.1:6379> xrange message - +
1) 1) "1622034850208-0"
   2) 1) "a"
      2) "1"
      3) "b"
      4) "2"
2) 1) "1622034862928-0"
   2) 1) "fff"
      2) "fff"
127.0.0.1:6379> xlen message
(integer) 2

xrange的复杂度是O(log N)

xread
xread读取最新消息,当不使用范围来读消息时,使用xread命令来订阅最新的消息。
看起来比较像订阅redis Pub/Sub和redis blocking list,不一样的地方在于

1. stream可以有多个客户端消费消息,每一个新消息会被分发到每一个客户端去消费,这一点和blocking list不同,blocking list的每个客户端得到不同消息,stream这一点和pub/sub相似
2. 在pub/sub中,redis消息不会存储,在分发之后就丢弃了,blocking list也是在客户端获取数据后从list删除消息,stream不同的是,stream不会删除消息,所有消息无期限的追加到stream中,除非用户删除消息。不同的消费者通过记录最后一条消息的ID来知道消息对于他是不是最新消息。
3.Stream可以通过Consumer Group来控制消费,而pub/sub和blocking list不支持。同一个stream的不同consumer group,特性包括,消费消息后的ack确认,检查待消费消息能力,未处理消息声明,单独的客户端只能看到自己消费的历史记录。

xread还支持阻塞的方式读消息,如

127.0.0.1:6379> xread block 20000 streams messages 1622602604435-0
1) 1) "messages"
   2) 1) 1) "1622602636399-0"
         2) 1) "a"
            2) "13"
            3) "b"
            4) "14"
(6.65s

consumer group读消息
类似于消息队列,加入同一个consumer group的consumer可以消费不同的消息,避免重复消费。如:

# 创建consumer group,$指定从现在开始的最新消息,MKSTREAM指定当没有名称为messages时创建messages
127.0.0.1:6379> xgroup create messages group1 $ MKSTREAM
OK
127.0.0.1:6379> xgroup create messages group2 $ MKSTREAM
OK
127.0.0.1:6379> xgroup create messages group3 $
OK
# 创建消息
127.0.0.1:6379> xadd messages * name apple
"1622612978575-0"
127.0.0.1:6379> xadd messages * name banana
"1622612982471-0"
127.0.0.1:6379> xadd messages * name orange
"1622612989308-0"
127.0.0.1:6379> xadd messages * name pear
"1622612991939-0"
127.0.0.1:6379> xadd messages * name tomato
# 消费消息
127.0.0.1:6379> XREADGROUP group group1 consumer1 count 1 streams messages >
1) 1) "messages"
   2) 1) 1) "1622612978575-0"
         2) 1) "name"
            2) "apple"
127.0.0.1:6379> XREADGROUP group group1 consumer1 count 1 streams messages >
1) 1) "messages"
   2) 1) 1) "1622612982471-0"
         2) 1) "name"
            2) "banana"
127.0.0.1:6379> XREADGROUP group group1 consumer2 count 1 streams messages >
1) 1) "messages"
   2) 1) 1) "1622612989308-0"
         2) 1) "name"
            2) "orange"
127.0.0.1:6379> XREADGROUP group group2 consumer2 count 1 streams messages >
1) 1) "messages"
   2) 1) 1) "1622612978575-0"
         2) 1) "name"
            2) "apple"
127.0.0.1:6379> XREADGROUP group group2 consumer1 count 1 streams messages >
1) 1) "messages"
   2) 1) 1) "1622612982471-0"
         2) 1) "name"
            2) "banana"
127.0.0.1:6379> XREADGROUP group group3 consumer1 count 1 streams messages >
1) 1) "messages"
   2) 1) 1) "1622612978575-0"
         2) 1) "name"
            2) "apple"

可以看到,一个消息只会被一个consumer group中的消费者消费一次。但是可以被多个consumer group分别消费。

原理

redis object

redis采用redisobject来统一存储数据,无论key-value键值对是哪一种数据结构,redis都首先使用redisObject来存储。
redisObject中.
redisObject中包括:

type,数据类型,包括五种数据类型,String,Hash,list,set,zset
encoding,数据编码,包括hashtable,String,long,ziplist,quicklist,inset,skiplist
pr,数据指针,指向真正的数据存储地址
lru,redis内存占满后,内存回收策略
refcount,引用计数,Redis采用“引用计数法”来管理对象,使用incrRefCount来增加对象的引用计数值,使用decrRefCount来减少对象的引用计数值,如果对象的引用计数值减为0则销毁之。

在redis存入一个k-v数据项,是在redis的hash表中存入了一个k为key,redisObject为value的键值对。

redis过期策略

expire,给key设置生存时间,超过生存时间后,key超过生存时间后,redis会自动删除key。

expire的特点:

  • expire的超时时间会在删除,覆盖value时请出去,如GET,SET,GETSET,*STORE命令会清除key的超时时间
  • HSET,lpush,INCR等修改value值的操作,不会清除超时时间
  • 可以使用PERSIST清除超时时间
  • 使用renam重新命名key,超时时间保持不变
  • 过期精度,2.6以前0-1秒误差,2.6以后0-1毫秒误差

redis的这个机制是如何实现的呢?
key设置expire超时时间后,会存储key对应的毫秒时间戳
被动过期和主动过期
被动过期
客户端尝试访问key时,先检查key的时间戳是否过期,如果过期返回空,没有过期返回key-value数据。
缺点:key过期后,没有再被访问,会导致key一直存在,浪费内存。
主动过期
redis每秒执行10次以下操作:
- step1,从具有超时过期的key中随机测试20个,删除过期key。
- step2,如果超过25%的key过期,重复step1
这样就保证了,过期的key最多不超过25%,1/4

被动过期是典型的空间换性能,主动过期是性能换空间。

从库过期策略

从库不主动过期,而是当主库key过期时,生成del命令,写入aof文件,从库从主库aof同步del命令,来删除过期key。

当slave节点连接到master时,不独立执行过期操作。但是slave还是会保存数据的状态,如过期时间,当slave被选举称为master时,又需要独立的执行过期操作。

内存数据淘汰

Redis存储用户数据,最多会分配maxmemory配置大小的内存,maxmemory在redis.conf中配置(可以少量超过)。

回收内存
删除过期的key,回收内存,存储新的用户数据。
redis达到maxmemory时,如何删除key,回收内存,有以下几种选择:
volatile-lru -> 根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
allkeys-lru -> 根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
volatile-lfu -> 根据LFU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
allkeys-lfu -> 根据LFU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
volatile-random -> 随机删除过期键,直到腾出足够空间为止。
allkeys-random -> 随机删除所有键,直到腾出足够空间为止。
volatile-ttl -> 根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
noeviction -> 不会删除任何数据,拒绝所有写入操作并返 回客户端错误信息,此 时Redis只响应读操作。

redis默认的策略就是noeviction:

maxmemory-policy noeviction

如果想要配置其他策略话,需要在配置文件中修改这个配置。

回收机制

  1. 客户端执行命令添加数据。
  2. redis计算需要的内存,内存不够时,通过回收策略回收内存。
  3. 回收后有足够内存,执行命令,如果内存不足,返回错误。

释放内存
redis删除key后,redis回收key占用内存,这些回收的内存可以重复给其他key使用。
而释放内存是指,redis回收内存后,释放给操作系统,操作系统可以重新分配给其他进程。

redis不会总是将内存释放给操作系统。
因为基础分配器无法轻松释放内存。如:通常大多数已删除key与仍然存在的其他未删除key分配在相同的内存页中。

所以:

  1. redis的maxmemory就需要根据峰值来配置,如高峰时需要10GB而平常需要5GB,这时候就需要配置10GB了
  2. redis虽然不会释放回收的内存,但是回收的内存可以重复利用,新的数据存储在回收的内存中。

LRU eviction

Redis用作缓存时,指定一定大小的内存,在缓存新数据时,有时需要淘汰掉旧的不用的数据,数据淘汰是缓存软件的一般特性,LRU就是一种数据淘汰策略。

LRU算法根据数据的历史访问记录来淘汰数据,认为数据如果最近被访问过,那么将来被访问的几率更高。

Least Recently Used 近期最少使用算法,Redis是近似的LRU算法,Redis会基于server.maxmemory_samples配置选取固定数目的key,然后比较它们的lru访问时间,然后淘汰最近最久没有访问的key,maxmemory_samples的值越大,Redis的近似LRU算法就越接近于严格LRU算法,但是相应消耗也变高。所以,频繁的进行这种内存回收是会降低redis性能的,主要是查找回收节点和删除需要回收节点的开销。

最简单的LRU算法是,维护一个链表

  1. 新数据插入链表头部
  2. 当缓存命中时,将数据插入链表头部
  3. 淘汰数据时,从尾部淘汰数据

如下图:
在这里插入图片描述

Approximated LRU algorithm
redis使用近似LRU算法,redis的LRU算法不是精确的实现,兼顾了性能和算法,redis LRU不能总是选择最应该被淘汰的key,而是对所有的key进行采样,在采样的key中选择要被淘汰的key。

Redis LRU算法可以通过调整样本数量来控制算法的精度,即:maxmemory-samples 5,值越大精度越高。

LFU eviction

redis 4.0引入,最小频率策略,根据key被访问的频率,认为数据被访问的频率越高,那么将来访问的几率更高。可以为redis提供更好的缓存命中率。

Least Frequently Used最不经常使用算法,跟踪key的使用频率,删除使用频率低的key。

redis的LFU也是近似的的算法,使用概率计数器counter(Morris计数器),使用对象的几位来估计对象的访问频率,结合时间,计数器随着时间的推移而减少。 LFU近似算法也是使用采样的方式。

和LRU不同的是,LFU有一些可调的参数:

lfu-log-factor可以调整计数器counter的增长速度,lfu-log-factor越大,counter增长的越慢。
lfu-decay-time是一个以分钟为单位的数值,可以调整counter的减少速度

counter并不是简单的访问一次就+1,而是采用了一个0-1之间的p因子控制增长。counter最大值为255。取一个0-1之间的随机数r与p比较,当r<p时,才增加counter,这和比特币中控制产出的策略类似。p取决于当前counter值与lfu_log_factor因子,counter值与lfu_log_factor因子越大,p越小,r<p的概率也越小,counter增长的概率也就越小。

持久化

RDB

RDB(Redis database)
按照指定的时间间隔,创建某一时间点的数据快照

优点:
数据文件紧凑,体积小,恢复快
备份灵活,指定时间点,定时fork子进程备份,redis服务正常处理客户端请求
缺点:
丢数据,本次备份时间点和下次备份时间点之间会丢失。
当数据量大事耗服务器性能

AOF

AOF(Append Only File)
AOF redis持久记录Redis服务接收到的每个写操作,记录的日志在重复器重启时重新播放,来恢复数据集。当日值太大时,Redis可以在后台重写日志。

优点
数据备份全,丢数据少。
AOF文件大是自动重写。
缺点
体积大,备份慢,数据恢复慢。
fsync写入磁盘策略,指定为always或everysec时,耗服务器性能,支持QPS低。

rewrite AOF

AOF文件不断的写入,达到需要rewrite的阈值是或者受到bgrewriteaof命令时,进行rewrite,rewrite流程如下:

  1. fork子进程进行rewrite操作,新建aof文件,将redis库中的key转为redis命令,写入到新建的aof文件
  2. 子进程rewrite过程中,父进程继续接受客户端请求,但是命令写入到aof_rewrite_buf_blocks中
  3. 子进程rewrite完成后,将aof_rewrite_buf_blocks中的内容写入到新的aof文件,同时切换aof文件为新的aof文件

RDB+AOF混合持久化

可以同时使用RDB和AOF,这种情况下,当Redis重启时,使用AOF恢复数据,因为AOF数据更全。

推荐同时使用AOF和RDB,AOF保证数据不丢,RDB用作冷备,当AOF不可用或者损坏时使用RDB进行备份。

集群

redis的集群策略包括主从、哨兵模式、集群模式和codis几种模式

主从

redis的主从模式非常简单,slave为master服务的复制品,当连接断开时,slave节点会自动重新连接、复制数据。
同步机制
复制使用三种机制:
: slave和master保持正常连接时,master发送命令流到slave,slave执行命令流保持和master同步,包括写,更新,key过期,key回收,删除等操作命令,都会被发送到slave。
: 当连接因为网络分区等原因中断时,slave自动发起重连,并尝试进行分区同步:尝试获取因为连接断开而丢失的命令。
: 尝试分区同步失败后,slave请求全量同步。master启动子进程,创建所有数据快照,发送到slave,并且继续发送数据修改的命令流。
同步异步
默认情况下,redis使用异步复制,master发送命令后,不关心命令的执行结果。
redis同样支持同步复制,master发送命令后,确保命令执行成功。

客户端可以使用wait命令,请求数据同步复制,wait可以确保多个redis slave中,有指定数量实例确认复制完成。这样并不能保证redis变为一个强CP系统,还需要再持久化时配置化策略。
特点

  • redis使用异步复制,同时slave异步确认复制的数据量。
  • 一个master可以有多个slave
  • slave可以接受其他slave的连接,除了多个slave连接到master的模式外,还可以是多个slave通过其他slave最终连接到master的级联模式。
  • redis replication在master端是非阻塞的,当其他slave在复制、分区复制或者全量复制阶段,redis继续处理请求。
  • redis replication在slave端也是非阻塞的,在其他slave复制、分区复制或全量复制阶段,slave也可以继续处理请求。需要在配置中指定,也可以指定为在复制命令流异常时,给所有请求返回错误
  • 复制可以用于读操作的水平扩展,也可以提高redis数据的安全性,高可用性。
  • 可以转移master的写磁盘操作到slave,slave来写aof日志,定时rdb保存数据。提高master性能。
    PS:但是转移持久化到slave时,不要配置master自动启动,否则master启动是空数据,slave同步空数据会导致slave清空,slave的aof也被清空。

Redis Replication原理

__sync__全量同步的序列图如下图
在这里插入图片描述
其中第4,5,8是在有客户端写入的情况下的序列图,如果么有客户端写入,那写入的流程变为1,2,3,6,8

psync,部分同步,当服务器出现短暂网络分区后,slave会发起psync部分同步活动,即部分同步。自redis2.8以后,redis开始支持部分同步。

redis为被发送的同步命令流创建缓冲区(in-memory backlog),同步命令流缓存在缓冲区内,并且master和slave都记录一个偏移量offset和master服务的replication ID(replication ID是随机字符串),replicaitonID标记master信息,而offset标记同步命令流的位置。

slave重新连接后,向master服务器请求psync部分同步,并且发送slave自己记录replicaiton ID 和 offset偏移量。

master的处理逻辑为:

  • 如果slave记录的replication ID和master的replication ID相同,并且slave记录的偏移量还在master的缓冲区内,master根据slave的偏移量,发送对应的命令流到slave,slave同步后同步工作继续进行
  • 如果slave记录的replication ID和master的replication ID不同,重新全量同步。

replicationID和offset
如果两个redis服务有相同的replicationID和offset,那么两个redis服务的数据相同,一个redis服务有两个Replicaiton ID,ReplicationID和Second ReplicationID,当slave变为master时,原来的ReplicaitonID变为second ReplicaitonID,并且重新生成ReplicationID。
保存原来的replicaitonID为Second ReplicationID是为了可以继续和其他slave节点进行部分同步,从而提高性能,因为其他slave几点记录的replicationId是不变的
生成新的ReplciaitonID是因为,原master发生网络分区的情况下,还会认为自己是master,就会违反两个redis服务有相同的replicationID和offset,那么这两个redis服务的数据相同的原则

搭建redis 主从服务
根据官方文档,首先安装redis,下载redis,见redis下载
下载完成后,解压,进入安装目录内。

# make
# make install

通过redis-server指定配置文件启动redis,这样就启动了一个redis服务

# redis-server redis.conf

下面配置主从服务,只需要修改从服务器的配置

replicaof 192.168.10.41 6379

重新启动从服务器就完成了主从服务的配置

优点:易于搭建,可以实现redis读写分离,减轻master压力,对master数据备份,可支持手动切换master
缺点:master节点存在单点故障,不能自动故障恢复

哨兵模式

redis哨兵提供了高可用,可以保证redis在发生某些故障是继续提供服务。

redis Sentinel 还提供了监控,通知,配置管理等服务,具体如下:
- 监控。Sentinel检查master和slave实例是否正常运行。
- 通知。Sentinel可以通过API通知系统管理员或者其他程序,它所监控的Redis服务发生异常
- 自动恢复。如果master不能正常工作,Sentinel启动故障转移程序,将其中一个slave变为master,并让其他slave连接新的master,使用redis服务的应用也会被通知使用新的redis IP地址和端口
- 配置管理。Sentinel充当redis的服务发现,客户端连接到Sentinels,询问Redis集群master的地址,如果发生故障转移,Sentinels将报告新地址。

搭建

redis安装之后,同时会安装redis-sentinel命令,用于sentinel的启动,所以只需要修改sentinel的配置文件,sentinel的配置文件是redis安装目录下的sentinel.conf文件,其中包括端口,是否后台运行等和redis相似的通用配置,这里不在赘述。下面是关键配置:

# sentinel监控 名称为 mymaster的redis master服务,redis master服务的主机为192.168.10.141 端口为6379,并且quorum2个sentinel同意时,改变master的状态,进行故障转移。不需要指定slave,sentinel自动发现slave
sentinel monitor mymaster 192.168.10.141 6379 2
# 指定时间内redis master不可达(ping没有反应或返回error),sentinel节点认为redis master服务不可用
sentinel down-after-milliseconds mymaster 6000
# 配置对于故障转移后的新master,有多少个slave可以同时重新配置使用。配置越小故障转移花费时间越长,
sentinel parallel-syncs mymaster 2

sentinel用来解决redis的单点故障问题,所以本身不能有单点故障问题,所以一般会启动三个sentinel。
并且一个sentinel可以监控多个redis主从集群,来进行故障转移,但是多个redis集群的 master名称不同。

  • quorum是就master不可访问达成共识的sentinel的数量,之后主机真正标记为故障
  • quorum配置的数量(上面的例子为2)只是用来检测异常,在真正的故障转移时,其中一个Sentinel作为Leader来执行故障转移,Leader是超过半数sentinel节点同意选举出来的

所以,Sentinel在有一半节点不可用时,不能进行redis故障转移。

同时我们需要配置master的requirepass到sentinel中,并且slave的requirepass和master相同,

sentinel auth-pass mymaster redis

配置完成后,启动sentine

# redis-sentinel sentinel.conf

启动之后进行简单测试:

  1. 登录sentinel,查看sentinel信息
# redis-cli -p 26379
127.0.0.1:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=192.168.10.141:6379,slaves=2,sentinels=2
  1. 查看master信息
127.0.0.1:26379> sentinel masters
1)  1) "name"
    2) "mymaster"
    3) "ip"
    4) "192.168.10.141"
    5) "port"
    6) "6379"
    7) "runid"
    8) "3388d383311f148d589244ecdf17fd8a6be44938"
。。。 。。。
  1. 查看slaves信息
127.0.0.1:26379> sentinel slaves mymaster
1)  1) "name"
    2) "192.168.10.142:6379"
    3) "ip"
    4) "192.168.10.142"
    ... ...
2)  1) "name"
    2) "192.168.10.143:6379"
    3) "ip"
    4) "192.168.10.143"
    5) "port"
    6) "6379"    
    ......
  1. sentinel failover命令手动发起故障转移
127.0.0.1:26379> sentinel failover mymaster
OK
  1. 三个sentinel每秒都向master发起ping信息,可以在redis master订阅_sentinel_:hello topic查看到,如下:
127.0.0.1:6379> subscribe __sentinel__:hello
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "__sentinel__:hello"
3) (integer) 1
1) "message"
2) "__sentinel__:hello"
3) "192.168.10.143,26379,a7dbc23f1ef41b6c1d5785e89399c970179e5d49,1,mymaster,192.168.10.141,6379,1"
1) "message"
... ...
  1. 订阅sentinel消息
    redis集群发生时间,或者sentinel发生故障转移时,sentinel会在特定的topic发布消息,客户端通过订阅消息获取集群相关信息,特别是master的切换信息,来进行应用连接redis的切换,如:
127.0.0.1:26379> subscribe +switch-master
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "+switch-master"
3) (integer) 1
1) "message"
2) "+switch-master"
3) "mymaster 192.168.10.142 6379 192.168.10.141 6379"

原理

SDOWN和ODOWN

  • SDOWN,主观下线,单个sentinel实例对redis master做出下线判断,不一定正确。
  • ODOWN,客观下线,多个sentinel实例对redis master做出下线判断,认证redis master下线

故障转移前的决策过程:

  1. sentinel任意节点每一秒给master发送ping心跳,如果redis master没有在master-down-after-milliseconds配置的时间内给出有效回复,sentinel节点认为redis master为SDOWN状态
  2. Sentinel 在给定的时间范围内, 从其他 Sentinel 那里接收到了足够数量(根据配置)的redis master下线报告, 那么 Sentinel 就会将主服务器的状态从主观下线改变为客观下线
  3. 报告SDOWN状态的sentinel将被选举为Leader来执行redis master的故障转移。

如图:
在这里插入图片描述

ps:(只有master节点的状态能达到ODOWN)

auto discovery

sentinel 和 redis replica节点可以自动发现,sentinel和其他sentinel保持联系,但是不需要配置所有哨兵的列表,哨兵可以通过redis master的PUB/SUB功能自动发现其他sentinel节点。

同样,Sentinel也会使用info replica命令在master节点发现master的replica列表。

  1. 每个sentinel节点向redis的master和replica节点的__sentinel__:hello sub/pub主题发送ping消息,包含sentinel的标识、IP、端口信息
  2. 每个sentinel又订阅redis的master和replica节点的__sentinel__:hello sub/pub主题,查找其他sentinel节点,发现是添加到对应redis master的sentinel列表
  3. ping消息还包括了redis master的信息,接收方sentinel还会对比接受到的master信息,如果比自己新,会更新自己保存的master信息
  4. sentinel将发现的sentinel与保存的sentinel对比,如果存在(unid 或相同地址(IP 和端口))相同,则用发现的sentinel替代原sentinel

网络分区时的一致性
在这种架构下,可能有三种分区:
1,客户端
2,redis实例
3,sentinel实例
如在下面三台机器的集群中,每台服务器部署Redis和Sentinel,Redis1为Master,当未发生分区时,Client A连接Redis1读写数据,当A发生网络分区时,Sentinel进行故障转移,Redis2 为新的Master,这是Client B连接Redis2正常读写数据。但是当Client A和Redis1的网络正常时,Client A继续向Redis1正常读写数据。
但是当网络恢复时,Redis1会作为redis2的slave存在,原来分区的时间内,客户端A写的数据会全部丢失。 在这里插入图片描述
当Redis作为缓存时,这种情况是正常存在的。
但是当Redis作为数据存储时,要避免出现丢失数据的情况。

可以通过以下两个配置来避免上面数据丢失的情况。

# 至少有一个slave写数据成功,否则不接受write请求
min-replicas-to-write 1
# replica超过10秒未确认写数据成功,认为写数据失败
min-replicas-max-lag 10

应用

优点:实现了redis的高可用,自动故障转移
缺点:

  • replica节点通常作为备份节点,不提供服务,浪费资源
  • replica节点不支持故障转移

redis cluster

redis cluster,数据自动跨多个节点进行分片。发生分区时,Redis Cluster提供一定程度的可用性,但是大量的节点不可用时,redis cluster不可用。

集群模式下redis节点需要两个端口,如:6379服务客户端,16379服务于集群节点之间通信。用docker搭建redis cluster时,需要使用host网络模式。

数据分片

redis cluster不使用一致性hash进行数据分片,而是使用hash槽的方式,redis cluster共有16384个hash槽,每个hash槽中包含多个key-value键值对,根据集群数量不同,每个集群节点上又分布多个hash槽,如三个节点(RedisA,RedisB,RedisC)的集群上,RedisA上分布0-5500的hash槽,RedisB上分布5501——11000的hash槽,RedisC上分区11001—16384的hash槽。一个key属于哪个hash槽,根据key使用CRC16校验算法计算。

动态的新增节点或者删除节点时,只需要移动节点上的hash槽,而不需要rehash,移动大部分的key。

master slave模式
redis cluster中有多个master,如上面的例子(RedisA,RedisB,RedisC)分别负责一部分的hash槽,当某一节点挂掉,如RedisB挂了,Redis就不能提供服务了,因为Redis Cluster不能对5501—11000这些hash槽进行操作了。

这时引入了master-slave模式,即每个Master节点(Redis A,RedisB,RedisC),分别又有Slave节点(Redis A1,RedisB1,RedisC1),slave节点从对应的master节点复制数据,当master节点挂掉时,redis cluster将slave提升为master,继续提供服务,当master和slave同时挂掉是,redis cluster不可用。

一致性

Redis Cluster不能保证强一致性,某些情况下Redis Cluster会丢失某些客户端写入数据,主要是因为异步的同步机制。如:客户端向RedisB写入数据,RedisB向客户端回复成功,但是此时还没有复制到RedisB的slave RedisB1,RedisB崩溃,RedisB1被提升为新的master,客户端写入数据丢失。

可以通过配置同步复制来提高一致性,但是会损失性能,一般需要取一个权衡。

再需要支持同步写入的情况下,如作为数据库使用时,通过wait命令实现,降低了数据丢失的可能性,但是redis没有实现强一致性,在多个slave的场景下,选举没有同步到数据的slave节点为master时,还是会丢失数据。

在发生分区时,客户端和master在同一个网络分区是,客户端还会向原master写入数据,当集群选举出新的master时,写入原master的数据丢失。可以通过配置cluster超时时间来减少网络分区时,可以继续写入的时间,超过超时时间,分区的master将不再接受客户端写入。

配置参数

  • cluster-enabled,是否开启cluster模式
  • cluster-config-file,redis cluster用来写入记录集群其他节点,其他节点状态等信息的文件,非用户配置文件。
  • cluster-node-timeout,Redis集群节点不可用时,但不被视为失败的最长时间。如果master不可用超过配置的时间,则不再接受客户端写请求。
  • cluster-slave-validity-factor,slave有效因子,设置为0,slave始终任务自己有效,尝试对master进行故障转移,无论master和slave断开的时间长短,如果值为正数,则最大断开时间为 有效因子*超时时间,如果节点为从节点,则最大断开时间后,开始故障转移。如果没有进行故障转移会导致redis cluster不可用。
  • cluster-migration-barrier,master保持连接的最小slave数量,redis-cluster根据此配置 自动将slave迁移到另一个master
  • cluster-require-full-coverage,默认情况下设置为yes,如果某个hash槽没有节点覆盖,集群停止服务,设置为no,则又hash槽没有覆盖到节点是,集群继续服务
  • cluster-allow-reads-when-down,如果设置为no,默认情况下,集群标记为失败时,redis不提供任何数据读写服务,设置为yes时,继续提供读数据服务,适用于高可用服务。

搭建redis cluster

1, 启动redis实例
现在有三台服务器,192.168.10.141,192.168.10.142,192.168.10.143每台服务器上启动两个redis实例,三个master三个slave,每台服务器上的redis实例分别使用端口7001和7002,配置如下:

port 7001或7002
cluster-enable yes
cluster-config-file node.conf
cluster-node-timeout 5000
appendonly yes

2, 创建cluster
redis5以上使用redis-cli创建redis-cluster

redis-cli --cluster create 192.168.10.141:7001 192.168.10.141:7002 192.168.10.142:7001 192.168.10.142:7002 192.168.10.143:7001 192.168.10.143:7002 --cluster-replicas 1
  • –cluster create,指定创建新的redis cluster
  • –cluster-replicas,指定每个master一个slave
  • 中间的ip:port,是指定用来创建redis cluster的redis实例地址(ip和端口号)

创建之后可以看到 all 16384 slots coverd字样,cluster创建成功

redis3或redis4使用redis-trib.rb创建redis-cluster

redis-trib.rb create --replicas 192.168.10.141:7001 192.168.10.141:7002 192.168.10.142:7001 192.168.10.142:7002 192.168.10.143:7001 192.168.10.143:7002

这样加就搭建了一个如下图的redis-cluster
在这里插入图片描述

也可以使用redis-cluster脚本创建redis cluster,在redis安装目录下的util/create-cluster目录下。具体可阅读redis-cluster目录下README文件查看帮助。

测试redis cluster

# redis-cli -c -p 7001 -a redis
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:7001> set a b
-> Redirected to slot [15495] located at 192.168.10.143:7001
OK
192.168.10.143:7001> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:5
cluster_stats_messages_ping_sent:1838
cluster_stats_messages_pong_sent:1832
cluster_stats_messages_meet_sent:1
cluster_stats_messages_sent:3671
cluster_stats_messages_ping_received:1832
cluster_stats_messages_pong_received:1839
cluster_stats_messages_received:3671

上面的例子,在192.168.10.141:7001 master节点上存入键值对(a:b),redis-cli将写请求重定向到192.168.10.143:7001的master节点上的hash槽。

redis cluster维护

**resharding **
重新分片,将hash槽从原节点移动到其他节点,使用如下命令按照提示操作即可:

redis-cli --cluster reshard 192.168.10.140:7001

指定集群中任意节点地址,都可以完成此操作,redis-cli会自动发现其他node。重新分片后,使用下面命令查看重新分片后的hash槽

redis-cli --cluster check 192.168.10.141:7001

也可以使用详细的命令进行重新分片,如下:

redis-cli --cluster reshard <host>:<port> --cluster-from <node-id> --cluster-to <node-id> --cluster-slots <number of slots> --cluster-yes

failover 故障转移

在某一master节点不可用后,redis-cluster会进行自动故障转移,将master的一个slave选举为新的master,并在原master在恢复后,最为新master的slave节点存在。

在可以预见故障发生前,可以提前进行故障转移,降低影响。使用CLUSTER FAILOVER进行故障转移,在需要进行故障转移的master节点的某一个slave节点执行命令

127.0.0.1:7001> CLUSTER FAILOVER
OK

在手动故障转移的过程中,连接到正在故障转移的master的客户端停止,master将便宜来那个发送的slave,slave复制偏移量并通知master切换配置,完成后,客户端请求重定向到新的master。

新增删除节点
新增节点

redis-cli --cluster add-node 192.168.10.141:7003 192.168.141:7004

在新增master节点后还需要使用重新分片,将hash槽分到新加入的节点,否则新加入的节点不能存储数据,知识将客户端请求重定向到其他节点
新增slave节点

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

删除节点,删除从节点可以直接删除,删除主节点则需要重新分片,将主节点的数据分到其他主节点,再删除主节点

redis-cli --cluster del-node 127.0.0.1:7000 `<node-id>`

副本迁移
可以使用CLUSTER REPLICATE命令将副本redis节点迁移到任意一个master节点。同时在cluster-migration-barrier指导下,redis cluster会自动迁移副本

升级节点

slave节点直接进行升级,master节点升级时,先手动进行故障转移,使master节点变为slave节点,再直接升级。

设计目标

  • 高性能,支持1000个节点的线性扩展,没有代理,使用异步复制,不对值进行合并操作。
  • 可接受的写入安全程度,系统尝试保留大多数与主节点连接的客户端的所有写入,部分场景下,确认的写入可能丢失。(3种)1,客户端与某个主节点在一个分区内。2,异步复制的过程中发生failover。3,异步aof或rdb持久化时,集群宕机。
  • 可用性:在主节点有从节点的情况下,自动故障转移。可以通过配置配置在部分hash槽没有被覆盖的情况下也提供服务(部分master不可用时)。

命令

  • redis cluster只支持一个数据库0,不支持select操作
  • redis cluster支持所有的单key操作,get,set等
  • 多key操作,多个key在同一个hash槽是,redis cluster也支持,但是在不同的hash槽时,redis cluster不支持
  • redis cluster支持hash标签,通过hash标签可以强制将key存储到同一个hash槽中,在重新分片的过程中,多key操作可能不可用。

codis

codis是一个分布式的redis解决方案,对于应用来说连接到codis和连接到一个单机的redis没有区别,codis底层处理请求的转发,不停机的数据迁移等工作。codis使用go编写

codis组件

  • codis-proxy,客户端连接到redis的代理服务,实现了redis协议,可以部署多个节点。
  • codis config,codis管理工具,图形化的工具,支持添加删除redis节点,添加删除proxy节点,发起数据迁移等操作,自带http服务
  • codis redis,codis维护的redis实例,基于redis2.8.21分支,支持slot和原子的数据迁移指令,codis-proxy和codis-config只能和这个版本redis才能正常运行。codis3基于redis3.2版本,支持更丰富的数据结构。
  • zookeeper,codis集群元数据存储,维护codis集群节点信息

Codis架构
在这里插入图片描述

CAP

redis低版本是个CP系统,高版本默认是AP系统,最终一致性,也可配置为强一致性。

redis 同步

Redis事务

MULTI,EXEC,DISCARD和WATCH四个命令是Redis事务的基础,实现一个事务中执行一组命令。

multi:开启事务
exec:执行事务
discard:回滚事务
watch:用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
unwatch: 取消监视一个或多个key

redis事务特性:

  • redis事务中的多个命令,按照顺序串行执行,并且执行过程中不会执行其他客户端的命令,保证隔离性 。
  • redis事务是原子性的,多个命令要么都执行,要么都不执行,正常情况下使用exec命令来保证,在执行exec命令后,事务内的多个操作才会全部生效。
  • 在使用aof时,redis保证一次write(2) 系统调用,保证多个操作写入磁盘,如果写入的过程中发生崩溃、宕机,导致aof日志不完整,则redis启动会失败,这时候需要调用redis-check-aof命令来修复aof文件,移除为执行成功的事务,保证redis正常启动。和mysql不同的是,redis没有自动的crash-recovery机制。

一次正常提交的事务如下:

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr a
QUEUED
127.0.0.1:6379(TX)> incr b
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 1
2) (integer) 1

redis事务执行过程中可能发生两种错误,

  1. 在exec命令之前,命令加入队列就发生错误,如命令语法错误,命令参数错误,命令名称错误,内存不够。
  2. 在exec命令执行时,命令执行错误,如对string执行list命令
    以上两种错误:
    对于第一种错误,客户端可以根据返回值来确定命令是否正确,如正确情况下返回QUEUED,以此来控制事务回滚。2.6.5以后,服务端也会记录错误,当exec时,如果发生第一种错误,redis服务会自动回滚事务。
    对于第二种错误,redis不会特殊处理,会发生部分命令成功而部分命令失败的违反一致性,原子性的情况,对此redis的解释是:
    1. 错误使用命令,只会发生在开发阶段,而不是生产环境。
    2. redis为了更简单和快速,不需要自动回滚事务。

redis通过乐观锁也就是cas(Compare And Swap),来实现事务的。
redis通过WATCH命令来支持CAS操作,进而实现事务,原理是在事务开始前检视一个或多个key,在exec时先比较监视的键值对,如果没有改变就执行exec,否则exec失败。如:

127.0.0.1:6379> watch test1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr test1
QUEUED
127.0.0.1:6379(TX)> exec
(nil)

watch test1键值对,开启事务,test1值自增,同时在其他客户端自增test1并成功,这是当前事务exec提交,发现提交失败。
用下面一张图来描述WATCH过程:

在这里插入图片描述
如通过watch操作完成zset的zpopmin操作,即原子性取出zset中的分数最小的值

127.0.0.1:6379> zrange scores 0 10 # 查看scores集合元素
1) "xiaoming"
2) "lily"
3) "james"
127.0.0.1:6379> watch scores # 监视scores集合
OK
127.0.0.1:6379> zrange scores 0 0 #取出第一个元素
1) "xiaoming"
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379(TX)> ZREM scores xiaoming #删除第一个元素
QUEUED
127.0.0.1:6379(TX)> exec # 执行事务
1) (integer) 1

多次调用watch具有相同的功能,exec之后,无论exec成功或者失败,watch的键都变为unwatch。

redis支持lua脚本,lua脚本是原子性的,任何通过事务完成的操作,都可以使用lua脚本来完成,通过脚本更简单快速。

CAS

Compare And Swap,redis原子性操作
基于lua脚本的CAS操作
String CAS操作

if redis.call("GET", KEYS[1]) == ARGV[1]
then
	redis.call("SET", KEYS[1], ARGV[2])
	return 1
else
	return 0
end

Hash CAS操作

if redis.call("HGET", KEYS[1], KEYS[2]) == ARGV[1]
then
	redis.call("HSET", KEYS[1], KEYS[2], ARGV[2])
	return 1
else
	return 0
end	

基于事务watch的CAS操作

事务已经讲过,这里不在赘述

分布式锁

redis通常用于分布式锁。
分布式锁特点

  • 互斥,锁多个客户端之间互斥
  • 可重入,客户端获取锁后,可以重复获取锁,而不需要等待。
  • 锁超时,支持锁过一定时间后,自动释放锁,避免死锁。
  • 非阻塞,没有获取到锁时,不会一直等待,而是直接返回程序,程序可继续执行其他逻辑。
  • 公平锁和非公平锁,公平锁按照请求加锁的顺序加锁,非公平锁则不按顺序,随机加锁。

redis分布式锁问题:
1,集群和哨兵模式下,故障转移时数据丢失,可能出现锁失效。
2,锁超时后,别的客户端拿到锁,锁失去互斥特性。

种类
排它锁,悲观锁,对于竞争持悲观态度,加锁,同时只有一个线程获取到锁
乐观锁,采用CAS的思想,给数据增加版本号,写数据时先读版本号,再根据版本号更新,如更新不成功,再读版本号再更新。

redis锁的使用

        try{
            Boolean lock = redisTemplate.opsForValue().setIfAbsent("test1", "1", 10, TimeUnit.SECONDS);
            if (lock) {
                log.info("get lock success");
            }else{
                log.info("get lock failed");
            }
        } catch (Exception e) {
            log.info("", e);
        }finally {
            if("1".equals(redisTemplate.opsForValue().get("test1").toString())){
                redisTemplate.delete("test1");
            }
        }

通信协议

IO模型

根据操作系统不同,采用epoll或kqueue的IO多路复用模型。

其他

lua脚本

使用
eval

127.0.0.1:6379> eval "return {KEYS[1],ARGV[1]}" 1 test1  1
1) "test1"
2) "1"

script load

127.0.0.1:6379> SCRIPT LOAD "return {KEYS[1],ARGV[1]}"  # 加入redis缓存
"bfbf458525d6a0b19200bfd6db3af481156b367b" # 返回sha1加密串
127.0.0.1:6379> 
127.0.0.1:6379> SCRIPT EXISTS bfbf458525d6a0b19200bfd6db3af481156b367b #判断sha1加密串对应lua脚本
1) (integer) 1
127.0.0.1:6379> EVALSHA bfbf458525d6a0b19200bfd6db3af481156b367b 1 test1 2
1) "test1"
2) "2"

从外部文件执行

# cat isActive.lua 
if redis.call("EXISTS", KEYS[1]) == 1 then
    return redis.call("INCR", KEYS[1])
else
    return nil
end
# redis-cli --eval ./isActive.lua test1
(integer) 7
# redis-cli --eval ./isActive.lua test1
(integer) 8

优势

  • 减少网络开销:多个请求通过脚本一次发送,减少网络延迟
  • 原子操作:将脚本作为一个整体执行,中间不会插入其他命令,无需使用事务
  • 复用:客户端发送的脚本永久存在redis中,其他客户端可以复用脚本
  • 灵活可嵌入性:可嵌入JAVA,C#等多种编程语言,支持不同操作系统跨平台交互

在原子性的CAS操作领域,性能比redis事务要高

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值