面试问题
MQ
每个阶段保证消息可靠性传输(消息丢失怎么办)
-
生产者
弄丢了数据:开启生产者-确认模式或开启事务(不推荐),确保生产者的消息能到达队列,如果报错可以先记录到日志中,再去修复数据 -
RabbitMQ
弄丢了数据:开启持久化 交换机+队列+消息持久化,确保消息未消费前在队列中不会丢失 -
消费端
弄丢了数据:关闭 RabbitMQ 的自动 ack,重试次数,超过多少次将失败后的消息投递到异常交换机进行人工处理。
消费幂等
- 全局唯一的 id
- 数据库主键的唯一性
消费积压怎么办
我在实际的开发中,没遇到过这种情况,不过,如果发生了堆积的问题,解决方案也所有很多的
- 提高消费者的消费能力,可以使用多线程消费任务
- 增加消费者,提高消费速度 使用工作队列模式,设置多个消费者消费同一个队列的消息
- 扩大队列容积,提高堆积上限,可以使用RabbitMQ惰性队列,惰性队列的好处主要是①接收到消息后直接存入磁盘而非内存②消费者要消费消息时才会从磁盘中读取并加载到内存③支持数百万条的消息存储
MySQL面试题
B+树
B+树相较于B树做了几个方面的优化:
- B+树的所有数据都存储在叶子节点,非叶子节点只存储索引
- 叶子节点中的数据使用双向链表进行关联
使用B+树的好处
-
非叶子节点只存索引:每一层能够存储的索引数量增加,同样多的数据树的层高要低,磁盘IO次数少。
-
叶子节点存储数据且叶子节点是双向链表来关联:范围查询的时候只需要两个节点进行遍历,而B树需要获取所有节点。全局扫描能力强。
-
B+树这种结构,主键如果是自增整型,可以避免增加数据带来的叶子节点分裂导致的大量运算问题。
索引
索引失效
InnoDB引擎有两种索引类型,主键索引和普通索引。用B+树的结构来存储索引。当使用索引列进行数据查询的时候,最终会到主键索引树中查询对应数据进行返回。
导致索引失效的情况:
- 函数转换:索引列上做运算
- 类型隐式转换(字符串需要+引号)
- 组合索引顺序
- 违背最左前缀原则 like%xxx
- 区分度太低
- 索引列上使用 != 、not、or
索引规则
- 覆盖索引:只需要在一棵索引树行就能获取SQL所需的所有列数据,无需回表,速度更快。
- 回表:需要再到主键索引树上查询一次的过程。
- 最左前缀原则:联合索引遵循此原则。如果建立了(a,b,c)的联合索引,相当于建立了(a,b)(a,c)(abc)
- 索引下推:索引遍历的过程中,对索引包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
SQL优化
- 硬件及操作系统层面的优化:避免资源浪费
- 硬件层:影响MySQL的性能因素有,CPU、可用内存大小、磁盘读写速度、网络带宽
- 架构设计层面的优化:磁盘IO访问量非常频繁
- 主从集群:单个MySQL节点容易单点故障,主从集群或者主主从集群可用保证服务的高可用性。
- 读写分离:读多写少,避免读写冲突。
- 分库分表:分库降低单个服务器节点的IO压力,分表降低单表数据量,从而提升SQL查询效率。
- 热点数据引入更高效的分布式数据库,Redis、MongoDB等。
- SQL优化
- 慢SQL定位和排查:慢查询日志和慢查询日志分析工具得到有问题的SQL列表。
- 执行计划分析:explain查看当前SQL执行计划(type key rows filterd)
- 使用show profile工具:可以分析当前会话中,SQL语句资源消耗情况的工具,可用于SQL调优的测量。针对运行慢点SQL可以得到所有的资源开销情况,如IO开销、CPU开销、内存开销等。
SQL优化规则:
-
查询要基于索引来进行数据扫描
-
避免在索引列上使用函数或运算、IS NULL 、IS NOT NULL、OR。
避免数据类型隐式转换(字符串字段要 引号)
-
最左前缀原则,like xxx%,联合索引中的列从左到右
-
查询有效列,而不用 *
-
小结果集驱动大结果集
什么是事务的隔离级别?MySQL的默认隔离级别是什么?
为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别,由低到高依次为读未提交、读已提交、可重复读、串行化,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。
SQL 标准定义了四个隔离级别:
- RU(读取未提交)
脏读、不可重复读、幻读
:允许读取尚未提交的数据变更。 - RC(读取已提交)
不可重复读、幻读
:允许读取并发事务已经提交的数据。解决脏读。 - RR(可重复读)
幻读
: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改。解决脏读和不可重复读。 - SERIALIZABLE(可串行化) :完全服从ACID的隔离级别。所有的事务依次逐个执行。解决脏读、不可重复读、幻读
MVCC在查询的时候,会生成一个readview的class结构,里面会保存一些事务ID数据(比如当前存活的事务ID),然后根据事务的ID进行比对来决定是否展示。
每个隔离级别都是基于锁和MVCC机制实现的,如下:
- 读未提交/RU:写操作加排他锁,读操作不加锁。
- 读已提交/RC:写操作加排他锁,读操作使用MVCC,每次查询都会生成一个新的readview,每次查询都能拿到最新的数据,产生了不可重复读和幻读。
- 可重复读/RR:写操作加排他锁,读操作依旧采用MVCC机制,只有在第一次查询的时候会生成一个readview,后面查询比对的都是第一个查询的时候的结果,后面查不到新的值。
- 序列化/Serializable:所有写操作加临键锁(具备互斥特性),所有读操作加共享锁。间隙锁,根据锁的条件锁定一个区间,所以没有幻读。
MVCC
主要实现MySQL的可重复读
MVCC的意思是多版本并发控制。MVCC通过维护一个数据的多个版本,使得读写操作没有冲突。
底层实现: 回滚指针(roll_pointer)+ undo_log + 事务id(trx_id)+ readview(快照读)
- 回滚指针(roll_pointer)+ undo_log:undo_log存储老版本数据,内部形成一个版本链,多个事务并行操作某一行,形成不同事务版本,通过roll_pointer指针形成一个链表。
- 事务id(trx_id)+ readview(快照读):解决事务查询选择版本问题,内部定义匹配规则和当前的一些事务id(trx_id)判断该访问哪个版本的数据。不同的隔离级别快照读是不一样的。
- RC读提交:每次查询都会生成一个新的readview,每次查询都能拿到最新的数据,产生了不可重复读和幻读。
- RR可重复读:只有在第一次查询的时候会生成一个readview,后面查询比对的都是第一个查询的时候的结果,后面查不到新的值。
JVM面试题
JVM内存结构(内存模型)
私有
- 程序计数器(PC):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
- Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
- 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
公有
- Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;垃圾回收在这里
- 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
垃圾回收
怎么判断对象是否可以被回收?
垃圾回收器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。
一般有两种方法来判断:
- 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
- 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
垃圾回收算法
- 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
- 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
- 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
- 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
并发
实现多线程的几种方法
- 继承Thread类;
- 实现Runnable接口;
- 实现Callable接口通过FutureTask包装器来创建Thread线程;
- 使用ExecutorService、Callable、Future实现有返回结果的多线程(也就是使用了ExecutorService来 管理前面的三种方式)。 基于线程池的方式
线程池核心参数
2.阻塞队列(workQueue):保存等待执行的任务的阻塞队列
3.最大线程数(maximumPoolSize):能添加的worker最大数量
4.创建线程工厂(ThreadFactory)
5.拒绝策略(RejectedExecutionHandler):当队列满且线程个数达到最大线程数则采取拒绝策略
- AbortPolicy:默认的策略,直接抛出RejectedExecutionException
- DiscardPolicy:不处理,直接丢弃
- DiscardOldestPolicy:将等待队列队首的任务丢弃,并执行当前任务
- CallerRunsPolicy:由调用线程处理该任务
6.存活时间(keepAliveTime):当前线程池的线程数量比核心线程数多,且闲置状态,则是这些线程的最大存活时间。
7.存活时间单位(TimeUnit)
线程池具体线程的分配方式
核心线程数—〉 等待队列—〉最大线程数—〉拒绝策略处理
当一个任务被添加到线程池:提交任务
1. 核心线程池是否满:未满---创建线程处理任务,满则执行2
2. 等待队列是否满:未满---任务加入等待队列,满则执行3
3. 线程池是否满(最大线程数):
未满---创建线程处理任务
满---拒绝策略处理线程
并发工具类
-
CountDownLatch:允许一个或多个线程等待其他线程完成操作。一个线程或多个等待另外n个线程完成之后才能执行。
-
CyclicBarrier (回环栅栏) :让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。作用就是让所有线程都等待完成后才会继续下一步行动。
初始化参数:等待线程数、达到数量后线程唤醒前执行的函数
- parities:屏障拦截的线程数量,计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。计数器可复用。
- Runnable参数:在达到parities数量后,所有其他线程被唤醒前被执行。
-
Semaphore (信号量) 用来控制同时访问资源的线程数量。应用场景:流量控制,特别是公共资源有限的应用场景,比如数据链接,限流等。
ConcurrentHashMap为什么线程安全
ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突
ConcurrentHashMap 1.7
- 数据结构:
Segment(大数组) + HashEntry(小数组) + 链表
,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突 - 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了
- 索引计算
- 假设大数组长度是 $2^m$,key 在大数组内的索引是 key 的二次 hash 值的高 m 位
- 假设小数组长度是 $2^n$,key 在小数组内的索引是 key 的二次 hash 值的低 n 位
- 扩容:每个小数组的扩容相对独立,小数组在超过扩容因子时会触发扩容,每次扩容翻倍
- Segment[0] 原型:首次创建其它小数组时,会以此原型为依据,数组长度,扩容因子都会以原型为准
ConcurrentHashMap 1.8
- 数据结构:
Node数组 + 链表或红黑树
,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 cas 而非 syncronized,进一步提升性能 - 并发度:Node 数组有多大,并发度就有多大,与 1.7 不同,Node 数组可以扩容
- 扩容条件:Node 数组满 3/4 时就会扩容
- 扩容单位:以链表为单位从后向前迁移链表,迁移完成的将旧数组头节点替换为 ForwardingNode
- 扩容时并发 get
- 根据是否为 ForwardingNode 来决定是在新数组查找还是在旧数组查找,不会阻塞
- 如果链表长度超过 1,则需要对节点进行复制(创建新节点),怕的是节点迁移后 next 指针改变
- 如果链表最后几个元素扩容后索引不变,则节点无需复制
- 扩容时并发 put
- 如果 put 的线程与扩容线程操作的链表是同一个,put 线程会阻塞
- 如果 put 的线程操作的链表还未迁移完成,即头节点不是 ForwardingNode,则可以并发执行
- 如果 put 的线程操作的链表已经迁移完成,即头结点是 ForwardingNode,则可以协助扩容
- 与 1.7 相比是懒惰初始化
- capacity 代表预估的元素个数,capacity / factory 来计算出初始数组大小,需要贴近 $2^n$
- loadFactor 只在计算初始数组大小时被使用,之后扩容固定为 3/4
- 超过树化阈值时的扩容问题,如果容量已经是 64,直接树化,否则在原来容量基础上做 3 轮扩容
Redis面试题
数据类型和数据结构
Redis有哪些数据类型
5种基础数据类型:
- String:可以存储图片或序列化对象。值最大512M
- 场景:共享session、分布式锁、计数器、限流。
- 内部编码:int(8字节长整型)/ embstr( <= 39字节字符串)/ raw( > 39字节字符串)
- Hash:哈希类型指的是v值本身是一个键值对。
- 场景:缓存用户信息等
- 内部编码:ziplist(压缩列表)、hashtable(哈希表)
- List:存储多个有序字符串。栈、队列、有限集合、消息队列。
- 场景:消息队列、文章列表
- 内部编码:ziplist(压缩列表)、linkedlist(链表)
- Set:存储多个字符串,不允许重复。
- 场景:用户标签、生成随机数抽奖、社交需求。
- 内部编码:intset(整数集合)、hashtable(哈希表)。
- ZSet:有序集合,已排序的字符串集合,不能重复。score权重值
- 场景:排行榜、社交需求(用户点赞)
- 内部编码:ziplist(压缩列表)、skiplist(跳表)。
3 种特殊数据类型 :Geo地理,HyperLogLog基数统计,Bitmaps位图
- Geo: Redis3.2 推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作
- HyperLogLog:用来做基数统计算法的数据结构,如统计网站的UV。估数,不精确去重方案。
- Bitmaps :位图。底层是基于字符串类型实现的,可以把 bitmaps 成作一个以比特位为单位的数组。统计和查找。
缓存穿透、缓存击穿和缓存雪崩
-
缓存穿透:查询一个缓存,为空—> 缓存空对象或布隆过滤器(很多个hash函数映射 判断是否存在)
-
缓存击穿:某个热点数据删除—> key永不过期
-
缓存雪崩:大量热点数据删除 —> 过期时间均匀分布 + 热点数据永不过期 或者采取限流降级
Redis和MySQL如何保证数据一致性
- 读的时候:先读缓存,缓存命中的话,直接返回。缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应。
- 更新的时候:先更新数据库,然后再删除缓存。(删除缓存可能删除不掉,要加入删除缓存重试机制,延迟删除或删除失败的key放入MQ重试删除)
Redis高可用
-
主从复制 + 哨兵:
- 主从复制:主节点写入数据并同步从节点,从节点读数据并同步主节点数据(游标标记复制偏移量),主节点下线,从节点顶上。(仅主从复制,主节点挂了需要程序员手动选择主节点提供写入)
- 多个哨兵-管理员:负责统筹协调从节点选主。隔10s用info命令问候主节点,判断主观下线,多个哨兵都认为下线则为客观下线启动故障转移。启动故障转移有三步
- 选新主,新主选择标准:优先级更高,复制偏移量最大
- 其他从节点从新主那同步
- 旧主改从节点
-
集群 + 主从复制
-
集群:多个节点写入
哈希槽16384,有一个大表存每个槽位对应的节点,每个节点写入时判断是否自己负责该槽位,不是则move给请求端(ip 端口) 给它
-
主从复制:集群的每个节点都有个从节点。不怕节点宕机。
-