ToB企服应用市场:ToB评测及商务社交产业平台

标题: Redis学习笔记 [打印本页]

作者: 吴旭华    时间: 2022-6-23 19:10
标题: Redis学习笔记
Redis

参考博客
https://www.cnblogs.com/beiluowuzheng/
https://www.cnblogs.com/hunternet/
如有侵权,请联系我删除,谢谢!
目录

一、什么是redis

        Redis是一个使用c语言开发的数据库。与传统数据库不同的是,Redis的数据是存储在内存中的,读写速度非常快,因此被广泛应用于缓存方向。另外,Redis除了做缓存之外,Redis也经常用来做分布式锁,甚至是消息队列。
        Redis 提供了多种数据类型来⽀持不同的业务场景,包括String,list,set,hash,sortedset。Redis 还⽀持事务 、持久化、Lua 脚本、多种集群⽅案
二、数据结构

1.1 SDS,简单动态字符串

Redis默认并未直接使用C字符串(C字符串,仅仅作为字符串字面量,用在一些无需对字符串进行修改的地方,如打印日志。)。Redis使用Struct的形式构造了一个SDS的抽象类型。当Redis需要一个可以被修改的字符串时,就会使用SDS来表示。
1.1.1 SDS底层结构

  1. struct sdshdr{
  2.     //字节数组用于保存字符串 sds遵循了c字符串以空字符结尾的惯例目的是为了重用c字符串函数库里的函数
  3.     char buf[];
  4.     //int 记录buf数组中未使用字节的数量 如上图free为0代表未使用字节的数量为0
  5.     int free;
  6.     //int 记录buf数组中已使用字节的数量即sds的长度 如上图len为5代表未使用字节的数量为5
  7.     int len;
  8.    
  9. }
复制代码
1.1.2 SDS内存重分配

在C语言中,如果对字符串进行修改,就需要面临内存重分配的情况。C字符串是有一个长度为n+1的字符数组,C字符串为静态数组,初始化后的数组长度不会改变,如果像增加字符串的长度,就需要重新分配一块更长的内存空间,否者会产生内存溢出。
1.1.3 二进制安全

为了时SDS能够保存诸如图片、音频、视频等二进制数据,Redis API使用len属性来判断字符串的长度,而不是使用空字符串作为判断依据。因此SDS是二进制安全的。但SDS依然遵循C字符串的惯例使用空字符串结尾,目的是能够复用一部分的库中的函数。
1.1.4 为什么使用SDS

1.1.5 SDS API

sdsnew创建一个包含给定C字符串的SDSO(N) ,N 为给定C字符串的长度sdsempty创建一个不包含任何内容的空SDSO(1)sdsfree释放给定的SDSO(1)sdslen返回SDS的已使用空间字节数这个值可以通过读取SDS的len属性来直接获得,复杂度为O(1)sdsavail返回SDS的未使用空间字节数这个值可以通过读取SDS的free属性来直接获得,复杂度为 O(1)sdsdup创建一个给定SDS的副本(copy)O(N),N为给定SDS的长度sdsclear清空SDS保存的字符串内容因为惰性空间释放策略,复杂度为O(1)sdscat将给定C字符串拼接到SDS字符串的末尾O(N),N为被拼接C字符串的长度sdscatsds将给定SDS字符串拼接到另一个SDS字符串的末尾O(N),N为被拼接SDS字符串的长度sdscpy将给定的C字符串复制到SDS里面,覆盖SDS原有的字符串O(N),N为被复制C字符串的长度sdsgrowzero用空字符将SDS扩展至给定长度O(N),N为扩展新增的字节数sdsrange保留SDS给定区间内的数据,不在区间内的数据会被覆盖或清除O(N),N为被保留数据的字节数sdstrim接受一个SDS和一个C字符串作为参数,从SDS左右两端分别移除所有在C字符串中出现过的字符O(M*N),M为SDS的长度,N为给定C字符串的长度sdscmp对比两个SDS字符串是否相同O(N),N为两个SDS中较短的那个SDS的长度1.1.6 Redis3.2之后的SDS

Redis3.2 之后的SDS共有五个结构体
  1. typedef char *sds;
  2. /* Note: sdshdr5 is never used, we just access the flags byte directly.
  3. * However is here to document the layout of type 5 SDS strings. */
  4. struct __attribute__ ((__packed__)) sdshdr5 {
  5.     unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
  6.     char buf[];// buf[0]: z:  0101001
  7. };
  8. struct __attribute__ ((__packed__)) sdshdr8 {
  9.     uint8_t len; /* used */
  10.     uint8_t alloc; /* excluding the header and null terminator */
  11.     unsigned char flags; /* 3 lsb of type, 5 unused bits */
  12.     char buf[];
  13. };
  14. struct __attribute__ ((__packed__)) sdshdr16 {
  15.     uint16_t len; /* used */
  16.     uint16_t alloc; /* excluding the header and null terminator */
  17.     unsigned char flags; /* 3 lsb of type, 5 unused bits */
  18.     char buf[];
  19. };
  20. struct __attribute__ ((__packed__)) sdshdr32 {
  21.     uint32_t len; /* used */
  22.     uint32_t alloc; /* excluding the header and null terminator */
  23.     unsigned char flags; /* 3 lsb of type, 5 unused bits */
  24.     char buf[];
  25. };
  26. struct __attribute__ ((__packed__)) sdshdr64 {
  27.     uint64_t len; /* used */
  28.     uint64_t alloc; /* excluding the header and null terminator */
  29.     unsigned char flags; /* 3 lsb of type, 5 unused bits */
  30.     char buf[];
  31. };
  32. #define SDS_TYPE_5  0
  33. #define SDS_TYPE_8  1
  34. #define SDS_TYPE_16 2
  35. #define SDS_TYPE_32 3
  36. #define SDS_TYPE_64 4
复制代码
Redis是如何创建SDS对象
  1. sds sdsnewlen(const void *init, size_t initlen) {
  2.     //这个指针会指向SDS起始位置
  3.     void *sh;
  4.     //sds也是一个指针,这里会指向SDS中buf的起始位置
  5.     sds s;
  6.     //根据不同的长度返回对应的SDS类型
  7.     char type = sdsReqType(initlen);
  8.     /* Empty strings are usually created in order to append. Use type 8
  9.      * since type 5 is not good at this. */
  10.     //如果判断要创建的SDS类型为5,且字符串为空串,则类型替换成SDS8
  11.     if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
  12.     //根据SDS类型获取内存占用大小,以便后续创建内存
  13.     int hdrlen = sdsHdrSize(type);
  14.     //fp用于指向SDS的flag地址
  15.     unsigned char *fp; /* flags pointer. */
  16.     //申请SDS大小+字符串长度+1的内存空间,这里+1是为了分配结束符号,C语言用\0表示字符串结束
  17.     sh = s_malloc(hdrlen+initlen+1);
  18.     //判断内存是否申请成功
  19.     if (sh == NULL) return NULL;
  20.     //判断是否处于init阶段
  21.     if (init==SDS_NOINIT)
  22.         init = NULL;
  23.     //如果不是init阶段则将申请来的内存清零
  24.     else if (!init)
  25.         memset(sh, 0, hdrlen+initlen+1);
  26.     //将s指向SDS的buf起始地址
  27.     s = (char*)sh+hdrlen;
  28.     //s指向buf的起始地址,往前一个字节即指向SDS的flag地址
  29.     fp = ((unsigned char*)s)-1;
  30.     switch(type) {
  31.         case SDS_TYPE_5: {
  32.             *fp = type | (initlen << SDS_TYPE_BITS);
  33.             break;
  34.         }
  35.         case SDS_TYPE_8: {
  36.             SDS_HDR_VAR(8,s);
  37.             sh->len = initlen;
  38.             sh->alloc = initlen;
  39.             *fp = type;
  40.             break;
  41.         }
  42.         case SDS_TYPE_16: {
  43.             //这里使用了内联方法,声明一个对应SDS类型的变量sh
  44.             //#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
  45.             SDS_HDR_VAR(16,s);
  46.             //初始化len和alloc
  47.             sh->len = initlen;
  48.             sh->alloc = initlen;
  49.             //fp指针指向的内存赋值为对应的SDS类型
  50.             *fp = type;
  51.             break;
  52.         }
  53.         case SDS_TYPE_32: {
  54.             SDS_HDR_VAR(32,s);
  55.             sh->len = initlen;
  56.             sh->alloc = initlen;
  57.             *fp = type;
  58.             break;
  59.         }
  60.         case SDS_TYPE_64: {
  61.             SDS_HDR_VAR(64,s);
  62.             sh->len = initlen;
  63.             sh->alloc = initlen;
  64.             *fp = type;
  65.             break;
  66.         }
  67.     }
  68.     //如果initlen和init都不为空,则将init指向的内存拷贝initlen个字节到buf
  69.     if (initlen && init)
  70.         memcpy(s, init, initlen);
  71.     //分配一个结束符
  72.     s[initlen] = '\0';
  73.     //返回buf起始地址
  74.     return s;
  75. }
复制代码
为什么SDS不用内存对齐

1.2 链表

1.2.1 list底层结构
  1. adlist.h
  2. //链表结点
  3. typedef struct listNode {
  4.     //前置节点
  5.     struct listNode *prev;
  6.     //后置节点
  7.     struct listNode *next;
  8.     //节点的值
  9.     void *value;
  10. } listNode;
  11. //list
  12. typedef struct list {
  13.     //表头节点
  14.     listNode *head;
  15.     //表尾节点
  16.     listNode *tail;
  17.     //节点值复制函数
  18.     void *(*dup)(void *ptr);
  19.     //节点值释放函数
  20.     void (*free)(void *ptr);
  21.     //节点值对比函数
  22.     int (*match)(void *ptr, void *key);
  23.     //链表所包含的节点数量
  24.     unsigned long len;
  25. } list;
复制代码
1.2.2 Redis的链表实现的特性

1.2.3 双向无环链表在Redis中的使用

操作\时间复杂度数组单链表双向链表rpush(从右边添加元素)O(1)O(1)O(1)lpush(从左边添加元素)0(N)O(1)O(1)lpop (从右边删除元素)O(1)O(1)O(1)rpop (从左边删除元素)O(N)O(1)O(1)lindex(获取指定索引下标的元素)O(1)O(N)O(N)len (获取长度)O(N)O(N)O(1)linsert(向某个元素前或后插入元素)O(N)O(N)O(1)lrem (删除指定元素)O(N)O(N)O(N)lset (修改指定索引下标元素)O(N)O(N)O(N)1.3 字典

数据库与哈希对象的底层实现就是字典。

1.3.1 Redis如何解决散列冲突

1.4 跳跃表

Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时, Redis就会使用跳跃表来作为有序集合健的底层实现
跳跃表在链表的基础上增加了多级索引以提升查找的效率,但其是一个空间换时间的方案,必然会带来一个问题——索引是占内存的。原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势必然会被放大,而缺点则可以忽略。
1.5 整数集合


整数集合(intset)并不是一个基础的数据结构,而是Redis自己设计的一种存储结构,是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现。各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。
1.5.1 整数集合升级

当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面。升级整数集合并添加新元素主要分三步来进行。

1.6 压缩列表

同整数集合一样压缩列表也不是基础数据结构,而是 Redis 自己设计的一种数据存储结构。它有点儿类似数组,通过一片连续的内存空间,来存储数据。不过,它跟数组不同的一点是,它允许存储的数据大小不同。

1.6.1 Redis压缩列表的构成

压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结枃。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值,如下图。

1.6.2 Redis压缩列表节点的构成


1.7 快速列表

考虑到链表的附加空间相对太高,prev 和 next 指针就要占去 16 个字节 (64bit 系统的指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率。因此Redis3.2版本开始对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist.
1.7.1 基本结构

quicklist 实际上是 zipList 和 linkedList 的混合体,它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。

三、Redis对象系统


Redis用到的所有主要数据结构,简单动态字符串(SDS)、双端链表、字典、压缩列表、整数集合、跳跃表。
Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,而每种对象又通过不同的编码映射到不同的底层数据结构。
3.1 Redis 对象类型和编码

Redis中的每个对象都由一个redisObject结构表示,该结构中和保存数据有关的三个属性分别是type属性、 encoding属性和ptr属性
  1. typedef struct redisObiect{
  2.   //type属性记录了对象的类型
  3.   unsigned type:4;
  4.   //encoding属性记录了对象使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现,
  5.   unsigned encoding:4;
  6.   //指向底层数据结构的指针
  7.   void *ptr;
  8. }
复制代码
Redis使用对象来表示数据库中的键和值,每次当我们在Redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的健(键对象),另一个对象用作键值对的值(值对象)。
3.2 优势

四、Redis对象类型

4.1 字符串

字符串对象是 Redis 中最基本的数据类型,也是我们工作中最常用的数据类型。redis中的键都是字符串对象,而且其他几种数据结构都是在字符串对象基础上构建的。字符串对象的值实际可以是字符串、数字、甚至是二进制,最大不能超过512MB 。
4.1.1 内部实现

Redis字符串对象底层的数据结构实现主要是int和简单动态字符串SDS,其通过不同的编码方式映射到不同的数据结构。字符串对象的内部编码有3种 :int、raw和embstr。Redis会根据当前值的类型和长度来决定使用哪种编码来实现。
使用embstr编码的字符串来保存短字符串值有以下好处:
4.1.2 常用操作
  1. 添加:set key value [ex seconds] [px milliseconds] [nx|xx]
复制代码
  1. 批量设置:MSET key value [key value ...]
  2. 批量获取:MGET key [key ...]
复制代码
命令描述时间复杂度`set key value [ex seconds] [px milliseconds] [nxxx]`设置值get key获取值O(1)del key [key ...]删除keyO(N)(N是键的个数)mset key [key value ...]批量设置值O(N)(N是键的个数)mget key [key ...]批量获取值O(N)(N是键的个数)incr key将 key 中储存的数字值增一O(1)decr key将 key 中储存的数字值减一O(1)incrby key increment将 key 所储存的值加上给定的增量值(increment)O(1)decrby key incrementkey 所储存的值减去给定的减量值(decrement)O(1)incrbyfloat key increment将 key 所储存的值加上给定的浮点增量值(increment)O(1)append key value如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾O(1)strlen key返回 key 所储存的字符串值的长度。O(1)setrange key offset value用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始O(1)getrange key start end返回 key 中字符串值的子字符O(N)(N是字符串的长度)4.1.3 应用场景

reids字符串的使用场景应该是最为广泛的,甚至有些对redis其它几种对象不太熟悉的人,基本所有场景都会使用字符串(序列化一下直接扔进去)。在众多的使用场景中总结一下大概分以下几种。
4.1.3.1 作为缓存层


Redis经常作为缓存层,来缓存一些热点数据。来加速读写性能从而降低后端的压力。一般在读取数据的时候会先从Redis中读取,如果Redis中没有,再从数据库中读取。在Redis作为缓存层使用的时候,必须注意一些问题,如:缓存穿透、雪崩以及缓存更新问题
4.1.3.2 计数器\限速器\分布式系统ID

计数器\限速器\分布式ID等主要是利用Redis字符串自增自减的特性。
4.1.3.3分布式系统共享session

因分布式系统通常有很多个服务,每个服务又会同时部署在多台机器上,通过负载均衡机制将将用户的访问均衡到不同服务器上。这个时候用户的请求可能分发到不同的服务器上,从而导致用户登录保存Session是在一台服务器上,而读取Session是在另一台服务器上因此会读不到Session。
这种问题通常的做法是把Session存到一个公共的地方,让每个Web服务,都去这个公共的地方存取Session。而Redis就可以是这个公共的地方。(数据库、memecache等都可以各有优缺点)。
4.2 哈希


在redis中,哈希类型是指Redis键值对中的值本身又是一个键值对结构,形如value=[{field1,value1},...{fieldN,valueN}]
4.2.1 内部实现

哈希类型的内部编码有两种:ziplist(压缩列表),hashtable(哈希表)。只有当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型。具体需要满足两个条件:
ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)
[field ...]删除一个或多个Hash的fieldO(N) N是被删除的字段数量。HEXISTS key field判断field是否存在于hash中O(1)HGET key field获取hash中field的值O(1)HGETALL key从hash中读取全部的域和值O(N) N是Hash的长度HINCRBY key field increment将hash中指定域的值增加给定的数字O(1)HINCRBYFLOAT key field increment将hash中指定域的值增加给定的浮点数O(1)HKEYS key获取hash的所有字段O(N) N是Hash的长度HLEN key获取hash里所有字段的数量O(1)HMGET key field [field ...]获取hash里面指定字段的值O(N) N是请求的字段数HMSET key field value [field value ...]设置hash字段值O(N) N是设置的字段数HSET key field value设置hash里面一个字段的值O(1)HSETNX key field value设置hash的一个字段,只有当这个字段不存在时有效O(1)HSTRLEN key field获取hash里面指定field的长度O(1)HVALS key获得hash的所有值O(N) N是Hash的长度HSCAN key cursor [MATCH pattern] [COUNT count]迭代hash里面的元素4.2.2 应用场景


4.3 列表List


列表(list)类型是用来存储多个有序的字符串,列表中的每个字符串称为元素(element),一个列表最多可以存储232-1个元素。在Redis中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。
4.3.1 内部实现

在Redis3.2版本以前列表类型的内部编码有两种。
而在Redis3.2版本开始对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist.
命令说明时间复杂度BLPOP key [key ...] timeout删除,并获得该列表中的第一元素,或阻塞,直到有一个可用O(1)BRPOP key [key ...] timeout删除,并获得该列表中的最后一个元素,或阻塞,直到有一个可用O(1)BRPOPLPUSH source destination timeout弹出一个列表的值,将它推到另一个列表,并返回它;或阻塞,直到有一个可用O(1)LINDEX key index获取一个元素,通过其索引列表O(N)LINSERT key BEFOREAFTER pivot value在列表中的另一个元素之前或之后插入一个元素O(N)LLEN key获得队列(List)的长度O(1)LPOP key从队列的左边出队一个元素O(1)LPUSH key value [value ...]从队列的左边入队一个或多个元素O(1)LPUSHX key value当队列存在时,从队到左边入队一个元素O(1)LRANGE key start stop从列表中获取指定返回的元素O(S+N)LREM key count value从列表中删除元素O(N)LSET key index value设置队列里面一个元素的值O(N)LTRIM key start stop修剪到指定范围内的清单O(N)RPOP key从队列的右边出队一个元O(1)RPOPLPUSH source destination删除列表中的最后一个元素,将其追加到另一个列表O(1)RPUSH key value [value ...]从队列的右边入队一个元素O(1)RPUSHX key value从队列的右边入队一个元素,仅队列存在时有效O(1)4.3.2 应用场景

4.4 集合set

Set 是一个无序并唯一的键值集合。它的存储顺序不会按照插入的先后顺序进行存储。
集合类型和列表类型的区别如下:
Set可以存储232-1个元素。Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题。
4.4.1 内部实现

集合类型的内部编码有两种:
4.4.2 使用场景

集合的主要几个特性,无序、不可重复、支持并交差等操作。因此集合类型比较适合用来数据去重和保障数据的唯一性,还可以用来统计多个集合的交集、错集和并集等,当我们存储的数据是无序并且需要去重的情况下,比较适合使用集合类型进行存储。
4.5 有序集合 sorted set


有序集合类型 (Sorted Set或ZSet) 相比于集合类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。
4.5.1 内部实现

有序集合是由ziplist或 skiplist组成的。
当数据比较少时,有序集合使用的是 ziplist 存储的,有序集合使用 ziplist 格式存储必须满足以下两个条件:
4.5.2 使用场景

五、类型检查与命令多态

Redis中用于操作键的命令基本上可以分为两种类型:
5.1 类型检查和实现

为了确保只有指定类型的键可以执行某些特定的命令,在执行一个类型特定的命令前,Redis会先检查输入键的类型是否正确,然后再决定是否执行给定的命令。类型特定命令所进行的类型检查是通过redisObject结构的type属性来实现的:
5.2 多态命令的实现


Redis除了会根据值对象的类型来判断键是否能够执行指令外,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令。举个栗子,列表对象有ziplist和linkedlist两种编码可用,其中前者使用压缩列表API来实现列表命令,而后者使用双端链表来实现列表命令
现在,考虑这样一个情况,如果我们对一个键执行LLEN命令,那么服务器除了要确保执行命令的是列表键之外,还要根据键的值对象所使用的编码方式来选择正确的LLEN命令实现:
借用面向对象方面的术语来说,我们可以认为LLEN命令时多态的,只要执行LLEN命令的是列表键,那么无论值对象时压缩列表还是双端链表,命令都可以正常执行。实际上,我们可以将DEL、EXPIRE、TYPE等命令也称为多态命令,因为无论输入的键是什么类型,这些命令都可以正确地执行。
DEL、EXPIRE等命令和LLEN等命令的区别在于,
六、内存回收

因为C语言并不具备自动回收内存的功能 ,所以Redis在自己对的对象系统中构建了一个引用计数技术来实现自动回收内存,通过这一机制,程序可以跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。每个对象的引用计数信息由redisObject结构的refcount属性记录:
  1. typedef struct redisObject {
  2.     unsigned type:4;/* Not used */
  3.     unsigned encoding:4;
  4.     unsigned lru:22;   
  5.     //引用计数
  6.     int refcount;
  7.     void *ptr;
  8. } robj;
复制代码
函数作用incrRefCount将对象的引用计数值增一decrRefCount将对象的引用计数值减一,当对象的引用计数值等于0时,释放对象resetRefCount将对象的引用计数值设置为0,但并不释放对象,这个函数通常在需要重新设置对象的引用计数值时使用
七 对象共享

除了用于实现引用计数内存回收机制之外,对象的引用计数属性还带有对象共享的作用。

在Redis中,让更多键共享一个值对象需要执行以下两个步骤:
目前来说,Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串时,服务器就会使用这些共享对象,而不是新创建对象。另外,创建共享字符串的数量可以通过修改redis.h/OBJ_SHARED_INTEGERS来修改
为什么Redis不共享包含字符串的对象?当服务器考虑将一个共享对象设置为键的值对象时,程序需要先检查给定的共享对象和键想创建的目标对象是否相同,只有在共享对象和目标对象完全相同的情况下,程序才会将共享对象用作键的值对象,而一个共享对象保存的值越复杂,验证共享对象和目标对象相同所需的复杂度就会越高,消耗CPU时间也越多:
因此,尽管共享更复杂的对象可以节约更多内存,但受到CPU时间的限制,Redis只对包含整数值的字符串对象进行共享
八、对象的空转时长

除了前面介绍过的type、encoding、ptr和refcount四个属性外,redisObject结构包含的最后一个属性为lru属性,该属性记录了对象最后一次被命令程序访问的时间:
  1. typedef struct redisObject {
  2.     unsigned type:4;
  3.     unsigned notused:2;   
  4.     unsigned encoding:4;
  5.     unsigned lru:22;      
  6.     int refcount;
  7.     void *ptr;
  8. } robj;
复制代码
OBJECT IDLETIME命令可以打印出给定键的空转时长,这一空转时长就是通过将当前时间减去键对象的lru时间计算得出。
OBJECT IDLETIME命令的实现是特殊的,这个命令在访问键时不会修改lru属性。除了可以被OBJECT IDLETIME命令打印出来之外,键的空转时长还有另外一项作用:如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru(仅对设置了过期时间的键采取LRU淘汰)或者allkeys-lru(对所有的键都采取LRU淘汰),那么当服务器占用的内存超过maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存
九、数据淘汰策略

9.1 过期淘汰策略

若果采用定时删除策略,当数据库中含有太多的过期数据,则会占用太多CPU时间,影响服务器响应时间和吞吐量,甚至造成堵塞。
9.1.3 惰性删除

Redis 4.0 增加了懒惰删除功能,懒惰删除需要使用异步线程对已删除的节点进行内存回收,这意味着 Redis 底层其实并不是单线程,它内部还有几个额外的鲜为人知的辅助线程。
这几个辅助线程在 Redis 内部有一个特别的名称,就是“BIO”,全称是 Background IO,意思是在背后默默干活的 IO 线程。
Redis 大佬 Antirez 实现懒惰删除时,它并不是一开始就想到了异步线程。它最初的尝试是在主线程里,使用类似于字典渐进式搬迁的方式来实现渐进式删除回收。
异步线程释放内存不用为每种数据结构适配一套渐进式释放策略,也不用搞个自适应算法来仔细控制回收频率,只是将对象从全局字典中摘掉,然后往队列里一扔,主线程就干别的去了。异步线程从队列里取出对象来,直接走正常的同步释放逻辑就可以了。
9.1.4 定期删除

定期删除策略的难点是确定删除操作执行的时长和效率
Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,默认每 100ms 进行一次过期扫描:

9.2 缓存淘汰策略

可以设置内存最大使用量maxmemory,当内存使用量超出时,会施行数据淘汰策略。
Redis 具体有 6 种淘汰策略,配置 maxmemory-policy
策略描述volatile-lru从已设置过期时间的数据集中挑选最近最少使用的数据淘汰volatile-ttl从已设置过期时间的数据集中挑选将要过期的数据淘汰volatile-random从已设置过期时间的数据集中任意选择数据淘汰allkeys-lru从所有数据集中挑选最近最少使用的数据淘汰allkeys-random从所有数据集中任意选择数据进行淘汰volatile-lfu对有过期时间的key采用LFU淘汰算法allkeys-lfu对全部key采用LFU淘汰算法noeviction禁止驱逐数据9.1.2 Redis的LRU算法 (Least recently used) 最近最少使用

实现 LRU 算法除了需要 key/value 字典外,还需要附加一个链表,链表中的元素按照一定的顺序进行排列。当空间满的时候,会踢掉链表尾部的元素。当字典的某个元素被访问时,它在链表中的位置会被移动到表头。所以链表的元素排列顺序就是元素最近被访问的时间顺序。
Redis中的LRU与常规的LRU实现并不相同,常规LRU会准确的淘汰掉队头的元素,但是Redis的LRU并不维护队列。只是根据配置的策略
9.1.3 LFU (Least frequently used) 最不经常使用

LFU是在Redis4.0后出现的,LRU的最近最少使用实际上并不精确。LFU 表示按最近的访问频率进行淘汰,它比 LRU 更加精准地表示了一个 key 被访问的热度。
在 LFU 模式下,lru 字段 24 个 bit 用来存储两个值,分别是 ldt(last decrement time) 和 logc(logistic counter)。
十、持久化

10.1 RDB持久化

因为Redis是内存数据库,它将自己的数据库状态存储在内存里面,所以如果不想办法将存储在内存中的数据库状态保存到磁盘中,那么一旦服务器进程退出,服务器中的数据库状态也会消失。为了解决这个问题,Redis提供了RDB持久化功能,可以将Redis内存中的数据库状态保存到磁盘中,避免数据意外丢失
10.1.1 是什么

在指定的时间间隔内将内存中的数据集快照写⼊磁盘, 也就是⾏话讲的 Snapshot 快照,它恢复时是将快照⽂件直接读到内存⾥
10.1.2 持久化流程


10.1.3 save vs bgsave

10.1.4 BGSAVE命令执行时的服务器状态

10.1.5 自动间隔性保存

因为BGSAVE命令可以在不阻塞服务器的情况下执行,所以Redis允许用户通过没设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。用户可以通过save选项设置多个保存条件,但只要其中一个条件被满足,服务器就会执行BGSAVE命令。

10.1.6 优缺点

10.2 AOF持久化

10.2.1 是什么

日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
10.2.2 持久化流程

(1)客户端的请求写命令会被append追加到AOF缓冲区内;
(2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
(3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
(4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
10.2.3 重写流程

如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。
(1)bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。
(3)子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
(4)1).子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
(5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。
10.2.4 优缺点

十一、文件事件

Redis是基于Reactor模式开发了自己的网络事件处理器,这个处理器被称为文件事件处理器:
虽然文件事件处理器以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接,这保持了Redis内部单线程设计的简单性
11.1文件事件处理器的构成

文件事件处理器的四个组成部分,它们分别是套接字、I/O多路复用程序、文件事件分派器,以及事件处理器

尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都放到一个队列中,然后通过这个队列,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的实践被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕),I/O多路复用程序才会继续向文件事件分派器传送下一个套接字
十二、主从复制

主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主
12.1 新版复制功能的实现

为了解决旧版复制功能在处理断线重复制情况时的低效问题,Redis从2.8版本开始,使用PSYNC命令代替SYNC命令来执行复制时的同步操作。PSYNC命令具有完整同步和部分同步两种模式:
部分重同步由以下三个部分构成:
十三、Redis哨兵高可用框架

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

十四 集群

Redis高可用集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。
Redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能,只需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。Redis集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置简单。高可用集群相较于哨兵集群,至少不会出现主节点下线后,整个集群在一段时间内处于不可用状态,直到选举出主节点。因为高可用集群有多个主节点,当我们需要向整个Redis服务写入大批量数据时,数据会根据写入的key算出一个hash值,将数据落地到不同的主节点上,所以当一个主节点下线后,落地到其他主节点的写请求还是正常的。

十五 redis引用问题解决

15.1 缓存穿透

15.2 缓存击穿

15.3 缓存雪崩


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4