Featured image of post Java面试手记

Java面试手记

Java面试手记

Redis面试经典案例

数据类型 特点 使用方式 典型应用场景
String
字符串
- 二进制安全,可存储任意数据(图片、序列化对象等)
- 最大容量512MB
- 可做整数自增/自减
SET key value
GET key
INCR key
MSET k1 v1 k2 v2
- 分布式锁
- 计数器(阅读量、点赞)
- Session共享
- 简单缓存
Hash
哈希表
- 键值对集合,适合存储对象
- 可单独操作字段,节省内存(相比String序列化存储)
HSET user:1 name "Tom" age 20
HGET user:1 name
HGETALL user:1
- 存储用户信息、商品详情
- 购物车(用户ID为key,商品ID为field,数量为value)
List
列表
- 双向链表结构,两端操作效率高
- 有序可重复
- 索引查询较慢(O(n))
LPUSH queue msg1
RPOP queue
LRANGE list 0 -1
- 消息队列(简单版)
- 最新消息/评论列表(如朋友圈时间线)
- 生产者-消费者模型
Set
集合
- 无序、不可重复
- 支持集合运算(交、并、差)
- 基于哈希表实现,查找O(1)
SADD tag:java "redis"
SMEMBERS tag:java
SINTER set1 set2
- 标签系统(共同关注、共同好友)
- 抽奖(随机抽取)
- 数据去重(如独立访客UV)
Sorted Set
有序集合
- 每个元素关联一个分数(score),按分数排序
- 元素唯一,分数可重复
- 基于跳表+哈希表,范围查询高效
ZADD rank 100 user1
ZRANGE rank 0 -1 WITHSCORES
ZREVRANK rank user1
- 排行榜(积分榜、热搜榜)
- 延迟队列(时间戳作为score)
- 带权重的任务调度
Bitmap
位图
- 本质是String,按位操作
- 节省空间,适合大规模布尔型统计
- 单个bit最大2^32位
SETBIT sign:2025 0 1
GETBIT sign:2025 0
BITCOUNT sign:2025
- 签到记录(365天占46字节)
- 活跃用户统计(每日登录)
- 布隆过滤器底层
HyperLogLog
基数统计
- 概率性数据结构,误差约0.81%
- 内存固定12KB
- 不存储元素本身,仅统计基数
PFADD uv:2025 user1
PFCOUNT uv:2025
PFMERGE total uv1 uv2
- 独立访客(UV)统计
- 搜索词唯一数量统计
- 大规模去重计数场景
Geospatial
地理空间
- 基于ZSet实现,存储经纬度
- 支持范围查询、距离计算
- 底层使用GeoHash编码
GEOADD city 116.40 39.90 "北京"
GEORADIUS city 100 30 1000 km
GEODIST city 北京 上海 km
- 附近的人/店铺
- 打车/外卖配送范围
- 地理位置距离计算
Stream
消息流
- 持久化消息队列(5.0+)
- 支持消费者组、消息确认
- 可追溯历史消息
XADD stream * msg "hello"
XREAD COUNT 2 STREAMS stream 0
XGROUP CREATE stream group1 0
- 可靠消息队列
- 事件溯源系统
- 数据管道(替代Kafka轻量场景)

缓存

缓存三兄弟(穿透,击穿,雪崩)

​ 1.缓存穿透:进行查询操作的时候,先查询redis,没有命中的话查询db,db如果查询到了数据,先写入redis进行缓存重构,然后返回数据,但是如果恶意攻击,大量请求不存在的数据,那么请求会一直打到数据库,数据库并发量小,会导致宕机,这就是缓存穿透 ​ 解决办法:1.使用布隆过滤器,布隆过滤器实际上是一个bitmap的数组,在启动redis时,先对布隆过滤器进行初始化,将key存储到布隆过滤器中,(原理是,会使用三个不同的hash算法,计算出三个不同的值,并将bitmap中的值改为1),查询请求到业务层的时候,会先进行判断,如果布隆过滤器中没有对应的key,那么直接返回null,但是布隆过滤器存在误判和内存占用率的问题,首先,如果计算出的hash值,正好与其他两个key计算出的值对应上了,那么也会判断为存在,进行查询,第二,想要误判率小,那么就要扩大bitmap这个数组的长度,增大了内存的占用,在使用布隆过滤器时,可以对数组长度和误判率进行初始化调整 ​ 2.将空值也缓存到redis中,即便数据库查询为空,也将对应的key和null存储到redis中,让下次请求不打到数据库,直接命中redis,但是会造成脏数据占用多的问题, ​ 个人觉得,小项目中使用方法二,大项目使用方法一

​ 2.缓存击穿:在进行查询操作的时候,如果热点数据突然过期,第一个请求会进行缓存重建,但是在缓存重建的过程中,如果有大量请求并发,会有大量并发请求到数据库,此时数据库承受不住高并发,会导致宕机,这就是缓存击穿。

​ 解决办法:1.使用互斥锁,在第一个请求到达业务层时,先进行判断,如果此时redis未命中,那么先获取锁,获取到锁之后查询数据库,并将数据库中的数据在redis中进行缓存重建,然后释放锁,返回数据。别的请求到达业务层后,redis未命中,同时获取锁失败,就会进行自旋,重新查询redis,直到获取到redis中的数据。优点是有强一致性,缺点是性能差。

​ 2.使用逻辑过期,也就是在redis中构建数据时,加上一个expire的过期字段,设置上过期时间,在第一个请求redis中命中数据之后,先检查一下逻辑过期时间,如果过期,那么获取锁,同时,新开一个线程,并返回过期数据,在新线程中,完成缓存重建。其他请求命中redis后,尝试获取锁,获取锁失败,直接返回当前旧数据。优点是性能好,高可用,但是不能保证数据强一致性

​ 个人觉得,使用逻辑过期更为合理,因为这样用户体验较好

​ 3.缓存雪崩:是指在同一时段,redis中有大量的key过期或者redis宕机,那么大量请求同时打到数据库,会给数据库带来巨大压力,可能导致数据库直接宕机,这就是缓存雪崩

​ 解决办法:1.给不同的key增加随机的TTL随机值

​ 2.如果是redis宕机,那么解决方案就是搭建redis集群提高服务的可用性(哨兵模式,集群模式)

​ 3.给缓存业务增加降级限流策略(nginx或者是spring cloud gateway)降级限流可以作为缓存所有问题的保底策略

​ 4.给业务添加多级缓存(Guava或Caffeine)但是这两个我都没了解过,多级缓存的话,我知道jvm有一个缓存机制,还有数据库也有自己的缓存机制

image-20251206175348720

双写一致性

​ redis作为缓存,mysql中的数据如何与redis中数据进行同步,这就是双写一致性

​ 首先,可以采用延迟双删的策略,就是先删除缓存,然后更改数据库,然后延时再删除一次缓存,这样后面查询的时候,拿到的就是数据库中最新的数据,但是延时多久拿,这个不太好确定

​ 还有一个问题就是,先删除缓存还是先删除数据库,都会有脏数据的出现,所以其实先后不重要

​ 1.允许延时一致的业务

​ 可以使用MQ等中间件,更新数据之后,通知缓存删除

​ 可以使用canal中间件,不需要修改业务代码,伪装为mysql的一个从节点,canal可以通过监听mysql的binlog文件,当所监听的表数据发生了修改操作时,会通知缓存进行删除

​ 2.要求数据强一致性的业务

​ 采用redisson提供的读写锁

​ 共享锁:读锁readlock,加锁之后,别的线程也可以进行读操作,但是不能进行写操作,如果进行写操作,也会阻塞读操作

​ 排他锁:writelock,也叫独占锁,加锁之后,别的线程无法进行读写操作,实际上底层也是 一个setnx,保证同一时间只有一个线程操作锁

缓存的持久化

redis作为缓存,提供了两种数据持久化的方式,RDB和AOF

RDB是一个二进制快照文件,将redis内存存储的数据写到磁盘上,当redis宕机需要新的实例时,从RDB快照文件中读取数据,但是这也会造成短时间的数据丢失,优点是文件小,数据恢复的速度快,但是占用的cpu和内存资源也较多

rdb执行原理:

image-20260326160717247

AOF是一个追加文件,也就是在redis进行写操作的时候,会将redis的操作命令记录下来,存储到磁盘中,当redis宕机时,会从这个文件中再执行一次命令,优点是数据基本不会丢失,但是恢复的速度慢

image-20260326160943568

两者对比:

image-20260326161009838

缓存数据过期策略

​ 1.惰性删除: 在需要使用到key的时候,先判断下其是否过期,如果过期,那么就删除当前key,这样的好处是,不会浪费cpu的资源来查询没有过期的key,坏处是,如果过期的key过多但是没有使用到,会占用内存

​ 2.定期删除:每隔一段时间,就对redis中的key进行检查,删除其中过期的key

​ SLOW模式:一个定时任务,执行的默认频率为10hz,每次不超过25ms,可以通过修改redis的配置文件的hz选项来进行调整

​ FAST模式:执行频率不固定,但是两次之间间隔不低于2ms,每次耗时不超过1ms

​ 优点:可以通过限制删除操作的时长和频率来减少操作对cpu的影响,定期删除,也能减少过期key对内存的占用

​ 缺点:难以确定删除操作的时长和频率

​ redis 的过期策略是,惰性删除+定期删除配合使用

缓存数据淘汰策略

​ 当redis中的内存不够用时,往redis中放入新的key,这时redis会按照某一种规则将内存中的数据删除掉,这就是缓存数据淘汰策略

​ redis中支持8种不同的策略来选择要删除的key

策略名称 淘汰范围 淘汰算法 适用场景 特点说明
noeviction 不淘汰 无淘汰 关键数据存储,数据安全优先 内存不足时拒绝写入,确保数据不丢失
allkeys-lru 所有键 LRU(最近最少使用) 通用缓存场景,全部数据可淘汰 近似LRU算法,采样淘汰最久未访问的键
volatile-lru 仅有过期时间的键 LRU(最近最少使用) 缓存与持久数据混合存储 只淘汰设置了过期时间的键中的LRU键
allkeys-random 所有键 随机选择 访问模式均匀的场景 从所有键中随机选择淘汰
volatile-random 仅有过期时间的键 随机选择 缓存数据随机访问模式 从有过期时间的键中随机选择淘汰
volatile-ttl 仅有过期时间的键 TTL(存活时间) 希望尽快释放过期键内存 淘汰剩余生存时间最短的键
allkeys-lfu 所有键 LFU(最不经常使用) 热点数据明显的场景 淘汰访问频率最低的键,保留热点数据
volatile-lfu 仅有过期时间的键 LFU(最不经常使用) 缓存数据中有明显热点 从有过期时间的键中淘汰访问频率最低的键

分布式锁

redisson实现分布式锁,其底层原理其实是setnx和lua脚本,可以保证原子性

在redisson提供的分布式锁中,可以通过看门狗机制来有效延长锁的持有时间,一个线程获取锁成功后,watchdog会给持有锁的线程进行续期,默认是每十秒续一次,避免业务还没完成,但是锁已经释放的情况

redisson的锁是可以实现重入的,因为redisson的锁在redis中使用的是hash结构,有一个大key,一个小key,大key可以自定义,但是小key就是当前线程的唯一id,如果是同一线程,那么就可以进行重入,一般需要使用到重入的情况,都是业务比较复杂

红锁可以解决redis主从数据一致的问题,但是性能很差,在redis集群中,通常使用的是主从集群结构,主节点一般负责写数据,从节点一般负责读数据,当有一台redis宕机之后,从节点成为主节点,当有一个线程获取到锁之后,主节点宕机,主节点还没来得及将数据同步到从节点,此时从节点成为了主节点,又有一个线程来获取锁,那么就会发生一把锁,两个线程持有的情况,不满足互斥锁的特性,会导致有脏数据,如果是需要主从数据一致性强的业务,建议使用zookeeper,zookeeper能保证数据的一致性

集群方案

单节点redis的并发能力是有限的,如果要提供redis的并发能力,那么就需要构建redis集群,实现读写分离,一般都是一主多从,主节点负责写数据,从节点负责写数据,这就是redis的主从同步

​ 1.主从复制

​ 全量同步:从节点执行replicaof命令,与主节点建立连接,从节点会向主节点发送一个replid和offset,replid是数据集id,id一致说明是同一个数据集,主节点先判断id是否与自己一致,如果不一致,那么说明是第一次同步,主节点返回自己的relid和当前的偏移量offset从节点,同时主节点执行bgsave,生成一个RDB文件,发送给从节点,在生成RDB的过程中,可能还会有命令执行,这时主节点会用一个repl_baklog的日志文件来记录RDB期间所有的命令,并发送给从节点,从节点将RDB文件加载后,再根据日志文件执行命令,offset就是日志文件中的偏移量,如果从节点的offset低于了主节点的offset,那么说明需要更新

​ 增量同步:从节点请求同步数据,如果不是第一次请求,那么获取到从节点的offset值,然后将repl_baklog中获取到offset值后的数据,发送给从节点,从节点执行命令,进行数据同步

​ 2.哨兵模式

​ 哨兵的作用,来实现主从集群的自动故障恢复 (监控,自动故障恢复,通知)

​ 哨兵模式的主要作用其实就是为了保证redis集群的高可用, 每一个sentinel都会去监听所有的实例,如果超过一般的哨兵都发现实例没有响应,那么哨兵就会认为这个实例已经宕机

​ 集群脑裂问题:当某一个时间段,sentinel和主节点和从节点不在一个网络分区,此时哨兵没有监测到主节点master的心跳,那么哨兵就会将从节点升为主节点,但是此时客户端的服务还在之前的主节点写入数据,当网络恢复之后,之前的主节点会强制降为从节点,清除数据,然后跟新的主节点同步数据,此时,就会造成大量数据的丢失,因为在同一时间段,出现了两个master,就像大脑分裂了一样,这就是集群脑裂问题

解决办法:可以通过修改redis的配置,可以设置最少的从节点数量,也就是如果当前master没有从节点,那么就拒绝请求,以及缩短主从数据同步的延迟时间,也就是master与slave之间的数据同步时间要短,如果出现脑裂,那么是没有办法同步数据的,如果达不到要求,那么也拒绝请求

​ 3.分片集群

​ 解决海量存储问题,高并发写的问题

​ 通过redis集群的分片,实现大量数据存储,集群中有多个master节点,每个master存储不同的数据,每个master还可以拥有自己的slave节点,可以形成主从集群的关系,因为有多个master,可以解决高并发写的问题,多个slave,也可以解决高并发读的问题,并且master之间还可以通过ping检测彼此健康状态,相当于自带了哨兵,客户端请求可以发送到任意节点,集群会自动路由,最终请求会转发到正确的节点

​ 因为redis分片集群引入了hash槽的概念,redis集群有16384个哈希槽,每个key通过crc16校验后对16384取模,决定放置于哪个槽,集群的每个节点负责一部分哈希槽

redis是单线程的,但是为什么还那么快

因为redis是纯内存操作,并且完成基于C语言完成,执行速度非常快,采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题,使用了多路复用IO模型,非阻塞IO

多路复用IO模型:使用单个线程同时监听多个socket,并在某个socket可读和可写时,得到通知,从而避免无效的等待,充分利用cpu资源,目前I/O多路复用普通采用的都是epoll模式,它会在通知用户进程socket准备就绪的同时,把已就绪的socket写入用户空间,不需要遍历socket来确定是否就绪,提升了性能

redis的网络模型:redis的网络模型就是使用了多路复用IO和任务派发的机制来应对多个socket请求

连接应答处理器

命令恢复处理器 在 redis 6.0之后,使用了多线程

命令请求处理器 在redis 6.0之后,将命令的转换使用了多线程,增加命令转换的速度,但是命令的执行还是单线程

MySQL面试经典案例

mysql的优化

定位慢查询

如聚合查询,多表查询,表数据量过大查询,深度分页查询(表象:查询时间过长,接口返回数据时间过慢)

方案1:开源工具

调试工具:Arthas 运维工具:Prometheus,Skywalking

方案2:mysql自带的慢日志(一般测试阶段使用,生产环境中会损失mysql的性能)

分析慢sql

MySQL EXPLAIN 工具字段详解表

字段 说明 常见值/含义 优化建议
id 查询标识符 1. 数字:执行顺序(越大越先执行) 2. 相同id:从上到下执行 3. 不同id:id大的先执行 用于理解复杂查询的执行顺序
select_type 查询类型 1. SIMPLE:简单查询(无子查询/UNION) 2. PRIMARY:主查询 3. SUBQUERY:子查询 4. DERIVED:派生表(FROM子句中的子查询) 5. UNION:UNION中的第二个及以后查询 6. UNION RESULT:UNION结果 识别复杂查询结构,优化子查询和派生表
table 访问的表 表名或别名,表示派生表 确认查询涉及的具体表
partitions 匹配的分区 分区表使用的分区名称 分区裁剪优化
type 访问类型(关键指标) 性能从好到差: 1. system:系统表,仅一行 2. const:通过主键/唯一索引查找 3. eq_ref:关联查询,使用唯一索引 4. ref:使用非唯一索引查找 5. range:索引范围扫描 6. index:全索引扫描 7. ALL:全表扫描 尽量避免ALL和index,优化为range或ref
possible_keys 可能使用的索引 查询可能使用的索引列表 检查是否有合适的索引未被使用
key 实际使用的索引 实际选择的索引,NULL表示未使用索引 对比possible_keys,确认索引选择是否合理
key_len 索引长度 使用的索引字节数,可判断索引使用情况 复合索引中查看是否充分利用索引
ref 索引引用 显示索引的哪一列被使用 检查关联查询的索引使用
rows 预估扫描行数 预估需要检查的行数 数值越大性能越差,考虑优化索引
filtered 过滤百分比 存储引擎返回数据在服务器层过滤的比例(0-100) 值越小表示过滤效果越好,但大量数据过滤可能需优化
Extra 额外信息(关键指标) 常见值: 1. Using index:覆盖索引 2. Using where:服务器层过滤 3. Using temporary:使用临时表 4. Using filesort:文件排序 5. Using join buffer:使用连接缓冲区 6. Impossible WHERE:WHERE条件不可能满足 关注Using filesort/temporary,这些通常需要优化

如果一条sql执行很慢的话,我们通常会使用mysql自动的执行计划explain来去查看这条sql的执行情况,比如在这里面可以通过key和key_len检查是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况。第二个,可以通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描,第三个可以通过extra建议来判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复

索引

什么是索引:索引 (index) 是帮助MysoL高效获取数据的数据结构 (有序) 。在数据之外,数据库系统还维护着满足特定查找算法的数据结构 (B+树) ,这些数据结构以某种方式引用 (指向) 数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。

索引的底层数据结构是什么:

MySQL的默认的存储引擎InnoDB采用的B+树的数据结构来存储索引,选择B+树的主要的原因是:第一阶数更多,路径更短,第二个磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据,第三是B+树便于扫库和区间查询,叶子节点是一个双向链表

B树和B+树的区别是什么呢?

第一:在B树中,非叶子节点和叶子节点都会存放数据,而B+树的所有的数据都会出现在叶子节点,在查询的时候,B+树查找效率更加稳定

第二:在进行范围查询的时候,B+树效率更高,因为B+树都在叶子节点存储,并且叶子节点是一个双向链表

聚集索引和非聚集索引(二级索引)

​ 什么是聚簇索引什么是非聚簇索引?

  • 聚簇索引(聚集索引):数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个
  • 非聚簇索引(二级索引):数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个

​ 什么是回表查询?

​ 通过二级索引找到对应的主键值,到聚集索引中查找整行数据,这个过程就是回表

覆盖索引:覆盖索引是指查询使用了索引,返回的列,必须在索引中全部能够找到

  • 使用id查询,直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。
  • 如果返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用select *

mysql超大分页怎么处理:

在数据量比较大时,limit分页查询,需要对数据进行排序,效率低,此时使用覆盖索引+子查询,先在子查询中查询id,因为id是覆盖索引,所以在索引中查询效率快,底层是B+树,所以其实范围查询效率是快的,然后将返回的id集合,再到原来的表中做关联查询,能提高很多效率

索引的创建原则:

  • 针对于数据量较大,且查询比较频繁的表建立索引。
  • 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。
  • 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
  • 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。
  • 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。
  • 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。
  • 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。

索引失效的场景:

索引失效情况及其解释:

索引失效情况 解释
违反最左前缀法则 在使用联合索引时,查询条件必须从索引的最左列开始,否则索引将无法生效。
范围查询右边的列,不能使用索引 当查询条件中使用了范围查询(如 ><BETWEEN)后,其右边的列将无法使用索引进行进一步筛选。
不要在索引列上进行运算操作,索引将失效 如果在索引列上进行函数运算、算术运算或表达式操作,数据库将无法直接使用该索引进行查询。
字符串不加单引号,造成索引失效(类型转换) 如果字符串类型的列在查询时未加引号,数据库可能会进行隐式类型转换,导致索引无法使用。
% 开头的 Like 模糊查询,索引失效 如果使用 LIKE '%xxx' 这种以通配符开头的模糊查询,索引将无法被有效利用,通常只支持 LIKE 'xxx%' 使用索引。

sql优化

优化方向 具体措施
1. 表的设计与数据类型选择 - 合理设计表结构,遵循范式与反范式平衡
- 选择合适数据类型(如用 INT 代替 BIGINT)
- 尽量使用 NOT NULL 约束
- 合理使用分区表
2. 索引优化 - 在 WHERE、ORDER BY、GROUP BY 相关列创建索引
- 优先使用联合索引
- 为区分度高的列建索引
- 控制索引数量,定期清理冗余索引
3. SQL 语句优化 - 避免 SELECT *,只取所需字段
- 使用 EXPLAIN 分析执行计划
- 避免 WHERE 中对字段进行函数运算
- 用 INNER JOIN 替代子查询(合适时)
- 优化分页查询,避免大数据量翻页
4. 主从复制与读写分离 - 配置主从复制,读操作分流至从库
- 使用读写分离中间件或框架
- 避免写操作阻塞读操作
- 监控主从同步延迟
5. 分库分表 - 水平分表(按时间、ID 范围等)
- 垂直分表(按业务模块拆分字段)
- 分库(按业务模块分库)
- 使用分库分表中间件(如 ShardingSphere)
- 合理设计分片键,处理跨库查询

事务

什么是事务:事务是一组操作的集合,它是一个不可分割的工作单位,系统会将这些操作一起提交或者撤销,即同时成功或者同时失败

ACID:

原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。

一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。

隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。

持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

事务并发问题:

问题 描述
脏读 一个事务读取到另一个事务尚未提交的数据。
不可重复读 一个事务先后读取同一条记录,但两次读取的数据不同。
幻读 一个事务按照条件查询数据时未找到对应行,但在插入数据时又发现该行已存在,仿佛出现“幻影”。

事务隔离级别:

隔离级别 中文说明 可能存在的问题
READ UNCOMMITTED 未提交读 脏读、不可重复读、幻读
READ COMMITTED 读已提交 不可重复读、幻读
REPEATABLE READ 可重复读 幻读
SERIALIZABLE 串行化

实际上可重复读的隔离级别可以解决部分幻读问题

在 MySQL InnoDB 引擎的“可重复读(REPEATABLE READ)”隔离级别下,幻读问题在实际应用中被有效解决了,但这属于数据库对标准隔离级别的增强实现,而非 SQL 标准的强制要求

  • 从 SQL 标准来看:可重复读级别本身允许幻读。
  • 从 MySQL InnoDB 实现来看:它通过 MVCC(多版本并发控制)间隙锁(Next-Key Lock) 两种机制的协同作用,分别解决了“快照读”和“当前读”场景下的幻读问题:
    1. MVCC:确保事务内普通的 SELECT 查询(快照读)基于一致性视图,看不到其他事务插入的数据。
    2. 间隙锁:对涉及范围查询的加锁操作(如 SELECT ... FOR UPDATE),会锁住记录之间的“间隙”,阻止其他事务插入新记录。

因此,虽然从严格意义上讲,标准并未强制要求可重复读解决幻读,但 InnoDB 的实际实现已经将幻读风险降至极低,可以认为在该级别下幻读被“有效防御”了。这也是 MySQL 选择 REPEATABLE READ 作为默认隔离级别的重要原因。

redolog和undolog:

redolog:在数据库存储层面,实际上存储分为两个层次,缓冲层和磁盘层,缓存层面是基于内存的,会将磁盘层中的数据页加载到缓冲层中便于数据的查询和修改,而当进行增删改查的操作时,首先会进入到缓冲层中查询数据页,然后进行修改,缓存层再将修改后的数据同步到磁盘层,在这中间,就有redolog bufffer来记录数据页的变化,然后同步到磁盘层,但是如果还未同步服务器就宕机了,那么就会造成数据的丢失,所以在事务提交之后,会将缓冲中的所有修改信息存到redolog file日志文件中,用于刷新脏页到磁盘时发生错误,进行数据恢复使用,这就是redolog

undolog:回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚 和 MVCC(多版本并发控制)。undo log和redo log记录物理日志不一样,它是逻辑日志。

  • 可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然。
  • 当update一条记录时,它记录一条对应相反的update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。

undo log和redo log的区别

  • redo log:记录的是数据页的物理变化,服务宕机可用来同步数据
  • undo log:记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据
  • redo log保证了事务的持久性,undo log保证了事务的原子性和一致性

保证事务的隔离性:

锁:排他锁(一个事务获取了数据行的排他锁,那么别的事务就不能获取数据行的其他锁)

MVCC:多版本并发控制,维护一个数据的多个版本,使得读写没有冲突

在数据库数据行中,实际上还隐藏了几个字段

隐藏字段 含义
DB_TRX_ID 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。
DB_ROLL_PTR 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。
DB_ROW_ID 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。

在事务进行的时候,会在trx_id生成一个id,记录每一次操作的事务id,是自增的,而回滚指针roll_pointer指向的是上一个版本的事务版本地址,undolog有两个作用,回滚日志,存储老版本的数据,同时,它也会存储一个版本链,一个链表,将历史的事务版本串联起来,在事务同时进行并发的时候,如果读取数据,那么会从readview中读取数据

  • readview

ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。

  • 当前读

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select … lock in share mode(共享锁),select … for update、update、insert、delete(排他锁)都是一种当前读。

  • 快照读

简单的select(不加锁)就是快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。

  • Read Committed:每次select,都生成一个快照读。
  • Repeatable Read:开启事务后第一个select语句才是快照读的地方。

readview中有这么几个字段

字段 含义
m_ids 当前活跃的事务ID集合
min_trx_id 最小活跃事务ID
max_trx_id 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的)
creator_trx_id ReadView创建者的事务ID

会根据规则判断,当前事务能读取到哪个版本的数据

image-20251212195602912

这样就能避免读写冲突的问题

readView解决的是一个事务查询选择版本的问题,根据readView的匹配规则和当前的一些事务id判断该访问那个版本的数据

不同的隔离级别快照读是不一样的,最终的访问的结果不一样

RC:每一次执行快照读时生成ReadView RR:仅在事务中第一次执行快照读时生成ReadView,后续复用

主从同步原理

MySQL主从复制的核心就是二进制日志binlog(DDL语句和DML语句)

① 主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中

② 从库通过IOthread线程读取主库的二进制日志文件Binlog,写入到从库的中继日志Relay Log。

③ 从库通过sqlthread线程重做中继日志中的事件,将改变反映它自己的数据

这就是mysql的主从同步原理

分库分表

以下是关于数据库分库分表策略的整理表格,结合了您的文本内容和相关知识:

策略类型 核心概念 主要目的 适用场景 优缺点 常见工具
垂直分库 业务模块拆分数据库,不同业务使用不同数据库 1. 解耦业务
2. 提高并发处理能力
3. 分散磁盘IO和网络连接压力
1. 业务模块清晰、耦合度低
2. 并发量高
3. 不同业务数据量差异大
优点:
- 业务解耦,便于维护
- 针对不同业务优化
- 降低单库连接数压力
缺点:
- 无法解决单表数据量过大问题
- 跨库查询复杂(需业务层或中间件支持)
ShardingSphere
MyCat
垂直分表 将一张表按字段使用频率拆分(冷热分离) 1. 减少单表字段数
2. 提高查询效率
3. 避免大字段影响性能
1. 表字段多,且有明显冷热区分
2. 某些字段占用空间大(如BLOB/TEXT)
3. 频繁查询的字段较少
优点:
- 提升热点数据查询速度
- 减少磁盘IO
- 表结构更清晰
缺点:
- 查询需关联多表
- 事务处理复杂
通常由ORM框架或业务代码实现
水平分库 同一业务数据按规则拆分到多个数据库 1. 解决海量数据存储问题
2. 分散写压力,提高并发
1. 单库数据量接近物理极限
2. 高并发写入场景
3. 数据持续快速增长
优点:
- 根本性解决数据量瓶颈
- 大幅提升并发处理能力
缺点:
- 跨库查询、事务、排序非常复杂
- 数据迁移和扩容难度大
ShardingSphere
MyCat
水平分表 同一张表的数据按规则拆分到多个结构相同的表中 1. 解决单表数据量过大问题
2. 提升查询和写入性能
1. 单表数据量过大(如千万级)
2. 索引效率下降
3. 维护困难(备份、DDL操作慢)
优点:
- 提升单表操作性能
- 维护相对简单(同库)
缺点:
- 仍在同一库,受限于单库资源
- 跨表查询复杂
ShardingSphere
MyCat

何时需要考虑分库分表?

  • 单表数据量 > 千万级 且性能明显下降
  • 数据库服务器CPU/IO使用率持续高位
  • 业务增长迅速,预计数据量将持续暴涨

框架面试经典案例

spring-单例bean是线程安全的吗?

不是线程安全的 Spring框架中有一个@Scope注解,默认的值就是singleton,单例的。

因为一般在spring的bean的中都是注入无状态的对象,没有线程安全问题,如果在bean中定义了可修改的成员变量,是要考虑线程安全问题的,可以使用多例或者加锁来解决

当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中针对该单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。

Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。

比如:我们通常在项目中使用的Spring bean都是不可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。

如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用域由“singleton”变更为“prototype”。

spring-aop相关面试题(待优化)

什么是AOP 面向切面编程,是一种编程范式,它的核心思想是:将那些遍布在多个类或方法中、与核心业务逻辑无关的公共功能(称为“横切关注点”)剥离出来,进行模块化设计和复用。

你们项目中有没有使用到AOP 记录操作日志,缓存,spring实现的事务

核心是:使用aop中的环绕通知+切点表达式(找到要记录日志的方法),通过环绕通知的参数获取请求方法的参数(类、方法、注解、请求方式等),获取到这些参数以后,保存到数据库

Spring中的事务是如何实现的 其本质是通过AOP功能,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

AOP与注解核心知识表格

Spring AOP核心注解表

注解 作用 使用位置 常用参数/值 使用场景
@Aspect 声明一个类是切面类 类上 标注切面类,Spring自动识别
@Pointcut 定义切入点表达式 方法上 切入点表达式字符串 定义哪些方法需要被增强
@Before 前置通知 方法上 value(切入点) 方法执行前执行,用于参数检查、日志
@After 后置通知 方法上 value(切入点) 方法执行后执行(无论异常),用于资源清理
@AfterReturning 返回通知 方法上 value(切入点), returning(返回值变量名) 方法成功返回后执行,可获取返回值
@AfterThrowing 异常通知 方法上 value(切入点), throwing(异常变量名) 方法抛出异常后执行,可获取异常信息
@Around 环绕通知 方法上 value(切入点) 最强大,包裹整个方法,控制是否执行

Java元注解(定义自定义注解用)

注解 作用 常用值 说明
@Target 指定注解可以应用的位置 ElementType.METHOD(方法)
ElementType.TYPE(类/接口)
ElementType.PARAMETER(参数)
ElementType.FIELD(字段)
可组合使用,如{ElementType.METHOD, ElementType.TYPE}
@Retention 指定注解的生命周期 RetentionPolicy.SOURCE(源码级)
RetentionPolicy.CLASS(字节码级)
**RetentionPolicy.RUNTIME**运行时级
AOP必须用RUNTIME
@Documented 是否包含在JavaDoc中 无参数 可选,提高文档质量
@Inherited 是否允许子类继承 无参数 子类会继承父类的注解

常用切入点表达式

表达式类型 语法格式 示例 匹配对象
execution execution(修饰符 返回类型 包.类.方法(参数)) execution(* com.service.*.*(..)) Service包下所有方法
@annotation @annotation(注解类型) @annotation(com.Log) 带@Log注解的方法
@within @within(注解类型) @within(org.springframework.stereotype.Service) 带@Service注解类的方法
within within(包路径) within(com.controller..*) controller包及子包下所有类
args args(参数类型) args(java.lang.String) 参数为String类型的方法

AOP核心概念对照表

概念 定义 对应Spring注解/类 示例
切面 封装横切关注点的模块 @Aspect标注的类 LogAspect
连接点 程序执行中可以插入切面的点 Spring中指方法执行 UserService.getUser()
通知 切面在连接点执行的动作 @Before, @After logBefore()方法
切点 匹配连接点的表达式 @Pointcut定义的方法 controllerMethods()
织入 将切面应用到目标对象的过程 Spring自动完成 运行时动态代理

Spring事务与AOP关系

组件 实现方式 对应AOP概念
Spring事务 通过AOP实现 环绕通知
@Transactional 自定义注解 @annotation切入点
事务管理器 切面中的增强逻辑 通知实现类
事务属性 注解中的参数 切面获取注解值

原理
@Transactional → 被AOP拦截 → 执行环绕通知 → 开启事务 → 执行业务方法 → 提交/回滚事务


快速配置步骤

步骤 操作 代码示例
1 添加依赖 <artifactId>spring-boot-starter-aop</artifactId>
2 创建切面类 @Component @Aspect public class LogAspect {}
3 定义切入点 @Pointcut("execution(* com..service.*.*(..))")
4 编写通知 @Around("pointcutMethod()") public Object log(...)
5 使用注解 在业务方法上添加自定义注解

注意事项

要点 说明
代理限制 Spring AOP基于代理,同类调用不会触发AOP
性能影响 过多AOP会影响性能,避免过度使用
执行顺序 多个切面使用@Order注解指定顺序
异常处理 @Around中需要捕获异常并重新抛出
RUNTIME必需 自定义注解必须用@Retention(RetentionPolicy.RUNTIME)

spring中事务失效的场景

pring 事务的实现核心是 AOP(面向切面编程)+ 动态代理,核心流程如下:

  1. 当你在方法上标注@Transactional时,Spring 启动时会扫描到这个注解,为目标类创建动态代理对象(JDK 动态代理或 CGLIB 代理)。
  2. 当调用代理对象的事务方法时,代理逻辑会先执行:
    • 开启事务(创建数据库连接,设置autoCommit=false);
    • 调用目标方法的真实逻辑;
    • 如果目标方法正常执行,提交事务;如果抛出未捕获的异常(默认是 RuntimeException/Error),回滚事务;
    • 最终释放数据库连接。
  3. 整个过程的关键:事务的控制逻辑在代理层,而非目标方法内部
事务失效场景 底层原理 解决方案
异常被内部捕获且未抛出 Spring 事务回滚需代理层捕获到指定异常,内部捕获后代理层判定 “执行成功”,触发提交 手动抛出异常(如throw new RuntimeException()
抛出检查异常未配置 rollbackFor Spring 默认仅对 RuntimeException/Error 回滚,检查异常(如 IOException)不在默认范围内 @Transactional中配置rollbackFor = Exception.class(覆盖默认规则)
非 public 方法标注事务 Spring 动态代理(JDK/CGLIB)仅对 public 方法生成事务增强逻辑,非 public 方法无代理层 将方法改为 public
同类中非事务方法调用事务方法 内部调用是目标对象自身调用,未经过代理对象,代理层的事务逻辑不生效 1. 注入自身代理对象调用;2. 通过 AopContext 获取代理对象调用;3. 拆分方法到不同类
事务方法被 final/private 修饰 CGLIB 代理需重写方法,final 方法无法重写,private 方法对子类不可见 去掉 final 修饰,方法改为 public/protected
数据源未配置事务管理器 Spring 事务依赖事务管理器(如 DataSourceTransactionManager)绑定连接,无配置则无法控制事务 配置对应数据源的事务管理器(如@Bean public DataSourceTransactionManager txManager(DataSource ds) { ... }
传播行为配置错误(如 NOT_SUPPORTED) 传播行为决定事务执行规则,NOT_SUPPORTED 会以非事务方式执行 根据业务需求选择正确的传播行为(如默认的 REQUIRED)
多线程调用事务方法 事务连接通过 ThreadLocal 绑定当前线程,新线程无原事务连接,操作独立 避免多线程拆分事务操作;若需多线程事务,用分布式事务框架(如 Seata)
数据库不支持事务(如 MyISAM 引擎) 事务最终依赖数据库引擎支持,不支持的引擎无法保证 ACID

spring的bean的生命周期

生命周期阶段 核心操作 底层原理 实际能做的事(面试话术)
① 获取 Bean 定义信息 通过BeanDefinition获取 Bean 的类名、作用域、依赖等元信息 Spring 解析 XML / 注解(如@Component)后,将 Bean 信息封装为BeanDefinition并注册到容器的BeanDefinitionRegistry 了解 Bean 的元数据机制,能解释 “Spring 如何识别开发者定义的 Bean”
② 实例化 Bean 调用构造函数创建 Bean 对象 单例 Bean 默认容器启动时通过反射实例化(Class.newInstance()Constructor.newInstance());多例 Bean 每次getBean()时实例化 可通过@Lazy延迟单例 Bean 实例化,优化容器启动速度
③ 依赖注入 为 Bean 的属性赋值(注入其他 Bean / 基本类型) 通过AutowiredAnnotationBeanPostProcessor解析@Autowired,用反射Field.set())或 setter 方法填充属性;单例 Bean 通过三级缓存解决循环依赖 能处理@Autowired注入逻辑,理解循环依赖的解决机制
④ 处理 Aware 接口 回调BeanNameAware/BeanFactoryAware/ApplicationContextAware的方法 容器检测到 Bean 实现 Aware 接口后,主动调用接口方法传递容器对象 / Bean 名称 实现BeanNameAware获取 Bean 在容器中的名称;实现ApplicationContextAware直接访问 Spring 上下文
⑤ BeanPostProcessor - 前置 执行所有BeanPostProcessorpostProcessBeforeInitialization() Spring 遍历容器中注册的BeanPostProcessor,逐个执行前置方法(责任链模式) 自定义 Bean 的前置增强,比如对 Bean 属性做统一加密 / 校验
⑥ 初始化方法 执行InitializingBean.afterPropertiesSet()或自定义init-method/@PostConstruct 接口方法由容器直接回调,init-method通过反射调用;@PostConstructCommonAnnotationBeanPostProcessor触发(JSR-250 规范) 在初始化阶段完成资源加载(如数据库连接)、配置初始化等业务逻辑
⑦ BeanPostProcessor - 后置 执行所有BeanPostProcessorpostProcessAfterInitialization() 同前置处理器的责任链机制,AOP 代理在此阶段创建(如AnnotationAwareAspectJAutoProxyCreator 实现 AOP 动态代理、对 Bean 做最终增强(比如包装为代理对象)
⑧ 销毁 Bean 执行DisposableBean.destroy()或自定义destroy-method/@PreDestroy 容器关闭时,单例 Bean 由DefaultSingletonBeanRegistry触发销毁;多例 Bean 由 GC 回收,Spring 不管理 在销毁阶段释放资源(如关闭数据库连接池)、执行收尾逻辑

spring中的循环依赖问题

一、什么是循环依赖?

循环依赖是指两个或两个以上的 Bean 互相持有对方,最终形成闭环依赖关系。例如:

  • A 依赖于 B
  • B 依赖于 A

这种互相依赖的情况在 Spring 中被称为“循环引用”。

二、Spring 如何处理循环依赖?

Spring 框架允许存在循环依赖,并通过三级缓存机制解决了大部分循环依赖问题。

三级缓存机制

缓存级别 名称 存储内容 作用
一级缓存 单例池 已经历完整生命周期、初始化完成的 Bean 对象 存放最终可用的 Bean
二级缓存 早期对象缓存 生命周期尚未走完的早期 Bean 对象 存放半成品的 Bean,用于解决循环依赖
三级缓存 ObjectFactory 缓存 缓存 ObjectFactory 对象,用于创建代理对象 存放 Bean 工厂,可生成代理对象

三、循环依赖解决流程(以 A、B 为例)

  1. 实例化 A

    • Spring 创建 A 的原始对象
    • 生成一个 ObjectFactory 对象(放入三级缓存)
  2. A 需要注入 B

    • 发现 B 对象不存在
    • 开始创建 B 对象
  3. 实例化 B

    • B 创建过程中发现需要注入 A
    • 从三级缓存中获取 A 的 ObjectFactory
    • 通过 ObjectFactory 获取 A 的早期对象(可能是代理对象)
  4. 完成依赖注入

    • 将 A 的(代理)对象注入给 B
    • B 创建成功
  5. 完成 A 的创建

    • 将创建好的 B 对象注入给 A
    • A 创建成功
    • 将 A 从二级/三级缓存升级到一级缓存

四、特殊场景:构造方法循环依赖

问题描述

当循环依赖发生在构造方法注入时:

  • A 通过构造函数依赖于 B
  • B 通过构造函数依赖于 A

为什么构造方法循环依赖无法解决?

  1. 实例化顺序问题:Spring 在调用构造函数时,Bean 尚未完全创建,无法放入缓存
  2. 设计限制:Spring 的三级缓存机制主要解决属性注入的循环依赖,构造器注入在设计上就不支持循环依赖

解决方案

方案一:改为 Setter/Field 注入(推荐)

将构造函数注入改为属性注入或 Setter 方法注入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Component
public class A {
    private B b;
    
    @Autowired
    public void setB(B b) {
        this.b = b;
    }
}

@Component
public class B {
    private A a;
    
    @Autowired
    public void setA(A a) {
        this.a = a;
    }
}

方案二:使用 @Lazy 注解

在构造函数参数上添加 @Lazy 注解,延迟加载依赖:

方案三:重新设计代码结构

  • 提取公共逻辑到第三个类中

  • 使用接口分离依赖

  • 考虑是否真的需要循环依赖,可能是设计问题

    方案四:使用 ApplicationContext

1
2
3
4
5
6
7
8
9
@Component
public class A {
    private B b;
    
    public A(ApplicationContext context) {
        // 在需要时才获取 B
        this.b = context.getBean(B.class);
    }
}

五、总结要点

  1. Spring 支持:Spring 通过三级缓存机制支持属性注入的循环依赖

  2. 不支持的情况构造方法注入的循环依赖不被支持,会抛出 BeanCurrentlyInCreationException

  3. 设计建议

    • 优先使用 Setter 或 Field 注入而非构造器注入
    • 尽量避免循环依赖,考虑代码重构
    • 必要时使用 @Lazy 注解延迟加载
  4. 原理核心:Spring 通过“提前暴露”尚未完全初始化的 Bean 引用(通过三级缓存),打破循环依赖的死锁状态。

SpringMVC-执行流程

视图阶段(jsp)

image-20260327190321363

前后端分离阶段

image-20260327190444892

image-20260327190637366

Springboot自动配置原理

1,在Spring Boot项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

2,其中@EnableAutoConfiguration是实现自动化配置的核心注解。该注解通过@lmport注解导入对应的配置选择器。内部就是读取了该项目和该项目引用的Jar包的的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名。在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。

3,条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。

Spring框架常用的注解

image-20260327191440439image-20260327191450118image-20260327191455451

mybaitis执行流程

  • 读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件
  • 构造会话工厂SqlSessionFactory
  • 会话工厂创建SqlSession对象(包含了执行SQL语句的所有方法)
  • 操作数据库的接口,Executor执行器,同时负责查询缓存的维护
  • Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
  • 输入参数映射
  • 输出结果映射

image-20260328180143385

mybatis的延迟加载

image-20260328183713168

SpringCloud

SpringCloud的五大组件是什么

image-20260401171846420

Eureka或者nacos作为注册中心,将服务端注册到注册中心中,如果使用的是nacos,那么还可以作为配置中心,用来存放公用的配置,feign负责的是微服务之间的互相调用,Hystrix负责的是服务熔断,gateway作为网关,进行负载均衡

image-20260401172128166

注册中心nacos和Eureka

image-20260401173856674

image-20260401173837867

AP是高可用模式,CP是强一致模式

ribbon负载均衡

image-20260401180421437

服务雪崩,熔断降级

image-20260401181917757

微服务是怎么进行监控的

image-20260401183931829

我们主要是采用市面上比较常用的监控链路工具,例如skywalking,它可以监控到各个服务之间的接口调用,物理机实例,在进行压测的时候就可以从控制面板上获取到各个接口和服务的调用时长,也可以获取到sql的调用时间,从而快速定位到问题,还有告警规则,在项目上线之后,如果服务出现了问题,达到了告警规则的边界,可以给相关负责人发送短信或者邮件,知道项目的bug情况,第一时间修复

微服务限流

image-20260401185157427

限流的话常见的算法有两种,漏桶算法和令牌桶算法,漏桶算法的话,请求是以固定的速率进行请求的,可以设定桶的容量以及每秒处理的请求数,令牌桶的话就是,设置桶的容量以及每秒可以生成的令牌数量,但是令牌是每秒都在一直生成的,如果没有请求进来,会进行保存,也就是说,假如一秒内可以生成三个令牌,桶里已经有了三个,那么一秒内就可以通过六个请求

如果进行限流操作的话,首先对整个系统进行压测,测试出当前系统所能承受的qps,然后设置最大请求连接数,首先在nginx中进行限流,当请求到达nginx的时候,先进行一次请求过滤,用漏桶算法处理请求,先过滤掉一层请求,请求到达服务端之后,再使用gateway网关进行过滤,使用固定的速率处理请求,最终到达服务端的请求数量就已经很小了,避免掉因为请求数量过多导致整个系统发生崩溃

CAP问题和BASE理论

CAP是指 Consistency(一致性) Availability(可用性) Partition tolerance (分区容错性)

因为是分布式系统,所以P(分区容错性)是一直会存在的,但是只能同时存在cp或者ap,二者只能存在一个,不能同时满足,而base理论就是用来解决cap问题的一种解决思路

image-20260405183222366

image-20260405183534685

分布式事务的解决方案

image-20260405191622716

首先是seata架构,它是alibaba提供的一个开源的分布式事务管理组件

image-20260405191305582

XA模式,保证事务的强一致性,实际上就是实现了CAP中的CP模式、

image-20260405191345570

AT模式,保证了服务之间的可用性,实际上就是实现了CAP中的AP模式

image-20260405191457718

TCC模式

image-20260405191516039

使用MQ作为分布式事务的解决方案

image-20260405191535330

如何保证接口的幂等性

image-20260406030136316

分布式任务调度-XXL-JOB

image-20260406031044274

xxl-job本质上就是一个任务调度中心,它支持很多种任务调度策略,例如轮询,lru,lfu,故障转移,分片广播,它是目前最常用的分布式任务调度组件

消息中间件

RabbitMQ

如何保证消息不丢失

image-20260406031957049

image-20260406032010903

image-20260406032021512

image-20260406032053980

如何解决MQ重复消费的情况

1.每条消息设置一个唯一的业务id

2.使用幂等性方案(分布式锁,数据库锁(乐观锁,悲观锁))

RabbitMQ-死信交换机

image-20260406190929777

怎么解决MQ中消息堆积的问题

首先我们知道,MQ中消息堆积的问题场景,当生产者发送大量消息,此时消费者的消费速度跟不上发送速度,就会造成消息的堆积,如果超出了队列上限,最先发送的消息会被发送到死信交换机,无法正常消费,一般来说,解决消息大量堆积的方法有三种

image-20260406191413470

RabbitMQ的高可用机制

RabbitMQ的高可用机制实际上也是采用了集群的思路,一般有三种,普通集群,镜像集群,仲裁集群

普通集群的方案,如果有一个节点宕机,就会造成消息的丢失,所以一般来说都会采用镜像集群的模式,镜像集群的模式类似于主从,主节点来完成所有操作,镜像节点去同步数据,如果主节点宕机,那么镜像节点就会成为新的主节点,但是如果消息数据还没来得及同步就宕机了,也会造成消息的丢失,如果对于消息一致性要求很高,这时可以采用仲裁队列来代替镜像队列,它是MQ3.8版本之后更新的一个新功能,采用了Raft协议,可以保证消息的强一致性,配置更改也很简单,只需要在队列创建时,声明这个队列是仲裁队列即可

image-20260406192303616

Kafka

kafka如何保证消息不丢失

//todo

常见集合篇

算法复杂度分析

image-20260407141751764

image-20260407142659557

List相关面试题

什么是数组

image-20260407144310693

ArrayList底层实现原理

image-20260407151351964

image-20260407151403335

如何实现数组和List之间的转换

如果是数组转换为list,使用jdk中java.util.Arrays的工具类中的aslist方法来进行转换

如果是list转换为数组,那么使用list中的toArray方法,无参toArray方法返回的是一个Object数组,如果需要返回对象数组,需要在方法中传入初始化长度的数组对象

但是要注意的是,aslist方法实际上底层只是对于原数组的一个引用,如果修改了原数组的元素数据,那么list中数据也会跟着改变。

toArray方法底层是创建了一个新的数据,所以数据不会受原list的影响

image-20260407152833175

ArrayList和LinkedList的区别

image-20260407154202218

image-20260407154220428

保证线程安全,这个场景问题我们当时遇到过,当时项目上线的时候,苹果官方要求给一个账号,他们去测试,因为我们当时是没有明文密码进行登录的,只有手机验证码登录,所以当时在登录方法中临时添加了一个list,用来存储不会被校验的手机号,使用固定的验证码,当时为了保证线程安全,首先是将队列定义在了方法内,但是因为这种情况可能会造成OOM内存泄漏,所以后面更改为了一个final对象,不允许更改,实际上也保证了线程安全

二叉树

image-20260407160901922

红黑树

红黑树的五个性质

image-20260407161351842

红黑树的复杂度

image-20260407161417770

散列表

image-20260407162607362

HashMap的底层实现原理

image-20260407162926120

image-20260407162937447

HashMap的put方法的具体流程

image-20260407164257590

这个图将put的原理阐述的很清楚,多看

image-20260407164745187

HashMap的扩容机制

image-20260407182250896

image-20260407182257929

hashmap的寻址算法

image-20260407183357829

image-20260407183840142

HashMap在1.7版本中的多线程死循环问题

image-20260407185306298

并发编程篇

线程与进程的区别

image-20260409160708565

并行和并发的区别

image-20260409160959853

创建线程的方式有哪些

创建线程的方式有四种方式

第一种是继承一个Thread类

image-20260409183933204

第二种是实现一个runnable接口

image-20260409185108371

第三种是实现一个Callable接口

image-20260409185117211

第四种是通过创建线程池来创建线程,但是也要继承runnable接口

image-20260409185126175

image-20260409185535292

线程之间的状态切换

image-20260409185936185

image-20260409190012062

如何保证线程的顺序执行

image-20260409190318204

notify和notifyall的区别是什么

notify随机唤醒一个wait的线程

notify则是唤醒所有wait的线程

java中wait()和sleep()方法有什么不同

image-20260409190852203

首先wait方法是必须要获取到对象的,也就是必须配合synchronized进行使用,而sleep不用

wait方法执行后会释放掉对象的锁,而sleep不会

如何停止一个正在运行的线程

有三种方式

第一种是自定义一个退出标志,使线程正常退出,也就是run方法结束之后,线程终止

第二种是通过stop方法来强行终止,但是这个方法已经被启用

具体原因是

  1. 立即解锁所有已获得的锁(最关键的问题)
    • stop()会强制线程立即停止,并释放它持有的所有监视器锁(synchronized锁)。
    • 这会导致被这些锁保护的对象状态突然暴露给其他线程。而此时,对象的数据可能只修改了一半,处于不一致状态
    • 其他等待锁的线程会立刻获得锁,并读取到这种损坏的数据,导致程序逻辑错乱、计算出错误结果,甚至崩溃。
  2. 破坏原子操作
    • 假设一个银行转账操作:先从一个账户扣款,再向另一个账户加款。如果stop()在这两步之间发生,锁被释放。
    • 结果就是:第一个账户钱少了,第二个账户钱却没增加。一笔钱凭空消失,数据永久损坏。
  3. 无法进行资源清理
    • 线程被强制停止时,无法执行任何清理代码(如关闭文件流、数据库连接、释放本地资源等)。
    • 这些资源泄露会积累,最终导致系统资源耗尽。
  4. 异常难以捕获和处理
    • stop()会让线程抛出一个ThreadDeath错误。虽然可以捕获它,但即使你捕获了,stop()依然会强制终止线程,你的清理代码未必能完整执行。而且,捕获这个错误并尝试恢复本身也是不安全的

第三种是使用interrupt方法,它属于Thread类,这个方法能实现优雅中断,不会强制终止线程,而是让线程自己决定什么时候终止,也是使用了退出标志的思想

  • Thread.interrupted()静态方法,返回当前线程中断标志,并清除标志
  • thread.isInterrupted():实例方法,只读,不清除标志

synchronized关键字的底层原理

image-20260410165650490

image-20260410165702955

进阶

在我们知道的synchronized锁中,其实包含了三种锁

第一种锁也就是通过monitor去实现的重量级锁

这种锁一般都是在多线程,并且有锁的竞争情况下使用,性能偏低,但是安全,通过Monitor的Owner去表明锁是否被持有

第二种锁是轻量级锁,首先介绍一下对象在内存中的存储结构

image-20260410193949433

在MarkWord中,也就是对象头,我们可以知道当前对象是否被加锁

image-20260410194034462

image-20260410194113364

如果是轻量级锁,在线程进入的时候,会在栈帧中记录一个lock record,里面包含了一个lock record 00地址,它会通过cas的方式,在确保原子性的情况下,和对象的mark word进行数据交换,将lock record 00地址放入到mark word中,表面当前线程已经持有了这把锁,轻量级锁实际上也是可重入锁,如果后续线程还要获取锁,就会在线程的栈帧中新加一个lock record,同样也会和对象进行cas操作,但是lock record为空

如果是偏向锁,也就是说只有当前线程会很长时间持有这把锁,不存在锁的竞争,那么就会使用偏向锁,偏向锁同样也是通过cas去完成数据交换,但是它是向对象存入自己的线程id,后续再获取锁,如果发现线程id和自己一样,就不会进行cas操作,直接生成lock record,性能上面来说,比轻量级锁更好

JMM(java内存模型)

image-20260410200013216

image-20260410200019090

CAS

image-20260412154852349

CAS本质上是一种乐观锁的思想,是通过自旋的思想来加锁的,在不加锁的情况下也能保证操作数据的原子性,在synchronized的轻量级锁和偏向锁中,获取锁的时候,就用到了CAS,并且CAS底层调用的是Unsafe类中的方法,实际上是操作系统的本地方法,因为没有使用到锁,不涉及线程的阻塞,所以它的效率是比较高的

volatile关键字

image-20260412163649944

image-20260412165102857

image-20260412165127044

保证线程之间共享变量可见性,因为编译器优化可能会导致变量消失,禁止指令重排序,保证指令按顺序执行,读写操作用了volatile修饰变量之后,会加上屏障

什么是AQS?

image-20260412181550189

  • 新的线程与队列中的线程共同来抢资源,是非公平锁
  • 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁

ReentranLock的实现原理

image-20260412182038410

image-20260412182053336

synchronized和lock的区别是什么

image-20260412185122710

lock锁中,主要区别的是它具备了许多synchronnized不具备的功能,公平锁,可打断,可超时,还可以使用多个条件变量,唤醒锁使用signal,signalall,和notify与notifyall是一样的

死锁产生条件以及排查方案

image-20260413101621883

死锁情况模拟

image-20260413101656186

ConcurrentHashMap

image-20260413103143573

image-20260413103256931

ConcurrentHashMap是一个线程安全的hashmap,在1.7版本,它主要使用的是一个segment数组加上hashEntry数组来实现的,并且segment数组长度不可变,底层使用Reentranlock来加锁,锁的粒度大,直接锁住一整个hashentry数组,性能较差,在1.8版本,放弃了臃肿的segment数组,底层实现与1.8版本的hashmap一样,都是使用数组加链表或红黑树,更新之后采用了CAS和synchronized锁,如果是添加新节点,就是用cas锁,保证多线程修改,只有一个能成功,如果是有hash冲突的情况,那么就会锁住链表或者红黑树的首节点,如果没有hash冲突,那么就不会有线程阻塞的情况,性能好了很多,锁的粒度也变小了

导致并发程序出现的问题原因

image-20260413113801645

要保证原子性,即为要保证线程中方法一次性执行完,不能中断或者被其他线程打断,这时候可以使用jvm中的sychronized或者jdk提供的lock来锁住线程,让方法保证原子性,其次,共享内存中的数据,要保证起线程之间的可见性,JIT会对代码进行优化,所以有时候共享变量会消失,使用volatile关键字去保证共享变量的可见性,它能去让编译器取消对这个共享变量的优化,也可以使用加锁的方式

锁如何保证可见性?

Java 内存模型(JMM)规定,锁的获取与释放和主内存之间有以下强制性的交互:

  1. 获取锁时:当一个线程获取锁时,它会清空自己的工作内存中的共享变量副本,强制后续的读取操作去主内存中重新获取最新的值
  2. 释放锁时:当一个线程释放锁时,它必须将在工作内存中被修改过的共享变量刷新回主内存中

这样,通过锁的"获取"和"释放"操作,就在不同线程间建立了一个"一写多读“的可见性桥梁:上一个释放锁的线程的修改,对下一个获取锁的线程必然可见 。

这是用锁来保证内存共享变量之间的可见性

最后是有序性,因为代码优化,代码执行时并不一定会按照代码行数顺序来执行,这就有可能会导致数据读写错误,使用volatile关键字来加上读写屏障,保证读写代码的顺序一致性

线程池的执行原理和核心参数

image-20260413154154313

image-20260413154225678

线程池中常见的阻塞队列

image-20260413163837800

一般来说都是使用ArrayBlockingQueue和LinkedBlockingQueue

image-20260413163911399

因为LinkedBlockingQueue出队和入队使用的是两把锁,所以实际上LinkedBlockingQueue效率会好一些,但是初始化的时候最好加上最大长度,也就是初始化成有界的状态。

如何确定核心线程数

image-20260413164259272

线程池的种类有哪些

image-20260413164540021image-20260413164654315

image-20260413165136443

image-20260413165147675

image-20260413165154044

为什么不建议使用Executor创建线程池

image-20260413165442436

image-20260413165424594

多线程使用场景

image-20260413172309112

如何控制方法中允许并发访问线程的数量

image-20260413174805261

image-20260413174815579

ThreadLocal的理解

image-20260413175612823

JVM相关面试题

JVM有哪些部分组成,运行流程是什么

image-20260415172032061

什么是程序计数器

image-20260415173037887

image-20260415173043124

什么是java堆

NovaBryan的博客
使用 Hugo 构建
主题 StackJimmy 设计