Bootstrap

后端开发面经系列 -- 金山一面面经

金山一面面经,狂虐网络和数据库

公众号:阿Q技术站

来源:https://www.nowcoder.com/discuss/551461694732570624

1、说说TCP的拥塞控制?

  1. 慢启动:TCP连接初始化时,发送方将初始的拥塞窗口设为一个较小的值,然后逐渐增加。这个增长是指数级的,即拥塞窗口大小按照指数增长。这有助于确保在网络中发送的数据量逐渐增加,直到达到网络的容量。
  2. 拥塞避免:一旦拥塞窗口大小达到一个阈值(通常是慢启动阈值),TCP切换到拥塞避免模式。在这个模式下,拥塞窗口以线性方式增加,而不是指数方式。这有助于防止拥塞窗口过快增长,从而更好地适应网络的状况。
  3. 快速重传:当发送方检测到丢失的数据包时,它不必等到超时发生,而是可以快速重传丢失的数据包。这减少了等待时间,提高了网络的效率。
  4. 快速恢复:当发生数据包丢失时,TCP通过减半拥塞窗口的大小,然后逐渐增加,而不是将窗口大小直接设置为1。这有助于保持一定的网络吞吐量,而不是完全暴力减少。
  5. 超时重传:如果在一段时间内未收到确认,TCP将假定数据包已丢失,并重新发送未确认的数据包。这是一种保守的拥塞控制机制,用于处理某些未检测到的数据包丢失情况。

2、TCP怎么保证可靠?

TCP通过多种机制来保证可靠性,确保在数据传输过程中不丢失、不重复、不失序,并能正确处理网络中可能出现的错误。

保证可靠性的主要机制:

  1. 序号和确认应答:使用序号对每个发送的字节进行标记,并通过确认应答机制来追踪已经成功接收的字节。发送方会等待接收方的确认应答,以确认数据已经安全到达,如果没有收到确认,则会重新发送数据。
  2. 超时重传:如果发送方在一定时间内未收到接收方的确认应答,就会认为数据包可能丢失了,触发超时重传机制,重新发送未确认的数据。
  3. 滑动窗口:使用滑动窗口机制来限制在网络中的未确认数据量。滑动窗口通过动态调整窗口大小,确保发送方不会同时发送过多的数据,以防止网络拥塞。
  4. 流量控制:通过流量控制机制来调整发送方的发送速率,以适应接收方的处理能力。接收方可以通过TCP报文中的窗口大小告诉发送方它还能接收多少字节的数据。
  5. 拥塞控制:通过拥塞控制机制来避免网络拥塞。拥塞控制包括慢启动、拥塞避免、快速重传和快速恢复等算法,可以动态调整发送速率,确保网络的稳定性。
  6. 有序交付:确保接收方按正确的顺序交付数据。每个TCP报文段都有一个序列号,接收方根据这些序列号将数据按正确的顺序组装。
  7. ACK机制:使用确认(ACK)机制,接收方会定期发送确认报文,告知发送方已成功接收到数据。发送方根据这些确认来确认数据的成功传输。
  8. 连接管理:通过三次握手和四次挥手来建立和关闭连接,确保通信的可靠性。这些过程包含了严格的状态转换和确认机制。

3、为什么握手只要三次而挥手要四次?

为什么握手是三次?

TCP建立连接时之所以只需要"三次握手",是因为在第二次"握手"过程中,服务器端发送给客户端的TCP报文是以SYN与ACK作为标志位的。SYN是请求连接标志,表示服务器端同意建立连接;ACK是确认报文,表示告诉客户端,服务器端收到了它的请求报文。

即SYN建立连接报文与ACK确认接收报文是在同一次"握手"当中传输的,所以"三次握手"不多也不少,正好让双方明确彼此信息互通。

为什么挥手是四次?

TCP释放连接时之所以需要“四次挥手”,是因为FIN释放连接报文与ACK确认接收报文是分别由第二次和第三次"挥手"传输的。为何建立连接时一起传输,释放连接时却要分开传输?

  • 建立连接时,被动方服务器端结束CLOSED阶段进入“握手”阶段并不需要任何准备,可以直接返回SYN和ACK报文,开始建立连接。
  • 释放连接时,被动方服务器,突然收到主动方客户端释放连接的请求时并不能立即释放连接,因为还有必要的数据需要处理,所以服务器先返回ACK确认收到报文,经过CLOSE-WAIT阶段准备好释放连接之后,才能返回FIN释放连接报文。

4、HTTP的发展历史?

HTTP是一种用于传输超文本的协议,它在不同版本中不断演化和改进,以满足不断增长的网络需求。

HTTP 1.0:

HTTP 1.0是最早的HTTP版本,定义于1996年。它的特点如下:

  1. 短连接: 每次请求/响应都需要建立和关闭TCP连接,导致了多次握手和释放的开销,降低了性能。
  2. 无状态: HTTP 1.0是无状态的,每个请求/响应都是独立的,服务器不会保存连接状态,需要在每次请求中包含所有信息。
  3. 明文传输: HTTP 1.0数据传输通常以明文方式进行,缺乏加密和安全性。
  4. HTTP头部: 头部信息较简单,只包含必要的元数据。

HTTP 1.1:

HTTP 1.1是HTTP 1.0的后续版本,于1999年发布。它引入了以下改进:

  1. 长连接: HTTP 1.1引入了持久连接,允许多个请求/响应共享一个TCP连接,减少了握手和释放的开销,提高了性能。
  2. 管道化: HTTP 1.1支持请求/响应的管道化,允许客户端发送多个请求而不等待响应,进一步提高了性能。
  3. Host头部: 引入了Host头部字段,允许在同一台服务器上托管多个域名,从而支持虚拟主机。
  4. 缓存控制: 引入了更精细的缓存控制机制,使缓存更有效。

HTTP/2.0:

HTTP/2.0是HTTP 1.1的进一步改进,于2015年发布。它引入了以下特性:

  1. 多路复用: HTTP/2.0允许多个请求/响应流共享一个TCP连接,通过二进制帧实现多路复用,提高了并发性能。
  2. 头部压缩: 引入了头部字段压缩,减小了每个请求的开销,减少了带宽占用。
  3. 服务器推送: 服务器可以主动将与请求相关的资源推送给客户端,减少了客户端请求的延迟。
  4. 优化流: HTTP/2.0通过优化请求和响应流的传输,提高了性能。

HTTP/3.0:

HTTP/3.0是最新的HTTP版本,于2020年发布。它采用了一种名为QUIC(Quick UDP Internet Connections)的新的传输协议,以进一步提高性能和安全性:

  1. 基于UDP: HTTP/3.0不再依赖于TCP,而是基于UDP传输,减少了连接建立的时延,提高了网络性能。
  2. 多路复用: 类似HTTP/2.0,HTTP/3.0支持多路复用,允许多个请求/响应流在一个连接上并发传输。
  3. 头部压缩: HTTP/3.0继续采用头部字段压缩,降低了请求开销。
  4. 连接迁移: HTTP/3.0支持在不同网络条件下迁移连接,以适应移动设备的切换。

总的来说,HTTP 1.0到HTTP/3.0的演化过程主要关注了性能、效率和安全性的提升。HTTP/2.0和HTTP/3.0引入了多路复用和头部压缩等重要特性,以改进传输效率和降低延迟。HTTP/3.0更进一步通过使用QUIC协议提供了更佳的性能和网络适应性。

5、POST和GET本质区别 POST几次GET最长url?

GET和POST的本质区别:

  1. 数据传输方式:
    • GET:将数据追加在URL后面,作为查询参数,以?分隔URL和参数,参数之间用&连接。
    • POST:将数据放在请求体中传输,而不是作为URL的一部分。
  2. 数据传输安全性:
    • GET:参数在URL中可见,对于敏感信息不安全。
    • POST:数据放在请求体中,相对于GET更安全。
  3. 数据传输大小限制:
    • GET:URL长度有限,浏览器和服务器对URL长度有限制,因此传输的数据量较小。
    • POST:携带的数据放在请求体中,通常比GET能传输更大的数据量。
  4. 缓存机制:
    • GET:可以被浏览器缓存。
    • POST:由于可能对服务器产生副作用,不容易被缓存。
  5. 安全性和幂等性:
    • GET:安全,幂等,多次调用无副作用。
    • POST:非安全,非幂等,可能有副作用。

GET最长URL:

浏览器和服务器对GET请求的URL长度有一定的限制,具体限制因浏览器和服务器而异。一般来说,常见的浏览器如 Chrome、Firefox 等对 URL 长度的限制是几千个字符。而服务器端也可能对接收的URL长度进行限制。

6、ARP怎么做的?

ARP(Address Resolution Protocol)是一种用于将网络层地址(如IP地址)映射到链路层地址(如MAC地址)的协议。主要用于在局域网中,通过已知IP地址获取相应的MAC地址。

ARP的工作过程如下:

  1. ARP请求: 当主机A知道目标主机B的IP地址,但不知道其MAC地址时,主机A会发送一个ARP请求广播帧,请求目标主机B回应其MAC地址。ARP请求帧中包含了A的IP地址和MAC地址,以及B的IP地址。
  2. ARP应答: 目标主机B收到ARP请求后,检查请求中的目标IP地址是否与自己的IP地址匹配。如果匹配,B会向A发送一个ARP应答帧,包含自己的MAC地址。
  3. ARP缓存: 主机A收到ARP应答后,将B的IP地址和MAC地址的映射关系存储在本地的ARP缓存中,以便将来的通信中使用。这样,下次A要与B通信时就不需要再发送ARP请求,直接使用缓存中的MAC地址。
  4. ARP缓存过期: ARP缓存中的映射关系有一定的时效性,如果一段时间内没有与目标主机通信,映射关系可能过期。当需要再次通信时,会重新发送ARP请求更新缓存。

7、Map为啥用红黑树不用其他二叉树?

主要是因为红黑树相对于普通的二叉搜索树(BST)和其他平衡二叉树结构具有一些优势。以下是一些使用红黑树的原因:

  1. 平衡性:红黑树是一种自平衡的二叉搜索树。通过保持树的平衡性,红黑树能够在进行插入和删除等操作时保持较低的高度,从而保持了对数级别的查找复杂度。
  2. 插入和删除效率:红黑树对于插入和删除操作的平均时间复杂度为O(log n),而普通的二叉搜索树在最坏情况下可能退化为链表,导致这些操作的时间复杂度为O(n)。红黑树的平衡性确保了这些操作的高效性。
  3. 查找效率:红黑树同样具有对数级别的查找效率,这是一种相对稳定的性能。
  4. 稳定性:红黑树相对于其他平衡二叉树结构来说,在插入和删除等操作上更为稳定。这是因为红黑树的平衡性质确保了树的高度相对较小。
  5. 简单而高效的平衡算法:红黑树的平衡算法相对于其他平衡二叉树结构来说较为简单,旋转操作的次数较少,实现起来相对容易,而且具有较低的常数因子。

8、MySQL为啥用b+做索引?

  1. 有序性: B+树是一种有序的树状数据结构,每个节点都按顺序存储着键值对。这种有序性使得在B+树上进行查找、范围查询和排序等操作非常高效。数据库中经常需要按照索引的顺序进行扫描,B+树的有序性对这类操作非常有利。
  2. 高度平衡: B+树是一种自平衡树,它保持了相对平衡的高度。这意味着在B+树中查找一个元素的时间复杂度是对数级别的,这对于快速检索大量数据非常重要。高度平衡还有助于保持在插入和删除操作后的树结构的平衡性。
  3. 范围查询效率: B+树的有序性和高度平衡性使得范围查询非常高效。范围查询是数据库中常见的操作,例如使用 WHERE column BETWEEN value1 AND value2 进行范围查找。B+树能够迅速定位范围的起始点和结束点,而且在这个范围内的元素都是有序的。
  4. 顺序访问性: B+树的叶子节点形成了一个有序链表,这意味着可以很容易地支持顺序访问。对于一些场景,如顺序扫描、排序操作等,B+树的顺序访问性非常有利。
  5. 支持唯一性约束: B+树天生就支持唯一性约束。在B+树上,每个节点的键值都是唯一的,这有助于数据库保持数据的唯一性。

9、Mysql解决不可重复读?

MySQL通过实现事务隔离级别来解决不可重复读的问题。不可重复读指的是在一个事务中,如果一个事务在读取某一行数据后,另一个事务修改了这一行数据并提交了,那么在第一个事务中再次读取同一行数据时,会发现数据发生了变化,导致读到不一致的数据。

事务隔离级别包括:读未提交(Read Uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。这些隔离级别的选择允许用户在事务的一致性和性能之间进行权衡。

解决不可重复读的主要隔离级别是“可重复读”(Repeatable Read)。

在可重复读隔离级别下,一个事务执行期间,其他事务无法对该事务的数据进行修改,直到该事务结束。这样可以保证在同一个事务中多次读取同一行数据时,得到的结果是一致的。

在MySQL中,使用如下的SQL语句设置事务隔离级别:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

或者在开始事务时指定:

START TRANSACTION ISOLATION LEVEL REPEATABLE READ;

需要注意的是,默认情况下MySQL的事务隔离级别是“可重复读”,因此大多数情况下不需要额外设置。但如果需要显式设置或更改事务隔离级别,可以使用上面的SQL语句。

10、MySQL存储过程?

MySQL存储过程是一组经过预编译的SQL语句的集合,它们被存储在数据库中,可以像调用普通SQL语句一样被调用和执行。存储过程通常用于完成特定的数据库操作,并且可以包含控制结构、变量、条件和循环等编程元素。

  1. 创建存储过程:使用CREATE PROCEDURE语句可以创建存储过程。例如:
DELIMITER //
CREATE PROCEDURE my_procedure()
BEGIN
    -- 存储过程的SQL语句
    SELECT * FROM my_table;
END //
DELIMITER ;

创建了一个名为my_procedure的存储过程,其中包含了一个简单的SQL语句。

  1. 参数:存储过程可以接受输入参数,并返回输出结果。参数可以是IN(输入)、OUT(输出)或INOUT(既输入又输出)类型。例如:
DELIMITER //
CREATE PROCEDURE my_procedure_with_param(IN input_param INT, OUT output_param INT)
BEGIN
    -- 存储过程的SQL语句
    SELECT input_param * 2 INTO output_param;
END //
DELIMITER ;

创建一个带有输入和输出参数的存储过程。

  1. 执行存储过程:使用CALL语句可以执行存储过程。例如:
CALL my_procedure_with_param(5, @result);
SELECT @result;

调用带有参数的存储过程,并通过@result变量获取了输出结果。

  1. 删除存储过程:使用DROP PROCEDURE语句可以删除存储过程。例如:
DROP PROCEDURE IF EXISTS my_procedure;

删除名为my_procedure的存储过程。

  1. 流程控制:存储过程可以包含流程控制语句,如条件语句和循环语句,以实现更复杂的逻辑。

存储过程的使用可以提高数据库的性能和可维护性,减少了重复编写SQL语句的工作。然而,在使用存储过程时需要注意合理的使用场景,避免滥用导致维护困难。

11、数据库与缓存一致?

数据库与缓存一致性是指在涉及数据读写的操作中,数据库和缓存之间的数据保持一致的状态。维持一致性对于系统的正确运行至关重要,否则可能导致数据不一致的问题。

  1. 读操作一致性: 当应用程序进行读取操作时,如果数据在数据库中发生了变化,缓存中的数据也应该能够及时更新。这确保了应用程序读取的数据是最新的。一般来说,可以通过缓存失效(缓存中的数据过期,需要重新从数据库中获取)或者主动刷新缓存的方式来保持读操作的一致性。
  2. 写操作一致性: 当应用程序进行写入操作时,除了更新数据库中的数据,还需要更新缓存,以确保缓存中的数据也是最新的。这可以通过在写入数据库后,同步更新缓存的方式来实现。注意,写操作的一致性可能涉及到并发写的处理,需要考虑事务的隔离级别和锁的机制。
  3. 缓存失效策略: 缓存中的数据不能永远保持有效,因此需要定义合适的缓存失效策略。失效策略可以基于时间(定时失效)、事件(数据库更新时失效)等进行选择。失效策略的选择要综合考虑业务需求和性能开销。
  4. 事务一致性: 如果系统中使用了事务,需要确保数据库和缓存在事务提交后保持一致。这可能涉及到事务的隔离级别和提交的时机。
  5. 缓存穿透和缓存雪崩: 缓存穿透是指缓存中无法命中的数据,导致每次都需要查询数据库,而缓存雪崩是指大量缓存同时失效,导致数据库负载激增。为了防止这两种情况,可以采用合适的缓存技术和策略,如使用分布式缓存、设置适当的缓存过期时间、使用热点数据预热等。

12、设计模式?

单例模式(Singleton Pattern):

思想: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

  1. 饿汉式(Eager Initialization)

在类加载的时候就创建实例,因此在使用时已经存在一个实例。

实现代码:

class SingletonEager {
private:
    // 私有的构造函数,防止外部实例化
    SingletonEager() {}

    // 静态实例,在类加载时初始化
    static SingletonEager instance;

public:
    // 公共的访问点
    static SingletonEager* getInstance() {
        return &instance;
    }
};

// 初始化静态实例
SingletonEager SingletonEager::instance;

优点:

  • 实现简单,线程安全(C++11之前,C++11及之后需要添加一些关键字保证线程安全)。

缺点:

  • 如果程序中未使用该单例,会造成资源浪费。
  1. 懒汉式(Lazy Initialization)

在需要使用时才创建实例,避免了不必要的资源浪费。

实现代码:

class SingletonLazy {
private:
    // 私有的构造函数,防止外部实例化
    SingletonLazy() {}

    // 静态实例,使用时初始化
    static SingletonLazy* instance;

public:
    // 公共的访问点,使用时创建实例
    static SingletonLazy* getInstance() {
        if (!instance) {
            instance = new SingletonLazy();
        }
        return instance;
    }
};

// 初始化静态实例为nullptr
SingletonLazy* SingletonLazy::instance = nullptr;

优点:

  • 节省了资源,只有在需要时才创建实例。

缺点:

  • 需要处理线程安全问题,否则可能导致多个线程同时创建实例。
  • 在多线程环境下,需要使用双重检查锁定或者其他机制来保证线程安全性。

工厂模式(Factory Pattern):

  • 思想: 定义一个创建对象的接口,但由子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。
  • 代码示例:
// 抽象产品
class Product {
public:
    virtual void create() = 0;
};

// 具体产品 A
class ConcreteProductA : public Product {
public:
    void create() override {
        std::cout << "Product A created.\n";
    }
};

// 具体产品 B
class ConcreteProductB : public Product {
public:
    void create() override {
        std::cout << "Product B created.\n";
    }
};

// 抽象工厂
class Factory {
public:
    virtual Product* createProduct() = 0;
};

// 具体工厂 A
class ConcreteFactoryA : public Factory {
public:
    Product* createProduct() override {
        return new ConcreteProductA();
    }
};

// 具体工厂 B
class ConcreteFactoryB : public Factory {
public:
    Product* createProduct() override {
        return new ConcreteProductB();
    }
};

观察者模式(Observer Pattern):

  • 思想:定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都得到通知并自动更新。
  • 代码示例:
// 抽象观察者
class Observer {
public:
    virtual void update() = 0;
};

// 具体观察者 A
class ConcreteObserverA : public Observer {
public:
    void update() override {
        std::cout << "Observer A received update.\n";
    }
};

// 具体观察者 B
class ConcreteObserverB : public Observer {
public:
    void update() override {
        std::cout << "Observer B received update.\n";
    }
};

// 主题
class Subject {
private:
    std::vector<Observer*> observers;

public:
    void attach(Observer* observer) {
        observers.push_back(observer);
    }

    void notify() {
        for (auto observer : observers) {
            observer->update();
        }
    }
};

13、修饰器模式与代理模式的区别?

修饰器模式(Decorator Pattern):

修饰器模式允许你通过将对象封装在装饰器类的对象中来动态地改变对象的行为。这种模式是对继承的一种有力补充,它提供了一种灵活的方式来扩展类的功能。

给个例子:

#include <iostream>

// Component接口
class Coffee {
public:
    virtual int cost() const = 0;
    virtual ~Coffee() {}
};

// ConcreteComponent具体组件
class SimpleCoffee : public Coffee {
public:
    int cost() const override {
        return 10;
    }
};

// Decorator装饰器
class CoffeeDecorator : public Coffee {
public:
    CoffeeDecorator(Coffee* decorated_coffee) : decorated_coffee_(decorated_coffee) {}

    int cost() const override {
        return decorated_coffee_->cost();
    }

private:
    Coffee* decorated_coffee_;
};

// ConcreteDecorator具体装饰器
class Milk : public CoffeeDecorator {
public:
    Milk(Coffee* decorated_coffee) : CoffeeDecorator(decorated_coffee) {}

    int cost() const override {
        return CoffeeDecorator::cost() + 5;
    }
};

// ConcreteDecorator具体装饰器
class Sugar : public CoffeeDecorator {
public:
    Sugar(Coffee* decorated_coffee) : CoffeeDecorator(decorated_coffee) {}

    int cost() const override {
        return CoffeeDecorator::cost() + 2;
    }
};

// 使用
int main() {
    Coffee* coffee = new SimpleCoffee();
    std::cout << "Cost: $" << coffee->cost() << std::endl;

    Coffee* milk_coffee = new Milk(coffee);
    std::cout << "Cost: $" << milk_coffee->cost() << std::endl;

    Coffee* sugar_milk_coffee = new Sugar(milk_coffee);
    std::cout << "Cost: $" << sugar_milk_coffee->cost() << std::endl;

    delete coffee;
    delete milk_coffee;
    delete sugar_milk_coffee;

    return 0;
}

代理模式(Proxy Pattern):

代理模式通过引入一个代理类,控制对原始对象的访问,并且可以在调用前后执行一些额外的操作。

再给个例子:

#include <iostream>

// Subject主题接口
class RealCoffee {
public:
    virtual int cost() const = 0;
    virtual ~RealCoffee() {}
};

// RealSubject真实主题
class SimpleRealCoffee : public RealCoffee {
public:
    int cost() const override {
        return 10;
    }
};

// Proxy代理
class CoffeeProxy : public RealCoffee {
public:
    CoffeeProxy(RealCoffee* real_coffee) : real_coffee_(real_coffee) {}

    int cost() const override {
        // 可以在调用前后执行一些额外的操作
        std::cout << "Proxy: Preprocessing..." << std::endl;
        int result = real_coffee_->cost();
        std::cout << "Proxy: Postprocessing..." << std::endl;
        return result;
    }

private:
    RealCoffee* real_coffee_;
};

// 使用
int main() {
    RealCoffee* real_coffee = new SimpleRealCoffee();
    std::cout << "Cost: $" << real_coffee->cost() << std::endl;

    RealCoffee* coffee_proxy = new CoffeeProxy(real_coffee);
    std::cout << "Cost: $" << coffee_proxy->cost() << std::endl;

    delete real_coffee;
    delete coffee_proxy;

    return 0;
}

在修饰器模式中,Decorator类和ConcreteDecorator类扩展了Component接口。而在代理模式中,Proxy类继承了Subject接口,但在代理中引入了一个真实的主题对象,而Decorator直接包含另一个组件。这两者的差别在于它们的目的和关注点:修饰器模式关注动态地添加或覆盖对象的行为,而代理模式关注对对象的访问控制。

14、索引失效原因?

  1. 数据分布不均匀: 如果索引列上的数据分布不均匀,即某些值出现的频率很高,而其他值很少出现,那么对于那些高频值的查询,使用索引并不能提高效率,反而可能增加IO操作。这种情况下,优化器可能会选择全表扫描而不是使用索引。
  2. 索引列上使用了函数: 当在索引列上使用函数时,索引可能失效。例如,WHERE UPPER(column_name) = 'VALUE',如果column_name上有索引,但是使用了UPPER函数,优化器可能无法使用索引。
  3. 使用了不等于操作符: 不等于操作符(<>!=)通常会导致索引失效。例如,WHERE column_name <> 'VALUE',在某些情况下,数据库可能不会使用索引,而是选择全表扫描。
  4. 使用了范围查询: 对于不同的数据库,范围查询的情况下,索引可能失效。例如,WHERE column_name BETWEEN 'VALUE1' AND 'VALUE2',有些数据库可能无法有效使用索引。
  5. 使用了OR操作符: 当OR操作符连接多个条件时,优化器可能会选择不使用索引。例如,WHERE column_name = 'VALUE1' OR column_name = 'VALUE2',在某些情况下可能不会使用索引。
  6. 表的统计信息过时: 数据库系统依赖于表的统计信息来生成查询计划。如果统计信息过时,优化器可能会做出错误的决策,导致索引失效。
  7. 小表或唯一值较少的列: 对于小表或者唯一值较少的列,数据库可能认为全表扫描更快速,而选择不使用索引。

15、数据区分度多大时用索引?

选择是否在某一列上创建索引通常取决于数据的区分度(selectivity)。数据的区分度是指该列中不同值的比例,通常用不同的值的数量与总行数的比率来表示。

高区分度:

  • 当数据列的区分度高时,即不同的值较多,创建索引是有利的。
  • 高区分度的列上的索引可以更有效地帮助数据库系统缩小搜索范围,提高查询效率。
  • 例如,性别列通常有较高的区分度,因为大多数情况下只有两个不同的值:男和女。

低区分度:

  • 当数据列的区分度低时,即不同的值较少,创建索引的效果可能会减弱。
  • 低区分度的列上的索引可能会增加存储和维护的成本,而查询时并没有显著提高性能。
  • 例如,一个只有几个不同状态值的列可能不适合创建索引。

在决定是否在某一列上创建索引时,需要考虑以下因素:

  1. 查询频率: 如果某列用于经常执行查询操作,特别是用于过滤或排序操作,那么在该列上创建索引是有利的。
  2. 表的大小: 对于小型表,全表扫描的代价可能相对较低,而对于大型表,索引更有可能提高查询性能。
  3. 写入操作频率: 索引的维护需要额外的资源,因此在频繁进行插入、更新、删除等写入操作的列上创建索引可能会带来额外的开销。
  4. 查询的选择性: 选择性是指查询条件返回的结果集占总行数的比例。选择性越高,索引的效果越好。

16 、Binlog类型

Binary Log(Binlog)是MySQL用于记录数据库中发生的更改的一种日志。Binlog记录了对MySQL数据库执行的所有更改操作,包括插入、更新和删除等。这对于数据备份、主从复制、故障恢复等方面都非常重要。

在MySQL中,有两种主要的Binlog类型:Statement-Based Logging(基于语句的日志)和Row-Based Logging(基于行的日志),还有一种混合类型。

  1. Statement-Based Logging (SBL):
    • 这是MySQL默认的Binlog格式。
    • 在SBL中,Binlog会记录执行的SQL语句。这意味着Binlog会记录执行的每个SQL语句,而不管这些语句实际上更改了多少行数据。
    • 优点是Binlog相对较小,因为它只记录SQL语句。
  2. Row-Based Logging (RBL):
    • 在RBL中,Binlog会记录实际更改的数据行。即使一条SQL语句更改了多行,每一行的变更都会被记录下来。
    • 这种格式的Binlog通常更大,但是它提供了更详细的信息,可以准确地重放每一行的更改。
  3. Mixed Logging:
    • Mixed Logging是一种混合模式,结合了SBL和RBL的优点。
    • 在Mixed Logging中,MySQL会根据情况选择使用SBL或RBL。通常,对于能够以语句形式精确重现的操作,使用SBL;对于无法以语句形式精确重现的操作,使用RBL。

选择Binlog的类型通常取决于数据库的特定需求和使用场景。Statement-Based Logging对于许多场景来说是足够的,而Row-Based Logging则提供了更详细和准确的信息,适用于需要精确数据更改记录的场景。混合模式则是一种折中的选择。

17、16kb的页一行1kb可以存多少数据

如果不思考直接回答16行那可能就是对数据库存储这块不怎么了解,所以一起看看阿Q怎么解的吧!

众所周知,MySQL的数据都储存在磁盘中而不是内存,具体的数据是存在行中的,而行是存在页中的。

页是InnoDB存储引擎磁盘管理的最小单位,默认一页是16k,那么可以存多少行呢?

学过计算机的都知道,1kb=1024b,那么16kb=1024x16=16384b。

我们常用的mysql 5.7 版本,UTF 编码,一个汉字等 3b,那么 16 kb = 16384 / 3 个汉字

所以说一行1kb,能存(16384-38-56-8)/1024大概是15行数据。

18、场景题:在大文件中找关键字->长字符串中找子串

在大文件中找关键字:

  1. 基于字符串匹配算法: 对于大文件,可以使用经典的字符串匹配算法,例如Knuth-Morris-Pratt (KMP) 算法或Boyer-Moore算法。这些算法可以在O(n + m)的时间内找到关键字,其中n是文本长度,m是关键字长度。
  2. 基于正则表达式: 如果关键字有一定的规律,可以使用正则表达式进行匹配。在大文件中,正则表达式引擎可能会提供一些优化。
  3. 分块处理: 将大文件划分为较小的块,然后逐块进行搜索。这有助于减小每次搜索的规模。

在长字符串中找子串:

  1. 基于标准库函数: 大多数编程语言的标准库都提供了字符串查找的函数,例如C++的std::string::find或Python的str.find
  2. KMP算法或Boyer-Moore算法: 这些经典的字符串匹配算法同样适用于在长字符串中找子串。
  3. 正则表达式: 如果子串有一定的规律,可以使用正则表达式进行匹配。
  4. 滑动窗口算法: 对于特定场景,可以使用滑动窗口算法,逐步移动窗口寻找子串。
  5. 基于后缀树或后缀数组: 对于较大规模的字符串匹配,可以考虑构建后缀树或后缀数组,以支持高效的子串匹配。

19、线程与进程?

进程:

  1. 定义:进程是一个独立的执行单元,有自己的地址空间、代码、数据和系统资源。
  2. 独立性:进程之间相互独立,一个进程的崩溃通常不会影响其他进程。
  3. 通信:进程之间通信相对较为复杂,通常需要使用进程间通信(IPC)机制,如管道、消息队列、共享内存等。
  4. 资源开销:进程有较大的资源开销,包括独立的内存空间、文件描述符、系统资源等。
  5. 创建:进程的创建和销毁相对较慢。

线程:

  1. 定义:线程是一个轻量级的执行单元,共享同一进程的地址空间和资源。
  2. 独立性:线程是进程的一部分,同一进程内的线程共享相同的资源,一个线程的问题可能影响整个进程。
  3. 通信:线程之间通信相对简单,可以直接共享进程内的数据。
  4. 资源开销:线程的资源开销相对较小,因为它们共享大部分资源。
  5. 创建:线程的创建和销毁相对较快。

共同点:

  1. 都是执行单元,执行一段代码。
  2. 都可以并发执行,提高程序的性能。
  3. 都能被操作系统调度和管理。

20、线程间通信?

  1. 共享内存: 多个线程共享同一块内存区域,通过读写这块内存来实现通信。需要注意的是,在共享内存的情况下,需要确保对共享数据的访问是原子的,否则可能发生竞态条件。
  2. 互斥锁(Mutex): 通过互斥锁可以保证在同一时刻只有一个线程能够访问共享资源。一个线程在访问共享资源之前会尝试获取锁,如果获取不到就等待,直到锁被释放。
  3. 条件变量(Condition Variable): 条件变量用于线程之间的条件同步。一个线程可以在满足特定条件时等待条件变量,而另一个线程在满足条件时发出信号或广播通知其他线程。
  4. 信号量(Semaphore): 信号量是一个计数器,用于控制对共享资源的访问。线程在访问资源之前必须获取信号量,访问完后释放信号量。
  5. 管道(Pipe): 管道是一种单向通信机制,可以用于父子进程或者通过线程间进行通信。一个线程写入管道,另一个线程从管道读取。
  6. 消息队列(Message Queue): 线程可以通过消息队列发送和接收消息。一个线程将消息放入队列,而另一个线程则从队列中取出消息。
  7. 套接字(Socket): 在网络编程中,套接字可以用于不同计算机上的线程间通信。套接字提供了一种通过网络进行通信的方式。
;