JavaWeb之SpringCloud原理与实战(四)

📅 2026/7/6 13:42:34
JavaWeb之SpringCloud原理与实战(四)
目录一、分布式缓存(redis集群)二、多级缓存一、分布式缓存(redis集群)单点redis的问题数据丢失问题:redis是内存存储,服务重启可能会丢失数据。解决方案: 实现Redis数据持久化并发能力问题单节点Redis并发能力虽然不错,但也无法满足如618这样的高并发场景解决方案: 搭建主从集群,实现读写分离故障恢复问题如果Redis宕机,则服务不可用,需要一种自动的故障恢复手段解决方案: 利用Redis哨兵,实现健康检测和自动恢复存储能力问题Redis基于内存,单节点能存储的数据量难以满足海量数据需求解决方案: 搭建分片集群,利用插槽机制实现动态扩容RDBRDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。快照文件称为RDB文件,默认是保存在当前运行目录。Redis停机时会执行一次RDB。Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:RDB的其它配置也可以在redis.conf文件中设置:bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入RDB文件。fork采用的是copy-on-write技术:当主进程执行读操作时,访问共享内存当主进程执行写操作时,则会拷贝一份数据,执行写操作总结:RDB方式bgsave的基本流程?fork主进程得到一个子进程,共享内存空间子进程读取内存数据并写入新的RDB文件用新的RDB文件替换旧的RDB文件RDB会在什么时候执行?save 60 1000代表什么含义?默认是服务停止时代表60秒内至少执行1000次修改则触发RDBRDB的缺点?RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险fork子进程、压缩、写出RDB文件都比较耗时AOFAOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:AOF的命令记录的频率也可以通过redis.conf文件来配置:配置项刷盘时机优点缺点Always同步刷盘可靠性高,几乎不丢数据性能影响大everysec每秒刷盘性能适中最多丢失1秒数据no操作系统控制性能最好可靠性差,可能丢失大量数据因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。Redis也会在触发阈值自动去重写AOF文件。阈值也可以在redis.conf中配置:RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用。RDBAOF持久化方式定时对整个内存做快照记录每一次执行的命令数据完整性不完整,两次备份之间会丢失相对完整,取决于刷盘策略文件大小会有压缩,文件体积小记录命令,文件体积很大宕机恢复速度很快慢数据恢复优先级低,因为数据完整性不如AOF高,因为数据完整性更高系统资源占用高,大量CPU和内存消耗低,主要是磁盘IO资源,但AOF重写时会占用大量CPU和内存资源使用场景可以容忍数分钟过的数据丢失,追求更快的启动速度对数据安全性要求较高搭建主从架构单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。集群结构我们搭建的主从集群结构如图:共包含三个节点: 一个主节点,两个从节点。这里我们会在同一台虚拟机中开启3个redis实例,模拟主从集群,信息如下:IPPORT角色192.168.40.1297001master192.168.40.1297002slave192.168.40.1297003slave准备实例和配置要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。创建目录我们创建三个文件夹,名字分别叫7001、7002、7003:# 进入/tmp目录 cd /tmp # 创建目录 mkdir 7001 7002 7003恢复原始配置修改redis-6.2.14/redis.conf文件,将其中的持久化模式改为默认的RDB模式,AOF保持关闭状态。# 开启RDB # save “” save 3600 1 save 300 100 save 60 10000 # 关闭AOF appendonly no拷贝配置文件到每个实例目录然后将redis-6.2.14/redis.conf文件拷贝到三个目录中(在/tmp目录执行下列命令):# 方式一: 逐个拷贝 cp redis-6.2.14/redis.conf 7001 cp redis-6.2.14/redis.conf 7002 cp redis-6.2.14/redis.conf 7003 # 方式二: 管道组合命令,一键拷贝 Echo 7001 7002 7003 | xargs -t -n 1 cp redis-6.2.14/redis.conf修改每个实例的端口、工作目录修改每个文件夹内的配置文件,将端口分别修改为7001、7002、7003,将RDB文件保存位置都修改为自己所在目录(在/tmp目录执行下列命令):sed -i -e ‘s/6379/7001/g’ -e ‘s/dir .\//dir \/tmp\/7001\//g’ 7001/redis.conf sed -i -e ‘s/6379/7002/g’ -e ‘s/dir .\//dir \/tmp\/7002\//g’ 7002/redis.conf sed -i -e ‘s/6379/7003/g’ -e ‘s/dir .\//dir \/tmp\/7003\//g’ 7003/redis.conf修改每个实例的声明IP虚拟机本身有多个IP,为了避免将来混乱,我们需要再redis.conf文件中指定每一个实例的绑定ip信息,格式如下:# redis实例的声明IP replica-announce-ip 192.168.40.129每个目录都要改,我们一键完成修改(在/tmp目录执行下列命令):# 逐一执行 sed -i ‘la replica-announce-ip 192.168.40.129’ 7001/redis.conf sed -i ‘la replica-announce-ip 192.168.40.129’ 7002/redis.conf sed -i ‘la replica-announce-ip 192.168.40.129’ 7003/redis.conf # 或者一键修改 printf ‘%s\n’ 7001 7002 7003 | xargs -I{} -t sed -i ‘la replica-announce-ip 192.168.40.129’ {}/redis.conf启动为了方便查看日志,我们打开3个ssh窗口,分别启动3个redis实例,启动命令:# 第1个 redis-server 7001/redis.conf # 第2个 redis-server 7002/redis.conf # 第3个 redis-server 7003/redis.conf开启主从关系现在三个实例还没有任何关系,要配置主从可以使用replicaof或者slaveof(5.0以前)命令。有临时和永久两种模式:修改配置文件(永久生效)在redis.conf中添加一行配置: slaveof masterip masterport使用redis-cli客户端连接到Redis服务,执行slaveof命令(重启后失效):slaveof masterip masterport注意: 在5.0以后新增命令replicaof,与slaveof效果一致。通过redis-cli命令连接7002,执行下面命令:# 连接7002 redis-cli -p 7002 # 执行slaveof slaveof 192.168.40.129 7001 # 连接7003 redis-cli -p 7003 # 执行replicaof replicaof 192.168.40.129 7001总结:假设有A、B两个redis实例,如何让B作为A的slave节点?在B节点执行命令: slaveof A的IP A的port数据同步原理主从第一次同步是全量同步:master如何判断slave是不是第一次来同步数据?这里会用到两个很重要的概念:Replication Id: 简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replidoffset: 偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。因此slave做数据同步,必须向master声明自己的replication id和offset,master才可以判断到底需要同步哪些数据。总结:简述全量同步的流程?slave节点请求增量同步master节点判断replid,发现不一致,拒接增量同步master将完整内存数据生成RDB,发送RDB到slaveslave清空本地数据,加载master的RDBmaster将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slaveslave执行接收到的命令,保持与master之间的同步主从第一次同步是全量同步,但如果slave重启后同步,则执行增量同步。注意: repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步。可以从以下几个方面来优化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提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave什么时候执行全量同步?slave节点第一次连接master节点时slave节点断开时间太久,repl_baklog中的offset已经被覆盖时什么时候执行增量同步?slave节点断开又恢复,并且在repl_baklog中能找到offset时哨兵的作用Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下:监控: Sentinel会不断检查你的master和slave是否按预期工作自动故障恢复: 如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主通知: Sentinel充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给redis的客户端服务状态监控Sentinel基于心跳机制监测服务状态,每个1秒向集群的每个实例发送ping命令:主观下线: 如果某Sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。客观下线: 若超过指定数量(quorum)的Sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。选举新的master一旦发现master故障,Sentinel需要在slave中选择一个作为新的master,选择依据是这样的:首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds*10)则会排除该slave节点然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举如果slave-priority 一样,则判断slave节点的offset值,越大说明数据越新,优先级越高最后是判断slave节点的运行id大小,越小优先级越高如何实现故障转移当选中了其中一个slave为新的master后(例如:slave1),故障转移的步骤如下:Sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master。Sentinel给所有其它slave发送slaveof 192.168.40.129 7002命令,让这些slave成为新master的从节点,开始从新的master上同步数据。最后,Sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点。总结:Sentinel的三个作用是什么?监控故障转移通知Sentinel如何判断一个redis实例是否健康?每个1秒发送一次ping命令,如果超过一定时间没有响应则认为是主观下线如果大多数Sentinel都认为实例主观下线,则判定服务下线故障转移步骤有哪些?首选选定一个slave作为新的master,执行slaveof no one然后让所有节点都执行slaveof 新的master修改故障节点配置,添加slaveof 新的master搭建Sentinel集群集群结构这里我们搭建一个三个节点形成的Sentinel集群,来监管之前的redis主从集群。如图:三个Sentinel实例信息如下:节点IPPORTs1192.168.40.12927001s2192.168.40.12927002s3192.168.40.12927003准备实例和配置要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。我们创建三个文件夹,分别是s1、s2、s3:# 进入/tmp目录 cd /tmp # 创建目录 mkdir s1 s2 s3然后我们在s1目录创建一个sentinel.conf文件,添加下面的内容:port 27001 sentinel announce-ip 192.168.40.129 sentinel monitor mymaster 192.168.40.129 7001 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 60000 dir “/tmp/s1”解读:port 27001: 是当前sentinel实例的端口sentinel monitor mymaster 192.168.40.129 7001 2: 指定主节点信息mymaster: 主节点名称,自定义,任意写192.168.40.129 7001: 主节点的ip和端口2: 选举master时的quorum值然后将s1/sentinel.conf文件拷贝到s2、s3两个目录中(在/tmp目录执行下列命令):# 方式一: 逐个拷贝 cp s1/sentinel.conf s2 cp s1/sentinel.conf s3 # 方式二: 管道组合命令,一键拷贝 echo s2 s3 | xargs -t -n 1 cp s1/sentinel.conf修改s2、s3两个文件夹内的配置文件,将端口分别修改为27002、27003:sed -i -e ‘s/27001/27002/g’ -e ‘s/s1/s2/g’ s2/sentinel.conf sed -i -e ‘s/27001/27003/g’ -e ‘s/s1/s3/g’ s3/sentinel.conf启动为了方便查看日志,我们打开3个ssh窗口,分别启动3个redis实例,启动命令:# 第1个 redis-sentinel s1/sentinel.conf # 第2个 redis-sentinel s2/sentinel.conf # 第3个 redis-sentinel s3/sentinel.confRedisTemplate的哨兵模式在sentinel集群监管下的Redis主从集群,其节点会因为自动故障转移而发生变化,Redis的客户端必须感知这种变化,及时更新连接信息。Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换。在pom文件中引入Redis的starter依赖:dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependency然后在配置文件application.yml中指定sentinel相关信息:spring: redis: sentinel: master: mymaster # 指定master名称 nodes: # 指定redis-sentinel集群信息 - 192.168.40.129:27001 - 192.168.40.129:27002 - 192.168.40.129:27003配置主从读写分离这里的ReadFrom是配置Redis的读取策略,是一个枚举,包括下面选择:MASTER: 从主节点读取MASTER_PREFERRED:有先从master节点读取,master不可用才读取replicaREPLICA:从slave(replica)节点读取REPLICA_PREFERRED: 优先从slave(replica)节点读取,所有的slave都不可用才读取master分片集群结构主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:海量数据存储问题高并发写的问题使用分片集群可以解决上述问题,分片集群特征:集群中有多个master,每个master保存不同数据每个master都可以有多个slave节点master之间通过ping检测彼此健康状态客户端请求可以访问集群任意节点,最终都会被转发到正确节点搭建分片集群集群结构分片集群需要的节点数量较多,这里我们搭建一个最小的分片集群,包含3个master节点,每个master包含一个slave节点,结构如下:这里我们会在同一台虚拟机中开启6个Redis实例,模拟分片集群,信息如下:IPPORT角色192.168.40.1297001master192.168.40.1297002master192.168.40.1297003master192.168.40.1298001slave192.168.40.1298002slave192.168.40.1298003slave准备实例和配置删除之前的7001、7002、7003这几个目录,重新创建出7001、7002、7003、8001、8002、8003目录:# 进入/tmp目录 cd /tmp # 删除旧的,避免配置干扰 rm -rf 7001 7002 7003 # 创建目录 mkdir 7001 7002 7003 8001 8002 8003在/tmp下准备一个新的redis.conf文件,内容如下:port 6379 # 开启集群功能 cluster-enabled yes # 集群的配置文件名称,不需要我们创建,由redis自己维护 cluster-config-file /tmp/6379/nodes.conf # 节点心跳失败的超时时间 cluster-node-timeout 5000 # 持久化文件存放目录 dir /tmp/6379 # 绑定地址 bind 0.0.0.0 # 让redis后台运行 daemonize yes # 注册的实例ip replica-announce-ip 192.168.40.129 # 保护模式 protected-mode no # 数据库数量 databases 1 # 日志 logfile /tmp/6379/run.log将这个文件拷贝到每个目录下:# 进入/tmp目录 cd /tmp # 执行拷贝 echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf修改每个目录下的reids.conf,将其中的6379修改为与所在目录一致:# 进入/tmp目录 cd /tmp # 修改配置文件 printf ‘%s\n’ 7001 7002 7003 8001 8002 8003 | xargs -I{} -t sed -i ‘s/6379/{}/g’ {}/redis.conf启动因为已经配置了后台启动模式,所以可以直接启动服务:# 进入/tmp目录 cd /tmp # 一键启动所有服务 printf ‘%s\n’ 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf通过ps查看状态:ps -ef |grep redis如果要关闭所有进程,可以执行命令:ps -ef | grep redis | awk ‘{print $2}’ | xargs kill或者(推荐这种方式):printf ‘%s\n’ 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-cli -p {} shutdown创建集群虽然服务启动了,但是目前每个服务之间都是独立的,没有任何关联。我们需要执行命令来创建集群,在redis5.0之前创建集群比较麻烦,5.0之后集群管理命令都集成到了redis-cli中。我们使用的是redis6.2.14版本,集群管理以及集成到了redis-cli中,格式如下:redis-cli --cluster create --cluster-replicas 1 192.168.40.129:7001 192.168.40.129:7002 192.168.40.129:7003 192.168.40.129:8001 192.168.40.129:8002 192.168.40.129:8003命令说明:redis-cli --cluster或者./redis-trib.rb: 代表集群操作命令create: 代表是创建集群–replicas 1或者–cluster-replicas 1: 指定集群中每个master的副本个数为1,此时节点总数/(replicas1)得到的就是master的数量。因此节点列表中过的前n个就是master,其它节点都是slave节点,随机分配到不同master。通过命令可以查看集群状态:redis-cli -p 7001 cluster nodes散列插槽Redis会把每一个master节点映射到0~16383个插槽(hash slot)上,数据key不是与节点绑定,而是与插槽绑定。Redis会根据key的有效部分计算插槽值,分两种情况:key中包含”{}”,且”{}”中至少包含1个字符,”{}”中的部分是有效部分key中不包含”{}”,整个key都是有效部分例如: key是num,那么就根据num计算,如果是{yajc}num,则根据yajc计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。总结:Redis如何判断某个key应该在哪个实例?将16384个插槽分配到不同的实例根据key的有效部分计算哈希值,对16384取余余数作为插槽,寻找插槽所在实例即可如何将同一类数据固定的保存在同一个redis实例?这一类数据使用相同的有效部分,例如key都以{typeId}为前缀添加一个节点到集群redis-cli --cluster提供了很多操作集群的命令,可以通过下面方式查看:比如:添加节点的命令:故障转移当集群中有一个master宕机会发生什么呢?首先是该实例与其它实例失去连接然后是疑似宕机最后是确定下线,自动提升一个slave为新的master数据迁移利用cluster failover命令可以手动让集群中的某个master宕机,切换到执行cluster failover命令的这个slave节点,实现无感知的数据迁移。其流程如下:手动的Failover支持三种不同模式:缺省: 默认的流程,如图1~6步force: 省略了对offset的一致性校验takeover: 直接执行第5步,忽略数据一致性,忽略master状态和其它master的意见RedisTemplate访问分片集群RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:引入Redis的starter依赖配置分片集群地址配置读写分离与哨兵模式相比,其中只有分片集群的配置方式略有差异,如下:spring: redis: cluster: nodes: - 192.168.40.129:7001 - 192.168.40.129:7002 - 192.168.40.129:7003 - 192.168.40.129:8001 - 192.168.40.129:8002 - 192.168.40.129:8003二、多级缓存传统缓存的问题传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库,存在下面的问题:请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈Redis缓存失效时,会对数据库产生冲击多级缓存方案用作缓存的Nginx是业务Nginx,需要部署为集群,再有专门的Nginx用来做反向代理:本地进程缓存缓存在日常开发中起着至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:分布式缓存,例如:Redis:优点: 存储容量更大、可靠性更好、需要再集群间共享缺点: 访问缓存有网络开销场景: 缓存数据量较大、可靠性要求较高、需要在集群间共享进程本地缓存,例如:HashMap、GuavaCache:优点: 读取本地内存,没有网络开销、速度更快缺点: 存储容量有限、可靠性较低、无法共享场景: 性能要求较高,缓存数据量较小Caffeine是一个基于java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。Caffeine示例可以通过item-service项目中的单元测试来学习Caffeine的使用:Caffeine提供额三种缓存驱逐策略:基于容量: 设置缓存的数据上限基于时间: 设置缓存的有效时间基于引用: 设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。初识LuaLua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。HelloWorld在Linux虚拟机的任意目录下,新建一个hello.lua文件添加下面的内容print(“HelloWorld!”)运行数据类型数据类型描述nil这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)boolean包含两个值:false和truenumber表示双精度类型的实浮点数string字符串由一对双引号或单引号来表示function由C或Lua编写的函数tableLua中的表(table)其实是一个”关联数组”(associative arrays),数组的索引可以是数字、字符串或表类型。在Lua里,table的创建是通过”构造表达式”来完成,最简单构造表达式是{},用来创建一个空表可以利用type函数测试给定变量或者值的类型:变量Lua声明变量的时候,并不需要指定数据类型:访问table:循环数组、table都可以利用for循环来遍历:遍历数组遍历table函数定义函数的语法:例如,定义一个函数,用来打印数据:条件控制类似java的条件控制,例如:if、else语法:与java不同,布尔表达式中的逻辑运算是基于英文单词:操作符描述实例and逻辑与操作符。若A为false,则返回A,否则返回B(A and B)为falseor逻辑或操作符。若A为true,则返回A,否则返回B(A or B)为truenot逻辑非操作符。与逻辑运算结果相反,如果条件为true,逻辑非为falsenot(A and B)为true初识OpenRestyOpenResty是一个基于Nginx的高性能Web平台,用于方便地搭建能够处理超高并发、扩展性极高的动态Web应用、Web服务和动态网关。具备下列特点:具备Nginx的完整功能基于Lua语言进行扩展,集成了大量精良的Lua库、第三方模块允许使用Lua自定义业务逻辑、自定义库安装OpenResty安装首先Linux虚拟机必须联网安装开发库首先要安装OpenResty的依赖开发库,执行命令:yum install -y pcre-devel openssl-devel gcc --skip-broken安装OpenResty仓库你可以在CentOS系统中添加OpenResty仓库,这样就可以便于未来安装或更新我们的软件包(yum check-update命令)。运行下面的命令就可以添加我们的仓库:yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo如果提示说命令不存在,则运行:yum install -y yum-utils安装OpenResty然后就可以像下面这样安装软件包,比如OpenResty:yum install -y openresty安装opm工具opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。如果你想安装命令行工具opm,那么可以像下面这样安装openresty-opm包:yum install -y openresty-opm目录结构默认情况下,OpenResty安装的目录是: /usr/local/openrestyOpenResty就是在Nginx基础上集成了一些Lua模块。配置Nginx的环境变量打开配置文件:vi /etc/profile在最下面加入两行:export NGINX_HOME/usr/local/openresty/nginx export PATH${NGINX_HOME}/sbin:$PATHNGINX_HOME: 后面是OpenResty安装目录下的Nginx的目录然后让配置生效:source /etc/profile启动和运行OpenResty底层是基于Nginx的,查看OpenResty目录的Nginx目录,结构与Windows中安装的Nginx基本一致:所以运行方式与Nginx基本一致:# 启动Nginx nginx # 重新加载配置 nginx -s reload # 停止 nginx -s stopnginx的默认配置文件注释太多,影响后序我们的编辑,这里将nginx.conf中的注释部分删除,保留有效部分。修改/usr/local/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 8081; server_name localhost; Location / { root html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location /50x.html { root html; } } }在linux的控制台输入命令以启动nginx:nginx然后访问页面: http://192.168.40.129:8081,注意ip地址替换为你自己的虚拟机ip。加载OpenResty的Lua模块:# lua模块 lua_package_path “/usr/local/openresty/lualib/?.lua;;”; # c模块 lua_package_cpath “/usr/local/openresty/lualib/?.so;;”;OpenResty获取请求参数OpenResty提供了各种API用来获取不同类型的请求参数:nginx内部发送Http请求nginx提供了内部API用以发送http请求:返回的响应内容包括:resp.status: 响应状态码resp.header: 响应头,是一个tableresp.body: 响应体,就是响应数据封装http查询的函数我们可以把http查询的请求封装为一个函数,放到OpenResty函数库中,方便后期使用。在/usr/local/openresty/lualib目录下创建common.lua文件:vi /usr/local/openresty/lualib/common.lua在common.lua中封装http查询的函数使用http函数查询数据我们刚才已经把http查询的请求封装为一个函数,放到OpenResty函数库中,接下来就可以使用这个库了。修改item.lua文件查询到的是商品,库存的JSON格式数据,我们需要将两部分数据组装,需要用到JSON处理函数库。JSON结果处理OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。引入cjson模块序列化反序列化修改item.lua内容如下:Tomcat集群的负载均衡# 反向代理配置,将/item路径的请求代理到Tomcat集群 location /item { proxy_pass http://tomcat-cluster } # tomcat集群配置 upstream tomcat-cluster { hash $request_uri; server 192.168.40.129:8081; Server 192.168.40.129:8082 }添加Redis缓存的需求冷启动与缓存预热冷启动: 服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会改数据库带来较大压力。缓存预热: 在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。我们数据量较少,可以在启动时将所有数据都放如缓存中。缓存预热利用docker安装Redis在item-service服务中引入Redis依赖dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependency配置Redis地址spring: redis: host: 192.168.40.129配置初始化类OpenResty的Redis模块OpenResty提供了操作Redis的模块,我们只要引入该模块就能直接使用:引入Redis模块,并初始化Redis对象封装函数,用来释放Redis连接,其实是放入连接池封装函数,从Redis读数据并返回Nginx本地缓存OpenResty为nginx提供了shard dict的功能,可以在nginx的多个worker之间共享数据,实现缓存功能。开启共享字典,在nginx.conf的http下添加配置:操作共享字典:修改后的查询逻辑:缓存同步策略缓存数据同步的常见方式有三种:设置有效期: 给缓存设置有效期,到期后自动删除。再次查询时更新。优势: 简单、方便缺点: 时效性差,缓存过期之前可能不一致场景: 更新频率较低,时效性要求低的业务同步双写: 在修改数据库的同时,直接修改缓存优势: 时效性强,缓存与数据库强一致缺点: 有代码侵入,耦合度高场景: 对一致性、时效性要求较高的缓存数据异步通知: 修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据优势: 低耦合,可以同时通知多个缓存服务缺点: 时效性一般,可能存在中间不一致状态场景: 时效性要求一般,有多个服务需要同步基于MQ的异步通知:基于Canal的异步通知:初识Canalcanal是阿里巴巴旗下的一款开源项目,基于java开发。基于数据库增量日志解析,提供增量数据订阅消费。Canal是基于MySQL的主从同步来实现的,MySQL主从同步的原理如下:MySQL master将数据变更写入二进制日志(binary log),其中记录的数据叫做binary log eventsMySQL slave将master的binary log events拷贝到它的中继日志(relay log)MySQL slave重放relay log中事件,将数据变更反映它自己的数据Canal就是把自己伪装成MySQL的一个slave节点,从而监听master的binary log变化。再把得到的变化信息通知给Canal的客户端,进而完成对其它数据库的同步。安装和配置Canal下面我们就开启MySQL的主从同步机制,让Canal来模拟slave。开启MySQL主从Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。这里以之前用Docker运行的MySQL为例:开启binlog打开MySQL容器挂载的日志文件,我的在/tmp/mysql/conf目录:修改文件:vi /tmp/mysql/conf/my.cnf添加内容:log-bin/var/lib/mysql/mysql-bin binlog-do-dbyajc配置解读:log-bin/var/lib/mysql/mysql-bin: 设置binary log文件的存放地址和文件名,叫做mysql-binbinlog-do-dbyajc: 指定对哪个database记录binary log events,这里记录yajc这个库最终效果:[mysqld] skip-name-resolve character_set_serveruft8 datadir/var/lib/mysql server-id1000 log-bin/var/lib/mysql/mysql-bin Binlog-do-dbyajc设置用户权限接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对yajc这个库的操作权限。create user canal’%’ IDENTIFIED by ‘cannal’; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT, SUPER ON *.* TO ‘canal’’%’ identified by ‘canal’; FLUSH PRIVILEGES;重启mysql容器即可:docker restart mysql测试设置是否成功: 在mysql控制台,或者navicat中,输入命令:show master status;安装Canal创建网络我们需要创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:docker network create yajc让MySQL加入这个网络:docker network connect yajc mysql将canal.tar上传到虚拟机,通过命令导入:docker load -i canal.tar然后运行命令创建Canal容器:docker run -p 11111:11111 --name canal \ -e canal.destinationsyajc \ -e canal.instance.master.addressmysql:3306 \ -e canal.instance.dbUsernamecanal \ -e canal.instance.dbPasswordcanal \ -e canal.instance.connectionCharsetUTF-8 \ -e canal.instance.tsdb.enabletrue \ -e canal.instance.gtidonfalse \ -e canal.instance.filter.regexyajc\\..* \ --network yajc \ -d canal/canal-server:v1.1.5说明:-p 11111:11111: 这是Canal的默认监听端口-e canal.instance.master.addressmysql:3306: 数据库地址和端口,如果不知道MySQL容器地址,可以通过docker inspect 容器id来查看-e canal.instance.dbUsernamecanal: 数据库用户名-e canal.instance.dbPasswordcanal: 数据库密码-e canal.instance.filter.regexyajc\…*: 要监听的表名称表名称监听支持的语法:MySQL数据解析关注的表,Perl正则表达式多个正则之间以逗号(,)分隔,转义符需要双斜扛(\)常见例子:所有表: .or.\…*Canal shema下所有表: canal\…*Canal下的以canal打头的表: canal\.canal.*Canal shema下的一张表: canal.test1多个规则组合使用然后以逗号隔开: canal\…*,mysql.test1,mysql.test2Canal客户端Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。我们会使用GitHub上的第三方开源的canal-starter。引入依赖:!-- canal --dependencygroupIdtop.javatool/groupIdartifactIdcanal-spring-boot-starter/artifactIdversion1.2.1-RELEASE/version/dependency编写配置:canal: destination: yajc # canal实例名称,要跟canal-server运行时设置的destination一致 server: 192.168.40.129:11111 # canal地址编写监听器,监听Canal消息:packagecom.yajc.item.canalCanalTable(“item’)// 指定要监听的表ComponentPublicclassItemHandlerimplementsEntryHandlerItem{// 指定表关联的实体类OverridePublicvoidinsert(Itemitem){// 监听到数据库的增、改、删的消息// 新增数据到Redis}OverridePublicvoidupdate(Itembefore,Itemafter){// 监听到数据库的增、改、删的消息// 更新Redis数据// 更新本地缓存}OverridePublicvoiddelete(Itemitem){// 监听到数据库的增、改、删的消息// 删除Redis数据// 清理本地缓存}}Canal推送给canal-client的是被修改的这一行数据(row),而我们引入的canal-client则会帮我们把行数据封装到Item实体类中。这个过程中需要知道数据库与实体的映射关系,要用到JPA的几个注解:多级缓存总结