2024面试offer收割宝典阿里篇

1.讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当 new 的时候,他们的执行顺序。

 在Java中,类的实例化顺序遵循以下规则:
1. 父类静态初始化块和静态字段:

  • 首先,当创建子类对象时,JVM会检查其父类。如果父类中有静态初始化块或静态字段,那么这些静态部分会按照他们在源代码中的顺序执行和初始化。
    // 父类
    public class Parent {
        static {
            // 静态初始化块
            System.out.println("Parent static initializer block");
        }
        static int parentStaticField = 1; // 静态字段
    
        // 构造函数等...
    }
    

2. 子类静态初始化块和静态字段:

  • 在父类的静态内容初始化完成后,接着初始化子类的静态初始化块和静态字段,同样按照它们在源代码中的顺序。 
    // 子类
    public class Child extends Parent {
        static {
            // 子类静态初始化块
            System.out.println("Child static initializer block");
        }
        static int childStaticField = 2; // 子类静态字段
    
        // 构造函数等...
    }
    

3. 父类实例初始化:

  • 当new一个子类对象时,首先调用父类的构造函数(默认或者明确指定的)。如果父类有实例初始化块(非静态初始化块),它会在构造函数体执行前运行。
    // 父类构造函数
    public Parent() {
        System.out.println("Parent constructor");
    }
    
    // 实例初始化块(假设存在)
    {
        System.out.println("Parent instance initializer block");
    }
    

4. 父类实例字段:

  • 父类构造函数中或初始化块中对实例字段的赋值操作。

5. 子类实例初始化块:

  • 接着执行子类的实例初始化块。 
// 子类实例初始化块
{
    System.out.println("Child instance initializer block");
}

6. 子类构造函数:

  • 最后,调用子类的构造函数。子类构造函数的第一条语句通常是super()来调用父类构造函数。如果没有显式调用,编译器会自动插入这条语句。 
    // 子类构造函数
    public Child() {
        super(); // 如果没有显示写出,会自动调用
        System.out.println("Child constructor");
    }
    
    // 子类实例字段赋值(在构造函数内完成)
    int childInstanceField = 3;
    

总结一下,实例化顺序为:

  • 父类静态初始化块
  • 父类静态字段
  • 子类静态初始化块
  • 子类静态字段
  • 父类构造函数
  • 父类实例初始化块
  • 父类实例字段赋值
  • 子类实例初始化块
  • 子类构造函数
  • 子类实例字段赋值

2.用过哪些 Map 类,都有什么区别,HashMap 是线程安全的吗,并发下使用的 Map 是什么,他们内部原理分别是什么,比如存储方式, hashcode,扩容,默认容量等。

在Java中,常见的Map实现类包括:
1. HashMap:

  • HashMap是非线程安全的,在多线程环境下如果多个线程同时对HashMap进行put、remove等操作可能会导致数据不一致或死锁等问题。
  • 内部实现基于哈希表(数组+链表/红黑树),通过key的hashCode值经过扰动函数计算出数组索引位置,并且当碰撞发生时采用链表或红黑树解决冲突。
  • 默认初始容量是16,扩容因子为0.75,即当元素数量达到阈值(容量 * 扩容因子)时会自动扩容为原来的两倍。
  • HashMap允许键和值都为null,但只有一个键可以为null

2. Hashtable:

  • Hashtable是线程安全的,它与HashMap类似,也是基于哈希表实现,但由于其同步机制,性能相对较低。
  • Hashtable不允许键和值为null

3. ConcurrentHashMap(并发场景使用):

  • ConcurrentHashMap从Java 5开始提供,它是线程安全的,并且在Java 8之后进行了重大优化,放弃了原有的分段锁机制,改用了一种更细粒度的CAS+Synchronized组合锁的方式,使得在高并发环境下的性能大大提高。
  • 内部同样采用了数组+链表/红黑树的数据结构,并针对并发访问做了优化设计,例如segment分段、CAS操作等。

4. TreeMap:

  • TreeMap是一个有序的Map实现,它的内部实现基于红黑树(自平衡二叉查找树),因此所有的键值对都是按照键的自然顺序或者定制的比较器排序的。
  • TreeMap非线程安全,同样支持null值作为键或值但键必须可比较,要么实现Comparable接口,要么在创建时提供Comparator。

5. LinkedHashMap:

  • LinkedHashMap继承自HashMap,保留了插入顺序或者最近最少使用的顺序(LRU缓存淘汰策略取决于构造函数的选择)。
  • 不是线程安全的,但也提供了额外的功能,如遍历顺序的控制。

对于hashcode,所有Map实现类都会依赖于键对象的hashCode()方法来确定元素在内部存储结构中的位置合理的hashCode设计能够减少哈希冲突,提高查找效率。而扩容是为了保证随着元素增多,Map的性能不会大幅度下降,通过动态调整容量和重新分配元素来维持一定的性能水平。 


3.JAVA8 的 ConcurrentHashMap 为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计?

在Java 8之前的版本中,ConcurrentHashMap使用了分段锁(Segment)的设计来实现并发控制每个Segment代表哈希表的一部分,这样可以将锁的粒度降低到一个segment级别,允许多个线程同时对不同segment进行读写操作,从而提高并发性能。

然而,这种设计存在以下问题:

  • 锁粒度不均衡:虽然理论上分段锁能提供细粒度的并发控制,但在实际应用中,数据分布并不总是均匀的,可能导致某些Segment上的竞争激烈,而其他Segment上几乎没有竞争,使得整体并发效率受限于最热点的那个Segment。
  • 空间开销:每个Segment都需要额外的空间和锁支持,对于小规模的数据集来说,这部分开销可能过大。
  • 扩展性限制:随着JVM的发展,尤其是引入了更强大的CAS原子指令后,基于无锁或者细粒度锁机制如CAS的操作能够更好地适应高并发环境,相比传统的ReentrantLock有更好的可伸缩性和性能表现。

在Java 8中,ConcurrentHashMap进行了重大的重构,放弃了原有的分段锁设计,转而采用了基于 CAS 和 synchronized 的组合技术,具体实现为:

  • 使用数组+链表/红黑树的数据结构。
  • 对每个桶(bucket)单独采用CAS操作进行更新,只有在CAS失败时才会尝试获取锁。
  • 当桶内元素数量超过阈值时,会通过扩容来减少碰撞,并且在扩容过程中同样利用了更加精细的锁策略来避免阻塞所有写操作。

如果我来设计,也会倾向于采取类似Java 8 ConcurrentHashMap的做法,即利用现代处理器提供的硬件特性(如CAS),结合synchronized以及自旋等技术,尽可能地减少锁的持有时间,并根据数据结构特点设计出既能保证并发安全又能最大化并发性能的方案。此外,还会考虑进一步优化锁的粒度,比如使用更加灵活的锁策略,以及在特定场景下采用无锁算法等手段。


4.有没有有顺序的 Map 实现类,如果有,他们是怎么保证有序的。

在Java中,有两个常见的Map实现类可以保证有序:
1. LinkedHashMap:

  • LinkedHashMap继承自HashMap,并添加了对元素插入顺序的维护。它通过使用一个双向链表(由每个条目的节点组成)来存储键值对,这样就可以按照插入顺序或访问顺序(取决于构造时的选择)来迭代其内容。当新的项被添加到映射中时,它们会被链接到链表的尾部,从而保持了插入顺序。
       Map<String, String> linkedHashMap = new LinkedHashMap<>();
       linkedHashMap.put("A", "Apple");
       linkedHashMap.put("B", "Banana");
       linkedHashMap.put("C", "Cherry");
    
       // 迭代输出将按插入顺序显示键值对
       for (Map.Entry<String, String> entry : linkedHashMap.entrySet()) {
           System.out.println(entry.getKey() + ": " + entry.getValue());
       }
       

2.  TreeMap:

  • TreeMap基于红黑树(自平衡二叉查找树)实现,它不是按照插入顺序排序,而是根据键的自然顺序(如果键实现了Comparable接口)或者自定义的Comparator排序。
       Map<String, String> treeMap = new TreeMap<>();
       treeMap.put("B", "Banana");
       treeMap.put("A", "Apple");
       treeMap.put("C", "Cherry");
    
       // 默认情况下,迭代输出将按升序排序显示键值对
       for (Map.Entry<String, String> entry : treeMap.entrySet()) {
           System.out.println(entry.getKey() + ": " + entry.getValue());
       }
    
       // 若要按自定义顺序排序,可以在创建时传入Comparator
       Comparator<String> reverseOrder = Collections.reverseOrder();
       Map<String, String> customSortedTreeMap = new TreeMap<>(reverseOrder);
       // ...
       

所以,如果你想保持插入顺序,应该选择LinkedHashMap;如果你需要键按照特定的排序规则排列,则应选择TreeMap。 


5.having 和 where 的区别? 

HAVING和WHERE在SQL查询中都用于过滤数据,但它们的应用场景和执行阶段有所不同:
1. WHERE子句:

  • WHERE子句是在SELECT语句执行前进行过滤的,它用于从数据库表或视图中筛选出满足特定条件的记录行。
  • WHERE子句中的条件可以涉及任何列(包括非聚合列),但是不能直接使用聚合函数(如SUM, AVG, MAX, MIN, COUNT)。
  • WHERE子句通常与单个行相关的条件一起使用。

示例: 

SELECT column1, column2
FROM table
WHERE column1 = 'value' AND column2 > 5;

2.HAVING子句:

  • HAVING子句则是对GROUP BY分组后的结果集进行过滤。也就是说,在进行了GROUP BY操作并对各个组应用了聚合函数之后,HAVING子句用来过滤那些不满足给定条件的组。
  • 只有在包含GROUP BY子句的查询中才能使用HAVING子句,并且HAVING子句中可以使用聚合函数以及其他在GROUP BY中出现过的列或表达式。

示例: 

SELECT column1, COUNT(column2)
FROM table
GROUP BY column1
HAVING COUNT(column2) > 10; -- 过滤出column1字段计数大于10的组

总结:

  • WHERE用于过滤原始数据源中的行记录,作用于单行级别。
  • HAVING则用于过滤GROUP BY分组后的统计结果,作用于分组级别,常常与聚合函数配合使用。

 6.游标的作用和使用?

数据库游标(Cursor)是一种用于遍历和操作数据库查询结果集的机制。它就像一个指针,允许您逐行访问从数据库查询返回的数据,而不是一次性获取所有记录。游标的使用场景通常涉及需要对大量数据进行逐行处理的情况,比如在循环中逐条更新、插入或删除记录,或者在PL/SQL等支持过程化编程的数据库环境中执行复杂的数据操作。
以下是游标的基本作用和使用步骤:
1. 游标的作用:

  • 逐行处理数据 - 游标使得应用程序能够按需一次处理一行数据,这对于大数据量操作时避免内存溢出很有帮助。
  • 事务控制 - 在处理数据的过程中,可以利用事务来确保数据的一致性,如果某个处理失败,可以回滚到未处理前的状态。
  • 精细控制 - 提供了更多细粒度的操作控制,如根据业务逻辑决定是否继续读取下一条记录或停止处理。

2. 游标的使用步骤: 以MySQL为例(不同数据库系统语法可能略有不同):

  • 声明游标: 
       DECLARE cursor_name CURSOR FOR SELECT column1, column2 FROM table_name WHERE condition;
       

    这里定义了一个名为cursor_name的游标,并指定了要从中检索数据的SELECT语句。

  • 打开游标:
       OPEN cursor_name;
       

            执行此命令后,游标开始指向查询结果集的第一行。

  • 提取数据: 使用FETCH命令获取当前指向的行的数据:
       FETCH cursor_name INTO variable1, variable2;
       

    执行此命令后,游标开始指向查询结果集的第一行。

  • 遍历数据: 重复FETCH命令直到没有更多的数据可提取为止,通常是通过循环结构实现:
       WHILE (condition) DO
         FETCH cursor_name INTO ...
         -- 对提取的数据进行处理...
       END WHILE;
       

  • 关闭游标: 当完成所有行的处理之后,关闭游标释放资源:
       CLOSE cursor_name;
       
  • 处理异常和回滚: 如果在处理过程中发生错误,可以在适当的地方使用ROLLBACK命令撤销已提交的更改。

请注意,在实际应用中,由于游标会导致额外的数据库资源消耗,因此应当谨慎使用,并尽可能优化查询性能,例如通过批量处理或直接SQL操作替代游标

对于大部分现代数据库应用场景,尤其是那些强调高并发和高性能的应用,应优先考虑其他更高效的方法,如批量INSERT、UPDATE和JOIN等操作。 


7.如何使用数据库中的定时器?触发器?定时任务?Oracle 中如何实现递归查询?

在MySQL中,定时器、触发器和定时任务的使用方式如下:
1. 定时器与定时任务

  • MySQL没有内置的定时器功能,但可以通过事件调度(Event Scheduler)实现定时任务。
  • 创建一个MySQL定时任务(事件): 
    CREATE EVENT your_event_name
    ON SCHEDULE EVERY 1 DAY STARTS '2024-02-20 10:30:00' -- 每天10:30执行
    DO
    BEGIN
      CALL YOUR_PROCEDURE(); -- 调用存储过程或执行SQL语句
    END;
    

    要启用MySQL事件调度器,请确保服务器已启动了事件调度器服务,并且全局变量event_scheduler设置为ON,可以通过以下命令查看和设置:

    -- 查看事件调度器状态
    SHOW VARIABLES LIKE 'event_scheduler';
    
    -- 启动事件调度器
    SET GLOBAL event_scheduler = ON; -- 或者在my.cnf配置文件中永久开启
    
    -- 停止事件调度器
    SET GLOBAL event_scheduler = OFF;
    
    -- 删除事件
    DROP EVENT your_event_name;
    

2. 触发器

  • 触发器是MySQL中在特定数据库操作发生时自动执行的一段代码。例如,在插入、更新或删除表中的记录时触发 
    DELIMITER //
    CREATE TRIGGER your_trigger_name
    AFTER INSERT ON your_table
    FOR EACH ROW
    BEGIN
      -- 触发器逻辑,如:更新另一个表或检查约束条件等
    END; //
    DELIMITER ;
    

3. 在MySQL中实现递归查询

  • MySQL不支持WITH RECURSIVE这样的标准SQL语法进行递归查询,但可以通过自定义存储过程、临时表或者循环来模拟递归行为。
  • 这里以自连接的方式来展示如何处理树形结构的递归查询示例:假设有一个表示树形结构的数据表test_tree,包含id, parent_id字段:
    CREATE TABLE test_tree (
      id INT PRIMARY KEY,
      parent_id INT,
      name VARCHAR(50)
    );
    
    -- 插入一些测试数据...
    
  • 为了遍历这个树形结构,可以创建一个存储过程: 
    DELIMITER //
    CREATE PROCEDURE recursive_tree(IN root_id INT)
    BEGIN
      DECLARE done INT DEFAULT FALSE;
      DECLARE cur_id INT;
      DECLARE cur_pid INT;
      DECLARE cur_name VARCHAR(50);
      DECLARE cur_level INT DEFAULT 0;
      DECLARE c CURSOR FOR SELECT id, parent_id, name FROM test_tree WHERE parent_id = root_id;
      DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
    
      OPEN c;
    
      read_loop: LOOP
        FETCH c INTO cur_id, cur_pid, cur_name;
        IF done THEN
          LEAVE read_loop;
        END IF;
    
        -- 输出当前节点信息
        SELECT cur_id, cur_pid, cur_name, cur_level;
    
        -- 进行下一层递归
        CALL recursive_tree(cur_id);
    
      END LOOP;
    
      CLOSE c;
    END; //
    DELIMITER ;
    
    -- 执行递归查询
    CALL recursive_tree(NULL); -- 如果根节点的parent_id为NULL
    

    上述存储过程用于递归地查询树形结构的所有节点,但请注意,这不是非常高效的解决方案,尤其对于大型数据集。在实际应用中,可能需要优化查询方法或考虑将递归逻辑转移到应用程序端实现。

4. Oracle中实现递归查询

  • 递归查询通常用于处理树形结构数据或其他需要自连接的情况,可以使用Common Table Expression (CTE) 来实现递归查询,尤其是从Oracle 11g R2开始引入了WITH RECURSIVE语句:
    -- 假设我们有一个表示树形结构的数据表TEST_TREE,包含ID, PID, IND, NAME字段
    -- ID为节点ID,PID为父节点ID
    
    WITH RECURSIVE tree_traversal (id, pid, ind, name, level) AS (
      SELECT id, pid, ind, name, 1 as level
      FROM TEST_TREE
      WHERE pid IS NULL -- 根节点
    
      UNION ALL
    
      SELECT t.id, t.pid, t.ind, t.name, tt.level + 1
      FROM TEST_TREE t
      JOIN tree_traversal tt ON t.pid = tt.id
    )
    SELECT * FROM tree_traversal ORDER BY level, ind;
    

    在这个例子中,递归查询遍历了整个树形结构,并返回了每一层节点的信息。第一部分是初始查询(根节点),第二部分是递归部分,通过自连接将每个节点与其父节点相连,从而构建出完整的树形路径。


8.高并发下如何保证修改数据安全 

在高并发环境下保证修改数据的安全,通常需要采用以下几种策略和机制:
1. 事务管理:

  • 使用数据库事务来确保ACID(原子性、一致性、隔离性和持久性)属性。通过BEGIN TRANSACTION、COMMIT和ROLLBACK语句来控制事务的边界,并确保即使在并发情况下,对同一行或多行数据的修改也能保持正确的一致性。

2. 并发控制:

  • 锁机制:数据库系统提供了多种锁类型如行级锁、表级锁、页级锁以及乐观锁(如MySQL中的行版本控制MVCC)。例如,在Java中使用JDBC时可以调用Connection对象的方法获取排它锁或共享锁,以防止多个事务同时修改同一行数据。 
         // JDBC示例,获取排他锁
         String sql = "SELECT * FROM your_table WHERE id = ? FOR UPDATE";
         preparedStatement = connection.prepareStatement(sql);
         preparedStatement.setInt(1, someId);
         preparedStatement.executeQuery();
         
  •  悲观锁与乐观锁:悲观锁假设会发生并发冲突,因此在访问数据前先加锁;而乐观锁则在更新数据时才检查是否发生冲突,常见的实现是利用版本号或者时间戳字段。

3. 隔离级别:

  • 设置合适的事务隔离级别(如SQL标准定义的READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE),避免脏读、不可重复读和幻读等问题。

4. 队列处理:

  • 对于批量修改或高并发场景,可以考虑将请求放入消息队列,后台服务按照一定的顺序或策略逐个处理,从而降低并发压力并实现串行化操作。

5. 分布式锁:

  • 在分布式环境中,可以使用分布式锁服务(如ZooKeeper、Redis等)来同步对共享资源的访问。

6. 数据库优化:

  • 数据库索引优化、查询优化减少锁的持有时间,合理设计数据表结构以减小锁的粒度。
  • 使用批量插入、更新操作来减少网络往返次数和锁定资源的时间。

7. 应用层面控制:

  • 如果业务允许,可以通过应用程序逻辑实现幂等性操作,这样即使在网络异常或并发问题导致的重试中也不会出现数据不一致的情况。

8. 分库分表:

  • 针对大数据量和高并发情况下的单表热点问题,可以通过分库分表技术分散读写压力,并结合分布式事务协调器来处理跨表的事务操作

综合运用以上策略,可以在高并发环境下有效提高数据修改的安全性和一致性。实际选择哪种方法取决于具体的业务需求和技术架构。


9.MyBatis 实现一对一有几种方式?具体怎么操作的?

在MyBatis中,实现一对一关系主要有两种方式:
1. 嵌套查询(Nested Select)

  • 使用这种方式时,我们不在映射文件中配置association标签,而是直接在一个select语句中通过子查询或者JOIN操作获取关联对象的数据。例如,在查询主表的同时通过JOIN或子查询得到从表的记录。
   <!-- UserMapper.xml -->
   <select id="selectUserWithAddress" resultType="com.example.User">
       SELECT u.*, a.* FROM user u
       LEFT JOIN address a ON u.id = a.user_id
       WHERE u.id = #{userId}
   </select>
   
   <!-- User.java -->
   public class User {
       private Long id;
       // 其他字段...
       private Address address;  // 用户地址对象
   }
   

2. 结果映射关联(Association)

  • 在映射文件中使用<association>标签来指定一对一关联关系。这样MyBatis会在查询主表后,根据配置执行一个新的查询以获取关联对象的数据。
       <!-- UserMapper.xml -->
       <resultMap id="userResultMap" type="com.example.User">
           <id property="id" column="user_id"/>
           <!-- 主表其他字段映射... -->
           <association property="address" javaType="com.example.Address">
               <id property="id" column="address_id"/>
               <!-- 从表其他字段映射... -->
               <select>SELECT * FROM address WHERE user_id = #{id}</select>
           </association>
       </resultMap>
    
       <select id="selectUserWithAddress" resultMap="userResultMap">
           SELECT * FROM user WHERE id = #{userId}
       </select>
       

在上述例子中,User类有一个Address类型的属性,通过配置映射关系,当查询用户信息时会同时查询并填充用户的地址信息。在结果映射关联方式下,关联查询可以自定义SQL和条件,比如通过#{id}引用外层查询的结果作为内层查询的条件。 


10. linux 命令,如何改文件权限。

在Linux系统中,修改文件或目录权限的命令是chmod。以下是一些基本用法和示例:
1. 数字模式(Numeric Mode):
通过数字来表示用户、组和其他用户的读写执行权限,每种权限对应一个数字:

  • r (读) = 4
  • w (写) = 2
  • x (执行) = 1

没有权限则为0。
例如,要将文件设置为所有者可读写执行,组内成员可读写,其他用户无任何权限,可以使用如下命令:

chmod 760 filename

        这里,7代表所有者有rwx权限(4+2+1),6代表组有rw权限(4+2),而0代表其他用户没有任何权限。
2. 符号模式(Symbolic Mode):
符号模式更加直观,可以直接指定增加或删除哪些权限。

  • u - 表示所有者(user)
  • g - 表示组(group)
  • o - 表示其他用户(others)
  • a - 表示所有用户(all)

权限操作符包括:

  • + - 添加权限
  • - - 删除权限
  • = - 设置为指定权限(覆盖原有权限)

例如,要给文件的所有用户添加执行权限,并移除组和其他用户的写权限,可以使用如下命令:

chmod a+x,g-w,o-w filename

 递归修改目录及其子目录下的权限:
如果需要修改目录及其包含的所有文件和子目录的权限,可以使用-R选项进行递归操作。

chmod -R u=rwX,g=rX,o=rX directory

上述命令会递归地给予所有者读写执行权限(rwX意味着如果有x权限,则保留或添加;如果没有则仅添加读写权限),给予组只读和执行权限,以及其他用户只读权限。
 

注意:执行以上命令时,请确保以具有相应权限的用户身份运行,否则可能会因权限不足导致无法修改文件权限。同时,X权限操作符在符号模式下只会对目录和已有执行权限的文件添加执行权限。


11.数组、链表(单向、双向、双端)、栈和队列、二叉树、红黑树、 哈希表、堆(最大和最小)

1. 数组(Array):

  • 数组是一种线性数据结构,它在内存中占用连续的空间来存储相同类型的数据元素。
  • 每个元素通过索引访问,索引通常是整数,表示元素在数组中的位置。数组支持随机访问,插入和删除操作可能需要移动后续元素以保持连续性,因此在动态变化的场景下效率较低。

2. 链表(单向、双向、双端):

  • 单向链表:每个节点包含一个数据域和一个指向下一个节点的指针域。只能从前往后遍历。
  • 双向链表:每个节点有两个指针,一个指向前一个节点,另一个指向后一个节点,允许双向遍历。
  • 双端链表(如Java中的LinkedList类):除了具备双向链表特性外,还提供了对头尾节点的快速访问能力。

3. 栈(Stack):

  • 栈是一种遵循“后进先出”(Last In First Out, LIFO)原则的线性数据结构
  • 只提供两个基本操作:push(入栈,将元素添加到栈顶)和pop(出栈,移除并返回栈顶元素)。常用于函数调用堆栈、表达式求值等场景。

4. 队列(Queue):

  • 队列是遵循“先进先出”(First In First Out, FIFO)原则的线性数据结构
  • 主要操作包括enqueue(入队,在队尾添加元素)和dequeue(出队,移除并返回队首元素)。
  • 适用于处理任务排队、消息传递等场景。

5. 二叉树(Binary Tree):

  • 二叉树是一种非线性数据结构,每个节点最多有两个子节点,通常被称作左孩子和右孩子。
  • 根据具体性质不同,有多种变体,如二叉搜索树(BST)、平衡二叉树(AVL树)等。

6. 红黑树(Red-Black Tree):

  • 红黑树是一种自平衡二叉查找树,每个节点都带有颜色属性,要么是红色要么是黑色。
  • 红黑树满足特定的性质,从而保证了任意节点到其所有后代叶子节点的最长路径不会超过最短路径的两倍,因此可以确保高效的查找、插入和删除操作。

7. 哈希表(Hash Table):

  • 哈希表是一种使用哈希函数实现键值对高效存取的数据结构。
  • 通过哈希函数将键映射到一个固定大小的数组中的位置,实现接近于常数时间复杂度的查找、插入和删除操作(理想情况下)。
  • 冲突解决策略通常采用开放地址法或链地址法(即链表或红黑树作为冲突元素的容器)。

8. 堆(Heap):

  • 最大堆:一种特殊的完全二叉树,对于任意非叶子节点 i ,其值(或键)总是大于或等于其子节点的值。主要用于实现优先队列,并且能够快速找到当前的最大元素。
  • 最小堆:与最大堆类似,但要求父节点的值小于或等于其子节点的值,这样最小元素总是在根节点上。同样适合实现优先队列功能,用于找出当前的最小元素。

12. 个人经验:栈和队列、哈希表、链表、二叉树的题较多,图的较少

更详细的数据结构解析可以参考数据结构专栏icon-default.png?t=N7T8https://blog.csdn.net/weixin_43285931/article/details/134932343


13. 查找:二分查找及其变形

二分查找(Binary Search)是一种在有序数组中查找特定元素的搜索算法。

其基本思想是从数组中间元素开始,如果中间元素正好是要查找的目标,则搜索结束;否则,如果目标值大于或小于中间元素,就将搜索范围缩小到中间元素对应区间的上半部分或下半部分,并重复此过程,直到找到目标值或者搜索区间为空。

  • 基础二分查找:
    public class BinarySearch {
        public static int binarySearch(int[] array, int target) {
            int left = 0;
            int right = array.length - 1;
    
            while (left <= right) {
                // 计算中间索引
                int mid = left + (right - left) / 2;
    
                // 比较中间元素和目标值
                if (array[mid] == target) {
                    // 找到目标值,返回其索引
                    return mid;
                } else if (array[mid] < target) {
                    // 目标值在右半部分,更新左边界
                    left = mid + 1;
                } else {
                    // 目标值在左半部分,更新右边界
                    right = mid - 1;
                }
            }
    
            // 如果循环结束还没找到目标值,说明目标值不在数组中
            return -1;
        }
    
        public static void main(String[] args) {
            int[] sortedArray = {2, 3, 6, 9, 15, 27, 38, 45, 67, 88};
            int target = 36;
            
            int result = binarySearch(sortedArray, target);
            if (result != -1) {
                System.out.println("元素 " + target + " 在数组中的索引为: " + result);
            } else {
                System.out.println("元素 " + target + " 不在数组中");
            }
        }
    }
    
  • 变形:在一个旋转排序数组中进行二分查找。假设一个原本有序的数组在某个点进行了旋转操作,例如 [0, 1, 2, 4, 5, 6, 7] 旋转后可能变成 [4, 5, 6, 7, 0, 1, 2],在这种情况下,普通的二分查找需要稍作修改才能正确工作:
public class RotatedBinarySearch {
    public static int rotatedBinarySearch(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return -1;
        }

        int left = 0;
        int right = nums.length - 1;

        while (left <= right) {
            int mid = left + (right - left) / 2;

            if (nums[mid] == target) {
                return mid;
            }

            // 左半边有序
            if (nums[left] <= nums[mid]) {
                if (target >= nums[left] && target < nums[mid]) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            } 
            // 右半边有序
            else {
                if (target > nums[mid] && target <= nums[right]) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
        }

        return -1;
    }

    public static void main(String[] args) {
        int[] rotatedArray = {4, 5, 6, 7, 0, 1, 2};
        int target = 2;
        
        int result = rotatedBinarySearch(rotatedArray, target);
        if (result != -1) {
            System.out.println("元素 " + target + " 在旋转数组中的索引为: " + result);
        } else {
            System.out.println("元素 " + target + " 不在旋转数组中");
        }
    }
}

14. 二叉树:前序、中序、后序遍历,按规定方式打印,两个节点之间操作(最近公共祖先、距离)等问题。

二叉树的遍历方式包括前序遍历(Preorder Traversal)、中序遍历(Inorder Traversal)和后序遍历(Postorder Traversal)。

二叉树遍历方法(前序、中序和后序)的优缺点主要与它们的顺序和用途有关:
1. 前序遍历(Root - Left - Right)

  • 优点:对于表达式树等具有特定结构的二叉树,前序遍历可以方便地生成或解析表达式。例如在编译器实现中,前序遍历可用于将抽象语法树转换为计算结果。
  • 缺点:如果是普通的二叉查找树(BST),前序遍历并不能直接体现其有序特性,不利于快速定位某个值。

2. 中序遍历(Left - Root - Right)

  • 优点:在二叉搜索树(BST)中,中序遍历可以得到一个递增排序的结果,因此常用于检查树是否是有效的BST以及进行排序操作。它能直观反映BST节点间的大小关系。
  • 缺点:如果树不是BST,则中序遍历没有特殊含义,无法直接反映数据的其他有用信息。

3. 后序遍历(Left - Right - Root)

  • 优点:在文件系统或其他需要“处理完子元素后再处理父元素”的场景下非常有用,如释放内存时需要先释放子节点再释放父节点。另外,在计算一棵满二叉树的所有叶子节点数或者计算表达式树的最终结果时,后序遍历是必需的。
  • 缺点:同样在BST中,后序遍历不能立即反映出树的有序性,且在一般情况下,由于最后访问根节点,不利于实时获取中间过程的结果。

4. 最近公共祖先(Lowest Common Ancestor, LCA):

  • 最近公共祖先是指在一个有根树中,给定两个节点p和q,找到一个节点x,它既是p的祖先也是q的祖先,并且x在所有这样的祖先中离根节点最远。在二叉搜索树中,可以通过比较节点值大小快速定位LCA;而在一般二叉树中,则通常需要递归或迭代遍历树结构来查找。 
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        while (root != null) {
            if (root.val > Math.max(p.val, q.val)) {  // 如果根节点大于两者,说明LCA在左子树中
                root = root.left;
            } else if (root.val < Math.min(p.val, q.val)) {  // 如果根节点小于两者,说明LCA在右子树中
                root = root.right;
            } else {  // 根节点在p和q之间或等于其中之一,说明找到了LCA
                return root;
            }
        }
        return null;  // 如果树为空或没有共同祖先,返回null
    }
    

5. 节点间的距离:

  • 计算两个节点在二叉树中的最长路径长度涉及确定它们各自的深度以及通过最近公共祖先进行计算。一种方法是分别从两个节点开始执行深度优先搜索以获取每个节点到根节点的深度,然后根据这两个深度计算最长路径。 

在实际应用中,选择哪种遍历方式取决于具体问题的需求和目的。而在算法效率方面,所有这些遍历方法的时间复杂度都是O(n),其中n是树中节点的数量,这是因为都需要遍历每个节点一次。但空间复杂度有所不同,基于栈或递归实现时,最坏情况下(完全不平衡的树)的空间复杂度也为O(n)。

以下是Java实现这三种遍历的方式,以一个通用的二叉树节点类TreeNode为基础:

// 定义二叉树节点类
public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int val) {
        this.val = val;
    }
}

// 前序遍历(根 - 左 - 右)
public void preorderTraversal(TreeNode root) {
    if (root != null) {
        System.out.print(root.val + " ");  // 打印根节点
        preorderTraversal(root.left);     // 遍历左子树
        preorderTraversal(root.right);    // 遍历右子树
    }
}

// 中序遍历(左 - 根 - 右)
public void inorderTraversal(TreeNode root) {
    if (root != null) {
        inorderTraversal(root.left);      // 遍历左子树
        System.out.print(root.val + " ");  // 打印根节点
        inorderTraversal(root.right);     // 遍历右子树
    }
}

// 后序遍历(左 - 右 - 根)
public void postorderTraversal(TreeNode root) {
    if (root != null) {
        postorderTraversal(root.left);     // 遍历左子树
        postorderTraversal(root.right);    // 遍历右子树
        System.out.print(root.val + " ");  // 打印根节点
    }
}

关于两个节点之间的操作:
最近公共祖先:找到给定两个节点在二叉树中的最近公共祖先节点。

public class TreeNode {
    // ... 其他属性和构造方法不变 ...
    
    // 最近公共祖先方法
    public TreeNode lowestCommonAncestor(TreeNode p, TreeNode q) {
        if (p == null || q == null) return null;

        // 如果当前节点为空或者当前节点是其中一个节点,则向上回溯
        if (this.val > Math.max(p.val, q.val)) {
            return lowestCommonAncestor(this.left, p);
        } 
        if (this.val < Math.min(p.val, q.val)) {
            return lowestCommonAncestor(this.right, p);
        }

        // 当前节点值位于p和q之间或等于其中之一,说明当前节点就是最近公共祖先
        return this;
    }
}

节点间的距离:计算两个节点在树中的最长路径长度。这个问题相对复杂一些,需要考虑多种情况,例如路径可能经过根节点也可能不经过根节点。一种可行的方法是分别从两个节点开始进行深度优先搜索(DFS),并记录下每个节点到根节点的深度,然后根据这两个深度计算最远的距离。

public int distanceBetweenNodes(TreeNode root, TreeNode node1, TreeNode node2) {
    Map<TreeNode, Integer> depths = new HashMap<>();
    dfs(root, depths, 0);

    return depths.get(node1) + depths.get(node2) - 2 * depths.get(lca(root, node1, node2));
}

private TreeNode lca(TreeNode root, TreeNode node1, TreeNode node2) {
    if (root == null || root == node1 || root == node2) {
        return root;
    }

    TreeNode leftLCA = lca(root.left, node1, node2);
    TreeNode rightLCA = lca(root.right, node1, node2);

    if (leftLCA != null && rightLCA != null) {
        return root;
    } else if (leftLCA != null) {
        return leftLCA;
    } else {
        return rightLCA;
    }
}

private void dfs(TreeNode node, Map<TreeNode, Integer> depths, int depth) {
    if (node == null) {
        return;
    }

    depths.put(node, depth);
    dfs(node.left, depths, depth + 1);
    dfs(node.right, depths, depth + 1);
}

15.Mybatis 是如何进行分页的?分页插件的原理是什么?

 MyBatis 进行分页操作通常是通过使用分页插件来实现的,这些插件能够在SQL执行前动态修改原始SQL语句以添加对应的分页参数。具体原理如下:

  • 配置分页插件: 在MyBatis的配置文件(如mybatis-config.xml)中,需要添加和配置分页插件,例如PageHelper、Mybatis-Pageable等。配置通常包括指定哪些SQL ID需要进行分页处理、数据库类型以及可能的其他自定义选项。
  • 拦截SQL执行: 当SqlSession执行查询方法时,分页插件会作为一个拦截器(Interceptor)被注册到MyBatis的Executor执行链中。当拦截到一个匹配SQL ID的查询请求时,插件就会介入执行流程。
  • 动态生成分页SQL: 分页插件会在实际执行SQL之前,根据传入的分页参数(如当前页码、每页大小)动态修改原SQL语句。对于支持limit和offset语法的数据库(如MySQL),插件会将原SQL拼接上LIMIT m OFFSET n这样的分页条件;对于不支持相同语法的数据库,插件会按照对应数据库的分页方式生成相应的SQL。
  • 执行并返回结果: 经过分页插件处理后的SQL语句会被传递给JDBC进行实际执行,并获取符合分页条件的数据。最终,插件会包装好包含总记录数和当前页数据的结果集返回给调用方。

以PageHelper为例,开发者在代码中设置分页参数后,插件会在SQL执行前完成分页逻辑的处理,简化了手动编写分页SQL的工作。


 16.Delete 误删数据没有备份怎么恢复?

在MySQL中,如果数据被误删且没有可用的备份(包括物理备份或逻辑备份以及binlog日志),恢复数据通常会变得非常困难,尤其是对于InnoDB存储引擎。但仍有几种可能的方法尝试恢复,这些方法的成功率取决于具体情况和数据库的状态:
1. 未开启binlog: 如果MySQL服务器未开启二进制日志(binlog),那么无法通过回滚事务来恢复数据。此时只能尝试以下非标准方法:

  • 使用文件系统级别的恢复工具(例如,在数据文件被覆盖之前如果有文件系统的快照或者磁盘镜像)。
  • 寻求专业的数据恢复服务,他们可能能通过底层存储分析来找回部分数据。

2. 已开启binlog: 如果MySQL开启了binlog,并且binlog文件还在,理论上可以通过解析binlog来找到误删前的数据状态并进行恢复。可以使用mysqlbinlog工具结合SQL语句重新执行以恢复数据。
 

3. InnoDB事务回滚段(Undo Logs): 在极少数情况下,如果删除操作刚发生不久,InnoDB存储引擎内部的undo日志可能还保留着足够的信息用于回滚事务,但这需要在事务提交之前并且undo日志还没有被清理的情况下才能实现。
 

4.数据页碎片: 如果数据文件中尚未被新数据覆盖的部分仍然包含已删除记录的信息,则有可能通过特定的数据恢复软件或服务去扫描和提取这些记录。
 

5. 操作系统层面恢复: 在数据文件被物理删除前,如果OS层面有备份、快照或者RAID等冗余机制,可以从这些来源尝试恢复。

总之,对于生产环境中的重要数据库,强烈建议定期进行全量和增量备份,并启用binlog以确保在出现类似问题时能够有效恢复数据。在实际操作中,请务必谨慎对待数据恢复工作,必要时应寻求专业人员的帮助。 


 17.MyBatis 实现一对多有几种方式,怎么操作的?

在MyBatis中,实现一对多关系通常有两种方式:
1. 基于嵌套查询(Nested Query): 嵌套查询是通过一次SQL查询语句来获取关联数据。这种方式在一个SQL查询里联接多个表,然后通过结果映射(resultMap)的collection元素解析出一对多的关系。
        例如,假设有一个Student和一个Course表,一个学生可以选修多门课程。在StudentMapper.xml中,可以如下配置:

   <resultMap id="studentResultMap" type="Student">
     <id property="id" column="student_id"/>
     <!-- 其他属性映射 -->
     <collection property="courses" ofType="Course">
       <select>
         SELECT * FROM Course WHERE student_id = #{id}
       </select>
     </collection>
   </resultMap>

   <select id="getStudentWithCourses" resultMap="studentResultMap">
     SELECT * FROM Student WHERE id = #{id}
   </select>
   

在这里,当查询一个学生时,会同时执行一个子查询去获取与该学生关联的所有课程。

2. 基于嵌套结果(Nested Results): 嵌套结果则是通过JOIN操作将关联表的数据一次性查出,并在结果映射中定义如何合并这些结果。这样只需要执行一次数据库查询。
        对于上述例子,在StudentMapper.xml中可能有这样的配置:

   <resultMap id="studentResultMap" type="Student">
     <id property="id" column="student_id"/>
     <!-- 其他属性映射 -->
     <collection property="courses" ofType="Course" column="{student_id=student_id}">
       <result property="courseId" column="course_id"/>
       <!-- 其他Course属性映射 -->
     </collection>
   </resultMap>

   <select id="getStudentWithCourses" resultMap="studentResultMap">
     SELECT s.*, c.* FROM Student s
     LEFT JOIN Course c ON s.id = c.student_id
     WHERE s.id = #{id}
   </select>
   

在这个例子中,我们使用JOIN查询联合了Student和Course表,并在结果映射中指定了如何从联合查询的结果集中提取并组装成一对多的对象结构。注意column属性用于指定外键关联字段。


18.什么是 MyBatis 的接口绑定?有哪些实现方式?

MyBatis 的接口绑定是指将自定义的 Java 接口与 XML 映射文件或注解关联起来的过程。在 MyBatis 中,我们通常会创建一个 Mapper 接口来定义数据访问层的操作方法,如 CRUD(Create, Read, Update, Delete)操作。通过接口绑定,当调用接口中的方法时,MyBatis 会根据配置找到对应的 SQL 语句并执行数据库操作。
1. MyBatis 提供了两种主要的接口绑定实现方式:

  • 基于 XML 文件的映射器配置: 在这种方式下,你需要创建一个接口和对应的 XML 映射文件。XML 文件中定义了接口中每个方法所对应的 SQL 语句、参数类型和返回结果类型。接口的方法名应与 XML 文件中定义的 SQL 映射 id 相同,同时输入输出参数类型也需对应一致。XML 文件中的 namespace 属性应当设置为该 Mapper 接口的全限定类名。
       // UserMapper.java 接口
       public interface UserMapper {
           User selectUserById(Integer id);
       }
    
       <!-- UserMapper.xml -->
       <?xml version="1.0" encoding="UTF-8" ?>
       <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
       <mapper namespace="com.example.mapper.UserMapper">
           <select id="selectUserById" resultType="com.example.model.User">
               SELECT * FROM users WHERE id = #{id}
           </select>
       </mapper>
       

2. 基于注解的映射器配置:

  • 使用注解的方式可以减少对 XML 配置文件的依赖。在 Mapper 接口中直接使用 MyBatis 提供的注解来定义 SQL 语句。例如,可以使用 @Select、@Insert、@Update 和 @Delete 注解来替代 XML 中的 SQL 映射
       // 使用注解的 UserMapper.java
       import org.apache.ibatis.annotations.Select;
    
       public interface UserMapper {
           @Select("SELECT * FROM users WHERE id = #{id}")
           User selectUserById(Integer id);
       }
       

    这两种方式都可以实现 MyBatis 的接口绑定,开发者可以根据项目需求和团队规范选择合适的绑定方式。


 19.使用 MyBatis 的 mapper 接口调用时有哪些要求?

在使用 MyBatis 的 mapper 接口调用时,需要遵循以下要求:
1. Mapper 接口与 XML 映射文件的关联:

  • Mapper 接口所在的包名和类名应与 XML 映射文件中的 namespace 属性完全一致。例如,如果接口是 com.example.mapper.UserMapper,那么对应的 XML 文件中 namespace 应为 "com.example.mapper.UserMapper"。

2. 方法名与 SQL 映射 ID 的匹配:

  • Mapper 接口中定义的方法名必须与 XML 映射文件中定义的 SQL 语句的 id 相同。例如,在 UserMapper 接口中有一个方法 selectUserById(Integer id),则对应的 XML 文件中应该有一个 <select id="selectUserById"> 标签。

3. 输入参数类型匹配:

  • Mapper 接口方法的输入参数类型要与 XML 中定义的 SQL 语句的 parameterType 属性所指定的类型相同。例如,如果方法接收一个 Integer 类型的参数,那么映射文件中的 parameterType 应该是 "java.lang.Integer" 或相应的全限定类名。

4. 输出结果类型匹配:

  • 方法的返回值类型(或方法内部的泛型)应与 XML 中 <select>、<insert>、<update> 或 <delete> 等标签的 resultType 或 resultMap 属性指定的类型一致。这表示方法执行后返回的结果集将按照这个类型进行转换和封装。

5. 方法签名的完整性和可空性:

  • 如果 SQL 执行的结果可能为空,mapper 接口方法可以返回包装类型(如 Optional<T>),或者返回集合类型但允许集合为空。

6. 事务管理:

  • 在实际应用中,调用 mapper 接口方法通常需要在一个事务环境中进行,确保数据操作的原子性和一致性。

通过遵循以上规则,MyBatis 能够正确解析并执行 SQL,将数据库操作结果自动映射到 Java 对象上。


 20. 介绍下你理解的操作系统中线程切换过程。

操作系统中的线程切换(Thread Switch)是指在多线程环境下,当CPU从执行一个线程的任务转移到执行另一个线程的任务时所进行的一系列操作。

线程切换可以是内核级线程的切换(Kernel-level thread switching)或用户级线程的切换(User-level thread switching),下面主要介绍的是由操作系统内核管理的内核级线程切换的过程
内核级线程切换的主要步骤通常包括:
1. 保存当前线程上下文:

  • 当操作系统决定要将CPU控制权从当前运行的线程转移到另一个线程时,首先需要保存当前线程的状态信息,这被称为“上下文”(Context)。上下文包括但不限于程序计数器(PC,记录下一条指令的位置)、处理器寄存器、堆栈指针以及其它任何与线程执行状态相关的硬件状态信息。

2. 更新进程控制块(Process Control Block, PCB):

  • 操作系统维护每个线程的PCB,其中存储了线程的上下文信息和状态。在切换过程中,需要更新当前线程的PCB,将其状态标记为就绪或者阻塞等,并可能更新调度优先级等信息。

3. 选择下一个线程:

  • 根据操作系统的线程调度策略(如时间片轮转、优先级调度等),选择一个新的线程来获得CPU资源并开始执行。这个过程涉及到查找就绪队列中下一个应该被调度的线程。

4. 恢复目标线程上下文:

  • 从新选中线程的PCB中取出其先前保存的上下文信息,包括程序计数器、寄存器值等,并将它们加载到CPU相应的寄存器中。

6. 切换页表或地址空间(如果适用):

  • 如果线程属于不同的进程,还可能涉及虚拟内存管理中的地址空间切换,即更新MMU(内存管理单元)以指向新的线程所在进程的页表。

7. 开始执行新线程:

  • 完成上述步骤后,CPU就可以从新线程上次中断的地方继续执行,仿佛它从未被打断过一样。

线程切换是一个相对复杂且开销较大的操作,频繁的线程切换会消耗大量的CPU时间,因此操作系统需要通过高效的调度算法来减少不必要的线程切换。

在现代操作系统中,为了提高性能,还会采用硬件支持的技术,如超线程技术(Hyper-Threading)和多核架构等来并发地执行多个线程,从而降低单个核心上的线程切换频率。


21. 进程和线程的区别。

进程和线程是操作系统中两个重要的概念,它们在资源分配、并发执行以及系统调度等方面具有显著的区别:
1. 资源分配:

  • 进程:操作系统分配资源的基本单位。每个进程都有自己独立的一组系统资源,如虚拟内存空间、打开的文件描述符、信号处理器、CPU时间片等。
  • 线程:是进程内部的执行单元,共享进程所拥有的大部分资源,如地址空间、全局变量、文件句柄等。但每个线程也有自己独立的栈空间和寄存器状态。

2. 并发执行:

  • 进程:多个进程可以在同一时间并行运行(如果硬件支持),并且它们各自有独立的内存空间,彼此互不影响。
  • 线程:一个进程中可以包含多个线程,这些线程在同一进程的地址空间内并发执行,相互之间可以直接读写相同的数据,无需通过IPC(进程间通信)机制。

3. 上下文切换:

  • 进程之间的上下文切换通常涉及到更多资源的保存和恢复(如地址空间、页表等),因此开销相对较大。
  • 线程之间的上下文切换则只涉及少量寄存器和栈信息的交换,所以相对而言,线程间的切换速度更快,开销更小。

4. 独立性:

  • 进程是程序的一次执行过程,它是操作系统能够独立调度和分派的基本单位,独立于其他进程运行。
  • 线程依赖于进程而存在,一个进程的所有线程都必须共享该进程的资源,并且一个线程的崩溃通常不会导致整个进程结束,除非所有非守护线程都结束或发生特定错误。

5. 创建与销毁:

  • 创建新进程时,操作系统需要为它分配独立的资源,包括内存空间等,开销较大。
  • 创建新线程时,由于共享父进程资源,所以开销相对较小,创建和销毁的速度也更快。

总结来说,进程提供了资源隔离和保护,适合于不同任务之间的隔离;而线程更适合于在一个进程中实现多个控制流的并发执行,以提高系统的并发性能。


 22. top 命令之后有哪些内容,有什么作用。

top 命令在Linux操作系统中是一个实时的系统监视工具,用于显示和监控当前系统中各个进程的状态信息。当你在终端执行 top 命令后,屏幕上会展示以下主要内容:
1. 系统总体信息:

  • 当前时间
  • 系统运行时长(自开机以来)
  • 系统平均负载(过去1分钟、5分钟、15分钟的平均负载数)
  • CPU使用情况(用户态CPU使用率、系统态CPU使用率、空闲CPU百分比、等待I/O完成的CPU百分比等)

2. CPU状态信息:

  • CPU占用率列表,通常按核心分组展示每个核心的使用情况。

3. 内存状态信息:

  • 物理内存总量、已使用量、空闲量、缓冲/缓存占用量等。
  • 交换空间的使用情况。

4. 任务(进程)列表:

  • PID(进程ID)
  • USER(拥有该进程的用户名)
  • PR(优先级)
  • NI(nice值,影响调度优先级)
  • VIRT(虚拟内存大小)
  • RES(常驻内存大小)
  • SHR(共享内存大小)
  • %CPU(最近一段时间内CPU占用率)
  • %MEM(内存占用率)
  • TIME+(累计CPU时间)
  • COMMAND(命令名或进程名称)


5. 动态更新与交互功能:

  • top 命令默认每几秒刷新一次数据(可以通过 -d 参数指定间隔),提供一个不断滚动更新的视图。
  • 提供多种交互操作,如排序(按F键选择排序字段)、过滤(例如只查看某个用户的进程,可以输入 u 用户名)、杀死进程(例如输入 k 进程号)等。

通过 top 命令,系统管理员可以直观地了解到系统的整体健康状况,包括哪些进程占用了大量的CPU资源、内存使用情况如何、是否存在过高的系统负载等问题,从而帮助进行性能调优和故障排查。


23. 线上 CPU 爆高,请问你如何找到问题所在。 

线上 CPU 爆高的问题排查通常涉及一系列系统监控和日志分析步骤。以下是一些基本的诊断思路:
1. 实时监控与初步定位:

  • 使用 tophtop 命令查看当前占用CPU较高的进程及其PID
  • 使用 ps -eo pid,ppid,user,%cpu,%mem,args --sort=-%cpu 查看按CPU使用率排序的所有进程详情。
  • 对于Java应用,可以使用 jstack 获取线程堆栈信息,找出消耗CPU最多的线程。

2. 系统日志分析:

  • 查看系统日志(如 /var/log/messages /var/log/syslog),查找是否有异常错误或警告信息。
  • 应用程序日志同样重要,特别是应用程序输出的日志级别更高的(如ERROR、WARN)内容,可能包含了CPU飙升的原因。

3. 性能分析工具:

  • 使用 strace 工具追踪指定进程的系统调用情况,看看是否有循环或者阻塞操作。
  • 对于Java应用,使用 jstatVisualVM 进行JVM性能分析,检查GC活动是否过于频繁或其他内存相关问题。
  • Linux下还可以使用 perf top 或 火焰图(Flame Graphs) 进行CPU性能分析,找出热点函数。

4. 数据库查询分析:

  • 如果是数据库相关的服务,检查SQL查询日志,查看是否存在慢查询或全表扫描等导致CPU过高的操作。

5. 负载均衡和网络流量监控:

  • 如果是集群环境,查看负载均衡器上的流量分布和后端服务器响应时间,判断是否存在某个节点处理压力过大导致CPU高。

6. 系统资源利用率观察:

  • 检查I/O吞吐量(例如使用 iostat)、内存使用情况(包括虚拟内存交换情况)、网络带宽使用情况等,以排除其它资源瓶颈引发的问题。

7. 持续监控与报警机制:

  • 完善监控告警体系,确保在CPU飙高时能及时收到通知,并且能够根据历史数据进行趋势分析,提前预警潜在风险。

通过以上步骤,一般可以找到造成CPU过高的原因,并针对性地优化代码、调整配置或扩容资源来解决问题。当然,在实际环境中,需要结合具体的应用场景和技术架构来进行细致深入的排查。


24.什么是 Spring MVC 框架的控制器? 

Spring MVC 框架的控制器(Controller)是该框架中用于处理用户请求的核心组件。在 Spring MVC 架构中,控制器负责接收来自客户端(如Web浏览器)的HTTP请求,并决定如何响应这些请求。控制器通常会解析请求参数、执行相应的业务逻辑(这可能涉及到与服务层或数据访问层的交互),然后选择合适的视图(View)将结果呈现给客户端。


具体来说,Spring MVC 中的控制器可以是一个实现了特定接口的类,或者更常见的是通过注解方式定义的类,比如使用 @Controller 注解标记一个Java类作为控制器。控制器类中的方法使用诸如 @RequestMapping 等注解来映射到不同的URL请求路径上。
        例如:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.ui.Model;

@Controller
public class ExampleController {

    @RequestMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("message", "Hello, Spring MVC!");
        return "hello"; // 返回的字符串代表视图名称,Spring 将会根据视图解析策略找到对应的视图模板进行渲染。
    }
}

在这个例子中,ExampleController 是一个Spring MVC 控制器,它有一个处理 /hello URL 请求的方法 hello()。当用户发起一个指向 /hello 的HTTP请求时,这个方法会被调用,并将一个消息添加到模型(Model)对象中,最后返回一个视图名,指示Spring MVC去查找并渲染名为 "hello" 的视图。这样就完成了从接收请求到生成响应的整个过程。


25.数据传输的事务定义有哪三种? 

  • 最多一次: 消息不会被重复发送,最多被传输一次,但也有可能一次不传
  • 最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输.
  • 精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的

26.谈谈对 Synchronized 关键字,类锁,方法锁,重入锁的理解 

在Java中,synchronized关键字用于实现线程间的同步和互斥访问,它提供了一种锁机制来确保多线程环境下的数据一致性。以下是对synchronized关键字以及类锁、方法锁、重入锁的理解:
1. Synchronized 关键字:

  • Synchronized 可以修饰实例方法或静态方法,也可以修饰代码块。
  • 当修饰实例方法时,锁定的是当前对象实例(即this),同一时间只有一个线程能够执行该方法。
  • 当修饰静态方法时,锁定的是类的Class对象,所有对该类的静态方法的调用都会被此锁控制,因此同一时间内只有一个线程能执行该类的所有静态同步方法。
  • 当修饰代码块时,可以指定一个具体的对象作为锁,只有获取到这个对象锁的线程才能进入同步代码块。

2. 类锁与方法锁:

  • 类锁:通常是指通过synchronized static修饰的静态方法或者synchronized修饰代码块时使用了类对象作为锁的情况。类锁作用于整个类,控制对类级别资源的并发访问。
  • 方法锁:则是指使用synchronized修饰的方法,其实质上也是通过方法对应的对象锁来实现的,对于非静态方法而言,是对象级别的锁;对于静态方法而言,是类级别的锁。

3. 重入锁 (Reentrant Lock):

  • Java中的synchronized关键字具有重入特性,这意味着当一个线程获得了某个对象的锁后,再次请求该对象的锁时是可以获得的,即同一个线程可以多次进入由它自己持有的同步代码块或方法。
  • 这个特性使得线程在调用包含synchronized的方法或代码块时,如果已经持有该对象的锁,则可以直接进入,不会发生阻塞,而是在完成操作后释放掉所有持有的锁。
  • 例如,在一个类内部有多个synchronized方法,若当前线程正在执行其中一个方法并持有该对象锁,此时它可以无阻塞地调用其他synchronized方法。

总结起来,synchronized关键字提供了一种内置的互斥机制,防止多个线程同时访问共享资源造成的数据不一致问题。

类锁和方法锁是从不同的粒度层面描述其作用的对象,而重入锁则是synchronized的一个重要特性,允许同一线程重复获取同一个锁,避免死锁并简化编程模型。


27.volatile 的原理

Java中的volatile关键字是解决多线程并发编程中内存可见性和有序性问题的关键机制。以下是关于volatile关键字原理的详细解释:
1. 内存可见性:

  • 当一个变量被声明为volatile时,Java内存模型(JMM, Java Memory Model)确保了对该变量的读操作总是能看到其他线程对该变量最后写入的值。
  • 在没有同步的情况下,线程可能从自己的工作内存(本地缓存或CPU缓存)而不是主内存中读取和写入变量的值,这可能导致数据不一致。而volatile关键字强制任何对该变量的读/写都直接在主内存进行,从而保证了不同线程对共享变量访问的可见性。

2. 禁止指令重排序优化:

  • volatile关键字还具有禁止编译器和处理器进行指令重排序优化的效果,确保对volatile变量的操作与其他所有内存操作之间存在某种形式的顺序关系。
  • 编译器在生成字节码时不会对volatile字段的读/写操作进行重排序,同时硬件层面上也会保证相应的内存屏障来维护程序执行的全局顺序一致性。

3. 内存屏障:

  • 为了实现上述效果,Java虚拟机(JVM)会在读取和写入volatile变量前后插入内存屏障(Memory Barrier/Fence)指令,这些指令可以防止某些类型的处理器优化,并确保正确同步所有线程对内存的访问。
  • 内存屏障分为读屏障和写屏障,读屏障确保在读取 volatile 变量之前,能够获取到所有之前的写操作;写屏障则确保在写入 volatile变量之后,所有后续的读操作都能看到该写入。

4. 不适合替代锁:

  • 尽管volatile提供了可见性和一定程度上的有序性保障,但它并不能完全替代synchronized关键字或者Lock机制,因为它不能保证原子性,即多个操作的完整性。
  • 对于涉及复合操作(如递增、递减等)的数据更新,即使使用volatile修饰,仍然无法避免并发下的数据竞争问题,此时需要借助锁或其他同步机制来保证。

总结来说,volatile关键字主要通过提供内存可见性和一定的内存操作顺序约束来帮助程序员编写正确的并发代码,但它并不适用于所有并发控制场景,特别是在需要原子性和互斥条件控制的地方。


28.使用 kafka 有没有遇到什么问题,怎么解决的。 

在使用 Apache Kafka 时,用户可能会遇到一系列问题。以下是几个常见问题及其解决方法:
1. 跨网络环境无法正常工作:

  • 确保网络配置正确无误,包括防火墙规则、路由策略等,确保Kafka集群节点之间以及客户端与集群之间的通信不受阻。
  • 配置正确的advertised.listeners参数,以便生产者和消费者能够通过外部网络连接到Kafka Broker

2. 数据节点不均衡:

  • 在创建Topic时合理设置分区数(partitions)并分配副本因子(replication-factor),以实现数据的均匀分布。
  • 使用Kafka管理工具如kafka-reassign-partitions.sh进行手动或自动的分区重分配,以平衡各Broker上的数据分布。

3. 日志数据过大堆积磁盘:

  • 设置合理的日志保留策略,比如基于时间的保留策略或者基于大小的保留策略,使用log.retention.hourslog.retention.bytes配置参数。
  • 定期清理旧的日志文件,并监控磁盘空间使用情况,确保有足够的存储空间。

4. 客户端注意事项:

  • 调整客户端配置以适应不同的应用场景,例如调整fetch.min.bytesfetch.max.bytes来控制每次拉取的数据量,优化网络带宽利用。
  • 如果有消费延迟问题,可以检查消费者的max.poll.recordsconsumer.timeout.ms等参数设置是否合适。

5. 特定场景问题:

  • 对于“订单未支付超过30分钟则修改数据库状态”的场景,可以设计一个定时任务或者消费者监听特定主题,判断消息的时间戳与当前时间差是否超过阈值,如果超过,则执行相应的业务逻辑。

6. Windows服务启动问题:

  • 检查Kafka安装路径及配置文件路径是否正确,确保命令行能够找到Kafka的相关文件。
  • 确认环境变量设置正确,特别是对于依赖于Java运行环境的Kafka服务。

7.  生产者端避免消息丢失:

  • acks配置:设置acks=all(默认为1),这样只有当所有ISR(In-Sync Replicas)都确认接收到消息后,生产者才会认为消息发送成功。
  • retries配置:设置合理的重试次数(如retries=Integer.MAX_VALUE),确保网络异常或Broker临时不可用时能重新尝试发送消息。
  • 幂等性(Idempotence):开启幂等性,通过设置enable.idempotence=true,确保即使在多次尝试发送相同消息的情况下,Kafka只保证一条消息被写入分区。
  • 批处理大小与linger.ms配合使用:设置较小的batch.size和较大的linger.ms值,这样生产者会在等待一定时间以积累更多消息后再发送批次,减少网络交互的同时也降低了单个消息未提交的风险。
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all"); // 确保所有ISR副本都确认接收到消息
props.put("retries", Integer.MAX_VALUE); // 设置无限重试次数
props.put("enable.idempotence", true); // 开启幂等性
props.put("batch.size", 16384); // 可根据实际情况调整批处理大小
props.put("linger.ms", 100); // 延迟发送以累积更多消息

Producer<String, String> producer = new KafkaProducer<>(props, new StringSerializer(), new StringSerializer());

 

8. 消费者端避免消息丢失与重复消费:

  • 手动提交偏移量:将消费者的自动提交关闭,即设置enable.auto.commit=false,然后在业务逻辑执行完之后才提交offset。
  • 使用事务或者手动提交offset结合幂等性的业务逻辑来确保消息被正确处理后才提交offset
  • 幂等性消费:如果业务允许,可以考虑实现幂等性的消费者逻辑,使得重复消费的消息对最终结果无影响。
  • Offset管理策略:使用commitSync()方法同步提交offset,确保消息已经被处理完成并且offset已经提交到Kafka Broker。
  •  Exactly Once语义:在Kafka 0.11版本及以后,如果消费者也是Kafka Streams应用或者使用了支持事务功能的Producer,可以通过事务机制实现Exactly Once语义。
  • Kafka服务端配置:
  • Replication Factor设置合适的副本因子(至少为3),以确保即使有Broker宕机也能保持数据的持久化。
  • Min ISR合理配置min.insync.replicas参数,它决定了在多少个副本被认为“同步”之前,不能认为消息是已提交的。
Properties consumerProps = new Properties();
consumerProps.put("bootstrap.servers", "localhost:9092");
consumerProps.put("group.id", "my-group");
consumerProps.put("enable.auto.commit", "false"); // 关闭自动提交offset
consumerProps.put("auto.offset.reset", "earliest"); // 或latest,取决于是否从头开始消费

KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps, new StringDeserializer(), new StringDeserializer());

// 订阅主题并循环消费
consumer.subscribe(Arrays.asList("my-topic"));

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));

    for (ConsumerRecord<String, String> record : records) {
        try {
            // 处理业务逻辑
            processMessage(record.value());

            // 业务逻辑执行成功后手动提交offset
            consumer.commitSync(Collections.singletonMap(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1)));
        } catch (Exception e) {
            // 错误处理和重试策略
            log.error("Error processing message at offset {}.", record.offset(), e);
            // 在这里可以考虑进行重试或记录错误日志,并确保在后续阶段能正确处理这些失败的消息
        }
    }
}

// ... 其他关闭资源的代码 ...

针对具体的问题,通常需要结合具体的错误信息、日志记录以及对Kafka架构和配置的理解来分析并解决问题。对于复杂问题,可能还需要进一步排查网络、系统资源瓶颈等方面的原因。


 29.MQ 有可能发生重复消费,如何避免,如何做到幂等。

消息队列(MQ)中确实有可能发生重复消费的情况,这通常发生在网络抖动、消费者宕机重启后重新拉取消息、消息中间件重试机制触发等场景。为了避免重复消费并实现幂等性,可以从以下几个方面来设计和实现:
1. 业务逻辑层面的幂等设计:

  • 对于写操作,如数据库更新或插入,可以在执行前先查询数据库状态,只有在满足特定条件时才进行操作。例如,在更新用户账户余额时,可以先根据用户ID查询当前余额,并判断新余额是否合法(如确保不会多次加款)。
  • 使用唯一键约束:在插入数据之前,检查是否存在该记录,如果存在则更新而不是插入。

2. 基于分布式锁:

  • 在处理消息前,尝试获取一个与消息相关的分布式锁(如使用Redis、Zookeeper等实现),确保同一时刻只有一个消费者能处理该消息。
  • 处理完消息后释放锁,其他试图处理相同消息的消费者由于无法获取到锁而跳过处理。

3. 消息去重标识:

  • 消息体中包含一个全局唯一的标识符(Message ID 或 Transaction ID),每次处理消息时将其存储到一个持久化的存储系统(如数据库或Redis)中,下次处理时检查该标识符是否已经处理过。
  • 如果是批量消息,可能需要维护一个已处理消息集合(比如用HashSet存储),避免处理过程中出现重复。

4. 幂等表/事务日志:

  • 创建一个幂等表,用于记录已成功处理的消息ID。当消费者接收到消息时,先检查这个表,如果发现消息已经被处理,则直接返回成功,否则执行实际业务逻辑并在完成后将消息ID存入幂等表。

5. 状态机模式:

  • 根据业务流程创建状态机模型,消息中的状态字段控制后续处理行为。消费者只处理未完成状态的消息,处理完成后更新消息状态为已完成。

6. RabbitMQ的ACK机制:

  • 在RabbitMQ中,设置手动确认模式(channel.basicConsume(..., true)),只有在消息被正确处理并持久化后才调用channel.basicAck(deliveryTag, false)确认消息,否则可以通过channel.basicNack(deliveryTag, false, true)拒绝并重新投递消息(注意不宜滥用,可能会造成死循环)。

7. Kafka幂等生产者和事务消费者:

  • Kafka 0.11版本及以后支持幂等生产者,开启后可以确保即使在失败重试的情况下也不会产生重复消息。
  • Kafka 0.11引入了事务概念,通过Kafka事务性消费者可以实现Exactly Once语义,即消息只会被准确地消费一次且仅在所有相关事务都提交后才会被视为已消费。

总之,实现消息队列中消息的幂等性是一个结合具体业务场景和所使用的MQ产品特性综合考虑的过程,不同的业务需求和技术选型可能需要采取不同的策略组合。


30.MQ 的消息延迟了怎么处理,消息可以设置过期时间么,过期了你们一般怎么处理。

 在消息队列(MQ)中,如果消息延迟了,首先需要排查造成延迟的原因,可能涉及网络问题、消费者处理能力不足、消息堆积导致的延迟等。针对这些原因可以采取以下措施:
1. 监控与优化:

  • 监控MQ系统的吞吐量、消息堆积情况和消费者的消费速度。
  • 调整消费者性能,如增加消费者实例数、提升单个消费者处理能力或优化业务逻辑。

2. 消息优先级:

  • 如果MQ支持,可以设置消息优先级,让重要程度高的消息优先被处理。

3. 死信队列与延时队列:

  • 一些MQ产品如RabbitMQ、RocketMQ提供了死信队列机制,当消息达到一定重试次数或者超过特定时间未被消费,可以将消息移入死信队列,以便后续分析或特殊处理。
  • RocketMQ还支持延时队列功能,可以设定消息在某个时间点之后才变为可消费状态。

4. 过期时间(TTL, Time To Live):

  • 大多数MQ都支持为消息设置过期时间。例如,在RabbitMQ中可以通过TTL属性设置消息的有效生存期,过期后消息会被移动到死信交换机;在Kafka中,可以为主题或分区配置保留策略来实现类似效果。

5. 过期消息处理:

  • 移动至死信队列:过期消息不会被正常消费者接收,而是被系统识别并移入死信队列,供专门的死信消费者进行后续处理,比如记录日志、发送通知或执行补偿操作。
  • 自动删除:有些MQ会直接删除过期的消息,不再提供给任何消费者。
  • 定制化处理:通过监听消息队列的过期事件,触发自定义的处理逻辑。

总结来说,消息延迟的问题要根据具体场景分析并针对性地解决。而消息过期后的处理,可以根据业务需求选择合适的方法,常见的做法是将过期消息放入死信队列以防止数据丢失,并能够追踪处理异常消息。


 31. Redis 集群方案应该怎么做?都有哪些方案?

Redis集群方案主要包括以下几种:
1. 主从复制(Replication):

  • 在基础的主从模式中,一个Redis节点作为主节点(Master),其他节点作为从节点(Slave)。主节点负责处理所有的写请求,然后将数据同步给从节点。这种方式可以实现读写分离和数据冗余,但无法解决单点故障问题。

2. 哨兵模式(Sentinel):

  • SentinelRedis提供的高可用性解决方案。它是一个监控系统,能够监控Redis主服务器和从服务器的状态,并在主服务器宕机时自动进行故障转移,选举新的主节点。通过配置多个Sentinel实例,可以形成哨兵集群,提高系统的可用性和稳定性。

3. Redis Cluster(分片集群)

  • Redis Cluster是Redis 3.0及更高版本内置的分布式解决方案,它实现了真正的数据分片(sharding)。每个Redis节点都可以成为集群的一部分,数据被分散存储在不同的节点上,每个节点都可以处理客户端请求。Cluster使用Gossip协议来管理集群状态,包括节点发现、故障检测以及自动故障转移等,无需中心化代理或协调器,理论上支持数千个节点的扩展。

4. 代理方式:除了Redis内置的集群方案外,还有一些代理层的解决方案,例如:

  • Twemproxy(也称为nutcracker):Twitter开源的一个快速、轻量级的代理服务,它可以将请求路由到后端的多个Redis实例上,实现简单的分片功能。
  • Codis:由豌豆荚开源,后来阿里云团队接手维护,提供了一套基于ZooKeeper的Redis集群解决方案,支持在线扩容、迁移等高级特性。

每种方案都有其适用场景和优缺点,选择方案时应根据实际业务需求(如数据量、并发量、读写比例、容灾要求等)来决定。对于大规模分布式系统,Redis Cluster通常是最直接且易于管理的选择;而对于对成本控制和运维复杂度有特殊要求的场景,则可能需要考虑代理层的集群方案。


32.谈谈你的参与的项目?能否画出整个项目的架构设计图,尽量包含 流程、部署等


33. 一次 web 请求响应中,那个部分最耗时,tcp 握手?业务逻辑处理?网络延迟?数据库查询?浏览器解析?

一次Web请求响应的时间消耗可以由多个部分组成,具体哪个部分最耗时会根据应用的具体情况和环境变化。以下是一般情况下各个阶段可能的耗时特点:
1. TCP握手(三次握手):

  • 在HTTP/1.x协议中,每次新的TCP连接都需要经历三次握手过程,这通常在几百毫秒内完成,对于高并发场景下频繁建立新连接的情况,TCP握手时间可能会成为显著延迟的一部分,尤其是在移动网络环境下。
  • HTTP/2及后续版本通过复用长连接来减少握手次数,从而降低这部分的耗时。

2. DNS解析:

  • DNS查找是请求发送前的一个重要步骤,如果本地没有缓存或者TTL过期,则需要向DNS服务器发起查询,这个过程也可能造成几十到几百毫秒不等的延迟

3. 网络延迟(RTT):

  • 这包括数据包在网络中传输的时间,受到客户端与服务器之间物理距离、中间路由器数量、网络拥塞状况等因素影响,是最不可控的部分之一,尤其对于跨国或跨洲际访问,网络延迟可能是主要瓶颈。

4. 业务逻辑处理:

  • 服务器接收到请求后执行业务代码,涉及数据验证、计算、业务规则判断等操作,复杂度不同,耗时从几毫秒到数秒不等,如果是CPU密集型或I/O密集型任务,可能成为性能瓶颈。

5. 数据库查询:

  • 如果应用涉及到数据库交互,查询效率对响应时间至关重要。简单的查询可能耗时极短,复杂的查询、大表全表扫描、未优化索引等情况可能导致数百毫秒甚至秒级以上的延迟

6. 磁盘I/O:

  • 如果应用涉及到大量文件读写或其他磁盘操作,磁盘I/O速度也会影响响应时间

7. 浏览器解析:

  • 接收到服务器响应的数据后,浏览器需要解析HTML、CSS、JavaScript等内容,并进行渲染。现代浏览器已经非常高效,但对于大型、复杂的前端应用,尤其是初次加载时,解析和渲染可能也需要一定时间。

综上所述,在实际应用场景中,上述各个环节都可能成为性能瓶颈。要确定哪个环节最耗时,通常需要通过分析工具(如Chrome开发者工具、网络监控工具、服务器端日志分析、数据库慢查询日志等)收集并分析数据来识别问题所在。在很多Web应用中,数据库查询和业务逻辑处理往往是决定响应时间的关键因素,但具体情况需具体分析。


34、分布式系统设计你会考虑哪些策略?

在设计分布式系统时,会考虑多种策略来保证系统的高可用性、可扩展性和数据一致性。以下是一些关键的设计策略:
1. 容错和冗余:

  • 心跳检测与故障恢复:通过周期性的心跳检测机制(如心跳包)监控各个节点的状态,并在节点失效时自动进行故障转移。如使用Kubernetes(k8s)的Pod副本集:在Kubernetes中创建一个副本集,确保服务始终有指定数量的Pod运行。。
  • 数据复制与主从模式:通过数据复制确保数据的冗余备份。例如使用Redis Sentinel进行主从切换,配置Redis哨兵集群监控主节点状态,当主节点宕机时自动选举新的主节点,并通知客户端。

2. 负载均衡:

  • 基于软件或硬件的负载均衡器,根据特定算法(比如轮询、最少连接数、哈希等)将请求分发到不同的服务节点。如使用Nginx作为反向代理和负载均衡器:通过Nginx配置文件设置upstream模块,根据轮询、权重等策略将请求分发到后端服务器
  • 自动伸缩策略:根据系统负载动态调整资源,增加或减少服务器节点以适应业务需求。如AWS Elastic Load Balancer (ELB) Google Cloud Load Balancing (GCLB):云服务商提供的负载均衡服务,可以自动扩展并提供多种健康检查机制。

3. 分区与分片:

  • 数据分区(Sharding):将大量数据分割成多个部分存储在不同节点上,从而分散负载并提高整体处理能力。例如在MySQL中,可以通过中间件如ShardingSphere进行水平拆分,或者直接在数据库层面使用MySQL Group Replication实现数据分片。
  • 分布式缓存:利用Redis Cluster或其他分布式缓存方案,对热点数据进行集中管理和快速访问。

4. 一致性保障:

  • CAP定理:在分布式环境下权衡一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)。如利用SeataGoogle Spanner或其他分布式事务解决方案保证跨多个服务的数据一致性
  • 一致性模型:根据实际需求选择强一致性、最终一致性或者因果一致性等不同的一致性模型,如RaftPaxos等协议保证强一致,或者使用Quorum读写策略实现弱一致性。

5. 通信机制:

  • RPC框架:使用高效的远程过程调用(RPC)框架,如gRPC、Thrift、Fegin等,实现跨节点的服务调用。
  • 消息队列:通过消息队列(MQ)异步解耦系统组件,实现流量削峰、可靠投递和分布式事务等场景。

6. 服务治理:

  • 服务注册与发现:服务实例注册到服务中心,客户端通过服务中心获取服务实例地址列表进行调用。如Nacos
  • 超时重试与熔断降级:设置合理的超时策略,对于失败的请求进行重试,当故障率超过阈值时触发熔断保护,避免雪崩效应。如HystrixSentinel

7. 分布式锁与协调:

  • 使用分布式锁服务,如ZooKeeper、etcd、redis等,解决分布式环境下的并发控制问题。

8. 安全与权限管理:

  • 在分布式系统中实施统一的安全认证与授权机制,保护数据和服务不被非法访问。如APIGatewal

9. 日志与监控:

  • 设计一套完整的日志收集与分析体系,结合实时监控工具,能够快速定位和解决问题。
  • ELK Stack(Elasticsearch, Logstash, Kibana):收集、解析、存储和可视化日志数据。
  • Prometheus和Grafana:用于系统性能监控,Prometheus负责数据采集和存储,Grafana提供丰富的图表展示和告警功能。

以上所述是分布式系统设计中的核心策略,具体实施时需要结合业务需求和技术栈特点进行选择和优化。


35、最常见的数据分布方式是什么?

在分布式系统中,最常见的数据分布方式是哈希分布(Hashing

这种分布方式基于数据的某个特征(通常是键值)计算哈希值,然后将哈希值映射到不同的节点或分区上,从而实现数据在集群中的均匀分布和负载均衡。


例如,在Redis集群或数据库分片场景中,通常使用一致性哈希算法来决定数据存储的位置。这样,当添加或移除节点时,只需重新映射部分哈希槽,而不是全部数据,从而有效减小了数据迁移的成本。
而在统计学领域,最常见的数据分布类型有:
1. 正态分布(Normal Distribution)
2. 均匀分布(Uniform Distribution)
3. 泊松分布(Poisson Distribution)
4. 指数分布(Exponential Distribution)
5. 伽玛分布(Gamma Distribution)
6. 偏态分布(Skewed Distribution)
7. 伯努利分布(Bernoulli Distribution)
8. 卡方分布(Chi-squared Distribution)
9. F分布(F-distribution)
10. 威布尔分布(Weibull Distribution)

这些分布用于描述不同类型的随机变量的概率分布特点,并广泛应用于各种实际问题分析与建模中。


36、谈一谈一致性哈希算法。

一致性哈希算法(Consistent Hashing)是一种分布式系统中常用的负载均衡和数据分布策略,旨在解决在动态变化的集群环境中,尽量保持数据映射关系稳定的问题。

该算法最早由麻省理工学院的Karger等人于1997年提出,主要用于缓存系统的负载均衡


传统哈希表或简单取模方式分配数据时,当集群中的节点数量发生变化时,比如新增或移除服务器,会导致大量数据需要重新定位到新的服务器上,这在大规模分布式系统中将带来很大的负担。而一致性哈希算法则通过引入虚拟环(Hash Ring)的概念来显著降低这种影响。
具体实现如下:
1. 构建哈希环:

  • 将整个哈希空间看作是一个首尾相连的圆环,这个圆环的范围通常为0到2^{32-1}(即一个32位整数的最大值),也可以是其他足够大的哈希值空间。
  • 每个服务器节点通过一个唯一的标识符(如IP地址或主机名)进行哈希计算,得到对应的哈希值,并将其映射到哈希环上。

2. 数据定位:

  • 当有数据需要存储时,同样对数据的键(key)进行相同的哈希函数计算,得到其哈希值并定位到哈希环上。
  • 顺时针查找第一个遇到的服务器节点作为数据的存储位置。

3. 节点增删的处理:

  • 如果添加了一个新节点,只需将新节点加入到哈希环中,它会接管一部分原来相邻节点的数据。
  • 如果删除了一个节点,则原本落在该节点上的数据会被分配给它的下一个节点(顺时针方向)。

4. 虚拟节点:

  • 在实际应用中,为了更好地平衡各个节点的负载,还会引入“虚拟节点”的概念。每个物理节点会在哈希环上对应多个虚拟节点,这样可以使得数据分布更为均匀。

通过一致性哈希算法,即使在节点数量发生变动的情况下,大多数数据仍然能够继续保存在原有的服务器上,只有部分数据需要迁移,从而极大地减少了服务中断时间和服务请求重新路由的次数,提高了系统的可用性和扩展性。这一算法广泛应用于分布式缓存、负载均衡器以及分布式数据库等领域。


37、paxos 是什么?

Paxos 是一种分布式一致性算法,由 Leslie Lamport 于1990年提出,并以希腊的帕索斯岛(Paxos)命名。

Paxos算法主要用于解决在分布式系统中如何就某一值达成共识的问题,即使存在网络分区、节点故障等不确定性条件,也可以保证系统的一致性。
Paxos中,节点分为三种角色:

  • 提案者(Proposer):负责发起提议,即尝试确定一个值被所有参与节点接受为最终决定。
  • 接受者(Acceptor):接收并响应提案者的提议,承诺不接受具有更低编号的提议,并且根据特定规则选择是否接受某个提议。
  • 学习者(Learner):从接受者那里获取已通过的提议结果,并将这个决定传播给其他节点或客户端。

Paxos算法的核心在于确保在一系列的提案和投票过程中,系统能够收敛到一个唯一的决策。它通常应用于分布式数据库、分布式存储系统以及需要强一致性的服务中,用于实现数据复制、状态同步等功能。尽管原始论文描述起来相对复杂,但经过后续研究者的努力,已经发展出了一些简化版本和易于理解的变种,如 Multi-PaxosRaft 等。


38、什么是 Lease 机制?

Lease机制,又称租约机制,是一种广泛应用于分布式系统中以维护一致性、协调资源和判定节点状态的协议。

在分布式环境中,Lease通常由一个权威实体(如服务器或主节点)颁发给其他参与系统的客户端或从节点,并规定了一个有效时间段,在此期间内:
1. 颁发者承诺:Lease的有效期内,颁发者保证不会做出与当前租约内容相冲突的操作。
2. 持有者权利:持有Lease的客户端或从节点在Lease有效期内有权执行特定操作,例如读取或更新数据,并假设这段时间内数据保持一致或服务可用。
3. 过期处理:Lease到期后,客户端必须放弃其基于租约获得的权利,并可能需要重新获取租约或者同步最新的状态。
在实际应用中,Lease机制有以下典型应用场景:

  • 分布式缓存系统:服务器可以向缓存节点颁发一定时间长度的Lease,确保在此期间内缓存的数据是最新的;当Lease到期时,缓存节点必须重新验证数据或丢弃缓存,避免陈旧数据被返回给客户端。
  • 元数据管理:在数据库系统中,修改元数据前,服务器会暂停新Lease的发放,以防止在变更过程中客户端持有旧的Lease继续进行不一致的操作。
  • 集群管理:在分布式计算和存储集群中,节点通过获取Lease来声明自己对某些资源的所有权或管理权,当Lease过期而未续约时,其他节点可以竞争并接管这些资源。

总的来说,Lease机制提供了一种在分布式环境下安全地进行临时授权的方法,它有助于解决网络延迟、故障恢复以及并发控制等问题,从而确保分布式系统的一致性和正确性。


39、如何理解选主算法?

选主算法(Leader Election Algorithm)是分布式系统中用于确定集群中某个节点作为领导者或协调者的一种机制。

在分布式环境下,为了确保数据一致性、执行一致的决策和管理资源,通常需要有一个被所有参与节点共同认可的主节点来负责核心操作和服务请求。
选主算法的基本目标是在网络环境可能存在故障的情况下,能够高效且正确地选出一个主节点,并确保:

  • 唯一性:同一时间只能存在一个主节点(Leader),防止出现多个“领导者”导致的数据不一致。
  • 可用性:当主节点失效时,剩余节点能快速重新选举出新的主节点,以保持系统的持续服务。
  • 稳定性:选举过程中要尽量减少不必要的主节点变更,避免频繁切换导致的服务中断和性能下降。
  • 公平性:在可能的情况下,所有节点都有机会成为主节点,或者根据某种策略均衡地分配主节点角色。

常见的选主算法包括但不限于:

  • Raft协议中的领导者选举:通过心跳消息和投票过程,在宕机或分区情况下实现领导者更换。
  • ZooKeeper的ZAB协议:基于多数派原理,保证集群中始终有且仅有一个Leader。
  • Paxos算法虽然主要解决共识问题,但也包含了隐含的选主步骤,即Proposer如何获得大多数Acceptor的认可来推进提案的批准。
  • Redis Sentinel哨兵模式:哨兵之间协商决定将哪个从节点提升为主节点。

每个算法的具体实现细节各异,但都是为了确保在分布式环境中即使发生部分节点故障也能维持系统正常运行。


40、OSI 有哪七层模型?TCP/IP 是哪四层模型。

OSI 七层模型:
1. 物理层(Physical Layer)

  • 功能:负责数据的传输比特流,定义了电压、接口、线缆规格等物理特性。
  • 协议示例:RS-232、Ethernet、Wi-Fi

2. 数据链路层(Data Link Layer)

  • 功能:负责帧的传输,提供节点间的可靠传输,并解决介质访问控制问题(MAC地址)。
  • 协议示例:Ethernet、HDLC、PPP、802.11(无线局域网)

3. 网络层(Network Layer)

  • 功能:负责IP分组或数据包在不同网络之间的路由选择,实现跨网络的数据传输。
  • 协议示例:IP(Internet Protocol)、ICMP(Internet Control Message Protocol)

4. 传输层(Transport Layer)

  • 功能:建立端到端的通信,确保数据可靠性传输和错误恢复,提供进程间通信服务。
  • 协议示例:TCP(Transmission Control Protocol)、UDP(User Datagram Protocol)

5. 会话层(Session Layer)

  • 功能:建立、管理和终止会话连接,协调交互过程中的同步问题。
  • 协议示例:此层在现代网络中实际应用较少,部分功能由应用层协议完成。

6. 表示层(Presentation Layer)

  • 功能:数据格式转换、加密解密、压缩解压缩等,确保应用层数据格式统一。
  • 协议示例:ASCII、JPEG、MPEG、SSL/TLS(安全套接字层/传输层安全协议)

7. 应用层(Application Layer)

  • 功能:为应用程序提供接口,处理特定的应用程序细节,如文件传输、电子邮件、网页浏览等。
  • 协议示例:HTTP(Hypertext Transfer Protocol)、FTP(File Transfer Protocol)、SMTP(Simple Mail Transfer Protocol)、DNS(Domain Name System)

TCP/IP 四层模型:
1. 网络接口层(Network Interface Layer)

  • 对应于OSI模型的物理层和数据链路层,主要关注硬件接口以及与物理媒体的交互。

2. 互联网层(Internet Layer)

  • 对应于OSI模型的网络层,主要负责IP寻址和路由选择,例如IP协议。

3. 传输层(Transport Layer)

  • 同样对应OSI模型的传输层,提供端到端的数据传输服务,包括TCP和UDP协议。

4. 应用层(Application Layer)

  • 对应OSI模型的会话层、表示层和应用层,包含了各种直接服务于应用程序的具体协议。

41、 正常情况下,当在 try 块或 catch 块中遇到 return 语句时,finally 语句块在方法返回之前还是之后被执行?

在Java中,无论try块或catch块中是否包含return语句,finally块都会在方法返回之前被执行。

这是因为在Java的异常处理机制中,finally块用于定义那些必须在任何情况下(包括正常执行、出现异常时)都要执行的代码。
具体流程如下:

  • 当控制流进入try块时,开始执行try块中的代码。
  • 如果try块中抛出一个异常,那么相应的catch块将被执行(如果有匹配的catch块)。
  • 不管try块或catch块中是否有return语句,以及程序是正常执行还是因为异常退出,finally块都会在方法返回之前被执行。
  • finally块执行完毕后,方法才会真正返回到调用者。

需要注意的是,如果finally块中也有return语句,那么它会覆盖try和catch块中的return结果,并且该方法会立即返回,不再执行finally块后面的代码


42、try、catch、finally 语句块的执行顺序。

见第41点 


43、Java 虚拟机中,数据类型可以分为哪几类?

Java虚拟机(JVM)中的数据类型可以分为两大类:
1. 原始类型(Primitive Types):
这些是Java语言的基本数据类型,由JVM直接支持,存储在栈中,并且具有固定的内存大小
包括以下几种类型及其默认值:

  • byte: 8位有符号整数,默认值为0。
  • short: 16位有符号整数,默认值为0。
  • int: 32位有符号整数,默认值为0。
  • long: 64位有符号整数,默认值为0L。
  • char: 16位无符号Unicode字符,默认值是'\u0000'(null字符)。
  • float: 32位单精度浮点数,默认值为0.0f。
  • double: 64位双精度浮点数,默认值为0.0d。
  • boolean: 表示逻辑状态,只有两个可能的值true或false,没有明确的默认值,但在实例化对象时其成员变量的默认值为false。

2. 引用类型(Reference Types):
引用类型指向堆上存储的对象实例。它们不直接存储数据,而是存储对实际数据的引用地址。
包括:

  • 类类型(Class Types),如自定义的类以及系统预定义的类(如String、ArrayList等)。
  • 接口类型(Interface Types)
  • 数组类型(Array Types),包括所有基本类型的数组和引用类型的数组。

此外,还有一个特殊的非对象类型,即returnAddress类型,它用于方法调用时返回地址的标识,主要用于java.lang.invoke.MethodHandle相关功能,在Java开发者日常编程中并不常见。


44、怎么理解栈、堆?堆中存什么?栈中存什么?

在Java虚拟机(JVM)内存模型中,栈和堆是两种不同的内存区域:
1. 栈(Stack):

  • 栈是线程私有的,每个线程都有自己的栈空间。当一个方法被执行时,JVM会为该方法创建一个新的栈帧(Stack Frame),并压入当前线程的栈顶。
  • 栈主要用于存储局部变量、方法参数、中间运算结果以及返回地址等数据。
  • 局部变量包括基本类型变量(如int、float、boolean等)以及对象引用(指向堆中的对象实例)。
  • 栈的特点是后进先出(LIFO),即最后入栈的数据最早被弹出。当方法执行完毕,对应的栈帧就会被销毁。

2. 堆(Heap):

  • 堆是所有线程共享的一块内存区域,在JVM启动时创建,用于存放所有的对象实例和数组。
  • 对象实例在堆上分配内存,包括对象的属性值以及类信息等。
  • 当我们通过new关键字创建一个对象时,JVM会在堆中为其分配一块内存,并初始化其属性值。同时,栈中的对象引用指向这块堆内存。
  • 堆内存由垃圾回收器管理,负责分配和回收对象所占的空间。

总结来说,栈主要存储的是方法调用过程中的程序上下文信息(如局部变量)和控制信息(如指令指针),而堆则存储的是实际的对象实例内容。


45、为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?

在编程语言和操作系统中,堆(Heap)和栈(Stack)之所以被区分出来,是因为它们在内存管理、生命周期、数据结构以及使用场景上具有显著的不同。这些差异主要体现在以下几个方面:
1. 内存分配与回收:

  • 栈:由编译器自动分配和释放空间,通常在函数调用时创建,函数返回时自动销毁。因此栈内存的生命周期是线程相关的,随着函数调用结束而结束。
  • 堆:程序员手动申请和释放内存,可以通过new(在Java等语言中)或系统调用来分配,在使用完毕后通过delete/free等操作来释放。如果不主动释放,就需要依赖垃圾回收机制(如Java和C#)。

2. 大小限制:

  • 栈:由于栈的特性,它的大小通常是静态确定的,并且相对较小。如果栈上的数据量过大,可能会导致栈溢出错误。
  • 堆:堆的大小通常比栈大得多,并且可以根据需要动态增长,但这也意味着需要复杂的内存管理算法来避免内存碎片和优化内存利用率。

3. 数据存储:

  • 栈:主要存储局部变量、函数参数和返回地址等临时性数据,这类数据在函数执行完毕后不再需要。
  • 堆:用于存储对象实例和数组等数据结构,这些数据可能在多个函数之间共享或长期存在。

4. 性能:

  • 栈:内存分配速度较快,因为它是连续的内存区域并且遵循先进后出(LIFO)的原则,分配和释放的操作成本较低。
  • 堆:内存分配速度相对较慢,因为它涉及到查找可用内存块、合并空闲空间等复杂操作。同时,访问堆中的对象可能涉及指针间接寻址,相比直接访问栈上数据可能存在额外开销。

5. 多线程共享:

  • 栈:每个线程都有自己的栈空间,互不干扰。
  • 堆:所有线程可以共享同一块堆内存,这意味着堆中的对象可以被多个线程访问和共享。

综上所述,堆和栈的分离设计是为了满足不同类型的内存需求和管理策略,使得程序能够在资源有限的情况下更高效地运行,并支持多种编程模式,包括面向对象编程中的对象生命周期管理和资源共享等。

  • 31
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

默语玄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值