Redis篇
我看你做的项目中,都用到了redis,你在最近的项目中哪些场景使用了redis呢?
- 验证你项目场景的真实性,二是为了深入发问的切入点
- 缓存 缓存三兄弟(穿透、击穿、雪崩)、双写一致、持久化、数据过期策略、数据淘汰策略
- 分布式锁 setnx、redisson
- 消息队列、延迟队列 何种数据类型
==缓存穿透==:查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查询数据库(可能原因是数据库被攻击了 发送了假的/大数据量的请求url)
解决方案一:缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存 {key:1, value:null}
优点:简单
缺点:消耗内存,可能会发生不一致的问题解决方案二:布隆过滤器 (拦截不存在的数据)
在缓存预热时,要预热布隆过滤器。根据id查询文章时查询布隆过滤器如果不存在直接返回
bitmap(位图):相当于一个以bit位为单位的数组,数组中每个单元只能存储二进制数0或1
布隆过滤器作用:可以用于检索一个元素是否在集合中
- 存储数据:id为1的数据,通过多个hash函数获取hash值,根据hash计算数组对应位置改为1
- 查询数据:使用相同hash函数获取hash值,判断对应位置是否都为1
存在误判率:数组越小 误判率越大
bloomFilter.tryInit(size, 0.05) //误判率5%
==缓存击穿==:给某一个key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发请求可能一瞬间把DB击穿
解决方案一:互斥锁【数据强一致性 性能差 (银行)】
1.查询缓存,未命中 → 2.获取互斥锁成功 → 3.查询数据库重建缓存数据 → 4.写入缓存 → 5.释放锁
1.查询缓存,未命中 → 2.获取互斥锁失败 → 3.休眠一会再重试 → 4.写入缓存重试 → 5.缓存命中
解决方案二:逻辑过期[不设置过期时间] 【高可用 性能优 不能保证数据绝对一致 (用户体验)】
也可以搞个永不过期 具体是先在业务里写好某种情况下 某些时候不会过期 比如疫情卖口罩时期在数据库一条数据里面添加一个 “expire”: 153213455
1.查询缓存,发现逻辑时间已过期 → 2.获取互斥锁成功 → 3.开启线程 ↓→ 4.返回过期数据
【在新的线程】→ 1.查询数据库重建缓存数据 → 2.写入缓存,重置逻辑过期时间 → 3.释放锁
1.查询数据缓存,发现逻辑时间已过期 → 2.获取互斥锁失败 → 3.返回过期数据
==缓存雪崩==:在同一个时段内大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来压力
- 解决方案一:给不同的key的TTL(过期时间)添加随机值
- 解决方案二:利用Redis集群提高服务的可用性 【哨兵模式、集群模式】
- 解决方案三:给缓存业务添加降级限流策略【nginx、springcloud、gateway】
- 解决方案四:给业务添加多级缓存 【Guava(做一级缓存 然后Redis是二级缓存)或Caffeine】
《缓存三兄弟》
穿透无中生有key,布隆过滤null隔离。
缓存击穿过期key,锁与非期解难题。
雪崩大量过期key,过期时间要随机。
面试必考三兄弟,可用限流来保底。
redis作为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)
写先插入数据库
更新先更新数据库 更新数据库成功但redis不成功 影响不大 因为后面会有过期删除 最终会一致,更新mysql后缓存可以删除也可以修改
更新完数据库直接删除缓存了 有过期时间兜底 最终会保持一致 我们项目中对数据敏感性一致性不高 我们追求实时性
如果是最终保持一致性的就MQ 我们对实时性不高 对数据敏感性 一致性高
删除问题不大 哪里都行!
读多写少的可以上缓存
mysql保存购物车表 但是再页面操作的时候 只操作redis 用mq给到消费者修改或定时任务 更新数据到mysql,MQ问题:我们对数据实时性要求不高 只需要保存最终一致性就行
你如果只写redis 万一丢了数据怎么办?
购物车丢点订单无影响 数据安全性要求不太高 mysql尽量不要搞购物车的表 都在redis的表 丢就丢了呗。
或者异步同步/定时任务
实时性要求 安全性要求 → MySQL
电商一般数据库和mysql都要存 → 读多写少
一定、一定、一定要设置前提,介绍自己的业务背景 (一致性要求高?允许延迟一致?)
① 介绍自己简历上的业务,我们当时是把文章的热点数据存入到了缓存中,虽然是热点数据,但是实时要求性并没有那么高,所以我们采用的是异步的方案同步的数据
② 我们当时是把抢卷的库存存入到了缓存中,这个需要实时的进行数据同步,为了保证数据的强一致性,我们当时采用的是redission提供的读写锁来保证数据的同步
双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致
读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间
写操作:延迟双删 [因为无论先删除缓存还是先删除数据库都可能会出数据不一致问题 有脏数据]
==基于redisson互斥锁:==[放入缓存中的数据 读多写少] 【强一致性业务 性能低】
- 共享锁:读锁readLock,加锁之后,其他线程可以共享读操作,但**不允许写操作**
- 排他锁:独占锁writeLock也叫,加锁之后,阻塞其他线程读写操作(只允许一个用户或进程独占地对数据进行读取和写入操作)
排他锁确保了写操作的原子性和一致性 - 读数据的时候添加共享锁(读不互斥、写互斥)
- 写数据的时候添加排他锁(阻塞其他线程的读写 因为读多写少)
redissionClient.getReadWriteLock(“xxxx”);
==异步通知==: 异步通知保证数据的最终一致性(需要保证MQ的可靠性)
需要在Redis中更新数据的同时,通知另一个服务进行某些操作。- 使用场景:
- 缓存与数据库双写: 当应用需要同时更新Redis缓存和数据库时,可以先将数据写入Redis,然后通过异步通知机制触发数据库的更新操作。
- 跨地域数据复制: 在跨地域部署的服务中,为了实现数据的最终一致性,可以在一个地域写入数据后,通过异步通知机制在另一个地域进行数据复制。
- 系统间数据同步: 在微服务架构中,不同的服务可能有自己的数据存储。当一个服务更新了数据后,可以通过异步通知机制告知其他相关服务进行数据同步。
- 使用场景:
==基于Canal的异步通知==:监听mysql的binlog
- 使用MQ中间件,更新数据之后,通知缓存删除
- 利用canal中间件,不需要修改业务代码,伪装为mysqls的一个从节点,canal通过读取binlog数据更新缓存
Redis作为缓存,数据的持久化是怎么做的?
Redis持久化:RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照,简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,数据恢复。
[root@localhost ~]# redis-cli
127.0.0.1:6379> save #由Redis主进程来执行RDB,会阻塞所有命令
ok
127.0.0.1:6379> bgsave #开启子进程执行RDB,避免主进程受到影响
Background saving started
Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:
// 900秒内,如果至少有1个key被修改,则执行bgsave
save 900 1
save 300 10
save 60 10000
==RDB的执行原理?==数据完整性高用RDB
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件
在LInux中主进程并无法直接读取物理内存,它只能通过虚拟内存去读。因此有页表(记录虚拟地址与物理地址的映射关系)去执行操作 同时 主进程也会fork(复制页表) 成为一个新的子进程(携带页表) → 写新RDB文件替换旧的RDB文件 → 磁盘
fork采用的是copy-on-write
技术:
- 当主进程执行读操作时,访问共享内存
- 当主进程执行写操作时,则会拷贝一份数据,执行写操作
优点:二进制数据重启后 Redis无需过多解析 直接恢复
==AOF==对数据不敏感要求不高
AOF全称为Append Only File(追加文件)底层硬盘顺序读写。Redis处理的每个写命令都会记录在AOF,可以看作是命令日志文件
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:
# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
AOF的命令记录的频率也可以通过redis.conf文件来配
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完毕先放入AOF缓冲区,然后表示每隔一秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
配置项 | 刷盘时机 | 优点 | 缺点 |
---|---|---|---|
Always | 同步刷盘 | 可靠性高,几乎不丢数据 | 性能影响大 |
everysec | 每秒刷盘 | 性能适中 | 最多丢失1秒数据 |
no | 操作系统控制 | 性能最好 | 可靠性较差,可能丢失大量数据 |
因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof
命令,可以让AOF文件执行重读功能,用最少的命令达到相同效果
Redis会在出发阈值时自动重写AOF文件。阈值也可以在redis.conf中配置
# AOF文件比上次文件 增多超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
★★★★★★★★ RDB与AOF对比 ★★★★★★★★
RDB和AOF各有优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用
RDB是二进制文件,在保存时体积较小恢复较快,但也有可能丢失数据,我们通常在项目中使用AOF来恢复数据,虽然慢但丢失数据风险小,在AOF文件中可以设置刷盘策略(每秒批量写入一次命令)
RDB | AOF | |
---|---|---|
持久化方式 | 定时对整个内存做快照哦 | 记录每一次执行的命令 |
数据完整性 | 不完整,两次备份之间会丢失 | 相对完整,取决于刷盘策略 |
文件大小 | 会有压缩,文件体积小 | 记录命令,文件体积大 |
宕机恢复速度 | 很快 | 慢 |
数据恢复优先级 | 低,因为数据完整性不如AOF | 高,因为数据完整性更高 |
系统资源占用 | 高,大量CPU和内存消耗 | 低,主要是磁盘IO资源 但AOF重写时会占用大量CPU和内存资源 |
使用场景 | 可以容忍数分钟的数据丢失,追求更快的启动速度 | 对数据安全性要求较高常见 |
假如Redis的key过期之后,会立即删除吗
Redis对数据设置数据的有效时间,数据过期以后就需要将数据从内存中删除掉。可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略)
==Redis数据删除策略-惰性删除==
惰性删除:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key
set name zhangsan 10
get name # 发现name过期了,直接删除key
优点:对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不会浪费时间进行过期检查
缺点:对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放
==Redis数据删除策略-定期删除==
定期删除:每隔一段时间,我们就会对一些key进行检查,删除里面过期的key (从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)
定期清理的两种模式:
- SLOW模式是定时模式,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件
redis.conf
的hz选项来调整这个次数 - FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。另外定期删除,也能有效释放过期键占用的内存
难点:难以确定删除操作执行的时长和频率
Redis过期删除策略: 惰性删除 + 定期删除 两种策略进行配合使用
假如缓存过多,内存是有限的,内存被占满了怎么办?
==数据淘汰策略==
当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据制除掉,这种数据的制除规则被称之为内存的淘汰策略
Redis支持8种不同策略来选择要删除的key:
noeviction:不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略
maxmemory-policy noeviction
volatile-ttl:对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰 (TTL:过期时间的key)
allkeys-random:对全体key,随机进行淘汰
volatile-random:对设置了TTL的key,随机进行淘汰
allkeys-lru:对全体key,基于LRU算法进行淘汰
LRU(Least Recently Used):最近最少使用,用当前时间减去最后一次访问时间,这个值越大测淘汰优先级越高 [逐出访问时间最少的]
LFU(Least Frequently Used):最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。[逐出频率最低的] 【电商会应用】allkeys-lfu:对全体key,基于LFU算法进行淘汰
volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰
淘汰策略 - 使用建议
1.优先使用 allkeys-lru
策略。充分利用LRU算法的优势,把最近最常访问的数据留在缓存中,如果业务有明显的冷热数据区分,建议使用。
2.如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用allkeys-random
,随机选择淘汰
3.如果业务中有置顶的需求,可以使用volatile-lru
策略,同时置顶数据不设置过期时间,这些数据就一直不会被删除,会淘汰其他设置过期时间的数据
4.如果业务中有短时高频访问的数据,可以使用allkeys-lfu
或volatile-lfu
策略
数据库有1000万数据,Redis只能缓存20w数据,如何保证Redis中的数据都是热点数据?
- 使用
allkeys-lru
(挑选最近最少使用的数据淘汰) 淘汰策略,留下来的都是经常访问的热点数据
Redis的内存用完了会发生什么?
- 主要看数据淘汰策略是什么?如果是默认的配置(noeviction),会直接报错
redis分布式锁,是如何实现的?
需要结合项目中的业务进行回答,通常情况下,分布式锁的使用场景:
集群情况下的定时任务、抢单、幂等性场景
如果使用互斥锁的话 那么在集群项目有多个服务器就会出现问题
==Redis分布式锁==
Redis实现分布式锁主要利用Redis的setnx命令,setnx是**SET if not exists**(如果不存在,则SET)的简写
获取锁
添加锁,NX是互斥、EX是设置超时时间
SET lock value NX EX 10释放锁
释放锁,删除即可
DEL key
Redis实现分布式锁如何合理的控制锁的有效时长?
- 根据业务执行时间预估
- 给锁续期
==redisson实现分布式锁 - 执行流程==
加锁 ↓→ 加锁成功 → Watch dog(看门狗)
每隔(releaseTime/3的时间做一次续期)→ Redis
↓ 操作redis → Redis
↓→→ 释放锁↑ → 通知看门狗无需继续监听 → Redis
加锁 → → → 是否加锁成功?→→→ ↓
↑←←while循环不断尝试获取锁←←←↓
public void redisLock() throws InterruptedException{
RLock lock = redissonClient.getLock("heimalock");
// boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
// 如果不设置中间的过期时间30 才会触发看门狗
// 加锁,设置过期时间等操作都是基于lua脚本完成的[调用redis命令来保证多条命令的原子性]
boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);
if(isLock){
try{
sout("执行业务");
} finally{
lock.unlock();
}
}
}
要加依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
==redisson实现分布式锁 - 可重入==
redis实现分布式锁是不可重入的 但是 redisson实现分布式锁是可以重入的
可重入原理:它俩是同一个线程 每个线程都有唯一的线程id 根据线程id唯一标识做判断 判断之前获取锁是不是同一个线程
利用hash结构记录线程id和重入次数
KEY VALUE VALUE field value heimalock thread1 0
public void add1(){
RLock lock = redissonClient.getLock("heimalock");
boolean isLock = lock.tryLock();
// 执行业务
add2();
// 释放锁
lock.unlock();
}
public void add2(){
RLock lock = redissonClient.getLock("heimalock");
boolean isLock = lock.tryLock();
// 执行业务
// 释放锁 锁次数-1不完全释放
lock.unlock();
}
==redisson实现分布式锁 - 主从一致性==
Redis Master主节点:主要负责写操作(增删改) 只能写
Redis Slave从节点:主要负责读操作只能读
当RedisMaster主节点突然宕机后 Java应用会去格外获取锁 这时两个线程就同时持有一把锁 容易出现脏数据
怎么解决呢?
- RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n/2+1),避免在一个redis实例【实现复杂、性能差、运维繁琐】怎么解决?→ CP思想:zookeeper
Redis集群有哪些方案?
==主从复制==
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离
主节点写操作→增删改 从节点读操作→查介绍一下redis的主从同步
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就要搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据
主从数据同步原理:
- 主从全量同步:
slave从节点执行
replicaof
命令建立链接 → 请求master主节点数据同步(replid+offset) → master判断是否是第一次同步(判断replid是否一致) → 是第一次, 返回master的数据版本信息(replid+offset) → slave保存版本信息 → master执行bgsave, 生成RDB → 发送RDB文件给slave → slave清空本地数据加载RDB数据 → 此时master记录RDB期间所有命令repl_balklog
→ 发送repl_backlog中的命令 → slave执行接收到的命令Replication ld: 简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid
offset: 偏移量,随着记录在repl baklog中的数据增多而逐渐增大。save完成同步时也会记录当前同步的ofset,如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。简述全量同步的流程?
• slave节点请求增量同步
• master节点判断replid,发现不一致,拒绝增量同步
• master将完整内存数据生成RDB,发送RDB到slave
• slave清空本地数据,加载master的RDB
• master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
• slave执行接收到的命令,保持与master之间的同步
能说一下,主从同步数据的流程吗?
全量同步
1.从节点请求主节点同步数据(replication id、offset)
2.主节点判断是否为第一次请求,是第一次就与从节点同步版本信息(replication id和offset)
3.主节点执行bgsave, 生成RDB文件后, 发送给从节点去执行
4.在RDB生成执行期间, 主节点会从命令的方式记录到缓冲区(日志文件)- 主从增量同步
主从增量同步(slave重启或后期数据变化)
① slave重启后 → 携带(replid+offset)找master → master判断请求replid是否一致 → 是第一次, 返回主节点replid和offset → 保存版本信息
② slave重启后 → 携带(replid+offset)找master → master判断请求replid是否一致 → 不是第一次, 回复continue向slave → master 去repl_baklog中获取offset后的数据 → 发送offset后的命令给slave → 执行命令增量同步
1.从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset值
2.主节点从命令日志中获取offset值后的数据,发送给节点进行数据同步
简述全量同步和增量同步区别?
•全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。
•增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave
什么时候执行全量同步?
•slave节点第一次连接master节点时
•slave节点断开时间太久,repl_baklog中的offset已经被覆盖时
什么时候执行增量同步?
•slave节点断开又恢复,并且在repl_baklog中能找到offset时
==哨兵模式==~~搭过集群,具体多少个节点是组长那边,不太清楚
[并发量不是太多 搭哨兵可以节省一点资源]
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复- 监控:Sentinel会不断检查您的master和slave是否按预期工作
- 自动故障恢复:如果master故障,Sentinel会将一个slave提升为一个master。当故障实例恢复后也以新的master为主
- 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
服务状态监控
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令期待回复pong
- 主观下线:如果某sentinel节点发现或某实例未在规定时间相应,则认为该实例主观下线
- 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线,quorum值最好超过Sentinel实例数量的一半
哨兵选主规则
- 首先判断主与从节点断开时间长短,如超过指定值就排该从节点
- 然后判断从节点的slave-priority值,越小优先级越高
- 如果slave-priority值一样,则判断slave节点的offset值,越大优先级越高 (数据是最全的)
- 最后是判断slave节点的运行id大小,越小优先级越高
redis集群(哨兵模式) 脑裂
因网络问题 主节点和从节点分别在不同的网络分区 这样sentinel只会监控到一部分从节点网络分区 导致RedisClient继续写主节点的数据,这时网络恢复了,哨兵会将老的master强制降级到slave(携带着脑裂前的最新数据),这个时候slave就会把自己数据清空去同步master数据,这时就存在真正的数据丢失了怎么解决?
redis中有两个配置参数:【若不能达成就拒绝客户端请求 这样就会避免大量数据丢失】
min-replicas-to-write 1 表示最少的salve节点为1
min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒怎么保证Redis的高并发高可用呢?
哨兵模式:实现主从集群的自动故障恢复(监控、自动故障恢复、通知)
你们使用redis是单点还是集群,哪种集群?
主从(1主1从) + 哨兵就可以了。单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点
redis集群脑裂,该怎么解决?
集群脑裂是由于主节点和从节点和sentinel处于不同网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当为网络恢复后,sentinel会将老的主节点降为从节点,此时再从新master同步数据,就会导致数据丢失
解决:我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,这样就会避免大量数据丢失。==分片集群==
主从和哨兵可以解决高可用、高并发读的问题,但是依然有两个问题没有解决:
- 海量数据存储问题
- 高并发写的问题
使用分片集群可用解决上述问题,分片集群特征:
- 集群中有多个master,每个master保存不同数据
- 每个master都可用有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可用访问集群任意节点,最终都会被转发到正确节点
分片集群结果 - 数据读写
Redis分片集群引入了哈希槽的概念,Redis集群有16384个哈希值,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽
存数据流程:
set name itheima → CRC16计算name的hash值(666666) → 666666%16384=11306 → 根据11306找寻所对应哈希槽的范围 并且插入数据redis的分片集群有什么用?
- 集群中有多个master,每个master保存不同数据。(解决高并发写的问题)
- 每个master都可以有多个slave节点。(解决高并发读的问题)
- master之间通过ping监测彼此健康状态
- 客户端请求可用访问集群任意节点,最终都会被转发到正确节点
redis的分片集群中数据是怎么存储和读取的?
- Redis 分片集群引入了哈希槽的概念,Redis 集群有16384个哈槽
- 将16384个插槽分配到不同的实例
- 读写数据:根据key的**有效部分**计算哈希值,对16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身做为有效部分)余数做为播槽,寻找插所在的实例
Redis是单线程的,但是为什么还那么快
- Redis是纯内存操作,执行速度非常快
- 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
- 使用I/O多路复用模型,非阻塞IO
解释一下I/O多路复用模型?
Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,I/O多路复用模型主要就是实现了高效的网络请求
是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源,目前的I/O多路复用都是采用的epol模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要换个历Socket来判断是否就绪,提升了性能
Redis网络模型:
就是使用I/O多路复用结合事件的处理器来应对多个Socket请求
连接应答处理器
命令回复处理器,在Redis6.0之后,为了提升更好的性能,使用了多线程来处理回复事件
命令请求处理器,在Redis6.0之后,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
==用户空间和内核空间==
- Linux系统中一个进程使用的内存情况划分两部分:内核空间、用户空间
- 用户空间只能执行受限的命令RIng3,而且不能直接调用系统资源必须通过内核提供的接口来访问
- 内核空间可以执行特权命令Ring0,调用一切系统资源
Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区
- 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
- 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
常见的IO模型
==阻塞IO==
阻塞IO就是两个阶段都必须阻塞等待:
阶段一:- 用户进程尝试读取数据(网卡数据等)
- 此时数据尚未到达,内核需要等待数据
- 此时用户进程也处于阻塞状态
阶段二:
- 数据到达并拷贝到内核缓冲区,代表已就绪
- 将内核数据拷贝到用户缓冲区
- 拷贝过程中,用户进程依然阻塞等待
- 拷贝完成,用户进程解除阻塞,处理数据
==非阻塞IO==
阶段一:
- 用户进程尝试读取数据(比如网卡数据)
- 此时数据尚未到达,内核需要等待数据
- 返回异常给用户进程
- 用户进程拿到error后,再次尝试读取
- 循环往复,直到数据就绪
阶段二:
- 将内核数据拷贝到用户缓冲区
- 拷贝过程中,用户进程依然阻塞等待
- 拷贝完成,用户进程解除阻塞,处理数据
==IO多路复用==
是利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源
IO多路复用是利用单个线程来同步监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。不过监听Socket的方式、通知的方式又有多种实现
- select
- poll
- epoll
差异:
★ select和polI只会通知用户进程有Socket就绪,但不确定具体是哪个Socket,需要用户进程逐个历Socket来确认
★ epoll则会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,阶段一:
- 用户进程调用select,指定要监听的Socket集合
- 内核监听对应的多个socket
- 任意一个或多个sacket数据就绪则返回readable
- 此过程中用户进程阻塞
阶段二:
- 用户进程找别就格的socket
- 依次调用recvfrom读取数据
- 内核将数据拷贝到用户空间
- 用户进程处理数据
Redis网络模型
MySQL篇
在MySQL中,如何定位慢查询?
1.介绍一下当时产生问题的场景(我们当时的一个接口测试的时候非常的慢,压测的结果大概5秒钟)
2.我们系统中当时采用了运维工具(Skywalking),可以监测出哪个接口,最终因为是sql的问题
3.在mysql中开启了慢日志查询,我们设置的值就是2秒,一旦sql执行超过2秒就会记录到日志中(调试阶段)
产生原因:
- 聚合查询
- 多表查询
- 表数据量过大查询
- 深度分页查询
方案一:==开源工具==[调试阶段才会开启 生产阶段不会开启]
- 调试工具Arthas
- 运维工具:Prometheus、SKywalking(接口访问时间)
方案二:==MySQL自带慢日志==
慢查询日志记录了所有执行时间超过指定参数(long_query_time, 单位:秒,默认10秒)的所有SQL语句的日志,如果要开启慢查询日志,需要在MySQL的配置文件(/etc/my.cnf)中配置信息:
# 开启MySQL慢日志查询开关
slow_query_log = 1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会被视为慢查询,记录慢查询日志
long_query_time = 2
那这个SQL语句执行很慢,如何分析呢?
可以采用MySQL自带的分析工具
explain
- 通过key和key_len检查是否命中了索引(索引本身存在是否有失效的情况)
- 通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描
- 通过extra建议判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复
产生原因:
- 聚合查询 → 新增临时表的数据
- 多表查询 → 优化SQL语句结构
- 表数据量过大查询 → 添加索引
- 深度分页查询
一个SQL语句执行很慢,如何分析?
可以采用EXPLAIN
或者DESC
命令获取MySQL如何执行SELECT语句的信息
# 直接在select语句之前加上关键字 explain/desc
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件;
mysql > explain select * from t_user where id = ‘1’
id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE t_user NULL const PRIMARY PRIMARY 98 const 1 100.00 NULL
- possible_key:当前sql可能会使用到的索引
- key:当前sql实际命中的索引 通过它俩查看是否可能会命中索引
- key_len:索引占用的大小 通过它俩查看是否可能会命中索引
- Extra:额外的优化建议 看是否走过覆盖索引或回表查询
Extra 含义 Using where; Using Index 查找使用了索引,需要的数据都在索引列中能找到,不需要回表查询数据 Using index condition 查找使用了索引,但是需要回表查询数据
- type:这条sql的连接的类型,性能由好到差为
- NULL
- system:查询系统中的表
- const:根据主键查询
- eq_ref:主键索引查询或唯一索引查询
- ref:索引查询
- range:范围查询
- index:索引树扫描
- all:全盘扫描
了解过索引吗?(什么是索引)
索引(index)是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护着满足特定查找算法的数据结构**(B+树)**,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引
- 索引(index)是帮助MySQL高效获取数据的数据结构(有序)
- 提高数据检索的效率,降低数据库的IO成本(不需要全表扫描)
- 通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗
索引的底层数据结构了解过吗?
MySQL的InnoDB引擎采用的B+树的数据结构来存储索引
- 阶数更多,路径更短
- 磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据
- B+树便于扫库和区间查询,叶子节点是一个双向链表
**MySQL默认使用的索引底层数据结构是B+树**。再聊B+树之前,先来聊聊二叉树和B树
==B Tree(矮胖树)==,B树是一种多叉路衡查找树,相对于二叉树,B树每个节点可以有多个分支,即多叉。以一颗最大度数(max-degree)为5(5阶)的b-tree为例,那这个B树每个节点最多存储4个key
==B+Tree== 是再BTree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是B+Tree实现其索引结构
B树与B+树对比:
- 磁盘读写代价B+树更低
- 查询效率B+树更加稳定
- B+树便于扫库和区间查询
B树要找12 首先找38 左面小 再去缩小范围16和29 找到12 → 但是我们只想要12的数据 B树会额外的把38,16,29的数据全查一遍最后才到12的数据
B+树是在叶子节点才会存储数据,在非叶子节点全是指针,这样就没有其他乱七八糟的数据影响 。且查找路径是差不多的,效率较稳定
便于扫库:比如我们要查询6-34区间的数据,先去根节点扫描一次38 → 16-29 → 由于叶子节点之间有双向指针,就可以一次性把所有数据都给拿到[无需再去根节点找一次]
什么是聚簇索引?什么是非聚簇索引(二级索引)?什么是回表?
- 聚簇索引(聚集索引):数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个
- 非聚簇索引(二级索引):数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个
- 回表查询:通过二级索引找到对应的主键值,到聚集索引中查找正行数据,这个过程就是回表
分类 | 含义 | 特点 |
---|---|---|
==聚集索引(Clustered Index)== | 将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据 | 必须有, 而且只有一个 |
==二级索引(Secondary Index)== | 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 | 可以存在多个 |
聚集索引选取规则:
- 如果存在主键,主键索引就是聚集索引
- 如果不存在主键,将使用第一个唯一 (UNIQUE) 索引作为聚集索引
- 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引
==回表查询==
select * from user where name = 'Arm';
知道什么叫覆盖索引吗?
覆盖索引是指查询使用了索引,返回的列,必须在索引中全部能够找到
- 使用id查询,直接走聚集索引查询,一次索引描述,直接返回数据,性能高
- 如果返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用 **select ***
[除非用的聚簇索引(主键)]
==覆盖索引==是指查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到
id | name | gender | createdate |
---|---|---|---|
2 | Arm | 1 | 2021-01-01 |
3 | Lily | 0 | 2021-05-04 |
5 | Rose | 0 | 2021-04-21 |
6 | Zoo | 1 | 2021-07-31 |
8 | Doc | 1 | 2021-02-26 |
11 | Lee | 1 | 2021-09-11 |
- id为主键,默认是主键索引
- name字段为普通索引
select * from tb_user where id = 1; 【覆盖索引】
select id, name from tb_user where name = 'Arm' 【覆盖索引】
select id, name, gender from tb_user where name = 'Arm' 【非覆盖索引】(需要回表查询)
MySQL超大分页怎么处理?
问题:再数据量比较大时,limit分页查询,需要对数据进行排序,效率低
解决方案:可以用覆盖索引 + 子查询处理
[我们先分页查询获取表中的id 并且对表的id进行排序 就能筛选出分页后的id集合(因为id是覆盖索引效率高) 最后再根据id集合到原来的表中做关联查询就可以得到提升了]
在数据量比较大时,如果用limit分页查询,在查询时,越往后,分页查询效率越低
mysql > select * from tb_sku limit 0,10;
10 rows in set (0.00 sec)
mysql > select * from tb_sku limit 9000000,10;
10 rows in set (11.05 sec)
因为,当在进行分页查询时,如果执行 limit 9000000,10,此时需要MySQL排序前9000010记录,仅仅返回9000000 - 9000010 的记录,其他记录丢失,查询排序的代价非常大。
==MySQL超大分页查询优化思路==:一般分页查询时,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引加子查询形式进行优化
# 超大分页处理:先通过覆盖索引找到符合条件的id,再通过这个id的覆盖索引查询到所有的列
select *
from tb_sku t,
(select id from tb_sku order by id limit 9000000,10) a
where t.id = a.id
# 10 rows in set (7.15 sec)
索引创建原则有哪些?
① 数据量较大,且查询比较频繁的表
② 常作为查询条件、排序、分组的字段
③ 字段内容区分度高
④ 内容较长,使用前缀索引
⑤ 尽量联合索引
⑥ 要控制索引的数量
⑦ 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它
- 先陈述自己再实际工作中是怎么用的
- 主键索引
- 唯一索引
- 根据业务创建的索引(复合索引)
创建索引的方式1
① SQL的方式
ALTER TABLE user_innodb ADD INDEX idx_name(name)
② 在建表的时候 去指定索引
...
PRIMARY KEY('id'),
KEY 'idx_name' ('name') USING HASH
③ 通过界面化工具去指定索引
字段旁边有个`索引` 可以去添加
=============================================
单个字段的索引 → 单列索引
多个字段的索引 → 联合索引
索引的类型
索引可以增加查询速度 同时也增加了更新/修改速度因为更新的第一步就是查询
① 普通索引 经过特殊设计的数据结构
② 唯一索引 唯一约束
[索引必须是唯一的 比如name就不行 因为名字可以很多建立普通索引]
③ 主键索引 在主键索引上添加了非空约束
④ 全文索引 一般使用搜索引擎,因为对中文的搜索不太友好美国英文开发的
[特殊的sql:select * from 表名 where match(字段名) against(‘马士兵教育’ IN NATURAL LANGUAGE MODE);]AVL树 右右型左旋 左子树与右子树的深度差绝对值不超过1
树的节点里应该放:键值+Value值+左右子树的地址left+right
Innodb一次会加载16k(16384字节=Redis的槽位) 内存到内存
不选红黑树是因为它是二叉的,我们需要多叉树
要用==B+树==全盘扫描能力更强 叶子节点是双向链表
因为稳定性比较好 B树非所见所得 B+树是稳定几层的查找数据因为数据都在最后一层叶子节点上
Innodb的索引方法是BTREE 不能改成HASH
**数据结构可视化网**:Data Structure Visualization
- 针对数据量较大,且查询比较频繁的表建立索引。单表超过10万数据(增加用户体验)
- 针对常作为查询条件(where)、排序(order by)、分组(group by) 操作的字段建立索引
- 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高 (比如address都在北京市)
- 如果是字符串类型的字段,字段的长度越长(描述信息…),可以针对于字段的特点,建立前缀索引
- 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引(避免回表),节省存储空间,提高查询效率
- 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率
- 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。
什么情况下索引会失效?
- 违反最左前缀法则
- 范围查询右边的列,不能使用索引
- 不要在索引列上进行运算操作,索引将失效
- 字符串不加单引号,造成索引失效。(类型转换)
- 以**%开头的Like模糊查询**,索引失效
[不影响正常查询业务 但未运用超大分页查询优化 会导致索引失效]
怎么哪块读判断索引是否失效了呢?
# 执行计划explain
【2024最新版MySQL索引讲解!一个视频带你彻底搞懂MySQL索引!!【马士兵】】https://www.bilibili.com/video/BV17z421i7Kb?vd_source=5966d6c3cf3709c10b3c53b278b0f4d3
什么情况下索引会失效?
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列。匹配最左前缀法则,走索引:
谈谈你对sql的优化经验?
- 表的设计优化,数据类型的选择
- 索引优化,索引创建原则
- sql语句优化,避免索引失效,避免使用select
- 主从复制、读写分离,不让数据的写入,影响读操作
- 分库分表
表的设计优化(参考阿里开发手册《嵩山版》)
- 比如设置合适的数值(tinyint、int、bigint) ,要根据实际情况选择
- 比如设置合适的字符串类型(char和varchar) char定长效率高,varchar可变长度,效率低
候选人: 这个我们主要参考的阿里出的那个开发手册《嵩山版》,就比如,在定义字段的时候需要结合字段的内容来选择合适的类型,如果是数值的话,像tinyint、int、bigint这些类型,要根据实际情况选择。如果是字符串类型,也是结合存储的内容来选择char和varchar或者text类型
索引优化(参考优化创建原则和索引失效)
SQL语句优化
SELECT语句务必指明字段名称 (避免直使用select *)
回表SQL语句要避免造成索引失效的写法
尽量使用union all代替union,union(不会重复)会多一次过滤, 效率低
select * from t_user where id > 2 union all | union select * from t_user where id < 5
避免在where子句中对字段进行表达式操作
join优化 能用inner join 就不用left join, right 如必须使用 一定要以小表为驱动;内链接会对两个表进行优化,优先把小表放到外边,把大表放到里边。left join 或 right join,不会重新调整顺序
for(int i = 0; i < 3; i++){ //只链接查询3次 for(int j = 0; j < 1000; j++){ } }
主从复制、读写分离(在生产环境下一般会搭建主库和从库 分开读操作和写操作)
如果数据库的使用场景读的操作比较多的时候,为了避免写的操作所造成的性能影响 可以采用读写分离的架构。读写分离解决的是,数据库的写入,影响了查询的效率。[Master(写) 和 Slave(读)]
分库分表(后面有介绍)
事务的特性是什么?可以详细的说一下吗?【ACID】
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
候选人:嗯,这个比较清楚,ACID,分别指的是:原子性、一致性、隔离性、持久性;
我举个例子:A向B转账500,转账成功,A扣除500元,B增加500元,原子操作体现在要么都成功,要么都失败。
在转账的过程中,数据要一致,A扣除了500,B必须增加500
在转账的过程中,隔离性体现在A像B转账,不能受其他事务干扰
在转账的过程中,持久性体现在事务提交后,要把数据持久化(可以说是落盘操作)
- **原子性(**Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
- 隔离性(lsolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境运行
- 持久性(Durabiity):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
并发事务带来哪些问题?怎么解决这些问题?MySQL默认隔离级别是?
- ==并发事务问题==:脏读、不可重复读、幻读
- ==隔离级别==:读未提交、读已提交、可重复读、串行化
问题 | 描述 |
---|---|
脏读 | 一个事务读到另外一个事务还没有提交的数据 |
不可重复读 | 一个事务先后读取同一条事务,但两次读取的数据不同,称之为不可重复读 |
幻读 | 一个事务按照条件查询数据时,没有对应的数据行,这同时另一个事务B(insert且commit)了事务,此时事务A在插入数据时候,又发现这行数据已经存在了,好像出现了”幻影“ |
怎么解决并发事务的问题呢??
对事务进行隔离 (× 是代表可以解决此问题)
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted 未提交读 | √ | √ | √ |
Read committed 读已提交 | × | √ | √ |
==Repeatable Read(默认) 可重复读== | × | × | √ |
Serializable 串行化 | × | × | × |
注意:**事务隔离级别越高,数据越安全,但是性能越低**
undo log 和 redo log的区别?
redo log:记录的是数据页的物理变化,服务宕机可用来同步数据
undo log:记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据
redo log 保证了事务的持久性,undolog保证了事务的原子性和一致性
- 缓冲池(buffer pool):主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改査操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度
- 数据页(page):是InnoD8 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。页中存储的是行数据
==redo log==
重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性
该日志文件由两部分组冲:重做日志缓冲(redo log buffer) 以及 **重做日志文件(redo log file)**,前者是在内存中,后者是在磁盘中。当事务提交之后会把所有修改信息都保存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。
==undo log==
回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚和 MVCC(多版本并发控制)。undolog 和 redolog记录物理日志不一样,它是逻辑日志
- **可以认为当delete一条记录时,undo log中会记录一条对应的insert记录**,反之亦然
- 当update一条记录时,它记录一条对应相反的update记录。当执行rolback时,就可以从undolog中的逻辑记录读取到相应的内容并进行回滚。
undo log可以实现事务的一致性和原子性
事务中的隔离性是如何保证的呢?
排他锁 (如果一个事务获取到了一个数据行的排他锁,其他事务就不能再获取该行的其他锁)
mvcc: 多版本并发控制 让MySQL中的多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突
隐藏字段:
① trx _id(事务id),记录每一次操作的事务id,是自增的
② roll _pointer(回滚指针),指向上一个版本的事务版本记录地址undo log:
① 回滚日志,存储老版本数据
② 版本链:多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过rollpointer指针形成一个链表readView:解决的是一个事务查询选择版本的问题
根据readView的匹配规则和当前的一些事务id判断该访问那个版本的数据》不同的隔离级别快照读是不一样的,最终的访问的结果不一样RC:每一次执行快照读时生成ReadView
RR:仅在事务中第一次执行快照读时生成ReadView,后续复用
面试官: 事务中的隔离性是如何保证的呢?(你解释一下MVCC)
候选人: 事务的隔离性是由锁和mvcc实现的。
其中mnvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,它的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undolog日志,第三个是readView读视图
隐藏字段是指:在mysq!中给每个表都设置了隐藏字段,有一个是x_id(事务id),记录每一次操作的事务id,是自增的;另一个字段是roll-pointer(回滚指针),指向上一个版本的事务版本记录地址
undolog主要的作用是记录回滚日志,存储老版本数据,在内部会形成一个版本链,在多个事务并行探作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表
readview解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务id判断该访问那个版本的数据,不同的隔离级别快照读是不一样的,最终的访问的结果不一样。如果是rc隔离级别,每一次执行快照读时生成ReadView,如果是r隔离级别仅在事务中第一次执行快照读时生成ReadView,后续复用
解释一下MVCC?
全程 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突
问题的来源:(橙色的)查询的是哪个事务版本的记录?
事务2 | 事务3 | 事务4 | 事务5 |
---|---|---|---|
开始事务 | 开始事务 | 开始事务 | 开始事务 |
修改id为30记录, age改为3 | 查询id为30的记录 | ||
提交事务 | |||
修改id为30记录, name改为A3 | |||
查询id为30的记录 | |||
提交文件 | 修改id为30的记录, age改为10 | ||
查询id为30的记录 | 查询id为30的记录 | ||
提交事务 |
MVCC-实现原理
- 记录中的隐藏字段
id | age | name | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
---|
- DB_TRX_ID:最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID
- DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合undo log, 指向上一个版本
- DB_ROW_ID:隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段
undo log
- 回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。
- 当insert的时候,产生的undolog日志只在回滚时需要,在事务提交后,可被立即删除。
- 而update、delete的时候,产生的undo log日志不仅在回滚时需要,mvcc版本访问也需要,不会立即被删除。
undo log版本链
不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录
- readview
ReadView(读视图) 是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id
ReadView中包含了四个核心字段
字段 | 含义 |
---|---|
m_ids | 当前活跃的事务ID集合 |
min_trx_id | 最小活跃事务ID |
max_trx_id | 预分配事务ID, 当前最大事务ID+1 (事务ID是自增的) |
creator_trx_id | ReadView创建者的事务ID |
- 当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select .. lock in share mode(共享锁),select .. for update、update、insert、delete(排他锁)都是一种当前读。
- 快照读
简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
Read Committed:每次select,都生成一个快照读。
Repeatable Read:开启事务后第一个select语句才是快照读的地方。
MySQL主从同步原理?
MySQL主从复制的核心就是二进制日志binlog[DDL(数据定义语言)语句 和 DML(数据操纵语言)语句]
主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中。
从库读取主库的二进制日志文件 Binlog,写入到从库的中继日志 Relay Log。
从库重做中继日志中的事件,将改变反映它自己的数据。
MySQL主从复制的核心就是二进制日志
二进制文件(BINLOG) 记录了所有的DDL(数据定义语言)语句 和 DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句

复制分成三步:
- Master主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中
- 从库读取主库的二进制日志文件Binlog,写入到从库的中继日志Relay Log
- slave重做中继日志中的事件,将改变反应他自己的数据
你们项目用过分库分表吗?
业务介绍
1,根据自己简历上的项目,想一个数据量较大业务(请求数多或业务累积大)
2,达到了什么样的量级(单表1000万或超过20G)具体拆分策略
1,水平分库,将一个库的数据拆分到多个库中,解决海量数据存储和高并发的问题
2,水平分表,解决单表存储和性能的问题
3,垂直分库,根据业务进行拆分,高并发下提高磁盘I0和网络连接数
4,垂直分表,冷热数据分离,多表互不影响
分担了访问压力、解决存储压力
分库分表的时机:
① 前提:项目业务数据逐渐增多,业务发展比较迅速【单表数据量达1000W或20G以后】
② 优化解决不了性能问题(主从读写分离、查询索引)
③ IO瓶颈(磁盘IO、网络IO)、CPU瓶颈(聚合查询、连接数太多)
拆分策略【垂直 ≈ 微服务、水平 ≈ 分配数值】
- ==垂直拆分==
- 垂直分库:以表为依据,根据业务将不同表拆分到不同库中
(特点:按业务对数据分级管理、维护、监控、扩展;在高并发下,提高磁盘IO和数据量连接数)- tb_user → 用户微服务
- tb_order → 订单微服务
- tb_sku → 商品微服务
- 垂直分表:以字段为依据,根据字段属性将不同字段拆分到不同表中
(把不常用的字段单独放在一张表;把text, blob等大字段[描述]拆分出来放在附表中)
(特点:冷热数据分离、减少IO过渡争抢,两表互不影响)
- 垂直分库:以表为依据,根据业务将不同表拆分到不同库中
- ==水平拆分==
- 水平分库:将一个库的数据拆分到多个库中
(解决了单库大数量,高并发的性能瓶颈问题;提高了系统的稳定性和可用性)
路由规则- 根据id节点取模
- 按id也就是范围路由,节点1(1-100万),节点2(100万-200万)
- 水平分表:将一个库的数据拆分到多个表中(可以在同一个库内)
(优化单一表数据量过大而产生的性能问题;避免IO争抢并减少锁表的几率)
- 水平分库:将一个库的数据拆分到多个库中
分库后的问题:↓↓
- 分布式事务一致性问题
- 跨节点关联查询
- 跨节点分页、排序函数
- 主键避重
使用分库分表中间件
- sharding-sphere
- mycat
Spring框架中的单例bean是线程安全的吗?
不是线程安全的,是这样的
当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中有对该单列状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。
Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。
比如:我们通常在项目中使用的Springbean都是不可可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用由“singleton”变更为“prototype”。
Spring框架中的bean是单例的
@Service @Scope("singleton") public class UserServiceImpl implements UserService{ }
- singleton:bean在每个Spring IOC容器中只有一个实例
- prototype:一个bean的定义可以有多个实例
Spring bean并没有可变的状态(比如Service类和DAO类), 所以在某种程度上说Spring的单例bean是线程安全的。但要尽可能的少创造可变参数比如count
@Controller @RequeestMapping("/user") public class UserController{ private int count; //成员方法需要考虑线程安全问题 @Autowired private UserService userService; @GetMapping("/getById/{id}") public User getById(@PathVariable("id") Integer id){ count++; sout(count); return userService.getById(id); } }
什么是AOP,你们项目中有没有用到AOP?
AOP称为面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。
常见AOP使用场景:
拒绝策略记录操作日志
nginx → 新增用户 → @Around(“pointcut()”) 环绕通知
缓存处理
Spring中内置的事务处理
Spring中的事务是如何实现的
Spring支持 编程式事务管理 和 声明式事务 管理两种方式。
- 编程式事务控制:需使用TransactionTemplate来进行实现,对业务代码有侵入性,项目中很少使用
- 声明式事务管理:声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
Spring中事务失效的场景有哪些?
异常捕获处理,自己处理了异常,没有抛出,解决:手动抛出
抛出检查异常,配置rollbackFor属性为Exception
非public方法导致的事务失效,改为public
考察对spring框架的深入理解、复杂业务的编码经验
==异常捕获处理==
原因:事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉
解决:在catch块添加throw new RuntimeException(“转账失败”) 抛出
==抛出检查异常==
原因:Spring默认只会回滚非检查异常
@Transactional public void update(...) throw FileNotFoundException{ ... new FileInputStream("dddd") ... }
解决:配置
rollbackFor
属性@Transcational(rollbackFor=Exception.class)
==非public方法==
@Transcational(rollbackFor=Exception.class) void update(...) throw FileNotFoundException{ ... new FileInputStream("dddd") ... }
原因:Spring为方法创建代理、添加事务通知、前提条件都是该方法是public的
解决:把方法改为public
Spring的bean的生命周期?
Spring容器是如何管理和创建bean实例
方便调试和解决问题① 通过BeanDefinition获取bean的定义信息
② 调用构造函数实例化bean
③ bean的依赖注入
④ 处理Aware接囗(BeanNameAware、BeanFactoryAware、ApplicationContextAware)
⑤ Bean的后置处理器BeanPostProcessor-前置
⑥ 初始化方法(InitializingBean、init-method)
⑦ Bean的后置处理器BeanPostProcessor-后置
⑧ 销毁bean
BeanDefinition
Spring容器在进行实例化时,会将xml配置的< bean >的信息封装成一个BeanDefinition对象,Spring根据BeanDefinition来创建Bean对象,里面有很多的属性来描述Bean
<bean id="userDao" class="com.itheima.dao.impl.UserDaolmpl" lazy-init="true"/><bean id="userService" class="com.itheima.service.UserServicelmpl" scope="singleton">
<property name="userDao" ref="userDao"></property>
</bean>
Spring中的循环引用?
★ 循环依赖:循环依赖其实就是循环引用, 也就是两个或两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于A
★ 循环依赖在spring中是允许存在,spring框架**依据三级缓存已经解决了大部分的循环依赖**
☆ 一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
☆ 二级缓存:缓存早期的bean对象(生命周期还没走完)
☆ 三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的
构造方法出现了循环依赖怎么解决?
A依赖于B,B依赖于A,注入的方式是构造函数
原因:由于bean的生命周期中构造函数是第一个执行的,spring框架并不能解决构造函数的的依赖注入
解决方案:使用**@Lazy进行懒加载**,什么时候需要对象再进行bean对象的创建public A(@Lazy B b){
sout(“A的构造方法执行了”);
this.b=b;
}
@Component @Component
public class A{ → ← public class B{
@Autowired ↑ ↑ @Autowired
private B b; →↑ ↑← private A a;
} }
什么是Spring的循环依赖??
==一级缓存==作用:限制bean在beanFactory中只存一份,即实现singleton scope,解决不了循环依赖
如果想打破循环依赖,就需要一个中间人的参与,这个中间人就是==二级缓存==如果一个对象是代理对象(被增强了)就不行
针对如果是代理对象的话如何解决呢? → ==三级缓存==
那如果构造方法出现了循环依赖怎么解决?
@Component @Component
public class A{ → ← public class B{
private B b; ↑ ↑ private A a;
public A(B c){ →↑ ↑← public B(A c){
sout(“A的构造方法执行了”) sout(“B的构造方法执行了”)
this.b=b; this.b=b;
} }
} }报错信息:Is there an unresolvable circular reference?
解决:@Lazy
延迟加载→什么时候需要对象的时候什么时候实例化对象public A(@Lazy B b){ sout("A的构造方法执行了"); this.b=b; }
Spring解决循环依赖是通过三级缓存
// 单实例对象注册器
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
private static final int SUPPRESSED EXCEPTIONS LIMIT= 100;
private final Map<String, Object>singletonObjects = new ConcurrentHashMap(256); 一级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16); 三级缓存
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16); 二级缓存
}
缓存名称 | 源码名称 | 作用 |
---|---|---|
一级缓存 | singletonObject | 单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象 |
二级缓存 | earlySingletonObjects | 缓存早期的bean对象(生命周期还没走完) |
三级缓存 | singletonFactories | 缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的 |
SpringMVC的执行流程知道吗?
- 视图阶段(老旧JSP等)
- 前后端分离阶段(接口开发,异步)
==视图阶段(jsp)==
- 用户发送出请求到前端控制器DispatcherServlet
- DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
- HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有)
- DispatcherServlet调用HandlerAdapter(处理器适配器)HandlerAdapter经过适配调用具体的处理器(Handler/Controller)Controller执行完成返回
- ModelAndView对象HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet
- DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)
- ViewReslover解析后返回具体View(视图)
- DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)
- DispatcherServlet响应用户
==前后端分离阶段(接口开发,异步请求)==
- 用户发送出请求到前端控制器DispatcherServlet
- DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
- HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet
- DispatcherServlet调用HandlerAdapter(处理器适配器)
- HandlerAdapter经过适配调用具体的处理器(Handler/Controller)
SpringBoot自动配置原理?
SpringBoot中最高频的一道面试题,也是框架最核心的思想
==@SpringBootConfiguration
==:该注解与 @Configuration 注解作用相同,用来声明当前也是一个配置类
==@ComponentScan
==:组件扫描,默认扫描当前引导类所在包及其子包
==@EnableAutoConfiguration
==:SpringBoot实现自动化配置的核心注解1,在Spring Boot项目中的引导类上有一个注解
@SpringBootApplication
,这个注解是对三个注解进行了封装,分别是:
@SpringBootConfiquration
@EnableAutoConfiquration
@ComponentScan
2,其中
@EnableAutoConfiguration
是实现自动化配置的核心注解。该注解通过@Import
注解导入对应的配置选择器。内部就是读取了该项目和该项目引用的jar包的classpath路径下META-INF/spring.factories
文件中的所配置的类的全类名。在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。3,条件判断会有像
@ConditionalOnClass
这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。
package com.itheima;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// SpringBoot的启动类
// 注意: 我们写的代码要在启动类的包或者子包中
// @SpringBootApplication注解中包含了 @ComponentScan,没有指定扫描哪个包,默认扫描当前类所在的包和子包
@SpringBootApplication
public class Day15TliasManagement01IocDiApplication {
// 启动项目, 内嵌的Tomcat会启动, 把项目部署到这个内嵌Tomcat中
public static void main(String[] args) {
SpringApplication.run(Day15TliasManagement01IocDiApplication.class, args);
}
}
按住
ctrl+左键
点击@SpringBootApplication
会弹到SpringBootApplication.class
界面
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
按住
ctrl+左键
点击@EnableAutoConfiguration
会弹到EnableAutoConfiguration.class
界面
# @Import({AutoConfigurationImportSelector.class})
# AutoConfigurationImportSelector是自动配置的选择器
# 会加载META-INF中的spring.factories文件的自动配置类...AutoConfiguration...
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.boot.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
Spring框架常见的注解有哪些?
注解 | 说明 |
---|---|
@Component、@Controller、@Service、@Repository | 使用在类上用于实例化Bean |
@Autowired | 使用在字段上用于根据类型依赖注入 |
@Qualifier | 结合@Autowired一起使用用于根据名称进行依赖注入 |
@Scope | 标注Bean的作用范围 |
@Configuration | 指定当前类是一个Spring配置类,当创建容器时会从该类上加载注解 |
@ComponentScan | 用于指定Spring在初始化容器时要扫描的包 |
@Bean | 使用在方法上,标注将该方法的返回值存储到Spring容器中 |
@Import | 使用@Import导入的类会被Spring加载到IOC容器中 |
@Aspect、@Before、@After、@Around、@Pointcut | 用于切面编程(AOP) |
SpringMVC框架常见的注解有哪些?
注解 | 说明 |
---|---|
@RequestMapping | 用于映射请求路径,可以定义在类上和方法上。用于类上,则标识类中的所有的方法都是以该地址作为父路径 |
@RequestBody | 注解实现接收http请求的json数据,将json转换为java对象 |
@RequestParam | 指定请求参数的名称 |
@PathViriable | 从请求路径中获取请求参数(/user/{id} ),传递给方法的形式参数 |
@ResponseBody | 注解实现将Controller方法返回对象转换成json对象响应给客户端 |
@RequestHeader | 获取指定的请求头数据 |
@RestController | @Controller + @ |
SpringBoot常见的注解有哪些?
注解 | 说明 |
---|---|
@SpringBootConfiguration | 组合了 -@Configuration注解 ,实现配置文件的功能 |
@EnableAutoConfiguration | 打开自动配置的功能,也可以关闭某个自动配置的选项 |
@ComponentScan | Spring组件扫描 |
MyBatis执行流程?
- 读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件
- 构造会话工厂SqlSessionFactory
- 会话工厂创建SqlSession对象(包含了执行SQL语句的所有方法)
- 操作数据库的接口,Executor执行器,同时负责查询缓存的维护
- Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
- 输入参数映射
- 输出结果映射
- 理解了各个组件的关系
- Sql的执行过程(参数映射、sql解析、执行和结果处理)

MyBatis是否支持延迟加载?
- 延迟加载的意思是:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。
- Mybatis支持一对一关联对象和一对多关联集合对象的延迟加载
- 在
Mybatis配置文件
中,可以配置是否启用延迟加载lazyLoadingEnabled=true/false
,默认是关闭的
延迟加载的底层原理知道吗?
- 使用CGLIB创建目标对象的代理对象
- 当调用目标方法时,进入拦截器invoke方法,发现目标方法是nul值,执行sql查询
- 获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了
查询用户的时候,把用户所属的订单数据也查询出来,这个是==立即加载==
查询**用户的(sql)时候,暂时不查询订单数据,当需要订单的时候,再查询订单(sql)**,这个就是==延迟加载==
延迟加载的实现步骤:
- 配置开启延迟加载: 在MyBatis的配置文件中(通常是
mybatis-config.xml
),需要设置两个属性:
lazyLoadingEnabled=true
:开启延迟加载。aggressiveLazyLoading=false
:关闭积极的延迟加载,即访问对象的时候不会立即加载其所有属性。- 映射文件配置: 在对应的Mapper映射文件中,对于需要延迟加载的关联查询,使用
select
标签定义延迟加载的SQL语句,并通过fetchType="lazy"
属性明确指定使用延迟加载。- 创建代理对象: 当执行查询操作时,MyBatis不会立即执行关联查询的SQL,而是返回一个代理对象。这个代理对象是使用CGLIB库创建的,它继承自目标对象。
- 拦截器方法调用: 当我们首次访问这个代理对象的某个方法(比如访问订单详情)时,实际上会调用CGLIB生成的代理对象的拦截器方法(
intercept
方法)。在拦截器方法中,会判断当前要访问的属性是否已经被加载:
- 如果属性已经被加载,则直接返回属性值。
- 如果属性未被加载,则会执行之前定义好的延迟加载SQL语句,从数据库中查询数据。
- 设置属性值: 查询得到数据后,MyBatis会将这些数据设置到目标对象的相应属性上,这样下次访问该属性时,就不需要再次查询数据库了。
底层原理:
- CGLIB代理:MyBatis使用CGLIB库创建目标对象的代理,当调用目标方法时,实际上会进入拦截器(Interceptor)的
intercept
方法。- 拦截器逻辑:在拦截器中,会判断当前调用的方法是否需要触发延迟加载。如果需要,则执行延迟加载的SQL查询。
- 结果处理:查询结果会被处理并设置到目标对象的属性上,这样目标对象的相关属性就持有了数据,后续访问将直接返回这些数据,而无需再次查询。
MyBatis在执行完延迟加载的SQL查询后,会获取查询结果,并将这些结果映射到目标对象的相应属性中
示例说明:
假设有一个用户
User
和订单Order
的关系,在查询用户时,通常不会立即加载其订单信息,而是当需要时再加载。以下是简化的代码示例:<!-- UserMapper.xml --> <resultMap id="userMap" type="User"> <id property="id" column="id"/> <result property="name" column="name"/> <!-- 延迟加载订单信息 --> <collection property="orders" column="id" ofType="Order" select="selectOrdersForUser" fetchType="lazy"/> </resultMap> <select id="selectUser" resultMap="userMap"> SELECT * FROM user WHERE id = #{id} </select> <select id="selectOrdersForUser" resultType="Order"> SELECT * FROM order WHERE user_id = #{id} </select>
在上述配置中,当调用
selectUser
查询用户信息时,不会立即查询订单信息。只有当程序中访问User
对象的orders
属性时,才会执行selectOrdersForUser
查询,这就是延迟加载的具体实现。

MyBatis的一级、二级缓存用过吗?
- 一级缓存:基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存
- 二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQLsession,默认也是采用PerpetualCache,HashMap 存储。需要单独开启,一个是核心配置,一个是mapper映射文件
MyBatis的二级缓存什么时候会清理缓存中的数据?
- 当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear。
- 本地缓存,基于PerpetualCache,本质是一个HashMap
- 一级缓存:作用域是session级别
- 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存
- 二级缓存:作用域是namespace和mapper的作用域,不依赖于session
- 二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQLsession,默认也是采用 PerpetualCache
HashMap 存储
- 二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQLsession,默认也是采用 PerpetualCache
注意事项
- 对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear
- 二级缓存需要缓存的数据实现Serializable接口
- 只有会话提交或者关闭以后,一级缓存中的数据才会转移到二级缓存中
SpringCloud篇
SpringCloud 5大组件有哪些?
回答原则:简单的问题不能答错
通常情况 | SpringCloudAlibba |
---|---|
Eureka:注册中心 | Nacos:注册中心/配置中心 |
Ribbon:负载均衡 | Ribbon:负载均衡 |
Feign:远程调用 | Feign:远程调用 |
Hystrix:服务熔断 | sentinel:服务保护 |
Zuul/Gateway:网关 | Gateway:服务网关 |
服务注册和发现是什么意思? SpringCloud 如何实现服务注册发现?
- 我们当时项目采用的
eureka
作为注册中心,这个也是SpringCloud体系的一个核心组件- 服务注册:服务提供者需要把自己的信息注册到eureka来保存这些信息,比如**服务名称、ip、端口**等等
- 服务发现:消费者向eureka拉取服务列表信息,如果服务提供者有集群,则消费者利用负载均衡算法,选择一个发起调用
- 服务监控:服务提供者会每隔30秒向eureka发送心跳,报告健康状态,如果eureka服务90秒没有收到心跳,从eureka中剔除
- 微服务中必须要使用的组件,考虑我们使用微服务的程度
- 注册中心的核心作用是:服务注册和发现
- 常见的注册中心:eureka、nocas、zookeeper
请你说一下nacos与eureka的区别?
- Nacos与Eureka的共同点 (注册中心)
- 都支持服务注册和服务拉取
- 都支持服务者心跳方式做健康检测
- Nacos与Eureka的区别 (注册中心)
- Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- 临时实例心跳不正常会被剔除,非临时实例则不会被提出
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用AP方式
高可用模式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式- Nacos还支持了配置中心,Eureka只有注册中心,也是选择选用nacos的一个重要原因
把RestTemplate替换成OpenFeign后它们的底层还是一样的吗?OpenFeign是远程调用
OpenFeign的底层原理也是根据服务名称,首先去远程注册中心拉取服务列表,底层也会在本地缓存一份,也会根据负载均衡选出一个实例,又运用了jdk的动态代理生成代理类,也会涉及到反射机制,最终拼出完整的url,发起http远程调用
@FeignClient(name = "service-provider")
public interface ServiceProviderClient {
// 定义接口方法,映射到服务提供者的具体API
@GetMapping("/api/resource")
String getResource();
}
你们项目负载均衡如何实现的?图1.1
微服务的负载均衡主要使用了一个组件Ribbon,比如,我们再使用feign远程调用的过程中,底层的负载均衡就是使用了Ribbon
- 负载均衡Ribbon,发起远程调用feign就会使用Ribbon
- Ribbon负载均衡策略有哪些
- 如果想自定义负载均衡策略如何实现?
Ribbon已经进入维护模式,Netflix不再积极开发新功能。而Spring Cloud LoadBalancer作为替代,不仅提供了Ribbon的核心功能,还引入了一些新特性和改进
Ribbon负载均衡策略有哪些?
- RoundRobinRule:简单轮询服务列表来选择服务器
- WeightedResponseTimeRule:按照权重来选择服务器,响应时间越长,权重越小
- RandomRule:随机选择一个可用的服务器
- BestAvaliableRule:忽略那些短路的服务器,并选择并发数较低的服务器
- RetryRule:重试机制的选择逻辑
- AvaliabilityFilteringRule:可用性敏感策略,先过滤非健康的,再选择连接数较小的实例
- ZoneAvoidanceRule:以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可用理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询
如果想自定义负载均衡策略如何实现?图1.2
- 创建类实现IRule接口,可以指定负载均衡策略(全局)
- 在客户端的配置文件中,可以配置某一个服务调用的负载均衡(局部)
首先,你需要创建一个类来实现
IRule
接口,这样就能自定义负载均衡的策略。java复制编辑import com.netflix.loadbalancer.IRule; import com.netflix.loadbalancer.Server; import com.netflix.loadbalancer.ZoneAwareLoadBalancer; import com.netflix.loadbalancer.RandomRule; import java.util.List; public class CustomLoadBalancerRule implements IRule { private IRule delegate = new RandomRule(); // 默认策略 @Override public Server choose(Object key) { // 在这里实现自己的负载均衡算法 // 比如,你可以使用 RoundRobin、Random 或者基于健康检查的策略 return delegate.choose(key); } @Override public void setLoadBalancer(ZoneAwareLoadBalancer<?> lb) { delegate.setLoadBalancer(lb); } @Override public ZoneAwareLoadBalancer<?> getLoadBalancer() { return delegate.getLoadBalancer(); } }
然后,你需要在 Spring 配置类或者启动类上注入该自定义的负载均衡策略。
java复制编辑import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RibbonConfig { @Bean public IRule customLoadBalancerRule() { return new CustomLoadBalancerRule(); } }
SpringCloud中什么是服务雪崩,怎么解决这个问题?
- 什么是==服务雪崩==?
一个服务失败,导致整条链路的服务都失败的情形
- 服务雪崩:一个服务失败,导致整条链路的服务都失败的情形
- ==熔断降级== && ==服务熔断==(解决)Hystix 服务熔断降级
服务降级
部分服务不可用:服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃,一般在实际开发中与Feign接口整合,编写降级逻辑
服务熔断
整个服务不可用:默认关闭,需要手动打开,如果监测到10秒内请求的失败率超过50%,就触发熔断机制。之后每隔5秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制。如果微服务可达,则关闭熔断机制,恢复正常请求
- 限流(预防)
微服务限流(漏桶算法、令牌桶算法)
你们项目中有没有做到限流?怎么做的?&& 限流常见的算法有哪些??
① 先来介绍一下业务,什么情况下去做限流,需要说明QPS具体是多少
- 我们有一个活动,到了假期就会抢购优惠券,QPS最高可以达到2000,平时10-50之间,为了应对突发流量,需要做限流
- 常规限流,为了防止恶意攻击,保护系统正常运行,我们当时系统能够承受最大的QPS是多少(压测结果)
② nginx限流
- 控制速率(突发流量),使用的漏桶算法来实现过滤,让请求以固定的速率处理请求,可以应对突发流量
- 控制并发数,限制单个ip的连接数和并发链接的总数
③ 网关限流
- 在SpringCloudGateway中支持局部过滤器RequestRateLimiter来做限流,使用的是令牌桶算法
- 可以根据ip或路径进行限流,可以设置每秒填充平均速率,和令牌桶总容量
解释原理:
QPS(Queries Per Second,每秒查询率)是衡量一个系统处理请求能力的指标,它表示服务器在一秒钟内能够处理的查询数量。这个指标常用于数据库和web服务器等应用,以评估系统在高并发情况下的性能。
以下是对您提到的两句话的分析:
- 活动期间的高并发处理:
- 背景知识: 在电子商务等应用中,促销活动往往会引起用户的大量点击和购买行为,导致短时间内流量剧增。
- 限流原理: 为了应对这种突发的高流量,系统需要实施限流措施。限流是为了保护系统资源不被过度消耗,确保系统的稳定性和可靠性。常见的限流算法有固定窗口、滑动窗口、令牌桶和漏桶等。
固定窗口: 假设每 1 分钟允许 100 次请求,10:00:00 到 10:01:00 期间的 100 次请求被允许,超出 100 次则会被限流,10:01:00 到 10:02:00 则重新开始计算。
滑动窗口: 每 60 秒内最多允许 100 次请求,滑动窗口的时间长度为 60 秒,窗口内的请求数会随着时间滑动更新,防止请求在时间边界上积压
令牌桶:假设每秒生成 10 个令牌,令牌桶的容量为 100 个令牌。如果 1 秒内有 15 个请求到达,则前 10 个请求能获得令牌并继续执行,剩余的 5 个请求需要等到下一个时间窗口令牌生成后再执行。
漏桶:假设每秒钟流出 10 个请求,漏桶的容量为 100 个请求。如果 1 秒钟内接收了 30 个请求,系统只会处理 10 个请求,剩余的 20 个请求被丢弃,直到下一个时间点。
- 实施方式: 在您提到的情况下,可以采用以下策略:
- 预判性扩容: 根据历史数据和活动规模预测流量,提前进行服务器资源的扩容。
- 动态限流: 在活动期间,根据实时监控的QPS数据动态调整限流阈值,保证系统平稳运行。
- 排队处理: 对于超出系统处理能力的请求,可以采用队列进行缓冲,分批次处理。
- 常规限流与系统最大承受QPS:
- 背景知识: 常规限流是为了在日常运行中防止恶意攻击(如DDoS攻击)和保护系统资源不被滥用。
- 压测结果: 系统的最大承受QPS是通过压力测试得出的。压力测试(也称为负载测试)是通过模拟高并发访问来测试系统的极限性能,以确定系统在保证稳定运行的前提下能够承受的最大QPS。
- 原理分析:
- 保护系统: 通过设定一个QPS上限,可以防止系统过载,保障系统的正常运行。
- 资源分配: 了解系统的最大承受QPS有助于合理分配资源,如数据库连接、内存和CPU等。
- 用户体验: 适当的限流可以保证用户的体验,避免因系统过载导致的响应缓慢或服务不可用。
在实施限流策略时,还需要考虑以下因素:- 业务优先级: 对于不同的业务请求,可能需要有不同的限流策略,优先保证核心功能的可用性。
- 用户体验: 限流策略应尽量减少对用户体验的影响,例如通过友好的错误提示或降级方案。
- 数据监控: 实时监控系统的QPS和其他关键指标,以便快速响应并调整限流策略。
综上所述,限流是确保系统在高并发情况下稳定运行的重要措施,而了解系统的最大承受QPS是制定合理限流策略的基础。
为什么要限流?
- 并发业务量大(突发流量)
- 防止用户恶意刷接口
限流的实现方式:
==Tomcat==
单体项目可以,分布式不行:可以设置最大连接数<Connector port="8080"...maxThreads="150"...>
==Nginx==:漏桶算法
固定速率露出(平滑)控制速率(突发流量)
==网关==:令牌桶算法
自定义拦截器
你们的微服务是怎么监控的?
我们项目中采用的skywalking进行监控的
- skywalking主要可以监控接口、服务、物理实例的一些状态。特别是在压测的时候可以看到众多服务中哪些服务和接口比较慢,我们可以针对性的分析和优化。
- 我们还在skywalking设置了告警规则,特别是在项目上线以后,如果报错,我们分别设置了可以给相关负责人发短信和发邮件,第一时间知道项目的bug情况,第一时间修复
skywalking
一个分布式系统的应用程序性能监控工具(Application Performance Management), 提供了完善的链路追踪能力,apache的顶级项目(前华为产品经理吴晟主导开源)
解释一下CAP和BASE分布式系统理论
- CAP 定理(一致性、可用性、分区容错性)
- 分布式系统节点通过网络连接,一定会出现分区问题(P)
- 当分区出现时,系统的一致性(C)和可用性(A)就无法同时满足
- BASE理论
- 基本可用
- 软状态
- 最终一致
- 解决分布式事务的思想和模型
- 最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据(AP)
- 强一致思想:各分支事务执行完业务不要提交,等待彼此结果。而后统一提交或回滚(CP)
- 分布式事务方案的指导
- 分布式系统设计方向
- 根据业务指导使用正确的技术选择
==CAP定理==分布式系统无法同时满足三个指标
- ==Consistency==(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。
主从一致 - ==Availability==(可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝
- ==Partition tolerance==(分区容错性):当出现网络分区现象后,系统能够继续运行
- Partition(分区):因为网络故障或其他原因导致分布式系统中的部分节点与其他节点失去链接,形成独立分区
- Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务
结论:
- 分布式系统节点之间肯定是需要网络链接的,分区 (P) 必然存在
- 如果保证访问的高可用性(A)可以持续对外提供服务,但不能保证数据的强一致性 AP
- 如果保证访问的数据强一致性(C)就要放弃高可用性 CP
==BASE理论==
BASE理论是对CAP的一种解决思路,包含三个思想:
- ==Basically Avaliable==(基本可用):分布式系统在出现故时,允许损失部分可用性,即保证核心可用
- ==Soft State==(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态
- ==Eventually Consistent==(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致性
CAP如何选择?
- CP[支付宝]或者AP[超级跑跑系统维护]
- 在什么场合,可用性高于一致性?
- 网页必须要保障可用性(一定能看到最重要 是不是最新的不重要)和分区容错
- 支付的时候一定要保障一致性(我可以保证不可用 但我不允许余额不一致)和分区容错
- 合适的才是最好的
你们采用哪种分布式事务解决方案?
● 简历上写的微服务,只要是发生了多个服务之间的写操作,都需要进行分布式事务控制
● 描述项目中采用的哪种方案(seataMQ)
⚪ seata的XA模式,CP,需要互相等待各个分支事务提交,可以保证强一致性,性能差 (银行业务 )
⚪ seata的AT模式,AP,底层使用undolog 实现,性能好 (互联网业务 )
⚪ seata的TCC模式,AP,性能较好,不过需要人工编码实现 (银行业务 )
⚪ MQ模式实现分布式事务,在A服务写数据的时候,需要在同一个事务内发送消息到另外一个事务异步,性能最好 (互联网业务 )
- Seata框架(XA、AT、TCC)
- MQ
Seata架构
- TC(Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚
- TM(Transaction Manager) - 事务管理器:定义全局事务的范围、开启全局事务、提交或回滚全局事务
- RM(Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
分布式服务的接口幂等性如何设计?
- 幕等: 多次调用方法或者接口不务状态,可以保证重复调用的结果和单次调用的结果一致
- 如果是**新增数据**,可以使用数据库的唯一索引
- 如果是**新增或修改数据**
- 分布式锁,性能较低
- 使用token+redis来实现,性能较好
● 第一次请求,生成一个唯一token存入redis,返回给前端
● 第二次请求,业务处理,携带之前的token,到redis进行验证,如果存在,可以执行业务,删除token; 如果不存在,则直接返回,不处理业务
幂等:多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致
需要幂等场景
- 用户重复点击(网络波动)
- MQ消息重复
- 应用使用失败或超时
请求方式 | 说明 |
---|---|
GET | 查询操作,天然幂等 |
POST | 新增操作,请求一次与请求多次造成的结果不同,不是幂等的 |
PUT | 更新操作,如果是以绝对值更新,则是幂等的。如果是通过增量的方式更新,则不是幂等的 |
DELETE | 删除操作,根据唯一值删除,是幂等的 |
update a set money = 500 where id = 1 【幂等】
update a set money = money + 500 where id = 1; 【非幂等】

你们项目中使用了什么分布式任务调度
xxl-job
是一个分布式任务调度平台,它致力于解决分布式场景下的任务调度问题,主要由调度中心和执行器两部分组成。调度中心负责统一管理任务调度,而执行器则是负责接收调度并执行任务逻辑的客户端。
xxl-job路由策略有哪些?
xxl-job提供了很多的路由策略,我们平时用的较多的就是:轮询、故障转移、分片广播
xxl-job任务执行失败怎么解决?
- 路由策略选择故障转移,使用健康的实例来执行任务
- 设置重试次数
- 查看日志+邮件警告来通知相关负责人解决
如果有大数据量的任务同时都现需要执行,怎么解决?
- 让多个实例一块去执行(部署集群),路由策略分片广播
- 在任务执行的代码中可以获取分片总数和当前分片,按照取模的方式分摊到各个实例执行
xxl-job解决的问题
- 解决集群任务的重复执行问题 xxl-job路由策略有哪些?
- cron表达式定义灵活
在页面上xxl-job任务执行失败怎么解决? - 定时任务失败了,重试和统计 如果有大数据量的任务同时都需要执行,怎么解决?
- 任务量大,分片执行
场景 1: 定时处理过期订单
假设用户下单后如果订单超过了某个时间没有支付,平台需要自动取消该订单并释放库存。这个任务需要在每天的某个固定时间(比如凌晨 2 点)运行。
解决的问题:
- 定时任务调度:XXL-Job 可以轻松管理该任务的执行时间和周期,确保每天准时执行,不需要开发者手动触发。
- 任务失败重试:如果该任务因为某些原因执行失败,XXL-Job 可以自动进行重试,并设置重试次数,确保任务最终被执行。
- 分布式执行:假设电商平台是一个分布式系统,订单数据存储在多个数据库中,XXL-Job 可以通过分布式执行确保每个数据库的订单都被正确处理
假设每晚 2 点有一个任务需要取消未支付的订单:
public class OrderJob { @JobHandler("orderCancelJobHandler") public void cancelUnpaidOrders() { // 查询所有未支付的订单 List<Order> unpaidOrders = orderService.findUnpaidOrders(); for (Order order : unpaidOrders) { if (order.isExpired()) { orderService.cancelOrder(order); inventoryService.releaseStock(order.getProductId(), order.getQuantity()); // 发送订单取消通知给用户 notificationService.sendOrderCancelledNotification(order.getUserId()); } } } }
场景 2: 定时更新商品库存
假设电商平台上销售的是一些有时效性的商品,商家需要定期更新商品的库存状态(例如,库存数量达到一定阈值时,自动下架商品,或者增加库存数量)。这个任务同样需要定时执行。
解决的问题:
- 任务分片:在商品很多的情况下,XXL-Job 可以通过任务分片的方式并行处理不同商品的库存更新,提升任务的执行效率。
- 任务优先级:根据不同商品的重要程度,XXL-Job 可以设置任务的优先级,确保关键商品的库存更新优先执行。
public class InventoryJob { @JobHandler("inventoryUpdateJobHandler") public void updateProductInventory() { // 获取需要更新库存的商品 List<Product> productsToUpdate = productService.findProductsForInventoryUpdate(); for (Product product : productsToUpdate) { inventoryService.updateInventory(product); if (product.getStockQuantity() <= product.getLowStockThreshold()) { productService.deactivateProduct(product); // 发送商品下架通知 notificationService.sendOutOfStockNotification(product.getId()); } } } }
场景 3: 定时发送促销活动通知
假设电商平台有一个促销活动,每个活动的开始和结束时间都由后台系统控制。需要在活动开始前 1 小时、活动结束时发送通知给用户。这些通知可以是短信、邮件或 APP 推送通知。
解决的问题:
- 定时任务管理:XXL-Job 可以定时触发通知任务,确保用户在活动前后及时收到通知。
- 高并发支持:在促销活动开始或结束时,平台可能会有大量的通知需要发送,XXL-Job 支持任务的并行处理,可以帮助我们高效地分发通知,避免性能瓶颈。
- 任务状态监控:XXL-Job 提供任务的实时监控功能,平台可以随时查看任务的执行情况,确保通知任务按时执行。
public class PromotionJob { @JobHandler("promotionNotifyJobHandler") public void sendPromotionNotifications() { // 获取当前正在进行的促销活动 List<Promotion> activePromotions = promotionService.findActivePromotions(); for (Promotion promotion : activePromotions) { if (promotion.isStartingSoon()) { notificationService.sendStartNotification(promotion); } else if (promotion.isEndingSoon()) { notificationService.sendEndNotification(promotion); } } } }
xxl-job路由策略有哪些?
实例找任务项执行任务 这种找机器的方式就是路由策略
消息中间件RabbitMQ+Kafka
消息中间件提供了服务与服务之间的异步调用,还可以服务与服务之间解耦
RabbitMQ:**消息不丢失、消息重复消费、消息堆积、延迟队列、死信队列、高可用机制**
Kafka:**消息不丢失、消息重复消费、高可用机制、高性能设计吞吐量达到百万级、数据存储和清理**
RabbitMQ-如何保证消息不丢失?
开启生产者确认机制,确保生产者的消息能到达队列
confirm到交换机ack 不到nack 和 return没到返回nack机制保证生产者把消息发过去达到队列成功返回
ack
,失败返回nack
【negative acknowledgment】
- 生产者发送消息到交换机。
- 交换机收到消息后,根据绑定规则(是否有匹配的队列)决定消息是否被正确路由。
- 如果消息成功路由到队列,交换机会向生产者返回
ack
确认。- 如果消息没有成功路由到任何队列,交换机会通过
return
将消息退回给生产者。- 生产者收到
ack
或nack
,可以处理消息确认或重试逻辑。开启持久化功能,确保消息未消费前在队列中不会丢失
durable = True
万一broker挂掉就惨了 保证至少成功一次消费
MQ是默认内存存储信息,开启持久化功能可以确保缓存在MQ中的消息不丢失[把数据存在磁盘上]# 声明持久化交换器 channel.exchange_declare(exchange='exchange_name', durable=True) # 声明持久化队列 channel.queue_declare(queue='queue_name', durable=True) # 发送持久化消息 channel.basic_publish(exchange='exchange_name', routing_key='routing_key', body='Hello World!', properties=pika.BasicProperties( delivery_mode=2, # 使消息持久化 ))
开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
消费者三种机制:RabbitMQ支持消费者确认机制,即:**消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息**,而Spring AMQP则允许配置三种确认模式:
manual:手动ack,需要在业务代码结束后,调用api发送ack。
auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack
none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除
开启消费者失败重试机制,多次重试失败后将消息投递到异常交换机,交由人工处理
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:
RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
- 异步发送(验证码、短信、邮件)
- MySQL和Redis,ES之间的数据同步
- 分布式事务
- 削峰填谷
==生产者确认机制==
RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功
RabbitMQ消息的重复消费问题如何解决的?
我们当时消费者是设置了自动确认机制,当服务还没来得及给MQ确认的时候,服务宕机了,导致服务重启之后,又消费了一次消息,这样就重复消费了
因为我们当时处理的支付(订单|业务唯一标识),它有一个业务的唯一标识,我们再处理消息时,先到数据库查询一下,这个数据是否存在,如果不存在,说明没有处理过,这个时候就可以正常处理这个消息了。如果已经存在这个数据了,就说明消息重复消费了,我们就不需要再消费了。
原因
- 网络抖动
- 消费者挂了
解决方案:适用于任何MQ(Kafka,RabbitMQ,RocketMQ)
- 每条消息设置一个唯一的标识id
- 幂等方案:【分布式锁、数据库锁(悲观锁、乐观锁)】
RabbitMQ中死信交换机?(RabbitMQ延迟队列有了解过吗)
如果用原来的定时任务 也可以但是 可能会有订单空窗期 如果没人消费的时候 它内部还是回去sql查询已下单 +(now()-下单时间)?15min : true, false
- 我们当时一个什么业务使用到了延迟队列(超时订单、限时优惠、定时发布)
- 其中延迟队列就用到了死信交换机和TTL(消息存活时间)实现的
- 消息超时未消费就会变成死信(死信的其他情况:拒绝被消费,队列满了)
延迟队列插件实现延迟队列
DelayExchange
- 声明一个交换机,添加delayed属性为true
- 发送消息时,添加
x-delay
头,值为超过时间什么样的消息会成为死信? ★ 消费者返回reject或者nack,且requeue参数设置为false【消息被拒绝并且不重入队列】 ★ 消息超时未消费 ★ 队列满了 如何给队列绑定死信交换机? ★ 给队列设置dead-letter-exchange属性,指定一个交换机 ★ 给队列设置dead-letter-routing-key属性,设置死信交换机与死信队列的RoutingKey ------------------------------------------------------------------------ ★ ★ ★ 使用 Spring AMQP 配置 ★ ★ ★ import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.TopicExchange; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RabbitMQConfig { // 定义普通队列 @Bean public Queue normalQueue() { return new Queue("normalQueue", true, false, false, Map.of("x-dead-letter-exchange", "dlx_exchange", "x-dead-letter-routing-key", "dlx_routing_key")); } // 定义死信队列 @Bean public Queue dlxQueue() { return new Queue("dlxQueue", true); } // 定义普通交换机 @Bean public Exchange normalExchange() { return new TopicExchange("normal_exchange"); } // 定义死信交换机 @Bean public Exchange dlxExchange() { return new TopicExchange("dlx_exchange"); } // 将普通队列与交换机绑定 @Bean public Binding bindNormalQueue() { return BindingBuilder.bind(normalQueue()).to(normalExchange()).with("normal.routing.key").noargs(); } // 将死信队列与死信交换机绑定 @Bean public Binding bindDLXQueue() { return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("dlx_routing_key").noargs(); } } ------------------------------------------------------------------------ 如果你希望将死信队列配置成带有过期时间或其他特殊属性的队列,可以在定义 dlxQueue 时增加更多的设置,例如 TTL(过期时间)。 例如,设置死信队列的 TTL: @Bean public Queue dlxQueue() { return QueueBuilder.durable("dlxQueue") .withArgument("x-message-ttl", 60000) // 设置TTL为60秒 .build(); }
延迟队列 = 死信交换机 + TTL (生存时间)
- 延迟队列:进入队列的消息会被延迟消费的队列
- 场景:超时订单、限时优惠,定时发布
死信交换机
当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):
- 消费者使用basic.reject 或 basic.nack声明消费失败,并且信息的requeue参数设置为false
- 消息是一个过期消息,超时无人消费
- 要投递的队列消息堆积满了,最早的消息可能成为死信
如果该队列配置了dead-letter-exchange
属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)
@Bean
public QUeue ttlQueue(){
return QueueBuilder.durable("simple.queue") // 指定队列名称,并持久化
.ttl(10000) // 设置队列的超时时间 10秒
.deadLetterExchange("dl.direct") // 指定死信交换机
.build();
}
TTL
TTL(Time-To-Live)。如果一个队列中的消息TTL结束仍未消费,则会变成死信,ttl超时分为两种情况:
- 消息所在的队列设置了存活时间
- 消息本身设置了存活时间
RabbitMQ如果有100万消息堆积在MQ,如何解决(消息堆积怎么解决)?
**解决消息堆积有三种思路 **
- 增加更多消费者,提高消费速度
- 在消费者内开启线程池加快消息处理速度
- 扩大队列容积,提高堆积上限
- 在声明队列的时候可以设置
x-queue-model
为lazy,即为惰性队列- 基于磁盘存储,消息上限高
- 性能比较稳定,但基于磁盘存储,受限于磁盘IO,时效性会降低
当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是堆积问题
**解决消息堆积有三种思路 **
- 增加更多消费者,提高消费速度
- 在消费者内开启线程池加快消息处理速度
- 扩大队列容积,提高堆积上限
达到上限发送的消息会变成死信,那我为什么不搞个死信交换机 而是用了上面的三种思路??
使用死信交换机(DLX, Dead Letter Exchange)是另一种处理消息堆积的方式,但它的作用更偏向于“消息过期”或“处理失败”的情况下将消息转发到另一个队列。并不直接解决生产者发送消息过快或消费者处理速度过慢的问题。通过死信交换机,你可以将无法处理的消息转发到其他队列,方便你后续进行分析或处理,但它并不能提高消费者处理消息的速度。
针对消息堆积的本质问题,解决方式更多的是优化消费者处理能力,而不是仅依赖死信交换机。具体而言,死信交换机和你的三种思路的关系如下:
- 死信交换机(DLX):当消息达到队列上限或无法消费时,消息被转发到死信队列。你可以分析死信队列中的消息,了解原因,并决定是重试、丢弃还是进行其他处理。它的作用是不丢失消息,但并不能帮助消除堆积。
- 增加消费者:这是直接针对堆积的根本解决方案,通过增加消费者数量来加速消息的处理。死信交换机无法直接解决消费者处理能力不足的问题。
- 开启消费者线程池:在单个消费者上开启线程池,可以提高消费者的处理能力,减少堆积。死信交换机并不能增加消息处理速度,它只是用来应对消费失败的情况。
- 扩大队列容量并使用惰性队列:惰性队列可以将消息存储在磁盘上,而非内存中,减轻内存压力,但这也会降低时效性,并不能解决生产者生产过快或消费者消费过慢的问题。死信交换机同样无法直接解决这一点。
总结来说,死信交换机是处理消息丢失或无法消费的方式,它和通过增加消费者、线程池、队列优化这些手段并不冲突,但也无法替代这些更直接的解决方案。你可以结合这两者,使用死信交换机来保障消息不丢失,同时采取上述方法来提高消息消费速度。
惰性队列
惰性队列特征如下:
- 接收到消息后直接存入磁盘而非内存
- 消费者要消费消息时才会从磁盘中读取并加载到内存
- 支持数百万条的消息存储
@Bean
public Queue lazyQueue(){
return QueueBuilder
.durable("lazy.queue")
.lazy()
.build();
}
@RabbitListener(queuesToDeclare = @Queue){
name = "lazy.queue",
durable = "true",
arguments = @Argument(name = "x-queue-mode"), value="lazy"
}
public void listenLazyQUeue(String msg){
log.info("接收到lazy.queue的消息:{}",msg);
}
RabbitMQ高可用机制有了解过吗? && 请描述 RabbitMQ 镜像队列的工作原理及其在高可用性场景下的优缺点
我们当时的项目在生产环境下,采用的是镜像模式搭建的集群,共有3个节点
镜像队列结构是一主多从(从就是镜像),所有(写)操作都是主节点完成,然后同步给镜像节点
主宕机后,镜像节点会代替成为新的主(如果在主从同步完成前,主就已经宕机,可能出现数据丢失)
那出现丢数据怎么解决呢?
我们可以采用仲裁队列,与镜像队列一样,都是主从模式,支持主从数据同步,主从同步基于Raft协议,强一致性,并且使用起来也非常简单,不需要格外的配置,在声明队列的时候只需要指定这个是仲裁队列即可
- 在生产环境下,使用集群来保证高可用性
- 普通集群、镜像集群、仲裁队列
普通集群
普通集群,或者叫标准集群(classic cluster)
- 会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的信息
- 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回
- 队列所在节点宕机,队列中的消息就会丢失
镜像集群
镜像集群:本质是主从模式,具备下面的特征:
- 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份
- 创建队列的节点被称为该队列的主节点,备份到的其他节点叫做该队列的镜像节点
- 一个队列的主节点可能是另一个队列的镜像节点
- 所有操作都是主节点完成,然后同步给镜像节点
- 主宕机后,镜像节点会替代成新的主

仲裁队列:.quorum()
仲裁队列是3.8版本以后才有的新功能,用来替代镜像队列
- 与镜像队列一样,都是主从模式,支持主从数据同步
- 使用非常简单,没有复杂的配置
- 主从同步基于Raft协议,强一致性
仲裁队列的工作原理如下:
- 主从模式:仲裁队列也是主从模式,支持主从数据同步。
- Raft 协议:主从同步基于 Raft 协议,确保数据的一致性和可靠性。
- 强一致性:所有写操作必须得到大多数节点的确认后才能完成,避免了数据丢失。
仲裁队列通过以下机制保证数据不丢失:
- 多数派确认:每次写操作需要得到大多数节点的确认,确保数据已经成功复制到多个节点。
- 自动故障转移:如果主节点宕机,Raft 协议会自动选举新的主节点,确保服务的连续性。
- 数据一致性:Raft 协议保证了数据的强一致性,即使在网络分区或节点宕机的情况下,也不会出现数据不一致的问题。
仲裁队列的优点是配置简单、数据强一致,但需要至少 3 个节点,并且在写操作上的延迟和资源消耗可能会比镜像队列高。
@Bean
public Queue quorumQueue(){
return QueueBuilder
.durable("quorum.queue") // 持久化
.quorum() // 仲裁队列
.build();
}
Kafka是如何保证消息不丢失?
需要从三个层面去解决这个问题
生产者发送消息到Brocker丢失
- 设置异步发送,发送失败使用回调进行记录或重发
- 失败重试,参数配置,可以设置重试次数消息
在Brocker中存储丢失
发送确认acks,选择all,让所有的副本都参与保存数据后确认
消费者从Brocker接收消息丢失
- 关闭自动提交偏移量,开启手动提交偏移量
- 提交方式:最好是同步+异步提交
使用Kafka在消息的收发过程中都会出现消息丢失,Kafka分别给出了解决方案
- 生产者发送消息到Brocker丢失
- 消息在Brocker中存储丢失
- 消费者从Brocker接收消息丢失
kafka-高产出的分布式消息系统(A high-throughput distributed messaging system)。
Kafka是一个高吞吐、分布式、基于发布订阅的消息系统,利用Kafka技术可以在廉价的PC Server上搭建起大规模消息系统。
Kafka的特性:
- 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作;
- 可扩展性:kafka集群支持热扩展;
- 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失;
- 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败);
- 高并发:支持数千个客户端同时读写;
- 支持实时在线处理和离线处理:可以使用Storm这种实时流处理系统对消息进行实时进行处理,同时还可以使用Hadoop这种批处理系统进行离线处理;
Kafka和其他组件比较,具有消息持久化、高吞吐、分布式、多客户端支持、实时等特性,适用于离线和在线的消息消费,如常规的消息收集、网站活性跟踪、聚合统计系统运营数据(监控数据)、日志收集等大量数据的互联网服务的数据收集场景。
- 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如Hadoop、Hbase、Solr等;
- 消息系统:解耦和生产者和消费者、缓存消息等;
- 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到Hadoop、数据仓库中做离线分析和挖掘;
- 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告;
- 流式处理:比如spark streaming和storm;
- 事件源;
- kafka在FusionInsight中的位置:
Kafka是如何保证消费的顺序性?
问题原因:
一个topic的数据可能存储在不同的分区中 ,每个分区都有一个按照顺序的存储的偏移量,如果消费者关联了多个分区不能保证顺序性==解决方案:==
- 发送消息时指定分区号
- 发送消息时按照相同的业务设置相同的key
应用场景:
- 即时消息中的单对单聊天和群聊,保证发送方消息发送顺序与接收方的顺序一致
- 充值转账两个渠道在同一个时间进行金额变更,短信通知必须要有顺序
承接上图消费者从Brocker接收消息丢失
:
如何做?→ topic分区中消息只能由消费者组中的唯一一个消费者处理,所以消息肯定是按照先后顺序进行处理的。但是它也仅仅是保证Topic的一个分区顺序处理,不能保证跨分区的消息先后处理顺序。所以,如果你想要顺序的处理Topic的所有消息,那就只提供一个分区。
// 指定分区
kafkaTemplate.sent("springboot-kafka-topic",0,"key-001","value-001");
// 相同的业务key
kafkaTemplate.sent("springboot-kafka-topic","key-001","value-001");
会计算key的hashcode值推断出它在哪个分区,如果要求有顺序性 就可以设置同一个key,此时hash值都是一样的 就可以在同一个分区存储
Kafka的高可用机制有了解过吗?
==集群模式==
一个kafka集群由多个broker实例组成,即使某一台宕机,也不会耽误其他broker继续对外提供服务
==分区备份机制==
- 一个topic有多个分区,每个分区有多个副本,有一个leader,其余的是follower,副本存储在不同的broker中
- 所有的分区副本的内容都是相同的,如果leader发生故障时,会自动将其中一个follower提升为leader,保证了系统的容错性、高可用性
解释一下复制机制中的ISR?
ISR (in-sync replica) 需要同步复制保存的follower
分区副本分为了两类,一个是ISR,与leader副本同步保存数据,另外一个普通的副本,是异步同步数据,当leader挂掉后,会优先从ISR副本列表中选取一个作为leader
// 一个topic默认分区的replication个数,不能大于集群中broker的个数。默认为1
default.replication.factor=3
// 最小的ISR副本个数
min.insync.replicas=2
Kafka数据清理机制了解过吗?
- kafka文件存储机制
- Kafka中topic的数据存储在分区上,分区如果文件过大会分段存储segment
- 每个分段都在磁盘上以索引(xxxx.index)和日志文件(xxx.log)的形式存储
- 分段的好处是,第一能够减少单个文件内容的大小,查找数据方便,第二方便kafka进行日志清理
- 数据清理机制
- 根据消息的保留时间,当消息保存的时间超过了指定的时间,就会触发清理,默认168小时(7天)
- 根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阈值,则开始删除最久的消息(默认关闭)
Kafka中实现高性能的设计有了解过吗?
- 消息分区:不受单台服务器的限制,可以不受限的处理更多的数据
- 顺序读写:磁盘顺序读写,提升读写效率
- 页缓存:把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问
- 零拷贝:减少上下文切换及数据拷贝
- 消息压缩:减少磁盘IO和网络IO
- 分批发送:将消息打包批量发送,减少网络开销
零拷贝
集合面试篇
算法复杂度分析
什么是算法时间复杂度?
- 时间复杂度表示了算法的执行时间与数据规模之间的增长关系
常见的时间复杂度有哪些?
口诀:常对幂指阶
- O(1)、O(n)、O(n^2)、O(logn)
什么是算法的空间复杂度?
- 表示算法占用的额外
存储空间
和数据规模之间
的增长关系
常见的空间复杂度:O(1)、O(n)、O(n^2)
为什么要进行复杂度分析?
- 指导你编写出性能更优的代码
- 评判别人写的代码的好坏
时间复杂度分析:来评估代码的执行耗时的
大O表示法:不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势
只要代码的执行时间**不随着n的增大而增大,这样的代码复杂度都是O(1)**
复杂度分析就是要弄清楚代码的执行次数和数据规模n之间的关系
时间复杂度:全称是渐进空间复杂度
,表示算法占用的额外存储空间
和数据规模之间
的增长关系
List相关面试题
数组是一种用连续的内存空间存储相同数据类型数组的线性数据结构
数组下标为什么从0开始
寻址公式是:
baseAddress + i * data TypeSize
计算下标的内存地址效率较高查找的时间复杂度
- 随机(通过下标)查询的时间复杂度是O(1)
- 查找元素(未知下标)的时间复杂度是O(n)
- 查找元素(未知下标但排序)通过二分查找的时间复杂度是O(logn)
插入和删除时间复杂度
插入和删除的时候,为了保证数组的内存连续性,需要挪动数组元素,平均复杂度为O(n)
底层实现
- 数据结构—数组
- ArrayList源码分析
面试问题
- ArrayList底层的实现原理是什么
- ArrayList list = new ArrayList(10)中的list扩容几次
- 如何实现数组和List之间的转换
- ArrayList和LinkedList的区别是什么
ArrayList源码分析
List< Integer > list = new ArrayList< Integer >();
list.add(1)
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
private static final long serialVersionUID = 8683452581122892189L;
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 用于空实例的共享空数组实例
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 用于默认大小的空实例的共享空数组实例
* 与上面的区分开,以了解添加第一个元素时要膨胀多少
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/* 存储ArrayList元素的数组缓冲区,ArrayList的容量就是这个数组缓冲区的长度 */
transient Object[] elementData; // non-private to simplify nested class access
/**
* ArrayList的大小(包含的元素数量)
* @serial
*/
private int size;
...
}
--------------------------------------------------------------------------------
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 创建一个真正存储集合位置的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 如果容量是0则创建一个新的数组给elementData
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
// 无参构造函数,默认创建空集合
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
-------------------------------------------------------------------------------
// Collection是所有单列集合的父接口
// 将 Collection 对象转换成数组,然后将数组的地址赋给 elementData
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 判断集合类型是否为不为object[]
// 在其他jdk此处是 ?? == ArrayList.class
if (elementData.getClass() != Object[].class)
// 不是的话就拷贝到数组elementData中
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
ArrayList源码分析-添加和扩容操作(第1次添加数据)

ArrayList底层的实现原理是什么
- ArrayList底层是用动态数组实现的
- ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
- ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组
- ArrayList在添加数据的时候
- 确保数组已使用长度(size)加1之后足够存下下一个数据
- 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用
grow
方法扩容(原来的1.5倍) - 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上
- 返回添加成功布尔值
ArrayList list = new ArrayList(10)中的list扩容几次
- 该语句只是声明和实例了一个
ArrayList
,指定了容量为10,未扩容
如何实现数组和List之间的转换
- 数组转List,使用JDK中
java.util.Arrays
工具类的asList
方法 - List转数组,使用List的
toArray
方法,无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组
使用 Hutool 工具库可以非常方便地实现数组和
List
之间的转换。Hutool 提供了ArrayUtil
和CollUtil
工具类来处理数组和集合之间的转换。问:我不能用BeanUtil吗?
答:BeanUtil
是 Hutool 工具库中用于 Java Bean 操作的工具类,主要用于 对象属性拷贝、Bean 转 Map、Map 转 Bean 等操作。它并不适用于 数组和 List 之间的转换。如果你误以为
BeanUtil
可以用于数组和 List 的转换,可能是因为它的名字容易让人误解。实际上,数组和 List 的转换应该使用ArrayUtil
或CollUtil
。正确的工具类选择
- 数组转 List:使用
ArrayUtil.toList
。- List 转数组:使用
CollUtil.toArray
。- Bean 属性拷贝:使用
BeanUtil.copyProperties
。- Bean 转 Map:使用
BeanUtil.beanToMap
。- Map 转 Bean:使用
BeanUtil.fillBeanWithMap
。更多的Hutool工具使用
高能预警1. 字符串工具类:
StrUtil
- 功能:字符串操作,如判空、格式化、截取、替换等。
- 常用方法:
StrUtil.isEmpty()
:判断字符串是否为空。StrUtil.format()
:格式化字符串。StrUtil.split()
:拆分字符串。StrUtil.join()
:连接字符串。
2. 日期时间工具类:
DateUtil
- 功能:日期和时间的格式化、解析、计算等。
- 常用方法:
DateUtil.now()
:获取当前时间。DateUtil.format()
:格式化日期。DateUtil.parse()
:解析字符串为日期。DateUtil.offsetDay()
:日期加减。
3. 文件工具类:
FileUtil
- 功能:文件和目录的操作,如读写、复制、删除等。
- 常用方法:
FileUtil.readUtf8String()
:读取文件内容为字符串。FileUtil.writeUtf8String()
:将字符串写入文件。FileUtil.copy()
:复制文件或目录。FileUtil.del()
:删除文件或目录。
4. JSON 工具类:
JSONUtil
- 功能:JSON 的解析和生成。
- 常用方法:
JSONUtil.parseObj()
:将 JSON 字符串解析为 JSON 对象。JSONUtil.parseArray()
:将 JSON 字符串解析为 JSON 数组。JSONUtil.toJsonStr()
:将对象转换为 JSON 字符串。
5. 集合工具类:
CollUtil
- 功能:集合操作,如创建集合、判空、过滤、分组等。
- 常用方法:
CollUtil.newArrayList()
:快速创建 ArrayList。CollUtil.isEmpty()
:判断集合是否为空。CollUtil.filter()
:过滤集合。CollUtil.group()
:对集合进行分组。
6. 反射工具类:
ReflectUtil
- 功能:反射操作,如调用方法、获取字段、创建对象等。
- 常用方法:
ReflectUtil.invoke()
:调用方法。ReflectUtil.getFieldValue()
:获取字段值。ReflectUtil.newInstance()
:创建对象实例。
7. HTTP 工具类:
HttpUtil
- 功能:HTTP 请求的发送和响应处理。
- 常用方法:
HttpUtil.get()
:发送 GET 请求。HttpUtil.post()
:发送 POST 请求。HttpUtil.downloadFile()
:下载文件。
8. 加密解密工具类:
SecureUtil
- 功能:常见的加密解密操作,如 MD5、SHA、AES 等。
- 常用方法:
SecureUtil.md5()
:计算 MD5 值。SecureUtil.sha256()
:计算 SHA-256 值。SecureUtil.aes()
:AES 加密解密。
9. IO 工具类:
IoUtil
- 功能:IO 流操作,如读写、关闭流等。
- 常用方法:
IoUtil.read()
:读取流内容。IoUtil.write()
:写入流内容。IoUtil.close()
:关闭流。
10. 随机工具类:
RandomUtil
- 功能:生成随机数、随机字符串等。
- 常用方法:
RandomUtil.randomInt()
:生成随机整数。RandomUtil.randomString()
:生成随机字符串。RandomUtil.randomEle()
:从集合中随机选择一个元素。
11. 验证工具类:
Validator
- 功能:数据验证,如邮箱、手机号、身份证等。
- 常用方法:
Validator.isEmail()
:验证是否为邮箱。Validator.isMobile()
:验证是否为手机号。Validator.isCitizenId()
:验证是否为身份证号。
12. 缓存工具类:
CacheUtil
- 功能:简单的缓存操作。
- 常用方法:
CacheUtil.newTimedCache()
:创建定时缓存。CacheUtil.put()
:添加缓存。CacheUtil.get()
:获取缓存。
13. 线程工具类:
ThreadUtil
- 功能:线程操作,如睡眠、创建线程池等。
- 常用方法:
ThreadUtil.sleep()
:线程睡眠。ThreadUtil.newExecutor()
:创建线程池。
14. Excel 工具类:
ExcelUtil
- 功能:Excel 文件的读写操作。
- 常用方法:
ExcelUtil.getReader()
:读取 Excel 文件。ExcelUtil.getWriter()
:写入 Excel 文件。
15. 压缩工具类:
ZipUtil
- 功能:文件或目录的压缩和解压缩。
- 常用方法:
ZipUtil.zip()
:压缩文件或目录。ZipUtil.unzip()
:解压缩文件。
16. 日志工具类:
Log
- 功能:简化日志操作。
- 常用方法:
Log.get()
:获取日志对象。Log.info()
:输出日志信息。
17. 数学工具类:
MathUtil
- 功能:数学计算,如四舍五入、最大值、最小值等。
- 常用方法:
MathUtil.round()
:四舍五入。MathUtil.max()
:获取最大值。MathUtil.min()
:获取最小值。
18. 网络工具类:
NetUtil
- 功能:网络相关操作,如获取本机 IP、Ping 等。
- 常用方法:
NetUtil.getLocalhost()
:获取本机 IP。NetUtil.ping()
:Ping 测试。
- 用
Arrays.asList
转List后,如果修改了数组内容,list受影响吗 - List用
toArray
转数组后,如果修改了List内容,数组受影响吗
再答:
- Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址
- list用了toArray转数组后,如果修改了list内容,数组不会受影响,当调用了toArray以后,在底层是它进行了数组拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响
LinkedList的数据结构—链表
单向链表
- 链表中的每一个元素称之为结点(Node)
- 物理存储单元上,非连续、非顺序的存储结构
- 单向链表:每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。记录下个结点地址的指针叫作后继指针 next
1.单向链表和双向链表的区别是什么
- 单向链表只有一个方向,结点只有一个后继指针 next。
- 双向链表它支持两个方向,每个结点不止有一个后继指针next指向后面的结点,还有一个前驱指针prev指向前面的结点
2.链表操作数据的时间复杂度是多少
查询 | 新增删除 | |
---|---|---|
单向链表 | 头O(1), 其他O(n) | 头O(1), 其他O(n) |
双向链表 | 头尾O(1), 其他O(n), 给定节点O(1) | 头尾O(1), 其他O(n), 给定节点O(1) |
ArrayList和LinkedList的区别
底层数据结构
- ArrayList 是动态数组的数据结构实现
- LinkedList 是双向链表的数据结构实现
操作数组效率
ArrayList 按照下标查询的时间复杂度O(1);【内存是连续的,根据寻址公式】,LinkedList不支持下标查询
查找(未知索引):ArrayList需要遍历,链表也需要遍历,时间复杂度都是O(n)
新增和删除
- ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)
- LinkedList 头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)
内存空间占用
- ArrayList 底层是数组,内存连续,节省内存
- LinkedList 是双向链表需要存储数据,和两个指针,更占用内存
线程安全
ArrayList和LinkedList都不是线程安全的
如果要保证线程安全,有两种方法
在方法内使用,局部变量则是线程安全的
使用线程安全的ArrayList和LinkedList
List<Object> syncArrayList = Collections.synchronizedList(new ArrayList<>()); List<Object> syncLinkedList = Collections.synchronizedList(new LinkedList<>());
HashMap相关面试题
二叉树
满二叉树
完全二叉树
二叉搜索树
二叉搜索树又名二叉查找树,有序二叉树或者排序二叉树,是二叉树中比较常用的一种类型二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值
红黑树
红黑树
散列表
数据结构—红黑树 什么是红黑树?
- 红黑树:也是一种自平衡的二叉搜索树(BST)
- 所有的红黑规则都是希望红黑树能够保证平衡
- 红黑树的时间复杂度:查找、添加、删除都是O(logn)
散列表
什么是散列表?
- 散列表(Hash Table)又叫哈希表/Hash表
- 根据键(Key)直接访问再内存存储位置值(Value)的数据结构
- 由数组演化而来的,利用了数组支持按照下标进行随机访问数据
散列冲突
- 散列冲突又成为哈希冲突,哈希碰撞
- 指多个key映射到同一个数组下标位置
散列冲突—链表法(拉链)
- 数组的每个下标位置称之为桶(bucket) 或者 槽(slot)
- 每个桶(槽)会对应一条链表
- hash冲突后的元素都放到相同槽位对应的链表中或红黑树中
在HashMap中的最重要的一个数据结构就是散列表,在散列表中又用到了红黑树和链表
散列表(Hash Table)又名为哈希表/Hash表,是根据键(Key)直接访问在内存存储位置值(value)的数据结构,它是由数组演化而来的,利用了数组支持按照下标进行随机访问数据的特性[根据寻址公式,时间复杂度O(1)]
说一下HashMap的实现原理
1.说一下HashMap的实现原理
HashMap的数据结构:底层使用hash表数据结构,即数组和链表或红黑树
添加数据时,计算key的值确定元素在数组中的下标
- key相同则替换
- 不同则存入链表或红黑树中
获取数据通过key的hash计算数组下标获取元素
2.HashMap的jdk1.7和jdk1.8有什么区别
- JDK1.8之前采用的拉链法,数组+链表
- JDK1.8之后采用数组+链表+红黑树
链表长度大于8且数组长度大于64则会从链表转化为红黑树
当我们往HashMap中put元素时,利用key的hashCode重新hash计算
出当前对象的元素在数组中的下标
HashMap的put方法的具体流程
1.判断键值对数组table是否为空或为null,否则执行resize()进行扩容 [初始化]
2.根据键值key计算hash值得到数组索引
3.判断table[i] == null,条件成立,直接新建节点添加
4.如果table[i] == null,不成立
4.1 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
4.2 判断table[i]是否为treeNode,即table[i]是否是红黑树,如果是红黑树,则直接在树中插入键值对
4.3 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,遍历过程中若发现key已经存在直接覆盖value
讲一下HashMap的扩容机制
HashMap源码分析
桶下标是hash值取模数组(长度)下标 capacity
HashMap的寻址算法
Hash值右移16位后与原来的hash值进行异或运算【扰动算法hash值更加均匀,减少hash冲突】
数组长度必须是2的n次幂 按位与运算的效果才能代替取模
HashMap在1.7情况下的多线程死循环问题
jdk7的数据结构是:数组+链表
在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环
进程和线程的区别?
两者对比:
- 进程是整个在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备,进程就是用来加载指令、管理内存、管理IO的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个**进程**
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。一个进程之内可以分为一到多个线程
core → 线程1[指令1,指令2,指令3…] 线程2[指令1,指令2,指令3…]
并行和并发的区别?
现在都是多核CPU,在多核CPU下
- 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
- 并行是同一时间动手做多件事的能力,4核CPU同时执行4个线程
==单核CPU== → 单核CPU下线程实际还是串行执行的
- 操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为15ms)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的
- 每个时间片只能用有一个线程被执行
- 总结一句话:**微观串行,宏观并行**
- 一般会将这种线程轮流使用CPU的做法称为并发(concurrent)
CPU | 时间片1 | 时间片2 | 时间片3 |
---|---|---|---|
core | 线程1 | 线程2 | 线程3 |
==多核CPU== → 每个核(core)都可以调度运行线程,这个时候线程是可以并行的
CPU | 时间片1 | 时间片2 | 时间片3 | 时间片4 |
---|---|---|---|---|
core1 | 线程1 | 线程2 | 线程3 | 线程3 |
core2 | 线程2 | 线程4 | 线程2 | 线程4 |
并发 (concurrent) 是同一时间应对 (dealing with) 多件事情的能力
并行 (parallel) 是同一时间动手做 (doing) 多件事情的能力
- 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这么多件事,这就是并发【单核CPU】
- 家庭主妇雇了个保姆,她们一起做这些事,这时既有并发,也有并行【会产生竞争,例如锅只有一个,一个人用锅时,另一个人就要等待】
- 雇了3个保姆,一个专门做饭,一个专门打扫卫生,一个专门喂奶,互不干扰,这就是并行
创建线程的方式有哪些?
public class MyThread extends Thread{
@Override
public void run(){
sout("MyThread...run...");
}
public static void main(String[] args){
// 创建MyThread对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
// 调用start方法启动线程
t1.start();
t2.start();
}
}
public class MyRunnable implements Runnable {
@Override
public void run() {
// 在这里编写要执行的任务
System.out.println("线程正在执行任务...");
}
public static void main(String[] args) {
// 创建MyRunnable实例
MyRunnable myRunnable = new MyRunnable();
// 创建线程并启动
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
// 调用start方法启动线程
t1.start();
t2.start();
}
}
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
sout(Thread.currentThread().getName());
return "ok";
}
public static void main(String[] args) {
// 创建MyCallable实例
MyCallable myCallable = new MyCallable();
// 使用FutureTask来包装Callable对象
FutureTask<String> ft = new FutureTask<String>(myCallable);
// 创建并启动线程
Thread t1 = new Thread(ft);
t1.start();
// 调用ft的get方法获取执行结果
String result = ft.get();
sout(result)
}
}
public class MyExecutors implements Runnable{
@Override
public void run(){
sout("MyRunnable...run...");
}
public static void main(String[] args){
// 创建线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(3);
threadPool.submit(new MyExecutors());
//submit用来提交线程
// 关闭线程池
threadPool.shutdown();
}
}
刚刚你说过,使用runnable和callable都可以创建线程,它们有什么区别呢?
- Runnable接口run方法没有返回值
- Callable接口call方法有返回值,要结合FutureTask配合可以用来获取异步执行的结果
FutureTask
是Future
的实现类,它可以包装一个Callable
或Runnable
对象,并允许我们在任务执行完毕后获取执行结果或取消任务。
FutureTask
可以在子线程中异步执行任务,而主线程可以通过调用FutureTask.get()
方法获取任务执行的结果。
- Callable接口的call()方法允许抛出异常;而Runnabble接口的run()方法的异常只能在内部消化,不能继续上抛
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class CallableExample { public static void main(String[] args) throws Exception { // 创建一个Callable任务 Callable<Integer> task = new Callable<Integer>() { @Override public Integer call() throws Exception { System.out.println("Task is running in the background..."); // 模拟耗时操作 Thread.sleep(2000); return 42; // 返回计算结果 } }; // 创建FutureTask对象,包装Callable任务 FutureTask<Integer> futureTask = new FutureTask<>(task); // 启动线程执行FutureTask Thread thread = new Thread(futureTask); thread.start(); // 主线程可以做一些其他工作 System.out.println("Main thread is doing something else..."); // 获取异步执行结果,阻塞直到任务完成 Integer result = futureTask.get(); // 这会阻塞主线程直到获取到结果 System.out.println("Task result: " + result); // 打印任务执行结果 } }
在启动线程的时候,可以使用run方法吗?run()和start()有什么区别?
start()是开启一个线程 run()跟开启普通方法一样
start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次
run():封装了要被线程执行的代码,可以被调用多次
线程包括哪些状态,状态之间是如何变化的?
状态:
新建New、可运行Runnable、阻塞Blocked、等待Waiting、时间等待Timed_waiting、终止Terminated线程状态之间如何变化:
- 创建线程对象是新建状态
- 调用了start()方法转变为可执行状态
- 线程获取到了CPU的执行权,执行结束是终止状态
- 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
- 如果没有获取锁(synchronized或lock) 进入阻塞状态,获得锁再切换为可执行状态
- 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可转换为可执行状态
- 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
Thread.java
public enum State {
/**
* 新建状态。线程已经被创建,但尚未启动。
*/
NEW,
/**
* 可运行状态。线程在JVM中是可运行的,这并不意味着它一定在运行,它可能在等待其他线程或操作系统的资源。
*/
RUNNABLE,
/**
* 阻塞状态。线程正在等待监视器锁,以进入一个同步块/方法,或者在调用Object.wait后等待重新进入同步块/方法。
*/
BLOCKED,
/**
* 等待状态。线程在等待另一个线程执行特定操作。例如,一个线程调用了Thread.join,它在等待指定的线程终止。
*/
WAITING,
/**
* 超时等待状态。线程在等待另一个线程执行特定操作,但它设置了超时时间。如果线程在指定时间内没有等待到所需条件,它将自动返回。
*/
TIMED_WAITING,
/**
* 终止状态。线程已经完成了执行。
*/
TERMINATED;
}
新建T1、T2、T3三个线程,如何保证它们按顺序执行?
可以使用线程中的join方法解决join() 等待线程运行结束
t.join() 阻塞调用此方法的线程进入timed_waiting 直到线程t执行完毕后,此线程再继续执行
Thread t1 = new Thread(()->{
sout("t1");
});
Thread t2 = new Thread(()->{
try{
t1.join();
}catch(InterruptedException e){
e.printStackTrance();
}
sout("t2");
})
Thread t3 = new Thread(()->{
try{
t2.join();
}catch(InterruptedException e){
e.printStackTrance();
}
sout("t3");
});
// 启动线程
t1.start();
t2.start();
t3.start();
notify() 和 notifyAll() 有什么区别?
- notifyAll:唤醒所有wait的线程
- notify:只随机唤醒一个wait线程
java中wait和sleep方法有什么区别?wait要和synchronized一起使用
共同点
wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态
不同点
方法归属不同
- sleep(long)是Thread的静态方法
- 而wait(),wait(long)都是Object的成员方法,每个对象都有
醒来时机不同
- 执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
- wait(long)和wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去
- 它们都可以被打断唤醒
锁特性不同【重点】
- wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
- wait方法执行后会释放锁对象,允许其他线程获得该锁对象 (我放弃cpu,但你们还可以用)
- 而sleep如果在synchronized代码块中执行,并不会释放锁对象 (我放弃cpu,你们也用不了)
如何停止一个正在运行的线程?
有三种方式可以停止线程
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
- 使用stop方法强行终止(不推荐,方法已作废)
- 使用interrupt方法中断线程
- 打断阻塞的线程(sleep, wait, join)的线程,线程会抛出InterruptedException异常
- 打断正常的线程,可以根据打断状态来标记是否退出线程
synchronized关键字的底层原理?底层:Monitor
- synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
- 它的底层由monitor实现的,monitor**是jvm级别的现象(C++实现)**,线程获得锁需要使用对象(锁)关联monitor
- 在monitor内部有三个属性,分别是
owner、entrylist、waitset
- owner是关联的获得锁的线程,并且只能关联一个线程;
- entrylist关联的是处于阻塞状态的线程;
- waitset关联的是处于Waiting状态的线程;
synchronized关键字的底层原理—进阶
Monitor实现的锁属于重量级锁,你了解过锁升级吗?
一旦锁发生了竞争,都会升级为重量级锁
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
描述 重量级锁 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低 【有多个线程来抢】 轻量级锁 线程加锁的时间是错开的(也就是没有竞争)可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性 偏向锁 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令
- Monitor实现的锁属于重量级锁,里面涉及到了用户态
权限低和内核态权限高的切换、进程的上下文切换,成本较高,性能比较低 - 在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下使用传统锁机制带来的性能开销问题
Monitor重量级锁
每个Java对象都可以关联一个Monitor对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针
加锁流程
- 在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
- 通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
- 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为nul,起到了一个重入计数器的作用。
- 如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程
- 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record.
- 如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
- 如果Lock Record的 Mark Word不为nul,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。
偏向锁性能比轻量级锁好
- 轻量级锁在没有竞争时(就自己这个线程)每次重入仍然需要执行 CAS 操作。
- Java6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
你谈谈JMM (Java内存模型)
Java内存模型
- JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
- JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程跟线程之间是相互隔离,线程跟线程相互需要通过主内存
CAS你知道吗?
- CAS全称是:Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
- CAS使用到的地方很多:AQS框架、AtomicXXX类
- 在操作共享变量的时候使用自旋锁,效率上更高一些
- CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现
在JUC(java.util.concurrent)包下实现的很多类都用到了CAS操作
- AbstractQueuedSynchronizer (AQS框架)
- AtomicXXX类
乐观锁和悲观锁的区别?
谈一谈你对volatile的理解?
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
保证线程间的可见性
用volatile修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见
禁止进行指令重排序
用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
什么是AQS?
- 是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的
- AQS内部维护了一个**先进先出的双向队列**,队列中存储的排队的线程
- 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无所状态),如果队列中有一个线程修改成功了state为1,则当前线程就相当于获取了资源。
- 在对state修改的时候使用CAS(compare and swap)操作,保证多个线程修改的情况下原子性
AQS(AbstractQueuedSynchronizer),即抽象队列同步器。它是构建锁或者其他同步组件的基础框架
AQS与Synchronized的区别
synchronized | AQS |
---|---|
关键字,C++语言实现 | java语言实现 |
悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
锁竞争激励都会升级为重量级锁,性能差 | 锁竞争激烈的情况下,提供了多种解决方案 |
AQS常见的实现类
- ReentrantLock 阻塞式锁
- Semaphore 信号量
- CountDownLatch 倒计时锁
ReentrantLock [rɪ’entrənt]lock 的实现原理?[关联HashMap线程不安全需加锁(synchronized或ReentrantLock)]
ReentrantLock主要利用CAS+AQS队列
CompareAndSwap+AbstractQueuedSynchronized来实现。**它支持公平锁和非公平锁,两者的实现类似构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁**。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:
- 可中断
synchronized不可中断 - 可设置超时时间
没有获得锁时只能进入等待[没有获取锁可以放弃锁] - 可以设置公平锁
synchronized只有非公平锁[也支持非公平锁] - 支持多个条件变量
- 与synchronized一样,都支持重入
synchronized和Lock有什么区别?
- 语法层面
synchronized是关键字,源码在jvm中,用c++语言实现
Lock是接口,源码由jdk提供,用java语言实现
使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁
- 功能层面
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock提供了许多synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量
Lock有适合不同场景的实现,如ReentrantLock、ReentrantReadWeiteLock(读写锁)
死锁产生的条件是什么
死锁:一个线程需要同时获取多把锁,这时就容易发生死锁
如何进行死锁诊断 ?
当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和jstack
- jps:输出JVM中运行的进程状态信息
- jstack:查看java进程内线程的堆栈信息
JVM中也有死锁,jvm没有超时机制不会解决 可以查看命令打印堆栈信息可以查看哪里产生死锁
你可以使用
jstack
命令来打印指定进程ID的Java堆栈跟踪信息。这个命令可以帮助你分析线程的状态
首先,找到你的Java进程ID(PID)。你可以使用
jps
命令来列出所有正在运行的Java进程及其PID。jps
使用
jstack
命令打印出该Java进程的堆栈跟踪。jstack -l <PID>
将
<PID>
替换为实际的进程ID。查找堆栈跟踪中的”DEADLOCK”关键字。
jstack
会自动检测死锁并在输出中报告。
其他解决工具,可视化工具
- jconsole
用于对jvm的 内存,线程,类 的监控,是一个基于jmx的GUI性能监控工具
打开方式:java安装目录 bin目录下 直接启动 jconsole.exe
就行
- VisualVM:故障处理工具
能够监控线程,内存情况,查看方法的cpu时间和内存中的对象,已被GC的对象,反向查看分配的堆栈
打开方式:java安装目录 bin目录下 直接启动 jvisualvm.exe
就行
死锁:两个线程争夺两个资源的时候 1线程拿到a 想拿b 2线程拿到了b 想拿a
四个原因:互斥条件 请求保持 不可剥夺 循环等待
产生死锁的四个因素 同时满足才会死锁 想要解决死锁 需要打破其中一个原因就行
- 互斥条件(Mutual Exclusion):资源不能被多个线程同时使用。即某个资源在一段时间内只能由一个线程占用,其他线程必须等待该资源被释放后才能使用。
- 持有和等待条件(Hold and Wait):线程至少持有一个资源,并且正在等待获取额外的资源,而该资源又被其他线程持有。
- 非抢占条件(No Preemption):已经分配给某个线程的资源在该线程完成任务前不能被抢占,即只能由线程自己释放。
- 循环等待条件(Circular Wait):存在一种线程资源的循环等待链,每个线程都在等待下一个线程所持有的资源。
在实际操作中,以下是一些打破死锁的具体方法:
银行家算法可以避免死锁
- 资源分配图:使用资源分配图来检测循环等待条件,并在检测到循环时采取措施。
- 锁排序:确保所有线程以相同的顺序获取锁,从而避免循环等待。
- 超时机制:线程在请求资源时设置超时时间,如果超过时间未获得资源,则放弃当前任务并释放已持有的资源。
- 死锁检测算法:运行死锁检测算法,如银行家算法,来检测系统中的死锁,并在必要时采取措施。
- 线程中断:允许系统或其他线程中断正在等待资源的线程。
- 回滚操作:如果检测到死锁,可以让某些线程回滚它们的工作,并释放资源,从而打破死锁。
MySQL是不会有死锁的 自身会检测 [让后面的超时释放回滚]
在分布式事务 线程1拿着资源a是数据库1 线程2拿着资源b是数据库2
JVM中也有死锁,jvm没有超时机制不会解决 可以查看命令打印堆栈信息可以查看哪里产生死锁
你可以使用
jstack
命令来打印指定进程ID的Java堆栈跟踪信息。这个命令可以帮助你分析线程的状态
首先,找到你的Java进程ID(PID)。你可以使用
jps
命令来列出所有正在运行的Java进程及其PID。jps
使用
jstack
命令打印出该Java进程的堆栈跟踪。jstack <PID>
将
<PID>
替换为实际的进程ID。查找堆栈跟踪中的”DEADLOCK”关键字。
jstack
会自动检测死锁并在输出中报告。
聊一下ConcurrentHashMap
ConcurrentHashMap
是一种线程安全的高效Map集合
底层数据结构:
JDK1.7底层采用分段的数组+链表实现
JDK1.8采用的数数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:
数组+红黑树+链表
,采用CAS + Synchronized来保证并发安全进行实现- CAS控制数组节点的添加
- synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题,效率得到提升
加锁的方式:
- JDK1.7采用
Segment
分段锁,底层使用的是ReentrantLock
- JDK1.8采用
CAS
自旋锁添加新节点,采用synchronized
锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好
导致并发程序出现问题的根本原因是什么 (Java程序中怎么保证多线程的执行安全)
Java并发编程三大特性
- 原子性
synchronized、lock:一个线程在CPU中操作不可暂停,也不可中断,要么执行完成,要么不执行
int ticketNum = 10;
public void getTicket(){
if(ticketNum <= 0){
return;
}
sout(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum);
// 非原子性操作
ticketNum--;
}
main{
TicketDemo demo = new TicketDemo();
for(int i = 0; i < 20; i++){
new Thread(demo::getTicket).start();
}
}
不是原子操作,怎么保证原子操作呢?
- synchronized:同步加锁
- JUC里面的lock:加锁
- 可见性
volatile、synchronized、lock
内存可见性:让一个线程对共享变量的修改对另一个线程可见
public class VolatileDemo{
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException{
new Thread(()->{
while(!flag){
sout("第一个线程执行完毕...");
}
}).start();
Thread.sleep(100);
new Thread(()->{
flag = true;
sout("第二个线程执行完毕...");
}).start();
}
}
解决方案:synchronized、volatile、LOCK
volatile:加在共享变量上面即可 → private static volatile boolean flag = false;
- 有序性
volatile
指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
int x;
int y;
@Actor
public void actor1(){
x = 1;
y = 1;
}
@Actor
public void actor2(II_Result r){
r.r1 = y;
r.r2 = x;
}
解决办法:在前面加上volatile
说一下线程池的核心参数
为什么要创建线程池 因为每次创建线程的时候就要占用一定的内存空间 无限创建线程回浪费内存严重会导致内存溢出
CPU有限的同一时刻只能同时处理一个线程 大量线程来的话就没有线程权 会造成线程等待 造成大量线程在之间切换也会导致性能变慢
在这个例子中,我们创建了一个线程池,核心线程数为5,最大线程数为10,如果线程池中的线程数大于核心线程数,则空闲线程在60秒后会被终止。工作队列使用ArrayBlockingQueue,其容量为100。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExample {
public static void main(String[] args) {
// 核心线程数
int corePoolSize = 5;
// 最大线程数 = (核心线程 + 救急线程的最大数目)
int maximumPoolSize = 10;
// 线程池中超过 corePoolSize 数量的空闲线程最大存活时间
long keepAliveTime = 60L;
// 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
TimeUnit unit = TimeUnit.SECONDS;
// 工作队列,用于存放提交的任务 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
// 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
ThreadFactory threadFactory = new ThreadFactory;
// 拒绝策略 - 当所有线程都繁忙,workQueue也繁忙时,会触发拒绝策略
RejectedExecutionHandler handler = new RejectedExecutionHandler;
// 创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue
);
// 示例:向线程池提交任务 threadPoolExecutor.submit()/.execute()
for (int i = 0; i < 20; i++) {
int taskNumber = i;
threadPoolExecutor.execute(() -> {
System.out.println("Executing task " + taskNumber);
// 模拟任务执行时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
threadPoolExecutor.shutdown();
}
}
一开始new的时候没有 是空的。先当一个任务提交给线程池时,线程池首先检查当前运行的线程数是否达到核心线程数。如果没有达到核心线程数,线程池会创建一个新的线程来执行任务。如果已经达到核心线程数,线程池会将任务放入工作队列中等待执行。如果工作队列满了,并且当前运行的线程数小于最大线程数,,线程池会创建新的线程来执行任务。如果工作队列满了,并且当前运行的线程数等于最大线程数,线程池会根据拒绝策略
- 丢弃任务抛出异常
- 丢弃任务不抛弃异常
- 丢弃队列最前面的任务,然后重新提交被拒绝的任务、
- 由主线程处理该任务来处理无法执行的任务。【线程池无法起到异步问题】
- 问题:想继续异步且不丢弃任务怎么办?
- 把这个业务先存到别的地方 ↓↓↓
- 自定义拒绝策略 自己写实现类实现拒绝策略 可以先存到mysql到时候再慢慢搞
线程池中有哪些常见的阻塞队列
线程工厂可以设置创建的属性:
守护线程:主线程(main)一天不死 守护线程不死 [同生共死]
非守护线程:new一个就是 [不是同生共死]
workQueue - 阻塞队列常用的队列:当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
- ArrayBlockingQueue: 基于数组结构的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。创建时需要指定容量。【底层是数组 随机读写的 **时间复杂度O(1)**】
- 开辟新空间创建新数组 把旧数组的数据迁移过去 new ArrayList为空 需要add才可以 扩容是+10 取1.5倍
- 高并发不会超过某个值 数组不会涉及到扩容 性能会好一些【比较稳定能预估】
- new的时候不用指定长度
- LinkedBlockingQueue: 基于链表结构的有界阻塞队列(如果不指定容量,则默认为
Integer.MAX_VALUE
,即视为无界)。按照先进先出的原则排序元素。【随机读写的 时间复杂度O(n) 随机读写快 查询慢 是通过二分查找定位到下标元素(通过下标访问数组和链表) 只会走一次二分查找】- 读中间的慢 读头尾快
- 新增元素不涉及到数组的迁移
- 一般情况下高并发推荐使用,因为队列
高级数据结构(可以用数组和链表的实现 由于底层数据结构不同)的特性是先进先出,链表不涉及到数组的扩容 末尾的最快是O(1)【不稳定】 - new的时候可指定长度是最大链表的长度
- 不可指定长度 [有界队列&无界队列] → 可能产生JVM的OOM
- DelayedWorkQueue:是一个优先级队列,它可以保证每次出队的任务都是当前队列中时间最靠前的
- SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作
ArrayBlockingQueue | LinkedBlockingQueue |
---|---|
强制有界 | 默认无界,支持有界 |
底层是数组 | 底层是链表 |
提前初始化Node数组 | 是懒惰的,创建节点的时候添加数据 |
Node需要是提前创建好的 | 入队会生成新Node |
一把锁 | 两把锁(头尾) |
如何确定核心线程数
① 高并发、任务执行时间短 → (CPU核数 + 1),减少线程上下文的切换
② 并发不高、任务执行时间长
- IO密集型任务 → (CPU核数 * 2 + 1)
- 计算密集型任务 → (CPU核数 + 1)
③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置参考②
- IO密集型任务:文件读写、DB读写、网络请求等 核心线程数大小设置为2N+1
- CPU密集型任务:计算型代码、Bitmap转换、Gson转换等 核心线程数大小设置为N+1
// 查看机器的CPU核数
public static void main(String[] args){
// 查看机器的CPU核数
System.out.println(Runtime.getRuntime().avaliableProcessors());
}
线程池的种类有哪些
在java.util.concurrent.Executors
类中提供了大量创建线程池的静态方法,常见的有四种
① 创建使用固定线程数的线程池
适用于任务已知,相对耗时的任务
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads, nThreads,0L,TimeUnit.MILLISECONDS.new LinkedBlockingQueue<Runnable>)
}
- 核心线程数与最大线程数一样,没有
救急线程 = 最大线程数 - 核心线程数
- 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
② 单线程化的线程池它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行→[先进先出]
适用于按照顺序执行的任务
public static ExecutorService newSingleThreadExecutor(){
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
- 核心线程数和最大线程数都是1
- 阻塞队列是
LinkedBlockingQueue
,最大容量为Integer.MAX_VALUE
③ 可缓存线程池
public static ExecutorService newCachedThreadPool(){
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
- 核心线程数为0
- 最大线程数是
Integer.MAX_VALUE
- 阻塞队列是
SynchronousQueue
: 不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作
④ 提供了 延迟
和 周期执行
功能的ThreadPoolExecutor
public ScheduledThreadPoolExecutor(int corePoolSize){
super(corePoolSize, Integer.MAX_VALUE,0,NANOSECONDS,new DelayedWorkQueue());
}
为什么不建议使用Executors创建线程池?
参考阿里开发手册
【强制】 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
1. FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2. CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
线程池的使用场景①:ES数据批量导入
CountDownLatch
CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时时(一个或多个线程,等待其他多个线程完成某件事情之后才能执行)
- 其中构造参数用来初始化等待计数值
await()
用来等待计数归零countDown()
用来让计数减一
多线程使用场景一 (es数据批量导入)
在我们项目上线之前,我们需要把数据库中的数据一次性的同步到es索引库中,但是当时的数据好像是1000万左右一次性读取数据肯定不行(oom异常),当时我就想到可以使用线程池的方式导入,利用CountDownLatch来控制就能避免一次性加载过多,防止内存溢出
DB(一千万) → 线程池(CountDownLatch) → Elasticearch
批量导入 → 查询总条数 → DB
↓ ↑ 批量导入到ES中 → ES
(固定每页2000条) 计算总页数 ↑ (countDownLatch.countDown())
↓ ↑ ↑
(总页数) CountDownLatch ↑ ↑
↓ ↑ ↑
分页查询文章数据 → [查询当前页的文章 → 创建任务批量导入ES → 提交到线程池执行]循环
(文章列表, countDownLatch)
↓
countDownLatch.await()
线程池的使用场景②:数据汇总
- 在一个电商网站中,用户下单之后,需要查询数据,数据包含了三部分:订单信息、包含的商品、物流信息;这三块信息都在不同的微服务中进行实现的,我们如何完成这个业务呢?
- 在实际开发的过程中,难免需要调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能
[统计的图文发布量、点赞数量、收藏数量、评论数量若不在同一台微服务下 或者 部分没有依赖关系]
- 在实际开发的过程中,难免需要调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能
线程池的使用场景③:异步调用
为了避免下一级方法影响上一级方法(性能考虑),可使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法相应时间
如何控制某个方法允许并发访问线程的数量
Semaphore信号量,是JUC包下的一个工具类,底层是AQS,我们可以通过其限制执行的线程数量
适用场景:
通常用于那些资源有明确访问数量限制的场景,常用于限流
Semaphore使用步骤
- 创建Semaphore对象,可以给一个容器
- semaphore.acquire():请求一个信号量,这时候的信号量个数 -1 (一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)
- semaphore.release():释放一个信号量,此时信号量个数 +1
谈一谈你对ThreadLocal的理解
- ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】避免争用引发的线程安全问题
- ThreadLocal 同时实现了线程内的资源共享
- 每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
- 调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线
程的 ThreadLocalMap 集合中- 调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中査找关联的资源值
- 调用remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
- ThreadLocal内存泄漏问题ThreadLocalMap 中的key是弱引用,值为强引用; key会被Gc释放内存,关联 value的内存并不会释放。建议主动remove 释放 key,value
ThreadLocal概述
ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal同时实现了线程内的资源共享
案例:使用JDBC操作数据库时,会将每一个线程的Connection放入各自的ThreadLocal中,从而保证每个线程都在各自的 Connection 上进行数据库的操作,避免A线程关闭了B线程的连接。
ThreadLocal基本使用
- set(value) 设置值
- get() 获取值
- remove() 清除值
ThreadLocal的实现原理 & 源码解析
ThreadLocal本质来说就是一个线程内部存储类,从而让多个线程只操作自己内部的值,从而实现线程数据隔离
JVM相关面试题
什么是JVM?
JVM = Java Virtual Machine
是java程序的运行环境
JVM是运行在操作系统中的 屏蔽了操作系统的差异
好处:
- 一次编码,到处运行
- 自动内存管理,垃圾回收机制
什么是程序计数器?
程序计数器:线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址
javap -v xx.class
:打印堆栈大小,局部变量的数量和方法的参数
找到Application的class文件后 → Build → Rebuild Project
编译一下→ 找到该Application的class文件黄色的→ Open in → Terminal → javap -v Application.class
你能给我详细介绍Java堆吗?
线程共享的区域:主要用来保存对象实例、数组等,当堆中没有内存空间可分配给实例,也无法再扩展,则抛出OutOfMemoryError
异常
- 组成:年轻代 + 老年代
- 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区
幸存者区 - 老年代主要保存生命周期长的对象,一般是一些老的对象
- 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区
- jdk1.7和1.8的区别
- 1.7中有一个永久代,存储的是类信息、静态变量、常量、编译后的代码
- 1.8移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出
什么是虚拟机栈?
Java Virtual machine Stacks
(Java虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈,先进后出
- 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
垃圾回收是否涉及栈内存?
不涉及,因为垃圾回收主要指的是堆内存。
这里当栈帧弹栈后,内存就会释放
栈内存分配越大越好吗?
未必,默认的栈内存通常为1024k
栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半
方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
什么情况下会导致栈内存溢出?
栈帧过多导致栈内存溢出,经典问题:递归调用
栈帧过大导致栈内存溢出
堆栈的区别是什么?
栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的的。堆会GC垃圾回收,而栈不会
栈内存是线程私有的,而堆内存是线程共有的。
两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.ang.OutOfMemoryError。
能不能解释一下方法区
- 方法区(Method Area)是各个线程共享的内存区域
- 主要存储类的信息、运行时常量池
- 虚拟机启动的时候创建,关闭虚拟机时释放
- 如果方法区域中的内存无法满足分配请求,则会抛出
OutOfMemoryError: Metaspace
常量池
可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
在Terminal中执行:javap -v Application.class
可以查看字节码结构 (类的基本信息、常量池、方法定义)
当类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
你听过直接内存吗?
直接内存:并不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统内存,常见于NIO操作时,用于数据缓冲区,它分配回收成本较高,但读写能力高。[平时的是BIO]
直接内存并不属于JVM中的内存结构,不由VM进行管理。是虚拟机的系统内存常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受 JVM 内存回收管理
什么是类加载器,类加载器有哪些?
类加载器
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来
- 引导类加载器(Bootstrap ClassLoader):
加载JAVA_HOME/jre/lib目录下的库- 这是最顶层的类加载器,它用于加载Java的核心库,这些库位于
<JAVA_HOME>/jre/lib
目录(比如rt.jar
、resources.jar
等),或者被-Xbootclasspath
参数指定的路径中。 - 引导类加载器是用原生代码(如C/C++)实现的,它属于JVM的一部分。
- 它并不继承自
java.lang.ClassLoader
,而是由JVM自身实现。
- 这是最顶层的类加载器,它用于加载Java的核心库,这些库位于
- 扩展类加载器(Extension ClassLoader):
加载JAVA_HOME/jre/lib/ext目录中的类- 它负责加载
<JAVA_HOME>/lib/ext
目录中,或者由系统属性java.ext.dirs
指定的路径中的类库。 - 它是
sun.misc.Launcher$ExtClassLoader
类的实例。
- 它负责加载
- 系统类加载器(System ClassLoader):
用于加载classPath下的类- 也称为应用类加载器(Application ClassLoader),它负责加载用户类路径(Classpath)上的所有类库。
- 系统类加载器是
sun.misc.Launcher$AppClassLoader
类的实例。 - 它是程序中默认的类加载器,可以通过
ClassLoader.getSystemClassLoader()
方法获取。
- 自定义加载器(CustomizeClassLoader)
自定义继承ClassLoader,实现自定义类加载规则- 用户还可以自定义类加载器。自定义类加载器通过继承
java.lang.ClassLoader
类并重写相应的方法来实现。自定义类加载器可以用于特定的需求,例如在Web容器中加载类,或者在运行时从网络或其他地方动态加载类。
- 用户还可以自定义类加载器。自定义类加载器通过继承
什么是双亲委派模型?
加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类
JVM为什么采用双亲委派机制?
- 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性
- 为了安全,保证类库API不会被修改
说一下类装载的执行过程?
加载:查找和导入class文件
验证:保证加载类的准确性
准备:为类变量分配内存并设置类变量初始值
解析:把类中的符号引用转换为直接引用
初始化:对类的静态变量,静态代码块执行初始化操作
使用:JVM 开始从入口方法开始执行用户的程序代码
卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象
类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)
- 通过类的全名,获得类的二进制数据流
- 解析类的二进制数据流为方法区内的数据结构(Java类模型)
- 创建
java.lang.Class
类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
对象什么时候可以被垃圾器回收
如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收
怎么确定什么是垃圾?
引用计数法
一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收
可达性分析算法
采用的都是通过可达性分析算法来确定哪些内容是垃圾
JVM垃圾回收算法有哪些?
标记清除算法
是将垃圾回收分为2个阶段,分别为标记和清除
- 根据可达性分析算法得出的垃圾进行标记
- 对这些标记为可回收的内容进行垃圾回收
复制算法
将原有的内存空间一分为二,每次只用其中的一块,正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收;无碎片,内存使用率低
标记清理算法
一般用于老年代标记清除算法一样,将存活对象都向内存另一端移动,然后清理边界以外的垃圾,无碎片,对象需要移动,效率低
JVM的分代回收是什么?
分代收集算法
在java8时,堆被分为了两份:新生代和老年代[1:2]
对于新生代,内部又分为了三个区域,Eden区,幸存者区survivor(分成from和to)【8:1:1】
MinorGC、MixedGC、FullGC的区别是什么
- MinorGC
(youngGC)发生在新生代的垃圾回收,暂停时间短(STW) - MixedGC:新生代 + 老年代 部分区域的垃圾回收,G1收集器特有
- FullGC:新生代 + 老年代 完整垃圾回收,暂停时间长(STW),应尽力避免
STW(Stop-The-World)
:暂停所有应用程序线程,等待垃圾回收的完成
JVM有哪些垃圾回收器?
在jvm中,实现了多种垃圾收集器,包括:
串行垃圾收集器
Serial和Serial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑
- Serial 作用于新生代,采用复制算法
- Serial Old 作用于老年代,采用标记-整理算法垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成
并行垃圾收集器
Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器
Parallel New作用于新生代,采用复制算法
Parallel Old作用于老年代,采用标记-整理算法
垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
CMS(并发)垃圾收集器
CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行
G1垃圾收集器
作用在新生代和老年代
详细聊一下G1垃圾回收器
- 应用于新生代和老年代,在JDK9之后默认使用G1
- 划分成多个区域,每个区域都可以充当eden,survivor,old,humongous,其中
humongous
专为大对象准备 - 采用复制算法
- 响应时间与吞吐量兼顾
- 分成三个阶段:新生代回收(STW)、并发标记(重新标记STW)、混合收集
- 如果并发失败(即回收速度赶不上创建新对象速度),就会触发
Full GC
强引用、软引用、弱引用、虚引用的区别
强引用:只要所有 GC Roots 能找到,就不会被回收
软引用:需要配合SoftReference
使用,当垃圾多次回收,内存依然不够时候会回收软引用对象
弱引用:需要配合WeakReference
使用,只要进行了垃圾回收,就会把引用对象回收
虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队由Reference Handler
线程调用虚引用相关方法释放直接内存
- 强引用:只有所有
GCRoots
对象都不通过【强引用】 引用该对象,该对象才能被垃圾回收
User user = new User();
GC Root → User对象
- 软引用:仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收
User user = new User();
SoftReference softReference = new SoftReference(user);
GC Root → SoftReference对象 →→虚线 User对象
一开始并不会对User对象进行回收 此时User对象就是软引用 如果内存还是不够 马上又再次进行了垃 圾回收 此时软引用的User就会被回收
- 弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
User user = new User();
WeakReference weakReference = new WeakReference(user)
GC Root → WeakReference对象 →→虚线 User对象
延申话题:ThreadLocal内存泄露问题
static class Entry extends WeakReference<ThreadLocal<?>>{ Object value; Entry(ThreadLocal<?>k, Object v){ super(k); // k是弱引用 value = v; // 强引用,不会被回收 } }
- 虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由
Reference Handler
线程调用虚引用相关方法释放直接内存
User user = new User();
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference phantomReference = new PhantomReference(user, queue);
JVM调优的参数可以在哪里设置?
war包
部署在tomcat
中设置修改
TOMCAT_HOME/bin/catalina.sh
文件D:\apache-tomcat-8.5.93\bin\catalina.sh
# OS specific support. $var _must_ be set to either true or false. JAVA_OPTS="-Xms512m -Xmx1024m" cygwin=false darwin=false os400=false hpux=false
jar包
部署在启动参数
设置通常在linux系统下直接加参数启动SpringBoot项目
—VMnohup java -Xms512m -Xmx1024n -jar xxxx.jar --spring.profiles.active=prod &
nohup:用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行
**参数&
**:让命令在后台执行,终端退出后命令仍然执行
JVM调优的参数都有哪些?
对于JVM调优,主要就是调整 年轻代、老年代、元空间
的内存大小及使用的垃圾回收器类型
设置堆空间大小
设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值
-Xms: 设置堆的初始化大小 -Xmx: 设置堆的最大大小 // 不指定单位默认为字节 -Xms:1024 -Xms:1024k
堆内存设置多少合适?
- 最大大小的默认值是物理内存的1/4,初始大小是物理内存的1/64
【不设置的情况下】 - 堆太小,可能会频繁的导致年轻代和老年代的垃圾回收,会产生STW,暂停用户线程
- 堆内存大肯定是好的,存在风险,假如发生了fullgc,它会扫描整个堆空间,暂停用户线程的时间长
- 最大大小的默认值是物理内存的1/4,初始大小是物理内存的1/64
虚拟机栈的设置
虚拟机栈的设置:每个线程默认会开启1M的内存,用于存放栈帧、调用参数、局部变量等,但一般256K就够用。通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。
-Xss 对每个线程stack大小的调整,-Xss128k
年轻代中Eden区和两个Survivor区的大小比例
设置年轻代中Eden区和两个Survivor区的大小比例。该值如果不设置,则默认比例为8:1:1。通过增大Eden区的大小来减少YGC发生的次数,但有时我们发现,虽然次数减少了,但Eden区满的时候,由于占用的空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优。
-XXSurvivorRatio=8,表示年轻代中的分配比率:survivor:eden = 2:8
年前代晋升老年代阈值【默认值为15,取值范围0-15】
-XX:MaxTenuringThreshold=threshold
设置垃圾回收收集器
通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:+UserG1GC
JVM调优的参数都有哪些?
命令工具
jps 进程状态信息
jstack 查看进程内线程的堆栈信息
产生死锁可以查看jmap 查看堆栈信息[生成堆转内存快照,内存使用信息]
jmap -head pid 显示Java堆的信息 jmap -dump:format=b,file=heap.hprof pid
format=b 表示以hprof二进制格式存储Java堆的内存
file=< filename > 用于指定快照dump文件的文件名
dump
:它是我们都可以通过工个进程或系统在某一给定的时间的快照。比如在进程崩溃时,甚至是任何时候,具将系统或某进程的内存备份出来供调试分析用,dump文件中包含了程序运行的模块信息、线程信息、堆调用信息、异常信息等数据,方便系统技术人品进行错误排查
jhat 堆转储快照分析工具
jstat JVM统计监测工具[可以用来显示垃圾回收信息、类加载信息、新生代统计信息等]
- 总结垃圾回收统计:
jstat -gcutil pid
- 垃圾回收统计:
jstat -gc pid
- 总结垃圾回收统计:
可视化工具
- jconsole 用于对jvm的内存,线程,类的监控, 是一个可视化工具
D:\java\jdk-11.0.20\bin\jconsole.exe
- VisualVM 能够监控线程,内存情况
只有jdk1.8有D:\java\jdk1.8.0_181\bin\jvisualvm.exe
- jconsole 用于对jvm的内存,线程,类的监控, 是一个可视化工具
Java内存泄露的排查思路?
内存泄漏通常是指堆内存,通常是指一些大对象不被回收的情况
1、通过jmap或设置jvm参数获取堆内存快照dump
2、通过工具,VisualVM去分析dump文件,VisualVM可以加载离线的dump文件
3、通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
4、找到对应的代码,通过阅读上下文的情况,进行修复即可
JVM Stacks 虚拟机栈 → StackOverFlowError
Heap 堆 → OutOfMemoryError:java heap space
Method Are/ MateSpace 方法区/元空间 → OutOfMemoryError: Metaspace
模拟堆空间溢出场景:-VM设置参数 → -Xmx10m
List<String> list = new ArrayList<>();
while(true){
list.add("北京");
}
-------------------------------------------
// OutOfMemoryError:java heap space
如何排查启动闪退、运行一段时间宕机
获取堆内存快照dump
- 使用jmap命令获取运行中程序的dump文件
【只有在项目运行时候才可以用】
jmap -head pid 显示Java堆的信息 jmap -dump:format=b,file=heap.hprof pid 【只有在项目运行时候才可以用】
使用vm参数获取dump文件
有的情况是内存溢出之后程序则会直接中断,而jmap只能打印在运行中的程序,所以建议通过参数的方式生成dump文件
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/app/dumps/
- 使用jmap命令获取运行中程序的dump文件
VisualVM区分析dump文件
通过查看堆内存的信息,定位内存溢出问题
CPU飙高排查方案与思路?
1.使用top命令查看占用cpu的情况
2.通过top命令查看后,可以查看是哪一个进程占用cpu较高
3.使用ps命令查看进程中的线程信息
4.使用jstack命令查看进程中哪些线程出现了问题,最终定位问题
使用top命令查看占用cpu的情况
哪个进程占用的cpu最高
finalShell中输入
top
查看进程中的线程信息
ps H -eo pid,tid,%cpu | gerp pid
jstack 查看进程内线程的堆栈信息
产生死锁可以查看因为是十六进程所以要十进程转换十六进程
直接linux输入printf "%x\n" Pid
然后就可以根据十六进制的去找哪个线程cpu占用
之后查看文件是cat xxx
设计模式
框架中的设计模式 + 项目中的设计模式
简单工厂模式
简单工厂包含如下角色
- 抽象产品:定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品 :实现或者继承抽象产品的子类
- 具体工厂:提供了创建产品的方法,调用者通过该方法来获取产品。
需求:设计一个咖啡店点餐系统。
设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡【AmericanCofee】和拿铁咖啡【LatteCoffee】); 再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能。
工厂方法模式完全遵循开闭原则
方法模式的主要角色:
抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一 一对应。

抽象工厂模式
工厂方法模式只考虑生产同等级的产品,抽象工厂可以处理等级产品的生产
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产–个等级的产品,而抽象工厂模式可生产多个等级的产品。一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂
策略模式
- 该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户
- 它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理
策略模式的主要角色如下:
抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口
具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
环境(Context)类:持有一个策略类的引用,最终给客户端调用。
策略模式—登录案例 (工厂模式 + 策略模式)
什么是策略模式
策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户
一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中
案例(工厂方法+策略)
- 介绍业务(登录、支付、解析excel、优惠等级…)
- 提供了很多种策略,都让spring容器管理
- 提供一个工厂:准备策略对象,根据参数提供对象
一句话总结:只要代码中有冗长的if-else 或switch 分支判断都可以采用策略模式优化
举一反三
- 订单的支付策略(支付宝、微信、银行卡..)
- 解析不同类型excel(xls格式、xlsx格式)
- 打折促销(满300元9折、满500元8折、满1000元7折..)
- 物流运费阶梯计算(5kg以下、5-10kg、10-20kg、20kg以上)
责任链模式—概述及案例
责任链模式:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
- 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
- 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
- 客户类(Cient)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
举一反三
- 内容审核(视频、文章、课程)
- 订单创建
- 简易流程审批
常见技术场景题
单点登录这块怎么实现的?
单点登录的英文名:Single Sign On (SSO),只需要登录一次,就可以访问所有信任的应用系统
① 先解释什么是单点登录:单点登录的英文名叫做:Single SignOn(简称SSO)
② 介绍自己项目中涉及到的单点登录(即使没涉及过,也可以说实现的思路)
③ 介绍单点登录的解决方案,以JWT为例
用户访问其他系统,会在网关判断token是否有效
如果token无效则会返回401(认证失败)前端跳转到登录页面
用户发送登录请求,返回浏览器一个token,浏览器把token保存到cookie
再去访问其他服务的时候,都需要携带token,由网关统一验证后路由到目标服务
权限认证是如何实现的?
后台的管理系统,更注重权限控制,最常见的就是RBAC模型来指导实现权限
RBAC(Role-Based Access Control)基于角色的访问控制
- 3个基础部分组成:用户、角色、权限
- 具体实现:
- 5张表:用户表、角色表、权限表、用户角色中间表、角色权限中间表
- 7张表:用户表、角色表、权限表、菜单表、用户角色中间表、角色权限中间表、权限菜单中间表
张三具有什么权限呢?
流程:张三登录系统 → 查询张三拥有的角色列表 → 再根据角色查询拥有的权限
权限框架:Apache shiro
、Spring Security(推荐)
上传数据的安全性你们怎么控制?
主要说的是数据在网络上传输如何保证安全
使用**非对称加密(或对称加密)**,给前端一个公钥让他把数据加密后传到后台,后台负责解密后处理数据
对称加密
文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥
你负责项目的时候遇到了哪些比较棘手的问题?怎么解决的?1+3
其次你也可以说说aop的实现,比如你们操作日志记录等,利用aop切面思想,通过环绕通知等但需封装出出个切面工具类。建议你们说说sql调优,比如商品列表页需要分页查询,但是几百万商品导致查询慢,如何优化的,这是一个
① 设计模式在项目中的应用
是为了遵循一系列的开发原则【工厂、策略、责任链】
- 什么背景[技术问题] → 登录的例子
- 过程[解决问题的过程]
- 最终落地方案
② 线上BUGJVM+多线程
- CPU飙高
- 内存泄露
- 线程死锁
③ 调优
- 慢接口
- 慢SQL
- 缓存方案
④ 组件封装
- 分布式锁
- 接口幂等
- 分布式事务
- 支付通用
你们项目中日志怎么采集的?
我们搭建了ELK日志采集系统
介绍**ELK**的三个组件:
Elasticsearch是全文搜索分析引擎,可以对数据存储、搜索、分析
Logstash是一个数据收集引擎,可以动态收集数据,可以对数据进行过滤、分析,将数据存储到指定的位置
Kibana是一个数据分析和可视化平台,配合Elasticsearch对数据进行搜索,分析,图表化展示
- 为什么要采集日志?
日志是定位系统问题的重要手段,可以根据日志信息快速定位系统中的问题
- 采集日志的方式有哪些
- ELK:即
ElasticSearch、LogStash、Kibanna
三个软件的首字母 - 常规采集:按天保存到一个日志文件
- ELK:即
查看日志的命令?查看是否在线查看过日志
实时监控日志的变化
实时监控某一个日志文件的变化:tail -f xx.log
实时监控日志文件最后100行的变化:tail -n 100 -f xx.log
按照行号查询
查询日志尾部最后100行日志:tail -n 100 xx.log
查询日志头部开始100行日志:head -n 100 xx.log
查询某一个日志行号区间:cat -n xx.log | tail -n +100 | head -n 100
(查询100行至200行的日志)按照关键字找日志的信息
查询日志文件中包含debug的日志行号:cat -n xx.log | grep "debug"
按照日期查询
日期必须在日志中出现过sed -n '/2025-01-14 14:22:31.070/,/ 2025-01-14 14:27:18.158/p' xx.log
日志太多,处理方式
- 分页查询日志信息:
cat -n xx.log | grep "debug" | more
- 筛选过滤后,输出到一个文件:
cat -n xx.log | grep "debug" > debug.txt
- 分页查询日志信息:
上线的项目远程Debug —— 生产问题怎么排查?本地调试远程代码
已经上线的bug排查的思路:
- 先分析日志,通常在业务中都会有日志的记录,或者查看系统日志,或者查看日志文件,然后定位问题
- 远程debug(通常公司的正式环境(生产环境)是不允许远程debug的。一般远程debug都是公司的测试环
境,方便调试代码)
远程debug
前提条件:远程的代码和本地的代码要保持一致
① 远程代码需要配置启动参数,把项目打包放到服务器后启动项目的参数:
java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 project-1.0-SNAPSHOT.jar
② idea中设置远程debug,找到idea中的Edit Configurations...
→ 添加一个Remote JVM debug
→ 右侧要配置Configuration
的Host → 添加上面的代码…
③ 在项目中点debug(绿色小虫子)
④ 访问远程服务器,在本地代码中打断点即可调试远程
怎么快速定位系统的瓶颈?
压测(性能测试),项目上线之前测评系统的压力
- 压测目的:给出系统当前的性能状况;定位系统性能瓶颈或潜在性能瓶颈
- 指标:响应时间、QPS、并发数、吞吐量、CPU利用率、内存使用率、磁盘IO、错误率
- 压测工具:LoadRunner、Apache Jmeter …
- 后端工程师:根据压测的结果进行解决或调优(接口、代码报错、并发达不到要求.)
监控工具、链路追踪工具,项目上线之后监控
- 监控工具:Prometheus+Grafana
- 链路追踪工具:skywalking、Zipkin
线上诊断工具Arthas(阿尔萨斯),项目上线之后监控、排查
核心功能:Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。
当你遇到以下类似问题而束手无策时,Arthas 可以帮助你解决:这个类从哪个jar 包加载的?为什么会报各种类相关的 Exception?
我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
是否有一个全局视角来查看系统的运行状况?
有什么办法可以监控到 JVM 的实时运行状态?
怎么快速定位应用的热点,生成火焰图?
怎样直接从 JVM 内查找某个类的实例?
怎么解决cpu飙高?
使用top命令查看占用cpu的情况
通过top命令查看后,可以查看是哪一个进程占用cpu较高
使用ps命令查看进程中的线程信息 使用top -H -p 进程Id
[找线程哪个使用多]
记住要打印%X十六进制
的
使用jstack命令查看进程中哪些线程出现了问题,最终定位问题jstack 进程PID | grep 16进制线程PID -A 20
使用top命令查看占用cpu的情况
哪个进程占用的cpu最高
finalShell中输入
top
查看进程中的线程信息
ps H -eo pid,tid,%cpu | gerp pid
jstack 查看进程内线程的堆栈信息
产生死锁可以查看因为是十六进程所以要十进程转换十六进程
直接linux输入printf "%x\n" Pid
然后就可以根据十六进制的去找哪个线程cpu占用
之后查看文件是cat xxx
2025/1/14 20:35 地点广州 完结撒花
项目难点?四方保险——day11-数据中心:时序数据库、看板展示【实战】
技术上的难点:时序数据库、看板展示
秒杀系统如何优化?
痛点描述:
- 瞬时并发量大
- 大量用户会在同一时间进行抢购
- 网站瞬时访问流量激增
- 库存少
- 访问请求数量远远大于库存数量
- 只有少部分用户能够秒杀成功
Ⅰ. 访问层 — 商品页
- 可以将静态秒杀页面放在cdn上[用户访问速度↑ 减轻服务器压力++]
Ⅱ. 访问层 — 秒杀按钮
- 活动前禁用按钮、点击后禁用按钮、滑动验证码[防羊毛党]、排队体验[提升用户体验]
Ⅲ. 中间转换层 — 多级负载均衡 & 限流 & 自动伸缩
- 通常会通过Nginx来进行负载均衡【单台Ng处理的并发量是两三万左右】
- 在它上层要做到硬件级别的隔离器 【F5/LVS】
- 通过Ng负载均衡到网关之后 通过客户端的负载均衡器Ribbon
- 4级的负载均衡 可以处理每秒上10W以上的QPS并发量
- 通过Docker或K8S来进行云服务器的动态伸缩的部署[秒杀开始自动扩容 秒杀结束自动缩减]
- 注意要在Ng上做好限流 防止一些绕过了我们前端的DDOS攻击 还需要在网关层通过
Sentinel
对不同的服务节点去设置限流以及熔断的机制 - 可以在秒杀中通过MQ做削锋填股 通过MQ可以减轻下游的压力 防止激增流量打垮下游数据库
Ⅳ. 服务端 — 用Redis做缓存减轻数据库压力
- 秒杀商品信息预热到Redis中 防止Redis被击穿我们的数据库
- 通过Redis的Lua脚本[保证多个操作的原子性]操作库存
- 防重 可以通过 redis的SETNX → 用 Token + 商品URL // IP + 商品URL 只能有一个有效
- 分布式锁保证请求的原子性 → Redisson的分布式锁
**Ⅴ. 数据库 — 读写分离 **
- 数据量很大就分库分表
订单超时自动取消是怎么实现的?
① JDK自带的延时队列
优点:简单,不需要借助其他第三方组件,成本低。
缺点:所有超时处理订单都要加入到DelayQueue
中,占用内存大,没办法做到分布式处理,之恶能在集群中挑选一台leader专门处理,效率低
不适合订单量比较大的
② 基于RocketMQ的定时消息 — 延时消息
优点:使用简单,和使用普通消息一样,支持分布式。精度高,支持任意时刻
缺点:使用限制:定时时长最大值24小时。
成本高:每个订单需要新增一个定时消息,且不会马上消费,给MQ带来很大的存储成本。
同一个时刻大量消息会导致消息延迟:定时消息的实现逻辑需要先经过定时存储等待触发,定时时间到达后才会被投递给消费者。因此,如果将大量定时消息的定时时间设置为同一时刻,则到达该时刻后会有大量消息同时需要被处理,会造成系统压力过大,导致消息分发延迟,影响定时精度。
③ 基于Redis的过期监听
设置过期时间:24小时内没有支付就会自动取消
缺点:(也是所有中间件的缺点)
- 不可靠 Redis在过期通知的时候,如果应用正好重启了,那么就有可能通知事件就丢了,会导致订单一直无法关闭,有稳定性问题。如果一定要使用Redis过期监听方案,建议再通过定时任务做补偿机制。
- 如果订单量大需要占用中间件大量的存储空间,需要额外维护成本。
④ 定时任务分布式处理【要按照成本思维的思考方式】
通过定时任务(任务调度)的批量处理 → 一次性把所有超时的订单全部捞出来 处理完再全部执行更新
如果使用中间件都要单独存储那些数据,如果存储压力大就要涉及到集群
如果对于超时精度比较高,超时时间在24小时内,且不会有峰值压力的场景下,推荐使用RocketMQ的定时消息解决方案
在电商业务下,许多订单超时场景都在24小时以上,对于超时精度没那么敏感,并且有海量订单需要批处理,推荐使用基于定时任务的跑批解决方案。
如何防止重复下单?
方案一:提交订单按钮置灰 [防止用户无意点击多次]
方案二:后端采用redis的setnx 来保证它的唯一幂等性
setnx:当我们调用setnx来去保存一个key和value的时候,如果这个value没有值的话,那么就会返回true保存成功;如果有值就会返回false → 保证多次存储只能存储一个值
怎么防止刷单?【人肉机刷单!!】
业务风控
提高羊毛门槛:实名认证、消费门槛、随机优惠
限制用户参与、中奖、奖励次数
根据用户的历史行为和忠诚度,提供不同层次的优惠,优待忠实用户
奖池(优惠券数量)限制上限
分布式集群架构下怎么保证并发安全?
让你设计一个扫码登录怎么实现?
生成二维码
请求登录页生成二维码,PC端请求后端生成一个二维码,此时在后端就会生成一个全局唯一的二维码ID,主要保存二维码的状态[二维码ID, NEW],状态设置到Redis设置过期时间,然后把当前的二维码ID返回给前端,然后生成二维码 【前后端都可以生成 → 返回Base64的编码给前端】此时的二维码就绑定了用户的ID让用户扫描。
扫码
PC端和后端会建立一个轮询的请求,不断的根据二维码ID去查询二维码状态,一旦状态改变页面也会改变。也可以通过长连接WebSocket获取状态 淘宝用的轮询、抖音用的长连接
,此时就可以扫码。
扫码前保证手机是登录状态 没有登录肯定是不能扫码的,登录后进行扫码就会携带手机端的用户token以及二维码的ID在后端去校验请求Token,如果校验成功就代表手机可以登录,此时可以变更二维码状态为扫描。前端就可以根据这个把页面变为待确认
状态
如何设计分布式日志存储架构?
使用redis出现缓存三兄弟如何解决?减轻数据库的压力
你在项目中用到了Redis对吧 介绍一下有没有遇到关于redis的什么问题?
暂时还没看!
12.使用redis出现缓存击穿雪崩穿透怎么解决_哔哩哔哩_bilibili
如何使用Redis记录用户连续登录了多少天?放在数据库里不合适
放在数据库不合适因为你要创建一个表 记录用户哪一天进行了签到 如果用户量很多就会很大的量
给你一亿个Redis keys统计双方的共同好友?
如何做一亿用户实时积分排行榜?
内存200M读取1G文件并统计内容重复次数?内存受限时
一次性读取肯定会OOM
可以根据缓冲区分块读取
查询200条数据耗时200ms,怎么在500ms内查询1000条数据?
SpringBoot如果有百万数据插入怎么优化?
SpringBoot可以同时处理多少请求?
volatile的应用场景?
为了保证我们并发编程的可见性和有序性
SQL的执行流程
单表最多数据量需要多大才涉及到分表?
Mysql引擎层BufferPool工作过程原理?
什么是聚集索引和非聚集索引?
count(*)、count(1)、count(字段) 谁更快?有什么区别?
tb_user
id | name |
---|---|
1 | 潘春尧 |
2 | NULL |
3 | 张三 |
Ⅰ. SELECT count(*) FROM tb_user; → 3
Ⅱ. SELECT count(1) FROM tb_user; → 3
Ⅲ. SELECT count(name) FROM tb_user; → 2
在功能上没有区别 Ⅲ.如果你统计的数据需要排除NULL 就可以用count(指定字段)
在性能上没有任何区别 非要比较就是count(1)
更胜一筹 因为不需要mysql在底层做任何的sql优化
SQL语句中使用了前模糊会导致索引失效?
分库分表id冲突解决方案?
深分页为什么慢,怎么优化?
MySQL的隔离级别实现原理MVCC ?
**MVCC ** (Multiversion Concurrency Control) 多版本并发控制器
它是事务隔离级别的无锁的实现方式,用于提高事务的并发性能
事务隔离级别 (isolation)
用来解决并发事务所产生一些问题:
并发:同一个时间,多个线程同时进行请求。
什么时候会发生并发问题:在并发情况下,对同一个数据(变量、对象)进行读写操作才会产生并发问题
并发会产生什么问题?
1.脏读一读已提交(行锁,读不会加锁)
2.不可重复度–重复读(行锁,读和写都会上锁)
3.幻影读–串行化(表锁)概念: 通过设置隔离级别可解决在并发过程中产生的那些问题
用户忘记密码,系统为什么不直接提供原密码,而让改密码
因为系统它也不知道我们的原密码是什么
服务端在保存密码的时候绝不会明文存到数据库,怕有数据库权限的人或者黑客恶意利用
必须用不可逆的加密算法
MD5
- 只能加密不可解密 但是它是hash算法 可能会有哈希冲突 至少加密2^128次方才有可能发生哈希碰撞
- 每次生成的密文都一样,不管加密多少次生成的密文都是一样的 可以通过暴力破解
- 解决暴力破解 就要在里面加盐 每次加密 解密 都要加入盐
HS256
- 增加加密字符长度 目前没有碰撞性
- 最好加入随机盐
BCrypt → 加入spring-security-core
依赖
- 盐是随机的
- 无法通过暴力破解
Git怎么修复线上的突发BUG线上突发Bug要修复,本地正在开发新需求
在git里我们通常会用一个单独的分支来进行管理
本地开发也会有一个单独的分支
可以将线上代码的分支签出来单独进行修复
- 正在开发的代码 → 暂存dev分支
- 严重故障:回滚上一个版本
非严重故障:在fix分支修复紧急Bug - 非常紧急:直接合并master分支上线
- 一般急:合并release分支,走测试、上线流程
- 非紧急:合并dev分支,走测试、线上流程
RestTemplate如何优化连接池
默认是没有连接池的
要用框架HTTPClient
、OKHTTP

Synchronized怎么提升性能
开发中有没有用到设计模式?怎么用的
策略模式 + 简单工厂 + 模板方法

SpringBoot如何防止反编译
有没有出现过Spring正常SpringBoot报错的情况?
SpringBoot配置文件敏感信息如何加密?
一个需求来了怎么办!
首先看这个需求 进行一个分析 分析这个需求跟哪些功能有关联 比如说在我做过的xxx里面 和什么关联 要思考怎么去做这个关联 数据库 代码层面 思考好之后 再去ER画图 写接口文档 再去开始写代码