Redis

304 阅读41分钟

#Redis为什么快?

内存存储:Redis是使用内存(in-memeroy)存储,没有磁盘IO上的开销

单线程实现:Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销

非阻塞IO:Redis使用多路复用IO技术,在poll,epool,kqueue选择最优IO实现

优化的数据结构:Redis有诸多可以直接应用的优化数据结构的实现,应用层可以直接使用原生的数据结构提升性能

数据结构操作

String

操作

增删改查 过期
SET  key  value 			//`存入字符串键值对`
MSET  key  value [key value ...] 	//批量存储字符串键值对
SET  key  value NX		//存入一个不存在的字符串键值对
GET  key 			//获取一个字符串键值
MGET  key  [key ...]	 	//批量获取字符串键值
DEL  key  [key ...] 		//删除一个键
SET key value EX 5         //设置键5秒过期
EXPIRE  key  seconds 		//设置一个键的过期时间(秒)

原子加减
INCR  key 			//将key中储存的数字值加1
DECR  key 			//将key中储存的数字值减1
INCRBY  key  increment 		//将key所储存的值加上increment
DECRBY  key  decrement 	//将key所储存的值减去decrement

场景

  1. 单值或对象缓存

  2. 分布式锁

SETNX  product:10001  true 		//返回1代表获取锁成功
SETNX  product:10001  true 		//返回0代表获取锁失败
。。。执行业务操作
DEL  product:10001			//执行完业务释放锁

SET product:10001 true  ex  10  nx	//防止程序意外终止导致死锁

RedisDB 数据结构

image.png

BitMap

Redis 其实只支持 5 种数据类型,并没有 BitMap 这种类型,BitMap 底层是基于 Redis 的字符串类型实现的。

在 Redis 中,可以把 Bitmaps 想象成一个以比特位为单位的数组,数组的每个单元只能存储0和1,数组的下标在 Bitmaps 中叫做偏移量。节省空间

image.png

操作

# 设置值,其中value只能是 01
setbit key offset value

# 获取值
getbit key offset

# 获取指定范围内值为 1 的个数
# start 和 end 以字节为单位
bitcount key start end

# BitMap间的运算
# operations 位移操作符,枚举值
  AND 与运算 &
  OR 或运算 |
  XOR 异或 ^
  NOT 取反 ~
# result 计算的结果,会存储在该key中
# key1 … keyn 参与运算的key,可以有多个,空格分割,not运算只能一个key
# 当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0。返回值是保存到 destkey 的字符串的长度(以字节byte为单位),和输入 key 中最长的字符串长度相等。
bitop [operations] [result] [key1] [keyn…]

# 返回指定key中第一次出现指定value(0/1)的位置
bitpos [key] [value]

#返回的长度不是bit数,是字节数
strlen [key]

但是需要注意,Redis 中字符串的最大长度是 512M,所以 BitMap 的 offset 值也是有上限的,其最大值是:

8 * 1024 * 1024 * 512 = 2^32

由于 C语言中字符串的末尾都要存储一位分隔符,所以实际上 BitMap 的 offset 值上限是:

(8 * 1024 * 1024 * 512) -1 = 2^32 - 1

场景

  1. 用户签到

很多网站都提供了签到功能,并且需要展示最近一个月的签到情况,这种情况可以使用 BitMap 来实现。 根据日期 offset = (今天是一年中的第几天) % (今年的天数),key = 年份:用户id。

  1. 统计活跃用户(用户登陆情况)

使用日期作为 key,然后用户 id 为 offset,如果当日活跃过就设置为1。具体怎么样才算活跃这个标准大家可以自己指定。

假如 20201009 活跃用户情况是: [1,0,1,1,0] 20201010 活跃用户情况是 :[ 1,1,0,1,0 ]

统计连续两天活跃的用户总数:

统计20201009 ~ 20201010 活跃过的用户:


bitop and dest1 20201009 20201010 
# dest1 中值为1的offset,就是连续两天活跃用户的ID
bitcount dest1

统计20201009 ~ 20201010 活跃过的用户:

bitop or dest2 20201009 20201010 
  1. 统计用户是否在线

如果需要提供一个查询当前用户是否在线的接口,也可以考虑使用 BitMap 。即节约空间效率又高,只需要一个 key,然后用户 id 为 offset,如果在线就设置为 1,不在线就设置为 0。

SDS

String底层实现是简单动态字符串sds(simple dynamic string),是可以修改的字符串。 它类似于Java中的ArrayList,它采用预分配冗余空间的方式来减少内存的频繁分配。

redis 中key value中的String list中的string等都是字符串类型。

  1. 实现布隆过滤器 developer.aliyun.com/article/773…

结构:

 struct sdshdr{
   int len;
   int free;
   char buf[];
}

redis为什么 会用SDS的结构,而不直接用c语言的字符串

  1. 计算字符串长度的区别

对于c来说,计算字符串的长度的方式就是遍历,遇到0就停止,所以复杂对是O(n),而SDS直接保存了字符串的长度,复杂度是O(1)

  1. 保证存储二进制数据的安全 因为SDS并不是以0为结尾的标志,当存储视频,音频的二进制数据时遇到\0的情况不会终止,自然就保证了二进制的安全。

  2. 内存管理策略(预分配内存和惰性空间释放策略)

redis是一个高速的缓存数据库,需要频繁的对字符串进行操作,如果内存分配错误,会导致很严重的后果,就算内存分配没问题,频繁的内存分配也是非常耗费时间的,所以这些都是应该去避免的

惰性空间释放策略

在SDS中首先用到了惰性空间释放策略,惰性空间释放用于优化SDS的字符串缩短操作。 当要缩短SDS保存的字符串时,程序并不立即使用内存充分配来回收缩短后多出来的字节,而是使用表头的free成员将这些字节记录起来,并等待将来使用。 源码如下

void sdsclear(sds s) {  //重置sds的buf空间,懒惰释放
    struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
    sh->free += sh->len;    //表头free成员+已使用空间的长度len = 新的free
    sh->len = 0;            //已使用空间变为0
    sh->buf[0] = '\0';         //字符串置空
}

预分配内存

扩容策略是字符串在长度小于 1M 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间。当长度超过1M 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配 1M大小的冗余空间。

  1. 兼容c语言函数库 (字符串后面会自动加上\0)

3.2版本以后的SDS结构

en和free都是int类型,都是4byte也就是32bit,能表示42亿左右的范围,大大的造成了空间的浪费,所以在3.2以后对SDS有一定的更改,会根据不同的字符串长度创建相应的类型

typedef char *sds;

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
.........

sdshdr5表示的是用5个bit位来表示数据的长度,sdshdr8就是表示用8个bit位来表示数据的长度,以此类推 sdshdr5的内存分配如图,flag占1个字节,8bit 前三位存储类型,只有在sdshdr5中flag的后五位表示len。flag实际意义是存储了类型信息,单独出一个sdshdr5可以进一步节省内存。

SDS是一篇连续的空间,所以指向字符串的指针向前偏移一个位置就可以得到type信息,进而得到len信息。

image.png

当需要存储的数据长度超过31,就需要用sdshdr8来表示 sdshdr8的内存分配如图

image.png 3. 计数器(阅读量)或分布式系统全局序列号

INCR article:readcount:{文章id}

GET article:readcount:{文章id}

INCRBY orderId 1000 //redis批量生成序列号提升性能

  1. web session 共享

spring session + redis实现session共享

Hash

HSET  key  field  value 			//存储一个哈希表key的键值
HSETNX  key  field  value 		//存储一个不存在的哈希表key的键值
HMSET  key  field  value [field value ...] 	//在一个哈希表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. 对象缓存 购物车
 
HMSET  user  {userId}:name  zhuge  {userId}:balance  1888
HMSET  user  1:name  zhuge  1:balance  1888
HMGET  user  1:name  1:balance  
  1. 相比String优缺点

优点:

  1. 便于管理
  2. 消耗的内存和cpu更少 缺点:
  3. 不能为每个字段设置过期时间,只能为key设置过期时间
  4. redis 集群架构下不适合大规模使用,因为如果hash结构的对象很大的话,只能存在集群的某一个节点上,导致访问单机压力过大,不过可以优化,使key利用一定规则分段存储。

List

操作


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指定

BLPOP  key  [key ...]  timeout	//从key列表表头弹出一个元素,若列表中没有元素,阻塞等待					timeout秒,如果timeout=0,一直阻塞等待
BRPOP  key  [key ...]  timeout 	//从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待					timeout秒,如果timeout=0,一直阻塞等待

场景

  1. 栈(微博信息流) Stack(栈) = LPUSH + LPOP

  2. 队列 Queue(队列)= LPUSH + RPOP

  3. 阻塞队列 Blocking MQ(阻塞队列)= LPUSH + BRPOP

ziplist

ziplist是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。ziplist可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以O(1)的时间复杂度在表的两端提供push和pop操作。

普通链表的缺点

  1. 地址指针太多会占用额外的内存空间
  2. 不连续导致内存碎片,造成内存浪费

数据结构

image.png

各个部分在内存上是前后相邻的,它们分别的含义如下:

  1. zlbytes: 32bit,表示ziplist占用的字节总数(也包括本身占用的4个字节)。
  2. zltail: 32bit,表示ziplist表中最后一项(entry)在ziplist中的偏移字节数。的存在,使得我们可以很方便地找到最后一项(不用遍历整个ziplist),从而可以在ziplist尾端快速地执行push或pop操作。
  3. zllen: 16bit, 表示ziplist中数据项(entry)的个数。zllen字段因为只有16bit,所以可以表达的最大值为2^16-1。这里需要特别注意的是,如果ziplist中数据项个数超过了16bit能表达的最大值,ziplist仍然可以来表示。那怎么表示呢?这里做了这样的规定:如果小于等于2^16-2(也就是不等于2^16-1),那么zllen就表示ziplist中数据项的个数;否则,也就是等于16bit全为1的情况,那么zllen就不表示数据项个数了,这时候要想知道ziplist中数据项总数,那么必须对ziplist从头到尾遍历各个数据项,才能计数出来。
  4. entry: 表示真正存放数据的数据项,长度不定。一个数据项(entry)也有它自己的内部结构,这个稍后再解释。
  5. zlend: ziplist最后1个字节,是一个结束标记,值固定等于255。

我们再来看一下每一个数据项<entry>的构成:

<prevrawlen><len><data>

我们看到在真正的数据(<data>)前面,还有两个字段:

<prevrawlen>: 表示前一个数据项占用的总字节数。这个字段的用处是为了让ziplist能够从后向前遍历(从后一项的位置,只需向前偏移prevrawlen个字节,就找到了前一项)。这个字段采用变长编码。 <len>: 表示当前数据项的数据长度(即<data>部分的长度)。也采用变长编码。 那么<prevrawlen>和<len>是怎么进行变长编码的呢?

image.png

image.png

ziplist本来就设计为各个数据项挨在一起组成连续的内存空间,这种结构并不擅长做修改操作。一旦数据发生改动,就会引发内存realloc,可能导致内存拷贝。

ziplist

quicklist zhangtielei.com/posts/blog-…

quicklist是3.2版本之后引入的。

根据上文谈到,ziplist会引入频繁的内存申请和释放,而linkedlist由于指针也会造成内存的浪费,而且每个节点是单独存在的,会造成很多内存碎片,所以结合两个结构的特点,设计了quickList。

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中删除

Set运算操作
SINTER  key  [key ...] 				//交集运算
SINTERSTORE  destination  key  [key ..]		//将交集结果存入新集合destination中
SUNION  key  [key ..] 				//并集运算
SUNIONSTORE  destination  key  [key ...]		//将并集结果存入新集合destination中
SDIFF  key  [key ...] 				//差集运算,以第一key 为中心,后面所有的                                                  //在第一个中的差集
SDIFFSTORE  destination  key  [key ...]		//将差集结果存入新集合destination中

场景

  1. 做交并查集 如抽奖程序,微信点赞,共同关注,可能认识,筛选等

Zset (sorted_set) 带分值排序字段的集合

操作

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下标的元素
ZREVRANGE key start stop [WITHSCORES]	//倒序获取有序集合key从start下标到stop下标的元素

Zset集合操作(做交并集时不考虑score,numkeys 是后面key的数量,没有差集)
ZUNIONSTORE destkey numkeys key [key ...] 	//并集计算
ZINTERSTORE destkey numkeys key [key …]	//交集计算

场景

  1. 排行榜
Zset集合操作实现排行榜
1)点击新闻
ZINCRBY  hotNews:20190819  1  守护香港
2)展示当日排行前十
ZREVRANGE  hotNews:20190819  0  9  WITHSCORES 
3)七日搜索榜单计算
ZUNIONSTORE  hotNews:20190813-20190819  7 
hotNews:20190813  hotNews:20190814... hotNews:20190819
4)展示七日排行前十
ZREVRANGE hotNews:20190813-20190819  0  9  WITHSCORES

Redis是单线程吗?

redis单线程是指网络IO和键值对读写模块是单线程执行的,其他模块如持久化,异步删除,集群数据同步等是由额外的线程执行的,redis6中网络IO是可以用多线程的,默认是关闭的

Redis 单线程为什么还能这么快?

  1. 基于内存操作
  2. IO多路复用(待补充
  3. 减少了cpu上下文切换的开销

scan 底层待补充

SCAN cursor [MATCH pattern] [COUNT count]

scan 参数提供了三个参数,第一个是 cursor 整数值(hash桶的索引值),第二个是 key 的正则模式, 第三个是一次遍历的key的数量(参考值,底层遍历的数量不一定),并不是符合条件的结果数量。第 一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历 到返回的 cursor 值为 0 时结束。

注意:但是scan并非完美无瑕, 如果在scan的过程中如果有键的变化(增加、 删除、 修改) ,那 么遍历效果可能会碰到如下问题: 新增的键可能没有遍历到, 遍历出了重复的键等情况, 也就是说 scan并不能保证完整的遍历出来所有的键, 这些是我们在开发时需要考虑的

scan 指令是一系列指令,除了可以遍历所有的 key 之外,还可以对指定的容器集合进行遍历。

zscan 遍历 zset 集合元素,

hscan 遍历 hash 字典的元素、

sscan 遍历 set 集合的元素。

注意点:

SSCAN 命令、 HSCAN 命令和 ZSCAN 命令的第一个参数总是一个数据库键。

而 SCAN 命令则不需要在第一个参数提供任何数据库键 —— 因为它迭代的是当前数据库中的所有数据库键。

如何定位大 key?

  1. 编写脚本遍历keys,通过 type 获取key的数据结构,再通过相应的len或size方法,扫描出每一种类型的前N名

如果是string结构,通过strlen判断;

如果是list结构,通过llen判断;

如果是hash结构,通过hlen判断;

如果是set结构,通过scard判断;

如果是sorted set结构,通过zcard判断。 2. redis-cli

redis-cli  --bigkeys 会分析keys大小的情况

image.png

为防止提高ops(redis中的OPS 即operation per second 每秒操作次数) 可以用redis-cli --bigkeys -i 0.1 每扫描100个休眠0.1秒

注意

这种方式能保证找到的key最长,但不一定是占用空间最大的,只有能确定String是占用最大内存的, 其他的不一定,比如:

现在有两个list类型的key,分别是:numberlist–[0,1,2],stringlist–[“123456789123456789”],

由于通过llen判断,所以numberlist要大于stringlist。

而事实上stringlist更占用内存。其他三种数据结构hash,set,sorted set都会存在这个问题。

查看慢日志,执行慢的命令的日志

SLOWLOG subcommand [argument]

subcommand主要有:

get,用法:slowlog get [argument],获取argument参数指定数量的慢日志。

len,用法:slowlog len,总慢日志数量。

reset,用法:slowlog reset,清空慢日志。

redis-cli slowlog get 5

命令耗时超过多少才会保存到slowlog中,可以通过命令config set slowlog-log-slower-than 2000配置并且不需要重启redis。默认是1s

注意:单位是微妙,2000微妙即2毫秒。

rename-command 重命名危险命令

rename-command flushdb flushddbb 清空db

rename-command flushall flushallall

rename-command keys keysys

查看keys 的数量

dbsize 或info最后一行

Redis持久化

RDB快照(snapshot)

在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。

你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。

比如说, 以下设置会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存次 数据集:

save 60 1000 //关闭RDB只需要将所有的save保存策略注释掉即可

还可以手动执行命令生成RDB快照,进入redis客户端执行命令save或bgsave可以生成dump.rdb文件, 每次命令执行都会将所有redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件。

bgsave的写时复制(COW)机制

Redis 借助操作系统提供的写时复制技术(Copy-On-Write, COW),在生成快照的同时,依然可以正常 处理写命令。简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。 bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些 数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,那 么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文 件,而在这个过程中,主线程仍然可以直接修改原来的数据。

AOF(append-only file)

快照功能并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失 最近写入、且仍未保存到快照中的那些数据。从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方 式: AOF 持久化,将修改的每一条指令记录进文件appendonly.aof中(先写入os cache,每隔一段时间 fsync到磁盘)

注意,如果执行带过期时间的set命令,aof文件里记录的是并不是执行的原始命令,而是记录key过期的 时间戳

 appendonly yes
 appendfsync always:每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
 appendfsync everysec:每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
 appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。

AOF重写

AOF文件里可能有太多没用指令,所以AOF会定期根据内存的最新数据生成aof文件

比如:

127.0.0.1:6379> incr readcount
 (integer) 13 127.0.0.1:6379> incr readcount
 (integer) 2
 127.0.0.1:6379> incr readcount
 (integer) 3
 127.0.0.1:6379> incr readcount
 (integer) 4
 127.0.0.1:6379> incr readcount
 (integer) 5

重写后:

*3
 $3
 SET
 $2
 readcount
 $1
 5

如下两个配置可以控制AOF自动重写频率

1 # auto‐aof‐rewrite‐min‐size 64mb //aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就
很快,重写的意义不大
2 # auto‐aof‐rewrite‐percentage 100 //aof文件自上一次重写后文件大小增长了100%则再次触发重写

当然AOF还可以手动重写,进入redis客户端执行命令bgrewriteaof重写AOF

注意,AOF重写redis会fork出一个子进程去做(与bgsave命令类似),不会对redis正常命令处理有太多 影响

RDB 和 AOF比较

命令 RDB AOF

image.png

生产环境可以都启用,redis启动时如果既有rdb文件又有aof文件则优先选择aof文件恢复数据,因为aof 一般来说数据更全一点。

Redis 4.0 混合持久化

重启 Redis 时,我们很少使用 RDB来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重 放,但是重放 AOF 日志性能相对 RDB来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很 长的时间。 Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。 通过如下配置可以开启混合持久化(必须先开启aof):

# aof‐use‐rdb‐preamble yes

如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将 重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一 起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改 名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。

于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,因此重启效率大幅得到提升。

混合持久化AOF文件结构如下

image.png

Redis数据备份策略:定时备份aof或RDB文件

  •  写crontab定时调度脚本,每小时都copy一份rdb或aof的备份到一个目录中去,仅仅保留最近 48小时的备份
  • 每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份
  •  每次copy备份的时候,都把太旧的备份给删了
  • 每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏

Redis主从架构

特点

一个主服务器可以配置多个从节点,从节点也可以配置子从节点,主服务器数据发生变化时可以发送命令同步到从服务器上,一般从服务器是只读的。当主服务器发生故障后,需手动恢复。可以实现数据的热备份,方便单点故障的恢复,可以负载一部分读的压力,是高可用架构的基础。

搭建

redis主从架构搭建,配置从节点步骤:

  1. 复制一份redis.conf文件
  2. 将相关配置修改为如下值:
  • port  6380

  • pidfile /var/run/redis_6380.pid # 把pid进程号写入pidfile配置的文件

  • logfile "6380.log"

  • dir /usr/local/redis‐5.0.3/6380  # 指定数据存放目录

  • 将bind的值配置为bind 0.0.0.0。默认是127.0.0.1 只能本机访问,如其他客户端访问需要配置。目前采用的方式,代码和Linux中的redis客户端都可以使用

    1.在bind中配置多个IP地址,bind 192.168.64.129 127.0.0.1

    2.将bind的值配置为bind 0.0.0.0

  • 配置主从复制

replicaof 192.168.0.60  6379  # 从本机 6379 的redis实例复制数据,Redis 5.0之前使用slaveof

replica‐read‐only yes # 配置从节点只读

如果我们需要slave对master的复制进行验证,可以在master中配置requirepass <password>选项设置密码 那么需要在从服务器中使用该密码,可以使用命令config set masterauth <password>,或者在配置文件中设置masterauth <password>

测试

启动从节点
redis‐server redis.conf
连接从节点
redis‐cli ‐p  6380
测试在 6379 实例上写数据, 6380 实例是否能及时同步新修改数据

原理

www.cnblogs.com/kismetv/p/9…

Redis的主从复制过程大体上分3个阶段:建立连接、数据同步、命令传播

建立连接

在从服务器执行replicaof命令,根据ip和端口号建立连接,从服务器会发送ping,主服务器恢复pong说明可用,否则断开连接发起重连,如果设置了密码,还会验证密码,验证成功后主服务器会保存从服务器的监听端口。

192.168.249.20:6379> info replication ... slave0:ip=192.168.249.22,port=6379,state=online,offset=700,lag=0 slave1:ip=192.168.249.21,port=6379,state=online,offset=700,lag=0 ...

复制代码数据同步

第一次从服务器复制主服务器时会进行全量复制,接下来就是命令传播阶段,主服务器每执行一个写命令会向从服务器传播相同的写命令,此阶段从服务器还会每秒发送一次心跳检测,带有偏移量,目的检查网络情况,检查执行的命令是否完整(这里也会利用复制积压缓冲区),如果网络断了重新恢复后执行增量复制,有可能执行全量复制。执行增量复制的条件有两个必须同时满足,一是从节点发送的runid必须和现有主节点id一致,而是offset后命令在复制积压缓存区都存在。

全量复制

从服务器连接主服务器,发送Psync <runid> <offset>,其中runid为上次复制的主节点的runid,offset为上次复制截止时从节点保存的复制偏移量。主服务器收到请求后会执行bgsave生成rgb文件并使用缓冲区继续记录此后执行的所有命令。主服务器向所有从服务器发送快照,从服务器拿到快照后删除旧数据并载入快照,发送完毕后向从服务器发送缓冲区的写命令,从服务器执行写命令。

增量复制(增量同步,断点续传)

当slave连接到master,会执行PSYNC <runid> <offset>,主服务器拿到的偏移量和自己对比,发送偏移量之后的命令。

偏移量

主节点和从节点各维护一个offset,表示主服务器送给从服务器的字节数。

复制积压缓存区

是一个固定大小的队列,备份最近主节点向从节点发送的数据,记录着每个字节的偏移量,默认1M,只有主节点维护,所有的从节点共享。为了提高网络中断时部分复制执行的概率,可以根据需要增大复制积压缓冲区的大小(通过配置repl-backlog-size);

管道(Pipeline)

客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完后再一次性读取服务的响 应,这样可以极大的降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一 次命令执行的网络开销。需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。 pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的响应中得到信 息;也就是pipeline并不是表达“所有command都一起成功”的语义,管道中前面命令失败,后面命令 不会有影响,继续执行。

减少了网络io,增大了内存消耗

Pipeline pl = jedis.pipelined();
23 for (int i = 0; i < 10; i++) {
24 pl.incr("pipelineKey");
25 pl.set("zhuge" + i, "zhuge");
26 }
27 List<Object> results = pl.syncAndReturnAll();
28 System.out.println(results);

lua脚本

  1. 减少网络开销
  2. 原子操作
  3. 代替事务
  4. 执行是单进程单线程

命令行

EVAL script numkeys key [key ...] arg [arg ...]

java

String script = " local count = redis.call('get', KEYS[1]) " +
 " local a = tonumber(count) " +
 " local b = tonumber(ARGV[1]) " +
 " if a >= b then " +
 " redis.call('set', KEYS[1], a‐b) " +
 " return 1 " +
 " end " +
 " return 0 ";
 Object obj = jedis.eval(script, Arrays.asList("product_count_10016"),
Arrays.asList("10"));
42 System.out.println(obj);

Redis哨兵高可用架构

sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。 哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,不会每次都通过 sentinel代理访问redis的主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)

image.png

redis哨兵架构搭建步骤:

1 1 、复制一份sentinel.conf文件
2 cp sentinel.conf sentinel‐26379.conf
3
4 2 、将相关配置修改为如下值:
5 port  26379
6 daemonize yes
7 pidfile "/var/run/redis‐sentinel‐26379.pid"
8 logfile "26379.log"
9 dir "/usr/local/redis‐5.0.3/data"
10 # sentinel monitor <master‐redis‐name> <master‐redis‐ip> <master‐redis‐port> <quorum>


11 # quorum是一个数字,指明当有多少个sentinel认为一个master失效时(值一般为:sentinel总数/ 2  +
1 ),master才算真正失效
12 sentinel monitor mymaster 192.168.0.60  6379 2  # mymaster这个名字随便取,客户端访问时会用
到
13
14 3 、启动sentinel哨兵实例
15 src/redis‐sentinel sentinel‐26379.conf
16
17 4 、查看sentinel的info信息
18 src/redis‐cli ‐p  26379
19 127.0.0.1: 26379 >info
20 可以看到Sentinel的info里已经识别出了redis的主从
21
22 5 、可以自己再配置两个sentinel,端口 2638026381 ,注意上述配置文件里的对应数字都要修改
23

sentinel集群都启动完毕后,会将哨兵集群的元数据信息写入所有sentinel的配置文件里去(追加在文件的 最下面),我们查看下如下配置文件sentinel-26379.conf,如下所示:

sentinel known‐replica mymaster 192.168.0.60  6380  #代表redis主节点的从节点信息

sentinel known‐replica mymaster 192.168.0.60  6381  #代表redis主节点的从节点信息

sentinel known‐sentinel mymaster 192.168.0.60 26380 52d0a5d70c1f90475b4fc03b6ce7c3c35760f #代表感知到的其它哨兵节点 sentinel known‐sentinel mymaster 192.168.0.60  26381  e9f530d3882f8043f76ebb8e1686438ba bd5ca6 #代表感知到的其它哨兵节点

当redis主节点如果挂了,哨兵集群会重新选举出新的redis主节点,同时会修改所有sentinel节点配置文件 的集群元数据信息同时还会修改sentinel文件里之前配置的mymaster对应的6379端口,改为 6380

sentinel monitor mymaster 192.168.0.60  6380 

当6379的redis实例再次启动时,哨兵集群根据集群元数据信息就可以将6379端口的redis节点作为从节点 加入集群

哨兵的Spring Boot整合Redis连接代码: 配置后会自动订阅哨兵服务

server:
2 port:  
3
4 spring:
5 redis:
6 database:  
7 timeout:  
8 sentinel: #哨兵模式
9 master: mymaster #主服务器所在集群名称
10 nodes: 192.168.0.60: 26379 ,192.168.0.60: 26380 ,192.168.0.60: 26381

Redis客户端命令对应的RedisTemplate中的方法列表:

image.png

image.png

image.png

高可用集群模式

哨兵模式不足:

  1. 存在访问瞬断
  2. 只有单个主节点,不能做到很高的并发
  3. 单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的 效率

image.png

redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis集群不需 要sentinel哨兵∙也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中 心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。redis集群的 性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单。

搭建

redis集群需要至少三个master节点,我们这里搭建三个master节点,并且给每个master再搭建一个slave节 点,总共6个redis节点,这里用三台机器部署6个redis实例,每台机器一主一从,搭建集群的步骤如下:

第一步:在第一台机器的/usr/local下创建文件夹redis‐cluster,然后在其下面分别创建2个文件夾如下
(1)mkdir ‐p /usr/local/redis‐cluster
 (2)mkdir 8001 8004

第一步:把之前的redis.conf配置文件copy到8001下,修改如下内容:
 (1)daemonize yes
 (2)port 8001(分别对每个机器的端口号进行设置)
 (3)pidfile /var/run/redis_8001.pid # 把pid进程号写入pidfile配置的文件
 (4)dir /usr/local/redis‐cluster/8001/(指定数据文件存放位置,必须要指定不同的目录位置,不然会
丢失数据)
 (5)cluster‐enabled yes(启动集群模式)
 (6)cluster‐config‐file nodes‐8001.conf(集群节点信息文件,这里800x最好和port对应上)
 (7)cluster‐node‐timeout 10000
 (8)# bind 127.0.0.1(bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通
过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可)
 (9)protected‐mode no (关闭保护模式)
(10)appendonly yes
如果要设置密码需要增加如下配置:
(11)requirepass zhuge (设置redis访问密码)
(12)masterauth zhuge (设置集群节点间访问密码,跟上面一致)
20 第三步:把修改后的配置文件,copy到8004,修改第2346项里的端口号,可以用批量替换:
 :%s/源字符串/目的字符串/g

 第四步:另外两台机器也需要做上面几步操作,第二台机器用80028005,第三台机器用80038006

 第五步:分别启动6个redis实例,然后检查是否启动成功
 (1)/usr/local/redis‐5.0.3/src/redis‐server /usr/local/redis‐cluster/800*/redis.conf
 (2)ps ‐ef | grep redis 查看是否启动成功

 第六步:用redis‐cli创建整个redis集群(redis5以前的版本集群是依靠ruby脚本redis‐trib.rb实现)
# 下面命令里的1代表为每个创建的主服务器节点创建一个从服务器节点
# 执行这条命令需要确认三台机器之间的redis实例要能相互访问,可以先简单把所有机器防火墙关掉,如果不
关闭防火墙则需要打开redis服务端口和集群节点gossip通信端口16379(默认是在redis端口号上加1W)
# 关闭防火墙
# systemctl stop firewalld # 临时关闭防火墙
# systemctl disable firewalld # 禁止开机启动
# 注意:下面这条创建集群的命令大家不要直接复制,里面的空格编码可能有问题导致创建集群不成功
(1)/usr/local/redis‐5.0.3/src/redis‐cli ‐a zhuge ‐‐cluster create ‐‐cluster‐replicas 1 1
92.168.0.61:8001 192.168.0.62:8002 192.168.0.63:8003 192.168.0.61:8004 192.168.0.62:8005 192.
168.0.63:8006
第七步:验证集群:391)连接任意一个客户端即可:./redis‐cli ‐c ‐h ‐p (‐a访问服务端密码,‐c表示集群模式,指定ip地址
和端口号)
如:/usr/local/redis‐5.0.3/src/redis‐cli ‐a zhuge ‐c ‐h 192.168.0.61 ‐p 800*
(2)进行验证: cluster info(查看集群信息)、cluster nodes(查看节点列表)
(3)进行数据操作验证
(4)关闭集群则需要逐个进行关闭,使用命令:
/usr/local/redis‐5.0.3/src/redis‐cli ‐a zhuge ‐c ‐h 192.168.0.60 ‐p 800* shutdown

集群的Spring Boot整合Redis连接代码

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.9.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.fiberhomebj</groupId>
    <artifactId>xingyv-webback-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>xingyv-webback-starter</name>
    <description>星域web后台</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>

    </build>

</project>
spring:
  redis:
    database: 0
    timeout: 3000
    password: zhuge
    cluster:
      nodes: 192.168.8.8:8001,192.168.8.9:8002,192.168.8.10:8003,192.168.8.8:8004,192.168.8.9:8005,192.168.8.10:8006
    lettuce:
      pool:
        max-active: 50
        min-idle: 10
        max-idle: 100
        max-wait: 1000
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class IndexController {

  private static final Logger logger = LoggerFactory.getLogger(IndexController.class);

  @Autowired
  private StringRedisTemplate stringRedisTemplate;

  @RequestMapping("/test_cluster")
  public void testCluster() throws InterruptedException {
      stringRedisTemplate.opsForValue().set("mxx", "666");
      System.out.println(stringRedisTemplate.opsForValue().get("mxx"));
  }
}

集群原理分析

Redis Cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每 个节点中。 当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地。这 样当客户端要查找某个 key 时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不 一致的情况,还需要纠正机制来实现槽位信息的校验调整。

槽位定位算法

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

跳转重定位

当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客 户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。客户端收到指 令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有 key 将使用新的槽 位映射表。

image.png

Redis集群节点间的通信机制

redis cluster节点间采取gossip协议进行通信 维护集群的元数据(集群节点信息,主从角色,节点数量,各节点共享的数据等)有两种方式:集中 式和gossip

集中式:

优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节 点读取的时候立即就可以立即感知到;不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数 据的存储压力。 很多中间件都会借助zookeeper集中式存储元数据。

gossip:

gossip协议包含多种消息,包括ping,pong,meet,fail等等。

meet:某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通 信;

ping:每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通 过

ping交换元数据(类似自己感知到的集群节点增加和移除,hash slot信息等);

pong: 对ping和meet消息的返回,包含自己的状态和其他信息,也可以用于信息广播和更新;

fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了。

gossip协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;缺点在于元数据更新有延时可能导致集群的一些操作会有一些滞后。

gossip通信的10000端口

每个节点都有一个专门用于节点间gossip通信的端口,就是自己提供服务的端口号+10000,比如7001,那么 用于节点间通信的就是17001端口。 每个节点每隔一段时间都会往另外几个节点发送ping消息,同时其他几 点接收到ping消息之后返回pong消息。

网络抖动

真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种各样的小问题。比如网络抖动就是非常常见 的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。 为解决这种问题,Redis Cluster 提供了一种选项cluster­node­timeout,表示当某个节点持续 timeout 的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频 繁切换 (数据的重新复制)。

Redis集群选举原理分析

  1. slave发现自己的master变为FAIL,将自己记录的集群currentEpoch(当前纪元,周期)加1,并向所有节点广播FAILOVER_AUTH_REQUEST 信息,
  2. 只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,根据谁先到投谁的原则对每一个epoch只发送一次ack
  3. slave收到超过半数master的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的)
  4. slave广播Pong消息通知其他集群节点 从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,作用是确保FAIL状态在集群中传播完毕,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票。

•延迟计算公式:

DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms

•SLAVE_RANK表示此slave已经从master复制数据的总量的排名。Rank越小代表已复制的数据越新。这种方 式下,持有最新数据的slave将会首先发起选举(理论上)。

集群脑裂数据丢失问题

什么是redis的集群脑裂?

redis的集群脑裂是指因为网络问题,导致redis master节点跟redis slave节点和sentinel集群处于不同的网络分区,此时因为sentinel集群无法感知到master的存在,所以将slave节点提升为master节点。此时存在两个不同的master节点,就像一个大脑分裂成了两个。 集群脑裂问题中,如果客户端还在基于原来的master节点继续写入数据,那么新的master节点将无法同步这些数据,当网络问题解决之后,sentinel集群将原先的master节点降为slave节点,此时再从新的master中同步数据,将会造成大量的数据丢失。

规避方法可以在redis配置里加上参数(这种方法不可能百分百避免数据丢失,参考集群leader选举机制):

 min‐replicas‐to‐write 1 //写数据成功最少需要同步的slave数量,这个数量可以模仿大于半数机制配置,比如
集群总共三个节点可以配置1,加上leader就是2,超过了半数,相当于最少半步同步才算写入成功

注意:这个配置在一定程度上会影响集群的可用性,比如slave要是少于1个,这个集群就算leader正常也不能 提供服务了,需要具体场景权衡选择。

集群是否完整才能对外提供服务

redis.conf的配置cluster-require-full-coverage默认为yes 表示集群需要完整性,某个小集群的全部节点都挂了,是客户端不能访问的,可以改为no

image.png

Redis集群为什么至少需要三个master节点,并且推荐节点数为奇数?

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

Redis集群对批量操作命令的支持

对于类似mset,mget这样的多个key的原生批量操作命令,redis集群只支持所有key落在同一slot的情况,如果根据key得到的槽位不同是会执行失败的,因为保证原子性,落在不同机器上有可能某些成功,某些失败,如 果有多个key一定要用mset命令在redis集群上操作,则可以在key的前面加上{XX},这样参数数据分片hash计 算的只会是大括号里的值,这样能确保不同的key能落到同一slot里去,示例如下: 1 mset {user1}:1:name zhuge {user1}:1:age 18 假设name和age计算的hash slot值不一样,但是这条命令在集群下执行,redis只会用大括号里的 user1 做 hash slot计算,所以算出来的slot值肯定相同,最后都能落在同一slot。

Redis高可用集群之水平扩展(增删小集群)

Redis高可用集群之水平扩展(增删小集群)