笔记小结:
- 请查看各个小节
- 总结:详细请查看
说明:
单点Redis的问题
笔记小结:
- 概述:Redis是一个内存数据库,它可以通过持久化机制将数据保存到磁盘上
- RDB(Redis数据备份文件):
- Redis会开启异步线程自动的备份数据文件
- Redis默认停机时会执行一次RDB
- Redis会执行RDB每十五分钟修改1次,每五分钟修改10次,每一分钟修改10000次即进行一次RDB持久化
- AOF(追加文件):
- Redis处理的每一个写命令都会记录在AOF文件
- 常用操作:开启AOF、修改记录频率、设置触发阈值
Redis是一个内存数据库,它可以通过持久化机制将数据保存到磁盘上,以防止数据丢失。
RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。
RDB缺点,Redis的执行间隔时间长,两次RDB之间写入数据有丢失的风险、fork子进程、压缩、写出RDB文件都比较耗时
说明:
- 快照文件称为RDB文件,默认是保存在当前运行目录。Redis默认停机时会执行一次RDB。
# 900秒内,如果至少有1个key被修改,则执行bgsave , 如果是save "" 则表示禁用RDB
save 900 1
save 300 10
save 60 10000
说明:
Redis内部有触发RDB的机制,可以在
redis.conf
文件中找到
# 是否压缩 ,建议不开启,压缩也会消耗cpu,磁盘的话不值钱
rdbcompression yes
# RDB文件名称
dbfilename dump.rdb
# 文件保存的路径目录
dir ./
说明:
- 默认情况下,是否压缩是处于开启状态
- RDB的其它配置也可以在
redis.conf
文件中设置
说明:
- 在Redis中,主进程不会去直接读取物理内存中的数据,而是通过页表的方式进行对物理内存的映射而进行去读
bgsave
命令开始时会fork
主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件- fork采用的是copy-on-write技术:当主进程执行读操作时,访问共享内存、当主进程执行写操作时,则会拷贝一份数据,执行写操作
AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。
说明:
每一个命令都记录到AOF文件中,命令文件不断的增大
# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
说明:
- AOF默认是关闭的,需要修改
redis.conf
配置文件来开启AOF- 开启AOF功能的时候,建议关闭RDB功能
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
说明:
- AOF的命令记录的频率也可以通过
redis.conf
文件来配
# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
说明:
- 因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义
- 通过执行
bgrewriteaof
命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果
说明:
RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用
笔记小结:
- 概述:主从集群,实现读写分离,提高数据可靠性与完整性
- 全量同步原理:数据标记
Replication
、偏移量offset
、生成RDB
文件、记录repl_baklog
Redis命令缓存区- 增量同步原理:Redis重启后进行
repl_baklog
命令缓存区的命令重写、偏移量offset
被覆盖、全量同步- 总结:详细请查看
说明:
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离
三个Redis节点信息如下:
IP | PORT | 角色 |
---|---|---|
10.13.164.55 | 6379 | master |
10.13.164.55 | 6380 | slave |
10.13.164.55 | 6381 | slave |
说明:
主节点用于写操作,子节点用于读取操作
步骤一:配置环境
说明:
本Redis主从节点采用Docker方式进行安装
1.创建文件和目录
cd /home
mkdir redis
cd redis
mkdir /home/redis/myredis1
mkdir data
touch myredis.conf
// 在myredis2和myredis3目录中分别创建 myredis.conf 配置文件和data目录此处省略命令
mkdir /home/redis/myredis2
mkdir /home/redis/myredis3
说明:查看结果
myredis.conf
文件内容如下bind 0.0.0.0
protected-mode no
port 6379
tcp-backlog 511
requirepass qweasdzxc
timeout 0
tcp-keepalive 300
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 30
always-show-logo yes
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ./
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-disable-tcp-nodelay no
replica-priority 100
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
appendonly yes
appendfilename "appendonly.aof"
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
lua-time-limit 5000
slowlog-max-len 128
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes
masterauth qweasdzxc # 配置主节点Redis的密码
注意:
注意,此处需要将port分别替换为 6380、 6381
步骤二:运行Docker服务
注意:
需要提前创建好
myredis.conf
文件和data
文件夹
1.请分别在主机上运行如下命令
sudo docker run \
--restart=always \
-p 6379 \
--net=host \
--name myredis1 \
-v /home/redis/myredis1/myredis.conf:/etc/redis/redis.conf \
-v /home/redis/myredis1/data:/data \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes \
--requirepass qweasdzxc
sudo docker run \
--restart=always \
-p 6380 \
--net=host \
--name myredis2 \
-v /home/redis/myredis2/myredis.conf:/etc/redis/redis.conf \
-v /home/redis/myredis2/data:/data \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes \
--requirepass qweasdzxc
sudo docker run \
--restart=always \
-p 6381 \
--net=host \
--name myredis3 \
-v /home/redis/myredis3/myredis.conf:/etc/redis/redis.conf \
-v /home/redis/myredis3/data:/data \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes \
--requirepass qweasdzxc
说明:查看结果
2.建立主从关系
slaveof 10.13.164.55 6379 # 配置 主节点的Ip地址以及端口号
步骤三:测试
info repilication
说明:查看结果
- 此处发现子节点的IP地址与端口相互对应
- 但在主节点中设置数据依旧能够在子节点中读取,说明搭建成功
说明:
主从第一次同步是全量同步。当子节点进行首次同步时,会向主节点发送请求,并判断携带过去的数据版本等信息。稍后主节点将已有的数据生成
RDB
文件发送子节点,若此时有较新的数据会被记录为命令保存在repl_baklog
文件中,不断的与子节点保持同步
补充:master如何判断slave是不是第一次来同步数据?
说明:
- 如果slave的replid与主节点不一致则代表为首次同步
- 如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
因此slave做数据同步,必须向master声明自己的replication id 和offset,master才可以判断到底需要同步哪些数据
同步流程:
- slave节点请求增量同步
- master节点判断replid,发现不一致,拒绝增量同步
- master将完整内存数据生成RDB,发送RDB到slave
- slave清空本地数据,加载master的RDB
- master将RDB期间的命令记录在
repl_baklog
Redis命令缓存区,并持续将log中的命令发送给slave- slave执行接收到的命令,保持与master之间的同步
说明:
主从第一次同步是全量同步,但如果slave重启后同步,则执行增量同步。
补充:
repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步
若全量同步,不得不做。那么我们可以优化Redis主从集群来对Redis主从集群做优化
Redis集群优化
说明:优化方案
- 在
master
中配置文件中配置repl-diskless-sync yes
启用无磁盘复制,避免全量同步时的磁盘IO。- Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
- 适当提高
repl_baklog
的大小,发现slave
宕机时尽快实现故障恢复,尽可能避免全量同步- 限制一个
master
上的slave
节点数量,如果实在是太多slave
,则可以采用主-从-从链式结构,减少master
压力
全量同步与增量同步
全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog
,逐个发送给slave
说明:
当slave节点第一次连接master节点时、当save节点断开时间太久,repl_baklog中的offset已经被覆盖时会执行全量同步
增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave
说明:
当slave节点断开又恢复,并且在repl_baklog中能找到offset时会执行增量同步
注意:
增量同步,可能同步失败,取决于
repl_baklog
区域是否被完全覆盖
补充:
slave节点宕机恢复后可以找master节点同步数据,那如果master节点宕机后,则无法进行恢复。若解决此问题请看下一节
笔记小结:
- 概述:
- 含义:监控和管理Redis实例的自动故障转移
- 监控状态:主观下线和客观下线
- Master选举权:断开时间长短、
slave-priority
权重值大小、offset
偏移量、运行id大小- 故障转移:让
slave
结点成为新master
的从节点、标记故障节点- 基本用例:导入
spring-boot-starter-data-redis
依赖、配置yml
文件的哨兵主节点,集群子节点、配置类配置LettuceClientConfigurationBuilderCustomizer
并设置集群的读取模式- 总结:详细请查看
Redis的哨兵机制(Sentinel)是Redis提供的一种高可用性解决方案,用于监控和管理Redis实例的自动故障转移。
哨兵机制的核心是一组独立运行的哨兵进程,它们监控Redis主节点和其对应的多个从节点,并在主节点发生故障时自动将一个从节点升级为新的主节点,从而实现故障转移。
哨兵的结构和作用有,监控:Sentinel 会不断检查您的master和slave是否按预期工作。自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主。通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
说明:
在Redis的配置文件中,可以设置为Sentinel的一半数量
若Sentinel发现Master节点故障,Sentinel需要在salve中选择一个作为新的master,规则如下:
当选中了其中一个slave为新的master后(例如slave1),故障的转移的步骤如下:
slaveof no one
命令,让该节点成为masterslaveof 192.168.150.101 7002
命令,让这些slave成为新master的从节点,开始从新的master上同步数据3个Sentinel
示例信息如下:
IP | PORT |
---|---|
10.13.164.55 | 27001 |
10.13.164.55 | 27001 |
10.13.164.55 | 27001 |
步骤一:配置环境
说明:
本哨兵集群节点采用Docker方式进行安装
1.创建文件和目录
cd /home
mkdir redis
cd redis
mkdir /home/redis/mysentinel1
vim myredis.conf
// 在myredis2和myredis3目录中分别创建 myredis.conf 配置文件
mkdir /home/redis/mysentinel2
mkdir /home/redis/mysentinel3
说明:查看结果
sentinel.conf
文件内容如下port 27001 # 注意,此处需要将sentinel.conf文件分别替换为 27002、27003
sentinel announce-ip 10.13.164.55
sentinel monitor mymaster 10.13.164.55 6379 2 # 注意此处IP和地址正确无误
sentinel auth-pass mymaster qweasdzxc
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
说明:
port 27001
: 设置当前 Redis Sentinel 的监听端口为 27001。sentinel announce-ip 10.13.164.55
: 设置 Sentinel 在向其他节点宣告自己的 IP 地址时使用的 IP 地址为 10.13.164.55。sentinel monitor mymaster 10.13.164.55 6379 2
: 设置 Sentinel 监控名为mymaster
的主节点,主节点的 IP 地址为 10.13.164.55,端口号为 6379,2
表示在主节点进入下线状态后 Sentinel 需要等待的时间(单位是秒),超过这个时间 Sentinel 将主节点标记为下线状态。sentinel auth-pass mymaster qweasdzxc
: 设置 Sentinel 连接主节点时需要使用的密码为qweasdzxc
,用于进行身份验证。sentinel down-after-milliseconds mymaster 5000
: 设置 Sentinel 认为主节点下线的时间阈值为 5000 毫秒(即 5 秒),如果在这个时间内没有收到主节点的响应,则认为主节点已经下线。sentinel failover-timeout mymaster 60000
: 设置进行故障转移的超时时间为 60000 毫秒(即 60 秒),如果在这个时间内没有完成故障转移,则认为故障转移失败。sentinel parallel-syncs mymaster 1
: 设置在进行故障转移时同时同步从节点的数量为 1,即同时同步一个从节点。这样可以避免同时对多个从节点进行同步造成的资源负载过大。
步骤二:运行Docker服务
说明:
请分别在主机上运行如下命令
docker run --restart=always \
--net=host \
--name mysentinel1 \
-v /home/redis/mysentinel1/sentinel.conf:/sentinel.conf \
-d redis redis-sentinel /sentinel.conf
docker run --restart=always \
--net=host \
--name mysentinel2 \
-v /home/redis/mysentinel2/sentinel.conf:/sentinel.conf \
-d redis redis-sentinel /sentinel.conf
docker run --restart=always \
--net=host \
--name mysentinel3 \
-v /home/redis/mysentinel3/sentinel.conf:/sentinel.conf \
-d redis redis-sentinel /sentinel.conf
注意:
配置文件
sentinel.conf
,需要与各自的监控节点相对应
步骤三:测试
2.查看7003的日志:
3.查看7002的日志:
说明:
此时已选举7003节点作为新的主节点,同我们的6380节点成为主节点打印消息一致
说明:
在Sentinel集群监管下的Redis主从集群,其节点会因为自动故障转移而发生变化,Redis的客户端必须感知这种变化,及时更新连接信息。Spring的
RedisTemplate
底层利用lettuce实现了节点的感知和自动切换
步骤一:导入依赖
pom.xml
文件<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
步骤二:添加配置
1.修改application.yaml
配置文件
logging:
level:
io.lettuce.core: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
server:
port: 8081
spring:
redis:
sentinel:
master: mymaster # 指定master名称
nodes: # 指定redis-sentinel集群信息
- 10.13.164.55:27001
- 10.13.164.55:27002
- 10.13.164.55:27003
password: qweasdzxc
2.添加RedisConfig
配置文件类
@Configuration
public class RedisConfig {
@Bean
LettuceClientConfigurationBuilderCustomizer getLettuceClientConfigurationBuilderCustomizer(){
// 设置集群的读取模式,先读取从结点,若失败则再读取主节点
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
}
步骤三:测试
1.编写HelloController
表现层类
@RestController
public class HelloController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/get/{key}")
public String hi(@PathVariable String key) {
return redisTemplate.opsForValue().get(key);
}
@GetMapping("/set/{key}/{value}")
public String hi(@PathVariable String key, @PathVariable String value) {
redisTemplate.opsForValue().set(key, value);
return "success";
}
}
2.进入控制台请求测试
3.查看Idea日志
说明:
日志打印正常,说明测试通过
4.测试主节点宕机
说明:
待故障节点恢复后会自动加入主节点,说明测试通过
补充:
主从和哨兵可以解决高可用、高并发读的问题。但是依然问题没有解决:海量数据存储问题、高并发写的问题
笔记小结:
- 概述:将数据划分为多个分片并分布到不同的节点上,可以实现数据的水平扩展和负载均衡。提高了集群的容量和性能
- 散列插槽:
- 含义:通过自定义的方式让数据可以让数据存在指定的Redis的插槽中
- 注意:自定义key时,key中包含**“{ }”,且“{ }”中至少包含1个字符,“{ }”中的部分是有效部分**
- 集群伸缩:
add-node
添加结点、reshard
分配插槽、del-node
删除结点- 故障转移:
cluster failover
成为主节点、原理,使用Offset
偏移量- Java访问:导入
spring-boot-starter-data-redis
依赖让SpringBoot
整合Redis、配置yml
文件的主节点,从集群从节点、配置类配置LettuceClientConfigurationBuilderCustomizer
设置集群的读取模式
Redis 分片集群是一种将数据分布在多个 Redis 节点上的方案,通过将数据划分为多个分片并分布到不同的节点上,可以实现数据的水平扩展和负载均衡。每个节点都可以独立地处理一部分数据,并且可以通过添加或删除节点来动态调整集群的容量和性能。
说明:
集群中有多个master,每个master保存不同数据。每个master都可以有多个slave节点。master之间通过ping监测彼此健康状态。客户端请求可以访问集群任意节点,最终都会被转发到正确节点
6个Redis
实例信息如下:
IP | PORT | 角色 |
---|---|---|
10.13.164.55 | 7001 | master |
10.13.164.55 | 7002 | master |
10.13.164.55 | 7003 | master |
10.13.164.55 | 7004 | slave |
10.13.164.55 | 7005 | slave |
10.13.164.55 | 7006 | slave |
步骤一:配置环境
说明:
本哨兵集群节点采用Docker方式进行安装
1.创建文件和目录
cd /home
mkdir redis
cd redis
mkdir /home/redis/myredis1
touch /home/redis/myredis1/redis.conf
mkdir /home/redis/myredis1/data
// 在myredis2到myredis6的目录中分别创建 myredis.conf 配置文件和data目录,此处省略命令
mkdir /home/redis/myredis2
……
mkdir /home/redis/myredis6
……
touch myredis.conf
mkdir data
说明:查看结果
myredis.conf
文件内容如下注意:每个结点对应的配置文件都需要端口等信息都需要单独设置
# 绑定地址
bind 0.0.0.0
# redis端口,不同节点端口不同分别是7001 ~ 7006
port 7001
#redis 访问密码
requirepass qweasdzxc
#redis 访问Master节点密码
masterauth qweasdzxc
# 关闭保护模式
protected-mode no
# 开启集群
cluster-enabled yes
# 集群节点配置
cluster-config-file nodes.conf
# 超时
cluster-node-timeout 5000
# 集群节点IP host模式为宿主机IP
cluster-announce-ip 10.13.164.55
# 集群节点端口,不同节点端口不同分别是7001 ~ 7006
cluster-announce-port 7001
cluster-announce-bus-port 17001
# 开启 appendonly 备份模式
appendonly yes
# 每秒钟备份
appendfsync everysec
# 对aof文件进行压缩时,是否执行同步操作
no-appendfsync-on-rewrite no
# 当目前aof文件大小超过上一次重写时的aof文件大小的100%时会再次进行重写
auto-aof-rewrite-percentage 100
# 重写前AOF文件的大小最小值 默认 64mb
auto-aof-rewrite-min-size 64mb
# 日志配置
# debug:会打印生成大量信息,适用于开发/测试阶段
# verbose:包含很多不太有用的信息,但是不像debug级别那么混乱
# notice:适度冗长,适用于生产环境
# warning:仅记录非常重要、关键的警告消息
loglevel notice
# 日志文件路径
logfile "/data/redis.log"
步骤二:运行容器
Redis结点1
sudo docker run \
--name myredis1 \
-p 7001:7001 \
-p 17001:17001 \
-v /home/redis/myredis1/redis.conf:/etc/redis/redis.conf \
-v /home/redis/myredis1/data/:/data \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes \
--requirepass qweasdzxc
Redis结点2
sudo docker run \
--name myredis2 \
-p 7002:7002 \
-p 17002:17002 \
-v /home/redis/myredis2/redis.conf:/etc/redis/redis.conf \
-v /home/redis/myredis2/data/:/data \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes \
--requirepass qweasdzxc
Redis结点3
sudo docker run \
--name myredis3 \
-p 7003:7003 \
-p 17003:17003 \
-v /home/redis/myredis3/redis.conf:/etc/redis/redis.conf \
-v /home/redis/myredis3/data/:/data \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes \
--requirepass qweasdzxc
Redis结点4
sudo docker run \
--name myredis4 \
-p 7004:7004 \
-p 17004:17004 \
-v /home/redis/myredis4/redis.conf:/etc/redis/redis.conf \
-v /home/redis/myredis4/data/:/data \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes \
--requirepass qweasdzxc
Redis结点5
sudo docker run \
--name myredis5 \
-p 7005:7005 \
-p 17005:17005 \
-v /home/redis/myredis5/redis.conf:/etc/redis/redis.conf \
-v /home/redis/myredis5/data/:/data \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes \
--requirepass qweasdzxc
Redis结点6
sudo docker run \
--name myredis6 \
-p 7006:7006 \
-p 17006:17006 \
-v /home/redis/myredis6/redis.conf:/etc/redis/redis.conf \
-v /home/redis/myredis6/data/:/data \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes \
--requirepass qweasdzxc
步骤三:创建集群
redis-cli --cluster create --cluster-replicas 1 -h 10.13.164.55 -p 7001 -a qweasdzxc 10.13.164.55:7001 10.13.164.55:7002 10.13.164.55:7003 10.13.164.55:7004 10.13.164.55:7005 10.13.164.55:7006
说明:
- 访问其中一台集群结点,连接上其中一台客户端并创建集群
- 查看结点状态
redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc cluster node
补充:参数解释
--cluster-replicas
用于创建 Redis 分片集群时指定每个主节点要拥有的从节点数量参数。为1
,表示可以为每个主节点自动创建一个从节点。
散列插槽(hash slots)是 Redis 分片集群中的一种数据分片机制。它将数据分散存储在多个节点上,实现数据的水平分布和负载均衡。
在 Redis 分片集群中,Redis Cluster 将整个数据集划分为固定数量的散列插槽(通常是 16384 个插槽)。每个键都会通过哈希函数计算得到一个插槽号(slot number),然后根据插槽号将键值对分配到对应的节点上。
在 Redis 分片集群中,数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:
说明:
key是num,那么就根据num计算,如果是{itcast}num,则根据itcast计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。若要取得该数据则根据key的有效部分计算哈希值,对16384取余,余数作为插槽,寻找插槽所在实例即可
补充:
如果将同一类数据固定的保存在同一个Redis实例,那么这一类数据使用相同的有效部分,例如key都以{typeId}为前缀
步骤一:搭建Redis服务
说明:
同搭建分片集群步骤类似,先创建一个7007结点,并运行
sudo docker run \
--name myredis7 \
-p 7007:7007 \
-p 17007:17007 \
-v /home/redis/myredis7/redis.conf:/etc/redis/redis.conf \
-v /home/redis/myredis7/data/:/data \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes \
--requirepass qweasdzxc
步骤二:添加结点到现有集群
# 格式
# add-node new_host:new_port existing_host:existing_port
# --cluster-slave
# --cluster-master-id
# 例如
redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc --cluster add-node 10.13.164.55:7007 10.13.164.55:7001
说明:查看结果
- 查看插槽数
redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc cluster nodes
- 发现此Master结点并没有分配插槽数,需要分配插槽才能继续使用
# 格式 reshard host:port
# --cluster-from
# --cluster-to
# --cluster-slots
# --cluster-yes
# --cluster-timeout
# --cluster-pipeline
# --cluster-replace
# 例如
redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc --cluster reshard 10.13.164.55:7001
说明:查看结果
- 重新分配7001插槽给7007结点
- 查看插槽数
redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc cluster nodes
步骤一:转移插槽
redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc --cluster reshard 10.13.164.55:7001
步骤二:删除结点
# 格式 del-node host:port node_id
# 例如
redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc --cluster del-node 10.13.164.55:7007 489417ac7de6be3997ba26911efa7fc95ce3be40
说明:查看结果
- 查看插槽数
redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc cluster nodes
- 此时可发现,7007结点已消失
watch redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc cluster nodes
说明:
- 观察Master结点变化,发现已经更换
步骤一:连接子结点
redis-cli -h 10.13.164.55 -p 7002 -a qweasdzxc
步骤二:切换结点
cluster failover
说明:
- 可以看出,再次变为主节点
补充:
- 利用cluster failover命令可以手动让集群中的某个master宕机,切换到执行cluster failover命令的这个slave节点,实现无感知的数据迁移
- 手动的Failover支持三种不同模式:缺省:默认的流程,如图1~6歩、force:省略了对offset的一致性校验、takeover:直接执行第5歩,忽略数据一致性、忽略master状态和其它master的意见
说明:
在Sentinel集群监管下的Redis分片集群,其节点会因为自动故障转移而发生变化,Redis的客户端必须感知这种变化,及时更新连接信息。Spring的
RedisTemplate
底层利用lettuce实现了节点的感知和自动切换
步骤一:导入依赖
pom.xml
文件<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
步骤二:添加配置
1.修改application.yaml
配置文件
logging:
level:
io.lettuce.core: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
server:
port: 8081
spring:
redis:
cluster:
nodes: # 指定分片集群的每一个节点信息
- 10.13.164.55:7001
- 10.13.164.55:7002
- 10.13.164.55:7003
- 10.13.164.55:7004
- 10.13.164.55:7005
- 10.13.164.55:7006
password: qweasdzxc
2.添加RedisConfig
配置文件类
@Configuration
public class RedisConfig {
@Bean
LettuceClientConfigurationBuilderCustomizer getLettuceClientConfigurationBuilderCustomizer(){
// 设置集群的读取模式,先读取从结点,若失败则再读取主节点
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
}
步骤三:测试
1.编写HelloController
表现层类
@RestController
public class HelloController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/get/{key}")
public String hi(@PathVariable String key) {
return redisTemplate.opsForValue().get(key);
}
@GetMapping("/set/{key}/{value}")
public String hi(@PathVariable String key, @PathVariable String value) {
redisTemplate.opsForValue().set(key, value);
return "success";
}
}
2.进入控制台请求测试
3.查看Idea日志
说明:
通过日志,可以发现读写是分离的
1.Redis主从集群与Redis分片集群优缺点对比:
Redis主从集群
优点:
数据复制:主节点将数据复制到从节点,实现数据的备份和冗余,提高数据的可靠性和可用性。
读写分离:主节点负责写操作,从节点负责读操作,提高了系统的并发处理能力和读取性能。
故障容错:当主节点发生故障时,可以自动切换为从节点为新的主节点,实现高可用性。
缺点:
Redis分片集群
优点:
数据分片:将数据分散存储在多个节点上,提高了存储容量和吞吐量。
并行处理:每个节点独立处理自己负责的数据片段,提高了系统的并发处理能力。
水平扩展:通过增加节点实现集群的扩展,支持更大规模的数据存储和处理。
缺点:
节点故障影响:当某个节点发生故障时,该节点负责的数据将无法访问,可能导致数据的丢失或不可用。
数据一致性:分片集群中的数据分布不一定均匀,可能导致部分节点负载较高,需要考虑数据的均衡性和一致性问题。
跨节点事务:分片集群中的事务操作跨越多个节点,需要考虑数据的一致性和并发控制的复杂性。
总结:
2.传统缓存策略:
说明:
传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库,当数据量达到亿级别时则会存在问题
说明:
那么,又该如何解决缓存失效和Tomcat的瓶颈呢,详细请查看下节
笔记小结:
- 请查看各个小节
- 总结:详细请查看
笔记小结:
- 概述:Redis 的多级缓存,它由多个层级的缓存组成,以提高系统的性能和扩展性。
- 工作流程:当数据访问时,会依次查询一级缓存,二级缓存,三级缓存……最后查询Tomcat
Redis 的多级缓存是一种常见的缓存架构,它由多个层级的缓存组成,以提高系统的性能和扩展性。每个缓存层级都有不同的特点和用途
说明:
用作缓存的Nginx是业务Nginx,需要部署为集群,再有专门的Nginx用来做反向代理
通过使用多级缓存,可以大大提高系统的性能和扩展性,减少对后端数据存储系统的访问次数,降低系统负载,并提供更好的用户体验。同时,多级缓存还可以根据数据的访问模式和重要性进行灵活的配置和管理,以满足不同的业务需求。
原理流程:
笔记小结:
- 概述:Caffeine是最佳命中率的高性能的本地缓存库
- 基本用法:创建Builder对象、get、set
- 缓存驱逐策略:缓存可根据时间和容量
maximumSize
和设置缓存过期时间expireAfterWrite
来设置缓存的更新频率
Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址:https://github.com/ben-manes/caffeine
Test
类@Test
void testBasicOps() {
// 1.创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder().build();
// 2.存数据
cache.put("gf", "迪丽热巴");
// 3.取数据
// 3.1不存在则返回null
String gf = cache.getIfPresent("gf");
System.out.println("gf = " + gf);
// 3.2不存在则去数据库查询
String defaultGF = cache.get("defaultGF", key -> {
// 这里可以去数据库根据 key查询value
return "柳岩";
});
System.out.println("defaultGF = " + defaultGF);
}
Caffeine 是一个基于 Java 的高性能缓存库,它提供了多种缓存驱逐策略以控制缓存的大小和内存占用。
值得注意的是,缓存驱逐时需要一定的时间,例如10秒20秒。以下是 Caffeine 支持的一些常见缓存驱逐策略
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1) // 设置缓存大小上限为 1
.build();
说明:
设置缓存的数量上限
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(10)) // 设置缓存有效期为 10 秒,从最后一次写入开始计时
.build();
说明:
设置缓存的有效时间
说明:
设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。
补充:
在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐
步骤一:导入依赖
pom.xml
文件<dependency>
<groupId>com.github.ben-manes.caffeinegroupId>
<artifactId>caffeineartifactId>
dependency>
步骤二:创建配置文件
CaffeineConfig
配置类@Configuration
public class CaffeineConfig {
@Bean
public Cache<Long, Item> itemCache() {
return Caffeine.newBuilder()
.initialCapacity(100) // 设置缓存的初始容量为100个条目
.maximumSize(10000) // 设置缓存的最大容量为10000个条目
.build();
}
@Bean
public Cache<Long, ItemStock> StockCache() {
return Caffeine.newBuilder()
.initialCapacity(100) // 设置缓存的初始容量为100个条目
.maximumSize(10000) // 设置缓存的最大容量为10000个条目
.build();
}
}
步骤三:实现查询
ItemController
类@GetMapping("/{id}")
public Item findById(@PathVariable("id") Long id) {
return itemCache.get(id, key -> itemService.query()
.ne("status", 3).eq("id", key)
.one());
}
@GetMapping("/stock/{id}")
public ItemStock findStockById(@PathVariable("id") Long id) {
return StockCache.get(id, key -> stockService.getById(key));
}
笔记小结:
- 概述:Lua 是一种轻量小巧的脚本语言,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
- 语法同python类似,详细查看各小节
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。官网:https://www.lua.org/
步骤一:创建Lua脚本
touch hello.lua
步骤二:添加一下内容
print("Hello World!")
步骤三:运行
lua hello.lua
数据类型 | 描述 |
---|---|
nil | 这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。 |
boolean | 包含两个值:false和true |
number | 表示双精度类型的实浮点数 |
string | 字符串由一对双引号或单引号来表示 |
function | 由 C 或 Lua 编写的函数 |
table | Lua 中的表(table)其实是一个"关联数组"(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过"构造表达式"来完成,最简单构造表达式是{},用来创建一个空表。 |
说明:
- 查看变量数据类型
print(type("hello,world"))
-- 声明字符串
local str = 'hello'
-- 字符串拼接可以使用 ..
local str2 = 'hello' .. 'world'
-- 声明数字
local num = 21
-- 声明布尔类型
local flag = true
-- 声明数组 key为索引的 table
local arr = {'java', 'python', 'lua'}
-- 声明table,类似java的map
local map = {name='Jack', age=21}
说明:
- 访问变量
-- 访问数组,lua数组的角标从1开始 print(arr[1]) -- 访问table print(map['name']) print(map.name)
-- 声明数组 key为索引的 table
local arr = {'java', 'python', 'lua'}
-- 遍历数组
for index,value in ipairs(arr) do
print(index, value)
end
-- 声明map,也就是table
local map = {name='Jack', age=21}
-- 遍历table
for key,value in pairs(map) do
print(key, value)
end
function 函数名( argument1, argument2..., argumentn)
-- 函数体
return 返回值
end
-- 例如
function printArr(arr)
for index, value in ipairs(arr) do
print(value)
end
end
if(布尔表达式)
then
--[ 布尔表达式为 true 时执行该语句块 --]
else
--[ 布尔表达式为 false 时执行该语句块 --]
end
说明:
笔记小结:
- 概述:OpenResty 是一个基于 Nginx的高性能 Web 平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关
OpenResty® 是一个基于 Nginx的高性能 Web 平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
OpenResty具备Nginx的完整功能、基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块、允许使用Lua自定义业务逻辑、自定义库
官方网站: https://openresty.org/cn/
说明:
本教程安装OpenResty是通过Docker方式进行
步骤一:创建目录
cd /home
mkdir openresty
cd /home/openresty
mkdir conf
mkdir lua
步骤二:安装OpenResty
docker run -id --name openresty -p 8080:8080 sevenyuan/openresty
步骤三:挂载配置
1.拷贝OpenResty
配置
docker cp openresty:/usr/local/openresty/nginx/conf/nginx.conf /home/openresty/conf
docker cp openresty:/usr/local/openresty/lualib /home/openresty
说明:查看结果
2.修改/home/openresty/nginx/conf/nginx.conf
配置
#user nobody;
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8080;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
步骤四:重新安装
1.删除OpenResty
docker rm -f openresty
2.安装OpenResty
docker run -id -p 8080:8080 \
--name openresty \
-v /home/openresty/conf/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf \
-v /home/openresty/lua:/usr/local/openresty/nginx/lua \
-v /home/openresty/lualib/:/usr/local/openresty/lualib \
-v /etc/localtime:/etc/localtime \
-d sevenyuan/openresty
说明:
不要添加
--restart always
属性,否则启动失败
步骤五:访问OpenResty
控制界面
说明:
能够在浏览器端进行访问
OpenResty
默认界面,说明安装成功
笔记小结:
- 概述:通过Nginx集群实现本地缓存方案
- Nginx反向代理请求,
upstream
使用方式- Nginx的请求参数动态处理
说明:
当客户端浏览器发送请求时,NGINX反向代理则会将请求转发到NGINX本地缓存中
步骤一:修改NGINX
反向代理
说明:
让
nginx
代理到OpenResty
业务集群,进行业务的处理
1.修改Nginx
反向代理到业务集群的路径
upstream nginx-cluster{
# 定义多个请求代理的服务器
server 10.13.167.28:8080;
}
server {
listen 8080;
server_name localhost;
# 当nginx拦截到任一api开头的请求时,会自动的代理到upstream后端服务器模块中
location /api {
proxy_pass http://nginx-cluster;
}
}
说明:
2.重新启动Nginx
反向代理
nginx.exe -s stop
start nginx
步骤二:修改NGINX
本地缓存
OpenResty
的配置nginx.conf
配置文件#user nobody;
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# 添加对OpenResty的Lua模块的加载
#lua 模块
lua_package_path "/home/openresty/lualib/?.lua;;";
#c模块
lua_package_cpath "/home/openresty/lualib/?.so;;";
server {
listen 8080;
server_name localhost;
# 添加对/api/item这个路径的监听
location /api/item {
# 默认的响应类型
default_type application/json;
# 响应结果有lua/item.lua文件来决定
content_by_lua_file lua/item.lua;
}
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
说明:
修改配置文件后,
OpenResty
会自动刷新,因此无需重启
补充:
步骤三:添加脚本执行文件
1.编写item.lua
文件
vim /home/openresty/lua/item.lua
2.添加文件内容如下
ngx.say('{"id":10001,"name":"SALSA AIR","title":"RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":27900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')
步骤四:重启OpenResty
docker restart openresty
步骤五:查看结果
1.查看浏览器的响应数据
说明:
说明数据响应成功
2.查看浏览器前端页面
说明:
可以看到,价格已发生改变,Nginx代理实验成功
在OpenResty中如何获取请求地址中的参数,其实OpenResty提供了各种API用来获取不同类型的请求参数:
说明:
获取路径占位符中的参数
步骤一:编辑Openresty
配置文件
OpenResty
的配置nginx.conf
文件location ~ /api/item/(\d+) {
# 默认的响应类型
default_type application/json;
# 响应结果有lua/item.lua文件来决定
content_by_lua_file lua/item.lua;
}
说明:
步骤二:编写对应Lua
脚本
item.lua
文件local id = ngx.var[1]
ngx.say('{"id":' .. id .. ',"name":"SALSA AIR","title":"RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":27900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')
说明:
..
表示拼接字符串
步骤三:演示
说明:
当请求ID值变动时,返回数据依旧会随着请求改变
笔记小结:
- 概述:封装Lua脚本HTTP请求,实现Tomcat集群查询
- 使用CJSON实现对象序列化与反序列化
说明:
当
OpenResty
发送请求时,首次查询不会直接查询Redis集群,而是向Tomcat进行查询获取
在nginx中如何发送请求地,其实nginx提供了内部API用来发送Http请求:
local resp = ngx.location.capture("/path",{
method = ngx.HTTP_GET, -- 请求方式
args = {a=1,b=2}, -- get方式传参数
body = "c=3&d=4" -- post方式传参数
})
说明:
使用nginx的
ngx.location.capture
API发送
返回的响应内容包括:
注意:
- 这里的path是路径,并不包含IP和端口。这个请求会被nginx内部的server监听并处理。
location /path { # 这里是windows电脑的ip和Java服务端口,需要确保windows防火墙处于关闭状态 proxy_pass http://192.168.150.1:8081; }
- 但是我们希望这个请求发送到Tomcat服务器,因此还需要编写一个server来对这个路径做反向代理
步骤一:创建common.lua
文件
在/home/openresty/lualib目录下创建common.lua文件,便于OpenResty的nginx.conf模块的导入
步骤二:编写common.lua
文件
1.封装发送HTTP
请求的函数
-- 函数,发送http请求,并解析响应
local function read_http(path, params)
local resp = ngx.location.capture(path,{
method = ngx.HTTP_GET,
args = params,
})
if not resp then
-- 记录错误信息,返回404
ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)
ngx.exit(404)
end
return resp.body
end
2.将方法导出
-- 将方法导出
local _M = {
read_http = read_http
}
return _M
OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。官方地址: https://github.com/openresty/lua-cjson/
使用方式:
local cjson = require ("cjson")
local obj = {
name = 'jack',
age = 21
}
local json = cjson.encode(obj)
local json = '{"name": "jack", "age": 21}'
-- 反序列化
local obj = cjson.decode(json);
print(obj.name)
前提:
需要封装在
Common.lua
中的函数
步骤一:添加OpenResty
的nginx.conf
代理地址
http {
……
server {
listen 8080;
server_name localhost;
# 这里是配置Tomcat服务的电脑的ip和Java服务端口,需要确保其防火墙处于关闭状态
location /item{
proxy_pass http://10.13.122.51:8081;
}
……
}
步骤二:修改item.lua
文件,实现真实业务逻辑
-- 导入common函数库
local common = require('common')
local read_http = common.read_http
-- 导入cjson库
local cjson = require('cjson')
-- 获取路径参数
local id = ngx.var[1]
-- 根据id查询商品
local itemJSON = read_http("/item/".. id, nil)
-- 根据id查询商品库存
local itemStockJSON = read_http("/item/stock/".. id, nil)
-- JSON转化为lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(itemStockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold
-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item))
步骤三:演示
1.查看后台日志
说明:
后台查询成功
2.查看浏览器返回数据
说明:
前端数据返回成功
笔记小结:
- 概述:修改Nginx的配置,实现
upstream
负载均衡配置
说明:
在实际开发中,Tomcat的环境部署是不一定是单机,会是Tomcat集群来部署,因此这里实现Tomcat多态部署测试
步骤一:配置OpenResty
本地缓存
1.修改OpenResty
的nginx.conf
的配置文件
http{
……
# tomcat集群配置
upstream tomcat-cluster{
hash $request_uri;
server 10.13.122.51:8081;
server 10.13.122.51:8082;
}
upstream tomcat-cluster{
……
server{
……
location /item {
proxy_pass http://tomcat-cluster;
}
……
}
}
注意:
在编写该配置文件时,文件格式需要统一,建议手敲,不要复制,否则会出奇怪的报错!!
说明:
- 此处运用Nginx的
hash $request_uri;
的负载均衡算法,避免不同进程的Tomcat数据冗余
2.重启OpenreSty
docker restart openresty
说明:
刷新
openresty
的nginx.conf
的配置
步骤二:启动Tomcat
集群
Tomcat
实例步骤三:演示
Idea
日志说明:
查看浏览器,访问成功
笔记小结:
- 概述:在项目启动时,实现Redis中的数据提前加载
- 基本用例:搭建
Handler
处理类,实现InitializingBean
接口,重写afterPropertiesSet
方法,在此方法中实现缓存预热
说明:
服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。因此这里采用缓存预热的方式进行启动
缓存预热:
在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。
前提:
需要存在有密码的
Redis
服务,详细请查看搭建Redis
日志
步骤一:导入依赖
Springboot
整合Redis
依赖<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
步骤二:添加配置文件
1.修改application.yml
配置文件
spring:
redis:
host: 10.13.167.28
port: 6379
password: qweasdzxc
2.添加Redis
的热处理机制
说明:
此项目因为数据较少,因此全部取出并放入Redis
@Configuration
public class RedisHandler implements InitializingBean {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
IItemService itemService;
@Autowired
IItemStockService iItemStockService;
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
/**
* 初始化缓存
* 此方法会在项目启动时,本类加载完成,和@Autowired加载完成之后执行该方法
* @throws Exception 异常
*/
public void afterPropertiesSet() throws Exception {
// 1.获得Item数据
List<Item> itemList = itemService.list();
for (Item item : itemList) {
// 2.设置Key
String key = "item:id:" + item.getId();
// 3.将数据序列化
String jsonItem = MAPPER.writeValueAsString(item);
stringRedisTemplate.opsForValue().set(key, jsonItem);
}
// 4.获取stock数据
List<ItemStock> stockList = iItemStockService.list();
for (ItemStock itemStock : stockList) {
// 5.设置Key
String key = "itemStock:id:" + itemStock.getId();
// 6.将数据序列化
String jsonItem = MAPPER.writeValueAsString(itemStock);
stringRedisTemplate.opsForValue().set(key, jsonItem);
}
}
}
步骤三:演示
说明:
可以看到此项目在启动时已经查阅过数据库
说明:
通过Redis控制软件可以看到,数据已存在Redis中
笔记小结:
- 概述:封装Lua脚本Redis查询函数,实现Redis的查询数据查询
说明:
Tomcat中已将数据预热的方式加载近Redis。修改项目逻辑,实现
OpenResty
优先查询Redis再查询Tomcat
步骤一:创建/改写common.lua
文件
在/home/openresty/lualib目录下创建/改写common.lua文件,便于OpenResty的nginx.conf模块的导入
步骤二:编写common.lua
文件
1.导入Redis
模块,初始化Redis
对象
-- 导入redis
local redis = require('resty.redis')
-- 初始化redis
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
2.封装释放Redis
连接函数
-- 关闭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
3.封装根据Key查询Redis
函数
-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port, password, key)
-- 获取一个连接
local ok, err = red:connect(ip, port)
if not ok then
ngx.log(ngx.ERR, "连接redis失败 : ", err)
return nil
end
-- 验证密码
if password then
local res, err = red:auth(password)
if not res then
ngx.log(ngx.ERR, "Redis 密码认证失败: ", err)
close_redis(red)
return nil
end
end
-- 查询redis
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数据为空, key = ", key)
end
close_redis(red)
return resp
end
4.导出该方法
-- 将方法导出
local _M = {
read_http = read_http, -- 此方法为封装HTTP请求的工具导出
read_redis = read_redis
}
return _M
注意:
此
common.lua
文件内的连接只适用于连接单节点的Redis,不能用于连接Redis主从、分片式集群。若需要连接Redis集群,请参考lua 连接redis集群_lua连接redis cluster_CurryYoung11的博客-CSDN博客
补充:查看Common.lua
完整代码
-- 导入redis local redis = require('resty.redis') -- 初始化redis local red = redis:new() red:set_timeouts(1000, 1000, 1000) -- 关闭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的方法 ip和port是redis地址,key是查询的key local function read_redis(ip, port, password, key) -- 获取一个连接 local ok, err = red:connect(ip, port) if not ok then ngx.log(ngx.ERR, "连接redis失败 : ", err) return nil end -- 验证密码 if password then local res, err = red:auth(password) if not res then ngx.log(ngx.ERR, "Redis 密码认证失败: ", err) close_redis(red) return nil end end -- 查询redis 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数据为空, key = ", key) end close_redis(red) return resp end -- 封装函数,发送http请求,并解析响应 local function read_http(path, params) local resp = ngx.location.capture(path,{ method = ngx.HTTP_GET, args = params, }) if not resp then -- 记录错误信息,返回404 ngx.log(ngx.ERR, "http查询失败, path: ", path , ", args: ", args) ngx.exit(404) end return resp.body end -- 将方法导出 local _M = { read_http = read_http, read_redis = read_redis } return _M
步骤一:修改item.lua
文件,实现真实业务逻辑
1.导入common
函数库
-- 导入common函数库
local common = require('common')
local read_redis = common.read_redis
2.封装查询函数
-- 封装查询函数
function read_data(key, path, params)
-- 查询本地缓存
local val = read_redis("10.13.164.55", 7001, "qweasdzxc", key)
-- 判断查询结果
if not val then
ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)
-- redis查询失败,去查询http
val = read_http(path, params)
end
-- 返回数据
return val
end
3.修改商品、库查询的业务
-- 获取路径参数
local id = ngx.var[1]
-- 根据Id查询商品
local itemJSON = read_data("item:id:" .. id, "/item/" .. id,nil)
-- 根据Id查询商品库存
local stockJson = read_data("item:stock:id:" .. id, "/item/stock/" .. id,nil)
补充:查看Item.lua
完整代码
-- 导入common函数库 local common = require('common') local read_http = common.read_http local read_redis = common.read_redis -- 导入cjson库 local cjson = require('cjson') -- 封装查询函数 function read_data(key, path, params) -- 查询本地缓存 local val = read_redis("10.13.167.28", 6379, "qweasdzxc", key) -- 判断查询结果 if not val then ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key) -- redis查询失败,去查询http val = read_http(path, params) end -- 返回数据 return val end -- 获取路径参数 local id = ngx.var[1] -- 查询商品信息 local itemJSON = read_data("item:id:" .. id, "/item/" .. id, nil) -- 查询库存信息 local stockJSON = read_data("item:stock:id:" .. id, "/item/stock/" .. id, nil) -- JSON转化为lua的table local item = cjson.decode(itemJSON) local stock = cjson.decode(stockJSON) -- 组合数据 item.stock = stock.stock item.sold = stock.sold -- 把item序列化为json 返回结果 ngx.say(cjson.encode(item))
步骤二:重启OpenResty
docker restart openresty
说明:
重启服务,刷新Nginx.conf的配置
步骤三:演示
1.查看Idea
说明:
由于之前已完成Redis的预热,现在停止Tomcat的服务
2.查看浏览器
说明:
虽然停止了Tomcat服务,但数据存在Redis中。
Openresty
集群首先会查询Redis,因此数据依旧显示正常
笔记小结:
概述:封装Lua脚本Nginx查询函数,实现Nginx本地缓存的数据查询
说明:
当客户端访问时,优先查询
OpenResty
本地缓存,再进行Redis的查询,若Redis未查询成功,则查询Tomcat。实现多级缓存最后一关
OpenResty为Nginx提供了shard dict的功能,可以在nginx的多个worker之间共享数据,实现缓存功能。
基本用例:
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m;
说明:
修改
Openresty
的nginx.conf
配置
-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache
-- 存储, 指定key、value、过期时间,单位s,默认为0代表永不过期
item_cache:set('key', 'value', 1000)
-- 读取
local val = item_cache:get('key')
前提:
需要开启
Openresty
的共享缓存的配置
步骤一:修改查询函数
1.修改item.lua
文件中的read_data
函数,实现先查本地缓存,再查Redis缓存,最后查询Tomcat
-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache
-- 封装查询函数
function read_data(key, expire, path, params)
-- 首先,查询本地缓存
local val = item_cache:get(key)
if not val then
ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询Redis, key: ", key)
-- 然后,查询redis
val = read_redis("10.13.167.28", 6379, "qweasdzxc", key)
-- 判断查询结果
if not val then
ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)
-- 最后,redis查询失败,去查询http
val = read_http(path, params)
end
end
-- 查询成功,把数据写入本地缓存
item_cache:set(key, val, expire)
-- 返回数据
return val
end
2.修改item.lua
文件中的调用实例
-- 查询商品信息
local itemJSON = read_data("item:id:" .. id, 1800, "/item/" .. id, nil)
-- 查询库存信息
local stockJSON = read_data("item:stock:id:" .. id, 60, "/item/stock/" .. id, nil)
补充:完整item.lua
代码
-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入cjson库
local cjson = require('cjson')
-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache
-- 封装查询函数
function read_data(key, expire, path, params)
-- 查询本地缓存
local val = item_cache:get(key)
if not val then
ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询Redis, key: ", key)
-- 查询redis
val = read_redis("10.13.167.28", 6379, "qweasdzxc", key)
-- 判断查询结果
if not val then
ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)
-- redis查询失败,去查询http
val = read_http(path, params)
end
end
-- 查询成功,把数据写入本地缓存
item_cache:set(key, val, expire)
-- 返回数据
return val
end
-- 获取路径参数
local id = ngx.var[1]
-- 查询商品信息
local itemJSON = read_data("item:id:" .. id, 1800, "/item/" .. id, nil)
-- 查询库存信息
local stockJSON = read_data("item:stock:id:" .. id, 60, "/item/stock/" .. id, nil)
-- JSON转化为lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold
-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item))
步骤二:演示
1.刷新Nginx
本地缓存集群数据
说明:
当用户首次访问Nginx缓存失败时,会查询Redis
2.停止Redis
服务
docker stop myredis
3.查询浏览器数据
说明:
当停止掉Redis服务后,数据响应成功,说明Nginx缓存已启动
笔记小结:
- 概述:将多节点之间共享的缓存数据进行同步更新
- Canal:是用于数据库增量日志解析,提供增量数据订阅&消费。监听数据库的增量日志实现数据请求的同步与相应处理
缓存同步是指在分布式系统中,多个节点之间共享缓存数据并保持一致性的过程。当缓存中的数据发生变化时,需要将这些变化同步到其他节点的缓存中,以确保所有节点获取的数据都是最新的
缓存数据同步的方式:
设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新
同步双写:在修改数据库的同时,直接修改缓存
**异步通知:**修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
异步通知实现缓存同步
1.基于MQ的异步通知
说明:
商品服务完成对数据的修改后,只需要发送一条消息到MQ中。缓存服务监听MQ消息,然后完成对缓存的更新
2.基于Canal的通知
说明:
商品服务完成商品修改后,业务直接结束,没有任何代码侵入,Canal监听MySQL变化,当发现变化后,立即通知缓存服务,缓存服务接收到canal通知,更新缓存
概述
Canal [kə’næl],译意为水道/管道/沟渠,canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。GitHub的地址:https://github.com/alibaba/canal
Canal是基于Mysql的主从同步来实现的,MySQL主从同步的原理如下:
说明:
基于MySQL生成的二进制日志来实现主从同步
Canal就是把自己伪装成MySQL的一个slave节点,从而监听master的binary log变化。再把得到的变化信息通知给Canal的客户端,进而完成对其它数据库的同步。
说明:
当使用
Canal
时,还可以完成对其他数据库的同步
步骤一:配置MySQL主从
1.修改my.cnf
开启Binlog
vim /home/mysql/conf/my.cnf
2.添加如下内容
log-bin=/home/mysql/mysql-bin # 设置binary log文件的存放地址和文件名,叫做mysql-bin
binlog-do-db=heima # 指定对哪个database记录binary log events,这里记录heima这个库
补充:my.cnf
完整内容
[mysqld] skip-name-resolve character_set_server=utf8 datadir=/home/mysql server-id=1000 log-bin=/home/mysql/mysql-bin # 设置binary log文件的存放地址和文件名,叫做mysql-bin binlog-do-db=heima # 指定对哪个database记录binary log events,这里记录heima这个库
3.设置用户权限
3.1添加cannal
用户,并设置权限
create user canal@'%' IDENTIFIED by 'canal'; # 创建canal新用户,并指定密码canal
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT, SUPER ON *.* TO canal@'%';# 授予权限
FLUSH PRIVILEGES; # 刷新权限
3.2重启Mysql
容器
docker restart mysql
3.3查看主数据库二进制日志
show master status;
说明:查看结果
Position
为同步数据的偏移量,类似与Redis
中的偏移量为了实现主从同步
步骤二:配置网络
1.创建网络
docker network create heima
说明:
创建网络,将MySQL、Canal放到同一个Docker网络中
2.MySQL
加入网络
docker network connect heima mysql
步骤三:安装Canal
1.拉取镜像
docker pull canal/canal-server:v1.1.5
2.运行Canal
容器
docker run -p 11111:11111 --name canal \
-e canal.destinations=heima \
-e canal.instance.master.address=mysql:3306 \
-e canal.instance.dbUsername=canal \
-e canal.instance.dbPassword=canal \
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false \
-e canal.instance.filter.regex=heima\\..* \
--network heima \
-d canal/canal-server:latest
注意:
记得修改对应的账户和连接密码
说明:
运行
Canal
容器时,加入heima
网络
补充:参数含义
-p 11111:11111
:这是canal的默认监听端口-e canal.instance.master.address=mysql:3306
:数据库地址和端口,如果不知道mysql容器地址,可以通过docker inspect 容器id
来查看-e canal.instance.dbUsername=canal
:数据库用户名-e canal.instance.dbPassword=canal
:数据库密码-e canal.instance.filter.regex=
:要监听的表名称
补充:表名正则
- mysql 数据解析关注的表,Perl正则表达式.多个正则之间以逗号(,)分隔,转义符需要双斜杠(\) ,例如:
- 所有表:.* or .\…
- canal schema下所有表: canal\…
- canal下的以canal打头的表:canal\.canal.
- canal schema下的一张表:canal.test1
- 多个规则组合使用然后以逗号隔开:canal\…*,mysql.test1,mysql.test2
步骤四:演示
1.查看Canal
状态
docker logs canal
说明:
- 说明
Canal
启动成功
2.查看heima
数据库记录日志
docker exec -it canal bash # 进入容器内部
tail -f /home/admin/canal-server/logs/heima/heima.log
说明:查看结果
补充:
- 若日志输出报错
2023-07-07 16:22:16.085 [MultiStageCoprocessor-other-heima-0] WARN com.taobao.tddl.dbsync.binlog.LogDecoder - Skipping unrecognized binlog event Unknown from: mysql-bin.000005:2262
- 当前MySQL版本与Canal版本不匹配,请更换二者版本
说明:
配置Canal实现MySQL变化后自动更新Redis缓存和JVM缓存
前提:
已完成安装和配置Canal
步骤一:导入依赖
pom.xml
文件
<dependency>
<groupId>top.javatoolgroupId>
<artifactId>canal-spring-boot-starterartifactId>
<version>1.2.1-RELEASEversion>
dependency>
步骤二:编写配置
application.yml
canal:
destination: heima # canal实例名称,要跟canal-server运行时设置的destination一致
server: 10.13.164.55:11111 # canal地址
步骤三:编写实体类
Item
类@Data
@TableName("tb_item")
public class Item {
@TableId(type = IdType.AUTO)
@Id //canal中, 标记表中的id字段
private Long id;//商品id
@Column(name = "name") //canal中, 标记表中与属性名不一致的字段,此处便于做演示,因此设置一下
private String name;//商品名称
private String title;//商品标题
private Long price;//价格(分)
private String image;//商品图片
private String category;//分类名称
private String brand;//品牌名称
private String spec;//规格
private Integer status;//商品状态 1-正常,2-下架
private Date createTime;//创建时间
private Date updateTime;//更新时间
@TableField(exist = false)
@Transient // canal中,标记不属于表中的字段
private Integer stock;
@TableField(exist = false)
@Transient
private Integer sold;
}
说明:
Canal推送给canal-client的是被修改的这一行数据(row),而我们引入的canal-client则会帮我们把行数据封装到Item实体类中。这个过程中需要知道数据库与实体的映射关系,要用到JPA的几个注解
步骤四:编写监听器
ItemHandler
类并实现EntryHandler
接口,并重写insert、update、delete
方法@CanalTable("tb_item") //指定要监听的表
@Component // 将监听交给Spring管理
public class ItemHandler implements EntryHandler<Item> {
@Autowired
RedisHandler redisHandler;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private Cache<Long, Item> itemCache;
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 监听到商品插入
*
* @param item 商品
*/
@Override
public void insert(Item item) {
// 写数据到JVM进程缓存
itemCache.put(item.getId(), item);
// 写数据到redis
saveItem(item);
}
/**
* 监听到商品数据修改
*
* @param before 商品修改前
* @param after 商品修改后
*/
@Override
public void update(Item before, Item after) {
// 写数据到JVM进程缓存
itemCache.put(after.getId(), after);
// 写数据到redis
saveItem(after);
}
/**
* 监听到商品删除
*
* @param item 商品
*/
@Override
public void delete(Item item) {
// 删除数据到JVM进程缓存
itemCache.invalidate(item.getId());
// 删除数据到redis
deleteItemById(item.getId());
}
private void saveItem(Item item) {
try {
String json = MAPPER.writeValueAsString(item);
stringRedisTemplate.opsForValue().set("item:id:" + item.getId(), json);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
private void deleteItemById(Long id) {
stringRedisTemplate.delete("item:id:" + id);
}
}
步骤五:演示
1.查看IDEA
日志
说明:
当前项目已实现
Canal
的MySQL的消息监听
2.修改数据库信息后,查看浏览器数据已改动
说明:
当看到数据变动时,说明JVM缓存已刷新,Redis数据已刷新,不信自己查看
说明:
现在,本地项目已完成搭建单结点
OpenResty
,若需要搭建多节点,记得再Nginx反向代理时,配置本地缓存的集群,并制作负载均衡
基于Redis6的学习,现提出如下疑问:
Openresty
缓存失效该如何解决?对于敏感的数据实现主动更新Redis
宕机该如何解决?需要使用Lua脚本访问Redis集群笔记小结:查看各个小结
笔记小结:
- Key的最佳实践:
- 固定格式:[业务名]:[数据名]:[id]
- 足够简短:不超过44字节
- 不包含特殊字符
- Value的最佳实践:
- 合理的拆分数据,拒绝BigKey
- 选择合适数据结构
- Hash结构的entry数量不要超过1000
- 设置合理的超时时间
- 恰当的数据类型,例如Hash结构等等
Redis的Key虽然可以自定义,但最好遵循下面的几个最佳实践约定:
说明:例如
优点:
补充:
- 当Key值超过44个字节后,会自动的用
raw
格式,而采用非连续性空间,因此占用更多的内存
补充:
存储一个数据时,在内存中占用字节往往比数据值本身更多,因为Redis底层会存储元信息
含义:
在Redis中,BigKey(大键)是指占用存储空间较大的键值对。当一个键值对的大小超过了Redis的配置阈值(默认为10KB),就被认为是一个BigKey。
BigKey的危害:
网络阻塞
对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢
数据倾斜
BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
Redis阻塞
对元素较多的hash、list、zset等做运算会耗时较旧,使主线程被阻塞
CPU压力
对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用
发现BigKey:
redis-cli --bigkeys
利用redis-cli提供的–bigkeys参数,可以遍历分析所有key,并返回Key的整体统计信息与每个数据的Top1的big key
scan扫描
自己编程,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度(此处不建议使用MEMORY USAGE)
第三方工具✔️
利用第三方工具,如 Redis-Rdb-Tools 分析RDB快照文件,全面分析内存使用情况
网络监控
自定义工具,监控进出Redis的网络数据,超出预警值时主动告警
删除BigKey:
redis 3.0 及以下版本
如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey
Redis 4.0以后
Redis在4.0后提供了异步删除的命令:unlink
说明:
说明:
选择合适数据结构进行存储,底层占用的空间更小
说明:
现在最严重的问题就是entry过多,造成了BigKey问题。那该如何解决呢?
说明:
String类型暴力简单,但内存没有太多的优化
说明:
当把Key值进行拆分,让每一个Hash的entry都为100,这样
entry
不超过500,所以数据会采用Hash表的存储方式,从而减少了内存存储
笔记小结:
- 概述:当时数据量传输过大时,可以使用批处理方案,减少网络传输的耗时,提供业务的执行时间
- 批量处理的方案:
- 原生的M操作
- Pipeline批处理
- 注意:Pipeline的多个命令之间不具备原子性。批处理时不建议一次携带太多命令
说明:
当有N次命令响应时,一条一条的命令传输中间会因为网络延迟的时间而增大响应时间。因为Redis命令执行时间的并发量并不高是五万分之一。因此,命令的响应时间会因为网络传输耗时而大大增加
说明:
当有N次命令响应时,一次执行多个命令,则会减少网络延迟的时间。此时Redis命令执行时的并发量也不高。因此,命令的响应时间会大大减少
Redis提供了很多Mxxx这样的命令,可以实现批量插入数据,例如:
代码示例:利用mset批量插入10万条数据
@Test
void testMxx() {
String[] arr = new String[2000];
int j;
for (int i = 1; i <= 100000; i++) {
j = (i % 1000) << 1;
arr[j] = "test:key_" + i;
arr[j + 1] = "value_" + i;
if (j == 0) {
jedis.mset(arr);
}
}
}
说明:
当使用位移运算符移动一位时,任何数字都会被乘以2,此时刚好将2000个容量大小的数组以Key:value的形式填满
注意:
不要在一次批处理中传输太多命令,否则单次命令占用带宽过多,会导致网络阻塞
MSET虽然可以批处理,但是却只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用Pipeline功能
@Test
void testPipeline() {
// 创建管道
Pipeline pipeline = jedis.pipelined();
for (int i = 1; i <= 100000; i++) {
// 放入命令到管道
pipeline.set("test:key_" + i, "value_" + i);
if (i % 1000 == 0) {
// 每放入1000条命令,批量执行
pipeline.sync();
}
}
}
说明:
再管道中,可以添加任一命令。将管道中的命令批量的发送到Redis中,依次执行。执行命令的时间较
MSET
命令多,因为管道内的命令到达Redis中时,会形成队列依次执行。若此时Redis中有多个命令执行,则会造成管道内的命令延时执行
说明:
MSET或Pipeline这样的批处理需要在一次请求中携带多条命令,而此时如果Redis是一个集群,那批处理命令的多个key必须落在一个插槽中,否则就会导致执行失败
代码示例:
@SpringBootTest
public class MultipleTest {
@Test
void testMsetInCluseter() {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
Map<String, String> map = new HashMap<>();
map.put("name", "yueyue");
map.put("age", "18");
stringRedisTemplate.opsForValue().multiSet(map);
}
}
说明:
当使用批处理命令时,Spring框架提供的一套对Redis的批处理操作会对是否对集群进行判断,来处理
笔记小结:
- 持久化配置:预留足够的内存空间,不与CPU高密集应用一起部署
- 慢查询:配置慢查询阈值以及慢查询容量上限,慢查询日志列表以及日志长度查询等运维操作
- 命令以及安全配置:禁用
keys *
等命令,设置Redis密码,开启防火墙
Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:
部署有关建议:
慢查询:慢查询日志是Redis服务端在命令执行前后计算每条命令的执行时长,当超过某个阈值是记录下来的日志。
说明:
慢查询,会因为Redis执行的命令较多而等待,等待而超过阈值
查看慢查询日志列表:
补充:
设置慢查询的阈值:
说明:
一般执行一条命令都是在十几微妙左右
慢查询会被放入慢查询日志中,日志的长度有上限,可以通过配置指定:
说明:
可以调整慢查询的日志,方便查询检索
补充:修改这两个配置可以使用:config set
命令
Redis会绑定在0.0.0.0:6379,这样将会将Redis服务暴露到公网上,而Redis如果没有做身份认证,会出现严重的安全漏洞.
漏洞重现方式:https://cloud.tencent.com/developer/article/1039000
漏洞出现的核心的原因有以下几点:
为了避免这样的漏洞,这里给出一些建议:
Redis一定要设置密码
禁止线上使用下面命令:keys、flushall、flushdb、config set等命令。可以利用rename-command禁用。
说明:
bind:限制网卡,禁止外网网卡访问
说明:
开启防火墙
不要使用Root账户启动Redis
尽量不是有默认的端口
笔记小结:
- 概述:适当配置内存中的复制缓存区域,AOF缓存区域,客户端缓存区的内存容量大小,可提高性能
当Redis内存不足时,可能导致Key频繁被删除、响应时间变长、QPS不稳定等问题。当内存使用率达到90%以上时就需要我们警惕,并快速定位到内存占用的原因。
内存占用 | 说明 |
---|---|
数据内存 | 是Redis最主要的部分,存储Redis的键值信息。主要问题是BigKey问题、内存碎片问题 |
进程内存 | Redis主进程本身运⾏肯定需要占⽤内存,如代码、常量池等等;这部分内存⼤约⼏兆,在⼤多数⽣产环境中与Redis数据占⽤的内存相⽐可以忽略。 |
缓冲区内存 | 一般包括客户端缓冲区、AOF缓冲区、复制缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,不当使用BigKey,可能导致内存溢出。 |
Redis提供了一些命令,可以查看到Redis目前的内存分配状态:
内存缓冲区常见的有三种:
默认的配置如下:
笔记小结:
- 概述:搭建集群需要考虑带宽,数据倾斜,数据完整性,客户端性能等诸多问题
- 注意:分片式集群以及主从集群中存在默认的集群数据完整性的配置问题,需要将
cluster-require-full-coverage
配置为false
即可根据需求提高Redis集群性能- 搭建的节点数不易太多,避免节点之间的业务时间超时
集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:
在Redis的默认配置中,如果发现任意一个插槽不可用,则整个集群都会停止对外服务:
补充:
为了保证高可用特性,这里建议将
cluster-require-full-coverage
配置为false
集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:
集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高。
解决途径:
集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:
注意:
单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用特性。如果主从能满足业务需求的情况下,尽量不搭建Redis集群。
sdown
表示,哨兵主观意识上认为此节点已宕机sentinel auth-pass mymaster qweasdzxc
步骤一:准备基础环境
1.创建目录
cd home
mkdir mysql
cd mysql
mkdir logs
mkdir data
mkdir conf
步骤二:运行容器
sudo docker run \
-p 3306:3306 \
--name mysql \
-v /home/mysql/logs:/logs \
-v /home/mysql/data:/var/lib/mysql \
-v /home/mysql/conf:/etc/mysql/conf.d \
-e MYSQL_ROOT_PASSWORD=qweasdzxc \
-d mysql:latest
步骤三:添加配置
1.进入conf
目录创建my.cnf
文件
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/home/mysql
server-id=1000
2.重启容器
docker restart mysql
步骤四:初始化项目表
/*
Navicat Premium Data Transfer
Source Server : 192.168.150.101
Source Server Type : MySQL
Source Server Version : 50725
Source Host : 192.168.150.101:3306
Source Schema : heima
Target Server Type : MySQL
Target Server Version : 50725
File Encoding : 65001
Date: 16/08/2021 14:45:07
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for tb_item
-- ----------------------------
DROP TABLE IF EXISTS `tb_item`;
CREATE TABLE `tb_item` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品id',
`title` varchar(264) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '商品标题',
`name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '商品名称',
`price` bigint(20) NOT NULL COMMENT '价格(分)',
`image` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品图片',
`category` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '类目名称',
`brand` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '品牌名称',
`spec` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '规格',
`status` int(1) NULL DEFAULT 1 COMMENT '商品状态 1-正常,2-下架,3-删除',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `status`(`status`) USING BTREE,
INDEX `updated`(`update_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 50002 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '商品表' ROW_FORMAT = COMPACT;
-- ----------------------------
-- Records of tb_item
-- ----------------------------
INSERT INTO `tb_item` VALUES (10001, 'RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4', 'SALSA AIR', 16900, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp', '拉杆箱', 'RIMOWA', '{\"颜色\": \"红色\", \"尺码\": \"26寸\"}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');
INSERT INTO `tb_item` VALUES (10002, '安佳脱脂牛奶 新西兰进口轻欣脱脂250ml*24整箱装*2', '脱脂牛奶', 68600, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t25552/261/1180671662/383855/33da8faa/5b8cf792Neda8550c.jpg!q70.jpg.webp', '牛奶', '安佳', '{\"数量\": 24}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');
INSERT INTO `tb_item` VALUES (10003, '唐狮新品牛仔裤女学生韩版宽松裤子 A款/中牛仔蓝(无绒款) 26', '韩版牛仔裤', 84600, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t26989/116/124520860/644643/173643ea/5b860864N6bfd95db.jpg!q70.jpg.webp', '牛仔裤', '唐狮', '{\"颜色\": \"蓝色\", \"尺码\": \"26\"}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');
INSERT INTO `tb_item` VALUES (10004, '森马(senma)休闲鞋女2019春季新款韩版系带板鞋学生百搭平底女鞋 黄色 36', '休闲板鞋', 10400, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/29976/8/2947/65074/5c22dad6Ef54f0505/0b5fe8c5d9bf6c47.jpg!q70.jpg.webp', '休闲鞋', '森马', '{\"颜色\": \"白色\", \"尺码\": \"36\"}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');
INSERT INTO `tb_item` VALUES (10005, '花王(Merries)拉拉裤 M58片 中号尿不湿(6-11kg)(日本原装进口)', '拉拉裤', 38900, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t24370/119/1282321183/267273/b4be9a80/5b595759N7d92f931.jpg!q70.jpg.webp', '拉拉裤', '花王', '{\"型号\": \"XL\"}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');
-- ----------------------------
-- Table structure for tb_item_stock
-- ----------------------------
DROP TABLE IF EXISTS `tb_item_stock`;
CREATE TABLE `tb_item_stock` (
`item_id` bigint(20) NOT NULL COMMENT '商品id,关联tb_item表',
`stock` int(10) NOT NULL DEFAULT 9999 COMMENT '商品库存',
`sold` int(10) NOT NULL DEFAULT 0 COMMENT '商品销量',
PRIMARY KEY (`item_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;
-- ----------------------------
-- Records of tb_item_stock
-- ----------------------------
INSERT INTO `tb_item_stock` VALUES (10001, 99996, 3219);
INSERT INTO `tb_item_stock` VALUES (10002, 99999, 54981);
INSERT INTO `tb_item_stock` VALUES (10003, 99999, 189);
INSERT INTO `tb_item_stock` VALUES (10004, 99999, 974);
INSERT INTO `tb_item_stock` VALUES (10005, 99999, 18649);
SET FOREIGN_KEY_CHECKS = 1;
#user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
# nginx的业务集群,nginx本地缓存、redis缓存、tomcat查询
upstream nginx-cluster{
server 10.13.164.55:8081;
}
server {
listen 8080;
server_name localhost;
location /api {
proxy_pass http://nginx-cluster;
}
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
步骤一:添加redis.conf
配置文件
bind 0.0.0.0
protected-mode no
port 6379
tcp-backlog 511
requirepass qweasdzxc
timeout 0
tcp-keepalive 300
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 30
always-show-logo yes
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ./
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-disable-tcp-nodelay no
replica-priority 100
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
appendonly yes
appendfilename "appendonly.aof"
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
lua-time-limit 5000
slowlog-max-len 128
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes
步骤二:运行Redis
容器
sudo docker run \
--restart=always \
-p 6379:6379 \
--name myredis \
-v /home/redis/myredis/redis.conf:/etc/redis/redis.conf \
-v /home/redis/myredis/data:/data \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes \
--requirepass qweasdzxc
说明:
若此Redis容器需要进行远程连接,那么需要配置密码,也就是添加
--requirepass
参数