Redis
- 概述
- Redis的数据结构和相干常用命令
- 数据长久化
- 内存治理与数据淘汰机制
- Pipelining
- 事务与Scripting
- Redis性能调优
- 主从复制与集群分片
- Redis Java客户端的抉择
本文将从Redis的根本个性动手,通过讲述Redis的数据结构和次要命令对Redis的根本能力进行直观介绍。之后概览Redis提供的高级能力,并在部署、保护、性能调优等多个方面进行更深刻的介绍和领导。本文适宜应用Redis的一般开发人员,以及对Redis进行选型、架构设计和性能调优的架构设计人员。
概述
Redis是一个开源的,基于内存的结构化数据存储媒介,能够作为数据库、缓存服务或音讯服务应用。Redis反对多种数据结构,包含字符串、哈希表、链表、汇合、有序汇合、位图、Hyperloglogs等。Redis具备LRU淘汰、事务实现、以及不同级别的硬盘长久化等能力,并且反对正本集和通过Redis Sentinel实现的高可用计划,同时还反对通过Redis Cluster实现的数据主动分片能力。
Redis的次要性能都基于单线程模型实现,也就是说Redis应用一个线程来服务所有的客户端申请,同时Redis采纳了非阻塞式IO,并精密地优化各种命令的算法工夫复杂度,这些信息意味着:
- Redis是线程平安的(因为只有一个线程),其所有操作都是原子的,不会因并发产生数据异样
- Redis的速度十分快(因为应用非阻塞式IO,且大部分命令的算法工夫复杂度都是O(1))
- 应用高耗时的Redis命令是很危险的,会占用惟一的一个线程的大量解决工夫,导致所有的申请都被拖慢。(例如工夫复杂度为O(N)的KEYS命令,严格禁止在生产环境中应用)
Redis的数据结构和相干常用命令
本节中将介绍Redis反对的次要数据结构,以及相干的罕用Redis命令。本节只对Redis命令进行简要的介绍,且只列出了较罕用的命令。如果想要理解残缺的Redis命令集,或理解某个命令的具体应用办法,请参考官网文档:https://redis.io/commands
Key
Redis采纳Key-Value型的根本数据结构,任何二进制序列都能够作为Redis的Key应用(例如一般的字符串或一张JPEG图片) 对于Key的一些注意事项:
- 不要应用过长的Key。例如应用一个1024字节的key就不是一个好主见,不仅会耗费更多的内存,还会导致查找的效率升高
- Key短到缺失了可读性也是不好的,例如"u1000flw"比起"user:1000:followers"来说,节俭了寥寥的存储空间,却引发了可读性和可维护性上的麻烦
- 最好应用对立的标准来设计Key,比方"object-typeattr",以这一标准设计出的Key可能是"user:1000"或"commentreply-to"
- Redis容许的最大Key长度是512MB(对Value的长度限度也是512MB)
String
String是Redis的根底数据类型,Redis没有Int、Float、Boolean等数据类型的概念,所有的根本类型在Redis中都以String体现。
与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)
上文提到过,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位带符号的整型数字,否则会返回谬误。也就是说,进行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相干的常用命令:
- 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这样的残缺遍历操作。
应审慎应用的List相干命令:
- 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理论是设计来用于实现队列,而不是用于实现相似ArrayList这样的列表的。如果你不是想要实现一个双端出入的队列,那么请尽量不要应用Redis的List数据结构。
为了更好反对队列的个性,Redis还提供了一系列阻塞式的操作命令,如BLPOP/BRPOP等,可能实现相似于BlockingQueue的能力,即在List为空时,阻塞该连贯,直到List中有对象能够出队时再返回。针对阻塞类的命令,此处不做具体探讨,请参考官网文档(https://redis.io/topics/data-...) 中"Blocking operations on lists"一节。
Hash
Hash即哈希表,Redis的Hash和传统的哈希表一样,是一种field-value型的数据结构,能够了解成将HashMap搬入Redis。Hash非常适合用于表现对象类型的数据,用Hash中的field对应对象的field即可。Hash的长处包含:
- 能够实现二元查找,如"查找ID为1000的用户的年龄"
- 比起将整个对象序列化后作为String存储的办法,Hash可能无效地缩小网络传输的耗费
- 当应用Hash保护一个汇合时,提供了比List效率高得多的随机拜访命令
与Hash相干的常用命令:
- 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)
应审慎应用的Hash相干命令:
- HGETALL :返回指定Hash中所有的field-value对。返回后果为数组,数组中field和value交替呈现。工夫复杂度O(N)
- HKEYS/HVALS :返回指定Hash中所有的field/value,工夫复杂度O(N)
上述三个命令都会对Hash进行残缺遍历,Hash中的field数量与命令的耗时线性相关,对于尺寸不可预知的Hash,应严格防止应用下面三个命令,而改为应用HSCAN命令进行游标式的遍历,具体请见 https://redis.io/commands/scan
Set
Redis Set是无序的,不可反复的String汇合。
与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
慎用的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数
上述几个命令波及的计算量大,应审慎应用,特地是在参加计算的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的次要命令:
- 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))
慎用的Sorted Set相干命令:
- 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做一次性的残缺遍历,特地是在Sorted Set的尺寸不可预知的状况下。能够通过ZSCAN命令来进行游标式的遍历(具体请见 https://redis.io/commands/scan ),或通过LIMIT参数来限度返回member的数量(实用于ZRANGEBYSCORE和ZREVRANGEBYSCORE命令),以实现游标式的遍历。
Bitmap和HyperLogLog
Redis的这两种数据结构相较之前的并不罕用,在本文中只做简要介绍,如想要具体理解这两种数据结构与其相干的命令,请参考官网文档https://redis.io/topics/data-... 中的相干章节
Bitmap在Redis中不是一种理论的数据类型,而是一种将String作为Bitmap应用的办法。能够了解为将String转换为bit数组。应用Bitmap来存储true/false类型的简略数据极为节俭空间。
HyperLogLogs是一种次要用于数量统计的数据结构,它和Set相似,保护一个不可反复的String汇合,然而HyperLogLogs并不保护具体的member内容,只保护member的个数。也就是说,HyperLogLogs只能用于计算一个汇合中不反复的元素数量,所以它比Set要节俭很多内存空间。
其余常用命令
- 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中的配置
数据长久化
Redis提供了将数据定期主动长久化至硬盘的能力,包含RDB和AOF两种计划,两种计划别离有其短处和短板,能够配合起来同时运行,确保数据的稳定性。
必须应用数据长久化吗?
Redis的数据长久化机制是能够敞开的。如果你只把Redis作为缓存服务应用,Redis中存储的所有数据都不是该数据的主体而仅仅是同步过去的备份,那么能够敞开Redis的数据长久化机制。但通常来说,依然倡议至多开启RDB形式的数据长久化,因为:
- RDB形式的长久化简直不损耗Redis自身的性能,在进行RDB长久化时,Redis主过程惟一须要做的事件就是fork出一个子过程,所有长久化工作都由子过程实现
- Redis无论因为什么起因crash掉之后,重启时可能主动复原到上一次RDB快照中记录的数据。这省去了手工从其余数据源(如DB)同步数据的过程,而且要比其余任何的数据恢复形式都要快
- 当初硬盘那么大,真的不缺那一点中央
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 1 save 300 10 save 60 10000
也能够通过BGSAVE 命令手工触发RDB快照保留。
RDB的长处:
- 对性能影响最小。如前文所述,Redis在保留RDB快照时会fork出子过程进行,简直不影响Redis解决客户端申请的效率。
- 每次快照会生成一个残缺的数据快照文件,所以能够辅以其余伎俩保留多个工夫点的快照(例如把每天0点的快照备份至其余存储媒介中),作为十分牢靠的劫难复原伎俩。
- 应用RDB文件进行数据恢复比应用AOF要快很多。
RDB的毛病:
- 快照是定期生成的,所以在Redis crash时或多或少会失落一部分数据。
- 如果数据集十分大且CPU不够强(比方单核CPU),Redis在fork子过程时可能会耗费绝对较长的工夫(长至1秒),影响这期间的客户端申请。
AOF
采纳AOF长久形式时,Redis会把每一个写申请都记录在一个日志文件里。在Redis重启时,会把AOF文件中记录的所有写操作程序执行一遍,确保数据恢复到最新。
AOF默认是敞开的,如要开启,进行如下配置:
appendonly yes
AOF提供了三种fsync配置,always/everysec/no,通过配置项[appendfsync]指定:
- appendfsync no:不进行fsync,将flush文件的机会交给OS决定,速度最快
- appendfsync always:每写入一条日志就进行一次fsync操作,数据安全性最高,但速度最慢
- appendfsync everysec:折中的做法,交由后盾线程每秒fsync一次
随着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慢
内存治理与数据淘汰机制
最大内存设置
默认状况下,在32位OS中,Redis最大应用3GB的内存,在64位OS中则没有限度。
在应用Redis时,应该对数据占用的最大空间有一个根本精确的预估,并为Redis设定最大应用的内存。否则在64位OS中Redis会无限度地占用内存(当物理内存被占满后会应用swap空间),容易引发各种各样的问题。
通过如下配置管制Redis应用的最大内存:
maxmemory 100mb
在内存占用达到了maxmemory后,再向Redis写入数据时,Redis会:
- 依据配置的数据淘汰策略尝试淘汰数据,开释空间
- 如果没有数据能够淘汰,或者没有配置数据淘汰策略,那么Redis会对所有写申请返回谬误,但读申请依然能够失常执行
在为Redis设置maxmemory时,须要留神:
- 如果采纳了Redis的主从同步,主节点向从节点同步数据时,会占用掉一部分内存空间,如果maxmemory过于靠近主机的可用内存,导致数据同步时内存不足。所以设置的maxmemory不要过于靠近主机可用的内存,留出一部分预留用作主从同步。
数据淘汰机制
Redis提供了5种数据淘汰策略:
- volatile-lru:应用LRU算法进行数据淘汰(淘汰上次应用工夫最早的,且应用次数起码的key),只淘汰设定了有效期的key
- allkeys-lru:应用LRU算法进行数据淘汰,所有的key都能够被淘汰
- volatile-random:随机淘汰数据,只淘汰设定了有效期的key
- allkeys-random:随机淘汰数据,所有的key都能够被淘汰
- volatile-ttl:淘汰残余有效期最短的key
最好为Redis指定一种无效的数据淘汰策略以配合maxmemory设置,防止在内存应用满后产生写入失败的状况。
一般来说,举荐应用的策略是volatile-lru,并辨识Redis中保留的数据的重要性。对于那些重要的,相对不能抛弃的数据(如配置类数据等),应不设置有效期,这样Redis就永远不会淘汰这些数据。对于那些绝对不是那么重要的,并且可能热加载的数据(比方缓存最近登录的用户信息,当在Redis中找不到时,程序会去DB中读取),能够设置上有效期,这样在内存不够时Redis就会淘汰这部分数据。
配置办法:
maxmemory-policy volatile-lru #默认是noeviction,即不进行数据淘汰
Pipelining
Redis提供许多批量操作的命令,如MSET/MGET/HMSET/HMGET等等,这些命令存在的意义是缩小保护网络连接和传输数据所耗费的资源和工夫。例如间断应用5次SET命令设置5个不同的key,比起应用一次MSET命令设置5个不同的key,成果是一样的,但前者会耗费更多的RTT(Round Trip Time)时长,永远应优先应用后者。
然而,如果客户端要间断执行的屡次操作无奈通过Redis命令组合在一起,例如:
SET a "abc" INCR b HSET 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 vCount 12384 > SET vCount 0 OK
如果在GET和SET命令之间插进来一个INCR vCount,就会使客户端拿到的vCount不精确。
Redis的事务能够确保复数命令执行时的原子性。也就是说Redis可能保障:一个事务中的一组命令是相对间断执行的,在这些命令执行实现之前,相对不会有来自于其余连贯的其余命令插进去执行。
通过MULTI和EXEC命令来把这两个命令退出一个事务中:
> MULTI OK > GET vCount QUEUED > SET vCount 0 QUEUED > EXEC 1) 12384 2) 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的性能优化,次要从上面几个层面动手:
- 最后的也是最重要的,确保没有让Redis执行耗时长的命令
- 应用pipelining将间断执行的命令组合执行
操作系统的Transparent huge pages性能必须敞开:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
- 如果在虚拟机中运行Redis,可能人造就有虚拟机环境带来的固有提早。能够通过./redis-cli --intrinsic-latency 100命令查看固有提早。同时如果对Redis的性能有较高要求的话,应尽可能在物理机上间接部署Redis。
- 检查数据长久化策略
- 思考引入读写拆散机制
长耗时命令
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)命令时产生问题次要有几个方法:
- 不要把List当做列表应用,仅当做队列来应用
- 通过机制严格控制Hash、Set、Sorted Set的大小
- 可能的话,将排序、并集、交加等操作放在客户端执行
- 相对禁止应用KEYS命令
- 防止一次性遍历汇合类型的所有成员,而应应用SCAN类的命令进行分批的,游标式的遍历
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 Log slowlog-max-len xxx #Slow Log的长度,即最大纪录多少条Slow Log
应用SLOWLOG GET [number] 命令,能够输入最近进入Slow Log的number条命令。应用SLOWLOG RESET 命令,能够重置Slow Log
网络引发的提早
- 尽可能应用长连贯或连接池,防止频繁创立销毁连贯
- 客户端进行的批量数据操作,应应用Pipeline个性在一次交互中实现。具体请参照本文的Pipelining章节
数据长久化引发的提早
Redis的数据长久化工作自身就会带来提早,须要依据数据的安全级别和性能要求制订正当的长久化策略:
- 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引发的提早
当Linux将Redis所用的内存分页移至swap空间时,将会阻塞Redis过程,导致Redis呈现不失常的提早。Swap通常在物理内存不足或一些过程在进行大量I/O操作时产生,应尽可能防止上述两种状况的呈现。
/proc//smaps文件中会保留过程的swap记录,通过查看这个文件,可能判断Redis的提早是否由Swap产生。如果这个文件中记录了较大的Swap size,则阐明提早很有可能是Swap造成的。
数据淘汰引发的提早
当同一秒内有大量key过期时,也会引发Redis的提早。在应用时应尽量将key的生效工夫错开。
引入读写拆散机制
Redis的主从复制能力能够实现一主多从的多节点架构,在这一架构下,主节点接管所有写申请,并将数据同步给多个从节点。在这一根底上,咱们能够让从节点提供对实时性要求不高的读申请服务,以减小主节点的压力。尤其是针对一些应用了长耗时命令的统计类工作,齐全能够指定在一个或多个从节点上执行,防止这些长耗时命令影响其余申请的响应。
对于读写拆散的具体阐明,请参见后续章节
主从复制与集群分片
主从复制
Redis反对一主多从的主从复制架构。一个Master实例负责解决所有的写申请,Master将写操作同步至所有Slave。借助Redis的主从复制,能够实现读写拆散和高可用:
- 实时性要求不是特地高的读申请,能够在Slave上实现,晋升效率。特地是一些周期性执行的统计工作,这些工作可能须要执行一些长耗时的Redis命令,能够专门布局出1个或几个Slave用于服务这些统计工作
- 借助Redis Sentinel能够实现高可用,当Master crash后,Redis Sentinel可能主动将一个Slave晋升为Master,持续提供服务
启用主从复制非常简单,只须要配置多个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个实例能力造成选举关系。
要害配置:
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 Sentinel实现的主动failover不是在同一个IP和端口上实现的,也就是说主动failover产生的新Master提供服务的IP和端口与之前的Master是不一样的,所以要实现HA,还要求客户端必须反对Sentinel,可能与Sentinel交互取得新Master的信息才行。
集群分片
为何要做集群分片:
- Redis中存储的数据量大,一台主机的物理内存曾经无奈包容
- Redis的写申请并发量大,一个Redis实例以无奈承载
当上述两个问题呈现时,就必须要对Redis进行分片了。Redis的分片计划有很多种,例如很多Redis的客户端都自行实现了分片性能,也有向Twemproxy这样的以代理形式实现的Redis分片计划。然而首选的计划还应该是Redis官网在3.0版本中推出的Redis Cluster分片计划。
本文不会对Redis Cluster的具体装置和部署细节进行介绍,重点介绍Redis Cluster带来的益处与弊病。
Redis Cluster的能力
- 可能主动将数据扩散在多个节点上
- 当拜访的key不在以后分片上时,可能主动将申请转发至正确的分片
- 当集群中局部节点生效时仍能提供服务
其中第三点是基于主从复制来实现的,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 Cluster永远是优于主从复制的抉择呢?
并不是。
软件架构永远不是越简单越好,简单的架构在带来显著益处的同时,肯定也会带来相应的弊病。采纳Redis Cluster的弊病包含:
- 保护难度减少。在应用Redis Cluster时,须要保护的Redis实例数倍增,须要监控的主机数量也相应减少,数据备份/长久化的复杂度也会减少。同时在进行分片的增减操作时,还须要进行reshard操作,远比主从模式下减少一个Slave的复杂度要高。
- 客户端资源耗费减少。当客户端应用连接池时,须要为每一个数据分片保护一个连接池,客户端同时须要放弃的连接数成倍增多,加大了客户端自身和操作系统资源的耗费。
- 性能优化难度减少。你可能须要在多个分片上查看Slow Log和Swap日志能力定位性能问题。
- 事务和LUA Script的应用成本增加。在Redis Cluster中应用事务和LUA Script个性有严格的限度条件,事务和Script中操作的key必须位于同一个分片上,这就使得在开发时必须对相应场景下波及的key进行额定的布局和标准要求。如果利用的场景中大量波及事务和Script的应用,如何在保障这两个性能的失常运作前提下把数据平均分到多个数据分片中就会成为难点。
所以说,在主从复制和集群分片两个计划中做出抉择时,应该从应用软件的性能个性、数据和拜访量级、将来倒退布局等方面综合思考,只在的确有必要 引入数据分片时再应用Redis Cluster。上面是一些倡议:
- 须要在Redis中存储的数据有多大?将来2年内可能倒退为多大?这些数据是否都须要长期保留?是否能够应用LRU算法进行非热点数据的淘汰?综合思考后面几个因素,评估出Redis须要应用的物理内存。
- 用于部署Redis的主机物理内存有多大?有多少能够调配给Redis应用?比照(1)中的内存需要评估,是否足够用?
- Redis面临的并发写压力会有多大?在不应用pipelining时,Redis的写性能能够超过10万次/秒(更多的benchmark能够参考 https://redis.io/topics/bench... )
- 在应用Redis时,是否会应用到pipelining和事务性能?应用的场景多不多?
综合下面几点思考,如果单台主机的可用物理内存齐全足以撑持对Redis的容量需要,且Redis面临的并发写压力间隔Benchmark值还尚有间隔,倡议采纳主从复制的架构,能够省去很多不必要的麻烦。同时,如果利用中大量应用pipelining和事务,也倡议尽可能抉择主从复制架构,能够缩小设计和开发时的复杂度。
Redis Java客户端的抉择
Redis的Java客户端很多,官网举荐的有三种:Jedis、Redisson和lettuce。
在这里对Jedis和Redisson进行比照介绍
Jedis:
- 轻量,简洁,便于集成和革新
- 反对连接池
- 反对pipelining、事务、LUA Scripting、Redis Sentinel、Redis Cluster
- 不反对读写拆散,须要本人实现
- 文档差(真的很差,简直没有……)
Redisson:
- 基于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的会话共享
- 文档较丰盛,有中文文档
对于Jedis和Redisson的抉择,同样应遵循前述的原理,只管Jedis比起Redisson有各种各样的有余,但也应该在须要应用Redisson的高级个性时再选用Redisson,防止造成不必要的程序复杂度晋升。
Jedis:
github:https://github.com/xetorthio/...
文档:https://github.com/xetorthio/...
Redisson:
github:https://github.com/redisson/r...
文档:https://github.com/redisson/r...