Redis 高级实用特性

0.268字数 8727阅读 627

安全性

设置客户端连接后进行任何其他指令前需要使用的密码。

警告:因为redis 速度相当快,所以在一台比较好的服务器下,一个外部的用户可以在一秒钟进
行150K 次的密码尝试,这意味着你需要指定非常非常强大的密码来防止暴力破解。

下面我们做一个实验,说明redis 的安全性是如何实现的。

# requirepass foobared requirepass beijing
我们设置了连接的口令是beijing

那么们启动一个客户端试一下:
[root@localhost redis-2.2.12]# src/redis-cli redis 127.0.0.1:6379> keys * (error) ERR operation not permitted redis 127.0.0.1:6379>

说明权限太小,我们可以在当前的这个窗口中设置口令

`redis 127.0.0.1:6379> auth beijing
OK
redis 127.0.0.1:6379> keys *

  1. "name"
    redis 127.0.0.1:6379>`

我们还可以在连接到服务器期间就指定一个口令,如下:
`[root@localhost redis-2.2.12]# src/redis-cli -a beijing
redis 127.0.0.1:6379> keys *

  1. "name"
    redis 127.0.0.1:6379>`

可以看到我们在连接的时候就可以指定一个口令。

主从复制

redis 主从复制配置和使用都非常简单。通过主从复制可以允许多个slave server 拥有和
master server 相同的数据库副本。

redis 主从复制特点:

  1. master 可以拥有多个slave
  2. 多个slave 可以连接同一个master 外,还可以连接到其他slave
  3. 主从复制不会阻塞master,在同步数据时,master 可以继续处理client 请求
  4. 提高系统的伸缩性

redis 主从复制过程:

当配置好slave 后,slave 与master 建立连接,然后发送sync 命令。无论是第一次连接还是重新连接,master 都会启动一个后台进程,将数据库快照保存到文件中,同时master 主进程会开始收集新的写命令并缓存。后台进程完成写文件后,master 就发送文件给slave,slave将文件保存到硬盘上,再加载到内存中,接着master 就会把缓存的命令转发给slave,后续master 将收到的写命令发送给slave。如果master 同时收到多个slave 发来的同步连接命令,master 只会启动一个进程来写数据库镜像,然后发送给所有的slave。

如何配置

配置slave 服务器很简单,只需要在slave 的配置文件中加入如下配置

slaveof 192.168.1.1 6379 #指定master 的ip 和端口

下面我们做一个实验来演示如何搭建一个主从环境:

# slaveof <masterip> <masterport> slaveof localhost 6379

我们在一台机器上启动主库(端口6379),从库(端口6378)

启动后主库控制台日志如下:
[root@localhost redis-2.2.12]# src/redis-server redis.conf [7064] 09 Aug 20:13:12 * Server started, Redis version 2.2.12 [7064] 09 Aug 20:13:12 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. [7064] 09 Aug 20:13:12 * The server is now ready to accept connections on port 6379 [7064] 09 Aug 20:13:13 - 0 clients connected (0 slaves), 539512 bytes in use [7064] 09 Aug 20:13:18 - 0 clients connected (0 slaves), 539512 bytes in use [7064] 09 Aug 20:13:20 - Accepted 127.0.0.1:37789 [7064] 09 Aug 20:13:20 * Slave ask for synchronization [7064] 09 Aug 20:13:20 * Starting BGSAVE for SYNC [7064] 09 Aug 20:13:20 * Background saving started by pid 7067 [7067] 09 Aug 20:13:20 * DB saved on disk [7064] 09 Aug 20:13:20 * Background saving terminated with success [7064] 09 Aug 20:13:20 * Synchronization with slave succeeded [7064] 09 Aug 20:13:23 - 0 clients connected (1 slaves), 547380 bytes in use

启动后从库控制台日志如下:
[root@localhost redis-2.2.12]# src/redis-server redis.slave [7066] 09 Aug 20:13:20 * Server started, Redis version 2.2.12 [7066] 09 Aug 20:13:20 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. [7066] 09 Aug 20:13:20 * The server is now ready to accept connections on port 6378 [7066] 09 Aug 20:13:20 - 0 clients connected (0 slaves), 539548 bytes in use [7066] 09 Aug 20:13:20 * Connecting to MASTER... [7066] 09 Aug 20:13:20 * MASTER <-> SLAVE sync started: SYNC sent [7066] 09 Aug 20:13:20 * MASTER <-> SLAVE sync: receiving 10 bytes from master [7066] 09 Aug 20:13:20 * MASTER <-> SLAVE sync: Loading DB in memory [7066] 09 Aug 20:13:20 * MASTER <-> SLAVE sync: Finished with success [7068] 09 Aug 20:13:20 * SYNC append only file rewrite performed [7066] 09 Aug 20:13:20 * Background append only file rewriting started by pid 7068 [7066] 09 Aug 20:13:21 * Background append only file rewriting terminated with success [7066] 09 Aug 20:13:21 * Parent diff flushed into the new append log file with success (0 bytes) [7066] 09 Aug 20:13:21 * Append only file successfully rewritten. [7066] 09 Aug 20:13:21 * The new append only file was selected for future appends. [7066] 09 Aug 20:13:25 - 1 clients connected (0 slaves), 547396 bytes in use

我们在主库上设置一对键值对

redis 127.0.0.1:6379> set name HongWan OK redis 127.0.0.1:6379>

在从库上取一下这个键

redis 127.0.0.1:6378> get name "HongWan" redis 127.0.0.1:6378>
说明主从是同步正常的.

那么我们如何判断哪个是主哪个是从呢?我们只需调用info 这个命令就可以得到主从的信息
了,我们在从库上执行info 命令
redis 127.0.0.1:6378> info . . . role:slave master_host:localhost master_port:6379 master_link_status:up master_last_io_seconds_ago:10 master_sync_in_progress:0 db0:keys=1,expires=0 redis 127.0.0.1:6378>
里面有一个角色标识,来判断是主库还是从库,对于本例是一个从库,同时还有一个master_link_status 用于标明主从是否异步,如果此值=up,说明同步正常;如果此值=down,
说明同步异步;
db0:keys=1,expires=0, 用于说明数据库有几个key,以及过期key 的数量。

事务控制

redis 对事务的支持目前还比较简单。redis 只能保证一个client 发起的事务中的命令可以连续的执行,而中间不会插入其他client 的命令。由于redis 是单线程来处理所有client 的请求的所以做到这点是很容易的。一般情况下redis 在接受到一个client 发来的命令后会立即处理并返回处理结果,但是当一个client 在一个连接中发出multi 命令,这个连接会进入一个事务上下文,该连接后续的命令并不是立即执行,而是先放到一个队列中。当从此连接受到exec 命令后,redis 会顺序的执行队列中的所有命令。并将所有命令的运行结果打包到一起返回给client.然后此连接就结束事务上下文。

简单事务控制

下面可以看一个例子
`redis 127.0.0.1:6379> get age
"33"
redis 127.0.0.1:6379> multi
OK
redis 127.0.0.1:6379> set age 10
QUEUED
redis 127.0.0.1:6379> set age 20
QUEUED
redis 127.0.0.1:6379> exec

  1. OK
  2. OK
    redis 127.0.0.1:6379> get age
    "20"
    redis 127.0.0.1:6379>`
    从这个例子我们可以看到2 个set age 命令发出后并没执行而是被放到了队列中。调用exec后2 个命令才被连续的执行,最后返回的是两条命令执行后的结果。

如何取消一个事务

我们可以调用discard 命令来取消一个事务,让事务回滚。接着上面例子
redis 127.0.0.1:6379> get age "20" redis 127.0.0.1:6379> multi OK redis 127.0.0.1:6379> set age 30 QUEUED redis 127.0.0.1:6379> set age 40 QUEUED redis 127.0.0.1:6379> discard OK redis 127.0.0.1:6379> get age "20" redis 127.0.0.1:6379>
可以发现这次2 个set age 命令都没被执行。discard 命令其实就是清空事务的命令队列并退出事务上下文,也就是我们常说的事务回滚。

乐观锁复杂事务控制

在本小节开始前,我们有必要向读者朋友简单介绍一下乐观锁的概念,并举例说明乐观锁是怎么工作的。

乐观锁:大多数是基于数据版本(version)的记录机制实现的。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表添加一个“version”字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1。此时,将提交数据的版本号与数据库表对应记录的当前版本号进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

乐观锁实例:假设数据库中帐户信息表中有一个version 字段,当前值为1;而当前帐户余额字段(balance)为$100。下面我们将用时序表的方式来为大家演示乐观锁的实现原理:

操作员A 操作员B
(1)、操作员A 此时将用户信息读出(此时version=1),并准备从其帐户余额中扣除$50($100-$50) (2)、在操作员A 操作的过程中,操作员B 也读入此用户信息(此时version=1),并准备从其帐户余额中扣除$20($100-$20)
(3)、操作员A 完成了修改工作,将数据版本号加1(此时version=2),连同帐户扣除后余额(balance=$50),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录version更新为2
(4)、操作员B 完成了操作,也将版本号加1( version=2 ) 并试图向数据库提交数据(balance=$80),但此时比对数据库记录版本时发现,操作员B 提交的数据版本号为2,数据库记录当前版本也为2,不满足“提交版本必须大于记录当前版本才能执行更新”的乐观锁策略,因此,操作员B 的提交被驳回

这样,就避免了操作员B 用基于version=1 的旧数据修改的结果来覆盖操作员A 的操作结果
的可能。

即然乐观锁比悲观锁要好很多,redis 是否也支持呢?答案是支持, redis 从2.1.0 开始就支持乐观锁了,可以显式的使用watch 对某个key 进行加锁,避免悲观锁带来的一系列问题。Redis 乐观锁实例:假设有一个age 的key,我们开2 个session 来对age 进行赋值操作,我们来看一下结果如何。

Session 1 Session 2
(1)第1 步redis 127.0.0.1:6379> get age "10" redis 127.0.0.1:6379> watch age OK redis 127.0.0.1:6379> multi OK redis 127.0.0.1:6379>
(2)第2 步 redis 127.0.0.1:6379> set age 30 OK redis 127.0.0.1:6379> get age "30" redis 127.0.0.1:6379>
(3)第3 步 redis 127.0.0.1:6379> set age 20 QUEUED redis 127.0.0.1:6379> exec (nil) redis 127.0.0.1:6379> get age "30" redis 127.0.0.1:6379>

从以上实例可以看到在
第一步,Session 1 还没有来得及对age 的值进行修改
第二步,Session 2 已经将age 的值设为30
第三步,Session 1 希望将age 的值设为20,但结果一执行返回是nil,说明执行失败,之后我们再取一下age 的值是30,这是由于Session 1 中对age 加了乐观锁导致的。

watch 命令会监视给定的key,当exec 时候如果监视的key 从调用watch 后发生过变化,则整个事务会失败。也可以调用watch 多次监视多个key.这样就可以对指定的key 加乐观锁了。注意watch 的key 是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。当然了exec,discard,unwatch 命令都会清除连接中的所有监视。

redis 的事务实现是如此简单,当然会存在一些问题。第一个问题是redis 只能保证事务的每个命令连续执行,但是如果事务中的一个命令失败了,并不回滚其他命令,比如使用的命令类型不匹配。下面将以一个实例的例子来说明这个问题:

`redis 127.0.0.1:6379> get age
"30"
redis 127.0.0.1:6379> get name
"HongWan"
redis 127.0.0.1:6379> multi
OK
redis 127.0.0.1:6379> incr age
QUEUED
redis 127.0.0.1:6379> incr name
QUEUED
redis 127.0.0.1:6379> exec

  1. (integer) 31
  2. (error) ERR value is not an integer or out of range
    redis 127.0.0.1:6379> get age
    "31"
    redis 127.0.0.1:6379> get name
    "HongWan"
    redis 127.0.0.1:6379>`
    从这个例子中可以看到,age 由于是个数字,那么它可以有自增运算,但是name 是个字符串,无法对其进行自增运算,所以会报错,如果按传统关系型数据库的思路来讲,整个事务都会回滚,但是我们看到redis 却是将可以执行的命令提交了,所以这个现象对于习惯于关系型数据库操作的朋友来说是很别扭的,这一点也是redis 今天需要改进的地方。

持久化机制

redis 是一个支持持久化的内存数据库,也就是说redis 需要经常将内存中的数据同步到磁盘来保证持久化。redis 支持两种持久化方式,一种是Snapshotting(快照)也是默认方式,另一种是Append-only file(缩写aof)的方式。下面分别介绍:

snapshotting 方式

快照是默认的持久化方式。这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。可以通过配置设置自动做快照持久化的方式。我们可以配置redis在n 秒内如果超过m 个key 被修改就自动做快照,下面是默认的快照保存配置

save 900 1 #900 秒内如果超过1 个key 被修改,则发起快照保存
save 300 10 #300 秒内容如超过10 个key 被修改,则发起快照保存
save 60 10000
下面介绍详细的快照保存过程:

  1. redis 调用fork,现在有了子进程和父进程。
  2. 父进程继续处理client 请求,子进程负责将内存内容写入到临时文件。由于os 的实时复制机制(copy on write)父子进程会共享相同的物理页面,当父进程处理写请求时os 会为父进程要修改的页面创建副本,而不是写共享的页面。所以子进程地址空间内的数据是fork时刻整个数据库的一个快照。
  3. 当子进程将快照写入临时文件完毕后,用临时文件替换原来的快照文件,然后子进程退出。

client 也可以使用save 或者bgsave 命令通知redis 做一次快照持久化。save 操作是在主线程中保存快照的,由于redis 是用一个主线程来处理所有client 的请求,这种方式会阻塞所有client 请求。所以不推荐使用。另一点需要注意的是,每次快照持久化都是将内存数据完整写入到磁盘一次,并不是增量的只同步变更数据。如果数据量大的话,而且写操作比较多,必然会引起大量的磁盘io操作,可能会严重影响性能。

下面将演示各种场景的数据库持久化情况

redis 127.0.0.1:6379> set name HongWan OK redis 127.0.0.1:6379> get name "HongWan" redis 127.0.0.1:6379> shutdown redis 127.0.0.1:6379> quit

我们先设置了一个name 的键值对,然后正常关闭了数据库实例,数据是否被保存到磁盘了
呢?我们来看一下服务器端是否有消息被记录下来了:

[6563] 09 Aug 18:58:58 * The server is now ready to accept connections on port 6379 [6563] 09 Aug 18:58:58 - 0 clients connected (0 slaves), 539540 bytes in use [6563] 09 Aug 18:59:02 - Accepted 127.0.0.1:58005 [6563] 09 Aug 18:59:03 - 1 clients connected (0 slaves), 547368 bytes in use [6563] 09 Aug 18:59:08 - 1 clients connected (0 slaves), 547424 bytes in use [6563] 09 Aug 18:59:12 # User requested shutdown... [6563] 09 Aug 18:59:12 * Saving the final RDB snapshot before exiting. [6563] 09 Aug 18:59:12 * DB saved on disk [6563] 09 Aug 18:59:12 # Redis is now ready to exit, bye bye... [root@localhost redis-2.2.12]#

从日志可以看出,数据库做了一个存盘的操作,将内存的数据写入磁盘了。正常的话,磁盘
上会产生一个dump 文件,用于保存数据库快照,我们来验证一下:

[root@localhost redis-2.2.12]# ll 总计 188 -rw-rw-r-- 1 root root 9602 2011-07-22 00-RELEASENOTES -rw-rw-r-- 1 root root 55 2011-07-22 BUGS -rw-rw-r-- 1 root root 84050 2011-07-22 Changelog drwxrwxr-x 2 root root 4096 2011-07-22 client-libraries -rw-rw-r-- 1 root root 671 2011-07-22 CONTRIBUTING -rw-rw-r-- 1 root root 1487 2011-07-22 COPYING drwxrwxr-x 4 root root 4096 2011-07-22 deps drwxrwxr-x 2 root root 4096 2011-07-22 design-documents drwxrwxr-x 2 root root 12288 2011-07-22 doc -rw-r--r-- 1 root root 26 08-09 18:59 dump.rdb -rw-rw-r-- 1 root root 652 2011-07-22 INSTALL -rw-rw-r-- 1 root root 337 2011-07-22 Makefile -rw-rw-r-- 1 root root 1954 2011-07-22 README -rw-rw-r-- 1 root root 19067 08-09 18:48 redis.conf drwxrwxr-x 2 root root 4096 08-05 19:12 src drwxrwxr-x 7 root root 4096 2011-07-22 tests -rw-rw-r-- 1 root root 158 2011-07-22 TODO drwxrwxr-x 2 root root 4096 2011-07-22 utils [root@localhost redis-2.2.12]#

硬盘上已经产生了一个数据库快照了。这时侯我们再将redis 启动,看键值还是否真的持久
化到硬盘了。

`redis 127.0.0.1:6379> keys *

  1. "name"
    redis 127.0.0.1:6379> get name
    "HongWan"
    redis 127.0.0.1:6379>`
    数据被完全持久化到硬盘了。

aof 方式

另外由于快照方式是在一定间隔时间做一次的,所以如果redis 意外down 掉的话,就会丢失最后一次快照后的所有修改。如果应用要求不能丢失任何修改的话,可以采用aof 持久化方式。

下面介绍Append-only file:

aof 比快照方式有更好的持久化性,是由于在使用aof 持久化方式时,redis 会将每一个收到
的写命令都通过write 函数追加到文件中(默认是appendonly.aof)。当redis 重启时会通过重
新执行文件中保存的写命令来在内存中重建整个数据库的内容。当然由于os 会在内核中缓
存 write 做的修改,所以可能不是立即写到磁盘上。这样aof 方式的持久化也还是有可能会
丢失部分修改。不过我们可以通过配置文件告诉redis 我们想要通过fsync 函数强制os 写入
到磁盘的时机。
有三种方式如下(默认是:每秒fsync 一次)
appendonly yes //启用aof 持久化方式

appendfsync always //收到写命令就立即写入磁盘,最慢,但是保证完全的持久化

appendfsync everysec //每秒钟写入磁盘一次,在性能和持久化方面做了很好的折中

appendfsync no //完全依赖os,性能最好,持久化没保证

接下来我们以实例说明用法:

`redis 127.0.0.1:6379> set name HongWan
OK
redis 127.0.0.1:6379> set age 20
OK
redis 127.0.0.1:6379> keys *

  1. "age"
  2. "name"
    redis 127.0.0.1:6379> shutdown
    redis 127.0.0.1:6379>`

我们先设置2 个键值对,然后我们看一下系统中有没有产生appendonly.aof 文件

[root@localhost redis-2.2.12]# ll 总计 184 -rw-rw-r-- 1 root root 9602 2011-07-22 00-RELEASENOTES -rw-r--r-- 1 root root 0 08-09 19:37 appendonly.aof -rw-rw-r-- 1 root root 55 2011-07-22 BUGS -rw-rw-r-- 1 root root 84050 2011-07-22 Changelog drwxrwxr-x 2 root root 4096 2011-07-22 client-libraries -rw-rw-r-- 1 root root 671 2011-07-22 CONTRIBUTING -rw-rw-r-- 1 root root 1487 2011-07-22 COPYING drwxrwxr-x 4 root root 4096 2011-07-22 deps drwxrwxr-x 2 root root 4096 2011-07-22 design-documents drwxrwxr-x 2 root root 12288 2011-07-22 doc -rw-rw-r-- 1 root root 652 2011-07-22 INSTALL -rw-rw-r-- 1 root root 337 2011-07-22 Makefile -rw-rw-r-- 1 root root 1954 2011-07-22 README -rw-rw-r-- 1 root root 19071 08-09 19:24 redis.conf drwxrwxr-x 2 root root 4096 08-05 19:12 src drwxrwxr-x 7 root root 4096 2011-07-22 tests -rw-rw-r-- 1 root root 158 2011-07-22 TODO drwxrwxr-x 2 root root 4096 2011-07-22 utils [root@localhost redis-2.2.12]#

结果证明产生了,接着我们将redis 再次启动后来看一下数据是否还在
`[root@localhost redis-2.2.12]# src/redis-cli
redis 127.0.0.1:6379> keys *

  1. "age"
  2. "name"
    redis 127.0.0.1:6379>`
    数据还存在系统中,说明系统是在启动时执行了一下从磁盘到内存的load 数据的过程。

aof 的方式也同时带来了另一个问题。持久化文件会变的越来越大。例如我们调用incr test命令100 次,文件中必须保存全部的100 条命令,其实有99 条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100 就够了。为了压缩aof 的持久化文件。redis 提供了bgrewriteaof 命令。收到此命令redis 将使用与快照类似的方式将内存中的数据以命令的方式保存到临时文件中,最后替换原来的文件。

具体过程如下

1、redis 调用fork ,现在有父子两个进程
2、子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令
3、父进程继续处理client 请求,除了把写命令写入到原来的aof 文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。
4、当子进程把快照内容写入以命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。
5、现在父进程可以使用临时文件替换老的aof 文件,并重命名,后面收到的写命令也开始往新的aof 文件中追加。

需要注意到是重写aof 文件的操作,并没有读取旧的aof 文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof 文件,这点和快照有点类似。接来我们看一下实际的例子:
我们先调用5 次incr age 命令:

redis 127.0.0.1:6379> incr age (integer) 21 redis 127.0.0.1:6379> incr age (integer) 22 redis 127.0.0.1:6379> incr age (integer) 23 redis 127.0.0.1:6379> incr age (integer) 24 redis 127.0.0.1:6379> incr age (integer) 25 redis 127.0.0.1:6379>

接下来我们看一下日志文件的大小
[root@localhost redis-2.2.12]# ll 总计 188 -rw-rw-r-- 1 root root 9602 2011-07-22 00-RELEASENOTES -rw-r--r-- 1 root root 259 08-09 19:43 appendonly.aof -rw-rw-r-- 1 root root 55 2011-07-22 BUGS -rw-rw-r-- 1 root root 84050 2011-07-22 Changelog

大小为259 个字节,接下来我们调用一下bgrewriteaof 命令将内存中的数据重新刷到磁盘的
日志文件中
redis 127.0.0.1:6379> bgrewriteaof Background append only file rewriting started redis 127.0.0.1:6379>

再看一下磁盘上的日志文件大小
[root@localhost redis-2.2.12]# ll 总计 188 -rw-rw-r-- 1 root root 9602 2011-07-22 00-RELEASENOTES -rw-r--r-- 1 root root 127 08-09 19:45 appendonly.aof -rw-rw-r-- 1 root root 55 2011-07-22 BUGS -rw-rw-r-- 1 root root 84050 2011-07-22 Changelog
日志文件大小变为127 个字节了,说明原来日志中的重复记录已被刷新掉了。

发布及订阅消息

发布订阅(pub/sub)是一种消息通信模式,主要的目的是解耦消息发布者和消息订阅者之间的耦合,这点和设计模式中的观察者模式比较相似。pub/sub 不仅仅解决发布者和订阅者直接代码级别耦合也解决两者在物理部署上的耦合。redis 作为一个pub/sub 的server,在订阅者和发布者之间起到了消息路由的功能。订阅者可以通过subscribe 和psubscribe 命令向redis server 订阅自己感兴趣的消息类型,redis 将消息类型称为通道(channel)。当发布者通过publish 命令向redis server 发送特定类型的消息时。订阅该消息类型的全部client 都会收到此消息。这里消息的传递是多对多的。一个client 可以订阅多个channel,也可以向多个channel发送消息。

下面做个实验。这里使用3 不同的client, client1 用于订阅tv1 这个channel 的消息,client2用于订阅tv1 和tv2 这2 个chanel 的消息,client3 用于发布tv1 和tv2 的消息。

Client 1 Client 2 Client 3
redis 127.0.0.1:6379> subscribe tv1 Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "tv1" 3) (integer) 1 redis 127.0.0.1:6379> subscribe tv1 tv2 Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "tv1" 3) (integer) 1 1) "subscribe" 2) "tv2" 3) (integer) 2
redis 127.0.0.1:6379> publish tv1 program1 (integer) 2 redis 127.0.0.1:6379>
redis 127.0.0.1:6379> subscribe tv1 Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "tv1" 3) (integer) 1 1) "message" 2) "tv1" 3) "program1" redis 127.0.0.1:6379> subscribe tv1 tv2 Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "tv1" 3) (integer) 1 1) "subscribe" 2) "tv2" 3) (integer) 2 1) "message" 2) "tv1" 3) "program1"
redis 127.0.0.1:6379> publish tv2 program2 (integer) 1 redis 127.0.0.1:6379>
redis 127.0.0.1:6379> subscribe tv1 redis 127.0.0.1:6379> subscribe tv1 tv2
Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "tv1" 3) (integer) 1 1) "message" 2) "tv1" 3) "program1" Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "tv1" 3) (integer) 1 1) "subscribe" 2) "tv2" 3) (integer) 2 1) "message" 2) "tv1" 3) "program1" 1) "message" 2) "tv2" 3) "program2"

下面将详细的解释一下上面的例子
1、client1 订阅了tv1 这个channel 这个频道的消息,client2 订阅了tv1 和tv2 这2 个频道的消息
2、client3 是用于发布tv1 和tv2 这2 个频道的消息发布者
3、接下来我们在client3 发布了一条消息”publish tv1 program1”,大家可以看到这条消息是发往tv1 这个频道的
4、理所当然的client1 和client2 都接收到了这个频道的消息
5、 然后client3 又发布了一条消息”publish tv2 program2”,这条消息是发往tv2 的,由于client1 并没有订阅tv1,所以client1 的结果中并没有显示出任何结果,但client2 订阅了这个频道,所以client2 是会有返回结果的。
我们也可以用psubscribe tv*的方式批量订阅以tv 开头的频道的内容。
看完这个小例子后应该对pub/sub 功能有了一个感性的认识。需要注意的是当一个连接通过subscribe 或者psubscribe 订阅通道后就进入订阅模式。在这种模式除了再订阅额外的通道或者用unsubscribe 或者punsubscribe 命令退出订阅模式,就不能再发送其他命令。另外使用psubscribe 命令订阅多个通配符通道,如果一个消息匹配上了多个通道模式的话,会多次收到同一个消息。

Pipeline 批量发送请求

redis 是一个cs 模式的tcp server,使用和http 类似的请求响应协议。一个client 可以通过一个socket 连接发起多个请求命令。每个请求命令发出后client 通常会阻塞并等待redis 服务处理,redis 处理完后请求命令后会将结果通过响应报文返回给client。基本的通信过程如下:

Client: INCR X Server: 1 Client: INCR X Server: 2 Client: INCR X Server: 3 Client: INCR X Server: 4

基本上四个命令需要8 个tcp 报文才能完成。由于通信会有网络延迟,假如从client 和server之间的包传输时间需要0.125 秒。那么上面的四个命令8 个报文至少会需要1 秒才能完成。这样即使redis 每秒能处理100 个命令,而我们的client 也只能一秒钟发出四个命令。这显示没有充分利用redis 的处理能力,怎么样解决这个问题呢? 我们可以利用pipeline 的方式从client 打包多条命令一起发出,不需要等待单条命令的响应返回,而redis 服务端会处理完多条命令后会将多条命令的处理结果打包到一起返回给客户端。通信过程如下

Client: INCR X Client: INCR X Client: INCR X Client: INCR X Server: 1 Server: 2 Server: 3 Server: 4

假设不会因为tcp 报文过长而被拆分。可能两个tcp 报文就能完成四条命令,client 可以将四个incr 命令放到一个tcp 报文一起发送,server 则可以将四条命令的处理结果放到一个tcp报文返回。通过pipeline 方式当有大批量的操作时候,我们可以节省很多原来浪费在网络延迟的时间,需要注意到是用pipeline 方式打包命令发送,redis 必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。具体多少合适需要根据具体情况测试。下面是个Java 使用pipeline 的实验:

import org.jredis.JRedis; import org.jredis.connector.ConnectionSpec; import org.jredis.ri.alphazero.JRedisClient; import org.jredis.ri.alphazero.JRedisPipelineService; import org.jredis.ri.alphazero.connection.DefaultConnectionSpec; public class TestPipeline { public static void main(String[] args) { long start = System.currentTimeMillis(); //采用pipeline 方式发送指令 usePipeline(); long end = System.currentTimeMillis(); System.out.println("用pipeline 方式耗时:" + (end - start) + "毫秒"); start = System.currentTimeMillis(); //普通方式发送指令 withoutPipeline(); end = System.currentTimeMillis(); System.out.println("普通方式耗时:" + (end - start) + "毫秒"); } //采用pipeline 方式发送指令 private static void usePipeline() { try { ConnectionSpec spec = DefaultConnectionSpec.newSpec( "192.168.115.170", 6379, 0, null); JRedis jredis = new JRedisPipelineService(spec); for (int i = 0; i < 100000; i++) { jredis.incr("test2"); } jredis.quit(); } catch (Exception e) { } } //普通方式发送指令 private static void withoutPipeline() { try { JRedis jredis = new JRedisClient("192.168.115.170", 6379); for (int i = 0; i < 100000; i++) { jredis.incr("test2"); } jredis.quit(); } catch (Exception e) { } } }

执行结果如下:

-- JREDIS -- INFO: Pipeline thread <response-handler> started. -- JREDIS -- INFO: Pipeline <org.jredis.ri.alphazero.connection.SynchPipelineConnection@1bf73fa> connected 用pipeline 方式耗时:11531 毫秒 -- JREDIS -- INFO: Pipeline <org.jredis.ri.alphazero.connection.SynchPipelineConnection@1bf73fa> disconnected -- JREDIS -- INFO: Pipeline thread <response-handler> stopped. 普通方式耗时:15985 毫秒

所以用两种方式发送指令,耗时是不一样的,具体是否使用pipeline 必须要基于大家手中的网络情况来决定,不能一切都按最新最好的技术来实施,因为它有可能不是最适合你的。

虚拟内存的使用

首先说明下redis 的虚拟内存与操作系统的虚拟内存不是一码事,但是思路和目的都是相同的。就是暂时把不经常访问的数据从内存交换到磁盘中,从而腾出宝贵的内存空间用于其他需要访问的数据。尤其是对于redis 这样的内存数据库,内存总是不够用的。除了可以将数据分割到多个redis server 外。另外的能够提高数据库容量的办法就是使用虚拟内存把那些不经常访问的数据交换到磁盘上。如果我们的存储的数据总是有少部分数据被经常访问,大部分数据很少被访问,对于网站来说确实总是只有少量用户经常活跃。当少量数据被经常访问时,使用虚拟内存不但能提高单台redis server 数据库的容量,而且也不会对性能造成太多影响。
redis 没有使用操作系统提供的虚拟内存机制而是自己在实现了自己的虚拟内存机制,主要的理由有两点:

  1. 操作系统的虚拟内存是以4k 页面为最小单位进行交换的。而redis 的大多数对象都远小于4k,所以一个操作系统页面上可能有多个redis 对象。另外redis 的集合对象类型如list,set可能存在于多个操作系统页面上。最终可能造成只有10%key 被经常访问,但是所有操作系统页面都会被操作系统认为是活跃的,这样只有内存真正耗尽时操作系统才会交换页面。

2、相比于操作系统的交换方式,redis 可以将被交换到磁盘的对象进行压缩,保存到磁盘的对象可以去除指针和对象元数据信息,一般压缩后的对象会比内存中的对象小10 倍,这样redis的虚拟内存会比操作系统虚拟内存能少做很多io 操作。

下面是vm 相关配置
vm-enabled yes #开启vm 功能
vm-swap-file /tmp/redis.swap #交换出来的value 保存的文件路径
vm-max-memory 1000000 #redis 使用的最大内存上限
vm-page-size 32 #每个页面的大小32 个字节
vm-pages 134217728 #最多使用多少页面
vm-max-threads 4 #用于执行value 对象换入换出的工作线程数量

redis 的虚拟内存在设计上为了保证key 的查找速度,只会将value 交换到swap 文件中。所以如果是内存问题是由于太多value 很小的key 造成的,那么虚拟内存并不能解决,和操作系统一样redis 也是按页面来交换对象的。redis 规定同一个页面只能保存一个对象。但是一个对象可以保存在多个页面中。在redis 使用的内存没超过vm-max-memory 之前是不会交换任何value 的。当超过最大内存限制后,redis 会选择较过期的对象。如果两个对象一样过期会优先交换比较大的对象,精确的公式swappability = age*log(size_in_memory)。对于vm-page-size 的设置应该根据自己的应用将页面的大小设置为可以容纳大多数对象的大小,太大了会浪费磁盘空间,太小了会造成交换文件出现碎片。对于交换文件中的每个页面,redis
会在内存中对应一个1bit 值来记录页面的空闲状态。所以像上面配置中页面数量(vm-pages 134217728 )会占用16M 内存用来记录页面空闲状态。vm-max-threads 表示用做交换任务的线程数量。如果大于0 推荐设为服务器的cpu 内核的数量,如果是0 则交换过程在主线程进行。

推荐阅读更多精彩内容