1 Star 0 Fork 0

heyifan/MarkDownNote

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
Redis.md 51.40 KB
一键复制 编辑 原始数据 按行查看 历史
yifan.he 提交于 2021-03-19 15:13 . a

Redis

基本数据类型

String

常用命令和介绍

String类型是二进制安全的。

  • set key value //存入字符串键值对
  • mset key value [key value......] //存入多个字符串键值对
  • setnx key value //存入一个不存在的支付窜键值对
  • get key //获取一个字符串值
  • mget key [key...] //根据key获取多个字符串值
  • del key [key ...] //删除一个key
  • expire key seconds //设置key的过期时间

原子加减

  • INCR key //将key对应的数字值加1
  • DECR key //将key对应的数字值减1
  • INCRBY key increment //将key上存储的数字值加上increment
  • DECR key decrement //将key上存储的数字值减去increment

应用场景

  • 单值缓存
  • 对象缓存 Set user:1 value (json格式数据)
  • 分布式锁
  • 下单减库存

​ SETNX product:10001 true //返回1代表获取锁成功 返回0代表获取锁失败

​ SET product:10001 true expire 10 seconds//防止程序意外终止而导致死锁

  • 公众号阅读量

​ INCR article:readcount:id

​ redis是个单线程应用程序 这样不会导致高并发的脏读,主从的redis 在后面会使用分布式锁,一般单体的redis并发量在9-10万左右

  • Web Session共享

​ 原理是把原有的tomcat存储用户信息转为redis 把用户的信息 序列化后 存入redis

  • 分布式系统全局序列号

​ INCRBY orderId 1000 //redis 批量生产订单id提示性能

​ 如项目使用 分库分表 ,就可以使用这个 ,目的是让主键ID 在都是唯一的 ,这个在实际场景非常重要

Map

常用命令和介绍

是一个Mapmap,指值本身又是一种键值对结构

  • HSET key field value //存储一个哈希表key的一个键值
  • HSET key field value field value //存储一个哈希表多个键值
  • HSETNX key field value //存储一个不存在的哈希表key的键值
  • HGET key field //获取一个哈希表key的一个值
  • HGET key field field .. //获取一个哈希表key的多个field值
  • HDEL key field field ..//删除一个哈希表key的多个field值
  • HLEN key //获取一个哈希表key的field数量值
  • HGETALL key //虎丘一个哈希表key的所有field和value
  • HINCRBY key field increment //为哈希表key中field键的值加上增量increment

应用场景

  • 电商购物车 :以用户id为key,商品id为field,value为商品数量

​ 添加商品 hset cart:userid goodsId 1

​ 删除商品 hdel cart:userid goodsId

​ 添加商品数量 hincyby cart:userid goodsId 1

​ 商品总数 hlen cart:userid

​ 获取购物车全部商品 hgetall cart:userId

  • 优点

​ 同类数据归类整合储存,方便数据管理

​ 相比string操作消耗内存与cpu更小

​ 相比string储存更节省空间

  • 缺点

​ 过期功能不支持到field,只能作用到key上

​ redis集群架构不适合大规模使用

hash的会分配槽位,集群中 会导致数据过于集中,没办法做分片。

List

常用命令和介绍

链表(redis 使用双端链表实现的 List),是有序的,value可以重复,可以通过下标取出对应的value值,左右两边都能进行插入和删除数据。

  • LPUSH key value value //将一个或多个值插入到key列表的表头(最左边)
  • RPUSH key 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 则一直阻塞

L-------------------------->R

**正序:**0 N

逆序 :-N -1

常用数据结构:

Statck: LPUSH+LPOP=FIFO

QUEUE: LPUSH+RPOP

Blocking QUEUE:LPUSH+BRPOP

应用场景

  • 消息队列(不推荐):使用lpop,lpush能实现队列功能,可以实现简单的点对点的消息队列,但现在有rabitMQ,kafka,顾不推荐。
  • 排行榜:lpush+larange list类型的range可以分页查看队列中的数据,可将每隔一段时间计算一次的排行榜存储在list类型中,如京东每日的手机销量排行、学校每次月考学生的成绩排名、斗鱼年终盛典主播排名等,每日计算一次,存储在list类型中,接口访问时,通过page和size分页获取打擂金曲

Set

常用命令和介绍

说明:

  1. 不允许有重复的元素,
  2. 集合中的元素是无序的,不能通过索引下标获取元素,
  3. 支持集合间的操作,可以取多个集合取交集、并集、差集

常用命令:

  • SADD key member member ... //添加元素,存在则忽略
  • SREM key member member.. //删除元素
  • SMEMBERS 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

应用场景

  • 微信抽奖

​ SADD key userId 将用户id添加进抽奖列表中

​ SMEMBERS key 查看抽奖用户列表

​ SRANDMEMBER key count /SPOP key count 随机获取count个用户id

  • 微信微博点赞,收藏,标签

​ 点赞:SADD like:消息ID 用户id

​ 取消点赞 SREM liek:消息ID 用户ID

​ 是否点赞 SISREMBER like:消息ID 用户ID

​ 获取点赞用户列表 SREMEBER like:消息ID

​ 获取点赞用户量 SCARD like:消息ID

  • 微信微博关注模型

​ 1.你关注的人

​ set guanzhu:我的id {张三、李四、王五、小明、程咬金}

​ 2.小明关注的人

​ set guanzhu:小明的id {张三、赵六、尼古拉斯}

​ 3.程咬金关注的人

​ set guanzhu:程咬金的id {小明、李四}

​ 4.我和小明的共同关注:

​ SINTER guanzhu:我的id guanzhu:小明的id

​ 得到就是 张三

​ 5.我关注的人也在关注他 【我关注的某人 否也关注小明】

​ SISMEMBER guanzhu:程咬金的id 小明的ID

​ SISMEMBER guanzhu:张三的id 小明的ID

​ SISMEMBER //判断 member 元素是否是集合 key 的成 员

6.我可能认识的人

SDIFF guanzhu:小明的id 我的ID

ZSet

常用命令和介绍

zset是一个有序集合,靠score来排序,允许score重复

常用命令:

zadd key score member 添加元素

​ zadd myzset 10 v1

zrange key start stop 查看区间元素 (0,-1全部元素)

​ zrange myzset 0 -1

zrangebyscore key min max 查看分数为min-max之间的value(只显示值),加上"("标识不包括,"+inf"标识无限制

​ zrangebyscore zset 9 10

​ zrangebyscore zset (9 10

​ zrangebyscore zset 9 +inf

zrangebyscore key min max withscores 显示值附带分数

​ zrangebyscore zset 9 10 withscores

zrem key value value... 删除一个或多个元素

​ zrem myzset v1 v2

zscard key 查看key中个数

zincryby key score value 增减元素的score

​ zincyby myzset 20 v1 -> score+20

zcount key score(start) score(stop) 获取分数之间的元素

​ zcount myzset 10 20

zrank key rember 获取rember的下标

​ zrank myzset v1

应用场景

  • 排行榜

​ 以时间戳为key,文章id为member,点击量或浏览量作为score储存,实现排行榜

  • 记录回复数

​ 以用户ID为key 文章id为member 回复加1

持久化

AOF

介绍

​ AOF通过保存Redis服务器的执行的写命令来记录数据库状态。redis服务的写命令会被写入OS Cache,每隔1s调用操作系统的fsync命令写入AOF。

redis内存文件(日志文件)是一定大的,但AOF文件是无限追加的,只有写命令就会写入AOF文件。

解决方案:AOF有rewrite。当AOF文件大小达到一定大小时,就会基于当时redis内存数据重新构建一份更加小的AOF文件,替换旧的数据。

*流程:*

  • redis 每接受一条命令就会追加日志文件
  • 将日志文件写入OS Cache中
  • 通过操作系统的fsync命令写入AOF文件

配置

appendonly yes

AOF持久化,默认是关闭的,默认是打开RDB持久化,AOF和RDB都开启了,redis重启的时候,也是优先通过AOF进行数据恢复的,因为AOF数据比较完整

fsync策略

appendfsync always
appendfsync everysec
appendfsync no
  • always: 每次写入一条数据,立即将这个数据对应的写日志fsync到磁盘上去,性能非常非常差,吞吐量很低; 确保说redis里的数据一条都不丢,选择该策略.
  • everysec: 每秒将os cache中的数据fsync到磁盘,这个最常用的,生产环境一般都这么配置,性能很高,QPS还是可以上万.
  • no: 仅仅redis负责将数据写入os cache就撒手不管了,然后后面os自己会时不时有自己的策略将数据刷入磁盘,不可控了.

rewrite流程

AOF文件膨胀到配置大小界限,触发AOF文件rewrite的命令BGREWEITEAOF。

redis进程fork一个子进程,基于redis当前的内存结构,构建新的AOF文件。

在构建新的AOF文件与旧的AOF文件没有任何关系,是基于当前redis内存数据结构,并且会对写入命令进程重构,如:多条同一个键的写入重构成一条命令。

在rewrite期间,redis会继续接受新的写入命令,并把他们缓存在重新缓存区中。

  • AOF文件膨胀到配置大小界限,触发AOF文件rewrite的命令BGREWEITEAOF。
  • redis进程fork一个子进程,基于redis当前的内存结构,构建新的AOF文件。
  • 在构建新的AOF文件与旧的AOF文件没有任何关系,是基于当前redis内存数据结构,并且会对写入命令进程重构,如:多条同一个键的写入重构成一条命令。
  • 在rewrite期间,redis会继续接受新的写入命令,并把他们缓存在重新缓存区中。
  • redis主进程收到rewrite写完信号后,把重新缓存池中的数据写入新的AOF文件,并用新的AOF文件替代旧的。删除旧的AOF文件。

rewrite相关配置

​ auto-aof-rewrite-percentage 100

​ auto-aof-rewrite-min-size 64mb

  1. auto-aof-rewrite-percentage 100:比如说上一次AOFrewrite之后,是128mb然后就会接着128mb继续写AOF的日志,如果发现增长的比例,超过了之前的100%,256mb,就可能会去触发一次rewrite。
  2. auto-aof-rewrite-min-size 64mb:达到第一个100%的条件之后,还要去跟min-size:64mb去比较,256mb > 64mb,才会去触发rewrite

RDB

介绍

RDB是redis按规定时间生成的内存数据快照,在redis启动时,会自动读取rdb文件还原数据,默认是开启的。新生成的RDB文件会将旧的文件替换掉,

//默认配置

save 900 1 
save 300 10 
save 60 10000

save n m //服务器在n秒内至少进行了m次修改(读不算)

  • 可以手动调用save或bgsave命令,同步或异步执行rdb快照生成
  • save可以有多个,就是多个快照检查点,到了检查点,就会check一下,如果有key发生了变化,就会生成新的dump.rdb文件

RDB文件生成

  • save:SAVE命令会阻塞Redis服务器进程,知道redis执行完毕,这期间服务器不能处理任何请求
  • bgsave BGSAVE命令不会阻塞Redis服务器进程,它会派生出一个子进程来进行创建RDB文件,服务器进程继续处理命令请求。
  • bgsave流程:
  • redis根据配置自己去尝试生成rdb快照文件
  • fork一个子进程
  • 子进程尝试将数据dump入到临时rdb快照文件
  • 完成rdb快照文件的生成之后,就替换之前的旧快照文件

saveparams

当redis服务启动时,会读取redis.config配置文件信息,将多个配置读入saveparam的数组中。该数组为redisServer结构的saveparams属性对应上。结构如下

redisServer |saveparams[0] |saveparams[1] |saveparams[2]|

.................... |seconds 900 | seconds 300 | seconds 60 |

saveparams----->|changs 1 | changes 10 | changes 1000|

.......................

dirty和release

  • dirty是一个计数器,记录距离上一次执行成功save或bgsave命令之后,服务器对数据库修改(新增,删除,更新)的次数,服务器执行成功一次数据库操作后,ditry就会更新。
  • release 熟悉是一个UNIX时间戳,记录了服务器上一次成功执行save和bgsave命令的时间

执行流程

redis服务的周期性函数ServerCorn默认每隔100ms就会执行一次,该函数会对运行中的服务进行维护,其中一项工作就是检查save选项设置的保存条件是否满足,满足的会执行bgsave操作。流程如下:

  1. 计算当前时间和上次执行时间差
  2. 遍历saveparams,满足条件则执行bgsave操作
  3. 更新dirty和release值

AOF和RDB的对比

优点

  • RDB会生成多个文件,每个时间端都会生成一个文件,适合做冷备,适合储存在远程的安全存储上去。
  • RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可
  • 相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速。

缺点

  • 如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据
  • RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒

企业级的实际使用策略

AOF和RDB

  1. 不要仅仅使用RDB,因为那样会导致你丢失很多数据
  2. 也不要仅仅使用AOF,因为那样有两个问题,第一,你通过AOF做冷备,没有RDB做冷备,来的恢复速度更快; 第二,RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug
  3. 综合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择; 用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复.

注意事项

  1. 默认开始RDB,AOF需要手动开启,俩者都开启,redis重启,数据恢复优先考虑AOF文件
  2. 如果RDB在执行snapshotting操作,那么redis不会执行AOF rewrite; 如果redis再执行AOF rewrite,那么就不会执行RDB snapshotting
  3. 如果RDB在执行snapshotting,此时用户执行BGREWRITEAOF命令,那么等RDB快照生成之后,才会去执行AOF rewrite。

高级数据结构

GEO

RedisGeo的存储结构

​ 其存储结构主要使用的是Redis的有序结构,其score是GeoHash的52位整数值

​ 127.0.0.1:6379> ZRANGE city 0 -1 WITHSCORES

​ 1) "hangzhou"

​ 2) "4054134257390783"

​ 3) "shanghai"

​ 4) "4054803462927619"

​ 5) "beijing"

​ 6) "4069885360207904"

Geohash原理:

​ 其原理比较容易理解,核心思想就是将球体转换为球面,区块转换为一点:

​ 主要分为三步

  • 将三维的地球变为二维的坐标
  • 在将二维的坐标转换为一维的点块
  • 最后将一维的点块转换为二进制在通过base32编码

​ 详细原理解析可以参考这边文章GeoHash核心原理解析

HyperLogLogs

BitMaps

介绍

通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身。Bitmaps 本身不是一种数据结构,实际上它就是字符串(key 对应的 value 就是上图中最后的一串二进制),但是它可以对字符串的位进行操作,利用了位运算的高性能。 Bitmaps 单独提供了一套命令,所以在 Redis 中使用 Bitmaps 和使用字符串的方法不太相同。可以把 Bitmaps 想象成一个以 位 为单位的数组,数组的每个单元只能存储01,数组的下标在Bitmaps中叫做偏移量

常用命令

语法:

新增:SETBIT key offset value key不存在时创建,字符串会进行伸展(grown)以确保它可以将 value保存在指定的偏移量上。当字符串值进行伸展时,空白位置以 0 填充 。offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内)。 对使用大的 offset 的 SETBIT 操作来说,内存分配可能造成 Redis 服务器被阻塞。 返回值:字符串值指定偏移量上原来储存的位(bit)。 示例: // SETBIT 会返回之前位的值(默认是 0)这里会生成126 个位 SETBIT testBit 125 1 (integer) 0 SETBIT testBit 125 0 (integer) 1 SETBIT testBit 125 1 (integer) 0 GETBIT testBit 125 (integer) 1 GETBIT testBit 100 (integer) 0 value 只能是 0 或者 1 二进制只能是0或者1 SETBIT testBit 618 2
(error) ERR bit is not an integer or out of range

应用场景

  • 签到
  • 登录的用户数量
  • 用户是否点赞过谋篇文章

底层数据结构

简单动态字符串(SDS)

Redis 是用 C 语言写的,但是对于Redis的字符串,却不是 C 语言中的字符串(即以空字符’\0’结尾的字符数组),它是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis的默认字符串表示。

SDS 定义

struct sdshdr{
     //记录buf数组中已使用字节的数量
     //等于 SDS 保存字符串的长度
     int len;
     //记录 buf 数组中未使用字节的数量
     int free;
     //字节数组,用于保存字符串
     char buf[];
}

SDS存储如图:

好处:

  • 常数复杂度获取字符串长度

    由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。通过 strlen key 命令可以获取 key 的字符串长度

  • 杜绝缓冲区溢出

    我们知道在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,由于__内存分配和释放策略__,在进行字符修改的时候,会首先根据记录的 free 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。

  • 内存分配释放策略

    C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。

      而对于SDS,由于len属性和free属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略:

    • 空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数,

      如果对SDS字符串修改后,len的值小于1MB,那么程序会分配和len同样大小的空间给free,此时len和free的值是相同,例如:如果SDS的字符串长度修改为15字节,那么会分配15字节空间给free,SDS的buf属性长度为15(len)+15(free)+1(空字符) = 31字节。

      如果SDS字符串修改后,len大于等于1MB,那么程序会分配1MB的空间给free。例如:SDS字符串长度修改为50MB那么程序会分配1MB的未使用空间给free,SDS的buf属性长度为 50MB(len)+1MB(free)+1byte(空字符)

    • 惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用。(当然SDS也提供了相应的API,当我们有需要时,也可以手动释放这些未使用的空间。)

链表(ListNode)

链表是一种常用的数据结构,C 语言内部是没有内置这种数据结构的实现,所以Redis自己构建了链表的实现。

链表定义:

typedef  struct listNode{
       //前置节点
       struct listNode *prev;
       //后置节点
       struct listNode *next;
       //节点的值
       void *value;  
}listNode

通过多个 listNode 结构就可以组成链表,这是一个双向链表,Redis还提供了操作链表的数据结构:

typedef struct list{
     //表头节点
     listNode *head;
     //表尾节点
     listNode *tail;
     //链表所包含的节点数量
     unsigned long len;
     //节点值复制函数
     void (*free) (void *ptr);
     //节点值释放函数
     void (*free) (void *ptr);
     //节点值对比函数
     int (*match) (void *ptr,void *key);
}list;

Redis链表特性:

  • 双端:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为O(1)。
  • 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结束。  
  • 带链表长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。
  • 多态:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值。

字典(Dict)

字典又称为符号表或者关联数组、或映射(map),是一种用于保存键值对的抽象数据结构。字典中的每一个键 key 都是唯一的,通过 key 可以对值来进行查找或修改。C 语言中没有内置这种数据结构的实现,所以字典依然是 Redis自己构建的。

Redis 的字典使用哈希表作为底层实现,定义如下:

typedef struct dictht{
     //哈希表数组
     dictEntry **table;
     //哈希表大小
     unsigned long size;
     //哈希表大小掩码,用于计算索引值
     //总是等于 size-1
     unsigned long sizemask;
     //该哈希表已有节点的数量
     unsigned long used;
}dictht

哈希表是由数组 table 组成,table 中每个元素都是指向 dict.h/dictEntry 结构,dictEntry 结构定义如下:

typedef struct dictEntry{
     //键
     void *key;
     //值
     union{
          void *val;
          uint64_tu64;
          int64_ts64;
     }v;
     //指向下一个哈希表节点,形成链表
     struct dictEntry *next;
}dictEntry

key 用来保存键,val 属性用来保存值,值可以是一个指针,也可以是uint64_t整数,也可以是int64_t整数。

  注意这里还有一个指向下一个哈希表节点的指针,我们知道哈希表最大的问题是存在哈希冲突,如何解决哈希冲突,有开放地址法和链地址法。这里采用的便是链地址法,通过next这个指针可以将多个哈希值相同的键值对连接在一起,用来解决哈希冲突

  • **哈希算法:**Redis计算哈希值和索引值方法如下:

    1、使用字典设置的哈希函数,计算键 key 的哈希值

    hash = dict->type->hashFunction(key);

    2、使用哈希表的sizemask属性和第一步得到的哈希值,计算索引值

    index = hash & dict->ht[x].sizemask;

  • **解决哈希冲突:**这个问题上面我们介绍了,方法是链地址法。通过字典里面的 *next 指针指向下一个具有相同索引值的哈希表节点.

  • **扩容和收缩:**跟HashMap一样,当装载因子(load factor)超过预定值时就会进行rehash,但当哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩。具体步骤:

    • 如果执行扩展操作,会基于原哈希表创建一个大小等于 ht[0].used*2n 的哈希表(也就是每次扩展都是根据原哈希表已使用的空间扩大一倍创建另一个哈希表)。相反如果执行的是收缩操作,每次收缩是根据已使用空间缩小一倍创建一个新的哈希表。
    • 重新利用上面的哈希算法,计算索引值,然后将键值对放到新的哈希表位置上。
    • 所有键值对都迁徙完毕后,释放原哈希表的内存空间。
  • 触发扩容的条件: 

    • 服务器目前没有执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于1。
    • 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于5。

    ps:负载因子 = 哈希表已保存节点数量 / 哈希表大小。

  • 渐近式 rehash

    扩容和收缩操作不是一次性、集中式完成的,而是分多次、渐进式完成的。如果保存在Redis中的键值对只有几个几十个,那么 rehash 操作可以瞬间完成,但是如果键值对有几百万,几千万甚至几亿,那么要一次性的进行 rehash,势必会造成Redis一段时间内不能进行别的操作。所以Redis采用渐进式 rehash,这样在进行渐进式rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行 增加操作,一定是在新的哈希表上进行的

跳跃表(SkipList)

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。查找效率堪比优化过的二叉平衡树(红黑树),且比平衡树的实现简单,查找单个key,skiplist和平衡树的时间复杂度都为O(log n)。平衡树的插入和删除操作可能引发树的旋转调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。具有如下性质:

  • 由很多层结构组成;

  • 每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点;

  • 最底层的链表包含了所有的元素;

  • 如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集);

  • 链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点;

    而SkipList是基于下图这种的链表设计出来的

这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半(上图中是7, 19, 26)。现在当查找数据的时候,可以先沿着这个新链表(**第一层链表)**进行查找。当碰到比待查数据大的节点时,再回到第二层链表进行查找。比如,要查找23,查找的路径是沿着上图中标红的指针所指向的方向进行的:整个查询路线如红色箭头。

**skiplist正是受这种多层链表的想法的启发而设计出来的,实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到O(log n)。**但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。删除数据也有同样的问题。

**skiplist为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。**比如:一个节点随机出的层数是3,那么就把它链入到第1层到第3层这三层链表中。

skiplist中一个节点的层数(level)是随机出来的,而且新插入一个节点不会影响其它节点的层数。因此,插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。这就降低了插入操作的复杂度。而**节点的层数(level)也不全是没有规则随机的,而是按照节点平均指针数目计算出来的。**如下图各个节点层数(level)是随机出来的一个skiplist,我们依然查找23,查找路径如图:

Redis中跳跃表节点定义如下:

typedef struct zskiplistNode {
     //层
     struct zskiplistLevel{
           //前进指针
           struct zskiplistNode *forward;
           //跨度
           unsigned int span;
     }level[];
     //后退指针
     struct zskiplistNode *backward;
     //分值
     double score;
     //成员对象
     robj *obj;
} zskiplistNode

多个跳跃表节点构成一个跳跃表:

typedef struct zskiplist{
     //表头节点和表尾节点
     structz skiplistNode *header, *tail;
     //表中节点的数量
     unsigned long length;
     //表中层数最大的节点的层数
     int level;
}zskiplist;

  • 搜索:从最高层的链表节点开始,如果比当前节点要大和比当前层的下一个节点要小,那么则往下找,也就是和当前层的下一层的节点的下一个节点进行比较,以此类推,一直找到最底层的最后一个节点,如果找到则返回,反之则返回空。
  • 插入:首先确定插入的层数,有一种方法是假设抛一枚硬币,如果是正面就累加,直到遇见反面为止,最后记录正面的次数作为插入的层数。当确定插入的层数k后,则需要将新元素插入到从底层到k层。
  • 删除:在各个层中找到包含指定值的节点,然后将节点从链表中删除即可,如果删除以后只剩下头尾两个节点,则删除这一层。

skiplist与平衡树、哈希表的比较

  • skiplist 和 各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。
  • 平衡树的插入 和 删除操作可能引发树的旋转调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
  • 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。
  • 从内存占用上来说,skiplist比平衡树更灵活一些。平衡树一般每个节点包含2个指针,而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于一个概率参数p。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。

整数集(intset)

intset是Redis集合的底层实现之一,当存储整数集合并且数据量较小的情况下Redis会使用intset作为set的底层实现,当数据量较大或者集合元素为字符串时则会使用dict实现set。

  • intset的数据结构定义

    typedef struct intset {
        uint32_t encoding; //intset的类型编码
        uint32_t length; //集合包含的元素数量
        int8_t contents[]; //保存元素的数组
    }
  • inset数据集合具有以下特点:

    • 所有的元素都保存在contents 数组中,且按照从小到大的顺序排列,并且不包含任何重复项。
    • intset将整数元素按顺序存储在数组里,并通过二分法降低查找元素的时间复杂度。
    • 虽然contents 数组申明成了int8_t类型,但contents数组中具体存储什么类型完全取决于encoding变量的值,类似于继承。它可以保存具体类型为int16_t、int32_t 或者int64_t 的整数值。

  • 元素升级

    当新增的元素类型比原集合元素类型的长度要大时(比如:原来是int16_t,现在新增一个int64_t的元素),需要对整数集合进行升级,才能将新元素放入整数集合中。具体步骤:

    • 根据新元素类型,扩展整数集合底层数组的大小,并为新元素分配空间。
    • 将底层数组现有的所有元素都转成与新元素相同类型的元素,并将转换后的元素放到正确的位置,放置过程中,维持整个元素顺序都是有序的。
    • 将新元素添加到整数集合中(保证有序)

    注意:升级能极大地节省内存;整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。

压缩列表(ZipList)

快速列表(QuickList)

缓存问题

缓存雪崩

发生场景

由于设置缓存时,__key都采用了相同expire__或者__更新策略__或者__数据热点__或者__缓存服务宕机__等原因,可能导致缓存数据同一时刻大规模不可用,或者都更新。 在某一时间内大量key集中失效,在高并发访问时,容易直接达到数据库,使mysql崩溃调

解决方案

  • 加互斥锁,当数据不存在时,利用sentx获取锁,成功后更新缓存,失败后重试
  • 更新策略在时间上做到比较均匀
  • 使用的热数据尽量分散到不同的机器上
  • 多台机器做主从复制或者多副本,实现高可用
  • 实现熔断限流机制,对系统进行负载能力控制
  • 在原有失效时间基础上增加一个随机值,比如1~5分钟的随机,这样每个缓存的过期时间重复率就会降低,集体失效概率也会大大降低。

缓存穿透

发生场景

大量请求穿过redis,直接访问mysql,大部分是访问不存在的值,在数据库查询值为null。

解决方案

  • 缓存空值
  • 逻辑效验
  • Bloom过滤,判断KEY是否存在

缓存击穿

发生场景

是指某一个key非常热点,大量的请求对这个key进行访问,当这个key在失效的瞬间,大量的请求会打到数据库,好像蛮力击穿一样

解决方案

  • 可以使用互斥锁避免大量请求同时落到db。
  • 布隆过滤器,判断某个容器是否在集合中
  • 可以将缓存设置永不过期(适合部分情况)
  • 做好熔断,限流 、降级,防止系统崩溃。
  • 缓存为准:使用异步线程负责维护缓存的数据,定期或根据条件触发更新,这样就不会触发更新。

事务

redis事务本质就是一组命令的集合,事务支持一次执行多个命令,一个事务中所有的命令都会被序列化(redis事务就是一次性,顺序性,排他性的执行队列中的一条条命令),但Redis的事务不具有原子性

事务错误&回滚:

  • 执行错误、

  • 入列错误,不会终止

  • 入列错误,会终止

关键词

  • MULTI 用于标记事务的开始,后续输入的命令会逐个放入执行队列中,这些命令输入后的结果总是OK,待执行EXEC命令后会原子化的执行这些命令。
  • EXEC用于序列化,顺序化的执行队列中的命令,然后恢复正常的连接状态。当使用WATCH命令监控某个key时,只有这个key没有被修改时,EXEC才会执行事务总的命令,这种方式
  • WATCH 当某个事务需要按条件执行时,需要使用该命令将制定的key监控,类似乐观锁,如果当事务执行时,发现这个key已经被修改过,那么这个事务将不会被执行,在监听key之前执行的会被回滚
  • UNWATCH 清除所有先前为一个事务监控的键。如果调用了EXEC或DISCARD,那么就不需要手动调用UNWATCh命令。
  • DISCARD 清楚事务里的队列的命令,然后恢复正常连接,相当于撤销功能。如果使用了WATCH命令,DISCARD命令就会将当前连接监控的所有键取消监控。

特性

  1. 单独的隔离性,事务中所有的命令都会序列化,按顺序执行,在执行时不会被其他的请求打断执行过程
  2. 没有隔离级别,队列中命令在没有提交前都不会被执行,也就不存在“*事务内的查询要看到事务里的更新,在事务外查询不能看到*”的问题
  3. 不能保证原子性(*事务是一个不可再分割的工作单位,事务中的操作要么都发生,要么都不发生*),redis事务中如果有一条命令执行失败了,后面的命令也会继续执行,不会回滚

IO模型

​ redis采用了多路复用的IO模型,多路复用IO模型解决了单线程多个用户阻塞访问的问题:

​ 但有一个客户端进程和服务器进程,服务器端read (sockfd1,bud,bufsize),此时客户端进程没有发送数据,那么read(阻塞调用)将阻塞直到客户端write(sockfd,but,size)发来数据。在一个客户和服务器通信时这没什么问题,当多个客户与服务器通信时,若服务器阻塞于其中一个客户sockfd1,当另一个客户的数据到达套接字sockfd2时,服务器仍不能处理,仍然阻塞在read(sockfd1,...)上。此时问题就出现了,不能及时处理另一个客户的服务。

多路复用解决思路:多路 I/O 复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗以及上下文切换的时间和性能的小号),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量

发布订阅

介绍

  1. Subscribe channel 【channel...】 订阅一个或多个频道,
  2. publish channel message 将message发送到指定的channel上,订阅该频道的客户端会接收到message
  3. **Psubscribe Pattern [Pattern......]订阅一个或多个符合给定模式的channel。每个模式以 * 作为匹配符,比如 it 匹配所有以 it 开头的频道( it.news 、 it.blog 、 it.tweets 等等), news. 匹配所有以 news. 开头的频道( news.it 、 news.global.today 等等),诸如此类。
  4. PUBSUB subcommand [argument [argument ...]] 查看订阅与发布系统状态。
  5. UNSUBSCRIBE [channel [channel ...]] 指退订给定的频道。
  6. PUNSUBSCRIBE [pattern [pattern ...]] 退订所有给定模式的频道。

原理

  1. 订阅的原理:每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 这个结构中的 pubsub_channels 属性是一个字典, 这个字典的键就用于保存所有被订阅的频道。当有客户端订阅此频道时,会将该客户端信息与此频道关联起来,存储在redisServer.pubsub_patterns链表里,如果有新的客户端订阅,依次添加。pubsub_patterns里的每个节点都包括一个redis.h/pubsubPattern结构:
typedef struct pubsubPattern {
    redisClient *client;//订阅模式的客户端信息
    robj *pattern ; //保存着被订阅的模式
} 
  1. 发布的原理:当向某个频道推送消息时,会去pubsub_channel字典里找键为当前值得频道,然后取这个频道下关联的所有客户端信息,给他们发送此消息。
  2. 退订频道:使用unsubscribe channel命令可以退订指定的频道,它是找到键对应的频道,然后删除当前的客户端的信息

哨兵

  1. 哨兵也是redisServer服务端程序,也会定时执行serverCron函数,但他只关注普通redis节点的监控和故障转移模块
  2. 默认状态下哨兵会定时(10s)向监控的master节点发送info命令,获取最新的主从配置信息,同时也会向所有的redis节点发送心跳。
  3. 当监控的master节点出现故障时,此哨兵会主观认为该master下线了,就会给其他的哨兵发送消息,当集群中其他的哨兵也认同master下线的数量达到一个值时,哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行故障切换(failover)操作,切换成功后,通过发布订阅模式,通知其他哨兵监控的从节点切换主机,这个过程称为客观下线。

主从复制

介绍

主库Master只负责写操作,从库Slave只负责读操作,一个主库可以有多个从库,但一个从库只能隶属于一个主库

全量复制

  • Slave发送同步请求,要求Master同步数据
  • Master向Slave发送run id和offset;当Slave上没有offset,执行全量复制
  • Slave执行bgsave masterinfo,保存Master的基本信息
  • Master执行bgsave生成快照
  • Master执行send RDB将快照发送到Slave的缓冲区
  • Slave将旧的RDB文件,并加载新的RDB;

开销问题

  • 生成RDB文件,即bgsave
  • RDB服务器间传输
  • 如果有AOF设置,达到重写阈值,会进行AOF重写

部分复制:

在2.8之后开始支持,可以减少全量复制的开销

​ 原理:

  • 每台机器启动后都会有一个当前进程相关的runid
  • 每个机器在写入数据后会有一个偏移量master_repl_offset
  • 通过偏移量来判断Slave和Master直接的数据差多少

​ 部分复制常用在主从连接断开,Master抖动时。

​ 流程如下:

  • 主从连接断开;
  • Slave尝试连接主句,Master写入复制缓冲区repl_back_buffer
  • 恢复连接后,Slave将自己当前runid和偏移量传输给Master,并请求同步数据
  • Master检查偏移量是否在缓冲区范围内,如果是,则进行复制,否则进行全量复制
  • 优点:部分复制直接使用缓冲区的数据进行RDB同步,相比全量复制,减少了RDB的生成和传输开销,同时,也减少了AOF重写阀值达到的几率。

三要素

  1. 服务器的运行id(run id)
  • *概念*:服务器每次运行时生成的id,用于身份识别,一台服务器每次启动时生成的运行id都是不同的。

  • *组成*:运行id由40位随机的16进制字符组成

  • *作用*:用于服务器之前传输,做身份识别

  • *实现方式*:运行id在服务器启动时自动生成,master在首次连接一个slave时,会把运行id发给slave,slave会保存这个id,通过info server命令可以查看服务器的运行id

    \2. 复制积压缓冲区

  • *概念*:复制积压缓冲区是一个先进先出的队列,用于存储服务器执行过的命令,每次命令传播,master都会将传播的命令记录在缓冲区。

  • 创建时间点:每台服务器启动时,如果有开启AOF或者被连接成为master节点,就会创建缓冲区

  • *组成*:缓冲区并不是直接把命令塞进去,而是用aof文件中记录命令的格式来存储,如命令set name jam,在缓冲区存的就是

  • $3

  • set

  • $5

  • name

  • $6

  • hyf

​ 这种格式也会把回车和换行符转义,变

​ 成"$3\r\nset\r\n$4\r\nname\r\n$3\r\njam\r\n"。另外缓

​ 冲区不仅仅只存储命令,命令在缓冲区是以字符存在

​ 的,针对每个字符都会有一个偏移量(offset)来对

​ 应,来记录字符在缓存区的位置。

​ 1523 1524 1525 1526 1527 1528 1529 15210 15211

​ $ 3 \ r \ n s e t

  • *数据来源*:运行id在服务器启动时自动生成,master在首次连接一个slave时,会把运行id发给slave,slave会保存这个id,通过info server命令可以查看服务器的运行id

​ 3. 偏移量

​ 上文可知,在缓冲区中,存在由一个偏移量,这个偏移量 是用来记录数据同步进行到的位置的。

在master中,会记录给各个slave发送的同步数据的偏移量,多少个slave就有多少个记录。

在slave中,会记录master在同步数据中发送过来的偏移量。

作用:在同步数据、对比slave与master数据的差异时,用来判断slave与master是否存在差异,如果有差异,也可以由此知道该从哪个位置开始恢复数据。

复制风暴

  1. *读写分离的问题*
  • 复制数据延迟:Slave延迟导致读写不一致。

​ 监控偏移量offset,如果超出范围就将读节点切换到

​ Master上,并重新全量复制到Slaves上

  • 读到过期数据:Redis采用懒惰性策略和采样式策略

​ 懒惰性策略是指只有操作key时才去看数据是否过

​ 期,采样式策略指定期会去采样,如果过期,自动删

​ 除。 当过期数量非常多时,采样速度比不上逻辑数据变

​ 化的速度,Slave只有读权限,不可以删除,就会出现过

​ 期数据。

​ Redis 3.2 以上版本修复此问题。

  • Slave节点故障

​ Slave节点通过持久化数据节点与主节点进行部分

​ 复制同步;Redis2.8实现Slave恢复后部分复制同步。

  • Master节点故障

​ 需要主动切换主从关系。

​ 使用Redis哨兵模式自动完成主从切换。

2.*复制风暴*

​ Master重启时,多个Slave需要复制,这个时候需要更换复制拓扑,通过在Slave下再分从机,减少主机Master的压力

配置文件

################################# REPLICATION #################################

# 复制选项,slave复制对应的master。

slaveof

# 如果master设置了requirepass,那么slave要连上master,需要有master的密码才行。

# masterauth就是用来配置master的密码,这样可以在连上master后进行认证。

# masterauth

# 当从库同主机失去连接或者复制正在进行,从机库有两种运行方式:

# 1) 如果slave-serve-stale-data设置为yes(默认设置),从库会继续响应客户端的请求。

# 2) 如果slave-serve-stale-data设置为no,除去INFO和SLAVOF命令之外的任何请求都会返回一个错误”SYNC with master in progress”。

slave-serve-stale-data yes

# 作为从服务器,默认情况下是只读的(yes)

slave-read-only yes

# 是否使用socket方式复制数据。

# 目前redis复制提供两种方式,disk和socket。

# 如果新的slave连上来或者重连的slave无法部分同步,就会执行全量同步,master会生成rdb文件。

# 有2种方式:

# 1.disk方式是master创建一个新的进程把rdb文件保存到磁盘,再把磁盘上的rdb文件传递给slave。

# 2.socket是master创建一个新的进程,直接把rdb文件以socket的方式发给slave。

# disk方式的时候,当一个rdb保存的过程中,多个slave都能共享这个rdb文件。

# socket的方式就的一个个slave顺序复制。

# 在磁盘速度缓慢,网速快的情况下推荐用socket方式。

repl-diskless-sync no

# diskless复制的延迟时间,防止设置为0。

# 一旦复制开始,节点不会再接收新slave的复制请求直到下一个rdb传输。

# 所以最好等待一段时间,等更多的slave连上来。

repl-diskless-sync-delay 5

# slave根据指定的时间间隔向服务器发送ping请求。

# 时间间隔可以通过 repl_ping_slave_period 来设置,默认10秒。

# repl-ping-slave-period 10

# 复制连接超时时间。

# master和slave都有超时时间的设置。

# master检测到slave上次发送的时间超过repl-timeout,即认为slave离线,清除该slave信息。

# slave检测到上次和master交互的时间超过repl-timeout,则认为master离线。

# 需要注意的是repl-timeout需要设置一个比repl-ping-slave-period更大的值,不然会经常检测到超时。

# repl-timeout 60

# 是否禁止复制tcp链接的tcp nodelay参数,可传递yes或者no。

# 默认是no,即使用tcp nodelay。

# 如果master设置了yes来禁止tcp nodelay设置,在把数据复制给slave的时候,会减少包的数量和更小的网络带宽。

# 但是这也可能带来数据的延迟。

# 默认我们推荐更小的延迟,但是在数据量传输很大的场景下,建议选择yes。

repl-disable-tcp-nodelay no

# 复制缓冲区大小,这是一个环形复制缓冲区,用来保存最新复制的命令。

# 这样在slave离线的时候,不需要完全复制master的数据,如果可以执行部分同步,只需要把缓冲区的部分数据复制给slave,就能恢复正常复制状态。

# 缓冲区的大小越大,slave离线的时间可以更长,复制缓冲区只有在有slave连接的时候才分配内存。

# 没有slave的一段时间,内存会被释放出来,默认1m。

# repl-backlog-size 5mb

# master没有slave一段时间会释放复制缓冲区的内存,repl-backlog-ttl用来设置该时间长度。

# 单位为秒。

# repl-backlog-ttl 3600

# 当master不可用,Sentinel会根据slave的优先级选举一个master。

# 最低的优先级的slave,当选master。

# 而配置成0,永远不会被选举。

# 注意:要实现Sentinel自动选举,至少需要2台slave。

slave-priority 100

# redis提供了可以让master停止写入的方式,如果配置了min-slaves-to-write,健康的slave的个数小于N,mater就禁止写入。

# master最少得有多少个健康的slave存活才能执行写命令。

# 这个配置虽然不能保证N个slave都一定能接收到master的写操作,但是能避免没有足够健康的slave的时候,master不能写入来避免数据丢失。

# 设置为0是关闭该功能,默认也是0。

# min-slaves-to-write 3

# 延迟小于min-slaves-max-lag秒的slave才认为是健康的slave。

# min-slaves-max-lag 10

马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Java
1
https://gitee.com/hyf216/mark-down-note.git
git@gitee.com:hyf216/mark-down-note.git
hyf216
mark-down-note
MarkDownNote
master

搜索帮助

Cb406eda 1850385 E526c682 1850385