java面试题(自用)

Redis相关面试题

🥇 基础知识以及应用场景

String
常用命令: set,get,decr,incr,mget 等。

String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。

常规key-value缓存应用;

常规计数:微博数,粉丝数等。

Hash
常用命令: hget,hset,hgetall 等。

hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时
候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信
息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息:

List(双向链表)
常用命令: lpush,rpush,lpop,rpop,lrange等

list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,

比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。

Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外
的内存开销。

另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分⻚查询,这
个很棒的一个功能,基于 redis 实现简单的高性能分⻚,可以做类似微博那种下拉不断分⻚的东⻄
(一⻚一⻚的往下走),性能高。

Set
常用命令: sadd,spop,smembers,sunion 等

set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。

当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某
个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、
并集、差集的操作。

比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。
Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,
具体命令如下:

(方便对二者求交集)

 sdiffstore set3 set1 set2
Sorted Set
常用命令: zadd,zrange,zrem,zcard等

和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。

举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理
解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。

相关技术:

在这里插入图片描述
缓存穿透:
缓存穿透: 查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库,而数据库的承受并发的能力是有限的

方案一: 缓存空数据: 如果查不到就返回空的数据, 仍然把这个空结果进行缓存( 优点: 简单  确定: 消耗内存)
方案二: 采用布隆过滤器 底层使用的bitmap 通过对当前的key,进行三次Hash计算,模以数组的大小,得到在数组中的位置,然后将三个位置的值改为1其中会存在误判率,数组越小误判率就越大( 其实好像跟,Hash冲突一样), 数组越大误判率就越小,但是同时带来了更多的内存消耗
(优:内存占用较少,没有多余的key   缺: 实现复杂,存在误判)
一般设置为百分之五以内

布隆过滤器的实现: Redisson 或者 Guava

    @PostMapping("/addUser")
    public Integer insert(@RequestBody MUser user) {
 
        RBloomFilter<String> bloomFilter
                = redisson.getBloomFilter(Constant.REDISSON_BLOOMFILTER_USER);
        //布隆过滤器计算的正确率为97%,初始化布隆过滤器容量为50000L
        bloomFilter.tryInit(500000L,0.03);
        if (bloomFilter.contains(user.getName())) {
            throw new InvalidArgumentException("用户名:" + user.getName() + "已经存在");
        }
 
        user.setId(null);
        int res = userMapper.insertSelective(user);
        if (res > 0) {
            //加入布隆过滤器
            bloomFilter.add(user.getName());
        }
        return user.getId();
    }

最终解决方案:

缓存预热通过实现 InitalizingBean 接口实现aftterPropertieSet方法

缓存预热案例如下: 也可以通过管道实现,尤其对于批量数据的导入。

@Override
    public void afterPropertiesSet() throws Exception {
        // 初始化缓存
        // 1.查询商品信息
        List<Item> itemList = itemService.list();
        // 2.放入缓存
        for (Item item : itemList) {
            // 2.1.item序列化为JSON
            String json = MAPPER.writeValueAsString(item);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        }
 
        // 3.查询商品库存信息
        List<ItemStock> stockList = stockService.list();
        // 4.放入缓存
        for (ItemStock stock : stockList) {
            // 2.1.item序列化为JSON
            String json = MAPPER.writeValueAsString(stock);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
        }
    }

缓存击穿:
缓存穿透: 给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮

解决方案: 
(1): 使用互斥锁
线程1 查询缓存未命中,此时就去获取分布式锁, 查询数据库, 重建缓存数据, 写入缓存
线程2 同时来了之后,查询缓存,未命中, 加锁, 失败,重试

(2): 逻辑过期
那么为什么不把数据设置为永不过期呢?
如果设置永不过期,你不知道现在缓存数据是不是最新的,你得有一个过期时间来触发去数据库刷新最新数据到缓存中
1. 设置多级缓存
2. 对于一些热点数据不设置过期时间
3. 通过分布式锁(当发现缓存没有命中时,首先去拿到分布式锁,再去数据库中查询,并将数据添加到缓存中)
4. 布隆过滤器(防止一些恶意请求的key)

缓存雪崩
首先注意缓存击穿和缓存雪崩的区别: 前者是一个热点key过期导致大量的请求到db, 后者是同一时间大量的key过期,导致大量的请求到db

缓存雪崩: 缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。 
缓存击穿: 通常针对同一个key过期,大量并发访问同一个key失败,造成数据压力过大

解决方案:
1. 给不同的key的ttl 添加随机值
2. 利用Redis集群提高服务的可用性  : (哨兵, 集群)
3. 给缓存业务添加降级限流策略     : Nginx 或者 spring CLoud gateway (降级策略可)
4. 给业务添加多级缓存		    : Caffeine或Guava
穿透无中生有key, 布隆过滤Null隔离               ----直接过滤掉布隆过滤器中没有key
缓存击穿过期key, 锁与非期解难题
雪崩大量过期key, 过期时间要随机
面试必考三兄弟, 可用限流做保底
🚩 双写一致性

当修改了数据库的数据也要同时更新缓存的数据,缓存和数据的数据要保持一致

先删缓存还是先修改数据库

读操作: 缓存命中,直接返回; 缓存未命中查询数据库, 写入缓存,设定超时时间

写操作: 延迟双删

为什么要使用延迟双删呢?

是这样的: 按照之前的逻辑, 先删除缓存再操作数据库, 线程1 把缓存删了, 线程2 来了,此时在查缓存未命中,虽然查询数据库,然后写入redis, 然后线程1 再更新数据库, 此时导致 redis的数据库是10, 数据库中是20

如果是先更新数据库,然后再删除缓存, 此时也有问题, 比如线程1,先查询缓存,发现缓存中没有数据, 然后查询数据库,此时线程2 来了更新数据库,然后删除缓存.此时线程1把刚才查到的数据填入缓存,即redis是10, 数据库中是20

但是我们一般不会使用延迟双删的策略, 因为一般这个延迟的时间控制不了. 如果前一个线程还没有执行完, 我们已经把数据删除给删除了,即会出现我们这边刚把数据删除, 那边线程把数据又给读到redis中, 导致数据不一致

此时可以使用读写锁: 读多写少, (保证了强一致性, 但是性能较低) 读的时候采用共享锁,写的时候使用排它锁

即: 在读的时候采用共享锁, 在写的时候对读的代码以及写的代码都实现排它锁(独享锁).

在这里插入图片描述

常用两种方案: 一致性要求高, 允许延迟一致

一致性要求高 CP

示意代码

允许延迟一致 AP

通过Mq异步的通知数据的修改(允许暂时的不一致, 但是会最终一致)

也可以使用 canal

canal,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费

canal的工作原理就是把自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地,比如MySQL,Kafka,Elastic Search等等。

redis的持久化
RDB

(触发RDB的四个条件 ) : 1. 关机(save)2. bgsave 3.使用save命令,4. 触发配置(bgsave)

  • save 900 1
  • save 300 10
  • save 60 10000

fork采用的是copy-on-write技术:

•当主进程执行读操作时,访问共享内存;

•当主进程执行写操作时,则会拷贝一份数据,执行写操作。

AOF

保存命令, 可以使用 bgrewriteaof 进行优化,但是此时会消耗大量的cpu和内存资源,

aof可配置:每次(Always),每秒(everysec),no(不开启)

混合模式

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

save 900 1 #在 900 秒(15分钟)之后,如果至少有 1 个key发生变化,Redis就会自动触发BGSAVE命
令创建快照。
save 300 10 #在 300 秒(5分钟)之后,如果至少有 10 个key发生变化,Redis就会自动触发BGSAVE命
令创建快照。
save 60 10000 #在 60 秒(1分钟)之后,如果至少有 10000 个key发生变化,Redis就会自动触发BGSAVE
命令创建快照。

appendonly yes
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步

如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

补充内容:AOF 重写

AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读入、分析或者写入操作。
在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作。

Redis的过期策略
惰性删除
惰性删除: 设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key

优点 :对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查

缺点 :对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放

定期删除
定期删除: 每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。

定期清理有两种模式:

SLOW模式
 SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的hz 选项来调整这个次数
FAST模式
FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。

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

(3) 定时删除 即按照规定的时间, 删除, 十分消耗CPU资源一般不采用

Redis 的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用

redis 的淘汰策略
数据的淘汰策略: 当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。

Redis支持8种不同策略来选择要删除的key:

主要分为两种, 第一种是对设置了ttl的key, 第二种是对所有

noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。

volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰

allkeys-random:对全体key ,随机进行淘汰。

volatile-random:对设置了TTL的key ,随机进行淘汰。

allkeys-lru: 对全体key,基于LRU算法进行淘汰

volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰

allkeys-lfu: 对全体key,基于LFU算法进行淘汰

volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰

LRU(Least Recently Used)最近最少使用:   用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
LFU(Least Frequently Used)最少频率使用: 会统计每个key的访问频率,值越小淘汰优先级越高。
数据淘汰策略建议

1.优先使用 allkeys-lru 策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。

2.如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random,随机选择淘汰。

3.如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。

4.如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略。

平时开发过程中用的比较多的就是allkeys-lru(结合自己的业务场景)

Redis分布式锁

什么场景下使用呢?

需要结合项目中的业务进行回答,通常情况下,分布式锁使用的场景: 集群情况下的单机定时任务、抢单、幂等性场景

xxl-job 分布式锁 + 单机定时任务

setnx 命令
  1. 首先要避免死锁, 因为方式获取锁的实例宕机 可以通过设置过期时间
  2. 锁的过期时间设置多长合适, 如果太短的话,业务还没有执行完就释放了锁, 依然会出现并发超卖问题 使用看门狗续期
Redisson
  • 原子性问题:可以利用Redis的LUA脚本来编写锁操作,确保原子性

  • 超时问题:利用WatchDog(看门狗)机制,获取锁成功时开启一个定时任务,在锁到期前自动续期,避免超时释放。而当服务宕机后,WatchDog跟着停止运行,不会导致死锁。

  • 锁重入问题:可以模拟Synchronized原理,放弃setnx,而是利用Redis的Hash结构来记录锁的持有者以及重入次数,获取锁时重入次数+1,释放锁是重入次数-1,次数为0则锁删除

  • 主从一致性问题:可以利用Redis官网推荐的RedLock机制来解决

    此时有个问题,对于集群模式如果获得到了主节点的锁, 但是主节点挂了, 此时就会产生一个新的主节点, 别的线程就可以获取到新的主节点中的锁, 那么就会有两把锁,这该怎么办???

    使用红锁, 也就是说如果是集群的话,如果能获得到多个节点的锁(n / 2 + 1) 才算是获得成功

在这里插入图片描述

每隔(releaseTime / 3)的时间做一次续期 releaseTime (释放时间): 获取锁的第一个参数

Redis集群的思想是AP思想,采用的是高可用性. 如果必须要求数据的强一致性可以使用zookeeper

在这里插入图片描述

红锁的缺点: 实现复杂, 性能差, 运维繁琐

Redis集群

主从模式解决:高并发;

哨兵模式解决:高可用。

分片集群: 海量存储. 高并发写

主从复制

⚜️ 注意: 我们在主从集群中一般会将数据持久化策略设置为 RDB ,因为我们后面需要主节点和从节点的全量同步是通过RDB

全量同步
  1. 从节点请求主节点同步数据(replication id、 offset )

  2. 主节点判断是否是第一次请求,是第一次就与从节点同步版本信息(replication id和offset)

  3. 主节点执行bgsave,生成rdb文件后,发送给从节点去执行

  4. 在rdb生成执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件)

  5. 把生成之后的命令日志文件发送给从节点进行同步

在这里插入图片描述

增量同步
  1. 从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset值

  2. 主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步

在这里插入图片描述
哨兵集群

监控、自动故障恢复、通知

在这里插入图片描述

哨兵选主规则:

  • 首先判断主与从节点断开时间长短,如超过指定值就排该从节点

  • 然后判断从节点的slave-priority值,越小优先级越高

  • 如果slave-prority一样,则判断slave节点的offset值,越大优先级越高

  • 最后是判断slave节点的运行id大小,越小优先级越高。

脑裂问题

当哨兵和master salve等节点不在同一个网络分区时, 如果此时Sentinel监控不到master节点,就会选举新的slave节点作为主节点, 而此时客户端和master的连接还没有断开, 此时如果有新的数据操作,在网络正常之后因为已经有了新的master,此时就会就会导致之前的数据丢失.

我们可以通过如下配置来避免;

redis中有两个配置参数:
min-replicas-to-write 1  : 表示最少的salve节点为1个
min-replicas-max-lag 5   : 表示数据复制和同步的延迟不能超过5秒
在这里插入图片描述
分片集群

  • 集群中有多个master,每个master保存不同数据

  • 每个master都可以有多个slave节点

  • master之间通过ping监测彼此健康状态

  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

🚩 redis管道技术

不要在一次批处理中传输太多命令,否则单次命令占用带宽过多,会导致网络阻塞

🚩 redis管道技术 在某些高并发的场景下,网络开销成了Redis速度的瓶颈,所以需要使用管道技术来实现突破。

原始:

在这里插入图片描述

学习网址: redis使用管道(Pipelining)提高查询速度

@Override
public Set<Long> isBizLiked(List<Long> bizIds) {
    // 1.获取登录用户id
    Long userId = UserContext.getUser();
    // 2.查询点赞状态   --- 使用管道查询
    List<Object> objects = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
        StringRedisConnection src = (StringRedisConnection) connection;
        for (Long bizId : bizIds) {
            String key = RedisConstants.LIKES_BIZ_KEY_PREFIX + bizId;
            src.sIsMember(key, userId.toString());
        }
        // 这里不用管
        return null;
    });
    // 3.返回结果
    return IntStream.range(0, objects.size()) // 创建从0到集合size的流
            .filter(i -> (boolean) objects.get(i)) // 遍历每个元素,保留结果为true的角标i
            .mapToObj(bizIds::get)// 用角标i取bizIds中的对应数据,就是点赞过的id
            .collect(Collectors.toSet());// 收集
}

🚩 Redis是单线程,但是为什么还是那么快?

  • 避免了不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
  • 采用I/O多路复用模型,非阻塞IO
  • 纯内存操作,所以执行速度比较快

🚩解释一下I/O多路复用模型

  • Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度I/O多路复用模型主要就是实现了高效的网络请求

  • 用户空间和内核空间

  • 常见的IO模型

    • 阻塞IO(Blocking IO)

    • 非阻塞IO(Nonblocking IO)

    • IO多路复用(IO Multiplexing)

  • Redis网络模型

🌍 用户空间和内核空间:

lLinux系统中一个进程使用的内存情况划分两部分:内核空间、用户空间

  • 用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源, 必须通过内核提供的接口来访问

  • 内核空间可以执行特权命令(Ring0),调用一切系统资源

用户空间权限较低,而当我们需要调用系统的一些资源的时候,就需要使用到内核空间,而内核空间的权限就比较高,可以调用一些的系统资源

在这里插入图片描述

A给B发消息. 最终是要将数据通过网卡发出去, 但是用户空间是没有权限操作网卡的,但是内核空间有权限,所以用户空间,就需要通过内核空间所提供的接口将消息发送到网卡.在消息发送的时候Linux为了提高IO效率,会在用户空间和内核空间都加入缓存.

  • 写数据时,要把用户缓存数据拷贝到内核缓冲区,然后写入设备
  • 读数据时,要从设备读取数据到内核缓存区, 然后拷贝到用户缓存区

耗时的两个地方:

  1. 用户空间需要数据, 需要从内核中获取,而此时内核空间缓存区中还没有数据,此时用户进程就只能在这里等待, 但是如果一直没有数据,进程就会一直在这等待
  2. 数据的拷贝, 读数据的时候需要将数据从内核拷贝到用户空间, 写数据的时候就需要从用户拷贝到内核中 , 这种来回的拷贝也是非常的影响效率
阻塞IO⛵️(BIO)

顾名思义, 阻塞IO就是两个阶段都必须阻塞等待:

阶段一:

  • 用户进程尝试读取数据 (比如网卡数据)
  • 此时数据尚未到达, 内核需要等待数据
  • 此时用户进程也处于阻塞状态

阶段二:

  • 数据到达并拷贝到内核缓冲区,代表已就绪
  • 将内核数据拷贝到用户缓冲区
  • 拷贝过程中,用户进程依然阻塞等待
  • 拷贝完成,用户进程解除阻塞,处理数据
非阻塞IO⛵️(NIO)

顾名思义,非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程

区别就是,进程之间也是来回切换的,不会造成阻塞影响其他进程吧

阶段一:

  1. 用户尝试读取数据(比如网卡数据)
  2. 此时数据尚未到达,内核需要等待数据
  3. 返回异常给用户进程
  4. 用户进程拿到error后,再次尝试读取
  5. 循环往复,直到数据就绪

阶段二:

  1. 将内核数据拷贝到用户缓冲区
  2. 拷贝过程中,用户进程依然阻塞等待
  3. 拷贝完成,用户进程解除阻塞,处理数据

可以看到,非阻塞IO模型中,用户进程在第一阶段是非阻塞,第二阶段是阻塞状态. 虽然是非阻塞,但是性能并没有得到提高,而且忙等机制会导致CPU空转,CPU使用率暴增

CPU空转是指CPU在工作时呈空闲状态,即CPU正在执行指令,但是这些指令却并没有对数据进行处理,也没有对计算机系统的其他部分进行操作,这时CPU出于等待状态,没有实际的计算操作,也没有产生实际的效果,因此被称为“空转”。

CPU空转的情况可能由以下原因引起:

程序设计问题:某些程序可能会出现死循环、死锁等情况,导致CPU空转。
资源竞争:多个程序同时争夺同一资源,如内存、硬盘等,从而导致CPU在等待资源时空转。
系统负载过高:当系统负载过高时,CPU在等待I/O操作等资源时就会空转。
CPU空转是一种资源浪费,会导致计算机性能下降,同时也会增加功耗、降低设备寿命等问题。因此,需要避免CPU空转的情况。
在这里插入图片描述
IO多路复用⛵️

利用单个线程来同时监听多个Socket,并在某个Socket可读,可写时得到通知,从而避免无效的等待,充分利用CPU资源

阶段一:

  1. 用户进程调用select,指定要监听的Socket集合
  2. 内核监听对应的多个Socket
  3. 任意一个或者多个Socket数据就绪则返回readable
  4. 此过程中用户进程阻塞

阶段二:

  1. 用户进程找到就绪的Socket
  2. 一次调用recvfrom读取数据
  3. 内核将数据拷贝到用户空间
  4. 用户进程处理数据

IO多路复用原理(大白话,通俗易懂)_.joker.的博客-CSDN博客

IO多路复用:

IO多路复用是利用单个线程来同时监听多个Socket,并在某个Socket可读,可写时得到通知,从而避免无效的等待,充分利用CPU资源.不过监听Socket的方式,通知的方式又有多种实现,常见的有:

  • select 连接数有限制(貌似是1024)
  • poll 连接数没有限制
  • epoll

差异:

select和poll只会通知用户进程有Socket就绪,但不确定具体是哪个Socket ,需要用户进程逐个遍历Socket来确认

epoll则会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间

🚩 redis的线程模型

单线程避免了上下文切换提高了线程的

redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。

文件事件处理器的结构包含 4 个部分:

多个 socket
IO 多路复用程序
文件事件分派器
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

在这里插入图片描述

🚩 BIO 、NIO、AIO有什么区别 ?

同步和异步:

同步: 发送一个请求,等待返回在发送下一个请求 同步可以避免出现死锁 和 脏读的问题

异步: 发送一个请求,不等待返回,随时可以再发送下一个请求,可以提高效率,保证并发

BIO:线程发起IO请求,不管内核是否准备好IO操作,从发起请求起,线程一直阻塞,直到操作完成。
NIO:线程发起IO请求,立即返回;内核在做好IO操作的准备之后,通过调用注册的回调函数通知线程做IO操作,线程开始阻塞,直到操作完成。
AIO:线程发起IO请求,立即返回;内存做好IO操作的准备之后,做IO操作,直到操作完成或者失败,通过调用注册的回调函数通知线程做IO操作完成或者失败。

BIO是一个连接一个线程。
NIO是一个请求一个线程。
AIO是一个有效请求一个线程。

BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
适用场景分析

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

📚 Redis面试题

面试官:什么是缓存穿透 ? 怎么解决 ?

候选人

嗯~~,我想一下

缓存穿透是指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。

解决方案的话,我们通常都会用布隆过滤器来解决它

面试官:好的,你能介绍一下布隆过滤器吗?

候选人

嗯,是这样~

布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是redisson实现的布隆过滤器。

它的底层主要是先去初始化一个比较大数组,里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。

当然是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%,其实这个误判是必然存在的,要不就得增加数组的长度,其实已经算是很划分了,5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。

面试官:什么是缓存击穿 ? 怎么解决 ?

候选人

嗯!!

缓存击穿的意思是对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。

解决方案有两种方式:

第一可以使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法

第二种方案可以设置当前key逻辑过期,大概是思路如下:

①:在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间

②:当查询的时候,从redis取出数据后判断时间是否过期

③:如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新

当然两种方案各有利弊:

如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么高,锁需要等,也有可能产生死锁的问题

如果选择key的逻辑删除,则优先考虑的高可用性,性能比较高,但是数据同步这块做不到强一致。

面试官:什么是缓存雪崩 ? 怎么解决 ?

候选人

嗯!!

缓存雪崩意思是设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。

解决方案主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

搭建集群提高系统的可用性

通过组件限流

面试官:redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)

候选人:嗯!就说我最近做的这个项目,里面有xxxx(根据自己的简历上写)的功能,需要让数据库与redis高度保持一致,因为要求时效性比较高,我们当时采用的读写锁保证的强一致性。

我们采用的是redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。

面试官:那这个排他锁是如何保证读写、读读互斥的呢?

候选人:其实排他锁底层使用也是setnx,保证了同时只能有一个线程操作锁住的方法

面试官:你听说过延时双删吗?为什么不用它呢?

候选人:延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。

面试官:redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)

候选人:嗯!就说我最近做的这个项目,里面有xxxx(根据自己的简历上写)的功能,数据同步可以有一定的延时(符合大部分业务)

我们当时采用的阿里的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。

面试官:redis做为缓存,数据的持久化是怎么做的?

候选人:在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF

面试官:这两种持久化方式有什么区别呢?

候选人:RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。

AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据

面试官:这两种方式,哪种恢复的比较快呢?

候选人:RDB因为是二进制文件,在保存的时候体积也是比较小的,它恢复的比较快,但是它有可能会丢数据,我们通常在项目中也会使用AOF来恢复数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令

面试官:Redis的数据过期策略有哪些 ?

候选人

嗯~,在redis中提供了两种数据过期删除策略

第一种是惰性删除,在设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。

第二种是 定期删除,就是说每隔一段时间,我们就对一些key进行检查,删除里面过期的key

定期清理的两种模式:

  • SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的 hz 选项来调整这个次数
  • FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms

Redis的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用。

面试官:Redis的数据淘汰策略有哪些 ?

候选人

嗯,这个在redis中提供了很多种,默认是noeviction,不删除任何数据,内部不足直接报错

是可以在redis的配置文件中进行设置的,里面有两个非常重要的概念,一个是LRU,另外一个是LFU

LRU的意思就是最少最近使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。

LFU的意思是最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高

我们在项目设置的allkeys-lru,挑选最近最少使用的数据淘汰,把一些经常访问的key留在redis中

面试官:数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ?

候选人

嗯,我想一下~~

可以使用 allkeys-lru (挑选最近最少使用的数据淘汰)淘汰策略,那留下来的都是经常访问的热点数据

面试官:Redis的内存用完了会发生什么?

候选人

嗯~,这个要看redis的数据淘汰策略是什么,如果是默认的配置,redis内存用完以后则直接报错。我们当时设置的 allkeys-lru 策略。把最近最常访问的数据留在缓存中。

面试官:Redis分布式锁如何实现 ?

候选人:嗯,在redis中提供了一个命令setnx(SET if not exists)

由于redis的单线程的,用了命令之后,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候是其他客户端是不能设置这个key的

面试官:好的,那你如何控制Redis实现分布式锁有效时长呢?

候选人:嗯,的确,redis的setnx指令不好控制这个问题,我们当时采用的redis的一个框架redisson实现的。

在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了

还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。

面试官:好的,redisson实现的分布式锁是可重入的吗?

候选人 :嗯,是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数

面试官:redisson实现的分布式锁能解决主从一致性的问题吗

候选人 :这个是不能的,比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。

我们可以利用redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,并且要求在大多数redis节点上都成功创建锁,红锁中要求是redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。

但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁

面试官:好的,如果业务非要保证数据的强一致性,这个该怎么解决呢?

候选人: 嗯~,redis本身就是支持高可用的,做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用zookeeper实现的分布式锁,它是可以保证强一致性的。

面试官:Redis集群有哪些方案, 知道嘛 ?

候选人:嗯~~,在Redis中提供的集群方案总共有三种:主从复制、哨兵模式、Redis分片集群

面试官:那你来介绍一下主从同步

候选人:嗯,是这样的,单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中

面试官:能说一下,主从同步数据的流程

候选人:嗯~~,好!主从同步分为了两个阶段,一个是全量同步,一个是增量同步

全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:

第一:从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。

第二:主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。

第三:在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保持了一致

当然,如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步

增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步

面试官:怎么保证Redis的高并发高可用

候选人:首先可以搭建主从集群,再加上使用redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端,所以一般项目都会采用哨兵的模式来保证redis的高并发高可用

面试官:你们使用redis是单点还是集群,哪种集群

候选人:嗯!,我们当时使用的是主从(1主1从)加哨兵。一般单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量的网络带宽,也没有办法使用lua脚本和事务

面试官:redis集群脑裂,该怎么解决呢?

候选人:嗯! 这个在项目很少见,不过脑裂的问题是这样的,我们现在用的是redis的哨兵模式集群的

有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致old master中的大量数据丢失。

关于解决的话,我记得在redis的配置中可以设置:第一可以设置最少的salve节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失

redis中有两个配置参数:
min-replicas-to-write 1  : 表示最少的salve节点为1个
min-replicas-max-lag 5   : 表示数据复制和同步的延迟不能超过5秒

面试官:redis的分片集群有什么作用

候选人:分片集群主要解决的是,海量数据存储的问题,集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点

面试官:Redis分片集群中数据是怎么存储和读取的?

候选人

嗯~,在redis集群中是这样的

Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围, key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,通过槽找到对应的节点进行存储。

取值的逻辑是一样的

面试官:Redis是单线程的,但是为什么还那么快?

候选人

嗯,这个有几个原因吧~~~

1、完全基于内存的,C语言编写

2、采用单线程,避免不必要的上下文切换可竞争条件

3、使用多路I/O复用模型,非阻塞IO

例如:bgsave 和 bgrewriteaof 都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞

file even handler 文件事件处理器

多个 socket
IO 多路复用程序
文件事件分派器
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
在这里插入图片描述

面试官:能解释一下I/O多路复用模型?

候选人:嗯~~,I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。

其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;

在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程

MySql相关面试题

相关技术

在这里插入图片描述

SQL优化

1.定位慢查询
方案一: 使用开源工具

调试工具: Arthas

运维工具: Prometheus, Skywalking

可以快速定位哪个接口访问慢,右边有排序

在这里插入图片描述

也可以点击上面的追踪,查看当前接口在什么时候耗时最慢, 并且也包含SQL的执行耗时

在这里插入图片描述

方案二: MySQL自带慢日志查询

默认未开启

慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志

如果要开启慢查询日志,需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:

# 开启MySQL慢日志查询开关
slow_query_log=1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=2

日志内容如下: 执行时间, 执行的语句

这里日志一般在测试的时候开启,而在正式的生产环境中是不会开启的, 因为会损耗一些性能

2.分析慢查询

一个SQL很慢如何分析

可以采用EXPLAIN 或者 DESC命令获取 MySQL 如何执行 SELECT 语句的信息

- 直接在select语句之前加上关键字 explain / desc
EXPLAIN   SELECT   字段列表   FROM   表名   WHERE  条件 ;

  • possible_key 当前sql可能会使用到的索引

  • key 当前sql实际命中的索引

  • key_len 索引占用的大小

  • Extra 额外的优化建议

  • type 这条sql的连接的类型,性能由好到差为NULL、system、const、eq_ref、ref、range、 index、all

前两个用不到.

建议中的内容:

Extra含义
Using where; Using Index查找使用了索引,需要的数据都在索引列中能找到,不需要回表查询数据
Using index condition查找使用了索引,但是需要回表查询数据
  • system:查询系统中的表
  • const:根据主键查询
  • eq_ref:主键索引查询或唯一索引查询 (这里只能返回一条数据)
  • ref:索引查询 (这里可能返回多条 比如: 根据地域查询)
  • range:范围查询
  • index:索引树扫描
  • all:全盘扫描
🐫 MySQL体系结构

在这里插入图片描述

3. 存储引擎

存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式 。存储引擎是基于表的,而不是基于库的,所以存储引擎也可被称为表类型。

三种存储引擎的对比:

特性MyISAMInnoDBMEMORY
事务安全不支持支持不支持
锁机制表锁表锁 / 行锁表锁
外键不支持支持不支持
在这里插入图片描述
1️⃣ 存储引擎的简单对比
  • MyISAM不支持事务,不支持外键,但是访问速度快,对事务完整性没有要求.

  • InnoDB存储引擎提供了具有提交,回滚和崩溃恢复能力的食物安全.但是比起MyISAM存储引擎, InnoDB写的处理效率差一些并且会占用更多的磁盘空间以保留数据和索引.

  • Memory存储引擎使用存在内存中的内容来创建表,每个Memory表实际对应一个磁盘文件.Memory类型的表访问的熟读非常的快, 因为他的数据是放在内存中, 并且默认使用Hash索引, 但是一旦MySQL服务关闭,表中的数据就会丢失掉, 表的结构还在.

在mysql中提供了很多的存储引擎,比较常见有InnoDB、MyISAM、Memory

  • InnoDB存储引擎是mysql5.5之后是默认的引擎,它支持事务、外键、表级锁和行级锁

  • MyISAM是早期的引擎,它不支持事务、只有表级锁、也没有外键,用的不多

  • Memory主要把数据存储在内存,支持表级锁,没有外键和事务,用的也不多

2️⃣ 存储引擎的选择
  1. 如果应用中不需要事务,处理基本的CRUD操作,那么MyISAM是不二之选, 速度快
  2. 如果需要支持事务, 选择InnoDB
3️⃣ 不同存储引擎底层的文件名称
在这里插入图片描述
4. 索引及底层结构

什么是索引?

索引(index)是帮助MySQL高效获取数据的数据结构(有序)。

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

B树

B-Tree,B树是一种多叉路平衡查找树,相对于二叉树,B树每个节点可以有多个分支,即多叉。

以一颗最大度数(max-degree)为5(5阶)的b-tree为例,那这个B树每个节点最多存储4个key

在这里插入图片描述

度数为 5 的B树, 一个节点最多可以有五个字节点,每个节点上最多可以有四个Key,左小右大,每个Key都有指针.

指针指向子节点的数据,指定范围内的数据.

相对于二叉树来说, B树是一个矮胖树.

B+树

B+Tree是在BTree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构

在这里插入图片描述

  • 非叶子节点只存储指针,不存储数据,只有在叶子节点才会存储数据

B树与B+树对比:

①:磁盘读写代价B+树更低;

②:查询效率B+树更加稳定;

③:B+树便于扫库和区间查询

④: B+树对于查询来说相对来说比较稳定, 每次查询的层级是一样的

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

聚簇索引(聚集索引):数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个

非聚簇索引(二级索引):数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个

分类含义特点
聚集索引(Clustered Index)将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据必须有,而且只有一个
二级索引(Secondary Index)将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键可以存在多个

聚集索引选取规则:

  • 如果存在主键,主键索引就是聚集索引。

  • 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引。

  • 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引。

聚簇索引中的叶子节点的数据是一整行的数据

二级索引存储的存储的不在是数据而是这个数据对应的主键值

回表查询

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

在这里插入图片描述

覆盖索引

查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到 。

select * from tb_user where id = 1 (覆盖索引 因为聚集索引中包含所有的数据)

select id,name from tb_user where name = ‘Arm’ (覆盖索引, 因为当前字段建立了二级索引, 包含id 和 name)

select id,name,gender from tb_user where name = ‘Arm’ (非覆盖索引, 因为需要回表查询)

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

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

在这里插入图片描述

在这里插入图片描述

创建索引的原则 🍀

1). 针对于数据量较大,且查询比较频繁的表建立索引。(比如单表数量超过十万)

2). 针对于常作为**查询条件(where)、排序(order by)、分组(group by)**操作的字段建立索引。

3). 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。

  • (1,2,3, 4,5 ) 区分度相对较高 (name: 胡歌 , 刘德华, 张艺兴, 彭昱畅, 张子枫,)
  • 1,2,2,2,3,4,5 (区分度不高) ( addr: 北京, 上海,天津, 廊坊, 武汉, 北京, 北京, 郑州)

4). 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引

5). 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。

6). 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。

7). 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。

在这里插入图片描述
索引失效 🚩

① 违反最左前缀法则

② 范围查询右边的列,不能使用索引

③ 不要在索引列上进行运算操作, 索引将失效

④ 字符串不加单引号,造成索引失效。(类型转换)

⑤ 以%开头的Like模糊查询,索引失效

联合索引中

1.违反了最左前缀法则

如果索引了多列(即,联合索引),要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列(1,2可以,但是1.3不行, 1 2 3,可以)。匹配最左前缀法则,走索引:

比如说:这里有三个字段做的联合索引, (name age address), 如果此时查询 where name , age ,此时联合索引就会失效. 在联合索引的底层结构中的,是优先按照第一个索引排序,然后按照第二个索引排序.

这里因为没有使用第一个就会导致索引失效

在这里插入图片描述

索引下推(Index Condition Pushdown): MySQL5.6新特性

索引下推只针对二级索引, 在通过主键查询到数据之后, 会继续的判断第二个值

示例如下:

五分钟搞懂MySQL索引下推 - 掘金 (juejin.cn)

在这里插入图片描述

没有使用索引下推 ICP

在这里插入图片描述

使用了索引下推, 就会减少回表次数提高查询性能

即在判断完第一个条件之后也会根据第二个值判断是否满足,不满足直接过滤

在这里插入图片描述

违反最左匹配原则的其他的案例:

在这里插入图片描述

这里有一个联合索引, 顺序是 name, status address , 判断下方索引是否失效

在这里插入图片描述

以上三个都满足

以下的案例不满足, 因为没有满足最左匹配原则,或者跳过了联合索引中的列

在这里插入图片描述

在这里插入图片描述

2. 范围查询右边的列,不能使用索引 。

在这里插入图片描述

根据前面的两个字段 name , status 查询是走索引的, 但是最后一个条件address 没有用到索引。

3. 不要在索引列上进行运算操作, 索引将失效

4. 字符串不加单引号, 造成索引失效

原因: 在数据上发生了类型的转换, 由于,在查询是,没有对字符串加单引号, MySQL的查询优化器,会自动的进行类型转换,造成索引失效。

原因是因为底层的优化器会通过函数进行类型转换, 所以也会失效

5. 以%开头的Like模糊查询,

以%开头的Like模糊查询,索引失效。如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。

SQL优化 🌈

表的设计优化, 数据类型的选择

索引优化, 索引创建原则

SQL语句优化, 避免索引失效,避免使用select *

主从复制、读写分离 , 不让数据的写入, 影响读操作

分库分表

1 表的设计优化

表的设计优化(参考阿里开发手册《嵩山版》)

①比如设置合适的数值(tinyint int bigint),要根据实际情况选择

②比如设置合适的字符串类型(char和varchar)char定长效率高,varchar可变长度,效率稍低

2 . SQL语句优化

①SELECT语句务必指明字段名称(避免直接使用select * )

②SQL语句要避免造成索引失效的写法

③尽量用union all代替union union会多一次过滤,效率低

④避免在where子句中对字段进行表达式操作

⑤Join优化 能用innerjoin 就不用left join right join,如必须使用 一定要以小表为驱动,

内连接会对两个表进行优化,优先把小表放到外边,把大表放到里边。left join 或 right join,不会重新调整顺序

// 类比
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 1000; j++) {
	// 
    }
}

3. 主从复制、读写分离

如果数据库的使用场景读的操作比较多的时候,为了避免写的操作所造成的性能影响 可以采用读写分离的架构。读写分离解决的是,数据库的写入,影响了查询的效率。

在这里插入图片描述

ACID: 实现的依赖

redo log 持久性

undo log 原子性和一致性

隔离性: 行锁, + MVCC

5. 事务相关
事务特性 ACID

事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。

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

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

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

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

隔离级别

并发事务问题:脏读、不可重复读、幻读

隔离级别:读未提交、读已提交、可重复读、串行化

问题描述
脏读一个事务读到另外一个事务还没有提交的数据。
不可重复读一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。
幻读一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了”幻影”。 (查询的时候没有这条数据,但是插入的时候已存在)
隔离级别脏读不可重复读幻读
Read uncommitted 未提交读
Read committed 读已提交×
**Repeatable Read(**默认) 可重复读××
Serializable 串行化×××

注意:事务隔离级别越高,数据越安全,但是性能越低。

undo log和redo log的区别

缓冲池(buffer pool):主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度

数据页(page):是InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。页中存储的是行数据

页(16k) < 区(64个页 1M ) < 段

首先具体命令不会直接操作磁盘,而是先将磁盘中对应的数据页添加到缓存中,随后操作这些数据页,而操作之后的数据页我们称之为脏页,随后在按照一定的频率进行数据同步,再将数据同步到磁盘中。

redo log

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性

该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中, 用于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用。

对于增删改之后的数据页,称为脏页 缓冲池中的数据默认是按照一定的频率刷新到磁盘. 但是这里存在一个问题, 就是在数据读写的时候 这里的IO是随机IO(采用同步的方式), 即通过Buffer pool 直接进行同步的数据是通过随机IO

这里因为日志文件是追加的是顺序的磁盘IO, 所以此时的效率来说就非常的高

WAL(Write - Ahead - Log) 先写日志技术

redo log, 在磁盘中有两种

在这里插入图片描述

undo log

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

可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,

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

undo log可以实现事务的一致性和原子性

redo log 和 undo log 的区别

redo log: 记录的是数据页的物理变化,服务宕机可用来同步数据

undo log :记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据

redo log 保证了事务的持久性,undo log保证了事务的原子性和一致性

MVCC

面试官:事务中的隔离性是如何保证的呢?(你解释一下MVCC)

候选人事务的隔离性是由锁和mvcc实现的。 这里锁的目的是在读的时候不能有其他去修改数据

其中mvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,它的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undo log日志,第三个是readView读视图

隐藏字段是指:在mysql中给每个表都设置了隐藏字段,有一个是trx_id(事务id),记录每一次操作的事务id,是自增的;另一个字段是roll_pointer(回滚指针),指向上一个版本的事务版本记录地址

undo log主要的作用是记录回滚日志,存储老版本数据,在内部会形成一个版本链,在多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表

readView解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务id判断该访问那个版本的数据,不同的隔离级别快照读是不一样的,最终的访问的结果不一样。如果是rc隔离级别,每一次执行快照读时生成ReadView,如果是rr隔离级别仅在事务中第一次执行快照读时生成ReadView,后续复用

【MySQL笔记】正确的理解MySQL的MVCC及实现原理_mysql mvcc 原理 详解_SnailMann的博客-CSDN博客

全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突a

MVCC的具体实现,主要依赖于数据库记录中的隐式字段、undo log日志、readView。

在这里插入图片描述

在这里插入图片描述

这里对于事务五,查询到的数据应该是什么?

第一部分: 隐藏字段

在这里插入图片描述

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

最后一个是聚簇索引在没有主键和唯一约束的情况下,会自己创建默认的聚簇索引,名字为rowid

第二部分:undo log日志

记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据,

回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。

当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。

而update、delete的时候,产生的undo log日志不仅在回滚时需要,mvcc版本访问也需要,不会立即被删除。

存在一个版本链

第三部分:readView
字段含义
trx_list当前活跃的事务ID集合 用于维护 Read View 生成时刻系统 正活跃的事务 ID 列表
min_trx_id最小活跃事务ID
max_trx_id预分配事务ID,当前最大事务ID+1(因为事务ID是自增的) 也就是 目前已出现过的事务 ID 的最大值 + 1
creator_trx_idReadView创建者的事务ID

**比较规则: 🍉 **

  • 首先比较 DB_TRX_ID < min_trx_id , 如果小于,则当前事务能看到 DB_TRX_ID 所在的记录,如果大于等于进入下一个判断

  • 接下来判断 DB_TRX_ID >= max_trx_id , 如果大于等于则代表 DB_TRX_ID 所在的记录在 Read View 生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断

  • 判断 DB_TRX_ID 是否在活跃事务之中,trx_list.contains (DB_TRX_ID),如果在,则代表我 Read View 生成时刻,你这个事务还在活跃,还没有 Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在 Read View 生成之前就已经 Commit 了,你修改的结果,我当前事务是能看见的

RC , RR 级别下的 InnoDB 快照读有什么不同?
正是 Read View 生成时机的不同,从而造成 RC , RR 级别下快照读的结果的不同

在 RR 级别下的某个事务的对某条记录的第一次快照读会创建一个快照及 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个 Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个 Read View,所以对之后的修改不可见;
即 RR 级别下,快照读生成 Read View 时,Read View 会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
而在 RC 级别下的,事务中,每次快照读都会新生成一个快照和 Read View , 这就是我们在 RC 级别下的事务中可以看到别的事务提交的更新的原因

MySQL主从同步原理
在这里插入图片描述

MySQL主从复制的核心就是二进制日志

二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句。

复制分成三步:

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

2.从库读取主库的二进制日志文件 Binlog ,写入到从库的中继日志 Relay Log

3.slave重做中继日志中的事件,将改变反映它自己的数据。

在这里插入图片描述
MySQL两阶段提交原理 🍉
在这里插入图片描述

在MySQL开启binLog日志之后需要同时完成redolog和binlog的事务写入,

分库分表

分库分表的时机:

1,前提,项目业务数据逐渐增多,或业务发展比较迅速

2,优化已解决不了性能问题(主从读写分离、查询索引…)

3,IO瓶颈(磁盘IO、网络IO)、CPU瓶颈(聚合查询、连接数太多)

拆分策略:

  • 垂直分库

  • 垂直分表

  • 水平分库

  • 水平分表

垂直分库:

以表为依据,根据业务将不同表拆分到不同库中。

特点:

1.按业务对数据分级管理、维护、监控、扩展

2.在高并发下,提高磁盘IO和数据量连接数

在这里插入图片描述

垂直分表

以字段为依据,根据字段属性将不同字段拆分到不同表中。

拆分规则:

  • 把不常用的字段单独放在一张表

  • 把text,blob等大字段拆分出来放在附表中

特点:

1,冷热数据分离

2,减少IO过渡争抢,两表互不影响

在这里插入图片描述
水平分库

将一个库的数据拆分到多个库中。

特点:

1.解决了单库大数量,高并发的性能瓶颈问题

2.提高了系统的稳定性和可用性

在这里插入图片描述

路由规则

根据id节点取模

按id也就是范围路由,节点1(1-100万 ),节点2(100万-200万)

水平分表:

将一个表的数据拆分到多个表中(可以在同一个库内)。

特点:

1.优化单一表数据量过大而产生的性能问题;

2.避免IO争抢并减少锁表的几率;

分库分表中间件

对于分库的操作我们一般的做法是通过中间件来对数据库的数据进行整合, 比如我们常用的就有 MyCat , Sharding jdbc

MyCat

MyCat 是客户端无感的做法, 就是对于客户端来说可以不知道Mysql服务的存在. 在Mycat中配置好用户的所要链接的信息, 以及所有数据库的信息,

Sharding jdbc

是一个jar包, 通过在服务端进行配置, 指定链接的信息,以及根据什么值的ID可以进行具体的 将数据插入到那一条表中,

但是如果是查询

MyCAT和 sharding jdbc 的使用 baomidou 读写分离组件(通过注解)

前者属于服务端组件, 后者属于客户端组件

前者的配置在Mycat中, 后者的配置在客户端的配置文件中

📚MySQL面试题

**面试官:**MySQL中,如何定位慢查询?

候选人:

嗯~,我们当时做压测的时候有的接口非常的慢,接口的响应时间超过了2秒以上,因为我们当时的系统部署了运维的监控系统Skywalking ,在展示的报表中可以看到是哪一个接口比较慢,并且可以分析这个接口哪部分比较慢,这里可以看到SQL的具体的执行时间,所以可以定位是哪个sql出了问题

如果,项目中没有这种运维的监控系统,其实在MySQL中也提供了慢日志查询的功能,可以在MySQL的系统配置文件中开启这个慢日志的功能,并且也可以设置SQL执行超过多少时间来记录到一个日志文件中,我记得上一个项目配置的是2秒,只要SQL执行的时间超过了2秒就会记录到日志文件中,我们就可以在日志文件找到执行比较慢的SQL了。

**面试官:**那这个SQL语句执行很慢, 如何分析呢?⛳️

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

**面试官:**了解过索引吗?(什么是索引)

**候选人:**嗯,索引在项目中还是比较常见的,它是帮助MySQL高效获取数据的数据结构主要是用来提高数据检索的效率降低数据库的IO成本,同时通过索引列对数据进行排序,降低数据排序的成本,也能降低了CPU的消耗

**面试官:**索引的底层数据结构了解过嘛 ?

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

**面试官:**B树和B+树的区别是什么呢?

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

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

**面试官:**什么是聚簇索引什么是非聚簇索引 ?

候选人:

好的~,聚簇索引主要是指数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个,一般情况下主键在作为聚簇索引的

非聚簇索引值的是数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个,一般我们自己定义的索引都是非聚簇索引

**面试官:**知道什么是回表查询嘛 ?

**候选人:**嗯,其实跟刚才介绍的聚簇索引和非聚簇索引是有关系的,回表的意思就是通过二级索引找到对应的主键值,然后再通过主键值找到聚集索引中所对应的整行数据,这个过程就是回表

备注:如果面试官直接问回表,则需要先介绍聚簇索引和非聚簇索引】

**面试官:**知道什么叫覆盖索引嘛 ?

**候选人:**嗯~,清楚的

覆盖索引是指select查询语句使用了索引,在返回的列,必须在索引中全部能够找到,如果我们使用id查询,它会直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。

如果按照二级索引查询数据的时候,返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用select *,尽量在返回的列中都包含添加索引的字段

**面试官:**MYSQL超大分页怎么处理 ?

**候选人:**嗯,超大分页一般都是在数据量比较大时,我们使用了limit分页查询,并且需要对数据进行排序,这个时候效率就很低,我们可以采用覆盖索引和子查询来解决

先分页查询数据的id字段,确定了id之后,再用子查询来过滤,只查询这个id列表中的数据就可以了

因为查询id的时候,走的覆盖索引,所以效率可以提升很多

**面试官:**索引创建原则有哪些?

**候选人:**嗯,这个情况有很多,不过都有一个大前提,就是表中的数据要超过10万以上,我们才会创建索引,并且添加索引的字段是查询比较频繁的字段,一般也是像作为查询条件,排序字段或分组的字段这些。

还有就是,我们通常创建索引的时候都是使用复合索引来创建,一条sql的返回值,尽量使用覆盖索引,如果字段的区分度不高的话,我们也会把它放在组合索引后面的字段。

如果某一个字段的内容较长,我们会考虑使用前缀索引来使用,当然并不是所有的字段都要添加索引,这个索引的数量也要控制,因为添加索引也会导致新增改的速度变慢。

**面试官:**什么情况下索引会失效 ?

**候选人:**嗯,这个情况比较多,我说一些自己的经验,以前遇到过的

比如,索引在使用的时候没有遵循最左匹配法则,第二个是,模糊查询,如果%号在前面也会导致索引失效。如果在添加索引的字段上进行了运算操作或者类型转换也都会导致索引失效。

我们之前还遇到过一个就是,如果使用了复合索引,中间使用了范围查询,右边的条件索引也会失效

所以,通常情况下,想要判断出这条sql是否有索引失效的情况,可以使用explain执行计划来分析

**面试官:**sql的优化的经验

**候选人:**嗯,这个在项目还是挺常见的,当然如果直说sql优化的话,我们会从这几方面考虑,比如

建表的时候、使用索引、sql语句的编写、主从复制,读写分离,还有一个是如果量比较大的话,可以考虑分库分表

**面试官:**创建表的时候,你们是如何优化的呢?

**候选人:**这个我们主要参考的阿里出的那个开发手册《嵩山版》,就比如,在定义字段的时候需要结合字段的内容来选择合适的类型,如果是数值的话,像tinyint、int 、bigint这些类型,要根据实际情况选择。如果是字符串类型,也是结合存储的内容来选择char和varchar或者text类型

**面试官:**那在使用索引的时候,是如何优化呢?

候选人:【参考索引创建原则 进行描述】

**面试官:**你平时对sql语句做了哪些优化呢?

**候选人:**嗯,这个也有很多,比如SELECT语句务必指明字段名称,不要直接使用select * ,还有就是要注意SQL语句避免造成索引失效的写法;如果是聚合查询,尽量用union all代替union ,union会多一次过滤,效率比较低;如果是表关联的话,尽量使用innerjoin ,不要使用用left join right join,如必须使用 一定要以小表为驱动

**面试官:**事务的特性是什么?可以详细说一下吗?

**候选人:**嗯,这个比较清楚,ACID,分别指的是:原子性、一致性、隔离性、持久性;我举个例子:

A向B转账500,转账成功,A扣除500元,B增加500元,原子操作体现在要么都成功,要么都失败

在转账的过程中,数据要一致,A扣除了500,B必须增加500

在转账的过程中,隔离性体现在A像B转账,不能受其他事务干扰

在转账的过程中,持久性体现在事务提交后,要把数据持久化(可以说是落盘操作)

面试官:并发事务带来哪些问题?

候选人

我们在项目开发中,多个事务并发进行是经常发生的,并发也是必然的,有可能导致一些问题

第一是脏读, 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。

第二是不可重复读:比如在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

第三是幻读(Phantom read):幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

面试官:怎么解决这些问题呢?MySQL的默认隔离级别是?

候选人:解决方案是对事务进行隔离

MySQL支持四种隔离级别,分别有:

第一个是,未提交读(read uncommitted)它解决不了刚才提出的所有问题,一般项目中也不用这个。第二个是读已提交(read committed)它能解决脏读的问题的,但是解决不了不可重复读和幻读。第三个是可重复读(repeatable read)它能解决脏读和不可重复读,但是解决不了幻读,这个也是mysql默认的隔离级别。第四个是串行化(serializable)它可以解决刚才提出来的所有问题,但是由于让是事务串行执行的,性能比较低。所以,我们一般使用的都是mysql默认的隔离级别:可重复读

面试官:undo log和redo log的区别

候选人:好的,其中redo log日志记录的是数据页的物理变化,服务宕机可用来同步数据,而undo log 不同,它主要记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据,比如我们删除一条数据的时候,就会在undo log日志文件中新增一条delete语句,如果发生回滚就执行逆操作;

redo log保证了事务的持久性,undo log保证了事务的原子性和一致性

面试官:事务中的隔离性是如何保证的呢?(你解释一下MVCC)

候选人:事务的隔离性是由锁和mvcc实现的。

其中mvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,它的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undo log日志,第三个是readView读视图

隐藏字段是指:在mysql中给每个表都设置了隐藏字段,有一个是trx_id(事务id),记录每一次操作的事务id,是自增的;另一个字段是roll_pointer(回滚指针),指向上一个版本的事务版本记录地址

undo log主要的作用是记录回滚日志,存储老版本数据,在内部会形成一个版本链,在多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表

readView解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务id判断该访问那个版本的数据,不同的隔离级别快照读是不一样的,最终的访问的结果不一样。如果是rc隔离级别,每一次执行快照读时生成ReadView,如果是rr隔离级别仅在事务中第一次执行快照读时生成ReadView,后续复用

面试官:MySQL主从同步原理

候选人:MySQL主从复制的核心就是二进制日志(DDL(数据定义语言)语句和 DML(数据操纵语言)语句),它的步骤是这样的:

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

第二:从库读取主库的二进制日志文件 Binlog ,写入到从库的中继日志 Relay Log 。

第三:从库重做中继日志中的事件,将改变反映它自己的数据

面试官:你们项目用过MySQL的分库分表吗?

候选人

嗯,因为我们都是微服务开发,每个微服务对应了一个数据库,是根据业务进行拆分的,这个其实就是垂直拆分。

面试官:那你之前使用过水平分库吗?

候选人

嗯,这个是使用过的,我们当时的业务是(xxx),一开始,我们也是单库,后来这个业务逐渐发展,业务量上来的很迅速,其中(xx)表已经存放了超过1000万的数据,我们做了很多优化也不好使,性能依然很慢,所以当时就使用了水平分库。

我们一开始先做了3台服务器对应了3个数据库,由于库多了,需要分片,我们当时采用的mycat来作为数据库的中间件。数据都是按照id(自增)取模的方式来存取的。

当然一开始的时候,那些旧数据,我们做了一些清洗的工作,我们也是按照id取模规则分别存储到了各个数据库中,好处就是可以让各个数据库分摊存储和读取的压力,解决了我们当时性能的问题

框架篇面试题

在这里插入图片描述
Spring框架中的单例bean是线程安全的吗?

Spring框架中的Bean是单例的嘛? 不是线程安全的

@Scope("singleton")

singleton : bean在每个Spring IOC容器中只有一个实例。

prototype:一个bean的定义可以有多个实例。

在这里插入图片描述

Spring bean并没有可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。

AOP的使用场景

AOP称为面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。

常见的AOP使用场景:

  • 记录操作日志

  • 缓存处理

  • Spring中内置的事务处理

Spring中事务的实现:

Spring支持编程式事务管理和声明式事务管理两种方式。

编程式事务控制: 需使用TransactionTemplate来进行实现,对业务代码有侵入性,项目中很少使用

声明式事务管理: 声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

通过注解的形式添加事务,底层的实现如下:

通过AOP的环绕通知, 采用声明式事务控制,进行具体业务逻辑的操作

在这里插入图片描述

SpringBoot中事务失效的原因🚩
1. 事务方法非public修饰

由于Spring的事务是基于AOP的方式结合动态代理来实现的。因此事务方法一定要是public的,这样才能便于被Spring做事务的代理和增强。

而且,在Spring内部也会有一个 org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource类,去检查事务方法的修饰符:

@Nullable
 protected TransactionAttribute computeTransactionAttribute(
  Method method, @Nullable Class<?> targetClass) {
   // Don't allow non-public methods, as configured.
   if (allowPublicMethodsOnly() && 
  !Modifier.isPublic(method.getModifiers())) {
      return null;
   }

    // ... 略

   return null;
 }
2. 非事务方法调用事务方法
@Service
public class OrderService {
    
    public void createOrder(){
        // ... 准备订单数据
        
        // 生成订单并扣减库存
        insertOrderAndReduceStock();
    }
    
    @Transactional
    public void insertOrderAndReduceStock(){
        // 生成订单
        insertOrder();
        // 扣减库存
        reduceStock();
    }
}

可以看到,insertOrderAndReduceStock方法是一个事务方法,肯定会被Spring事务管理。Spring会给OrderService类生成一个动态代理对象,对insertOrderAndReduceStock方法做增加,实现事务效果。

但是现在createOrder方法是一个非事务方法,在其中调用了insertOrderAndReduceStock方法,这个调用其实隐含了一个this.的前缀。也就是说,这里相当于是直接调用原始的OrderService中的普通方法,而非被Spring代理对象的代理方法。那事务肯定就失效了!

3. 事务方法的异常被捕获了

异常被捕获了但是没有往外抛异常,所以事务没有发现方法中出现错误,所以也就没有回滚

 @Transactional
    public void createOrder(){
        // ... 准备订单数据
        // 生成订单
        insertOrder();
        // 扣减库存
        reduceStock();
    }

    private void reduceStock() {
        try {
            // ...扣库存
        } catch (Exception e) {
            // 处理异常
        }
    }

在这段代码中,reduceStock方法内部直接捕获了Exception类型的异常,也就是说方法执行过程中即便出现了异常也不会向外抛出。

而Spring的事务管理就是要感知业务方法的异常,当捕获到异常后才会回滚事务。

现在事务被捕获,就会导致Spring无法感知事务异常,自然不会回滚,事务就失效了。

4. 事务异常类型不对
@Transactional(rollbackFor = RuntimeException.class)
public void createOrder() throws IOException {
    // ... 准备订单数据

    // 生成订单
    insertOrder();
    // 扣减库存
    reduceStock();

    throw new IOException();
}

Spring的事务管理默认感知的异常类型是RuntimeException,当事务方法内部抛出了一个IOException时,不会被Spring捕获,因此就不会触发事务回滚,事务就失效了。

因此,当我们的业务中会抛出RuntimeException以外的异常时,应该通过@Transactional注解中的rollbackFor属性来指定异常类型:

@Transactional(rollbackFor = Exception.class)
5.事务传播行为不对
    @Transactional
    public void createOrder(){
        // 生成订单
        insertOrder();
        // 扣减库存
        reduceStock();
        throw new RuntimeException("业务异常");
    }

	@Transactional  // 默认的是如果当前没有事务,自己创建事务,如果有事务则加入
    public void insertOrder() {
    
    }
	// 不管当前方法所在方法有没有都开启一个事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void reduceStock() {
        
    }

在示例代码中,事务的入口是createOrder()方法,会开启一个事务,可以成为外部事务。在createOrder()方法内部又调用了insertOrder()方法和reduceStock()方法。这两个都是事务方法。

不过,reduceStock()方法的事务传播行为是REQUIRES_NEW,这会导致在进入reduceStock()方法时会创建一个新的事务,可以成为子事务。insertOrder()则是默认,因此会与createOrder()合并事务。

因此,当createOrder方法最后抛出异常时,只会导致insertOrder方法回滚,而不会导致reduceStock方法回滚,因为reduceStock是一个独立事务。

所以,一定要慎用传播行为,注意外部事务与内部事务之间的关系。

6.没有被Spring管理

即当前类没有被SpringBoot扫描

SpringBean的生命周期 🚩
BeanDefinition

Spring容器在进行实例化时,会将xml配置的<bean>的信息封装成一个BeanDefinition对象,Spring根据BeanDefinition来创建Bean对象,里面有很多的属性用来描述Bean

<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl" lazy-init="true"/>
    
<bean id="userService" class="com.itheima.service.UserServiceImpl" scope="singleton">
        <property name="userDao" ref="userDao"></property>
</bean>

  • beanClassName:bean 的类名

  • initMethodName:初始化方法名称

  • propertyValues:bean 的属性值

  • scope:作用域

  • lazyInit:延迟初始化

BeanDefinition类中的方法

这里即使使用了注解, 底层其实也是一样的

Bean的生命周期示意图: 🏰

Bean的创建(调用构造函数) -------------------->>>>>>>> 初始化赋值 ---------------------->>>>>>>>>>>> 销毁

在这里插入图片描述

aop使用的地方一定要是在后置方法的后面 after

CGLIB 的实现是通过实现父类, 实现代理 ( 但是如果是final 就不可以 )

因为这里有名字所以可以通过反射来获取当前类的对象

构造函数:

依赖注入: 比如通过 @Autowire 和 @Value 等注解标识的类

(这里需要注意: 如果在容器中还没有这个Bean , 那么就会先去创建这个bean , 然后再回来执行下面的操作)

Aware接口: (方便对Bean进一步的扩展)

  • BeanNameAware 初始化过程中获取Bean的名称
  • BeanFactory 初始化的过程中获取Bean工厂
  • ApplicationContextAware 初始化的过程中获取IOC容器

BeanPostProcessor: Bean的后置处理器, 用来增强Bean的功能 在Bean初始化方法执行之前进行回调

  • BeanPostProcessor#before

  • BeanPostProcessor#after

    (如果一个类被增强了, 那么通常是使用Bean的后置处理器来执行的)

    • AOP

    判断是否添加了注解,或者是否被切入, 如果是那么就创建代理对象, 如果没有继续执行其他代码

初始化方法:

  • InitializingBean 如果当前的Bean实现了这个接口的话,就会执行重写方法里面的代码
  • @PostConstruct 自定义的初始化方法 , 如果使用了这个注解, 就会执行当前方法中的代码

销毁Bean:

@PreDestroy 使用了这个注解, 那么当前Bean在销毁的时候就会执行这个代码.

Bean的声明周期演示测试类 🉑
package com.itheima.lifecycle;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Component
public class User implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean {

    public User() {
        System.out.println("User的构造方法执行了.........");
    }

    private String name ;

    @Value("张三")
    public void setName(String name) {
        System.out.println("setName方法执行了.........");
    }

    // BeanNameAware 接口的方法
    @Override
    public void setBeanName(String name) {
        System.out.println("setBeanName方法执行了.........");
    }

    // BeanFactoryAware 接口的方法
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("setBeanFactory方法执行了.........");
    }

    // ApplicationContextAware 接口的方法
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("setApplicationContext方法执行了........");
    }

    
    @PostConstruct
    public void init() {
        System.out.println("init方法执行了.................");
    }

    // InitializingBean  接口的方法
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("afterPropertiesSet方法执行了........");
    }

    @PreDestroy
    public void destory() {
        System.out.println("destory方法执行了...............");
    }

}
实现Bean的后置处理器
package com.itheima.lifecycle;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.InvocationHandler;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
public class MyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (beanName.equals("user")) {
            System.out.println("postProcessBeforeInitialization方法执行了->user对象初始化方法前开始增强....");
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (beanName.equals("user")) {
            System.out.println("postProcessAfterInitialization->user对象初始化方法后开始增强....");
            //cglib代理对象
            Enhancer enhancer = new Enhancer();
            //设置需要增强的类
            enhancer.setSuperclass(bean.getClass());
            //执行回调方法,增强方法
            enhancer.setCallback(new InvocationHandler() {
                @Override
                public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                    //执行目标方法
                    return method.invoke(method,objects);
                }
            });
            //创建代理对象
            return enhancer.create();
        }
        return bean;
    }

}

控制台输出结果:

在这里插入图片描述

Spring中的循环循环引用问题:

在这里插入图片描述

Spring 解决循环依赖方法 2.6 以后就默认禁用了 🚩

三级缓存解决循环依赖问题

2.6之前默认开启, 之后需要手动开启(官方希望我们能够在代码层面解决这个问题)

spring:
	main:
    	allow-circular-references: true  # 是否允许循环依赖的   

Spring循环依赖是通过三级缓存, 对应的三级缓存如下:

在这里插入图片描述

缓存名称源码名称作用
一级缓存singletonObjects单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
二级缓存earlySingletonObjects缓存早期的bean对象(生命周期还没走完)
三级缓存singletonFactories缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

这里有一个问题? 那如果是多例模式Spring是怎么处理的?

非单例bean 不能缓存,每次使用都需要根据 bean definition新创建

一级缓存作用: 限制Bean在beanFactory中只存一份,即实现singleton scope , 解决不了循环依赖

二级缓存作用: 如果要想打破循环依赖, 就需要一个中间人的参与, 这个中间人就是二级缓存。

三级缓存的作用: 缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

代理对象二级缓存是解决不了的, 通过Bean的生命周期我们可以看到, 代理对象的创建是在初始化方法的后面,即Bean的后置处理器的后置方法中. 而此时的注入还是在实例化bean的阶段, 而此时的做法就是通过一个中间人, ObjectFactory带代替. 这个类的作用就是创建对象使用的.

这里即使创建了对象, 其实也仅仅是一个对象的引用, 因为还没有到创建的时机

当对象B注入对象A时,因为此时的对象A还没有被创建所以这里就先拿到对象A的对象工厂, 等到真正使用的时候在通过对象工厂创建, 先把当前对象的创建方式暴露出去

可以这样理解: 因为在实例化阶段还是没有办法实现创建对象(), 但是我可以告诉**(暴露出拿到我对象的方法)**你以后, 我的对象创建之后你可以通过我的对象工厂(ObjectFactory)拿到我创建之后的对象, 我预先把这种方式告诉你.等你真正需要的时候在获取

注: 这里的需要一定在对象创建之后, 因为代理对象的创建时机是在初始化赋值的阶段

构造注入循环依赖问题需要手动解决 🌾

这里其实也很好理解: 因为在实例化对象时就需要注入其他的对象, 但是因为其他的对象没有办法创建,即连一个半成品都不是, 所以缓存对于这种情况来说就没有作用了.

解决办法: 通过在注入的实例前面加上懒加载的注解, 即在用的时候再去获取.

循环依赖:循环依赖其实就是循环引用,也就是两个或两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于A

循环依赖在spring中是允许存在,spring框架依据三级缓存已经解决了大部分的循环依赖

①一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象

②二级缓存:缓存早期的bean对象(生命周期还没走完)

③三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

SpringMVC的执行流程
V1 视图阶段(老旧JSP等)

处理器映射器的作用: 根据下面的图可以这样理解, 在映射器的底层维护了一个Map集合, 键是路径, value是 是类名#方法名

处理器适配器的作用: 处理参数以及处理返回值

在这里插入图片描述

示例: 可以观看下面的方法,每个方法都有路径 , 参数, 每个参数的形式都是不同的, 那么应该怎么处理呢?

其实就是处理器适配中,根据不同的参数, 来处理, 然后交给Handler 处理, 随后返回,在通过返回值类型进行处理. 策略模式:

在通过视图解析器,将逻辑视图转换为真正的视图,

随后在视图中通过 el, jstl表达式将数据进行填充

在这里插入图片描述

①用户发送出请求到前端控制器DispatcherServlet

②DispatcherServlet收到请求调用HandlerMapping(处理器映射器)

③HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet。

④DispatcherServlet调用HandlerAdapter(处理器适配器)

⑤HandlerAdapter经过适配调用具体的处理器(Handler/Controller)

⑥Controller执行完成返回ModelAndView对象

⑦HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet

⑧DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)

⑨ViewReslover解析后返回具体View(视图)

⑩DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)

⑪DispatcherServlet响应用户

V2 前后端分离阶段

在这里插入图片描述

①用户发送出请求到前端控制器DispatcherServlet

②DispatcherServlet收到请求调用HandlerMapping(处理器映射器)

③HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet。

④DispatcherServlet调用HandlerAdapter(处理器适配器)

⑤HandlerAdapter经过适配调用具体的处理器(Handler/Controller)

⑥方法上添加了@ResponseBody

⑦通过HttpMessageConverter来返回结果转换为JSON并响应

  • fastjson 阿里的,
  • Jackson 默认的使用
spring:
  mvc:
    servlet:
      load-on-startup: 0  # 默认是-1(懒加载) 设置成2 或正整数表示启动时就会创建DispatcherServlet
SpringBoot自动配置的原理🚩

首先在SpringBoot项目中都会有一个启动类的注解 @SpringBootApplication ,这个注解是一个复合注解,其中包含:

# 元注解
@Target({ElementType.TYPE})      # 表示作用的范围
@Retention(RetentionPolicy.RUNTIME) # 表示存在的时机
@Documented   # 表示是否生成文档
@Inherited    # 表示是否可以继承

# 表名当前是一个配置类
@SpringBootConfiguration
# 开启自动配置 SpringBoot实现自动化配置的核心注解。
@EnableAutoConfiguration
# 扫描当前包以及子包 组件扫描,默认扫描当前引导类所在包及其子包。
@ComponentScan(xxxx)

@SpringBootConfiguration:该注解与 @Configuration 注解作用相同,用来声明当前也是一个配置类。

@ComponentScan:组件扫描,默认扫描当前引导类所在包及其子包。

@EnableAutoConfiguration:SpringBoot实现自动化配置的核心注解。

@EnableAutoConfiguration 注解也是一个复合注解: 其中主要的一个注解是@Import

通过@Import注解导入对应的配置选择器。

@Import({AutoConfigurationImportSelector.class})

好那下面就看他具体的实现

AutoConfigurationImportSelector 类实现了DeferredImportSelector接口 这个接口又实现了 ImportSelector接口, 而在这个接口中有一个方法

String[] selectImports(AnnotationMetadata var1);

获取所有需要的导入的类的信息

看一下在AutoConfigurationImportSelector 的具体实现:

    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }

其中调用了 getAutoConfigurationEntry方法 而在这个方法中又调用了一个重要的方法 getCandidateConfigurations

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
        // 如果没有找到
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

我们通过错误信息提示可以看到他是去找 META-INF/spring.factories 下面的文件去了. 而在这个文件中就是一些自动状态的类的路径 , 大致如下:

在这里插入图片描述

然后再根据这个类上的条件注入注解, 判断是否满足这些条件, 如果满足,则创建这个对象.从而实现了自动装配

对于条件装配不太理解的可以参考这个笔记

@Conditional四个较常用的派生注解总结_yfs1024的博客-CSDN博客

另外还有一点, 在SpringBoot2.7之后好像是在

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中

IDEA中使用org.springframework.boot.autoconfigure.AutoConfiguration.imports没有被识别_ZhangBlossom的博客-CSDN博客

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = new ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));
        ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).forEach(configurations::add);
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
}

2.7以及之后的版本错误信息提示就变成了上面的提示, 可见有两个文件.

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 + @ResponseBody
SpringBoot常见注解:
注解说明
@SpringBootConfiguration组合了- @Configuration注解,实现配置文件的功能
@EnableAutoConfiguration打开自动配置的功能,也可以关闭某个自动配置的选
@ConfigurationProperties读取配置文件中的配置
Mybatis的执行流程 🚩

理解了各个组件的关系

Sql的执行过程(参数映射、sql解析、执行和结果处理)

mybatis-config.xml

在这里插入图片描述
  • mybatis-config.xml 通过核心配置文件加载环境, 加载一些映射文件.

  • Executor 封装JDBC的一些操作 并维护了一个二级缓存

  • MappedStatement的 作用是 封装某一个方法(标签)的数据库的操作

在这里插入图片描述

①读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件

②构造会话工厂SqlSessionFactory

③会话工厂创建SqlSession对象(包含了执行SQL语句的所有方法)

④操作数据库的接口,Executor执行器,同时负责查询缓存的维护

⑤Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息

⑥输入参数映射

⑦输出结果映射

Mybatis是否支持延迟加载

什么是延迟加载?

查询用户的时候,把用户所属的订单数据也查询出来,这个是立即加载

查询用户的时候,暂时不查询订单数据,当需要订单的时候,再查询订单,这个就是延迟加载

在这里插入图片描述

默认不开启延迟加载, 如果需要开启需要手动的开启 , 开启方式有两种,

  1. 局部延迟加载

    fetchType=“lazy”

  2. 全局延迟加载

    <settings>
        <setting name="cacheEnabled" value="true"/><!--  开启二级缓存 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 开启驼峰命名自动映射 -->
       <setting name="lazyLoadingEnabled" value = "true" />   <!-- 开启全局延迟加载-->
    </settings>
    <resultMap id="userResultMap" type="user" autoMapping="true">
        <id property="id" column="id"></id>
        <result property="username" column="username"></result>
        <result property="password" column="password"></result>
        <result property="gender" column="gender"></result>
        <result property="addr" column="addr"></result>
        <collection property="orderList" ofType="order" 
                    column="id" 
                    select="com.itheima.mapper.OrderMapper.findByUid"
                    fetchType="lazy"  # 开启延迟加载
        >
            
        </collection>
    </resultMap>

延迟加载的原理

  1. 使用CGLIB创建目标对象的代理对象 比如这里的user对象

  2. 当调用目标方法user.getOrderList()时,进入拦截器invoke方法,发现user.getOrderList()是null值,执行sql查询order列表

  3. 把order查询出来,然后调用user.setOrderList(List orderList) ,接着完成user.getOrderList()方法的调用

在这里插入图片描述

延迟加载的原理:

首先会通过CGLIB为当前对象创建一个代理对象, 然后通过代理对象, 去执行目标方法, 判断是否为null,如果是那么就执行sql 查询, 将结果返回, 如果不为空,说明已经查过了, 直接返回

Mybatis一二级缓存 🚩

通过上面Mybatis的执行流程中, 我们知道这个缓存是Executor 维护的

  • 本地缓存,基于PerpetualCache,本质是一个HashMap

  • 一级缓存:作用域是session级别

  • 二级缓存:作用域是namespace和mapper的作用域,不依赖于session 二级缓存默认是关闭的

一级缓存 (session级别)

一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,

默认打开一级缓存

在这里插入图片描述

<setting name="cacheEnabled" value="true"/>  <!--  开启二级缓存 -->
二级缓存

二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQL session,默认也是采用 PerpetualCache,HashMap 存储

在这里插入图片描述

开启方式,两步:

1,全局配置文件

<settings>
    <setting name="cacheEnabled" value="true />
</settings>

2,映射文件

使用标签让当前mapper生效二级缓存

接口开启方法:使用@CacheNamespace注解
//通过注解开启二级缓存的分开关
@CacheNamespace
public interface EmpMapper {
}
二级缓存注意事项:

1,对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear

2,二级缓存需要缓存的数据实现Serializable接口

3,只有会话提交或者关闭以后,一级缓存中的数据才会转移到二级缓存中

📚框架篇面试题

面试官:Spring框架中的单例bean是线程安全的吗?

候选人

嗯!

不是线程安全的,是这样的

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

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

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

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

面试官:什么是AOP

候选人

aop是面向切面编程,在spring中用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合,一般比如可以做为公共日志保存,事务处理等

面试官:你们项目中有没有使用到AOP

候选人

我们当时在后台管理系统中,就是使用aop来记录了系统的操作日志

主要思路是这样的,使用aop中的环绕通知+切点表达式,这个表达式就是要找到要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,比如类信息、方法信息、注解、请求方式等,获取到这些参数以后,保存到数据库

面试官:Spring中的事务是如何实现的

候选人

spring实现的事务本质就是aop完成,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

面试官:Spring中事务失效的场景有哪些

候选人

嗯!这个在项目中之前遇到过,我想想啊

第一个,如果方法上异常捕获处理,自己处理了异常,没有抛出,就会导致事务失效,所以一般处理了异常以后,别忘了跑出去就行了

第二个,如果方法抛出检查异常,如果报错也会导致事务失效,最后在spring事务的注解上,就是@Transactional上配置rollbackFor属性为Exception,这样别管是什么异常,都会回滚事务

第三,我之前还遇到过一个,如果方法上不是public修饰的,也会导致事务失效

嗯,就能想起来那么多

面试官:Spring的bean的生命周期

候选人

嗯!,这个步骤还是挺多的,我之前看过一些源码,它大概流程是这样的

首先会通过一个非常重要的类,叫做BeanDefinition获取bean的定义信息,这里面就封装了bean的所有信息,比如,类的全路径,是否是延迟加载,是否是单例等等这些信息

在创建bean的时候,第一步是调用构造函数实例化bean

第二步是bean的依赖注入,比如一些set方法注入,像平时开发用的@Autowire都是这一步完成

第三步是处理Aware接口,如果某一个bean实现了Aware接口就会重写方法执行

第四步是bean的后置处理器BeanPostProcessor,这个是前置处理器

第五步是初始化方法,比如实现了接口InitializingBean或者自定义了方法init-method标签或@PostContruct

第六步是执行了bean的后置处理器BeanPostProcessor,主要是对bean进行增强,有可能在这里产生代理对象

最后一步是销毁bean

面试官:Spring中的循环引用

候选人

嗯,好的,我来解释一下

循环依赖:循环依赖其实就是循环引用,也就是两个或两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于A

循环依赖在spring中是允许存在,spring框架依据三级缓存已经解决了大部分的循环依赖

①一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象

②二级缓存:缓存早期的bean对象(生命周期还没走完)

③三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

面试官:那具体解决流程清楚吗?

候选人

第一,先实例A对象,同时会创建ObjectFactory对象存入三级缓存singletonFactories

第二,A在初始化的时候需要B对象,这个走B的创建的逻辑

第三,B实例化完成,也会创建ObjectFactory对象存入三级缓存singletonFactories

第四,B需要注入A,通过三级缓存中获取ObjectFactory来生成一个A的对象同时存入二级缓存,这个是有两种情况,一个是可能是A的普通对象,另外一个是A的代理对象,都可以让ObjectFactory来生产对应的对象,这也是三级缓存的关键

第五,B通过从通过二级缓存earlySingletonObjects 获得到A的对象后可以正常注入,B创建成功,存入一级缓存singletonObjects

第六,回到A对象初始化,因为B对象已经创建完成,则可以直接注入B,A创建成功存入一次缓存singletonObjects

第七,二级缓存中的临时对象A清除

面试官:构造方法出现了循环依赖怎么解决?

候选人

由于bean的生命周期中构造函数是第一个执行的,spring框架并不能解决构造函数的的依赖注入,可以使用@Lazy懒加载,什么时候需要对象再进行bean对象的创建

面试官:SpringMVC的执行流程知道嘛

候选人

嗯,这个知道的,它分了好多步骤

1、用户发送出请求到前端控制器DispatcherServlet,这是一个调度中心

2、DispatcherServlet收到请求调用HandlerMapping(处理器映射器)。

3、HandlerMapping找到具体的处理器(可查找xml配置或注解配置),生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet。

4、DispatcherServlet调用HandlerAdapter(处理器适配器)。

5、HandlerAdapter经过适配调用具体的处理器(Handler/Controller)。

6、Controller执行完成返回ModelAndView对象。

7、HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet。

8、DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)。

9、ViewReslover解析后返回具体View(视图)。

10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。

11、DispatcherServlet响应用户。

当然现在的开发,基本都是前后端分离的开发的,并没有视图这些,一般都是handler中使用Response直接结果返回

面试官:Springboot自动配置原理

候选人

嗯,好的,它是这样的。

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

  • @SpringBootConfiguration

  • @EnableAutoConfiguration

  • @ComponentScan

其中@EnableAutoConfiguration是实现自动化配置的核心注解。

该注解通过@Import注解导入对应的配置选择器。关键的是内部就是读取了该项目和该项目引用的Jar包的的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名。

在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。

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

面试官:Spring 的常见注解有哪些?

候选人

嗯,这个就很多了

第一类是:声明bean,有@Component、@Service、@Repository、@Controller

第二类是:依赖注入相关的,有@Autowired、@Qualifier、@Resourse

第三类是:设置作用域 @Scope

第四类是:spring配置相关的,比如@Configuration,@ComponentScan 和 @Bean

第五类是:跟aop相关做增强的注解 @Aspect,@Before,@After,@Around,@Pointcut

面试官:SpringMVC常见的注解有哪些?

候选人

嗯,这个也很多的

有@RequestMapping:用于映射请求路径;

@RequestBody:注解实现接收http请求的json数据,将json转换为java对象;

@RequestParam:指定请求参数的名称;

@PathViriable:从请求路径下中获取请求参数(/user/{id}),传递给方法的形式参数;@ResponseBody:注解实现将controller方法返回对象转化为json对象响应给客户端。@RequestHeader:获取指定的请求头数据,还有像@PostMapping、@GetMapping这些。

面试官:Springboot常见注解有哪些?

候选人

嗯~~

Spring Boot的核心注解是@SpringBootApplication , 他由几个注解组成 :

  • @SpringBootConfiguration: 组合了- @Configuration注解,实现配置文件的功能;
  • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项
  • @ComponentScan:Spring组件扫描

面试官:MyBatis执行流程

候选人

好,这个知道的,不过步骤也很多

①读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件

②构造会话工厂SqlSessionFactory,一个项目只需要一个,单例的,一般由spring进行管理

③会话工厂创建SqlSession对象,这里面就含了执行SQL语句的所有方法

④操作数据库的接口,Executor执行器,同时负责查询缓存的维护

⑤Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息

⑥输入参数映射

⑦输出结果映射

面试官:Mybatis是否支持延迟加载?

候选人

是支持的~

延迟加载的意思是:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。

Mybatis支持一对一关联对象和一对多关联集合对象的延迟加载

在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false,默认是关闭的

面试官:延迟加载的底层原理知道吗?

候选人

嗯,我想想啊

延迟加载在底层主要使用的CGLIB动态代理完成的

第一是,使用CGLIB创建目标对象的代理对象,这里的目标对象就是开启了延迟加载的mapper

第二个是当调用目标方法时,进入拦截器invoke方法,发现目标方法是null值,再执行sql查询

第三个是获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了

面试官:Mybatis的一级、二级缓存用过吗?

候选人

嗯~~,用过的~

mybatis的一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存

关于二级缓存需要单独开启

二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQL session,默认也是采用 PerpetualCache,HashMap 存储。

如果想要开启二级缓存需要在全局配置文件和映射文件中开启配置才行。

面试官:Mybatis的二级缓存什么时候会清理缓存中的数据

候选人

嗯!!

当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear。

微服务篇

在这里插入图片描述

SpringCloud

SpringCloud五大组件

在这里插入图片描述

  • Eureka : 注册中心

  • Ribbon : 负载均衡

  • Feign : 远程调用

  • Hystrix : 服务熔断

  • Zuul/Gateway : 网关

随着SpringCloudAlibba在国内兴起 , 我们项目中使用了一些阿里巴巴的组件

  • 注册中心/配置中心 Nacos

  • 负载均衡 Ribbon

  • 服务调用 Feign

  • 服务保护 sentinel

  • 服务网关 Gateway

服务注册和发现是什么意思
  • 我们当时项目采用的eureka作为注册中心,这个也是spring cloud体系中的一个核心组件

  • 服务注册:服务提供者需要把自己的信息注册到eureka,由eureka来保存这些信息,比如服务名称、ip、端口等等

  • 服务发现:消费者向eureka拉取服务列表信息,如果服务提供者有集群,则消费者会利用负载均衡算法,选择一个发起调用

  • 服务监控:服务提供者会每隔30秒向eureka发送心跳,报告健康状态,如果eureka服务90秒没接收到心跳,从eureka中剔除

在这里插入图片描述

nacos与eureka的区别?

在这里插入图片描述

  • Nacos与eureka的共同点(注册中心)

    • ①都支持服务注册和服务拉取

    • ②都支持服务提供者心跳方式做健康检测

  • Nacos与Eureka的区别(注册中心)

    • ①Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式

    • ②临时实例心跳不正常会被剔除,非临时实例则不会被剔除

    • Nacos支持服务列表变更的消息推送模式,服务列表更新更及时(当发现某个实例的状态改变时会主动的推送服务列表)

    • ④Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式

  • Nacos还支持了配置中心,eureka则只有注册中心,也是选择使用nacos的一个重要原因

  • Nacos能够实现配置文件的热更新

负载均衡的实现 Ribbon

在这里插入图片描述

下面是我之前学习时候的笔记, 可以做一个参考

Eureka注册中心及Ribbon的源码跟踪_yfs1024的博客-CSDN博客

Ribbon负载均衡策略
  • RoundRobinRule:简单轮询服务列表来选择服务器

  • WeightedResponseTimeRule:按照权重来选择服务器,响应时间越长,权重越小

  • RandomRule:随机选择一个可用的服务器

  • BestAvailableRule:忽略那些短路的服务器,并选择并发数较低的服务器

  • RetryRule:重试机制的选择逻辑

  • AvailabilityFilteringRule:可用性敏感策略,先过滤非健康的,再选择连接数较小的实例

  • ZoneAvoidanceRule:以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询

Ribbon 负载均衡策略项目中应该如何实现

对于负载均衡的规则定义可以在服务的消费者中进行定义: 定义的形式有两种

1. 代码方式:通过@Bean (全局)

在order-service(消费者)中的OrderApplication类中,定义一个新的IRule:

@Bean
public IRule randomRule(){
    return new RandomRule();
}
2. 配置文件方式(局部)

在order-service的application.yml文件中,添加新的配置也可以修改规则:

注意: 配置顶格: 因为当前配置不在谁的配置下

userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则 
服务雪崩(级联失败)

雪崩: 一个服务失败,导致整条链路的服务都失败的情形

服务降级

服务熔断是针对某个服务,服务降级是针对某个接口

服务降级是服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃

如果降级太多,则会触发熔断机制

// feign 接口
@Component
@FeignClient(value = "order-service",fallbackFactory = OrderServiceFallbackFactory.class)
public interface OrderService {

}

// 降级类
@Component
@Slf4j
public class OrderServiceFallbackFactory implements FallbackFactory<OrderService> {
    // 重写方法,为降级方法
}
服务熔断

Hystrix 熔断机制,用于监控微服务调用情况, 默认是关闭的,如果需要开启需要在引导类上添加注解:@EnableCircuitBreaker

如果检测到 10 秒内20次请求的失败率超过 50%,就触发熔断机制。之后每隔 5 秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制。如果微服务可达,则关闭熔断机制,恢复正常请求

熔断时间是5秒, 尝试放行一次请求, 如果成功则关闭断路器

限流(预防)

为什么要限流呢?

  • 并发增大(突发流量)

  • 防止用户恶意刷接口

在这里插入图片描述

一般来说可以我们可以从三个方面来解决限流.

Tomcat

设置最大连接数

在这里插入图片描述

Nginx (漏桶算法)
    1. 控制速率(突发流量), 这样到达服务的请求就是匀速的.
    1. 控制并发连接数

在这里插入图片描述

在这里插入图片描述

  • key:定义限流对象,binary_remote_addr就是一种key,基于客户端ip限流

  • Zone:定义共享存储区来存储访问信息,10m可以存储16wip地址访问信息

  • Rate:最大访问速率,rate=10r/s 表示每秒最多请求10个请求

  • lburst=20:相当于桶的大小

  • Nodelay:快速处理

网关限流(令牌桶算法)

在这里插入图片描述

yml配置文件中,微服务路由设置添加局部过滤器RequestRateLimiter

CAP定理 🌍

  • Consistency(一致性)

  • Availability(可用性)

  • Partition tolerance (分区容错性)

分布式系统无法同时满足这三个指标。分区容错对于分布式系统是必不可少的指标. 所以 就还有CP AP

Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致

在这里插入图片描述

Availability (可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝

Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。

Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务

结论:

  • 分布式系统节点之间肯定是需要网络连接的,分区(P)是必然存在的

  • 如果保证访问的高可用性(A),可以持续对外提供服务,但不能保证数据的强一致性–> AP

  • 如果保证访问的数据强一致性(C),就要放弃高可用性 --> CP

BASE理论 🌍

BASE理论是对CAP 的一种解决思路, 包含三个思想:

  • BasicallyAvailable (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。

  • Soft State(软状态): 在一定时间内,允许出现中间状态,比如临时的不一致状态。

  • Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。

解决分布式事务的思想和模型:

  • 最终一致性思想: 各个分支实物分别执行并提交, 如果有不一致的情况,在想办法恢复数据**(AP)**

  • 强一致思想: 各个分支事务执行完业务不要提交,等待彼此结果. 而后同意提交或者回滚**(CP)**

分布式事务解决方案 🚩

方案一: 采用现成分布式事务架构 Seata

Seata事务管理中有三个重要的角色:

  • TC (Transaction Coordinator) - 事务协调者: 维护全局和分支事务的状态,协调全局事务提交或回滚。

  • TM (Transaction Manager) - 事务管理器: 定义全局事务的范围、开始全局事务、提交或回滚全局事务。

  • RM (Resource Manager) - 资源管理器: 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

XA模式(CP)

(在第一阶段的过程中会锁定资源, 从而导致效率很低)

RM一阶段的工作:

  1. 注册分支事务到TC
  2. 执行分支业务sql但不提交
  3. 报告执行状态到TC

TC二阶段工作:

​ TC检测各分支事务执行状态

  • 如果都成功, 通知所有的RM提交事务
  • 如果失败, 通知所有的RM回滚事务

RM二阶段的工作:

接收TC指令, 提交或回滚事务

XA模式-强一致性

AT模式(AP)

AT模式同样是分阶段提交的事务模式, 不过弥补了XA模型中资源锁定周期过长的缺陷.

阶段一RM的工作:

  • 注册分支事务
  • 记录undo-log(数据快照)
  • 执行业务sql并提交
  • 报告事务状态

阶段二提交时RM的工作

  • 删除undo-log即可

阶段二回滚时RM的工作

  • 根据undo-log 恢复数据到更新前

在这里插入图片描述

MQ分布式事务

在这里插入图片描述

在这里插入图片描述

分布式服务端幂等性如果设计

幂等: 多次调用方法或者接口不会改变业务的状态,可以保证重复调用的结果和单次调用的结果一致

需要幂等的场景:

  • 用户重复点击(网络波动)( a,预支付ID<数据库表中设置唯一约束> b, 使用redis锁, 判断是否已经请求过了 也可以使用setnx, 锁的粒度是在用户和用户当前的订单上面

  • MQ消息重复消费

  • 应用使用失败或超时重试机制

接口幂等: (分天然幂等, 和不是天然幂等)

基于RESTful API的角度对部分常见类型请求的幂等性特点进行分析

请求方式说明
GET查询操作,天然幂等
POST新增操作,请求一次与请求多次造成的结果不同,不是幂等的
PUT更新操作,如果是以绝对值更新,则是幂等的。如果是通过增量的方式更新,则不是幂等的
DELETE删除操作,根据唯一值删除,是幂等的 (但是也会有一些特殊的情况, 具体如下)
// 这里因为不管操作多少次产生的结果都和一次调用时一样的
update t_item set money = 500 where id = 1;

// 这里每次调用都会对原始的数据进行改变所以不是天然幂等
update t_item set money = money + 500 where id = 1;

删除非幂等情况一

这里对于删除操作其实也要看具体的场景. 如果没有返回值的场景下是支持天然幂等的.

// 比如下面的代码在Service层中 , controller 层会调用下面的代码, 然后根据返回值判断是否删除成功再做对应的处理, 但是数据只有一条, 如果存在多次删除同一条数据的请求, 那么此时就不是幂等了, 因为删除成功和删除失败的处理逻辑不同
public Integer deleteById(Integer id){
	return 0/1;
}

删除非幂等情况二

带查询条件的删除就不一定满足幂等了, 例如: 再根据条件删除一批数据后, 这时候新增加了一条数据也满足条件, 然后有执行了一次删除, 那么此时就会将这条刚才新增的数据也该删除. 所以这种情况我们也是需要考虑的.

对于接口的幂等, 我们具体实现的思路可以有如下几个:

新增 ----------------> 数据库唯一索引约束 | 乐观锁

新增, 删除 ------------> token + redis | 分布式锁

token + redis

在这里插入图片描述

乐观锁实现新增幂等

如果更新已有数据,可以进行加锁更新,也可以设计表结构时使用乐观锁,通过version来做乐观锁,这样既能保证执行效率,又能保证幂等, 乐观锁的version版本在更新业务数据要自增
update table set version = version + 1 where id = #{id} and version = #{version}
示例: 当有重复请求的时候,第一个请求会获取当前商品的version版本号,得到的version为1,紧接着由于第一个请求还没更新商品的version,第二个请求获取的version依然也是1, 这时候第一个请求操作更新的时候带上version并作为条件并且自增更新,这时候商品的version就会变成2,当第二个请求去操作更新的时候明显version不一致导致更新失败。

xxl-job

解决的问题:
  • 解决集群任务的重复执行问题

  • cron表达式定义灵活

  • 定时任务失败了,重试和统计

  • 任务量大,分片执行

XXl-JOB调度策略如下:
在这里插入图片描述

1.FIRST(第一个):固定选择第一个机器;

2.LAST(最后一个):固定选择最后一个机器;

3.ROUND(轮询)

4.RANDOM(随机):随机选择在线的机器;

5.CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。

6.LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;

7.LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;

8.FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;

9.BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;

10.SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;

XXL-JOB两个版本的比较
1️⃣ XXL-JOB 2.2 旧的版本:

任务方法:

public ReturnT<String> methodName(String param){
    
    // 业务逻辑
    
     return ReturnT.SUCCESS;
}
xxl-job 获取当前机器的信息和总的实例的数量
ShardingUtil.ShardingVo   shardingVo  = ShardingUtil.getShardingVo();
int total = shardingVo.getTotal(); // 获取总的实例数
int index = shardingVo.getIndex(); // 获取当前实例的索引  从0开始
2️⃣ XXL-JOB 2.3 之后的版本:

任务方法:

public void mehtodName(){
    
    // 业务逻辑
}
获取当前机器的信息和总的实例的数量

分片实现的核心

int index = XxlJobHelper.getShardIndex();
int total = XxlJobHelper.getShardTotal();
🚩 分片广播任务

分片广播任务: 执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数开发分片任务

在这里插入图片描述

(1)数据如何分片查询 ,SQL语句如下:

SELECT * FROM `tb_vending_machine` WHERE MOD(id,3)=0 
SELECT * FROM `tb_vending_machine` WHERE MOD(id,3)=1  
SELECT * FROM `tb_vending_machine` WHERE MOD(id,3)=2  

对id进行取余,分几片就对几取余,取余结果=当前分片索引(从0开始)

(2)修改调度中心扫描售货机任务的配置,路由策略选择"分片广播"

(3)修改任务调度方法 ,使用以下代码 获取 分片总数和当前分片索引

📚微服务面试题

**面试官:**Spring Cloud 5大组件有哪些?

候选人:

早期我们一般认为的Spring Cloud五大组件是

  • Eureka : 注册中心
  • Ribbon : 负载均衡
  • Feign : 远程调用
  • Hystrix : 服务熔断
  • Zuul/Gateway : 网关

随着SpringCloudAlibba在国内兴起 , 我们项目中使用了一些阿里巴巴的组件

  • 注册中心/配置中心 Nacos

  • 负载均衡 Ribbon

  • 服务调用 Feign

  • 服务保护 sentinel

  • 服务网关 Gateway

**面试官:**服务注册和发现是什么意思?Spring Cloud 如何实现服务注册发现?

候选人:

我理解的是主要三块大功能,分别是服务注册 、服务发现、服务状态监控

我们当时项目采用的eureka作为注册中心,这个也是spring cloud体系中的一个核心组件

服务注册:服务提供者需要把自己的信息注册到eureka,由eureka来保存这些信息,比如服务名称、ip、端口等等

服务发现:消费者向eureka拉取服务列表信息,如果服务提供者有集群,则消费者会利用负载均衡算法,选择一个发起调用

服务监控:服务提供者会每隔30秒向eureka发送心跳,报告健康状态,如果eureka服务90秒没接收到心跳,从eureka中剔除

**面试官:**我看你之前也用过nacos、你能说下nacos与eureka的区别?

候选人:

我们当时xx项目就是采用的nacos作为注册中心,选择nacos还要一个重要原因就是它支持配置中心,不过nacos作为注册中心,也比eureka要方便好用一些,主要相同不同点在于几点:

  • 共同点

Nacos与eureka都支持服务注册和服务拉取,都支持服务提供者心跳方式做健康检测

  • Nacos与Eureka的区别

①Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式

②临时实例心跳不正常会被剔除,非临时实例则不会被剔除

③Nacos支持服务列表变更的消息推送模式,服务列表更新更及时

④Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式

**面试官:**你们项目负载均衡如何实现的 ?

候选人:

是这样~~

在服务调用过程中的负载均衡一般使用SpringCloud的Ribbon 组件实现 , Feign的底层已经自动集成了Ribbon , 使用起来非常简单

当发起远程调用时,ribbon先从注册中心拉取服务地址列表,然后按照一定的路由策略选择一个发起远程调用,一般的调用策略是轮询

面试官: Ribbon负载均衡策略有哪些 ?

候选人:

我想想啊,有很多种,我记得几个:

  • RoundRobinRule:简单轮询服务列表来选择服务器

  • WeightedResponseTimeRule:按照权重来选择服务器,响应时间越长,权重越小

  • RandomRule:随机选择一个可用的服务器

  • ZoneAvoidanceRule:区域敏感策略,以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询(默认)

面试官: 如果想自定义负载均衡策略如何实现 ?

候选人:

提供了两种方式:

1,创建类实现IRule接口,可以指定负载均衡策略,这个是全局的,对所有的远程调用都起作用

2,在客户端的配置文件中,可以配置某一个服务调用的负载均衡策略,只是对配置的这个服务生效远程调用

面试官: 什么是服务雪崩,怎么解决这个问题?

候选人:

服务雪崩是指一个服务失败,导致整条链路的服务都失败的情形,一般我们在项目解决的话就是两种方案,第一个是服务降级,第二个是服务熔断,如果流量太大的话,可以考虑限流

服务降级:服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃,一般在实际开发中与feign接口整合,编写降级逻辑

服务熔断:默认关闭,需要手动打开,如果检测到 10 秒内请求的失败率超过 50%,就触发熔断机制。之后每隔 5 秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制。如果微服务可达,则关闭熔断机制,恢复正常请求

**面试官:**你们的微服务是怎么监控的?

候选人:

我们项目中采用的skywalking进行监控的

1,skywalking主要可以监控接口、服务、物理实例的一些状态。特别是在压测的时候可以看到众多服务中哪些服务和接口比较慢,我们可以针对性的分析和优化。

2,我们还在skywalking设置了告警规则,特别是在项目上线以后,如果报错,我们分别设置了可以给相关负责人发短信和发邮件,第一时间知道项目的bug情况,第一时间修复

**面试官:**你们项目中有没有做过限流 ? 怎么做的 ?

候选人:

我当时做的xx项目,采用就是微服务的架构,因为xx因为,应该会有突发流量,最大QPS可以达到2000,但是服务支撑不住,我们项目都通过压测最多可以支撑1200QPS。因为我们平时的QPS也就不到100,为了解决这些突发流量,所以采用了限流。

【版本1】

我们当时采用的nginx限流操作,nginx使用的漏桶算法来实现过滤,让请求以固定的速率处理请求,可以应对突发流量,我们控制的速率是按照ip进行限流,限制的流量是每秒20

【版本2】

我们当时采用的是spring cloud gateway中支持局部过滤器RequestRateLimiter来做限流,使用的是令牌桶算法,可以根据ip或路径进行限流,可以设置每秒填充平均速率,和令牌桶总容量

**面试官:**限流常见的算法有哪些呢?

候选人:

比较常见的限流算法有漏桶算法和令牌桶算法

漏桶算法是把请求存入到桶中,以固定速率从桶中流出,可以让我们的服务做到绝对的平均,起到很好的限流效果

令牌桶算法在桶中存储的是令牌,按照一定的速率生成令牌,每个请求都要先申请令牌,申请到令牌以后才能正常请求,也可以起到很好的限流作用

它们的区别是,漏桶和令牌桶都可以处理突发流量,其中漏桶可以做到绝对的平滑,令牌桶有可能会产生突发大量请求的情况,一般nginx限流采用的漏桶,spring cloud gateway中可以支持令牌桶算法

面试官:什么是CAP理论?

候选人

CAP主要是在分布式项目下的一个理论。包含了三项,一致性、可用性、分区容错性

  • 一致性(Consistency)是指更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致(强一致性),不能存在中间状态。

  • 可用性(Availability) 是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。

  • 分区容错性(Partition tolerance) 是指分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

面试官:为什么分布式系统中无法同时保证一致性和可用性?

候选人

嗯,是这样的~~

首先一个前提,对于分布式系统而言,分区容错性是一个最基本的要求,因此基本上我们在设计分布式系统的时候只能从一致性(C)和可用性(A)之间进行取舍。

如果保证了一致性(C):对于节点N1和N2,当往N1里写数据时,N2上的操作必须被暂停,只有当N1同步数据到N2时才能对N2进行读写请求,在N2被暂停操作期间客户端提交的请求会收到失败或超时。显然,这与可用性是相悖的。

如果保证了可用性(A):那就不能暂停N2的读写操作,但同时N1在写数据的话,这就违背了一致性的要求。

面试官:什么是BASE理论?

候选人

嗯,这个也是CAP分布式系统设计理论

BASE是CAP理论中AP方案的延伸,核心思想是即使无法做到强一致性(StrongConsistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。它的思想包含三方面:

1、Basically Available(基本可用):基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用。

2、Soft state(软状态):即是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。

3、Eventually consistent(最终一致性):强调系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。其本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

**面试官:**你们采用哪种分布式事务解决方案?

候选人:

我们当时是xx项目,主要使用到的seata的at模式解决的分布式事务

seata的AT模型分为两个阶段:

1、阶段一RM的工作:① 注册分支事务 ② 记录undo-log(数据快照)③ 执行业务sql并提交 ④报告事务状态

2、阶段二提交时RM的工作:删除undo-log即可

3、阶段二回滚时RM的工作:根据undo-log恢复数据到更新前

at模式牺牲了一致性,保证了可用性,不过,它保证的是最终一致性

**面试官:**分布式服务的接口幂等性如何设计?

候选人:

嗯,我们当时有一个xx项目的下单操作,采用的token+redis实现的,流程是这样的

第一次请求,也就是用户打开了商品详情页面,我们会发起一个请求,在后台生成一个唯一token存入redis,key就是用户的id,value就是这个token,同时把这个token返回前端

第二次请求,当用户点击了下单操作会后,会携带之前的token,后台先到redis进行验证,如果存在token,可以执行业务,同时删除token;如果不存在,则直接返回,不处理业务,就保证了同一个token只处理一次业务,就保证了幂等性

**面试官:**xxl-job路由策略有哪些?

候选人:

xxl-job提供了很多的路由策略,我们平时用的较多就是:轮询、故障转移、分片广播…

**面试官:**xxl-job任务执行失败怎么解决?

候选人:

有这么几个操作

第一:路由策略选择故障转移,优先使用健康的实例来执行任务

第二,如果还有失败的,我们在创建任务时,可以设置重试次数

第三,如果还有失败的,就可以查看日志或者配置邮件告警来通知相关负责人解决

**面试官:**如果有大数据量的任务同时都需要执行,怎么解决?

候选人:

我们会让部署多个实例,共同去执行这些批量的任务,其中任务的路由策略是分片广播

在任务执行的代码中可以获取分片总数和当前分片,按照取模的方式分摊到各个实例执行就可以了

消息中间件篇

几种常见MQ的对比:

RabbitMQActiveMQRocketMQKafka
公司/社区RabbitApache阿里Apache
开发语言ErlangJavaJavaScala&Java
协议支持AMQP,XMPP,SMTP,STOMPOpenWire,STOMP,REST,XMPP,AMQP自定义协议自定义协议
可用性一般
单机吞吐量一般非常高
消息延迟微秒级毫秒级毫秒级毫秒以内
消息可靠性一般一般

追求可用性:Kafka、 RocketMQ 、RabbitMQ

追求可靠性:RabbitMQ、RocketMQ

追求吞吐能力:RocketMQ、Kafka

追求消息低延迟:RabbitMQ、Kafka

RabbitMQ知识

参考笔记: RabbitMQ进阶_yfs1024的博客-CSDN博客

RabbitMQ如何保证消息的不丢失

在这里插入图片描述

四个方面考虑:

  • 开启生产者确认机制,确保生产者的消息能到达队列

  • 开启持久化功能,确保消息未消费前在队列中不会丢失

  • 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack

  • 开启消费者失败重试机制,多次重试失败后将消息投递到异常交换机,交由人工处理

1. 生产者消息确认

RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。这种机制必须给每个消息指定一个唯一ID(这里我们使用UUID)。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。

  • publisher confirm 发送者确认
  • 消息成功投递到交换机,返回ack
  • 消息未投递到交换机,返回nack
  • publisher-return,发送者回执
    • 消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。
2.消息持久化
  • 交换机持久化
  • 队列持久化
  • 消息持久化
3. 消费者消息确认机制

RabbitMQ是阅后即焚机制,RabbitMQ确认消息被消费者消费后会立刻删除。

而RabbitMQ是通过消费者回执来确认消费者是否成功处理消息的:消费者获取消息后,应该向RabbitMQ发送ACK回执,表明自己已经处理消息。

而SpringAMQP则允许配置三种确认模式:

  • manual:手动ack,需要在业务代码结束后,调用api发送ack。

  • auto:自动ack,由spring监测 listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack

    (模式类似事务机制,出现异常时返回nack)

  • none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除

4.消息失败重试机制

我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。

修改consumer服务的application.yml文件,添加内容:

spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval: 1000 # 初识的失败等待时长为1秒
          multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 3 # 最大重试次数
          stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false

失败策略:

在之前的测试中,达到最大重试次数后,消息会被丢弃,这是由Spring内部机制决定的。

在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecovery接口来处理,它包含三种不同的实现:

RejectAndDontRequeueRecoverer: 重试耗尽后,直接reject,丢弃消息。默认就是这种方式

ImmediateRequeueMessageRecoverer: 重试耗尽后,返回nack,消息重新入队(这其实还是auto)

RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机(常用)

RabbitMQ消息重复消费问题如何解决的
1. 给每一条消息设置一个唯一的标识ID

在消费者消费的时候判断消息的ID是否已经存在

2. 幂等方案

分布式锁, 数据库锁, (悲观锁, 乐观锁)

RabbitMQ中死信交换机
  • 延迟队列:进入队列的消息会被延迟消费的队列

  • 场景:超时订单、限时优惠、定时发布

延迟队列 = 死信交换机+TTL(生存时间)

  • 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false

  • 消息是一个过期消息,超时无人消费

  • 要投递的队列消息堆积满了,最早的消息可能成为死信

绑定死信交换机

TTL

TTL,也就是Time-To-Live。如果一个队列中的消息TTL结束仍未消费,则会变为死信,ttl超时分为两种情况:

  • 消息所在的队列设置了存活时间

  • 消息本身设置了存活时间

延迟对列

设置当前队列为延迟队列

在这里插入图片描述

设置消息的存活时间

在这里插入图片描述

RabbitMQ中的消息堆积是怎么处理的?

当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是消息堆积问题

解决消息堆积有三种种思路:

  • 增加更多消费者,提高消费速度

  • 在消费者内开启线程池加快消息处理速度

  • 扩大队列容积,提高堆积上限

惰性队列

惰性队列的特征如下:

  • 接收到消息后直接存入磁盘而非内存

  • 消费者要消费消息时才会从磁盘中读取并加载到内存

  • 支持数百万条的消息存储

实现方式1(配置的方式)

在队列后面,添加@Lazy , 代表这是一个惰性队列

在这里插入图片描述

实现方式2(注解的方式)

在这里插入图片描述

解决消息堆积有三种种思路:

  • 增加更多消费者,提高消费速度

  • 在消费者内开启线程池加快消息处理速度

  • 扩大队列容积,提高堆积上限,采用惰性队列

    • 在声明队列的时候可以设置属性x-queue-mode为lazy,即为惰性队列
    • 基于磁盘存储,消息上限高
    • 性能比较稳定,但基于磁盘存储,受限于磁盘IO,时效性会降低
RabbitMQ的高可用机制有了解过嘛
  • 在生产环境下,使用集群来保证高可用性

  • 普通集群、镜像集群、仲裁队列

普通集群

普通集群,或者叫标准集群(classic cluster),具备下列特征:

  • 会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的消息。

  • 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回

  • 队列所在节点宕机,队列中的消息就会丢失 (所以一般不使用这种模式)

镜像集群

镜像集群:本质是主从模式,具备下面的特征:

  • 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份。

  • 创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。

  • 一个队列的主节点可能是另一个队列的镜像节点

  • 所有操作都是主节点完成,然后同步给镜像节点

  • 主宕机后,镜像节点会替代成新的主

仲裁队列

仲裁队列:仲裁队列是3.8版本以后才有的新功能,用来替代镜像队列,具备下列特征:

  • 与镜像队列一样,都是主从模式,支持主从数据同步

  • 使用非常简单,没有复杂的配置

  • 主从同步基于Raft协议,强一致

在这里插入图片描述

需要我们在声明队列的使用添加一个参数即可

📚消息中间件篇面试题

面试官:RabbitMQ-如何保证消息不丢失

候选人

嗯!我们当时MYSQL和Redis的数据双写一致性就是采用RabbitMQ实现同步的,这里面就要求了消息的高可用性,我们要保证消息的不丢失。主要从三个层面考虑

第一个是开启生产者确认机制,确保生产者的消息能到达队列,如果报错可以先记录到日志中,再去修复数据

第二个是开启持久化功能,确保消息未消费前在队列中不会丢失,其中的交换机、队列、和消息都要做持久化

第三个是开启消费者确认机制为auto,由spring确认消息处理成功后完成ack,当然也需要设置一定的重试次数,我们当时设置了3次,如果重试3次还没有收到消息,就将失败后的消息投递到异常交换机,交由人工处理

面试官:RabbitMQ消息的重复消费问题如何解决的

候选人

嗯,这个我们还真遇到过,是这样的,我们当时消费者是设置了自动确认机制,当服务还没来得及给MQ确认的时候,服务宕机了,导致服务重启之后,又消费了一次消息。这样就重复消费了

因为我们当时处理的支付(订单|业务唯一标识),它有一个业务的唯一标识,我们再处理消息时,先到数据库查询一下,这个数据是否存在,如果不存在,说明没有处理过,这个时候就可以正常处理这个消息了。如果已经存在这个数据了,就说明消息重复消费了,我们就不需要再消费了

面试官:那你还知道其他的解决方案吗?

候选人

嗯,我想想~

其实这个就是典型的幂等的问题,比如,redis分布式锁、数据库的锁都是可以的

面试官:RabbitMQ中死信交换机 ? (RabbitMQ延迟队列有了解过嘛)

候选人

嗯!了解过!

我们当时的xx项目有一个xx业务,需要用到延迟队列,其中就是使用RabbitMQ来实现的。

延迟队列就是用到了死信交换机和TTL(消息存活时间)实现的。

如果消息超时未消费就会变成死信,在RabbitMQ中如果消息成为死信,队列可以绑定一个死信交换机,在死信交换机上可以绑定其他队列,在我们发消息的时候可以按照需求指定TTL的时间,这样就实现了延迟队列的功能了。

我记得RabbitMQ还有一种方式可以实现延迟队列,在RabbitMQ中安装一个死信插件,这样更方便一些,我们只需要在声明交互机的时候,指定这个就是死信交换机,然后在发送消息的时候直接指定超时时间就行了,相对于死信交换机+TTL要省略了一些步骤

面试官:如果有100万消息堆积在MQ , 如何解决 ?

候选人

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

第一:提高消费者的消费能力 ,可以使用多线程消费任务

第二:增加更多消费者,提高消费速度

​ 使用工作队列模式, 设置多个消费者消费消费同一个队列中的消息

第三:扩大队列容积,提高堆积上限

可以使用RabbitMQ惰性队列,惰性队列的好处主要是

①接收到消息后直接存入磁盘而非内存

②消费者要消费消息时才会从磁盘中读取并加载到内存

③支持数百万条的消息存储

面试官:RabbitMQ的高可用机制有了解过嘛

候选人

嗯,熟悉的~

我们当时项目在生产环境下,使用的集群,当时搭建是镜像模式集群,使用了3台机器。

镜像队列结构是一主多从,所有操作都是主节点完成,然后同步给镜像节点,如果主节点宕机后,镜像节点会替代成新的主节点,不过在主从同步完成前,主节点就已经宕机,可能出现数据丢失

面试官:那出现丢数据怎么解决呢?

候选人

我们可以采用仲裁队列,与镜像队列一样,都是主从模式,支持主从数据同步,主从同步基于Raft协议,强一致。

并且使用起来也非常简单,不需要额外的配置,在声明队列的时候只要指定这个是仲裁队列即可

面试官:Kafka是如何保证消息不丢失

候选人

嗯,这个保证机制很多,在发送消息到消费者接收消息,在每个阶段都有可能会丢失消息,所以我们解决的话也是从多个方面考虑

第一个是生产者发送消息的时候,可以使用异步回调发送,如果消息发送失败,我们可以通过回调获取失败后的消息信息,可以考虑重试或记录日志,后边再做补偿都是可以的。同时在生产者这边还可以设置消息重试,有的时候是由于网络抖动的原因导致发送不成功,就可以使用重试机制来解决

第二个在broker中消息有可能会丢失,我们可以通过kafka的复制机制来确保消息不丢失,在生产者发送消息的时候,可以设置一个acks,就是确认机制。我们可以设置参数为all,这样的话,当生产者发送消息到了分区之后,不仅仅只在leader分区保存确认,在follwer分区也会保存确认,只有当所有的副本都保存确认以后才算是成功发送了消息,所以,这样设置就很大程度了保证了消息不会在broker丢失

第三个有可能是在消费者端丢失消息,kafka消费消息都是按照offset进行标记消费的,消费者默认是自动按期提交已经消费的偏移量,默认是每隔5s提交一次,如果出现重平衡的情况,可能会重复消费或丢失数据。我们一般都会禁用掉自动提价偏移量,改为手动提交,当消费成功以后再报告给broker消费的位置,这样就可以避免消息丢失和重复消费了

面试官:Kafka中消息的重复消费问题如何解决的

候选人

kafka消费消息都是按照offset进行标记消费的,消费者默认是自动按期提交已经消费的偏移量,默认是每隔5s提交一次,如果出现重平衡的情况,可能会重复消费或丢失数据。我们一般都会禁用掉自动提价偏移量,改为手动提交,当消费成功以后再报告给broker消费的位置,这样就可以避免消息丢失和重复消费了

为了消息的幂等,我们也可以设置唯一主键来进行区分,或者是加锁,数据库的锁,或者是redis分布式锁,都能解决幂等的问题

面试官:Kafka是如何保证消费的顺序性

候选人

kafka默认存储和消费消息,是不能保证顺序性的,因为一个topic数据可能存储在不同的分区中,每个分区都有一个按照顺序的存储的偏移量,如果消费者关联了多个分区不能保证顺序性

如果有这样的需求的话,我们是可以解决的,把消息都存储同一个分区下就行了,有两种方式都可以进行设置,第一个是发送消息时指定分区号,第二个是发送消息时按照相同的业务设置相同的key,因为默认情况下分区也是通过key的hashcode值来选择分区的,hash值如果一样的话,分区肯定也是一样的

面试官:Kafka的高可用机制有了解过嘛

候选人

嗯,主要是有两个层面,第一个是集群,第二个是提供了复制机制

kafka集群指的是由多个broker实例组成,即使某一台宕机,也不耽误其他broker继续对外提供服务

复制机制是可以保证kafka的高可用的,一个topic有多个分区,每个分区有多个副本,有一个leader,其余的是follower,副本存储在不同的broker中;所有的分区副本的内容是都是相同的,如果leader发生故障时,会自动将其中一个follower提升为leader,保证了系统的容错性、高可用性

面试官:解释一下复制机制中的ISR

候选人

ISR的意思是in-sync replica,就是需要同步复制保存的follower

其中分区副本有很多的follower,分为了两类,一个是ISR,与leader副本同步保存数据,另外一个普通的副本,是异步同步数据,当leader挂掉之后,会优先从ISR副本列表中选取一个作为leader,因为ISR是同步保存数据,数据更加的完整一些,所以优先选择ISR副本列表

面试官:Kafka数据清理机制了解过嘛

候选人

嗯,了解过~~

Kafka中topic的数据存储在分区上,分区如果文件过大会分段存储segment

每个分段都在磁盘上以索引(xxxx.index)和日志文件(xxxx.log)的形式存储,这样分段的好处是,第一能够减少单个文件内容的大小,查找数据方便,第二方便kafka进行日志清理。

在kafka中提供了两个日志的清理策略:

第一,根据消息的保留时间,当消息保存的时间超过了指定的时间,就会触发清理,默认是168小时( 7天)

第二是根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阈值,则开始删除最久的消息。这个默认是关闭的

这两个策略都可以通过kafka的broker中的配置文件进行设置

面试官:Kafka中实现高性能的设计有了解过嘛

候选人

Kafka 高性能,是多方面协同的结果,包括宏观架构、分布式存储、ISR 数据同步、以及高效的利用磁盘、操作系统特性等。主要体现有这么几点:

消息分区:不受单台服务器的限制,可以不受限的处理更多的数据

顺序读写:磁盘顺序读写,提升读写效率

页缓存:把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问

零拷贝:减少上下文切换及数据拷贝

消息压缩:减少磁盘IO和网络IO

分批发送:将消息打包批量发送,减少网络开销

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yfs1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值