Redis 知识点超级大合集

文章目录

    • 1、Redis 概要
      • 1-1、概要
      • 1-2、Redis相比memcache有哪些优势
      • 1-3、Redis的适用场景
        • 1-3-1、会话缓存(Session Cache)
        • 1-3-2、全页缓存(FPC)
        • 1-3-3、队列
        • 1-3-4、排行榜/计数器
        • 1-3-5、发布/订阅
        • 1-3-6、分布式锁
    • 2、Redis的数据类型以及相关常用命令
      • 2-0、关于Redis的key
      • 2-1、String
      • 2-2、Hash
      • 2-3、List
      • 2-4、Set
      • 2-5、Sorted Set
      • 2-6、更多的Redis数据类型
      • 2-7、从海量Key里查询出某一个固定前缀的Key
        • 2-7-1、使用KEYS
        • 2-7-2、使用SCAN cursor
      • 2-8、主要数据类型的小结
    • 3、Redis数据的淘汰策略以及数据生存时间
      • 3-1、数据淘汰策略
      • 3-2、影响数据生存时间的一些操作
      • 3-3、如何更新生存时间
    • 4、Redis持久化
      • 4-1、持久化方式
      • 4-2、AOF
        • AOF的优点
        • AOF的缺点
      • 4-3、RDB
        • RDB的优点
        • RDB的缺点
      • 4-4、持久化策略 / Redis 数据恢复
      • 4-5、RDB-AOF混合模式
    • 5、Redis管道 Pineline
    • 6、Redis事务
      • 6-1、Redis事务
      • 6-2、Redis事务相关命令
        • MULTI、EXEC、DISCARD
        • WATCH
      • 6-3、Redis事务不支持回滚
      • 6-4、脚本
    • 7、Redis集群
      • 7-1、Redis单机模式的问题
      • 7-2、主从模式
        • 7-2-1、主从模式的特点
        • 7-2-2、全量同步过程
        • 7-2-3、增量同步过程
        • 7-2-4、Slave节点同步数据时的服务策略
      • 7-3、哨兵模式
        • 7-3-1、哨兵模式的特点
        • 7-3-2、哨兵模式工作过程
      • 7-4、Cluster模式
        • 7-4-1、Cluster模式特点
        • 7-4-2、Cluster模式工作过程
        • 7-4-3、Redis Cluster节点间的通讯协议:Gossip 协议
        • 7-4-4、补充:一致性哈希算法
        • 7-4-5、补充:分布式寻址方式总结
      • 7-5、第三方提供的Redis集群方案
    • 8、数据分片(Sharding)
      • 8-1、数据分片的实现
      • 8-2、hash tags
      • 8-3、数据分片的限制
    • 8.5、关于哨兵(主从)模式与Redis Cluster模式的取舍
    • 9、Redis实现分布式锁
      • 9-1、分布式锁需要解决的问题
      • 9-2、单节点实现分布式锁
      • 9-3、Redis 分布式锁 Redlock 算法
      • 9-4、ZK分布式锁
    • 10、Redis实现消息队列
    • 11、Redis缓存的雪崩、穿透、击穿
      • 11-1、Redis 缓存雪崩
      • 11-2、Redis 缓存穿透
      • 11-3、Redis 缓存击穿
    • 12、Redis是单线程模型但是为什么能保证高性能
      • 12-1、基本原因
      • 12-2、Redis 的文件事件处理器
      • 12-3、Redis中一次请求的响应过程
      • 12-4、补充: Redis 的时间事件
    • 13、Redis 与 MySQL 的数据同步
    • 14、Redis性能调优
    • 15、其他
      • 15-1、Redis的重要版本
      • 15-2、Redis的Java客户端的选择

1、Redis 概要

1-1、概要

Redis的全称:Remote Dictionary Server。

Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。

因为是纯内存操作,Redis的性能非常出色,官宣QPS 10万+,是已知性能最快的Key-Value DB。

Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存多种数据结构,此外单个value的最大限制是1GB,不像 memcached只能保存1MB的数据,因此Redis可以用来实现很多有用的功能,
比方说用他的List来做FIFO双向链表,实现一个轻量级的高性 能消息队列服务,用他的Set可以做高性能的tag系统等等。

Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。

1-2、Redis相比memcache有哪些优势

  1. memcache所有的值均是简单的字符串,Redis数据类型丰富
  2. memcache不支持数据持久化存储
  3. memcache不支持主从、集群、数据分片

1-3、Redis的适用场景

1-3-1、会话缓存(Session Cache)

最常用就是使用Redis做会话缓存。Redis相比与其他存储的优势在于可持久化。

1-3-2、全页缓存(FPC)

除基本的会话token之外,Redis还提供简便的FPC平台。即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度下降。

1-3-3、队列

Redis在内存存储引擎领域的最大一个优点就是提供list和set操作,这使得Redis能做为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如Python)对 list 的 push/pop 操作。

1-3-4、排行榜/计数器

Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(set)和有序集合(Sorted Set)也使得我们在执行这些操作时变得非常简单,Redis只是正好提供了这两种数据结构。

1-3-5、发布/订阅

Redis自带的发布/订阅功能可使用场景非常多。可以在社交网络连接中使用,还可以作为基于发布/订阅的脚本触发器,甚至可以用来建立聊天系统。

1-3-6、分布式锁

2、Redis的数据类型以及相关常用命令

2-0、关于Redis的key

Redis采用Key-Value结构存储数据,任何二进制序列都可以作为Redis的Key使用(普通的字符串或一张JPEG图片)。

一些注意事项:

  • 不要使用过长的Key。不仅会消耗更多的内存,还会导致查找的效率降低
  • 同时Key也没必要短到缺失了可读性,会引发可维护性上的麻烦
  • Redis允许的最大Key长度是512MB(Value的长度限制也是512MB)

2-1、String

最基本的数据类型,其值最大可存储512M,二进制安全(Redis的String可以包含任何二进制数据,包含jpg对象等)。

Redis没有Integer、Float、Boolean等数据类型的概念,所有的基本类型在Redis中都以String体现。

常用命令:

  • 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位带符号的整型数字,否则会返回错误。也就是说value的值必须在[-2^63 ~ 2^63 - 1]范围内。

    同时由于Redis采用单线程模型,天然线程安全的,这使得INCR/DECR命令可以非常便利的实现高并发场景下的精确控制。

2-2、Hash

String元素组成的字典,和传统的哈希表一样,是一种field-value型的数据结构,可以理解成将HashMap搬入Redis。

Redis中的Hash与List相比,提供了效率高得多的随机访问命令。

常用命令:

  • 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)

应慎用的命令:

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

上述三个命令都会对Hash进行完整遍历,Hash中的field数量与命令的耗时线性相关,应尽量避免使用,而改为使用HSCAN命令进行游标式的遍历。可以参看本章的 “2-7-2、使用SCAN cursor” 一节。

2-3、List

链表型的数据结构,可以在List的两端执行插入元素和弹出元素的操作。虽然支持在特定index上插入和读取元素的功能,但其时间复杂度较高(O(N)),应慎用。

常用命令:

  • 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)
  • BLPOP/BRPOP/BLPUSH/BRPUSH:阻塞操作。比如POP,在List为空时进行阻塞,直到List中有对象可以出队时再返回。类似Java中的BlockingQueue。

应慎用的命令:

  • LRANGE:返回指定List中指定范围的元素(双端包含,即LRANGE key 0 10会返回11个元素),时间复杂度O(N)。应尽可能控制一次获取的元素数量,一次获取过大范围的List元素会导致延迟,同时对长度不可预知的List,避免使用LRANGE key 0 -1这样的完整遍历操作。
  • 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长度大的情况下耗时会明显增加,应慎用。

补充说明:Redis的List实际是设计来用于实现队列的,而不是实现类似Java的ArrayList这样的数据类型的。

2-4、Set

String元素组成的无序集合,通过哈希表实现(增删改查时间复杂度为O(1)),不允许重复。

使用smembers遍历set中的元素时,其顺序也是不确定的,是通过hash运算过后的结果。Redis还对集合提供了求交集、并集、差集等操作,可以实现如同共同关注,共同好友等功能。

常用命令:

  • 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

应慎用的命令:

  • 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数

上述几个命令涉及的计算量大,应谨慎使用。需要遍历时,优先使用SSCAN命令。

2-5、Sorted Set

Sorted Set是有序的、不可重复的String集合。Sorted Set中的每个元素都需要指派一个分数(score),Sorted Set会根据score对元素进行升序排序。如果多个member拥有相同的score,则以字典序进行升序排序。

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))

应慎用的命令:

  • 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做一次性的完整遍历。可以通过ZSCAN命令来进行游标式的遍历。

或通过LIMIT参数来限制返回member的数量(适用于ZRANGEBYSCORE和ZREVRANGEBYSCORE命令)

2-6、更多的Redis数据类型

  • Stream,一个强大的支持多播的可持久化的消息队列,Redis 5.0追加。
  • HyperLogLog,主要用于数量统计的数据结构。和Set类似,维护一个不可重复的String集合,但是HyperLogLogs并不维护具体的内容,只维护个数。也就是说只能用于计算一个集合中不重复的元素数量,非常节省内存空间。
  • Geo,用于支持存储地理位置信息。
  • Bitmap,在Redis中并不是一种实际的数据类型,是一种将String作为Bitmap使用的方法。与java中的Bitmap用途和原理相同,可以理解为将String转换为bit数组,简单地存储每一位为true/false,极为节省空间。

2-7、从海量Key里查询出某一个固定前缀的Key

2-7-1、使用KEYS

使用KEYS [pattern]:查找所有符合给定模式pattern的key

但是keys会一次性返回所有符合条件的key,所以会造成Redis的卡顿,形成隐患。

另外如果一次性返回所有key,对内存的消耗在某些条件下也是巨大的。

使用例:

keys test* //返回所有以test为前缀的key

从性能和安全性上考虑,应该优先使用SCAN命令。

2-7-2、使用SCAN cursor

使用SCAN cursor [MATCH pattern] [COUNT count]

  • cursor:游标
  • MATCH pattern:查询key的条件
  • count:返回的条数

SCAN是一个基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程。

SCAN以0作为游标,开始一次新的迭代,直到命令返回游标0完成一次遍历。

此命令并不保证每次执行都返回某个给定数量的元素,甚至会返回0个元素,但只要游标不是0,程序都不会认为SCAN命令结束,但是返回的元素数量大概率符合count参数。另外,SCAN支持模糊查询。

使用例:

SCAN 0 MATCH test* COUNT 10 //每次返回10条以test为前缀的key

另外,对于Hash、Set、Sorted Set分别有对应的HSCAN、SSCAN、ZSCAN命令可以使用。

参考资料:Redis官网对于SCAN的说明

2-8、主要数据类型的小结

数据类型 特点 应用场景
String 任意二进制数据,最大512M
List 两端压入或弹出 用于实现队列,而不是ArrayList,避免按下标访问
Hash 同java的HashMap 结构化数据
Set 无序集合,不可重复 交集、并集、差集
Sorted Set 有序集合,不可重复 Set增强版。各种需要排序的数据。可实现延时队列

3、Redis数据的淘汰策略以及数据生存时间

3-1、数据淘汰策略

在Redis中,允许用户设置最大使用内存大小server.maxmemory,当Redis 内存数据量上升到一定大小的时候,就会施行数据淘汰策略。

  1. voltile-lru:从已设置过期时间的数据集(service.db[i].expires)中挑选最近最少使用的数据淘汰。
  2. volatile-ttl:从已设置过期时间的数据集(service.db[i].expires)中挑选将要过期 数据淘汰。
  3. volatile-random:从已设置过期时间的数据集(service.db[i].expires)中任意选择数据淘汰。
  4. allkeys-lru:从数据集(service.db[i].dict)中挑选最少使用的数据淘汰。
  5. allkeys-random:从数据集(service.db[i].dict)中任意选择数据淘汰。
  6. no-enviction(驱逐):禁止驱逐数据,追加数据失败后抛出异常。

其中volatile和allkeys规定了是对已设置过期时间的数据集淘汰数据、还是从全部数据集淘汰数据;

后面的lru、ttl以及random是三种不同的淘汰策略,再加上一种no-enviction永不回收的策略。

ttl和random比较容易理解,实现也会比较简单。主要是lru最近最少使用淘汰策略,设计上会对key 按失效时间排序,然后取最先失效的key进行淘汰。

3-2、影响数据生存时间的一些操作

生存时间可以通过使用 DEL 命令来删除整个 key 来移除,或者被 SET 命令覆盖。

也就是说,修改key对应的value和使用另外相同的key和value来覆盖以后,当前数据的生存时间有可能变得不同。

另外,如果使用RENAME对一个key进行改名,那么改名后的key的生存时间和改名前一样。

RENAME命令的另一种可能是,尝试将一个带生存时间的key改名成另一个带生存时间的 another_key,这时旧的another_key(以及它的生存时间)会被删除,然后 key 会改名为 another_key ,新的 another_key 的生存时间也和原本的 key 一样。

使用PERSIST命令可以在不删除 key 的情况下,移除 key 的生存时间,让 key 重新成为一个persistent key (即永久有效)

3-3、如何更新生存时间

可以对一个已经带有生存时间的key执行EXPIRE命令,新指定的生存时间会取代旧的生存时间。

4、Redis持久化

4-1、持久化方式

Redis目前有两种持久化方式:RDB和AOF。

AOF(Append-Only-File)为增量持久化,记录每次对服务器写的操作。追加保存每次写的操作到文件末尾。Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。

RDB(Redis Database)是通过保存某个时间点的全量数据快照实现数据的持久化,当恢复数据时,直接通过rdb文件中的快照,将数据恢复。

简单来说,RDB备份的是数据库的数据,AOF备份的是接收到的指令。

4-2、AOF

采用AOF持久方式时,Redis会把每一个写请求都记录在一个日志文件里。在Redis重启时,会把AOF文件中记录的所有写操作顺序执行一遍,确保数据恢复到最新。

AOF默认是关闭的,如要开启,进行如下配置:

redis.conf:
appendonly yes

# appendsync always
  appendfsync everysec
# appendfsync no

AOF提供了三种fsync配置:always/everysec/no,通过配置项[appendfsync]指定:

  • appendfsync no:不进行fsync,将flush文件的时机交给OS决定,速度最快
  • appendfsync always:每写入一条日志就进行一次fsync操作,数据安全性最高,但速度最慢
  • appendfsync everysec:折中的做法,交由后台线程每秒fsync一次

AOF的实时性取决于fsync的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。

但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。

随着AOF不断地记录写操作日志,必定会出现一些无用的日志,例如某个时间点执行了命令SET key1 “abc”,在之后某个时间点又执行了SET key1 “bcd”,那么第一条命令很显然是没有用的。

大量的无用日志会让AOF文件过大,也会让数据恢复的时间过长。
所以Redis提供了AOF rewrite功能,可以重写AOF文件,只保留能够把数据恢复到最新状态的最小写操作集。

AOF rewrite可以通过BGREWRITEAOF命令触发,也可以配置Redis定期自动进行:

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

上面两行配置的含义是,Redis在每次AOF rewrite时,会记录完成rewrite后的AOF日志大小,当AOF日志大小在该基础上增长了100%后,自动进行AOF rewrite。

同时如果增长的大小没有达到64mb,则不会进行rewrite。

AOF的优点

  • 最安全,在启用appendfsync always时,任何已写入的数据都不会丢失,使用在启用appendfsync everysec也至多只会丢失1秒的数据。
  • AOF文件在发生断电等问题时也不会损坏,即使出现了某条日志只写入了一半的情况,也可以使用redis-check-aof工具轻松修复。
  • AOF文件易读,可修改,在进行了某些错误的数据清除操作后,只要AOF文件没有rewrite,就可以把AOF文件备份出来,把错误的命令删除,然后恢复数据。

AOF的缺点

  • AOF文件通常比RDB文件更大
  • 性能消耗比RDB高
  • 数据恢复速度比RDB慢

4-3、RDB

采用RDB持久方式,Redis会定期保存数据快照至一个rbd文件中,并在启动时自动加载rdb文件,恢复之前保存的数据。

RDB配置文件:

redis.conf:
save 900 1    #在900s内如果有1条数据被写入,则产生一次快照。
save 300 10   #在300s内如果有10条数据被写入,则产生一次快照
save 60 10000 #在60s内如果有10000条数据被写入,则产生一次快照
stop-writes-on-bgsave-error yes
  #stop-writes-on-bgsave-error :
  #如果为yes则表示,当备份进程出错的时候,主进程就停止进行接受新的写入操作,这样是为了保护持久化的数据一致性的问题。

其中save配置的是Redis进行快照保存的时机:

save [seconds] [changes]

意为在[seconds]秒内如果发生了[changes]次数据修改,则进行一次RDB快照保存。

可以配置多条save指令,让Redis执行多级的快照保存策略。

Redis默认开启RDB快照保存,默认的RDB策略参看上面的配置文件。

也可以通过命令手工触发RDB快照保存。

  • SAVE:阻塞Redis的服务器进程,直到RDB文件被创建完毕。SAVE命令很少被使用,因为其会阻塞主线程来保证快照的写入,由于Redis是使用一个主线程来接收所有客户端请求,这样会阻塞所有客户端请求。

  • BGSAVE:该指令会Fork出一个子进程来创建RDB文件,不阻塞服务器进程,子进程接收请求并创建RDB快照,父进程继续接收客户端的请求。

    BGSAVE保存快照的原理:fork和cow。fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

    关于cow操作的细节,参考这篇文章:Redis 中 BGSAVE 名利持久化的细节问题

Redis自动生成rdb文件时使用的是BGSAVE的方式。

在以下场景下Redis会自动触发生成rdb文件:

  • 主从复制时,主节点自动触发
  • 执行Debug Reload
  • 执行Shutdown且没有开启AOF持久化

RDB的优点

  • 对性能影响最小。Redis在保存RDB快照时会fork出子进程进行,几乎不影响Redis处理客户端请求的效率。
  • 每次快照会生成一个完整的数据快照文件,所以可以辅以其他手段保存多个时间点的快照(例如把每天0点的快照备份至其他存储媒介中),作为非常可靠的灾难恢复手段。
  • 使用RDB文件进行数据恢复比使用AOF要快很多。

RDB的缺点

  • 快照是定期生成的,所以在Redis crash时或多或少会丢失一部分数据。
  • 如果数据集非常大且CPU不够强(比如单核CPU),Redis在fork子进程时可能会消耗相对较长的时间(长至1秒),影响这期间的客户端请求。

4-4、持久化策略 / Redis 数据恢复

当redis重启的时候会优先载入AOF文件来恢复原始的数据。如果没有AOF文件,则加载RDB文件。如果RDB也不存在,则数据恢复失败报错。

如果想优先保证数据安全性,应该要开启AOF模式。因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。不过RDB 恢复数据集的速度比AOF恢复的速度要快。

如果可以承受数分钟以内的数据丢失,那么可以只使用RDB持久化(使用命令bgsave进行全量持久化时,耗时较长,不够实时,所以会造成数据丢失)。RDB便于数据库备份。

如果只把Redis作为缓存服务使用,Redis中存储的所有数据都不是该数据的主体而仅仅是同步过来的备份,那么可以关闭Redis的数据持久化机制。

但通常来说,仍然建议至少开启RDB方式的数据持久化,因为:

  • RDB方式的持久化几乎不损耗Redis本身的性能,在进行RDB持久化时,Redis主进程唯一需要做的事情就是fork出一个子进程,所有持久化工作都由子进程完成。
  • Redis无论因为什么原因crash掉之后,重启时能够自动恢复到上一次RDB快照中记录的数据。这省去了手工从其他数据源(如DB)同步数据的过程,而且要比其他任何的数据恢复方式都要快。

4-5、RDB-AOF混合模式

redis4.0之后推出了此种持久化方式,RDB作为全量备份,AOF作为增量备份,并且将此种方式作为默认方式使用。

在RDB-AOF方式下,持久化策略首先将缓存中数据以RDB方式全量写入文件,再将写入后新增的数据以AOF的方式追加在RDB数据的后面,在下一次做RDB持久化的时候将AOF的数据重新以RDB的形式写入文件。

这种方式既可以提高读写和恢复效率,也可以减少文件大小,同时可以保证数据的完整性。

在此种策略的持久化过程中,子进程会通过管道从父进程读取增量数据,在以RDB格式保存全量数据时,也会通过管道读取数据,同时不会造成管道阻塞。可以说,在此种方式下的持久化文件,前半段是RDB格式的全量数据,后半段是AOF格式的增量数据。此种方式是目前较为推荐的一种持久化方式。

5、Redis管道 Pineline

Redis基于请求/响应模型,单个请求处理需要一一应答。如果需要同时执行大量命令,则每条命令都需要等待上一条命令执行完毕后才能继续执行,这中间不仅仅多了RTT,还多次使用了系统IO。

虽然Redis提供了一些批量处理命令,比如 MSET/MGET/HMSET/HMGET ,但是它们只能将相同的指令进行合并。

管道Pipeline可以让Redis批量执行指令,将多次IO往返的时间缩减为一次。

但是如果指令之间存在依赖关系,则需要分批发送指令。意思是说Pipeline只能用于执行连续且无相关性的命令,当某个命令的执行需要依赖于前一个命令的返回结果时,就无法使用Pipeline。Pipeline并不保证命令执行时的顺序。要规避这一局限性则必须使用脚本。

6、Redis事务

6-1、Redis事务

Redis的事务可以确保复数命令执行时的原子性。

事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

6-2、Redis事务相关命令

MULTI、EXEC、DISCARD

通过MULTI和EXEC命令来把这两个命令加入一个事务中:

> MULTI
OK
> GET vCount
QUEUED
> SET vCount 0
QUEUED
> EXEC
1) 12384
2) OK

Redis在接收到MULTI命令后便会开启一个事务,这之后的所有读写命令都会保存在队列中但并不执行,直到接收到EXEC命令后,Redis会把队列中的所有命令连续顺序执行,并以数组形式返回每个命令的返回结果。

可以使用DISCARD命令放弃当前的事务,将保存的命令队列清空。

WATCH

在Redis的事务中,WATCH命令可用于提供CAS(check-and-set)功能,是一个对事务的乐观锁。假设我们通过WATCH命令在事务执行之前监控了多个Keys,倘若在WATCH之后有任何Key的值发生了变化,EXEC命令执行的事务都将被放弃,同时返回Null multi-bulk应答以通知调用者事务执行失败。

例如,我们假设Redis中并未提供incr命令来完成键值的原子性递增,如果要实现该功能,我们只能自行编写相应的代码。其伪码如下:

val = GET mykey
val = val + 1
SET mykey $val

以上代码只有在单连接的情况下才可以保证执行结果是正确的,在同一时刻有多个客户端在同时执行该段代码,那么就会出现并发问题。

这种情况下需要借助WATCH命令的帮助:

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

和此前代码不同的是,新代码在获取mykey的值之前先通过WATCH命令监控了该键,此后又将set命令包围在事务中,这样就可以有效的保证每个连接在执行EXEC之前,如果当前连接获取的mykey的值被其它连接的客户端修改,那么当前连接的EXEC命令将执行失败。这样调用者在判断返回值后就可以获悉val是否被重新设置成功。

UNWATCH命令可以取消watch对所有key的监控。

6-3、Redis事务不支持回滚

需要注意的是,Redis事务不支持回滚:
如果一个事务中的命令出现了语法错误,大部分客户端驱动会返回错误,2.6.5版本以上的Redis也会在执行EXEC时检查队列中的命令是否存在语法错误,如果存在,则会自动放弃事务并返回错误。

但如果一个事务中的命令有非语法类的错误(比如对String执行HSET操作),无论客户端驱动还是Redis都无法在真正执行这条命令之前发现,所以事务中的所有命令仍然会被依次执行。在这种情况下,会出现一个事务中部分命令成功部分命令失败的情况,然而与RDBMS不同,Redis不提供事务回滚的功能,所以只能通过其他方法进行数据的回滚。

6-4、脚本

通过EVAL与EVALSHA命令,可以让Redis执行LUA脚本。这就类似于RDBMS的存储过程一样,可以把客户端与Redis之间密集的读/写交互放在服务端进行,避免过多的数据交互,提升性能。

Scripting功能是作为事务功能的替代者诞生的,事务提供的所有能力Scripting都可以做到。Redis官方推荐使用LUA Script来代替事务,其效率和便利性都超过了事务。

参看“9、Redis实现分布式锁”一节中eval命令的写法。

7、Redis集群

7-1、Redis单机模式的问题

  • 单点故障
  • 容量瓶颈无法扩容

7-2、主从模式

Redis一般是使用一个Master节点来进行写操作,而若干个Slave节点进行读操作,实现读写分离。

另外定期的数据备份操作也是单独选择一个Slave去完成,这样可以最大程度发挥Redis的性能。

Master和Slave的数据不是一定要即时同步的,但是在一段时间后Master和Slave的数据是趋于同步的,保证最终一致性。

7-2-1、主从模式的特点

  • Master可以进行读写操作,当写操作导致数据发生变化时,将自动同步给Slave,Slave通常是只读的,并且接受从Master同步过来的数据。
  • 一台Master可以有多台Slave,但每台Slave只能有一个Master。
  • 某台Slave宕机不影响其他Slave和Master的读写,重新启动后会将数据重新从Master同步过来。
  • Master宕机后不影响Slave的读,但该集群不再提供对Redis的写入功能。
  • Master宕机后不会从Slave中选举主节点。

启用主从复制非常简单,只需要一行配置信息:

slaveof 192.168.1.1 6379  #指定Master的IP和端口

7-2-2、全量同步过程

全量同步一般发生在Slave初始化阶段,但其实在任何时候Slave都可以向Master发起全量同步的请求,这时Slave会将Master上的所有数据都复制一份。

  1. Slave连接主服务器,发送SYNC命令。
  2. Master执行BGSAVE命令生成RDB文件
  3. 在保存数据快照期间,Master用缓冲区记录收到的所有写命令。
  4. Master执行完BGSAVE命令后,将rdb文件发送给Slave,并在发送期间继续记录收到的写命令。
  5. Slave收到RDB文件后丢弃所有旧数据,载入收到的RDB。
  6. Master快照发送完毕后开始向Slave发送缓冲区中的写命令。
  7. Slave完成对RDB的载入,执行来自Master缓冲区的写命令。

7-2-3、增量同步过程

Redis增量同步一般发生在Slave已经初始化完成,开始正常连接Master的阶段。

  1. Master接收到用户的操作指令,判断是否需要传播到Slave。
  2. 将操作记录追加到AOF文件。
  3. 将操作传播到其它Slave:1.对齐主从库(汇报同步偏移量);2.往响应缓存写入指令。
  4. 将缓存中的数据发送给Slave。

7-2-4、Slave节点同步数据时的服务策略

slave 节点在做同步的时候,也不会阻塞自己提供的查询操作,它会用旧的数据集来提供服务。

但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会阻塞主进程,暂停对外服务了。

7-3、哨兵模式

主从模式弊端:当Master宕机后,Redis集群将不能对外提供写入操作。

Redis2.8开始,Redis正式提供了哨兵模式(Redis Sentinel)的架构,来解决主从切换问题。

7-3-1、哨兵模式的特点

  • 哨兵模式是建立在主从模式的基础上,当Master节点宕机之后,哨兵会从Slave节点中选举一个节点作为Master,并修改它们的配置文件,使其他的Slave指向新的Master。
  • 当原先宕机的Master节点重新启动时,他将不再是Master,而是作为新Master的一个Slave节点存在。
  • 哨兵节点是一个特殊的Redis节点(不存储数据),本质上也是一个进程,所以也有挂掉的可能,所以哨兵也存在集群模式。

7-3-2、哨兵模式工作过程

  • 每隔10秒,每个哨兵节点会向Master和Slave节点发送info命令获取最新的拓扑结构。
  • 每隔1秒,每个哨兵节点会向Master和Slave节点还有其它哨兵节点发送ping命令做心跳检测,看看是否存在不可达的节点。
  • 主观下线,如果某个哨兵向一个节点发出的心跳检测没有得到响应,那么该哨兵认为该节点已经下线。
  • 客观下线,当哨兵主观下线的节点是主节点时,哨兵会向其他的哨兵询问对主节点的判断,当下线判断超过一定个数时,那么哨兵会认为主节点确实已经下线,那么会对主节点进行客观下线的判定。
  • 故障转移,当Master节点客观下线时,哨兵会从Slave节点中选择一个节点作为Master节点,选择规则是选择与主节点复制相似度最高的节点,选择完成后会将其余的Slave节点指向新的Master节点,并监控原来的Master节点,当它回复后作为新Master节点的Slave存在,并且同步新Master节点的数据。
  • 选举领导者哨兵节点:当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。
  • 当使用sentinel模式的时候,客户端不用直接连接Redis,而是连接哨兵的ip和port,由哨兵来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,哨兵就会感知并将新的master节点提供给使用者。

由于哨兵需要选择领导者节点,所以需要至少部署3个实例才能形成选举关系。

哨兵模式的关键配置信息如下:

# Master实例的IP、端口,以及选举需要的赞成票数
sentinel monitor mymaster 127.0.0.1 6379 2  
# 多长时间没有响应视为Master失效
sentinel down-after-milliseconds mymaster 60000  
# 两次failover尝试间的间隔时长
sentinel failover-timeout mymaster 180000  
#如果有多个Slave,可以通过此配置指定同时从新Master进行数据同步的Slave数,避免所有Slave同时进行数据同步导致查询服务也不可用
sentinel parallel-syncs mymaster 1  

7-4、Cluster模式

哨兵模式同样存在一些缺点:哨兵无法对Slave进行自动故障转移,在读写分离场景下,Slave故障会导致读服务不可用;哨兵无法解决负载均衡、存储能力受到单机限制的问题。

Redis Cluster模式是Redis3.0之后推荐的一种解决方案,是由多个主节点群组成的分布式服务器群。它具有复制、高可用和分片的特性。

Redis Cluster集群不需要哨兵也能完成节点移除和故障转移的功能。这种集群模式没有中心节点,可水平扩展,且集群配置简单。

7-4-1、Cluster模式特点

  • 多个Redis节点互联,数据共享。
  • 所有的节点都是主从模式,其中Slave不提供服务,只提供备用。
  • 不支持同时处理多个Key,因为需要分发到多个节点上。
  • 支持在线增加、删除节点。
  • 客户端可以连接任何一个Master节点进行读写。

7-4-2、Cluster模式工作过程

  • Redis Cluster有固定的16384个hash slot(槽),每个master都会持有部分slot,比如有3个master,那么可能每个master持有5000多个hash slot;
  • 在redis cluster写入数据的时候,计算每个key的CRC16值,然后对16384取模,可以获取key对应的hash slot;
  • 写请求可以发送到任意一个master上去,任意一个master都会计算这个key对应hash slot,找到对应的master,然后写入。
  • 主观下线(pfail):集群中的每个节点都会定期向其他节点发送ping消息,如果在一段时间内一直通信失败,则发送节点方认为接收节点存在故障,把接收节点标为主观下线(pfail)状态。
  • 客观下线(fail):当某个节点判断另一个节点主观下线后,相应的节点状态就会在集群中进行传播,如果集群中半数以上主节点(注意是主节点,Slave节点无人权)都将它标为主观下线,那么该节点为客观下线。
  • 主节点客观下线以后进行故障转移。先是判断该主节点的Slave节点是否具有当主节点的资格,若Slave节点与主节点断开连接超过一定的时间那么就没有资格。之后根据从节点与主节点之间的偏移量进行延迟选举,保证偏移量最小的slave节点获得更多的票。

另外,Redis Cluster集群目前无法做数据库选择,默认在0数据库。

还有,由于哈希槽数量是16384,所以理论上Redis Cluster主节点的数量上限也就是16384。

7-4-3、Redis Cluster节点间的通讯协议:Gossip 协议

通常情况下,集群元数据的维护有两种方式:集中式、Gossip 协议。

Redis Cluster 的节点间采用 Gossip 协议进行通信。

可以把 Gossip 写一下的通信过程,想想成病毒传播,从发起通信的一方开始把消息“感染”遍整个集群。

  • Gossip 是周期性的散播消息,把周期限定为 1 秒
  • 被感染节点随机选择 k 个邻接节点散播消息(fanout)
  • 每次散播消息都选择尚未发送过的节点进行散播,直到相邻节点都被散播过消息
  • 收到消息的节点不再往发送节点散播,比如 A → B,那么 B 进行散播的时候,不再发给 A。

Gossip 协议的优点在于 扩展性好(允许节点任意增删)、容错率高、去中心化、一致性收敛(集群的不一致可以在很短时间内收敛到一致)、实现简单。

不足之处在于 消息延迟,不适用于对实时性高的场景;节点接收消息时会出现冗余(多次接收)。

7-4-4、补充:一致性哈希算法

Redis对于集群中节点的分布采用了哈希槽的方法,而不是一致性哈希算法。关于一致性哈希,可以参考这份资料:

参考:一致性哈希算法

7-4-5、补充:分布式寻址方式总结

  1. 分布式寻址算法
  • 直接hash散列
  • 一致性哈希(自动缓存迁移)+ 虚拟节点(负载均衡)
  • Redis Cluster 的哈希槽
  1. 优点
  • 无中心架构,支持动态扩容
  • 具备Sentinel的监控和自动failover(故障转移)能力
  • 客户端连接集群中任何一个可用节点即可
  • 客户端直连redis服务,免去了proxy代理层的损耗
  1. 缺点
  • 运维复杂,数据迁移需要人工干预
  • 只能使用0号数据库
  • 不支持跨分片操作(pipeline等)
  • 分布式逻辑和存储模块耦合等

7-5、第三方提供的Redis集群方案

Twemproxy是Twitter开源的缓存代理系统。

它相当于一个代理,使用方法和普通redis无任何区别,设置好它下属的多个redis实例后,使用时在需要连接redis的地方改为连接Twemproxy。

Twemproxy会以一个代理的身份接收请求并使用一致性hash算法,将请求转接到具体redis,将结果再返回twemproxy。使用方式简便(只需修改连接端口),适用于旧项目扩展。

Twemproxy自身可以形成集群,客户端连接任意一个Twemproxy实例即可。

另外还有豌豆荚开源的 codis ,特点与Twemproxy基本一致,支持在节点数量改变情况下,旧节点数据恢复到新hash节点。

8、数据分片(Sharding)

8-1、数据分片的实现

当Redis中存储的数据量大,一台主机的物理内存已经无法容纳时,就需要考虑进行数据分片。

分片指的是按照某种规则去划分数据,分散存储在多个节点上。通过将数据分到多个Redis服务器上,来减轻单个Redis服务器的压力。分片后可以让Redis管理更大的内存,Redis将可以使用集群内所有机器的内存。

通过 7-4-2 一节可以看到,数据“计算key的CRC16值,然后对16384取模,找到对应的hash slot”这个过程,实际上就是在实现数据分片存储。Redis Cluster模式下,每一个主节点存储的数据都是不一样的。所以Redis Cluster模式也是数据分片的解决方案,同时是目前推荐的方案。

Redis Cluster对于数据分片的支持:

  • 能够自动将数据分散在多个节点上
  • 当访问的key不在当前分片上时,能够自动将请求转发至正确的分片(即查询路由 Query routing)
  • 当集群中部分节点失效时仍能提供服务(因为Cluster集群中每个节点都采用主从模式)

8-2、hash tags

在基础的分片原则上,Redis还支持hash tags功能,以hash tags要求的格式key,将会确保进入同一个Slot中。

例如:{uiv}user:1000和{uiv}user:1001拥有同样的hash tag {uiv},就一定会保存在同一个Slot中。

8-3、数据分片的限制

使用Redis Cluster时,pipelining、事务和LUA Script功能涉及的key必须在同一个数据分片上,否则将会返回错误。

可以在数据存储的时候,就是用hash tags功能将相关数据保存在同一个数据分片上。

8.5、关于哨兵(主从)模式与Redis Cluster模式的取舍

单纯从功能的强大上来说,Redis Cluster模式是碾压哨兵模式的。

但是在平时的工程实践中,同样要考虑硬件成本、开发难易度、运维复杂度、问题排查难度、性能优化难度等等各方面的因素,并不是越强大越复杂的架构就越好。

  1. 计划在Redis中存储什么样的数据?存储数据的量有多大?未来1年、3年可能发展到多大?是不是不做数据分片就存不下?
  2. 存储的是什么性质的数据,大量数据都需要长期保存吗?使用LRU算法做数据淘汰是否会影响系统应用?
  3. 开发中会大量使用事务、管道、lua脚本吗?
  4. Redis面临的并发压力有多大?(参考:根据Redis官宣,单机实例,QPS 10万+)

如果单台服务器的内存大小和性能足以应对未来3年的业务发展,那么使用哨兵主从模式足以应对,可以减少日常的很多麻烦。

(关于Redis的性能问题,可以参看 Redis官网的benchmark How fast is Redis? )

9、Redis实现分布式锁

9-1、分布式锁需要解决的问题

  • 互斥性:任意时刻只有一个客户端获取到锁,不能有两个客户端同时获取到锁。
  • 安全性:锁只能被持有该锁的客户端删除,不能由其它客户端删除。
  • 死锁:获取锁的客户端因为某些原因而宕机继而无法释放锁,其它客户端再也无法获取锁而导致死锁,此时需要有特殊机制来避免死锁。
  • 容错:当各个节点,如某个redis节点宕机的时候,客户端仍然能够获取锁或释放锁。

9-2、单节点实现分布式锁

从Redis2.6.12版本开始,使用Set操作,将setnx和expire融合在一起执行。

SETKEYvalue[EX seconds][PX milliseconds][NX|XX]
  • EX second:设置键的过期时间为second秒。
  • PX millisecond:设置键的过期时间为millisecond毫秒。
  • NX:只在键不存在时,才对键进行设置操作。
  • XX:只在键已经存在时,才对键进行设置操作。
  • 注:SET操作成功完成时才会返回OK,否则返回nil。

加锁代码实现:

/**
 * 获取分布式锁
 * @param key
 * @param uniqueId 请求的唯一值。作为解锁时的验证手段
 * @param seconds
 * @return
 */
public static boolean tryLock(String key, String uniqueId, int seconds) {
    return "OK".equals(jedis.set(key, uniqueId, "NX", "EX", seconds));
}

解锁的代码实现:

/**
 * 释放分布式锁
 * @param key
 * @param uniqueId
 */
public static void releaseLock(String key, String uniqueId) {
    if (uniqueId.equals(jedis.get(key))) {
        jedis.del(key);
    }
}

上述代码无法保证get和del方法的原子性问题,更严谨的解锁方式是使用lua脚本:

/**
 * 释放分布式锁
 * @param key
 * @param uniqueId
 */
public static boolean releaseLock(String key, String uniqueId) {
    String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "return redis.call('del', KEYS[1]) else return 0 end";
    return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(uniqueId)).equals(1L);
}

这种实现的一个最大的问题点,就是在加锁之后如果实际业务运行时间大于锁的过期时间的话,持有的锁会被无端释放。

然而如果锁的过期时间过长,在业务处理自身崩溃无暇解锁的情况下,锁会长时间阻塞,降低系统吞吐量。

9-3、Redis 分布式锁 Redlock 算法

RedLock算法,是Redis作者提出的一种利用Redis集群来实现分布式锁的方法。此种方式比单节点的方法更安全。

如果使用的是 Redisson 客户端的话,可以使用getRedLock()方法直接使用RedLock。

其核心思想是这样的:

  1. 顺序向所有节点请求加锁
  2. 根据一定的超时时间来推断是不是跳过该节点
  3. N/2 + 1 个节点加锁成功并且花费时间小于锁的有效期则认定加锁成功

更详细的过程说明和算法分析请自行搜索。

最有趣的是在Redis官网的RedLock页面上,还贴有“神仙打架”的链接,一位资深分布式架构师对RedLock提出的质疑,以及Redis作者的回复。

9-4、ZK分布式锁

如果对于Redis的单节点锁和RedLock的可靠性都存疑的话,可以尝试使用ZK来实现分布式锁,一些观点认为ZK更为可靠。

相关实现方法请自行搜索。

(其实ZK也是存在问题的,现阶段100%可靠的分布式锁是不存在的吧。各种方法拼的都是99.99……%后面小数点到几位)

10、Redis实现消息队列

Redis5.0 增加了一个新的数据结构Stream,它是一个新的强大的支持多播的可持久化的消息队列,大量借鉴了Kafka的设计。

或者使用基础数据类型实现建议消息队列:使用 Redis 实现简单的消息队列

11、Redis缓存的雪崩、穿透、击穿

11-1、Redis 缓存雪崩

Redis 缓存雪崩

11-2、Redis 缓存穿透

Redis 缓存穿透

11-3、Redis 缓存击穿

Redis 缓存击穿

12、Redis是单线程模型但是为什么能保证高性能

12-1、基本原因

以下章节其实只是解释了,为什么单线程能做到高并发。但是为什么能达到一个极高的性能(10w+QPS)就必须去研究Redis底层的数据结构和各种性能优化手段了。

比如Redis是使用C语言开发的,但是它的字符串没有使用C语言的字符串,而是使用了SDS(Simple Dynamic String,简单动态字符串)这种结构体来保存字符串。其他还有跳表的使用、压缩列表(ziplist)的使用,编码转化技术等等。所以不深入研究源码,实际上是无法回答这个问题的

  1. Redis完全基于内存,绝大部分请求是纯粹的内存操作,存取均不会受到硬盘IO的限制,执行效率高。
  2. 使用单线程模型处理并发请求,可以避免频繁的上下文切换和锁的竞争。
  3. Redis使用非阻塞的I/O多路复用模型(Redis采用的I/O多路复用函数:epoll/kqueue/evport/select)。

12-2、Redis 的文件事件处理器

Redis 内部使用文件事件处理器 file event handler(基于Reactor模式),这个文件事件处理器是单线程的。

而文件事件就是服务器对socket操作的抽象,每当一个socket准备好执行连接应答(accept)、写入、读取、关闭等操作时,就会产生一个文件事件。

文件事件处理器包含四个部分:

  • 多个 socket
  • I/O 多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

I/O 多路复用程序会监听多个socket,socket会并发产生各种不同的请求,请求被放入队列,由并行变成串行。
事件分派器消费队列中的请求,每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

12-3、Redis中一次请求的响应过程

在此模型下,Redis中一次请求的响应过程是这样的:

假设此时客户端发送了一个 set key value 请求

  1. redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将事件压入队列;
  2. 事件分派器从队列中获取到该事件,之前 socket01 的 AE_READABLE 事件已经与命令请求处理器关联(当Redis服务器进行初始化的时候,会将命令请求处理器和服务器监听socket的 AE_READABLE 事件关联起来),事件分派器将事件交给命令请求处理器来处理;
  3. 命令请求处理器读取 socket01 的 key value 并在内存中完成 key value 的设置;
  4. 操作完成后,命令请求处理器将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联;
  5. 如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中;
  6. 事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok;
  7. 之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。

这里面的两类事件 AE_READABLE和AE_WRITABLE ,可读和可写的主体指的是socket。比如socket接收到了客户端的set key请求,等待Redis服务器来读取自己,这时产生的事件就是 AE_READABLE 。

如果一个socket同时出现这两种事件,那么文件分派器会优先处理 AE_READABLE 事件

12-4、补充: Redis 的时间事件

Redis 服务器是事件驱动的,其主要处理的事件除了上面的文件事件,还有时间事件。

Redis 目前的时间事件只有周期性事件一类,不使用定时事件。

Redis 服务器将所有的时间事件都放在了一个无序列表中,每当时间事件执行器运行时,它就会遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

Redis 以周期性时间事件方式来运行 serverCron 函数,该函数主要负责执行以下工作:

  • 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等
  • 清理数据库中过期的键
  • 关闭和清理连接失效的客户端
  • 尝试进行AOF或RDB持久化操作
  • 如果服务器是主服务器,那么对从服务器进行定期数据同步
  • 如果处于集群模式,对集群进行定期同步和连接测试

文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件。并且由于文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器也不会中断正在执行的事件处理,也不会对事件进行抢占。所以时间事件的实际处理时间经常会比设定的时间稍晚一些(因为即使时间到了,时间事件也不可以抢占文件事件的资源)。

13、Redis 与 MySQL 的数据同步

Redis 与 MySQL 的数据同步

14、Redis性能调优

尽管Redis是一个非常快速的内存数据存储媒介,也并不代表Redis不会产生性能问题。

  1. Redis采用单线程模型,所有的命令都是由一个线程串行执行的,所以当某个命令执行耗时较长时,会拖慢其后的所有命令,这使得Redis对每个任务的执行效率更加敏感。要确保没有让Redis执行耗时长的命令,适当运用Pipeline将连续执行的命令组合执行。

  2. 尽可能在物理机上直接部署Redis。如果在虚拟机中运行Redis,注意查看虚拟机环境的固有延迟,对虚拟机进行优化。

  3. 尽量避免使用时间复杂度为O(N)的命令,N的数量级不可预知时会阻塞Redis线程。官网对每个命令的时间复杂度都有说明,可以参阅。

  • 不要把List当做列表使用,仅当做队列来使用
  • 通过业务代码严格控制Hash、Set、Sorted Set的大小
  • 尽可能将排序、并集、交集等操作放在客户端执行
  • 禁止使用KEYS命令
  • 避免一次性遍历集合类型的所有成员的命令。使用SCAN类的命令进行分批的,游标式的遍历
  • 利用Redis的show log功能记录耗时较长的命令,进行分析优化
  1. 数据持久化也可能引发较大延迟

    持久化不是做的越全就越好,需要根据数据的安全级别和性能要求制定合理的持久化策略。

  • AOF + fsync always的设置虽然能够绝对确保数据安全,但每个操作都会触发一次fsync,会对Redis的性能有较大影响

  • AOF + fsync every second,每秒fsync一次是比较折中的方案,

  • AOF + fsync never会提供AOF持久化方案下的最优性能(写盘时机由OS控制)

  • 使用RDB持久化通常会提供比使用AOF更高的性能,但需要注意RDB的策略配置

  • 每一次RDB快照和AOF Rewrite都需要Redis主进程进行fork操作。fork操作本身可能会产生较高的耗时,与CPU和Redis占用的内存大小有关。根据具体的情况合理配置RDB快照和AOF Rewrite时机,避免过于频繁的fork带来的延迟

    Redis在fork子进程时需要将内存分页表拷贝至子进程,如果Redis实例占用24GB内存的话,共需要拷贝24GB / 4kB * 8 = 48MB的数据。在单核Xeon 2.27Ghz的物理机上,这一fork操作耗时216ms。

    可以通过INFO命令返回的latest_fork_usec字段查看上一次fork操作的耗时(微秒)

  • 注意,有观点认为,“对数据安全性要求不高的情况下,可以考虑Master不做任何持久化工作,在Slave上开启AOF专门做备份”。这是有重大隐患的。因为一旦Master宕机,重启之后,由于没有做持久化,数据是空的,然后数据同步到Slave,Slave的数据也会被清空。尤其是自动重启的情况,哨兵/Sentinel 还没有来得及做failover,Slave节点没有变成Master节点。

  1. 数据淘汰引发延迟

    当同一秒内有大量key过期时,也会引发Redis的延迟。可以在设置过期时间时追加一个小的随机数。

  2. 尽可能实施读写分离策略

    尤其是针对一些使用了长耗时命令的统计类任务,完全可以指定在一个从节点上执行,避免长耗时命令影响其他请求的响应。

  3. 为了网络传输的稳定性,所有节点尽可能部署在同一个局域网内

  4. 避免在Master节点上挂载过多Slave节点,而使用单向链表结构,在Slave上面挂载Slave。

  5. Swap引发延迟

    当Linux将Redis所用的内存分页移至swap空间时,将会阻塞Redis进程,导致Redis出现不正常的延迟。Swap通常在物理内存不足或一些进程在进行大量I/O操作时发生。

    /proc//smaps文件中会保存进程的swap记录,通过查看这个文件,能够判断Redis的延迟是否由Swap产生。如果这个文件中记录了较大的Swap size,则说明延迟很有可能是Swap造成的。

  6. 尽可能使用Hash

    Hash使用的内存非常小。相比于复数个key-value,将数据模型抽象到一个Hash里面性能会更好。

15、其他

15-1、Redis的重要版本

Redis版本号的命名规则:

  • 版本号第二位如果是奇数,则为非稳定版本 如2.7
  • 版本号第二位如果是偶数,则为稳定版本 如2.8
  • 当前奇数版本就是下一个稳定版本的开发版本,如2.7版本是3.8版本的开发版本
版本号 发布日期 重要功能
2.6 2012.10 Lua脚本
从节点只读功能
benchmark功能
2.8 2013.11 部分复制psync
Redis Sentinel 第二版
增加set命令
3.0 2015.04 cluster集群
LRU算法性能提升
大量命令性能提升
3.2 2016.05 新增GEO数据格式
新RDB格式
4.0 2017.07 模块系统
psync 2.0
LRU优化
异步多线程删除LazyFree
混合持久化方案
兼容NAT和Docker
redis-cell:一个基于漏斗算法的原子性限流模块
布隆过滤器以插件的形式加载到 Redis Server 中
5.0 2018.10 新增Stream数据格式
RDB增加LFU和LRU
核心代码重构
6.0 预计2020.04 ACL功能对用户进行更细粒度的权限控制
SSL
RESP3:新的 Redis 通信协议
客户端缓存功能
IO多线程(指客户端交互部分,非执行命令多线程)
Proxy 功能,让 Cluster 拥有像单实例一样简单的接入方式

15-2、Redis的Java客户端的选择

对于最常见的两种Java客户端Jedis和Redisson,尽管Jedis比起Redisson有不足,但也应该在需要使用Redisson的高级特性时再选用Redisson,避免造成不必要的程序复杂度提升。

  1. Jedis:

轻量,简洁,便于集成和改造。

支持连接池,支持pipelining、事务、LUA Scripting、Redis Sentinel、Redis Cluster。

不支持读写分离,需要自己实现

  1. Redisson:

Redisson是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象 (Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet, Map, ConcurrentMap, List, ListMultimap, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, ReadWriteLock, AtomicLong, CountDownLatch, Publish / Subscribe, HyperLogLog)。

支持异步请求、支持连接池、支持pipeline、LUA Scripting、Redis Sentinel、Redis Cluster。

不支持事务,官方建议以LUA Scripting代替事务。

支持读写分离,支持读负载均衡,在主从复制和Redis Cluster架构下都可以使用。

你可能感兴趣的:(请叫我攻城狮)