你们项目中没有做过限流?怎么做的?

介绍项目业务,什么情况下用到了限流

  • 我们当时有一个活动,到了假期回抢购优惠券,QPS最高可以达到2000,平时10-50之间,为了应对突发流量,需要做限流

  • 常规限流,为了防止恶意攻击,保护系统正常运行,我们当时系统能够承受最大的QPS是多少(压测的结果,例如:1000)

Nginx限流

  • 控制速率(突发流量),使用漏桶算法来实现过滤,让请求以固定的速率处理请求,可以应对突发流量

  • 控制并发数,限制单个IP的链接数和并发链接的总数

网关限流

  • 在Spring Cloud Gateway中支持局部过滤器RequestRateLimiter来做限流,使用的是令牌算法

  • 可以根据IP或路径进行限流,可以设置每秒填充平均速率,和令牌桶总容量

限流常见的算法有哪些呢?

  • 漏桶算法

  • 令牌算法

两者区别:https://serve.ulucky.cn/notes/%E9%99%90%E6%B5%81%E7%AE%97%E6%B3%95.html

解释一下CAP和BASE

CAP定理(一致性、可用性、分区容错性)

  • 分布式系统节点通过网络连接,一定会出现分区问题(P)

  • 当分区出现时,系统的一致性(C)和可用性(A)就无法同时满足

BASE理论

  • 基本可用

  • 软状态

  • 最终一致

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

  • 最终一致性思想:各分支事务分别执行和提交,如果有不一致的情况,再想办法恢复数据(AP)

  • 强一致思想:各分支事务执行完业务不要提交,等待彼此结果,而后统一提交或回滚(CP)

你们采用哪种分布式事务的解决方案?

使用Seata框架

  • Seata的XA模式,CP,需要互相等待各个分布式事务提交,可以保证强一致性,性能差

  • Seata的AT模式,AP,底层使用的是undo log实现,性能好

  • Seata的TCC模式,AP,性能较好,不过需要人工编码实现

  • MQ模式实现分布式事务,在A服务写数据的时候,需要在同一个事务内发送消息到另外一个事务,异步,性能最好

点击查看详情

MQ分布式事务流程图

分布式服务的接口幂等性如何设计呢?

  • 幂等:多次调用接口或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致

  • 如果是新增数据,可以试用数据库的唯一索引

  • 如果是新增或修改数据呢?

    • 分布式锁,性能较低

    • Token+Reids来实现,性能好

      • 第一次请求,生成一个唯一的token存入Redis,返回给前端

      • 第二次请求,业务处理,携带之前的Token,到Redis进行验证,如果存在,可以执行业务,删除token;,如果不存在,则直接返回,不处理业务

你们项目中用了什么分布式任务调度?

xxl-job路由策略有哪些?

  • xxl-job提供了很多的路由策略,我们平时用的较多的是:轮询、故障转移、分片广播....

xxl-job任务执行失败怎么解决?

  • 路由策略选择故障转移,使用健康的实例来执行任务

  • 设置重试次数

  • 查看日志+邮件告警来通知相关负责人解决

如果有大数据量的任务同时都要执行,怎么解决?

  • 让多个实例一块去执行(部署集群),路由策略分片广播

  • 在任务执行的代码中可以获取分片总数和当前分片,按照取模的方式分摊到各个实例执行

RabbitMQ-如何保证消息不丢失?

  • 开启生产者确认机制,确保生产者的消息能到达队列

  • 开启持久化功能,确保消息未消费前在队列中不会丢失

  • 开启消费者确认机制为auto,由spring确认消息成功后完成ack

  • 开启消费者失败重试机制,多次重试失败后将消息投递到异常交换机,交由人工处理

详细讲解

RabbitMQ消息的重复消费的问题如何解决?

  • 原因:

    • 网络抖动

    • 消费者挂了

  • 解决方案:

    • 每条消息设置一个唯一的标识ID

    • 幂等方案:【分布式锁、数据库锁(悲观锁、乐观锁)】

RabbitMQ死信交换机?(RabbitMQ延迟队列了解过嘛)

  • 我们当时一个什么业务用到了延时队列(超时订单、现时优惠、定时发布)

  • 其中延迟队列就用到了死信交换机和TTL(消息存活时间)实现的

  • 消息超时未被消费就会变成死信(私信的其他情况:拒绝被消费,队列满了)

延迟队列插件实现延迟队列DelayExchange

  • 声明一个交换机,添加delayed属性为true

  • 发送消息时,添加x-delay头,值为超时时间

RabbitMQ如果有100万消息堆积在MQ,如何解决?(消息堆积怎么解决)

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

  • 增加更多消费者,提高消费速度

  • 在消息者内开启线程池加快消息处理速度

  • 扩大队列容积,提高堆积上限,采用惰性队列

    • 在声明队列的时候可以设置x-queue-mode为lazy,即为惰性队列

    • 基于磁盘存储,消息上限高

    • 性能比较稳定,但基于磁盘存储,受限于磁盘IO,时效性会降低

RabbitMQ的高可用有了解过吗?

  • 在生产环境下,我们当时采用的镜像模式搭建的集群,共有3个节点

  • 镜像队列结构是一主多从(从就是镜像),所有操作都是主节点完成,然后同步给镜像节点

  • 主宕机后,镜像节点会替代成新的主(如果主从同步完成前,主就已经宕机,可能出现数据丢失)

那出现丢失数据怎么办呢?

我们可以采用仲裁队列,与镜像队列一样,都是主从模式,支持主从数据同步,主从同步基于Raft协议,强一致,并且使用起来非常简单,不需要额外的配置,在声明队列的时候只需要指定这个是仲裁队列即可

设计模式-工厂模式

简单工厂模式

  • 抽象产品:定义了产品的规范,描述了产品的主要特性和功能

  • 具体产品:实现或继承抽象产品的子类

  • 具体工厂:提供创建了产品的方法,调用者通过改方法来获取产品

工厂方法模式

  • 抽象工厂(Abstract Factory):提供创建了产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。

  • 具体工厂(ConcreteFactory):主要实现抽象工厂中的抽象方法,完成具体产品创建。

  • 抽象产品(Prudoct):定义了产品的规范,描述了产品的主要特性和功能。

  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。

    • 优点:

      • 用户只需要知道具体的工厂的名称就可以得到索要的产品,无须知道产品的具体创建过程;

      • 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则

    • 缺点:

      • 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。

抽象工厂模式

  • 产品族:一个品牌下面的所有产品,例如华为下面的电脑、手机称为华为的产品族;

  • 产品等级:多个品牌下面的同种产品;例如华为和小米都有手机电脑为一个产品等级;

策略模式

哪些场景使用策略模式?

  • 订单的支付策略(支付宝、微信、银行卡)

  • 解析不同类型的Excel(xls格式、xlsx格式)

  • 打折促销(满300元打9折、满500打8折、满1000打7折)

  • 物流运费计算等等...

什么是策略模式呢?

  • 策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户

  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中

案例(工厂方法+策略)

  • 介绍业务(登录、支付、解析Excel、优惠等级..)

  • 提供了很多种策略,都让Spring容器管理

  • 提供一个工厂:准备策略对象,根据参数提供对象

什么是程序计数器?

  • 线程私有的,每个线程都有一份,内部保存的字节码的行号,主要是用于记录正在执行的字节码指令的地址。

程序计数器

JVM架构全解析

你能详细介绍一下Java堆吗?

  • Java堆是一个线程共享的区域,主要用来保存对象实例、数组等,内存不够抛出OutOfMemoryError异常。

  • 组成:年轻代+老年代

    • 年轻代本划分为三部分,Eden区和两个大小严格相同的Survivor区

    • 老年代主要保存生命周期长的对象,一般是一些老的对象

  • JDK1.7和JDK1.8的区别

    • 1.7中拥有一个永久代,存储的类信息、静态变量、常量,编译后的代码

    • 1.8移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出

什么是虚拟机栈?

Java Virtual machine Stacks(Java 虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈,先进后出

  • 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

垃圾回收是否涉及栈内存呢?

  • 垃圾回收主要指的是堆内存,当栈帧弹栈以后,内存就回释放

栈内存分配的越大越好吗?

  • 未必,默认的栈内存为1024k

  • 栈帧过大会导致线程数变少,例如,机器总内存为512M,目前能活动的线程则为512个,如果把栈内存改为2048k,那么能活动的栈帧就回减半

方法内从局部变量是否线程安全?

  • 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的

  • 如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

栈内存溢出情况

  • 栈帧过多导致栈内存溢出,典型问题:递归调用

  • 栈帧过大导致栈内存溢出

堆栈的区别是什么?

  • 栈内存一般回用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的,堆会GC垃圾回收,而栈不会。

  • 栈内存是线程私有的,而堆内存是线程共享的。

  • 两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常

    • 栈内存不足:java.lang.StackOverFlowError

    • 堆内存不足:java.lang.OutOfMemoryError

能不能解释一下方法区?

  • 方法区(Method Area)是各个线程共享的内存区域

  • 主要存储类的信息、运行时常量池

  • 虚拟机启动的时候创建,关闭虚拟机时释放

  • 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError:MetaSpace

介绍一下运行时常量池

  • 常量池:可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

  • 当类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

JVM方法区与运行时常量池详解

你听过直接内存吗?

  • 并不属于JVM中的内存结构,不由JVM进行管理,是虚拟机的系统内存

  • 常见于NIO操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受JVM内存回收管理

什么是类加载器呢?

  • JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来

类加载器有哪些呢?

  • 启动类加载器(BootStrap ClassLoder):加载JAVA_HOME/jre/lib目录下的库

  • 扩展类加载器(ExtClassLoder):主要加载JAVA_HOME/jre/lib/ext目录中的类

  • 应用加载器(ApplClassLoder):用户加载ClassPath下的类

  • 自定义类加载器(CustomizeClassLoder):自定义继承ClassLoder,实现自定义类加载规则

什么双亲委派模型?

  • 加载某一个类,先委托上一个加载器进行加载,如果上级加载器也有上级,则会继续委托上级,如果该委托上级没有被加载,子类加载器尝试加载该类

JVM为什么采用双亲委派极致?

  • 通过双亲委派机制可与避免一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性

  • 为了安全保证API类库不会被修改