# 26.1.10字节跳动后端一二面
# MySQL主从同步的原理是啥?
- MySQL的主从同步依赖于Binlog实现,可以分为写入、同步、回放三步操作。主库对数据进行增删改操作后,会按顺序记录MySQL上所有变化并以二进制形式Binlog保存在磁盘上,主库的Binlog Dump线程会推送Binlog内容给从库,从库通过IO线程拉取主库的Binlog并写入中继日志(Relay Log),再由SQL线程重放中继日志中的操作,实现主从数据一致;
- SQL线程采用的是异步复制的模式,主库无需等待从库确认即可执行后续操作。

# binlog里都存了些啥?
- Binlog是MySQL服务层的二进制日志,全局生效、所有引擎共用。记录MySQL中操作的执行顺序、事务提交标记和所有数据变更类的操作事件,包括DDL语句(CREATE/ALTER/DROP)和DML语句(INSERT/UPDATE/DELETE),但是不存储只读查询操作。
# Binlog和数据库是啥关系?
- Biglog是MySQL数据库的数据变更专属日志,数据库对数据进行操作后会由binlog记录,不存储实际数据。数据库的数据可以通过Binlog回溯恢复。
# MySQL是怎么保证ACID的?
- MySQL依靠Undo日志,也就是回滚日志实现原子性(Atomicity)。在事务执行的每一步会记录反向日志,事务失败或回滚时可以通过Undo日志撤销已执行的修改,回滚到事务开始前的状态;
- MySQL依靠Redo日志(重做日志)和内存刷盘机制实现持久性(Durability),当事务提交时,数据变化会先顺序写入Redo日志中,日志落盘则判定提交成功。如果此时数据库宕机,重启后也会通过Redo日志重做所有已提交的事务,实现数据的永久保存。
- MySQL依靠锁机制和MVCC双重保证隔离性(Isolation),锁机制可以解决写写冲突、MVCC通过版本链解决读写冲突;同时提供读未提交、读已提交、可重复读和串行化四种隔离级别,默认是可重复读,能够避免脏读、不可重复读、降低幻读的出现。
- 事务的一致性(Consistency)是通过原子性、隔离性与持久性共同保障的,确认事务执行前后数据库的数据符合业务规则。
# 监控MySQL性能,一般看哪些指标?
- 我一般使用show global status查看数据库的实时状态或者查看数据库的慢查询日志监控MYSQL的性能。
- 了解执行指标时我会关注QPS(每秒查询数),表示数据库每秒处理的查询数量,如果QPS高可能意味着数据库的负载较大,可能存在性能瓶颈。代码中需要减少不必要的查询,可以使用索引提高查询效率;还可以关注TPS(每秒事务数),如果数值变化大可能是事务中存在锁等待时间或慢查询。
- 如果想关注连接与线程状态,可以看当前活跃的连接数(Threads_connected),该数值持续接近最大连接数时可能出现连接泄漏问题;还有正在执行查询的线程数,如果超过了CPU核心数2倍可能存在并发问题;
- 还有磁盘IO与日志的指标,比如redo log的刷盘频率和binlog的写入速度,磁盘的吞吐量等。如果磁盘IO有瓶颈,会导致日志刷盘阻塞而阻塞事务提交。
# 怎么保证MySQL和Redis的数据一致性?
- 首先需要根据业务场景选择适合的一致性方案,对于大多数场景来说,只需保证最终一致性,允许在缓存删除失败或并发读的情况下导致的短暂不一致发生。读操作先查缓存,命中则返回,未命中则查DB后写入缓存并返回;写操作则先更新DB再删除缓存。
- 在交易等核心场景下,需要保证强一致性时,可以使用分布式锁+双写同步实现,在写操作时先获取分布式锁,更新DB和缓存的数据之后再释放锁,可以避免并发读写导致的不一致,同一时间只有一个请求操作数据。
- 在高并发场景下,使用DB更新和MQ异步更新缓存保证最终一致性,写操作更新DB后发送MQ消息,消费端异步删除/更新缓存,同时添加定时任务对比数据库与缓存数据,作为兜底机制。
# 聊聊你对进程、线程、协程的理解?
- 进程是操作系统进行资源分配的基本单位,线程是轻量级的进程,是进行任务调度和执行的基本单位,而协程是用户态的轻量级线程,由编程语言运行时调度,必须依附于线程存在。
- 进程内可以有多个线程,进程的结束会终止所有线程,线程崩溃可能导致进程崩溃,但是进程崩溃不会影响其他进程。没有线程的进程可以看做单线程。一个线程可以创建上万甚至几十万个协程。
- 在开销方面,进程有独立的代码和数据空间,切换会有较大开销;同一类线程共享代码和数据空间,有独立的运行栈和程序计数器,切换开销小;协程的切换开销更小,因为它是用户态的线程,切换时不需要切换到内核态。
# 怎么监控和优化慢SQL?
- 当出现页面加载过慢或者接口压测响应时间过长时需要检查是否存在慢查询的情况。
- 我一般会通过开启数据库的慢查询日志找到慢的Sql,查询SQL的执行时间进行慢SQL筛选。
- 找到慢SQL后就检查SQL语句,可以执行explain语句,观察type字段可以看sql查询数据的方式,全表或者全索引查询会导致查询变慢;key字段可以看到实际使用的索引,判断索引的命中情况;extra字段可以判断是否出现了回表情况,可以尝试添加索引或修改返回字段修复。
- SQL语句还可能有关联表过多或者子查询嵌套过深问题。如果数据量过多,分页查询未优化也会导致查询变慢。
- 如果SQL本身没有问题,可能是数据库的配置不合理或者存在资源瓶颈,可以检查数据库的缓存大小,再看是否有CPU、内存或磁盘IO使用率过高的问题。
# 跳表的原理是啥?
- Redis的Zset对象使用了跳表这种数据结构,跳表是一种有序的多层索引链表,在普通有序单链表的基础上建立多级稀疏索引层,最底层是完整的有序数据链表,上层每一级索引都是下一级的部分节点映射;查询时从最顶层索引开始,快速定位数据的区间范围,逐层向下跳转缩小查找范围,最终到原始链表精准匹配;插入和删除时,通过随机算法决定节点的索引层级并同步维护各级索引,保证索引的稀疏性;
- 跳表使用空间换时间,把链表的查询、插入、删除的时间复杂度从O(n)优化到O(logn)。
# HashMap和ConcurrentHashMap在使用场景上有啥区别?
- HashMap适用于单线程下键值对的存储场景,如业务数据临时缓存,通过唯一键快速查询对应值,且有存储null键值的场景。
- ConcurrentHashMap:适用于高并发场景下的键值对存储场景,如多线程读写的缓存,秒杀活动中更新商品库存,能够保证线程安全同时兼顾性能。
# 怎么让HashMap变得线程安全?
HashMap线程不安全:

在HashMap中有三种常见方式实现线程安全。在HashMap的操作中添加显式锁(如ReentrantLock)可以保证线程安全;或者使用Collections.synchronizedMap(new HashMap<>()),通过该方法创建一个线程安全的HashMap对象,底层会对HashMap的所有方法加synchronized关键字以实现线程安全,方法简单但是并发性能较差;也可以使用ConcurrentHashMap类,这是专门用于多线程环境中的哈希表实现,JDK1.7是分段锁(Segment数组+ReentrantLock),JDK1.8变为对数组的每个链表头/红黑树根节点加锁使锁的粒度更细,结合CAS和volatile实现线程安全,允许多个线程同时读操作提高并发性能,还能解决HashMap扩容时的死循环问题;
# 你熟悉哪些设计模式?具体用在项目哪里了?
- 我了解设计模式有创建型、结构性和行为型三类。创建型设计模式有单例模式,用于保证类仅有一个实例并提供全局访问入口,工厂模式用于封装对象创建逻辑,将对象的创建与使用解耦;结构性设计模式有适配器模式,可以兼容不匹配的接口,装饰器模式可以动态增强对象的功能,类似的还有代理模式可以控制对象访问并附加增强逻辑;行为型设计模式有用于封装不同算法并实现快速切换的策略模式,还有观察者模式,可以实现对象之间一对多的依赖通知。
- 我在项目中也使用过几种常见的设计模式,实现了解耦、复用、扩展等功能。项目的全局配置管理和Redis连接池、线程池等我会使用单例模式实现,保证类的实例全局唯一;在通知的场景中我利用观察者模式实现状态变更时自动通知订阅者,解耦业务触发方和处理方,订阅者增加时也不用更改业务代码;我还利用装饰者模式实现了对接口的同一增强处理,项目中的日志模块、参数校验和异常处理等都使用了装饰者模式进行增强,在不修改原有逻辑的基础上动态地为接口添加功能,扩展性很好。
# 实现一个线程池,有哪些要点和方案?

- 创建线程池时首先需要考虑线程池参数的合理性,需要关注的参数有核心线程数、最大线程数、阻塞队列大小。
- 需要根据任务类型设置合理的核心线程数,如果是CPU密集型任务核心线程数可以设置为CPU的核心数,充分利用CPU资源;如果是I/O密集型任务,线程大部分时间在等待I/O操作,可以设置大一点的核心线程数,如CPU核心数的2倍;考虑系统的资源限制设置最大线程数,如果过大可能会耗尽系统资源;需要根据场景设置合理的阻塞队列大小,避免队列容量过小任务被拒绝或者过大导致任务等待时间过长;根据实际情况的需求判断任务是否有优先级,可以使用PriorityBlockingQueue作为阻塞队列,实现任务的优先级排序。
- 在编写线程池代码时也需要确保线程池在正确的时机启动和关闭,可以使用shutdown或shutdownNow方法关闭线程池;shutdown会等待执行中的任务完成再关闭线程池,shutdownNow则会尝试中断正在执行的任务,关闭线程池的同时返回尚未执行的任务列表;向线程池提交任务时需要在提交前对任务进行去重,或者在任务本身中增设标志位标识完成情况,避免网络不稳定时浪费线程池资源;如果任务涉及共享资源需要采取同步措施,防止线程池并发执行任务时出现数据不一致或者资源竞争的情况。
# 设计一个评论系统,要支持高并发写入、分页查询、热评展示,还要考虑防刷。
- 系统的总体架构可以使用SpringBoot框架,Kafka做消息队列,MySQL做数据库存储,Redis做缓存。
- 实现高并发写入,依靠异步写入、分库分表、缓存实现。客户端提交的评论先写入Kafka消息队列,由后台消费者异步处理,避免高并发写入MySQL;同时给MySQL存储层进行分库分表分散单表数据量和写入压力,解决性能瓶颈;新评论可以优先写入Redis缓存,保证前台可以获取最新评论,兼顾性能与安全。
- 实现分页查询可以使用游标分页,比如使用时间戳+ID的分页方案,避免OFFSET深分页导致的全表扫描问题,保证分页查询效率稳定;
- 热评展示可以使用Redis的Sorted Set数据结构,定义热评计算规则后将score作为排序依据,每次查询时根据score排序返回TopN条数据,实现实时更新的热评展示。
- 设计防刷机制时可以使用滑动窗口算法,利用Redis和Lua脚本实现用户ID加IP的滑动窗口限流,限制单个用户/IP在固定时间窗口内的评论次数,避免恶意用户或刷评论行为;还可以设计验证码校验,或者设计异常行为分析,标记风险用户并进行拦截。
# 场景题:100G的数据,怎么找出Top10?
- 我认为应该先使用分治思想,对100G大文件进行哈希分片切割,确保相同数据一定在同一个小文件中,并将每个小文件大小控制在内存可处理范围内;然后遍历每一个小文件,使用HashMap进行频次统计,可以为每个小文件创建一个大小等于10的小顶堆,遍历哈希表的频次结果后保留当前文件前十的高频数据,压缩数据量;最后将每个小文件的Top10数据合并,创建一个大小为10的小顶堆并重新筛选,得到最终结果。
评论
验证登录状态...