Redis学习之数据结构与持久化(一)

362 阅读19分钟

API的使用

通用命令

1)keys

遍历所有key。

可以使用keys he*遍历所有以he开头的键。

使用方式:热备从节点,scan。

2)dbsize

计算key的总数。

3)exists

检查key是否存在。

4)del key

删除指定的key-value。

5)expire key seconds

设置key的过期时间。

6)ttl key

查看key剩余的过期时间。

7)persist key

去掉key的过期时间。

8)type key

查看key的类型。

速度快的原因

1)纯内存。

2)非阻塞IO。

3)避免线程切换和静态消耗。

数据结构和内部编码

String

Redis String类型是可以与Redis键关联的最简单的值类型。它是Memcached中唯一的数据类型,因此新手在Redis中使用它也很自然。

由于Redis键是字符串,当我们使用字符串类型作为值时,我们将字符串映射到另一个字符串。字符串数据类型对于许多用例很有用,例如缓存HTML片段或页面。

set命令

set:不管key是否存在,都设置。

setnx:key不存在,才设置。

set key value xx:key存在,才设置。

mget、mset命令

mget key1 key2 key3,....:批量获取key,原子操作。

mset key1 value1 key2 value2 key3 value3:批量设置key-value。

HASH

实际上是一个map->map的结构,可以很方便的存储Java类。Redis哈希看起来正是人们可能期望看到一个“哈希”,使用字段值对:

> hmset user:1000 username antirez birthyear 1977 verified 1
OK
> hget user:1000 username
"antirez"
> hget user:1000 birthyear
"1977"
> hgetall user:1000
1) "username"
2) "antirez"
3) "birthyear"
4) "1977"
5) "verified"
6) "1"

相关命令:

hget、hset、hdel、hesists、hlen、hmget、hmset。

List

所述LPUSH命令将一个新元素到一个列表,在左侧(在头部),而RPUSH命令将一个新元素到一个列表,在右侧(在尾部)。最后, LRANGE命令从列表中提取元素范围:

> rpush mylist A
(integer) 1
> rpush mylist B
(integer) 2
> lpush mylist first
(integer) 3
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"

请注意,LRANGE需要两个索引,即要返回的范围的第一个和最后一个元素。两个索引都可以是负数,告诉Redis从结尾开始计数:所以-1是最后一个元素,-2是列表的倒数第二个元素,依此类推。

正如您所见,RPUSH附加了列表右侧的元素,而最后的LPUSH附加了左侧的元素。

Redis列表中定义的一个重要操作是弹出元素的能力。弹出元素是从列表中检索元素并同时从列表中删除元素的操作。您可以从左侧和右侧弹出元素,类似于如何在列表的两侧推送元素:

> rpush mylist a b c
(integer) 3
> rpop mylist
"c"
> rpop mylist
"b"
> rpop mylist
"a"

Capped lists(上限列表)

在许多用例中,我们只想使用列表来存储最新的项目,无论它们是什么:社交网络更新,日志或其他任何内容。

Redis允许我们使用列表作为上限集合,只记住最新的N项并使用LTRIM命令丢弃所有最旧的项。

LTRIM命令类似于LRANGE,但是**,而不是显示元件的指定范围**它设置在该范围作为新的列表值。超出给定范围之外的所有元素。

一个例子将使它更清楚:

> rpush mylist 1 2 3 4 5
(integer) 5
> ltrim mylist 0 2
OK
> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"

上面的LTRIM命令告诉Redis只从索引0到2中获取列表元素,其他所有内容都将被丢弃。这允许一个非常简单但有用的模式:一起执行List推操作+ List修剪操作以添加新元素并丢弃超出限制的元素:

LPUSH mylist <some element>
LTRIM mylist 0 999

上面的组合添加了一个新元素,并且只将1000个最新元素放入列表中。使用LRANGE,您可以访问顶级项目,而无需记住非常旧的数据。

注意:虽然LRANGE在技术上是一个O(N)命令,但是访问列表的头部或尾部的小范围是一个恒定时间操作。

Set

Redis集是字符串的无序集合。该 SADD命令添加新的元素到set。对于集合执行许多其他操作也是可能的,例如测试给定元素是否已存在,执行多个集合之间的交集,并集或差异等等。

> sadd myset 1 2 3
(integer) 3
> smembers myset
1. 3
2. 1
3. 2

在这里,我已经为我的集合添加了三个元素,并告诉Redis返回所有元素。正如您所看到的那样,它们没有排序 - Redis可以在每次调用时以任意顺序返回元素,因为与用户没有关于元素排序的合同。

Redis有命令来测试会员资格。例如,检查元素是否存在:

> sismember myset 3
(integer) 1
> sismember myset 30
(integer) 0

Sorted sets

排序集是一种数据类型,类似于Set和Hash之间的混合。与集合一样,有序集合由唯一的,非重复的字符串元素组成,因此在某种意义上,有序集合也是一个集合。

但是,虽然内部集合中的元素没有排序,但是有序集合中的每个元素都与浮点值相关联,称为分数 (这就是为什么类型也类似于散列,因为每个元素都映射到一个值)。

此外,排序集合中的元素按顺序排列(因此它们不是根据请求排序的,顺序是用于表示排序集合的数据结构的特性)。它们按照以下规则订购:

  • 如果A和B是两个具有不同分数的元素,如果A.score> B.score,那么A> B。
  • 如果A和B具有完全相同的分数,如果A字符串按字典顺序大于B字符串,则A> B。A和B字符串不能相等,因为有序集只有唯一元素。

让我们从一个简单的例子开始,添加一些选定的黑客名称作为有序集合元素,其出生年份为“得分”。

> zadd hackers 1940 "Alan Kay"
(integer) 1
> zadd hackers 1957 "Sophie Wilson"
(integer) 1
> zadd hackers 1953 "Richard Stallman"
(integer) 1
> zadd hackers 1949 "Anita Borg"
(integer) 1
> zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
> zadd hackers 1914 "Hedy Lamarr"
(integer) 1
> zadd hackers 1916 "Claude Shannon"
(integer) 1
> zadd hackers 1969 "Linus Torvalds"
(integer) 1
> zadd hackers 1912 "Alan Turing"
(integer) 1

正如您所看到的,ZADD与SADD类似,但是需要一个额外的参数(放在要添加的元素之前),即分数。 ZADD也是可变参数,因此您可以自由指定多个得分 - 值对,即使在上面的示例中未使用它。

对于排序集,返回按出生年份排序的黑客列表是微不足道的,因为实际上它们已经排序了。

实现说明:排序集是通过包含跳过列表和散列表的双端口数据结构实现的,因此每次添加元素时,Redis都会执行O(log(N))操作。这很好,但是当我们要求排序的元素时,Redis根本不需要做任何工作,它已经全部排序了:

> zrange hackers 0 -1
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"
6) "Richard Stallman"
7) "Sophie Wilson"
8) "Yukihiro Matsumoto"
9) "Linus Torvalds"

如果我想以相反的方式订购它们,最小到最老的怎么办?使用ZREVRANGE而不是ZRANGE:

> zrevrange hackers 0 -1
1) "Linus Torvalds"
2) "Yukihiro Matsumoto"
3) "Sophie Wilson"
4) "Richard Stallman"
5) "Anita Borg"
6) "Alan Kay"
7) "Claude Shannon"
8) "Hedy Lamarr"
9) "Alan Turing"

使用WITHSCORES参数也可以返回分数:

> zrange hackers 0 -1 withscores
1) "Alan Turing"
2) "1912"
3) "Hedy Lamarr"
4) "1914"
5) "Claude Shannon"
6) "1916"
7) "Alan Kay"
8) "1940"
9) "Anita Borg"
10) "1949"
11) "Richard Stallman"
12) "1953"
13) "Sophie Wilson"
14) "1957"
15) "Yukihiro Matsumoto"
16) "1965"
17) "Linus Torvalds"
18) "1969"

在切换到下一个主题之前,只需要关于排序集的最后一点。排序集的分数可以随时更新。仅针对已经包含在已排序集合中的元素调用ZADD将使用O(log(N))时间复杂度更新其得分(和位置)。因此,当有大量更新时,排序集合是合适的。

由于这个特性,常见的用例是排行榜。典型的应用程序是一个Facebook游戏,在这个游戏中,您可以结合使用高分进行排序的用户以及获取排名操作,以显示前N个用户以及排行榜中的用户排名(例如,“你是这里#4932的最佳成绩“)。

配置建议

maxToTal

理论值=命令平均执行时间*业务总QPS数。

maxIdle&&minIdle(最大空闲数与最少空闲数)

1)maxIdle=maxTotal,减少创建新连接的开销。

2)预热minIdle,减少第一次启动后的新连接的开销。

redis常用功能

慢查询

生命周期

1)慢查询发生在第三阶段。

2)客户端超时不一定慢查询,但慢查询是客户端超时的一个可能因素。

两个配置

1)slowlog-max-len

1:先进先出队列

2:固定长度

3:保存在内存内

2)slowlog-log-slower-than

慢查询阈值(单位:微秒)

命令

1)slowlog get [n]:获取慢查询队列。

2)slowlog len:获取慢查询队列长度。

3)slowlog reset:清空慢查询队列。

运维经验

1)slowlog-max-len不要设置过小,通常设置1000左右。

2)slowlog-log-slower-than不要设置过大,默认10ms,通常设置1ms。

3)理解生命周期。

4)定期持久化慢查询。

Pipeline

Redis是使用客户端 - 服务器模型和所谓的请求/响应协议的TCP服务器。

这意味着通常通过以下步骤完成请求:

  • 客户端向服务器发送查询,并通常以阻塞方式从套接字读取服务器响应。
  • 服务器处理该命令并将响应发送回客户端。

例如,四个命令序列是这样的:

  • 客户: INCR X.
  • 服务器: 1
  • 客户: INCR X.
  • 服务器: 2
  • 客户: INCR X.
  • 服务器: 3
  • 客户: INCR X.
  • 服务器: 4

客户端和服务器通过网络链接连接。这样的链接可以非常快(环回接口)或非常慢(在因特网上建立的连接,在两个主机之间有许多跳)。无论网络延迟是什么,数据包都有时间从客户端传输到服务器,然后从服务器返回到客户端以进行回复。

此时间称为RTT(Round Trip Time)。当客户端需要连续执行许多请求时(例如,将多个元素添加到同一列表或使用多个键填充数据库),很容易看出这会如何影响性能。例如,如果RTT时间是250毫秒(在因特网上的链路非常慢的情况下),即使服务器能够每秒处理100k个请求,我们也能够以每秒最多四个请求进行处理。

如果使用的接口是环回接口,则RTT要短得多(例如我的主机报告0,044毫秒,ping 127.0.0.1),但如果你需要连续执行多次写入,它仍然很多。

幸运的是,有一种方法可以改进这个用例。

Redis Pipelining

可以实现请求/响应服务器,以便即使客户端尚未读取旧响应,它也能够处理新请求。这样就可以将多个命令发送到服务器而无需等待回复,最后只需一步即可读取回复。

这被称为流水线技术,并且是几十年来广泛使用的技术。例如,许多POP3协议实现已经支持此功能,大大加快了从服务器下载新电子邮件的过程。

Redis从很早就开始支持流水线操作,因此无论您运行什么版本,都可以使用Redis进行流水线操作。这是使用原始netcat实用程序的示例:

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

重要说明:当客户端使用流水线发送命令时,服务器将被强制使用内存对回复进行排队。因此,如果您需要使用流水线发送大量命令,最好将它们作为具有合理数量的批次发送,例如10k命令,读取回复,然后再次发送另外10k命令,依此类推。速度将几乎相同,但使用的额外内存将最大为此10k命令的回复排队所需的数量。

mset与pipeline对比

mset:原子操作。

pipeline:非原子操作。

使用建议

1)注意每次pipeline携带数据量。

2)pipeline每次只能作用在一个Redis节点上。

发布订阅

SUBSCRIBE,UNSUBSCRIBE和PUBLISH 实现了发布/订阅消息传递范例,其中(引用维基百科)发件人(发布者)没有被编程为将其消息发送给特定接收者(订阅者)。相反,发布的消息被表征为信道,而不知道可能存在什么(如果有的话)订户。订阅者表达对一个或多个频道的兴趣,并且仅接收感兴趣的消息,而不知道有哪些(如果有的话)发布者。发布者和订阅者的这种分离可以允许更大的可扩展性和更动态的网络拓扑。

例如,为了订阅频道foo,bar客户端发出提供频道名称的SUBSCRIBE:

SUBSCRIBE foo bar

其他客户端发送到这些频道的消息将由Redis推送到所有订阅的客户端。

订阅一个或多个频道的客户端不应发出命令,尽管它可以订阅和取消订阅其他频道。对订阅和取消订阅操作的回复以消息的形式发送,以便客户端可以只读取连贯的消息流,其中第一个元素指示消息的类型。订阅客户端上下文中允许的命令是SUBSCRIBE,PSUBSCRIBE,UNSUBSCRIBE,PUNSUBSCRIBE, PING和QUIT。

请注意,redis-cli在订阅模式下不会接受任何命令,并且只能退出模式Ctrl-C。

消息是具有三个元素的Array回复

第一个元素是消息的类型:

  • subscribe:表示我们成功订阅了作为回复中第二个元素的通道。第三个参数表示我们当前订阅的频道数。
  • unsubscribe:表示我们成功取消订阅作为回复中第二个元素的频道。第三个参数表示我们当前订阅的频道数。当最后一个参数为零时,我们不再订阅任何通道,并且客户端可以发出任何类型的Redis命令,因为我们在Pub / Sub状态之外。
  • message:它是由另一个客户端发出的PUBLISH命令收到的消息。第二个元素是原始通道的名称,第三个参数是实际的消息有效负载。

Redis的持久化

Redis提供了不同的持久性选项:

  • RDB持久性以指定的时间间隔执行数据集的时间点快照。
  • AOF持久性记录服务器接收的每个写入操作,将在服务器启动时再次播放,重建原始数据集。使用与Redis协议本身相同的格式以仅追加方式记录命令。当Redis太大时,Redis能够重写日志。
  • 如果您愿意,只要服务器正在运行,您就可以根据需要禁用持久性。
  • 可以在同一实例中组合AOF和RDB。请注意,在这种情况下,当Redis重新启动时,AOF文件将用于重建原始数据集,因为它保证是最完整的。

最重要的是要理解RDB和AOF持久性之间的不同权衡。让我们从RDB开始:

RDB的优势

  • RDB是Redis数据的一个非常紧凑的单文件时间点表示。RDB文件非常适合备份。例如,您可能希望在最近24小时内每小时归档您的RDB文件,并且每天保存RDB快照30天。这使您可以在发生灾难时轻松恢复数据集的不同版本。
  • RDB非常适合灾难恢复,可以将单个压缩文件传输到远端数据中心,也可以传输到Amazon S3(可能是加密的)。
  • RDB最大限度地提高了Redis的性能,因为Redis父进程为了坚持不懈而需要做的唯一工作就是分配一个将完成所有其余工作的孩子。父实例永远不会执行磁盘I/O或类似操作。
  • 与AOF相比,RDB允许使用大数据集更快地重启。

RDB的缺点

  • 如果您需要在Redis停止工作时(例如断电后)将数据丢失的可能性降至最低,则RDB并不好。您可以配置生成RDB的不同保存点(例如,在对数据集进行至少五分钟和100次写入之后,但您可以有多个保存点)。但是,您通常每五分钟或更长时间创建一个RDB快照,因此如果Redis因任何原因停止工作而没有正确关闭,您应该准备丢失最新的数据分钟。
  • RDB经常需要fork()才能使用子进程持久存储在磁盘上。如果数据集很大,Fork()可能会非常耗时,并且如果数据集非常大且CPU性能不佳,可能会导致Redis停止服务客户端几毫秒甚至一秒钟。AOF也需要fork(),但你可以调整你想要重写日志的频率而不需要对耐久性进行任何权衡。

AOF优势

  • 使用AOF Redis更持久:您可以使用不同的fsync策略:no fsync at all, fsync every second, fsync at every query。使用fsync的默认策略,每秒写入性能仍然很好(使用后台线程执行fsync,并且当没有fsync正在进行时,主线程将努力执行写入。)但是您只能丢失一秒的写入。
  • AOF日志是仅附加日志,因此如果停电,则没有搜索,也没有损坏问题。即使由于某种原因(磁盘已满或其他原因)日志以半写命令结束,redis-check-aof工具也能够轻松修复它。
  • 当Redis太大时,Redis能够在后台自动重写AOF。重写是完全安全的,因为当Redis继续附加到旧文件时,使用创建当前数据集所需的最小操作集生成一个全新的文件,并且一旦第二个文件准备就绪,Redis会切换两个并开始附加到新的那一个。
  • AOF以易于理解和解析的格式一个接一个地包含所有操作的日志。您甚至可以轻松导出AOF文件。例如,即使您使用FLUSHALL命令刷新了所有错误,如果在此期间未执行重写日志,您仍然可以保存数据集,只需停止服务器,删除最新命令,然后重新启动Redis。

AOF的缺点

  • AOF文件通常比同一数据集的等效RDB文件大。
  • 根据确切的fsync策略,AOF可能比RDB慢。一般来说,fsync设置为every second性能仍然非常高,并且在fsync禁用的情况下,即使在高负载下也应该与RDB一样快。即使在写入负载很大的情况下,RDB仍能够提供有关最大延迟的更多保证。
  • 在过去,我们遇到了特定命令中的罕见错误(例如,有一个涉及阻塞命令,如BRPOPLPUSH)导致生成的AOF在重新加载时不会重现完全相同的数据集。这个错误很少见,我们在测试套件中进行测试,自动创建随机复杂数据集并重新加载它们以检查一切正常,但RDB持久性几乎不可能出现这种错误。为了更清楚地说明这一点:Redis AOF逐步更新现有状态,如MySQL或MongoDB,而RDB快照一次又一次地创建所有内容,这在概念上更加健壮。但是 —— 1)应该注意的是,每次通过Redis重写AOF时,都会从数据集中包含的实际数据开始重新创建,与总是附加的AOF文件(或者重写旧的AOF而不是读取内存中的数据)相比,对bug的抵抗力更强。2)我们从未向用户提供过关于在现实世界中检测到的AOF损坏的单一报告。

好的,那我该怎么用?

一般的迹象是,如果您希望一定程度的数据安全性与PostgreSQL为您提供的数据安全性相当,则应使用两种持久性方法。

如果你非常关心你的数据,但是在发生灾难的情况下仍然会有几分钟的数据丢失,你可以单独使用RDB。

有许多用户单独使用AOF,但我们不鼓励它,因为不时有RDB快照是进行数据库备份,更快重启以及AOF引擎中出现错误的好主意。

注意:由于所有这些原因,我们可能最终将AOF和RDB统一为未来的单一持久性模型(长期计划)。

有关详细的服务器配置请查阅我之前的博客。

常见的持久化开发运维问题

改善fork

1)优先使用物理机或者高效支持fork操作的虚拟化技术。

2)控制Redis实例最大可用内存:maxmemory。

3)合理配置Linux内存分配策略:vm.overcommit_memory=1。

4)降低fork频率:例如放宽AOF重写自动触发时机,不必要的全量复制。

子进程开销和优化

CPU

开销:RDB和AOF文件生成,属于CPU密集型。

优化:不做CPU绑定,不和CPU密集型部署。

内存

开销:fork内存开销,copy-on-write。

优化:echo never > /sys/kernel/mm/transparent_hugepage/enabled。

硬盘

开销:AOF和RDB文件写入,可以结合iostat,iotop分析。

优化:no-appendfsync-on-rewrite = yes

AOF阻塞

定位

1)Redis日志

2)info Persistence

3)硬盘使用情况