当前位置:   article > 正文

redis的详细介绍_redis原子值

redis原子值

一、概述

Redis是一个开源的,基于内存的结构化数据存储媒介,可以作为数据库、缓存服务或消息服务使用。

Redis支持多种数据结构,包括字符串、哈希表、链表、集合、有序集合、位图、Hyperloglogs等。

Redis具备LRU淘汰、事务实现、以及不同级别的硬盘持久化等能力,并且支持副本集和通过Redis Sentinel实现的高可用方案,同时还支持通过Redis Cluster实现的数据自动分片能力。

Redis的主要功能都基于单线程模型实现,也就是说Redis使用一个线程来服务所有的客户端请求

同时Redis采用了非阻塞式IO,并精细地优化各种命令的算法时间复杂度,这些信息意味着:
在这里插入图片描述

2、Redis的数据结构和相关常用命令

本节将介绍Redis支持的主要数据结构,以及相关的常用Redis命令

2.1、Key

Redis采用Key-Value型的基本数据结构,任何二进制序列都可以作为Redis的Key使用(例如普通的字符串或一张JPEG图片)
关于Key的一些注意事项:
在这里插入图片描述

2.2、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"
  • 1

库存扣减+余量校验:

DECR inv:remain
  • 1

当DECR命令返回值大于等于0时,说明库存余量校验通过,如果返回小于0的值,则说明库存已耗尽。

假设同时有300个并发请求进行库存扣减,Redis能够确保这300个请求分别得到99到-200的返回值,每个请求得到的返回值都是唯一的,绝对不会找出现两个请求得到一样的返回值的情况。

例2:自增序列生成
实现类似于RDBMS的Sequence功能,生成一系列唯一的序列号
设置序列起始值:

SET sequence "10000"
  • 1

获取一个序列值:

INCR sequence
  • 1

直接将返回值作为序列使用即可。
获取一批(如100个)序列值:

INCRBY sequence 100
  • 1

假设返回值为N,那么[N - 99 ~ N]的数值都是可用的序列值。

当多个客户端同时向Redis申请自增序列时,Redis能够确保每个客户端得到的序列值或序列范围都是全局唯一的,绝对不会出现不同客户端得到了重复的序列值的情况。

2.3、List

Redis的List是链表型的数据结构,可以使用LPUSH/RPUSH/LPOP/RPOP等命令在List的两端执行插入元素和弹出元素的操作。

虽然List也支持在特定index上插入和读取元素的功能,但其时间复杂度较高(O(N)),应小心使用。
与List相关的常用命令:
在这里插入图片描述
注意:应尽可能控制一次获取的元素数量,一次获取过大范围的List元素会导致延迟,同时对长度不可预知的List,避免使用LRANGE key 0 -1这样的完整遍历操作。

应谨慎使用的List相关命令:
在这里插入图片描述
由于Redis的List是链表结构的,上述的三个命令的算法效率较低,需要对List进行遍历,命令的耗时无法预估,在List长度大的情况下耗时会明显增加,应谨慎使用。

换句话说,Redis的List实际是设计来用于实现队列,而不是用于实现类似ArrayList这样的列表的。

如果你不是想要实现一个双端出入的队列,那么请尽量不要使用Redis的List数据结构。

为了更好支持队列的特性,Redis还提供了一系列阻塞式的操作命令,如BLPOP/BRPOP等,能够实现类似于BlockingQueue的能力,即在List为空时,阻塞该连接,直到List中有对象可以出队时再返回。

2.4、Hash

Hash即哈希表,Redis的Hash和传统的哈希表一样,是一种field-value型的数据结构,可以理解成将HashMap搬入Redis。
Hash非常适合用于表现对象类型的数据,用Hash中的field对应对象的field即可。
Hash的优点包括:
在这里插入图片描述
与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命令进行游标式的遍历

2.5、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相关命令:

在这里插入图片描述
上述几个命令涉及的计算量大,应谨慎使用,特别是在参与计算的Set尺寸不可知的情况下,应严格避免使用。

2.6、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相关命令:

在这里插入图片描述
上述几个命令,应尽量避免传递[0 -1]或[-inf +inf]这样的参数,来对Sorted Set做一次性的完整遍历,特别是在Sorted Set的尺寸不可预知的情况下。

2.7、Bitmap和HyperLogLogs

Redis的这两种数据结构相较之前的并不常用,在本文中只做简要介绍

Bitmap在Redis中不是一种实际的数据类型,而是一种将String作为Bitmap使用的方法。

可以理解为将String转换为bit数组。使用Bitmap来存储true/false类型的简单数据极为节省空间

HyperLogLogs是一种主要用于数量统计的数据结构,它和Set类似,维护一个不可重复的String集合

但是HyperLogLogs并不维护具体的member内容,只维护member的个数。

也就是说,HyperLogLogs只能用于计算一个集合中不重复的元素数量,所以它比Set要节省很多内存空间。

2.8、其他常用命令

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中的配置

3、数据持久化

Redis提供了将数据定期自动持久化至硬盘的能力,包括RDB和AOF两种方案。

两种方案分别有其长处和短板,可以配合起来同时运行,确保数据的稳定性。

3.1、必须使用数据持久化吗?

Redis的数据持久化机制是可以关闭的,如果你只把Redis作为缓存服务使用,Redis中存储的所有数据都不是该数据的主体而仅仅是同步过来的备份,那么可以关闭Redis的数据持久化机制。

但通常来说,仍然建议至少开启RDB方式的数据持久化,因为:
在这里插入图片描述

3.2、RDB

采用RDB持久方式,Redis会定期保存数据快照至一个rbd文件中,并在启动时自动加载rdb文件,恢复之前保存的数据。

可以在配置文件中配置Redis进行快照保存的时机:

save [seconds] [changes]
  • 1

意为在[seconds]秒内如果发生了[changes]次数据修改,则进行一次RDB快照保存,例如:

save 60 100
  • 1

会让Redis每60秒检查一次数据变更情况,如果发生了100次或以上的数据变更,则进行RDB快照保存。

可以配置多条save指令,让Redis执行多级的快照保存策略。
Redis默认开启RDB快照,默认的RDB策略如下

save 900 1
save 300 10
save 60 10000
  • 1
  • 2
  • 3

也可以通过BGSAVE命令手工触发RDB快照保存

RDB的优点
在这里插入图片描述

RDB的缺点
在这里插入图片描述

3.3、AOF

采用AOF持久方式时,Redis会把每一个写请求都记录在一个日志文件里。
在Redis重启时,会把AOF文件中记录的所有写操作顺序执行一遍,确保数据恢复到最新。

AOF默认是关闭的,如要开启,进行如下配置:

appendonly yes
  • 1

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
  • 1
  • 2

上面两行配置的含义是,Redis在每次AOF rewrite时,会记录完成rewrite后的AOF日志大小,当AOF日志大小在该基础上增长了100%后,自动进行AOF rewrite。同时如果增长的大小没有达到64mb,则不会进行rewrite。

AOF的优点
在这里插入图片描述
AOF的缺点
在这里插入图片描述

4、内存管理与数据淘汰机制

4.1、最大内存设置

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

在使用Redis时,应该对数据占用的最大空间有一个基本准确的预估,并为Redis设定最大使用的内存。

否则在64位OS中Redis会无限制地占用内存(当物理内存被占满后会使用swap空间),容易引发各种各样的问题。

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

maxmemory 100mb
  • 1

在内存占用达到了maxmemory后,再向Redis写入数据时,Redis会:
在这里插入图片描述
在为Redis设置maxmemory时,需要注意:
如果采用了Redis的主从同步,主节点向从节点同步数据时,会占用掉一部分内存空间
如果maxmemory过于接近主机的可用内存,会导致数据同步时内存不足。
所以设置的maxmemory不要过于接近主机可用的内存,留出一部分预留用作主从同步。

4.2、数据淘汰机制

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

5、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"
  • 1
  • 2
  • 3

此时便可以使用Redis提供的pipelining功能来实现在一次交互中执行多条命令。
使用pipelining时,只需要从客户端一次向Redis发送多条命令(以\r\n)分隔,Redis就会依次执行这些命令,并且把每个命令的返回按顺序组装在一起一次返回,比如:

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

大部分的Redis客户端都对Pipelining提供支持,所以开发者通常并不需要自己手工拼装命令列表。

5.1、Pipelining的局限性

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

通过Scripting功能,可以规避这一局限性

6、事务与Scripting

Pipelining能够让Redis在一次交互中处理多条命令,然而在一些场景下,我们可能需要在此基础上确保这一组命令是连续执行的。
比如获取当前累计的PV数并将其清0

> GET vCount
12384
> SET vCount 0
OK
  • 1
  • 2
  • 3
  • 4

如果在GET和SET命令之间插进来一个INCR vCount,就会使客户端拿到的vCount不准确。

Redis的事务可以确保复数命令执行时的原子性
也就是说Redis能够保证:一个事务中的一组命令是绝对连续执行的,在这些命令执行完成之前,绝对不会有来自于其他连接的其他命令插进去执行。

通过MULTI和EXEC命令来把这两个命令加入一个事务中:

> MULTI
OK
> GET vCount
QUEUED
> SET vCount 0
QUEUED
> EXEC
1) 12384
2) OK
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Redis在接收到MULTI命令后便会开启一个事务,这之后的所有读写命令都会保存在队列中但并不执行
直到接收到EXEC命令后,Redis会把队列中的所有命令连续顺序执行,并以数组形式返回每个命令的返回结果

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

需要注意的是,Redis事务不支持回滚:如果一个事务中的命令出现了语法错误,大部分客户端驱动会返回错误

2.6.5版本以上的Redis也会在执行EXEC时检查队列中的命令是否存在语法错误,如果存在,则会自动放弃事务并返回错误

但如果一个事务中的命令有非语法类的错误(比如对String执行HSET操作),无论客户端驱动还是Redis都无法在真正执行这条命令之前发现,所以事务中的所有命令仍然会被依次执行。

在这种情况下,会出现一个事务中部分命令成功部分命令失败的情况,然而与RDBMS不同,Redis不提供事务回滚的功能,所以只能通过其他方法进行数据的回滚

6.1、通过事务实现CAS

Redis提供了WATCH命令与事务搭配使用,实现CAS乐观锁的机制
假设要实现将某个商品的状态改为已售:

if(exec(HGET stock:1001 state) == "in stock")
    exec(HSET stock:1001 state "sold");
  • 1
  • 2

这一伪代码执行时,无法确保并发安全性,有可能多个客户端都获取到了"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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

WATCH的机制是:在事务EXEC命令执行时,Redis会检查被WATCH的key,只有被WATCH的key从WATCH起始时至今没有发生过变更,EXEC才会被执行。

如果WATCH的key在WATCH命令到EXEC命令之间发生过变化,则EXEC命令会返回失败

6.2、Scripting

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

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

关于Scripting的具体使用,本文不做详细介绍,请参考官方文档 lua脚本

7、Redis性能调优

尽管Redis是一个非常快速的内存数据存储媒介,也并不代表Redis不会产生性能问题。

前文中提到过,Redis采用单线程模型,所有的命令都是由一个线程串行执行的

所以当某个命令执行耗时较长时,会拖慢其后的所有命令,这使得Redis对每个任务的执行效率更加敏感

针对Redis的性能优化,主要从下面几个层面入手:

1、最初的也是最重要的,确保没有让Redis执行耗时长的命令

2、使用pipelining将连续执行的命令组合执行

3、操作系统的Transparent huge pages功能必须关闭:
echo never > /sys/kernel/mm/transparent_hugepage/enabled

4、如果在虚拟机中运行Redis,可能天然就有虚拟机环境带来的固有延迟。

5、可以通过./redis-cli --intrinsic-latency 100命令查看固有延迟。同时如果对Redis的性能有较高要求的话,应尽可能在物理机上直接部署Redis。

6、检查数据持久化策略

7、考虑引入读写分离机制

7.1、长耗时命令

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类命令的使用请参考官方文档

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

slowlog-log-slower-than xxxms  #执行时间慢于xxx毫秒的命令计入Slow Log
slowlog-max-len xxx  #Slow Log的长度,即最大纪录多少条Slow Log
  • 1
  • 2

使用SLOWLOG GET [number]命令,可以输出最近进入Slow Log的number条命令

使用SLOWLOG RESET命令,可以重置Slow Log

7.2、网络引发的延迟

尽可能使用长连接或连接池,避免频繁创建销毁连接

客户端进行的批量数据操作,应使用Pipeline特性在一次交互中完成。

7.3、数据持久化引发的延迟

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

1、AOF + fsync always的设置虽然能够绝对确保数据安全,但每个操作都会触发一次fsync,会对Redis的性能有比较明显的影响

2、AOF + fsync every second是比较好的折中方案,每秒fsync一次

3、AOF + fsync never会提供AOF持久化方案下的最优性能

4、使用RDB持久化通常会提供比使用AOF更高的性能,但需要注意RDB的策略配置

5、每一次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操作的耗时(微秒)

7.4、Swap引发的延迟

当Linux将Redis所用的内存分页移至swap空间时,将会阻塞Redis进程,导致Redis出现不正常的延迟

Swap通常在物理内存不足或一些进程在进行大量I/O操作时发生,应尽可能避免上述两种情况的出现

/proc/smaps文件中会保存进程的swap记录,通过查看这个文件,能够判断Redis的延迟是否由Swap产生。

如果这个文件中记录了较大的Swap size,则说明延迟很有可能是Swap造成的。

7.5、数据淘汰引发的延迟

当同一秒内有大量key过期时,也会引发Redis的延迟。在使用时应尽量将key的失效时间错开。

7.6、引入读写分离机制

Redis的主从复制能力可以实现一主多从的多节点架构,在这一架构下,主节点接收所有写请求,并将数据同步给多个从节点。

在这一基础上,我们可以让从节点提供对实时性要求不高的读请求服务,以减小主节点的压力。

尤其是针对一些使用了长耗时命令的统计类任务,完全可以指定在一个或多个从节点上执行,避免这些长耗时命令影响其他请求的响应。

8、主从复制与集群分片

8.1、主从复制

Redis支持一主多从的主从复制架构。一个Master实例负责处理所有的写请求,Master将写操作同步至所有Slave。

借助Redis的主从复制,可以实现读写分离和高可用:
在这里插入图片描述

启用主从复制非常简单,只需要配置多个Redis实例,在作为Slave的Redis实例中配置:

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

当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的信息才行

8.1.1.1、redis主从复制详细步骤

在现有企业中80%公司大部分使用的是redis单机服务,在实际的场景当中单一节点的redis容易面临风险。
面临问题
1、机器故障。我们部署到一台 Redis 服务器,当发生机器故障时,需要迁移到另外一台服务器并且要保证数据是同步的。而数据是最重要的,如果你不在乎,基本上也就不会使用 Redis 了。

2、容量瓶颈。当我们有需求需要扩容 Redis 内存时,从 16G 的内存升到 64G,单机肯定是满足不了。当然,你可以重新买个 128G 的新机器。

解决办法
要实现分布式数据库的更大的存储容量和承受高并发访问量,我们会将原来集中式数据库的数据分别存储到其他多个网络节点上。

Redis 为了解决这个单一节点的问题,也会把数据复制多个副本部署到其他节点上进行复制,实现 Redis的高可用,实现对数据的冗余备份,从而保证数据和服务的高可用。

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave),数据的复制是单向的,只能由主节点到从节点。

默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
在这里插入图片描述
主从复制的作用
1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。

3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

4、读写分离:可以用于实现读写分离,主库写、从库读,读写分离不仅可以提高服务器的负载能力,同时可根据需求的变化,改变从库的数量。

5、高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

主从复制原理
主从复制过程大体可以分为3个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段。

在从节点执行 slaveof 命令后,复制过程便开始运作,下面图示可以看出复制过程大致分为6个过程。
在这里插入图片描述
主从配置之后的日志记录也可以看出这个流程。
1、保存主节点(master)信息
执行 slaveof 后 Redis 会打印如下日志:
在这里插入图片描述
2、从节点与主节点建立网络连接
从节点(slave)内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接。
在这里插入图片描述
从节点与主节点建立网络连接。

从节点会建立一个 socket 套接字,从节点建立了一个端口为51234的套接字,专门用于接受主节点发送的复制命令。从节点连接成功后打印如下日志:
在这里插入图片描述
如果从节点无法建立连接,定时任务会无限重试直到连接成功或者执行 slaveofnoone 取消复制。

关于连接失败,可以在从节点执行 info replication 查看 master_link_down_since_seconds 指标,它会记录与主节点连接失败的系统时间。从节点连接主节点失败时也会每秒打印如下日志,方便发现问题:
在这里插入图片描述
3、发送 ping 命令
连接建立成功后从节点发送 ping 请求进行首次通信, ping 请求主要目的如下:

1、检测主从之间网络套接字是否可用。
2、检测主节点当前是否可接受处理命令。
如果发送 ping 命令后,从节点没有收到主节点的 pong 回复或者超时,比如网络超时或者主节点正在阻塞无法响应命令,从节点会断开复制连接,下次定时任务会发起重连。
在这里插入图片描述
在这里插入图片描述
从节点发送的 ping 命令成功返回,Redis 打印如下日志,并继续后续复制流程:
在这里插入图片描述
4、权限验证

如果主节点设置了 requirepass 参数,则需要密码验证,从节点必须配置 masterauth 参数保证与主节点相同的密码才能通过验证。如果验证失败复制将终止,从节点重新发起复制流程。

5、同步数据集

主从复制连接正常通信后,对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点,这部分操作是耗时最长的步骤。

6、命令持续复制

当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性。

8.2、集群分片

为何要做集群分片:
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中。

在这里插入图片描述

8.3、主从复制 vs 集群分片

在设计软件架构时,要如何在主从复制和集群分片两种部署方案中取舍呢?

从各个方面看,Redis Cluster都是优于主从复制的方案
Redis Cluster能够解决单节点上数据量过大的问题
Redis Cluster能够解决单节点访问压力过大的问题
Redis Cluster包含了主从复制的能力

那是不是代表Redis Cluster永远是优于主从复制的选择呢?

并不是!!!

软件架构永远不是越复杂越好,复杂的架构在带来显著好处的同时,一定也会带来相应的弊端。采用Redis Cluster的弊端包括:

1、维护难度增加
在使用Redis Cluster时,需要维护的Redis实例数倍增,需要监控的主机数量也相应增加,数据备份/持久化的复杂度也会增加。

同时在进行分片的增减操作时,还需要进行reshard操作,远比主从模式下增加一个Slave的复杂度要高。

2、客户端资源消耗增加
当客户端使用连接池时,需要为每一个数据分片维护一个连接池,客户端同时需要保持的连接数成倍增多,加大了客户端本身和操作系统资源的消耗。

3、性能优化难度增加
你可能需要在多个分片上查看Slow Log和Swap日志才能定位性能问题。

4、事务和LUA Script的使用成本增加
在Redis Cluster中使用事务和LUA Script特性有严格的限制条件

事务和Script中操作的key必须位于同一个分片上,这就使得在开发时必须对相应场景下涉及的key进行额外的规划和规范要求。

如果应用的场景中大量涉及事务和Script的使用,如何在保证这两个功能的正常运作前提下把数据平均分到多个数据分片中就会成为难点。

所以,在主从复制和集群分片两个方案中做出选择时,应该从应用软件的功能特性、数据和访问量级、未来发展规划等方面综合考虑,只在确实有必要引入数据分片时再使用Redis Cluster。

下面是一些建议:
在这里插入图片描述
综合上面几点考虑,如果单台主机的可用物理内存完全足以支撑对Redis的容量需求,且Redis面临的并发写压力距离Benchmark值还尚有距离,建议采用主从复制的架构,可以省去很多不必要的麻烦。

同时,如果应用中大量使用pipelining和事务,也建议尽可能选择主从复制架构,可以减少设计和开发时的复杂度。

9、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,避免造成不必要的程序复杂度提升。

10、Redis内存统计和内存划分

工欲善其事必先利其器,在说明Redis内存之前首先说明如何统计Redis使用内存的情况。

在客户端通过redis-cli连接服务器后(后面如无特殊说明,客户端一律使用redis-cli),通过info命令可以查看内存使用情况:

info memory
  • 1

在这里插入图片描述
其中,info命令可以显示redis服务器的许多信息,包括服务器基本信息、CPU、内存、持久化、客户端连接信息等等;memory是参数,表示只显示内存相关的信息。

返回结果中比较重要的几个说明如下:

(1)used_memory:Redis分配器分配的内存总量(单位是字节),包括使用的虚拟内存(即swap);Redis分配器后面会介绍。used_memory_human只是显示更友好。

(2)used_memory_rs:Redis进程占据操作系统的内存(单位是字节),与top及ps命令看到的值是一致的;除了分配器分配的内存之外,used_memory_rss还包括进程运行本身需要的内存、内存碎片等,但是不包括虚拟内存。

因此,used_memory和used_memory_rss,前者是从Redis角度得到的量,后者是从操作系统角度得到的量。二者之所以有所不同,一方面是因为内存碎片和Redis进程运行需要占用内存,使得前者可能比后者小,另一方面虚拟内存的存在,使得前者可能比后者大。

由于在实际应用中,Redis的数据量会比较大,此时进程运行占用的内存与Redis数据量和内存碎片相比,都会小得多;因此used_memory_rss和used_memory的比例,便成了衡量Redis内存碎片率的参数;这个参数就是mem_fragmentation_ratio。

(3)mem_fragmentation_ratio:内存碎片比率,该值是used_memory_rss / used_memory的比值。

mem_fragmentation_ratio一般大于1,且该值越大,内存碎片比例越大。mem_fragmentation_ratio<1,说明Redis使用了虚拟内存,由于虚拟内存的媒介是磁盘,比内存速度要慢很多,当这种情况出现时,应该及时排查,如果内存不足应该及时处理,如增加Redis节点、增加Redis服务器的内存、优化应用等。

一般来说,mem_fragmentation_ratio在1.03左右是比较健康的状态(对于jemalloc来说);上面截图中的mem_fragmentation_ratio值很大,是因为还没有向Redis中存入数据,Redis进程本身运行的内存使得used_memory_rss 比used_memory大得多。

(4)mem_allocator:Redis使用的内存分配器,在编译时指定;可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc;截图中使用的便是默认的jemalloc。

10.1、内存划分

Redis作为内存数据库,在内存中存储的内容主要是数据(键值对);通过前面的叙述可以知道,除了数据以外,Redis的其他部分也会占用内存。

Redis的内存占用主要可以划分为以下几个部分:
1、数据
作为数据库,数据是最主要的部分;这部分占用的内存会统计在used_memory中。

Redis使用键值对存储数据,其中的值(对象)包括5种类型,即字符串、哈希、列表、集合、有序集合。这5种类型是Redis对外提供的,实际上,在Redis内部,每种类型可能有2种或更多的内部编码实现;此外,Redis在存储对象时,并不是直接将数据扔进内存,而是会对对象进行各种包装:如redisObject、SDS等;这篇文章后面将重点介绍Redis中数据存储的细节。

2、进程本身运行需要的内存

Redis主进程本身运行肯定需要占用内存,如代码、常量池等等;这部分内存大约几兆,在大多数生产环境中与Redis数据占用的内存相比可以忽略。这部分内存不是由jemalloc分配,因此不会统计在used_memory中。

补充说明:除了主进程外,Redis创建的子进程运行也会占用内存,如Redis执行AOF、RDB重写时创建的子进程。当然,这部分内存不属于Redis进程,也不会统计在used_memory和used_memory_rss中。

3、缓冲内存

缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等;其中,客户端缓冲存储客户端连接的输入输出缓冲;复制积压缓冲用于部分复制功能;AOF缓冲区用于在进行AOF重写时,保存最近的写入命令。在了解相应功能之前,不需要知道这些缓冲的细节;这部分内存由jemalloc分配,因此会统计在used_memory中。

4、内存碎片

内存碎片是Redis在分配、回收物理内存过程中产生的。例如,如果对数据的更改频繁,而且数据之间的大小相差很大,可能导致redis释放的空间在物理内存中并没有释放,但redis又无法有效利用,这就形成了内存碎片。内存碎片不会统计在used_memory中。

内存碎片的产生与对数据进行的操作、数据的特点等都有关;此外,与使用的内存分配器也有关系:如果内存分配器设计合理,可以尽可能的减少内存碎片的产生。后面将要说到的jemalloc便在控制内存碎片方面做的很好。

如果Redis服务器中的内存碎片已经很大,可以通过安全重启的方式减小内存碎片:因为重启之后,Redis重新从备份文件中读取数据,在内存中进行重排,为每个数据重新选择合适的内存单元,减小内存碎片。

11、Redis数据存储的细节

11.1、概述

关于Redis数据存储的细节,涉及到内存分配器(如jemalloc)、简单动态字符串(SDS)、5种对象类型及内部编码、redisObject。在讲述具体内容之前,先说明一下这几个概念之间的关系。

下图是执行set hello world时,所涉及到的数据模型。
在这里插入图片描述
(1)dictEntry:Redis是Key-Value数据库,因此对每个键值对都会有一个dictEntry,里面存储了指向Key和Value的指针;next指向下一个dictEntry,与本Key-Value无关。

(2)Key:图中右上角可见,Key(”hello”)并不是直接以字符串存储,而是存储在SDS结构中。

(3)redisObject:Value(“world”)既不是直接以字符串存储,也不是像Key一样直接存储在SDS中,而是存储在redisObject中。实际上,不论Value是5种类型的哪一种,都是通过redisObject来存储的;而redisObject中的type字段指明了Value对象的类型,ptr字段则指向对象所在的地址。不过可以看出,字符串对象虽然经过了redisObject的包装,但仍然需要通过SDS存储。

实际上,redisObject除了type和ptr字段以外,还有其他字段图中没有给出,如用于指定对象内部编码的字段;后面会详细介绍。

(4)jemalloc:无论是DictEntry对象,还是redisObject、SDS对象,都需要内存分配器(如jemalloc)分配内存进行存储。以DictEntry对象为例,有3个指针组成,在64位机器下占24个字节,jemalloc会为它分配32字节大小的内存单元。

下面来分别介绍jemalloc、redisObject、SDS、对象类型及内部编码。

11.2、jemalloc

Redis在编译时便会指定内存分配器;内存分配器可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc。

jemalloc作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好。jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。
jemalloc划分的内存单元如下图所示:
在这里插入图片描述
例如,如果需要存储大小为130字节的对象,jemalloc会将其放入160字节的内存单元中。

11.3、redisObject

前面说到,Redis对象有5种类型;无论是哪种类型,Redis都不会直接存储,而是通过redisObject对象进行存储。
redisObject对象非常重要,Redis对象的类型、内部编码、内存回收、共享对象等功能,都需要redisObject支持,下面将通过redisObject的结构来说明它是如何起作用的。
redisObject的定义如下(不同版本的Redis可能稍稍有所不同):

typedef struct redisObject {
  unsigned type:4;
  unsigned encoding:4;
  unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
  int refcount;
  void *ptr;
} robj;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

redisObject的每个字段的含义和作用如下:
(1)type

type字段表示对象的类型,占4个比特;目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。

当我们执行type命令时,便是通过读取RedisObject的type字段获得对象的类型;如下图所示:
在这里插入图片描述
(2)encoding
encoding表示对象的内部编码,占4个比特。

对于Redis支持的每种类型,都有至少两种内部编码,例如对于字符串,有int、embstr、raw三种编码。通过encoding属性,Redis可以根据不同的使用场景来为对象设置不同的编码,大大提高了Redis的灵活性和效率。以列表对象为例,有压缩列表和双端链表两种编码方式;如果列表中的元素较少,Redis倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入;当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双端链表。

通过object encoding命令,可以查看对象采用的编码方式,如下图所示:
在这里插入图片描述
5种对象类型对应的编码方式以及使用条件,将在后面介绍。
(3)lru
lru记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同(如4.0版本占24比特,2.6版本占22比特)。

通过对比lru时间与当前时间,可以计算某个对象的空转时间;object idletime命令可以显示该空转时间(单位是秒)。object idletime命令的一个特殊之处在于它不改变对象的lru值。

在这里插入图片描述
lru值除了通过object idletime命令打印之外,还与Redis的内存回收有关系:如果Redis打开了maxmemory选项,且内存回收算法选择的是volatile-lru或allkeys—lru,那么当Redis内存占用超过maxmemory指定的值时,Redis会优先选择空转时间最长的对象进行释放。

(4)refcount

refcount与共享对象

refcount记录的是该对象被引用的次数,类型为整型。refcount的作用,主要在于对象的引用计数和内存回收。当创建新对象时,refcount初始化为1;当有新程序使用该对象时,refcount加1;当对象不再被一个新程序使用时,refcount减1;当refcount变为0时,对象占用的内存会被释放。

Redis中被多次使用的对象(refcount>1),称为共享对象。Redis为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。

共享对象的具体实现
Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为O(1);对于普通字符串,判断复杂度为O(n);而对于哈希、列表、集合和有序集合,判断的复杂度为O(n^2)。

虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。

就目前的实现来说,Redis服务器在初始化时,会创建10000个字符串对象,值分别是09999的整数值;当Redis需要使用值为09999的字符串对象时,可以直接使用这些共享对象。10000这个数字可以通过调整参数REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值进行改变。

共享对象的引用次数可以通过object refcount命令查看,如下图所示。命令执行的结果页佐证了只有0~9999之间的整数会作为共享对象。
在这里插入图片描述
(5)ptr
ptr指针指向具体的数据,如前面的例子中,set hello world,ptr指向包含字符串world的SDS。
(6)总结
综上所述,redisObject的结构与对象类型、编码、内存回收、共享对象都有关系;一个redisObject对象的大小为16字节:

4bit+4bit+24bit+4Byte+8Byte=16Byte。

11.4、SDS

Redis没有直接使用C字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示,不是直接存储字符串,而是使用了SDS。SDS是简单动态字符串(Simple Dynamic String)的缩写。

(1)SDS结构
sds的结构如下:
在这里插入图片描述
其中,buf表示字节数组,用来存储字符串;len表示buf已使用的长度,free表示buf未使用的长度。下面是两个例子。
在这里插入图片描述
通过SDS的结构可以看出,buf数组的长度=free+len+1(其中1表示字符串结尾的空字符);所以,一个SDS结构占据的空间为:free所占长度+len所占长度+ buf数组的长度=4+4+free+len+1=free+len+9。

(2)SDS与C字符串的比较
SDS在C字符串的基础上加入了free和len字段,带来了很多好处:

获取字符串长度:SDS是O(1),C字符串是O(n)
缓冲区溢出:使用C字符串的API时,如果字符串长度增加(如strcat操作)而忘记重新分配内存,很容易造成缓冲区的溢出;而SDS由于记录了长度,相应的API在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。
修改字符串时内存的重分配:对于C字符串,如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。而对于SDS,由于可以记录len和free,因此解除了字符串长度和空间数组长度之间的关联,可以在此基础上进行优化:空间预分配策略(即分配内存时比实际需要的多)使得字符串长度增大时重新分配内存的概率大大减小;惰性空间释放策略使得字符串长度减小时重新分配内存的概率大大减小。
存取二进制数据:SDS可以,C字符串不可以。因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而SDS以字符串长度len来作为字符串结束标识,因此没有这个问题。
此外,由于SDS中的buf仍然使用了C字符串(即以’\0’结尾),因此SDS可以使用C字符串库中的部分函数;但是需要注意的是,只有当SDS用来存储文本数据时才可以这样使用,在存储二进制数据时则不行(’\0’不一定是结尾)。

(3)SDS与C字符串的应用
Redis在存储对象时,一律使用SDS代替C字符串。例如set hello world命令,hello和world都是以SDS的形式存储的。而sadd myset member1 member2 member3命令,不论是键(”myset”),还是集合中的元素(”member1”、 ”member2”和”member3”),都是以SDS的形式存储。除了存储对象,SDS还用于存储各种缓冲区。

只有在字符串不会改变的情况下,如打印日志时,才会使用C字符串。

11.4、Redis的对象类型与内部编码

前面已经说过,Redis支持5种对象类型,而每种结构都有至少两种编码;这样做的好处在于:一方面接口与实现分离,当需要增加或改变内部编码时,用户使用不受影响,另一方面可以根据不同的应用场景切换内部编码,提高效率。

Redis各种对象类型支持的内部编码如下图所示(图中版本是Redis3.0,Redis后面版本中又增加了内部编码,略过不提;本章所介绍的内部编码都是基于3.0的):
在这里插入图片描述
关于Redis内部编码的转换,都符合以下规律:编码转换在Redis写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换。

11.4.1、字符串

(1)概况

字符串是最基础的类型,因为所有的键都是字符串类型,且字符串之外的其他几种复杂类型的元素也是字符串。

字符串长度不能超过512MB

(2)内部编码
字符串类型的内部编码有3种,它们的应用场景如下:

int:8个字节的长整型。字符串值是整型时,这个值使用long整型表示。
embstr:<=39字节的字符串。embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。
raw:大于39个字节的字符串
示例如下图所示:
在这里插入图片描述
embstr和raw进行区分的长度,是39;是因为redisObject的长度是16字节,sds的长度是9+字符串长度;因此当字符串长度是39时,embstr的长度正好是16+9+39=64,jemalloc正好可以分配64字节的内存单元。

(3)编码转换
当int数据不再是整数,或大小超过了long的范围时,自动转化为raw。

而对于embstr,由于其实现是只读的,因此在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了39个字节。示例如下图所示:

在这里插入图片描述

11.4.2、列表

(1)概况
列表(list)用来存储多个有序的字符串,每个字符串称为元素;一个列表可以存储2^32-1个元素。Redis中的列表支持两端插入和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队列、栈等。

(2)内部编码
列表的内部编码可以是压缩列表(ziplist)或双端链表(linkedlist)。

双端链表:由一个list结构和多个listNode结构组成;典型结构如下图所示:

在这里插入图片描述
通过图中可以看出,双端链表同时保存了表头指针和表尾指针,并且每个节点都有指向前和指向后的指针;链表中保存了列表的长度;dup、free和match为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。而链表中每个节点指向的是type为字符串的redisObject。
压缩列表:压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块(而不是像双端链表一样每个节点是指针)组成的顺序型数据结构;具体结构相对比较复杂,略。与双端链表相比,压缩列表可以节省内存空间,但是进行修改或增删操作时,复杂度较高;因此当节点数量较少时,可以使用压缩列表;但是节点数量多时,还是使用双端链表划算。

压缩列表不仅用于实现列表,也用于实现哈希、有序列表;使用非常广泛。
(3)编码转换
只有同时满足下面两个条件时,才会使用压缩列表:列表中元素数量小于512个;列表中所有字符串对象都不足64字节。如果有一个条件不满足,则使用双端列表;且编码只可能由压缩列表转化为双端链表,反方向则不可能。

下图展示了列表编码转换的特点:

在这里插入图片描述
其中,单个字符串不能超过64字节,是为了便于统一分配每个节点的长度;这里的64字节是指字符串的长度,不包括SDS结构,因为压缩列表使用连续、定长内存块存储字符串,不需要SDS结构指明长度。后面提到压缩列表,也会强调长度不超过64字节,原理与这里类似。

其他的类型参考

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/157411
推荐阅读
相关标签
  

闽ICP备14008679号