【已offer】陕西某国企科技部实习生面经汇总 后端开发 springcloud

投递流程:

*****

2-12 投递简历

2-13 约面

2-14 半小时技术面拷打 1小时后确定通过

面试官迟到5分钟 全程要求视频面试

自我介绍

mysql的索引

回答时只回答了5种索引类型,还是太紧张了www

面试官您好,作为一名大学软件工程专业的学生,我在学习和实践中对 MySQL 索引有了较为深入的理解。下面我将从索引的概念、类型、原理、使用场景和注意事项几个方面进行介绍。

概念与作用

索引在 MySQL 里就像是书籍的目录,它是一种特殊的数据结构。在数据库里,数据通常存储在磁盘上,当我们执行查询操作时,如果没有索引,数据库系统就得逐行扫描数据,数据量一大,查询速度就会变得非常慢。而索引能够帮助数据库快速定位到我们需要的数据行,减少磁盘 I/O 次数,极大地提高查询效率。比如在学生管理系统里,如果要查询某个特定学号的学生信息,有了索引就能迅速找到对应记录。

索引类型

  • 普通索引:这是最基础的索引类型,没有什么特殊限制,主要作用就是加快数据的访问速度。我在做课程设计的小型电商系统时,对商品表的商品名称列创建了普通索引,这样在根据商品名称进行模糊查询时,速度提升很明显。创建语句大概是 CREATE INDEX idx_product_name ON products (product_name);
  • 唯一索引:它要求索引列的值必须唯一,但允许有空值。像在用户表中,用户的邮箱地址一般会创建唯一索引,保证每个用户的邮箱是唯一的。创建语句是 CREATE UNIQUE INDEX idx_user_email ON users (email);
  • 主键索引:它是一种特殊的唯一索引,不允许有空值。在设计数据库表时,主键通常用来唯一标识每一行数据,数据库会自动为主键创建索引。比如在订单表中,订单编号一般作为主键,创建表时指定主键的语句类似 CREATE TABLE orders (order_id INT PRIMARY KEY, ...);
  • 全文索引:主要用于对文本数据进行全文搜索。我在做一个简单的新闻系统时,对新闻内容列创建了全文索引,这样就能快速搜索包含特定关键词的新闻。创建语句是 CREATE FULLTEXT INDEX idx_news_content ON news (content);
  • 组合索引:是在多个列上创建的索引。比如在学生成绩表中,经常需要根据学生的班级和成绩进行查询,就可以创建组合索引。创建语句是 CREATE INDEX idx_class_score ON scores (class, score);

拓展:

索引原理

在 MySQL 中,最常用的索引数据结构是 B - Tree 及其变种 B+Tree。B - Tree 是一种平衡树,每个节点可以包含多个键值和指针。根节点和内部节点存储键值和指向子节点的指针,叶子节点存储实际的数据或指向数据的指针。这种结构使得树的高度相对较低,能减少磁盘 I/O 次数。

而 B+Tree 是 B - Tree 的优化版本,在 MySQL 的 InnoDB 存储引擎中广泛使用。它的所有数据都存储在叶子节点中,内部节点只存储键值和指向子节点的指针,并且叶子节点之间通过指针相连形成有序链表,这使得范围查询更加高效。

使用场景和注意事项

  • 使用场景
  • 注意事项

mysql的存储引擎

常见存储引擎及特点

InnoDB

  • 事务支持:InnoDB 最大的特点之一就是支持事务。事务具有 ACID(原子性、一致性、隔离性、持久性)特性,这对于一些对数据完整性和一致性要求较高的场景非常重要。比如在银行系统的转账操作中,一个账户的扣款和另一个账户的入账必须作为一个不可分割的事务来处理,使用 InnoDB 就能很好地保证这一点。
  • 外键约束:它支持外键约束,这使得表与表之间可以建立关联关系,保证数据的参照完整性。例如在一个订单管理系统中,订单表和客户表之间可以通过外键建立关联,确保每个订单都对应一个有效的客户。
  • 聚簇索引:InnoDB 使用聚簇索引,数据和索引是存储在一起的。主键索引的叶子节点存储了完整的数据行,这使得基于主键的查询非常高效。不过,如果主键不是顺序插入的,可能会导致索引页的分裂,影响性能。
  • 应用场景:适用于大多数需要事务处理的场景,如电商系统的订单管理、金融系统的交易记录等。

MyISAM

  • 不支持事务:MyISAM 不支持事务处理,这意味着它无法保证一组操作的原子性。在数据操作过程中,如果出现错误,无法进行回滚操作。
  • 不支持外键:没有外键约束功能,表与表之间的关联需要通过应用程序来实现。
  • 表级锁:MyISAM 使用表级锁,在进行写操作时会锁定整个表,这可能会导致并发性能较低。但在读取操作较多的场景下,由于读操作不会阻塞,性能表现较好。
  • 空间和数据存储:它的索引和数据是分开存储的,索引文件和数据文件是独立的。这种存储方式使得 MyISAM 在空间利用上可能不如 InnoDB 高效。
  • 应用场景:适用于对查询性能要求较高、对事务和外键需求不高的场景,如新闻网站的文章存储、简单的日志记录等。

Memory

  • 数据存储在内存中:Memory 存储引擎将数据存储在内存中,而不是磁盘上,这使得数据的读写速度非常快。但由于数据存储在内存中,一旦服务器重启,数据就会丢失。
  • 哈希索引(默认):默认使用哈希索引,哈希索引在等值查询时性能非常高,但不适合范围查询。
  • 应用场景:适用于临时数据的存储和快速查询场景,如缓存一些经常变化但对数据持久化要求不高的数据,或者用于存储一些中间结果集。

存储引擎的选择依据

  • 事务需求:如果业务需要严格的事务处理,如涉及资金交易、数据完整性要求高的操作,应选择 InnoDB 存储引擎。
  • 并发读写:如果读操作远远多于写操作,且对事务和外键没有严格要求,可以考虑 MyISAM 存储引擎;如果需要高并发的读写操作,InnoDB 的行级锁机制更适合。
  • 数据持久化:如果数据需要持久保存,不适合使用 Memory 存储引擎;而对于临时数据或缓存数据,可以考虑使用 Memory 存储引擎来提高性能。
  • 索引类型需求:如果经常进行等值查询,可以考虑使用 Memory 存储引擎的哈希索引;如果需要范围查询,InnoDB 的 B - Tree 索引更合适。

对比一下InnoDB和MyISAM存储引擎的区别

不会 换问题了www

1. 事务支持

  • InnoDB:支持事务,具备事务的 ACID(原子性、一致性、隔离性、持久性)特性。这对于确保数据的完整性和一致性非常重要,例如在电商系统的订单处理流程中,一个订单的创建、库存的扣减以及支付记录的生成必须作为一个原子操作来处理,任何一个环节出现问题都可以进行回滚,保证数据的正确性。
  • MyISAM:不支持事务。如果在操作过程中出现错误,无法进行回滚操作,可能会导致数据不一致。例如在批量更新数据时,如果中途出现错误,已经更新的数据无法恢复到原来的状态。

2. 外键约束

  • InnoDB:支持外键约束,通过外键可以建立表与表之间的关联关系,保证数据的参照完整性。比如在一个学生管理系统中,学生表和班级表之间可以通过外键关联,确保每个学生都属于一个有效的班级。当尝试删除一个还有学生关联的班级时,数据库会根据外键约束进行相应的处理,防止出现数据不一致的情况。
  • MyISAM:不支持外键约束。表与表之间的关联需要通过应用程序来实现,这增加了开发的复杂度,并且数据的参照完整性需要由应用程序来保证。

3. 锁机制

  • InnoDB:支持行级锁和表级锁,默认使用行级锁。行级锁的粒度更细,在进行写操作时,只锁定需要操作的行,而不是整个表,这使得并发性能更高。例如在一个高并发的在线商城中,多个用户同时对不同的商品进行下单操作,行级锁可以让这些操作并行进行,提高系统的响应速度。
  • MyISAM:只支持表级锁。在进行写操作时,会锁定整个表,这会导致其他用户无法对该表进行读写操作,并发性能较低。在写操作频繁的场景下,可能会出现大量的锁等待,影响系统的性能。

4. 索引与数据存储

  • InnoDB:采用聚簇索引,数据和索引是存储在一起的。主键索引的叶子节点存储了完整的数据行,而辅助索引的叶子节点存储的是主键值。这使得基于主键的查询非常高效,但如果主键不是顺序插入的,可能会导致索引页的分裂,影响性能。
  • MyISAM:索引和数据是分开存储的。索引文件和数据文件是独立的,索引文件中存储的是数据的物理地址。这种存储方式使得在进行全表扫描时,MyISAM 的性能可能会比 InnoDB 稍高,但在基于索引的查询方面,InnoDB 通常更有优势。

5. 数据恢复

  • InnoDB:由于支持事务和日志记录,在出现崩溃或故障时,可以通过日志文件进行数据恢复,保证数据的一致性和完整性。例如在服务器突然断电后,重启时 InnoDB 可以根据日志文件将未完成的事务进行回滚或提交,确保数据处于正确的状态。
  • MyISAM:没有类似的事务日志机制,在出现崩溃或故障时,数据恢复相对困难,可能会导致数据丢失或不一致。

6. 空间利用

  • InnoDB:由于需要存储事务信息、回滚段等额外的数据,空间利用率相对较低。但在处理大量数据和高并发场景时,其性能优势可以弥补空间上的不足。
  • MyISAM:空间利用率相对较高,因为它不需要存储额外的事务信息。但在数据更新频繁的场景下,可能会出现碎片问题,影响性能。

7. 应用场景

  • InnoDB:适用于大多数需要事务处理和高并发读写的场景,如电商系统、金融系统、企业资源规划(ERP)系统等。
  • MyISAM:适用于对查询性能要求较高、对事务和外键需求不高的场景,如新闻网站的文章存储、简单的日志记录系统等。

综上所述,InnoDB 和 MyISAM 在功能和性能上有各自的特点,在实际应用中需要根据具体的业务需求来选择合适的存储引擎。

b+锁和b+树

B+ 树

1. 定义和基本结构

B+ 树是一种自平衡的多路搜索树,它是 B 树的一种变形结构。在 B+ 树中,所有的数据都存储在叶子节点,内部节点只存储键值和指向子节点的指针。叶子节点之间通过指针相连,形成一个有序链表。

例如,对于一个存储学生成绩的数据结构,如果使用 B+ 树,内部节点可能只存储成绩的范围信息(如 60 - 70 分、70 - 80 分等),而实际的学生成绩记录则存储在叶子节点中,并且叶子节点按成绩从小到大的顺序依次连接。

2. 特性

  • 平衡性:B+ 树的所有叶子节点都在同一层,这保证了树的高度是相对稳定的。无论数据如何插入或删除,树都会通过自动调整来保持平衡,从而保证了查询效率的稳定性。
  • 多路搜索:B+ 树的每个节点可以有多个子节点,这使得树的分支因子较大,树的高度相对较低。相比于二叉树,B+ 树可以在更少的磁盘 I/O 操作内找到所需的数据。
  • 范围查询高效:由于叶子节点之间通过指针相连,B+ 树在进行范围查询时非常高效。只需要找到范围的起始节点,然后沿着链表依次遍历即可。

3. 应用场景

在数据库系统中,B+ 树被广泛应用于索引结构。例如 MySQL 的 InnoDB 存储引擎就使用 B+ 树作为索引的数据结构。通过在表的某些列上创建 B+ 树索引,可以大大提高数据的查询速度。比如在一个大型的电商数据库中,对商品价格列创建 B+ 树索引,当用户按照价格范围筛选商品时,数据库可以利用 B+ 树快速定位到符合条件的商品记录。

B+ 锁

1. 概念

B+ 锁通常是指在使用 B+ 树作为数据结构时所采用的锁机制,其目的是为了保证并发访问 B+ 树时的数据一致性和完整性。在多用户并发访问数据库的场景下,多个事务可能同时对 B+ 树索引进行读写操作,如果没有合适的锁机制,可能会导致数据不一致的问题。

2. 常见的锁类型及应用

  • 共享锁(S 锁):多个事务可以同时对一个节点加共享锁,用于读操作。当一个事务对 B+ 树的某个节点加上共享锁后,其他事务也可以对该节点加共享锁进行读操作,但不能加排他锁进行写操作。例如,多个用户同时查询数据库中的同一张表的索引信息,这些查询事务可以同时对相关的 B+ 树节点加共享锁。
  • 排他锁(X 锁):排他锁用于写操作,当一个事务对 B+ 树的某个节点加上排他锁后,其他事务不能对该节点加任何类型的锁。例如,当一个事务需要对数据库中的某条记录进行更新操作时,它会对 B+ 树中该记录对应的索引节点加排他锁,以防止其他事务同时对该节点进行读写操作。
  • 意向锁:为了提高并发性能,在 B+ 树中还引入了意向锁。意向锁表示事务在更底层的节点上有某种锁的意图。例如,意向共享锁表示事务打算在某个子节点上加共享锁,意向排他锁表示事务打算在某个子节点上加排他锁。意向锁可以让数据库系统快速判断某个节点是否有子节点被加锁,从而避免对整个树进行遍历检查。

3. 锁的使用原则和并发控制

在使用 B+ 锁时,需要遵循一定的原则来保证并发控制的正确性。例如,在进行写操作时,通常需要先获取排他锁;在进行读操作时,可以获取共享锁。同时,为了避免死锁的发生,需要对锁的获取顺序进行合理的规划。在实际的数据库系统中,会采用一些死锁检测和恢复机制来处理可能出现的死锁情况。

总之,B+ 树为数据库提供了高效的索引结构,而 B+ 锁则保证了在并发环境下对 B+ 树操作的正确性和一致性。

锁机制

锁的类型

1. 共享锁(S 锁)

共享锁也叫读锁,多个事务可以同时对同一数据对象加共享锁。也就是说,当一个事务给某数据加上共享锁后,其他事务还能对该数据加共享锁来进行读操作,但在这些共享锁释放之前,任何事务都不能对该数据加排他锁进行写操作。

比如在一个在线图书馆系统中,多个读者可以同时查看同一本书的基本信息,这时数据库会给这本书的信息记录加上共享锁,保证多个读者能同时读取,互不干扰,但在这些读者都关闭查看界面(释放共享锁)之前,图书管理员无法修改这本书的信息。

2. 排他锁(X 锁)

排他锁又称写锁,一旦一个事务对某数据对象加上排他锁,就只有该事务能对该数据进行读写操作,其他事务既不能加共享锁读数据,也不能加排他锁写数据,直到该事务释放排他锁。

还是以在线图书馆系统为例,当图书管理员要修改某本书的信息时,会给这本书的记录加上排他锁,此时其他读者不能查看也不能修改这本书的信息,只有等管理员完成修改并释放排他锁后,其他事务才能对该数据进行操作。

3. 意向锁

意向锁用于表明事务在更底层的节点上有某种锁的意图。它有两种类型:意向共享锁(IS 锁)和意向排他锁(IX 锁)。意向共享锁表示事务打算在某个子节点上加共享锁,意向排他锁表示事务打算在某个子节点上加排他锁。

例如,在数据库的表级和行级锁场景中,如果一个事务要对表中的某一行加排他锁,它会先在表级加上意向排他锁,这样数据库就能快速判断表中是否有子节点(行)被加锁,避免对整个表的行进行遍历检查。

锁的粒度

1. 行级锁

行级锁的粒度最小,它只对操作的数据行加锁。这种锁的优点是并发度高,多个事务可以同时对不同的行进行操作,相互之间的干扰较小。但缺点是加锁和解锁的开销较大,因为需要对每一行数据进行单独的锁管理。

比如在一个电商系统的订单表中,多个用户可以同时修改自己的订单信息,每个用户的操作只会对自己的订单行加锁,不会影响其他用户的订单操作。

2. 表级锁

表级锁的粒度最大,它会对整个表加锁。表级锁的加锁和解锁开销较小,但并发度较低,因为一旦一个事务对表加了锁,其他事务就不能对该表的任何行进行操作。

例如,在进行数据库备份或者对表结构进行修改时,通常会使用表级锁,保证在操作过程中表的数据不会被其他事务修改。

3. 页级锁

页级锁的粒度介于行级锁和表级锁之间,它对数据库中的一个数据页加锁。一个数据页通常包含多个数据行,所以页级锁的并发度和加锁开销也介于行级锁和表级锁之间。

常见问题及应对措施

1. 死锁

死锁是指两个或多个事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。例如,事务 A 持有资源 X 的锁并请求资源 Y 的锁,而事务 B 持有资源 Y 的锁并请求资源 X 的锁,这样两个事务就会陷入无限等待的状态。

应对死锁的方法主要有死锁预防、死锁检测和死锁解除。死锁预防可以通过按顺序获取锁来避免循环等待;死锁检测则是通过一定的算法(如等待图算法)来检测系统中是否存在死锁;当检测到死锁后,需要选择一个或多个事务进行回滚,释放其持有的锁,以解除死锁。

2. 锁粒度选择不当

如果锁粒度选择过大,会降低并发性能;如果锁粒度选择过小,会增加加锁和解锁的开销。

在实际应用中,需要根据具体的业务场景来选择合适的锁粒度。对于读操作频繁、写操作较少的场景,可以选择共享锁和较大的锁粒度;对于写操作频繁的场景,则需要选择排他锁和较小的锁粒度,以提高并发性能。

综上所述,合理运用锁机制的类型和粒度,并有效处理常见问题,对于保障数据库系统在并发环境下的正常运行至关重要。

java 锁

面试官您好,在 Java 编程里,锁机制是实现多线程同步和并发控制的重要手段,能确保多个线程安全地访问共享资源。下面我会结合 Java 中的关键字和锁类来详细介绍。

synchronized 关键字

1. 基本概念和用法

synchronized 是 Java 中最基本的用于实现线程同步的关键字,它可以修饰方法或代码块。当一个线程访问被 synchronized 修饰的方法或代码块时,会自动获取该对象的锁,其他线程必须等待该线程释放锁后才能进入。

2. 修饰实例方法

synchronized 修饰实例方法时,锁对象是当前实例对象。例如:

public class SynchronizedExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

在这个例子中,多个线程同时调用 increment 方法时,只有一个线程能获取到 SynchronizedExample 实例的锁,从而保证 count 变量的线程安全。

3. 修饰静态方法

synchronized 修饰静态方法时,锁对象是当前类的 Class 对象。例如:

public class SynchronizedStaticExample {
    private static int staticCount = 0;

    public static synchronized void incrementStatic() {
        staticCount++;
    }
}

这里,多个线程同时调用 incrementStatic 方法时,会竞争 SynchronizedStaticExample.class 对象的锁,确保 staticCount 变量的并发安全。

4. 修饰代码块

synchronized 还可以修饰代码块,此时可以指定锁对象。例如:

public class SynchronizedBlockExample {
    private int value = 0;
    private final Object lock = new Object();

    public void updateValue() {
        synchronized (lock) {
            value++;
        }
    }
}

在这个代码中,多个线程同时调用 updateValue 方法时,会竞争 lock 对象的锁,只有获取到锁的线程才能执行 value++ 操作。

Lock 接口及其实现类

1. 基本概念

Lock 是 Java 5 引入的一个接口,提供了比 synchronized 更灵活的锁机制。它定义了获取锁和释放锁的方法,需要手动调用这些方法来实现锁的获取和释放。

2. ReentrantLock

ReentrantLockLock 接口的一个常用实现类,它支持可重入锁,即同一个线程可以多次获取同一把锁而不会发生死锁。例如:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private int num = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            num++;
        } finally {
            lock.unlock();
        }
    }
}

在这个例子中,increment 方法使用 ReentrantLock 来保证线程安全。通过 lock.lock() 方法获取锁,在 try 块中执行需要同步的操作,最后在 finally 块中使用 lock.unlock() 方法释放锁,确保无论是否发生异常,锁都会被释放。

3. ReentrantReadWriteLock

ReentrantReadWriteLock 是一种读写锁,它将锁分为读锁和写锁。多个线程可以同时获取读锁进行读操作,但在写操作时需要获取写锁,并且写锁与读锁、写锁与写锁之间是互斥的。例如:

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private int data = 0;
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

    public int readData() {
        readLock.lock();
        try {
            return data;
        } finally {
            readLock.unlock();
        }
    }

    public void writeData(int newData) {
        writeLock.lock();
        try {
            data = newData;
        } finally {
            writeLock.unlock();
        }
    }
}

在这个代码中,readData 方法使用读锁,多个线程可以同时调用该方法进行读操作;writeData 方法使用写锁,在写操作时会阻塞其他读和写操作,保证数据的一致性。

synchronized 与 Lock 的比较

  • 语法层面synchronized 是 Java 关键字,由 JVM 实现锁的获取和释放;Lock 是一个接口,需要手动调用方法来获取和释放锁。
  • 灵活性Locksynchronized 更灵活,例如 Lock 可以实现公平锁、可中断锁等,而 synchronized 是隐式的非公平锁。
  • 性能方面:在低并发场景下,synchronizedLock 的性能差异不大;在高并发场景下,Lock 的性能可能会更好,因为它可以通过一些高级特性进行优化。

综上所述,Java 中的锁机制通过 synchronized 关键字和 Lock 接口及其实现类,为多线程编程提供了丰富的同步和并发控制手段,开发者可以根据具体的业务场景选择合适的锁机制。

redis淘汰策略

面试官您好,Redis 是一种内存数据库,内存空间有限。当 Redis 内存使用达到一定阈值时,为了保证新数据能够正常写入,就需要采用淘汰策略来移除部分旧数据。以下是 Redis 常见的淘汰策略:

1. 不进行淘汰(noeviction)

  • 策略说明:这是 Redis 的默认淘汰策略。当内存使用达到上限后,新写入操作会报错,而读操作、删除操作等仍然可以正常执行。
  • 适用场景:适用于对数据完整性要求极高,不允许有数据被淘汰的场景。比如某些缓存系统中,缓存的数据是经过复杂计算或从重要数据源获取的,一旦被淘汰可能会导致后续业务出现严重问题。

2. 从设置了过期时间的键中淘汰

2.1 volatile-lru(Least Recently Used)

  • 策略说明:从设置了过期时间的键中,淘汰最近最少使用的键。Redis 会维护一个键的访问时间记录,当需要淘汰数据时,会优先选择最久未被访问的键进行删除。
  • 适用场景:适用于缓存场景,对于一些可能会过期的数据,优先淘汰那些长时间未被使用的,这样可以保证经常被访问的数据能够留在缓存中,提高缓存命中率。例如在电商系统中,缓存商品的促销信息,对于长时间未被用户访问的商品促销信息可以优先淘汰。

2.2 volatile-lfu(Least Frequently Used)

  • 策略说明:从设置了过期时间的键中,淘汰最不经常使用的键。Redis 会统计每个键的访问频率,当内存不足时,淘汰访问频率最低的键。
  • 适用场景:当数据的访问频率差异较大时,这种策略比较合适。比如在一个新闻网站的缓存系统中,热门新闻的访问频率会远高于普通新闻,使用 volatile - lfu 策略可以优先保留热门新闻的缓存,淘汰访问频率低的普通新闻缓存。

2.3 volatile-random

  • 策略说明:从设置了过期时间的键中,随机淘汰一个键。这种策略不考虑键的使用频率和访问时间等因素,只是随机选择一个设置了过期时间的键进行删除。
  • 适用场景:当数据之间没有明显的重要性差异,且对缓存的命中率要求不是特别高时,可以使用这种策略。例如一些临时缓存的场景,缓存的数据只是为了减少短期内的重复计算,随机淘汰不会对系统造成太大影响。

2.4 volatile-ttl

  • 策略说明:从设置了过期时间的键中,淘汰剩余时间(TTL,Time To Live)最短的键。也就是优先删除那些即将过期的键。
  • 适用场景:适用于需要严格控制数据过期时间的场景。比如在一个限时活动的缓存系统中,活动有明确的开始和结束时间,使用 volatile - ttl 策略可以保证在活动结束前不会误删数据,同时在活动结束后能及时释放内存。

3. 从所有键中淘汰

3.1 allkeys-lru

  • 策略说明:从所有的键中,淘汰最近最少使用的键。不管键是否设置了过期时间,都会参与淘汰过程。
  • 适用场景:当缓存的数据没有设置过期时间,或者大部分数据都没有过期时间,但又需要保证缓存中保留的是最近经常使用的数据时,可以使用这种策略。例如在一个全局的对象缓存系统中,缓存的对象没有明确的过期时间,使用 allkeys - lru 可以保证经常被访问的对象留在缓存中。

3.2 allkeys-lfu

  • 策略说明:从所有的键中,淘汰最不经常使用的键。同样不考虑键是否设置了过期时间,根据键的访问频率进行淘汰。
  • 适用场景:与 volatile - lfu 类似,但适用于所有键都可能参与淘汰的场景。比如在一个通用的缓存系统中,存储了各种类型的数据,且没有对数据设置过期时间,使用 allkeys - lfu 可以根据数据的使用频率进行合理的内存管理。

3.3 allkeys-random

  • 策略说明:从所有的键中,随机淘汰一个键。不区分键是否设置了过期时间,完全随机地选择一个键进行删除。
  • 适用场景:当对数据的重要性和使用频率没有特别的区分需求,且需要快速释放内存时,可以使用这种策略。例如在一些简单的临时数据缓存场景中,数据的随机性较大,使用随机淘汰策略可以快速腾出内存空间。

在实际应用中,需要根据具体的业务需求和数据特点来选择合适的 Redis 淘汰策略,以确保 Redis 能够高效稳定地运行。

常见docker命令

面试官您好,Docker 是一个用于开发、部署和运行应用程序的开源平台,通过容器化技术可以实现应用的快速部署和隔离。下面我为您介绍一些常见的 Docker 命令:

镜像操作命令

1. 拉取镜像(docker pull)

  • 功能:从 Docker 镜像仓库(如 Docker Hub)下载指定的镜像。
  • 示例:拉取最新版本的 MySQL 镜像
docker pull mysql

如果要指定版本,可以在镜像名后面加上冒号和版本号,例如拉取 MySQL 8.0 版本的镜像:

docker pull mysql:8.0

2. 查看本地镜像(docker images)

  • 功能:列出本地已经下载的所有 Docker 镜像。
  • 示例
docker images

该命令会显示镜像的仓库名、标签、镜像 ID、创建时间和大小等信息。

3. 删除本地镜像(docker rmi)

  • 功能:删除本地指定的 Docker 镜像。
  • 示例:删除名为 my - image 的镜像
docker rmi my - image

如果要删除多个镜像,可以在命令后面依次列出镜像名或镜像 ID,用空格分隔。

4. 构建镜像(docker build)

  • 功能:根据 Dockerfile 构建一个新的 Docker 镜像。
  • 示例:假设当前目录下有一个 Dockerfile,使用以下命令构建镜像,-t 选项用于指定镜像的标签:
docker build -t my - custom - image:1.0 .

最后的 . 表示使用当前目录作为构建上下文。

容器操作命令

1. 创建并启动容器(docker run)

  • 功能:根据指定的镜像创建一个新的容器并启动它。
  • 示例:创建并启动一个基于 MySQL 镜像的容器,设置数据库的 root 用户密码为 password,并将容器的 3306 端口映射到主机的 3306 端口:
docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password mysql

其中,-d 表示让容器在后台运行,-p 用于端口映射,-e 用于设置环境变量。

2. 查看运行中的容器(docker ps)

  • 功能:列出当前正在运行的 Docker 容器。
  • 示例
docker ps

如果要查看所有容器(包括已停止的),可以使用 -a 选项:

docker ps -a

3. 停止容器(docker stop)

  • 功能:停止指定的正在运行的容器。
  • 示例:停止 ID 为 123456 的容器
docker stop 123456

也可以使用容器的名称来停止容器。

4. 启动已停止的容器(docker start)

  • 功能:启动一个已经停止的容器。
  • 示例:启动名为 my - container 的容器
docker start my - container

5. 进入容器(docker exec)

  • 功能:在一个正在运行的容器中执行命令,通常用于进入容器的交互式终端。
  • 示例:进入 ID 为 123456 的容器的交互式终端
docker exec -it 123456 /bin/bash

其中,-i 表示保持标准输入打开,-t 表示分配一个伪终端。

6. 删除容器(docker rm)

  • 功能:删除指定的容器,删除前容器必须处于停止状态。
  • 示例:删除名为 my - container 的容器
docker rm my - container

如果要强制删除一个正在运行的容器,可以使用 -f 选项。

其他常用命令

1. 查看容器日志(docker logs)

  • 功能:查看指定容器的日志信息。
  • 示例:查看名为 my - container 的容器的日志
docker logs my - container

如果要实时查看日志,可以使用 -f 选项。

2. 查看容器资源使用情况(docker stats)

  • 功能:实时查看指定容器或所有容器的资源使用情况,如 CPU、内存、网络等。
  • 示例:查看所有容器的资源使用情况
docker stats

以上就是一些常见的 Docker 命令,在实际使用中可以根据具体需求灵活运用这些命令来管理 Docker 镜像和容器。

jvm内存模型

面试官您好,下面我按照 Java 程序从启动到运行结束的流程,通俗易懂地分析 JVM 内存模型。

1. 程序启动与类加载阶段

当我们启动一个 Java 程序,JVM 会先开始工作。首先就是类加载过程,这个阶段主要涉及到方法区和运行时常量池。

方法区

JVM 会去找到程序所需要的类文件(.class 文件),把这些类的信息加载到方法区里。类信息就像是类的“设计蓝图”,包括类的结构、方法定义、字段定义等等。比如说我们有一个 Person 类,里面有 nameage 这些字段,还有 sayHello() 这样的方法,这些信息都会被加载到方法区。多个线程都可以共享这些类信息,因为不管哪个线程要使用 Person 类,用的都是同一套“设计蓝图”。

运行时常量池

在加载类的过程中,类文件里的常量池信息会被放到运行时常量池。常量池里有各种字面量(像字符串常量 "Hello"、整数常量 10 等)和符号引用(比如类名、方法名、字段名等的引用)。运行时常量池有个特点是动态性,在程序运行的时候也能往里面加新的常量。就好比我们在程序里用 String 类的 intern() 方法,就可以把新的字符串常量放到运行时常量池里。

2. 线程创建与栈空间分配

接下来,JVM 会为程序创建线程,每个线程都会有自己独立的程序计数器和虚拟机栈,本地方法栈也是线程私有的。

程序计数器

程序计数器就像是一个小指针,它指向当前线程正在执行的字节码指令的位置。每个线程执行的代码可能不一样,执行进度也不同,所以每个线程都有自己的程序计数器。比如说线程 A 在执行 for 循环里的某条指令,线程 B 在执行 if - else 语句里的另一条指令,它们的程序计数器就分别指向各自当前执行的指令。

虚拟机栈

当线程开始执行方法的时候,虚拟机栈就发挥作用了。每调用一个方法,就会在虚拟机栈里创建一个栈帧。栈帧就像是一个小盒子,里面装着这个方法运行时需要的东西,比如局部变量(方法里定义的变量)、操作数栈(用来进行运算的临时存储空间)、动态链接(用来找到方法的实际代码位置)、方法出口(方法执行完后回到哪里继续执行)等。

举个例子,有一个 add 方法用来计算两个数的和:

public class Calculator {
    public int add(int a, int b) {
        int result = a + b;
        return result;
    }

    public static void main(String[] args) {
        Calculator calc = new Calculator();
        int sum = calc.add(3, 5);
    }
}

main 方法调用 add 方法时,就会在虚拟机栈里为 add 方法创建一个栈帧,把 abresult 这些局部变量放到栈帧的局部变量表里,在操作数栈里进行 a + b 的运算。当 add 方法执行完,这个栈帧就会从虚拟机栈里弹出。

本地方法栈

如果程序里调用了本地方法(用 C、C++ 等语言写的方法),本地方法栈就会为这些本地方法创建栈帧,和虚拟机栈为 Java 方法服务类似。

3. 对象创建与堆内存使用

在程序运行过程中,经常会创建对象,这时候就会用到 Java 堆。

Java 堆是所有线程共享的一块内存区域,几乎所有的对象实例都在堆里分配内存。还是拿上面的 Calculator 类来说,在 main 方法里有 Calculator calc = new Calculator(); 这行代码,就会在堆里创建一个 Calculator 对象的实例。这个对象就像一个真实存在的东西,它有自己的状态(也就是字段的值)。多个线程都可以访问堆里的这个 Calculator 对象。

随着程序不断运行,会创建越来越多的对象,堆里的内存就会被占用。当有些对象不再被使用了,就会变成垃圾对象。垃圾回收器会定期对堆进行垃圾回收,把这些不再使用的对象占用的内存释放出来,这样堆就有空间可以继续创建新的对象了。

4. 程序结束与资源释放

当 Java 程序运行结束,JVM 会释放它所占用的所有内存资源。方法区里的类信息、运行时常量池里的常量、堆里的对象、各个线程的程序计数器、虚拟机栈和本地方法栈里的栈帧等都会被清理掉,整个 JVM 内存模型所管理的内存就都恢复到初始状态了。

综上所述,JVM 内存模型通过各个内存区域的协同工作,保证了 Java 程序从启动到运行结束整个过程的正常进行,并且有效地管理了内存资源。

java线程

面试官您好,Java 线程是 Java 并发编程的核心概念,它允许程序同时执行多个任务,充分利用多核处理器的性能。下面我会从线程的创建、生命周期、同步机制等方面进行详细介绍。

线程的创建

在 Java 中,创建线程主要有三种方式:

1. 继承 Thread 类

通过继承 Thread 类,并重写其 run() 方法来定义线程要执行的任务。示例代码如下:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("This is a thread created by extending Thread class.");
    }
}

public class ThreadCreationExample {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start(); 
    }
}

在上述代码中,MyThread 类继承了 Thread 类,并重写了 run() 方法。在 main 方法中,创建了 MyThread 的实例,并调用 start() 方法来启动线程。

2. 实现 Runnable 接口

实现 Runnable 接口,并实现其 run() 方法,然后将该实现类的实例作为参数传递给 Thread 类的构造函数。示例代码如下:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("This is a thread created by implementing Runnable interface.");
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start(); 
    }
}

这种方式的优点是可以避免单继承的限制,一个类可以在实现 Runnable 接口的同时继承其他类。

3. 实现 Callable 接口

Callable 接口与 Runnable 接口类似,但 Callablecall() 方法可以有返回值,并且可以抛出异常。通常结合 FutureTask 来使用。示例代码如下:

import java.util.concurrent.*;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 1 + 2;
    }
}

public class CallableExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        Integer result = futureTask.get(); 
        System.out.println("The result is: " + result);
    }
}

在上述代码中,MyCallable 类实现了 Callable 接口,其 call() 方法返回一个整数。通过 FutureTask 可以获取线程执行的结果。

线程的生命周期

Java 线程的生命周期包含以下几种状态:

1. 新建(New)

当创建一个 Thread 对象或实现了 RunnableCallable 接口的对象时,线程处于新建状态。此时线程还没有开始执行,只是在内存中被创建出来。

2. 就绪(Runnable)

当调用线程的 start() 方法后,线程进入就绪状态。处于就绪状态的线程已经准备好执行,但还需要等待 CPU 分配时间片。

3. 运行(Running)

当 CPU 为就绪状态的线程分配了时间片后,线程进入运行状态,开始执行 run() 方法中的代码。

4. 阻塞(Blocked)

线程在某些情况下会进入阻塞状态,暂时停止执行。常见的阻塞情况包括:

  • 等待阻塞:调用 wait() 方法后,线程会进入等待状态,直到其他线程调用相同对象的 notify()notifyAll() 方法唤醒它。
  • 同步阻塞:当线程尝试获取一个被其他线程占用的锁时,会进入同步阻塞状态,直到锁被释放。
  • 其他阻塞:调用 sleep()join() 等方法也会使线程进入阻塞状态。

5. 终止(Terminated)

线程执行完 run() 方法中的代码,或者因为异常退出 run() 方法,线程就会进入终止状态,此时线程的生命周期结束。

线程同步机制

在多线程环境中,多个线程可能会同时访问共享资源,为了避免数据不一致等问题,需要使用线程同步机制。

1. synchronized 关键字

synchronized 关键字可以修饰方法或代码块,保证同一时刻只有一个线程可以执行被修饰的方法或代码块。示例代码如下:

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizedExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("The final count is: " + counter.getCount());
    }
}

在上述代码中,increment() 方法被 synchronized 修饰,保证了同一时刻只有一个线程可以对 count 进行自增操作,避免了数据不一致的问题。

2. Lock 接口

Lock 接口是 Java 5 引入的另一种线程同步机制,提供了比 synchronized 更灵活的锁控制。常见的实现类有 ReentrantLock。示例代码如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class LockCounter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

public class LockExample {
    public static void main(String[] args) throws InterruptedException {
        LockCounter lockCounter = new LockCounter();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                lockCounter.increment();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                lockCounter.increment();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("The final count is: " + lockCounter.getCount());
    }
}

在上述代码中,使用 ReentrantLock 来实现线程同步,通过 lock.lock() 获取锁,在 finally 块中使用 lock.unlock() 释放锁,确保锁一定会被释放。

线程池

线程池是一种管理线程的机制,它可以预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务,任务执行完后线程不会销毁,而是返回线程池等待下一个任务。使用线程池可以减少线程创建和销毁的开销,提高系统性能。

Java 提供了 ExecutorService 接口和 Executors 类来创建和管理线程池。示例代码如下:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        executorService.submit(() -> {
            System.out.println("Task 1 is running.");
        });
        executorService.submit(() -> {
            System.out.println("Task 2 is running.");
        });
        executorService.shutdown(); 
    }
}

在上述代码中,使用 Executors.newFixedThreadPool(2) 创建了一个固定大小为 2 的线程池,然后向线程池提交了两个任务。最后调用 shutdown() 方法关闭线程池。

综上所述,Java 线程为并发编程提供了强大的支持,通过合理地创建、管理和同步线程,可以提高程序的性能和响应能力。

springcloud springcloudalibaba 微服务常用组件

面试官您好,Spring Cloud 和 Spring Cloud Alibaba 都是用于构建微服务架构的框架,下面我分别介绍它们各自的常用组件。

Spring Cloud 常用组件

1. Eureka

  • 功能:作为服务注册与发现组件,它就像是微服务架构中的“通讯录”。各个微服务启动时会将自己的信息(如服务名、IP 地址、端口等)注册到 Eureka Server 上,同时也会从 Eureka Server 获取其他服务的信息。这样,服务之间就可以通过服务名进行相互调用,而不需要硬编码具体的 IP 地址和端口。
  • 工作模式:采用客户端 - 服务器(Client - Server)模式,Eureka Server 是服务注册中心,Eureka Client 是各个微服务实例。Eureka 具有高可用性,支持集群部署,多个 Eureka Server 之间会相互同步服务信息。

2. Ribbon

  • 功能:是一个客户端负载均衡组件,当一个服务需要调用另一个服务的多个实例时,Ribbon 会根据一定的负载均衡策略(如轮询、随机等)选择一个合适的服务实例进行调用,从而提高系统的性能和可用性。
  • 使用方式:通常与 Eureka 配合使用,Ribbon 会从 Eureka Server 获取服务的所有实例信息,然后根据策略进行选择。例如,在一个电商系统中,订单服务需要调用库存服务的多个实例,Ribbon 会帮助订单服务选择合适的库存服务实例来处理请求。

3. Feign

  • 功能:是一个声明式的 HTTP 客户端,它让编写 HTTP 客户端变得更加简单。开发人员只需要定义一个接口,并使用注解来描述这个接口需要调用的 HTTP 请求,Feign 就会自动生成实现类来完成请求的发送和响应的处理。
  • 优势:可以与 Ribbon 集成,实现负载均衡的 HTTP 调用。同时,它还支持与 Hystrix 集成,实现服务的熔断和降级。例如,在一个微服务架构中,服务 A 要调用服务 B 的某个接口,开发人员只需要定义一个 Feign 接口,就可以像调用本地方法一样调用服务 B 的接口。

4. Hystrix

  • 功能:是一个用于实现服务容错的组件,它提供了服务熔断、服务降级、线程隔离等功能。当某个服务出现故障或响应时间过长时,Hystrix 可以快速熔断该服务的调用,避免故障的蔓延;同时,它还可以提供降级逻辑,返回一个默认的响应结果,保证系统的可用性。
  • 原理:Hystrix 通过线程池或信号量来隔离不同的服务调用,当某个服务的调用出现问题时,不会影响其他服务的调用。例如,在一个旅游预订系统中,酒店预订服务出现故障,Hystrix 可以熔断该服务的调用,并返回一个提示信息给用户,避免整个系统崩溃。

5. Zuul

  • 功能:是一个 API 网关组件,它作为系统的统一入口,接收所有客户端的请求,并将请求路由到相应的微服务。同时,Zuul 还可以进行请求过滤,实现身份验证、限流、日志记录等功能。
  • 应用场景:在微服务架构中,客户端不需要直接与各个微服务进行交互,而是通过 Zuul 网关来访问服务,这样可以简化客户端的开发,提高系统的安全性和可维护性。例如,一个移动应用可以通过 Zuul 网关来访问电商系统的商品服务、订单服务等。

Spring Cloud Alibaba 常用组件

1. Nacos

  • 功能:集服务注册与发现和配置管理功能于一体。作为服务注册中心,它可以替代 Eureka,微服务可以将自己的信息注册到 Nacos Server 上,并从 Nacos Server 获取其他服务的信息;作为配置中心,它可以管理微服务的配置信息,当配置发生变化时,微服务可以实时获取最新的配置。
  • 特点:具有简单易用、高性能、支持多种存储方式等特点。例如,在一个金融系统中,各个微服务可以将自己注册到 Nacos 上,同时从 Nacos 获取数据库连接配置、业务参数配置等信息。

2. Sentinel

  • 功能:是一个强大的流量控制、熔断降级组件。它可以对微服务的流量进行实时监控和控制,当流量超过预设的阈值时,可以进行限流操作;同时,当服务出现异常或响应时间过长时,也可以进行熔断和降级处理,保证系统的稳定性。
  • 使用方式:提供了丰富的规则配置方式,支持基于 QPS、线程数等多种指标进行流量控制。例如,在一个电商大促活动中,Sentinel 可以对商品详情页的访问流量进行限流,避免服务器因流量过大而崩溃。

3. RocketMQ

  • 功能:是一个分布式消息队列,用于实现微服务之间的异步通信和解耦。微服务可以通过 RocketMQ 发送和接收消息,实现数据的异步处理和系统的解耦。例如,在一个订单处理系统中,订单服务可以将订单创建的消息发送到 RocketMQ,库存服务和物流服务可以从 RocketMQ 接收消息并进行相应的处理。
  • 特点:具有高吞吐量、高可靠性、支持多种消息模式等特点。

4. Seata

  • 功能:是一个分布式事务解决方案,用于解决微服务架构中的分布式事务问题。在微服务架构中,一个业务操作可能会涉及多个微服务的调用,Seata 可以保证这些调用要么全部成功,要么全部失败,实现分布式事务的一致性。
  • 模式:支持多种分布式事务模式,如 AT 模式、TCC 模式、SAGA 模式等。例如,在一个跨多个微服务的转账业务中,Seata 可以保证转账操作的原子性,避免出现数据不一致的问题。

综上所述,Spring Cloud 和 Spring Cloud Alibaba 提供了丰富的组件,这些组件可以帮助开发人员快速构建和管理微服务架构,提高系统的性能、可用性和可维护性。

hashmap底层实现

面试官您好,HashMap 是 Java 中非常常用的集合类,用于存储键值对。下面我会结合 JDK 8 及之后的版本,从数据结构、存储过程、扩容机制等方面详细介绍其底层实现。

数据结构

HashMap 底层采用数组 + 链表 + 红黑树的结构来存储数据。

数组

数组被称为哈希桶(bucket),每个数组元素是一个链表或红黑树的头节点。数组的初始长度是 16,通过 hash 函数计算键的哈希值,然后根据哈希值对数组长度取模,得到该键值对应该存储在数组中的位置。这个数组的主要作用是快速定位元素,通过哈希值可以直接找到对应的数组索引,时间复杂度为 。

链表

当多个键通过哈希函数计算得到相同的数组索引时,就会发生哈希冲突。HashMap 采用链表来解决哈希冲突,将这些键值对以链表的形式存储在对应的数组位置上。链表中的每个节点包含键、值、哈希值以及指向下一个节点的引用。在 JDK 8 之前,插入新节点采用的是头插法;而在 JDK 8 及之后,为了避免链表成环的问题,采用了尾插法。

红黑树

当链表的长度达到一定阈值(默认为 8),并且数组长度达到 64 时,链表会转换为红黑树。红黑树是一种自平衡的二叉搜索树,它的查找、插入和删除操作的时间复杂度都是 ,相比于链表在数据量较大时性能更优。当树中的节点数量减少到一定阈值(默认为 6)时,红黑树会转换回链表。

存储过程

当调用 put(K key, V value) 方法存储键值对时,HashMap 会按照以下步骤进行操作:

  1. 计算哈希值:首先调用键的 hashCode() 方法得到哈希码,然后通过哈希函数对哈希码进行二次哈希,减少哈希冲突的概率。在 HashMap 中,哈希函数的实现如下:
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  1. 确定数组位置:通过计算得到的哈希值对数组长度取模,得到键值对应该存储的数组索引。在 HashMap 中,为了提高取模运算的效率,采用了 (n - 1) & hash 的方式,其中 n 是数组的长度。
  2. 检查数组位置:如果该数组位置为空,直接创建一个新节点存储键值对;如果该位置已经有节点,则需要进一步处理哈希冲突。
  3. 处理哈希冲突

扩容机制

HashMap 中的元素数量达到一定阈值时,会触发扩容操作。扩容的主要目的是减少哈希冲突,提高查找、插入和删除操作的性能。

扩容阈值

扩容阈值(threshold)等于数组长度乘以负载因子(loadFactor),默认的负载因子是 0.75。当 HashMap 中的元素数量超过扩容阈值时,会触发扩容操作。

扩容过程

  1. 创建新数组:将数组长度扩大为原来的 2 倍。
  2. 重新计算哈希值和数组位置:遍历原数组中的每个节点,根据新的数组长度重新计算每个键值对的哈希值和数组位置,并将其插入到新数组中。在 JDK 8 及之后,为了提高扩容效率,采用了一种优化策略,即根据节点的哈希值与原数组长度进行按位与运算的结果,将节点分为两类,分别插入到新数组的原位置或原位置 + 原数组长度的位置,避免了重新计算所有节点的哈希值和数组位置。

查找和删除过程

查找过程

当调用 get(Object key) 方法查找键对应的值时,HashMap 会先计算键的哈希值,然后根据哈希值找到对应的数组位置。如果该位置是链表,遍历链表比较键的哈希值和 equals() 方法,找到相同的键则返回对应的值;如果该位置是红黑树,调用红黑树的查找方法查找对应的值。

删除过程

当调用 remove(Object key) 方法删除键值对时,HashMap 同样先计算键的哈希值,找到对应的数组位置。如果该位置是链表,遍历链表找到要删除的节点,然后将其从链表中移除;如果该位置是红黑树,调用红黑树的删除方法删除节点。删除节点后,如果树的节点数量减少到 6,将红黑树转换回链表。

综上所述,HashMap 通过数组 + 链表 + 红黑树的结构,以及合理的哈希函数和扩容机制,实现了高效的键值对存储和查找功能。

布隆过滤器

面试官您好,布隆过滤器(Bloom Filter)是由 Burton Howard Bloom 在 1970 年提出的一种空间效率极高的概率型数据结构。下面我会从基本概念、原理、应用场景、优缺点等方面详细介绍它。

基本概念

布隆过滤器主要用于判断一个元素是否存在于一个集合中。它的特点是判断结果可能存在误判,但不会漏判。也就是说,如果布隆过滤器判断一个元素不存在,那么这个元素一定不存在;但如果判断一个元素存在,这个元素有可能实际上并不存在。

原理

布隆过滤器的核心是一个位数组(通常是一个很长的二进制数组)和多个哈希函数。

初始化

在初始化布隆过滤器时,会创建一个长度为 的位数组,初始状态下所有位都被置为 0。同时,选择 个不同的哈希函数。

插入元素

当要插入一个元素时,会使用这 个哈希函数分别对该元素进行哈希计算,得到 个哈希值。然后将位数组中对应这些哈希值的位置置为 1。例如,有一个元素 ,经过 3 个哈希函数计算得到的哈希值分别对应位数组的第 2、5、8 位,那么就将这三位都置为 1。

查询元素

当查询一个元素是否存在时,同样使用这 个哈希函数对该元素进行哈希计算,得到 个哈希值。然后检查位数组中对应这些哈希值的位置是否都为 1。如果有任何一位为 0,那么可以确定该元素一定不存在于集合中;如果所有位都为 1,那么只能说该元素可能存在于集合中,存在一定的误判概率。

应用场景

缓存穿透问题解决

在缓存系统中,当大量请求查询一个不存在于缓存和数据库中的数据时,会导致请求直接穿透缓存访问数据库,给数据库带来很大压力。布隆过滤器可以在缓存之前进行过滤,将可能存在的数据放入缓存查询范围,不存在的数据直接拦截,从而避免大量无效请求访问数据库。例如,在一个电商系统中,对于商品 ID 的查询,可以使用布隆过滤器过滤掉那些不存在的商品 ID 请求。

垃圾邮件过滤

可以使用布隆过滤器来判断一封邮件的发件人地址、主题等信息是否在已知的垃圾邮件列表中。如果布隆过滤器判断该邮件的相关信息可能在垃圾邮件列表中,就可以将其标记为疑似垃圾邮件进行进一步检查。

网页爬虫

在网页爬虫系统中,需要判断一个网页是否已经被爬取过。使用布隆过滤器可以快速判断一个网页的 URL 是否已经在已爬取的集合中,避免重复爬取,提高爬虫的效率。

优缺点

优点

  • 空间效率高:布隆过滤器只需要一个位数组和几个哈希函数,不需要存储具体的元素内容,因此占用的空间远远小于传统的数据结构(如哈希表)。
  • 查询效率高:插入和查询操作的时间复杂度都是 ,其中 是哈希函数的个数,查询速度非常快。

缺点

  • 存在误判率:布隆过滤器的判断结果是概率性的,存在一定的误判率。误判率与位数组的长度、哈希函数的个数以及插入元素的数量有关。
  • 无法删除元素:由于布隆过滤器的位数组中多个元素可能会共享某些位,删除一个元素时不能简单地将对应位清零,否则会影响其他元素的判断结果。

布隆过滤器参数的选择

为了控制布隆过滤器的误判率,需要合理选择位数组的长度 和哈希函数的个数 。一般来说,位数组长度越长、哈希函数个数越多,误判率越低,但会增加空间和计算开销。可以根据预期插入的元素数量 和期望的误判率 来计算合适的 和 :

  • 位数组长度 的计算公式:
  • 哈希函数个数 的计算公式:

综上所述,布隆过滤器是一种非常实用的数据结构,在很多场景下可以发挥重要作用,但在使用时需要根据具体需求权衡其优缺点,并合理选择参数。

(项目)Nacos+Feign实现服务间高效调用Gateway+GlobalFilter

面试官您好,我对这两部分内容有所了解,下面分别为您详细介绍 Nacos + Feign 实现服务间高效调用以及 Gateway + GlobalFilter 的相关内容。

Nacos + Feign 实现服务间高效调用

1. Nacos 简介

Nacos 是 Spring Cloud Alibaba 中的一个重要组件,它集服务注册与发现、配置管理功能于一体。作为服务注册中心,Nacos 可以让各个微服务在启动时将自己的信息(如服务名、IP 地址、端口等)注册到 Nacos Server 上,同时也能从 Nacos Server 获取其他服务的信息。这使得服务之间可以通过服务名进行调用,而无需硬编码具体的 IP 地址和端口。作为配置中心,Nacos 可以管理微服务的配置信息,当配置发生变化时,微服务可以实时获取最新的配置。

2. Feign 简介

Feign 是一个声明式的 HTTP 客户端,它简化了编写 HTTP 客户端的过程。开发人员只需定义一个接口,并使用注解描述该接口需要调用的 HTTP 请求,Feign 就会自动生成实现类来完成请求的发送和响应的处理。Feign 还可以与 Ribbon 集成,实现负载均衡的 HTTP 调用。

3. Nacos + Feign 实现服务间高效调用的流程

  • 服务注册:各个微服务在启动时,会将自己的信息注册到 Nacos Server 上。例如,有一个订单服务和一个商品服务,它们都会将自己的服务名、IP 地址和端口等信息注册到 Nacos。
  • 依赖引入:在需要调用其他服务的微服务中,引入 Feign 和 Nacos 的依赖。
  • Feign 接口定义:定义一个 Feign 接口,使用 @FeignClient 注解指定要调用的服务名。例如:
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "product-service")
public interface ProductServiceClient {
    @GetMapping("/products/{productId}")
    String getProductById(Long productId);
}

在这个例子中,ProductServiceClient 是一个 Feign 接口,@FeignClient(name = "product-service") 表示要调用名为 product-service 的服务。

  • 服务调用:在需要调用服务的地方注入 Feign 接口,并调用其方法。例如:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {
    @Autowired
    private ProductServiceClient productServiceClient;

    @GetMapping("/orders/{orderId}/product")
    public String getProductForOrder(@PathVariable Long orderId) {
        // 调用商品服务获取商品信息
        return productServiceClient.getProductById(1L);
    }
}
  • 负载均衡:Feign 会与 Ribbon 集成,根据 Nacos 提供的服务实例信息,实现负载均衡的服务调用。当有多个商品服务实例时,Ribbon 会根据一定的策略(如轮询、随机等)选择一个合适的实例进行调用。

Gateway + GlobalFilter

1. Spring Cloud Gateway 简介

Spring Cloud Gateway 是 Spring Cloud 生态系统中的 API 网关组件,它作为系统的统一入口,接收所有客户端的请求,并将请求路由到相应的微服务。同时,它还提供了请求过滤、限流、熔断、重试等功能,能够增强系统的安全性和可维护性。

2. GlobalFilter 简介

GlobalFilter 是 Spring Cloud Gateway 中的全局过滤器接口,实现该接口可以对所有经过网关的请求和响应进行统一的处理。全局过滤器会在路由过滤器之前执行,并且可以对请求和响应进行修改、记录日志、进行权限验证等操作。

3. Gateway + GlobalFilter 的应用场景和使用方式

  • 应用场景
  • 使用方式
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 在请求处理之前进行操作
        ServerHttpRequest request = exchange.getRequest();
        System.out.println("Request URL: " + request.getURI());

        // 继续处理请求
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 1; // 指定过滤器的执行顺序
    }
}

在这个例子中,CustomGlobalFilter 实现了 GlobalFilter 接口,在 filter 方法中记录了请求的 URL,并继续处理请求。getOrder 方法返回的整数表示过滤器的执行顺序,数值越小越先执行。

综上所述,Nacos + Feign 可以实现服务间的高效调用,而 Gateway + GlobalFilter 可以对网关的请求和响应进行统一处理,它们都是微服务架构中非常实用的技术组合。

网络编程了解吗

主要问操作系统 计算机网络等

进程和线程区别

面试官您好,进程和线程是操作系统中两个非常重要的概念,它们在很多方面存在区别,下面我从多个维度详细介绍:

定义与概念

  • 进程:进程是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。简单来说,进程可以看作是一个正在运行的程序实例。例如,当我们打开一个浏览器,操作系统就会为浏览器程序创建一个进程,这个进程拥有自己独立的内存空间、系统资源等,用于执行浏览器的各项功能,如网页加载、渲染等。
  • 线程:线程是进程中的一个执行单元,是 CPU 调度和分派的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源。以浏览器进程为例,它可能包含多个线程,比如一个线程负责处理用户的输入操作,一个线程负责网络请求,还有一个线程负责页面的渲染等。

资源分配

  • 进程:每个进程都有自己独立的内存空间、系统资源(如文件描述符、打开的文件等)。不同进程之间的内存空间是相互隔离的,一个进程不能直接访问另一个进程的内存。这就保证了进程之间的独立性和安全性。例如,一个文本编辑器进程和一个音乐播放器进程,它们各自拥有自己的内存区域来存储程序代码、数据等,彼此互不干扰。
  • 线程:线程共享所属进程的资源,包括内存空间、文件句柄等。线程只拥有自己独立的栈空间和程序计数器等少量资源。由于线程共享进程的资源,所以线程之间的通信和数据共享相对容易,但也需要注意同步和互斥的问题,以避免数据不一致等错误。比如在一个多线程的数据库连接池进程中,各个线程可以共享连接池中的数据库连接资源。

调度与切换

  • 进程:进程的调度和切换涉及到内核态和用户态的转换,开销较大。当操作系统需要从一个进程切换到另一个进程时,需要保存当前进程的上下文(包括寄存器状态、内存映射等),然后加载另一个进程的上下文。这个过程涉及到大量的系统调用和资源的重新分配,因此切换时间较长。例如,当从一个游戏进程切换到一个办公软件进程时,系统需要进行复杂的上下文切换操作。
  • 线程:线程的调度和切换开销相对较小。由于线程共享进程的资源,在进行线程切换时,只需要保存和恢复线程的少量上下文信息(如栈指针、程序计数器等),不需要像进程切换那样进行大量的资源重新分配。所以线程的切换速度比进程快得多,能够更高效地利用 CPU 资源。比如在一个多线程的图像处理程序中,不同线程之间的切换可以快速进行,以实现图像的并行处理。

并发性

  • 进程:进程之间可以并发执行,多个进程可以在同一时间内交替使用 CPU 资源。但由于进程的独立性和切换开销大,进程级的并发通常需要操作系统进行复杂的调度和管理。例如,在一个多核处理器的系统中,多个进程可以同时在不同的核心上运行,实现真正的并行处理。
  • 线程:线程的并发性更高。一个进程内的多个线程可以同时执行不同的任务,充分利用 CPU 的多核特性。线程之间的并发执行可以通过线程调度器进行高效管理,实现更细粒度的并发控制。例如,在一个视频编辑软件中,多个线程可以同时进行视频解码、音频处理、画面渲染等任务,提高软件的处理效率。

健壮性与容错性

  • 进程:由于进程之间相互独立,一个进程的崩溃通常不会影响其他进程的正常运行。例如,当一个浏览器插件进程崩溃时,一般不会导致整个浏览器进程崩溃,用户可以继续使用浏览器的其他功能。这使得进程在健壮性和容错性方面表现较好。
  • 线程:线程之间共享进程的资源,一个线程的崩溃可能会导致整个进程崩溃。因为线程的错误可能会破坏共享的内存空间或其他资源,影响到其他线程的正常执行。例如,在一个多线程的服务器程序中,如果一个线程出现内存访问错误,可能会导致整个服务器进程崩溃。

创建与销毁开销

  • 进程:创建和销毁进程的开销较大。创建一个进程需要分配内存空间、初始化系统资源、加载程序代码等一系列操作;销毁进程时也需要释放这些资源。例如,在一个大型的分布式系统中,频繁创建和销毁进程会消耗大量的系统资源,影响系统的性能。
  • 线程:创建和销毁线程的开销相对较小。线程的创建只需要分配少量的栈空间和初始化线程的上下文信息;销毁线程时也只需要释放这些少量的资源。因此,在需要频繁创建和销毁执行单元的场景下,使用线程更为合适。比如在一个网络爬虫程序中,为每个网页请求创建一个线程可以更高效地处理大量的请求。

综上所述,进程和线程在定义、资源分配、调度切换、并发性、健壮性以及创建销毁开销等方面都存在明显的区别。在实际应用中,需要根据具体的需求和场景来选择使用进程还是线程,或者将它们结合使用,以达到最佳的性能和效果。

http2.0与1.0相比优化

面试官您好,HTTP 2.0 是 HTTP 协议的下一代版本,相较于 HTTP 1.0 有诸多显著的优化,极大地提升了网络传输性能和效率,以下为您详细介绍:

二进制分帧

  • HTTP 1.0 问题:HTTP 1.0 基于文本格式传输数据,数据以换行符分割的 ASCII 文本形式发送。这种文本协议在解析时容易出错,而且解析效率较低,不同的字符编码也可能带来兼容性问题。
  • HTTP 2.0 优化:HTTP 2.0 将所有传输的信息分割为更小的消息和帧,并采用二进制格式编码。二进制分帧层位于应用层(HTTP 语义)和传输层(TCP/UDP)之间。这样做使得协议解析更加高效、准确,避免了文本解析的模糊性,也提高了传输效率。

多路复用

  • HTTP 1.0 问题:HTTP 1.0 是串行请求的,同一时间一个连接只能处理一个请求。当有多个请求时,需要依次排队等待处理,如果前面的请求处理缓慢,后面的请求就会被阻塞,即出现“队头阻塞”问题。而且为了提高并发性能,客户端往往需要创建多个 TCP 连接,但过多的连接会消耗大量的服务器资源和网络带宽。
  • HTTP 2.0 优化:HTTP 2.0 允许在一个 TCP 连接上同时发起多个请求和响应,这些请求和响应可以交错进行,并且能够独立地发送和接收,互不干扰。每个请求和响应都有一个唯一的流 ID,通过流 ID 可以将不同的帧重新组合成完整的消息。这样就解决了“队头阻塞”问题,提高了连接的利用率和传输效率。

头部压缩

  • HTTP 1.0 问题:HTTP 1.0 的请求和响应头部包含了大量的信息,如 Cookie、User - Agent 等,而且每次请求都会重复发送这些头部信息,导致传输效率低下,尤其是在移动网络环境下,会浪费大量的带宽和时间。
  • HTTP 2.0 优化:HTTP 2.0 使用 HPACK 算法对头部进行压缩。HPACK 算法会在客户端和服务器端维护一个静态表和动态表,对于经常出现的头部字段和值,只需要发送一个索引,而不是完整的头部信息。对于新的头部字段和值,会添加到动态表中,并使用索引进行传输。这样大大减少了头部的传输量,提高了传输效率。

服务器推送

  • HTTP 1.0 问题:在 HTTP 1.0 中,客户端发起请求,服务器响应请求,是一种被动的请求 - 响应模式。如果客户端需要获取多个资源,需要多次发起请求,增加了延迟。
  • HTTP 2.0 优化:HTTP 2.0 支持服务器推送功能,服务器可以在客户端请求之前,主动将客户端可能需要的资源推送给客户端。例如,当客户端请求一个 HTML 页面时,服务器可以同时推送该页面所需的 CSS、JavaScript 等资源,减少了客户端的请求次数,提高了页面的加载速度。

优先级和流量控制

  • HTTP 1.0 问题:HTTP 1.0 没有提供有效的机制来指定请求的优先级,服务器只能按照请求到达的顺序依次处理,无法根据资源的重要性进行优化。
  • HTTP 2.0 优化:HTTP 2.0 允许为每个流(即每个请求和响应)分配一个优先级,服务器可以根据优先级来决定资源的分配和处理顺序,优先处理重要的请求。同时,HTTP 2.0 还支持流量控制,客户端和服务器可以通过设置窗口大小来控制数据的发送速率,避免网络拥塞。

综上所述,HTTP 2.0 在性能、效率和功能上相较于 HTTP 1.0 有了很大的提升,能够更好地满足现代网络应用对高速、高效数据传输的需求。

最终环节:

确认到岗时间 实习天数

反问环节:

还需要提升哪些方向? springcloud springcloudalibaba

主要工作内容: 协助前端写后端接口 主要微服务框架下 高并发 内部网站开发等

#选择和努力,哪个更重要?##牛客创作赏金赛#
时雨h 的实习 面试记录 文章被收录于专栏

时雨h 的实习 面试记录

全部评论
这才是佬啊
2 回复 分享
发布于 02-16 00:30 陕西
这就是哈基雨吗?膜拜大佬
2 回复 分享
发布于 02-15 18:50 湖北
能学到东西吗xd
1 回复 分享
发布于 02-24 12:28 陕西
大二上学期学的,都忘了差不多,我看你写的篇子,有好多在书上没有的
1 回复 分享
发布于 02-22 15:57 河南
这些岗是哪投的,ssob吗
1 回复 分享
发布于 02-19 16:35 陕西
没有要求写算法吗
1 回复 分享
发布于 02-17 14:31 北京
强呀
1 回复 分享
发布于 02-17 01:30 四川
举报了
1 回复 分享
发布于 02-17 01:30 四川
这么快就拿到了,太强了
1 回复 分享
发布于 02-17 00:01 四川
佬学得好快,我记得你十一月还不会这些呢
1 回复 分享
发布于 02-16 19:24 河南
我靠,好难
1 回复 分享
发布于 02-16 12:09 上海
薪资如何
点赞 回复 分享
发布于 02-18 13:00 江苏

相关推荐

评论
19
60
分享

创作者周榜

更多
牛客网
牛客企业服务