Redis 是一个高性能的key-value数据库, 支持主从同步, 完全实现了发布/订阅机制, 因此可以用于聊天室等场景. 主要表现于多个浏览器之间的信息同步和实时更新.
和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。数据可以从master向任意数量的slave上同步,slave可以是关联其他slave的master。这使得Redis可执行单层树复制。存盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得slave在任何地方同步树时,可订阅一个频道并接收master完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助.
本文的内容参考博文超强、超详细Redis数据库入门教程.
环境准备
可以从Redis官网下载最新的redis, 然后安装即可.
make install
cd utils && sudo ./install_server.sh
安装过程中, 会有一些端口, 配置文件路径等的设置, 这里选择默认端口6379. 然后执行redis-server即可在默认端口上启动成功.
42068:M 16 May 09:57:11.403 # Server started, Redis version 3.0.1
42068:M 16 May 09:57:11.404 * The server is now ready to accept connections on port 6379
redis-cli
执行redis-cli即可启动命令行工具, 也可自行指定host及port.
redis-cli -h localhost -p 6379
提供了非常丰富的命令对redis的数据进行操作, 数据都是以key-value的形式存储的, 因此get/set操作是使用最常见的.
使用help+命令即可查看命令帮助.
对string的操作
127.0.0.1:6379> exists hello // 查看key是否存在
(integer) 1
127.0.0.1:6379> set hello world // 设置hello-world
OK
127.0.0.1:6379> get hello // 获取hello的值
"world"
127.0.0.1:6379> type hello // 值类型
string
127.0.0.1:6379[2]> substr hello 1 2 // 获取substring
"or"
127.0.0.1:6379[2]> append hello ! // 字符串连接
(integer) 6
127.0.0.1:6379[2]> get hello
"world!"
127.0.0.1:6379> set haha heihei
OK
127.0.0.1:6379> keys h* // 返回满足条件的所有key
1) "hello"
3) "haha"
127.0.0.1:6379> randomkey // 随机返回一个key
"haha"
127.0.0.1:6379> randomkey
"hello"
127.0.0.1:6379> rename hello hehe // 重命名key
OK
127.0.0.1:6379> get haha
"heihei"
127.0.0.1:6379> expire haha 10
(integer) 1 // 设置haha的活动时间(s)
127.0.0.1:6379> ttl haha
(integer) 7 // 获取haha的活动时间
127.0.0.1:6379> get haha
(nil) // expire 时间到期后,该haha-heihei会删除
127.0.0.1:6379[2]> del hello
(integer) 0
对list的操作
redis中的list在底层实现上是链表. 因此, 无论list的空间复杂度是多少, 其时间复杂度是常数级别的. 即使用lpush在10个元素的list头部插入新元素, 和在上万个元素的lists头部插入新元素的速度相当. 但lists中的元素定位会比较慢.
常见操作有lpush, rpush, lrange等.
127.0.0.1:6379[2]> lpush list1 1 // 头部插入数据
(integer) 1
127.0.0.1:6379[2]> lpush list1 2
(integer) 2
127.0.0.1:6379[2]> rpush list1 0 // 尾部插入数据
(integer) 3
127.0.0.1:6379[2]> lrange list1 0 1 // 列出编号0~1的元素
1) "2"
2) "1"
127.0.0.1:6379[2]> lrange list1 0 -1 // 列出编号0到倒数第一个的元素
1) "2"
2) "1"
3) "0"
127.0.0.1:6379[2]> llen list1 // lists长度
127.0.0.1:6379[2]> lindex list1 1 // 根据index获取元素
"1"
127.0.0.1:6379[2]> ltrim list1 1 2 // 截取,仅保留编号1~2之间的元素
OK
127.0.0.1:6379[2]> lrange list1 0 10
1) "1"
2) "0"
(integer) 3
127.0.0.1:6379[2]> lset list1 1 haha // 给1位置的元素赋值为haha
OK
127.0.0.1:6379[2]> lset list1 2 haha // 赋值,不能超出lists范围
(error) ERR index out of range
127.0.0.1:6379[2]> lrange list1 0 10
1) "1"
2) "haha"
127.0.0.1:6379[2]> rpush list1 haha
(integer) 3
127.0.0.1:6379[2]> rpush list1 haha
(integer) 4
127.0.0.1:6379[2]> lrange list1 0 10
1) "1"
2) "haha"
3) "haha"
4) "haha"
127.0.0.1:6379[2]> lrem list1 2 haha 删除2个值为haha的元素
(integer) 2
127.0.0.1:6379[2]> lrange list1 0 10
1) "1"
2) "haha"
127.0.0.1:6379[2]> lpop list1
"1"
127.0.0.1:6379[2]> lrange list1 0 10
1) "haha"
可以使用list来实现一个消息队列(如博客的评论内容), 确保先后顺序, 而不必像mysql那样使用order by来排序.
使用lrange还可以很方便地实现分页功能.
对set的操作
redis的set是无序的集合, 其中的元素没有先后顺序.
常见操作如下:
127.0.0.1:6379[2]> sadd set1 0 // 像set1中添加元素0
(integer) 1
127.0.0.1:6379[2]> sadd set1 1
(integer) 1
127.0.0.1:6379[2]> smembers set1 // 返回set1中的所有元素
1) "0"
2) "1"
127.0.0.1:6379[2]> scard set1 // 返回set的基数
(integer) 2
127.0.0.1:6379[2]> sismember set1 0 // 测试set1中是否包含元素0
(integer) 1
127.0.0.1:6379[2]> srandmember set1 // 随机返回一个元素
"0"
127.0.0.1:6379[2]> sadd set2 0
(integer) 1
127.0.0.1:6379[2]> sadd set2 2
(integer) 1
127.0.0.1:6379[2]> sinter set1 set2 // 求交集
1) "0"
127.0.0.1:6379[2]> sinterstore set3 set1 set2 // 保存交集到set3
(integer) 1
127.0.0.1:6379[2]> smembers set3
1) "0"
127.0.0.1:6379[2]> sunion set1 set2 // 求并集
1) "0"
2) "1"
3) "2"
127.0.0.1:6379[2]> sdiff set1 set2 // 求差集
1) "1"
127.0.0.1:6379[2]> sdiff set2 set1
1) "2"
对zset的操作
zset是一种有序集合(sorted set), 其中每个元素都关联一个序号score.
常见操作有zrange, zadd, zrevrange等
127.0.0.1:6379[2]> zadd zset1 1 dianping.com // 添加元素, score为1
(integer) 1
127.0.0.1:6379[2]> zadd zset1 2 baidu.com
(integer) 1
127.0.0.1:6379[2]> zadd zset1 3 qq.com
(integer) 1
127.0.0.1:6379[2]> zcard zset1 // 返回zset的基数
(integer) 3
127.0.0.1:6379[2]> zscore zset1 dianping.com // 返回元素的score
"1"
127.0.0.1:6379[2]> zrank zset1 dianping.com // 返回元素的rank
(integer) 0
127.0.0.1:6379[2]> zrange zset1 0 1 // 选取元素, score
从小到大
1) "dianping.com"
2) "baidu.com"
127.0.0.1:6379[2]> zrevrange zset1 0 1 // score从大到小
1) "qq.com"
2) "baidu.com"
127.0.0.1:6379[2]> zrem zset1 qq.com // 删除元素
(integer) 0
127.0.0.1:6379[2]> zincrby zset1 5 taobao.com // 如果元素不存在, 则添加该元素, 设置score为5
"5"
127.0.0.1:6379[2]> zcard zset1
(integer) 3
127.0.0.1:6379[2]> zincrby zset1 10 dianping.com // 如果元素存在, 则其score增加10
"11"
127.0.0.1:6379[2]> zrange zset1 0 10 withscores
1) "taobao.com"
2) "5"
3) "dianping.com"
4) "11"
此外, 还有zrevrank, zrevrange, zrangebyscore, zremrangebyrank, zramrangebyscore, zinterstore/zunionstore等操作.
对hash的操作
hash也是一种非常常见的结构.
127.0.0.1:6379[2]> hset hash1 key1 value1 // 添加元素
(integer) 1
127.0.0.1:6379[2]> hget hash1 key1 // 获取元素的value
"value1"
127.0.0.1:6379[2]> hexists hash1 key1
(integer) 1
127.0.0.1:6379[2]> hset hash1 key2 value2
(integer) 1
127.0.0.1:6379[2]> hlen hash1
(integer) 2
127.0.0.1:6379[2]> hkeys hash1 // 获取hash1的所有key
1) "key1"
2) "key2"
127.0.0.1:6379[2]> hvals hash1 // 获取hash1的所有value
1) "value1"
2) "value2"
127.0.0.1:6379[2]> hmget hash1 key1 key2 // 获取元素
1) "value1"
2) "value2"
127.0.0.1:6379[2]> hgetall hash1 // 获取key/value
1) "key1"
2) "value1"
3) "key2"
4) "value2"
127.0.0.1:6379[2]> hset hash1 key3 10
(integer) 1
127.0.0.1:6379[2]> hincrby hash1 key3 5 // 将key3的value增加15(仅限整数)
(integer) 15
127.0.0.1:6379[2]> hmset hash1 key4 value4 key5 value5 // 批量添加元素
OK
其他操作
dbsize 查看所有key的数目
flushdb 删除当前选择数据库中的所有key
flushall 删除所有数据库中的所有key
save: 将数据同步保存到磁盘
bgsave: 异步保存
lastsave: 上次成功保存到磁盘的Unix时间戳
info: 查询server信息
config: 配置server
slaveof: 改变复制策略设置
publish-subscribe
redis的发布/订阅机制使用非常简便, 如下图:
subscribe chatchannel 订阅该chatchannel频道, 则会实时接收到该频道的消息;
publish chatchannel ‘hello’ 向chatchannel频道发布消息, 所有订阅者都能收到.
这种机制, 可以非常方便地使用在类似于聊天室的场景中.
redis持久化
redis提供了两种持久化的方式,分别是RDB(Redis DataBase)和AOF(Append Only File)。
RDB,简而言之,就是在不同的时间点,将redis存储的数据生成快照并存储到磁盘等介质上;
AOF,则是换了一个角度来实现持久化,那就是将redis执行过的所有写指令记录下来,在下次redis重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
其实RDB和AOF两种方式也可以同时使用,在这种情况下,如果redis重启的话,则会优先采用AOF方式来进行数据恢复,这是因为AOF方式的数据恢复完整度更高。
如果你没有数据持久化的需求,也完全可以关闭RDB和AOF方式,这样的话,redis将变成一个纯内存数据库,就像memcache一样。
RDB
RDB方式,是将redis某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。
redis在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总是完整可用的。
对于RDB方式,redis会单独创建(fork)一个子进程来进行持久化,而主进程是不会进行任何IO操作的,这样就确保了redis极高的性能。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。
虽然RDB有不少优点,但它的缺点也是不容忽视的。如果你对数据的完整性非常敏感,那么RDB方式就不太适合你,因为即使你每5分钟都持久化一次,当redis故障时,仍然会有近5分钟的数据丢失。所以,redis还提供了另一种持久化方式,那就是AOF。
AOF
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重写
AOF重写的内部运行原理,我们有必要了解一下。
在重写即将开始之际,redis会创建(fork)一个“重写子进程”,这个子进程会首先读取现有的AOF文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。
与此同时,主工作进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的AOF文件中,这样做是保证原有的AOF文件的可用性,避免在重写过程中出现意外。
当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中。
当追加结束后,redis就会用新AOF文件来代替旧AOF文件,之后再有新的写指令,就都会追加到新的AOF文件中了。
主从(master-slave)
像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匹配,其次会检查“请求的偏移位置”是否存在于自己的缓冲区中,如果两者都满足的话,主服务器就会向从服务器发送增量内容。
增量同步功能,需要服务器端支持全新的PSYNC指令。这个指令,只有在redis-2.8之后才具有。
其他部分
事物处理, redis配置的部分, 请参考博文超强、超详细Redis数据库入门教程.