ITPub博客

首页 > 数据库 > 数据库开发技术 > 小米有点意思,选择不躺平,冲!

小米有点意思,选择不躺平,冲!

原创 数据库开发技术 作者:皇子谈技术 时间:2024-03-14 07:50:00 0 删除 编辑

免费学习网站:https://itgogogo.cn

大家好,我是皇子。

相信很多人都还记得雷军大佬 3 年前,开启的 100 亿豪赌,“这将是我人生中最后一次重大的创业项目”。

他为造车下足了血本,甚至“愿意押上人生全部的声誉”。

好消息是,随着小米集团宣布,首款智能汽车将在 3 月底正式上市,股价也是连续两日上涨,这场新能源汽车的战役算是正式拉开帷幕。

不得不说,造型挺好看的,重要的是C级高性能轿车,期待价钱也能感动人心。


因为相信小米公司的基本盘是稳固的,具备良好潜力的。有同学已经不想再躺平了,决定好好准备下,冲小米。

下面分享一位小米后端的面经,总共两轮技术面,面试内容除了后端的八股文,还有项目经验的介绍,我把相关的内容,帮大家做些整理。

考察的知识点:

  • 链表算法:合并两个有序链表
  • 分布式系统:分布式的优缺点、分布式与集群架构的区别、如何实现分布式锁

链表算法

想起面试的最大痛苦,莫过于每次都要重温算法了,但是既然选择不躺平,就得往前冲!放心,我只会挑些经典的,从简单到困难去进阶,重要的是算法思维。

❝

单链表解法💡tips:双指针

合并两个有序链表

❝

leetcode 原题:https://leetcode.cn/problems/merge-two-sorted-lists/description/

难度系数:简单

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

看完题目思考 60 秒...

不知道大家有没有思路,下面我们一步步看代码,看注释,去理解。

❝

代码是经过验证的,并且提供测试用例

package listnode

func MergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode {
// 创建一个虚拟头节点,便于处理边界情况
dummy := &ListNode{Val: 0, Next: nil} // 虚拟头节点
tail := dummy // 尾部指针,初始指向虚拟头节点

// 使用循环遍历两个链表直到至少有一个链表遍历完
for l1 != nil && l2 != nil {
// 比较当前节点的值,将值较小的节点链接到结果链表上
if l1.Val < l2.Val {
tail.Next = l1 // 连接l1节点
l1 = l1.Next // 移动l1指针到下一个节点
} else {
tail.Next = l2 // 连接l2节点
l2 = l2.Next // 移动l2指针到下一个节点
}
tail = tail.Next // 更新结果链表的尾部指针
}

// 当一个链表遍历完后,将另一个未遍历完的链表剩余部分接到结果链表后面
if l1 != nil {
tail.Next = l1 // 若l1还未遍历完,将其余部分接到结果链表
} else {
tail.Next = l2 // 若l2还未遍历完,将其余部分接到结果链表
}

// 返回新链表的实际头部(即虚拟头节点的下一个节点)
return dummy.Next
}


测试用例:有点长,本着可以方便大家直接拿去验证,还是附上去了。不需要的跳过,看下题。

package listnode

import (
"reflect"
"testing"
)

// 合并两个有序链表
func TestMergeTwoLists(t *testing.T) {
type args struct {
list1 *ListNode
list2 *ListNode
}
tests := []struct {
name string
args args
want *ListNode
}{
{
name: "",
args: args{
list1: &ListNode{
Val: 1,
Next: &ListNode{
Val: 2,
Next: nil,
},
},
list2: nil,
},
want: &ListNode{
Val: 1,
Next: &ListNode{
Val: 2,
Next: nil,
},
},
},
{
name: "",
args: args{
list1: &ListNode{
Val: 1,
Next: nil,
},
list2: &ListNode{
Val: 1,
Next: &ListNode{
Val: 2,
Next: nil,
},
},
},
want: &ListNode{
Val: 1,
Next: &ListNode{
Val: 1,
Next: &ListNode{
Val: 2,
Next: nil,
},
},
},
},
{
name: "",
args: args{
list1: &ListNode{
Val: 1,
Next: &ListNode{
Val: 3,
Next: &ListNode{
Val: 5,
Next: &ListNode{
Val: 7,
Next: nil,
},
},
},
},
list2: &ListNode{
Val: 2,
Next: &ListNode{
Val: 4,
Next: &ListNode{
Val: 6,
Next: &ListNode{
Val: 8,
Next: nil,
},
},
},
},
},
want: &ListNode{
Val: 1,
Next: &ListNode{
Val: 2,
Next: &ListNode{
Val: 3,
Next: &ListNode{
Val: 4,
Next: &ListNode{
Val: 5,
Next: &ListNode{
Val: 6,
Next: &ListNode{
Val: 7,
Next: &ListNode{
Val: 8,
Next: nil,
},
},
},
},
},
},
},
},
},
}
for _, tt := range tests {
// reflect.DeepEqual 和 遍历链表都可以判断结果与期望值是否一致
// 判断结果与期望值是否一致方法一
t.Run(tt.name, func(t *testing.T) {
if got := MergeTwoLists(tt.args.list1, tt.args.list2); !reflect.DeepEqual(got, tt.want) {
t.Errorf(">1: MergeTwoListsV1() = %v, want %v", got, tt.want)
}
})
}
}


分布式系统

分布式的优缺点

要理解分布式的优缺点,我们先看个例子:

有一个电商网站,在采用分布式架构后,商品展示、订单处理、支付服务、库存管理等都可以作为独立的服务运行在不同的服务器上。

这样,当某一服务面临高峰期压力时,只需单独扩容该服务即可;同时,如果某个服务器发生故障,只会影响对应服务,其他服务依然能够正常运行,保证了网站整体的可用性。

但同时,这也意味着在设计时必须考虑到商品库存状态在多个服务之间如何实时同步、订单状态在不同阶段如何跨服务一致等问题。


下面用更加从专业性的角度解析分布式的优缺点。

1、 可扩展性(Scalability):

  • 垂直扩展(Scale Up):当单一节点性能不足以满足需求时,分布式架构允许我们通过添加更多机器来水平扩展(Scale Out),而不是升级单个机器的硬件配置,从而使得系统能够处理更大的流量和数据量。

  • 伸缩灵活性:可以根据业务需求的变化动态增减服务器资源,避免过度投资或因资源不足导致服务质量下降。


2、 高可用性(High Availability):

  • 无单点故障(No Single Point of Failure):通过冗余和分布式服务,即使部分节点失效,整个系统仍能继续运行,增强了系统的稳定性和容错性。
  • 负载均衡(Load Balancing):通过负载均衡器可以将用户请求均匀分布到各个节点,减少单个节点的压力,提高系统整体性能。

3、 业务解耦(Decoupling):

  • 模块化:分布式架构鼓励将复杂的系统划分为多个独立的服务或微服务,每个服务专注于完成特定的业务功能,降低了系统之间的相互依赖性,便于开发和维护。
  • 可重用性:服务之间通过API接口交互,可以方便地复用已有的服务,促进代码重用和快速迭代。

4、 并发处理能力强:

  • 分布式处理:多个节点可以并行处理请求,提升了系统的并发处理能力和总体吞吐量。

5、 运维便利:

  • 独立部署:每个服务可以独立部署、更新和扩展,不影响其他服务的运行,提高了运维效率。

然而,分布式架构不是银弹,也存在一些显著的缺点:

1、 架构复杂性:

  • 设计复杂:需要考虑分布式事务、数据一致性、网络延迟、跨节点通信等问题,设计难度加大。
  • 测试复杂:由于服务间交互,测试覆盖需要模拟各种网络条件和异常情况,调试和集成测试更为复杂。

2、 通信成本:

  • 网络开销:分布式架构下,服务间的通信需要通过网络进行,增加了通信延迟和可能出现的不稳定因素。
  • 数据一致性:在分布式环境中实现强一致性要求较高的技术和算法支持,如分布式锁、分布式事务、最终一致性模型等。

3、 运维管理:

  • 监控和管理:需要更多的工具和策略来监控大量服务的状态和性能,跟踪日志和排查问题变得更为繁琐。
  • 安全性:更多的接入点可能导致安全风险增加,需要强化安全措施和策略。

4、 开发和团队协作:

  • 学习曲线陡峭:开发者需要熟悉分布式系统的设计模式、中间件、协议等,新成员融入团队的成本增加。
  • 服务依赖管理:服务间的依赖关系复杂,版本管理和依赖升级需谨慎规划和实施。

分布式与集群架构的区别

维度 分布式架构 集群架构
定义 将系统拆分成多个独立且协同工作的服务或组件,分别部署在不同的物理或虚拟节点上,通过网络通信协作 多台服务器或节点共同提供服务,它们运行相同或相似的应用程序,形成一个整体以实现高可用、负载均衡或性能提升
架构特性 模块化、松耦合、服务多样 同质化、高冗余、聚焦同一任务
扩展方式 水平扩展,增加服务节点 水平扩展,增加相同应用实例
容错性 单个服务失败不影响整体服务,可通过服务冗余和故障转移保障高可用 单点故障可能影响集群,但可通过冗余节点和负载均衡器实现故障转移
数据处理 数据分散在不同节点,需处理分布式数据一致性问题 数据可能共享或冗余,一致性问题相对简单
负载均衡 自然具备负载均衡属性,可根据服务负载动态分配请求 依赖负载均衡器进行请求分配,节点间负载相对均匀
系统复杂度 设计、实现和运维相对复杂,涉及分布式协调、事务处理等难题 实现相对简单,但高可用和负载均衡机制的设置和维护有一定复杂度
通信 节点间通信频繁,可能涉及复杂的消息传递和数据同步 节点间通信相对简单,主要关注节点内数据同步和状态保持
开发与部署 鼓励微服务化,独立开发、部署和升级,有利于敏捷开发 通常一次性部署,更新可能需要批量操作


分布式锁的实现方式

基于数据库实现

实现步骤:

  • 利用数据库的事务和唯一索引特性来实现锁的排他性。
  • 加锁时,插入一条具有唯一约束的记录,代表取得锁。
  • 解锁时,删除这条记录,表示释放锁。
  • 可能的变体包括使用 SELECT ... FOR UPDATE(悲观锁)阻止其他事务读取直到事务结束,或利用乐观锁(如版本号机制)检查在更新之前数据是否已被改变。


基于Redis实现

实现步骤:

  • 使用Redis的原子操作如SETNX(设置并判断是否存在)来获取锁,配合EXPIRE设置锁的有效时间,防止死锁。
  • 若要支持锁续期,可以采用Redlock算法(Redis作者提出的改进方案)来提高锁的安全性和可靠性。
  • 释放锁时,通过比较锁标识符来验证持有锁的身份,然后执行DEL命令删除键。


基于消息队列实现

实现步骤:

  • 通过向队列发送消息,只有一个消费者可以成功消费(如FIFO队列),从而间接实现锁的功能。
  • 锁释放通常是通过完成业务流程后显式确认或队列消息超时自动返回至队列头部。


推荐阅读

百度,别淘汰程序员,没人用 ping 了

AI 时代,一个程序员的觉悟

挺看好的一位阿里妹子,很上进

错过阿里,冲腾讯吧

原文作者:皇子谈技术;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2023-12-21

  • 博文量
    113
  • 访问量
    56524