复制(replication),可以让其他服务器拥有一个不断更新的数据副本,从而使得拥有数据副本的服务器可以用于处理客户端发送的读请求。对于高负载应用来说,复制是不可或缺的一个特性。
关系型数据库通常会使用一个主服务器(master)向多个从服务器(slave)发送更新,并使用从服务器来处理所有读请求。Redis也采用了同样的方法来实现自己的复制特性,并将其用作扩展性能的一种手段。
复制相关配置选项
当从服务器连接主服务器时,主服务器会执行BGSAVE操作,为了正确地使用复制特性,用户需要保证服务器已经正确地设置了dir选项和dirname选项。
配置slaveof host port选项即可连接主服务器。
下面将演示怎么实现一个简单的复制系统。我们在一台机器上起两个Redis实例,监听不同的端口,其中一个作为主库,另外一个作为从库。首先不加任何参数来启动一个Redis实例作为主数据库:
主库默认监听6379端口。
接着新建一个终端,加上slaveof参数启动另一个Redis实例作为从库,并且监听6380端口:
从控制台输出中可以看到,从库已经连接到主库:126.0.0.1:6379了,我们可以分别在主库和从库中使用info replication命令看一看当前实例在复制系统中的相关信息
现在可以测试一下主从库的数据同步了:
$ redis-cli -p 6379
127.0.0.1:6379> set test-replicate lawtech
OK
$ redis-cli -p 6380
127.0.0.1:6380> get test-replicate
"lawtech"
127.0.0.1:6380> set x y
(error) READONLY You can't write against a read only slave.
可以看到,在主库中添加的数据确实同步到了从库中。但是,我们在向从库中写入数据时报错了,这是因为在默认情况下,从库是只读的。我们可以在从库的配置文件中加上如下的配置项允许从库写数据:
slave-read-only no
但是,因为从库中修改的数据不会被同步到任何其他数据库,并且一旦主库修改了数据,从库的数据就会因为自动同步被覆盖,所以一般情况下,不建议将从库设置为可写。
相同的道理,配置多台从库也使用相同的方法,都在从库的配置文件中加上slaveof参数即可。
此外,我们可以在客户端使用命令
SLAVEOF 新主库地址 新主库端口
来修改当前数据库的主库,如果当前数据库已经是其他库的从库, 则当前数据库会停止和原来的数据库的同步而和新的数据库同步。
最后,从数据库还可以通过运行命令:
SLAVEOF NO ONE
来停止接受来自其他数据库的同步而升级成为主库。
Redis复制的启动过程
从服务器连接主服务器时,主服务器会创建一个快照文件并将其发送至从服务器,但这只是主从复制执行过程的其中一步,下表列举出复制过程主从服务器执行的所有操作:
| 步骤 | 主服务器操作 | 从服务器操作 |
|---|---|---|
| 1 | ( 等待命令进入) | 连接(或者重连接)主服务器,发送SYNC命令 |
| 2 | 开始执行BGSAVE,并使用缓冲区记录BGSAVE之后执行的所有写命令 | 根据配置选项来决定时继续使用现在的数据来处理客户端命令,还是向发送请求的客户端返回错误 |
| 3 | BGSAVE执行完毕,向从服务器发送快照文件,并在发送期间继续使用缓冲区记录被执行的写命令 | 丢弃所有旧的数据,开始载入主服务器发来的快照文件 |
| 4 | 快照文件发送完毕,开始向从服务器发送存储在缓冲区里面的写命令 | 完成对快照文件的解释操作,像往常一样开始接受命令请求 |
| 5 | 缓冲区存储的写命令发送完毕;从现在开始,没执行一个写命令,就像从服务器发送相同的写命令 | 执行主服务器发来的所有存储在缓冲区里面的写命令;从现在开始,接收并执行主服务器传来的每个写命令 |
由上述步骤可以看出,有必要给Redis主服务器留30%~45%的内存用于执行BGSAVE命令和创建记录写命令的缓冲区。另外,从服务器还有一点需要注意的是,从服务器在进行同步时,会清空自己的所有数据,因为第3步中,从服务器会丢弃所有旧数据。
警告:Redis不支持主主复制(master-master replication)
当多个从服务器尝试连接同一个主服务器的时候,就会出现下表所示的两种情况中的其中一种:
| 当有新的从服务器连接主服务器时 | 主服务器的操作 |
|---|---|
| 上述步骤3尚未执行 | 所有从服务器都会接收相同的快照文件和相同的缓冲区写命令 |
| 上述步骤3正在执行或者已经执行 | 当主服务器与较早进行连接的从服务器执行完复制所需的5个步骤之后,主服务器会与新连接的从服务器执行一次新的步骤1至步骤5 |
由此可以看出多个从服务器的同步对网络的开销挺大的,有可能会影响到主服务器接收写命令,甚至是与主服务器位于同一网络中的其他硬件。
主从链
创建多个从服务器可能造成网络不可用,此时可以使用另外一个解决方案,从服务器拥有自己的从服务器,并由此形成主从链(master/slave chaining)。
从服务器对从服务器进行复制在操作上和从服务器对主服务器进行复制的唯一区别在于。如果从服务器X拥有从服务器Y,那么当从服务器X在执行启动过程表中步骤4时,X将断开与Y的连接,导致Y需要重新连接并重新同步(resync)。
当读请求的重要性明显高于写请求的重要性,并且读请求的数量需求远远超出一台Redis服务器可以处理的范围时,用户就需要添加新的从服务器来处理读请求,随着负载不断上升,主服务器可能会无法快速地更新所有从服务器。
为了缓解这个问题,可以创建一个由Redis主/从节点(master/slave node)组成的中间层来分担主服务器的复制工作,如下图所示:
上面这个示例中,树的中层有3个帮助开展复制工作的服务器,底层有9个从服务器。其中,只有3台从服务器和主服务器通信,其他都向从服务器同步数据,从而降低了系统的负载。
检验硬盘写入
为了将数据保存在多台机器中,用户首先需要为主服务器设置多个从服务器,然后对每个从服务器设置appendonly yes选项和appendfsync everysec选项(如有需要,也可以对主服务器这样设置),但这只是第一步:因为用户还需要等待主服务器发送的写命令到达从服务器,并且在执行后续操作前,检查数据是否已经被写入了硬盘中。
整个操作分两个环节:
- 验证主服务器是否已经将写数据发送至从服务器:用户需要在向主服务器写入真正的数据之后,再向主服务器写入一个唯一的虚构值(unique dummy value),然后通过检查虚构值是否存在于从服务器来判断数据是否已经到达从服务器。
- 判断数据是否已经被保存到硬盘中:检查INFO命令的输出结果中
aof_pending_bio_fsync属性的值是否为0,如果是,则数据已经被保存到了硬盘中。
在向主服务器写入数据后,用户可以将主服务器和从服务器的连接作为参数调用下面的代码来自动进行上述操作:
# _*_ coding: utf-8 _*_
import uuid
import time
def wait_for_sync(mconn, sconn):
identifier = str(uuid.uuid4())
# 将令牌添加至主服务器
mconn.zadd('sync:wait', identifier, time.time())
# 如果有必要的话,等待从服务器完成同步
while not sconn.info()['master_link_status'] != 'up':
time.sleep(.001)
# 等待从服务器接收数据更新
while not sconn.zscore('sync:wait', identifier):
time.sleep(.001)
# 最多只等待1秒
deadline = time.time() + 1.01
# 检查数据更新是否已经被同步到了硬盘
while time.time() < deadline:
if sconn.info()['aof_pending_bio_fsync'] == 0:
break
time.sleep(.001)
# 清理刚刚创建的新令牌以及之前可能留下的旧令牌
mconn.zrem('sync:wait', identifier)
mconn.zremrangebyscore('sync:wait', 0, time.time() - 900)
为了确保操作可以正确执行,wait_for_sync()函数会首先确认从服务器已经连接上主服务器,然后检查自己添加到等待同步有序集合(sync wait ZSET)里面的值是否已经存在于从服务器,在发现值存在后,等待从服务器将缓冲区的所有数据写入硬盘里。最后,确认数据已经被保存到硬盘之后,函数会执行一些清理操作。
通过同时使用复制和AOF持久化,用户可以增强Redis对于系统崩溃的抵抗能力。
# Redis, Python 🐶 怕是要给老板下跪了哦~ 🐶 赞赏
微信打赏
支付宝打赏



