攻克面试难关:学习他人失败的教训,向成功靠拢。

90 阅读13分钟

美团,帮大家吃的更好,生活更好。

一、项目开发过程中有遇到什么比较难的问题吗?

**不同维度:**怎么发现某个问题、怎么去解决的、如何查看解决后的效果

  • 需求理解

    日常开发流程基本上就是产品提出需求、然后技术进行开发,但是在这个过程中有那么几次都是没有理解清楚产品需求的意图导致开发做完之后不符合产品的预期。我刚毕业的时候,曾经有过这么几次的经验。当时我也很懊悔,明明我加班加点的去做,到了最后还是不满意。后来深度思考下这个过程中存在的一个点就是 **在还没有去理解清楚需求的本质的前提条件下,就进入开发模式,最终就会导致需求不符合题意。**在抓到问题的痛点后,后面的需求,每次沟通的时候都会把该需求的背景、目标、后期扩展等都跟产品理解清楚,然后在需求评审上根据现有后台服务能力综合评判出优劣势确定需求的可行性。如果自己把握不住的话,期间也会问大佬同事帮忙评估。这个之后就再也没有出现过类似的情况了。

  • 方案设计

    方案设计上,在刚开始进行方案设计的时候脑子总是经常性的短路,业务场景模型也抽象不出来。后来针对这种问题,去了解各个中间件提供的数据结构和分布式能力。然后在次基础上将业务模型按照输入-状态机-输出这个模型进行抽象,可能在输入和输出这里要按照限定的业务场景进行额外处理,但是总体上都是在该模型框架下进行方案设计,同时又根据场景限定条件、业务复杂性、缓存一致性、耦合性等相关条件决定出使用哪个第三方组件来确定整体方案框架。在这个基础的条件下,同时去参考同一个场景下的业界已经开源、成熟的方案进行参考。可以优先从chatgpt、google、内网上去搜索查看相关开源的方案进行参考。

  • 技术开发

    当需求评审、方案设计都已经确定OK之后,那就需要进行业务开发,而在业务开发这里,我也遇到过几个头疼的问题:1、业务不熟悉,导致需求开发时忙来忙去。2、线上Bug等。3、内存泄漏。以上三点都是我在日常开发过程中存在且已经发生过的问题。当出现问题后进行总结归纳,我发现这些问题其实都是可以去避免发生的,只要我们在前期做够工作的话。对于第一点,在业务不忙的时候去熟悉团队负责的各个服务模块,同时梳理该服务在我们团队的定位是什么,然后在去了解该服务的上游服务是哪些,以及该服务有用到哪些下游服务等,同时在去了解下该服务内核心实现逻辑和方案。在熟悉了所有核心服务模块之后,也就再也没有出现过类似的事情了。而对于线上的Bug这种情况,在日常开发中也确实出现过一些小的问题,对于大的需求反而不会出现问题,这个原因就是小的需求自测很随意,同时单元测试也不是很完善等。我在梳理这件事情之后,然后严格按照单元测试、功能自测、测试测试、线上灰度自测四个步骤来进行开发,基本上没有出现过Bug。

    有一个很重要的点就是功能测试没有问题,然后灰度,在线上抽样测试,发现服务内存暴涨,涨完之后也没有迭下去。之后再测试环境重新复现,用pprof观察内存树状图,发现某个SDK的TCP链接没有关闭导致内存暴涨。当修复后兵灰度正式环境发现内存降下去了。

二、算法:数组中最接近目标和的三数之和。

要求:时间复杂度O(n2)

// threeSumClosest 最接近的三数之和。
func threeSumClosest(nums []int, target int) int {
	if len(nums) < 3 {
		return 0
	}
	sort.Slice(nums, func(i, j int) bool {
		return nums[i] < nums[j]
	})
	result := 0
	mini := 1<<63 - 1
	for idx, num := range nums {
		var (
			left  = 0
			right = len(nums) - 1
		)
		// 判断退出条件。
		for left < right && left != idx && right != idx {
			curSum := nums[left] + nums[right] + num
			// sum 在target左侧,此时要求最接近的那么需要让下一个sum靠近target,也就是需要Left++。
			if curSum < target {
				if target-curSum < mini {
					result = curSum
					mini = target - curSum
				}
				left++
			} else {
				if curSum-target < mini {
					result = curSum
					mini = curSum - target
				}
				right--
			}
		}
	}
	return result
}

三、B树和B+树的区别。

B树和B+树是在数据库和文件系统重常用的两种数据结构,他们都是用来实现关联数组(数据库索引)的一种平衡结构。而在InnoDB中,B树和B+树在存储方面也是有很大的不同的,B树非叶子节点也会存储相关数据信息,不仅仅存储索引相关信息。相反,B+树只会在叶子节点存储数据信息,而非叶子节点只存储索引相关信息。而我们都知道,MySQL为了提升IO方面的性能,内部对操作系统的Page重新维护了一个Page,操作系统的Page为4K,而MySQL的一个Page为16K,所以这个在B树和B+树两种情况下会有明显的不同,对于B树,非叶子节点存储索引之外的信息回导致B树的整个树的高度要高于B+树。因为B+树只会在叶子节点存储数据信息,非叶子节点只存储索引相关信息,所以一个Page 16K,B树存储的索引信息会比B+树少很多,自然而然,B树的高度要比B+树的高度高。每高一个高度MySQL都要进行一次磁盘IO,树的高度越低,其进行磁盘IO的次数就越低,同理其性能就越高。

B+树比B树还有一个特性就是,B+树叶子节点会存储前后节点的指针,也就是在叶子节点维护一个双向链表,这样在进行范围查找时性能会比B树高很多。而B树在进行范围遍历时不仅仅需要遍历当前内部节点,还需要遍历当前节点关联的叶子节点来进行范围查找,这个要比B+树多进行几次IO,所以性能会比较低。

四、MySQL的事务隔离级别?

  1. 读未提交:存在脏读

    InnoDB如果设置该隔离级别的话,事务A能够读取到事务B还没有提交的数据,此时,当事务B在业务逻辑层发生某些问题时,要进行回滚,那么事务A读取到的数据就是脏数据。

  2. 读已提交:不可重复读

    InnoDB设置该隔离级别,事务A只能读取到事务B已经提交的事务,假设AB两个事务,A事务select * from t where id = 3之后,B事务新增update t set id = 333 where id = 3,之后B事务进行提交,之后A事务再次执行select * from t where id = 3则会找不到对应的数据,因为该隔离级别下能够读取到其他已经提交的事务。

  3. 可重复度:存在幻读。InnoDB默认的隔离机制,很有必要去理解其原理

    我们知道MySQL的可重复读是通过MVCC多版本并发控制机制来实现的,但是在可重复读的隔离级别下,其仍然会存在幻读的情况。

    在可重复读内,有两种类型的读,一种是快照读,一种是当前读。而MVCC机制就是利用的快照读,在该快照读的前提下可以避免幻读,但是在当前读的环境下是无法避免幻读的,我们举个例子:事务A:select * from t where id > 1 and id < 10 for update,在该模式下是用的快照读,同时另外一个事务:insert t value(3) ,然后进行commit,此时事务A在执行,则会查到id=3的行记录,此时则会存在幻读的情况,如果要避免则需要升级隔离级别为串行读隔离级别。

    **MVCC机制:**MySQL在每一行都会加一个默认的事务字段,记录该行最后一次修改的事务id,同时保存一个指针,指向undolog中该记录行的历史版本链。每一个事物执行的时候都会自动生成一个唯一的自增的ID用来表示当前事务ID,同时,每一个事务执行时如果是快照读只能看到小于当前事务ID的事务执行结果,否则就要去undolog历史版本连 中找到第一个小于该事务ID版本的数据行来进行操作。

  4. 串行读:完美解决,性能低

五、最左前缀匹配原则?

在了解最左前缀匹配规则外,我们还需要理解下什么是索引。我们都知道,索引就是一种方便用于快速查找某个数据的一个数据结构,这个在MySQL上也能体现出来,MySQL的存储就是在主键索引的B+树的数据结构上进行存储的。同时,对于索引其还可以创建联合索引,对于构建一个abc的联合索引来说,其在创建索引结构时也就是构建n叉树时,其会先通过a来进行索引构建,然后在通过b来进行索引构建,最后在通过ab之下通过c来构建索引树结构,在这个前提下,如果用户查询通过ab条件来查询,则其能够直接命中ab索引,如果其直接通过c作为条件来查询,那么其就相当于是全表扫描,根本没有用上我们创建的abc联合索引。

所以只要是联合索引其都能够利用最左前缀匹配原则来进行索引命中或者索引设计,其实主键也是可以设置成联合索引的。

六、MySQL锁相关?具体的锁在什么情况下才会上锁?

我们限定在可重复度隔离级别下。

我们重点说一下Record Lock,Gap Lock,Next-Key Lock三种维度的锁。Record Lock也就是行锁或者是排它锁,当我们进行执行SQL:update t set name = xx where id = 1的时候,会对id=1的行添加Record Lock行锁,此时如果当前事务未提交的,其他事务执行select * from t where id = 1 for update 的时候发现该行已经添加了Record Lock锁,则会等待A事务commit之后再查询数据。因为这个是排它锁。

我们在来说一说Gap Lock间隙锁,当我们执行select * from t where id >1 and id < 10 for update时,但是我们没有提交该事务,之后其他事务执行select * from t where id = 5 for update 时将会阻塞,因为其发现在1-10行记录内已经添加了间隙锁:(1,10),而我们要查询的是5,在该锁定范围内,同时我们又是当前读,需要对id=5的添加排它锁,所以会阻塞等待事务A执行且释放排它锁之后才能上锁。

Next-Key Lock锁是Record LockGap Lock的结合,当我们执行select * from t where id > 1 and id <= 10 for update时,其会将Gap Lock间隙锁升级为Next-Key Lock,加锁范围是:(1,10]。其是对1-10之间加间隙锁,针对10添加行锁。

七、Redis单线程还是多线程?

www.cnblogs.com/mumage/p/12…

Redis6.0之前Redis都是默认单线程的,而在Redis6.0其支持了多线程能力,但是默认是关闭的。

Redis在处理Socket网络数据包时都是串行读取,也就是所谓的单线程,但是在4.0之后其为了处理脏数据等也是通过子进程来进行处理,也就是多线程。Redis其是作用在内存上的,CPU并不是影响其吞吐量的核心成本问题,其吞吐量主要影响在网络传输上,所以6.0之前其一直没有支持多线程。而且单线程在内其内部的AeEvent事件机制上具有很高的读写性能,而且处理起来比较简单。

Redis6.0在执行多线程上的核心是利用多核心CPU去接受网络包和解析本次请求内容,其在执行具体命令时仍然是串行读取的。

八、Redis海量Key如何解决?

Redis的哈希结构在其底层实现上有两种实现,一种是ziplist,一种是hastable,一共是这两种方式,同时Redis其内部针对每一个String都会生成一个SDS对象,该SDS对象包含16个字节的len、free等,还有具体内容数据。而ziplist是将字符串内容通过紧凑内存的形式存放在一起。省略掉了多余的16个字节内容。所以具体优化上我们可以从这个方面考虑。

  1. 理解Redis哈希结构在什么情况下会使用ziplist,在什么情况下会使用hastable。

    默认情况下,Redis会在哈希表内存key-filed数量不超过512,并且value字节数不超过64的情况下会使用ziplist。相反则会使用hastable来实现。

  2. 针对每一个Key不适用String字符串类型,而是使用Number类型,Redis Number类型占用8个字节,比SDS要少占用很多内存。

  3. 业务内部按照预估数据量进行分桶,每个桶的用户数据量不超过512,同时为了避免哈希冲突,在额外多加几个Bucket,后续按照用户的唯一ID进行计算出其属于那个Bucket,然后在该Bucket内通过哈希结构来进行存储该用户的数据信息,记住,Value字段不能超过64个字节。

  4. 在整个过程中,唯一要解决的就是哈希冲突问题,如果不同的唯一标识符映射到同一个桶内,那么就会存在冲突问题,这里提供一种思路:唯一标识符通过算法A映射到不同的桶,然后用算法B映射到不同的哈希Key中。这个是一个思路,还有一个思路就是内层Key直接使用唯一标识符即可。