面经-北森后端开发实习一面(线程安全和锁未完成)
为什么离开了法大大实习?
阿巴阿巴阿巴。。。。。
为什么当时要用kafka进行UGC消息推送?业务场景是什么?
原因:
- kakfa是一个高吞吐、低延迟的消息队列,UGC消息需要实时推送给其他用户,比如评论、点赞等,因此kafka可以确保消息被快速处理,从而提供更好的实时性和用户体验。
- 因为kafka 的可靠性比较高、消息会被持久化在磁盘上,确保消息不会丢失。
- 因为kafka可以支持多个消费组和分区,UGC在高并发的情况下需要处理大量的消息,而kafka可以轻松应对高负载和大规模的UGC
- 解耦异步,主要是因为异步的特性,不会因为同步处理的方式导致主线程阻塞,导致性能下降。使用消息队列,可以将这些操作封装成消息,放入消息队列中,异步处理这些操作,不影响主线程的执行,提高系统性能和响应速度。
kafka的生产和消费逻辑?
生产实现逻辑:
1、连接到kafka集群,创建一个producer
2、生产者通过producer对象将消息发送到指定的topic。消息可以包含键和值,键可以帮助消息路由到特定的分区,如果没有特定的key就会通过自带的负载均衡机制选择一个分区。
3、通过key实现分区,指定了键的话将使用哈希函数映射到对应的分区,保证相同键的消息将始终被写入相同的分区,从而也可以保持消息的顺序。
4、生产者发送消息也会把消息持久化到磁盘,即使消息被消费,他也会根据kafka的配置的保留时间内保留在磁盘中。
消费实现逻辑:
1、消费者连接到kafka集群,创建一个consumer对象,并订阅一个或者多个主题
2、消费者使用consumer从指定的主题中拉取消息,其中消费者可以选择从分区的起始位置开始消费或者从上次消费的偏移量继续消费。
3、进行消息分配的时候,由于每个topic可能有多个partition,多个消费者可以同时消费同一个topic不同的partition,实现负载均衡和并行处理。(这时候可能会有一个问题,一个消费者组可以消费同一个分区吗?为什么每个分区只能被消费者组中的一个消费者消费?一个消费组可以同时消费不同的分区吗?)
当时开发的过程中,kafka采用的路由分区策略是什么?
使用的默认的分区策略
就是会使用消息的key进行哈希,然后根据哈希值对分区总数取模得到一个分区号,将消息路由到对应的分区。这样就可以保证具有相同key的消息被写入同一个分区,保证消息有序。
kafka如何保证顺序消费?
保证相关消息都发送到同一个分区这样就可以保证有序了,即通过一个partition消费对应的消息,因为生产者发送消息的时候,消息会逐一添加到该partition的日志中,并会分配一个唯一的offset,以保证从此offset开始进行消息的消费,从而保证消息顺序的有序性。
如果kafka消息阻塞的话怎么处理?
生产者角度:
可能是因为生产者的生产速率太快导致,超过了消费者的消费能力,可以通过控制生产者的生产速率来适配消费者的处理速率
消费者角度:
可以创建多个消费者组或消费者实例,对不同的topic下不同的partition进行消费,但是最好是一个partition只被一个消费者消费,但一个消费者可以消费多个partition。
硬件存储问题:
由于存储空间满了,导致消息无法写入,因此消费者也无法进行消费,清空一定的磁盘空间即可。
kafka是怎么保证主从副本的数据一致性?
1、生产者向kafka的主副本发送消息,主副本会将消息追加到日志里面,进行持久化操作,并将消息分发给从副本。
2、从副本会周期性从主副本拉取数据保存到自己的本地log以保证同步。
3、ISR机制(最重要的一点):Kafka会维护一个ISR的集合,它保存了相对主副本实时同步的从副本,如果某个从副本没有跟上主副本的进度,就会被移出ISR机制,直到它后面又跟上主副本的进度之后才会回到ISR集合中。
4、当所有ISR的从副本都确认收到了消息,这时候主副本就会把消息标记为已提交,意味着消息写入了足够多的副本中,确保数据的可靠性。
如果主副本没了、这时候会怎么选择从副本?
会从ISR中同步的从副本选择作为主副本,然后此时被选举成为主副本的副本就会同步消息到其他的从副本中,来保证数据同步和备份。
消息丢失的场景有哪些?以及怎么去解决这个问题?
- 生产者丢失消息
- 生产者使用producer.send方法的时候,因为这个方法是异步的,会立即返回,如果此时出现了网络波动就会出现消息丢失的情况,导致Broker并没有接收到生产者发过来的消息
- Kafka Broker 服务端丢失消息
- 如果因为leader broker宕机了触发选举过程,集群选举了一个落后的leader之后就会出现消息丢失的情况。
- 由于broker持久化消息需要先通过写入页缓存,再从页缓存写入磁盘,即通过异步批量刷盘的方式写入磁盘,也就是说需要消息达到一定的量和时间间隔才会去刷盘,具体的刷盘操作一般是由操作系统调度,如果刷盘之前导致broker宕机了也会出现消息丢失的情况。
- 消费者丢失消息
- 拉取消息之后,先提交offset,后处理消息:如果处理消息的时候突然宕机或者处理错误,但是此时offset又已经提交,等消费者重启恢复之后,就会从offset的下一个开始消费,之前未处理完的消息就会不再做处理,这时候对于消费者来说就是消息丢失了。
- 拉取消息之后,先处理消息,再提交offset:如果消息已经处理好了,但是要提交offset的时候突然宕机了,这时候重启服务,就会出现消费者会重新拉取上次消费了的消息进行再次消费的情况,此时会出现重复消费的情况或者直接丢失消息。
有没有调用过一些三方接口?怎么实现的?
1、文本审核内容审核
2、音视频内容审核
对于微信接口的AccessToken怎么处理那个有效时间无效问题?
定时刷新,将对应的AccessToken存在Redis里面,每次需要使用的时候,先从缓存中找,如果缓存中的东西失效了,那么就重新获取一次token进行令牌刷新并存在Redis里面,但是存取的时间是6000s,防止处理事务处理过程中出现失效的情况。
如果服务器CPU或者内存满了怎么分析?
1、使用监控工具确定是什么资源占用率高
2、使用系统工具查看占用cpu高的进程
3、查看日志找到异常或者错误信息,来定位具体是什么地方导致线程阻塞从而导致的cpu飙升,并及时优化代码。
对于高并发问题,要怎么处理数据的一致性问题以及保证线程安全?
解决方案:
- 使用并发容器,如ConcurrentHashMap,它是线程安全的数据结构,可用于高并发场景。
- 通过分布式锁的方式,保证数据一致性,确保在同一时间只有一个线程可以访问共享资源。
- 根据业务需求,设置合适的数据库隔离级别,如读未提交、读已提交、可重复读、串行化,以保证数据的一致性和隔离性。
常用的锁有哪些?
介绍一下互斥锁、自旋锁
互斥锁:用于保护共享资源,同一时间只允许一个线程访问共享资源,其他线程需要等待锁的释放。
Redis的Set扩容机制如何实现?
1、当Set元素数量达到一定的阈值的时候,创建新的哈希表
2、Redis会逐一将原来哈希表的元素根据新的哈希映射函数分配到新的哈希表中
3、更新指针,这个指针指向的是存储哈希表的内存地址,以便在扩容过程中可以正确地切换到新的哈希表。
介绍一下hashmap以及如何解决哈希冲突的?
问烂了、已经不想打字了,补充几个其他相关的题
为什么hashmap要在红黑树节点小于等于6的时候转回链表?
- 最主要的原因是因为节点数小于等于6的时候,链表和红黑树查询平均时间复杂度几乎一样,都是O(n),在性能差异不大的情况下,红黑树的维护成本远远大于链表的维护成本。
- 根据概率论的泊松分布,造成哈希冲突导致桶的的链表长度等于6的概率很大,而查询的平均复杂度和红黑树一样,在同等效率下选择开销更小的链表结构。
为什么hashmap负载因子是0.75?
1、避免频繁扩容带来的性能损失
2、提高空间利用率,负载因子 = 存储的元素数量 / 哈希表可存储元素的总数量
3、较小的负载因子能够减少哈希冲突,提高性能,这也是经验之谈。
哈希扩容的时候,是根据什么来扩容的?
元素数量 / 哈希表的容量与负载因子进行比较,
索引失效有哪些?
6种,左右模糊或左模糊,对索引使用函数,对索引隐式类型转换(查询的时候数据类型转换,实际上会用到函数CAST),对索引进行表达式计算(where id + 1 = 10不行),联合索引非左匹配原则,where子句有OR
对索引使用函数,对索引隐式类型转换(查询的时候数据类型转换,实际上会用到函数CAST),对索引进行表达式计算(where id + 1 = 10不行),这三种造成失效的原因就是索引存的是原始的值,而不是使用函数或者通过计算得到的新的值,如果建立对某个字段使用函数或者计算的索引来解决这个问题。例如建立索引alter table t_user add key idx_name_length ((length(name)));
介绍一些JWT,比较一下和Cookie-Session的区别?
阿巴阿巴阿巴。。。。。
JWT是怎么解决防篡改问题的?
阿巴阿巴阿巴。。。。。
介绍一下如何判断死亡对象的方法以及垃圾收集算法
判断对象死亡的方法:
- 引用计数法:当被引用了就计数+1,引用失效就-1,当计数器的值==0就说明此对象已经死亡。
- 可达性分析算法:如果某个对象不能有一条路径到达GC ROOTS,如果没有的话,就说明这个对象已经死亡,有的话就说明,这个对象没有死亡。
这个时候实际上只能表示对象可以回收了,但是不一定代表着一定会回收,宣布一个对象死亡,要进行两次标记,可达性分析算法中不可达对象就会先第一次被标记并进行一次筛选,筛选条件就是此对象是否有必要执行finalize(),如果finalize()方法没有被覆盖,或者被虚拟栈调用过了,就会被认为没有必要被执行。被判定需要执行的对象就会被放入一个队列进行二次标记,除非这个对象跟引用链上的任何一个对象建立关联,否则就会被回收。
线程和进程的区别
简介
- 进程:计算机正在运行的一个程序实例,比如打开微信
- 线程:又称轻量级进程,多个线程可以在同一个进程中同时执行,并且共享进程的资源比如:内存空间、网络连接等,举例:你打开的微信就有一个线程专门用来拉取别人发你的最新的消息。
- 一个进程可以产生多个线程,多个进程间共享堆和方法区,每个线程自己独立的程序计数器、虚拟机栈和本地方法栈。
- 进程线程最大的不同就是各进程是独立的,而线程不一定因为同一进程中的线程极有可能会相互影响。
- 线程执行开销小,不利于资源的管理和保护但是进程相反。
介绍一下协程(不会)
协程是相较于线程更加轻量级的一种执行单位,协程通常不会被内核调度所以协程切换不会涉及内核级的上下文切换,核心思想就是可以暂停和回复的执行单元,一个可以在某一个点挂起,然后执行权交给其他协程,等其他协程执行完毕之后可以恢复,从挂起的地方继续执行。
协程的并行数也受限于CPU的核心数,多个协程在同一线程中交替执行,充分利用了单线程的资源。但协程并发的时候可以通过调度器来控制哪个协程何时执行。
平时怎么学习的?
阿巴阿巴阿巴阿巴阿巴阿巴。。。
了不了解ElasticSearch?(并不了解)
所以无解