欢迎点击下方👇关注我,记得星标哟~
文末会有重磅福利赠送
今天的面经来自咱们训练营的一位超级机灵的小姐姐。她加入我们还不到一个月
的时间,凭借着自己的努力加上我们的助力,先是顺利通过了百度
的第一轮面试,紧接着又告诉我,阿里巴巴
那边的第一轮面试也过了,真是好事成双啊!
她以前底子不是特别好,虽然能约到面试,但是经常在一面就被刷下来
,这个真的是很多人的共性问题。
加入我们的训练营后,有了系统的指导和持续的监督,再加上她本人也积极上进
,各个方面都提升得很快。现在面试的情况,她自己是这么和我说的:”能和面试官聊起来,而且聊的挺开心“。
但是接下来还得继续加油,争取拿到更多的offer
,到时候选择权就在她手里了。
今天,我想跟大家分享的就是她在百度面试的经历
,面试的职位是在百度旗下的一款汽车资讯平台——有驾
。如果你对这个职位或者面试经验感兴趣,就请继续往下看看哈。
面试题目
跟她项目内容相关的问题没有具体答案
自我介绍 说一下你的项目? 说一下你们的微服务架构? 介绍一下项目内容中台? 项目中的权限系统的存储是如何设计的?
微服务拆分太细了有什么问题?
通信成本增加:
每个微服务之间通过网络进行通信,频繁的网络请求会增加延迟。 序列化和反序列化的开销也会增加,尤其是在使用JSON等文本格式时。
部署和维护复杂度增加:
更多的服务意味着更多的部署单元,需要更多的服务器资源和运维工作。 需要更多的监控和日志管理,以确保每个服务的健康状态。
服务间依赖关系复杂:
服务之间的依赖关系变得更加复杂,可能导致“服务雪崩”现象,即一个小服务的故障可能引发整个系统的瘫痪。 需要更多的协调和管理来确保各个服务之间的兼容性和一致性。
团队协作难度增加:
团队成员需要了解更多的服务和接口,增加了学习和沟通的成本。 协调多个团队的开发进度和发布计划变得更加困难。
项目中是怎么使用gRPC的?gRPC和HTTP+JSON比较有什么差异?
gRPC的使用:
服务定义:使用Protocol Buffers(protobuf)定义服务接口和消息格式。 客户端和服务端代码生成:通过protoc编译器生成客户端和服务端的代码。 高效通信:使用高效的二进制协议,减少传输数据的大小和解析时间。 流式传输:支持客户端流、服务器流和双向流,适用于需要大量数据交换的场景。
gRPC与HTTP+JSON的差异:
协议:gRPC使用HTTP/2协议,支持双向流式传输;HTTP+JSON使用HTTP/1.1协议,主要支持请求-响应模式。 数据格式:gRPC使用二进制格式,传输效率更高;HTTP+JSON使用文本格式,易于阅读和调试。 代码生成:gRPC可以自动生成客户端和服务端代码,减少开发工作量;HTTP+JSON需要手动编写请求和响应处理代码。 性能:gRPC在大数据传输和高并发场景下性能更好;HTTP+JSON在简单应用场景下更灵活。
项目中的权限系统存储是如何设计的?
用户表(Users):
存储用户基本信息,如用户名、密码、邮箱等。 示例字段: user_id
,username
,password
,email
,created_at
角色表(Roles):
存储角色信息,如角色名称和描述。 示例字段: role_id
,role_name
,description
权限表(Permissions):
存储具体的权限信息,如查看、编辑、删除等。 示例字段: permission_id
,permission_name
,description
用户角色关联表(User_Roles):
存储用户和角色的关联关系。 示例字段: user_id
,role_id
角色权限关联表(Role_Permissions):
存储角色和权限的关联关系。 示例字段: role_id
,permission_id
权限系统的缓存是缓存的什么内容,大小多少,什么数据格式?
缓存内容:
用户ID 用户的角色ID列表 用户的权限列表
缓存大小:
具体大小取决于用户数量和权限数量,但通常会设置合理的过期时间(如30分钟),以避免占用过多内存。
数据格式:
键值对形式,键通常为用户ID,值为包含角色ID和权限列表的JSON字符串。 示例: {
"user_id": "123",
"roles": ["admin", "user"],
"permissions": ["read", "write", "delete"]
}
缓存有大Key如何解决?
分片:
将大Key的数据拆分成多个小Key,每个小Key存储一部分数据。 例如,可以按角色ID分片,每个角色ID对应一个Key。
使用更高效的数据结构:
使用Redis的哈希表(Hash)来存储复杂的数据结构。 示例: HSET user:123 roles admin,user
HSET user:123 permissions read,write,delete
限制单个Key的大小:
在应用层限制单个Key的大小,超过一定阈值时进行分片存储。
使用批量提交进行优化,有没有丢失数据的可能?如何解决?
丢失数据的可能性:
网络中断:在批量提交过程中,如果网络中断,部分数据可能没有成功提交。 数据库错误:数据库在处理批量提交时可能出现错误,导致部分数据丢失。
解决方案:
事务管理:将批量提交放在一个事务中,确保所有操作要么全部成功,要么全部失败。 检查点机制:在每次批量提交前记录一个检查点,如果发生异常,可以从最后一个成功的检查点重新开始。 幂等性设计:设计幂等性的批量提交接口,即使多次提交同一批数据也不会产生副作用。 重试机制:在网络中断或其他临时错误时,自动重试批量提交操作。
如果评论表要分表怎么设计?
分表策略:
示例:按年份和月份分表,如 comments_2023_10
、comments_2023_11
。水平分表:按时间戳或评论所属的文章ID进行分表。 垂直分表:将不同类型的字段分开存储,如将评论内容和元数据分开存储。
分表后的管理:
路由机制:使用中间件或代理层自动路由请求到正确的表。 统一接口:对外提供统一的接口,内部根据规则路由到不同的表。 数据迁移:在分表后,需要将现有数据迁移到新的表结构中。
分完表后,上线新表怎么做?
准备阶段:
创建新的表结构,并确保所有的业务逻辑已经适配新的表结构。 进行充分的测试,确保新表结构的正确性和性能。
灰度发布:
选择一小部分用户或流量,逐步将请求路由到新表。 监控新表的表现,确保没有异常情况。
全面切换:
一旦确认新表稳定可靠,可以逐步增加路由到新表的流量比例。 最终完全切换到新表,关闭旧表的写入功能。
回滚机制:
准备好回滚方案,如果新表出现问题,可以迅速切换回旧表。
新表上线后应该读哪个表?
读取策略:
双写双读:在新表和旧表都写入数据,读取时优先从新表读取,如果新表中没有数据再从旧表读取。 渐进切换:一开始主要从旧表读取数据,随着新表数据的积累,逐渐增加从新表读取的比例。 最终切换:当新表的数据足够完整且稳定后,完全切换到新表读取。
监控和日志:
监控新表的读取性能和数据完整性。 记录读取日志,分析数据的一致性和完整性。
聊一聊数据库的索引?
索引的作用:
加速查询:通过索引可以快速定位到目标数据,减少全表扫描的时间。 唯一性约束:索引可以用于确保某个字段的值是唯一的。
索引类型:
B-Tree索引:最常见的索引类型,适用于范围查询和精确查询。 哈希索引:适用于精确查询,但不支持范围查询。 全文索引:用于全文搜索,支持复杂的文本匹配。 位图索引:适用于布尔值或枚举值的查询。
索引的代价:
存储开销:索引需要额外的存储空间。 写入性能:插入、更新和删除数据时需要维护索引,可能会影响写入性能。
索引优化:
选择合适的字段:为经常用于查询条件的字段创建索引。 避免过度索引:不必要的索引会增加存储和维护成本。 复合索引:为多个字段创建复合索引,提高查询效率。
索引的叶子节点存的什么?MySQL一页多大?
叶子节点的内容:
聚簇索引:叶子节点存储完整的行数据。 非聚簇索引:叶子节点存储指向聚簇索引的指针(通常是主键值)。
MySQL一页的大小:
默认情况下,MySQL的InnoDB存储引擎一页的大小是16KB。 可以通过配置文件中的 innodb_page_size
参数调整页大小,但通常不建议修改。
不使用自增ID作为主键,使用随机ID作为主键乱序插入有什么问题?
页分裂:
随机ID插入会导致页分裂,即当插入的数据不能放入现有的数据页时,需要将数据页拆分成两个页面。 页分裂会增加磁盘I/O操作,降低插入性能。
磁盘碎片:
随机插入会导致数据在磁盘上分布不均匀,形成磁盘碎片。 磁盘碎片会降低读取性能,因为连续读取变得困难。
索引树的高度:
随机ID插入会导致B-Tree索引树的高度增加,影响查询性能。 插入顺序ID时,索引树的高度相对较低,查询性能更好。
内存使用:
随机ID插入会增加缓冲池的碎片,影响内存使用效率。
了解页分裂吗?
页分裂的原因:
当向已满的数据页中插入新记录时,数据库引擎会将该页分成两个页面,以便腾出空间来存放新记录。 常见于随机插入和更新操作。
页分裂的影响:
性能下降:页分裂会增加磁盘I/O操作,降低插入和更新性能。 磁盘碎片:数据在磁盘上分布不均匀,影响读取性能。 索引碎片:索引树的结构变得不规则,影响查询性能。
避免页分裂的方法:
预分配空间:在创建表时预分配足够的空间,减少页分裂的发生。 顺序插入:尽量使用自增ID或其他有序的主键,减少随机插入。 定期优化:定期对表和索引进行优化,如使用 OPTIMIZE TABLE
命令。
聚簇索引和覆盖索引的问题
聚簇索引:
定义:聚簇索引是表数据的物理存储方式,决定了数据在磁盘上的排列顺序。 特点:每个表只能有一个聚簇索引,通常是主键索引。 优点:查询速度快,因为数据按索引顺序存储。 缺点:插入和更新性能较差,因为需要维护数据的物理顺序。
覆盖索引:
定义:覆盖索引是指一个非聚簇索引包含了查询所需要的所有列,这样查询可以直接通过索引完成,而不需要再回表查找其他信息。 特点:可以显著提高查询性能,减少磁盘I/O操作。 创建方法:在创建索引时,确保索引包含查询所需的全部列。
聊一聊事务?
事务的定义:
事务是一组数据库操作,这些操作要么全部成功执行,要么全部不执行,以保证数据的一致性和完整性。
ACID特性:
原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成。 一致性(Consistency):事务执行前后,数据库必须处于一致状态。 隔离性(Isolation):事务之间相互隔离,一个事务的执行不会受到其他事务的干扰。 持久性(Durability):事务一旦提交,其结果是永久的,即使系统发生故障也不会丢失。
事务的用途:
数据一致性:确保多个操作作为一个整体执行,避免部分操作成功而部分操作失败的情况。 并发控制:通过事务隔离级别,控制并发事务之间的相互影响,避免数据冲突。
事务如何实现的?
日志文件:
预写日志(WAL):在事务开始时,数据库会先记录所有即将发生的变更到日志文件中,然后再真正地修改数据。 日志文件的作用:用于恢复未完成的事务状态,确保事务的持久性。
事务日志:
Redo日志:记录事务对数据的修改操作,用于事务提交后的数据恢复。 Undo日志:记录事务前的数据状态,用于事务回滚和多版本并发控制(MVCC)。
事务管理:
开始事务:标记事务的开始,开始记录日志。 提交事务:将所有日志记录写入磁盘,确保事务的持久性。 回滚事务:撤销事务中的所有操作,恢复到事务开始前的状态。
Go的map是线程安全的吗?
标准map:
Go语言中的标准 map
不是线程安全的。如果多个goroutine同时读写同一个 map
,可能会导致程序崩溃。
线程安全的map:
使用互斥锁(Mutex):在并发访问 map
时,使用sync.Mutex
或sync.RWMutex
来同步访问。var mu sync.Mutex
var m = make(map[string]int)
func add(key string, value int) {
mu.Lock()
defer mu.Unlock()
m[key] = value
}
func get(key string) int {
mu.Lock()
defer mu.Unlock()
return m[key]
}**使用 sync.Map
**:sync.Map
是Go标准库提供的线程安全版本的map
。var m sync.Map
func add(key string, value int) {
m.Store(key, value)
}
func get(key string) int {
val, _ := m.Load(key)
return val.(int)
}
Go的slice扩容怎么做?
自动扩容:
当一个 slice
的空间不足时,Go运行时会自动为其分配更大的内存空间,并复制原有数据到新的位置。扩容时,通常会将容量扩大到原来的两倍左右。
扩容机制:
初始容量: slice
的初始容量通常是固定的,如16或32。扩容公式:当 slice
的长度达到容量上限时,新的容量通常是当前容量的两倍。newCap := oldCap + oldCap/2
if newCap > maxSliceCap(len) {
newCap = maxSliceCap(len)
}
性能优化:
预分配容量:在创建 slice
时预分配足够的容量,减少扩容次数。s := make([]int, 0, 1000) // 初始长度为0,容量为1000
手动扩容:在特定场景下,可以手动进行扩容操作,避免频繁的自动扩容。 if cap(s) < neededCapacity {
newS := make([]int, len(s), neededCapacity)
copy(newS, s)
s = newS
}
编程题:合并区间
题目描述: 给定一个区间的集合,合并所有重叠的区间。
解题思路:
排序:首先按照区间的起始位置进行排序。 遍历:遍历排序后的区间,对于每个新区间,如果它的起始位置小于或等于当前合并区间的结束位置,则说明这两个区间是重叠的,应该合并;否则,开始一个新的合并区间。
示例代码:
package main
import (
"fmt"
"sort"
)
// Interval 表示一个区间
type Interval struct {
Start int
End int
}
// mergeIntervals 合并区间
func mergeIntervals(intervals []Interval) []Interval {
iflen(intervals) == 0 {
return intervals
}
// 按照区间的起始位置排序
sort.Slice(intervals, func(i, j int) bool {
return intervals[i].Start < intervals[j].Start
})
var result []Interval
current := intervals[0]
for _, interval := range intervals[1:] {
if interval.Start <= current.End {
// 区间重叠,合并区间
current.End = max(current.End, interval.End)
} else {
// 区间不重叠,将当前合并区间加入结果集
result = append(result, current)
current = interval
}
}
// 添加最后一个合并区间
result = append(result, current)
return result
}
// max 返回两个整数中的较大值
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
intervals := []Interval{
{1, 3},
{2, 6},
{8, 10},
{15, 18},
}
merged := mergeIntervals(intervals)
fmt.Println("Merged Intervals:", merged)
}
输出:
Merged Intervals: [{1 6} {8 10} {15 18}]
早日上岸!
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。
点击下方文章,看看他们是怎么找到好工作的!