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" |
具体解释
- Redis 默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize no
- 当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入
/var/run/redis.pid
文件,可以通过pidfile
指定pidfile /var/run/redis.pid
- 指定 Redis 监听端口,默认端口为 6379
port 6379
- 绑定的主机地址
bind 127.0.0.1
- 当客户端闲置多长时间后关闭连接,如果指定为 0,表示关闭该功能
timeout 300
- 指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为 verbose,
loglevel verbose
- 日志记录方式,默认为标准输出,如果配置 Redis 为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给 /dev/null,
logfile stdout
- 设置数据库的数量,默认数据库为 0,可以使用
SELECT
命令在连接上指定数据库 id,databases 16
- 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
save
Redis默认配置文件中提供了三个条件:save 900 1
,save 300 10
,save 60 10000
, 分别表示 900 秒(15 分钟)内有 1 个更改,300 秒(5 分钟)内有 10 个更改以及 60 秒内有 10000 个更改。 - 指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大
rdbcompression yes
- 指定本地数据库文件名,默认值为 dump.rdb,
dbfilename dump.rdb
- 指定本地数据库存放目录
dir ./
- 设置当本机为 slave 服务时,设置 master 服务的IP地址及端口,在 Redis 启动时,它会自动从 master 进行数据同步
slaveof
- 当 master 服务设置了密码保护时,slave 服务连接 master 的密码
masterauth
- 设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过
AUTH
命令提供密码,默认关闭requirepass foobared
- 设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息
maxclients 128
- 指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key,当此方法处理后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区,
maxmemory
- 指定是否在每次更新操作后进行日志记录,Redis 在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis 本身同步数据文件是按上面 save 条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为 no,
appendonly no
- 指定更新日志文件名,默认为 appendonly.aof,
appendfilename appendonly.aof
- 指定更新日志条件,共有3个可选值:
- no:表示等操作系统进行数据缓存同步到磁盘(快)
- always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
- everysec:表示每秒同步一次(折衷,默认值)
appendfsync everysec
- 指定是否启用虚拟内存机制,默认值为 no,简单的介绍一下,VM 机制将数据分页存放,由Redi s将访问量较少的页即冷数据 swap 到磁盘上,访问多的页面由磁盘自动换出到内存中
vm-enabled no
- 虚拟内存文件路径,默认值为
/tmp/redis.swap
,不可多个 Redis 实例共享vm-swap-file /tmp/redis.swap
- 将所有大于 vm-max-memory 的数据存入虚拟内存,无论 vm-max-memory 设置多小,所有索引数据都是内存存储的(Redis 的索引数据 就是 keys),也就是说,当 vm-max-memory 设置为 0 的时候,其实是所有 value 都存在于磁盘。默认值为 0,
vm-max-memory 0
- Redis swap 文件分成了很多的 page,一个对象可以保存在多个 page 上面,但一个 page 上不能被多个对象共享,vm-page-size 是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page 大小最好设置为 32 或者 64bytes;如果存储很大大对象,则可以使用更大的 page,如果不确定,就使用默认值
vm-page-size 32
- 设置 swap 文件中的 page 数量,由于页表(一种表示页面空闲或使用的 bitmap)是在放在内存中的,在磁盘上每 8 个 pages 将消耗 1byte 的内存。
vm-pages 134217728
- 设置访问 swap 文件的线程数,最好不要超过机器的核数,如果设置为 0,那么所有对 swap 文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为 4,
vm-max-threads 4
- 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
glueoutputbuf yes
- 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法,
hash-max-zipmap-entries 64
,hash-max-zipmap-value 512
- 指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍)
activerehashing yes
- 指定包含其它的配置文件,可以在同一主机上多个 Redis 实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
include /path/to/local.conf
数据结构
Redis 中的 value 支持五种数据类型: strings, lists, sets, sorted sets, hashes。关于 key 的设计,有几点需要注意:
- key 不要太长,尽量不要超过 1024 字节,这不仅消耗内存,而且会降低查找的效率;
- key 也不要太短,太短的话,key 的可读性会降低;
- 在一个项目中,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 的应用相当广泛,随便举几个例子:
- 我们可以利用 lists 来实现一个消息队列,而且可以确保先后顺序,不必像 MySQL 那样还需要通过 ORDER BY 来进行排序
- 利用 LRANGE 还可以很方便的实现分页的功能
- 在博客系统中,每片博文的评论也可以存入一个单独的 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 的内存,但是可以计算 [Math Processing Error]264 个不同的基数。具体怎么实现的我还没有看源码,但估计跟 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 文件,而是报错退出。这时可以通过以下步骤来修复出错的文件:
- 备份被写坏的 AOF 文件
- 运行
redis-check-aof –fix
进行修复 - 用
diff -u
来看下两个文件的差异,确认问题点 - 重启 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 这四个指令是事务处理的基础。
- MULTI用来组装一个事务;
- EXEC用来执行一个事务;
- DISCARD用来取消一个事务;
- 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 内部会进行以下一些操作:
- 客户端 socket 会被设置为非阻塞模式,因为 Redis 在网络事件处理上采用的是非阻塞多路复用模型。
- 为这个 socket 设置
TCP_NODELAY
属性,禁用 Nagle 算法。Nagle 算法实际就是当需要发送的数据攒到一定程度时才真正进行发包,通过这种方式来减少 header 数据占比的问题。不过在高互动的环境下是不必要的,一般来说,在客户端/服务器模型中会禁用。更多信息可在参考链接中查看。 - 创建一个可读的文件事件用于监听这个客户端 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:
这种形式,只需要确定统一的哈希函数,然后通过取模确定应该保存在哪个分区即可。
总结
相比于在学校必须手写自己的缓存,使用 Redis(或是 memcache)简直太爽了,工作一段时间了,越发觉得技术的门槛其实越来越低,如何打造高效团队,如何把架构设计得更加合理,才是真正体现差距的地方。
参考链接
- Redis 官方网站
- Redis 命令大全
- Linux大棚版redis入门教程
- Redis 教程
- Redis 作者博客
- Redis 作者 Github 主页
- Nagle 算法