大家好,最近有位读者去面试了oppo,给大家整理了面试真题的答案。希望对大家有帮助哈,一起学习,一起进步。
大家平时做的项目,如果很多知识点跟面试八股文相关的话,就可以相对条理清晰地写到简历去。
-1
到缓存,代表数据库没记录。下次判断-1
,就不查库了,以解决缓存穿透问题。还有平时你做的项目,有一些比较好的设计,都可以说一下哈,比如你是如何保证数据一致性的,怎么优化接口性能的。
一般你讲述你做的项目时,面试官会根据你项目涉及的一些面试点,然后抽他感兴趣的一两个来问。所以大家对哪些知识点熟悉,讲述项目时,就说你用该知识点,解决了什么问题。
分布式事务:就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单来说,分布式事务指的就是分布式系统中的事务,它的存在就是为了保证不同数据库节点的数据一致性。
聊到分布式事务,大家记得这两个理论哈:CAP理论 和 BASE 理论
分布式事务的几种解决方案:
2PC(二阶段提交)方案
2PC,即两阶段提交,它将分布式事务的提交拆分为2个阶段:prepare和commit/rollback
,即准备阶段和提交执行阶段。在prepare准备阶段需要等待所有参与子事务的反馈,因此可能造成数据库资源锁定时间过长,不适合并发高以及子事务生命周长较长的业务场景。并且协调者宕机的话,所有的参与者都收不到提交或回滚指令。
3PC
两阶段提交分别是:CanCommit,PreCommit 和 doCommit
,这里不再详述。3PC 利用超时机制解决了 2PC 的同步阻塞问题,避免资源被永久锁定,进一步加强了整个事务过程的可靠性。但是 3PC 同样无法应对类似的宕机问题,只不过出现多数据源中数据不一致问题的概率更小。
TCC
TCC 采用了补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:Try-Confirm-Cancel
TCC方案让应用可以自定义数据库操作的粒度,降低了锁冲突,可以提升性能。但是应用侵入性强,try、confirm、cancel三个阶段都需要业务逻辑实现。
本地消息表
ebay最初提出本地消息表这个方案,来解决分布式事务问题。业界目前使用这种方案是比较多的,它的核心思想就是将分布式事务拆分成本地事务进行处理。可以看一下基本的实现流程图:
最大努力通知
最大努力通知方案的目标,就是发起通知方通过一定的机制,最大努力将业务处理结果通知到接收方。
seata
Saga 模式是 Seata 提供的长事务解决方案。核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
Saga的并发度高,但是一致性弱,对于转账,可能发生用户已扣款,最后转账又失败的情况。
如果你调用下游接口超时了,是不是考虑重试?如果重试,下游接口就需要支持幂等啦。
实现幂等一般有这8种方案:
大家可以结合自己公司的MySQL架构聊聊。[]
数据的库高可用方案
哈希表、有序数组和搜索树。
这主要考察联合索引的最左前缀原则知识点。
N
个字段。比如组合索引(a,b,c)
可以相当于建了(a),(a,b),(a,b,c)
三个索引,大大提高了索引复用能力。M
个字符。因此给你ab,ac,abc
字段,你可以直接加abc
联合索引和ac
联合索引即可。
四大数据库隔离级别,分别是读未提交,读已提交,可重复读,串行化(Serializable)
。
MySQL选择Repeatable Read(可重复读)
作为默认隔离级别,我们的数据库隔离级别选的是读已提交
。
binlog的格式也有三种:statement,row,mixed。设置为statement
格式,binlog记录的是SQL的原文。又因为MySQL在主从复制的过程是通过binlog
进行数据同步,如果设置为读已提交(RC)隔离级别,当出现事务乱序的时候,就会导致备库在 SQL 回放之后,结果和主库内容不一致。
比如一个表t,表中有两条记录:
CREATE TABLE t (
a int(11) DEFAULT NULL,
b int(11) DEFAULT NULL,
PRIMARY KEY a (a),
KEY b(b)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
insert into t1 values(10,666),(20,233);
两个事务并发写操作,如下:
在读已提交(RC)
隔离级别下,两个事务执行完后,数据库的两条记录就变成了(30,666)、(20,666)
。这两个事务执行完后,binlog也就有两条记录,因为事务binlog用的是statement
格式,事务2先提交,因此update t set b=666 where b=233
优先记录,而update t set a=30 where b=666
记录在后面。
当bin log
同步到从库后,执行update t set b=666 where b=233
和update t set a=30 where b=666
记录,数据库的记录就变成(30,666)、(30,666)
,这时候主从数据不一致啦。
因此MySQL的默认隔离离别选择了RR
而不是RC
。RR
隔离级别下,更新数据的时候不仅对更新的行加行级锁,还会加间隙锁(gap lock)
。事务2要执行时,因为事务1增加了间隙锁,就会导致事务2执行被卡住,只有等事务1提交或者回滚后才能继续执行。
并且,MySQL还禁止在使用statement
格式的binlog
的情况下,使用READ COMMITTED
作为事务隔离级别。
那为什么MySQL官方默认隔离级别是RR,而有些大厂选择了RC作为默认的隔离级别呢?
RC 在加锁的过程中,不需要添加Gap Lock
和 Next-Key Lock
的,只对要修改的记录添加行级锁就行了。因此RC的支持的并发度比RR高得多,
正是因为RR隔离级别增加了Gap Lock
和 Next-Key Lock
锁,因此它相对于RC,更容易产生死锁。
先回忆下什么是不可重复读。假设现在有两个事务A和B:
事务A被事务B干扰到了!在事务A范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。
RR隔离级别实现原理,就是MVCC多版本并发控制,而MVCC是是通过Read View+ Undo Log
实现的,Undo Log 保存了历史快照,Read View可见性规则帮助判断当前版本的数据是否可见。
Undo Log
版本链长这样:
Read view 的几个重要属性
m_ids
:当前系统中那些活跃(未提交)的读写事务ID, 它数据结构为一个List。min_limit_id
:表示在生成Read View时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值。max_limit_id
:表示生成Read View时,系统中应该分配给下一个事务的id值。creator_trx_id
: 创建当前Read View的事务IDRead view 可见性规则如下:
trx_id < min_limit_id
,表明生成该版本的事务在生成Read View前,已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。trx_id>= max_limit_id
,表明生成该版本的事务在生成Read View后才生成,所以该版本不可以被当前事务访问。min_limit_id =<trx_id< max_limit_id
,需要分3种情况讨论
- 3.1 如果
m_ids
包含trx_id
,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_id
等于creator_trx_id
的话,表明数据是自己生成的,因此是可见的。- 3.2 如果
m_ids
包含trx_id
,并且trx_id
不等于creator_trx_id
,则Read View生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不见的;- 3.3 如果
m_ids
不包含trx_id
,则说明你这个事务在Read View生成之前就已经提交了,修改的结果,当前事务是能看见的。
查询一条记录,基于MVCC,是怎样的流程
假设存在事务A和B,SQL执行流程如下
在可重复读(RR)隔离级别下,一个事务里只会获取一次Read View,都是副本共用的,从而保证每次查询的数据都是一样的。
假设当前有一张core_user表,插入一条初始化数据,如下:
基于MVCC,我们来看看执行流程
变量 | 值 |
---|---|
m_ids | 100,101 |
max_limit_id | 102 |
min_limit_id | 100 |
creator_trx_id | 100 |
然后回到版本链:开始从版本链中挑选可见的记录:
由图可以看出,最新版本的列name的内容是孙权,该版本的trx_id值为100。开始执行read view可见性规则校验:
min_limit_id(100)=<trx_id(100)<102;
creator_trx_id = trx_id =100;
由此可得,trx_id=100的这个记录,当前事务是可见的。所以查到是name为孙权的记录。
4 . 事务B进行修改操作,把名字改为曹操。把原数据拷贝到undo log,然后对数据进行修改,标记事务ID和上一个数据版本在undo log的地址。
5 . 事务B提交事务
6 . 事务A再次执行查询操作,因为是RR(可重复读)隔离级别,因此会复用老的Read View副本,Read View对应的值如下
变量 | 值 |
---|---|
m_ids | 100,101 |
max_limit_id | 102 |
min_limit_id | 100 |
creator_trx_id | 100 |
然后再次回到版本链:从版本链中挑选可见的记录:
从图可得,最新版本的列name的内容是曹操,该版本的trx_id值为101。开始执行read view可见性规则校验:
min_limit_id(100)=<trx_id(101)<max_limit_id(102);
因为m_ids{100,101}包含trx_id(101),
并且creator_trx_id (100) 不等于trx_id(101)
所以,trx_id=101
这个记录,对于当前事务是不可见的。这时候呢,版本链roll_pointer
跳到下一个版本,trx_id=100
这个记录,再次校验是否可见:
min_limit_id(100)=<trx_id(100)< max_limit_id(102);
因为m_ids{100,101}包含trx_id(100),
并且creator_trx_id (100) 等于trx_id(100)
所以,trx_id=100这个记录,对于当前事务是可见的,所以两次查询结果,都是name=孙权的那个记录。即在可重复读(RR)隔离级别下,复用老的Read View副本,解决了不可重复读的问题。
一个消息从生产者产生,到被消费者消费,主要经过这3个过程:
因此如何保证MQ不丢失消息,可以从这三个阶段阐述:
生产端如何保证不丢消息呢?确保生产的消息能顺利到达存储端。
如果是RocketMQ
消息中间件的话,Producer
生产者提供了三种发送消息的方式,分别是:
生产者要想发消息时保证消息不丢失,可以:
Broker
。send
消息异常或者返回非成功状态,可以发起重试。RocketMQ
的事务消息机制就是为了保证零丢失来设计的如何保证存储端的消息不丢失呢?确保消息持久化到磁盘,那就是刷盘机制嘛。
刷盘机制分同步刷盘和异步刷盘:
RocketMQ
的存储端Broker
才返回一个成功的ACK响应。它保证消息不丢失,但是影响了性能。PageCache
缓存,就返回一个成功的ACK响应。这样提高了MQ的性能,但是如果这时候机器断电了,就会丢失消息。除了同步刷盘机制,还有一个维度需要考虑。Broker
一般是集群部署的,有主节点和从节点。消息到Broker
存储端,只有主节点和从节点都写入成功,才反馈成功的ack
给生产者。这就是同步复制,它保证了消息不丢失,但是降低了系统的吞吐量。与之对应即是异步复制,只要消息写入主节点成功,就返回成功的ack
,它速度快,但是会有性能问题。
消费者执行完业务逻辑,再反馈会Broker
说消费成功,这样才可以保证消费阶段不丢消息。
事务消息主要用来解决消息生产者和消息消费者的数据一致性问题。我们先来回忆一下:一条普通的消息队列消息,从产生到被消费,经历的流程:
消息队列的事务消息流程是怎样的呢?
我们举个下订单清空购物车的例子吧。订单系统创建完订单后,然后发消息给下游系统购物车系统,清空购物车。
有些伙伴可能有疑惑,如果消费者消费失败怎么办呢?那数据是不是不一致啦?所以就需要消费者消费成功,执行业务逻辑成功,再反馈ack嘛。如果消费者消费失败,那就自动重试嘛,接口支持幂等即可。
判断一个数是奇数还是偶数,我们最容易想到的就是对2取余。
if( x % 2 )
// 奇数
else
// 偶数
还有一种方法,就是与1相与( &1
),具体实现如下:
if( x & 1 )
// 奇数
else
// 偶数
spring声明式事务,即@Transactional
,它可以帮助我们把事务开启、提交或者回滚的操作,通过Aop的方式进行管理。
在spring的bean的初始化过程中,就需要对实例化的bean进行代理,并且生成代理对象。生成代理对象的代理逻辑中,进行方法调用时,需要先获取切面逻辑,@Transactional注解的切面逻辑类似于@Around,在spring中是实现一种类似代理逻辑。
可以按业务领域、功能、重要程度进行划分。
如果是我们公司的话,使用了水平分库的方式,就是一个用户注册时,就划分了属于哪个数据库,然后具体的表结构是一样的。
业界还有垂直分库,就是按照不同的系统中的不同业务进行拆分,比如拆分成用户库、订单库、积分库、商品库,把它们部署在不同的数据库服务器。
分表的话也有水平分表和垂直分表,垂直分表就是将一些不常用的、数据较大或者长度较长的列拆分到另外一张表,水平分表就是可以按照某种规则(如hash取模、range),把数据切分到多张表去。一张订单表,按时间range拆分如下:
range划分利于数据迁移,但是存在数据热点问题。hash取模划分,不会存在明显的热点问题,但是不利于扩容。可以range+hash取模结合使用。
分布式ID可以使用雪花算法生成
雪花算法是一种生成分布式全局唯一ID的算法,生成的ID称为Snowflake IDs。这种算法由Twitter创建,并用于推文的ID。
一个Snowflake ID有64位。
Java 异常的顶层父类是Throwable
,它生了两个儿子,大儿子叫Error
,二儿子叫Exception
。
OutOfMemoryError
什么是RuntimeException(运行时异常)?
运行时异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
常见的RuntimeException异常:
什么是CheckedException(可检查的异常)?
从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等。
常见的 Checked Exception 异常:
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8