一、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(列表) 链表(双向链表) 增删快,提供了操作某一段元素的API 1,最新消息排行等功能(比如朋友圈的时间线) 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 时,所涉及到的数据模型:
学习笔记之Redis

  • 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结构组成;典型结构如下图所示:
学习笔记之Redis
通过图中可以看出,双端链表同时保存了表头指针和表尾指针,并且每个节点都有指向前和指向后的指针;链表中保存了列表的长度;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时)各部分关系如下图所示:
学习笔记之Redis
下面从底层向上依次介绍:

  • 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
一般事务包括三个过程:

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

相同点:无论在事务状态下, 还是在非事务状态下, 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一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
学习笔记之Redis## 2.AOF(append only file)持久化
原理是将Reids的操作日志以追加的方式写入文件。
以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
同时Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。重写原理:aof 文件持续增长而大时,会 fork 出一条新进程来将文件重写(也就是先写临时文件最后再 rename),遍历新进程的内存中的数据,每条记录有一条 set 语句,重写 aof 文件的操作,并没有读取旧的的 aof 文件,而是将整个内存的数据库内容用命令的方式重写了一个新的 aof 文件,这点和快照有点类似。
学习笔记之Redis
参考: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自动完成故障发现和转移,并通知应用方,实现高可用性。
学习笔记之Redis

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

3.哨兵的三个定时任务

学习笔记之Redis

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

学习笔记之Redis

工作方式
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选举

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

6.故障转移

学习笔记之Redis

Redis 复制(Replication)

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

传输延迟

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

高可用读写分离

学习笔记之Redis

参考:
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)单主节点复制风暴:主节点重启,多从节点全量复制
解决:更换复制拓扑如下图:
学习笔记之Redis

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

(2)单机器复制风暴:机器宕机后的大量全量复制,如下图:
学习笔记之Redis
当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

相关文章: