当前位置:   article > 正文

黑马Redis6高级篇_redis 黑马

redis 黑马

文章目录

1.分布式缓存

笔记小结:

  • 请查看各个小节
  • 总结:详细请查看

1.1概述

image-20230703193355238

说明:

​ 单点Redis的问题

1.2Redis持久化

笔记小结:

  1. 概述:Redis是一个内存数据库,它可以通过持久化机制将数据保存到磁盘上
  2. RDB(Redis数据备份文件):
    • Redis会开启异步线程自动的备份数据文件
    • Redis默认停机时会执行一次RDB
    • Redis会执行RDB每十五分钟修改1次,每五分钟修改10次,每一分钟修改10000次即进行一次RDB持久化
  3. AOF追加文件):
    • Redis处理的每一个写命令都会记录在AOF文件
    • 常用操作:开启AOF、修改记录频率、设置触发阈值

1.2.1概述

​ Redis是一个内存数据库,它可以通过持久化机制将数据保存到磁盘上,以防止数据丢失。

1.2.2RDB

1.2.2.1概述

RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。

​ RDB缺点,Redis的执行间隔时间长,两次RDB之间写入数据有丢失的风险、fork子进程、压缩、写出RDB文件都比较耗时

image-20230703194701015

说明:

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

image-20230703194026690

1.2.2.2基本用例
  • 修改记录频率
# 900秒内,如果至少有1个key被修改,则执行bgsave , 如果是save "" 则表示禁用RDB
save 900 1  
save 300 10  
save 60 10000 
  • 1
  • 2
  • 3
  • 4

说明:

Redis内部有触发RDB的机制,可以在redis.conf文件中找到

  • 其余参数设置
# 是否压缩 ,建议不开启,压缩也会消耗cpu,磁盘的话不值钱
rdbcompression yes

# RDB文件名称
dbfilename dump.rdb  

# 文件保存的路径目录
dir ./ 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

说明:

  • 默认情况下,是否压缩是处于开启状态
  • RDB的其它配置也可以在redis.conf文件中设置
1.2.2.3原理

image-20230703195659534

说明:

  • 在Redis中,主进程不会去直接读取物理内存中的数据,而是通过页表的方式进行对物理内存的映射而进行去读
  • bgsave命令开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件
  • fork采用的是copy-on-write技术:当主进程执行读操作时,访问共享内存、当主进程执行写操作时,则会拷贝一份数据,执行写操作

1.2.3AOF

1.2.3.1概述

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

image-20230703202349098

说明:

​ 每一个命令都记录到AOF文件中,命令文件不断的增大

1.2.3.2基本用例
  • 开启AOF
# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
  • 1
  • 2
  • 3
  • 4

说明:

  • AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF
  • 开启AOF功能的时候,建议关闭RDB功能
  • 修改记录频率
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always 
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec 
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

说明:

  • AOF的命令记录的频率也可以通过redis.conf文件来配

image-20230703203107575

  • 设置触发阈值
# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写 
auto-aof-rewrite-min-size 64mb 
  • 1
  • 2
  • 3
  • 4

说明:

  • 因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义
  • 通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果

image-20230703203217395

1.2.4总结

image-20230703203716075

说明:

​ RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用

1.3Redis主从

笔记小结:

  1. 概述:主从集群,实现读写分离,提高数据可靠性与完整性
  2. 全量同步原理:数据标记Replication 偏移量offset生成RDB文件、记录repl_baklogRedis命令缓存区
  3. 增量同步原理:Redis重启后进行repl_baklog命令缓存区的命令重写、偏移量offset覆盖、全量同步
  4. 总结:详细请查看

1.3.1概述

image-20230703204046762

说明:

​ 单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离

1.3.2搭建主从集群

image-20210630111505799

三个Redis节点信息如下:

IPPORT角色
10.13.164.556379master
10.13.164.556380slave
10.13.164.556381slave

说明:

​ 主节点用于写操作,子节点用于读取操作

步骤一:配置环境

说明:

​ 本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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

说明:查看结果

image-20230704100054374

  • 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的密码
  • 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

注意:

注意,此处需要将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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

说明:查看结果

image-20230704075631620

2.建立主从关系

slaveof  10.13.164.55 6379  # 配置 主节点的Ip地址以及端口号
  • 1

步骤三:测试

  • 在Master节点上查看连接状态
info repilication
  • 1

说明:查看结果

image-20230704154349094

  • 此处发现子节点的IP地址与端口相互对应

image-20230704102559036

  • 但在主节点中设置数据依旧能够在子节点中读取,说明搭建成功

1.3.3全量同步原理

image-20230704103819310

说明:

​ 主从第一次同步是全量同步。当子节点进行首次同步时,会向主节点发送请求,并判断携带过去的数据版本等信息。稍后主节点将已有的数据生成RDB文件发送子节点,若此时有较新的数据会被记录为命令保存在repl_baklog文件中,不断的与子节点保持同步

补充:master如何判断slave是不是第一次来同步数据?

  • Replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid
  • offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset

image-20230704104536158

说明:

  • 如果slave的replid与主节点不一致则代表为首次同步
  • 如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。

​ 因此slave做数据同步,必须向master声明自己的replication id 和offset,master才可以判断到底需要同步哪些数据

同步流程:

  1. slave节点请求增量同步
  2. master节点判断replid,发现不一致,拒绝增量同步
  3. master将完整内存数据生成RDB,发送RDB到slave
  4. slave清空本地数据,加载master的RDB
  5. master将RDB期间的命令记录在repl_baklogRedis命令缓存区,并持续将log中的命令发送给slave
  6. slave执行接收到的命令,保持与master之间的同步

1.3.4增量同步原理

image-20230704111645808

说明:

​ 主从第一次同步是全量同步,但如果slave重启后同步,则执行增量同步

补充:

​ repl_baklog大小有上限,写后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步

1.3.5总结

若全量同步,不得不做。那么我们可以优化Redis主从集群来对Redis主从集群做优化

  • Redis集群优化

    说明:优化方案

    • master中配置文件中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO。
    • Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
    • 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
    • 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力

    image-20230704112031578

  • 全量同步与增量同步

    1. 全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave

      说明:

      ​ 当slave节点第一次连接master节点时、当save节点断开时间太久,repl_baklog中的offset已经被覆盖时会执行全量同步

    2. 增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave

      说明:

      ​ 当slave节点断开又恢复,并且在repl_baklog中能找到offset时会执行增量同步

      注意:

      ​ 增量同步,可能同步失败,取决于repl_baklog区域是否被完全覆盖

补充:

​ slave节点宕机恢复后可以找master节点同步数据,那如果master节点宕机后,则无法进行恢复。若解决此问题请看下一节

1.4Redis哨兵

笔记小结:

  1. 概述:
    • 含义:监控和管理Redis实例的自动故障转移
    • 监控状态:主观下线和客观下线
    • Master选举权:断开时间长短、slave-priority权重值大小、offset偏移量、运行id大小
    • 故障转移:让slave结点成为新master的从节点、标记故障节点
  2. 基本用例:导入spring-boot-starter-data-redis依赖配置yml文件的哨兵主节点,集群子节点、配置类配置LettuceClientConfigurationBuilderCustomizer并设置集群的读取模式
  3. 总结:详细请查看

1.4.1概述

1.4.1.1含义

​ Redis的哨兵机制(Sentinel)是Redis提供的一种高可用性解决方案,用于监控和管理Redis实例的自动故障转移。

​ 哨兵机制的核心是一组独立运行的哨兵进程,它们监控Redis主节点和其对应的多个从节点,并在主节点发生故障时自动将一个从节点升级为新的主节点,从而实现故障转移。

image-20230704141105927

​ 哨兵的结构和作用有,监控:Sentinel 会不断检查您的master和slave是否按预期工作。自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主。通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端

1.4.1.2服务状态监控

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

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

image-20230704141313476

说明:

​ 在Redis的配置文件中,可以设置为Sentinel的一半数量

1.4.1.3Masterr选举权

若Sentinel发现Master节点故障,Sentinel需要在salve中选择一个作为新的master,规则如下:

  • 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
  • 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举
  • 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
  • 最后是判断slave节点的运行id大小,越小优先级越高
1.4.1.4故障转移

当选中了其中一个slave为新的master后(例如slave1),故障的转移的步骤如下:

  • sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
  • sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令,让这些slave成为新master的从节点,开始从新的master上同步数据
  • 最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点

image-20230704143242907

1.4.2搭建哨兵集群

image-20230704141105927

3个Sentinel示例信息如下:

IPPORT
10.13.164.5527001
10.13.164.5527001
10.13.164.5527001

步骤一:配置环境

说明:

​ 本哨兵集群节点采用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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

说明:查看结果

image-20230704151026887

  • 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

说明:

  1. port 27001: 设置当前 Redis Sentinel 的监听端口为 27001。
  2. sentinel announce-ip 10.13.164.55: 设置 Sentinel 在向其他节点宣告自己的 IP 地址时使用的 IP 地址为 10.13.164.55。
  3. sentinel monitor mymaster 10.13.164.55 6379 2: 设置 Sentinel 监控名为 mymaster 的主节点,主节点的 IP 地址为 10.13.164.55,端口号为 6379,2 表示在主节点进入下线状态后 Sentinel 需要等待的时间(单位是秒),超过这个时间 Sentinel 将主节点标记为下线状态。
  4. sentinel auth-pass mymaster qweasdzxc: 设置 Sentinel 连接主节点时需要使用的密码为 qweasdzxc,用于进行身份验证。
  5. sentinel down-after-milliseconds mymaster 5000: 设置 Sentinel 认为主节点下线的时间阈值为 5000 毫秒(即 5 秒),如果在这个时间内没有收到主节点的响应,则认为主节点已经下线。
  6. sentinel failover-timeout mymaster 60000: 设置进行故障转移的超时时间为 60000 毫秒(即 60 秒),如果在这个时间内没有完成故障转移,则认为故障转移失败。
  7. 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
  • 1
  • 2
  • 3
  • 4
  • 5
docker run --restart=always \
--net=host \
--name  mysentinel2 \
-v /home/redis/mysentinel2/sentinel.conf:/sentinel.conf \
-d redis redis-sentinel /sentinel.conf
  • 1
  • 2
  • 3
  • 4
  • 5
docker run --restart=always \
--net=host \
--name  mysentinel3 \
-v /home/redis/mysentinel3/sentinel.conf:/sentinel.conf \
-d redis redis-sentinel /sentinel.conf
  • 1
  • 2
  • 3
  • 4
  • 5

注意:

​ 配置文件sentinel.conf,需要与各自的监控节点相对应

步骤三:测试

1.停止Master节点查询sentinel日志image-20210701222857997

2.查看7003的日志:

image-20210701223025709

3.查看7002的日志:

image-20210701223131264

说明:

​ 此时已选举7003节点作为新的主节点,同我们的6380节点成为主节点打印消息一致

1.4.3基本用例

说明:

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

步骤一:导入依赖

  • 修改pom.xml文件
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId> 
</dependency>
  • 1
  • 2
  • 3
  • 4

步骤二:添加配置

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

2.添加RedisConfig配置文件类

@Configuration
public class RedisConfig {
    @Bean
    LettuceClientConfigurationBuilderCustomizer getLettuceClientConfigurationBuilderCustomizer(){
        // 设置集群的读取模式,先读取从结点,若失败则再读取主节点
        return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

步骤三:测试

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";
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

2.进入控制台请求测试

image-20230704175039056

3.查看Idea日志

说明:

​ 日志打印正常,说明测试通过

4.测试主节点宕机

image-20230704175334479

说明:

​ 待故障节点恢复后会自动加入主节点,说明测试通过

1.4.4总结

  • Sentinel的三个作用是什么?
    • 故障转移
    • 监控
    • 通知
  • Sentinel如何判断一个redis实例是否健康?
    • 每隔1秒发送一次ping命令,如果超过一定时间没有相向则认为是主观下线
    • 如果大多数sentinel都认为实例主观下线,则判定服务下线
  • 故障转移步骤有哪些?
    • 首先选定一个slave作为新的master,执行slaveof no one
    • 然后让所有节点都执行slaveof 新master
    • 修改故障节点,执行slaveof 新master

补充:

​ 主从和哨兵可以解决高可用、高并发读的问题。但是依然问题没有解决:海量数据存储问题、高并发写的问题

1.5Redis分片集群

笔记小结:

  1. 概述:将数据划分为多个分片并分布到不同的节点上,可以实现数据的水平扩展和负载均衡。提高了集群的容量和性能
  2. 散列插槽:
    • 含义:通过自定义的方式让数据可以让数据存在指定的Redis的插槽中
    • 注意:自定义key时,key中包含**“{ }”,且“{ }”中至少包含1个字符,“{ }”的部分是有效部分**
  3. 集群伸缩:add-node 添加结点、reshard分配插槽del-node删除结点
  4. 故障转移:cluster failover成为主节点、原理,使用Offset偏移量
  5. Java访问:导入spring-boot-starter-data-redis依赖SpringBoot整合Redis、配置yml文件的主节点,从集群从节点、配置类配置LettuceClientConfigurationBuilderCustomizer设置集群的读取模式

1.5.1概述

​ Redis 分片集群是一种将数据分布在多个 Redis 节点上的方案,通过将数据划分为多个分片并分布到不同的节点上,可以实现数据的水平扩展和负载均衡。每个节点都可以独立地处理一部分数据,并且可以通过添加或删除节点来动态调整集群的容量和性能。

image-20230704192200419

说明:

​ 集群中有多个master,每个master保存不同数据。每个master都可以有个slave节点。master之间通过ping监测彼此健康状态。客户端请求可以访问集群任意节点,最终都会被转发到正确节点

1.5.2搭建分片集群

image-20230704192711405

6个Redis实例信息如下:

IPPORT角色
10.13.164.557001master
10.13.164.557002master
10.13.164.557003master
10.13.164.557004slave
10.13.164.557005slave
10.13.164.557006slave

步骤一:配置环境

说明:

​ 本哨兵集群节点采用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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

说明:查看结果

image-20230704203318700

  • 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"
  • 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

步骤二:运行容器

  • 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

步骤三:创建集群

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 
  • 1

说明:

  • 访问其中一台集群结点,连接上其中一台客户端并创建集群

image-20230704211910510

  • 查看结点状态redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc cluster node

image-20230704212042062

补充:参数解释

--cluster-replicas 用于创建 Redis 分片集群时指定每个主节点要拥有的从节点数量参数。为 1,表示可以为每个主节点自动创建一个从节点。

1.5.3散列插槽

​ 散列插槽(hash slots)是 Redis 分片集群中的一种数据分片机制。它将数据分散存储在多个节点上,实现数据的水平分布和负载均衡。

​ 在 Redis 分片集群中,Redis Cluster 将整个数据集划分为固定数量的散列插槽(通常是 16384 个插槽)。每个键都会通过哈希函数计算得到一个插槽号(slot number),然后根据插槽号将键值对分配到对应的节点上。

image-20230704213609355

​ 在 Redis 分片集群中,数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:

  • key中包含"{}",且“{}”中至少包含1个字符,“{}”中的部分是有效部分
  • key中不包含“{}”,整个key都是有效部分

image-20230704213644475

说明:

​ key是num,那么就根据num计算,如果是{itcast}num,则根据itcast计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。若要取得该数据则根据key的有效部分计算哈希值,对16384取余,余数作为插槽,寻找插槽所在实例即可

补充:

​ 如果将同一类数据固定的保存在同一个Redis实例,那么这一类数据使用相同的有效部分,例如key都以{typeId}为前缀

1.5.4集群伸缩

  • 添加结点

步骤一:搭建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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

步骤二:添加结点到现有集群

# 格式
# add-node       new_host:new_port existing_host:existing_port
#               --cluster-slave
#                --cluster-master-id <arg>
# 例如
 redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc --cluster add-node 10.13.164.55:7007 10.13.164.55:7001
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

说明:查看结果

image-20230705071930728

  • 查看插槽数redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc cluster nodes

image-20230705072306064

  • 发现此Master结点并没有分配插槽数,需要分配插槽才能继续使用
  • 分配插槽
# 格式 reshard        host:port
#                 --cluster-from <arg>
#                 --cluster-to <arg>
#                 --cluster-slots <arg>
#                 --cluster-yes
#                 --cluster-timeout <arg>
#                 --cluster-pipeline <arg>
#                 --cluster-replace
# 例如
redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc --cluster reshard 10.13.164.55:7001
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

说明:查看结果

  • 重新分配7001插槽给7007结点

image-20230705073336346

  • 查看插槽数redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc cluster nodes

image-20230705073506235

  • 删除结点

步骤一:转移插槽

redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc --cluster reshard 10.13.164.55:7001
  • 1

image-20230705074319405

步骤二:删除结点

# 格式 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
  • 1
  • 2
  • 3

说明:查看结果

image-20230705074448159

  • 查看插槽数redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc cluster nodes

image-20230705074605542

  • 此时可发现,7007结点已消失

1.5.5故障转移

  • 查看主从切换
watch redis-cli -h 10.13.164.55 -p 7001 -a qweasdzxc cluster nodes
  • 1

说明:

image-20230705075921104

  • 观察Master结点变化,发现已经更换

image-20230705080308741

  • 数据迁移

步骤一:连接子结点

redis-cli -h 10.13.164.55 -p 7002 -a qweasdzxc
  • 1

步骤二:切换结点

cluster failover
  • 1

说明:

image-20230705091557789

  • 可以看出,再次变为主节点

补充:

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

image-20230705091729298

  • 手动的Failover支持三种不同模式:缺省:默认的流程,如图1~6歩、force:省略了对offset的一致性校验、takeover:直接执行第5歩,忽略数据一致性、忽略master状态和其它master的意见

1.5.6基本用例

说明:

​ 在Sentinel集群监管下的Redis分片集群,其节点会因为自动故障转移而发生变化,Redis的客户端必须感知这种变化,及时更新连接信息。Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换

步骤一:导入依赖

  • 修改pom.xml文件
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId> 
</dependency>
  • 1
  • 2
  • 3
  • 4

步骤二:添加配置

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

2.添加RedisConfig配置文件类

@Configuration
public class RedisConfig {
    @Bean
    LettuceClientConfigurationBuilderCustomizer getLettuceClientConfigurationBuilderCustomizer(){
        // 设置集群的读取模式,先读取从结点,若失败则再读取主节点
        return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

步骤三:测试

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";
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

2.进入控制台请求测试

image-20230704175039056

3.查看Idea日志

image-20230705093255725

说明:

​ 通过日志,可以发现读写是分离的

1.6总结

1.Redis主从集群与Redis分片集群优缺点对比

  • Redis主从集群

    • 优点:

      • 数据复制:主节点将数据复制到从节点,实现数据的备份和冗余,提数据的可靠性可用性

      • 读写分离:主节点负责写操作,从节点负责读操作,提了系统的并发处理能力读取性能

      • 故障容错:当主节点发生故障时,可以自动切换为从节点为新的主节点,实现高可用性

    • 缺点:

      • 写操作依赖于主节点,主节点的性能稳定性对整个集群的影响较大。
      • 读写分离可能导致数据的延迟,因为从节点的数据不一定与主节点实时同步。
  • Redis分片集群

    • 优点:

      • 数据分片:将数据分散存储在多个节点上,提存储容量和吞吐量。

      • 并行处理:每个节点独立处理自己负责的数据片段,提了系统的并发处理能力。

      • 水平扩展:通过增加节点实现集群的扩展,支持更大规模的数据存储和处理

    • 缺点:

      • 节点故障影响:当某个节点发生故障时,该节点负责的数据将无法访问,可能导致数据的丢失或不可用。

      • 数据一致性:分片集群中的数据分布不一定均匀,可能导致部分节点负载较高,需要考虑数据的均衡性和一致性问题。

      • 跨节点事务:分片集群中的事务操作跨越多个节点,需要考虑数据的一致性和并发控制的复杂性。

  • 总结:

    • Redis 分布式缓存具有高性能高可用性和丰富的功能,适用于大多数场景。然而,需要根据具体业务需求和数据特性来权衡其优缺点,并进行合理的配置和管理
    • 分片集群适合对数据量、读写操作较为分散的场景,提供横向扩展和高吞吐量的能力。但需要注意数据的均衡性、节点故障和跨节点事务等问题

2.传统缓存策略:

image-20230705094345937

​ 说明:

​ 传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库,当数据量达到亿级别时则会存在问题

  • 请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈。
  • Redis缓存失效时,会对数据库产生冲击。

说明:

​ 那么,又该如何解决缓存失效和Tomcat的瓶颈呢,详细请查看下节

2.多级缓存

笔记小结:

  • 请查看各个小节
  • 总结:详细请查看

2.1概述

笔记小结:

  • 概述:Redis 的多级缓存,它由级的缓存组成,以提高系统的性能和扩展性。
  • 工作流程:当数据访问时,会依次查询一级缓存,二级缓存,三级缓存……最后查询Tomcat

​ Redis 的多级缓存是一种常见的缓存架构,它由多个层级的缓存组成,以提高系统的性能和扩展性。每个缓存层级都有不同的特点和用途

image-20230705094215712

说明:

​ 用作缓存的Nginx是业务Nginx,需要部署为集群,再有专门的Nginx用来做反向代理

​ 通过使用多级缓存,可以大大提高系统的性能和扩展性,减少对后端数据存储系统的访问次数,降低系统负载,并提供更好的用户体验。同时,多级缓存还可以根据数据的访问模式和重要性进行灵活的配置和管理,以满足不同的业务需求。

原理流程:

  1. 当应用程序需要获取数据时,首先查询一级缓存(L1 缓存),如果数据存在于一级缓存中,则直接返回数据,无需访问后端数据存储系统。
  2. 如果一级缓存中不存在所需数据,则查询二级缓存(L2 缓存),如果数据存在于二级缓存中,则将数据返回给应用程序,并更新一级缓存。
  3. 如果二级缓存中也不存在所需数据,则查询三级缓存(L3 缓存),如果数据存在于三级缓存中,则将数据返回给应用程序,并更新一级和二级缓存。
  4. 如果数据在所有缓存层级中都不存在,则应用程序从后端数据存储系统中获取数据,并将数据存储到各级缓存中,以供后续访问使用。

2.2JVM进程缓存

笔记小结:

  • 概述:Caffeine是最佳命中率的高性能的本地缓存库
  • 基本用法:创建Builder对象、get、set
  • 缓存驱逐策略:缓存可根据时间和容量maximumSize和设置缓存过期时间expireAfterWrite来设置缓存的更新频率

2.2.1概述

​ Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址:https://github.com/ben-manes/caffeine

image-20230705124324479

2.2.2案例-基本用法

  • 创建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);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

2.2.3缓存驱逐策略

​ Caffeine 是一个基于 Java 的高性能缓存库,它提供了多种缓存驱逐策略以控制缓存的大小和内存占用。

​ 值得注意的是,缓存驱逐时需要一定的时间,例如10秒20秒。以下是 Caffeine 支持的一些常见缓存驱逐策略

  • 基于容量
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(1) // 设置缓存大小上限为 1 
        .build();
  • 1
  • 2
  • 3
  • 4

说明:

​ 设置缓存的数量上限

  • 基于时间
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
        .expireAfterWrite(Duration.ofSeconds(10)) // 设置缓存有效期为 10 秒,从最后一次写入开始计时 
        .build();
  • 1
  • 2
  • 3
  • 4

说明:

​ 设置缓存的有效时间

  • 基于引用

说明:

​ 设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

补充:

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

2.2.4基本用例

步骤一:导入依赖

  • 修改pom.xml文件
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4

步骤二:创建配置文件

  • 创建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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

步骤三:实现查询

  • 修改控制层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));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

2.3Lua语法入门

笔记小结:

  • 概述:Lua 是一种轻量小巧的脚本语言,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展定制功能。
  • 语法同python类似,详细查看各小节

2.3.1概述

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

image-20230705150439564

2.3.2基本用例

步骤一:创建Lua脚本

touch hello.lua
  • 1

步骤二:添加一下内容

print("Hello World!")  
  • 1

步骤三:运行

lua hello.lua
  • 1

image-20230705155029778

2.3.3数据类型

数据类型描述
nil这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。
boolean包含两个值:false和true
number表示双精度类型的实浮点数
string字符串由一对双引号或单引号来表示
function由 C 或 Lua 编写的函数
tableLua 中的表(table)其实是一个"关联数组"(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过"构造表达式"来完成,最简单构造表达式是{},用来创建一个空表。

说明:

  • 查看变量数据类型
print(type("hello,world"))
  • 1

2.3.4变量

-- 声明字符串
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}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

说明:

  • 访问变量
-- 访问数组,lua数组的角标从1开始
print(arr[1])
-- 访问table
print(map['name'])
print(map.name)
  • 1
  • 2
  • 3
  • 4
  • 5

2.3.5循环

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

2.3.6函数

  • 定义函数
function 函数名( argument1, argument2..., argumentn)
    -- 函数体
    return 返回值
end

-- 例如
function printArr(arr)
    for index, value in ipairs(arr) do
        print(value)
    end
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

2.3.7条件控制

  • 条件控制
if(布尔表达式)
then
   --[ 布尔表达式为 true 时执行该语句块 --]
else
   --[ 布尔表达式为 false 时执行该语句块 --]
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

说明:

image-20230705160213755

2.4OpenResty快速入门

笔记小结:

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

2.4.1概述

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

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

官方网站: https://openresty.org/cn/

image-20230705162409807

2.4.2安装

说明:

​ 本教程安装OpenResty是通过Docker方式进行

步骤一:创建目录

cd /home
mkdir openresty
cd /home/openresty
mkdir conf
mkdir lua
  • 1
  • 2
  • 3
  • 4
  • 5

步骤二:安装OpenResty

docker run -id --name openresty -p 8080:8080 sevenyuan/openresty
  • 1

步骤三:挂载配置

1.拷贝OpenResty配置

docker cp openresty:/usr/local/openresty/nginx/conf/nginx.conf /home/openresty/conf
docker cp openresty:/usr/local/openresty/lualib /home/openresty
  • 1
  • 2

说明:查看结果

image-20230706103107320

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
  • 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

步骤四:重新安装

1.删除OpenResty

docker rm -f openresty
  • 1

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

说明:

​ 不要添加--restart always 属性,否则启动失败

步骤五:访问OpenResty控制界面

image-20230705171605738

说明:

​ 能够在浏览器端进行访问OpenResty默认界面,说明安装成功

2.5查询本地缓存

笔记小结:

  • 概述:通过Nginx集群实现本地缓存方案
  • Nginx反向代理请求,upstream使用方式
  • Nginx的请求参数动态处理

2.5.1概述

yeVDlwtfMx

说明:

​ 当客户端浏览器发送请求时,NGINX反向代理则会将请求转发到NGINX本地缓存中

2.5.2基本用例

步骤一:修改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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

说明:

image-20230705202752941

2.重新启动Nginx反向代理

nginx.exe -s stop
start nginx
  • 1
  • 2

步骤二:修改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;
        }
    }
}
  • 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

说明:

​ 修改配置文件后,OpenResty会自动刷新,因此无需重启

补充:

image-20230705203800479

步骤三:添加脚本执行文件

1.编写item.lua文件

vim /home/openresty/lua/item.lua
  • 1

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}')
  • 1

步骤四:重启OpenResty

docker restart openresty
  • 1

步骤五:查看结果

1.查看浏览器的响应数据

image-20230706103630077

说明:

​ 说明数据响应成功

2.查看浏览器前端页面

image-20230706103722817

说明:

​ 可以看到,价格已发生改变,Nginx代理实验成功

2.5.3请求参数处理

​ 在OpenResty中如何获取请求地址中的参数,其实OpenResty提供了各种API用来获取不同类型的请求参数:

image-20230706143250833

2.5.4基本示例-改进

说明:

​ 获取路径占位符中的参数

步骤一:编辑Openresty配置文件

  • 修改OpenResty的配置nginx.conf文件
location ~ /api/item/(\d+) {
    # 默认的响应类型
    default_type application/json;
    # 响应结果有lua/item.lua文件来决定
    content_by_lua_file lua/item.lua;
}        
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

说明:

image-20230706143905031

步骤二:编写对应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}')
  • 1
  • 2

说明:

..表示拼接字符串

步骤三:演示

image-20230706163906222

image-20230706163922882

说明:

​ 当请求ID值变动时,返回数据依旧会随着请求改变

2.6查询Tomcat

笔记小结:

  • 概述:封装Lua脚本HTTP请求,实现Tomcat集群查询
  • 使用CJSON实现对象序列化与反序列化

2.6.1概述

在这里插入图片描述

说明:

​ 当OpenResty发送请求时,首次查询不会直接查询Redis集群,而是向Tomcat进行查询获取

2.6.2发送HTTP请求

在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方式传参数
})
  • 1
  • 2
  • 3
  • 4
  • 5

说明:

​ 使用nginx的 ngx.location.captureAPI发送

返回的响应内容包括:

  • resp.status:响应状态码
  • resp.header:响应头,是一个table
  • resp.body:响应体,就是响应数据

注意:

  • 这里的path是路径,并不包含IP和端口。这个请求会被nginx内部的server监听并处理。
 location /path {
     # 这里是windows电脑的ip和Java服务端口,需要确保windows防火墙处于关闭状态
     proxy_pass http://192.168.150.1:8081; 
 }
  • 1
  • 2
  • 3
  • 4
  • 但是我们希望这个请求发送到Tomcat服务器,因此还需要编写一个server来对这个路径做反向代理

2.6.3封装HTTP请求工具

步骤一:创建common.lua文件

在/home/openresty/lualib目录下创建common.lua文件,便于OpenResty的nginx.conf模块的导入
  • 1

步骤二:编写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 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

2.将方法导出

-- 将方法导出
local _M = {  
    read_http = read_http
}  
return _M
  • 1
  • 2
  • 3
  • 4
  • 5

2.6.4CJSON工具类

​ OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。官方地址: https://github.com/openresty/lua-cjson/

使用方式:

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

2.6.5基本用例

前提:

​ 需要封装在Common.lua中的函数

步骤一:添加OpenRestynginx.conf代理地址

http {
    ……  

    server {
        listen       8080;
        server_name  localhost;
        # 这里是配置Tomcat服务的电脑的ip和Java服务端口,需要确保其防火墙处于关闭状态
        location /item{
                proxy_pass http://10.13.122.51:8081;
        }
    ……
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

步骤二:修改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
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

步骤三:演示

1.查看后台日志

image-20230706161254797

说明:

​ 后台查询成功

2.查看浏览器返回数据

image-20230706161404707

说明:

​ 前端数据返回成功

2.7Tomcat集群负载均衡

笔记小结:

  • 概述:修改Nginx的配置,实现upstream负载均衡配置

2.7.1概述

image-20210821111023255

说明:

​ 在实际开发中,Tomcat的环境部署是不一定是单机,会是Tomcat集群来部署,因此这里实现Tomcat多态部署测试

2.7.2基本用例

步骤一:配置OpenResty本地缓存

1.修改OpenRestynginx.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;
    }
    ……
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

注意:

​ 在编写该配置文件时,文件格式需要统一,建议手敲,不要复制,否则会出奇怪的报错!!

说明:

  • 此处运用Nginx的hash $request_uri;的负载均衡算法,避免不同进程的Tomcat数据冗余

image-20230706170434176

2.重启OpenreSty

docker restart openresty
  • 1

说明:

​ 刷新openrestynginx.conf的配置

步骤二:启动Tomcat集群

  • Idea运行多个Tomcat实例

image-20230706170306693

步骤三:演示

  • 查看Idea日志

image-20230706173728061

image-20230706173739679

说明:

​ 查看浏览器,访问成功

2.8Redis预热

笔记小结:

  • 概述:在项目启动时,实现Redis中的数据提前加载
  • 基本用例:搭建Handler处理类,实现InitializingBean接口,重写afterPropertiesSet方法,在此方法中实现缓存预热

2.8.1概述

image-20230706194406268

说明:

​ 服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。因此这里采用缓存预热的方式进行启动

缓存预热
在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。

2.8.2基本用例

前提:

​ 需要存在有密码的Redis服务,详细请查看搭建Redis日志

步骤一:导入依赖

  • 导入Springboot整合Redis依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4

步骤二:添加配置文件

1.修改application.yml配置文件

spring:
  redis:
    host: 10.13.167.28
    port: 6379
    password: qweasdzxc
  • 1
  • 2
  • 3
  • 4
  • 5

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);
        }
    }
}
  • 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

步骤三:演示

image-20230706200551200

说明:

​ 可以看到此项目在启动时已经查阅过数据库

image-20230707103133576

说明:

​ 通过Redis控制软件可以看到,数据已存在Redis中

2.9查询Redis缓存

笔记小结:

  • 概述:封装Lua脚本Redis查询函数,实现Redis的查询数据查询

2.9.1概述

说明:

​ Tomcat中已将数据预热的方式加载近Redis。修改项目逻辑,实现OpenResty优先查询Redis再查询Tomcat

2.9.2封装Reids查询工具

步骤一:创建/改写common.lua文件

在/home/openresty/lualib目录下创建/改写common.lua文件,便于OpenResty的nginx.conf模块的导入
  • 1

步骤二:编写common.lua文件

1.导入Redis模块,初始化Redis对象

-- 导入redis
local redis = require('resty.redis')
-- 初始化redis
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
  • 1
  • 2
  • 3
  • 4
  • 5

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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
  • 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

4.导出该方法

-- 将方法导出
local _M = {  
    read_http = read_http, -- 此方法为封装HTTP请求的工具导出
    read_redis = read_redis
}  
return _M
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

注意:

​ 此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
  • 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

2.9.3基本用例

步骤一:修改item.lua文件,实现真实业务逻辑

1.导入common函数库

-- 导入common函数库
local common = require('common')
local read_redis = common.read_redis
  • 1
  • 2
  • 3

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

补充:查看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))
  • 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

步骤二:重启OpenResty

docker restart openresty
  • 1

说明:

​ 重启服务,刷新Nginx.conf的配置

步骤三:演示

1.查看Idea

image-20230707104843265

说明:

​ 由于之前已完成Redis的预热,现在停止Tomcat的服务

2.查看浏览器

image-20230707104937799

说明:

​ 虽然停止了Tomcat服务,但数据存在Redis中。Openresty集群首先会查询Redis,因此数据依旧显示正常

2.10Nginx本地缓存

笔记小结:

概述:封装Lua脚本Nginx查询函数,实现Nginx本地缓存的数据查询

2.10.1概述

image-20210821114742950

说明:

​ 当客户端访问时,优先查询OpenResty本地缓存,再进行Redis的查询,若Redis未查询成功,则查询Tomcat。实现多级缓存最后一关

2.10.2本地缓存API

OpenResty为Nginx提供了shard dict的功能,可以在nginx的多个worker之间共享数据,实现缓存功能。

基本用例:

  • 开启共享字典
 # 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
 lua_shared_dict item_cache 150m; 
  • 1
  • 2

说明:

​ 修改Openrestynginx.conf配置

  • 操作共享字典
-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache
-- 存储, 指定key、value、过期时间,单位s,默认为0代表永不过期
item_cache:set('key', 'value', 1000)
-- 读取
local val = item_cache:get('key')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2.10.2基本用例

前提:

​ 需要开启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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

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)
  • 1
  • 2
  • 3
  • 4

补充:完整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
  • 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

步骤二:演示

1.刷新Nginx本地缓存集群数据

image-20230707131043460

image-20230707131106151

说明:

​ 当用户首次访问Nginx缓存失败时,会查询Redis

2.停止Redis服务

docker stop myredis
  • 1

3.查询浏览器数据

image-20230707131706947

说明:

​ 当停止掉Redis服务后,数据响应成功,说明Nginx缓存已启动

2.11缓存同步

笔记小结:

  • 概述:将多节点之间共享的缓存数据进行同步更新
  • Canal:是用于数据库增量日志解析,提供增量数据订阅&消费。监听数据库的增量日志实现数据请求的同步与相应处理

2.11.1概述

​ 缓存同步是指在分布式系统中,多个节点之间共享缓存数据并保持一致性的过程。当缓存中的数据发生变化时,需要将这些变化同步到其他节点的缓存中,以确保所有节点获取的数据都是最新的

缓存数据同步的方式:

  • 设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新

    • 优势:简单、方便
    • 缺点:时效性差,缓存过期之前可能不一致
    • 场景:更新频率较低,时效性要求低的业务
  • 同步双写:在修改数据库的同时,直接修改缓存

    • 优势:时效性强,缓存与数据库强一致
    • 缺点:有代码侵入,耦合度高;
    • 场景:对一致性、时效性要求较高的缓存数据
  • **异步通知:**修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据

    • 优势:低耦合,可以同时通知多个缓存服务
    • 缺点:时效性一般,可能存在中间不一致状态
    • 场景:时效性要求一般,有多个服务需要同步

异步通知实现缓存同步

1.基于MQ的异步通知

image-20210821115552327

说明:

​ 商品服务完成对数据的修改后,只需要发送一条消息到MQ中。缓存服务监听MQ消息,然后完成对缓存的更新

2.基于Canal的通知

image-20210821115719363

说明:

​ 商品服务完成商品修改后,业务直接结束,没有任何代码侵入,Canal监听MySQL变化,当发现变化后,立即通知缓存服务,缓存服务接收到canal通知,更新缓存

2.11.2Canal介绍

概述

​ Canal [kə’næl],译意为水道/管道/沟渠,canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。GitHub的地址:https://github.com/alibaba/canal

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

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

image-20230707144732830

说明:

​ 基于MySQL生成的二进制日志来实现主从同步

​ Canal就是把自己伪装成MySQL的一个slave节点,从而监听master的binary log变化。再把得到的变化信息通知给Canal的客户端,进而完成对其它数据库的同步。

image-20230707144849745

说明:

​ 当使用Canal时,还可以完成对其他数据库的同步

2.11.3安装和配置Canal

步骤一:配置MySQL主从

1.修改my.cnf开启Binlog

vim /home/mysql/conf/my.cnf
  • 1

2.添加如下内容

log-bin=/home/mysql/mysql-bin # 设置binary log文件的存放地址和文件名,叫做mysql-bin
binlog-do-db=heima # 指定对哪个database记录binary log events,这里记录heima这个库
  • 1
  • 2

补充: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这个库
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.设置用户权限

3.1添加cannal用户,并设置权限

create user canal@'%' IDENTIFIED by 'canal'; # 创建canal新用户,并指定密码canal
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT, SUPER ON *.* TO canal@'%';# 授予权限
FLUSH PRIVILEGES; # 刷新权限
  • 1
  • 2
  • 3

3.2重启Mysql容器

docker restart mysql
  • 1

3.3查看主数据库二进制日志

show master status;
  • 1

说明:查看结果

image-20230707154013132

  • Position为同步数据的偏移量,类似与Redis中的偏移量为了实现主从同步

步骤二:配置网络

1.创建网络

docker network create heima
  • 1

说明:

​ 创建网络,将MySQL、Canal放到同一个Docker网络中

2.MySQL加入网络

docker network connect heima mysql
  • 1

步骤三:安装Canal

1.拉取镜像

docker pull canal/canal-server:v1.1.5
  • 1

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

注意:

​ 记得修改对应的账户和连接密码

说明:

​ 运行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正则表达式.多个正则之间以逗号(,)分隔,转义符需要双斜杠(\) ,例如:
    1. 所有表:.* or .\…
    2. canal schema下所有表: canal\…
    3. canal下的以canal打头的表:canal\.canal.
    4. canal schema下的一张表:canal.test1
    5. 多个规则组合使用然后以逗号隔开:canal\…*,mysql.test1,mysql.test2

步骤四:演示

1.查看Canal状态

docker logs canal
  • 1

说明:

image-20230707155904690

  • 说明Canal启动成功

2.查看heima数据库记录日志

docker exec -it canal bash # 进入容器内部
tail -f /home/admin/canal-server/logs/heima/heima.log 
  • 1
  • 2

说明:查看结果

image-20230707162659693

补充:

image-20230707162340037

  • 若日志输出报错
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
  • 1
  • 当前MySQL版本与Canal版本不匹配,请更换二者版本

2.11.4基本用例

image-20230707165606219

说明:

​ 配置Canal实现MySQL变化后自动更新Redis缓存和JVM缓存

前提:

​ 已完成安装和配置Canal

步骤一:导入依赖

  • 修改pom.xml文件
<!--canal-->
<dependency>
    <groupId>top.javatool</groupId>
    <artifactId>canal-spring-boot-starter</artifactId> 
    <version>1.2.1-RELEASE</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

步骤二:编写配置

  • 修改application.yml
canal:
  destination: heima # canal实例名称,要跟canal-server运行时设置的destination一致
  server: 10.13.164.55:11111 # canal地址 
  • 1
  • 2
  • 3

步骤三:编写实体类

  • 修改实体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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

说明:

​ Canal推送给canal-client的是被修改的这一行数据(row),而我们引入的canal-client则会帮我们把行数据封装到Item实体类中。这个过程中需要知道数据库与实体的映射关系,要用到JPA的几个注解

步骤四:编写监听器

  • 新增ItemHandler类并实现EntryHandler<Item>接口,并重写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
  • 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

步骤五:演示

1.查看IDEA日志

image-20230707172842908

说明:

​ 当前项目已实现Canal的MySQL的消息监听

2.修改数据库信息后,查看浏览器数据已改动

image-20230707173214876

说明:

​ 当看到数据变动时,说明JVM缓存已刷新,Redis数据已刷新,不信自己查看

2.12总结

image-20230707173858153

说明:

​ 现在,本地项目已完成搭建单结点OpenResty,若需要搭建多节点,记得再Nginx反向代理时,配置本地缓存的集群,并制作负载均衡

基于Redis6的学习,现提出如下疑问:

  • 如果,Openresty缓存失效该如何解决?对于敏感的数据实现主动更新
  • 单结点Redis宕机该如何解决?需要使用Lua脚本访问Redis集群
  • 还有没有别的多级缓存的替代方案?暂时没有发现

3.最佳实践

3.1Redis键值设计

笔记小结:查看各个小结

3.1.1优雅的Key结构

笔记小结:

  • Key的最佳实践:
    • 固定格式:[业务名]:[数据名]:[id]
    • 足够简短:不超过44字节
    • 不包含特殊字符
  • Value的最佳实践:
    • 合理的拆分数据,拒绝BigKey
    • 选择合适数据结构
    • Hash结构的entry数量不要超过1000
    • 设置合理的超时时间
  • 恰当的数据类型,例如Hash结构等等

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

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

说明:例如

image-20230707201822154

优点:

  • 可读性强
  • 避免key冲突
  • 方便管理
  • 更节省内存: key是string类型,底层编码包含int、embstr和raw三种。embstr在小于44字节使用,采用连续内存空间,内存占用更小

补充:

image-20230707200638460

  • 当Key值超过44个字节后,会自动的用raw格式,而采用非连续性空间,因此占用更多的内存

补充:

​ 存储一个数据时,在内存中占用字节往往比数据值本身更多,因为Redis底层会存储元信息

3.1.2拒绝BigKey

含义:

​ 在Redis中,BigKey(大键)是指占用存储空间较大的键值对。当一个键值对的大小超过了Redis的配置阈值(默认为10KB),就被认为是一个BigKey。

  • Key本身的数据量过大:一个String类型的Key,它的值为5 MB。
  • Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个。
  • Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100 MB。

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

    说明:

    image-20230707225427173

3.1.3恰当的数据类型

image-20230708071312670

说明:

​ 选择合适数据结构进行存储,底层占用的空间更小

image-20230708071406138

说明:

​ 现在最严重的问题就是entry过多,造成了BigKey问题。那该如何解决呢?

image-20230708071649253

说明:

​ String类型暴力简单,但内存没有太多的优化

image-20230708071731035

说明:

​ 当把Key值进行拆分,让每一个Hash的entry都为100,这样entry不超过500,所以数据会采用Hash表的存储方式,从而减少了内存存储

3.2批处理优化

笔记小结:

  • 概述:当时数据量传输过大时,可以使用批处理方案,减少网络传输的耗时,提供业务的执行时间
  • 批量处理的方案:
    • 原生的M操作
    • Pipeline批处理
  • 注意:Pipeline的多个命令之间不具备原子性。批处理时不建议一次携带太多命令

3.2.1概述

image-20230708090729318

说明:

​ 当有N次命令响应时,一条一条的命令传输中间会因为网络延迟的时间而增大响应时间。因为Redis命令执行时间的并发量并不高是五万分之一。因此,命令的响应时间会因为网络传输耗时而大大增加

image-20230708090807456

说明:

​ 当有N次命令响应时,一次执行多个命令,则会减少网络延迟的时间。此时Redis命令执行时的并发量也不高。因此,命令的响应时间会大大减少

3.2.2MSET

Redis提供了很多Mxxx这样的命令,可以实现批量插入数据,例如:

  • mset
  • hmset

代码示例:利用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);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

说明:

​ 当使用位移运算符移动一位时,任何数字都会被乘以2,此时刚好将2000个容量大小的数组以Key:value的形式填满

注意:

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

3.2.1Pipeline

​ 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();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

说明:

​ 再管道中,可以添加任一命令。将管道中的命令批量的发送到Redis中,依次执行。执行命令的时间较MSET命令多,因为管道内的命令到达Redis中时,会形成队列依次执行。若此时Redis中有多个命令执行,则会造成管道内的命令延时执行

3.2.2集群下的批处理优化

image-20230708093116538

说明:

​ 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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

说明:

​ 当使用批处理命令时,Spring框架提供的一套对Redis的批处理操作会对是否对集群进行判断,来处理

3.3服务端优化

笔记小结:

  • 持久化配置:预留足够的内存空间,不与CPU高密集应用一起部署
  • 慢查询:配置慢查询阈值以及慢查询容量上限,慢查询日志列表以及日志长度查询等运维操作
  • 命令以及安全配置:禁用keys *命令,设置Redis密码,开启防火墙

3.3.1持久化配置

Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:

  • 用来做缓存的Redis实例尽量不要开启持久化功能
  • 建议关闭RDB持久化功能,使用AOF持久化
  • 利用脚本定期在slave节点做RDB,实现数据备份
  • 设置合理的rewrite阈值,避免频繁的bgrewrite
  • 配置no-appendfsync-on-rewrite = yes,禁止在rewrite期间做aof,避免因AOF引起的阻塞
image-20230708152409743

部署有关建议:

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

3.3.2慢查询

慢查询:慢查询日志是Redis服务端在命令执行前后计算每条命令的执行时长,当超过某个阈值是记录下来的日志。

image-20230708153852914

说明:

​ 慢查询,会因为Redis执行的命令较多而等待,等待而超过阈值

查看慢查询日志列表:

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

补充:

image-20230708154547858

设置慢查询的阈值:

  • slowlog-log-slower-than:慢查询阈值,单位是微秒。默认是10000,建议1000

说明:

​ 一般执行一条命令都是在十几微妙左右

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

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

说明:

​ 可以调整慢查询的日志,方便查询检索

补充:修改这两个配置可以使用:config set命令

image-20230708154024660

image-20230708154009768

3.3.3命令及安全配置

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

​ 漏洞重现方式:https://cloud.tencent.com/developer/article/1039000

漏洞出现的核心的原因有以下几点:

  • Redis未设置密码
  • 利用了Redis的config set命令动态修改Redis配置
  • 使用了Root账号权限启动Redis

为了避免这样的漏洞,这里给出一些建议:

  • Redis一定要设置密码

  • 禁止线上使用下面命令:keys、flushall、flushdb、config set等命令。可以利用rename-command禁用。

    说明:

    image-20230708155305281

  • bind:限制网卡,禁止外网网卡访问

    说明:

    image-20230708155348490

  • 开启防火墙

  • 不要使用Root账户启动Redis

  • 尽量不是有默认的端口

3.4内存配置

笔记小结:

  • 概述:适当配置内存中的复制缓存区域,AOF缓存区域,客户端缓存区的内存容量大小,可提高性能

3.4.1概述

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

内存占用说明
数据内存是Redis最主要的部分,存储Redis的键值信息。主要问题是BigKey问题、内存碎片问题
进程内存Redis主进程本身运⾏肯定需要占⽤内存,如代码、常量池等等;这部分内存⼤约⼏兆,在⼤多数⽣产环境中与Redis数据占⽤的内存相⽐可以忽略。
缓冲区内存一般包括客户端缓冲区、AOF缓冲区复制缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,不当使用BigKey,可能导致内存溢出。

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

  • info memory
image-20230708161032665
  • memory xxx
image-20230708161045843

3.4.2内存缓冲区配置

内存缓冲区常见的有三种:

  • 复制缓冲区:主从复制的repl_backlog_buf,如果太小可能导致频繁的全量复制,影响性能。通过repl-backlog-size来设置,默认1mb
  • AOF缓冲区:AOF刷盘之前的缓存区域,AOF执行rewrite的缓冲区。无法设置容量上限
  • 客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大1G且不能设置。输出缓冲区可以设置

image-20230708161207023

默认的配置如下:

image-20230708161156547

3.5集群最佳实践

笔记小结:

  • 概述:搭建集群需要考虑带宽,数据倾斜,数据完整性,客户端性能等诸多问题
  • 注意:分片式集群以及主从集群中存在默认的集群数据完整性的配置问题,需要将 cluster-require-full-coverage配置为false即可根据需求提高Redis集群性能
  • 搭建的节点数不易太多,避免节点之间的业务时间超时

3.5.1概述

集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:

  • 集群完整性问题
  • 集群带宽问题
  • 数据倾斜问题
  • 客户端性能问题
  • 命令的集群兼容性问题
  • lua和事务问题

3.5.2集群完整性问题

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

image-20230708162151708

补充:

​ 为了保证高可用特性,这里建议将 cluster-require-full-coverage配置为false

3.5.3集群带宽问题

集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:

  • 插槽信息
  • 集群状态信息

集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高。

解决途径:

  • 避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群。
  • 避免在单个物理机中运行太多Redis实例
  • 根据节点数,带宽等配置合适的cluster-node-timeout值,保证节点失效检测。

3.5.4集群还是主从

集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:

  • 集群完整性问题
  • 集群带宽问题
  • 数据倾斜问题
  • 客户端性能问题
  • 命令的集群兼容性问题
  • lua和事务问题

注意:

​ 单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用特性。如果主从能满足业务需求的情况下,尽量不搭建Redis集群。

日志

搭建Redis哨兵时出现一直sdown 问题

  • sdown表示,哨兵主观意识上认为此节点已宕机

image-20230704164413851

  1. 检查哨兵的配置文件Master 节点的Ip和端口是否配置正确
  2. 检查哨兵的配置文件是否配置集群的连接密码sentinel auth-pass mymaster qweasdzxc
  3. 检查系统的防火墙是否开放Redis集群中各个节点的端口

搭建MySQL

步骤一:准备基础环境

1.创建目录

cd home
mkdir mysql
cd mysql
mkdir logs
mkdir data
mkdir conf
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

步骤二:运行容器

  • 使用Docker执行如下命令
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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

步骤三:添加配置

1.进入conf目录创建my.cnf文件

[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/home/mysql
server-id=1000
  • 1
  • 2
  • 3
  • 4
  • 5

2.重启容器

docker restart mysql
  • 1

步骤四:初始化项目表

/*
 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;

  • 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

搭建Nginx

#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;
        }
    }
}
  • 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

搭建Redis

步骤一:添加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
  • 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

步骤二:运行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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

说明:

​ 若此Redis容器需要进行远程连接,那么需要配置密码,也就是添加--requirepass 参数

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/682857
推荐阅读
相关标签
  

闽ICP备14008679号