Redis 指南

Redis 是 C 实现的基于内存并可持久的键值对数据库,在分布式服务中常常被用作缓存。除此之外还可以利用其特点做许多有趣的应用,所以我们不仅需要会用,更需要理解其工作机制。


更新记录

  • 2016.09.13: 添加配置部分更详细的说明及实战应用部分
  • 2016.07.05: 初稿

Redis 的具体介绍在官方网站和维基百科都有,这里我们只要记住几个关键词既可:开源、C 语言、网络交互、基于内存、可持久化、键值对、数据库。作者是 Salvatore Sanfilippo,他的博客和 github 主页都放到文末的参考链接里,有兴趣的同学可以去看看。

根据 Redis 主页上的介绍,许多公司都在使用 Redis,比较著名的有 Twitter GitHub Weibo Pinterest Snapchat Craigslist Digg StackOverflow Flickr 等等,想要了解更多的话,可以参考 Who uses Redis?

也有另一种叫法,称为数据结构服务器,因为保存的 value 可以是字符串(string)、字典(map)、列表(list)、集合(sets)或有序集合(sorted set)。那么键值对存储这么多,到底 Redis 有什么不同之处呢?一是原子性操作,二是在内存中运行。

Redis 脚本使用 Lua 解释器来执行脚本。 Reids 2.6 版本通过内嵌支持 Lua 环境。执行脚本的常用命令为 EVAL。基本语法为 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]

基本使用

安装

在 Mac 下的安装非常简单,只需要 brew install redis 即可,如果需要开机启动,按照安装完成后的提示输出一条命令即可。然后我们输入 redis-server 应该就能看到如下信息

dawang:~ dawang$ redis-server
62799:C 29 Jun 09:48:45.735 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
62799:M 29 Jun 09:48:45.737 * Increased maximum number of open files to 10032 (it was originally set to 4864).
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 3.2.0 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 62799
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
62799:M 29 Jun 09:48:45.738 # Server started, Redis version 3.2.0
62799:M 29 Jun 09:48:45.738 * The server is now ready to accept connections on port 6379

我们看到目前 Redis 运行在 standalone 模式(相对于分布式),对应的端口是 6379。至于为什么是 6379,这背后是有故事的,原文在这里(打开之后搜索6379),我简要翻译一下:

6379 是 MERZ 这个单词在九宫格输入时的按键顺序,那么问题来了,MERZ 又是什么鬼?简单来说来自于一个意大利 showgirl,名字叫做 Alessia Merz,图片这里就不放了。作者日常生活中会创造一些『俚语』,merz 这个词已经用了十年,意思也一直在变化。起初他们 merz 来表示很蠢的事情,比方说『Hey, that’s merz!』,之后意思有些变化,指的是那些有一定技术含量但没什么意义而且还很蠢(stupid)的事儿,或者是那些需要大量技能和耐心才能完成但仍旧很蠢(stupid)的事儿(看出来了吗,核心是 Stupid)。作者举了两个例子,比方说用一台 GPS 和一辆破车大半夜为制作 3D 地图采样,或者在明知道自己不会去买彩票的情况下还是研究大量的彩票信息来找到其『不随机』的证据。总结一下就是有 hack value 的事情,或者是那些为了 hack value 而去做事的人。于是自然而然的,merz 在拨号键盘上对应的数字就成为了 Redis 的端口号。

在 Linux 下除了使用包管理器来安装外,也可以手动安装,这样比较容易进行管理,这里是我自己在使用的安装脚本。编译完成后可以使用 src/redis-serversrc/redis-cli 进行启动和交互。

除了前面使用过的 redis-server 命令,我们还可以使用 redis-cli 命令来启动 redis 客户端,比如:

dawang:~ dawang$ redis-cli
127.0.0.1:6379> set name 'wdxtub'
OK
127.0.0.1:6379> get name
"wdxtub"

如果需要在远程 Redis 上执行命令,也可以用 redis-cli 命令,具体的方式为 redis-cli -h host -p port -a password

其他一些工具为(在 src 文件夹中使用 find . -type f -executable):

  • ./redis-check-aof 检查并修复 AOF 文件
  • ./redis-sentinel Redis 集群管理
  • ./redis-benchmark 性能测试
  • ./redis-check-rdb 检查并修复 RDB 文件

连接

基本命令有 5 个,可以覆盖日常使用的大部分场景,比方说心跳检测、切换数据库之类的:

  • AUTH password: 验证密码
  • ECHO message: 打印字符串
  • PING: 查看服务是否在运行,如果在运行,就输出 PONG
  • QUIT: 关闭当前连接
  • SELECT index: 选择指定的数据库

状态

我们可以使用 INFO 命令来了解当前 Redis 数据库的基本状态,更多的命令请查阅参考链接中给出的地址,这里不赘述:

# Server
redis_version:3.2.0
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:dd22954a73c7ae64
redis_mode:standalone
os:Darwin 15.5.0 x86_64
arch_bits:64
multiplexing_api:kqueue
gcc_version:4.2.1
process_id:882
run_id:c1a9de73731957965cf2cb53cf52e5acb93a3705
tcp_port:6379
uptime_in_seconds:95465
uptime_in_days:1
hz:10
lru_clock:8070643
executable:/usr/local/opt/redis/bin/redis-server
config_file:/usr/local/etc/redis.conf
# Clients
connected_clients:2
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0
# Memory
used_memory:1025232
used_memory_human:1001.20K
used_memory_rss:892928
used_memory_rss_human:872.00K
used_memory_peak:1119232
used_memory_peak_human:1.07M
total_system_memory:17179869184
total_system_memory_human:16.00G
used_memory_lua:37888
used_memory_lua_human:37.00K
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
mem_fragmentation_ratio:0.87
mem_allocator:libc
# Persistence
loading:0
rdb_changes_since_last_save:0
rdb_bgsave_in_progress:0
rdb_last_save_time:1467687446
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:0
rdb_current_bgsave_time_sec:-1
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
# Stats
total_connections_received:5
total_commands_processed:69
instantaneous_ops_per_sec:0
total_net_input_bytes:3232
total_net_output_bytes:3374
instantaneous_input_kbps:0.00
instantaneous_output_kbps:0.00
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:0
evicted_keys:0
keyspace_hits:14
keyspace_misses:0
pubsub_channels:1
pubsub_patterns:0
latest_fork_usec:255
migrate_cached_sockets:0
# Replication
role:master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
# CPU
used_cpu_sys:10.67
used_cpu_user:5.92
used_cpu_sys_children:0.02
used_cpu_user_children:0.01
# Cluster
cluster_enabled:0
# Keyspace
db0:keys=2,expires=0,avg_ttl=0

配置

Redis 的配置文件可以在安装目录下找到,名为 redis.conf,我们来看看都有什么配置

127.0.0.1:6379> CONFIG GET *
1) "dbfilename"
2) "dump.rdb"
3) "requirepass"
4) ""
5) "masterauth"
6) ""
7) "unixsocket"
8) ""
9) "logfile"
10) ""
11) "pidfile"
12) "/usr/local/var/run/redis.pid"
13) "maxmemory"
14) "0"
15) "maxmemory-samples"
16) "5"
17) "timeout"
18) "0"
19) "auto-aof-rewrite-percentage"
20) "100"
21) "auto-aof-rewrite-min-size"
22) "67108864"
23) "hash-max-ziplist-entries"
24) "512"
25) "hash-max-ziplist-value"
26) "64"
27) "list-max-ziplist-size"
28) "-2"
29) "list-compress-depth"
30) "0"
31) "set-max-intset-entries"
32) "512"
33) "zset-max-ziplist-entries"
34) "128"
35) "zset-max-ziplist-value"
36) "64"
37) "hll-sparse-max-bytes"
38) "3000"
39) "lua-time-limit"
40) "5000"
41) "slowlog-log-slower-than"
42) "10000"
43) "latency-monitor-threshold"
44) "0"
45) "slowlog-max-len"
46) "128"
47) "port"
48) "6379"
49) "tcp-backlog"
50) "511"
51) "databases"
52) "16"
53) "repl-ping-slave-period"
54) "10"
55) "repl-timeout"
56) "60"
57) "repl-backlog-size"
58) "1048576"
59) "repl-backlog-ttl"
60) "3600"
61) "maxclients"
62) "10000"
63) "watchdog-period"
64) "0"
65) "slave-priority"
66) "100"
67) "min-slaves-to-write"
68) "0"
69) "min-slaves-max-lag"
70) "10"
71) "hz"
72) "10"
73) "cluster-node-timeout"
74) "15000"
75) "cluster-migration-barrier"
76) "1"
77) "cluster-slave-validity-factor"
78) "10"
79) "repl-diskless-sync-delay"
80) "5"
81) "tcp-keepalive"
82) "0"
83) "cluster-require-full-coverage"
84) "yes"
85) "no-appendfsync-on-rewrite"
86) "no"
87) "slave-serve-stale-data"
88) "yes"
89) "slave-read-only"
90) "yes"
91) "stop-writes-on-bgsave-error"
92) "yes"
93) "daemonize"
94) "no"
95) "rdbcompression"
96) "yes"
97) "rdbchecksum"
98) "yes"
99) "activerehashing"
100) "yes"
101) "protected-mode"
102) "yes"
103) "repl-disable-tcp-nodelay"
104) "no"
105) "repl-diskless-sync"
106) "no"
107) "aof-rewrite-incremental-fsync"
108) "yes"
109) "aof-load-truncated"
110) "yes"
111) "maxmemory-policy"
112) "noeviction"
113) "loglevel"
114) "notice"
115) "supervised"
116) "no"
117) "appendfsync"
118) "everysec"
119) "syslog-facility"
120) "local0"
121) "appendonly"
122) "no"
123) "dir"
124) "/usr/local/var/db/redis"
125) "save"
126) "900 1 300 10 60 10000"
127) "client-output-buffer-limit"
128) "normal 0 0 0 slave 268435456 67108864 60 pubsub 33554432 8388608 60"
129) "unixsocketperm"
130) "0"
131) "slaveof"
132) ""
133) "notify-keyspace-events"
134) ""
135) "bind"
136) "127.0.0.1"

具体解释

  1. Redis 默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程daemonize no
  2. 当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入 /var/run/redis.pid 文件,可以通过 pidfile 指定 pidfile /var/run/redis.pid
  3. 指定 Redis 监听端口,默认端口为 6379 port 6379,如果端口设置为 0 的话,redis 便不监听端口了。此时可以通过 unix socket 方式来和外部通信,比如 /unixsocket /tmp/redis.sock, unixsocketperm 755
  4. 绑定的主机地址 bind 127.0.0.1
  5. 当客户端闲置多长时间后关闭连接,如果指定为 0,表示关闭该功能 timeout 300
  6. TCP 连接保活策略,可以通过 tcp-keepalive 配置项来进行设置,单位为秒,假如设置为 60 秒,则 server 端会每 60 秒向连接空闲的客户端发起一次ACK请求,以检查客户端是否已经挂掉,对于无响应的客户端则会关闭其连接。所以关闭一个连接最长需要120秒的时间。如果设置为0,则不会进行保活检测:tcp-keepalive 0
  7. 指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为 verbose, loglevel verbose
  8. 日志记录方式,默认为标准输出,如果配置 Redis 为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给 /dev/null, logfile stdout
  9. 设置数据库的数量,默认数据库为 0,可以使用 SELECT <dbid> 命令在连接上指定数据库 id, databases 16,这16个数据库的编号将是0到15。默认的数据库是编号为0的数据库。用户可以使用select 来选择相应的数据库。
  10. 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件(快照),可以多个条件配合 save <seconds> <changes> Redis默认配置文件中提供了三个条件:save 900 1, save 300 10, save 60 10000, 分别表示 900 秒(15 分钟)内有 1 个更改,300 秒(5 分钟)内有 10 个更改以及 60 秒内有 10000 个更改。
  11. 如果你想禁用RDB持久化的策略,只要不设置任何save指令就可以,或者给save传入一个空字符串参数也可以达到相同效果 save ""
  12. 如果用户开启了RDB快照功能,那么在redis持久化数据到磁盘时如果出现失败,默认情况下,redis会停止接受所有的写请求。这样做的好处在于可以让用户很明确的知道内存中的数据和磁盘上的数据已经存在不一致了。如果redis不顾这种不一致,一意孤行的继续接收写请求,就可能会引起一些灾难性的后果。如果下一次RDB持久化成功,redis会自动恢复接受写请求。
  13. 如果你不在乎这种数据不一致或者有其他的手段发现和控制这种不一致的话,你完全可以关闭这个功能,以便在快照写入失败时,也能确保 redis 继续接受新的写请求: stop-writes-on-bgsave-error yes
  14. 指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大 rdbcompression yes
  15. 在存储快照后,我们还可以让 redis 使用 CRC64 算法来进行数据校验,但是这样做会增加大约 10% 的性能消耗,如果你希望获取到最大的性能提升,可以关闭此功能 rdbchecksum yes
  16. 设置快照文件的名称,默认值为 dump.rdb, dbfilename dump.rdb
  17. 设置快照文件的存放位置 dir ./
  18. 通过 slaveof 配置项可以控制某一个redis作为另一个redis的从服务器,通过指定IP和端口来定位到主redis的位置。一般情况下,我们会建议用户为从redis设置一个不同频率的快照持久化的周期,或者为从redis配置一个不同的服务端口等等。设置当本机为 slave 服务时,设置 master 服务的IP地址及端口,在 Redis 启动时,它会自动从 master 进行数据同步 slaveof <masterip> <masterport>
  19. 当 master 服务设置了密码保护时,slave 服务连接 master 的密码 masterauth <master-password>
  20. 设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH <password> 命令提供密码,默认关闭 requirepass foobared
  21. 当从 redis 失去了与主 redis 的连接,或者主从同步正在进行中时,redis 该如何处理外部发来的访问请求呢?这里,从 redis 可以有两种选择:如果 slave-serve-stale-data设置为yes(默认),则从 redis 仍会继续响应客户端的读写请求。如果slave-serve-stale-data设置为no,则从 redis 会对客户端的请求返回“SYNC with master in progress”,当然也有例外,当客户端发来 INFO 请求和 SLAVEOF 请求,从 redis 还是会进行处理。
  22. 你可以控制一个从 redis 是否可以接受写请求。将数据直接写入从 redis,一般只适用于那些生命周期非常短的数据,因为在主从同步时,这些临时数据就会被清理掉。自从 redis2.6 版本之后,默认从 redis 为只读 slave-read-only yes
  23. 只读的从 redis 并不适合直接暴露给不可信的客户端。为了尽量降低风险,可以使用 rename-command 指令来将一些可能有破坏力的命令重命名,避免外部直接调用 rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52
  24. 我们甚至可以禁用掉CONFIG命令,那就是把CONFIG的名字改成一个空字符串 rename-command CONFIG "" 但需要注意的是,如果你使用AOF方式进行数据持久化,或者需要与从redis进行通信,那么更改指令的名字可能会引起一些问题。
  25. 从 redis 会周期性的向主 redis 发出 PING 包。默认是10秒 repl-ping-slave-period 10
  26. 在主从同步时,可能在这些情况下会有超时发生:1.以从redis的角度来看,当有大规模IO传输时。2.以从redis的角度来看,当数据传输或PING时,主redis超时。3.以主redis的角度来看,在回复从redis的PING时,从redis超时。用户可以设置上述超时的时限,不过要确保这个时限比 repl-ping-slave-period的值要大,否则每次主 redis 都会认为从 redis 超时 repl-timeout 60
  27. 我们可以控制在主从同步时是否禁用 TCP_NODELAY。如果开启 TCP_NODELAY,那么主 redis 会使用更少的 TCP 包和更少的带宽来向从 redis 传输数据。但是这可能会增加一些同步的延迟,大概会达到40毫秒左右。如果你关闭了 TCP_NODELAY,那么数据同步的延迟时间会降低,但是会消耗更多的带宽 repl-disable-tcp-nodelay no
  28. 我们还可以设置同步队列长度。队列长度(backlog)是主 redis 中的一个缓冲区,在与从 redis 断开连接期间,主 redis 会用这个缓冲区来缓存应该发给从 redis 的数据。这样的话,当从 redis 重新连接上之后,就不必重新全量同步数据,只需要同步这部分增量数据即可 repl-backlog-size 1mb
  29. 如果主 redis 等了一段时间之后,还是无法连接到从 redis,那么缓冲队列中的数据将被清理掉。我们可以设置主 redis 要等待的时间长度。如果设置为 0,则表示永远不清理。默认是 1 个小时 repl-backlog-ttl 3600
  30. 我们可以给众多的从redis设置优先级,在主redis持续工作不正常的情况,优先级高的从redis将会升级为主redis。而编号越小,优先级越高。比如一个主redis有三个从redis,优先级编号分别为10、100、25,那么编号为10的从redis将会被首先选中升级为主redis。当优先级被设置为0时,这个从redis将永远也不会被选中。默认的优先级为100 slave-priority 100
  31. 假如主 redis 发现有超过 M 个从 redis 的连接延时大于 N 秒,那么主 redis 就停止接受外来的写请求。这是因为从 redis 一般会每秒钟都向主 redis 发出 PING,而主 redis 会记录每一个从 redis 最近一次发来 PING 的时间点,所以主 redis 能够了解每一个从 redis 的运行情况 min-slaves-to-write 3min-slaves-max-lag 10
  32. 设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息 maxclients 128
  33. 指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key,当此方法处理后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区, maxmemory <bytes>
  34. 需要注意的一点是,如果你的redis是主redis(说明你的redis有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素。
  35. 对于内存移除规则来说,redis提供了多达6种的移除规则。他们是:
    • volatile-lru:使用LRU算法移除过期集合中的key
    • allkeys-lru:使用LRU算法移除key
    • volatile-random:在过期集合中移除随机的key
    • allkeys-random:移除随机的key
    • volatile-ttl:移除那些TTL值最小的key,即那些最近才过期的key。
    • noeviction:不进行移除。针对写操作,只是返回错误信息。
    • 无论使用上述哪一种移除规则,如果没有合适的key可以移除的话,redis都会针对写请求返回错误信息。
  36. LRU算法和最小TTL算法都并非是精确的算法,而是估算值。所以你可以设置样本的大小。假如redis默认会检查三个key并选择其中LRU的那个,那么你可以改变这个key样本的数量 maxmemory-samples 3
  37. 指定是否在每次更新操作后进行日志记录,Redis 在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis 本身同步数据文件是按上面 save 条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为 no, appendonly no
  38. 指定更新日志文件名,默认为 appendonly.aof, appendfilename appendonly.aof
  39. 指定更新日志条件,共有3个可选值:
    • no:表示等操作系统进行数据缓存同步到磁盘(快)
    • always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
    • everysec:表示每秒同步一次(折衷,默认值)appendfsync everysec
  40. 指定是否启用虚拟内存机制,默认值为 no,简单的介绍一下,VM 机制将数据分页存放,由Redi s将访问量较少的页即冷数据 swap 到磁盘上,访问多的页面由磁盘自动换出到内存中 vm-enabled no
  41. 虚拟内存文件路径,默认值为 /tmp/redis.swap,不可多个 Redis 实例共享 vm-swap-file /tmp/redis.swap
  42. 将所有大于 vm-max-memory 的数据存入虚拟内存,无论 vm-max-memory 设置多小,所有索引数据都是内存存储的(Redis 的索引数据 就是 keys),也就是说,当 vm-max-memory 设置为 0 的时候,其实是所有 value 都存在于磁盘。默认值为 0, vm-max-memory 0
  43. Redis swap 文件分成了很多的 page,一个对象可以保存在多个 page 上面,但一个 page 上不能被多个对象共享,vm-page-size 是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page 大小最好设置为 32 或者 64bytes;如果存储很大大对象,则可以使用更大的 page,如果不确定,就使用默认值 vm-page-size 32
  44. 设置 swap 文件中的 page 数量,由于页表(一种表示页面空闲或使用的 bitmap)是在放在内存中的,在磁盘上每 8 个 pages 将消耗 1byte 的内存。vm-pages 134217728
  45. 设置访问 swap 文件的线程数,最好不要超过机器的核数,如果设置为 0,那么所有对 swap 文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为 4, vm-max-threads 4
  46. 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启 glueoutputbuf yes
  47. 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法, hash-max-zipmap-entries 64, hash-max-zipmap-value 512
  48. 指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍)activerehashing yes
  49. 指定包含其它的配置文件,可以在同一主机上多个 Redis 实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件 include /path/to/local.conf
  50. lua脚本的最大运行时间是需要被严格限制的,要注意单位是毫秒 lua-time-limit 5000。如果此值设置为0或负数,则既不会有报错也不会有时间限制

数据结构

Redis 中的 value 支持五种数据类型: strings, lists, sets, sorted sets, hashes。关于 key 的设计,有几点需要注意:

  1. key 不要太长,尽量不要超过 1024 字节,这不仅消耗内存,而且会降低查找的效率;
  2. key 也不要太短,否则可读性会降低;
  3. 在一个项目中,key 最好使用统一的命名模式,例如 user:10000:passwd

另外本文只是一个基本的指南,不会涵盖太多的命令,具体请参考 Redis 的官方文档。

Strings

有人说,如果只使用 redis 中的字符串类型,且不使用 redis 的持久化功能,那么,redis 就和 memcache 非常非常的像了。我们可以做一些基本的尝试:

127.0.0.1:6379> set visitcount "2"
OK
127.0.0.1:6379> get visitcount
"2"
127.0.0.1:6379> incr visitcount
(integer) 3
127.0.0.1:6379> get visitcount
"3"

在遇到数值操作的时候,redis 会将字符串类型转换成数值,但是如果不是数值会怎么样呢?我们来试试看:

127.0.0.1:6379> set test "test"
OK
127.0.0.1:6379> get test
"test"
127.0.0.1:6379> incr test
(error) ERR value is not an integer or out of range

Redis 中的数值操作指令有一个很好的特性是原子性,很多网站都利用 redis 的这个特性来做技术统计

Lists

Redis 中的 list 的底层实现不是数组而是链表,这就使得在头尾插入新元素的复杂度是常数级别的,但定位元素的时候如果 list 的大小比较大的话就会很耗时。lists 的常用操作包括LPUSH、RPUSH、LRANGE 等。我们可以用 LPUSH 在 lists 的左侧插入一个新元素,用 RPUSH 在 lists 的右侧插入一个新元素,用 LRANGE 命令从 lists 中指定一个范围来提取元素。简单来试一下:

127.0.0.1:6379> lpush onelist 1
(integer) 1
127.0.0.1:6379> lpush onelist 2
(integer) 2
127.0.0.1:6379> rpush onelist 0
(integer) 3
127.0.0.1:6379> rpush onelist 5
(integer) 4
127.0.0.1:6379> lrange onelist 0 -1
1) "2"
2) "1"
3) "0"
4) "5"

lists 的应用相当广泛,随便举几个例子:

  1. 我们可以利用 lists 来实现一个消息队列,而且可以确保先后顺序,不必像 MySQL 那样还需要通过 ORDER BY 来进行排序
  2. 利用 LRANGE 还可以很方便的实现分页的功能
  3. 在博客系统中,每片博文的评论也可以存入一个单独的 list 中

Sets

Redis 中的集合是无序集合,基本的操作对应于集合的操作,比方说添加、删除、交并差集等等,例如:

127.0.0.1:6379> sadd oneset 1
(integer) 1
127.0.0.1:6379> sadd oneset 2
(integer) 1
127.0.0.1:6379> smembers oneset
1) "1"
2) "2"
127.0.0.1:6379> sismember oneset 1
(integer) 1
127.0.0.1:6379> sismember oneset 2
(integer) 1
127.0.0.1:6379> sismember oneset 3
(integer) 0
127.0.0.1:6379> sadd twoset 1
(integer) 1
127.0.0.1:6379> sadd twoset 3
(integer) 1
127.0.0.1:6379> sunion oneset twoset
1) "1"
2) "2"
3) "3"

集合的常见应用场景也很多,比方说文章的标签;群聊中的成员等等。

Sorted Sets

顾名思义,就是把无序的集合弄有序了,每个元素会关联一个分数 score,也就是排序的依据。因为关于有序集合的相关操作都是以 z 开头的,所以通常我们把有序集合称为 zsets。还是来看看具体的例子:

127.0.0.1:6379> zadd onezset 1 wdxtub.com
(integer) 1
127.0.0.1:6379> zadd onezset 2 wdxtub.com/about
(integer) 1
127.0.0.1:6379> zadd onezset 0 wdxtub.com/life
(integer) 1
127.0.0.1:6379> zrange onezset 0 -1
1) "wdxtub.com/life"
2) "wdxtub.com"
3) "wdxtub.com/about"
127.0.0.1:6379> zrange onezset 0 -1 withscores
1) "wdxtub.com/life"
2) "0"
3) "wdxtub.com"
4) "1"
5) "wdxtub.com/about"
6) "2"

Hashes

哈希是 Redis 2.0 之后才增加支持的数据结构,简单来说就是一个字典,直接看具体例子就很好懂了:

127.0.0.1:6379> HMSET user:dawang username wdxtub password wdxtub.com age 26
OK
127.0.0.1:6379> HGETALL user:dawang
1) "username"
2) "wdxtub"
3) "password"
4) "wdxtub.com"
5) "age"
6) "26"
127.0.0.1:6379> HSET user:dawang age 36
(integer) 0
127.0.0.1:6379> HGETALL user:dawang
1) "username"
2) "wdxtub"
3) "password"
4) "wdxtub.com"
5) "age"
6) "36"

HyperLogLog

Redis 2.8.9 版本中添加了这个新的结构,命名也很有趣,走 ABB 的套路,比如范冰冰高圆圆李思思就是这么个意思。这个结构是用来做基数统计的,基数统计是什么,来看两个例子:1) 数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为 5。2) 一个网站有很多访客记录,因为每个人不一定只访问一次,如果我想知道独立访客的人数的话,就需要计算一个基数,这时候就可以用 HyperLogLog。好处在于即使数据量非常大,计算所需的空间是小而固定的。每个 HyperLogLog 的键只占用 12KB 的内存,但是可以计算 $2^64$ 个不同的基数。具体怎么实现的我还没有看源码,但估计跟 bloomfilter 的思路是一样的。简单举个例子:

127.0.0.1:6379> PFADD wdxtub life
(integer) 1
127.0.0.1:6379> PFADD wdxtub about
(integer) 1
127.0.0.1:6379> PFADD wdxtub progress
(integer) 1
127.0.0.1:6379> PFADD wdxtub life
(integer) 0
127.0.0.1:6379> PFCOUNT wdxtub
(integer) 3

因为内部设计的算法,会尽量避免出现碰撞,所以在例子中大概不会出现统计不准的情况,不过在数据量变大之后,统计数值就不再是准确值。

持久化

虽然是内存数据库,一般来说为了保险起见,还是会有一些持久化的机制,Redis 采用了其中两种方式,一是 RDB(Redis DataBase),也就是存数据,另一种是 AOF(Append Only File),也就是存操作。当然,即使是 Redis 本身提供的,我们也可以选择用还是不用,如果两种都不用的化,Redis 就和 memcache 差不多了。

具体的命令也很简单,直接 SAVE 即可,会在安装目录中创建 dump.rdb 文件。恢复数据时,只需要将备份文件移动到 redis 安装目录并启动 redis 即可,具体目录在哪里可以通过 CONFIG GET dir 来查看,比方说在我的机器上:

127.0.0.1:6379> CONFIG GET dir
1) "dir"
2) "/usr/local/var/db/redis"

如果用 BGSAVE 的话,就是在后台进行备份,不会阻塞进程。

RDB

本段内容来自 Linux大棚版redis入门教程

RDB 方式,是将 redis 某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。

Redis 在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总是完整可用的。

对于 RDB 方式,redis 会单独创建(fork)一个子进程来进行持久化,而主进程是不会进行任何IO操作的,这样就确保了redis极高的性能。

如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。

虽然 RDB 有不少优点,但它的缺点也是不容忽视的。如果你对数据的完整性非常敏感,那么 RDB 方式就不太适合你,因为即使你每 5 分钟都持久化一次,当 redis 故障时,仍然会有近 5 分钟的数据丢失。所以,redis 还提供了另一种持久化方式,那就是 AOF。

AOF

本段内容来自 Linux大棚版redis入门教程

AOF,英文是 Append Only File,即只允许追加不允许改写的文件。如前面介绍的,AOF 方式是将执行过的写指令记录下来,在数据恢复时按照从前到后的顺序再将指令都执行一遍,就这么简单。

我们通过配置 redis.conf 中的 appendonly yes 就可以打开 AOF 功能。如果有写操作(如SET等),redis 就会被追加到 AOF 文件的末尾。

默认的 AOF 持久化策略是每秒钟 fsync 一次(fsync 是指把缓存中的写指令记录到磁盘中),因为在这种情况下,redis 仍然可以保持很好的处理性能,即使 redis 故障,也只会丢失最近 1 秒钟的数据。

如果在追加日志时,恰好遇到磁盘空间满、inode 满或断电等情况导致日志写入不完整,也没有关系,redis 提供了 redis-check-aof 工具,可以用来进行日志修复。

因为采用了追加方式,如果不做任何处理的话,AOF 文件会变得越来越大,为此,redis 提供了 AOF 文件重写(rewrite)机制,即当 AOF 文件的大小超过所设定的阈值时,redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集。举个例子或许更形象,假如我们调用了 100 次INCR指令,在 AOF 文件中就要存储 100 条指令,但这明显是很低效的,完全可以把这 100 条指令合并成一条 SET 指令,这就是重写机制的原理。

在进行 AOF 重写时,仍然是采用先写临时文件,全部完成后再替换的流程,所以断电、磁盘满等问题都不会影响 AOF 文件的可用性,这点大家可以放心。

AOF方式的另一个好处,我们通过一个“场景再现”来说明。某同学在操作 redis 时,不小心执行了 FLUSHALL,导致 redis 内存中的数据全部被清空了,这是很悲剧的事情。不过这也不是世界末日,只要 redis 配置了 AOF 持久化方式,且 AOF 文件还没有被重写(rewrite),我们就可以用最快的速度暂停 redis 并编辑 AOF 文件,将最后一行的 FLUSHALL 命令删除,然后重启 redis,就可以恢复 redis 的所有数据到 FLUSHALL 之前的状态了。是不是很神奇,这就是 AOF 持久化方式的好处之一。但是如果 AOF 文件已经被重写了,那就无法通过这种方法来恢复数据了。

虽然优点多多,但 AOF 方式也同样存在缺陷,比如在同样数据规模的情况下,AOF 文件要比 RDB 文件的体积大。而且,AOF 方式的恢复速度也要慢于 RDB 方式。

如果你直接执行 BGREWRITEAOF 命令,那么 redis 会生成一个全新的 AOF 文件,其中便包括了可以恢复现有数据的最少的命令集。

如果运气比较差,AOF 文件出现了被写坏的情况,也不必过分担忧,redis 并不会贸然加载这个有问题的 AOF 文件,而是报错退出。这时可以通过以下步骤来修复出错的文件:

  1. 备份被写坏的 AOF 文件
  2. 运行 redis-check-aof –fix 进行修复
  3. diff -u 来看下两个文件的差异,确认问题点
  4. 重启 redis,加载修复后的 AOF 文件

AOF 重写的内部运行原理,我们有必要了解一下。在重写即将开始之际,redis 会创建(fork)一个“重写子进程”,这个子进程会首先读取现有的 AOF 文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。

与此同时,主工作进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的 AOF 文件中,这样做是保证原有的 AOF 文件的可用性,避免在重写过程中出现意外。

当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新 AOF 文件中。

当追加结束后,redis 就会用新 AOF 文件来代替旧 AOF 文件,之后再有新的写指令,就都会追加到新的 AOF 文件中了。

我们应该选择RDB还是AOF,官方的建议是两个同时使用。这样可以提供更可靠的持久化方案。

增强功能

主从同步

本段内容大部分来自 Linux大棚版redis入门教程

像 MySQL 一样,redis 是支持主从同步的,而且也支持一主多从以及多级从结构。主从结构,一是为了纯粹的冗余备份,二是为了提升读性能,比如很消耗性能的 SORT 就可以由从服务器来承担。在具体的实践中,可能还需要考虑到具体的法律法规原因,单纯的主从结构没有办法应对多机房跨国可能带来的数据存储问题,这里需要特别注意一下

redis 的主从同步是异步进行的,这意味着主从同步不会影响主逻辑,也不会降低 redis 的处理性能。主从架构中,可以考虑关闭主服务器的数据持久化功能,只让从服务器进行持久化,这样可以提高主服务器的处理性能。

在主从架构中,从服务器通常被设置为只读模式,这样可以避免从服务器的数据被误修改。但是从服务器仍然可以接受 CONFIG 等指令,所以还是不应该将从服务器直接暴露到不安全的网络环境中。如果必须如此,那可以考虑给重要指令进行重命名,来避免命令被外人误执行。

具体的同步原理也值得了解一下:

从服务器会向主服务器发出 SYNC 指令,当主服务器接到此命令后,就会调用 BGSAVE 指令来创建一个子进程专门进行数据持久化工作,也就是将主服务器的数据写入 RDB 文件中。在数据持久化期间,主服务器将执行的写指令都缓存在内存中。

在 BGSAVE 指令执行完成后,主服务器会将持久化好的 RDB 文件发送给从服务器,从服务器接到此文件后会将其存储到磁盘上,然后再将其读取到内存中。这个动作完成后,主服务器会将这段时间缓存的写指令再以 redis 协议的格式发送给从服务器。

另外,要说的一点是,即使有多个从服务器同时发来 SYNC 指令,主服务器也只会执行一次BGSAVE,然后把持久化好的 RDB 文件发给多个下游。在 redis2.8 版本之前,如果从服务器与主服务器因某些原因断开连接的话,都会进行一次主从之间的全量的数据同步;而在 2.8 版本之后,redis 支持了效率更高的增量同步策略,这大大降低了连接断开的恢复成本。

主服务器会在内存中维护一个缓冲区,缓冲区中存储着将要发给从服务器的内容。从服务器在与主服务器出现网络瞬断之后,从服务器会尝试再次与主服务器连接,一旦连接成功,从服务器就会把“希望同步的主服务器ID”和“希望请求的数据的偏移位置(replication offset)”发送出去。主服务器接收到这样的同步请求后,首先会验证主服务器ID是否和自己的ID匹配,其次会检查“请求的偏移位置”是否存在于自己的缓冲区中,如果两者都满足的话,主服务器就会向从服务器发送增量内容。

事务处理

本段内容大部分来自 Linux大棚版redis入门教程

数据库原理中很重要的一个概念是『事务』,简单来说就是把一系列动作看做一个整体,如果其中一个出了问题,应该把状态恢复到执行该整体之前的状态。在 Redis 中,MULTI、EXEC、DISCARD、WATCH 这四个指令是事务处理的基础。

  1. MULTI用来组装一个事务;
  2. EXEC用来执行一个事务;
  3. DISCARD用来取消一个事务;
  4. WATCH用来监视一些key,一旦这些key在事务执行之前被改变,则取消事务的执行。

举个例子:

127.0.0.1:6379> set oneid 2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> INCR oneid
QUEUED
127.0.0.1:6379> INCR oneid
QUEUED
127.0.0.1:6379> INCR oneid
QUEUED
127.0.0.1:6379> PING
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 3
2) (integer) 4
3) (integer) 5
4) PONG

在上面的例子中,我们看到了 QUEUED 的字样,这表示我们在用 MULTI 组装事务时,每一个命令都会进入到内存队列中缓存起来,如果出现 QUEUED 则表示我们这个命令成功插入了缓存队列,在将来执行 EXEC 时,这些被 QUEUED 的命令都会被组装成一个事务来执行。

对于事务的执行来说,如果 redis 开启了 AOF 持久化的话,那么一旦事务被成功执行,事务中的命令就会通过 write 命令一次性写到磁盘中去,如果在向磁盘中写的过程中恰好出现断电、硬件故障等问题,那么就可能出现只有部分命令进行了 AOF 持久化,这时 AOF 文件就会出现不完整的情况,这时,我们可以使用 redis-check-aof 工具来修复这一问题,这个工具会将 AOF 文件中不完整的信息移除,确保 AOF 文件完整可用。

然后我们来说说 WATCH 这个指令,它可以帮我们实现类似于“乐观锁”的效果,即CAS(check and set)。WATCH本身的作用是“监视key是否被改动过”,而且支持同时监视多个key,只要还没真正触发事务,WATCH都会尽职尽责的监视,一旦发现某个key被修改了,在执行EXEC时就会返回nil,表示事务无法触发。例如:

127.0.0.1:6379> set name wdxtub
OK
127.0.0.1:6379> watch name
OK
127.0.0.1:6379> set name wdxtub.com
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name wdxtub.com/about
QUEUED
127.0.0.1:6379> get name
QUEUED
127.0.0.1:6379> exec
(nil)

因为 name 在 exec 之前被改变了,可以认为这个值是脏(dirty) 的,于是之后的操作很可能是危险且没有意义的,自然就不会执行了。

发布订阅

Redis 的发布/订阅(pub/sub) 是一种消息通信模型,Redis 客户端可以订阅任意数量的频道,一旦某频道接收到消息时,订阅它的客户端便会收到消息。这里我们需要两个终端来完成这次实验,在终端 1 中做如下操作:

127.0.0.1:6379> SUBSCRIBE wdxtubBlog
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "wdxtubBlog"
3) (integer) 1

然后在终端 2 中向该频道发送消息

127.0.0.1:6379> PUBLISH wdxtubBlog "new post updated!"
(integer) 1
127.0.0.1:6379> PUBLISH wdxtubBlog "visit wdxtub.com for more!"
(integer) 1

然后我们在终端 1 中就可以看到对应的消息:

127.0.0.1:6379> SUBSCRIBE wdxtubBlog
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "wdxtubBlog"
3) (integer) 1
1) "message"
2) "wdxtubBlog"
3) "new post updated!"
1) "message"
2) "wdxtubBlog"
3) "visit wdxtub.com for more!"

性能测试

在配置好 Redis 后,我们可以通过自带的性能测试来查看 Redis 在这台服务器上的表现,据此决定是否应该进行配置和服务调整,例如:

dawang:~ dawang$ redis-benchmark -n 100000
====== PING_INLINE ======
100000 requests completed in 1.26 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.75% <= 1 milliseconds
100.00% <= 1 milliseconds
79302.14 requests per second
====== PING_BULK ======
100000 requests completed in 1.27 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.81% <= 1 milliseconds
100.00% <= 1 milliseconds
78988.94 requests per second
====== SET ======
100000 requests completed in 1.30 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.37% <= 1 milliseconds
99.93% <= 2 milliseconds
99.96% <= 3 milliseconds
100.00% <= 3 milliseconds
76687.12 requests per second
====== GET ======
100000 requests completed in 1.28 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.71% <= 1 milliseconds
100.00% <= 1 milliseconds
78125.00 requests per second
====== INCR ======
100000 requests completed in 1.26 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.68% <= 1 milliseconds
100.00% <= 1 milliseconds
79554.50 requests per second
====== LPUSH ======
100000 requests completed in 1.25 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.64% <= 1 milliseconds
99.99% <= 2 milliseconds
100.00% <= 2 milliseconds
80000.00 requests per second
====== RPUSH ======
100000 requests completed in 1.27 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.51% <= 1 milliseconds
99.96% <= 2 milliseconds
99.99% <= 3 milliseconds
100.00% <= 3 milliseconds
78926.60 requests per second
====== LPOP ======
100000 requests completed in 1.27 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.81% <= 1 milliseconds
100.00% <= 1 milliseconds
78926.60 requests per second
====== RPOP ======
100000 requests completed in 1.27 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.59% <= 1 milliseconds
100.00% <= 1 milliseconds
78431.38 requests per second
====== SADD ======
100000 requests completed in 1.25 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.87% <= 1 milliseconds
100.00% <= 1 milliseconds
80000.00 requests per second
====== SPOP ======
100000 requests completed in 1.25 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.81% <= 1 milliseconds
100.00% <= 1 milliseconds
79744.82 requests per second
====== LPUSH (needed to benchmark LRANGE) ======
100000 requests completed in 1.27 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.49% <= 1 milliseconds
100.00% <= 1 milliseconds
78492.93 requests per second
====== LRANGE_100 (first 100 elements) ======
100000 requests completed in 5.58 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.96% <= 1 milliseconds
96.58% <= 2 milliseconds
99.50% <= 3 milliseconds
99.79% <= 4 milliseconds
99.89% <= 5 milliseconds
99.93% <= 6 milliseconds
99.95% <= 7 milliseconds
99.97% <= 8 milliseconds
99.97% <= 9 milliseconds
99.98% <= 10 milliseconds
99.98% <= 29 milliseconds
99.99% <= 30 milliseconds
100.00% <= 30 milliseconds
17927.57 requests per second
====== LRANGE_300 (first 300 elements) ======
100000 requests completed in 10.84 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.01% <= 1 milliseconds
0.11% <= 2 milliseconds
89.47% <= 3 milliseconds
99.59% <= 4 milliseconds
99.95% <= 5 milliseconds
100.00% <= 5 milliseconds
9222.54 requests per second
====== LRANGE_500 (first 450 elements) ======
100000 requests completed in 15.23 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.01% <= 1 milliseconds
0.07% <= 2 milliseconds
1.48% <= 3 milliseconds
78.19% <= 4 milliseconds
98.97% <= 5 milliseconds
99.76% <= 6 milliseconds
99.89% <= 7 milliseconds
99.93% <= 8 milliseconds
99.96% <= 9 milliseconds
99.97% <= 10 milliseconds
99.98% <= 11 milliseconds
99.99% <= 12 milliseconds
100.00% <= 13 milliseconds
100.00% <= 14 milliseconds
100.00% <= 15 milliseconds
100.00% <= 15 milliseconds
6564.26 requests per second
====== LRANGE_600 (first 600 elements) ======
100000 requests completed in 19.84 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.00% <= 1 milliseconds
0.00% <= 2 milliseconds
0.07% <= 3 milliseconds
0.77% <= 4 milliseconds
68.46% <= 5 milliseconds
98.20% <= 6 milliseconds
99.64% <= 7 milliseconds
99.85% <= 8 milliseconds
99.96% <= 9 milliseconds
99.99% <= 10 milliseconds
100.00% <= 10 milliseconds
5039.31 requests per second
====== MSET (10 keys) ======
100000 requests completed in 1.71 seconds
50 parallel clients
3 bytes payload
keep alive: 1
83.56% <= 1 milliseconds
100.00% <= 2 milliseconds
100.00% <= 2 milliseconds
58343.06 requests per second

测试内容还是不少的,可以根据这些数据来进行优化相关工作。

连接与管道

Redis 通过监听一个 TCP 端口或者 Unix socket 的方式来接收来自客户端的连接,当一个连接建立后,Redis 内部会进行以下一些操作:

  1. 客户端 socket 会被设置为非阻塞模式,因为 Redis 在网络事件处理上采用的是非阻塞多路复用模型。
  2. 为这个 socket 设置 TCP_NODELAY 属性,禁用 Nagle 算法。Nagle 算法实际就是当需要发送的数据攒到一定程度时才真正进行发包,通过这种方式来减少 header 数据占比的问题。不过在高互动的环境下是不必要的,一般来说,在客户端/服务器模型中会禁用。更多信息可在参考链接中查看。
  3. 创建一个可读的文件事件用于监听这个客户端 socket 的数据发送

Redis 管道技术可以在服务端未响应时,客户端可以继续向服务端发送请求,并最终一次性读取所有服务端的响应。管道技术最显著的优势是提高了 redis 服务的性能。

分区

本段内容主要来自 Redis 分区

分区是分割数据到多个 Redis 实例的处理过程,因此每个实例只保存 key 的一个子集。分区的优势有很多,尤其是在大数据当道的今天,更需要利用合理的分区机制来完成更加复杂的工作。

  • 通过利用多台计算机内存的和值,允许我们构造更大的数据库
  • 通过多核和多台计算机,允许我们扩展计算能力
  • 通过多台计算机和网络适配器,允许我们扩展网络带宽

分区实际上把数据进行了隔离,如果原本应该在同一分区的数据被放在了不同分区,或者原本没有太多关系的数据因为新的业务产生了关系,就会遇到一些问题:

  • 涉及多个 key 的操作通常是不被支持的。举例来说,当两个 set 映射到不同的 redis 实例上时,你就不能对这两个 set 执行交集操作
  • 涉及多个 key 的 redis 事务不能使用
  • 当使用分区时,数据处理较为复杂,比如你需要处理多个 rdb/aof 文件,并且从多个实例和主机备份持久化文件
  • 增加或删除容量也比较复杂。redis 集群大多数支持在运行时增加、删除节点的透明数据平衡的能力,但是类似于客户端分区、代理等其他系统则不支持这项特性。然而,一种叫做 presharding 的技术对此是有帮助的。

Redis 有两种类型分区。 假设有 4 个 Redis实例 R0,R1,R2,R3,和类似 user:1,user:2 这样的表示用户的多个 key,对既定的 key 有多种不同方式来选择这个 key 存放在哪个实例中。也就是说,有不同的系统来映射某个 key 到某个 Redis 服务。

范围分区

最简单的分区方式是按范围分区,就是映射一定范围的对象到特定的 Redis 实例。比如,ID 从 0 到 10000 的用户会保存到实例 R0,ID 从 10001 到 20000 的用户会保存到 R1,以此类推。这种方式的不足之处是要有一个区间范围到实例的映射表,同时还需要各种对象的映射表,通常对 Redis 来说并非是好的方法。

哈希分区

另外一种分区方法是 hash 分区。这对任何 key 都适用,也无需是 object_name: 这种形式,只需要确定统一的哈希函数,然后通过取模确定应该保存在哪个分区即可。

实战应用

最近需要自己设计系统架构,一个核心思路是利用已有机制降低设计复杂度,这样可以避免很多无谓的『坑』,尽量少自己去造轮子,而是用经过实践检验的工具。

而且一开始就要考虑得稍微长远一些,从扩展性到 key 设计都是,还要善待 redis 中的数据,不用了就自己清理,而不是过分依赖 redis 本身。简单的原则如下

  • 由于Redis单线程(严格意义上不是单线程,但认为对request的处理是单线程的)的模型,对大数据结构的使用一定要慎重
  • 每个实例的内存容量也应该有一定的限制,不然故障恢复的时候会很痛苦
  • 集群的解决方案基本目前公认就是 codis 了,不过看目前的数据量,走单点和 1 个备份应该是行得通的,具体要看看压力测试的结果
  • 业务中最重要的是 KV, SQL 和事务,不要为了所谓的『架构合理』增加太多理解难度

Key 过期机制

现在项目中的每条记录都有一定的存活时间,原本的机制是在 MySQL 的每条记录中增加一个 status 字段,定时跑一个程序根据时间戳来进行检查。但是怎么想都感觉有点折腾,难道不能依赖数据库的已有机制吗?

能的!使用 Key 的过期机制,每个 Key 过期之后自动失效,就不用增加额外复杂度了。如果需要刷新,直接对该 key 执行 EXPIRE 操作,即可。

即使如此,我们也需要善待数据,注意清理,尽量使用小对象,不能过分依赖这个机制解决所有问题。

Google 的应用

来自Codis作者黄东旭细说分布式Redis架构设计和踩过的那些坑们

Google在他们的广告业务中遇到这个问题,既需要高性能,又需要分布式事务,还必须保证一致性:),Google在此之前是通过一个大规模的MySQL集群通过sharding苦苦支撑,这个架构的可运维/扩展性实在太差。这要是在一般公司,估计也就忍了,但是Google可不是一般公司,用原子钟搞定Spanner,然后再Spanner上构建了SQL查询层F1。我在第一次看到这个系统的时候,感觉简直惊艳,应该是第一个可以真正称为NewSQL的公开设计的系统。所以,BigTable(KV)+F1(SQL)+Spanner(高性能分布式事务支持),同时Spanner还有一个非常重要的特性是跨数据中心的复制和一致性保证(通过Paxos实现),多数据中心,刚好补全了整个Google的基础设施的数据库栈,使得Google对于几乎任何类型的业务系统开发都非常方便。我想,这就是未来的方向吧,一个可扩展的KV数据库(作为缓存和简单对象存储),一个高性能支持分布式事务和SQL查询接口的分布式关系型数据库,提供表支持。

总结

相比于在学校必须手写自己的缓存,使用 Redis(或是 memcache)简直太爽了,工作一段时间了,越发觉得技术的门槛其实越来越低,如何打造高效团队,如何把架构设计得更加合理,才是真正体现差距的地方。

参考链接

捧个钱场?