本文将从Redis的基本特性入手,经过讲述Redis的数据结构和主要命令对Redis的基本能力进行直观介绍。以后概览Redis提供的高级能力,并在部署、维护、性能调优等多个方面进行更深刻的介绍和指导。java
本文适合使用Redis的普通开发人员,以及对Redis进行选型、架构设计和性能调优的架构设计人员。redis
目录算法
概述数据库
Redis是一个开源的,基于内存的结构化数据存储媒介,能够做为数据库、缓存服务或消息服务使用。设计模式
Redis支持多种数据结构,包括字符串、哈希表、链表、集合、有序集合、位图、Hyperloglogs等。数组
Redis具有LRU淘汰、事务实现、以及不一样级别的硬盘持久化等能力,而且支持副本集和经过Redis Sentinel实现的高可用方案,同时还支持经过Redis Cluster实现的数据自动分片能力。缓存
Redis的主要功能都基于单线程模型实现,也就是说Redis使用一个线程来服务全部的客户端请求,同时Redis采用了非阻塞式IO,并精细地优化各类命令的算法时间复杂度,这些信息意味着:安全
Redis的数据结构和相关经常使用命令性能优化
本节中将介绍Redis支持的主要数据结构,以及相关的经常使用Redis命令。本节只对Redis命令进行扼要的介绍,且只列出了较经常使用的命令。若是想要了解完整的Redis命令集,或了解某个命令的详细使用方法,请参考官方文档:网络
https://redis.io/commands
Key
Redis采用Key-Value型的基本数据结构,任何二进制序列均可以做为Redis的Key使用(例如普通的字符串或一张JPEG图片)
关于Key的一些注意事项:
String
String是Redis的基础数据类型,Redis没有Int、Float、Boolean等数据类型的概念,全部的基本类型在Redis中都以String体现。
与String相关的经常使用命令:
上文提到过,Redis的基本数据类型只有String,但Redis能够把String做为整型或浮点型数字来使用,主要体如今INCR、DECR类的命令上:
INCR/DECR系列命令要求操做的value类型为String,并能够转换为64位带符号的整型数字,不然会返回错误。
也就是说,进行INCR/DECR系列命令的value,必须在[-2^63 ~ 2^63 – 1]范围内。
前文提到过,Redis采用单线程模型,自然是线程安全的,这使得INCR/DECR命令能够很是便利的实现高并发场景下的精确控制。
例1:库存控制
在高并发场景下实现库存余量的精准校验,确保不出现超卖的状况。
设置库存总量:
SET inv:remain "100"
库存扣减+余量校验:
DECR inv:remain
当DECR命令返回值大于等于0时,说明库存余量校验经过,若是返回小于0的值,则说明库存已耗尽。
假设同时有300个并发请求进行库存扣减,Redis可以确保这300个请求分别获得99到-200的返回值,每一个请求获得的返回值都是惟一的,绝对不会找出现两个请求获得同样的返回值的状况。
例2:自增序列生成
实现相似于RDBMS的Sequence功能,生成一系列惟一的序列号
设置序列起始值:
SET sequence "10000"
获取一个序列值:
INCR sequence
直接将返回值做为序列使用便可。
获取一批(如100个)序列值:
INCRBY sequence 100
假设返回值为N,那么[N – 99 ~ N]的数值都是可用的序列值。
当多个客户端同时向Redis申请自增序列时,Redis可以确保每一个客户端获得的序列值或序列范围都是全局惟一的,绝对不会出现不一样客户端获得了重复的序列值的状况。
List
Redis的List是链表型的数据结构,可使用LPUSH/RPUSH/LPOP/RPOP等命令在List的两端执行插入元素和弹出元素的操做。虽然List也支持在特定index上插入和读取元素的功能,但其时间复杂度较高(O(N)),应当心使用。
与List相关的经常使用命令:
工做一到五年的java 开发工程师朋友能够加入咱们Java架构交流:760940986
领取获取往期Java高级架构资料、源码、笔记、视频。Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术
应谨慎使用的List相关命令:
因为Redis的List是链表结构的,上述的三个命令的算法效率较低,须要对List进行遍历,命令的耗时没法预估,在List长度大的状况下耗时会明显增长,应谨慎使用。
换句话说,Redis的List实际是设计来用于实现队列,而不是用于实现相似ArrayList这样的列表的。若是你不是想要实现一个双端出入的队列,那么请尽可能不要使用Redis的List数据结构。
为了更好支持队列的特性,Redis还提供了一系列阻塞式的操做命令,如BLPOP/BRPOP等,可以实现相似于BlockingQueue的能力,即在List为空时,阻塞该链接,直到List中有对象能够出队时再返回。针对阻塞类的命令,此处不作详细探讨,请参考官方文档(https://redis.io/topics/data-types-intro) 中”Blocking operations on lists”一节。
Hash
Hash即哈希表,Redis的Hash和传统的哈希表同样,是一种field-value型的数据结构,能够理解成将HashMap搬入Redis。
Hash很是适合用于表现对象类型的数据,用Hash中的field对应对象的field便可。
Hash的优势包括:
与Hash相关的经常使用命令:
应谨慎使用的Hash相关命令:
上述三个命令都会对Hash进行完整遍历,Hash中的field数量与命令的耗时线性相关,对于尺寸不可预知的Hash,应严格避免使用上面三个命令,而改成使用HSCAN命令进行游标式的遍历,具体请见
https://redis.io/commands/scan
Set
Redis Set是无序的,不可重复的String集合。
与Set相关的经常使用命令:
慎用的Set相关命令:
上述几个命令涉及的计算量大,应谨慎使用,特别是在参与计算的Set尺寸不可知的状况下,应严格避免使用。能够考虑经过SSCAN命令遍历获取相关Set的所有member(具体请见 https://redis.io/commands/scan ),若是须要作并集/交集/差集计算,能够在客户端进行,或在不服务实时查询请求的Slave上进行。
Sorted Set
Redis Sorted Set是有序的、不可重复的String集合。Sorted Set中的每一个元素都须要指派一个分数(score),Sorted Set会根据score对元素进行升序排序。若是多个member拥有相同的score,则以字典序进行升序排序。
Sorted Set很是适合用于实现排名。
Sorted Set的主要命令:
慎用的Sorted Set相关命令:
上述几个命令,应尽可能避免传递[0 -1]或[-inf +inf]这样的参数,来对Sorted Set作一次性的完整遍历,特别是在Sorted Set的尺寸不可预知的状况下。能够经过ZSCAN命令来进行游标式的遍历(具体请见 https://redis.io/commands/scan ),或经过LIMIT参数来限制返回member的数量(适用于ZRANGEBYSCORE和ZREVRANGEBYSCORE命令),以实现游标式的遍历。
Bitmap和HyperLogLog
Redis的这两种数据结构相较以前的并不经常使用,在本文中只作简要介绍,如想要详细了解这两种数据结构与其相关的命令,请参考官方文档
https://redis.io/topics/data-types-intro 中的相关章节
Bitmap在Redis中不是一种实际的数据类型,而是一种将String做为Bitmap使用的方法。能够理解为将String转换为bit数组。使用Bitmap来存储true/false类型的简单数据极为节省空间。
HyperLogLogs是一种主要用于数量统计的数据结构,它和Set相似,维护一个不可重复的String集合,可是HyperLogLogs并不维护具体的member内容,只维护member的个数。也就是说,HyperLogLogs只能用于计算一个集合中不重复的元素数量,因此它比Set要节省不少内存空间。
其余经常使用命令
数据持久化
Redis提供了将数据按期自动持久化至硬盘的能力,包括RDB和AOF两种方案,两种方案分别有其长处和短板,能够配合起来同时运行,确保数据的稳定性。
必须使用数据持久化吗?
Redis的数据持久化机制是能够关闭的。若是你只把Redis做为缓存服务使用,Redis中存储的全部数据都不是该数据的主体而仅仅是同步过来的备份,那么能够关闭Redis的数据持久化机制。
但一般来讲,仍然建议至少开启RDB方式的数据持久化,由于:
RDB
采用RDB持久方式,Redis会按期保存数据快照至一个rbd文件中,并在启动时自动加载rdb文件,恢复以前保存的数据。能够在配置文件中配置Redis进行快照保存的时机:
save [seconds] [changes]
意为在[seconds]秒内若是发生了[changes]次数据修改,则进行一次RDB快照保存,例如
save 60 100
会让Redis每60秒检查一次数据变动状况,若是发生了100次或以上的数据变动,则进行RDB快照保存。
能够配置多条save指令,让Redis执行多级的快照保存策略。
Redis默认开启RDB快照,默认的RDB策略以下:
save 900 1save 300 10save 60 10000
也能够经过BGSAVE命令手工触发RDB快照保存。
RDB的优势:
RDB的缺点:
AOF
采用AOF持久方式时,Redis会把每个写请求都记录在一个日志文件里。在Redis重启时,会把AOF文件中记录的全部写操做顺序执行一遍,确保数据恢复到最新。
AOF默认是关闭的,如要开启,进行以下配置:
appendonly yes
AOF提供了三种fsync配置,always/everysec/no,经过配置项[appendfsync]指定:
随着AOF不断地记录写操做日志,一定会出现一些无用的日志,例如某个时间点执行了命令SET key1 “abc”,在以后某个时间点又执行了SET key1 “bcd”,那么第一条命令很显然是没有用的。大量的无用日志会让AOF文件过大,也会让数据恢复的时间过长。
因此Redis提供了AOF rewrite功能,能够重写AOF文件,只保留可以把数据恢复到最新状态的最小写操做集。
AOF rewrite能够经过BGREWRITEAOF命令触发,也能够配置Redis按期自动进行:
auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb
上面两行配置的含义是,Redis在每次AOF rewrite时,会记录完成rewrite后的AOF日志大小,当AOF日志大小在该基础上增加了100%后,自动进行AOF rewrite。同时若是增加的大小没有达到64mb,则不会进行rewrite。
AOF的优势:
AOF的缺点:
内存管理与数据淘汰机制
最大内存设置
默认状况下,在32位OS中,Redis最大使用3GB的内存,在64位OS中则没有限制。
在使用Redis时,应该对数据占用的最大空间有一个基本准确的预估,并为Redis设定最大使用的内存。不然在64位OS中Redis会无限制地占用内存(当物理内存被占满后会使用swap空间),容易引起各类各样的问题。
经过以下配置控制Redis使用的最大内存:
maxmemory 100mb
在内存占用达到了maxmemory后,再向Redis写入数据时,Redis会:
在为Redis设置maxmemory时,须要注意:
数据淘汰机制
Redis提供了5种数据淘汰策略:
最好为Redis指定一种有效的数据淘汰策略以配合maxmemory设置,避免在内存使用满后发生写入失败的状况。
通常来讲,推荐使用的策略是volatile-lru,并辨识Redis中保存的数据的重要性。对于那些重要的,绝对不能丢弃的数据(如配置类数据等),应不设置有效期,这样Redis就永远不会淘汰这些数据。对于那些相对不是那么重要的,而且可以热加载的数据(好比缓存最近登陆的用户信息,当在Redis中找不到时,程序会去DB中读取),能够设置上有效期,这样在内存不够时Redis就会淘汰这部分数据。
配置方法:
maxmemory-policy volatile-lru #默认是noeviction,即不进行数据淘汰
Pipelining
Pipelining
Redis提供许多批量操做的命令,如MSET/MGET/HMSET/HMGET等等,这些命令存在的意义是减小维护网络链接和传输数据所消耗的资源和时间。
例如连续使用5次SET命令设置5个不一样的key,比起使用一次MSET命令设置5个不一样的key,效果是同样的,但前者会消耗更多的RTT(Round Trip Time)时长,永远应优先使用后者。
然而,若是客户端要连续执行的屡次操做没法经过Redis命令组合在一块儿,例如:
SET a "abc"INCR bHSET c name "hi"
此时即可以使用Redis提供的pipelining功能来实如今一次交互中执行多条命令。
使用pipelining时,只须要从客户端一次向Redis发送多条命令(以rn)分隔,Redis就会依次执行这些命令,而且把每一个命令的返回按顺序组装在一块儿一次返回,好比:
$ (printf "PINGrnPINGrnPINGrn"; sleep 1) | nc localhost 6379+PONG+PONG+PONG
大部分的Redis客户端都对Pipelining提供支持,因此开发者一般并不须要本身手工拼装命令列表。
Pipelining的局限性
Pipelining只能用于执行连续且无相关性的命令,当某个命令的生成须要依赖于前一个命令的返回时,就没法使用Pipelining了。
经过Scripting功能,能够规避这一局限性
事务与Scripting
Pipelining可以让Redis在一次交互中处理多条命令,然而在一些场景下,咱们可能须要在此基础上确保这一组命令是连续执行的。
好比获取当前累计的PV数并将其清0
> GET vCount12384> SET vCount 0OK
若是在GET和SET命令之间插进来一个INCR vCount,就会使客户端拿到的vCount不许确。
Redis的事务能够确保复数命令执行时的原子性。也就是说Redis可以保证:一个事务中的一组命令是绝对连续执行的,在这些命令执行完成以前,绝对不会有来自于其余链接的其余命令插进去执行。
经过MULTI和EXEC命令来把这两个命令加入一个事务中:
> MULTIOK> GET vCountQUEUED> SET vCount 0QUEUED> EXEC1) 123842) OK
Redis在接收到MULTI命令后便会开启一个事务,这以后的全部读写命令都会保存在队列中但并不执行,直到接收到EXEC命令后,Redis会把队列中的全部命令连续顺序执行,并以数组形式返回每一个命令的返回结果。
可使用DISCARD命令放弃当前的事务,将保存的命令队列清空。
须要注意的是,Redis事务不支持回滚:
若是一个事务中的命令出现了语法错误,大部分客户端驱动会返回错误,2.6.5版本以上的Redis也会在执行EXEC时检查队列中的命令是否存在语法错误,若是存在,则会自动放弃事务并返回错误。
但若是一个事务中的命令有非语法类的错误(好比对String执行HSET操做),不管客户端驱动仍是Redis都没法在真正执行这条命令以前发现,因此事务中的全部命令仍然会被依次执行。在这种状况下,会出现一个事务中部分命令成功部分命令失败的状况,然而与RDBMS不一样,Redis不提供事务回滚的功能,因此只能经过其余方法进行数据的回滚。
经过事务实现CAS
Redis提供了WATCH命令与事务搭配使用,实现CAS乐观锁的机制。
假设要实现将某个商品的状态改成已售:
if(exec(HGET stock:1001 state) == "in stock") exec(HSET stock:1001 state "sold");
这一伪代码执行时,没法确保并发安全性,有可能多个客户端都获取到了”in stock”的状态,致使一个库存被售卖屡次。
使用WATCH命令和事务能够解决这一问题:
exec(WATCH stock:1001);if(exec(HGET stock:1001 state) == "in stock") { exec(MULTI); exec(HSET stock:1001 state "sold"); exec(EXEC);}
WATCH的机制是:在事务EXEC命令执行时,Redis会检查被WATCH的key,只有被WATCH的key从WATCH起始时至今没有发生过变动,EXEC才会被执行。若是WATCH的key在WATCH命令到EXEC命令之间发生过变化,则EXEC命令会返回失败。
Scripting
经过EVAL与EVALSHA命令,可让Redis执行LUA脚本。这就相似于RDBMS的存储过程同样,能够把客户端与Redis之间密集的读/写交互放在服务端进行,避免过多的数据交互,提高性能。
Scripting功能是做为事务功能的替代者诞生的,事务提供的全部能力Scripting均可以作到。Redis官方推荐使用LUA Script来代替事务,前者的效率和便利性都超过了事务。
关于Scripting的具体使用,本文不作详细介绍,请参考官方文档
https://redis.io/commands/eval
Redis性能调优
尽管Redis是一个很是快速的内存数据存储媒介,也并不表明Redis不会产生性能问题。
前文中提到过,Redis采用单线程模型,全部的命令都是由一个线程串行执行的,因此当某个命令执行耗时较长时,会拖慢其后的全部命令,这使得Redis对每一个任务的执行效率更加敏感。
针对Redis的性能优化,主要从下面几个层面入手:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
长耗时命令
Redis绝大多数读写命令的时间复杂度都在O(1)到O(N)之间,在文本和官方文档中均对每一个命令的时间复杂度有说明。
一般来讲,O(1)的命令是安全的,O(N)命令在使用时须要注意,若是N的数量级不可预知,则应避免使用。例如对一个field数未知的Hash数据执行HGETALL/HKEYS/HVALS命令,一般来讲这些命令执行的很快,但若是这个Hash中的field数量极多,耗时就会成倍增加。
又如使用SUNION对两个Set执行Union操做,或使用SORT对List/Set执行排序操做等时,都应该严加注意。
避免在使用这些O(N)命令时发生问题主要有几个办法:
Redis提供了SCAN命令,能够对Redis中存储的全部key进行游标式的遍历,避免使用KEYS命令带来的性能问题。同时还有SSCAN/HSCAN/ZSCAN等命令,分别用于对Set/Hash/Sorted Set中的元素进行游标式遍历。SCAN类命令的使用请参考官方文档:
https://redis.io/commands/scan
Redis提供了Slow Log功能,能够自动记录耗时较长的命令。相关的配置参数有两个:
slowlog-log-slower-than xxxms #执行时间慢于xxx毫秒的命令计入Slow Logslowlog-max-len xxx #Slow Log的长度,即最大纪录多少条Slow Log
使用SLOWLOG GET [number]命令,能够输出最近进入Slow Log的number条命令。
使用SLOWLOG RESET命令,能够重置Slow Log
网络引起的延迟
数据持久化引起的延迟
Redis的数据持久化工做自己就会带来延迟,须要根据数据的安全级别和性能要求制定合理的持久化策略:
Redis在fork子进程时须要将内存分页表拷贝至子进程,以占用了24GB内存的Redis实例为例,共须要拷贝24GB / 4kB * 8 = 48MB的数据。在使用单Xeon 2.27Ghz的物理机上,这一fork操做耗时216ms。 能够经过INFO命令返回的latest_fork_usec字段查看上一次fork操做的耗时(微秒)
Swap引起的延迟
当Linux将Redis所用的内存分页移至swap空间时,将会阻塞Redis进程,致使Redis出现不正常的延迟。Swap一般在物理内存不足或一些进程在进行大量I/O操做时发生,应尽量避免上述两种状况的出现。
/proc/<pid>/smaps文件中会保存进程的swap记录,经过查看这个文件,可以判断Redis的延迟是否由Swap产生。若是这个文件中记录了较大的Swap size,则说明延迟颇有多是Swap形成的。
数据淘汰引起的延迟
当同一秒内有大量key过时时,也会引起Redis的延迟。在使用时应尽可能将key的失效时间错开。
引入读写分离机制
Redis的主从复制能力能够实现一主多从的多节点架构,在这一架构下,主节点接收全部写请求,并将数据同步给多个从节点。
在这一基础上,咱们可让从节点提供对实时性要求不高的读请求服务,以减少主节点的压力。
尤为是针对一些使用了长耗时命令的统计类任务,彻底能够指定在一个或多个从节点上执行,避免这些长耗时命令影响其余请求的响应。
关于读写分离的具体说明,请参见后续章节
主从复制与集群分片
主从复制
Redis支持一主多从的主从复制架构。一个Master实例负责处理全部的写请求,Master将写操做同步至全部Slave。
借助Redis的主从复制,能够实现读写分离和高可用:
启用主从复制很是简单,只须要配置多个Redis实例,在做为Slave的Redis实例中配置:
slaveof 192.168.1.1 6379 #指定Master的IP和端口
当Slave启动后,会从Master进行一次冷启动数据同步,由Master触发BGSAVE生成RDB文件推送给Slave进行导入,导入完成后Master再将增量数据经过Redis Protocol同步给Slave。以后主从之间的数据便一直以Redis Protocol进行同步
使用Sentinel作自动failover
Redis的主从复制功能自己只是作数据同步,并不提供监控和自动failover能力,要经过主从复制功能来实现Redis的高可用,还须要引入一个组件:Redis Sentinel
Redis Sentinel是Redis官方开发的监控组件,能够监控Redis实例的状态,经过Master节点自动发现Slave节点,并在监测到Master节点失效时选举出一个新的Master,并向全部Redis实例推送新的主从配置。
Redis Sentinel须要至少部署3个实例才能造成选举关系。
关键配置:
另外须要注意的是,Redis Sentinel实现的自动failover不是在同一个IP和端口上完成的,也就是说自动failover产生的新Master提供服务的IP和端口与以前的Master是不同的,因此要实现HA,还要求客户端必须支持Sentinel,可以与Sentinel交互得到新Master的信息才行。
集群分片
为什么要作集群分片:
当上述两个问题出现时,就必需要对Redis进行分片了。
Redis的分片方案有不少种,例如不少Redis的客户端都自行实现了分片功能,也有向Twemproxy这样的以代理方式实现的Redis分片方案。然而首选的方案还应该是Redis官方在3.0版本中推出的Redis Cluster分片方案。
本文不会对Redis Cluster的具体安装和部署细节进行介绍,重点介绍Redis Cluster带来的好处与弊端。
Redis Cluster的能力
其中第三点是基于主从复制来实现的,Redis Cluster的每一个数据分片都采用了主从复制的结构,原理和前文所述的主从复制彻底一致,惟一的区别是省去了Redis Sentinel这一额外的组件,由Redis Cluster负责进行一个分片内部的节点监控和自动failover。
Redis Cluster分片原理
Redis Cluster中共有16384个hash slot,Redis会计算每一个key的CRC16,将结果与16384取模,来决定该key存储在哪个hash slot中,同时须要指定Redis Cluster中每一个数据分片负责的Slot数。Slot的分配在任什么时候间点均可以进行从新分配。
客户端在对key进行读写操做时,能够链接Cluster中的任意一个分片,若是操做的key不在此分片负责的Slot范围内,Redis Cluster会自动将请求重定向到正确的分片上。
hash tags
在基础的分片原则上,Redis还支持hash tags功能,以hash tags要求的格式明明的key,将会确保进入同一个Slot中。例如:{uiv}user:1000和{uiv}user:1001拥有一样的hash tag {uiv},会保存在同一个Slot中。
使用Redis Cluster时,pipelining、事务和LUA Script功能涉及的key必须在同一个数据分片上,不然将会返回错误。如要在Redis Cluster中使用上述功能,就必须经过hash tags来确保一个pipeline或一个事务中操做的全部key都位于同一个Slot中。
有一些客户端(如Redisson)实现了集群化的pipelining操做,能够自动将一个pipeline里的命令按key所在的分片进行分组,分别发到不一样的分片上执行。可是Redis不支持跨分片的事务,事务和LUA Script仍是必须遵循全部key在一个分片上的规则要求。
主从复制 vs 集群分片
在设计软件架构时,要如何在主从复制和集群分片两种部署方案中取舍呢?
从各个方面看,Redis Cluster都是优于主从复制的方案
那是否是表明Redis Cluster永远是优于主从复制的选择呢?
并非。
软件架构永远不是越复杂越好,复杂的架构在带来显著好处的同时,必定也会带来相应的弊端。采用Redis Cluster的弊端包括:
因此说,在主从复制和集群分片两个方案中作出选择时,应该从应用软件的功能特性、数据和访问量级、将来发展规划等方面综合考虑,只在确实有必要引入数据分片时再使用Redis Cluster。
下面是一些建议:
综合上面几点考虑,若是单台主机的可用物理内存彻底足以支撑对Redis的容量需求,且Redis面临的并发写压力距离Benchmark值还尚有距离,建议采用主从复制的架构,能够省去不少没必要要的麻烦。同时,若是应用中大量使用pipelining和事务,也建议尽量选择主从复制架构,能够减小设计和开发时的复杂度。
Redis Java客户端的选择
Redis的Java客户端不少,官方推荐的有三种:Jedis、Redisson和lettuce。
在这里对Jedis和Redisson进行对比介绍
Jedis:
Redisson:
对于Jedis和Redisson的选择,一样应遵循前述的原理,尽管Jedis比起Redisson有各类各样的不足,但也应该在须要使用Redisson的高级特性时再选用Redisson,避免形成没必要要的程序复杂度提高。
工做一到五年的java 开发工程师朋友能够加入咱们Java架构交流群:760940986 领取获取往期Java高级架构资料、源码、笔记、视频。Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术