一文弄懂Redis基本理论

一、Redis:

官网:https://redis.io/
中文网:http://www.redis.net.cn/

简介

Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。
Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。
Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
Redis支持数据的备份,即master-slave模式的数据备份。
Redis 的主要功能都基于单线程模型实现,也就是说 Redis 使用一个线程来服务所有的客户端请求,同时 Redis 采用了非阻塞式 IO多路复用技术,并精细地优化各种命令的算法时间复杂度,这些信息意味着:

  • Redis 是线程安全的(因为只有一个线程),其所有操作都是原子的,不会因并发产生数据异常
  • Redis 的速度非常快(因为使用非阻塞式 IO,且大部分命令的算法时间复杂度都是 O(1))
  • 使用高耗时的 Redis 命令是很危险的,会占用唯一的一个线程的大量处理时间,导致所有的请求都被拖慢。(例如时间复杂度为 O(N) 的 KEYS 命令,严格禁止在生产环境中使用)

Redis 优势

  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

Redis内存占用参考

To give you a few examples (all obtained using 64-bit instances):

  • An empty instance uses ~ 3MB of memory.
  • 1 Million small Keys -> String Value pairs use ~ 85MB of memory.
  • 1 Million Keys -> Hash value, representing an object with 5 fields, use ~ 160 MB of memory.

参考:https://redis.io/topics/faq#redis-is-single-threaded-how-can-i-exploit-multiple-cpu–cores

二、Redis为什么那么快?

  • 1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
  • 2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
  • 3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,采用了线程封闭的观念,把任务封闭在一个线程,自然避免了线程安全问题,因此也就没有了加锁解锁导致可能出现死锁而导致的性能消耗;
    我们首先要明白,上边的种种分析,都是为了营造一个Redis很快的氛围!官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)

但是,我们使用单线程的方式是无法发挥多核CPU 性能,不过我们可以通过在单机开多个Redis 实例来完善!

  • 4、使用多路I/O复用模型,非阻塞IO;
    这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,原理可以参考另一篇笔记:《学习笔记之IO多路复用模型》
  • 5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

参考:https://www.cnblogs.com/qwangxiao/p/8535202.html

三、Redis数据结构和底层存储

类型简介特性场景
String(字符串)二进制安全可以包含任何数据,比如jpg图片或者序列化的对象,一个键最大能存储512M
List(列表)链表(双向链表)增删快,提供了操作某一段元素的API1,最新消息排行等功能(比如朋友圈的时间线) 2,消息队列
Hash(字典)键值对集合,即编程语言中的Map类型键值对集合,即编程语言中的Map类型适合存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值存储、读取、修改用户属性
Set(集合)哈希表实现,元素不重复1、添加、删除,查找的复杂度都是O(1) 2、为集合提供了求交集、并集、差集等操作1、共同好友 2、利用唯一性,统计访问网站的所有独立ip 3、好友推荐时,根据tag求交集,大于某个阈值就可以推荐
Sorted Set(有序集合)将Set中的元素增加一个权重参数score,元素按score有序排列数据插入集合时,已经进行天然排序1、排行榜 2、带权重的消息队列

1、String(字符串)

String 是 Redis 的基础数据类型,Redis 没有 Int、Float、Boolean 等数据类型的概念,所有的基本类型在 Redis 中都以 String 体现。
string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。
string类型是Redis最基本的数据类型,一个键最大能存储512MB。
下图是执行 set hello world 时,所涉及到的数据模型:
在这里插入图片描述

  • dictEntry:Redis 是 Key-Value 数据库,因此对每个键值对都会有一个 dictEntry,里面存储了指向 Key 和 Value 的指针;next 指向下一个 dictEntry,与本 Key-Value 无关。
  • Key:图中右上角可见,Key(”hello”)并不是直接以字符串存储,而是存储在 SDS 结构中。
  • RedisObject:Value(“world”)既不是直接以字符串存储,也不是像 Key 一样直接存储在 SDS 中,而是存储在 RedisObject 中。

实际上,不论 Value 是 5 种类型的哪一种,都是通过 RedisObject 来存储的;而 RedisObject 中的 type 字段指明了 Value 对象的类型,ptr 字段则指向对象所在的地址。
不过可以看出,字符串对象虽然经过了 RedisObject 的包装,但仍然需要通过 SDS 存储。
字符串类型的内部编码有3种,它们的应用场景如下:

  • int:8个字节的长整型。字符串值是整型时,这个值使用long整型表示。
  • embstr:<=39字节的字符串。 embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。 因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。
  • raw:大于39个字节的字符串

其编码转换过程为:
当int数据不再是整数,或大小超过了long的范围时,自动转化为raw。
对于embstr,由于其实现是只读的,因此在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了39个字节。

String常用命令:

  • SET:为一个 key 设置 value,可以配合 EX/PX 参数指定 key 的有效期,通过 NX/XX 参数针对 key 是否存在的情况进行区别操作,时间复杂度 O(1)
  • GET:获取某个 key 对应的 value,时间复杂度 O(1)
  • GETSET:为一个 key 设置 value,并返回该 key 的原 value,时间复杂度 O(1)
  • MSET:为多个 key 设置 value,时间复杂度 O(N)
  • MSETNX:同 MSET,如果指定的 key 中有任意一个已存在,则不进行任何操作,时间复杂度 O(N)
  • MGET:获取多个 key 对应的 value,时间复杂度 O(N)

上文提到过,Redis 的基本数据类型只有 String,但 Redis 可以把 String 作为整型或浮点型数字来使用,主要体现在 INCR、DECR 类的命令上:

  • INCR:将 key 对应的 value 值自增 1,并返回自增后的值。只对可以转换为整型的 String 数据起作用。时间复杂度 O(1)
  • INCRBY:将 key 对应的 value 值自增指定的整型数值,并返回自增后的值。只对可以转换为整型的 String 数据起作用。时间复杂度 O(1)
  • DECR/DECRBY:同 INCR/INCRBY,自增改为自减。
  • INCR/DECR 系列命令要求操作的 value 类型为 String,并可以转换为 64 位带符号的整型数字,否则会返回错误。
    也就是说,进行 INCR/DECR 系列命令的 value,必须在 [-2^63 ~ 2^63 - 1] 范围内。
    前文提到过,Redis 采用单线程模型,天然是线程安全的,这使得 INCR/DECR 命令可以非常便利的实现高并发场景下的精确控制。

2、List(列表)

列表(list)用来存储多个有序的字符串,每个字符串称为元素;一个列表可以存储 2^32-1 个元素。(4294967295, 每个列表超过40亿个元素)。
Redis 中的列表支持两端插入和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队列、栈等。
列表的内部编码可以是压缩列表(ziplist)或双端链表(linkedlist)。
双端链表: 由一个list结构和多个listNode结构组成;典型结构如下图所示:
在这里插入图片描述
通过图中可以看出,双端链表同时保存了表头指针和表尾指针,并且每个节点都有指向前和指向后的指针;链表中保存了列表的长度;dup、free和match为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。而链表中每个节点指向的是type为字符串的redisObject。
压缩列表: 压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块(而不是像双端链表一样每个节点是指针)组成的顺序型数据结构;具体结构相对比较复杂,略。与双端链表相比,压缩列表可以节省内存空间,但是进行修改或增删操作时,复杂度较高;因此当节点数量较少时,可以使用压缩列表;但是节点数量多时,还是使用双端链表划算。

其编码转换过程为:
只有同时满足下面两个条件时,才会使用压缩列表:列表中元素数量小于512个;列表中所有字符串对象都不足64字节。
如果有一个条件不满足,则使用双端列表;且编码只可能由压缩列表转化为双端链表,反方向则不可能。

List 相关的常用命令

  • LPUSH:向指定 List 的左侧(即头部)插入 1 个或多个元素,返回插入后的 List 长度。时间复杂度 O(N),N 为插入元素的数量
  • RPUSH:同 LPUSH,向指定 List 的右侧(即尾部)插入 1 或多个元素
  • LPOP:从指定 List 的左侧(即头部)移除一个元素并返回,时间复杂度 O(1)
  • RPOP:同 LPOP,从指定 List 的右侧(即尾部)移除 1 个元素并返回
  • LPUSHX/RPUSHX:与 LPUSH/RPUSH 类似,区别在于,LPUSHX/RPUSHX 操作的 key 如果不存在,则不会进行任何操作
  • LLEN:返回指定 List 的长度,时间复杂度 O(1)
  • LRANGE:返回指定 List 中指定范围的元素(双端包含,即 LRANGE key 0 10 会返回 11 个元素),时间复杂度 O(N)。应尽可能控制一次获取的元素数量,一次获取过大范围的 List 元素会导致延迟,同时对长度不可预知的 List,避免使用 LRANGE key 0 -1 这样的完整遍历操作。

应谨慎使用的 List 相关命令:

  • LINDEX:返回指定 List 指定 index 上的元素,如果 index 越界,返回 nil。index 数值是回环的,即 - 1 代表 List 最后一个位置,-2 代表 List 倒数第二个位置。时间复杂度 O(N)
  • LSET:将指定 List 指定 index 上的元素设置为 value,如果 index 越界则返回错误,时间复杂度 O(N),如果操作的是头 / 尾部的元素,则时间复杂度为 O(1)
  • LINSERT:向指定 List 中指定元素之前 / 之后插入一个新元素,并返回操作后的 List 长度。如果指定的元素不存在,返回 - 1。如果指定 key 不存在,不会进行任何操作,时间复杂度 O(N)

由于 Redis 的 List 是链表结构的,上述的三个命令的算法效率较低,需要对 List 进行遍历,命令的耗时无法预估,在 List 长度大的情况下耗时会明显增加,应谨慎使用。

3、Hash(字典)

Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
Redis 中每个 hash 可以存储 2^32 - 1 键值对(4294967295, 每个列表超过40亿个元素)。

内层的哈希使用的内部编码可以是压缩列表(ziplist)和哈希表(hashtable)2 种,但对外暴露的都哈希则是只使用hashtable.

压缩列表前面已介绍。与哈希表相比,压缩列表用于元素个数少、元素长度小的场景;其优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(n)变为了O(1),但由于哈希中元素数量较少,因此操作的时间并没有明显劣势。
hashtable:一个hashtable由1个dict结构、2个dictht结构、1个dictEntry指针数组(称为bucket)和多个dictEntry结构组成。
正常情况下(即hashtable没有进行rehash时)各部分关系如下图所示:
在这里插入图片描述
下面从底层向上依次介绍:

  • dictEntry:结构用于保存键值对,其结构跟String的结构定义一样。
  • bucket:是一个数组,数组的每个元素都是指向dictEntry结构的指针。注:redis中bucket数组的大小计算规则如下:大于dictEntry的、最小的2^n;例如,如果有1000个dictEntry,那么bucket大小为1024;如果有1500个dictEntry,则bucket大小为2048。
  • dictht:各属性介绍如:

table属性是一个指针,指向bucket;
size属性记录了哈希表的大小,即bucket的大小;
used记录了已使用的dictEntry的数量;
sizemask属性的值总是为size-1,这个属性和哈希值一起决定一个键在table中存储的位置。

  • dict: 一般来说,通过使用 dictht 和 dictEntry 结构,便可以实现普通哈希表的功能。但是 Redis 的实现中,在 dictht 结构的上层,还有一个 dict 结构。下面说明 dict 结构的定义及作用。dict结构如下图:

其中:type 属性和 privdata 属性是为了适应不同类型的键值对,用于创建多态字典。ht 属性和 trehashidx 属性则用于 rehash,即当哈希表需要扩展或收缩时使用。
ht 是一个包含两个项的数组,每项都指向一个 dictht 结构,这也是 Redis 的哈希会有 1 个 dict、2 个 dictht 结构的原因。
通常情况下,所有的数据都是存在放 dict 的 ht[0] 中,ht[1] 只在 rehash 的时候使用。
dict 进行 rehash 操作的时候,将 ht[0] 中的所有数据 rehash 到 ht[1] 中。然后将 ht[1] 赋值给 ht[0],并清空 ht[1]。
因此,Redis 中的哈希之所以在 dictht 和 dictEntry 结构之外还有一个 dict 结构,一方面是为了适应不同类型的键值对,另一方面是为了 rehash。

其编码转换过程为:
Redis 中内层的哈希既可能使用哈希表,也可能使用压缩列表。
只有同时满足下面两个条件时,才会使用压缩列表:哈希中元素数量小于 512 个;哈希中所有键值对的键和值字符串长度都小于 64 字节。
如果有一个条件不满足,则使用哈希表;且编码只可能由压缩列表转化为哈希表,反方向则不可能。
Hash 相关的常用命令:

  • HSET:将 key 对应的 Hash 中的 field 设置为 value。如果该 Hash 不存在,会自动创建一个。时间复杂度 O(1)
  • HGET:返回指定 Hash 中 field 字段的值,时间复杂度 O(1)- HMSET/HMGET:同 HSET 和 HGET,可以批量操作同一个 key 下的多个 field,时间复杂度:O(N),N 为一次操作的 field 数量- HSETNX:同 HSET,但如 field 已经存在,HSETNX 不会进行任何操作,时间复杂度 O(1)- HEXISTS:判断指定 Hash 中 field 是否存在,存在返回 1,不存在返回 0,时间复杂度 O(1)- HDEL:删除指定 Hash 中的 field(1 个或多个),时间复杂度:O(N),N 为操作的 field 数量- HINCRBY:同 INCRBY 命令,对指定 Hash 中的一个 field 进行 INCRBY,时间复杂度 O(1)

应谨慎使用的 Hash 相关命令:

  • HGETALL:返回指定 Hash 中所有的 field-value 对。返回结果为数组,数组中 field 和 value 交替出现。时间复杂度 O(N)
  • HKEYS/HVALS:返回指定 Hash 中所有的 field/value,时间复杂度 O(N)

上述三个命令都会对 Hash 进行完整遍历,Hash 中的 field 数量与命令的耗时线性相关,对于尺寸不可预知的 Hash,应严格避免使用上面三个命令,而改为使用 HSCAN 命令进行游标式的遍历,

4、Set(集合)

Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据,另外Set不能通过索引来操作元素。
集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。

集合的内部编码可以是整数集合(intset)或哈希表(hashtable)。
哈希表前面已经讲过,这里略过不提;需要注意的是,集合在使用哈希表时,值全部被置为 null。

整数集合适用于集合所有元素都是整数且集合元素数量较小的时候,与哈希表相比,整数集合的优势在于集中存储,节省空间。

其编码转换过程为:
只有同时满足下面两个条件时,集合才会使用整数集合:集合中元素数量小于512个;集合中所有元素都是整数值。如果有一个条件不满足,则使用哈希表;且编码只可能由整数集合转化为哈希表,反方向则不可能。

Set 相关的常用命令:

  • SADD:向指定 Set 中添加 1 个或多个 member,如果指定 Set 不存在,会自动创建一个。时间复杂度 O(N),N 为添加的 member 个数
  • SREM:从指定 Set 中移除 1 个或多个 member,时间复杂度 O(N),N 为移除的 member 个数
  • SRANDMEMBER:从指定 Set 中随机返回 1 个或多个 member,时间复杂度 O(N),N 为返回的 member 个数
  • SPOP:从指定 Set 中随机移除并返回 count 个 member,时间复杂度 O(N),N 为移除的 member 个数
  • SCARD:返回指定 Set 中的 member 个数,时间复杂度 O(1)
  • SISMEMBER:判断指定的 value 是否存在于指定 Set 中,时间复杂度 O(1)
  • SMOVE:将指定 member 从一个 Set 移至另一个 Set

慎用的 Set 相关命令:

  • SMEMBERS:返回指定 Hash 中所有的 member,时间复杂度 O(N)
  • SUNION/SUNIONSTORE:计算多个 Set 的并集并返回 / 存储至另一个 Set 中,时间复杂度 O(N),N 为参与计算的所有集合的总 member 数
  • SINTER/SINTERSTORE:计算多个 Set 的交集并返回 / 存储至另一个 Set 中,时间复杂度 O(N),N 为参与计算的所有集合的总 member 数
  • SDIFF/SDIFFSTORE:计算 1 个 Set 与 1 或多个 Set 的差集并返回 / 存储至另一个 Set 中,时间复杂度 O(N),N 为参与计算的所有集合的总 member 数。

上述几个命令涉及的计算量大,应谨慎使用,特别是在参与计算的 Set 尺寸不可知的情况下,应严格避免使用。可以考虑通过 SSCAN 命令遍历获取相关 Set 的全部 member

5、Sorted Set(有序集合)

Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。Sorted Set 非常适合用于实现排名。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。

有序集合的内部编码可以是压缩列表(ziplist)或跳跃表(skiplist)。 ziplist在列表和哈希中都有使用,前面已经讲过,这里略过不提。
跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
除了跳跃表,实现有序数据结构的另一种典型实现是平衡树;
大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此redis中选用跳跃表代替平衡树。跳跃表支持平均O(logN)、最坏O(N)的复杂点进行节点查找,并支持顺序操作。

其编码转换过程为:
只有同时满足下面两个条件时,才会使用压缩列表:有序集合中元素数量小于128个;有序集合中所有成员长度都不足64字节。如果有一个条件不满足,则使用跳跃表;且编码只可能由压缩列表转化为跳跃表,反方向则不可能。

Sorted Set 的主要命令:

  • ZADD:向指定 Sorted Set 中添加 1 个或多个 member,时间复杂度 O(Mlog(N)),M 为添加的 member 数量,N 为 Sorted Set 中的 member 数量
  • ZREM:从指定 Sorted Set 中删除 1 个或多个 member,时间复杂度 O(Mlog(N)),M 为删除的 member 数量,N 为 Sorted Set 中的 member 数量
  • ZCOUNT:返回指定 Sorted Set 中指定 score 范围内的 member 数量,时间复杂度:O(log(N))
  • ZCARD:返回指定 Sorted Set 中的 member 数量,时间复杂度 O(1)
  • ZSCORE:返回指定 Sorted Set 中指定 member 的 score,时间复杂度 O(1)
  • ZRANK/ZREVRANK:返回指定 member 在 Sorted Set 中的排名,ZRANK 返回按升序排序的排名,ZREVRANK 则返回按降序排序的排名。时间复杂度 O(log(N))
  • ZINCRBY:同 INCRBY,对指定 Sorted Set 中的指定 member 的 score 进行自增,时间复杂度 O(log(N))

慎用的 Sorted Set 相关命令:

  • ZRANGE/ZREVRANGE:返回指定 Sorted Set 中指定排名范围内的所有 member,ZRANGE 为按 score 升序排序,ZREVRANGE 为按 score 降序排序,时间复杂度 O(log(N)+M),M 为本次返回的 member 数
  • ZRANGEBYSCORE/ZREVRANGEBYSCORE:返回指定 Sorted Set 中指定 score 范围内的所有 member,返回结果以升序 / 降序排序,min 和 max 可以指定为 - inf 和 + inf,代表返回所有的 member。时间复杂度 O(log(N)+M)
  • ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除 Sorted Set 中指定排名范围 / 指定 score 范围内的所有 member。时间复杂度 O(log(N)+M)

上述几个命令,应尽量避免传递 [0 -1] 或 [-inf +inf] 这样的参数,来对 Sorted Set 做一次性的完整遍历,特别是在 Sorted Set 的尺寸不可预知的情况下。可以通过 ZSCAN 命令来进行游标式的遍历(具体请见 https://redis.io/commands/scan ),或通过 LIMIT 参数来限制返回 member 的数量(适用于 ZRANGEBYSCORE 和 ZREVRANGEBYSCORE 命令),以实现游标式的遍历。

参考:
http://www.runoob.com/redis/redis-data-types.html
https://www.sohu.com/a/250213991_262549
https://cloud.tencent.com/developer/article/1116044
http://www.redis.net.cn/order/

四、redis事务

Redis 通过 MULTI 、 DISCARD 、 EXEC 、 WATCH和UNWATCH 四个命令来实现事务功能,使用 MULTI 、 DISCARD 和 EXEC 三个命令实现的一般事务; 配合WATCH和UNWATCH能够实现乐观锁(CAS,即check and set)的事务。

命令描述返回值
MULTI标记一个事务块的开始。返回值是一个简单的字符串,总是OK
EXEC执行所有事务块内的命令。返回值是一个数组,其中的每个元素分别是原子化事务中的每个命令的返回值。 当使用WATCH命令时,如果事务执行中止,那么EXEC命令就会返回一个Null值。
DISCARD取消事务,放弃执行事务块内的所有命令。返回值是一个简单的字符串,总是OK。
WATCH key [key …]监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。返回值是一个简单的字符串,总是OK。
UNWATCH取消 WATCH 命令对所有 key 的监视,如果调用了EXEC or DISCARD,则没有必要再手动调用UNWATCH返回值是一个简单的字符串,总是OK。

在这里插入图片描述
一般事务包括三个过程:

事务中的命令和普通命令在执行上的相同与不同

相同点:无论在事务状态下, 还是在非事务状态下, Redis 命令都由同一个函数执行, 所以它们共享很多服务器的一般设置, 比如 AOF 的配置、RDB 的配置,以及内存限制,等等。
不同点:

  • 非事务状态下的命令以单个命令为单位执行,前一个命令和后一个命令的客户端不一定是同一个;而事务状态则是以一个事务为单位,执行事务队列中的所有命令:除非当前事务执行完毕,否则服务器不会中断事务,也不会执行其他客户端的其他命令。
  • 在非事务状态下,执行命令所得的结果会立即被返回给客户端;而事务则是将所有命令的结果集合到回复队列,再作为 EXEC命令的结果返回给客户端。

redis事务不支持回滚

Redis命令在事务执行时可能会失败,但仍会继续执行剩余命令而不是Rollback(事务回滚)。如果你使用过关系数据库,这种情况可能会让你感到很奇怪。然而针对这种情况具备很好的解释:

  • Redis命令可能会执行失败,仅仅是由于错误的语法被调用(命令排队时检测不出来的错误),或者使用错误的数据类型操作某个Key: 这意味着,实际上失败的命令都是编程错误造成的,都是开发中能够被检测出来的,生产环境中不应该存在。(这番话,彻底甩锅,“都是你们自己编程错误,与我们无关”。)
  • 由于不必支持Rollback,Redis内部简洁并且更加高效。

“如果错误就是发生了呢?”这是一个反对Redis观点的争论。然而应该指出的是,通常情况下,回滚并不能挽救编程错误。鉴于没有人能够挽救程序员的错误,并且Redis命令失败所需的错误类型不太可能进入生产环境,所以我们选择了不支持错误回滚(Rollback)这种更简单快捷的方法。

参考:https://baijiahao.baidu.com/s?id=1613631210471699441&wfr=spider&for=pc

redis乐观锁的实现

假设存在一个String类型的状态值,state,需要对其进行CAS操作:

   WATCH state
   value = GET state;
   if value == 1
       UNWATCH state
       Return false;
   MULTI
   SET state 1
   result = EXEC
   if result == success
       return true;
   return false;

通过上述的基本应用可以知道,Redis是通过WATCH命令,来保证当前事务的数据是否被修改过,如果被修改了,则整个事务会中止,不再执行。那么,Redis在实现的时候,会保存对应的watch key,然后中途如果该Key被修改了,则会将对应的所有客户端的标志位都置为CLIENT_DIRTY_CAS,表示数据被修改,后续执行EXEC的时候则会被中断,从而实现事务。而UNWATCH命令则是从保存的watch_keys里面移除。MULTI命令仅仅将客户端的标志位flags置为CLIENT_MULTI,表示处于MULTI状态,该状态下,后续的命令(除了MULTI/WATCH/DISCARD/EXEC)外,其它命令都会被保存到一个列表里面,直到EXEC或者DISCARD命令执行。如果中途出现了语法错误之类的命令,则会将flags置为CLIENT_DIRTY_EXEC。后续执行EXEC时,如果flags存在CLIENT_DIRTY_CAS或者CLIENT_DIRTY_EXEC,则整个事务会被中止,不执行任何命令。

参考:https://www.cnblogs.com/jabnih/p/7118254.html

Redis分布式锁的实现

Redis实现分布式锁主要用到命令是SETNX命令(SET if Not eXists)。
  语法:SETNX key value
  功能:当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。
使用Redis构建锁:
  思路:将“lock:”+参数名设置为锁的键,使用SETNX命令尝试将一个随机的uuid设置为锁的值,并为锁设置过期时间,使用SETNX设置锁的值可以防止锁被其他进程获取。如果尝试获取锁的时候失败,那么程序将不断重试,直到成功获取锁或者超过给定是时限为止。

public String acquireLockWithTimeout(
        Jedis conn, String lockName, long acquireTimeout, long lockTimeout){
        String identifier = UUID.randomUUID().toString(); //锁的值
        String lockKey = "lock:" + lockName; //锁的键
        int lockExpire = (int)(lockTimeout / 1000); //锁的过期时间
        long end = System.currentTimeMillis() + acquireTimeout; //尝试获取锁的时限
        while (System.currentTimeMillis() < end) { //判断是否超过获取锁的时限
            if (conn.setnx(lockKey, identifier) == 1){ //判断设置锁的值是否成功
                conn.expire(lockKey, lockExpire); //设置锁的过期时间
                return identifier; //返回锁的值
            }

            if (conn.ttl(lockKey) == -1) { //判断如果没有设置过期时间,则重新设置过期时间
                conn.expire(lockKey, lockExpire);
            }
            try {
                Thread.sleep(100); //等待0.1秒后重新尝试设置锁的值
            }catch(InterruptedException ie){
                Thread.currentThread().interrupt();
            }
        }
        // 获取锁失败时返回null
        return null;
}

锁的释放:
  思路:使用WATCH命令监视代表锁的键,然后检查键的值是否和加锁时设置的值相同,并在确认值没有变化后删除该键。

public boolean releaseLock(Jedis conn, String lockName, String identifier) {
        String lockKey = "lock:" + lockName; //锁的键
        while (true){
            conn.watch(lockKey); //监视锁的键
            if (identifier.equals(conn.get(lockKey))){ //判断锁的值是否和加锁时设置的一致,即检查进程是否仍然持有锁
                Transaction trans = conn.multi();
                trans.del(lockKey); //在Redis事务中释放锁
                List<Object> results = trans.exec();
                if (results == null){   
                    continue; //事务执行失败后重试(监视的键被修改导致事务失败,重新监视并释放锁)
                }
                return true;
            }
            conn.unwatch(); //解除监视
            break;
        }
        return false;
}

参考:
http://www.cnblogs.com/Jason-Xiang/p/5364252.html

另:
java版本分布式锁:https://github.com/redisson/redisson
其他语言版本的分布式锁:http://www.redis.cn/topics/distlock.html

五、Redis持久化

Redis支持两种数据持久化方式:RDB方式和AOF方式。前者会根据配置的规则定时将内存中的数据持久化到硬盘上,后者则是在每次执行写命令之后将命令记录下来。两种持久化方式可以单独使用,但是通常会将两者结合使用

1.RDB持久化

原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化。
指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
在这里插入图片描述

2.AOF(append only file)持久化

原理是将Reids的操作日志以追加的方式写入文件。
以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
同时Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。重写原理:aof 文件持续增长而大时,会 fork 出一条新进程来将文件重写(也就是先写临时文件最后再 rename),遍历新进程的内存中的数据,每条记录有一条 set 语句,重写 aof 文件的操作,并没有读取旧的的 aof 文件,而是将整个内存的数据库内容用命令的方式重写了一个新的 aof 文件,这点和快照有点类似。
在这里插入图片描述
参考:https://www.jianshu.com/p/472f3850a333

六、Redis的高可用详解:Redis哨兵、复制、集群的设计原理,以及区别

谈到Redis服务器的高可用,如何保证备份的机器是原始服务器的完整备份呢?这时候就需要哨兵和复制。

  • 1.哨兵(Sentinel):可以管理多个Redis服务器,它提供了监控,提醒以及自动的故障转移的功能。
  • 2.复制(Replication):则是负责让一个Redis服务器可以配备多个备份的服务器。

Redis正是利用这两个功能来保证Redis的高可用。

哨兵(sentinal)

哨兵是Redis集群架构中非常重要的一个组件,哨兵的出现主要是解决了主从复制出现故障时需要人为干预的问题。

1.Redis哨兵主要功能

  • (1)集群监控:负责监控Redis master和slave进程是否正常工作
  • (2)消息通知:如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
  • (3)故障转移:如果master node挂掉了,会自动转移到slave node上
  • (4)配置中心:如果故障转移发生了,通知client客户端新的master地址

2.Redis哨兵的高可用

原理:当主节点出现故障时,由Redis Sentinel自动完成故障发现和转移,并通知应用方,实现高可用性。
在这里插入图片描述

  • 哨兵机制建立了多个哨兵节点(进程),共同监控数据节点的运行状况。
  • 同时哨兵节点之间也互相通信,交换对主从节点的监控状况。
  • 每隔1秒每个哨兵会向整个集群:Master主服务器+Slave从服务器+其他Sentinel(哨兵)进程,发送一次ping命令做一次心跳检测。

3.哨兵的三个定时任务

在这里插入图片描述

4.哨兵的主观下线和客观下线

在这里插入图片描述

工作方式
1):每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令
2):如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线。
3):如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。
4):当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线
5):在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令
6):当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次
7):若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除。
若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。
注:slave和sentinel在主观下线后没有后续的故障转移操作,只有与master客观下线后才会执行故障转移

5.sentinel leader选举

在这里插入图片描述
leader选举选举过程很快,基本上哪个哨兵节点最先判断出这个主节点客观下线,就会在各个哨兵节点中发起投票机制Raft算法(选举算法),最终被投为领导者的哨兵节点完成主从自动化切换的过程。

6.故障转移

在这里插入图片描述

Redis 复制(Replication)

Redis为了解决单点数据库问题,会把数据复制多个副本部署到其他节点上,通过复制,实现Redis的高可用性,实现对数据的冗余备份,保证数据和服务的高度可靠性。
数据复制原理(执行步骤)
在这里插入图片描述
①从数据库向主数据库发送sync(数据同步)命令。
②主数据库接收同步命令后,会保存快照,创建一个RDB文件。
③当主数据库执行完保持快照后,会向从数据库发送RDB文件,而从数据库会接收并载入该文件。
④主数据库将缓冲区的所有写命令发给从服务器执行。
⑤以上处理完之后,之后主数据库每执行一个写命令,都会将被执行的写命令发送给从数据库。
注意:在Redis2.8之后,主从断开重连后会根据断开之前最新的命令偏移量进行增量复制。
在这里插入图片描述

传输延迟

主从一般部署在不同机器上,复制时存在网络延时问题,redis提供repl-disable-tcp-nodelay参数决定是否关闭TCP_NODELAY,默认为关闭
参数关闭时:无论大小都会及时发布到从节点,占带宽,适用于主从网络好的场景,
参数启用时:主节点合并所有数据成TCP包节省带宽,默认为40毫秒发一次,取决于内核,主从的同步延迟40毫秒,适用于网络环境复杂或带宽紧张,如跨机房

高可用读写分离

在这里插入图片描述

参考:
https://www.cnblogs.com/ysocean/p/9143118.html
https://www.cnblogs.com/restart30/p/9242683.html
https://www.liangzl.com/get-article-detail-29695.html

七、Redis常见问题QA

1、读写分离后读取到过期数据

原因:redis的从库是无法主动的删除已经过期的key的,所以如果做了读写分离,就很有可能在从库读到脏数据
原因分析:redis在删除过期key的时候,是有两种策略,第一种是懒惰型策略,即只有当redis操作这个key的时候,发现这个key过期,就会把这个key删除。第二种是定期采样一些key进行删除。针对上面说的两种过期策略,会有个问题,即如果我们过期key的数量非常多,而采样速度根本比不上过期key的生成速度时会造成很多过期数据没有删除,但在redis里master和slave达成一种协议,slave是不能处理数据的(即不能删除数据)而我们的客户端没有及时读到到过期数据同步给master将key删除,就会导致slave读到过期的数据
解决方案:

  • 1 通过scan命令扫库:
    当redis中的key被scan的时候,相当于访问了该key,同样也会做过期检测,充分发挥redis惰性删除的策略,这个方法能大大降低了脏数据读取的概率,答案缺点也比较明显,会造成一定的数据库压力,谨慎合理是哟个,够则有可能影响线上业务的效率
  • 2 升级redis到新的版本:
    在redis3.2中,redis加入了一个新特性来解决主从不一致导致读取到过期数据问题,在db.c文件中,作者对lookupKeyRead做了相应的修改,增加了key是否过期以及对主从库的判断,如果key已过期,当前访问的master则返回null;当前访问的是从库,且执行的是只读命令也返回null(老版本从库真实的返回该操作的结果,如果该key过期后主库没有删除,就返回为null)

参考:https://www.cnblogs.com/roxy/p/8093913.html

2、节点RunID不匹配

我们主节点重启(RunID发生变化),对于slave来说,它会保存之前master节点的RunID,如果它发现了此时master的RunID发生变化,那它会认为这是master过来的数据可能是不安全的,就会采取一次全量复制
解决办法:对于这类问题,我们只有是做一些故障转移的手段,例如master发生故障宕掉,我们选举一台slave晋升为master(哨兵或集群)

3、复制积压缓冲区不足

我在全量复制与部分复制那篇文章提到过,master生成RDB同步到slave,slave加载RDB这段时间里,master的所有写命令都会保存到一个复制缓冲队列里(如果主从直接网络抖动,进行部分复制也是走这个逻辑),待slave加载完RDB后,拿offset的值到这个队列里判断,如果在这个队列中,则把这个队列从offset到末尾全部同步过来,这个队列的默认值为1M。而如果发现offset不在这个队列,就会产生全量复制。
解决办法:增大复制缓冲区的配置 rel_backlog_size 默认1M,我们可以设置大一些,从而来加大我们offset的命中率。这个值,我们可以假设,一般我们网络故障时间一般是分钟级别,那我们可以根据我们当前的QPS来算一下每分钟可以写入多少字节,再乘以我们可能发生故障的分钟就可以得到我们这个理想的值。

4、规避复制风暴

什么是复制风暴?举例:我们master重启,其master下的所有slave检测到RunID发生变化,导致所有从节点向主节点做全量复制。尽管redis对这个问题做了优化,即只生成一份RDB文件,但需要多次传输,仍然开销很大。
(1)单主节点复制风暴:主节点重启,多从节点全量复制
解决:更换复制拓扑如下图:
在这里插入图片描述

  • 1.我们将原来master与slave中间加一个或多个slave,再在slave上加若干个slave,这样可以分担所有slave对master复制的压力。(这种架构还是有问题:读写分离的时候,slave1也发生了故障,怎么去处理?)
  • 2.如果只是实现高可用,而不做读写分离,那当master宕机,直接晋升一台slave即可。

(2)单机器复制风暴:机器宕机后的大量全量复制,如下图:
在这里插入图片描述
当machine-A这个机器宕机重启,会导致该机器所有master下的所有slave同时产生复制。(灾难)
解决:

  • 1.主节点分散多机器(将master分散到不同机器上部署)
  • 2.还有我们可以采用高可用手段(slave晋升master)就不会有类似问题了。

5、缓存穿透

原因:缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决办法:

  • (1)对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃。还有最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
  • (2)也可以采用一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

6、缓存雪崩

原因:如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。
这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上
解决办法:

  • 1、在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
  • 2、可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存
  • 3、不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀
  • 4、做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。

全文参考:
https://my.oschina.net/u/3371837/blog/1789452
https://redisbook.readthedocs.io/en/latest/index.html

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值