Redis 知识点超级大合集

文章目录

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.62012.10Lua脚本
从节点只读功能
benchmark功能
2.82013.11部分复制psync
Redis Sentinel 第二版
增加set命令
3.02015.04cluster集群
LRU算法性能提升
大量命令性能提升
3.22016.05新增GEO数据格式
新RDB格式
4.02017.07模块系统
psync 2.0
LRU优化
异步多线程删除LazyFree
混合持久化方案
兼容NAT和Docker
redis-cell:一个基于漏斗算法的原子性限流模块
布隆过滤器以插件的形式加载到 Redis Server 中
5.02018.10新增Stream数据格式
RDB增加LFU和LRU
核心代码重构
6.0预计2020.04ACL功能对用户进行更细粒度的权限控制
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架构下都可以使用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值