Redis 是 C 实现的基于内存并可持久的键值对数据库,在分布式服务中常常被用作缓存。除此之外还可以利用其特点做许多有趣的应用,所以我们不仅需要会用,更需要理解其工作机制。
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 的端口号。
除了前面使用过的 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
基本命令有 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"
具体解释
daemonize no
/var/run/redis.pid
文件,可以通过 pidfile
指定 pidfile /var/run/redis.pid
port 6379
bind 127.0.0.1
timeout 300
loglevel verbose
logfile stdout
SELECT <dbid>
命令在连接上指定数据库 id, databases 16
save <seconds> <changes>
Redis默认配置文件中提供了三个条件: save 900 1
, save 300 10
, save 60 10000
, 分别表示 900 秒(15 分钟)内有 1 个更改,300 秒(5 分钟)内有 10 个更改以及 60 秒内有 10000 个更改。 rdbcompression yes
dbfilename dump.rdb
dir ./
slaveof <masterip> <masterport>
masterauth <master-password>
AUTH <password>
命令提供密码,默认关闭 requirepass foobared
maxclients 128
maxmemory <bytes>
appendonly no
appendfilename appendonly.aof
appendfsync everysec
vm-enabled no
/tmp/redis.swap
,不可多个 Redis 实例共享 vm-swap-file /tmp/redis.swap
vm-max-memory 0
vm-page-size 32
vm-pages 134217728
vm-max-threads 4
glueoutputbuf yes
hash-max-zipmap-entries 64
, hash-max-zipmap-value 512
activerehashing yes
include /path/to/local.conf
Redis 中的 value 支持五种数据类型: strings, lists, sets, sorted sets, hashes。关于 key 的设计,有几点需要注意:
user:10000:passwd
另外本文只是一个基本的指南,不会涵盖太多的命令,具体请参考 Redis 的官方文档。
有人说,如果只使用 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 的这个特性来做技术统计
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 的应用相当广泛,随便举几个例子:
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"
集合的常见应用场景也很多,比方说文章的标签;群聊中的成员等等。
顾名思义,就是把无序的集合弄有序了,每个元素会关联一个分数 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"
哈希是 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"
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
的话,就是在后台进行备份,不会阻塞进程。
本段内容来自 Linux大棚版redis入门教程
RDB 方式,是将 redis 某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。
Redis 在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总是完整可用的。
对于 RDB 方式,redis 会单独创建(fork)一个子进程来进行持久化,而主进程是不会进行任何IO操作的,这样就确保了redis极高的性能。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。
虽然 RDB 有不少优点,但它的缺点也是不容忽视的。如果你对数据的完整性非常敏感,那么 RDB 方式就不太适合你,因为即使你每 5 分钟都持久化一次,当 redis 故障时,仍然会有近 5 分钟的数据丢失。所以,redis 还提供了另一种持久化方式,那就是 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 文件,而是报错退出。这时可以通过以下步骤来修复出错的文件:
redis-check-aof –fix
进行修复 diff -u
来看下两个文件的差异,确认问题点 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 这四个指令是事务处理的基础。
举个例子:
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 内部会进行以下一些操作:
TCP_NODELAY
属性,禁用 Nagle 算法。Nagle 算法实际就是当需要发送的数据攒到一定程度时才真正进行发包,通过这种方式来减少 header 数据占比的问题。不过在高互动的环境下是不必要的,一般来说,在客户端/服务器模型中会禁用。更多信息可在参考链接中查看。 Redis 管道技术可以在服务端未响应时,客户端可以继续向服务端发送请求,并最终一次性读取所有服务端的响应。管道技术最显著的优势是提高了 redis 服务的性能。
本段内容主要来自 Redis 分区
分区是分割数据到多个 Redis 实例的处理过程,因此每个实例只保存 key 的一个子集。分区的优势有很多,尤其是在大数据当道的今天,更需要利用合理的分区机制来完成更加复杂的工作。
分区实际上把数据进行了隔离,如果原本应该在同一分区的数据被放在了不同分区,或者原本没有太多关系的数据因为新的业务产生了关系,就会遇到一些问题:
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:
这种形式,只需要确定统一的哈希函数,然后通过取模确定应该保存在哪个分区即可。