Redis分布式缓存

Redis集群

单节点Redis问题

  • 数据丢失问题:redis是内存存储,服务重启可能会导致数据丢失
  • 并发能力问题:redis本身具有较强的并发能力,但是无法满足高并发的场景
  • 故障恢复问题:Redis宕机会导致服务不可用,需要一种自动故障恢复的方法
  • 存储能力问题:Redis单节点存储数据量难以满足海量数据需求

image-20230108005158866

Redis持久化

RDB持久化

RDB(Redis Database Backup file-Redis数据备份文件,也称Redis数据快照)指将内存中的所有数据记录到磁盘中。当Redis实例故障重启时,从磁盘读取快照文件,恢复数据。

快照文件称为RDB文件,默认保存在当前运行目录中。Redis停机时会执行一次RDB。

image-20230108010846854

RDB文件

关闭服务之前,进行一次RDB文件的保存

image-20230108191819684

查看挂载目录下的RDB文件

image-20230108192014178

Redis配置RDB

Redis内部存在触发RDB的机制,可以在配置文件redis.conf文件中进行配置。

image-20230108011229515

image-20230109001222260

image-20230109001325781

image-20230109001523581

Redis的Fork原理

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。异步完成fork后读取内存数据并写入RDB文件中。

在linux系统中,所有的进程都没有办法直接操作物理内存,操作系统会分配虚拟内存,而主进程只能操作虚拟内存,操作系统会维护一个虚拟内存与物理内存之间的映射关系表(称为页表),bgsave进行fork子进程时,只将页表进行复制。

image-20230109002527682

为了防止同时读写带来的脏数据,fork采用copy-on-write技术

  • 当主进程执行读操作时,访问只读的共享内存空间

  • 当主进程执行写操作时,会拷贝一份数据副本,进行读写操作

image-20230109002542007

总结
RDB的bgsave的基本流程
  • fork主进程得到一个子进程,共享内存空间
  • 子进程读取内存数据并写入新的RDB文件
  • 用新RDB文件替换旧的RDB文件
RDB会执行时间,save命令的含义
  • 默认是服务停止时执行
  • save 60 1000代表60秒内至少执行1000次修改则触发RDB
RDB的缺点
  • RDB执行时间间隔较长,两次RDB之间写入数据存在丢失的风险
  • Fork子进程、压缩、创建RDB文件都是比较耗时

image-20230109003106114

AOF持久化

AOF定义

AOF(Append Only File)是追加文件,Redis处理的每一个写命令都会记录到在AOF文件,可以看作是命令日志文件。

image-20230110090633806

AOF开启配置

AOF默认是关闭状态,在配置文件redis.conf中开启AOF

1
2
3
4
#  开启AOF功能,默认是关闭状态
appendonly yes
# AOF文件名称
appendfilename "appendonly.aof"

通过redis.conf文件来配置AOF命令记录的频率

1
2
3
4
5
6
# 表示每次执行一次写命令,立即记录到AOF文件中
appendfsync always
# 写命令执行完后先放入AOF缓冲区,表示每隔一秒将缓冲区的数据写到AOF文件(默认方案)
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓存区内容写到AOF文件
appendfsync no

image-20230110091731459

appendfsync配置项 刷盘时机 优点 缺点
always 同步刷盘 可靠性高,数据几乎不会丢失 性能影响大
everysec 每秒刷盘 性能适中 可能会丢失1秒的数据
no 操作系统控制 性能最好 可靠性比较差,可能会丢失大量数据

关闭redis,会有一次AOF文件的同步

image-20230110093258645

redis会从AOF文件中进行一次数据加载

image-20230110093514995

由于AOF是记录命令,AOF文件会比RBD文件大,而且AOF会记录同一个key的多次写操作,只有最后一次写操作命令才有意义。

通过brewriteaof命令可以后台开启独立线程异步让AOF文件执行重写功能,用最少的命令达到相同的效果

image-20230110094021351

image-20230110094001829

Redis会在触发阈值时自动重写AOF文件,阈值可以在配置文件redis.conf中进行配置

1
2
3
4
# AOF文件与上一次重写后文件增加超过百分比触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上触发重写
auto-aof-rewrite-min-size 64mb

image-20230110095108606

总结

image-20230110095557170

Redis主从

搭建主从架构

单节点redis的并发能力是存在上限的,要提高redis的并发能力,需要搭建主从集群,实现读写分离

image-20230110100433494

搭建redis从节点

使用docker搭建从节点,端口分别是6378和6377

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
28
29
30
31
32
version: '3.1'
services:
master:
environment:
- TZ=Asia/Shanghai
image: redis
container_name: redis-master
command: redis-server --replica-announce-ip 1.117.34.49 --replica-announce-port 6376 --requirepass 123456 --masterauth 123456 --appendonly yes
ports:
- 6376:6379
volumes:
- /mydata/redis-cluster/redis-master:/data
slave1:
environment:
- TZ=Asia/Shanghai
image: redis
container_name: redis-slave1
command: redis-server --slaveof 1.117.34.49 6379 --requirepass 123456 --replica-announce-ip 1.117.34.49 --replica-announce-port 6377 --masterauth 123456 --appendonly yes
ports:
- 6377:6379
volumes:
- /mydata/redis-cluster/redis-slave1:/data
slave2:
environment:
- TZ=Asia/Shanghai
image: redis
container_name: redis-slave2
command: redis-server --slaveof 1.117.34.49 6379 --requirepass 123456 --replica-announce-ip 1.117.34.49 --replica-announce-port 6378 --masterauth 123456 --appendonly yes
ports:
- 6378:6379
volumes:
- /mydata/redis-cluster/redis-salve2:/data

开启主从关系的两种方式

  • 修改配置文件(永久生效)

    1
    2
    # 在配置文件redis.conf中添加配置
    slaveof <masterip> <masterport>
  • 使用redis-cli客户端,执行slaveof命令(重启失效)

    1
    2
    3
    4
    # 在redis5.0之前的版本执行slaveof命令
    slaveof <masterip> <masterport>
    # 在redis5.0之后的版本执行replicaof命令
    replicaof <masterip> <masterport>

    由于在docker容器内部进行操作,即使在一台主机上,也不能使用localhost代替masterip

image-20230110121320282

进入主节点,查看主从关系

1
INFO REPLICATION

image-20230110123157694

在主节点存储数据,在从节点可以查询

image-20230110123641859

image-20230110123707448

总计

image-20230110104432429

主从复制原理

在2.8版本之前只有全量复制,在2.8版本之后有全量复制和增量复制

  • 全量(同步)复制:第一次同步
  • 增量(同步)复制:会将主从库网络断连期间主库收到的命令,同步给从库

全量同步

当启动多个Redis实例时,它们之间就可以通过replicaof命令(或者slaveof命令)形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步

img

第一阶段:主从库建立连接时,从库发送主库一个psync命令请求数据同步,主库根据这个命令的参数启动辅助,psync命令包括对应replid和偏移量offset两个参数,当replid与主库的replid不一致时,主库会使用全量复制并发送FULLRESYNC响应命令并带上两个参数:replid和offset(偏移量用于记录复制进度)

第二阶段:主库执行bgsave命令,依赖内存快照生成RDB文件并将文件发送到从库,从库接收到RDB文件后,会先清空本地当前数据,加载RDB文件。在主库将文件发送往从库的过程,这个过程时比较耗时的且有可能受网络波动的影响,但主库并不会被阻塞,正常接收请求,为了保证数据一致性,主库会将RDB文件生成后的所有写命令记录到内存中专门的repl_baklog中

第三阶段:将第二阶段记录的repl_baklog中的写命令发送给从库,从库接收到的命令并执行,完成同步。

从节点日志

image-20230110152016410

主节点日志

image-20230110152327231

全量复制总结

image-20230110151505640

增量同步

主从第一次同步是全量同步,如果从节点重启或者网络闪断后,则执行增量同步

img

repl_baklog是环形缓冲区,存储大小存在上限,在从库重启或者网络闪断太久,从库会丢失掉那部分被新的写命令覆盖掉时,无法进行增量同步,从库和主库之间要进行全量复制。

从库记录着自己的relid,每个从库的复制进度不一定相同,从库重连时,主库会根据从库各自的复制进度决定这个从库是进行增量重复还是全量重复。

image-20230110172014196

优化主从复制

优化全量同步性能
  • 在master配置文件中配置repl-diskless-sync-yes启用无磁盘复制,不生成RDB文件直接发送数据给从节点,避免全量同步时的磁盘IO

  • 减少Redis单节点上的内存占用,减少RDB导致过多的磁盘IO和网络IO

尽量减少全量同步
  • 适当提高repl_baklog的大小,发现slave宕机或者网络闪断时,尽快实现故障恢复,尽可能避免全量同步
降低主节点同步压力
  • 限制一个主节点上的从节点的数量,可以采用主-从-从的链式结构,减少master压力

image-20230110174714704

总结

image-20230110174830537

Redis哨兵

slave节点宕机恢复后,可以找到master节点同步数据,但是当master节点发生宕机时,我们需要怎么解决

在Redis主从集群中,哨兵机制是实现主从库自动切换的关键机制,它有效地解决主从复制模式下的故障转移问题

sentinel

哨兵机制(Redis Sentinel)的作用和原理

在Redis2.8版本开始引入Redis Sentinel(Redis哨兵),哨兵的核心功能是实现主从集群的自动故障转移。

哨兵的作用

  • 监控(monitoring):哨兵不断检查master和slave是否按照预期工作
  • 自动故障恢复(automatic failover):如果master不能正常工作,sentinel会将失效master的一个slave升级为新的master并让其他从节点同步新的master,当故障实例恢复后,以新的master作为主节点
  • 配置提供者(configuration provider):在客户端进行初始化时,通过连接哨兵来获得当前redis集群服务的主节点地址
  • 通知(notification):当集群发生故障转移时,哨兵会将最新的变更信息发送给redis客户端

image-20230110235424893

服务状态监控

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令

  • 主观下线:如果某sentinel节点发现实例未在规定时间内响应,则认为该实例主观下线
  • 客观下线:若超过指定数量(quorum:一般为sentinel实例数量的一半)的sentinel认为该实例主观下线,则该实例为客观下线

img

image-20230111093909624

哨兵集群的选举

为了安全性,一般哨兵会搭建分布式集群,作为分布式集群,必然涉及共识问题(即选举问题)

哨兵的选举机制一般是一个简单的Raft算法:选举的票数大于等于num(sentinel)/2+1时,该选举者将成为新的主节点

新主库的选出

master被判定客观下线,sentinel要从剩余的从库中选择一个新的master

  • 过滤掉不健康的(下线或断线),没有响应哨兵ping的slave
  • 过滤掉与master断开时间长短的slave,如果超过指定值(down-after-milliseconds*10),则会直接被排除
  • 选择slave节点中salve-priority值最小的即优先级最高的(在配置文件redis.conf中配置)
  • 选择复制偏移量最大即offset值最大的,越大说明数据越新,复制最完整的从节点
  • 选择slave节点的运行id较小的

img

故障的转移

  • sentinel给选举出的slave节点发送replicaof on one命令,让其脱离从节点,升级为主节点
  • 将其他从节点发送slaveof命令,指向新的主节点,从新的master同步数据
  • 通知应用程序客户端RedisClient主节点的变更信息即新的主节点的地址
  • 修改原主节点即故障节点的配置文件,将其标记为slave,当故障节点恢复后会自动变成新主节点的从节点

img

总结

image-20230111103827196

搭建哨兵集群

image-20230111103943735

创建配置文件

1
2
3
4
5
6
7
port <port>	# 哨兵实例运行端口
sentinel announce-ip <ip> # 哨兵指定ip地址
sentinel monitor <master_name> <master_ip> <master_port> <quonum> # 哨兵指定主节点自定义名称、ip、端口以及用于选举的quonum
sentinel down-after-milliseconds <master_name> 5000 # 主观判定不可达时间
sentinel failover-timeout <master_name> 5000 # 故障转移超时时间
sentinel auth-pass <master_name> <master_password> # 设置master与slave验证密码
dir /tmp # 工作目录

image-20230111110631501

1
2
3
4
5
6
7
8
port 27001
protected-mode no
sentinel monitor mymaster 1.117.34.49 6376 2
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 18000
sentinel auth-pass mymaster 123456
dir /tmp

编写Docker-compose.yml配置文件

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
28
29
version: '3'
services:
sentinel1:
image: redis
restart: always
container_name: sentinel1
ports:
- 27001:27001
command: redis-sentinel /usr/local/etc/redis/sentinel.conf
volumes:
- /mydata/sentinel/conf/sentinel1/:/usr/local/etc/redis/
sentinel2:
image: redis
restart: always
container_name: sentinel2
ports:
- 27002:27002
command: redis-sentinel /usr/local/etc/redis/sentinel.conf
volumes:
- /mydata/sentinel/conf/sentinel2/:/usr/local/etc/redis/
sentinel3:
image: redis
restart: always
container_name: sentinel3
ports:
- 27003:27003
command: redis-sentinel /usr/local/etc/redis/sentinel.conf
volumes:
- /mydata/sentinel/conf/sentinel3/:/usr/local/etc/redis/

文件结构

image-20230116014331859

运行服务

1
2
cd /mydata/sentinel/
docker-compose up -d

image-20230118171244229

测试故障转移

关闭主节点

image-20230116231427217

查看sentinel日志

image-20230116231417144

查看转移的主节点

image-20230116231716032

RedisTemplate哨兵模式

Sentinel集群监管下的Redis主从集群中,其主节点会由于自动故障转移而发生变化,Redis客户端必须感知变化并及时更新连接信息。Spring的RedisTemplate底层利用lettuce实现了节点感知和自动切换。

Spring配置哨兵模式

引入redis的starter依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置文件指定sentinel信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
logging:
level:
io.lettuce.core: debug

redis:
password: 123456
timeout: 10000ms
lettuce:
pool:
max-active: 10 # 连接池最大连接数
max-idle: 8 # 连接池最大空闲连接数
min-idle: 2 # 连接池最小空闲连接数
max-wait: -1ms # 连接池最大阻塞等待时间,负值表示没有限制
sentinel:
master: mymaster # 指定主节点名称
nodes: # 指定redis-sentinel集群信息
- 1.117.34.49:27001
- 1.117.34.49:27002
- 1.117.34.49:27003
password: 123456 # Redis服务器连接密码
配置主从读写分离
1
2
3
4
@Bean
public LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer() {
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}

ReadFrom是读取策略:

  • MASTER:从主节点读取
  • MASTER_PREFERRED:优先从主节点读取,主机点不可用才读取从节点
  • REPLICA:从从节点读取
  • REPLICA_PREFERRED:优先从从节点读取,所有的从节点不可用才读取主节点

image-20230117000659492

Redis分片集群

分片集群

主从集群哨兵模式解决了高可用,高读并发的问题,但是写能力和存储能力无法进行扩展:

  • 海量数据存储问题
  • 高并发写问题

主节点分片集群的可以存储海量数据,同时吸收高写并发能力:

  • 集群中有多个主节点,每个主节点存储不同的数据
  • 每个主节点分片可以有多个从节点
  • 主节点之间通过ping监控彼此健康状态,实现故障转移
  • 客户端请求可以访问集群中任意的节点并且被转发到正确的节点上

image-20230118232513698

docker-compose部署redis集群

编写docker-compose文件
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
version: '3'

services:
redis-master1:
environment:
- TZ=Asia/Shanghai
image: redis
container_name: redis-master1
command: redis-server --port 7000 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --cluster-announce-ip 1.117.34.49 --cluster-announce-port 7000 --cluster-announce-bus-port 17000 --appendonly yes --protected-mode no
volumes:
- /mydata/redis-cluster/redis-master1/data:/data
ports:
- 7000:7000
- 17000:17000

redis-master2:
environment:
- TZ=Asia/Shanghai
image: redis
container_name: redis-master2
command: redis-server --port 7001 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --cluster-announce-ip 1.117.34.49 --cluster-announce-port 7001 --cluster-announce-bus-port 17001 --appendonly yes --protected-mode no
volumes:
- /mydata/redis-cluster/redis-master2/data:/data
ports:
- 7001:7001
- 17001:17001

redis-master3:
environment:
- TZ=Asia/Shanghai
image: redis
container_name: redis-master3
command: redis-server --port 7002 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --cluster-announce-ip 1.117.34.49 --cluster-announce-port 7002 --cluster-announce-bus-port 17002 --appendonly yes --protected-mode no
volumes:
- /mydata/redis-cluster/redis-master3/data:/data
ports:
- 7002:7002
- 17002:17002

redis-slave1:
environment:
- TZ=Asia/Shanghai
image: redis
container_name: redis-slave1
command: redis-server --port 7003 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --cluster-announce-ip 1.117.34.49 --cluster-announce-port 7003 --cluster-announce-bus-port 17003 --appendonly yes --protected-mode no
volumes:
- /mydata/redis-cluster/redis-slave1/data:/data
ports:
- 7003:7003

redis-slave2:
environment:
- TZ=Asia/Shanghai
image: redis
container_name: redis-slave2
command: redis-server --port 7004 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --cluster-announce-ip 1.117.34.49 --cluster-announce-port 7004 --cluster-announce-bus-port 17004 --appendonly yes --protected-mode no
volumes:
- /mydata/redis-cluster/redis-slave2/data:/data
ports:
- 7004:7004

redis-slave3:
environment:
- TZ=Asia/Shanghai
image: redis
container_name: redis-slave3
command: redis-server --port 7005 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --cluster-announce-ip 1.117.34.49 --cluster-announce-port 7005 --cluster-announce-bus-port 17005 --appendonly yes --protected-mode no
volumes:
- /mydata/redis-cluster/redis-slave3/data:/data
ports:
- 7005:7005
  • --cluster-enabled yes:开启集群
  • --cluster-config-file nodes.conf:集群配置文件
  • cluster-node-timeout 5000:节点心跳失败的超时时间
  • --cluster-announce-ip 1.117.34.49:节点的注册实例ip
  • --cluster-announce-port 7001:节点的注册实例端口
    • --cluster-announce-bus-port 17001:节点的注册总线端口
  • --appendonly yes:开启 备份 模式
  • --protected-mode no:关闭 保护 模式
启动集群
1
docker-compose up -d
建立集群
1
docker exec -it redis-master1 redis-cli --cluster create --cluster-replicas 1 1.117.34.49:7000 1.117.34.49:7001 1.117.34.49:7002 1.117.34.49:7003 1.117.34.49:7004 1.117.34.49:700

image-20230605161835616

  • redis-cli --cluster:代表 集群命令操作
  • create:代表 创建集群操作
  • --cluster-replicas 1:代表 指定集群中每个master的副本个数是1,此时节点总数 = 节点总数 / (replicas + 1)即master的数量,其他节点都是slave节点,随机分配到不同的master

image-20230605213210215

查看集群状态
1
redis-cli -p 7000 cluster nodes

image-20230605230352481

散列插槽

Redis会将每一个master节点映射到0-16383(共16364个)插槽(hash slot)中

查看集群信息

image-20230605230511919

数据key不是与节点绑定的绑定的,而是与插槽绑定

image-20230605150922830

  • key中包含”{}”并且”{}”中至少包含一个字符串,则”{}”为有效部分
  • key中不包含”{}”,整个key都是有效部分

例如:当存储的key是 a 时,那么根据 a 计算;如果是{test}a,则根据{a}test计算。计算方式是利用CRC16算法得到一个 Hash 值,然后对于 15495 取余,得到的结果就是 slot 值

1
2
docker exec -it redis-master1 /bin/bash
redis-cli -c -h 1.117.34.49 -p 7000

image-20230605231048064

总结

image-20230605180107940

Redis如何判断某个key应该在哪一个实例

将16384个插槽分配不同的实例,根据key的有效部分计算出哈希值,将哈希值对16384取余,余数作为插槽,寻找插槽所处实例即可

如何将 同一类数据 固定的保持在同一个redis实例

这一类数据使用相同的有效部分({}中部分为有效部分,例如这些数据的key都以 {typeId} 为前缀)

image-20230605232159040

集群伸缩

添加一个节点到集群

操作集群的命令

Redis提供了很多 操作集群 的命令

1
redis-cli --cluster help

image-20230605232421959

新增节点到集群
1
2
3
4
5
6
7
8
9
10
11
12
13
14
version: '3'

services:
redis-master1:
environment:
- TZ=Asia/Shanghai
image: redis
container_name: redis-master4
command: redis-server --port 7006 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --cluster-announce-ip 1.117.34.49 --cluster-announce-port 7006 --cluster-announce-bus-port 17006 --appendonly yes --protected-mode no
volumes:
- /mydata/redis-cluster/redis-add/data:/data
ports:
- 7006:7006
- 17006:17006
1
docker exec -it redis-master1 redis-cli --cluster add-node 1.117.34.49:7006 1.117.34.49:7000

image-20230605233457310

查看集群状态

发现 新增的节点 没有 插槽

1
docker exec -it redis-master1 redis-cli -p 7000 cluster nodes

image-20230605233535709

分配插槽

image-20230605234011814

1
2
# redis-cli --cluster reshard --cluster-from 迁出节点ID --cluster-to 接收节点ID --cluster-slots 迁出槽数量 迁出节点ip 端口
docker exec -it redis-master1 redis-cli --cluster reshard --cluster-from 2728947563b6bbe2905978a00d340fac6961b9d3 --cluster-to 8991b780768b54fd5942006f32c4f562a949d33c --cluster-slots 2765 1.117.34.49 7000

image-20230605234709707

重新查看节点状态
1
docker exec -it redis-master1 redis-cli -p 7000 cluster nodes

image-20230606000656791

故障转移

故障转移

  • 实例与其他实例失去连接
  • 其他实例心跳机制判断该节点是否宕机
  • 确定节点宕机,自动提升一个从节点成为新的主节点

image-20230605235458216

数据迁移

利用 cluster failover命令 手动 让集群中的某个主节点 宕机,切换到执行 cluster failover 命令这个从节点,实现无感知到数据迁移

image-20230606000044589

手动Failover支持的三种不同模式
  • 缺省:默认流程
  • force:省略对于2-3步对于offset的一致性校验
  • takeover:直接执行第5步,忽略数据一致性,忽略master状态和其他master意见

RedisTemplate访问分片分配

RedisTemplate底层同样基于 lettuce 实现了 分片集群 的支持

image-20230606000943355

  • 引入 redis 的 starter 依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 配置分片集群地址

1
2
3
4
5
6
7
8
9
10
11
spring:
redis:
cluster:
nodes:
- 1.117.34.49 7000
- 1.117.34.49 7001
- 1.117.34.49 7002
- 1.117.34.49 7003
- 1.117.34.49 7004
- 1.117.34.49 7005
- 1.117.34.49 7006
  • 配置读写分离

1
2
3
4
@Bean
public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer() {
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}

多级缓存

传统缓存问题

image-20230606004054502

多级缓存方案

多级缓存是充分利用请求处理的每一个环节,添加缓存,减轻Tomcat压力,提升服务器性能

image-20230606113933783

image-20230606114422392

image-20230606114514952

Nginx代理

image-20230606120101026

JVM进程缓存

本地进程缓存

缓存数据的读取速度非常快,能对大量减少对数据库的访问,减少数据库的压力

  • 分布式缓存:例如Redis

优点:集群部署支持数据共享,分片集群确保存储容量大,哨兵机制保证可靠性

缺点:访问缓存存在网络开销和网络延迟

场景:缓存数据量大、可靠性要求高,数据集群共享

  • 进程本地缓存:例如HashMap、GuavaCache

优点:直接读取本地内存,没有网络开销,速度更快

缺点:存储容量有限,可靠性较低,无法共享

场景:性能要求较高,缓存数据量较小

Caffeine

Caffine是基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库

image-20230606121617734

Caffine的使用
引入依赖
1
2
3
4
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
Cache手动创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CACHE = Caffeine.newBuilder()
// 初始数量
.initialCapacity(INITIAL_CAPACITY)
// 最大数量
.maximumSize(CACHE_SIZE)
// 最后一次更新指定过期时间(当expireAfterWrite和expireAfterAccess同时存在,以expireAfterWrite为准)
.expireAfterWrite(EXPIRE_TIME, TimeUnit.SECONDS)
// 最后一次读取指定过期时间
.expireAfterAccess(EXPIRE_TIME, TimeUnit.SECONDS)
// 监听缓存被异常
.removalListener((key, value, cause) -> {
log.info("缓存 {} -> {} 已经被移除", key, value);
})
// 命中统计
.recordStats()
.build();

image-20230606141847447

Caffine的三种缓存驱逐策略
  • 基于容量:设置缓存的 数量上限
1
2
3
4
CACHE = Caffeine.newBuilder()
// 最大数量
.maximumSize(CACHE_SIZE)
.build();
  • 基于时间:设置缓存的 有效时间
1
2
3
4
CACHE = Caffeine.newBuilder()
// 最后一次更新指定过期时间
.expireAfterWrite(EXPIRE_TIME, TimeUnit.SECONDS)
.build();
  • 基于引用:设置缓存为软引用和弱引用,利用GC回收缓存数据(性能较差,不建议使用)

在默认情况下,当一个缓存元素过期时,Caffine不会自动立即将其清理和驱逐,而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐

Lua语法

初始Lua

Lua是一种轻量小巧的脚本语言,用标准的C语言编写并以源代码形式开发,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

image-20230606172134668

变量和循环

数据类型

image-20230606172213633

image-20230606172547233

变量

image-20230606172707493

image-20230606200205411

循环

数组和table都可以使用for循环来遍历

image-20230606200459614

  • 遍历数组
1
2
3
4
5
6
-- 声明数组 key为索引的 table
local arr = {'java', 'pyrhon','lua'}
-- 遍历数组
for index, value in ipairs(arr) do
print(index, value)
end
  • 遍历table
1
2
3
4
5
6
-- 声明map即table
local map = {name = 'jack', age = 21}
-- 遍历table
for key, value in pairs(map) do
print(key, value)
end

条件控制和函数

函数
1
2
3
4
function 函数名(args1, args2...argsn
-- 函数体
return 返回值
end

image-20230606201927218

1
2
3
4
5
6
7
8
9
local arr = {'java', 'pyrhon','lua'}

function printArr(arr)
for index, value in ipairs(arr) do
print(value)
end
end

printArr(arr)
条件控制

image-20230606203011103

1
2
3
4
5
6
if (布尔表达式)
then
-- { 布尔表达式 true 时执行该语句块 }
else
-- { 布尔表达式 false 时执行该语句块 }
end
1
2
3
4
5
6
7
8
9
10
function printArr(arr)
if (not arr) then # nil 表示一个无效值 在 布尔表达式 中默认为 false
print('数组不能为空!')
return nil;
end
for index, value in ipairs(arr) do
print(value)
end
end
printArr(nil)

多级缓存

openResty

OpenResty是一个基于Nginx的高性能Web平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点:

  • 具备具备Nginx的完整功能
  • 基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块
  • 允许使用Lua自定义业务逻辑、自定义库

image-20230606205946157

安装openresty

安装容器
1
docker pull openresty/openresty
运行容器
1
docker run --name openresty -d -p 90:80 openresty/openresty
挂载目录
1
mkdir -p /mydata/openresty/nginx	
1
docker cp openresty:/usr/local/openresty/nginx/  /mydata/openresty/
重新运行容器
1
docker rm -f openresty
1
2
3
docker run --name openrestry -d -p 90:80 \
-v /mydata/openresty/nginx/:/usr/local/openresty/nginx/ \
openresty/openresty

openResty入门

image-20230607095543389

步骤一:修改Nginx.conf文件

image-20230607100526394

在nginx.conf的http中,添加对OpenResty的Lua模块的加载
1
2
3
4
# 加载 lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
# 加载 c 模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
在nginx.conf的server下面,添加对 指定路径 的监听
1
2
3
4
5
6
location /api/product {
# 默认的响应类型(返回的json)
default_type application/json
# 响应数据由 lua/product.lua 文件来指定
content_by_lua_file lua/product.lua
}
步骤二:编写lua文件
在nginx目录创建文件夹lua以及新建lua文件
1
2
3
cd /mydata/openresty/nginx
mkdir lua
touch lua/product.lua
编辑内容
1
2
-- 返回模拟数据(ngx.say()函数用于写入数据到Response中)
ngx.say('{"id":10001,"name":"SALSA AIR"}')
重启加载配置
1
docker restart openresty

请求参数处理

获取参数的API

OpenResty中提供了一些API用来获取不同类型的前端请求参数

image-20230607113139500

路径占位符
1
2
3
4
5
6
7
/product/1
-- 正则表达式匹配
location ~ /product/(\+d) {
content_by_lua_file lua/product.lua;
}
-- 匹配到的参数会存入ngx.var数组中,可以通过 角标 获取
local id = ngx.var[1]
请求头
1
2
3
id:1
-- 获取请求头
local headers = ngx.req.get_headers()
GET 请求参数
1
2
3
?id = 1
-- 获取 get 请求参数 返回值是 table 类型
local param = ngx.req.get_uri_args()
POST 请求参数
1
2
3
4
5
id = 1
-- 获取请求体
ngx.req.read_body()
-- 获取 post 表单参数 返回值 table 类型
local body = ngx.req.get_post_args()
JSON 参数
1
2
3
4
{"id": 1}
-- 读取请求体
ngx.req.read_body()
-- 获取 body 中的 json 参数 返回值 string 类型
示例
路径占位符
1
2
3
4
5
6
7
# nginx.conf
location ~ /api/product/(\d+) {
# 默认的响应类型(返回的json)
default_type application/json
# 响应数据由 lua/product.lua 文件来指定
content_by_lua_file lua/product.lua
}
编辑lua文件
1
2
3
-- product.lua
local id = ngx.var[1]
ngx.say('{"id":'..id ..',"name":"SALSA AIR"}')

查询Tomcat

image-20230607151021993

之前我们通过 nginx 代理转发到 openresty 通过执行 lua 文件,返回模拟数据,在真实的场景中,需要 openresty 请求 tomcat 服务器拿到真实的数据进行返回

image-20230607110852695

nginx内部发送http请求

nginx提供了内部API用以发送http请求

1
2
3
4
5
6
-- product.lua
local resp = ngx.location.capture("/path", {
method = ngx.HTTP_GET, -- 请求方式
args = {a=1, b=2, ...}, -- get 方式传参(param)
body = "c=3&d=4" -- post 方式传参(body)
})
返回的响应内容包括
  • resp.status:响应状态码
  • resp.header:响应头(即table键值对)
  • resp.body:响应体(响应数据)

注意:/path 是路径,并不包括 IP 和 端口,这个请求会被nginx内部的server监听并进行反向代理至 tomcat 服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# nginx.conf
# 匹配 正则 获取 路径和路径占位符
location ~ /path/(\d+) {
# 默认响应类型
default_type application/json;
# 响应结果由lua文件执行
content_by_lua_file lua/product.lua
}

# 执行lua文件 lua文件需要向tomcat发送请求 请求由本身的nginx监听并反向代理至指定的tomcat服务器
location /path {
# 反向代理的路径
proxy_pass http://{host}:{port}
}
封装HTTP请求查询的函数

可以将http查询的请求封装为一个函数,放到OpenResty函数库中,方便后期使用

在/usr/local/openrrsty/lualib目录下创建get.lua文件
1
vi /usr/local/openrrsty/lualib/common.lua
在get.lua中封装http查询的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 封装函数 发送 http 请求 并解析响应
local function read_http(path, params)
local resp = ngx.location.capture(path, {
method = ngx.HTTP_GET,
args = params
}
)
if not resp then
ngx.log(ngx.ERR, "http not found, path:", path, ",args:", args)
ngx.exit(404)
end
end
-- 将 方法 导出
local _M = {
read_http = read_http
}
return _M
示例
1
2
3
4
5
6
7
8
9
10
11
12
-- 导入get函数库(由于文件就在lualib下,无需加上路径名,直接写文件名即可)
local get = require('get')
-- 拿到函数库的方法
local read_http = get.read_http;

-- 获取路径请求参数
local id = ngx.var[1]
-- 调用通用函数
local resp = read_http("/product".. id, nil);

-- 返回参数
ngx.say(resp)
JSON结果处理

OpenResty提供了一个cjson的模块用来处理JSON序列化和反序列化,可以使用此工具完成数据的组合工作

  • 引入cjson模块
1
local cjson = require "cjson"
  • 序列化
1
2
3
4
5
local obj = {
name = 'jack'
age = 22
}
local json = cjson.encode(obj)
  • 反序列化
1
2
3
4
local json = '{"name": "jack", "age": 21}'
-- 反序列化
local obj = cjson.dencode(json);
print(obj.name)

Tomcat集群负载均衡

配置反向代理集群配置
1
2
3
4
5
6
7
8
9
10
11
# 反向代理配置 将/item路径的请求代理到tomcat集群
location /path {
# 反向代理的路径
proxy_pass http://tomcat_cluster
}

# tomcat 集群配置(默认采用 轮询 的方式)
upstream tomcat_cluster {
server {host1}:{port1};
server {host2}:{port2};
}
修改负载均衡策略

由于 默认 的集群模式反向代理是采用轮询的方式进行访问,这就导致了同样参数的请求会落到不同的服务器,由于进程缓存不同步,导致不同的tomcat进程缓存都需加载

修改负载均衡策略,基于用户请求的uri做简单hash,让同一个请求始终到一台服务器上,让进程缓存只需在一台服务器上加载

1
2
3
4
5
upstream tomcat_cluster {
hash $request_uri;
server {host1}:{port1};
server {host2}:{port2};
}

Redis缓存预热

在nginx请求tomcat之前,可以再加一层redis缓存,让请求优先查询缓存

image-20230607163849229

冷启动和缓存预热
冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有商品都在第一次查询时添加到缓存中,可能会给数据库带来较大的压力
缓存预热:面对冷启动的问题,我们可以利用大数据统计用户访问的热点数据,在项目启动时,将这些热点数据提前查询并保存到缓存中。或者数据量比较少时,可以在启动时将所有数据放入缓存中
缓存预热
编写初始化类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class CacheHandler implements InitializingBean {

/**
* 该方法会在 CacheHandler 创建并成员变量都完成初始化后执行
*
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
// 初始化缓存 完成预热

// 查询大数据热点数据列表

// 遍历列表将数据存储到redis缓存中

}
}

查询Redis缓存

OpenResty提供了操作Redis的模块,使用只需要引入该模块就可以直接使用

  • 引入Redis模块并初始化Redis对象
1
2
3
4
5
6
-- 引入 redis 模块
local redis = require("resty.redis")
-- 初始化 redis 对象
local red = redis:new()
-- 设置 redis 超时时间
red:set_timeouts(1000, 1000, 1000)
  • 封装函数(用来释放Redis连接-释放进入连接池)
1
2
3
4
5
6
7
8
9
10
11
-- 关闭 redis 连接的工具方法 其实释放进入连接池
local function close_redis(red)
-- 连接的空闲时间(单位是毫秒)
local pool_max_idle_time = 10000
-- 连接池大小
local pool_size = 100
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.log(ngx.ERR, "放入 redis 连接池失败", err)
end
end
  • 封装函数(从Redis读取数据并返回)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 查询 redis 方法  ip和port是redis地址 key是查询的key
local function redis_read(ip, port, key)
-- 获取一个连接客户端对象
local ok, err = red.connect(ip, port)
if not ok then
ngx.log(ngx.ERR, "连接 redis 失败:", err)
return nil
end
-- 使用 redis客户端对象 获取指定的key的 数据
local resp, err = red.get(key)
-- 查询失败处理
if not resp then
ngx.log(ngx.ERR, "连接 redis 失败:", err, "key =", key)
end
-- 得到的数据为空处理
if resp = ngx.null then
resp = nil
ngx.log(ngx.ERR, "查询 redis 数据为空:", err, "key =", key)
end
close_redis(red)
return resp
end
示例

将上述操作 Redis 客户端的函数封装到通用函数库里(与之前的流程一样)

根据具体业务封装函数
1
2
3
4
5
6
7
8
9
10
11
-- 封装 函数,先查询 redis, redis 未命中 再去请求 tomcat 服务器
local function read_data(key, path, params)
-- 先查询 redis
local resp = redis_read("127.0.0.0", "6379", key)
-- 判断 redis 是否命中
if not resp then
-- redis 未命中 再去请求 tomcat 服务器
resp = read_http(path, params)
end
return resp
end
调用函数
1
2
3
4
5
6
7
8
-- 获取路径参数
local id = ngx.var[1]

-- 调用 函数查询信息
local product = read_data("pms:product:"..id, "/path/".. id, nil)

-- 返回 查询结果
ngx.say(product)

Nginx本地缓存

image-20230608004036209

OpenResty为Nginx提供了shard dict的功能,可以为nginx的多个worker进程之间共享数据(内部共享内存),实现缓存功能

  • 开启共享字典

    在 nginx.conf 的 http 下添加配置

1
2
# 开启共享字典 (本地缓存) 名称为:item_cache, 大小150m
lua_shard_dict item_cache 150m;
  • 操作共享字典
1
2
3
4
5
6
-- 获取 本地缓存对象
local item_cache = ngx.shard.item_cache
-- 存储数据( 指定key、value、过期时间-单位:秒,默认0代表永不过期)
item_cache:set('key', 'value', 1000)
-- 读取数据
local val = item_cache:get('key')

缓存同步

缓存同步策略

image-20230608010649362

基于Canal的异步通知

image-20230608010851585

初始Canal

image-20230608095755843

Canal(译为水道/管道/渠道),Canal是阿里巴巴旗下的一款开源项目,基于Java开发,基于数据库增量日志分析,提供增量数据订阅&消费

Canal是基于 MySQL的主从同步 来实现的,MySQL 主从同步的原理如下:

  • MySQL Master 将数据变更写入到二进制日志(binary log),其中记录的数据为 binary log events
  • MySQL Slave 将 Master 的 binary log events 拷贝到它的中继日志(relay log)
  • MySQL Slave 重放 Relay Log 中事件,将数据变更反映到它自己的数据

Canal 就会将自己 伪装成 一个 MySQL 的一个Slave 节点,从而监听 Master 的 binary log 的变化,再把得到的变化信息通知给 Canal 客户端,进而完成其他数据库的同步

image-20230608100622549

安装Canal

指定binary log文件和database
修改MySQL配置文件

MySQL 配置文件挂载在宿主机的 /mydata/mysql/conf 目录中

1
vim /mydata/mysql/conf/my.cnf
1
2
3
4
# 设置 binary log 文件的存放地址 /var/lib/mysql/ 和文件名 mysql-bin
log-bin = /var/lib/mysql/mysql-bin
# 指定 database 记录 binary log events
binlog-do-db=mall

image-20230608230343587

重启MySQL服务
1
docker restart mysql
查看 /mydata/mysql/data/ 目录下的 binary log 日志文件

image-20230608230547120

设置用户权限
1
2
3
4
5
CREATE user canal@'%' IDENTIFIED BY 'canal';

GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT, SUPER ON *.* TO 'canal'@'%' WITH GRANT OPTION

FLUSH PRIVILEGES;

image-20230608232108177

创建网络并将MySQL容器加入网络
1
docker network create canal_default

image-20230609125417874

1
docker network connect canal_default mysql

image-20230609125504137

创建Canal容器
1
docker pull canal/canal-server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 启动 容器 | 查看配置文件路径 | 拷贝 | 重新启动容器
docker run --name canal -d canal/canal-server

docker exec -it canal bash
cd /home/admin/canal-server/conf/
exit

mkdir -p /mydata/canal/conf/
docker cp canal:/home/admin/canal-server/conf/ /mydata/canal/

docker rm -f canal
docker run --name canal --network canal_default -p 11111:11111 -d \
-v /mydata/canal/conf:/home/admin/canal-server/conf \
-v /mydata/canal/log:/home/admin/canal-server/logs \
canal/canal-server

image-20230609130240354

image-20230609132641562

修改配置文件 instance.properties

image-20230609132211508

重新启动并查看日志
1
2
docker restart canal
tail -f n500 /mydata/canal/log/example/example/log

image-20230609134449190

监听Canal

Canal客户端

Canal提供了各种语言的客户端,当Canal监听到 binlog 变化时,会通知 canal 客户端

image-20230609134850285

Canal 提供了各种语言的客户端,当 Canal 监听到 binlog 变化时,会通知 Canel 的客户端

编写Canal客户端

这里使用 第三方开源的canal-starter

引入依赖
1
2
3
4
5
6
<!-- Canal -->
<dependency>
<groupId>top.javatool</groupId>
<artifactId>canal-spring-boot-starter</artifactId>
<version>1.2.1-RELEASE</version>
</dependency>
编写配置文件
1
2
3
canal:
destination: example # canal 示例名称
server: 1.117.34.49:11111 # canal 地址
编写监听器
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
28
29
30
31
32
33
34
35
@CanalTable(value = "pms_product_category") // 指定监听的表名
@Component //
public class ProductCategoryHandler implements EntryHandler<PmsProductCategory> {

/**
* 监听新增数据并执行相应逻辑
*
* @param pmsProductCategory
*/
@Override
public void insert(PmsProductCategory pmsProductCategory) {

}

/**
* 监听更新数据并执行相应逻辑
*
* @param before
* @param after
*/
@Override
public void update(PmsProductCategory before, PmsProductCategory after) {

}

/**
* 监听删除数据并执行相应逻辑
*
* @param pmsProductCategory
*/
@Override
public void delete(PmsProductCategory pmsProductCategory) {

}
}

Canal 推送给 canal客户端 的是 被修改的这一行数据(row),而我们引入的canal客户端需要我们把数据封建到实体类中,这个过程需要知道数据库与实体类的映射关系,需要用的JPA的几个注解

1
2
3
4
import javax.persistence.*;
@Id // 主键
@Column // 列
@Transient // 无需映射

总结

image-20230609151614437

Redis最佳实践

Redis键值设计

Key结构

image-20230609152806473

Redis的Key可以自定义,但是最好尊循下面几个最佳实践的约定:

  • 遵循基本格式:[业务名称]:[数据名]:[id]
  • 长度不超过44字节
  • 不包括特殊字符

优点:1、可读性强,易于管理、避免冲突;2、节省内存空间:key是string类型,底层编码包括int、embstr和raw三种(embstr在小于44个字节使用,采用连续内存空间,内存占用更小)

拒绝BigKey

image-20230609154724693

BigKey 通常以 Key的大小Key中成员的数量 来综合判定

  • Key 本身的数据量过大
  • Key 中成员数量过多
  • Key 中成员的数据量过大
1
MEMORY USAGE

image-20230609154432582

推荐值
  • 单个Key的Value小于10KB
  • 对于集合类型的Key,建议元素数量小于1000

Bigkey的危害

  • 网络阻塞:对Bigkey执行读请求时,少量的QPS就可能会导致带宽使用率被占满
  • 数据倾斜:BigKey所在的Redis实例内存使用率远超其他实例,无使数据分片的内存资源达到均衡
  • Redis阻塞:对于元素较多的hash、list、zset等做运算耗时较久,使得主线程(Redis是单线程)被阻塞
  • CPU压力:对BigKey的数据序列化和反序列化会导致CPU的使用率飙升

image-20230610141352716

发现BigKey

image-20230614150332437

  • redis-cli --bigkeys
1
redis-cli -a 密码	 --bigkeys

image-20230614150807161

  • Scan 扫描

    利用 Scan 批量扫描 Redis 中的所有Key, 再利用 strlen, hlen等命令判断 key 的长度

image-20230614151001448

image-20230614151149174

  • 第三方工具

利用第三方工具,如 Redis-Rdb-Tools 分析 RDB 快照文件,全面分析内存使用情况

  • 网络监控

自定义监控,监控进出Redis的网络数据,超出预警值主动告警

删除BigKey

BigKey 内存占用较多,即便删除也需要消耗很长的时间,导致Redis主进程阻塞,引发一系列的问题

image-20230614153328669

  • Redis 3.0 及以下版本

如果是集合类型,则遍历BigKey元素,先逐个删除单个元素,最后删除BigKey

image-20230614153649056

Redis在 4.0 后提供了 异步删除的命令-unlink

image-20230614153605063

恰当的数据类型

对象

推荐使用 hash 结构

json字符串:优点-实现简单粗暴 缺点-数据耦合没有灵活性
字段分散:灵活访问对象任意字段 缺点-占用空间大,无法同一控制
Hash:底层使用ziplist,空间占用小,灵活访问对象的任意字段 缺点-代码相对复杂

image-20230614154848514

大键值对
存在问题
  • Hash 的 entry 数量超过 512 时,会使用 哈希表 而不是 ZipList,内存占用较多
  • 通过 hash-max-ziplist-entries 配置 entry 上限,但是 如果 entry 过多会导致 Bigkey 问题

image-20230614155227280

image-20230614155327132

如果拆分为String类型,由于string结构底层没有太多的内存优化,存在很多元数据等要存,内存占用较多,其次当业务上想要批量当获取这些数据时比较麻烦

image-20230614155500829

划分Hash

拆分为小的Hash,将id/100作为key,将id%100作为field,每100个元素作为一个Hash

image-20230614155543651

image-20230614155733292

总结

image-20230614160008531

Key
  • 固定格式:**[业务名]:[数据名]:[id]**
  • 足够简短:不超过44字节
  • 不包括特殊字符
Value
  • 合理的拆分数据,拒绝BigKey
  • 选择合适的数据结构
  • Hash结构的entry数量不要超过1000
  • 设置合理的超时时间

批处理优化

Pipeline

单个命令的执行流程

image-20230614164041486

N个命令的执行流程

image-20230614164244348

N个命令批量执行

image-20230614164318494

MSET

不要在一次批处理中传输大多的命令,否则单次命令占用带宽过多,会导致网络阻塞

image-20230614164627706

image-20230614164548509

image-20230614165510774

Pipeline

MSET 虽然可以进行批处理,但是只能操作部分数据类型,因此如果有对复杂的数据类型的批处理需求时,建议使用Pipeline功能

image-20230614165944139

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testPipeline() {
// 创建管道
Pipeline pipeline = jedis.pipelined();
for (int i = 1; i< 100000; i++) {
// 将命令放入管道中
pipeline.set("test:key:"+i, i);
if (i % 1000) {
pipeline.sync();
}
}
}
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
/**
* 批量添加并设置失效时间
*/
@Override
public List<Object> setBypipeline(Map<String, Object> map, long time) {
return redisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
// 开启事务
operations.multi();
ValueOperations<String, Object> kvValueOperations = (ValueOperations<String, Object>) operations.opsForValue();
map.entrySet()
.forEach(stringObjectEntry -> kvValueOperations.set(
stringObjectEntry.getKey(),
stringObjectEntry.getValue(),
time,
TimeUnit.SECONDS
));
return operations.exec();
}
});
}

/**
* 批量添加
*/
redisTemplate.opsForValue().multiSet(map);
总结

批处理方案:1、原生的m操作 2、Pipeline批量操作(推荐)

注意事项:1、批处理不建议一次携带太多命令 2、Pipeline的多个命令之间不具备原子性

集群下的批处理

如果MSET或者``Pipeline在一次请求中携带了多条命令,而此时如果Redis`是一个集群,那么批量的命令多个Key计算出来的hash值必须落到一个插槽中,否则会导致执行失败(No way to dispatch this command to Redis Cluster because keys have different slots)

image-20230614174309626

串行slot并行slot 的区别在于是否开启多个线程异步的执行各组命令,hash_tag虽然耗时非常短,但是容易出现数据倾斜

redisTemplate.opsForValue().multiSet(Map<? extends K, ? extends V> map) 底层就是 并行slot 在客户端计算每个 keyslot,将 slot 一致分为一组,每组都利用 Pipeline 批处理,异步并行执行各组命令

1
2
// 批量设置
redisTemplate.opsForValue().multiSet(map);

image-20230614201259765

1
2
3
4
// 批量查询
redisTemplate.opsForValue().multiGet(Arrays.asList());
// 批量
redisTemplate.delete(Arrays.asList());

服务端优化

持久化配置

image-20230614234223416

Redis 持久化可以保证数据安全,但是会带来额外的开销,因此持久化要遵循下列建议

  • 用来作为缓存业务的Redis实例尽量不要开启持久化功能,对于安全性要求比较高的业务可以开启持久化

  • 建议关闭RDB持久化功能,使用AOF持久化

  • 利用脚本定期Slave节点做RDB,实现数据备份

  • 在使用AOF持久化时,设置合理的rewrite阈值避免频繁的bgrewrite

image-20230614232448489

  • 配置 no-appendfsync-on-rewrite=yes禁止rewrite期间进行AOF,避免因AOF带来巨大的磁盘IO,引起的主线程阻塞

image-20230614233818024

部署建议
  • Redis实例的物理机要预留足够的内存,以应对fork和rewrite
  • 单个Redis实例内存上限不要过高(4/8G),可以加快fork速度,减少主从同步,数据迁移的压力
  • 不要与CPU密集型应用部署在一起
  • 不要与高硬盘负载应用一起部署,例如数据库,消息队列

慢查询

慢查询:在Redis执行时耗时超过某个阈值的命令,称为慢查询

image-20230614234931532

由于Redis是单线程执行命令即主线程执行命令,Redis有一个队列让命令请求进行排队依次执行,当一个查询命令执行时间过长时,会导致其他后面的请求命令等待超时

慢查询的阈值可以通过配资指定

  • slowlog-log-slower-than慢查询阈值,单位微秒,默认是10000(10ms),建议1000(1ms)

慢查询会被存放在慢查询日志中,日志的长度存在上限,可以通过配置指定

image-20230615000248437

  • slowlog-max-len:慢查询日志(本质是一个队列)的长度,默认128,建议1000

image-20230615000119711

查看慢查询日志列表

  • slowlog len:查询慢查询日志长度
  • slowly get [n]读取n条慢查询日志
  • slow reset清空慢查询列表

image-20230615000749772

image-20230615000440826

命令和安全配置

Redis 默认会绑定在 0.0.0.0:6379,这会将Redis服务暴露到公网上,如果Redis没有做身份认证,会出现严重的安全漏洞

漏洞出现核心原因:

  • Redis 未设置密码(默认的 redis服务 没有密码)
  • 利用 Redis 的 config set 命令动态修改 Redis 配置
  • 使用了 Root 账号权限启动 Redis

image-20230615102015014

image-20230615103250818

为了避免这样的漏洞:

  • Redis 一定要 设置密码
  • 禁止线上使用如下命令:keysflushallflushedconfig set等命令,可以利用 rename-command禁用
1
2
rename-command CONFIG CONFIGURATION  # 此时 CONFIG 被修改为 CONFIGURATION
rename-command KEYS '' # 此时 KEYS 无效

image-20230615102157383

image-20230615102234229

  • 配置 bind 限制网卡,禁止外网网卡访问
1
bind 0.0.0.0  # 默认是 开放 0.0.0.0

image-20230615103329093

  • 开启防火墙
1
systemctl enable firewalld
  • 不用使用root 账户 启动 Redis,防止 root 权限修改本地目录和文件
  • 尽量不要使用默认端口

内存配置

当Redis内存不足时,可能会导致 key 频繁被删除,响应时间变长,QPS不稳定等问题,当内存使用率达到90%以上时需要警惕并快速定位到内存占用的原因

Redis 内存分配
  • 数据内存:Redis最主要的部分,存储Redis键值信息,主要信息是BigKey问题以及内存碎片问题

  • 进程内存:Redis主进程本身运行占用内存,如代码、常量池等等,这部分内存占用一般几兆,在大多数生产环境中,它与Redis数据占用内存相比可以忽略不计

  • 缓存区内存:一般包括客户端缓冲区、AOF缓冲区、复制缓冲区等。客户端缓冲区包括输入缓冲区输出缓冲区两种,这部分内存占用波动较大,如果不当使用BigKey,可能会导致内存溢出

image-20230615104551494

查看内存分配命令

Redis 提供了一些命令,可以查看Redis目前的内存分配状态

  • INFO MEMORY
  • MEMORY STATS

image-20230615110759069

image-20230615105348749

image-20230615105541610

image-20230615105318565

内存缓冲区配置

内存缓冲区的常见三种:

  • 复制缓冲区:主从复制用于增加同步的repl_backlog_buf,如果该缓冲区的大小设置的大小,会增量的数据缓冲区无法全部保存,从而导致频繁的全量同步,影响性能:可以通过 repl-backlog-size 来设置,默认是1mb

  • AOF缓冲区:AOF 刷新磁盘之前的缓存区域,AOF 执行 rewrite 的缓冲区,无法设置容量的上限

  • 客户端缓冲区:分为输入缓存区和输出缓冲区,输入缓冲区最大1G且不能设置,输出缓冲区可以设置

    1
    2
    3
    4
    client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
    # <class> 客户端类型:normal:普通客户端 replica:主从辅助客户端 pubsub:PubSub客户端
    # <hard limit>:缓冲区上限在超过 limit 后断开
    # <soft limit> <soft seconds> 缓冲区上限,在超过soft limit并且持续soft seconds秒后断开客户端

image-20230615111746298

也可以通过命令查看内存缓冲区大小和每个客户端的缓冲区占用情况

1
2
3
4
# 查看客户端的输入缓冲区和输出缓冲区
INFO clients
# 查看每个客户端的缓冲区占用情况
clients list

image-20230615111943651

image-20230615112023461

集群最佳实践

image-20230615125121828

  • 集群完整性问题

在Redis的默认配置中, 如果发现任意一个插槽不可用,则整个集群都会停止对外服务

可以通过修改 cluster-require-full-coverage false 让部分插槽不可用时,其他插槽依旧对外正常服务,来保证汲取高可用的特性

1
require-full-coverage false

image-20230615125210887

  • 集群带宽问题

集群节点之间会不断的互相 ping 来确定集群中其他节点的状态,每次 ping 携带的信息至少包括:插槽信息集群状态信息。这就导致集群中节点越多,集群的状态信息数量也就越大,10个节点的相关信息可能就到达了1kb,此时每次集群的互通所需的带宽就会非常高

解决途径

  • 避免大集群,集群节点数据不要过多,最好少于1000,如果业务比较大,则可以建立多个集群
  • 避免在单个物理机上运行太多的Redis实例,因为单个物理机的带宽是有限的
  • 配置合适的 cluster-node-timeout 值,不要让 ping 的频率过高,也不能让频率过慢,否则会导致可用性降低

image-20230615132552248

注意:单体Redis(主从Redis已经可以达到万级别的QPS,并且由于哨兵机制,它也具备很强的高可用特性,如果主从能够满足业务需求的情况下,尽量不要搭建Redis集群)

image-20230615133113121