Redis高并发高可用的分布式缓存
Redis分布式缓存
单节点Redis存在问题:
1 | -数据丢失问题: 内存存储, 服务重启可能丢失数据 |
Redis持久化
RDB持久化
RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。
快照文件称为RDB文件,默认是保存在当前运行目录。
执行时机
RDB持久化在以下四种情况下会执行:
执行save命令
执行bgsave命令

Redis停机时
Redis停机时会执行一次save命令,实现RDB持久化。
触发RDB条件时
1
2
3
4# redis.conf 文件
save 3600 1
save 300 100
save 60 100003600秒内,如果至少有1个key被修改,则执行bgsave , 如果是
save ""则表示禁用RDB
其他配置
1 | # bgsave报错后停止接收写操作 |
RDB操作原理
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。用新RDB文件替换旧的RDB文件
fork采用的是copy-on-write技术:
当主进程执行读操作时,访问共享内存;
当主进程执行写操作时,则会拷贝一份数据,执行写操作。避免影响子进程

缺点
- RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险
- fork子进程、压缩、写出RDB文件都比较耗时
AOF持久化
AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。

AOF配置
1 | # 是否开启AOF功能,默认是no |
三种策略对比:
| 配置项 | 刷盘时机 | 优点 | 缺点 |
|---|---|---|---|
| Always | 同步刷盘 | 可靠性高,几乎不丢数据 | 性能影响大 |
| everysec | 每秒刷盘 | 性能适中 | 最多丢失1秒数据 |
| no | 操作系统控制 | 性能最好 | 可靠性较差,可能丢失大量数据 |
AOF文件重写
因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。

1 | 如图,AOF原本有三个命令,但是set num 123 和 set num 666都是对num的操作,第二次会覆盖第一次的值,因此第一个命令记录下来没有意义。 |
Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置:
1 | # AOF文件比上次文件 增长超过多少百分比则触发重写 |
RDB与AOF对比
RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用。
| RDB | AOF | |
|---|---|---|
| 持久化方式 | 定时对整个内存做快照 | 记录每一次执行的命令 |
| 数据完整性 | 不完整, 两次备份之间会丢失 | 相对完整, 取决于刷盘fsync策略 |
| 文件大小 | 会有压缩, 文件体积小 | 记录命令, 文件体积很大 |
| 宕机恢复速度 | 很快 | 慢 |
| 数据恢复优先级 | 低, 因为数据完整性不如AOF | 高, 因为数据完整性更高 |
| 系统资源占用 | 高, 大量CPU和内存消耗 | 低, 主要是磁盘IO资源, 但AOF重写时会占用大量CPU和内存资源 |
| 使用场景 | 可以容忍数分钟的数据丢失, 追求更快的启动速度/异地容灾 | 对数据安全性要求较高常见 |
Redis主从
单机安装Redis
修改redis.conf文件
1 | # 绑定地址,默认是127.0.0.1,会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问 |
搭建集群

集群包含三个节点, 一个主节点, 两个从节点:
| IP | PORT | 角色 |
|---|---|---|
| 192.168.253.138 | 16379 | master |
| 192.168.253.136 | 16379 | replica |
| 192.168.253.137 | 16379 | replica |
修改redis.conf文件
1 | # 开启RDB |
开启主从关系
现在三个实例还没有任何关系,要配置主从可以使用replicaof 或者slaveof(5.0以前)命令。
有临时和永久两种模式:
修改配置文件(永久生效)
在redis.conf中添加一行配置:
slaveof <masterip> <masterport>修改从节点redis.conf文件
1
2
3
4# 开启主从
salveof 192.168.253.138 16379
# 主服务器密码
masterauth xxxxxxxx使用redis-cli客户端连接到redis服务,执行slaveof命令(重启后失效):
1
replicaof <masterip> <masterport>
在5.0以后新增命令
replicaof,与salveof效果一致。
查看主从状态
1 | # 连接 192.168.253.138 16379 |

数据同步原理
主从同步由slave服务器发起, 总是请求部分同步(psync), 由master服务器判断全量还是部分同步

主从同步参数
slave服务器会携带 ReplicationId 和 offset , 供master服务器判断是否为第一次同步, 是否需要同步
Replication Id
简称
replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replidoffset
偏移量,随着记录在
repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。

主从同步流程
slave节点请求增量同步
master节点判断
replid,发现不一致,拒绝增量同步master将完整内存数据生成RDB,发送RDB到slave
slave清空本地数据,加载master的RDB
master将RDB期间的命令记录在
repl_baklog,并持续将log中的命令发送给slaveslave执行接收到的命令,保持与master之间的同步
repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步。
主从同步配置优化
在master中配置
repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO。Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
适当通过配置
repl-backlog-size提高repl_baklog的大小,默认1mb, 发现slave宕机时尽快实现故障恢复,尽可能避免全量同步限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力
Redis哨兵
Redis提供了哨兵(Sentinel)机制实现主从集群的自动故障恢复
哨兵的作用及原理
哨兵作用
监控
Sentinel 会不断检查您的master和slave是否按预期工作
自动故障恢复
如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
需要将预设master提前配置密码(
masterauth xxxxxx), 否则宕机修复后会重复请求当前master通知
Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
服务状态监控
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
主观下线(
sdown):如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。客观下线(
odown):若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。

选举机制
一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
1 | - 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点 |
故障转移
当选中了其中一个slave为新的master后(例如slave1),故障的转移的步骤如下:
1 | - sentinel给备选的slave1节点发送slaveof no one命令,注释redis.conf文件中的原有slaveof数据, 让该节点成为master |

搭建哨兵集群
搭建一个三节点形成的Sentinel集群,来监管之前的Redis主从集群:

三个sentinel实例信息如下:
| 节点 | IP | PORT |
|---|---|---|
| s1 | 192.168.253.141 | 26379 |
| s2 | 192.168.253.142 | 26379 |
| s3 | 192.168.253.143 | 26379 |
修改sentinel.conf配置文件
1 | vi /usr/local/src/redis-6.2.6/sentinel.conf |
1 | # 指定sentinel实例服务端口 |
sentinel monitor mymaster 192.168.253.138 16379 2:指定主节点信息
mymaster:主节点名称,自定义,任意写192.168.253.138 16379:主节点的ip和端口2:选举master时的quorum值
启动
1 | /usr/local/bin/redis-sentinel /usr/local/src/redis-6.2.6/sentinel.conf |


Redis分片集群
主从/哨兵机制可以解决高可用、高并发问题. 但仍然存在海量数据存储问题和高并发写情况下的主从同步问题, 使用Cluster分片集群可以很好地解决问题

分片集群特征:
集群中有多个master,每个master保存不同数据
每个master都可以有多个slave节点
master之间通过ping监测彼此健康状态
客户端请求可以访问集群任意节点,最终都会被转发到正确节点
搭建分片集群
提供六台服务器, 安装Redis服务, 模拟分片集群
| IP | PORT | 角色 |
|---|---|---|
| 192.168.253.136 | 16379 | master |
| 192.168.253.137 | 16379 | master |
| 192.168.253.138 | 16379 | master |
| 192.168.253.141 | 16379 | slave |
| 192.168.253.142 | 16379 | slave |
| 192.168.253.143 | 16379 | slave |
配置redis.con文件
1 | port 16379 |
启动分片, 创建集群
我们需要执行命令来创建集群,在Redis5.0之前创建集群比较麻烦,5.0之后集群管理命令都集成到了redis-cli中。
Redis5.0之前
Redis5.0之前集群命令都是用redis安装包下的src/redis-trib.rb来实现的。因为redis-trib.rb是有ruby语言编写的所以需要安装ruby环境。
1
2
3安装依赖
sudo apt install zlib ruby rubygems
gem install redis然后通过命令来管理集群:
1
2
3
4进入redis的src目录
cd /usr/local/src/redis-6.2.6/src
创建集群
./redis-trib.rb create --replicas 1 192.168.253.136:16379 192.168.253.137:16379 192.168.253.138:16379 192.168.253.141:16379 192.168.253.142:16379 192.168.253.143:16379 -a 123456Redis5.0以后
集群管理以及集成到了redis-cli中,格式如下:
1
2
3
4重启redis, 读取配置
sudo systemctl restart redis
执行分片命令
redis-cli --cluster create --cluster-replicas 1 192.168.253.136:16379 192.168.253.137:16379 192.168.253.138:16379 192.168.253.141:16379 192.168.253.142:16379 192.168.253.143:16379 -a 123456命令说明:
redis-cli --cluster或者./redis-trib.rb:代表集群操作命令create:代表是创建集群--replicas 1或者--cluster-replicas 1:指定集群中每个master的副本个数为1,此时节点总数 ÷ (replicas + 1)得到的就是master的数量。因此节点列表中的前n个就是master,其它节点都是slave节点,随机分配到不同master

查看集群状态
1 | redis-cli -c -h 192.168.253.136 -p 16379 -a 123456 |

1 | redis-cli -c -h 192.168.253.136 -p 16379 -a 123456 cluster nodes |

错误处理
1 | 报错 (error) CLUSTERDOWN Hash slot not served |
关闭集群
1 | printf '%s\n' 136 137 138 141 142 143 | xargs -I{} -t redis-cli -h 192.168.253.{} -p 16379 -a 123456 shutdown |

散列插槽
运行原理
Redis会把每一个master节点映射到0~16383共16384个插槽hash slot上,查看集群信息时就能看到:

数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:
- key中包含”
{}“,且“{}”中至少包含1个字符,“{}”中的部分是有效部分 - key中不包含“
{}”,整个key都是有效部分
实例
例如:key是num,那么就根据num计算,如果是{order}num,则根据order计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。

总结
Redis如何判断某个key应该在哪个实例?
1 | - 将16384个插槽分配到不同的实例 |
如何将同一类数据固定的保存在同一个Redis实例?
1 | 使用相同的有效部分,例如key都以{typeId}为前缀 |
集群伸缩
集群控制通过使用redis-cli --cluster 命令执行, 具体参数查看help文档
1 | redis-cli --cluster help |
1 | Cluster Manager Commands: |
删除节点
1 | redis-cli -p 16379 -a 123456 --cluster del-node 192.168.253.136:16379 3926c0d86253cd6ee7c347c1ee3bff6fa6ea2286 |

添加节点
1 | redis-cli -h 192.168.253.138 -p 16379 -a 123456 --cluster add-node 192.168.253.136:16379 192.168.253.138:16379 |

1 | 分配插槽 |

故障转移
master节点宕机
当节点宕机后, cluster集群会自动执行故障转移
1 | 监听分片节点状态 |
节点疑似宕机

节点确定下线, 下属slave 143节点提升为master节点

重启宕机节点
1 | 137节点重启 |
重启后137节点变为slave

恢复master节点身份
手动故障转移, 利用cluster failover命令可以手动让集群中的某个master宕机,切换到执行cluster failover命令的这个slave节点,实现无感知的数据迁移。其流程如下:

1 | redis-cli -h 192.168.253.137 -p 16379 -a 123456 cluster failover |

1 | 手动的Failover支持三种不同模式: |
多级缓存
多层缓存框架:

OpenResty
OpenResty® 是一个基于 Nginx的高性能 Web 平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点:
具备Nginx的完整功能
基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块
允许使用Lua自定义业务逻辑、自定义库
1 | 官方网站: https://openresty.org/cn/ |
安装及使用介绍请查看本文档集合 Nging中的Lua操作简介 - ngx_lua模块 - OpenResty文档
实现请求接口访问路由
Nginx代理服务器配置
修改
nginx/nginx.conf1
vi /etc/nginx/nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# 反向代理
upstream nginx-cluster{
hash $request_uri; # 采取hash策略, 确保同一URI缓存命中
server 192.168.253.136;
server 192.168.253.137;
}
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
location /api {
proxy_pass http://nginx-cluster;
}
}
}Nginx负载均衡服务器配置
OpenResty监听请求
1
vi /usr/local/openresty/nginx/nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# 加载lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
# 加载c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m;
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
location ~ /api/item(\s+) {
# 响应类型,这里返回json
default_type application/json;
# 响应数据由 lua/item.lua这个文件来决定
content_by_lua_file lua/item.lua;
}
}
}
编写Lua脚本
在负载均衡服务器上编写Lua脚本实现业务
1.预先封装通用函数
1 | -- vi /usr/local/openresty/lualib/common.lua |
2.实现服务Lua脚本
查询顺序: Nginx本地缓存->Redis缓存->服务缓存->数据库
1 | vi /usr/local/openresty/lualib/item.lua |
1 | -- 引入cjson模块 |
缓存预热
可以通过服务代码编写, 在项目启动时执行, 将统计热点数据存入Redis缓存,
也可以通过编写Lua脚本预热数据
缓存同步
缓存同步策略
缓存数据同步的常见方式有三种:
设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新
优势:简单、方便
缺点:时效性差,缓存过期之前可能不一致
场景:更新频率较低,时效性要求低的业务
同步双写:在修改数据库的同时,直接修改缓存
优势:时效性强,缓存与数据库强一致
缺点:有代码侵入,耦合度高;
场景:对一致性、时效性要求较高的缓存数据
异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
- 优势:低耦合,可以同时通知多个缓存服务
- 缺点:时效性一般,可能存在中间不一致状态
- 场景:时效性要求一般,有多个服务需要同步
实现方式
基于MQ的异步通知流程

基于Canal的异步通知流程
Canal- 伪装为数据库slave节点, 依据监听binlog, 实现修改数据后同步缓存的动作

Canal
**Canal **,译意为水道/管道/沟渠,canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。GitHub的地址:https://github.com/alibaba/canal
Canal是基于mysql的主从同步来实现的,MySQL主从同步的原理如下:
1 | - MySQL master 将数据变更写入二进制日志( binary log),其中记录的数据叫做binary log events |
Canal就是把自己伪装成MySQL的一个slave节点,从而监听master的binary log变化。再把得到的变化信息通知给Canal的客户端,进而完成对其它数据库的同步。

目标数据库配置
配置文件修改
1
2
3
4
5
6
7
8# $ sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf
= 100
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 10
max_binlog_size = 100M
#binlog_do_db = include_database_name
binlog_do_db = redis_project
# $ sudo service mysql restart配置解读:
log-bin=/var/lib/mysql/mysql-bin.log:设置binary log文件的存放地址和文件名binlog-do-db=redis_project:指定对哪个database记录binary log events
创建Canal专用用户, 设定权限
1
2
3create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
FLUSH PRIVILEGES;查看master状态
1
show master status;

安装Canal
1 | 安装Java环境 |
1 | wget https://github.com/alibaba/canal/releases/download/canal-1.1.5/canal.deployer-1.1.5.tar.gz |
配置Canal
1 | # $ vi /usr/local/src/canal/conf/canal.properties |
1 | # $ vi /usr/local/src/canal/conf/example/instance.properties |
canal.instance.filter.regex=:要监听的表名称. 表名称监听支持的语法:
1 | mysql 数据解析关注的表,Perl正则表达式. |
运行Canal
1 | cd /usr/local/src/canal |
查看运行状态


代码处理
1 | composer require xingwenge/canal_php |
1 |
|
修改数据库数据显示

最终方案流程
