字节跳动后端二面

📍1. 数据库的事务性质,InnoDB是如何实现的?

数据库事务具有ACID特性,即原子性、一致性、隔离性和持久性。InnoDB通过以下机制实现这些特性:

🚀 实现细节:

  • 原子性:通过undo log实现事务回滚。
  • 一致性:通过事务的ACID属性和数据库约束保证。
  • 隔离性:使用锁和MVCC(多版本并发控制)实现不同隔离级别。
  • 持久性:利用redo log确保数据在系统崩溃后能够恢复。

🔧 MySQL事务示例:

sql 复制代码
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

📍2. MySQL中数据的存储结构?

MySQL中的数据存储结构涉及表空间、段、区、页和行。InnoDB使用B+树结构存储数据和索引,聚簇索引将数据和主键索引存储在一起。

🚀 存储层次:

  • 表空间:逻辑存储单元。
  • :表空间内的逻辑部分,如数据段、索引段。
  • :由连续的页组成。
  • :最小存储单位,通常16KB。
  • :实际数据记录。

🔧 InnoDB数据存储示例:

sql 复制代码
SHOW TABLE STATUS LIKE 'table_name';

📍3. MySQL的主从复制原理以及主从延迟的解决方案?

MySQL主从复制通过binlog和中继日志实现数据同步。主从延迟可通过以下方法解决:

🚀 复制原理:

  • 主库记录binlog。
  • 从库I/O线程获取binlog并写入中继日志。
  • SQL线程执行中继日志中的SQL。

🚀 延迟解决方案:

  • 优化网络和硬件。
  • 并行复制。
  • 减少主库负载。
  • 使用半同步复制。

🔧 主从配置示例:

ini 复制代码
[mysqld]
log-bin=mysql-bin
server-id=1

📍4. Kafka怎么保证消息不丢、重复发了怎么办?

Kafka通过生产者、broker和消费者的协调保证消息不丢失。重复消息通过幂等性和去重机制处理。

🚀 消息不丢:

  • 生产者:设置acks=all。
  • Broker:使用持久化和min.insync.replicas。
  • 消费者:手动提交偏移量。

🚀 重复消息处理:

  • 幂等性生产者。
  • 消息去重。

🔧 Kafka生产者配置示例:

properties 复制代码
acks=all
retries=3

📍5. 你的项目中,接口调用如何保证幂等?

接口幂等性通过唯一标识符、乐观锁、分布式锁、状态机和Token机制实现,确保重复请求产生相同结果。

🚀 幂等实现:

  • 唯一标识符:使用UUID和数据库唯一索引。
  • 乐观锁:版本号控制。
  • 分布式锁:Redisson或ZooKeeper。

🔧 幂等性示例代码:

java 复制代码
// 使用UUID生成唯一标识符
UUID uuid = UUID.randomUUID();

📍6. 你的项目中,如何保证分布式事务的一致性?

分布式事务一致性可通过两阶段提交、补偿事务、基于消息的最终一致性、最大努力通知和Saga模式实现。

🚀 一致性策略:

  • 两阶段提交(2PC):XA协议。
  • 补偿事务(TCC):Try-Confirm-Cancel。
  • 基于消息的最终一致性:消息队列。

🔧 Seata分布式事务示例:

xml 复制代码
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>1.4.2</version>
</dependency>

📍7. 项目中的限流怎么做的,为什么这么做?

限流通过固定窗口、滑动窗口、令牌桶和漏桶算法实现,保护系统稳定性、防止资源耗尽和恶意攻击。

🚀 限流算法:

  • 固定窗口:Guava RateLimiter。
  • 滑动窗口:Redis滑动窗口。
  • 令牌桶:令牌生成和消耗。

🚀 限流原因:

  • 保护系统稳定性。
  • 防止资源耗尽。

🔧 Guava RateLimiter示例:

java 复制代码
RateLimiter rateLimiter = RateLimiter.create(10.0); // 每秒10个请求
rateLimiter.acquire(); // 获取令牌

📍8. 如何设计群消息已读?

群消息已读通过存储已读状态、消息发送接收同步、已读列表展示和批量更新实现,优化使用分页加载和缓存。

🚀 设计思路:

  • 已读状态存储:数据库或缓存。
  • 消息发送和接收:更新已读状态。
  • 已读列表展示:展示已读成员。

🔧 已读状态更新示例:

java 复制代码
// 更新消息已读状态
updateMessageReadStatus(userId, messageId, true);

📍9. 多线程题目:10个线程模拟赛马,所有马就绪后才能开跑,所有马到达终点后裁判宣布赛马成绩。

问题描述:

使用多线程模拟赛马比赛,要求所有马(线程)都准备好后才能开始比赛,所有马到达终点后裁判宣布比赛结果。

解题思路:

  • 使用sync.WaitGroup来同步多个线程。
  • 使用sync.Mutex来保护共享资源的访问。
  • 每个马(线程)在准备好后通知主线程,主线程在所有马都准备好后发出开始信号。
  • 所有马到达终点后,裁判宣布比赛结果。

代码实现(Golang):

go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

type Horse struct {
	ID     int
	Ready  bool
	Finish bool
	mu     sync.Mutex
}

func (h *Horse) run(start, finish *sync.WaitGroup) {
	defer finish.Done()

	// 马准备好
	h.mu.Lock()
	h.Ready = true
	h.mu.Unlock()
	fmt.Printf("Horse %d is ready!\n", h.ID)

	// 等待所有马准备好
	start.Wait()

	// 模拟赛跑
	time.Sleep(time.Duration(h.ID) * time.Second)
	h.mu.Lock()
	h.Finish = true
	h.mu.Unlock()
	fmt.Printf("Horse %d has finished!\n", h.ID)
}

func main() {
	const numHorses = 10
	var start, finish sync.WaitGroup
	horses := make([]*Horse, numHorses)

	// 初始化马
	for i := 0; i < numHorses; i++ {
		horses[i] = &Horse{ID: i + 1}
	}

	// 设置WaitGroup
	start.Add(1)
	finish.Add(numHorses)

	// 启动赛马线程
	for _, horse := range horses {
		go horse.run(&start, &finish)
	}

	// 等待所有马准备好
	for {
		allReady := true
		for _, horse := range horses {
			horse.mu.Lock()
			if !horse.Ready {
				allReady = false
			}
			horse.mu.Unlock()
		}
		if allReady {
			break
		}
		time.Sleep(100 * time.Millisecond)
	}

	// 所有马准备好,开始比赛
	fmt.Println("All horses are ready! Start racing!")
	start.Done()

	// 等待所有马到达终点
	finish.Wait()
	fmt.Println("All horses have finished! Race is over!")
}

代码解析:

  • Horse结构体:包含马的ID、准备状态、完成状态和一个互斥锁。
  • run函数:每个马(线程)的执行函数,模拟马的准备、等待开始信号、赛跑和到达终点。
  • 主函数:初始化马、设置WaitGroup、启动线程、等待所有马准备好、发出开始信号、等待所有马到达终点并宣布比赛结果。

📍10. LeetCode 394,给定一个经过编码的字符串,返回它解码后的字符串。

问题描述:

给定一个编码字符串,格式为k[encoded_string],其中k是一个正整数,encoded_string是一个字符串。要求解码这个字符串,返回解码后的结果。

解题思路:

  • 使用栈来处理嵌套的编码字符串。
  • 遍历字符串,遇到数字、字母、[]时分别处理。
  • 遇到数字时,解析完整的数字并压入数字栈。
  • 遇到[时,将当前的字符串压入字符串栈,并重置当前字符串。
  • 遇到]时,弹出数字栈和字符串栈,将当前字符串重复相应次数后与弹出的字符串拼接。
  • 遇到字母时,直接拼接到当前字符串。

代码实现(Golang):

go 复制代码
package main

import (
	"fmt"
	"strconv"
	"strings"
)

func decodeString(s string) string {
	var numStack []int
	var strStack []string
	var currentNum int
	var currentStr strings.Builder

	for i := 0; i < len(s); i++ {
		char := s[i]

		switch {
		case char >= '0' && char <= '9':
			// 解析数字
			num, _ := strconv.Atoi(string(char))
			currentNum = currentNum*10 + num
		case char == '[':
			// 将当前数字和字符串压入栈
			numStack = append(numStack, currentNum)
			strStack = append(strStack, currentStr.String())
			// 重置当前数字和字符串
			currentNum = 0
			currentStr.Reset()
		case char == ']':
			// 弹出数字和字符串
			num := numStack[len(numStack)-1]
			numStack = numStack[:len(numStack)-1]
			prevStr := strStack[len(strStack)-1]
			strStack = strStack[:len(strStack)-1]
			// 重复当前字符串并拼接到前一个字符串
			currentStr.WriteString(strings.Repeat(currentStr.String(), num))
			currentStr = strings.Builder{}
			currentStr.WriteString(prevStr)
		default:
			// 字母直接拼接到当前字符串
			currentStr.WriteByte(char)
		}
	}

	return currentStr.String()
}

func main() {
	encoded := "3[a2[c]]"
	decoded := decodeString(encoded)
	fmt.Println(decoded) // 输出: "accaccacc"
}

代码解析:

  • numStack:用于存储数字的栈。
  • strStack:用于存储字符串的栈。
  • currentNum:当前解析的数字。
  • currentStr:当前解析的字符串。
  • 遍历字符串 :根据字符类型分别处理数字、[]和字母。
  • 解码过程:通过栈的压入和弹出操作,处理嵌套的编码字符串。

欢迎关注我的小红书一起来讨论。

相关推荐
blzlh7 小时前
春招面试万字整理,全程拷打,干货满满(3)
前端·javascript·面试
uhakadotcom8 小时前
Locust压力测试:轻松评估系统性能
后端·面试·github
uhakadotcom8 小时前
Airflow 入门指南:轻松管理工作流
后端·面试·github
池鱼ipou9 小时前
面试必看:深入浅出 JavaScript 事件循环与异步编程技巧
前端·javascript·面试
柯ran9 小时前
C++面试准备一(常考)
开发语言·c++·面试
uperficialyu9 小时前
2025年03月18日柯莱特(外包宁德)一面前端面试
前端·面试·职场和发展
huangkaihao11 小时前
像讲故事一样的STAR法则:从像素到视口的自适应之旅
前端·面试·程序员
紫色风铃11 小时前
图解版LIS,一篇文章教会你什么是最长递增子序列
前端·算法·面试
拉不动的猪11 小时前
刷刷题43 (Vue3 语法糖(<script setup>)与传统写法对比及示例)
前端·javascript·面试
Danta12 小时前
面试题大杂烩😘😘
前端·javascript·面试