redis详解

工作原理

事件模型

redis以其高性能而闻名,它最大程度地利用了 单线程,非阻塞,多路复用的I/O模型 来快速地处理请求。

redis详解_第1张图片 redis详解_第2张图片

通信协议

Redis基本上就是一个接受并处理来自客户端请求的非阻塞、I/O复用的TCP服务器。我们可以使用各种编程语言通过TCP协议与Redis进行通信。对Redis来说,这种通信协议叫做Redis Serialization Protocal(RESP,Redis序列化协议)。
例如我们利用nc向redis服务器发送命令:
echo -e "*3\r\n\$3\r\nset\r\n\$5\r\nmykey\r\n\$1\r\n1\r\n" | nc 127.0.0.1 6379
返回+OK
解析方式:

  • *代表这是一个数组类型
  • 3代表数组的大小
  • \r\n是RESP中每个部分的终结符
  • 3 之前的反斜杠是 3之前的反斜杠是 3之前的反斜杠是符号的转义符
  • $3表示接下来是3个字符组成的字符串
  • set为字符串本身
  • +OK是命令的相应字符串(加号表示相应是一个简单字符串类型,:代表结果是一个整型,-号代表错误消息)
    出于性能考虑,我们可以使用RESP在一次对Redis服务器的调用中发送多个命令。总之,客户端发送给Redis服务器的命令实际就是一串字符串组成的RESP数组,服务器根据不同的解析方式进行响应。
    解决多次向redis服务器发送命令产生的时延:
  1. 使用RESP在一次对Redis服务器的调用中发送多个命令
  2. 使用redis-cli的—pipe选项通过管道发送命令;将 1 中使用的RESP原始报文直接写入txt文件中也有同样效果。
    pipeline.txt
set mykey myvalue
asdd myset value1 value2
get mykey
scard myset

为了让文本文件中的每一行都必须以\r\n,而不是\n结束,可以使用命令unix2dos实现:
unix2dos pipeline.txt
cat pipeline.txt | redis-cli -- pipe

存储编码

redis会根据存储的字符串自动匹配相应的编码方式
int :用于能够使用64位有符号整数表示的字符串
embstr :用于长度小于或等于44字节的字符串,这种类型的编码在内存使用和性能方面更有效率
raw :用于长度大于44字节的字符串

127.0.0.1:6379> set mykey 123456
OK
127.0.0.1:6379> object encoding mykey
"int"
127.0.0.1:6379> set mykey "string"
OK
127.0.0.1:6379> object encoding mykey
"embstr"

ziplist :对于长度小于配置中hash-max-ziplist-entries选项配置的值(默认512b),且所有元素的大小都小于配置中hash-max-ziplist-value选项配置的值(默认64b)的哈希,采用此编码。对于较小的哈希而言可以节省占用空间。
ziplist :对于长度小于配置中zset-max-ziplist-entries选项配置的值(默认128b),且所有元素的大小都小于配置中zset-max-ziplist-value选项配置的值(默认64b)的有序集合,采用此编码。对于较小的集合而言可以节省占用空间。
skiplist :当ziplist不适用时有序集合使用的默认编码。
intset :对于元素都是整数,且长度小于配置中set-max-intset-entries选项配置的值(默认512b)的集合,采用此编码。对于较小的集合而言可以节省占用空间。
hashtable :当ziplist,intset不适用时使用的默认编码。
Sparse(稀疏) :对于长度小于配置中hill-space-max-bytes选项设置的值(默认3000)的HLL对象,采用此编码。稀疏表示方式存储效率更高,但可能会消耗更多的CPU资源。
Dense(稠密) :当稀疏方式不适用时的默认编码。
geohash :基于一种52位整数的表示(实现了低于一米的精度)。当需要一个标准的geohash字符串是,可以使用geohash命令来获取一个长度为11的字符串。性能取决于中心点和半径所决定的圆形区域的外接矩阵中成员的个数。

特性分析

关于过期

用expire(指定过期时间(s))和expireat(指定绝对UNIX时间戳) 指定键的过期时间,ttl命令获取过期时间。
Redis是否宕机不影响一个设置了过期时间键的过期时间。宕机时过期时间的绝对时间戳会被持久化到RDB文件中,再次启动时并不会改变。
过期键redis会定期运行一个基于概率的算法来进行主动删除。如果客户端尝试访问已过期的键, redis会立即将其从内存中删除。因为redis对已过期的键的删除是随机不可预期的,所以有些已过期的键可能永远不会被删除,太多过期键我们可以通过执行SCAN命令主动触发删除操作。

redis详解_第3张图片

清除一个键的过期时间:

  • persist命令使其成为持久键
  • 键的值被替换或删除,包括set, getset 和 *store 在内的命令会清除过期时间。但是列表,集合,哈希的元素不会清除过期时间,因为修改元素的操作并不会替换键所关联的值对象。
  • 被另一个没有过期时间的键重命名

关于事务

对于秒杀类应用而言,超卖问题发生的原因在于我们获取计数器值的时候涉及竞争状态,计数器的值也许在业务场景中是有效的(即大于0),但计数器的值在减少之前可能已经被其他请求修改了。我们可以使用redis的事务来避免这种情况发生。
redis详解_第4张图片

redis事务虽然也是将一组操作原子化执行,但其与关系数据库事务不同,redis事务没有回滚功能。
在一个redis事务中可能会出现以下两种类型的错误:

  1. 命令语法错误。在exec开始时整个事务会快速失败,且所有命令都不会被处理。
  2. exec执行过程中发生错误。不会回滚操作,而且发生错误命令之后的命令会继续执行。

关于发布-订阅(PubSub)

发布订阅是一种历史悠久的消息传递模式。简单的说,想要发布事件(event)的发布者(publisher)会把消息(message)发送到一个PubSub频道(channel),这个频道会把时间投递(deliver)给对这个频道感兴趣的每一个订阅者(subscriber)。许多流行的消息中间件都是利用这个模式来构建消息投递系统,Redis也是如此。
订阅者1订阅A消息:

127.0.0.1:6379> subscribe A
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "A"
3) (integer) 1

订阅者2订阅A, B消息:

127.0.0.1:6379> subscribe A B
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "A"
3) (integer) 1
1) "subscribe"
2) "B"
3) (integer) 2

发布者发布 A 消息:

127.0.0.1:6379> publish A "A xxx"
(integer) 2

订阅者1和订阅者2都收到消息:

1) "message"
2) "A"
3) "A xxx"

发布者发布 B 消息:

127.0.0.1:6379> publish B "B xxx"
(integer) 1

只有订阅者2收到消息:

1) "message"
2) "B"
3) "B xxx"

pubsub 命令用于进行频道管理。如pubsub channels可以获取当前活跃的频道。

Redis中基于PubSub的键空间通知(keyspace notification)功能允许客户端订阅一个Redis频道来接受命令发布或者数据改变的事件。
订阅匹配__key*__:*的频道

127.0.0.1:6379> psubscribe __key*__:*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__key*__:*"
3) (integer) 1
1) "pmessage"
2) "__key*__:*"
3) "__keyspace@0__:foo"
4) "set"
1) "pmessage"
2) "__key*__:*"
3) "__keyevent@0__:set"
4) "foo"

redis发送命令(此时会向keyspace和keyevent两个频道发布消息)

127.0.0.1:6379> set foo 1
OK

关于Lua

Lua作为服务器端一种轻量级的脚本语言,Redis可以使用Lua脚本将一组操作原子化执行,比redis事务具有更强大的功能和程序逻辑,能够处理涉及复杂逻辑判断或循环处理的业务场景。

  • 可以使用redis-cli --eval来执行Lua脚本或者直接在Lua程序中使用EVAL命令来执行。
  • Lua脚本可以缓存在redis服务器进程中,想执行时就利用脚本的唯一标识找到脚本来执行。
  • 可以debug执行Lua脚本。

常见应用场景

  • 由于访问延迟低以及会话超时管理,可以用于分布式web服务器存储会话
  • 可以使用简单incr命令为浏览量计数,也可以基于哈希、有序集合、HyperLogLog等数据类型构建更高级的计数器或数据捕获系统。
  • 有序集合可以轻松实现排行榜
  • 列表的push/pop可以用来实现简单的任务队列
  • 列表通过lpush xx 和ltrim xx 0 10可以让列表始终包含最新增加的10条数据
  • 可以用作缓存,加速数据库的查询过程。可以对缓存设置过期时间及回收策略。
    redis适用于不强制要求符合ACID规范的数据场景。

主从复制

本机测试主从复制过程

拷贝一份默认的配置文件redis.conf命名为redis-slave.conf,修改以下字段成为从实例的配置文件:

port 6380
pidfile /var/run/redis_6380.pid
dir ./slave
#要复制的主实例ip及端口
replicaof 127.0.0.1 6379
#masterauth  主实例有密码的在这里设
#默认从实例是只读的,这里取消只读
replica-read-only no

也可以在redis-cli中使用slaveof命令,将动态地当前的redis实例变为另一个实例的从实例。

启动主实例redis-server conf/redis-slave.conf,启动从实例 redis-server conf/redis-slave.conf

连接主:

redis-cli -p 6379
info replication #查看复制关系信息
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=112,lag=0
master_replid:2f807e3b31be4f6af413af163b45963a7176a158
master_replid2:0000000000000000000000000000000000000000

连接从:

redis-cli -p 6380
info replication  #查看复制关系信息
# Replication
role:slave
master_host:127.0.0.1
master_port:6379

正确操作结果:在主实例上分别设置键值,在从实例可以取到;在从实例上设置键值,在主实例上可以取到;从实例shutduwn之后再连接依旧可以获取主实例的值。

复制原理分析

master_replid:标识主实例,主实例启动时生成;
master_repl_offset:主实例复制流中的偏移标记。
两者通常用来表示与主实例同步的最后一个快照。

完全重新同步需要主实例建立线程去存储RDB文件,并将文件发送给从实例。
部分重新同步从实例读取RDB文件存放的偏移量发送给主实例,主实例只需要根据的偏移量将缓冲区里的命令发送给从实例。
在Redis4.0以后才有的部分重新同步,以前一直是完全重新同步。当一个从实例被提升为主实例,新的主实例能够接受其他从实例的部分重新同步。

复制调优实践

简单测试复制过程

1.启动两个实例
ps: mac使用127.0.0.2地址要执行下这句sudo ifconfig lo0 alias 127.0.0.2 netmask 0xFFFFFFFF

$ M_IP=127.0.0.1
$ M_PORT=6379
$ M_RDB_NAME=master.rdb
$ M_OUT=master.out
$ S1_IP=127.0.0.2
$ S1_PORT=6380
$ S1_OUT=slave_1.out
$ S1_RDB_NAME=slave_1.rdb
$ nohup /usr/local/redis/bin/redis-server --port $M_PORT --bind $M_IP --dbfilename $M_RDB_NAME > $M_OUT &
$ nohup /usr/local/redis/bin/redis-server --port $S1_PORT --bind $S1_IP --dbfilename $S1_RDB_NAME > $S1_OUT &

2.实例启动完成后向一个实例中写入数据

echo set redis hello | nc $M_IP $M_PORT
echo lpush num 1 2 3 |  nc $M_IP $M_PORT

3.配置主从复制
echo slaveof 127.0.0.1 6379 | nc $S1_IP $S1_PORT

4.数据同步完成后,中断主实例和从实例之间的网络连接

# linux

echo “modify iptables”
iptables -I INPUT -s 127.0.0.1 -d 127.0.0.2 -j DROP
Iptabels -I OUTPUT -s 127.0.0.2 -d 127.0.0.1 -j DROP

# mac

sudo cp /etc/pf.conf /etc/pf-redis.conf
sudo vim /etc/pf-redis.conf
# 在anchor "com.apple/*" 后加以下两行
block drop in inet from 127.0.0.1 to 127.0.0.2
block drop out inet from 127.0.0.2 to 127.0.0.1
sudo pfctl -d
sudo pfctl -ef /etc/pf-redis.conf

5.网络中断期间,向主实例导入测试数据
sh prepdata.sh 1000

cd /Users/bijiayang/Desktop/file/my/study/redis
rm -rf repl.data
touch repl.data
for i in $(seq 1 $1)
do
echo $i
echo "set redis$i hello$i" >> repl.data
done

du -sh repl.data
cat repl.data | redis-cli --pipe $M_IP -p $M_PORT

1000条数据du 结果24KB
40000条数据du 结果1M

6.数据导入结束后,恢复主从实例间的网络连接

# linux
echo “restore iptables”
iptables -D INPUT -s 127.0.0.1 -d 127.0.0.2 -j DROP
iptables -D OUTPUT -s 127.0.0.2 -d 127.0.0.1 -j DROP

# mac
sudo pfctl -d
sudo pfctl -ef /etc/pf.conf

iptables文档: http://ipset.netfilter.org/iptables.man.html
pfctl文档:http://man.openbsd.org/pfctl

7.恢复网络连接后,等待主从实例进行数据重新同步完成后,同时关闭主从实例

echo "shutdown" | nc $M_IP $M_PORT
echo "shutdown" | nc $S1_IP $S1_PORT

Possible Problem :
Error reply to PING from master: '-DENIED Redis is running in protected mode because protected mode is enabled, no bind address was specified, no authentication password is requested to clients. In this mode connections are only accepted from the loopback interface.
Solution:

  1. 不想重启的话就在redis-cli下执行config set protected-mode no
  2. /usr/local/redis/redis.conf文件中设置protected-mode no,重启redis
  3. 重启redis:redis-server redis_master.conf --protected-mode no
  4. redis.conf文件中设置bind 127.0.0.1 127.0.0.2
  5. redis.conf文件中设置如下权限验证密码
主实例配置
# vi redis_master.conf
          port 6379
          requirepass redis
从实例配置
# vi redis_slave.conf
          port 6379
          slaveof 192.168.56.10 6379
          masterauth redis
          requirepass redis  # 当前实例如果还有从服务器就需要,否则不需要
日志分析与结论

从实例第一次连接主实例进行同步时,从实例没有主实例的复制ID信息,会进行完全重新同步。
master.out

13334:M 16 Jul 2019 17:53:23.723 * Replica 127.0.0.2:6380 asks for synchronization
13334:M 16 Jul 2019 17:53:23.723 * Partial resynchronization not accepted: Replication ID mismatch (Replica asked for '033eba642ec0e3269c81e408ab9085db28c19962', my replication IDs are '9e4db2221538f2706e57c3fb6e2fcba888d15bcb' and '0000000000000000000000000000000000000000')
13334:M 16 Jul 2019 17:53:23.724 * Starting BGSAVE for SYNC with target: disk
13334:M 16 Jul 2019 17:53:23.725 * Background saving started by pid 13352
13352:C 16 Jul 2019 17:53:23.728 * DB saved on disk
13334:M 16 Jul 2019 17:53:23.823 * Background saving terminated with success
13334:M 16 Jul 2019 17:53:23.824 * Synchronization with replica 127.0.0.2:6380 succeeded

slave_1.out

Before turning into a replica, using my master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
13335:S 16 Jul 2019 17:53:23.721 * REPLICAOF 127.0.0.1:6379 enabled (user request from 'id=3 addr=127.0.0.2:59312 fd=7 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=23 qbuf-free=32745 obl=0 oll=0 omem=0 events=r cmd=slaveof')
13335:S 16 Jul 2019 17:53:23.721 * Connecting to MASTER 127.0.0.1:6379
13335:S 16 Jul 2019 17:53:23.721 * MASTER <-> REPLICA sync started
13335:S 16 Jul 2019 17:53:23.722 * Non blocking connect for SYNC fired the event.
13335:S 16 Jul 2019 17:53:23.723 * Master replied to PING, replication can continue...
13335:S 16 Jul 2019 17:53:23.723 * Trying a partial resynchronization (request 033eba642ec0e3269c81e408ab9085db28c19962:1).
13335:S 16 Jul 2019 17:53:23.725 * Full resync from master: 05465ab144737b75b5f9df409efba72a9d008f50:0
13335:S 16 Jul 2019 17:53:23.725 * Discarding previously cached master state.
13335:S 16 Jul 2019 17:53:23.824 * MASTER <-> REPLICA sync: receiving 226 bytes from master
13335:S 16 Jul 2019 17:53:23.824 * MASTER <-> REPLICA sync: Flushing old data
13335:S 16 Jul 2019 17:53:23.824 * MASTER <-> REPLICA sync: Loading DB in memory
13335:S 16 Jul 2019 17:53:23.825 * MASTER <-> REPLICA sync: Finished with success

24KB数据的同步,部分重新同步。
master.out

13334:M 16 Jul 2019 17:58:09.932 * Replica 127.0.0.2:6380 asks for synchronization
13334:M 16 Jul 2019 17:58:09.938 * Partial resynchronization request from 127.0.0.2:6380 accepted. Sending 40893 bytes of backlog starting from offset 113.

slave_1.out

13335:S 16 Jul 2019 17:58:09.931 * Non blocking connect for SYNC fired the event.
13335:S 16 Jul 2019 17:58:09.931 * Master replied to PING, replication can continue...
13335:S 16 Jul 2019 17:58:09.932 * Trying a partial resynchronization (request 05465ab144737b75b5f9df409efba72a9d008f50:113).
13335:S 16 Jul 2019 17:58:09.938 * Successful partial resynchronization with master.
13335:S 16 Jul 2019 17:58:09.938 * MASTER <-> REPLICA sync: Master accepted a Partial Resynchronization.

1M数据的同步,完全重新同步。
master.out

14422:M 16 Jul 2019 18:30:02.236 * Replica 127.0.0.2:6380 asks for synchronization
14422:M 16 Jul 2019 18:30:02.236 * Unable to partial resync with replica 127.0.0.2:6380 for lack of backlog (Replica request was: 29).
14422:M 16 Jul 2019 18:30:02.236 * Starting BGSAVE for SYNC with target: disk
14422:M 16 Jul 2019 18:30:02.236 * Background saving started by pid 14445
14445:C 16 Jul 2019 18:30:02.269 * DB saved on disk
14422:M 16 Jul 2019 18:30:02.342 * Background saving terminated with success
14422:M 16 Jul 2019 18:30:02.344 * Synchronization with replica 127.0.0.2:6380 succeeded

slave_1.out

14423:S 16 Jul 2019 18:29:43.191 * MASTER <-> REPLICA sync started
14423:S 16 Jul 2019 18:30:02.235 * Non blocking connect for SYNC fired the event.
14423:S 16 Jul 2019 18:30:02.235 * Master replied to PING, replication can continue...
14423:S 16 Jul 2019 18:30:02.236 * Trying a partial resynchronization (request 627249f633a6947b54de20d99737c54e2943a53f:29).
14423:S 16 Jul 2019 18:30:02.237 * Full resync from master: 627249f633a6947b54de20d99737c54e2943a53f:1837925
14423:S 16 Jul 2019 18:30:02.237 * Discarding previously cached master state.
14423:S 16 Jul 2019 18:30:02.342 * MASTER <-> REPLICA sync: receiving 898012 bytes from master
14423:S 16 Jul 2019 18:30:02.347 * MASTER <-> REPLICA sync: Flushing old data
14423:S 16 Jul 2019 18:30:02.347 * MASTER <-> REPLICA sync: Loading DB in memory
14423:S 16 Jul 2019 18:30:02.390 * MASTER <-> REPLICA sync: Finished with success

为什么上面两次24KB和1M数据的同步方式不一样呢?主实例上的一段内存(环形缓冲区,称为replication backlog)会跟踪最近所有写入命令,这个缓冲区实际上是一个固定长度的列表。redis使用这个backlog缓冲区来决定同步方式(主实例接收从实例请求中的offset和master_replid,判断master_replid是否匹配,再判断最后一个offset是否能从backlog缓冲区取到),默认的缓冲区大小是1M。
有时在数据写入量较大的时候,超过了缓冲区容量,部分重新同步会被拒绝,而启动完全重新同步。所以我们一般要根据自己的实际情况,将缓冲区大小repl_backlog_size设为流量写入峰值期间的合适大小。我们可以在峰值期间利用info replication查看master_repl_offset的变化量从而进行估算。

其他优化参数:
Repl-backlog-ttl:如果所有的从实例与主实例全部断开,主实例释放backlog占用的内存的等待时间,默认3600s;
Repl-disable-tcp-nodelay:设为yes,redis会尝试将几个小包合并成一个包来减少带宽,这在主从实例位置较远的时候有些作用,可能会造成约40ms的复制延迟;
Repl-disless-sync:设为yes来将复制机制从默认的disk-backed切换为dissless,就是使用无盘复制,直接将rdb文件内容发送给从实例无需在磁盘上创建RDB文件,可以节省磁盘I/O和内存,在磁盘速度不快或内存使用率很高的情况下可以使用此选项。
Repl-ping-slave-period:主实例默认每隔10s(可通过Repl-ping-slave-period配置)向从实例发送一个PING命令判断从实例是否正常运行,而从实例每秒会向主实例发送RELICONF ACK {offset}来报告它的复制偏移量。
Repl-timeout:主从实例间没有数据传输流量即ACK传输,或两次PING的时间间隔时间比超时时间长,那么主从实例之间的复制连接将被断开,下次要进行重新的复制连接。我们应尽量避免长时间的阻塞操作。
Client-output-buffer-limit:主从复制建立时,主实例加载rdb文件到缓冲区(Slave client buffer),并将缓冲区内容发送给从实例,默认slave 256 MB 64 MB 60,含义是当缓冲区大小超过64MB持续60时,主实例会关闭连接。

持久化简单介绍

持久化文件通常用于存储Redis数据,以防意外可用与恢复Redis数据。

RDB

RDB介绍

Redis通过持久化将内存中的数据存储到rdb文件中,相当于保存了当时的数据库快照。
启用RDB持久化config set save “900 1”,表示 900 秒内有 1 个键发生了改变就会进行一次RDB快照;禁用RDB持久化config set save “”。永久设置就去redis.conf中设置。
save/bgsave:手动执行RDB转储,bgsave是非阻塞的。
info persistence获取持久化相关指标和状态

RDB文件
127.0.0.1:6379> hset hs k1 v1 k2 v2
(integer) 2
127.0.0.1:6379> lpush listkey v1 v2
(integer) 2
127.0.0.1:6379> zadd zset 1 v1 2 v2
(integer) 2
127.0.0.1:6379> save
OK

安装bvi(yum install bvibrew install bvi)
打开redis数据目录中的文件bvi dump.rdb
redis详解_第5张图片

REDIS0009:rdb文件版本
redis-ver:Redis实例的版本
redis-bits:运行redis实例的主机的架构
ctime:RDB创建时的Unix时间戳
used-mem:转储时使用的内存大小
repl_stream_db:复制链中数据库的索引,启用了复制机制才存在
aof-preamble:是否在AOF文件的开头放置RDB快照(即开启混合持久化)
repl-id:主实例的ID
repl-offset:主实例的偏移

FA 为辅助操作码,其后总跟一个键值对。
RDB文件中,保存了数据库的元数据之后,还将保存数据库索引(FE 00,索引为0)、哈希大小调整代码、数据库大小调整操作代码,以及过期大小调整操作代码。
RDB文件以EOF和CRC64校验结束。

AOF

AOF介绍

一个RDB数据转储文件仅包含某个时间点上的数据快照,虽然我们可以定期地将数据转储到RDB文件中,但当Redis进程崩溃或出现故障时,两次RDB转储之间的数据将会永久丢失。
AOF(append-only file)是一种只记录Redis写入命令的追加式日志文件。因为每个写入命令都会被追加到文件中,所以AOF的数据一致性比RDB更高。

启用AOF持久化config set appendonly yes,禁用RDB持久化config set appendonly yes。永久设置就去redis.conf中设置。
info persistence获取持久化相关指标和状态。
查看redis数据目录中的是否存在文件appendonly.aof,这是默认文件名,可修改。

AOF文件写入:操作系统维护一个缓冲区,Redis的命令先写入缓冲区,操作系统会等缓冲区被填满或超过指定时限后再将缓冲区中的数据写入磁盘,如果计算机发生停机,那么缓冲区中的数据将会丢失。Redis可以通过系统调用 fsync() 立即将缓冲区数据刷新到磁盘中,这是一个阻塞调用,只有磁盘设备报告缓冲区中的数据写入结束后才会返回。
可以通过配置参数appendfsync调整调用fsync()的频率:
always:每个写入命令都调用fsync()。服务器崩溃或故障时只会丢失最后一个命令,但阻塞调用会降低redis的写入性能。
everysec:每秒调用一次fsync()。在意外灾难事件中只有一秒的数据会丢失。
no:永不调用fsync()。
当redis服务器关闭时,fsync()会被显示调用,以确保写入缓冲区的所有数据都会被刷新到磁盘中。

AOF文件
redis详解_第6张图片

文件的格式就是Redis通信协议所使用的RESP格式。
开头的SELECT 0:表示选择索引为0的数据库。Redis支持多个数据库,这些数据库由0到15的数字指定,默认的数据库索引是0。
当文件越来越大,通过 AOF write (bgrewriteaof)来压缩AOF文件。压缩规则例如:已经删除的键的相关命令会被删除,多个set命令会用一个mset命令替代等。

RDB和AOF结合使用

有两种使用方式

  • 导入一些测试数据,然后调用save命令创建RDB文件,bgrewriteaof命令重写AOF文件,会发现数据同时以RDB和AOF的形式保存了。
  • 设置开启混合持久化功能,将参数aof-use-rdb-preamble设为yes,导入一些测试数据,然后调用save命令创建RDB文件,bgrewriteaof命令重写AOF文件,RDB文件会正常保存,但在重写AOF文件时,会把当前数据集以RDB的格式转储到AOF文件的开始部分,在重写之后,Redis会继续使用传统的AOF格式在AOF文件中记录写入命令。

高可用和集群—Sentinel

前面说的主从复制和持久化都是只是做到数据备份,但当redis主实例宕机,却无法自动恢复。我们需要用redis集群来实现高可用。Sentinel机制是redis提供的Redis Cluster方案。

Sentinel介绍

Sentinel(哨兵)充当主从实例的守卫者,单个哨兵也可能失效,且对主实例的故障迁移的决策是基于冲裁系统的,所以至少需要3个哨兵才能组成一个健壮的分布式系统来持续监控Redis主实例的状态。
如果有多个哨兵进程检测到主实例下线,其中的一个哨兵进程会被选举出来负责推选一个实例替代原有实例。

Sentinel实践

Sentinel配置
redis详解_第7张图片

启动一主两从实例,三个Sentinel实例

启动实例方式不再赘述。
redis-server --port 6379 --bind 127.0.0.1 &
redis-server --port 6380 --bind 127.0.0.2 &
redis-server --port 6381 --bind 127.0.0.3 &

echo slaveof 127.0.0.1 6379 | nc 127.0.0.2 6380
echo slaveof 127.0.0.1 6379 | nc 127.0.0.3 6381

下面说下启动Sentinel方式:
Sentinel.conf原文件可从源码中复制出来进行修改,我的 sentinel1.conf 配置如下:

bind 127.0.0.1 127.0.0.2 127.0.0.3
port 26379
daemonize no
pidfile /var/run/redis-sentinel.pid
logfile ""
dir /tmp
# 至少2个sentinel确认master实例已下线才可判断该master实例已下线,可增加行监听新的主实例
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
# 最多有1个slave同时对新的master进行同步,设为1确保不会有多个salve因replication不可用
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
sentinel deny-scripts-reconfig yes

启动sentinel

redis-server /usr/local/redis/etc/sentinel1.conf --sentinel
redis-server /usr/local/redis/etc/sentinel2.conf --sentinel
redis-server /usr/local/redis/etc/sentinel3.conf --sentinel

启动好之后再查看 sentinel1.conf 的内容,可以看出被添加了几行信息。在之后我们对Sentinel配置的更改也会同步更改到这个文件里并且带上版本号,方便我们下次可以使用最新的配置来启动

cat sentinel1.conf 

bind 127.0.0.1 127.0.0.2 127.0.0.3
port 26379
daemonize no
pidfile "/var/run/redis-sentinel.pid"
sentinel myid a468bd3204a7c709a38111d1c6643a5dee774434
sentinel deny-scripts-reconfig yes
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
# Generated by CONFIG REWRITE
dir "/Users/bijiayang/Desktop/file/my/study/redis"
sentinel known-replica mymaster 127.0.0.2 6380
sentinel known-sentinel mymaster 127.0.0.1 26381 021970dffc4f7723164b94878230075d96a46f2d
sentinel known-sentinel mymaster 127.0.0.1 26380 c31eda3f098ef5cb48b5e72f5f551c880b0cbcec
sentinel current-epoch 0

查看哨兵信息,最后一行可以看出检测到了主实例mymaster,2个从实例和3个哨兵:

redis-cli -h 127.0.0.1 -p 26379

127.0.0.1:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3

sentinel进程如何检测到从服务器和其他哨兵?
redis详解_第8张图片

Sentinel简单测试
  1. 手动触发主实例故障迁移
    连接哨兵 sentinel2
    127.0.0.2:26380> sentinel failover mymaster
    查看原来的主实例info replication,已经进行了故障转移,现在变成了从实例
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:127.0.0.2
master_port:6380
...

查看Sentinel1.conf,可以看出主实例的地址变成了127.0.0.2 6380

cat sentinel1.conf 

bind 127.0.0.1 127.0.0.2 127.0.0.3
port 26379
daemonize no
pidfile "/var/run/redis-sentinel.pid"
sentinel myid a468bd3204a7c709a38111d1c6643a5dee774434
sentinel deny-scripts-reconfig yes
sentinel monitor mymaster 127.0.0.2 6380 2
sentinel config-epoch mymaster 1
sentinel leader-epoch mymaster 0
# Generated by CONFIG REWRITE
dir "/Users/bijiayang/Desktop/file/my/study/redis"
sentinel known-replica mymaster 127.0.0.3 6381
sentinel known-replica mymaster 127.0.0.1 6379
sentinel known-sentinel mymaster 127.0.0.1 26381 021970dffc4f7723164b94878230075d96a46f2d
sentinel known-sentinel mymaster 127.0.0.1 26380 c31eda3f098ef5cb48b5e72f5f551c880b0cbcec
sentinel current-epoch 1

故障迁移过程

  • 当一个主实例下线时,会自动推举出一个领头Sentinel
    每个Sentinel向其他Sentinel发送请求称为领头Sentinel的命令,先到先得,超过半数的Sentinel支持才可成为领头Sentinel。没有Sentinel获得超半数的支持,则重新进行选举。
  • 由领头Sentinel来选出新的主实例
    过滤掉不符合的服务器(下线状态、近5s内未回复过领头Sentinel、超过指定时间与主服务器断开连接)之后,选出优先级最高的服务器,若优先级相同,则选出复制偏移量最大(保存着最新数据)的服务器,若复制偏移量相同,则选出运行ID最小的服务器。
    redis详解_第9张图片
  1. 模拟主实例下线
    经过上面的手动触发故障迁移,现在的主实例为127.0.0.2 6380,我们用SHUTDOWN关闭这个主实例。
    查看另外两个实例的状态,127.0.0.3:6380已经被推举为了主实例:
127.0.0.2:6380> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.3,port=6381,state=online,offset=759564,lag=1
...


127.0.0.3:6381> info replication
# Replication
role:slave
master_host:127.0.0.2
master_port:6380
...
  1. 模拟两个从实例下线
    我们先设置min-slave-to-write为 1 ,即我们写入数据的主实例至少要有一个从实例存在才允许写入,然后关闭两个从实例,只剩一个主实例在运行,我们是无法对主实例进行写入操作的。
    这样可以避免:主实例被网络隔离但可以进行写入,另外两个实例有一个被推举为主实例,然后原来的主实例恢复网络时已经变成了从实例,在网络隔离期间对旧的主实例写入的数据将丢失。
Sentinel管理

使用redis-cli连接到其中一个哨兵,对其进行管理。

| 工作项目 | 状态 |
|–|–|–|
|sentinel get-master-addr-by-name |获取当前主实例信息|
|sentinel masters|获取所有被监控的主实例的状态|
|sentinel slaves |获取一个被监控主实例的所有从实例的信息|
|sentinel set mymaster down-after-milliseconds 1000|更新哨兵的配置|
|sentinel set mymaster notification-script mymaster notify.py|每当有Sentinel事件发生时,就会触发脚本,脚本可用于发送邮件等|
|sentinel set mymaster client-reconfig-script reconfig.bash|发生故障迁移时,就会触发脚本,脚本可用于一些配置的更新等|

Redis Cluster

当Redis中的数量急剧增长时,必须对其进行分区,我可以利用Redis Cluster(Redis集群)实现自动的数据分片(sharding)和高可用。

redis详解_第10张图片

配置以上集群步骤如下:

  1. 启动6个实例
    其中一个实例配置文件如下:
daemonize yes
pidfile “/redis/run/redis-6379.pid”
port 6379
bind 127.0.0.1 127.0.0.2 127.0.0.3
logfile “/redis/log/redis-6379.log”
dbfilename “dump-6379.rdb”
dir “/redis/data”
...
cluster enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 10000

cluster-config-file配置实例需要覆写的配置文件路径,实例启动好后,会发现配置的文件被修改了。
2. 确认所有实例启动好,redis-cli 连接某一个实例,执行cluster meet命令让每个redis实例发现彼此。

127.0.0.1:6379> cluster meet 127.0.0.1 6379
127.0.0.1:6379> cluster meet 127.0.0.1 6380
127.0.0.1:6379> cluster meet 127.0.0.2 6379
127.0.0.1:6379> cluster meet 127.0.0.2 6380
127.0.0.1:6379> cluster meet 127.0.0.3 6379
127.0.0.1:6379> cluster meet 127.0.0.3 6380
  1. 分配数据槽(slot),只需给主实例分配数据槽。数据key被按照HASH_SLOT = CRC16(key) mod的算法分布到16384个哈希槽中,当前节点只能处理当前节点分配到的槽,数据key所属槽不是当前节点的,会返回错误并指引用户去负责此槽的节点。
    当数据库中的16384个槽都有节点在处理时,集群才处于上线状态。
for i in {0..5400}; do redis-cli -h 127.0.0.1 -p 6379 cluster addslots $i; done
for i in {5401..11000}; do redis-cli -h 127.0.0.2 -p 6380 cluster addslots $i; done
for i in {11001..16383}; do redis-cli -h 127.0.0.3 -p 6381 cluster addslots $i; done
  1. 分配主实例的从实例,redis-cli 连接每个需要设为从实例的实例,为其设置主实例的节点ID,来确认主从关系。
cluster replicate node-id

以上四步就完成了Redis集群的配置,我们可以通过cluster nodes查看主从复制状态,cluster info获取集群相关信息。

你可能感兴趣的:(redis,redis)