当前位置: 首页 > 图灵资讯 > 技术篇> Redis基础、高级特性与性能调优——一篇文章搞定

Redis基础、高级特性与性能调优——一篇文章搞定

来源:图灵教育
时间:2023-07-06 15:40:31

Redis基础、高级特性与性能调优——一篇文章搞定_数据

本文将从Redis的基本特征开始,通过描述Redis的数据结构和主要命令,直观地介绍Redis的基本能力。然后概述Redis提供的先进能力,并在部署、维护、性能调整等方面进行更深入的介绍和指导。本文适用于使用Redis的普通开发人员和选择Redis、架构设计和性能调整的架构设计人员。

目录
  • 概述
  • Redis的数据结构及相关常用命令
  • 数据持久化
  • 内存管理和数据淘汰机制
  • Pipelining
  • Scripting事务
  • Redis性能调优
  • 主要从复制和集群分片
  • Redis Java客户端的选择
概述

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-type:id:attr以这个标准设计的Key可能是“user:1000"或"comment:1234:reply-to"
  • Redis允许的最大Key长度为512MB(Value的长度限制也为512MB)
String

String是Redis的基本数据类型,Redis没有Int、Float、所有的基本类型,如Boolean,都体现在Redis中。

与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,时间复杂度Oalue(N)
  • MSETNX:与MSET一样,如果指定的key中有任何一个已经存在,则不进行任何操作,时间复杂度OET(N)
  • MGET:获取多个key对应的value,时间复杂度Oalue(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的左侧(即头部),并返回插入后的List长度。时间复杂性O(N),N是插入元素的数量
  • RPUSH:与LPUSH相同,将1或多个元素插入指定List的右侧(即尾部)
  • LPOP:从指定List左侧(即头部)移除一个元素并返回,时间复杂度O(1)
  • RPOP:与LPOP相同,从指定List的右侧(即尾部)移除一个元素并返回
  • LPUSHX/RPUSHX:类似于LPUSH/RPUSH,区别在于,LPUSHX如果不存在/RPUSHX操作的key,则不会进行任何操作
  • LLEN:时间复杂度O(1)返回指定List的长度
  • LRANGE:返回指定List中指定范围的元素(包含在两端,即LRANGE key 0 10将返回11个元素),时间复杂度O(N)。一次获得的元素数量应尽可能控制。一次获得过多的List元素会导致延迟。同时,避免使用LRANGE key 0 -这种完整的遍历操作。

List相关命令应谨慎使用:

  • LINDEX:如果index越界,返回指定list指定index上的元素并返回nil。index值是回环的,即-1代表list的最后一个位置,-2代表list的倒数第二个位置。时间复杂性O(N)
  • LSET:将指定List指定index上的元素设置为value,如果index越界,则返回错误,时间复杂性O(N),如果操作头/尾元素,时间复杂度为O(1)
  • LINSERT:在指定List中的指定元素之前/之后插入一个新元素,并返回操作后的List长度。如果指定元素不存在,则返回-1。如果指定的key不存在,则不会进行任何操作,并且时间复杂(N)

由于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的优点包括:

  • 可实现二元搜索,如“1000用户年龄搜索ID”
  • Hash可以有效地减少网络传输的消耗,而不是将整个对象序列化为String存储方法
  • 使用Hash维护一个集合时,提供了比List更有效的随机访问命令

与Hash相关的常用命令:

  • HSET:将key对应的Hash中的field设置为value。如果Hash不存在,它将自动创建一个。时间复杂性O(1)
  • HGET:时间复杂度O(1)返回指定Hash中field字段的值
  • 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(一个或多个),时间复杂度:O(N),为操作的field数量N
  • HINCRBY:与INCRBY命令一起,INCRBY是指定Hash中的field,时间复杂度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:将一个或多个member添加到指定的SET中,如果指定的SET不存在,它将自动创建一个。时间复杂性OEM(N),为N添加的member数量
  • SREM:时间复杂度OER从指定的SET中删除一个或多个member(N),移除的member数为N
  • SRANDMEMBER:时间复杂度OER从指定的SET中随机返回一个或多个member(N),N为返回的member数量
  • SPOP:时间复杂度OER从指定SET中随机删除并返回countmember(N),移除的member数为N
  • 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:计算一个Set和一个或多个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中添加一个或多个member,时间复杂度OER(Mlog(N)),M是添加的member数量,N是Sorted member在Set中的数量
  • ZREM:指定Sorted 在Set中删除一个或多个member,时间复杂度OER(Mlog(N)),M是删除的member数量,N是Sorted member在Set中的数量
  • ZCOUNT:返回指定的Sorted Set中指定score范围内的member数量,时间复杂度:O(log(N))
  • ZCARD:返回指定的Sorted 时间复杂度O(1)Set中member的数量
  • ZSCORE:返回指定的Sorted Set中指定member的score,时间复杂度O(1)
  • ZRANK/ZREVRANK:Sorteded返回指定的member 在Set排名中,ZRANK返回按升序排名,ZREVRANK返回按降序排名。时间复杂性O(log(N))
  • ZINCRBY:与INCRBY一起,指定Sorted Set中指定member的score自增,时间复杂度Ocore(log(N))

Sorted谨慎使用 Set相关命令:

  • ZRANGE/ZREVRANGE:返回指定的Sorted Set中指定排名范围内的所有member,ZRANGE按score排序,ZREVRANGE按score排序,时间复杂(log(N)+M),M是本次返回的member数
  • ZRANGEBYSCORE/ZREVRANGEBYSCORE:返回指定的Sorted Set中指定score范围内的所有member,返回结果按升序/降序排序,min和max可指定为-inf和+inf,代表返回所有member。时间复杂度OEM(log(N)+M)
  • ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除Sorted Set中指定的排名范围/指定Score范围内的所有member。时间复杂性OEM(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-types-intro 相关章节

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),删除的key数量为N
  • 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重新加载rediss.配置conf
数据持久化

Redis提供了将数据定期自动持续到硬盘的能力,包括RDB和AOF,它们有优缺点,可以同时运行,以确保数据的稳定性。

数据是否必须持久化?

Redis的数据可持续性机制可以关闭。如果您只使用Redis作为缓存服务,Redis中存储的所有数据都不是数据的主体,而只是同步的备份,则可以关闭Redis的数据可持续性机制。但一般来说,至少建议打开RDB模式的数据可持续性,因为:

  • RDB模式的持久性几乎不会损害Redis本身的性能。在RDB持久性方面,Redis主流程中唯一需要做的就是fork生成一个子流程,所有持久性工作都是通过子流程完成的
  • Redis可以自动恢复上次RDB快照中记录的数据,无论是什么原因导致crash丢失。这节省了从其他数据源(如DB)手动同步数据的过程,并且比任何其他数据恢复方法都快
  • 现在硬盘这么大,真的不缺那个地方
RDB

RDB持久,Redis将定期将数据快照保存到RBD文件中,并在启动时自动加载RDB文件,以恢复以前保存的数据。Redis可以在配置文件中保存快照:

save [seconds] [changes]

意为在[seconds]如果发生在秒内[changes]如果修改次数据,将保存RDB快照,例如

save 60 100

Redis将每60秒检查一次数据变更。如果发生100次或更多的数据变更,则保存RDB快照。Redis可以配置多个save指令来执行多级快照保存策略。Redis默认打开RDB快照,RDB策略如下:

save 900 1save 300 10save 60 10000

RDB快照也可以通过BGSAVE命令手动触发。

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提供AOFF rewrite功能,可重写AOF文件,只保留能将数据恢复到最新状态的最小写作操作集。AOF 可通过BGREWRITEAOF命令触发rewrite,也可配置Redis定期自动进行:

auto-aof-rewrite-percentage 100auto-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文件没有回顾,就可以备份AOF文件,删除错误的命令,然后恢复数据。

AOF的缺点:

  • AOF文件通常比RDB文件更大
  • 性能消耗高于RDB
  • 数据恢复速度比RDB慢
内存管理和数据淘汰机制最大的内存设置

默认情况下,Redis在32位OS中最大使用3GB内存,而在64位OS中没有限制。

在使用Redis时,应基本准确地估计数据占用的最大空间,并为Redis设置最大使用的内存。否则,Redis将无限期地占用64位OS中的内存(当物理内存被占用时使用swap空间),这很容易导致各种问题。

Redis使用的最大内存通过以下配置进行控制:

maxmemory 100mb

Redis会在内存占用达到maxmemory后,将数据写入Redis时,:

  • 尝试根据配置的数据淘汰策略淘汰数据,释放空间
  • 如果没有数据可以淘汰,或者没有配置数据淘汰策略,Redis会将所有写作请求返回错误,但读取请求仍能正常执行

在为Redis设置maxmemory时,需要注意:

  • 如果使用Redis的主从同步和主节点到节点的同步数据,则会占用部分内存空间。如果maxmemory离主机的可用内存太近,则数据同步时内存不足。因此,设置的maxmemory不应太接近主机的可用内存,并留出部分预留作为主从同步。
数据淘汰机制

Redis提供了五种数据淘汰策略:

  • 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,即不淘汰数据

PipeliningPipelining

Redis提供了许多批量操作命令,如MSET/MGET/HMSET/HMGET等,这些命令的意义在于减少维护网络连接和传输数据所消耗的资源和时间。比如连续使用5个SET命令设置5个不同的key,效果相同,但前者会消耗更多的RTT(Round Trip Time)长度,应始终优先考虑后者。

但是,如果客户端需要连续执行的多个操作不能通过Redis命令结合在一起,例如:

SET a "abc"INCR bHSET c name "hi"

此时,Redis提供的pipelining功能可用于在一次交互中执行多个命令。使用pipelining时,只需从客户端发送多个命令(以\r\n)分离后,Redis将依次执行这些命令,并按顺序组装每个命令的返回,例如:

$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379+PONG+PONG+PONG

大多数Redis客户端都支持Pipelining,因此开发人员通常不需要手动组装命令列表。

Pipelining的局限性

Pipelining只能用于执行连续且无关的命令。当一个命令的生成需要依赖于前一个命令的返回时,Pipelining就不能使用了。

这种局限性可以通过Scripting功能来避免

Scripting事务

Pipelining允许Redis在一次交互中处理多个命令,但在某些情况下,我们可能需要确保这组命令在此基础上持续执行。

例如,获得当前累计PV数并清除其0

> GET vcount12384> SET vCount 0OK

如果将INCRR插入GET和SET命令之间,则 vCount,这将使客户端获得的VCount不准确。

Redis的事务可以保证复数命令执行的原子性。也就是说,Redis可以保证一个事务中的一组命令是绝对连续的,在这些命令完成之前,其他连接的命令永远不会插入执行。

通过MULTI和EXEC命令将这两个命令添加到一个事务中:

> MULTIOK> GET vCountQUEUED> SET vCount 0QUEUED> EXEC1) 123842) OK

Redis将在收到MULTI命令后打开一个事务。之后,所有读写命令将保存在队列中,但不会执行。在收到EXEC命令之前,Redis将连续执行队列中的所有命令,并以几组形式返回每个命令的返回结果。

使用DISCARD命令放弃当前事务,清空保存的命令队列。

需要注意的是,Redis事务不支持回滚:如果事务中的命令出现语法错误,大多数客户驱动程序将返回错误,2.6.在执行EXEC时,超过5个版本的Redis还将检查队列中的命令是否存在语法错误。如果存在,它将自动放弃事务并返回错误。但是,如果事务中的命令有非语法错误(如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

Redis可以通过EVAL和EVALSHA命令执行LUA脚本。这类似于RDBMS的存储过程,可以将客户端和Redis之间的密集读写交互放在服务端,避免过多的数据交互,提高性能。

Scripting功能是作为事务功能的替代品诞生的,Scripting可以做到事务提供的所有能力。Redis官方推荐使用LUA Script代替事务,前者的效率和便利性都超过了事务。

本文不详细介绍Scripting的具体用途,请参考官方文件https。://redis.io/commands/eval

Redis性能调优

虽然Redis是一种非常快速的内存数据存储媒介,但这并不意味着Redis不会产生性能问题。正如前面提到的,Redis采用单线程模型,所有命令都是由一个线程串行执行的,因此当一个命令长时间执行时,所有命令都会减慢,这使得Redis对每个任务的执行效率更加敏感。

对于Redis的性能优化,主要从以下几个方面入手:

  • 最初也是最重要的,确保Redis不会执行耗时的命令
  • 连续执行的命令组合使用pipelining执行
  • Transparenttntt huge 必须关闭pages功能:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

  • 如果Redis在虚拟机中运行,自然会有虚拟机环境带来的固有延迟。可以通过./redis-cli --intrinsic-latency 100命令检查固有延迟。同时,如果对Redis的性能要求较高,则应尽可能在物理机器中Redis直接部署在上面。
  • 检查数据持久性策略
  • 考虑引入读写分离机制
长耗时命令

绝大多数Redis读写命令的时间复杂度在O(1)到O(1)(N)每个命令的时间复杂性在文本和官方文档中都有说明。

通常来说,O(1)命令是安全的,O(N)使用命令时应注意,如果N的数量级不可预测,则应避免使用。例如,HGETALL//HKEYS/HVALS命令,一般来说,这些命令执行得很快,但是如果这个Hash中有大量的field,时间就会翻倍。另一个例子是使用SUNION对两个Set进行Union操作,或者使用SORT对List/Set进行排序操作。

避免使用这些O(N)主要有几种方法可以解决命令中的问题:

  • 不要用List作为列表,只用作队列
  • 严格控制Hash、Set、Sorted Set的大小
  • 如有可能,在客户端执行排序、并集、交集等操作。
  • 绝对禁止使用KEYS命令
  • 避免使用SCAN类命令对一次性遍历集合类型的所有成员进行分批,游标式遍历

Redis为Redis中存储的所有key提供了SCAN命令,以避免使用keys命令带来的性能问题。还有SSCAN/同时也有SSCAN/HSCAN/ZSCAN等命令分别用于Set//Hash/Sorted 游标式遍历Set中的元素。使用SCAN类命令请参考官方文件:https://redis.io/commands/scan

Redis提供Slowwis Log功能,可自动记录耗时较长的命令。有两个相关的配置参数:

slowlog-log-slower-than xxxms  #执行时间慢于xxx毫秒的命令计入Slow Logslowlog-max-len xxx  #Slow Log的长度,也就是说,Slow的最大记录是多少? Log

使用SLOWLOG GET [number]最近进入Slow的命令可以输出 lognumber命令。使用SLOWLOG RESET命令可以重置Slow Log

由网络引起的延迟
  • 尽量使用长连接或连接池,避免频繁创建销毁连接
  • 客户端的批量数据操作应利用Pipeline特性在一次交互中完成。请参考本文的Pipelining章节
数据持久化引起的延迟

Redis的数据持久化本身就会带来延迟,需要根据数据的安全水平和性能要求制定合理的持久化策略:

  • AOF + fsync 虽然always的设置绝对可以保证数据的安全,但每个操作都会触发fsync,对Redis的性能有明显的影响
  • AOF + fsync every second是一个更好的折中方案,fsync每秒一次
  • AOF + fsync 在AOF持久性方案下,never将提供最佳性能
  • RDB持久化通常比AOF提供更高的性能,但需要注意RDB的战略配置
  • 每次RDB快照和AOF Rewrite都需要Redis主流程进行fork操作。fork操作本身可能会产生高耗时,这与CPU和Redis占用的内存大小有关。RDB快照和AOF应根据具体情况合理配置 rewrite时机,避免过于频繁的fork造成的延迟

在fork子过程中,Redis需要将内存分页表复制到子过程中,以占用24GB内存的Redis实例为例,共需复制24GB / 4kB * 8 = 48MB数据。使用单Xeon 这款fork操作在2.27Ghz物理机上耗时216ms。

latest_可以通过INFO命令返回fork_usec字段查看上一次fork操作的耗时(微秒)

由Swap引起的延迟

当Linux将Redis中使用的内存分页移到Swap空间时,Redis过程将被堵塞,导致Redis异常延迟。Swap通常发生在物理内存不足或某些过程中进行大量I/O操作时,应尽量避免上述两种情况。

/proc/<pid>Swap记录将保存在smaps文件中。通过查看此文件,可以判断Redis的延迟是否由Swap产生。如果在此文件中记录了较大的Swapp size,说明延迟很可能是Swap造成的。

数据淘汰导致的延迟

如果大量的key在同一秒内过期,也会导致Redis延迟。使用时应尽量错开key的失效时间。

引入读写分离机制

Redis的主从复制能力可以实现一主多从的多节点架构。在这个架构下,主节点接收所有的写作请求,并将数据同步到多个节点。在此基础上,我们可以让节点提供对实时性要求较低的读取请求服务,以减轻主节点的压力。特别是对于一些使用长时间命令的统计任务,可以从一个或多个节点指定,以避免这些长时间命令影响其他请求的响应。

关于读写分离的具体说明,请参见后续章节

主要从复制和集群分片主复制

Redis支持一主多从的主从复制架构。Master实例负责处理所有写作请求,Master将写作操作同步到所有Slave。借助Redis的主从复制,可以实现读写分离和高可用性:

  • 实时要求不是特别高,可以在Slave上完成,提高效率。特别是对于一些定期执行的统计任务,可能需要执行一些长期的Redis命令,可以专门规划一个或多个Slave来服务这些统计任务
  • 借助Redis Sentinel可以实现高可用性,当Master crash后,Redis Sentinel可以自动将Slave提升为Master,并继续提供服务

启用主从复制非常简单,只需配置多个Redis实例,在SlaveRedis实例中配置:

slaveof 192.168.1.1 6379  #指定Master的IP和端口

当Slave启动时,冷启动数据将从Master同步。Master将触发BGSAVE生成RDB文件,并将其推送到Slave进行导入。导入完成后,Master将通过Redis同步增量数据 Protocol同步给Slave。从那以后,主从之间的数据一直使用Redis Protocol同步

使用Sentinel作为自动failoverer

Redis的主从复制功能本身只是数据同步,不提供监控和自动failover能力。为了实现Redis的高可用性,还需要引入一个组件:Redis Sentinel

Redis Sentinel是Redis官方开发的监控组件,可以监控Redis实例的状态,通过Master节点自动发现Slave节点,在监控Master节点失效时选择新的Master,并将新的主从配置推送到所有Redis实例。

Redis 为了形成选举关系,Sentinel需要至少部署三个例子。

关键配置:

sentinel monitor mymaster 127.0.0.1 6379 2  #Master实例IP、端口,选举所需的赞成票数sentinel down-after-milliseconds mymaster 60000  #多长时间没有响应被视为Master失效的sentinellel? failover-timeout mymaster 180000  #两次failover尝试之间的间隔时间sentinel parallel-syncs mymaster 1  #若有多个Slave,Slave数量可以通过此配置指定,同时从新的Master进行数据同步,避免所有Slave同时进行数据同步,导致查询服务不可用

另外需要注意的是,Redis Sentinel实现的自动failover不是在同一IP和端口上完成的,也就是说,自动failover生成的新master提供的IP和端口与以前的master不同。因此,为了实现HA,客户端必须支持Sentinel,能够与Sentinel互动,获取新的Master信息。

集群分片

为什么要做集群分片:

  • Redis中存储的数据量很大,主机的物理内存无法容纳
  • Redis的写作请求并发量大,一个Redis实例无法承载

当上述两个问题出现时,必须对Redis进行分片。Redis的分片方案有很多种,比如很多Redis的客户端都实现了自己的分片功能,也有代理实现的Redis分片方案,比如Twemproxy。但是,首选方案也应该是Redis官方在3.0版本中推出的Redis cluster分片方案。

本文不会对Rediss进行对Redisss 介绍Cluster的具体安装和部署细节,重点介绍Redister Cluster带来的好处和缺点。

Redis Cluster的能力
  • 数据可以自动分散在多个节点上
  • 当访问的key不在当前的分片上时,请求可以自动转发到正确的分片
  • 当集群中部分节点失效时,仍然可以提供服务

第三点是基于主从复制,Redis Cluster的每个数据分片都采用了主从复制的结构,其原理与上述主从复制完全一致。唯一的区别是节省了Redis Redissentinel是一个额外的组件 Cluster负责分片内部的节点监控和自动failover。

Redis Cluster分片原理

Redis Cluster共16384hash slot,Redis会计算每个key的CRC16,将结果与16384取模,以确定key存储在哪个hash 在slot中,还需要指定Rediss 负责Cluster中每个数据分片的Slot数。Slot的分配可以在任何时候重新分配。

在读写key时,客户端可以将Cluster中的任何一个分片连接起来,如果操作的key不在该分片负责的slot范围内,Redis Cluster会自动将请求重定向正确的分片。

hash tags

原则上,Redis还支持基本的分片hash tags功能,hash 标签所需格式明确的key,将确保进入同一个Slot。例如:{uiv}user:1000和{uiv}user:1001拥有相同的hash tag {uiv},它将保存在同一个Slot中。

使用Redis Cluster,pipelining、事务和LUA Script功能所涉及的key必须在同一数据分片上,否则将返回错误。如果你想在Redis 在Cluster中使用上述功能,必须使用hash 确保一个pipeline或一个事务中操作的所有key都位于同一个slot中。

一些客户端(如Redisson)实现了集群pipelining操作,可以根据key所在的片段自动分组pipeline中的命令,分别发送到不同的片段。但Redis不支持跨片事务、事务和LUA Script仍然必须遵循一个片段中所有key的规则要求。

主从复制 vs 集群分片

在设计软件架构时,如何选择主从复制和集群分片的两种部署方案?

从各个方面看,Redis Cluster优于主从复制方案

  • Redis Cluster可以解决单个节点数据量过大的问题
  • Redis Cluster可以解决单节点访问压力过大的问题
  • Redis Cluster包含了主从复制的能力

是否代表Redisss? Cluster总是优于主从复制?

并不是。

软件架构永远不会越复杂越好。复杂的架构不仅会带来显著的好处,还会带来相应的缺点。使用Redis Cluster的缺点包括:

  • 维护难度增加。使用Redis。 Cluster时,需要维护的Redis实例数倍增,需要监控的主机数量也相应增加,数据备份/持久化的复杂性也会增加。同时,在分片的增减操作中,Reshard操作也需要进行,这比主模式下增加Slave的复杂性要高得多。
  • 客户端资源消耗增加。当客户端使用连接池时,需要为每个数据部分维护连接池。客户端同时需要维护的连接数量翻了一番,增加了客户端本身和操作系统资源的消耗。
  • 性能优化难度增加。您可能需要查看多个片段的Slow 只有Log和Swap日志才能定位性能问题。
  • 事务和LUA Script的使用成本增加了。在Redis 使用Cluster和LUA Script特性有严格的限制,Script中操作的key必须位于同一部分,这使得在开发过程中必须对相应场景中涉及的key进行额外的规划和规范要求。如果应用场景涉及到大量事务和Script的使用,很难在保证这两个功能正常运行的前提下将数据平均分配到多个数据片中。

因此,在主要从复制和集群分片两个方案中进行选择时,应综合考虑应用软件的功能特性、数据和访问量级以及未来的发展规划。只有在确实需要引入数据分片时才能使用Redis Cluster。以下是一些建议:

  1. Redis中需要存储多少数据?未来两年可能会发展多少?所有这些数据都需要长期保存吗?LRU算法可以用来淘汰非热点数据吗?综合考虑以上因素,评估Redis需要使用的物理内存。
  2. 用于部署Redis的主机的物理内存有多大?Redis可以分配多少?比较(1)内存需求评估是否足够?
  3. Redis将面临多大的并发写作压力?在不使用pipelining的情况下,Redis的写作性能可以超过10万次/秒(可以参考更多的benchmark https://redis.io/topics/benchmarks )
  4. pipelining和事务功能会在使用Redis时使用吗?使用的场景有多少?

考虑到以上几点,如果单个主机的可用物理内存完全足以支持Redis的容量需求,而Redis面临的并发写作压力仍然远离Benchmark值,建议采用主从复制架构,可以节省很多不必要的麻烦。同时,如果pipelining和事务在应用中得到广泛应用,建议在设计和开发过程中尽量选择主从复制架构,以降低复杂性。

Redis Java客户端的选择

Redis有很多Java客户端,官方推荐有三种:Jedis、Redisson和lettuce。

这里对Jedis和Redison进行比较介绍

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的选择也应遵循上述原则。虽然Jedisson与Redison相比存在各种不足,但在需要使用Redison的高级特性时,也应选择Redison,以避免提高不必要的程序复杂性。