Blage's Coding Blage's Coding
Home
算法
  • 手写Spring
  • SSM
  • SpringBoot
  • JavaWeb
  • JAVA基础
  • 容器
  • Netty

    • IO模型
    • Netty初级
    • Netty原理
  • JVM
  • JUC
  • Redis基础
  • 源码分析
  • 实战应用
  • 单机缓存
  • MySQL

    • 基础部分
    • 实战与处理方案
    • 面试
  • ORM框架

    • Mybatis
    • Mybatis_Plus
  • SpringCloudAlibaba
  • MQ消息队列
  • Nginx
  • Elasticsearch
  • Gateway
  • Xxl-job
  • Feign
  • Eureka
  • 面试
  • 工具
  • 项目
  • 关于
🌏本站
🧸GitHub (opens new window)
Home
算法
  • 手写Spring
  • SSM
  • SpringBoot
  • JavaWeb
  • JAVA基础
  • 容器
  • Netty

    • IO模型
    • Netty初级
    • Netty原理
  • JVM
  • JUC
  • Redis基础
  • 源码分析
  • 实战应用
  • 单机缓存
  • MySQL

    • 基础部分
    • 实战与处理方案
    • 面试
  • ORM框架

    • Mybatis
    • Mybatis_Plus
  • SpringCloudAlibaba
  • MQ消息队列
  • Nginx
  • Elasticsearch
  • Gateway
  • Xxl-job
  • Feign
  • Eureka
  • 面试
  • 工具
  • 项目
  • 关于
🌏本站
🧸GitHub (opens new window)
  • 面试

    • 个人向面试问题
    • 分散的面试问题
    • 面试问题合集
      • 网络
        • 1、TCP三次握手和四次挥手
        • 为什么三次??为什么四次??
        • 2、TIME_WAIT状态延迟关闭连接的必要性
        • 3、TIME_WAIT的连接数量过多
        • 4、OSI、HTTP、URL解析
        • 5、重定向和转发
        • 6、cookie和session区别
        • 7、DNS解析的过程?
        • 8、TCP拆包沾包半包问题
        • 9、TCP拥塞控制算法?TCP为什么是可靠的?
      • 操作系统
        • 1、分页置换算法以及代码实现
        • 2、死锁解决方法
        • 3、进程之间的通信方式?
        • 4、内核态和用户态之间的区别?为什么区分两个?
        • 5、操作系统内存管理
        • 6、进程调度
        • 7、死锁的必要条件?
        • 8、页表+TLB+Cache的访问次数
      • JAVA基础
        • 1、三大特性?重写和重载
        • 2、static和final
        • 3、HashMap的put操作?容量为什么是2的整数次幂?哈希扰动函数?红黑树的优势?死循环问题?
        • 容量2的N次方?
        • 哈希值如何计算?
        • 说说红黑树相比其他树优势?
        • 死循环问题?
        • 4、ConcurrentHashMap底层如何实现线程安全?get方法是否上锁?
        • 5、StringBuilder和StringBuffer区分
        • 6、说说类内的静态初始化块执行顺序
        • 7、equals和hashcode的重写问题?
        • 8、Java异常?throw和throws
        • 9、ThreadLocal介绍?内存泄露的问题?
        • ThreadLocal的内存泄露问题?
        • 10、ArrayList的grow扩容过程?
        • 11、Hashmap链表插入方式
        • 12、注解的底层原理?
        • 13、反射的底层机制
      • MySQL
        • 1、MySQL四个特点?隔离级别?脏读、不可重复读、幻读?
        • 2、MySQL为什么使用B+树?
        • 3、如何建立索引?如何查看是否使用索引?最左匹配原则?
        • 4、聚簇索引和非聚簇索引
        • 5、索引什么时候会失效
        • 6、truncate、delete、drop之间的区别
        • 7、join语句
        • 8、SQL优化方案
        • 9、慢SQL语句如何排查?
        • 10、MySQL数据类型和选用的场景
        • 11、执行一条语句的过程
        • 12、Mysql不使用跳表、B树的原因?
        • 13、redolog与binlog区别?两阶段提交是什么?
        • 14、MySQL的刷盘机制?
      • JUC并发
        • 1、volatile保持内存可见性
        • 2、synchronized底层实现?如何获取锁?锁升级
        • 3、谈谈ReentrantLock?和Synchronized对比?
        • 4、线程池参数?核心线程数设置?
        • 5、创建线程的方法?
        • 6、obj.wait、thread.join、thread.sleep的区别?
        • 7、说说CAS思想?缺点是什么?如何解决?
        • 8、什么时候使用CAS和悲观锁?
      • JVM内存
        • 1、JVM内存分配?一个对象从创建到GC的整个过程?
        • 2、双亲委派机制?如何破坏?
        • 双亲委派机制破坏?
        • 3、如何判断一个对象是不是垃圾?
        • 4、垃圾回收算法有哪些?
        • 5、垃圾回收器有哪些?
        • 6、什么是三色标记法?如何解决错标和漏标?
        • 7、minor GC
        • 8、类加载流程
        • 9、对象什么时候跑到堆外面?
        • 10、CMS垃圾回收器?G1垃圾回收器?ZGC垃圾回收器?使用场景
        • 11、jdk1.8默认使用什么垃圾回收器?
      • Redis
        • 1、说说Redis、Memcache、Guava/Caffeine
        • 2、Redis数据类型有哪些?SDS和跳表?
        • 3、Redis持久化机制?
        • 4、Redis内存淘汰策略?过期策略?
        • 5、缓存穿透、缓存击穿、缓存雪崩?对应解决方法?
        • 6、热点key问题?
        • 7、大key问题?
        • 8、如何保证数据库和缓存一致性?
        • 9、Redis的主从模式、哨兵模式?
        • 10、基于Redis能够做什么?
        • 11、谈谈Redis的单线程模型?
        • 12、redis如何设置持久化模式
        • 13、redis刷新策略
        • 14、redisson如何加锁保证原子性?
        • 15、setnx有哪些风险
        • 16、redis一致性hash
      • 消息队列
        • 1、介绍kafka中的topic、partition、replica?
        • 2、kafka与zookeeper之间的关系
        • 3、kafka如何保证消息的顺序消费?
        • 4、kafka如何保证消息不会丢失?
        • 5、kafka如何保证消息的幂等性消费?
        • 6、谈谈死信队列
        • 7、如何处理消息积压问题?
        • 8、kafka为什么这么快?
      • Spring
        • 1、Spring说说IOC,整个Bean的生命周期?
        • 2、Spring的Aware依赖倒置?
        • 3、Spring的AOP切面实现?
        • 4、Spring 如何解决循环依赖问题?
        • 5、Spring的 Event事件机制—观察者模式?
        • 6、Spring的自动扫描@component?那么@Autowired呢?
        • 7、Spring当中的FactoryBean和BeanFactory有什么区别?
        • 8、Spring的设计模式有哪些
      • Netty
        • 1、谈谈BIO、NIO、AIO
        • 2、Reactor模型
        • 3、Netty和NIO之间的区别
        • 4、半包和粘包问题,如何解决?
        • 5、Netty零拷贝
        • 6、Netty为什么快
      • 项目一
        • 1、抽奖项目设计模式
        • 2、数据库路由组件
        • 3、谈谈规则引擎设计的意义是什么?如何实现的?
        • 4、介绍一下整个抽奖活动的主链路?
        • 5、说说抽奖活动的秒杀场景
        • 6、动态路由导致事务失效如何解决?
        • 7、如何防止超领和超发?
        • 8、谈谈两个kafka异步流程?
        • 9、项目中遇到什么问题?
        • 10、抽奖项目调优经验
        • 11、抽奖项目数据库表设计
        • 12、项目DDD划分成几个领域?
        • 13、组合模式
        • 14、递增分布式ID的方案
      • 项目二
        • 1、项目包含哪几个功能模块?简要每个模块的功能和作用?
        • 2、网关通信会话流程如何进行编排
        • 3、说明算力注册和服务发现starter的设计?
        • 4、Redis服务发布订阅使用场景?
        • 5、编程式Docker如何实现?应用场景是什么?
        • 6、谈谈注册中心数据库表的设计
        • 7、如何利用SPI?Spring如何利用拓展点?
        • 8、项目遇到的问题
        • 问题一:关于Netty服务端绑定的问题
        • 问题二:服务上报的问题
        • 9、系统的性能瓶颈在哪儿?
        • 10、Netty如何实现断线重连?
        • 11、网关高可用可以做哪些处理?
        • 12、该网关与springcloud网关有什么区别?
        • 13、其它系统想要接入你的网关,需要哪些步骤?
        • 14、网关为什么自研?和市面上的产品区别在哪儿?
        • 15、网关如何进行区分?
        • 16、RPC服务上报后协议变了,网关如何进行处理?
        • 17、服务降级方案怎么进行设计?
      • 其它
        • 1、说说JWT安全认证?
        • 2、说说Github Actions如何工作?
        • 3、操作系统内核的工作
        • 4、top指令
        • 5、lsof指令全称是什么
        • 6、乐观锁悲观锁使用场景
        • 7、反射违背面向对象的封装性吗?
        • 8、Mybatis和Mybatis-plus的区别
        • 9、跨域问题
        • 10、CPU中断之后进程的处理流程?
        • 11、CAS算法?
        • 12、Nginx负载策略
        • 13、两个线程交替打印奇数偶数
        • 14、协程
        • 15、Docker容器虚拟化技术
        • 16、数组和链表在内存存储上的区别?
      • 备战
        • 技术选型问题?kafka和RocketMQ?网关使用Netty?注册中心数据存储?
        • MySQL锁分类?死锁问题?事务的锁的关系?
        • Spring事务?事务传播?
        • 设计高性能接口
        • Dubbo协议与HTTP协议
        • 孤儿进程?僵尸进程?
        • 硬中断?软中断?
        • JVM栈帧对象释放
        • kafka消息到消费者是推还是拉模式?
        • 为什么要分库,分表?如何分?
        • 分库和分表存在的问题?
      • 备战2
        • 什么是泛型擦除?
        • mybatis配置xml文件的$与#占位符有什么区别?
        • 消息队列可以用来做什么?MQ和RPC区别是什么?
        • java内存泄漏?介绍一些四种引用类型?
        • java为什么不支持多继承?什么时候采用继承和组合?
        • CGLib和JDK动态代理之间的区别
        • 分布式锁的方案有哪些?
      • 职业发展开放性问题
        • 1、个人职业规划是什么?
        • 2、如何看待拼多多?
        • 3、看过哪些技术博客
        • 4、说说你从这几个项目中学到了什么?
        • 5、谈一下你的优缺点
        • 6、项目中遇到的问题
        • 7、开发和算法之间的选择
    • 简历的面试问题

  • 工具

  • 项目

  • 关于

  • 更多
  • 面试
phan
2023-04-18
目录

面试问题合集

# 网络

# 1、TCP三次握手和四次挥手

三次握手:告诉对方自己报文的序列号和期望号。

四次挥手:停止发送报文,但是仍可接收。“发送”和“回复”是异步的,因此需要四次。

# 为什么三次??为什么四次??

计算机网络复杂,具有如下特点:

  • TCP是可靠传输协议,具有超时重传等机制

  • 数据传输可能会出现数据包丢失,超时到达等情况

需要结合具体场景分析,经验证三次&&四次是一种比较好的做法,能够最大限度的保证数据可靠传输。(三次能够确保双方的序列号和期望号都得到确认)

# 2、TIME_WAIT状态延迟关闭连接的必要性

四次挥手流程:

  • client发给server,停止发送报文
  • server发给client,收到
  • server发给client,停止回复报文
  • client发给server,确认。进入TIME_WAIT状态,2MSL之后才关闭连接

因此等待2个报文传输的时间,是保证server能够收到最后一个client发送的报文。(如果2MSL内client没收到任何报文,说明服务端已经接受到最后一个报文;而如果client在2MSL之内收到server发送的第三个报文,说明server没收到第四个报文,因此触发重传机制,此时client需要重新发送第四个报文)

# 3、TIME_WAIT的连接数量过多

现象:说明当前高并发短连接的TCP数量过多。每个TIME_WAIT的TCP连接都会占一个端口,不关闭会导致后来的TCP连接无法创建。

解决:核心思路让服务端快速处理请求,快速关闭客户端的连接。比如设置减少2MSL时间,断开Nginx代理服务器连接。

# 4、OSI、HTTP、URL解析

物理,链路,网络,传输,会话,表示,应用

HTTP1.1支持长连接。HTTP2.0支持新的数据压缩和编码方式。

HTTP端口号80,而HTTPS为443。后者安全性更高,需要用到安全证书。

URL解析:①缓存查映射(浏览器,路由器,本地)②通过DNS对URL进行解析,获取对应IP。③建立TCP连接,发起HTTP请求 ④接收服务器数据,并渲染。

# 5、重定向和转发

重定向:地址栏变化,初次request数据不共享

转发:地址栏不变,服务端能共享第一次request数据

# 6、cookie和session区别

两者都用于追踪客户数据状态

cookie存在客户端的浏览器上。而session存在服务器上

# 7、DNS解析的过程?

DNS解析过程:

  1. 首先从浏览器缓存,系统缓存查看映射
  2. DNS服务器会向顶级域名服务器,二级域名服务器发送请求,解析获取IP地址
    • 直接查询:DNS服务器查询完A后,A返回结果,告诉要去B进行访问,然后DNS服务器就会向B请求
    • 间接查询:DNS查完A后,A不能解决,直接转发给B进行解析

# 8、TCP拆包沾包半包问题

拆包指接收方的缓存区大小大于一个数据包的大小,那么需要将一个完整的数据包拆分成两段进行接收和读取,从而出现沾包和半包的问题。本质上是因为没有划分数据包之间的隔离标志或是分割符导致的。

# 9、TCP拥塞控制算法?TCP为什么是可靠的?

慢开始、拥塞避免、快重传:核心就是发送方的窗口先指数级增加,如果超过了某个门限再慢慢增加,如果此时网络堵塞了,那么就减小发送窗口,重新设置为一个值。

TCP是可靠的主要原因如下:

  • 确认和重传机制
  • 流量控制:避免数据丢失
  • 三握四挥

# 操作系统

# 1、分页置换算法以及代码实现

FIFO:先进先出

LRU:最近最久未使用。双向链表节点(value,首尾指针)+Map以O(1)复杂度访问节点

LFU:淘汰最不常访问的内存页,同使用次数的采用LRU策略。节点属性维护count使用次数+TreeMap维护次数-LRU链的缓存+Map以O(1)拿到节点的使用次数

# 2、死锁解决方法

进程持有资源的同时竞争公共资源,造成多个进程同时等待资源释放的现象。

解决方法:

  • 一次性竞争和分配所有资源。不能吃着碗里的,想着锅里的
  • 进程之间可以剥夺共享资源
  • 按序请求资源
  • 设计超时销毁任务

# 3、进程之间的通信方式?

同一台主机进程之间的通信方式如下几种:

  • 管道通信
  • 消息队列
  • 共享内存:通过在共享内存当中,对象obj的wait方法和notify方法,控制线程之间进行通知;volatile和synchronied进行读写变量。

如果跨主机之间进行通信,则通过socket进行通信。

# 4、内核态和用户态之间的区别?为什么区分两个?

内核态:涉及操作系统底层和硬件资源的管理。包括内存管理、任务调度、系统管理。

用户态:权限较低,一般只用于执行用户程序。

区分用户态和核心态主要两个方面:

  • 可维护性更高,便于划分功能和边界
  • 安全性上,防止用户程序直接调度系统资源,造成系统崩溃

# 5、操作系统内存管理

动态分区算法:最优先匹配、最佳适应、最坏适应。

内存淘汰算法:FIFO、LRU

虚存技术:解决内存空间不够的问题。一部分程序先装入内存,然后另一部分装入磁盘空间,执行的时候程序不在内存,则根据地址映射从磁盘读取物理页到内存当中。

# 6、进程调度

进程调度算法:FIFO、最短作业优先、高响应比优先、时间片轮转、最高优先级调度。

# 7、死锁的必要条件?

死锁是指多个进程同时进入阻塞等待的状态,它们都在等待某个资源的释放。产生死锁的必要条件:

  1. 资源是互斥使用的
  2. 非抢占式
  3. 进程占有资源并等待
  4. 循环等待

# 8、页表+TLB+Cache的访问次数

发生缺页中断之后,执行的流程如下:

  • 首先查看Cache有没有,如果有则直接读取。
  • 查看TLB快表是否命中,如果命中则根据对应的物理页地址读取到内存当中,并更新Cache
  • 查看页表,将数据页从磁盘读入内存当中,并更新Cache+TLB。此时总共进行了两次内存访问。

# JAVA基础

# 1、三大特性?重写和重载

三大特性:继承,封装,多态(方法重写)。JAVA单继承多实现

重写:父类方法子类重写覆盖。动态绑定,根据对象类型调用执行方法

重载:多个同名方法,按需加载形参匹配的方法,静态绑定,编译时根据形参选择方法

# 2、static和final

static:静态方法可以直接通过类名进行调用,没有this的概念,所有对象共同享有这个方法。

静态属性则是所有类对象共享一份,通过类名直接调用和读写。

final:final修饰的一切不能进行修改。final变量不能进行修改、final方法不能进行重写(相当于private)、final类不能被继承。

# 3、HashMap的put操作?容量为什么是2的整数次幂?哈希扰动函数?红黑树的优势?死循环问题?

哈希表核心结构为数组+链表+红黑树。put操作和对应扩容步骤如下:

  1. 根据key计算出当前数组索引
  2. 查看该索引桶下面,是否存在key值
    • 如果有,则直接将新的value进行覆盖
    • 没有,则需要插入新的节点,插入新的链表节点或者是红黑树节点。如果是链表需要判断是否需要转换为红黑树,同时java8采用尾插法,将整个链表按照插入顺序进行维护
  3. 判断整个数组的数量是否大于0.75倍,如果是则需要扩容为原来数组容量的两倍

# 容量2的N次方?

容器扩容以及索引计算都涉及到取模运算,因此计算时,等价为与二进制全1进行与运算。

threshold=capacity * loadFactor

# 哈希值如何计算?

扰动函数:先获取hashcode值,然后将hashcode值高16位与低16位做异或运算,从而得以融合高位信息,减少碰撞。最后再对长度取模得到数组索引值。

# 说说红黑树相比其他树优势?

哈希map用红黑树存储节点:

  • 普通二叉树:极端情况可能退化成一条链
  • 平衡二叉树:维护起来比较困难,旋转次数多

# 死循环问题?

jdk1.7在头插法+多线程+插入时,可能会构造出环形链表,导致出现死循环。

jdk1.8使用尾插法不会存在这种问题

# 4、ConcurrentHashMap底层如何实现线程安全?get方法是否上锁?

结构和HashMap一样,采用数组+链表+红黑树

同时有多线程竞争操作同一个index下的链表/红黑树时,采用Synchronized和CAS进行并发控制:

  • 当前数组index元素为null,则使用cas插入数据,防止初始同时有两个插入操作
  • 当前数组index元素不为null,则使用synchronized

get方法不会syn上锁,因为节点node是volatile修饰的,从而线程可见,防止读到脏数据。

# 5、StringBuilder和StringBuffer区分

StringBuilder效率高,但是线程不安全。后者则反之

# 6、说说类内的静态初始化块执行顺序

静态代码块——》初始代码块——》构造方法——》main方法

其中静态代码块只会在该类对象第一个创建时执行一次,第二第三个对象创建时不会再执行。

public class User{
	static {
		//静态代码块
	}
	{
		//初始代码块
	}
	public User(){
		//构造方法
	}
	public static void main(){
		//main方法
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 7、equals和hashcode的重写问题?

讨论两个相等的概念,内容相等和引用地址相等。

而上述讨论两个函数的重写问题,主要是为了保持“相等”这个概念的一致性。

因为equals和hashcode默认都是比较引用地址,如果equals需要重写成内容相等,那么hashcode也需要重写,保证内容相等。

# 8、Java异常?throw和throws

Throwable分为Error和Exception。

  • Error错误表示用户不能处理的错误,包括虚拟机异常、内存异常等
  • Exception代表可以处理的异常,包括运行时异常(空指针,数组越界),编译时异常(SQL,IO)。

throw只能在方法内部抛出异常,可以直接在方法体内处理,也可以不处理。

throws声明抛出多个异常,在方法声明抛出,表示当前方法可能会抛出该异常,调用该方法的方法需要处理该异常,或者继续声明抛出,交给父调用处理。

# 9、ThreadLocal介绍?内存泄露的问题?

每个线程维护一个ThreadLocalMap,其中key为ThreadLocal对象,value为存储的值。

ThreadLocal是线程隔离的,也就是每个线程根据同一个ThreadLocal对象,访问到的变量副本都是不一样的。

# ThreadLocal的内存泄露问题?

ThreadLocal在使用时,存在两条引用链:

  • Thread——》ThreadLocal:线程直接使用ThreadLocal对象,进行存储
  • Thread——》ThreadLocalMap——》ThreadLocal:内部实现原理,其中弱引用说的是Map持有对ThreadLocal的弱引用。

为什么是弱引用?

弱引用的特点:A持有B对象的弱引用,那么下一次GC回收时,B对象会被进行垃圾回收。

如果当前线程不使用ThreadLocal对象将其置为null,则只剩下一条map对ThreadLocal的引用:

  • 如果它是弱引用,那么它能够被自动回收
  • 如果它是强引用:那么虽然用户线程不使用ThreadLocal了,该对象依旧不会被回收,存在map的强引用链,从而内存泄漏

相反,value不能设置为弱引用,因为引用关系是thread通过threadLocal找到value,如果是弱引用则直接GC回收了,导致线程通过key找不到value。

真正的内存泄露问题?

内存溢出的本质是因为ThreadLocalMap当中,key和value强弱引用不一致导致的。当key进行回收时,剩下的value因为被map强引用,因此内存的value值在ThreadLocal不被使用后不会被回收,出现内存溢出。

因此map内部set、get、remove方法都会对key为null的entry进行清除。当且仅当没有①threadlocal置为空 ②没有调用过上述方法 才会导致内存泄漏问题。

所以程序需要调用remove方法删除ThreadLocal对象和value。

# 10、ArrayList的grow扩容过程?

添加元素时,会检查当前数组是否是满的,如果满了则需要执行grow方法扩容为原来的1.5倍。在grow方法中,会新开辟一个数组,然后将旧的数组拷贝复制过去,旧的数组在下一次gc就会被回收。

# 11、Hashmap链表插入方式

jdk1.7:头插法在多线程的条件下,可能会出现环形链表的情况导致死锁。

jdk1.8:为了解决这个问题,采用尾插法避免死锁。但是多线程下还是可能会出现数据覆盖的情况。

# 12、注解的底层原理?

定义注解:指明是否是继承哪个注解、作用范围、作用对象(注解在方法还是属性)。

所有注解都继承了Annotation这个接口,当声明好注解之后,可以通过反射,或者是AOP切面方法,拿到这个注解对象。

注解本质上是一个接口,每次我们拿到的注解对象都是代理对象;获取注解的属性值时,都会调用代理对象的属性+括号的方法。构造对象时默认参数值都存放在常量池。

# 13、反射的底层机制

java支持反射,而C++不支持反射,主要在于JVM支持根据引用,找到①堆中对象的存放地址②方法区中class类型信息。

java通过反射技术能够动态拿到类的属性、方法、构造器等信息,并在堆中生成对应的对象,包括Class对象、Method对象等,这些属性与元空间中的类信息之间都存在索引,从而提高编程灵活性。

# MySQL

# 1、MySQL四个特点?隔离级别?脏读、不可重复读、幻读?

MySQL四个特性包括:

  • A原子性:事务为不可分割的最小单位,要么同时操作成功,要么同时失败。实现原理是通过undo log,如果某个事务执行失败,数据库能够根据日志,撤销回滚整个事务的所有操作。

  • C一致性:数据库是完整的,一致的。基于其它三个特性实现。

  • I隔离性:事务之间的操作是不可见的,主要通过MVCC+视图数组实现事务隔离:①MVCC:每行数据都会有多个版本号(相当于事务ID),从而支持回滚 ②视图:每个事务开启后都会创建一个事务ID视图数组,根据当前活跃事务、已提交事务、未提交事务来指定一系列规则,从而决定当前事务能够看到哪个版本号的数据,也就是数据的可见性。隔离级别主要包括:

    • 读未提交:能够读到其它事务操作但是没有提交的数据
    • 读已提交:能够读到其它事务提交的数据,能够解决“脏读”问题。
      • 原理:每个语句执行之前,都会重新构建“视图数组”,因此已提交事务,以及当前事务的更新都是可见的。
    • 可重复读:同一个事务开启后,读到的数据都是不变的,解决“不可重复读”问题,刚提交的未提交的事务都读不到;解决了部分“幻读”
      • 原理:开启事务时,创建一个全局的视图数组快照,后续的读都会复用第一次视图数组。
      • 解决部分幻读问题:①通过MVCC解决快照读出现的幻读 ②for update添加间隙锁,能够解决当前读的部分问题,但是依然存在幻读。
    • 串行化:读写锁。同一个记录发生读写冲突后,只能有一个事务获取锁进行操作。后访问的事务必须等待当前事务的读锁或写锁释放,才能进行操作,解决“幻读”问题。
      • 幻读:同一个事务查询语句得到的记录数量不一致。
  • D持久性:提交事务后,对数据库的操作和影响是永久的。实现原理是通过redo log和bin log,保证某个数据即使因为宕机,也能进行数据恢复

    • 为了提高性能,在内存和磁盘之间加了一层Buffer pool,读数据和写数据不需要进行磁盘IO。但是如果系统宕机,Buffer pool没能及时更新磁盘,会导致数据丢失,因此引入了redo log,相当于账本,每次操作都会先记录在redo log,然后再对Buffer Pool操作。

    • redo log:记录的是每个数据页做了什么修改,文件空间用完被覆盖

    • bin log:二进制内容,记录的是SQL语句,可追加写

# 2、MySQL为什么使用B+树?

B+树存储索引,主要从三个方面进行叙述:

  • 相比于B树,矮胖树三四层可以存储上亿个节点,
  • 同时树高比较小,根据索引查询时,IO次数少,性能高
  • 子节点构成一个双向链表,更擅长范围查询

# 3、如何建立索引?如何查看是否使用索引?最左匹配原则?

如何建立索引:

  • 根据where后面的字段,多字段还可以联合索引
  • 数据表中,属于比较独特唯一的字段属性
  • 有时候,还可以基于where字段+select字段建立索引,利用索引下推

explain+查询语句,其中查看key字段优化器使用了哪个索引,extra字段可能有几种情况:

  • using index:使用索引覆盖
  • using filesort:进行排序。可能是磁盘排序,或者是内存排序
  • using temporary:使用临时表保存中间结果,常见group by
  • using join buffer:关联查询

最左匹配原则:对于查询语句where查询条件的字段,需要是联合索引字段的左边字段,才能用到索引。

# 4、聚簇索引和非聚簇索引

聚簇索引和非聚簇索引相对于叶子节点数据来说:

  • 聚簇索引:叶子节点相当于找到了数据
  • 非聚簇索引:找到叶子节点,只是相当于找了一个指针,需要根据指针进一步查找对应的数据

# 5、索引什么时候会失效

索引查询语句包含 不等于,大于,小于,不在,或,计算表达式,隐式类型转换等,则索引失效。

# 6、truncate、delete、drop之间的区别

drop:速度上最快,删除数据+表结构

truncate:删除整表数据。不需要写undo log日志,因此速度比较快,但是不能回滚

delete:删除自定义数据,可设置where条件。支持回滚

# 7、join语句

A left join B:返回A表和B表匹配的内容,对于无法匹配的行记录,保留A部分内容,B列置为空。

法则:①尽量让join on走索引 ②小表作为驱动表,因为需要全表查询;而被驱动表尽量走索引

整个过程:先查出驱动表的所有数据作为驱动表,然后每条数据会挨个与被驱动动比较;因此全表扫描需要是小表,被驱动表最好走索引。

# 8、SQL优化方案

通过explain语句查看执行情况

  • 索引:建立新的索引(根据group by后面的内容建立索引);使用force index使用指定索引
  • 语句优化:检查语句防止索引失效;limit索引优化(找到起始offset位置的数据,然后只需要where+limit count找出对应的数据,还可以通过给where添加索引)
  • 数据库:分库分表(分表:用户id作为路由;分库:根据业务进行拆分),主从复制,读写分离

# 9、慢SQL语句如何排查?

1、首先在MySQL中设置慢查询的阈值,查询时间超过这个的就算是慢查询语句。开启慢查询后会将所有慢查询语句记录在慢查询日志中。

2、分析和优化慢查询日志当中的SQL语句,通过explain分析索引、extra字段。

# 10、MySQL数据类型和选用的场景

varchar:可变长字符串。一般用于名称、uuid、描述(InnoDB中一行数据最多是65535字节,扣除长度字段2个字节保存+NULL值列表,等于35532

bigint:存储8个字节大小的整型数据。一般用于自增主键、业务相关的ID(比如订单号、报名号)

tinyint:用于一些状态字段、类型字段

datetime:时间

decimal:存储小数

# 11、执行一条语句的过程

  • 查询缓存:是否有执行过缓存的
  • 分析器:分析SQL语句是否符合语言规范
  • 优化器:生成执行策略,包括是否使用索引
  • 执行器:操作引擎,返回结果

# 12、Mysql不使用跳表、B树的原因?

1、为什么不使用跳表:跳表是多级链表+链表的结构,从磁盘IO次数来说,跳表需要开辟额外的空间存储索引节点,因此存储相同数据的B+树磁盘IO次数比跳表的次数更加少;另外,新增数据时,跳表除了直接在底层链表插入数据外,还需要根据随机函数添加不同层级的节点指针。

2、为什么不适用平衡二叉树、红黑树、B-树:B+树每个节点(也就是每个页)允许有更多的子节点,因此同样的数据量使用B+树存储,它的树高是最小的,查找每个数据的磁盘IO次数就是最小。

特别地,对于B树来说,它的非叶子节点和叶子节点都会存储数据,从而导致非叶子节点能够存储的key索引更加少,因此同等数据量下只能添加树高,磁盘IO次数增加。

# 13、redolog与binlog区别?两阶段提交是什么?

redolog:循环写,记录的是当前数据页做了哪些修改,写满之后需要将数据从数据库读到内存,应用redolog刷盘同步,刷盘后的数据会从redolog删除。常用于恢复数据。

binlog:追加写,记录的是SQL语句的逻辑操作,是一个二进制文件,常用于①保存数据库的历史记录②高可用主备同步。

两阶段提交流程:

  • redolog提交进入prepare阶段
  • 写binlog
  • redolog提交commit阶段

两阶段提交是为了保证redolog与binlog内容一致;而redolog具有crash-safe的能力,是因为binlog不能感知哪些数据已经完成刷盘,而redolog天然就记录了那些没有刷盘的数据。

根据宕机的时刻可以分为以下几种情况,核心保证redolog和binlog一致性:

  • 一阶段宕机:直接根据redolog中的事务id回滚
  • 二阶段宕机:校验binlog完整性:
    • 如果binlog不完整,则直接根据redolog的事务id进行回滚
    • 如果binlog完整,根据binlog恢复redolog日志

输盘时机:

  • redolog写满了之后提交

  • 双一配置:每次事务提交后,都会将内存的redolog和binlog进行刷盘,至少两次磁盘IO次数。

  • 因此后续提出了组提交,多个事务都合并为一次进行日志刷盘。

# 14、MySQL的刷盘机制?

MySQL刷盘主要分为两种:

  • 数据刷盘:将Buffer Pool当中的脏页刷盘持久化,其中触发刷盘的时机①redolog写满了 ②buffer pool写满需要内存淘汰。其中可以修改两个参数,一个是刷盘速度,另一个是脏页连带数量。
  • 日志刷盘:两阶段提交。

# JUC并发

# 1、volatile保持内存可见性

volatile修饰的变量多线程可见,核心机制:

  • 底层防止指令重排:保证代码按序执行。单线程没有问题,多线程数据依赖问题可能会出错
  • 内存屏障:保障不同线程见到的变量都是最新的变量值,内存可见性。底层通过嗅探技术,保证线程内存和系统内存的变量值都是一致的,一旦某个线程改了这个值,那么其它线程内存的副本会直接失效。

# 2、synchronized底层实现?如何获取锁?锁升级

实现:通过monitor监视对象实现。

正常获取锁流程(轻量级锁):每个对象的对象头中mark word存放锁的状态信息。当线程A获取该对象的锁时,会进行两个操作:

  • 复制对象头的mark word字段到当前线程栈帧
  • 通过CAS,将对象的mark word改为指向当前线程栈帧的指针
  • 底层是通过monitor对象实现,每个锁都对应一个monitor对象,线程进入syn时需要竞争获取这个monitor对象,本质上就是判断monitor对象的owner字段是不是当前线程。

锁升级分为如下几个步骤:

  • 偏向锁:锁偏向于第一个获取该对象的线程,同一个线程多次访问不需要重复CAS,直接保存线程ID
  • 轻量级锁:同上采用CAS机制,其它锁竞争时会自旋轮询CAS,响应时间快。释放锁时,如果CAS失败说明同时有其它线程竞争锁,锁升级为重量级锁。
  • 重量级锁:其它线程竞争锁失败,直接挂起,直到线程释放重量级锁,唤醒通知其它所有线程。吞吐量高,但是需要线程上下文切换,响应时间低。

# 3、谈谈ReentrantLock?和Synchronized对比?

ReentrantLock特性:

  • 可重入锁:拿到锁的线程能够重复进入公共代码段执行,能够再次调用lock方法进入同步块。实现方法是在tryAcquire中,通过API判断当前线程是否是获取锁的线程,如果是则tryLock成功,计数器加一,否则失败。
  • 排他性:同一时刻只能有一个线程获取锁。
  • 公平或非公平:获取锁的顺序与线程请求tryLock的顺序保持一致,默认非公平锁实现。
    • 公平:判断AQS同步队列当中,当前节点是否有前驱节点。如果是首节点才能CAS
    • 非公平:加入同步队列后,直接进行CAS,因此后加入的节点也可能CAS成功,也就是非公平
    • 非公平锁性能高于公平锁的地方在于,非公平锁线程挂起和唤醒的开销要小于公平锁。

两者有如下不同:

  • Reen需要显示编程获取锁和释放锁,而Syn隐式获取锁,线程执行完同步块后自动释放锁
  • Reen只能在代码块内使用,lock方法;而synchronized可以加在代码块,方法上。
  • 两者都是可重入锁,前者是计数器机制,后者是偏向锁机制。
  • Reen是公平或非公平。而Syn是非公平锁
  • Reen是通过AQS同步器实现,而Synchronized通过JVM监视器实现

# 4、线程池参数?核心线程数设置?

七大线程池参数:

  • 核心线程数
  • 最大线程数
  • 阻塞队列类型:优先级队列(先来后到,优先级大小)、有界队列(限定阻塞队列的大小,很多阻塞队列因为高并发场景,造成任务堆积,如果是无界则内存溢出,系统不可用)
  • 拒绝策略:①丢弃当前任务 还是 丢弃队列中的任务 ②是否抛出异常
  • 存活时间:空闲线程不接到任务,经过多久会自行释放。核心线程数不释放
  • 时间单位
  • 线程工厂

整个流程如下:

  1. 如果当前线程数小于核心线程数,则直接创建(获取全局锁)
  2. 大于核心线程数,则先加入阻塞队列
  3. 如果阻塞队列任务满了,则创建新的线程(获取全局锁),从队列取出任务,并执行
  4. 当前线程数超过最大线程数,则直接拒绝任务

如果CPU核心数是N,则根据不同任务类型设置核心线程数:

  • 计算型任务:CPU一直在运行,直接设置为N+1
  • IO任务:线程需要等待数据,一个线程可能会占据大量时间,为了充分利用CPU资源,设置为2N+1
  • 混合型任务:如果分成的两个IO和计算时间相当,可以分别设置两个线程池处理对应的任务。

# 5、创建线程的方法?

定义任务:①实现Runnable接口方法 ②实现Callable方法,任务方法需要有返回类型

通过new Thread创建一个新的线程,然后传入任务,调用线程start方法线程进入运行态

# 6、obj.wait、thread.join、thread.sleep的区别?

obj.wait:事件通知机制。需要使用一个中间对象obj来实现线程之间的通知机制,

thread.join:当前线程调用threadb.join方法后,当前线程会被阻塞,直到b线程终止,才会唤醒当前线程

thread.sleep:线程进入等待状态,隔某段时间后自动唤醒

# 7、说说CAS思想?缺点是什么?如何解决?

CAS:比较和交换。通过版本号的思想,对一个共享变量进行操作和维护。

缺点如下:

  • ABA问题,java底层提供并发包,解决版本号问题
  • 多个变量的共享读写操作,不能通过CAS,需要通过锁
  • 高并发条件下,CAS容易失败,系统轮询开销大

# 8、什么时候使用CAS和悲观锁?

低并发情况下使用CAS,提高并发度。

高并发情况下使用悲观锁,因为CAS轮询开销比较大。

# JVM内存

# 1、JVM内存分配?一个对象从创建到GC的整个过程?

内存主要分为几个区域:

  • 方法区:存放类的静态变量,静态代码块,常量池。
  • 堆:存放对象实例。这个区域是共享的
  • 线程栈:线程私有的,存放执行方法的栈帧,本地变量
  • 方法栈:执行外部native方法
  • 程序计数器:线程隔离

对象从创建到消亡整个流程:

  • 执行类加载过程
  • 进行JVM堆内存分配:
    1. 对象加入Eden区,如果对象过大,则直接加入老年代Old区。(减少Eden到S区之间对象拷贝的开销)
    2. Eden区满了,进行一次Minor GC,把对象拷贝到S区,然后计数器加一。每进行一次Minor GC都会加一,直到到达15,则将对象从S区加入到Old代
    3. 直到Full GC
  • 在堆中,初始化对象的属性字段为零值
  • 栈帧指向堆内存空间

# 2、双亲委派机制?如何破坏?

双亲委派机制核心如下:

  • 自底向上委托:由子类向父类自底向上委托父类的loadclass方法处理
  • 自顶向下加载:再自顶向下负责加载该类,如果当前类加载器不能加载,则交给子类加载器

主要分为客户自定义类加载器-》应用类加载器-》拓展类加载器-》启动类加载器。双亲委派机制的意义在于,能够保证代码安全,特别是核心代码实现,防止核心代码被篡改。

# 双亲委派机制破坏?

①破坏类加载器:自定义类加载器,直接重写类方法loadClass

②Tomcat进行类加载时,绕开了Application应用加载器,直接交给ext进行加载。

③SPI机制以及JDBC:因为外部的实现类在第三方jar包指定的路径下,而这只能由应用类加载器负责,启动类委托应用类加载器则会导致整个委托关系倒置,因此JDK进行妥协,搞了一个上下文类加载器

# 3、如何判断一个对象是不是垃圾?

存活算法如下:

  • 引用计数法:每个对象如果被栈帧应用,则引用计数加一,否则减一。该方法不能解决循环依赖问题,如果两个对象相互引用,那么引用计数永远不会变为零,导致两个垃圾对象不能被标记
  • 根可达性算法:根据GC ROOT向下搜索每个对象的引用关系,如果某一个对象根据GC ROOT的搜索链找不到该对象,那么判定为垃圾对象。(GC ROOT包括虚拟机栈,方法区的静态块,常量池)

# 4、垃圾回收算法有哪些?

常见三种垃圾回收算法:

  • 标记清除法:将垃圾对象标记出来,然后单独对标记的内存空间进行GC回收。缺点在于存在内存碎片。
  • 标记压缩法:将存活对象统一移动到JVM的一端,然后可以直接清除回收其它的空间。
  • 内存复制法:整个JVM只使用一般的内存空间创建对象,垃圾回收时,将一半内存当中存活的对象复制到另一半的内存(复制的存活对象在另一半内存空间可以是连续的),然后直接回收整个一半的内存空间。缺点在于浪费空间,且效率低。

其中对于新生代和老年代而言:

  • 新生代使用复制算法更优:①频繁GC,需要减少内碎。②每次存活对象比较少,复制代价低。
  • 老年代使用标记算法更优:①复制代价比较大 ②每次死亡对象比较少

# 5、垃圾回收器有哪些?

垃圾回收器:控制GC线程进行垃圾回收,减少对JVM正在运行线程的干预。减少STW状态

区分新生代老年代的垃圾回收器:

  • 串行化垃圾回收器:只用一个GC线程,执行垃圾回收工作。缺点在于,垃圾回收效率比较低

  • 并行垃圾回收器:使用多个GC线程,执行垃圾回收算法。

  • 上面两种垃圾回收器,在GC线程在执行垃圾回收时,都需要进入STW状态冻结其它JVM正在运行线程,因此后续有提出了CMS:

  • CMS垃圾回收器:核心思路是让GC线程与用户线程执行并行执行。具体做法分为一下几个步骤:

    1. STW:标记出GC ROOT直接关联对象
    2. 并行阶段,所有被标记的对象执行根可达算法,找出所有垃圾对象
    3. STW:重新标记出上一个阶段当中,工作线程新产生的垃圾对象
    4. 并行回收所有垃圾对象(缺点因为是并行执行垃圾回收,该过程产生的浮动垃圾对象只能交给下一个阶段回收)

不区分新生代老年代的垃圾回收器:

  • G1:将整个内存分块,每次GC回收“垃圾对象”最多的区域

# 6、什么是三色标记法?如何解决错标和漏标?

三色标记法:用于解决CMS垃圾回收器的标记算法。包括黑(对象+子属性均标记完)、灰(对象标记完,子属性没有标记)、白(都没有标记)。标记完毕后,如果还是白色,说明是垃圾对象。

漏标记:正常被引用的对象,没有被标记导致被垃圾对象清除。解决:二次STW重新标记阶段

# 7、minor GC

每次执行根可达算法,将S0区的存活对象复制到S1区当中,回收S0区的垃圾对象。下一次Minor GC则会来回执行这个过程。

# 8、类加载流程

  • 加载:将类的二进制字节流文件加载进虚拟机,转换成数据结构,通过类加载器
  • 验证:校验字节流文件是否符合规范
  • 准备:为类变量,也就是静态变量,在方法区中开辟空间并设置初始零值
  • 解析:将符号引用转化为直接引用;执行的时候找到类变量和方法的偏移量,从而能够调用找到对象的位置。
  • 初始化:初始化类变量

# 9、对象什么时候跑到堆外面?

经过对象的逃逸分析之后,如果判断对象没有发生逃逸(全局逃逸,参数逃逸),对象的应用没有跑到静态变量、方法返回值,那么就可能采用栈上分配进行优化。

对象分配到栈上。随着栈帧出栈,方法结束会将对象进行销毁。

# 10、CMS垃圾回收器?G1垃圾回收器?ZGC垃圾回收器?使用场景

CMS:concurrent mark sweep 并发标记清理,CMS属于老年代的垃圾回收器,需要配合新生代的垃圾回收器使用。

  • 初始阶段:暂停所有线程,标记GC root相关节点
  • 并发标记:GC线程执行根可达算法,标记出所有可达对象
  • 重新标记:暂停所有线程,重新标记出并行阶段新产生的垃圾对象
  • 并发清除:执行垃圾回收算法。可能会出现浮动垃圾

G1:Garbage-First ,适用于服务器的垃圾回收器,适用于产生内碎比较多、需要可控的GC停顿时间。采用标记—复制算法。

  • 时间预测模型,并在后台维护了一个优先列表,每次根据给定时间,选择价值最高的region进行回收
  • 整体上基于标记-压缩算法,较少内存的内碎。

ZGC:适用于低延迟,对响应时间要求比较高的场景,比如证券系统。采用标记--复制算法,STW阶段停顿时间非常低,大多数阶段与用户线程并发执行。采用染色指针和读屏障实现并发标记、并发转移阶段(G1的转移阶段是完全STW的)。

# 11、jdk1.8默认使用什么垃圾回收器?

新生代和老年代默认都使用并行垃圾回收器。jdk1.8优先选择“吞吐量”,也就是处理GC的时间越短越好;而CMS则追求“响应时间”,选择GC程序与用户程序并发执行。

# Redis

# 1、说说Redis、Memcache、Guava/Caffeine

Redis:分布式缓存:支持多种数据结构,包括跳表、zset、位图;支持持久化、集群部署

Memcache:分布式缓存数据库;存在内存;仅支持存储key-value数据;不支持持久化

Guava/Caffeine:本地缓存,多个节点使用一份缓存数据;占用服务器的堆内存;当微服务水平部署多个实例时,对应也要创建新的缓存,因此不具有一致性;具有定时释放、缓存过期、淘汰机制

# 2、Redis数据类型有哪些?SDS和跳表?

Redis数据结构如下:

  • String

  • Hash哈希表:适合存储对象数据,如购物车

  • List列表:按插入顺序进行排序

  • Set集合:数据不可重复,适合进行集合操作比如并集、交集等;比如朋友关系

  • Zset集合:有序集合,每个元素会设置一个分数;如排名

  • HyperLogLog

  • Bitmap:位图,可以用于统计用户签到:key为某天日期,offset为对应用户id(第几个用户),因此根据key拿到的二进制非零位数为对应当天活跃用户数量;然后某段时间则取出连续key的结果进行与操作;另外,反过来,位图的长度可以代表用户的长度,key代表用户id,offset代表第几天,第几天打卡则置为1

动态字符串SDS:

  • 字符串内存分配采用预分配和懒删除:分配空间时多分配几个连续空间地址,这样下次分配的时候就不用重新分配;删除时只会进行标记,并不会实际释放对应的内存空间,(这样下次使用的时候就不用重新分配)
  • 空间长度大小:直接记录length值,O(1)获取

跳表:

  • 不同层次的链表的长度不同,最底层链表的长度最长,跨度最低;每层的链表都是有序的;节点除了指向当前层次的下一个节点外,还会有一个指针指向更低层次链表的指针。
  • 跨度越高,链表节点越少;如果当前查询节点大小在当前节点和下一个节点之间,那么则进入下一个更低层次的链表进行搜索。时间是logn

# 3、Redis持久化机制?

Redis持久化分为两种:

  • RDB:每隔一段时间持久化当前Redis数据。适合用于备份数据;但是可能会丢失数据;通过bgsave会fork一个子进程,并不会阻塞主线程
  • AOF:追加写,文件较大,因此启动时耗时较长;每执行一次指令都会追加写AOF文件;适合用于数据恢复;不同于redo log,AOF是执行完指令之后才进行复制重写,不会阻塞指令的执行,但是可能会造成数据丢失

# 4、Redis内存淘汰策略?过期策略?

内存淘汰策略:LRU,LFU,FIFO

过期策略:

  • 周期删除:定期删除缓存的过期key
  • 惰性删除:过期了不删除,仅在下一次使用时判断是否过期再进行删除。因此如果过期并且不再使用,则导致内存空间浪费

# 5、缓存穿透、缓存击穿、缓存雪崩?对应解决方法?

Redis生产问题(保证高可用):

  • 缓存穿透
    • 问题:内存和数据库都不存在对应数据,请求打到数据库造成崩溃。
    • 解决:①缓存无效数据,key-null,并设置对应TTL过期时间 ②设置布隆过滤器,本质上是通过哈希映射+数组,如果数组不存在则说明一定不存在缓存或者数据库;如果布隆数组存在则说明可能存在也可能不存在,取决于哈希算法和数组长度。
  • 缓存击穿
    • 问题:大量热点key失效,请求打穿redis直达数据库。
    • 解决:①热点key设置较长过期时间,或者是不设置过期时间。②缓存预热,提前将热点数据通过定时任务加到缓存 ③数据库访问设置互斥锁,并设置写回策略。
  • 缓存雪崩
    • 问题:大量key同时失效
    • 解决:①每个不同的key设置不同随机的过期时间 ②服务降级限流

# 6、热点key问题?

热点key:同时有大量请求线程打到redis的热点key上,可能成为redis的系统瓶颈。占用系统带宽

解决方案:

  • 建立集群,将热点key打散到多个redis节点当中
  • 设置热点key多级缓存,本地缓存>>redis>>数据库
  • 读写分离:建立主从redis节点,同一个热点key进行读写操作分离

# 7、大key问题?

大key问题:redis缓存的key占用内存空间过大,导致对key操作时比较影响系统性能。

解决方案:

  • 重新选择redis合适的数据类型和数据结构
  • 把大key拆分成多个小key分别存储

# 8、如何保证数据库和缓存一致性?

对于Redis缓存,我的理解是,它的引入主要是为了解决AP问题,帮数据库分担流量,保证系统可用,因此客观上来说,缓存和数据库出现不一致的问题是无法避免的。

而造成缓存和数据库不一致的问题,本质上是因为系统对数据库和缓存读写操作并不是原子性的,同时多线程环境下程序执行顺序不可控,比较常用的一种读写方案如下:

  • 更新:先更新数据库,然后再删除缓存。对于删除数据操作失败导致的脏读,如果要求强一致性,那么可以将这两个操作放在一个事务中。
  • 读操作:先读缓存,然后再读数据库;如果缓存没有,需要从数据库将数据更新到缓存,同时设置过期时间。

无论通过什么方法,我们都可以通过其它一些手段尽可能保证操作流程执行成功,包括:设置兜底逻辑、定时任务重试机制、

# 9、Redis的主从模式、哨兵模式?

主从模式:一主多从。主节点负责写操作,同时将节点信息更新到其它从节点,从节点负责读操作。

哨兵模式:在主从模式下,主节点因为数据同步压力大节点宕机,此时哨兵节点会执行如下操作:

  • 判断主节点是否下线:监听主节点的哨兵节点会定时发送ping指令,利用心跳检测判断节点是否存活;其它哨兵节点会向监控该节点的哨兵节点进行确认(因为也有可能是哨兵节点宕机了),从而最终判断主节点是否真的下线。
  • 监视这个主服务的所有哨兵节点,会执行Raft算法,选举出一个新的leader 哨兵节点:
    • 初始化每个节点都是跟随者节点,初始化Term计时器置为0
    • 某个节点超时后,会成为候选节点,首先给自己投一票,并向其它节点发送投票申请
    • 其它追随者节点会向发送第一个请求的候选者投票,获得超过半数支持的候选者会成为领导者。
  • 哨兵leader,会根据优先级、复制偏移量等规则,从从节点当中选出主节点。

集群模式:水平拓展;数据分片

# 10、基于Redis能够做什么?

  • 内存数据库
  • 发布通知订阅
  • 分布式锁:①使用setnx ②使用redisson
    • SETNX:设置一个key和超时释放时间。不存在则设置成功,并返回1;若已存在则设置失败(获取锁失败),返回0。整个查看key以及判断key的操作都是原子的。

# 11、谈谈Redis的单线程模型?

Redis里面说的单线程模型,指的是接受连接-》解析事件-》事件执行-》响应 这条链路是单线程的,而其它模块,包括持久化、连接关闭等都是另外启一个线程执行。

而Redis单线程快的原因,主要是两点:

  • 基于内存操作,所以肯定很快。CPU不是瓶颈,因此采取单线程。
  • 它是利用IO多路复用,处理客户端连接时,不会阻塞服务端线程;避免使用多线程模型导致的线程上下文切换。

具体来说,只创建一个服务端线程(相当于一个IO多路复用程序),负责监听多个套接字的连接请求和数据请求。当套接字的数据准备好后,会轮询执行每个套接字,比如问套接字1好了没,好了就丢给事件分发器,分别处理对应的读事件或者是写事件,然后再问第二个套接字,以此类推。

IO多路复用程序分为以下几种:

  • select:只能监听1024个套接字
  • poll:可以监听任意数量的套接字
  • epoll:监听任意数量套接字,并且轮询时只会执行准备好的套接字

# 12、redis如何设置持久化模式

在redis.conf这个文件中进行配置

# 13、redis刷新策略

1.内存刷新策略:fifo,lru,lfu

2.过期刷新策略:周期式,懒汉式

3.数据库缓存不一致导致的主动刷新:先改数据库,然后删除缓存

# 14、redisson如何加锁保证原子性?

redisson实现分布式锁,主要是通过redis+lua脚本实现,封装了加锁的API,通过lua脚本获取锁,看门狗机制保证不会死锁。

# 15、setnx有哪些风险

1、redis主从同步时,主节点中A用户拿到锁之后突然宕机,此时锁占有没来及同步给从节点,导致从节点变为主节点后,其它的节点重新竞争锁。

2、setnx+expire实现分布式锁时,如果过期时间设置过短,当前业务流程执行时间长,就可能导致当前A线程还没执行完自己的业务,分布式锁就自动释放。B线程获取锁执行时,A线程执行完后误删了B线程的锁,从而导致不可控的并发问题。解决方法:通过redisson的看门狗机制,观察如果客户端线程还持有锁,则续上超时时间。

# 16、redis一致性hash

一致性hash算法用来解决分布式场景下数据存储的问题。

  • 索引计算:不同于直接对机器数量取模,索引计算直接对2^32-1取模。
  • 哈希环:key-value和机器都会进行索引计算,映射到哈希环上的某个位置
  • 每个节点顺序存储在顺时针方向第一个机器节点
  • 容错性和数据倾斜:
    • 节点机器数量改变时,只会影响到部分节点数据
    • 通过虚拟节点解决数据倾斜的问题(大部分数据放到一个节点),同一个机器节点计算多个虚拟节点放入哈希环当中,实际数据存储的位置为虚拟节点对应的物理节点。

实际redis集群没有使用,而是采用哈希槽。一致性hash可以在客户端实现。

# 消息队列

# 1、介绍kafka中的topic、partition、replica?

除了正常生产者和消费者外,kafka如下概念:

  • broker:相当于kafka消息队列实例,多个broker组成集群
  • topic:订阅者监听的通道
  • partition分区:每个topic下都会有多个分区,相当于队列的概念。其中多个分区不一定都在同一个broker,因此多分区可以提供负载均衡功能,监听同一个topic的多个消费者可以负载到多个不同的kafka节点(broker)进行消费。
  • replica副本:每个分区都会有多个副本,副本分为一个leader多个follower。多个副本相当于提供了消息的高可用机制,某个分区节点的消息丢失,可以通过该分区的其它follower进行复制。

# 2、kafka与zookeeper之间的关系

zookeeper相当于一个注册中心,它为kafka提供以下服务:

  • broker消息队列节点注册
  • topic注册
  • 分区注册
  • 负载均衡

# 3、kafka如何保证消息的顺序消费?

在kafka中,同一个队列(partition分区)的消息天然保证顺序性,通过offset维护。

因此实现消息顺序消费的关键在于,保证生产者生产的消息能够顺序发送到同一个分区中:

  • 每个topic只设定一个分区
  • 生产者发送消息时,指定topic的同时,还需要指定partition

# 4、kafka如何保证消息不会丢失?

保证消息不丢失从三个方面考虑,以kafka为例:

  • 发送端:生产者发送消息后,设置回调函数,当broker收到消息之后触发回调函数,才能确保消息发送到kafka
  • kafka端:broker节点可能会突然宕机,导致消息没能持久化。解决办法:利用副本replica“集群”的特点,设置多组副本。(牺牲性能换取安全性);设置acks=all,确保所有副本都受到消息之后,才触发生产者的回调。
  • 消费者端:关闭自动提交offset给broker,仅当消费者处理完消息之后,才会手动提交offset。另一种方法是,消费者收到消息之后,马上提交offset,然后通过异步+重试的方法消费消息。

# 5、kafka如何保证消息的幂等性消费?

破坏幂等性的原因只要在于,offset没能提交,因此主要解决方案:

  • 消费者消息利用mysql主键、redis的key 这些天然幂等性的属性进行校验
  • 拿到消息后马上提交offset,然后异步做数据兜底。

# 6、谈谈死信队列

kafka内部支持重试机制,消息消费失败默认重复消费,超过重试次数后,会将消息放到死信队列进行消费。可以基于死信队列实现:

  • 信息延迟消费。刻意让消息执行失败,超时进入死信队列。

# 7、如何处理消息积压问题?

从两个方面进行考虑:

  • broker段:在消费者数量足够多的情况下,此时消费瓶颈在broker,需要拓展topic分发消息。可以创建一个新的临时topic,启动一个程序,将topic下的挤压消息分发到临时topic,
  • 消费端:起N台机器进行消费者服务,提高消息的消费效率

# 8、kafka为什么这么快?

问题可以从以下几个方面来回答:

  • 集群部署:kafka可以水平拓展部署,同一份数据可以存储在多个broker的分区下
  • 支持批量处理和异步消费:多个消息可以放入同一个分区当中;同时消费时提交offset可以异步进行,先交偏移量然后再消费消息
  • kafka零拷贝技术:减少数据在内存和磁盘的复制次数
  • 注册中心:因为是多个分区,因此注册中心可以采用负载均衡

# Spring

# 1、Spring说说IOC,整个Bean的生命周期?

整个IOC容器的创建过程中,核心方法是refresh方法,包括如下:

  • 解析XML文件,创建Bean定义对象
  • 执行BeanFactoryPostProcessor拓展,修改Bean定义对象的属性值。包括①普通字符串类型②对应的Reference依赖对象类型
  • 注册BeanPostProcessor
  • 初始化事件发布者publisher
  • 所有Bean对象预加载:取出所有BeanDefinitionMap中的对象,根据bean定义信息创建对象。

其中创建Bean的生命周期doCreateBean如下:

  • 实例化:通过反射拿到类构造器,newInstance
  • 属性填充:主要根据setField反射进行
    • 普通属性
    • 注解的容器对象、@Autowired(通过getBean方法创建对象)、@value对象(解析资源文件)
  • 初始化:initializeBean方法,其中会拓展执行BeanPostProcessor前置方法和后置方法
    • 前置方法:Aware对象注入
    • 执行①InitializingBean的afterPropertySet方法拓展点 ②初始化方法
    • 后置方法:AOP代理对象创建

# 2、Spring的Aware依赖倒置?

用户业务代码逻辑需要拓展,使用IOC容器当中的容器对象,包括:整个容器对象、Bean对象名称、上下文。流程如下:

  • 用户实现对应的aware接口
  • IOC容器会在初始化Bean的时候,将当前容器的上下文对象、Bean工厂等对象,调用set方法将对象传给用户自定义的业务类

# 3、Spring的AOP切面实现?

核心都是通过instanceOf、isAssignableFrom方法,判断bean对象是不是实现了某个接口,或者是继承某个父类。

AOP核心是通过代理对象实现:代理对象并非所有方法都设置了切面方法,通过intercept方法拦截到方法执行时,判断当前方法是否用户自定义拦截方法和拦截类型,如果是则进行前置后置处理增强(织入切面方法)。

将代理对象织入Bean生命周期时,在BeanPostProcessor后置增强当中,如果对象是代理对象,则创建一个代理对象,并加入IOC进行管理。

  • 整个IOC中,创建的Bean只能是普通对象,或者是代理对象
  • 对象某个方法织入了AOP切面,那么只能保存代理对象。因为通过代理对象执行的方法会被拦截

# 4、Spring 如何解决循环依赖问题?

首先对于普通对象之间的循环依赖,二级缓存已经能够解决。三级缓存解决的是需要AOP代理对象的循环依赖问题。各级缓存设置如下:

  • 一级缓存:存放成品对象
  • 二级缓存:存放半成品对象,以及对应的成品对象。这里实际上是一个动态的过程
  • 三级缓存:存放的是函数式接口。所谓暴露指的是,可以向缓存存入一个lambda表达式,当getObject方法调用的时候,才会真正执行lambda表达式,构造代理对象。

本质上就是在B属性注入时,能够利用三级缓存触发A代理对象的提前创建。而只使用二级缓存,只能显式提前创建AOP代理对象,这会破坏Bean的生命周期,因此需要使用三级缓存。

  1. A对象实例化——》注入三级缓存,提供代理对象创建的一个入口——》属性填充B对象
  2. B对象实例化——》加入三级缓存,也有一个——》属性填充A对象
  3. 触发A对象的代理创建,放入二级缓存
  4. B对象创建完毕,触发自己的三级缓存的创建过程——》放入二级缓存——》B创建完毕,放入一级缓存
  5. A对象创建结束——》加入一级缓存

# 5、Spring的 Event事件机制—观察者模式?

观察模式分为三个部分:

  • 推送者:负责将事件推送给监听者,起桥梁的作用
  • 事件event:自定义事件
  • 监听者listener:当监听的事件触发之后,自动执行业务代码,核心实现是自定义的监听者接口中,声明时指定了监听的事件作为泛型。

发生事件时,具体流程如下:

  • 调用publisher推送者发送事件,会将该事件广播给监听者。
  • 遍历所有监听者缓存,如果当前事件和监听者泛型属于同一个类别(或者是子类),则说明当前监听者匹配,调用执行监听者的onxxEvent,执行用户自定义的业务逻辑。

# 6、Spring的自动扫描@component?那么@Autowired呢?

包扫描路径+@component:本质上是通过扫描路径,拿到所有注解的类名className,然后构建Bean定义信息

@Autowired:本质上是在属性填充阶段,获取到所有注解的beanName,通过递归调用getBean方法创建出自动分配的对象,然后反射setField填充给Bean

# 7、Spring当中的FactoryBean和BeanFactory有什么区别?

BeanFactory相当于一个产生Bean的工厂,包含所有Bean定义信息,可以BeanFactory创建Bean对象

FactoryBean提供用户自定义创建Bean的接口,创建的Bean不会经过复杂的生命周期流程。

# 8、Spring的设计模式有哪些

单例模式:Bean默认都是单例的

模板模式:最常见,抽象类定义模板方法交给子类实现;getBean方法

工厂模式:BeanFactory创建Bean工厂

策略模式:xmlBeanDefinitionReader、PropertyBeanDefinitionReader。 Bean定义信息读取对象

代理模式:AOP的实现就是通过代理模式实现

观察者模式:multicast、event、listener

# Netty

# 1、谈谈BIO、NIO、AIO

正常数据需要经过几个传输阶段:网卡、内核空间、用户空间、用户程序。

BIO:用户程序请求数据,如果没有数据,会被阻塞在网卡这里

NIO:用户请求时,如果网卡没有准备好数据,将数据拷贝到内核空间,那么不会阻塞直接返回。因此可以用IO多路复用程序,提高资源利用率。但是,数据拷贝到内核空间和用户空间这段时间是同步的,直到拷贝到程序都会被阻塞。

AIO:数据准备,以及数据拷贝都是异步的。

select和poll通过代理的方式,当”数据准备好“才会通知,轮询所有套接字。epoll则是只会轮询处理数据准备好的套接字。

# 2、Reactor模型

事件驱动。所有请求分为两个核心操作:连接操作和处理操作。

单线程模型:只有一个线程负责客户端连接和处理操作。一个线程在连接时,另外一个不能进行处理

多线程模型:一个线程负责连接操作,多个线程负责处理请求

主从线程:多个线程分别处理连接操作和处理操作。

在Netty当中,boss线程就是负责连接操作,而worker线程负责处理操作

# 3、Netty和NIO之间的区别

NIO编程灵活性差,没有处理半包、粘包、重连的问题。

Netty内部默认支持多种协议格式,实现对应的编码器和解码器,同时可以自定义解决半包和粘包问题。另外Netty经过许多优秀开源组件的考验,包括Dubbo、RocketMQ等等,性能强大。

# 4、半包和粘包问题,如何解决?

粘包:指的是当前数据包包含下一个数据包的内容

半包:当前数据包的数据不完整

产生半包和粘包问题根本在于,双方协议没有指定对应的数据包开始符号、结束符号。需要自定义结束符、数据包大小。Netty内部默认实现。

# 5、Netty零拷贝

零拷贝指的是,数据不需要从一个存储区域拷贝到另一个存储区域。比如从硬件设备拷贝到内核空间、从内核空间拷贝到用户空间。

其中Netty零拷贝主要是节省了用户空间的拷贝:

  • composition:ByteBuf之间如果需要合并,比如head和body,那么传统做法是需要开辟一个空间,然后分别将两个ByteBuf复制到这个合并的空间当中。而Netty可以直接合成,虽然物理上内存不是连续的,通过改变readIndex读指针来实现逻辑上是连续。
  • splice:同一块内存空间支持分割成多个部分,不需要重新开辟空间进行拷贝。每个ByteBuf都会维护自己的读索引和写索引。
  • transferTo:通过地址映射,内核空间的数据不需要拷贝到用户空间。

# 6、Netty为什么快

1、Netty基于NIO和IO多路复用,能够处理大量并发连接

2、Netty内部零拷贝机制(组合composition+splice+transferto)

# 项目一

# 1、抽奖项目设计模式

项目中使用到的设计模式如下:

  • 工厂模式:奖品分发工厂。根据奖品编号,从工厂Map当中取出对应的奖品对象。
  • 策略模式:抽奖策略,根据不同的策略类型,调用具体对象的抽象算法的具体实现。分为总体概率、单项概率:
    • 总体概率:抽到奖品的概率,会根据奖池的商品总数、当前奖品数量动态改变。Random类生成一个随机数之后,看抽到数落在哪个奖品概率区间,其中每个奖品的区间长度等于奖品概率
    • 单项概率:抽到奖品的概率固定,如果抽到的奖品数量变为零,则返回没抽到。这里可以通过令牌桶将算法时间复杂度简化O(1),具体来说,创建一个大小为100的数组,然后商品概率乘上100记为n,将n个数组的值设置成该商品的ID。生成的随机数直接根据string[random]获取抽到的奖品编号
  • 模板模式:定义抽奖流程。抽象类定义整个流程的编排,然后子类实现具体流程。doDrawExec方法定义抽奖流程:获取抽奖策略、查询商品数量、执行抽奖过程、封装抽奖结果。
  • 状态模式:定义几个状态对象,在每个状态对象内部定义流转到其它对象的逻辑,用户在外部传入一个状态接口。抽奖活动状态的流转,包括编辑、提审、通过、拒绝、撤审、运行、关闭

# 2、数据库路由组件

组件分为三部分实现:

  • 通过Spring AOP切面,在需要分库分表的SQL方法执行时,获取“分表”的key字段,拿到形参的值,根据形参计算分库分表的哈希值
  • 实现分表:通过mybatis@interceptor拦截器,拦截到statementHandler方法的prepare方法,拿到对应SQL语句,通过正则、反射修改SQL语句的表名,实现分表路由。
  • 实现分库:创建一个新的数据源,并重写determineCurrentLookupKey方法,决定数据源key。每次执行SQL语句getConnnection时,都会调用该方法。因此可以在里面替换数据库名路由。因为是ThreadLocal,因此每个线程的路由都是不同。
  • 获取SpringBoot配置的路由信息:通过EnvironmentAware获取对应的配置对象,从而拿到配置参数。

# 3、谈谈规则引擎设计的意义是什么?如何实现的?

在抽奖过程中,用户可以自由抽奖,可以点击官方推荐的按钮报名活动进行抽奖。而抽奖引擎就是用于官方控制成本、精细化运营。比如某个活动A,限制只有年龄大于30、性别女、购物金额达到多少的用户才能参加。

实现方式采用基于组合模式的决策树实现,非叶子节点表示决策的属性,叶子节点表示最终决策的活动ID。举例来说,比如当前节点属性是性别,如果是男则走左子树,女则走右子树。

  • 数据库存储两个表,一个是节点表,每个节点存储决策属性、节点ID、属性值;另一个是边表,存储父节点ID、从节点ID、表达式(大于、小于)、比较值。
  • 实现决策时,首先根据数据库表封装每个节点的聚合对象,包括当前非叶子节点属性+所有边节点的集合。

# 4、介绍一下整个抽奖活动的主链路?

整个抽奖链路包括五个部分:

  1. 报名活动
    • 校验活动:包括判断刷单、活动过期、用户没有抽奖次数
    • redis活动数量减一
    • 落库报名记录:添加活动报名记录,扣减个人活动次数
  2. 异步扣减活动数量
  3. 执行抽奖算法
    • 获取抽奖策略,以及产品中奖概率
    • 生成随机数,判断落在哪个产品区间
  4. 封装抽奖结果,并落库抽奖记录
    • 落库中奖记录:报名记录锁定,添加中奖记录
  5. 异步发奖

# 5、说说抽奖活动的秒杀场景

秒杀场景主要在活动报名阶段,每个用户报名活动时,首先获取key,然后扣减redis中的活动数量。

其中key设置为活动ID+redis活动报名数量。参考concurrenthashMap进行设计,锁的粒度变细,相比于仅设置整个活动ID为key的做法,可以提高获取锁的成功率。

缺点:细粒度意味着需要占用更多的redis内存,100个活动就需要存100个分布式锁。此外,如果获取锁的流程失败,如何恢复也是一个问题?是否需要redis加回去,还是仅仅删除key。

# 6、动态路由导致事务失效如何解决?

通过spring提供的编程式事务来解决,Transactiontemplate来控制事务全部执行,或者全部失败。

在项目当中主要有两个地方:

  • 用户扣减活动报名次数+用户添加活动报名记录
  • 插入中奖记录+修改活动报名记录状态

# 7、如何防止超领和超发?

超领问题:

  • 问题:指的是用户同一时刻快速点击活动报名按钮两次,从而一次活动报名多次。
  • 解决方法:保证活动报名记录表的幂等性,设置一个唯一的uuid字段,它等于活动ID+用户ID+用户剩余报名次数。因此只要出现多条报名记录uuid重复,数据库插入时就会报异常。

超发问题:

  • 问题:超领问题指的是系统对于一条中奖记录,发送多次奖品。
  • 解决方法:保证中奖记录表的幂等性,设置唯一的uuid字段。一条中奖记录记录对应一条活动报名记录,设置值为对应活动报名ID。

# 8、谈谈两个kafka异步流程?

核心:通过kafka异步执行不干扰主链路的其它链路,保证从用户点击活动抽奖,到界面弹出抽奖结果的相应,这个响应时间尽可能快。

第一个kafka流程:异步更改数据库的活动数量;这里并没有设置回调确保消息一定消费成功,因此可能会导致数据库活动数量扣减失败,导致出现”数据库库存剩余,但是不可秒杀“的问题。

第二个kafka流程:异步发货;中奖记录有两个字段,包括发奖状态字段、MQ消息补偿字段。此处设置MQ回调:

  • 如果消息发送消费成功,中奖记录发奖状态更改,那么触发成功回调,MQ字段不更改。
  • 如果消费失败,没有发奖成功,那么触发失败回调,MQ字段更改为”待补偿“。
  • 后台启动一个定时任务,扫描所有中奖记录表的记录,如果MQ状态字段为”待补偿“,则重新消费这条消息。

# 9、项目中遇到什么问题?

1、动态路由组件:希望设计一个能够根据SQL语句的某个字段,路由到任意分库和分表;如何解决更换数据源导致的事务失效问题

2、秒杀场景:如何减小锁的粒度,提高获取锁的成功率。

3、决策树:如何存储DB,保存决策树的节点以及边

# 10、抽奖项目调优经验

项目采用两台4c8G的阿里云服务器进行压测:

  • 一台只部署springBoot项目,并对外开放Rest接口
  • 部署Mysql、中间件redis、kafka、xxljob

使用jmeter进行梯度压测,循环次数都设置2000,线程数从10开始每隔5进行递增。最终TPS稳定在600左右(这里将吞吐当成TPS),这里测的是整个链路。

优化:

  • 针对所有查询语句进行优化:①用户信息表建立联合索引,uid和活动id,当时吞吐翻了三四倍;以及查询用户活动报名记录,也是添加uid和活动id联合索引 ②查活动表直接添加活动id作为索引,但是效果没有很明显,可能是数据量不够。
  • 针对RTT响应时间进行优化:查询活动信息,把从数据库查询换成从缓存redis查询,也就是将活动信息存到redis中,key为活动ID。使用redis哈希这个数据结构进行存储。最终这个活动查询接口快了不少。

# 11、抽奖项目数据库表设计

数据库表设计包含以下几个表:

  • 活动信息表:包括活动ID、活动状态、活动起止日期、活动数量
  • 用户信息表:用户ID、活动ID、用户可报名活动次数、当前报名次数。(创建联合约束用户id-活动ID
  • 奖品表:奖品ID、奖品类型、奖品数量
  • 抽奖策略表:策略ID、单体还是总体概率
  • 抽奖详情表:策略ID,奖品ID,奖品概率、当前奖品数量
  • 活动报名记录表:活动ID,用户ID,uuid防重、当前活动状态、领取ID,剩余活动报名次数,报名ID
  • 中奖记录表:活动ID,用户ID,奖品ID,发奖状态,发奖方式,发奖时间,MQ状态,订单ID
  • 决策树节点信息表:决策属性,决策树ID,节点类型、节点值
  • 决策树边路径信息表:父节点、从节点、决策值、决策表达式

# 12、项目DDD划分成几个领域?

包括规则引擎、抽奖策略、活动报名、奖品发送。

# 13、组合模式

组合模式主要用于处理整体-部分的对象关系,在项目中主要是用于决策树节点的存储,区分非叶子结点和叶子节点,通过每个非叶子节点决策走哪条树茎,最终决策出叶子节点的活动ID返回给用户。

# 14、递增分布式ID的方案

方案对比如下:

  • UUID:无序不能用到索引,长度比较大耗费磁盘存储空间。字符串存储
  • 数据库自增ID:分库场景下,可能会出现ID重复的情况,不能水平拓展
  • 雪花算法:分布式场景下能够保证整体序列递增。但是依赖机器时钟,可能会出现重复ID生成。

# 项目二

# 1、项目包含哪几个功能模块?简要每个模块的功能和作用?

项目可以分为三个模块进行介绍:

  • 启动引擎、核心通信、启动助手:启动引擎作为SpringBoot程序入口,内嵌了核心通信、启动助手两个包。
    • 核心通信:Netty服务端,相当于一个网关算力。用于监听客户端连接,并执行处理和协议转换
    • 启动助手:SpringBoot starter程序,启动引擎启动服务时,能够向注册中心注册当前算力节点,并拉取服务信息构建缓存(当前节点负责转发哪些API接口)。
  • 注册中心:作为一个中间者,主要提供整个网关服务相关的数据库操作。采用SpringBoot+MySQL实现,其它组件或者程序通过Hutool的http连接调用注册中心的服务。
  • 服务上报:它是一个SprigBoot starter,当服务提供方,比如Dubbo、Http提供服务时,能够将当前服务信息向注册中心注册。让整个网关能够感知到当前的服务。

# 2、网关通信会话流程如何进行编排

在Netty服务端中,通过给channel指定和编排多个handler,当请求到达通道后,会顺序调用多个handler进行处理。本项目主要编排了三个handler:

  • 请求参数解析handler:包括截取url获取映射,获取请求参数,包括application/json和multipart,获取表单数据
  • 鉴权handler:拿到请求体中的uid和token,利用jwt解码验证当前用户是否授权,没有则直接返回writeAndFlush,不会进行下一步接口调用
  • 服务调用handler:根据uri的接口映射,从全局缓存中拿到对应的泛化调用对象,并且传入参数。结果写回通道响应客户端。

# 3、说明算力注册和服务发现starter的设计?

服务助手这个starter实现了两个主要功能:

  • 算力注册:将当前内嵌的网关信息,包括网关名称、网关地址、监听端口等注册到数据库。
  • 服务发现:获取当前网关节点所支持转发的接口和方法,并加入configuration缓存当中。

通过实现ApplicationContextAware接口实现,会在初始化之前执行。

# 4、Redis服务发布订阅使用场景?

使用Redis主要是因为,之前没怎么听说Redis还有这个订阅发送的功能,一般都是作为缓存来用的嘛,所以就打算用redis试一试,拓展技术广度。其实用其他MQ产品也是一样的。

场景:因为前面也提到了,服务提供方启动后,会注册服务接口信息,然后才能启动网关算力节点(整个启动引擎)。因此如果后续需要另外添加新的接口方法,那么就需要重启网关节点,显然这是不合适的。

方法:当服务提供方启动服务之后,会发送信息给监听者,topic主题是网关ID,也就是负责转发当前接口的网关;内容是系统ID,代表当前整个提供服务的系统。然后启动引擎那边会设置一个监听者代码,收到消息后会重新拉取该系统的注册信息,更新缓存。

# 5、编程式Docker如何实现?应用场景是什么?

实现:使用docker-java包,指定docker在服务器的路径,通过java连接到服务器的docker,从而可以编程式调用docker的容器。

场景:网关整个处理请求的链路是这样的,请求先打到一台Nginx上,然后Nginx根据url路由到对应的网关服务器地址上。因此我们配置好Nginx的上游服务器地址后,如果后续需要启动新的网关节点,那么就需要更改Nginx配置并重启。

需求:能不能我启动网关节点之后,不需要重新启动Nginx,也能被Nginx代理到当前网关节点上?

做法:要实现以上需求,就需要每个网关节点启动之后,动态刷新Nginx的配置,同时reload整个docker服务使其生效。具体来说分为两步:

  1. 更新Nginx配置文件:每个网关节点启动后,会刷新容器内部的一个配置文件,因为整个容器内部将该文件地址,关联挂载到了容器外部,也就是服务器中的文件,而该外部文件也是Docker容器Nginx关联的外部文件。相当于A与B关联,A更新之后B也会更新,因为B和C也是关联的,因此C也会更新,而C就是Docker内部Nginx的配置文件。new一个File,然后写入。
  2. reload重启Nginx,则是通过docker-java连接,然后根据容器名获取容器ID,exec进入容器之后,调用cmd脚本Nginx reload生效。

但是这种方案缺陷在于,如果是Nginx集群,或者网关节点和Nginx不在同一台机器上,则无法刷新。

调研过其它网关产品的做法,阿帕奇神禹(soul)解决网关动态刷新的做法,通过启动另外一个注册中心(Nacos、zookeeper)监视存活的网关实例,然后OpenResty(Nginx+lua)连接注册中心,就可以拿到实时的存活实例,更新到上游服务器。

# 6、谈谈注册中心数据库表的设计

数据表包括如下:

  • 网关信息表:包含网关ID、IP端口、网关状态,每条记录对应一个网关实例
  • 网关分配表:网关ID、应用ID,同一个应用下的所有接口和方法都交给该网关ID负责协议转换
  • 应用信息表:应用ID、注册中心
  • 接口信息表:应用ID、接口ID、接口名、版本号
  • 方法信息表:应用ID、接口ID、方法ID、形参类型、请求方式(get/post)、uri、鉴权标志

# 7、如何利用SPI?Spring如何利用拓展点?

SPI:通过spring.factories文件指定@Bean对象,该对象@configuration进行标记,主SpringBoot的IOC容器就会将第三方外部Bean加入到容器进行管理。

网关项目中,主要用到了几个拓展点:

  • closeEvent:容器关闭事件触发之后,会执行Netty服务的关闭,防止占用资源。
  • ApplicationContextAware:aware触发算力注册、服务拉取。
  • BeanPostProcessor:IOC容器执行初始化方法后,后置增强会执行RPC服务上报功能

# 8、项目遇到的问题

# 问题一:关于Netty服务端绑定的问题

当时尝试将网关部署在虚拟机的时候,服务端bind需要绑定监听的IP和端口:

  • 监听IP设置成虚拟机IP,启动的时候发现没法正常启动
  • 监听IP设置成127.0.0.1,可以正常启动但是收到外部请求

经过查询和了解,这个服务端bind指的是监听的网卡IP,也就是说它会接收到发送往当前主机某个网卡的所有请求,交给程序处理。因此,如果设置的IP当前主机网卡没有这个地址,那么就会报错。

于是尝试了ifconfig查看本机网卡,果然只有一个虚拟网卡,另一个是127.0.0.1。因此设置虚拟机IP是感知不到的。而127.0.0.1是一个本地环回地址,因此只有在虚拟机内部通过127.0.0.1发送才能接收到,而外部或者其它客户端发送往这个地址,只会发送到自己的机器。

所以最终的方案是设置成0.0.0.0,因为网关挂载外部某个端口,那么向当前虚拟机IP发送的请求,肯定接受到。它表示监听发送往当前主机的所有网卡。

# 问题二:服务上报的问题

因为只有先启动RPC服务提供方,向注册中心注册,再启动网关算力,网关才能感知到接口服务。因此如果后续需要启动新的服务,那么需要重新启动网关。

所以就希望能不能不重启网关,后续新的接口启动后,网关也能感知到并且刷新缓存。因此想到了事件发布订阅这么一个通知机制。

# 9、系统的性能瓶颈在哪儿?

整个网关系统有多条链路,包括算力注册、服务注册,其中性能瓶颈主要集中在这条链路:客户端连接-》参数解析-》获取服务调用-》封装结果并响应返回。因此可以从以下几个方面进行优化:

  • 网络IO:当多个用户请求打进来的时候,Netty需要处理大量的IO读写事件。可以采用Netty零拷贝等一些方法提高IO性能。
  • 线程池:如何设置好Netty线程池参数是提高系统的关键。EventLoop对应一个Netty线程,分为两种,boss线程负责处理连接事件,worker线程则负责执行业务操作。一般来说,服务端监听了几个IP,那么就设置几个boss线程;worker线程则设置为CPU核心数*2,因为worker线程并不是一直都在工作的,可能客户端数据没有到。
  • 内存设置:网关很多对象都是朝生夕死的,因此设置合理的JVM内存大小可以防止出现内存问题。可以将新生代设置大一些,让所有对象都在新生代存活和销毁。
  • 服务提供方:提高RPC方法调用的效率可以提高系统整体的性能。比如采用Dubbo的异步执行、负载均衡。

# 10、Netty如何实现断线重连?

一般重连可以通过定时任务,重复执行客户端连接的代码。

本系统因为客户端不需要处理数据,因此没有编写客户端代码,客户端通过网络连接实现服务端的连接。如果断了那么客户端直接刷新浏览器就好了。

# 11、网关高可用可以做哪些处理?

  • 异地多机房部署多个网关实例
  • 网关实例执行RPC服务调用失败,需要进行重试机制
  • 负载均衡:前端LVS+硬件负载均衡F5+Nginx

# 12、该网关与springcloud网关有什么区别?

SpringCloud网关主要是处理Http连接的一些问题,包括降级、熔断、限流

本网关系统主要是负责协议转换,控制API接口调用的管理

# 13、其它系统想要接入你的网关,需要哪些步骤?

  • 首先需要在注册中心,配置相关URL的映射,什么样的请求格式才会打到对应RPC服务
  • 网关内部需要拓展当前协议格式,要转发给你提供的API接口,请求格式是什么,参数是什么
  • 提供方需要暴露服务,嵌入我们的SDK服务上报,如果是Dubbo那么系统正好实现;而如果是其它服务,则需要调用注册中心接口,上传接口、方法、参数等信息。

# 14、网关为什么自研?和市面上的产品区别在哪儿?

我觉得区别主要在两个方面:

  • 拓展性:自研网关可以很好的拓展支持公司内部协议,拓展实现其它功能
  • 维护成本低:自研的产品减少依赖包版本导致的不兼容问题,同时代码全程都是可控的,因此不会出现安全问题,比如之前log4j的漏洞

# 15、网关如何进行区分?

数据库中有一个网关分配表,每一条网关ID对应一个应用ID,这是一个一对多的关系,该应用ID下的所有接口和方法都负责交给该网关进行转发。

如果需要同一个接口下方法1交给网关A转发,方法2交给网关B,那么需要进一步细粒度的设计这个网关分配表。

# 16、RPC服务上报后协议变了,网关如何进行处理?

处理如下:

  • 首先网关Handler内部需要支持当前协议的转发,保证RPC服务能够被正确的转发和调用
  • 数据库中,网关的接口方法数据也需要更新,包括方法参数,请求格式等等

# 17、服务降级方案怎么进行设计?

服务降级可以通过一个插件实现,嵌入到服务提供方;当服务启动注册后开启服务治理配置,当触发服务降级时,可以设置返回一个错误码。

# 其它

# 1、说说JWT安全认证?

本质上是一种数据签名方式,用于加密认证。服务端保存一个私钥,利用私钥进行加密解密,根据结果判断用户的信息是否被篡改过、被认证过。

# 2、说说Github Actions如何工作?

部署前端项目时,push推到master分支时,github会自动读取并执行workflow下的配置脚本。

具体来说,在jobs下定义每个操作步骤step,包括切换分支、安装node.js、执行前端代码部署脚本、运行。

# 3、操作系统内核的工作

  • 负责网络IO,将网卡的数据接收到内核空间,以及将数据发送出去
  • 协议解析,将数据包发往对应的协议端口
  • 维护当前主机和远程主机的连接

# 4、top指令

通过top指令,查看整个服务器的资源占用,包括CPU、内存、交换区的使用占比。然后会列出所有进程的使用情况。

如果想要查看某个进程的资源占用情况,使用如下语句:

top -p 指定进程的pid,查询对应进程的资源使用情况

# 5、lsof指令全称是什么

lsof:list open files 列出进程打开的所有文件。

# 6、乐观锁悲观锁使用场景

1、乐观锁主要用于读多写少的场景,底层通过版本号实现。

2、悲观锁主要用在数据激烈竞争的场景、写多读少。synchronized和lock都是悲观锁

# 7、反射违背面向对象的封装性吗?

反射并不会破坏对象的封装性,那些有限定符限制访问的还是遵循对应的约束。

可以通过setAccessible修改权限,但是这是一种暴力的方法。

# 8、Mybatis和Mybatis-plus的区别

Mybatis:所有dao操作都需要在xml文件当中配置SQL语句、ID映射

MP:通过内置的Mapper,直接调用默认方法,实现数据表的CRUD操作。

# 9、跨域问题

跨域问题:因为浏览器同源策略,导致当前页面不能进行跨域访问,包括IP+端口+协议必须要保持一致。

解决方案:在controller上面注解@CrossOrigin。本质都是在响应头当中,加入允许跨域的字段。

# 10、CPU中断之后进程的处理流程?

  • 保存进程状态:寄存器
  • 切换上下文,执行中断处理程序
  • 恢复现场
  • 返回用户态,继续执行中断代码

# 11、CAS算法?

CAS:compare and swap

广泛用于并发控制的算法,基于乐观锁通过检查版本号实现。

首先读取要修改的变量值,更改时如果发现该数据已经被改变,则更新失败;否则更新成功。

# 12、Nginx负载策略

  1. 默认轮询方式:按照请求时间将请求负载到不同的服务器
  2. 权重:每个服务器设置权重,权重的大小与被负载的概率成正比
  3. 最短连接:每次Nginx会将请求负载到连接数量最少的服务器
  4. 哈希

# 13、两个线程交替打印奇数偶数

1、synchronized+wait+notify

2、BlockingQueue

# 14、协程

1、适用于IO密集型任务场景,比如连接数多、读写频繁、

2、协程运行于线程之上,一个线程包含多个协程

3、协程是用户自定义的,操作系统不会感知到协程的存在,因此切换时不存在内核态的上下文切换开销。

# 15、Docker容器虚拟化技术

技术发展:VM虚拟机的问题在于,只实现了操作系统级别的虚拟化,每次迁移都需要重新安装操作系统,迁移比较重;Docker实现了进程级别的虚拟化,不同进程之间感知不到对方的存在,容器之间的资源都是隔离的,包括库、程序、资源配置等。 虚拟机运行的是操作系统,而docker运行的是应用。

实现docker虚拟化的核心技术:

  • linux namespace:在每个namespace中,能够控制每个容器能够看到的pid、网络等资源,每个资源都是容器独一份的,最终实际运行时在映射到Linux的全局资源
  • control group:限制每个容器能够使用的具体硬件资源,包括内存,磁盘。

# 16、数组和链表在内存存储上的区别?

数组内存地址是连续的;而链表在物理上是不连续的。因此从某种意义上来说,数组比链表更快,差距可能出现在计算下一个访问节点的内存地址上。

# 备战

# 技术选型问题?kafka和RocketMQ?网关使用Netty?注册中心数据存储?

消息队列选型:

  • 单机kafka吞吐是比RocketMQ高的,主要原因在于①kafka零拷贝②kafka发送消息时支持批量压缩(问题:缓存GC,生产者宕机)
  • kafka主要定位是日志,而rocketmq可以满足订单、交易、充值等场景。

网关技术选型:

  • springcloud gateway底层使用的webflux就是基于Netty搭建,它是一个高性能的通信框架。

注册中心数据存储:

  • Nacos:嵌入式数据库,MySQL。分成三个部分provider、server、consumer。其中nacos-server就是一个springboot程序,提供服务注册和服务发现接口,以及持久化接口。
  • zookeeper:主要是通过磁盘持久化+内存持久化实现。数据写到内存之后,异步写到磁盘当中。磁盘文件包括日志+快照,每次开机恢复时都会取最新 id 的快照和日志进行恢复,读到内存。
  • 京东的注册中心:采用redis+mysql水平分片实现。保证高可用
  • 注册中心需要保证AP原则,考虑扩容和容灾,

# MySQL锁分类?死锁问题?事务的锁的关系?

MySQL锁划分:

  • 根据锁粒度可以分为表锁、行锁、页锁;
  • 根据锁功能分为共享S锁、排他X锁;
  • 根据操作性能分为乐观锁(更新时才会进行冲突检测)、悲观锁(在更新前先锁定数据,防止被篡改);资源竞争激烈时使用悲观锁,防止乐观锁重试浪费资源。
  • 根据行锁算法可以分成记录锁(锁住的实际上是索引记录,如果没有走索引会隐式创建索引,然后锁全表所有记录)、gap间隙锁(锁区间)、临建锁(记录+间隙锁)。具体是哪种行锁情况,会根据执行SQL语句中的范围查询、等值查询,唯一索引等条件退化成不同的锁。

死锁主要发生于持有某个锁资源同时,等待其它事务释放锁资源。造成多个事务同时等待的局面。比如:

  • 表死锁:线程1先获取A的表锁,然后等待获取B的表锁;线程2先获取B的表锁,然后等待获取A的表锁
  • 行死锁:事务1首先在记录5添加X锁,然后等待记录10释放行锁;事务2首先在记录10添加X锁,然后等待记录5释放行锁

如何解决死锁:

  • 核心是按序申请资源,按序加锁;程序批量处理数据时,如果能够进行排序,每个线程按序处理数据,则可以减少死锁出现的可能
  • 根据情况创建合适的索引,防止不走索引锁住表的每一行记录,增加出现死锁的概率
  • 线上如果发生死锁,则根据数据库监控工具,查询事务状况、锁资源状况;从而直接kill进程,或者是回滚事务

事务本质上是锁+MVCC实现的结果,进行了底层封装;对于用户来说,如果使用事务能够解决并发问题,那么则无需额外操作,否则需要手动加锁。

# Spring事务?事务传播?

Spring事务本质上就是通过AOP实现的,实际上的回滚操作都是通过数据库事务支持实现的。

通常使用数据库事务,需要通过①拿到conn,并将autoCommit设置为false。②通过conn调用rollback回滚事务。而Spring使用事务通过AOP方法,将这两个步骤省略去掉。

事务传播:两个Spring事务注解方法之间,如果存在嵌套调用关系,那么事务作用的范围是否会扩大,主要包括七个事务传播级别。比如A调用B方法,B进行事务传播注解,那么B抛出异常后,A中其它方法是否会进行回滚。

# 设计高性能接口

使用Guava的RateLimiter,限制接口访问频率。

高并发三架马车:限流、缓存、降级:

  • 限流
    • 漏桶算法:请求到达会先放入队列,处理器按照固定频率从队列当中取出任务执行。队列满了之后,请求会被抛弃。
    • 令牌桶算法:按照一定频率向阻塞队列当中放入令牌,请求到达会向阻塞队列当中取出令牌。阻塞队列没有令牌则请求被阻塞。
  • 降级

# Dubbo协议与HTTP协议

HTTP协议是应用层协议,它是在TCP之上的,发送数据量会很大,同时每次请求都需要握手挥手。适用于外部系统连接,跨语言通信服务。

Dubbo协议是TCP协议进行传输,默认使用TCP长连接,速度更加快,适用于内部系统互联。通信是基于Netty的NIO实现的。

其中Dubbo泛化调用中创建三个重的实例,向注册中心获取泛化服务对象:

  • 应用配置对象
  • 注册中心配置对象
  • 引用配置对象:指定全限定接口名

拿到接口的泛化调用对象之后,通过invoke方法,形参中指定方法名,参数类型,参数,即可向服务提供方发起远程调用。

# 孤儿进程?僵尸进程?

孤儿进程:子进程还没结束,父进程先退出,导致子进程找不到父进程。此时操作系统会设置init进程收留子进程。

僵尸进程:子进程退出后,子进程资源没有被回收。父进程如果死循环,一直没有调用wait方法收尸,则会产生僵尸进程。

# 硬中断?软中断?

硬中断:硬件外部设备到达CPU的中断,通知CPU外设状态变更。比如收到数据。

软中断:程序产生。

# JVM栈帧对象释放

JVM对每个栈帧只有两个操作:每个方法执行时入栈,执行完毕之后出栈。不存在GC操作,但是有可能出现OOM溢出的情况。

# kafka消息到消费者是推还是拉模式?

结论:kafka采用的拉模式。

推模式:

  • 消息由broker节点推向消费者
  • 缺点:消费速率和推送消息速率不一致,消费过慢导致消息会在消费者端堆积爆仓
  • 场景:适用于实时性要求高,消费速率较快的场景

拉模式

  • 消息由消费者定期向broker节点拉取
  • 缺点:消费者不知道具体什么时候消息到达,因此存在延迟
  • 场景:减少broker负担。broker可以感知消费者的消费速度,支持批量传输。

# 为什么要分库,分表?如何分?

分库分表主要是为了分散存储,减轻DB性能压力。

分库场景:

  • 单个数据库的数据量暴增,导致磁盘容量可能会撑爆
  • 并发场景下,单库的连接数有限,大量请求到来时数据库可能扛不住
  • 如何分库:根据业务模块比如订单库、商品库进行划分,拆分成不同功能的数据库,分担读写压力

分表场景:

  • 单个数据表数据量达到500w或者2000w可能就需要考虑分表,否则数据量多会增加磁盘IO次数,访问效率降低
  • 如何分表:基于某一个字段计算出一个分表键

①水平分指的是按照以记录为单位进行切分 ②垂直分库按照业务属性进行划分 ③垂直分表按照字段的活跃性

# 分库和分表存在的问题?

分表:分表策略如果不对,可能会出现数据倾斜的问题,大多数记录分到同一个表上,这种情况下需要更改分表策略。

分库:不同数据源会导致事务失效。

分库分表存在的查询问题:①join表查询、以及基于一些全表数据的查询group by order by、分页查询都会出现问题 ②不能使用数据库自增ID,需要使用分布式ID

# 备战2

# 什么是泛型擦除?

在JVM编译期间,对象指定泛型在字节码中会被擦除,统一替换为Object原始类型。因此可以通过反射,向一个String的列表插入Integer数据。

实际使用时,如果向容器插入不同类型的数据会报错,原因在于:

  • 编译前进行类型检查,编译时会进行类型擦除
  • 基于引用对象进行检查,如果引用指明了容器类型,那么通过引用调用方法时,则会进行类型检查。

# mybatis配置xml文件的$与#占位符有什么区别?

#{}占位符一般用于将程序中的变量填入SQL语句中占位符的位置,执行效率更高;

${}占位符存在两个问题:①程序传入什么值,最终SQL语句就是什么类型的数据,不会进行类型转换和区分,比如“ ‘ name ’ ” ②SQL注入风险,它采用字符串拼接的方式

# 消息队列可以用来做什么?MQ和RPC区别是什么?

消息队列的作用:

  • 异步:将执行的流程交给另一个消息监听者消费,和主流程并行执行
  • 解耦:减少服务之间的依赖关系;比如A调用B服务,可以将B服务通过MQ抽离出来执行。
  • 消息可靠性:MQ通过持久化,重试等机制保证消息不丢失,能够异步消费。

RPC场景:①需要服务返回值回调 ②适用于不同系统不同服务之间的调用

MQ场景:需要解耦程序之间不同组件之间的通信;保证数据安全传输;需要使用同一个系统的上下文信息

# java内存泄漏?介绍一些四种引用类型?

对象在程序结束,或者程序不再使用的情况下,分配的内存空间没有被回收从而造成内存泄漏。

四种引用:

  • 强引用:程序代码中的引用默认为强引用
  • 软引用:在内存空间不够的情况下进行回收。一般用于有用但是非必需的场景,比如缓存。
  • 弱引用:下一次GC一定会被回收。
  • 虚引用:随时会被回收。

# java为什么不支持多继承?什么时候采用继承和组合?

java不支持多继承主要有两个方面:

  • 多继承会出现“菱形问题”,重写方法的调用链会出现一些歧义问题。
  • 即使技术上能够解决,但是从java面向对象的编程思想上看,java采用继承更多是对当前对象更高层次的抽象,而非更多层次的抽象。而如果需要“更多层次的拓展”方法,java提供了多接口实现的方法。

代码复用包含继承 和 组合两种方式:

  • 对象创建:组合需要依次创建多个依赖使用的组合对象;而继承只需要创建子类对象
  • 独立与耦合:组合能够使组合对象与整体对象解耦,彼此相对独立:而继承则破坏了父类的封装性
  • 可拓展性:组合具有较好的可拓展性,支持调用不同组合类的方法;而对于继承,java只支持单继承,因此灵活性差。

使用场景:从抽象概念上来说,如果是is-A类型,A类确实是B类的抽象类型,那么可以使用继承复用父类方法;而除此之外的其它情况,都优先考虑使用组合,effective java也是优先推荐使用组合。

# CGLib和JDK动态代理之间的区别

动态代理技术:

  • 运行时生成字节码:本质上是在程序运行时生成代理类的字节码文件,然后交给JVM进行类加载,生成代理类信息。其中JDK是通过直接写Class字节码实现,而CGLib是通过ASM字节码框架。
  • 实现方式:JDk基于接口实现代理,它只能代理目标类实现的接口方法。而CGLib是通过创建目标类的子类,在代理类的方法中可以重写目标类的方法,并在方法前后插入自定义增强方法和逻辑。
  • 方法调用:JDK调用方法时,是通过反射invoke间接调用;而CGLib则是直接调用父类方法super,性能更好。
  • 使用场景:JDK一般用于实现接口的代理,比如Spring AOP和日志;而CGlib则用于不需要实现接口对象的代理,提高性能。

# 分布式锁的方案有哪些?

1、数据库:主键ID或唯一字段插入成功才能获取锁;乐观锁,加入版本号字段

2、redis:setnx、redisson、lua脚本

3、zookeeper:利用临时节点和watch机制,在锁目录下下创建临时节点

# 职业发展开放性问题

# 1、个人职业规划是什么?

理想情况下,我期望的职业发展是这样的:

  • 前三年时间充分理解和熟悉业务,并结合具体场景拓宽开发技能,向着中级开发工程师,再到高级开发工程师迈进。
  • 后两年除了关注产品功能实现之外,还需要能够多关注方案设计等方面,努力向着架构师这个目标发展。

当然这个过程是比较理想的,作为一个职场新人小白,我认为最重要的就是提高自己的技术实力和核心竞争力,多向周围有经验的前辈学习和请教,努力追赶他们的步伐,争取在自己负责的业务上做出更多的成绩,在专业领域更加具有影响力,成为团队不可缺少的一环。

而对于前面提高的职业规划和晋升方面,我认为个人技术和能力达到要求后,得到公司领导认可,一切都是水到渠成的,因此加入公司后,对于自己更重要的事是如何让自己快速成长起来,承担起自己的职责。

# 2、如何看待拼多多?

拼多多作为国内电商领域的龙头企业,它之所以能够成功我认为可以从两个方面探讨:

  • 人效:什么是人效,我有看过一些网络上公布的销售额数据,比如淘宝天猫销售额达到五千亿,而咱们多多能干到两千五百亿;但是人家阿里有十几万人,有很多人,而咱们多多好像是只有几千人吧。因此从人效上看别人是和我们比不了的,毕竟员工也需要公司发工资养这嘛。
  • 用户/广告:我们知道现在做电商广告就意味着用户,淘宝,京东广告都是满天飞,机场、以及软件进入画面摇一摇都是,而拼多多在广告这方面投入很少,或者说是精准有效,它主要是从两个方面切入:
    • 心智占领:拼多多通过百亿补贴这些活动,在广大用户心中已经树立起了一个便宜的形象,作为用户我肯定希望买东西越便宜越好,因此用户每次想要购买商品的时候,都会先想“要不去拼多多看看吧,那里便宜”。所以我不需要买多少广告就有很多买家用户。
    • 精准投放:多多广告主要在一些直播平台或者是视频软件上,比如B站某个up打个广告,然后评论区附带一个链接,从而刺激很大一部分年轻人的进行消费。

从业务角度说,多多通过百亿补贴等这些活动覆盖大量一二线用户,在电商领域做到头部地位,另外还有社区团购的多多买菜这些业务也都做到了第一梯队的地位,因此一家公司能够在做好核心业务的同时,能够有能力孵化其它业务,我认为这家公司在组织领导能力、产品创新、技术研发等方面都是很顶尖的。

# 3、看过哪些技术博客

公开的博客:比如左耳耗子老师,还有一些不知名的开发者的博客,他们都会分享一些小场景的设计、重构、SDK开发。

Github:有时候没事就上去trending看一下哪些有意思的开源项目;同时也关注了一些知名阿里、美团等一些团队。

# 4、说说你从这几个项目中学到了什么?

抽象项目当中:

  • 设计模式的使用
  • MQ回调+定时任务消息补偿
  • 对于数据库路由组件设计有了更深的理解,开发spring starter

网关项目:

  • 设计模式的使用
  • Spring拓展点使用
  • Netty搭建服务端,编排通道事件
  • 对设计网关有了更深的理解,包括网关节点探活与负载,服务刷新等

# 5、谈一下你的优缺点

优点:

  • 善于复盘总结:有记录博客的习惯。因为写一遍讲述给别人看,也是一种知识输出的手段,可以加强记忆和理解。
  • 不服输,坚持:面对问题或者困难能够拆解成一个个子问题,再逐个解决。做事情能够持之以恒。
  • 主动,有责任感:能够主动承担一些任务,并且主动交流沟通。研究生阶段主动承担----

缺点:

  • 有时候结果不符合预期,或者是自己的努力和心血没有得到回报的时候会焦虑吧。
  • 英语还能再提高一些。

# 6、项目中遇到的问题

在抽奖系统中,执行完抽奖生成抽奖记录响应给用户后,需要异步修改中奖记录状态字段为待发货,实际压测的时候发现会出现消息消费失败的情况,于是查阅了kafka保证消息不丢失的方案。

第一步就是根据kafka的一些配置,生产者端设置一个回调,onfailure失败逻辑中重试发送消息;MQ端在配置文件设置单分区多副本,并且acks设置为all,表示消息发送到多个副本之后才返回给生产者回调。

但是压测之后还发现依然存在消息消费失败的情况,也就是状态位字段不正确。于是就去调研了一下对应的消息自动补偿的方案,普遍做法是后台另启一个定时任务,重新消费失败的记录。

第二步的做法就是,在中奖记录表中设置一个MQ状态字段,如果onfailure触发失败回调则将它置为1。定时任务会扫描整个中奖记录表中MQ状态为1的字段,并消费修改这条记录。

# 7、开发和算法之间的选择

首先兴趣(本科接触了java,对此比较感兴趣,第一点java整个语言的生态比较好,无论是一些issue还是迭代,以及相关组件依赖包的开发,都有很多开发者在参与;第二点选择开发能够接触并且使用到的工具和插件更丰富一些,这点我认为算法是比不上开发的)

其次做好开发,需要理解用户各种不同的需求,理解不同的业务,因此能够碰到的场景更丰富一些。而算法可能更多的是给到一批数据,期望输出得到什么样的数据。

编辑 (opens new window)
上次更新: 2024/04/18, 16:33:48
分散的面试问题
散列与哈希算法

← 分散的面试问题 散列与哈希算法→

Theme by Vdoing | Copyright © 2023-2024 blageCoder
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式