复盘面经(4):阿里巴巴高德出行java/go实习二面 @byebyeneu

作者:byebyeneu

链接:https://www.nowcoder.com/feed/main/detail/e27819c6f45a42a2beeda9483de4f582

来源:牛客网

1、自我介绍

2、MySQL用自增id的原因有什么?

我觉得MySQL用自增id主要有两方面,一是给数据库中的每一行数据加一个唯一标识,这样在查询的时候就可以快速的找到目标数据。即使我们不使用自增id,MySQL也会在底层给数据库表中的每一行内置一个唯一id,用来当作唯一标识。自增id一般都用来做主键,可以给它加一个索引,这样根据id查询的时候效率更高,MySQL使用B+树做索引,它的查询时间复杂度是O(logn)。

  • 综合标准答案:

我觉得在MySQL中使用自增ID主要有二个原因

  1. 数据库唯一标识明确区分:自增ID可以为每一行数据提供一个唯一的、明确的标识。方便在数据库中准确地定位和操作特定的数据行。避免重复:确保在插入新数据时不会出现ID重复的情况,保证数据的完整性。
  2. 提高查询效率索引优化:自增ID通常作为主键,数据库可以对其进行高效的索引,使得基于ID的查询、排序等操作速度更快。连续存储:数据在存储时可能会因为自增ID的连续性而更倾向于连续存储,减少磁盘寻道时间,提高读取性能。
  • 可能延申的问题数据库索引底层数据结构

3、MVCC是用来解决什么问题的?

我的答案:解决mysql多个事务并发执行过程中出现的 脏读、不可重复读、幻读问题。

MVCC(Multi-Version Concurrency Control,多版本并发控制)主要用于解决数据库并发操作时的一系列问题,以提高数据库的并发性能和数据一致性,下面详细介绍它所解决的问题:

1. 读写操作冲突问题

  • 读写阻塞问题:在传统的锁机制中,读操作和写操作可能会相互阻塞。例如,当一个事务对某条数据进行写操作时,会对该数据加锁,此时其他事务的读操作需要等待写操作完成并释放锁后才能进行;反之,当有读操作持有锁时,写操作也需要等待。这会导致数据库的并发性能下降。
  • MVCC的解决方式:MVCC通过为数据保存多个版本,使得读操作可以读取旧版本的数据,而无需等待写操作完成。写操作也可以在不影响读操作的情况下进行,实现了读写操作的并发执行,提高了数据库的并发性能。例如,在MySQL的InnoDB存储引擎中,普通的SELECT语句采用快照读,读取的是数据的一个快照版本,不会被正在进行的写操作阻塞。

2. 数据一致性问题

  • 脏读问题:一个事务读取到另一个未提交事务修改的数据。如果未提交事务最终回滚,那么读取到的数据就是无效的,这会破坏数据的一致性。
  • 不可重复读问题:在同一个事务中,多次读取同一数据得到不同的结果。这通常是因为在两次读取之间,有其他事务对该数据进行了修改并提交。
  • 幻读问题:在一个事务中,按照某个条件查询数据,在多次查询过程中,由于其他事务插入或删除了符合该条件的数据,导致查询结果的数量发生变化,就像出现了“幻觉”一样。
  • MVCC的解决方式:不同的事务隔离级别下,MVCC通过不同的策略来解决这些问题。例如,在“读已提交(READ COMMITTED)”隔离级别下,MVCC避免了脏读问题,因为每次读操作都会获取最新已提交的数据版本;在“可重复读(REPEATABLE READ)”隔离级别下,MVCC解决了不可重复读问题,因为在同一个事务中,所有读操作都使用事务开始时生成的快照,保证了多次读取结果的一致性;在MySQL的InnoDB存储引擎中,“可重复读”隔离级别还通过特殊的锁机制(如间隙锁和临键锁)结合MVCC解决了幻读问题。

3. 并发性能问题

  • 锁机制的局限性:单纯使用锁机制来控制并发会导致大量的锁竞争,使得事务需要频繁地等待锁的释放,从而降低了数据库的并发处理能力。尤其是在高并发场景下,锁机制可能会成为性能瓶颈。
  • MVCC的解决方式:MVCC减少了锁的使用,通过版本控制的方式允许事务在不相互阻塞的情况下并发执行。读操作不需要获取锁就可以读取数据的旧版本,写操作也可以在不影响其他读操作的情况下进行,从而提高了数据库的并发性能,使得数据库能够处理更多的并发事务。

4、MVCC底层是怎么实现的?

这就要提到MVCC,多版本并发控制。它主要根据快照中的四个字段以及每一行记录的两个隐藏列来实现的。

快照里面有四个字段,第一个字段是创建这个快照的事物ID。第二个字段是创建快照时整个系统中所有活跃且未提交的事物ID的集合。第三个字段是第二个字段里面的所有活跃未提交的事物ID集合中最小的事物ID,第四个字段是创建快照时全局事物的最大事物ID+1,也就是未来创建下一个事物的ID。

在说一下每个记录中的两个隐藏列,第一个列是当前记录最近一次修改的事物ID,第二个隐藏列是一个指针,指向了这个记录的所有的该条记录的所有旧版本记录。我们知道对某一行数据进行修改时,会将原数据加入到undo log日志中,这一行数据的所有旧版本记录形成了一个链表,每一个节点都指向它前一个节点的记录。所以我们可以通过这个去找到它的所有之前的记录。回滚的时候也会用到这个。

有了这些信息之后,就可以知道哪些数据可见哪些不可见了。

比如现在要查询一条记录,首先查看这个条记录隐藏列中的事物ID字段,通过这个事物ID来判断可不可见,怎么判断的呢?

  1. 如果这个记录的事物ID小于快照中的第三个字段,也就是活跃未提交的最小事物ID,如果小于的话证明这一行记录是事物开启前就已经提交了的数据,所以是可见的。
  2. 如果这个记录的事物ID大于等于快照中的第四个字段,也就是全局事物ID+1,那就证明这个记录是在当前事物开启后创建的,那就是不可见,所以就需要通过回滚指针去寻找之前的旧版本记录,去查询每个旧版本记录的事物ID对自己可不可见。一直找到可见的为止。
  3. 如果这个记录的事物ID大于等于第三个字段但小于第四个字段,说明这个记录很可能是还未提交的事物修改的。所以需要检查这个事物ID是否在第二个字段的活跃未提交集合当中,如果在证明不可见,就需要通过指针找可见的记录。如果不在,证明创建当前事物的时候已经提交了,那么就是可见的。

所以通过这些信息就能判断哪些记录可见哪些不可见,这就是MVCC,多版本并发控制。

5、如何用redis实现一个分布式锁,需要注意什么

执行一条命令:set lock ex,忘了命令咋写的了,这条命令的意思就是 当这个key不存在时,就去加入这条数据,如果存在了,就不加入数据。这样就实现了分布式锁。还要给这个数据加一个存活时间,防止执行这条命令成功的线程没有释放锁。

需要注意:

  1. 一定要有存活时间,不然锁可能一直因为阻塞不被释放
  2. 释放锁的时候要判断锁是不是自己的,如果获取锁的线程A阻塞了,锁过了存活时间已经失效了,另外一个线程B获取到了这把锁,此时线程A又恢复了,他主动把锁释放了,但此时这把锁是线程B,碰巧被释放后,线程B的锁就没了,那么他执行的时候就不安全了,因为锁还可以被其他线程获取,所以释放锁的时候还需要判断锁是不是自己的。还需要注意的地方就是,判断锁和释放锁是两个步骤,这俩不是原子性的,如果线程A判断锁是自己的,然后又阻塞了,锁又自动失效了,其他线程又获得锁了,线程A又复活了,它又把别人的锁给释放了,所以要保证判断锁和释放锁这两个操作是原子性的,怎么实现呢,可以使用lua脚本,在lua脚本中编写判断锁和释放锁的逻辑,直接调用这个脚本,它的执行是原子性的,要么成功要么失败。这样就保证了不会释放别人的锁。

6、redis是一个kv数据库,redis底层如何实现快速根据key查出value的

redis底层有一个Hash表,这个表的key是所有数据类型的key,value是它的数据结构的指针。无论存放的是String、Hash表、List、Set数据结构,都是先通过最开始的hash表去查询对应的对象实例。它的快速就在于这个Hash表。

既然要说Hash表,就要说它的底层结构,以及get、put、扩容等操作。

它的底层是数组+链表。首先要执行get方法的时候,首先会将这个key经过哈希函数得到一个哈希值,再将这个哈希值得到它在数组中的下标,知道了下标之后,就去获取数组中或者下标的元素,如果发现位null,返回null,如果发现有数据,就去检查是否是当前要查询的key,如果不同,就通过第一个节点的后继指针去遍历整个链表,直到找到整个key为止。为什么它是一个链表呢,因为可能会又多个key激素按得到的下标是相同的,那么就会出现哈希冲突,为了解决哈希冲突,使用了链表,将所有下标相同的键值对通过指针连接起来,以此来解决哈希冲突。如果整个链表元素多了,查询的时间复杂度就变成O(n)了,所以为了解决整个问题,需要进行扩容。

7.redis的数据扩容方式是怎么样的

这里说的应该是哈希表的扩容吧。它的扩容是渐进式扩容,先来说说一般的扩容。

实际上,对于每个hash对象都定义了两个哈希表,这样做的目的就是为了扩容的。如果发现现在使用的其中一个哈希表需要扩容,那么它就会将另一个哈希表的大小设置为当前哈希表的两倍,然后将原哈希表中的数据都迁移到另一个哈希表中,迁移完成后,原哈希表中的数据都释放,将新的哈希表与原哈希表角色互换,在新的哈希表上创建一个新的哈希表,方便下次扩容。

一般的扩容有一个问题,就是迁移数据到另一个哈希表所花费的时间太长,影响性能,如果在迁移的过程有其他请求想要获取或者新增数据,都会因为迁移受影响。所以为了解决整个问题提出了渐进式扩容。渐进式扩容不是一次性将所有数据都迁移,而是当有新增、删除、查找、更新命令要执行时,先将这些命令执行完成,然后将原哈希表中的该命令的下标上的所有元素都迁移到新哈希表中,这样就实现了渐进式扩容。随着这些增删改查的请求越来越多,那么原哈希表中的数据会越来越少,数据都迁移到新哈希表中了,这样就完成了扩容操作。

8.为什么redis扩容采用的是渐进式扩容,而不是java下的hashmap扩容方式

我觉得是redis要保证高性能,我们都知道redis的性能很高,查询等操作效率很快。所以为了提升性能,使用了渐进式扩容。而Java里面的hashmap不需要关心性能。

Redis扩容采用渐进式扩容,而非像 Java 中 HashMap 那样一次性扩容,主要和两者的应用场景、数据规模以及设计目标等因素相关,以下为你详细分析:

数据规模和并发读写

  • Redis:通常应用于处理大规模数据和高并发的读写请求场景。如果采用类似 Java HashMap 一次性扩容的方式,在数据量非常大时,一次性将所有数据从旧哈希表迁移到新哈希表会带来大量的计算和内存操作,会导致 Redis 服务在扩容期间出现明显的卡顿,影响服务的可用性和响应性能,这对于高并发的生产环境是不可接受的。
  • Java **HashMap**:主要在单个 JVM 进程内使用,处理的数据规模相对较小。而且其使用场景大多是在普通的业务代码中,即使出现短暂的卡顿,对整个系统的影响也相对较小。因此,HashMap 可以在扩容时一次性完成数据迁移,以保证后续操作的高效性。

单线程模型

  • Redis:采用单线程模型处理客户端的请求,这意味着在同一时间只能执行一个操作。如果在扩容时进行一次性数据迁移,会阻塞其他客户端的请求,导致服务不可用。而渐进式扩容将数据迁移操作分散到多个请求处理过程中,在处理客户端请求的间隙逐步完成数据迁移,避免了对正常业务的影响,保证了 Redis 服务的高并发处理能力。
  • Java **HashMap**:运行在多线程的 Java 环境中,虽然在进行扩容时会有一定的性能开销,但可以通过加锁等机制来控制并发访问,避免数据不一致的问题。而且 Java 程序可以利用多线程的优势,在后台线程中进行扩容操作,减少对主线程的影响。

内存使用和碎片问题

  • Redis:对内存的使用非常敏感,需要尽可能减少内存碎片和内存占用。渐进式扩容可以在扩容过程中更灵活地管理内存,根据实际的数据迁移情况逐步释放旧哈希表的内存,避免一次性分配大量连续内存导致的内存碎片问题。
  • Java **HashMap**:在 JVM 的内存管理机制下,内存分配和回收由 JVM 自动处理。虽然一次性扩容可能会导致内存使用的短暂波动,但 JVM 的垃圾回收机制可以在一定程度上缓解内存碎片问题,因此不需要像 Redis 那样考虑复杂的内存管理策略。

持久化和数据一致性

  • Redis:支持多种持久化方式(如 RDB 和 AOF),在扩容过程中需要保证数据的一致性和持久化的正确性。渐进式扩容可以更好地与持久化机制配合,确保在数据迁移过程中不会影响持久化文件的完整性。例如,在 AOF 持久化模式下,渐进式扩容可以保证写操作日志的顺序性和一致性。
  • Java **HashMap**:通常不涉及持久化的问题,主要关注内存中的数据操作。因此,在扩容时不需要考虑与持久化机制的协同问题。

9、你讲讲写代码可能出现oom的例子,你又是怎么解决的

读取数据时没有分页,导致读取了很多数据到JVM中,有可能导致内存溢出。

使用while循环,循环中创建了对象,while执行了很多次,导致内存溢出。

使用递归调用,没有编写退出条件,导致一直递归,就会内存溢出。

在 Java 代码中,有多种情况可能会导致 OOM(Out of Memory)错误,以下为你详细介绍不同类型的 OOM 示例:

堆内存溢出(java.lang.OutOfMemoryError: Java heap space)

示例代码

import java.util.ArrayList;
import java.util.List;

public class HeapOOM {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            // 每次向列表中添加一个 1MB 大小的字节数组
            list.add(new byte[1024 * 1024]);
        }
    }
}

原因分析

程序不断创建新的字节数组并添加到列表中,随着时间推移,堆内存被不断占用。当堆内存达到 JVM 所分配的最大堆内存限制时,就会抛出 java.lang.OutOfMemoryError: Java heap space 异常。

永久代/元空间溢出(java.lang.OutOfMemoryError: PermGen space 或 java.lang.OutOfMemoryError: Metaspace)

示例代码(Java 8 及以后元空间溢出)

import java.lang.reflect.Proxy;

public class MetaspaceOOM {
    public static void main(String[] args) {
        while (true) {
            // 不断创建动态代理类
            Proxy.newProxyInstance(MetaspaceOOM.class.getClassLoader(),
                    new Class<?>[]{Runnable.class},
                    (proxy, method, args1) -> null);
        }
    }
}

原因分析

在 Java 8 之前,永久代用于存储类的元数据信息;Java 8 及以后,元空间替代了永久代。上述代码不断创建动态代理类,会生成大量的类元数据,当元空间达到其最大限制时,就会抛出 java.lang.OutOfMemoryError: Metaspace 异常。

栈溢出(java.lang.StackOverflowError)

示例代码

public class StackOverflow {
    public static void recursiveMethod() {
        // 递归调用自身,没有终止条件
        recursiveMethod();
    }

    public static void main(String[] args) {
        recursiveMethod();
    }
}

原因分析

每个 Java 线程都有自己的栈空间,用于存储方法调用的栈帧。当方法递归调用时,每一次调用都会在栈上创建一个新的栈帧。如果递归没有终止条件,栈帧会不断增加,最终导致栈空间耗尽,抛出 java.lang.StackOverflowError 异常。

直接内存溢出(java.lang.OutOfMemoryError: Direct buffer memory)

示例代码

import java.nio.ByteBuffer;

public class DirectMemoryOOM {
    public static void main(String[] args) {
        while (true) {
            // 不断分配直接内存
            ByteBuffer.allocateDirect(1024 * 1024);
        }
    }
}

原因分析

Java 通过 ByteBuffer.allocateDirect() 方法可以分配直接内存,直接内存不受 JVM 堆内存的限制,但受操作系统物理内存和 JVM 参数 -XX:MaxDirectMemorySize 的限制。当不断分配直接内存,且超过其最大限制时,就会抛出 java.lang.OutOfMemoryError: Direct buffer memory 异常。

线程耗尽导致的 OOM

示例代码

public class ThreadOOM {
    public static void main(String[] args) {
        while (true) {
            new Thread(() -> {
                try {
                    Thread.sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

原因分析

每个线程都需要一定的内存来维护其栈空间和其他线程相关的数据。当不断创建新线程,且系统资源无法满足更多线程的内存需求时,就会抛出 OOM 异常。

全部评论
mark
点赞 回复 分享
发布于 03-12 05:38 广东
接好运
点赞 回复 分享
发布于 02-14 23:39 陕西

相关推荐

评论
6
34
分享

创作者周榜

更多
牛客网
牛客企业服务