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适合的场景主要局限在较小数据量的高性能操作和运算上。
最常用就是使用Redis做会话缓存。Redis相比与其他存储的优势在于可持久化。
除基本的会话token之外,Redis还提供简便的FPC平台。即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度下降。
Redis在内存存储引擎领域的最大一个优点就是提供list和set操作,这使得Redis能做为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如Python)对 list 的 push/pop 操作。
Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(set)和有序集合(Sorted Set)也使得我们在执行这些操作时变得非常简单,Redis只是正好提供了这两种数据结构。
Redis自带的发布/订阅功能可使用场景非常多。可以在社交网络连接中使用,还可以作为基于发布/订阅的脚本触发器,甚至可以用来建立聊天系统。
Redis采用Key-Value结构存储数据,任何二进制序列都可以作为Redis的Key使用(普通的字符串或一张JPEG图片)。
一些注意事项:
最基本的数据类型,其值最大可存储512M,二进制安全(Redis的String可以包含任何二进制数据,包含jpg对象等)。
Redis没有Integer、Float、Boolean等数据类型的概念,所有的基本类型在Redis中都以String体现。
常用命令:
注意事项:
同时由于Redis采用单线程模型,天然线程安全的,这使得INCR/DECR命令可以非常便利的实现高并发场景下的精确控制。
String元素组成的字典,和传统的哈希表一样,是一种field-value型的数据结构,可以理解成将HashMap搬入Redis。
Redis中的Hash与List相比,提供了效率高得多的随机访问命令。
常用命令:
应慎用的命令:
上述三个命令都会对Hash进行完整遍历,Hash中的field数量与命令的耗时线性相关,应尽量避免使用,而改为使用HSCAN命令进行游标式的遍历。可以参看本章的 “2-7-2、使用SCAN cursor” 一节。
链表型的数据结构,可以在List的两端执行插入元素和弹出元素的操作。虽然支持在特定index上插入和读取元素的功能,但其时间复杂度较高(O(N)),应慎用。
常用命令:
应慎用的命令:
由于Redis的List是链表结构的,上述的命令的算法效率较低,需要对List进行遍历,命令的耗时无法预估,在List长度大的情况下耗时会明显增加,应慎用。
补充说明:Redis的List实际是设计来用于实现队列的,而不是实现类似Java的ArrayList这样的数据类型的。
String元素组成的无序集合,通过哈希表实现(增删改查时间复杂度为O(1)),不允许重复。
使用smembers遍历set中的元素时,其顺序也是不确定的,是通过hash运算过后的结果。Redis还对集合提供了求交集、并集、差集等操作,可以实现如同共同关注,共同好友等功能。
常用命令:
应慎用的命令:
上述几个命令涉及的计算量大,应谨慎使用。需要遍历时,优先使用SSCAN命令。
Sorted Set是有序的、不可重复的String集合。Sorted Set中的每个元素都需要指派一个分数(score),Sorted Set会根据score对元素进行升序排序。如果多个member拥有相同的score,则以字典序进行升序排序。
Sorted Set非常适合用于实现排名。
常用命令:
应慎用的命令:
上述命令应避免传递[0 -1]或[-inf +inf]这样的参数,来对Sorted Set做一次性的完整遍历。可以通过ZSCAN命令来进行游标式的遍历。
或通过LIMIT参数来限制返回member的数量(适用于ZRANGEBYSCORE和ZREVRANGEBYSCORE命令)
使用KEYS [pattern]:查找所有符合给定模式pattern的key
但是keys会一次性返回所有符合条件的key,所以会造成Redis的卡顿,形成隐患。
另外如果一次性返回所有key,对内存的消耗在某些条件下也是巨大的。
使用例:
keys test* //返回所有以test为前缀的key
从性能和安全性上考虑,应该优先使用SCAN命令。
使用SCAN cursor [MATCH pattern] [COUNT 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的说明
数据类型 | 特点 | 应用场景 |
---|---|---|
String | 任意二进制数据,最大512M | |
List | 两端压入或弹出 | 用于实现队列,而不是ArrayList,避免按下标访问 |
Hash | 同java的HashMap | 结构化数据 |
Set | 无序集合,不可重复 | 交集、并集、差集 |
Sorted Set | 有序集合,不可重复 | Set增强版。各种需要排序的数据。可实现延时队列 |
在Redis中,允许用户设置最大使用内存大小server.maxmemory,当Redis 内存数据量上升到一定大小的时候,就会施行数据淘汰策略。
其中volatile和allkeys规定了是对已设置过期时间的数据集淘汰数据、还是从全部数据集淘汰数据;
后面的lru、ttl以及random是三种不同的淘汰策略,再加上一种no-enviction永不回收的策略。
ttl和random比较容易理解,实现也会比较简单。主要是lru最近最少使用淘汰策略,设计上会对key 按失效时间排序,然后取最先失效的key进行淘汰。
生存时间可以通过使用 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 (即永久有效)
可以对一个已经带有生存时间的key执行EXPIRE命令,新指定的生存时间会取代旧的生存时间。
Redis目前有两种持久化方式:RDB和AOF。
AOF(Append-Only-File)为增量持久化,记录每次对服务器写的操作。追加保存每次写的操作到文件末尾。Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
RDB(Redis Database)是通过保存某个时间点的全量数据快照实现数据的持久化,当恢复数据时,直接通过rdb文件中的快照,将数据恢复。
简单来说,RDB备份的是数据库的数据,AOF备份的是接收到的指令。
采用AOF持久方式时,Redis会把每一个写请求都记录在一个日志文件里。在Redis重启时,会把AOF文件中记录的所有写操作顺序执行一遍,确保数据恢复到最新。
AOF默认是关闭的,如要开启,进行如下配置:
redis.conf:
appendonly yes
# appendsync always
appendfsync everysec
# appendfsync no
AOF提供了三种fsync配置:always/everysec/no,通过配置项[appendfsync]指定:
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。
采用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文件:
当redis重启的时候会优先载入AOF文件来恢复原始的数据。如果没有AOF文件,则加载RDB文件。如果RDB也不存在,则数据恢复失败报错。
如果想优先保证数据安全性,应该要开启AOF模式。因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。不过RDB 恢复数据集的速度比AOF恢复的速度要快。
如果可以承受数分钟以内的数据丢失,那么可以只使用RDB持久化(使用命令bgsave进行全量持久化时,耗时较长,不够实时,所以会造成数据丢失)。RDB便于数据库备份。
如果只把Redis作为缓存服务使用,Redis中存储的所有数据都不是该数据的主体而仅仅是同步过来的备份,那么可以关闭Redis的数据持久化机制。
但通常来说,仍然建议至少开启RDB方式的数据持久化,因为:
redis4.0之后推出了此种持久化方式,RDB作为全量备份,AOF作为增量备份,并且将此种方式作为默认方式使用。
在RDB-AOF方式下,持久化策略首先将缓存中数据以RDB方式全量写入文件,再将写入后新增的数据以AOF的方式追加在RDB数据的后面,在下一次做RDB持久化的时候将AOF的数据重新以RDB的形式写入文件。
这种方式既可以提高读写和恢复效率,也可以减少文件大小,同时可以保证数据的完整性。
在此种策略的持久化过程中,子进程会通过管道从父进程读取增量数据,在以RDB格式保存全量数据时,也会通过管道读取数据,同时不会造成管道阻塞。可以说,在此种方式下的持久化文件,前半段是RDB格式的全量数据,后半段是AOF格式的增量数据。此种方式是目前较为推荐的一种持久化方式。
Redis基于请求/响应模型,单个请求处理需要一一应答。如果需要同时执行大量命令,则每条命令都需要等待上一条命令执行完毕后才能继续执行,这中间不仅仅多了RTT,还多次使用了系统IO。
虽然Redis提供了一些批量处理命令,比如 MSET/MGET/HMSET/HMGET ,但是它们只能将相同的指令进行合并。
管道Pipeline可以让Redis批量执行指令,将多次IO往返的时间缩减为一次。
但是如果指令之间存在依赖关系,则需要分批发送指令。意思是说Pipeline只能用于执行连续且无相关性的命令,当某个命令的执行需要依赖于前一个命令的返回结果时,就无法使用Pipeline。Pipeline并不保证命令执行时的顺序。要规避这一局限性则必须使用脚本。
Redis的事务可以确保复数命令执行时的原子性。
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
通过MULTI和EXEC命令来把这两个命令加入一个事务中:
> MULTI
OK
> GET vCount
QUEUED
> SET vCount 0
QUEUED
> EXEC
1) 12384
2) OK
Redis在接收到MULTI命令后便会开启一个事务,这之后的所有读写命令都会保存在队列中但并不执行,直到接收到EXEC命令后,Redis会把队列中的所有命令连续顺序执行,并以数组形式返回每个命令的返回结果。
可以使用DISCARD命令放弃当前的事务,将保存的命令队列清空。
在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的监控。
需要注意的是,Redis事务不支持回滚:
如果一个事务中的命令出现了语法错误,大部分客户端驱动会返回错误,2.6.5版本以上的Redis也会在执行EXEC时检查队列中的命令是否存在语法错误,如果存在,则会自动放弃事务并返回错误。
但如果一个事务中的命令有非语法类的错误(比如对String执行HSET操作),无论客户端驱动还是Redis都无法在真正执行这条命令之前发现,所以事务中的所有命令仍然会被依次执行。在这种情况下,会出现一个事务中部分命令成功部分命令失败的情况,然而与RDBMS不同,Redis不提供事务回滚的功能,所以只能通过其他方法进行数据的回滚。
通过EVAL与EVALSHA命令,可以让Redis执行LUA脚本。这就类似于RDBMS的存储过程一样,可以把客户端与Redis之间密集的读/写交互放在服务端进行,避免过多的数据交互,提升性能。
Scripting功能是作为事务功能的替代者诞生的,事务提供的所有能力Scripting都可以做到。Redis官方推荐使用LUA Script来代替事务,其效率和便利性都超过了事务。
参看“9、Redis实现分布式锁”一节中eval命令的写法。
Redis一般是使用一个Master节点来进行写操作,而若干个Slave节点进行读操作,实现读写分离。
另外定期的数据备份操作也是单独选择一个Slave去完成,这样可以最大程度发挥Redis的性能。
Master和Slave的数据不是一定要即时同步的,但是在一段时间后Master和Slave的数据是趋于同步的,保证最终一致性。
启用主从复制非常简单,只需要一行配置信息:
slaveof 192.168.1.1 6379 #指定Master的IP和端口
全量同步一般发生在Slave初始化阶段,但其实在任何时候Slave都可以向Master发起全量同步的请求,这时Slave会将Master上的所有数据都复制一份。
Redis增量同步一般发生在Slave已经初始化完成,开始正常连接Master的阶段。
slave 节点在做同步的时候,也不会阻塞自己提供的查询操作,它会用旧的数据集来提供服务。
但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会阻塞主进程,暂停对外服务了。
主从模式弊端:当Master宕机后,Redis集群将不能对外提供写入操作。
Redis2.8开始,Redis正式提供了哨兵模式(Redis Sentinel)的架构,来解决主从切换问题。
由于哨兵需要选择领导者节点,所以需要至少部署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
哨兵模式同样存在一些缺点:哨兵无法对Slave进行自动故障转移,在读写分离场景下,Slave故障会导致读服务不可用;哨兵无法解决负载均衡、存储能力受到单机限制的问题。
Redis Cluster模式是Redis3.0之后推荐的一种解决方案,是由多个主节点群组成的分布式服务器群。它具有复制、高可用和分片的特性。
Redis Cluster集群不需要哨兵也能完成节点移除和故障转移的功能。这种集群模式没有中心节点,可水平扩展,且集群配置简单。
另外,Redis Cluster集群目前无法做数据库选择,默认在0数据库。
还有,由于哈希槽数量是16384,所以理论上Redis Cluster主节点的数量上限也就是16384。
通常情况下,集群元数据的维护有两种方式:集中式、Gossip 协议。
Redis Cluster 的节点间采用 Gossip 协议进行通信。
可以把 Gossip 写一下的通信过程,想想成病毒传播,从发起通信的一方开始把消息“感染”遍整个集群。
Gossip 协议的优点在于 扩展性好(允许节点任意增删)、容错率高、去中心化、一致性收敛(集群的不一致可以在很短时间内收敛到一致)、实现简单。
不足之处在于 消息延迟,不适用于对实时性高的场景;节点接收消息时会出现冗余(多次接收)。
Redis对于集群中节点的分布采用了哈希槽的方法,而不是一致性哈希算法。关于一致性哈希,可以参考这份资料:
参考:一致性哈希算法
Twemproxy是Twitter开源的缓存代理系统。
它相当于一个代理,使用方法和普通redis无任何区别,设置好它下属的多个redis实例后,使用时在需要连接redis的地方改为连接Twemproxy。
Twemproxy会以一个代理的身份接收请求并使用一致性hash算法,将请求转接到具体redis,将结果再返回twemproxy。使用方式简便(只需修改连接端口),适用于旧项目扩展。
Twemproxy自身可以形成集群,客户端连接任意一个Twemproxy实例即可。
另外还有豌豆荚开源的 codis ,特点与Twemproxy基本一致,支持在节点数量改变情况下,旧节点数据恢复到新hash节点。
当Redis中存储的数据量大,一台主机的物理内存已经无法容纳时,就需要考虑进行数据分片。
分片指的是按照某种规则去划分数据,分散存储在多个节点上。通过将数据分到多个Redis服务器上,来减轻单个Redis服务器的压力。分片后可以让Redis管理更大的内存,Redis将可以使用集群内所有机器的内存。
通过 7-4-2 一节可以看到,数据“计算key的CRC16值,然后对16384取模,找到对应的hash slot”这个过程,实际上就是在实现数据分片存储。Redis Cluster模式下,每一个主节点存储的数据都是不一样的。所以Redis Cluster模式也是数据分片的解决方案,同时是目前推荐的方案。
Redis Cluster对于数据分片的支持:
在基础的分片原则上,Redis还支持hash tags功能,以hash tags要求的格式key,将会确保进入同一个Slot中。
例如:{uiv}user:1000和{uiv}user:1001拥有同样的hash tag {uiv},就一定会保存在同一个Slot中。
使用Redis Cluster时,pipelining、事务和LUA Script功能涉及的key必须在同一个数据分片上,否则将会返回错误。
可以在数据存储的时候,就是用hash tags功能将相关数据保存在同一个数据分片上。
单纯从功能的强大上来说,Redis Cluster模式是碾压哨兵模式的。
但是在平时的工程实践中,同样要考虑硬件成本、开发难易度、运维复杂度、问题排查难度、性能优化难度等等各方面的因素,并不是越强大越复杂的架构就越好。
如果单台服务器的内存大小和性能足以应对未来3年的业务发展,那么使用哨兵主从模式足以应对,可以减少日常的很多麻烦。
(关于Redis的性能问题,可以参看 Redis官网的benchmark How fast is Redis? )
从Redis2.6.12版本开始,使用Set操作,将setnx和expire融合在一起执行。
SETKEYvalue[EX seconds][PX milliseconds][NX|XX]
加锁代码实现:
/**
* 获取分布式锁
* @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);
}
这种实现的一个最大的问题点,就是在加锁之后如果实际业务运行时间大于锁的过期时间的话,持有的锁会被无端释放。
然而如果锁的过期时间过长,在业务处理自身崩溃无暇解锁的情况下,锁会长时间阻塞,降低系统吞吐量。
RedLock算法,是Redis作者提出的一种利用Redis集群来实现分布式锁的方法。此种方式比单节点的方法更安全。
如果使用的是 Redisson 客户端的话,可以使用getRedLock()方法直接使用RedLock。
其核心思想是这样的:
更详细的过程说明和算法分析请自行搜索。
最有趣的是在Redis官网的RedLock页面上,还贴有“神仙打架”的链接,一位资深分布式架构师对RedLock提出的质疑,以及Redis作者的回复。
如果对于Redis的单节点锁和RedLock的可靠性都存疑的话,可以尝试使用ZK来实现分布式锁,一些观点认为ZK更为可靠。
相关实现方法请自行搜索。
(其实ZK也是存在问题的,现阶段100%可靠的分布式锁是不存在的吧。各种方法拼的都是99.99……%后面小数点到几位)
Redis5.0 增加了一个新的数据结构Stream,它是一个新的强大的支持多播的可持久化的消息队列,大量借鉴了Kafka的设计。
或者使用基础数据类型实现建议消息队列:使用 Redis 实现简单的消息队列
Redis 缓存雪崩
Redis 缓存穿透
Redis 缓存击穿
以下章节其实只是解释了,为什么单线程能做到高并发。但是为什么能达到一个极高的性能(10w+QPS)就必须去研究Redis底层的数据结构和各种性能优化手段了。
比如Redis是使用C语言开发的,但是它的字符串没有使用C语言的字符串,而是使用了SDS(Simple Dynamic String,简单动态字符串)这种结构体来保存字符串。其他还有跳表的使用、压缩列表(ziplist)的使用,编码转化技术等等。所以不深入研究源码,实际上是无法回答这个问题的
Redis 内部使用文件事件处理器 file event handler(基于Reactor模式),这个文件事件处理器是单线程的。
而文件事件就是服务器对socket操作的抽象,每当一个socket准备好执行连接应答(accept)、写入、读取、关闭等操作时,就会产生一个文件事件。
文件事件处理器包含四个部分:
I/O 多路复用程序会监听多个socket,socket会并发产生各种不同的请求,请求被放入队列,由并行变成串行。
事件分派器消费队列中的请求,每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
在此模型下,Redis中一次请求的响应过程是这样的:
假设此时客户端发送了一个 set key value 请求
这里面的两类事件 AE_READABLE和AE_WRITABLE ,可读和可写的主体指的是socket。比如socket接收到了客户端的set key请求,等待Redis服务器来读取自己,这时产生的事件就是 AE_READABLE 。
如果一个socket同时出现这两种事件,那么文件分派器会优先处理 AE_READABLE 事件
Redis 服务器是事件驱动的,其主要处理的事件除了上面的文件事件,还有时间事件。
Redis 目前的时间事件只有周期性事件一类,不使用定时事件。
Redis 服务器将所有的时间事件都放在了一个无序列表中,每当时间事件执行器运行时,它就会遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。
Redis 以周期性时间事件方式来运行 serverCron 函数,该函数主要负责执行以下工作:
文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件。并且由于文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器也不会中断正在执行的事件处理,也不会对事件进行抢占。所以时间事件的实际处理时间经常会比设定的时间稍晚一些(因为即使时间到了,时间事件也不可以抢占文件事件的资源)。
Redis 与 MySQL 的数据同步
尽管Redis是一个非常快速的内存数据存储媒介,也并不代表Redis不会产生性能问题。
Redis采用单线程模型,所有的命令都是由一个线程串行执行的,所以当某个命令执行耗时较长时,会拖慢其后的所有命令,这使得Redis对每个任务的执行效率更加敏感。要确保没有让Redis执行耗时长的命令,适当运用Pipeline将连续执行的命令组合执行。
尽可能在物理机上直接部署Redis。如果在虚拟机中运行Redis,注意查看虚拟机环境的固有延迟,对虚拟机进行优化。
尽量避免使用时间复杂度为O(N)的命令,N的数量级不可预知时会阻塞Redis线程。官网对每个命令的时间复杂度都有说明,可以参阅。
数据持久化也可能引发较大延迟
持久化不是做的越全就越好,需要根据数据的安全级别和性能要求制定合理的持久化策略。
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节点。
数据淘汰引发延迟
当同一秒内有大量key过期时,也会引发Redis的延迟。可以在设置过期时间时追加一个小的随机数。
尽可能实施读写分离策略
尤其是针对一些使用了长耗时命令的统计类任务,完全可以指定在一个从节点上执行,避免长耗时命令影响其他请求的响应。
为了网络传输的稳定性,所有节点尽可能部署在同一个局域网内
避免在Master节点上挂载过多Slave节点,而使用单向链表结构,在Slave上面挂载Slave。
Swap引发延迟
当Linux将Redis所用的内存分页移至swap空间时,将会阻塞Redis进程,导致Redis出现不正常的延迟。Swap通常在物理内存不足或一些进程在进行大量I/O操作时发生。
/proc//smaps文件中会保存进程的swap记录,通过查看这个文件,能够判断Redis的延迟是否由Swap产生。如果这个文件中记录了较大的Swap size,则说明延迟很有可能是Swap造成的。
尽可能使用Hash
Hash使用的内存非常小。相比于复数个key-value,将数据模型抽象到一个Hash里面性能会更好。
Redis版本号的命名规则:
版本号 | 发布日期 | 重要功能 |
---|---|---|
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 拥有像单实例一样简单的接入方式 |
对于最常见的两种Java客户端Jedis和Redisson,尽管Jedis比起Redisson有不足,但也应该在需要使用Redisson的高级特性时再选用Redisson,避免造成不必要的程序复杂度提升。
轻量,简洁,便于集成和改造。
支持连接池,支持pipelining、事务、LUA Scripting、Redis Sentinel、Redis Cluster。
不支持读写分离,需要自己实现
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架构下都可以使用。