面试问题

MQ

每个阶段保证消息可靠性传输(消息丢失怎么办)

  • 生产者弄丢了数据:开启生产者-确认模式或开启事务(不推荐),确保生产者的消息能到达队列,如果报错可以先记录到日志中,再去修复数据

  • RabbitMQ 弄丢了数据:开启持久化 交换机+队列+消息持久化,确保消息未消费前在队列中不会丢失

  • 消费端弄丢了数据:关闭 RabbitMQ 的自动 ack,重试次数,超过多少次将失败后的消息投递到异常交换机进行人工处理。

消费幂等

  • 全局唯一的 id
  • 数据库主键的唯一性

消费积压怎么办

我在实际的开发中,没遇到过这种情况,不过,如果发生了堆积的问题,解决方案也所有很多的

  1. 提高消费者的消费能力,可以使用多线程消费任务
  2. 增加消费者,提高消费速度 使用工作队列模式,设置多个消费者消费同一个队列的消息
  3. 扩大队列容积,提高堆积上限,可以使用RabbitMQ惰性队列,惰性队列的好处主要是①接收到消息后直接存入磁盘而非内存②消费者要消费消息时才会从磁盘中读取并加载到内存③支持数百万条的消息存储

MySQL面试题

B+树

B+树相较于B树做了几个方面的优化:

  • B+树的所有数据都存储在叶子节点,非叶子节点只存储索引
  • 叶子节点中的数据使用双向链表进行关联

使用B+树的好处

  • 非叶子节点只存索引:每一层能够存储的索引数量增加,同样多的数据树的层高要低,磁盘IO次数少。

  • 叶子节点存储数据且叶子节点是双向链表来关联:范围查询的时候只需要两个节点进行遍历,而B树需要获取所有节点。全局扫描能力强。

  • B+树这种结构,主键如果是自增整型,可以避免增加数据带来的叶子节点分裂导致的大量运算问题。

索引

索引失效

InnoDB引擎有两种索引类型,主键索引和普通索引。用B+树的结构来存储索引。当使用索引列进行数据查询的时候,最终会到主键索引树中查询对应数据进行返回。

导致索引失效的情况:

  1. 函数转换:索引列上做运算
  2. 类型隐式转换(字符串需要+引号)
  3. 组合索引顺序
  4. 违背最左前缀原则 like%xxx
  5. 区分度太低
  6. 索引列上使用 != 、not、or

索引规则

  • 覆盖索引:只需要在一棵索引树行就能获取SQL所需的所有列数据,无需回表,速度更快。
  • 回表:需要再到主键索引树上查询一次的过程。
  • 最左前缀原则:联合索引遵循此原则。如果建立了(a,b,c)的联合索引,相当于建立了(a,b)(a,c)(abc)
  • 索引下推:索引遍历的过程中,对索引包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

SQL优化

  • 硬件及操作系统层面的优化:避免资源浪费
    • 硬件层:影响MySQL的性能因素有,CPU、可用内存大小、磁盘读写速度、网络带宽
  • 架构设计层面的优化:磁盘IO访问量非常频繁
    1. 主从集群:单个MySQL节点容易单点故障,主从集群或者主主从集群可用保证服务的高可用性。
    2. 读写分离:读多写少,避免读写冲突。
    3. 分库分表:分库降低单个服务器节点的IO压力,分表降低单表数据量,从而提升SQL查询效率。
    4. 热点数据引入更高效的分布式数据库,Redis、MongoDB等。
  • SQL优化
    1. 慢SQL定位和排查:慢查询日志和慢查询日志分析工具得到有问题的SQL列表。
    2. 执行计划分析:explain查看当前SQL执行计划(type key rows filterd)
    3. 使用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机制实现的,如下:

  1. 读未提交/RU:写操作加排他锁,读操作不加锁。
  2. 读已提交/RC:写操作加排他锁,读操作使用MVCC,每次查询都会生成一个新的readview,每次查询都能拿到最新的数据,产生了不可重复读和幻读。
  3. 可重复读/RR:写操作加排他锁,读操作依旧采用MVCC机制,只有在第一次查询的时候会生成一个readview,后面查询比对的都是第一个查询的时候的结果,后面查不到新的值。
  4. 序列化/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 没有任何引用链相连时,则证明此对象是可以被回收的。

垃圾回收算法

  • 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
  • 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
  • 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
  • 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

并发

实现多线程的几种方法

  1. 继承Thread类;
  2. 实现Runnable接口;
  3. 实现Callable接口通过FutureTask包装器来创建Thread线程;
  4. 使用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命令问候主节点,判断主观下线,多个哨兵都认为下线则为客观下线启动故障转移。启动故障转移有三步
      1. 选新主,新主选择标准:优先级更高,复制偏移量最大
      2. 其他从节点从新主那同步
      3. 旧主改从节点
  • 集群 + 主从复制

    • 集群:多个节点写入

      哈希槽16384,有一个大表存每个槽位对应的节点,每个节点写入时判断是否自己负责该槽位,不是则move给请求端(ip 端口) 给它

    • 主从复制:集群的每个节点都有个从节点。不怕节点宕机。

posted @ 2024-12-24 14:38  三角形代表重生  阅读(16)  评论(0)    收藏  举报