Zer0e's Blog

2024面试复盘13

字数统计: 10.6k阅读时长: 39 min
2024/08/15 Share

前言

今天zoom面试,亏我还那么期待,感觉好像面试官水平不咋样啊,面试完后一个小时直接发拒绝邮件了,我真是醉了,我感觉大部分回答的还可以吧,除了小部分不懂的问题,不会又是刷KPI的吧。艹!

全程基本都是八股文,有数据库相关,有java相关,我印象比较深的是一个explain的filesort没答上来,然后es的一个match和match_phrase好像答反了。不至于就挂掉啊,真奇怪。难道是我跟面试官说话的态度不好吗?好像有一两次我觉得他说的不对,反驳了一下。

算了,就不应该抱有期待的,就这样吧,复盘一下。

复盘

为什么用xxl-job?@scheduler和xxl-job

高可用部署+任务失败重试+多种调度方式+动态任务配置.

@Scheduled 注解是 Spring 的一部分,集成简单,适合在 Spring 环境中进行定时任务调度。适合单个应用程序内部的定时任务管理,不支持分布式任务调度。

XXL-JOB 是一个分布式任务调度平台,提供了更加全面和强大的任务调度功能,适用于大规模分布式系统的任务调度需求。支持分布式部署,可以在多个服务器或实例上运行调度任务,适用于分布式系统和大规模应用。支持多种调度方式,包括简单任务、分片任务、定时任务等。

特性 @Scheduled XXL-JOB
适用场景 单个 Spring 应用程序内的定时任务 大规模分布式系统的任务调度
任务调度方式 基于 Cron 表达式、固定延迟、固定频率 支持简单任务、分片任务、定时任务等
分布式支持 不支持 支持
管理界面 不提供 提供图形化管理界面
集成方式 Spring 注解配置 需要集成客户端库并配置管理平台
容错性 低(单机环境下) 高(支持高可用部署和容错机制)

xxl-job如何控制单一任务只有一次执行

在任务开始执行前,任务执行器会尝试获取任务的锁。通常使用数据库中的记录或分布式锁机制来确保任务只有一个实例在执行。

任务执行器会在任务执行前更新任务状态,例如在数据库中标记任务为“正在执行”。这有助于防止任务被重复下发或执行。

数据库的表锁和行锁

表锁是指对整个表施加锁定,确保在锁定期间,其他事务无法对该表进行修改或读取操作(具体行为取决于锁的类型)。表锁会锁定整个表,导致在锁定期间,其他事务无法访问该表。这可能会导致较大的并发访问瓶颈。

行锁是指对数据库表中的特定行施加锁定。行锁可以更细粒度地控制对表中数据的访问,通常在高并发环境中使用。行锁只锁定表中的特定行,允许其他事务访问表中的其他行。这可以提高并发性能和资源利用率。

特性 表锁 行锁
粒度 锁定整个表 锁定特定行
并发 可能导致高并发访问瓶颈 提高并发性能
实现复杂度 简单易用 较复杂
性能影响 性能可能较差,尤其在高并发环境下 性能较好,适合高并发环境

行锁膨胀

当数据库中的锁定行数量非常多时,某些数据库系统可能会自动升级为表锁,以简化锁管理和避免过多的锁开销。这种情况被称为锁膨胀

操作涉及整个表时,例如执行一个需要对整个表进行锁定的查询或更新,数据库系统可能会选择表锁以保证操作的完整性。例如在事务中执行了一条没有索引条件的查询,引发全表扫描,行锁 膨胀为表锁。

当行锁锁定的数据没有主键或唯一索引时,数据库系统可能会自动将行锁膨胀为表锁。

数据库的死锁?回滚事务如何处理?

看录像之后,发现答得不好。

表级锁死锁

假设有两个事务,分别在操作两个表:表A和表B。
事务1试图先锁定表A,然后锁定表B。
而事务2试图先锁定表B,然后锁定表A。
用户A和用户B加锁的顺序如下:

  • 用户A–》表1(表锁)–》表2(表锁)
  • 用户B–》表2(表锁)–》表1(表锁)

如果这两个事务同时执行,它们将会相互等待对方释放锁,从而导致死锁。

这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。

  1. 仔细分析程序的逻辑,对于数据库的多表操作时,尽量按照相同的顺序进行处理,
  2. 尽量避免同时锁定两个资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源。

行级锁死锁

原因1

如果在事务中执行了一条没有索引条件的查询,引发全表扫描,行锁 膨胀 为表锁( 或者等价于 表级锁)

多个这样的 锁表事务 执行后,就很容易产生死锁和阻塞,最终应用系统会越来越慢,发生阻塞或 死锁。

解决方案

  1. SQL语句中不要使用太复杂的关联多表的查询;
  2. 使用explain“执行计划”对SQL语句进行分析,对于有全表扫描和全表锁定的SQL语句,建立相应的索引进行优化。

原因2:

两个事务分别想拿到对方持有的锁,互相等待,于是产生死锁

1
2
3
4
Session_1执行:select * from account where id=1 for update;
Session_2执行:select * from account where id=2 for update;
Session_1执行:select * from account where id=2 for update;
Session_2执行:select * from account where id=1 for update;

这种情况下,mysql会自动检测死锁并回滚事务,按照AI的回答,mysql会自动分析回滚等待时间最长,回滚代价小,数据影响少的事务进行回滚。

xxl任务执行失败时的处理

  • “故障转移”发生在调度阶段,在执行器集群部署时,如果某一台执行器发生故障,该策略支持自动进行Failover切换到一台正常的执行器机器并且完成调度请求流程。
  • “失败重试”发生在”调度 + 执行”两个阶段,支持通过自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;

mysql事务隔离级别?项目中使用的是什么?

  • READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化) :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

mysql为什么使用RR级别?

Mysql在5.0这个版本以前,binlog只支持STATEMENT这种格式!

而这种格式在读已提交(Read Commited)这个隔离级别下主从复制是有bug的,因此Mysql将可重复读(Repeatable Read)作为默认的隔离级别

假设主节点两个事务A, B,并且隔离级别为RC,事务A先执行了删除操作,但是未提交事务;事务B执行了插入操作,先提交了事务。最后事务A提交事务。

此时主节点上查询应该是有数据的才对,因为delete早于insert。

但是由于提交时间不一样,所以bin log记录的顺序是先insert然后delete,导致从节点上无数据。

解决方案有两种!

  1. 隔离级别设为可重复读(Repeatable Read),在该隔离级别下引入间隙锁。当Session 1执行delete语句时,会锁住间隙。那么,Ssession 2执行插入语句就会阻塞住!
  2. 将binglog的格式修改为row格式,此时是基于行的复制,自然就不会出现sql执行顺序不一样的问题!奈何这个格式在mysql5.1版本开始才引入。因此由于历史原因,mysql将默认的隔离级别设为可重复读(Repeatable Read),保证主从复制不出问题!

项目中选了哪个隔离级别?为什么?

项目中是不用读未提交(Read UnCommitted)和串行化(Serializable)两个隔离级别,原因有二

采用读未提交(Read UnCommitted),一个事务读到另一个事务未提交读数据,从逻辑上都说不过去!

采用串行化(Serializable),每个次读操作都会加锁,快照读失效,一般是使用mysql自带分布式事务功能时才使用该隔离级别!

那为什么很多互联网大厂为选读已提交(Read Commited)作为事务隔离级别?

原因1:在RR隔离级别下,存在间隙锁,导致出现死锁的几率比RC大的多!

假设表数据如下,并执行sql

1
2
3
4
5
6
7
8
9
10
+----+-------+
| id | type |
+----+-------+
| 1 | red |
| 2 | white |
| 5 | red |
| 7 | white |
+----+-------+

select * from test where id <3 for update;

在RR隔离级别下,存在间隙锁,可以锁住(2,5)这个间隙,防止其他事务插入数据!
而在RC隔离级别下,不存在间隙锁,其他事务是可以插入数据!

原因2:在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行

1
update test set type = 'blue' where type = 'white'; 

在RC隔离级别下,其先走聚簇索引,进行全部扫描

但在实际中,MySQL做了优化,发现不满足后,会调用unlock_row方法,把不满足条件的记录放锁。

然而,在RR隔离级别下,走聚簇索引,进行全部扫描,最后会将整个表锁上。

原因3:在RC隔离级别下,半一致性读(semi-consistent)特性增加了update操作的并发性

在5.1.15的时候,innodb引入了一个概念叫做“semi-consistent”,减少了更新同一行记录时的冲突,减少锁等待。
所谓半一致性读就是,一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本,由MySQL上层判断此版本是否满足update的where条件。若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)!

假设有两个事务,事务1和事务2!
事务1执行:

1
update test set type = 'blue' where type = 'red'; 

先不Commit事务!

与此同时事务2执行:

1
update test set type = 'blue' where type = 'white'; 

事务 2尝试加锁的时候,发现行上已经存在锁,InnoDB会开启semi-consistent read,返回最新的committed版本(1,red),(2,white),(5,red),(7,white)。MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)!
而在RR隔离级别下,事务2只能等待!

在RC级别下,不可重复读问题需要解决么?

不用解决,这个问题是可以接受的!毕竟你数据都已经提交了,读出来本身就没有太大问题!Oracle的默认隔离级别就是RC。

可重复读实现原理

可重复读(Repeatable Read) 是 MySQL 的事务隔离级别之一,旨在保证在一个事务中多次读取相同数据时的结果一致性。为了实现这一点,MySQL 使用了 多版本并发控制(MVCC, Multi-Version Concurrency Control) 机制。

它是通过在每个数据行上维护多个版本的数据来实现的。当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。

MVCC 的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB 通过数据行的 DB_TRX_IDRead View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改

隐藏字段

在内部,InnoDB 存储引擎为每行数据添加了三个 隐藏字段

  • DB_TRX_ID(6字节):表示最后一次插入或更新该行的事务 id。此外,delete 操作在内部被视为更新,只不过会在记录头 Record header 中的 deleted_flag 字段将其标记为已删除
  • DB_ROLL_PTR(7字节) 回滚指针,指向该行的 undo log 。如果该行未被更新,则为空
  • DB_ROW_ID(6字节):如果没有设置主键且该表没有唯一非空索引时,InnoDB 会使用该 id 来生成聚簇索引。

ReadView

Read View 主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”

主要有以下字段:

  • m_low_limit_id:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见
  • m_up_limit_id:活跃事务列表 m_ids 中最小的事务 ID,如果 m_ids 为空,则 m_up_limit_idm_low_limit_id。小于这个 ID 的数据版本均可见
  • m_idsRead View 创建时其他未提交的活跃事务 ID 列表。创建 Read View时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids 不包括当前事务自己和已提交的事务(正在内存中)
  • m_creator_trx_id:创建该 Read View 的事务 ID

undo log

undo log 主要有两个作用:

  • 当事务回滚时用于将数据恢复到修改前的样子
  • 另一个作用是 MVCC ,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 undo log 读取之前的版本数据,以此实现非锁定读

InnoDB 存储引擎中 undo log 分为两种:insert undo logupdate undo log

  1. insert undo log:指在 insert 操作中产生的 undo log。因为 insert 操作的记录只对事务本身可见,对其他事务不可见,故该 undo log 可以在事务提交后直接删除。不需要进行 purge 操作
  2. update undo logupdatedelete 操作中产生的 undo log。该 undo log可能需要提供 MVCC 机制,因此不能在事务提交时就进行删除。提交时放入 undo log 链表,等待 purge线程 进行最后的删除.

数据可见性算法

InnoDB 存储引擎中,创建一个新事务后,执行每个 select 语句前,都会创建一个快照(Read View),快照中保存了当前数据库系统中正处于活跃(没有 commit)的事务的 ID 号。其实简单的说保存的是系统中当前不应该被本事务看到的其他事务 ID 列表(即 m_ids)。当用户在这个事务中要读取某个记录行的时候,InnoDB 会将该记录行的 DB_TRX_IDRead View 中的一些变量及当前事务 ID 进行比较,判断是否满足可见性条件。

RC 和 RR 隔离级别下 MVCC 的差异

在事务隔离级别 RCRR (InnoDB 存储引擎的默认事务隔离级别)下,InnoDB 存储引擎使用 MVCC(非锁定一致性读),但它们生成 Read View 的时机却不同

  • 在 RC 隔离级别下的 每次select 查询前都生成一个Read View (m_ids 列表)
  • 在 RR 隔离级别下只在事务开始后 第一次select 数据前生成一个Read View(m_ids 列表)

声明式事务标记在private方法上

无法生效,因为声明式事务是基于AOP实现的。Spring 生成的代理对象仅能拦截 public 方法,因为代理对象是 public 方法的拦截器,而无法直接拦截 private 方法。因此,private 方法上的 @Transactional 注解不会被 Spring 事务管理器识别和应用。

explain分析有哪些指标

id

  • 描述:查询的唯一标识符,通常用于标识查询中的不同 SELECT 或 UNION 子句的执行顺序。
  • 意义:在复杂查询中,id 指标帮助你理解查询的执行顺序和逻辑结构。

select_type

  • SIMPLE:简单的查询,不包括子查询或 UNION。
  • PRIMARY:最外层的 SELECT 查询。
  • UNION:UNION 中的 SELECT 查询。
  • SUBQUERY:子查询中的 SELECT 查询。
  • DERIVED:派生表(子查询中的 SELECT)中的查询。

table

显示查询涉及的表。

type

  • system:表只有一行,最优。
  • const:用于匹配常量,效率很高。
  • eq_ref:通过唯一索引进行匹配,效率高。
  • ref:非唯一索引匹配,效率较高。
  • range:范围查询(使用索引进行范围查找)。
  • index:全索引扫描,效率低于使用索引的其他方式。
  • ALL:全表扫描,最差,性能较低。

possible_keys

列出查询中可能用到的索引。

key

实际使用的索引。了解查询中实际使用了哪个索引。与 possible_keys 对比,确定是否选择了最优的索引。

key_len

表示实际使用的索引的长度(字节数)。通过分析 key_len,可以了解使用的索引的有效性。较长的 key_len 可能意味着索引覆盖了更多的列。

ref

显示哪些列或常量用于索引查找。

rows

表示 MySQL 估算需要读取的行数。估算的行数可以帮助你了解查询的复杂性。较高的行数可能意味着查询会扫描大量数据,从而影响性能。

filtered

表示在读取的行中,多少百分比的行被过滤掉(即匹配条件的行)。

extra

  • Using index:查询只使用了索引,没有访问表数据,通常意味着索引覆盖了查询。
  • Using where:查询使用了 WHERE 子句来过滤结果。
  • Using temporary:查询使用了临时表,通常意味着查询需要复杂的处理,可能会影响性能。
  • Using filesort:查询需要额外的排序操作,通常意味着性能较差。

extra的file sort是什么情况?如何优化

Using filesort 出现时,意味着 MySQL 必须对结果集进行排序,但不能使用索引中的数据直接进行排序。因此,MySQL 使用临时文件来完成排序过程。

Using filesort 发生在以下情况:

  1. 无法使用索引排序:如果查询中需要对结果集进行排序,但所用的索引不能满足排序需求,MySQL 会使用额外的排序步骤。即使使用了索引,排序顺序可能与查询中的排序要求不匹配。
  2. 多个排序条件:当查询中有多个排序条件时,索引可能无法完全满足所有排序要求,从而需要额外的排序操作。
  3. ORDER BY 不匹配索引顺序:即使有索引,如果 ORDER BY 子句中的排序顺序与索引的顺序不匹配,也会触发文件排序。

如何优化?

使用合适的索引:确保你的 ORDER BY 子句中的字段有适当的索引。如果排序字段与 WHERE 子句中的字段有共同的索引,那么 MySQL 可能会使用这个索引进行排序。

优化查询: 如果排序不是必需的,可以考虑删除 ORDER BY 子句或优化排序条件,以减少对排序操作的需求。尽量简化排序字段。例如,如果只需对一个字段进行排序,确保只在该字段上创建索引,而不是包括多个字段。

使用索引覆盖查询: 如果查询只涉及到索引中的字段,MySQL 可以使用覆盖索引来避免回表操作。在这种情况下,排序可以直接通过索引完成,无需额外的文件排序。

1
2
3
SELECT column1, column2 FROM table
WHERE column1 = 'value'
ORDER BY column2;

这里的 column1 和 column2 都应该在索引中,以便可以使用索引直接排序。

调整配置参数: **调整 sort_buffer_size**:增加 sort_buffer_size 配置参数的值可以提高排序操作的效率,减少文件排序的频率。这是 MySQL 在排序操作中使用的内存缓冲区的大小。

事务提交后,如何执行一些后置处理?如何知道事务已经提交?

不查不知道,一查吓一跳,原来有这么多方法。

使用 TransactionSynchronizationManagerTransactionSynchronization

Spring 提供了 TransactionSynchronizationManagerTransactionSynchronization 机制,可以在事务提交后执行自定义逻辑。这是一种非常灵活的方法,可以在事务提交后注册回调方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public class MyTransactionSynchronization implements TransactionSynchronization {
@Override
public void beforeCommit(boolean readOnly) {
// Do nothing
}

@Override
public void afterCommit() {
// 执行事务提交后的处理逻辑
System.out.println("事务已经提交");
}

@Override
public void afterCompletion(int status) {
// 事务完成后的处理
if (status == STATUS_COMMITTED) {
System.out.println("事务提交完成");
} else if (status == STATUS_ROLLED_BACK) {
System.out.println("事务回滚");
}
}

@Override
public void beforeCompletion() {
// Do nothing
}

@Override
public void suspend() {
// Do nothing
}

@Override
public void resume() {
// Do nothing
}
}

在需要执行后置处理的地方注册 TransactionSynchronization

1
2
3
4
5
6
7
8
9
10
import org.springframework.transaction.support.TransactionSynchronizationManager;

public class MyService {
public void myTransactionalMethod() {
// 业务逻辑

// 注册 TransactionSynchronization
TransactionSynchronizationManager.registerSynchronization(new MyTransactionSynchronization());
}
}

使用 @Transactional 注解的 afterCommitafterRollback 属性

还真没用过。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.transaction.annotation.Transactional;

public class MyTransactionalService {

@Transactional
public void myTransactionalMethod() {
// 业务逻辑
}

@Transactional(afterCommit = "postCommit")
public void myTransactionalMethodWithCallback() {
// 业务逻辑
}

public void postCommit() {
// 事务提交后的处理逻辑
System.out.println("事务提交后处理");
}
}

使用 ApplicationListener 监听事务事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.springframework.context.ApplicationListener;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public class MyTransactionEventListener {

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAfterCommit() {
// 事务提交后的处理逻辑
System.out.println("事务已经提交,执行后置处理");
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleAfterRollback() {
// 事务回滚后的处理逻辑
System.out.println("事务回滚");
}
}

注册监听器

1
2
3
4
@Bean
public MyTransactionEventListener myTransactionEventListener() {
return new MyTransactionEventListener();
}

使用 TransactionCallback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

public class MyService {

private final TransactionTemplate transactionTemplate;

public MyService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}

public void myTransactionalMethod() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
// 业务逻辑

// 事务提交后的处理
System.out.println("事务已经提交,执行后置处理");
}
});
}
}

深度分页查询优化

1
2
# MySQL 在无法利用索引的情况下跳过1000000条记录后,再获取10条记录
SELECT * FROM t_order ORDER BY id LIMIT 1000000, 10

基于游标的分页

1
SELECT * FROM table WHERE id > ? ORDER BY id LIMIT 10;

在第一次查询中,? 是初始的游标值(例如,0),在后续查询中,它是上一次查询的最后一条记录的 id

这种优化方式限制比较大,且一般项目的 ID 也没办法保证完全连续。

子查询

阿里巴巴《Java 开发手册》中也有对应的描述:

利用延迟关联或者子查询优化超多分页场景。

1
2
# 通过子查询来获取 id 的起始值,把 limit 1000000 的条件转移到子查询
SELECT * FROM t_order WHERE id >= (SELECT id FROM t_order limit 1000000, 1) LIMIT 10;

不过,子查询的结果会产生一张新表,会影响性能,应该尽量避免大量使用子查询。并且,这种方法只适用于 ID 是正序的。在复杂分页场景,往往需要通过过滤条件,筛选到符合条件的 ID,此时的 ID 是离散且不连续的。

延迟关联

延迟关联的优化思路,跟子查询的优化思路其实是一样的:都是把条件转移到主键索引树,减少回表的次数。不同点是,延迟关联使用了 INNER JOIN(内连接) 包含子查询。

1
2
3
SELECT t1.* FROM t_order t1
INNER JOIN (SELECT id FROM t_order limit 1000000, 10) t2
ON t1.id = t2.id;

除了使用 INNER JOIN 之外,还可以使用逗号连接子查询

1
2
3
SELECT t1.* FROM t_order t1,
(SELECT id FROM t_order limit 1000000, 10) t2
WHERE t1.id = t2.id;

覆盖索引

索引中已经包含了所有需要获取的字段的查询方式称为覆盖索引。

避免 InnoDB 表进行索引的二次查询,也就是回表操作: InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。

可以把随机 IO 变成顺序 IO 加快查询效率: 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。

1
2
3
4
# 如果只需要查询 id, code, type 这三列,可建立 code 和 type 的覆盖索引
SELECT id, code, type FROM t_order
ORDER BY code
LIMIT 1000000, 10;

不过,当查询的结果集占表的总行数的很大一部分时,可能就不会走索引了,自动转换为全表扫描。当然了,也可以通过 FORCE INDEX 来强制查询优化器走索引,但这种提升效果一般不明显。

什么是倒排索引

倒排索引 也被称作反向索引(inverted index),是用于提高数据检索速度的一种数据结构,空间消耗比较大。倒排索引首先将检索文档进行分词得到多个词语/词条,然后将词语和文档 ID 建立关联,从而提高检索效率。

倒排索引使用 词语/词条(Term) 来作为索引关键字,并同时记录了哪些 文档(Document) 中有这个词语。

match query和term query

说实话没用过。。。

match 查询用于全文搜索,通常用于检索文档中包含某个单词或短语的文档。match 查询会经过分词(Tokenization)过程,将查询词分解成多个词项,然后进行搜索。这意味着 match 查询适合处理用户输入的自然语言文本。

term 查询用于精确匹配,通常用于检索字段中确切匹配的值。这意味着 term 查询不会经过分析或分词过程,适合用于关键词、标识符或固定的值。

使用redis分布式锁如何解决主从同步问题

确保写操作在主节点:所有的分布式锁操作(包括获取和释放锁)都应该在主节点上执行。这样可以保证在任何时候,锁的状态都是最新的。

避免在从节点上执行锁操作:在设计系统时,确保所有的锁操作请求都不会被重定向到从节点。这可以通过客户端库来实现,或者在应用层面进行控制。

使用 RedLock 算法:RedLock 是一种设计用于 Redis 的分布式锁获取算法,它通过在多个 Redis 节点上尝试获取锁来提高锁的安全性。如果 Redis 集群使用了主从复制,RedLock 算法可以确保锁的安全性。

监控主从复制延迟:监控主从复制的延迟情况,如果延迟过高,可能需要考虑优化复制性能或重新评估锁的超时时间。

redission中也进行了一些处理

  1. 多节点锁获取:Redisson 通过在所有的 Redis 服务器上尝试获取锁来实现这一点。即使在主从复制的环境中,这样也能确保锁的安全性
  2. 自动检测和配置主从节点:Redisson 提供了自动检测和配置主从节点的功能,这有助于设置主从复制并保持数据一致性

redis分布式锁保证安全性?不会被其他线程操作?

  • 使用 SET 命令的 NX 选项:这确保了只有当 key 不存在时,才能设置成功,从而避免了多个客户端同时获取锁。
  • 设置合理的超时时间:使用 EXPX 选项设置超时时间,确保即使客户端出现问题,锁也能在一定时间后自动释放。

分布式锁的值可以设置成UUID或者服务标识+线程标识,只要确保唯一。释放锁时,需要先判断现值与获取锁时的标识是否一致。

1
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

mybatis缓存机制

MyBatis 提供了两种缓存机制:一级缓存(本地缓存)和 二级缓存(全局缓存)。

一级缓存是 MyBatis 的本地缓存,每次 SQL 查询会在当前 SqlSession 中缓存查询结果。一级缓存的生命周期与 SqlSession 绑定,即 SqlSession 在关闭时缓存会被清空。

作用范围:每个 SqlSession 具有自己的一级缓存,缓存的数据只能在当前 SqlSession 内部访问。

自动生效:MyBatis 默认启用一级缓存,不需要额外配置。

缓存清空:在以下情况下一级缓存会被清空:

  • SqlSessioncommit()rollback() 操作。
  • 执行了 clearCache() 方法。
  • 执行了增、删、改操作(因为这些操作可能影响到缓存的有效性)。

二级缓存是 MyBatis 的全局缓存,缓存的生命周期与 SqlSessionFactory 绑定,跨多个 SqlSession 实例共享。适用于多个 SqlSession 使用相同的数据源的情况。

作用范围:多个 SqlSession 可以共享二级缓存,缓存的数据可以被不同的 SqlSession 访问。

需要配置:二级缓存需要在 MyBatis 配置文件中启用,并为每个 Mapper 映射器配置缓存。

缓存清空:当执行增、删、改操作时,相关的缓存会被清空。

脏数据问题:二级缓存可能导致脏数据问题,因为多个 SqlSession 可能共享相同的缓存数据。如果一个 SqlSession 更新了数据但没有及时清空或更新缓存,其他 SqlSession 可能会读取到旧的数据。

缓存失效条件

  • 一级缓存:在 SqlSession 执行插入、更新或删除操作后,默认会清空一级缓存。
  • 二级缓存:在执行插入、更新或删除操作后,如果配置了相应的映射语句的 flushCache 属性为 true,则会清空涉及的命名空间的二级缓存。

mybatis什么时候读取会产生脏数据

多会话并发操作:在多线程或多会话环境中,如果一个会话(SqlSession)读取了数据并将其缓存,而另一个会话对相同的数据进行了修改并提交了事务,那么第一个会话的缓存就会变成脏数据。

缓存未设置为只读:MyBatis的二级缓存默认是只读的(readOnly属性为true),这意味着缓存的数据不会被修改。如果错误地将缓存设置为可写(readOnly属性为false),那么缓存的数据可能会被修改,导致脏数据。

事务管理不当: 事务的隔离级别可能不够高,导致其他事务能看到未提交的数据。MyBatis 的一级缓存和二级缓存都受事务管理的影响,事务的隔离级别可能导致缓存中的数据与数据库中的实际数据不一致。

缓存键不准确:如果缓存键的生成逻辑不正确,可能会导致不同的数据请求被错误地映射到同一个缓存键,从而覆盖彼此的数据,产生脏数据。

从AI回答中挑了几个可能原因。

sql监控插件如何实现

使用 MyBatis 自定义拦截器

MyBatis 提供了拦截器机制,可以拦截执行 SQL 的关键点,例如 ExecutorStatementHandlerResultSetHandler 等。通过自定义拦截器,可以在 SQL 执行前后记录时间,并计算执行耗时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;

import java.util.Properties;

@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class SqlTimeInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
try {
return invocation.proceed(); // 执行SQL
} finally {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;

MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
String sqlId = mappedStatement.getId();

System.out.println("SQL [" + sqlId + "] 执行耗时: " + executionTime + " ms");
}
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
// 可配置属性
}
}

然后注册这个拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisConfig {

@Autowired
private MybatisProperties mybatisProperties;

@Bean
public SqlSessionFactory sqlSessionFactory(javax.sql.DataSource dataSource) throws Exception {
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setDataSource(dataSource);

// 注册自定义拦截器
SqlTimeInterceptor sqlTimeInterceptor = new SqlTimeInterceptor();
configuration.addInterceptor(sqlTimeInterceptor);

org.mybatis.spring.SqlSessionFactoryBean sessionFactoryBean = new org.mybatis.spring.SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
sessionFactoryBean.setConfiguration(configuration);

return sessionFactoryBean.getObject();
}
}

使用 MyBatis-Plus 内置功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

@Bean
public PerformanceInterceptor performanceInterceptor() {
PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
performanceInterceptor.setMaxTime(1000); // 设置SQL最大执行时间,超过将抛出异常
performanceInterceptor.setFormat(true); // 格式化SQL输出
return performanceInterceptor;
}
}

mq如何保证顺序性

Kafka通过分区(partition)来保证消息的顺序性。在Kafka中,消息在每个分区内是按顺序存储的,消费者在读取消息时也是按照这个顺序读取的。只要消息被写入同一个分区,Kafka就能保证这些消息在消费时是按照生产的顺序被处理的。

如果希望保证某些消息的顺序性(例如,某个用户的所有操作日志必须按顺序处理),可以使用消息键(key)来控制消息的分区选择。Kafka根据消息键对分区数取模(hashing),将具有相同键的消息放入同一个分区,从而保证了这些消息在分区内的顺序。

消费者组中的每个消费者消费一个或多个分区,从而保证了这些消息在单个消费者内的顺序。

在RabbitMQ中,消息在队列中是按顺序存储的,消费者从队列中消费消息时,消息的消费顺序与生产顺序一致。因此,使用一个队列并确保一个消费者处理队列中的消息时,可以保证消息的顺序性。

如果多个消费者同时从同一个队列中消费,RabbitMQ会以轮询方式分配消息,这可能导致顺序被打乱。因此,如果需要严格的顺序,应该使用单个消费者。

通过设计不同的队列来处理顺序性。例如,如果有多种类型的消息需要保持顺序,可以为每种类型创建单独的队列,并保证每个队列由一个消费者处理。

队列能有多少消费者

这个问题我面试之后挺气的。面试官说我topic和队列搞混了,他应该认为分区是队列的概念。查了一下,其实当成队列也没毛病,是我孤陋寡闻了。

可以简单理解成kafka分区就是队列。

kafka

单个消费者组内的消费者数量:受限于topic的分区数量,最多可以有与分区数相同的消费者并行消费。如果消费者数量超过分区数,多余的消费者将处于空闲状态。

多个消费者组:Kafka允许同一个topic有多个消费者组,每个消费者组的消费者数与分区数的关系相同。多个消费者组可以独立并行消费同一个topic,从而允许无限制数量的消费者整体上并行消费。

rabbitmq

理论上,RabbitMQ能支持的消费者数量没有固定的上限。但实际可支持的消费者数量取决于服务器的资源(CPU、内存、网络带宽)、RabbitMQ的配置以及消息处理的速度。

线程池如何设置核心线程数和最大线程数

核心线程数:

  1. CPU密集型任务:核心线程数 = CPU核心数 + 1
  2. I/O密集型任务:核心线程数 = CPU核心数 * 2核心线程数 = CPU核心数 * (1 + 任务等待时间/任务执行时间)

最大线程数:

最大线程数是线程池中允许存在的最大线程数。当任务数超过核心线程数并且队列已满时,线程池会创建新线程,直到达到最大线程数。如果线程数达到了最大值,新的任务将被拒绝。最大线程数应该足够大,以应对高峰负载时的需求,但也不宜过大,以免系统资源耗尽导致性能下降。

对于大多数I/O密集型应用,最大线程数通常设置为核心线程数的2倍或3倍。

对于CPU密集型应用,最大线程数可以设置为核心线程数或略大一些。

在一个线程池中,核心线程数为3,最大线程数为10,此时有五个任务提交,会有多少个线程产生

这题原来我答错了。要复习一下线程池的处理逻辑。

  1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
  2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
  3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
  4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用RejectedExecutionHandler.rejectedExecution()方法。

gpt给我的解答是:

情景1:使用无界队列(如LinkedBlockingQueue

  • 核心线程数(3):首先,线程池会尝试使用核心线程来处理任务。如果核心线程数未达到上限,那么新任务会直接创建新线程来执行,直到核心线程数达到3。
  • 最大线程数(10):如果所有核心线程都在工作,而队列没有容量限制,那么新任务会被放入队列中,而不会创建新的线程。
  • 五个任务提交的处理方式
    1. 线程池会创建3个核心线程来处理前3个任务。
    2. 剩余的两个任务会被放入队列,等待空闲线程来处理。
  • 最终结果:线程池将会有3个活跃线程在处理任务。

情景2:使用直接提交策略(如SynchronousQueue

  • 直接提交策略:使用SynchronousQueue时,每个任务必须被立即处理或执行,如果没有空闲线程来处理新任务,则会创建新的线程,直到达到最大线程数。
  • 五个任务提交的处理方式
    1. 线程池会创建3个核心线程来处理前3个任务。
    2. 剩余的两个任务由于没有空闲线程可用,线程池会创建2个非核心线程(达到最大线程数5)来处理这两个任务。
  • 最终结果:线程池将会有5个活跃线程在处理任务。

我对比了一下,gpt说的没有错。SynchronousQueue是没有容量的。所以会直接创建线程去处理。

后话

复盘下来,好像确实有比较多的问题答得不好,比如mybatis相关,还有最后一个线程池相关我也记错了。

在此向两位面试官道歉,虽然面试过程中并没有特别不愉快,但是一个小时后就发拒绝邮件使我变得暴躁,把原因都归结于你们的不专业上,但是复盘之后发现是自己水平不够,很多问题即使有文档帮助依旧回答的不够好。在此郑重道歉。

CATALOG
  1. 1. 前言
  2. 2. 复盘
    1. 2.1. 为什么用xxl-job?@scheduler和xxl-job
    2. 2.2. xxl-job如何控制单一任务只有一次执行
    3. 2.3. 数据库的表锁和行锁
      1. 2.3.0.1. 行锁膨胀
  3. 2.4. 数据库的死锁?回滚事务如何处理?
    1. 2.4.1. 表级锁死锁
    2. 2.4.2. 行级锁死锁
  4. 2.5. xxl任务执行失败时的处理
  5. 2.6. mysql事务隔离级别?项目中使用的是什么?
    1. 2.6.1. mysql为什么使用RR级别?
    2. 2.6.2. 项目中选了哪个隔离级别?为什么?
    3. 2.6.3. 在RC级别下,不可重复读问题需要解决么?
  6. 2.7. 可重复读实现原理
    1. 2.7.1. 隐藏字段
    2. 2.7.2. ReadView
    3. 2.7.3. undo log
    4. 2.7.4. 数据可见性算法
    5. 2.7.5. RC 和 RR 隔离级别下 MVCC 的差异
  7. 2.8. 声明式事务标记在private方法上
  8. 2.9. explain分析有哪些指标
  9. 2.10. extra的file sort是什么情况?如何优化
  10. 2.11. 事务提交后,如何执行一些后置处理?如何知道事务已经提交?
    1. 2.11.1. 使用 TransactionSynchronizationManager 和 TransactionSynchronization
    2. 2.11.2. 使用 @Transactional 注解的 afterCommit 和 afterRollback 属性
    3. 2.11.3. 使用 ApplicationListener 监听事务事件
    4. 2.11.4. 使用 TransactionCallback
  11. 2.12. 深度分页查询优化
    1. 2.12.1. 基于游标的分页
    2. 2.12.2. 子查询
    3. 2.12.3. 延迟关联
    4. 2.12.4. 覆盖索引
  12. 2.13. 什么是倒排索引
  13. 2.14. match query和term query
  14. 2.15. 使用redis分布式锁如何解决主从同步问题
  15. 2.16. redis分布式锁保证安全性?不会被其他线程操作?
  16. 2.17. mybatis缓存机制
    1. 2.17.1. 缓存失效条件
  17. 2.18. mybatis什么时候读取会产生脏数据
  18. 2.19. sql监控插件如何实现
    1. 2.19.1. 使用 MyBatis 自定义拦截器
    2. 2.19.2. 使用 MyBatis-Plus 内置功能
  19. 2.20. mq如何保证顺序性
  20. 2.21. 队列能有多少消费者
    1. 2.21.1. kafka
    2. 2.21.2. rabbitmq
  21. 2.22. 线程池如何设置核心线程数和最大线程数
  22. 2.23. 在一个线程池中,核心线程数为3,最大线程数为10,此时有五个任务提交,会有多少个线程产生
    1. 2.23.1. 情景1:使用无界队列(如LinkedBlockingQueue)
    2. 2.23.2. 情景2:使用直接提交策略(如SynchronousQueue)
  • 3. 后话