前言
当我想好好复盘一下的时候总是通知我面试。怪不得说找工作是一件很辛苦的事情,既要找机会,又要面试,又要复盘,又要改简历,中途还得穿插学习,有点顶不住,忙里偷闲复盘一下。
复盘
两个面试放在一起讲了。一个都在问项目另一个八股文比较多。
项目
深挖项目,无言。
- 任务分片怎么做?
- 优化点?
java和python多线程什么差别?
原回答:python一般使用threading.Thread直接创建线程,java里一般采用线程池管理线程。python的多线程无法利用到多核优势。
网上看别人的回答:1.python不是真正的多线程。(GIL的问题)2.Java中,每个线程都有自己的堆栈空间,线程之间的堆栈空间是独立的。Python中,所有线程共享相同的内存空间,因此需要特别小心避免数据竞争和死锁。
工作最大的挑战是什么?
项目管理和统筹。
最近有看什么书
复盘+。当然也没看多少,时间根本不够。
多线程编程时,如何确保数据安全
加锁是比较实用的操作。
常用的锁?
java里就是synchronized还有lock,其他的有分布式锁。
synchronized关键字和java里的可重入锁有什么区别
这里面试官应该是想说synchronized和ReentrantLock。因为synchronized也是可重入的。
- 两者都是可重入锁。
- ReentrantLock可以支持公平和非公平。
- ReentrantLock提供了一种能够中断等待锁的线程的机制,通过
lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 - 可实现选择性通知(锁可以绑定多个条件):
synchronized
关键字与wait()
和notify()
/notifyAll()
方法相结合可以实现等待/通知机制。ReentrantLock
类当然也可以实现,但是需要借助于Condition
接口与newCondition()
方法。
synchronized关键字加在普通方法和静态方法有什么区别
- 修饰实例方法 (锁当前对象实例)
- 修饰静态方法 (锁当前类)
- 修饰代码块 (锁指定对象/类)
线程池的几个常用参数
ThreadPoolExecutor
3 个最重要的参数:
corePoolSize
: 任务队列未达到队列容量时,最大可以同时运行的线程数量。maximumPoolSize
: 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue
: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor
其他常见参数 :
keepAliveTime
:线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁。unit
:keepAliveTime
参数的时间单位。threadFactory
:executor 创建新线程的时候会用到。handler
:拒绝策略(后面会单独详细介绍一下)。
常见拒绝策略策略
ThreadPoolExecutor.AbortPolicy
:抛出 RejectedExecutionException
来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
ThreadPoolExecutor.DiscardPolicy
:不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy
:此策略将丢弃最早的未处理的任务请求。
多线程导致死锁的原因
互斥条件:该资源任意一个时刻只由一个线程占用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
死锁的检测和避免
使用jmap
、jstack
等命令查看 JVM 线程栈和堆内存的情况。如果有死锁,jstack
的输出中通常会有 Found one Java-level deadlock:
的字样,后面会跟着死锁相关的线程信息。另外,实际项目中还可以搭配使用top
、df
、free
等命令查看操作系统的基本情况,出现死锁可能会导致 CPU、内存等资源消耗过高。
如何预防死锁? 破坏死锁的产生的必要条件即可:
- 破坏请求与保持条件:一次性申请所有的资源。
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
有一个任务需要等待几个子任务执行完成,需要怎么实现
Semaphore,CountDownLatch, CyclicBarrier。
假设有四个子任务
1 | final Semaphore semaphore = new Semaphore(-4); |
1 | final CountDownLatch countDownLatch = new CountDownLatch(4); |
1 | final CyclicBarrier cb = new CyclicBarrier(5); |
sql查询慢怎么排查?加了索引还是比较慢怎么排查
使用explain检查sql。
优化手段:
- 避免使用select * ,原因是会消耗更多CPU,增加带宽,无法使用mysql优化器覆盖索引的优化。
- 分页优化。使用子查询或内连接,使用子查询的id作为主查询的条件。
1 | SELECT `score`, `name` FROM `cus_order` |
- 尽量避免多表做 join
- 建议不要使用外键与级联
- 选择合适的字段类型。某些字符串可以转换成数字类型存储比如可以将 IP 地址转换成整型数据;对于非负型的数据 (如自增 ID,整型 IP,年龄) 来说,要优先使用无符号整型来存储;小数值类型(比如年龄、状态表示如 0/1)优先使用 TINYINT 类型;对于日期类型来说, 一定不要用字符串存储日期。可以考虑 DATETIME、TIMESTAMP 和 数值型时间戳;金额字段用 decimal,避免精度丢失;尽量使用自增 id 作为主键;不建议使用 NULL 作为列默认值;
索引失效的场景
- SELECT * 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖;
- 创建了组合索引,但查询条件未准守最左匹配原则;
- 在索引列上进行计算、函数、类型转换等操作;
- 以 % 开头的 LIKE 查询比如 LIKE ‘%abc’;;
- 查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到;
- IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同);
发生隐式转换;
未满足最左匹配为什么会索引失效
在 InnoDB 中联合索引只有先确定了前一个(左侧的值)后,才能确定下一个值。
innoDb索引结构?b+树有什么优点?
innodb使用B+树。
- 由于B+树在内部节点上不包含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子节点上关联的数据也具有更好的缓存命中率。
- B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
代码里如何管理事务的?事务没生效的场景?
声明式事务,@Transactional。当然也可以用代码手动开启一个事务,称作编程式事务。
失效场景:
- 没有被spring代理。如将注解标注在接口方法上,被final、static关键字修饰的类或方法,类方法内部调用
- 框架不支持。非public修饰的方法,spring底层直接限制事务管理;多线程,一个事务是建立在一个数据库连接上的;数据库本身不支持事务,比如myisam
- 错误使用@Transactional。错误的传播机制;rollbackFor设置错误,默认情况下事务仅回滚运行时异常和Error;内部异常被catch
分布式事务有用过吗?
CAP 理论和 BASE 理论。CAP 也就是 Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性) 这三个单词首字母组合。
CAP 定理(CAP theorem)指出对于一个分布式系统来说,当设计读写操作时,只能能同时满足以下三点中的两个:
- 一致性(Consistence) : 所有节点访问同一份最新的数据副本
- 可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
- 分区容错性(Partition tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。
为啥无同时保证 CA 呢?
举个例子:若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。
选择的关键在于当前的业务场景,没有定论,比如对于需要确保强一致性的场景如银行一般会选择保证 CP 。
BASE 是 Basically Available(基本可用) 、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。
BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。
分布式一致性的 3 种级别:
- 强一致性 :系统写入了什么,读出来的就是什么。
- 弱一致性 :不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
- 最终一致性 :弱一致性的升级版。,系统会保证在一定时间内达到数据一致的状态,
业界比较推崇是最终一致性级别,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。
分布式事务的解决方案有很多,比如:2PC、3PC、TCC、本地消息表、MQ 事务(Kafka 和 RocketMQ 都提供了事务相关功能) 、Saga 等等。
2PC 将事务的提交过程分为 2 个阶段:准备阶段 和 提交阶段 。
准备阶段的核心是“询问”事务参与者执行本地数据库事务操作是否成功。
- 事务协调者/管理者(后文简称 TM) 向所有涉及到的 事务参与者(后文简称 RM) 发送消息询问:“你是否可以执行事务操作呢?”,并等待其答复。
- RM 接收到消息之后,开始执行本地数据库事务预操作比如写 redo log/undo log 日志,此时并不会提交事务 。
- RM 如果执行本地数据库事务操作成功,那就回复“Yes”表示我已就绪,否则就回复“No”表示我未就绪。
提交阶段的核心是“询问”事务参与者提交本地事务是否成功。
当所有事务参与者都是“就绪”状态的话:
- TM 向所有参与者发送消息:“你们可以提交事务啦!”(Commit 消息)
- RM 接收到 Commit 消息 后执行 提交本地数据库事务 操作,执行完成之后 释放整个事务期间所占用的资源。
- RM 回复:“事务已经提交” (ACK 消息)。
- TM 收到所有 事务参与者 的 ACK 消息 之后,整个分布式事务过程正式结束。
当任一事务参与者是“未就绪”状态的话:
- TM 向所有参与者发送消息:“你们可以执行回滚操作了!”(Rollback 消息)。
- RM 接收到 Rollback 消息 后执行 本地数据库事务回滚 执行完成之后 释放整个事务期间所占用的资源。
- RM 回复:“事务已经回滚” (ACK 消息)。
- TM 收到所有 RM 的 ACK 消息 之后,中断事务。
2PC 的优点:
- 实现起来非常简单,各大主流数据库比如 MySQL、Oracle 都有自己实现。
- 针对的是数据强一致性。不过,仍然可能存在数据不一致的情况。
2PC 存在的问题:
- 同步阻塞 :事务参与者会在正式提交事务之前会一直占用相关的资源。比如用户小明转账给小红,那其他事务也要操作用户小明或小红的话,就会阻塞。
- 数据不一致 :由于网络问题或者TM宕机都有可能会造成数据不一致的情况。比如在第2阶段(提交阶段),部分网络出现问题导致部分参与者收不到 Commit/Rollback 消息的话,就会导致数据不一致。
- 单点问题 : TM在其中也是一个很重要的角色,如果TM在准备(Prepare)阶段完成之后挂掉的话,事务参与者就会一直卡在提交(Commit)阶段。
3PC 是人们在 2PC 的基础上做了一些优化得到的。3PC 把 2PC 中的 准备阶段(Prepare) 做了进一步细化,分为 2 个阶段:
- 准备阶段(CanCommit)
- 预提交阶段(PreCommit)
准备阶段 RM 不会执行事务操作,TM 只是向 RM 发送 准备请求 ,顺便询问一些信息比如事务参与者能否执行本地数据库事务操作。RM 回复“Yes”、“No”或者直接超时未回复。
如果准备阶段所有的 RM 回复 “Yes”的话,TM 就会向所有的 RM 发送 PreCommit 消息(预提交请求) ,RM 收到消息之后会执行本地数据库事务预操作比如写 redo log/undo log 日志。
如果准备阶段有任一 RM 回复“NO” 或者直接超时未回复的话,TM 就会给所有 RM 发送 Abort 消息(中断请求) ,RM 收到消息后直接中断事务。这样其实对 RM 来说损失并不大,因为本质上 RM 到现在还并没有实际做什么事情。
如果 RM 成功的执行了事务预操作,就返回 “YES”。否则,返回“No”(最后的反悔机会)。
预提交阶段 TM 与 RM 都引入了超时机制,如果 参与者 没有收到 TM 的 PreCommit 消息,或者 TM 没有收到参与者返回的预执行结果状态,那么在超过等待时间后,事务就会中断,这就避免了事务的阻塞。
3PC 还同时在事务管理者和事务参与者中引入了 超时机制 ,如果在一定时间内没有收到事务参与者的消息就默认失败,进而避免事务参与者一直阻塞占用资源。2PC 中只有事务管理者才拥有超时机制,当事务参与者长时间无法与事务协调者通讯的情况下(比如协调者挂掉了),就会导致无法释放资源阻塞的问题。
不过,3PC 并没有完美解决 2PC 的阻塞问题,引入了一些新问题比如性能糟糕,而且,依然存在数据不一致性问题。因此,3PC 的实际应用并不是很广泛,多数应用会选择通过复制状态机解决 2PC 的阻塞问题。
TCC(补偿事务)
- Try(尝试)阶段 : 尝试执行。完成业务检查,并预留好必需的业务资源。
- Confirm(确认)阶段 :确认执行。当所有事务参与者的 Try 阶段执行成功就会执行 Confirm ,Confirm 阶段会处理 Try 阶段预留的业务资源。否则,就会执行 Cancel 。
- Cancel(取消)阶段 :取消执行,释放 Try 阶段预留的业务资源。
TCC 模式不需要依赖于底层数据资源的事务支持,但是需要我们手动实现更多的代码,属于 侵入业务代码 的一种分布式解决方案。
- 2PC/3PC 依靠数据库或者存储资源层面的事务,TCC 主要通过修改业务代码来实现。
- 2PC/3PC 属于业务代码无侵入的,TCC 对业务代码有侵入。
- 2PC/3PC 追求的是强一致性,在两阶段提交的整个过程中,一直会持有数据库的锁。TCC 追求的是最终一致性,不会一直持有各个业务资源的锁。
MQ 事务
RocketMQ 、 Kafka、Pulsar 、QMQ 都提供了事务相关的功能。事务允许事件流应用将消费,处理,生产消息整个过程定义为一个原子操作。
- MQ 发送方(比如物流服务)在消息队列上开启一个事务,然后发送一个“半消息”给 MQ Server/Broker。事务提交之前,半消息对于 MQ 订阅方/消费者(比如第三方通知服务)不可见
- “半消息”发送成功的话,MQ 发送方就开始执行本地事务。
- MQ 发送方的本地事务执行成功的话,“半消息”变成正常消息,可以正常被消费。MQ 发送方的本地事务执行失败的话,会直接回滚。
MQ 的事务消息使用的是两阶段提交(2PC)
好的Java代码应该具备什么?
开放题,最关键的应该是代码可读。
面向对象编程的五个基本原则
单一功能、开闭原则、里氏替换、接口隔离以及依赖反转
啊这,完全没印象了。
常用的设计模式?
4年前写的一系列文章。需要再好好看看。
策略模式跟模板模式的区别
模板模式一般只针对一套算法
,注重对同一个算法的不同细节进行抽象提供不同的实现。而策略模式注重多套算法多套实现
,在算法中间不应该有交集,因此算法和算法只间一般不会有冗余代码!
策略模式关注多种算法,模板模式关注一种算法。策略模式不同策略之间代码很少冗余。
redis的场景?分布式锁的场景?为什么采用redis做分布式锁?
之前复盘过。redis具有高效性、原子性操作、过期时间设置、Lua脚本支持以及高可用性和容错性等特性,使其成为一种可靠的分布式锁解决方案
不使用redis做分布式锁可以用哪些替代?
用数据库的悲观锁。之前看xxl的原理时有涉及到。
用数据库如何做分布式锁?锁名称的产生逻辑?
select for update + 唯一索引。
锁名称应满足唯一性,如订单号。
kafka和rabbitmq有什么区别
前两篇复盘过。其实差别不是很大。
kafka的结构?
Producer: 特指消息的生产者
Consumer : 特指消息的消费者
**Consumer Group : **消费者组,可以并行消费Topic中partition的消息
Broker:缓存代理,Kafa 集群中的一台或多台服务器统称为 broker。
Topic:特指 Kafka 处理的消息源(feeds of messages)的不同分类。
Partition:Topic 物理上的分组,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列。partition 中的每条消息都会被分配一个有序的 id(offset)
Message:消息,是通信的基本单位,每个 producer 可以向一个 topic(主题)发布一些消息
Producers(是个动词):消息和数据生产者,向 Kafka 的一个 topic 发布消息的过程叫做 producers
Consumers(是个动词):消息和数据消费者,订阅 topics 并处理其发布的消息的过程叫做 consumers
Kafka通过Zookeeper存储集群的meta等信息。
一个Topic可以认为是一类信息,逻辑上的队列,每条消息都要指定Topic。为了使得Kafka的吞吐量可以线性提高,物理上将Topic分成一个或多个Partition。每个Partition在存储层面时append log文件,消息push进来后,会被追加到log文件的尾部,每条消息在文件中的位置成为offset(偏移量),offset是一个long型数字,唯一的标识一条信息。因为每条消息都追加到Partition的尾部,所以属于磁盘的顺序写,效率很高。
如何保证消息的可靠性
之前有讲过。
解决过生产上什么问题?
答得不好。回答了一个排查cpu内存过高的一个步骤逻辑,重新整理下。
watchdog的原理
之前复盘过。
通过创建不过期的key实现锁会有什么问题
异常情况锁无法释放。
锁续期失败后的处理逻辑
watchdog中锁续期失败后不再续期。
rabbitmq其他队列了解过吗
这里回答错了,我们用的Fanout、Direct、Topic指的是交换机的类型。
Classic,Quorum,Stream才是队列类型。经典队列是 RabbitMQ 提供的原始队列类型,一般我们使用的都是这个。
仲裁队列Quorum在分布式环境下对消息的可靠性保障更高。官方文档中明确表示,未来可能会使用Quorum仲裁队列来替代传统的Classic队列。Quorum队列基于Raft一致性协议实现,是一种新型的分布式消息队列。与Classic队列相比,Quorum队列以牺牲部分高级队列特性为代价,来换取更高的消息可靠性。
特性 | Classic |
Quorum |
---|---|---|
非持久化队列(Non-durable queues ) |
支持 | 不支持 |
独占队列(Exclusivity ) |
支持 | 不支持 |
每条消息的持久化(Per message persistence ) |
每条消息 | 总是 |
会员变更(Membership changes ) |
自动 | 手动 |
消息TTL (Message TTL ) |
支持 | 支持(3.10版本开始) |
队列TTL (Queue TTL ) |
支持 | 支持 |
队列长度限制(Queue length limits ) |
支持 | 支持 |
懒加载(Lazy behaviour ) |
支持 | 始终 |
消息优先级(Message priority ) |
支持 | 不支持 |
消费者优先级(Consumer priority ) |
支持 | 支持 |
死信交换(Dead letter exchanges ) |
支持 | 支持 |
毒消息处理(Poison message handling ) |
不支持 | 支持 |
全局Qos (Global QoS Prefetch ) |
支持 | 不支持 |
Stream
队列是RabbitMQ
自3.9.0
版本开始引入的一种新的数据队列类型,也是目前官方最为推荐的队列类型。这种队列类型的消息是持久化到磁盘并且具备分布式备份的,更适合于消费者多,读消息非常频繁的场景
- 大规模分发(
large fan-outs
)
当想要向多个订阅者发送相同的消息时,以往的队列类型必须为每个消费者绑定一个专用的队列。如果消费者的数量很大,这就会导致性能低下。而Stream
队列允许任意数量的消费者使用同一个队列的消息,从而消除绑定多个队列的需求。
- 消息回溯(
Replay/Time-travelling
)
RabbitMQ
已有的这些队列类型,在消费者处理完消息后,消息都会从队列中删除,因此,无法重新读取已经消费过的消息。而Stream
队列允许用户在日志的任何一个连接点开始重新读取数据。
- 高吞吐性能(
Throughput Performance
)
Stream
队列的设计以性能为主要目标,对消息传递吞吐量的提升非常明显。
- 大日志(
Large logs
)
RabbitMQ
一直以来有一个让人诟病的地方,就是当队列中积累的消息过多时,性能下降会非常明显。但是Stream
队列的设计目标就是以最小的内存开销高效地存储大量的数据。
rabbitmq的灰度发布策略?
这个我猜测面试官是想问服务的发布顺序,因为他后面又问了一下。发布顺序肯定是新消费者新生产者,下线旧生产者旧消费者。
但是这个确实也引出了我的一个思考,如果说消费的逻辑有改变,那么怎么进行灰度?
首先应该老消费逻辑和新消费逻辑都要保留,提供过渡。生产者只保留新的也没问题,如果是要基于逻辑的一个灰度,那么可能业务上也需要配合,毕竟有些灰度逻辑网关没法保证始终落在新副本上。
mq的流量大概有多少
目前是800-1000左右。