本文适合使用Redis的普通开发人员,以及对Redis进行选型、架构设计和性能调优的架构设计人员。
目录
概述 Redis的数据结构和相关常用命令 数据持久化 内存管理与数据淘汰机制 Pipelining 事务与Scripting Redis性能调优 主从复制与集群分片 Redis Java客户端的选择
概述
Redis支持多种数据结构,包括字符串、哈希表、链表、集合、有序集合、位图、Hyperloglogs等。
Redis具备LRU淘汰、事务实现、以及不同级别的硬盘持久化等能力,并且支持副本集和通过Redis Sentinel实现的高可用方案,同时还支持通过Redis Cluster实现的数据自动分片能力。
Redis是线程安全的(因为只有一个线程),其所有操作都是原子的,不会因并发产生数据异常 Redis的速度非常快(因为使用非阻塞式IO,且大部分命令的算法时间复杂度都是O(1)) 使用高耗时的Redis命令是很危险的,会占用唯一的一个线程的大量处理时间,导致所有的请求都被拖慢。(例如时间复杂度为O(N)的KEYS命令,严格禁止在生产环境中使用)
Redis的数据结构和相关常用命令
Key
关于Key的一些注意事项:
不要使用过长的Key。例如使用一个1024字节的key就不是一个好主意,不仅会消耗更多的内存,还会导致查找的效率降低 Key短到缺失了可读性也是不好的,例如"u1000flw"比起"user:1000:followers"来说,节省了寥寥的存储空间,却引发了可读性和可维护性上的麻烦 最好使用统一的规范来设计Key,比如"object-type:id:attr",以这一规范设计出的Key可能是"user:1000"或"comment:1234:reply-to" Redis允许的最大Key长度是512MB(对Value的长度限制也是512MB)
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)
INCR:将key对应的value值自增1,并返回自增后的值。只对可以转换为整型的String数据起作用。时间复杂度O(1) INCRBY:将key对应的value值自增指定的整型数值,并返回自增后的值。只对可以转换为整型的String数据起作用。时间复杂度O(1) DECR/DECRBY:同INCR/INCRBY,自增改为自减。
也就是说,进行INCR/DECR系列命令的value,必须在[-2^63 ~ 2^63 - 1]范围内。
例1:库存控制
SET inv:remain "100"
DECR inv:remain
例2:自增序列生成
SET sequence "10000"
INCR sequence
INCRBY sequence 100
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这样的完整遍历操作。
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)
Hash
Hash非常适合用于表现对象类型的数据,用Hash中的field对应对象的field即可。
Hash的优点包括:
可以实现二元查找,如"查找ID为1000的用户的年龄" 比起将整个对象序列化后作为String存储的方法,Hash能够有效地减少网络传输的消耗 当使用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)
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
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数
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)
Bitmap和HyperLogLog
其他常用命令
EXISTS:判断指定的key是否存在,返回1代表存在,0代表不存在,时间复杂度O(1) DEL:删除指定的key及其对应的value,时间复杂度O(N),N为删除的key数量 EXPIRE/PEXPIRE:为一个key设置有效期,单位为秒或毫秒,时间复杂度O(1) TTL/PTTL:返回一个key剩余的有效时间,单位为秒或毫秒,时间复杂度O(1) RENAME/RENAMENX:将key重命名为newkey。使用RENAME时,如果newkey已经存在,其值会被覆盖;使用RENAMENX时,如果newkey已经存在,则不会进行任何操作,时间复杂度O(1) TYPE:返回指定key的类型,string, list, set, zset, hash。时间复杂度O(1) CONFIG GET:获得Redis某配置项的当前值,可以使用*通配符,时间复杂度O(1) CONFIG SET:为Redis某个配置项设置新值,时间复杂度O(1) CONFIG REWRITE:让Redis重新加载redis.conf中的配置
数据持久化
必须使用数据持久化吗?
但通常来说,仍然建议至少开启RDB方式的数据持久化,因为:
RDB方式的持久化几乎不损耗Redis本身的性能,在进行RDB持久化时,Redis主进程唯一需要做的事情就是fork出一个子进程,所有持久化工作都由子进程完成 Redis无论因为什么原因crash掉之后,重启时能够自动恢复到上一次RDB快照中记录的数据。这省去了手工从其他数据源(如DB)同步数据的过程,而且要比其他任何的数据恢复方式都要快 现在硬盘那么大,真的不缺那一点地方
RDB
save [seconds] [changes]
save 60 100
可以配置多条save指令,让Redis执行多级的快照保存策略。
Redis默认开启RDB快照,默认的RDB策略如下:
save 900 1
save 300 10
save 60 10000
对性能影响最小。如前文所述,Redis在保存RDB快照时会fork出子进程进行,几乎不影响Redis处理客户端请求的效率。 每次快照会生成一个完整的数据快照文件,所以可以辅以其他手段保存多个时间点的快照(例如把每天0点的快照备份至其他存储媒介中),作为非常可靠的灾难恢复手段。 使用RDB文件进行数据恢复比使用AOF要快很多。
快照是定期生成的,所以在Redis crash时或多或少会丢失一部分数据。 如果数据集非常大且CPU不够强(比如单核CPU),Redis在fork子进程时可能会消耗相对较长的时间(长至1秒),影响这期间的客户端请求。
AOF
appendonly yes
appendfsync no:不进行fsync,将flush文件的时机交给OS决定,速度最快 appendfsync always:每写入一条日志就进行一次fsync操作,数据安全性最高,但速度最慢 appendfsync everysec:折中的做法,交由后台线程每秒fsync一次
所以Redis提供了AOF rewrite功能,可以重写AOF文件,只保留能够把数据恢复到最新状态的最小写操作集。
AOF rewrite可以通过BGREWRITEAOF命令触发,也可以配置Redis定期自动进行:
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
最安全,在启用appendfsync always时,任何已写入的数据都不会丢失,使用在启用appendfsync everysec也至多只会丢失1秒的数据。 AOF文件在发生断电等问题时也不会损坏,即使出现了某条日志只写入了一半的情况,也可以使用redis-check-aof工具轻松修复。 AOF文件易读,可修改,在进行了某些错误的数据清除操作后,只要AOF文件没有rewrite,就可以把AOF文件备份出来,把错误的命令删除,然后恢复数据。
AOF文件通常比RDB文件更大 性能消耗比RDB高 数据恢复速度比RDB慢
内存管理与数据淘汰机制
最大内存设置
maxmemory 100mb
根据配置的数据淘汰策略尝试淘汰数据,释放空间 如果没有数据可以淘汰,或者没有配置数据淘汰策略,那么Redis会对所有写请求返回错误,但读请求仍然可以正常执行
如果采用了Redis的主从同步,主节点向从节点同步数据时,会占用掉一部分内存空间,如果maxmemory过于接近主机的可用内存,导致数据同步时内存不足。所以设置的maxmemory不要过于接近主机可用的内存,留出一部分预留用作主从同步。
数据淘汰机制
volatile-lru:使用LRU算法进行数据淘汰(淘汰上次使用时间最早的,且使用次数最少的key),只淘汰设定了有效期的key allkeys-lru:使用LRU算法进行数据淘汰,所有的key都可以被淘汰 volatile-random:随机淘汰数据,只淘汰设定了有效期的key allkeys-random:随机淘汰数据,所有的key都可以被淘汰 volatile-ttl:淘汰剩余有效期最短的key
maxmemory-policy volatile-lru #默认是noeviction,即不进行数据淘汰
Pipelining
例如连续使用5次SET命令设置5个不同的key,比起使用一次MSET命令设置5个不同的key,效果是一样的,但前者会消耗更多的RTT(Round Trip Time)时长,永远应优先使用后者。
SET a "abc"
INCR b
HSET c name "hi"
使用pipelining时,只需要从客户端一次向Redis发送多条命令(以\r\n)分隔,Redis就会依次执行这些命令,并且把每个命令的返回按顺序组装在一起一次返回,比如:
$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG
Pipelining的局限性
事务与Scripting
> GET vCount
12384
> SET vCount 0
OK
> MULTI
OK
> GET vCount
QUEUED
> SET vCount 0
QUEUED
> EXEC
1) 12384
2) OK
如果一个事务中的命令出现了语法错误,大部分客户端驱动会返回错误,2.6.5版本以上的Redis也会在执行EXEC时检查队列中的命令是否存在语法错误,如果存在,则会自动放弃事务并返回错误。
但如果一个事务中的命令有非语法类的错误(比如对String执行HSET操作),无论客户端驱动还是Redis都无法在真正执行这条命令之前发现,所以事务中的所有命令仍然会被依次执行。在这种情况下,会出现一个事务中部分命令成功部分命令失败的情况,然而与RDBMS不同,Redis不提供事务回滚的功能,所以只能通过其他方法进行数据的回滚。
通过事务实现CAS
if(exec(HGET stock:1001 state) == "in stock")
exec(HSET stock:1001 state "sold");
exec(WATCH stock:1001);
if(exec(HGET stock:1001 state) == "in stock") {
exec(MULTI);
exec(HSET stock:1001 state "sold");
exec(EXEC);
}
Scripting
Redis性能调优
前文中提到过,Redis采用单线程模型,所有的命令都是由一个线程串行执行的,所以当某个命令执行耗时较长时,会拖慢其后的所有命令,这使得Redis对每个任务的执行效率更加敏感。
最初的也是最重要的,确保没有让Redis执行耗时长的命令 使用pipelining将连续执行的命令组合执行 操作系统的Transparent huge pages功能必须关闭:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
如果在虚拟机中运行Redis,可能天然就有虚拟机环境带来的固有延迟。可以通过./redis-cli --intrinsic-latency 100命令查看固有延迟。同时如果对Redis的性能有较高要求的话,应尽可能在物理机上直接部署Redis。 检查数据持久化策略 考虑引入读写分离机制
长耗时命令
又如使用SUNION对两个Set执行Union操作,或使用SORT对List/Set执行排序操作等时,都应该严加注意。
不要把List当做列表使用,仅当做队列来使用 通过机制严格控制Hash、Set、Sorted Set的大小 可能的话,将排序、并集、交集等操作放在客户端执行 绝对禁止使用KEYS命令 避免一次性遍历集合类型的所有成员,而应使用SCAN类的命令进行分批的,游标式的遍历
slowlog-log-slower-than xxxms #执行时间慢于xxx毫秒的命令计入Slow Log
slowlog-max-len xxx #Slow Log的长度,即最大纪录多少条Slow Log
使用SLOWLOG RESET命令,可以重置Slow Log
网络引发的延迟
尽可能使用长连接或连接池,避免频繁创建销毁连接 客户端进行的批量数据操作,应使用Pipeline特性在一次交互中完成。具体请参照本文的Pipelining章节
数据持久化引发的延迟
AOF + fsync always的设置虽然能够绝对确保数据安全,但每个操作都会触发一次fsync,会对Redis的性能有比较明显的影响 AOF + fsync every second是比较好的折中方案,每秒fsync一次 AOF + fsync never会提供AOF持久化方案下的最优性能 使用RDB持久化通常会提供比使用AOF更高的性能,但需要注意RDB的策略配置 每一次RDB快照和AOF Rewrite都需要Redis主进程进行fork操作。fork操作本身可能会产生较高的耗时,与CPU和Redis占用的内存大小有关。根据具体的情况合理配置RDB快照和AOF Rewrite时机,避免过于频繁的fork带来的延迟
Redis在fork子进程时需要将内存分页表拷贝至子进程,以占用了24GB内存的Redis实例为例,共需要拷贝24GB / 4kB * 8 = 48MB的数据。在使用单Xeon 2.27Ghz的物理机上,这一fork操作耗时216ms。
可以通过INFO命令返回的latest_fork_usec字段查看上一次fork操作的耗时(微秒)
Swap引发的延迟
数据淘汰引发的延迟
引入读写分离机制
在这一基础上,我们可以让从节点提供对实时性要求不高的读请求服务,以减小主节点的压力。
尤其是针对一些使用了长耗时命令的统计类任务,完全可以指定在一个或多个从节点上执行,避免这些长耗时命令影响其他请求的响应。
主从复制与集群分片
主从复制
借助Redis的主从复制,可以实现读写分离和高可用:
实时性要求不是特别高的读请求,可以在Slave上完成,提升效率。特别是一些周期性执行的统计任务,这些任务可能需要执行一些长耗时的Redis命令,可以专门规划出1个或几个Slave用于服务这些统计任务 借助Redis Sentinel可以实现高可用,当Master crash后,Redis Sentinel能够自动将一个Slave晋升为Master,继续提供服务
slaveof 192.168.1.1 6379 #指定Master的IP和端口
使用Sentinel做自动failover
sentinel monitor mymaster 127.0.0.1 6379 2 #Master实例的IP、端口,以及选举需要的赞成票数
sentinel down-after-milliseconds mymaster 60000 #多长时间没有响应视为Master失效
sentinel failover-timeout mymaster 180000 #两次failover尝试间的间隔时长
sentinel parallel-syncs mymaster 1 #如果有多个Slave,可以通过此配置指定同时从新Master进行数据同步的Slave数,避免所有Slave同时进行数据同步导致查询服务也不可用
集群分片
Redis中存储的数据量大,一台主机的物理内存已经无法容纳 Redis的写请求并发量大,一个Redis实例以无法承载
Redis的分片方案有很多种,例如很多Redis的客户端都自行实现了分片功能,也有向Twemproxy这样的以代理方式实现的Redis分片方案。然而首选的方案还应该是Redis官方在3.0版本中推出的Redis Cluster分片方案。
Redis Cluster的能力
能够自动将数据分散在多个节点上 当访问的key不在当前分片上时,能够自动将请求转发至正确的分片 当集群中部分节点失效时仍能提供服务
Redis Cluster分片原理
hash tags
有一些客户端(如Redisson)实现了集群化的pipelining操作,可以自动将一个pipeline里的命令按key所在的分片进行分组,分别发到不同的分片上执行。但是Redis不支持跨分片的事务,事务和LUA Script还是必须遵循所有key在一个分片上的规则要求。
主从复制 vs 集群分片
Redis Cluster能够解决单节点上数据量过大的问题 Redis Cluster能够解决单节点访问压力过大的问题 Redis Cluster包含了主从复制的能力
维护难度增加。在使用Redis Cluster时,需要维护的Redis实例数倍增,需要监控的主机数量也相应增加,数据备份/持久化的复杂度也会增加。同时在进行分片的增减操作时,还需要进行reshard操作,远比主从模式下增加一个Slave的复杂度要高。 客户端资源消耗增加。当客户端使用连接池时,需要为每一个数据分片维护一个连接池,客户端同时需要保持的连接数成倍增多,加大了客户端本身和操作系统资源的消耗。 性能优化难度增加。你可能需要在多个分片上查看Slow Log和Swap日志才能定位性能问题。 事务和LUA Script的使用成本增加。在Redis Cluster中使用事务和LUA Script特性有严格的限制条件,事务和Script中操作的key必须位于同一个分片上,这就使得在开发时必须对相应场景下涉及的key进行额外的规划和规范要求。如果应用的场景中大量涉及事务和Script的使用,如何在保证这两个功能的正常运作前提下把数据平均分到多个数据分片中就会成为难点。
下面是一些建议:
需要在Redis中存储的数据有多大?未来2年内可能发展为多大?这些数据是否都需要长期保存?是否可以使用LRU算法进行非热点数据的淘汰?综合考虑前面几个因素,评估出Redis需要使用的物理内存。 用于部署Redis的主机物理内存有多大?有多少可以分配给Redis使用?对比(1)中的内存需求评估,是否足够用? Redis面临的并发写压力会有多大?在不使用pipelining时,Redis的写性能可以超过10万次/秒(更多的benchmark可以参考 https://redis.io/topics/benchmarks ) 在使用Redis时,是否会使用到pipelining和事务功能?使用的场景多不多?
Redis Java客户端的选择
轻量,简洁,便于集成和改造 支持连接池 支持pipelining、事务、LUA Scripting、Redis Sentinel、Redis Cluster 不支持读写分离,需要自己实现 文档差(真的很差,几乎没有……)
基于Netty实现,采用非阻塞IO,性能高 支持异步请求 支持连接池 支持pipelining、LUA Scripting、Redis Sentinel、Redis Cluster 不支持事务,官方建议以LUA Scripting代替事务 支持在Redis Cluster架构下使用pipelining 支持读写分离,支持读负载均衡,在主从复制和Redis Cluster架构下都可以使用 内建Tomcat Session Manager,为Tomcat 6/7/8提供了会话共享功能 可以与Spring Session集成,实现基于Redis的会话共享 文档较丰富,有中文文档
github:https://github.com/xetorthio/jedis
文档:https://github.com/xetorthio/jedis/wiki
github:https://github.com/redisson/redisson
文档:https://github.com/redisson/redisson/wiki