SpringCloud + RabbitMQ + Docker + Redis + 搜索 + 分布式--高级篇笔记

目录

1、初识Sentinel

1.1、雪崩问题及解决方案

1.2、限流规则

1.2.1、流控模式有哪些?

1.2.2、流控效果有哪些?

1.3、隔离和降级

1.3.1、Sentinel支持的雪崩解决方案

1.3.2、Feign整合Sentinel的步骤

1.3.3、线程隔离的两种手段是?

1.3.4、信号量隔离的特点是?

1.3.5、线程池隔离的特点是?

1.3.6、Sentinel熔断降级的策略有哪些?

1.4、授权规则

1.4.1、获取请求来源的接口是什么?

1.4.2、处理BlockException的接口是什么?

1.5、规则持久化

1.5.1、Sentinel的三种配置管理模式是什么?

2、分布式事务理论基础

2.1.1、简述CAP定理内容?

2.1.2、简述BASE理论三个思想?

3、初识seata

3.1、Seata架构

3.2、动手实践

3.2.1、XA模式

3.2.2、AT模式

3.2.3、TCC模式

3.2.4、Saga模式

 4、Redis

4.1、Redis持久化

4.1.1、Redis持久化-RDB持久化

4.1.2、Redis持久化-AOP持久化

 4.2、Redis主从

4.2.1、Redis主从-搭建主从架构

4.2.2、Redis主从-主从数据同步原理

4.3、Redis哨兵

4.3.1、哨兵的作用和原理

4.3.2、搭建哨兵集群

5、多级缓存

5.1、 JVM进程缓存

5.1.1、传统缓存的问题

 5.1.2、多级缓存方案

5.1.3、本地进程缓存

5.2、Lua语法入门

5.2.1、初识Lua

 5.2.2、变量和循环

5.2.3、条件控制、函数

 5.3、多级缓存

5.3.1、安装OpenResty

5.4、缓存同步

5.4.1、缓存同步策略

6、服务异步通讯-RabbitMQ的高级特性

6.1、消息可靠性

6.1.1、生产者消费确认

6.1.2、消息持久化

6.1.3、消费者消息确认

6.1.4、消费失败重试机制

6.2、死信交换机

6.2.1、初识死信交换机

6.2.2、TTL

6.3、惰性队列

 6.3.1、消息堆积问题


1、初识Sentinel

Sentinel定位是分布式系统的流量防卫兵。目前互联网应用基本上都使用微服务,微服务的稳定性是一个很重要的问题,而限流、熔断降级是微服务保持稳定的一个重要的手段。

1.1、雪崩问题及解决方案

什么是雪崩问题?

  • 微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况。

如何避免因瞬间高并发流量而导致服务故障(预防)?

  • 流量控制

如何避免因服务故障引起的雪崩问题(出现故障后的解决措施,避免雪崩)?

  • 超时处理
  • 线程隔离
  • 降级熔断

1.2、限流规则

1.2.1、流控模式有哪些?

  • 直接:对当前资源限流

 

  • 关联:高优先级资源触发阈值,对低优先级资源限流

  • 链路:阈值统计时,只统计从指定资源进入当前资源的请求,是对请求来源的限流

1.2.2、流控效果有哪些?

  • 快速失败:QPS超过阈值时,拒绝新的请求
  • warm up:QPS超过阈值时,拒绝新的请求;QPS阈值是逐渐提升的,可以避免冷启动时高并发导致服务宕机
  • 排队等待:请求会进入队列,按照阈值允许的时间间隔依次执行请求;如果请求预期等待时长大于超时时间,直接拒绝

1.3、隔离和降级

1.3.1、Sentinel支持的雪崩解决方案

  • 线程隔离(舱壁模式)
  • 降级熔断

1.3.2、Feign整合Sentinel的步骤

  • 在application.yml中配置:feign.sentinel.enable=true
  • 给FeignClient编写FallbackFactory并注册为Bean
  • 将FallbackFactory配置到FeignClient

1.3.3、线程隔离的两种手段是?

  • 信号量隔离
  • 线程池隔离

1.3.4、信号量隔离的特点是?

  • 基于计数器模式,简单,开销小

1.3.5、线程池隔离的特点是?

  • 基于线程池模式,有额外开销,但隔离控制更强

1.3.6、Sentinel熔断降级的策略有哪些?

  • 慢调用比例:超过指定时长的调用为慢调用,统计单位时长内慢调用的比例,超过阈值则熔断
  • 异常比例:统计单位时长内异常调用的比例,超过阈值则熔断
  • 异常数:统计单位时长内异常调用的次数,超过阈值则熔断

1.4、授权规则

1.4.1、获取请求来源的接口是什么?

  • RequestOriginParser

1.4.2、处理BlockException的接口是什么?

  • BlockExceptionHandler

1.5、规则持久化

1.5.1、Sentinel的三种配置管理模式是什么?

  • 原始模式:sentinel默认模式,将规则保存在内存,重启服务会丢失
  • pull模式:保存在本地文件或数据库,定时去读取
  • push模式:保存在nacos,监听变更实时更新

2、分布式事务理论基础

CAP原则又称CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)

可用性(A):保证每个请求不管成功或者失败都有响应。

分区容忍性(P):系统中任意信息的丢失或失败不会影响系统的继续运作。

2.1.1、简述CAP定理内容?

  • 分布式系统节点通过网络连接,一定会出现分区问题(P)
  • 当分区出现时,系统的一致性(C)和可用性(A)就无法同时满足

思考:elasticsearch集群是CP还是AP?

  • ES集群出现分区时,故障节点就会被剔除集群,数据分片会重新分配到其它节点,保证数据一致。因此是低可用性,高一致性,属于CP。

2.1.2、简述BASE理论三个思想?

  • 基本可用
  • 软状态
  • 最终一致

解决分布式事务的思想和模型:

  • 全局事务:整个分布式事务
  • 分支事务:分布式事务中包含的每个子系统的事务
  • 最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据
  • 强一致思想:各分支事务执行玩业务不要提交,等待彼此结果。而后统一提交或回滚

3、初识seata

Seata 是阿里开源的一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。

3.1、Seata架构

Seata事务管理中三个重要的角色:

  • TC(Transaction Coordinator)-事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
  • TM(Transaction Manager)-事务管理器:定义全局事务的范围,开始全局事务、提交或回滚全局事务。
  • RM(Resource Manager)-资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

3.2、动手实践

  • XA 模式
  • AT 模式
  • TCC 模式
  • SAGA 模式

3.2.1、XA模式

XA模式原理

XA规范是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA规范描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范提供了支持。

XA模式的优点是什么?

  • 事务的强一致性,满足ACID原则
  • 常用数据库都支持,实现简单,并且没有代码侵入

XA模式的缺点是什么?

  • 因为一阶段需要锁定数据库,等待二阶段结束才能释放,性能较差
  • 依赖关系型数据库事务实现

3.2.2、AT模式

AT模式原理

AT模式同样是分阶段提交的事务模型,不过却弥补了XA模型中资源锁定周期过长缺陷

简述AT模式与XA模式最大的区别是什么?

  • XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源
  • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚
  • XA模式强一致;AT模式最终一致

AT模式的优点:

  • 一阶段完成直接提交事务,释放数据库资源,性能比较好
  • 利用全局锁实现读写隔离
  • 没有代码侵入,框架自动完成回滚和提交

AT模式的缺点:

  • 两阶段之间属于软状态,属于最终一致
  • 框架的快照功能会影响性能,但比XA模式要好很多

3.2.3、TCC模式

TCC模式原理

TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

  • Try:资源的检测和预留
  • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功
  • Cancel:预留资源释放,可以理解为try的方向操作

TCC模式的每个阶段是做什么的?

  • Try:资源检查和预留
  • Confirm:业务执行和提交
  • Cancel:预留资源的释放

TCC的优点是什么?

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比AT模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

TCC的缺点是什么?

  • 有代码侵入,需要人为编写Try、Confirm和Cancel接口,太麻烦
  • 软状态,事务是最终一致
  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理
package cn.itcast.account.service;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

@LocalTCC
public interface AccountTCCService {

    /**
     * Try逻辑,@TwoPhaseBusinessAction中的name属性要与当前方法名一致,用于指定Try逻辑对应的方法
     * @param userId 用户id
     * @param money 所剩余额
     */
    @TwoPhaseBusinessAction(name = "deduct",commitMethod = "confirm",rollbackMethod = "cancel")
    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                @BusinessActionContextParameter(paramName = "money") int money);

    /**
     * 二阶段confirm确认方法,可以另命名,但要保证与commitMethod一致
     * @param context 上下文,可以传递try方法的参数
     * @return boolean 执行是否成功
     */
    boolean confirm(BusinessActionContext context);

    /**
     * 二阶段回滚方法,要保证与rollbackMethod一致
     */
    boolean cancel(BusinessActionContext context);
}
package cn.itcast.account.service.impl;

import cn.itcast.account.entity.AccountFreeze;
import cn.itcast.account.mapper.AccountFreezeMapper;
import cn.itcast.account.mapper.AccountMapper;
import cn.itcast.account.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
public class AccountTCCServiceImpl implements AccountTCCService {

    @Autowired
    private AccountMapper accountMapper;

    @Autowired
    private AccountFreezeMapper freezeMapper;

    @Override
    @Transactional
    public void deduct(String userId, int money) {
        //0.获取事务id
        String xid = RootContext.getXID();
        //判断freeze中是否有冻结记录,如果有,一定是CANCEL执行过,我要拒绝执行
        AccountFreeze ordFreeze = freezeMapper.selectById(xid);
        if (ordFreeze != null) {
            return;
        }
        //1.扣除可用金额
        accountMapper.deduct(userId,money);
        //2.记录冻结金额,事务状态
        AccountFreeze freeze = new AccountFreeze();
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        freeze.setState(AccountFreeze.State.TRY);
        freeze.setXid(xid);
        freezeMapper.insert(freeze);
    }

    @Override
    public boolean confirm(BusinessActionContext context) {
        //1.获取事务id
        String xid = context.getXid();
        //2.根据id删除冻结记录
        int count = accountMapper.deleteById(xid);
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        //0.查询冻结记录
        String xid = context.getXid();
        String userId = context.getActionContext("userId").toString();
        AccountFreeze freeze = freezeMapper.selectById(xid);
        //空回滚的判断,判断freeze是否为null,为null证明try没执行,需要空回滚
        if (freeze == null){
            //证明try没执行,需要空回滚
            freeze = new AccountFreeze();
            freeze.setFreezeMoney(0);
            freeze.setState(AccountFreeze.State.CANCEL);
            freeze.setUserId(userId);
            freeze.setXid(xid);
            freezeMapper.insert(freeze);
            return true;
        }
        //幂等判断
        if (freeze.getState() == AccountFreeze.State.CANCEL){
            //已经处理过一次CANCEL了,无需重复处理
            return true;
        }
        //1.恢复可用余额
        accountMapper.refund(freeze.getUserId(),freeze.getFreezeMoney());
        //2.将冻结金额清零,修改为CANCEL
        freeze.setFreezeMoney(0);
        freeze.setState(AccountFreeze.State.CANCEL);
        int count = freezeMapper.updateById(freeze);
        return count == 1;
    }
}

3.2.4、Saga模式

Saga模式是SEATA提供的长事务解决方案。也分为两个阶段:

  • 一阶段:直接提交本地事务
  • 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚

saga模式优点:

  • 事务参与者可以基于事件驱动实现异步调用,吞吐高
  • 一阶段直接提交事务,无锁,性能好
  • 不用编写TCC中的三个阶段,实现简单

缺点:

  • 软状态持续事件不确定,时效性差
  • 没有锁,没有事务隔离,会有脏写

 4、Redis

4.1、Redis持久化

4.1.1、Redis持久化-RDB持久化

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

快照文件称为RDB文件,默认是保存在当前运行目录。

持久化过程

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

fork采用的是copy-on-write技术:

  • 当主进程执行读操作时,访问共享内存;
  • 当主进程执行写操作时,则会拷贝一份数据,执行写操作

 RDB方式bgsave的基本流程?

  • fork主进程得到一个子进程,共享内存空间
  • 子进程读取内存数据并写入新的RDB文件
  • 用新的RDB文件替换旧的RDB文件

RDB会在什么时候执行?save 60 1000 代表什么含义?

  • 默认是服务停止时
  • 代表60秒内至少执行1000次修改则触发RDB

RDB的缺点?

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

4.1.2、Redis持久化-AOP持久化

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

AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:

#是否开启AOF功能,默认是no
appendonly yes
#AOF文件的名称
appendfilename "appendonly.aof"

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

#表示每执行一次写命令,立即记录到AOF文件
appendfsync always
#写命令执行完先放入AOF缓冲区,然后表示每个1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
#写命令执行完先访谈缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no

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

Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置:

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

 4.2、Redis主从

4.2.1、Redis主从-搭建主从架构

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

 假设有A、B两个Redis实例,如何将B作为A的slave节点?

  • 在B节点执行命令:slaveof  A的IP  A的port

使用命令 info replication 查看节点信息

4.2.2、Redis主从-主从数据同步原理

主从同步第一次是全量同步

 master如何判断slave是不是第一次来同步数据?这里会用到两个很重要的概念:

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

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

 简述全量同步的流程?

  • slave节点请求增量同步
  • master节点判断replid,发现不一致,拒绝增量同步
  • master将完整内存数据生成RDB,发送RDB到slave
  • slave清空本地文件,加载master的RDB
  • master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
  • slave执行接收到的命令,保持与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时

4.3、Redis哨兵

4.3.1、哨兵的作用和原理

哨兵的作用

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.16.3 7002命令,让这些slave成为新master的从节点,开始从新的master上同步数据
  • 最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点

4.3.2、搭建哨兵集群

Redis

分片集群结构

创建集群,我们使用的是Redis6.2.4版本,集群管理以及集成到了redis-cli中,格式如下:

redis-cli --cluster create --cluster-replicas 1 192.168.16.3:7001 192.168.16.3:7002 192.168.16.3:7003 192.168.16.3:8001 192.168.16.3:8002 192.168.16.3:8003

 主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:

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

使用分片集群可以解决上述问题,分片集群特征:

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

 散列插槽

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

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

如何将同一类数据固定的保存在同一个Redis实例?

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

 集群伸缩

案例:向集群中添加一个新的master节点,并向其中存储 num = 10

需求:

  • 启动一个新的redis实例,端口为7004
  • 添加7004到之前的集群,并作为一个master节点
  • 给7004节点分配插槽,使得num这个key可以存储到7004实例

 向集群中添加一个master节点

  • 192.168.16.3:7004 添加节点的ip和端口
  • 192.168.16.3:7001 集群中已经存在的节点
redis-cli --cluster add-node 192.168.16.3:7004 192.168.16.3:7001

 插槽分配

主从和哨兵可以实现读写分离和故障转移,但redis集群它也实现了自动故障转移(不需要配置哨兵模式)和读写分离的效果。

5、多级缓存

5.1、 JVM进程缓存

5.1.1、传统缓存的问题

传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库,存在下面的问题:

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

 5.1.2、多级缓存方案

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

5.1.3、本地进程缓存

缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:

分布式缓存,例如Redis:

  • 优点:存储容量更大,可靠性更好、可以在集群中共享
  • 缺点:访问缓存有网络开销
  • 场景:缓存数据量较大、可靠性要求更高、需要在集群间共享

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

  • 优点:读取本地内存,没有网络开销,速度更快
  • 缺点:存储容量有限、可靠性较低、无法共享
  • 场景:性能要求较高,缓存数据量较小

案例:实现商品的查询的本地进程缓存

利用Caffeine实现下列需求:

  • 给根据id查询商品的业务添加缓存,缓存未命中时查询数据库
  • 给根据id查询商品库存的业务添加缓存,缓存未命中时查询数据库
  • 缓存初始大小为100
  • 缓存上限为10000
package com.heima.item.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<Long, Item> itemCache(){
        return Caffeine.newBuilder() //构建
                .initialCapacity(100)  //初始化大小
                .maximumSize(10_000) //上限最大值
                .build();
    }

    @Bean
    public Cache<Long, ItemStock> itemStockCache(){
        return Caffeine.newBuilder() //构建
                .initialCapacity(100)  //初始化大小
                .maximumSize(10_000) //上限最大值
                .build();
    }
}
package com.heima.item.web;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.benmanes.caffeine.cache.Cache;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import com.heima.item.pojo.PageDTO;
import com.heima.item.service.IItemService;
import com.heima.item.service.IItemStockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("item")
public class ItemController {

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    @Autowired
    private Cache<Long,Item> itemCache;
    @Autowired
    private Cache<Long,ItemStock> itemStockCache;


    @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 itemStockCache.get(id,key -> stockService.getById(id));
    }
}

5.2、Lua语法入门

5.2.1、初识Lua

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

 5.2.2、变量和循环

数据类型

 变量

 循环

 

5.2.3、条件控制、函数

函数

 

 条件控制

print('hello world!')


local arr = {'java','lua','python'}
local map = {name='jack',age=21}

local function printArr(arr)
  if (not arr) then
    print('数组不能为空!')
    return nil
  end
  for i, var in ipairs(arr) do
     print(var)
   end
end


printArr(arr)
local arr2 = {100, 200, 300}
printArr(arr2)
printArr(nil)


for key,value in pairs(map) do
  print(key,value)
end

 5.3、多级缓存

5.3.1、安装OpenResty

OpenResty是一个基于Nginx的高性能web平台,用于方便地搭建能够处理高并发,扩展性极高的动态web应用

web服务和动态网关,具体下列特点:

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

官方网站:OpenResty® - 中文官方站

5.4、缓存同步

5.4.1、缓存同步策略

缓存数据同步的常见方式有三种:

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

  • 优势:简单、方便
  • 缺点:时效性差,缓存过期之前可能不一致
  • 场景:更新频率较低,时效性要求低的业务

同步双写:在修改数据库的同时,直接修改缓存

  • 优势:时效性强,缓存与数据库强一致
  • 缺点:有代码侵入,耦合度高
  • 场景:对一致性,时效性要求较高的缓存数据

异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据

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

6、服务异步通讯-RabbitMQ的高级特性

MQ的常见问题

6.1、消息可靠性

6.1.1、生产者消费确认

RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。

结果有两种请求:

publisher-confirm,发送者确认

  • 消息成功投递到交换机,返回ack
  • 消息为投递到交换机,返回nack

publisher-return,发送者回执

  • 消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因,调用ReturnCallback

6.1.2、消息持久化

发送者

    @Test
    public void testDurableMessage() {
        // 1.准备消息
        Message message = MessageBuilder.withBody("hello, spring".getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
                .build();
        // 2.发送消息
        rabbitTemplate.convertAndSend("simple.queue", message);
    }

消费者,声明持久化,底层默认已经持久化

package cn.itcast.mq.config;

import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;

// @Configuration
public class CommonConfig {
    @Bean
    public DirectExchange simpleDirect(){
        return new DirectExchange("simple.direct");
    }
    @Bean
    public Queue simpleQueue(){
        return QueueBuilder.durable("simple.queue").build();
    }
}

6.1.3、消费者消息确认

RabbitMQ支持消费者确认机制,即:消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息。

而SpringAMQP则允许配置的三种确认模式:

  • manual:手动ack,需要在业务代码结束后,调用api发送ack。
  • auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack
  • none:关闭ack,MQ假定消费者获取信息后会成功处理,因此消息投递后立即被删除

配置方式是修改消费者的application.yml,添加下面配置:

spring: 
  rabbitmq:    
    listener:
      simple:
        prefetch: 1
        acknowledge-mode: auto #none,关闭ack;manual,手动ack,auto,自动ack

6.1.4、消费失败重试机制

当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力:

 我们可以利用Spring的resty机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。

spring: 
  rabbitmq: 
    listener:
      simple: 
        prefetch: 1       
        retry:
          enabled: true  #开启消费者失败重试
          initial-interval: 1000 #初始的失败等待时长为1秒
          multiplier: 3 #下次失败的等待时长倍数,下次等待时长 = multiplier * last - interval
          max-attempts: 4  #最大重试次数
          stateless: true #true无状态,flase有状态。如果业务中包含事务,这里改为flase,默认是true

如何确保RabbitMQ消息的可靠性?

  • 开启生产者确认机制,确保生产者的消息能到达队列
  • 开启持久化功能,确保消息未消费前在队列中不会丢失
  • 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
  • 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理

6.2、死信交换机

6.2.1、初识死信交换机

当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):

  • 消费者使用basic.reject或basic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息是一个过期信息,超时无人消费
  • 要投递的队列消息堆积满了,最早的消息可能成为死信

如果该队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机成为死信交换机(Dead Letter Exchange,简称DLX)

 什么样的消息会成为死信?

  • 消息被消费者reject或者返回nack
  • 消息超时未消费
  • 队列满了

如何给队列绑定死信交换机?

  • 给队列设置dead-letter-exchange属性,绑定一个交换机
  • 给队列设置dead-letter-exchange-key属性,设置死信交换机与死信队列的RoutingKey

6.2.2、TTL

 TTL,也就是Time-To-Live。如果一个队列中的消息TTL结束仍未消费,则会变为死信,ttl超时分为两种情况:

  • 消息所在的队列设置了存活时间
  • 消息本身设置了存活时间

消息超时的两种方式是?

  • 给队列设置ttl属性,进入队列后超过ttl时间的消息变为死信
  • 给消息设置ttl属性,队列接收到消息超过ttl时间后变为死信
  • 两者共存时,以时间短的ttl为准

给消费者队列设置超时时间 ttl = 10000

package cn.itcast.mq.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;

// @Configuration
public class TTLMessageConfig {

    @Bean
    public DirectExchange ttlDirectExchange(){
        return new DirectExchange("ttl.direct");
    }

    @Bean
    public Queue ttlQueue(){
        return QueueBuilder
                .durable("ttl.queue")
                .ttl(10000)
                .deadLetterExchange("dl.direct")
                .deadLetterRoutingKey("dl")
                .build();
    }

    @Bean
    public Binding ttlBinding(){
        return BindingBuilder.bind(ttlQueue()).to(ttlDirectExchange()).with("ttl");
    }
}

给发送者设置消息超时时间 ttl = 5000

@Test
    public void testTTLMessage() {
        // 1.准备消息
        Message message = MessageBuilder
                .withBody("hello, ttl messsage".getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
                .setExpiration("5000")
                .build();
        // 2.发送消息
        rabbitTemplate.convertAndSend("ttl.direct", "ttl", message);
        // 3.记录日志
        log.info("消息已经成功发送!");
    }

如何实现发送一个消息20秒后消费者才能收到消息?

  • 给消息的目标队列指定死信交换机
  • 消费者监听与死信交换机绑定的队列
  • 发送消息时给消息设置ttl为20秒

6.3、惰性队列

 6.3.1、消息堆积问题

当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。最早接收到的消息,可能就会成为死信,会被丢弃,这就是消息堆积问题。

解决消息堆积的三种思路:

  • 增加更多的消费者,提高消费速度
  • 在消费者内开启线程池加快消息处理速度
  • 扩大队列容积,提高堆积上限

消息堆积问题的解决方案?

  • 队列上绑定多个消费者,提高消费速度
  • 给消费者开启线程池,提高消费速度
  • 使用惰性队列,可以在mq中保存更多消息

生产者

 /**
     * 惰性队列
     * @throws InterruptedException
     */
    @Test
    public void testLazyQueue() throws InterruptedException {
        long b = System.nanoTime();
        for (int i = 0; i < 1000000; i++) {
            // 1.准备消息
            Message message = MessageBuilder
                    .withBody("hello, Spring".getBytes(StandardCharsets.UTF_8))
                    .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)
                    .build();
            // 2.发送消息
            rabbitTemplate.convertAndSend("lazy.queue", message);
        }
        long e = System.nanoTime();
        System.out.println(e - b);
    }

    /**
     * 普通队列
     * @throws InterruptedException
     */
    @Test
    public void testNormalQueue() throws InterruptedException {
        long b = System.nanoTime();
        for (int i = 0; i < 1000000; i++) {
            // 1.准备消息
            Message message = MessageBuilder
                    .withBody("hello, Spring".getBytes(StandardCharsets.UTF_8))
                    .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)
                    .build();
            // 2.发送消息
            rabbitTemplate.convertAndSend("normal.queue", message);
        }
        long e = System.nanoTime();
        System.out.println(e - b);
    }

消费者

package cn.itcast.mq.config;

import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;

// @Configuration
public class LazyConfig {

    @Bean
    public Queue lazyQueue() {
        return QueueBuilder.durable("lazy.queue")
                .lazy()
                .build();
    }

    @Bean
    public Queue normalQueue() {
        return QueueBuilder.durable("normal.queue")
                .build();
    }
}

惰性队列的优点有哪些?

  • 基于磁盘存储,消息上限高
  • 没有间歇性的page-out,性能比较稳定

惰性队列的缺点有哪些?

  • 基于磁盘存储,消息时效性降低
  • 性能受限于磁盘的IO

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要构建一个基于上述技术栈的应用程序,涉及多个组件和技术,下面是一些关键点的简介: 1. **Spring Boot**: 是一个快速开发框架,简化了Java应用的配置和启动过程。 - 示例:用于创建简单的RESTful API服务[^4]。 2. **Spring Cloud**: 提供了一组工具和服务来扩展微服务架构。 - 功能包括服务发现、配置中心、API网关等[^5]。 3. **RabbitMQ**: 消息队列服务,支持异步通信和解耦。 - 在Spring Cloud中集成,可以用来实现消息驱动架构[^6]。 4. **Redis**: 缓存数据库,提高应用程序性能。 - 可以缓存热点数据或会话信息[^7]。 5. **Elasticsearch**: 分布式搜索和分析引擎,常用于全文检索。 - 支持复杂查询和实时数据分析[^8]。 6. **Xxl-sso**: 企业级权限管理系统,用于身份验证和授权[^9]。 7. **LCN**: 可能指的是Linux容器网络,Docker的基础组件。 - 管理容器间的网络连接[^10]。 8. **Nginx**: 反向代理服务器,优化HTTP请求和负载均衡。 - 与Spring Boot结合时,可能作为API Gateway[^11]。 9. **七牛云**: 对象存储服务,用于文件上传和管理。 - 存储静态资源[^12]。 10. **Swagger2**: 开源API文档生成工具。 - 用于自动生成API文档[^13]。 11. **MySQL**: 关系型数据库,存储业务数据。 - 数据持久化[^14]。 12. **Maven**: 项目管理和依赖管理工具。 - 用于构建和打包项目[^15]。 13. **GitLab**: 代码版本控制系统,用于版本控制和协作开发。 - 版本控制和CI/CD[^16]。 14. **Docker**: 虚拟化平台,便于部署和运行应用。 - 快速构建可移植的环境[^10]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值