记录些Spring+题集(50)

1、说说TCP和UDP?

TCP:

  1. 面向连接:TCP 是一种连接导向的协议,它在通信开始前需建立连接,并在通信结束后释放连接。这确保了数据传输的可靠性,但也会增加一些额外开销。

  2. 可靠性:TCP 提供可靠的数据传输,确保数据包按顺序到达,并能重新发送丢失或损坏的数据包,以确保数据完整性。

  3. 流控制:TCP 支持流量控制机制,防止发送方发送过多数据,从而避免网络拥塞。这通过滑动窗口机制实现。

  4. 拥塞控制:TCP 具有内置的拥塞控制机制,可监测网络拥塞情况,并动态调整发送速率以防止拥塞。

  5. 有序性:TCP 确保数据包按顺序到达,对于需要数据有序性的应用至关重要。

  6. 连接开销:由于建立和维护连接的开销,TCP 的成本相对较高。这使其适用于需要可靠性的应用,如网页浏览、文件传输和电子邮件。

UDP:

  1. 无连接:UDP 是一种无连接的协议,无需建立连接,数据包可直接发送至目的地。这降低了通信开销,但也意味着它不提供连接的可靠性。

  2. 不可靠性:UDP 不提供数据包的可靠性,数据包可能会丢失、重复或乱序。它适用于对数据可靠性要求不高的应用,如音视频流媒体和在线游戏。

  3. 无流控制:UDP 不提供内置的流量控制机制,发送方可以以任意速率发送数据,不考虑接收方的处理能力。

  4. 无拥塞控制:UDP 没有内置的拥塞控制机制,不会主动监测网络拥塞情况,在拥塞网络中可能导致数据包丢失。

  5. 低开销:由于没有连接建立和维护的开销,UDP 具有较低的开销,适用于需要低延迟的应用,如实时音视频传输。

2、说说TCP可靠机制?

  1. 序列号和确认号:TCP 数据包中包含序列号和确认号。发送方使用序列号对数据包进行编号,接收方使用确认号来确认已成功接收数据。这确保了数据包的有序传输。

  2. 确认机制:接收方会定期发送确认(ACK)数据包,告知发送方已成功接收到的数据包。若发送方在一定时间内未收到确认,则会重新发送相应数据包。

  3. 超时重传:若发送方在一定时间内未收到确认,则会假定数据包丢失并进行超时重传。这确保了即使数据包在传输过程中丢失,最终仍能成功传输。

  4. 流控制:TCP 使用滑动窗口机制进行流控制。接收方通过通告窗口大小告知发送方还能接收多少数据。这有助于防止发送方发送过多数据,从而避免数据丢失和网络拥塞。

  5. 拥塞控制:TCP 内置拥塞控制机制,可监测网络拥塞情况。若发现网络拥塞,发送方会降低发送速率,以避免进一步加重拥塞。

  6. 有序性:TCP 确保数据包按顺序到达接收方,即使网络中可能出现乱序传输,TCP 会将数据包按正确顺序重新排列。

  7. 可靠的连接建立和终止:TCP 在连接建立和终止过程中采用多个握手与挥手步骤,确保连接可靠性。这包括三次握手和四次挥手过程。

3、说说MySQL引擎?

两个常见的:

  1. InnoDB:作为 MySQL 的默认存储引擎,InnoDB 支持事务处理和高度数据完整性。它提供行级锁定和外键约束,适用于注重数据一致性和事务处理的应用。

  2. MyISAM:MyISAM 是 MySQL 的另一种存储引擎,它支持全文本搜索和表级锁定。MyISAM 不支持事务处理,但适用于需要快速读取和写入的应用,如数据仓库和日志记录。

一些作为了解的:

  1. MEMORY:MEMORY 存储引擎将数据存储在内存中,因此读取速度非常快,但数据不会持久保存。适用于缓存和临时数据存储。

  2. NDB Cluster:NDB Cluster 存储引擎是 MySQL 的集群存储引擎,支持分布式数据库。适用于需要高可用性和负载均衡的应用。

  3. Archive:Archive 存储引擎用于高压缩率的数据存储,适用于归档数据和历史数据存储。

  4. TokuDB:TokuDB 存储引擎专注于大数据处理,支持高性能的数据插入和查询。适用于处理大量数据的应,如日志和分析应用。

  5. Federated:Federated 存储引擎允许在一个 MySQL 数据库中访问另一个远程 MySQL 数据库的表。适用于分布式数据访问。

  6. Blackhole:Blackhole 存储引擎实则不存储数据,仅将数据传递至其他存储引擎。可用于数据复制和数据分发。

  7. CSV:CSV 存储引擎用于读取和写入 CSV 文件格式的数据。适用于与其他应用程序交换数据。

  8. JSON:JSON 存储引擎支持 JSON 数据类型,用于存储和查询 JSON 数据。

4、说说索引失效?

索引失效是指在数据库查询中,数据库管理系统无法充分利用现有索引来加速查询,而需要进行全表扫描或全索引扫描的情况。这种情况可能导致查询性能下降。

索引失效的一些原因:

  1. 未使用索引列进行查询:如果查询条件不包含在索引列中,索引就无法加速查询。例如,如果有一个名为 "name" 的索引,但查询是基于 "age" 列的,那么索引就无法用于加速查询。

  2. 使用函数或操作符处理索引列:当在查询中对索引列使用函数或操作符时,索引可能会失效。例如,如果对 "name" 列进行了 UPPER() 函数操作,索引就无法使用。

  3. 使用不等号条件:某些不等号条件(如不等于、大于、小于等)可能导致索引使用受限。通常情况下,等于条件(=)更容易使用索引。

  4. 数据分布不均匀:若索引列的数据分布不均匀,即某些值出现次数很多,而其他值出现次数很少,索引的选择性较低,可能不会被选用加速查询。

  5. 索引列类型不匹配:若查询条件的数据类型与索引列的数据类型不匹配,索引可能会失效。例如,索引列是字符串类型,但查询条件使用了数字,索引可能无法使用。

  6. 索引顺序不匹配:针对复合索引,若查询条件的列顺序与索引的列顺序不匹配,索引可能无法加速查询。

  7. 表数据量太小:对于非常小的表,全表扫描通常比使用索引更高效,因此数据库管理系统可能会选择不使用索引。

5、说说脏读,不可重复读,幻读?

  1. 脏读(Dirty Read):脏读是指在一个事务读取另一个事务未提交的数据。当一个事务读取另一个事务的数据,而后者后来回滚,导致读取的数据实际上是无效的。这可能导致严重的数据不一致问题。

  2. 不可重复读(Non-Repeatable Read):不可重复读发生在一个事务内的两次查询之间,另一个事务修改了数据,使得第一次查询的结果与第二次查询的结果不一致。这是因为第一次查询返回的数据在第二次查询之前已经被修改。

  3. 幻读(Phantom Read):幻读是在一个事务内的两次查询之间,另一个事务插入了新的数据行,导致第一次查询的结果与第二次查询的结果不一致。尽管数据未被修改,但由于新数据的插入,第二次查询可能返回不同的结果。

这些问题与数据库的隔离级别相关。

SQL 标准定义了四种隔离级别:读未提交(Read Uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)、和串行化(Serializable)。不同的隔离级别会导致不同的数据一致性问题:

  • 在读未提交隔离级别下,允许脏读、不可重复读和幻读。

  • 在读提交隔离级别下,防止脏读,但允许不可重复读和幻读。

  • 在可重复读隔离级别下,防止脏读和不可重复读,但允许幻读。

  • 在串行化隔离级别下,防止脏读、不可重复读和幻读,但性能可能受到一定的影响。

6、MySQL事务隔离级别和幻读解决

  • MVCC(多版本并发控制):MVCC 是 MySQL 采用的一种机制,通过在数据库中保存不同版本的数据来解决并发读写问题。在 MVCC 中,每个事务启动时会获得一个事务 ID,读操作只能看到等于或小于该事务 ID 的数据版本,从而实现数据隔离。MVCC 主要应用于可重复读和读提交隔离级别,以解决不可重复读和幻读问题。然而,在可重复读隔离级别下,仍可能发生幻读。

  • Next-Key Locks:Next-Key Locks 是 MySQL 中的锁机制,在索引上创建了锁,包括满足条件的记录以及下一个记录的锁。这可以防止其他事务在当前事务读取或修改记录时插入新的记录,从而避免幻读。Next-Key Locks 主要用于可重复读隔离级别,以解决幻读问题。

需要注意的是,MVCC 和 Next-Key Locks 能有效减轻幻读问题,但无法在所有情况下完全解决幻读。在某些特定情况下,如涉及范围查询,仍然可能发生幻读。

在这种情况下,应根据应用的需求和数据一致性的要求选择事务隔离级别,权衡性能和一致性。若对数据一致性要求较高,可以选择串行化隔离级别,但要注意性能可能受到影响。

7、设计与实现一个分布式 IM 系统?

第一个项目:《腾讯太狠:10亿QPS的IM,如何实现?

第二个项目:《1000Wqps生产级高并发IM,怎么架构?

图片

8、算法题:树的先中后序遍历

前序遍历

前序遍历是一种深度优先遍历方式,它的访问顺序是先访问根节点,然后递归地访问左子树和右子树。

递归:

思路:

  1. 访问当前节点(根节点)。

  2. 递归遍历左子树。

  3. 递归遍历右子树。

示例代码:

#include <iostream>

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

void preorderTraversal(TreeNode* root) {
    if (root == nullptr) {
        return;
    }
    
    // 1. 访问当前节点
    std::cout << root->val << " ";
    
    // 2. 递归遍历左子树
    preorderTraversal(root->left);
    
    // 3. 递归遍历右子树
    preorderTraversal(root->right);
}

int main() {
    // 构建一个示例二叉树
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);
    root->left->left = new TreeNode(4);
    root->left->right = new TreeNode(5);

    // 执行先序遍历
    std::cout << "Preorder Traversal: ";
    preorderTraversal(root);
    
    return 0;
}

非递归

思路:

  1. 创建一个栈,以及一个指向树根节点的指针。

  2. 将根节点入栈。

  3. 弹出栈顶节点,访问它。

  4. 如果该节点有右子节点,将右子节点入栈,再将左子节点入栈(这样确保左子节点在栈顶)。

  5. 重复步骤3和4,直到栈为空。

示例代码:

void preorderTraversal(TreeNode* root) {
    std::stack<TreeNode*> s;
    TreeNode* current = root;

    while (current || !s.empty()) {
        while (current) {
            std::cout << current->val << " ";
            if (current->right) {
                s.push(current->right);
            }
            current = current->left;
        }
        if (!s.empty()) {
            current = s.top();
            s.pop();
        }
    }
}

中序遍历

中序遍历也是一种深度优先遍历方式,它的访问顺序是先遍历左子树,然后访问根节点,最后遍历右子树。

递归

思路:

  1. 递归遍历左子树。

  2. 访问当前节点(根节点)。

  3. 递归遍历右子树。

示例代码:

#include <iostream>

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

void inorderTraversal(TreeNode* root) {
    if (root == nullptr) {
        return;
    }
    
    // 1. 递归遍历左子树
    inorderTraversal(root->left);
    
    // 2. 访问当前节点
    std::cout << root->val << " ";
    
    // 3. 递归遍历右子树
    inorderTraversal(root->right);
}

int main() {
    // 构建一个示例二叉树
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);
    root->left->left = new TreeNode(4);
    root->left->right = new TreeNode(5);

    // 执行中序遍历
    std::cout << "Inorder Traversal: ";
    inorderTraversal(root);
    
    return 0;
}

非递归

思路:

  1. 创建一个栈,以及一个指向树根节点的指针。

  2. 从根节点开始,将所有左子节点入栈,直到达到最左边的叶子节点。

  3. 出栈一个节点,访问它。

  4. 如果该节点有右子节点,将右子节点设为当前节点,然后重复步骤2。

  5. 重复步骤3和4,直到栈为空且当前节点为空。

示例代码:

void inorderTraversal(TreeNode* root) {
    std::stack<TreeNode*> s;
    TreeNode* current = root;

    while (current || !s.empty()) {
        while (current) {
            s.push(current);
            current = current->left;
        }
        current = s.top();
        s.pop();
        std::cout << current->val << " ";
        current = current->right;
    }
}

后序遍历

后序遍历同样是一种深度优先遍历方式,它的访问顺序是先遍历左子树,然后遍历右子树,最后访问根节点。

递归

思路:

  1. 递归遍历左子树。

  2. 递归遍历右子树。

  3. 访问当前节点(根节点)。

示例代码:

#include <iostream>

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

void postorderTraversal(TreeNode* root) {
    if (root == nullptr) {
        return;
    }
    
    // 1. 递归遍历左子树
    postorderTraversal(root->left);
    
    // 2. 递归遍历右子树
    postorderTraversal(root->right);
    
    // 3. 访问当前节点
    std::cout << root->val << " ";
}

int main() {
    // 构建一个示例二叉树
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);
    root->left->left = new TreeNode(4);
    root->left->right = new TreeNode(5);

    // 执行后序遍历
    std::cout << "Postorder Traversal: ";
    postorderTraversal(root);
    
    return 0;
}

非递归

思路:

  1. 创建两个栈,一个用于节点的遍历,另一个用于保存遍历结果。

  2. 将根节点压入栈1。

  3. 从栈1弹出一个节点,然后将它压入栈2。

  4. 将当前节点的左子节点和右子节点分别压入栈1。

  5. 重复步骤3和4,直到栈1为空。

  6. 此时栈2中的元素就是后序遍历的顺序,将它们依次出栈并访问。

示例代码:

void postorderTraversal(TreeNode* root) {
    std::stack<TreeNode*> s1, s2;
    s1.push(root);

    while (!s1.empty()) {
        TreeNode* current = s1.top();
        s1.pop();
        s2.push(current);

        if (current->left) {
            s1.push(current->left);
        }
        if (current->right) {
            s1.push(current->right);
        }
    }

    while (!s2.empty()) {
        std::cout << s2.top()->val << " ";
        s2.pop();
    }
}

9、算法题:二进制加法

描述:

给定两个 01 字符串 a 和 b ,请计算它们的和,并以二进制字符串的形式输出。

输入为 非空 字符串且只包含数字 1 和 0

思路:

  1. 创建一个空字符串 result 用于保存结果,以及两个变量 carry(用于表示进位,初始化为0)和 i(用于从字符串末尾向前遍历)。

  2. 从字符串 a 和 b 的末尾开始,逐位取出字符并转换为整数,分别为 numA 和 numB。如果字符串已经遍历完,则对应的数字设为0。

  3. 计算当前位的和,即 sum = numA + numB + carry。注意,carry 初始为0,但在每次迭代后可能会变为1。

  4. 计算当前位的结果和进位:currentResult = sum % 2 和 carry = sum / 2

  5. 将 currentResult 转换为字符并插入到 result 的前面。

  6. 将 i 向前移动一位。

  7. 重复步骤2到步骤6,直到遍历完字符串 a 和 b

  8. 如果最后一次迭代后 carry 为1,表示有进位,将"1"插入到 result 的前面。

  9. 返回 result 作为最终结果。

示例代码:

#include <iostream>
#include <string>
using namespace std;

string addBinary(string a, string b) {
    string result = "";
    int carry = 0;
    int i = a.length() - 1, j = b.length() - 1;

    while (i >= 0 || j >= 0) {
        int numA = (i >= 0) ? (a[i] - '0') : 0;
        int numB = (j >= 0) ? (b[j] - '0') : 0;
        int sum = numA + numB + carry;
        int currentResult = sum % 2;
        carry = sum / 2;
        result = to_string(currentResult) + result;
        i--;
        j--;
    }

    if (carry == 1) {
        result = "1" + result;
    }

    return result;
}

int main() {
    string a = "1011";
    string b = "1101";
    string sum = addBinary(a, b);
    cout << "Sum: " << sum << endl;
    return 0;
}

10、项目中使用k8s,如何进行网络隔离的?

在 Kubernetes(K8s)中,网络隔离可以通过使用不同的Network Policies  网络策略和适当的 CNI 插件来实现。

网络策略是 Kubernetes 的一个核心功能,它允许管理员控制 Pod 之间的网络通信。使用 Network Policies 可以灵活地控制 Pod 之间的网络通信,而使用CNI 插件如Calico 可以提供额外的网络隔离功能。

同时,你可以使用 Kubernetes Java 客户端库来管理这些策略。

10.1. 使用 Network Policies

Kubernetes 支持使用 Network Policies 来控制 Pod 之间的网络通信。Network Policies 允许你定义规则,以允许或拒绝特定命名空间中的 Pod 之间的通信。

例如,以下是一个简单的 Network Policy,它拒绝从命名空间 ns1 到命名空间 ns2 的所有网络通信:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all
  namespace: ns1
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

你还可以使用 podSelector 来选择特定的 Pod,并使用 ingress 或 egress 规则来允许或拒绝这些 Pod 与其他 Pod 的通信。

10.2. 使用 Calico 网络CNI 插件

CNI(Container Network Interface)是一个标准,它定义了 Kubernetes 如何与网络插件交互。

Calico 是一个常见的 Kubernetes CNI 插件,它也提供了一种强大的方式来控制网络隔离。Calico 支持使用 Calico 网络策略来定义网络规则。

例如,以下是一个简单的 Calico 网络策略,它允许从命名空间 ns1 到命名空间 ns2 的所有网络通信:

apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: allow-all
  namespace: ns1
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

你还可以使用 podSelector 来选择特定的 Pod,并使用 ingress 或 egress 规则来允许或拒绝这些 Pod 与其他 Pod 的通信。

10.3. Java 代码实现

在 Java 应用程序中,可以使用 Kubernetes Java 客户端库来创建和管理 Network Policies。

以下是一个简单的 Java 代码片段,它展示了如何使用 Kubernetes Java 客户端库来创建一个 Network Policy:

import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.apis.NetworkingV1Api;
import io.kubernetes.client.openapi.models.V1NetworkPolicy;
import java.io.IOException;
public class NetworkPolicyExample {
    public static void main(String[] args) throws IOException, ApiException {
        ApiClient client = Configuration.getDefaultApiClient();
        NetworkingV1Api api = new NetworkingV1Api(client);
        V1NetworkPolicy policy = new V1NetworkPolicy();
        policy.setMetadata(null);
        policy.setSpec(null);
        // Set the desired network policy spec
        policy.getSpec().setPodSelector("");
        policy.getSpec().setPolicyTypes(Arrays.asList("Ingress", "Egress"));
        // Create the network policy
        api.createNamespacedNetworkPolicy("ns1", policy);
    }
}

这个代码片段创建了一个空的 Network Policy,你可以根据需要修改它。

11、在 springcloud-gateway 中 api 信息如何保存?

网关平台可以基于 成熟的网关如 kong、springcloud-gateway 进行扩展,二次开发。

如果使用 springcloud-gateway 进行 扩展, 是可以进行 路由规则的外部加载的, 可以从 redis、db 加载路由规则,设计和实现一个自己的 RouteDefinitionRepository 路由定义仓库 就OK了

从redis 加载路由规则的  参考代码如下

@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {

    public static final String GATEWAY_ROUTES = "geteway_routes";

    @Resource
    private StringRedisTemplate redisTemplate;

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        List<RouteDefinition> routeDefinitions = new ArrayList<>();
        redisTemplate.opsForHash().values(GATEWAY_ROUTES).stream()
            .forEach(routeDefinition -> routeDefinitions.add(JSON.parseObject(routeDefinition.toString(), RouteDefinition.class)));
        return Flux.fromIterable(routeDefinitions);
    }
}

springcloud-gateway 启动的时候,会进行 RouteDefinitionRepository 仓库的创建,如果自义了,就使用 自定义的,如果没有自定义,就默认用InMemoryRouteDefinitionRepository,这缓存了从配置文件读取的路由规则。

而做动态路由的关键就在这里。

即通过自定义的RouteDefinitionRepository类,来提供路由配置信息。替代 默认的 InMemoryRouteDefinitionRepository。springcloud gateway 的路由规则,可以从 外部数据源加载, 同样可以从nacos 加载。

springcloud-gateway  使用 响应式编程,这种编程模式和命令式相比,思维模式很不人性化, 执行流程跳跃性太强, 调试的时候,往往不知道下一步去了哪里。

所以,做技术选型的时候,既没有使用  springcloud-gateway ,也没有使用kong,二是基于netty 去设计 api 网关平台。性能高不说。

12、你们jvm是如何监控的?监控方式有哪些?

在 Java 中,JVM(Java Virtual Machine)的监控主要涉及对 JVM 内部状态和资源的使用情况进行监控和管理,以便在必要时对 JVM 进行优化和调整。

监控方式
1. JVM 参数监控

JVM 提供了一系列的参数,可以通过这些参数来控制和监控 JVM 的行为。例如,可以使用 -verbose:gc 参数来开启垃圾回收的详细输出,从而了解 GC 的频率、时间和回收的垃圾对象数量等信息。 另一个例子是,可以使用 -Xmx 参数来设置 JVM 最大允许的堆内存大小,从而限制堆内存的使用量。

2. JConsole & VisualVM

JConsole 和 VisualVM 是两个常用的图形化工具,用于监控和分析 JVM 的性能。它们可以显示 JVM 的各种指标,如 CPU 使用率、线程栈信息、死锁和垃圾回收等。

3. 使用 Java 代码监控

Java 代码可以通过一些 API 来获取 JVM 的信息。例如,可以使用 ManagementFactory 类来获取 JVM 的内存使用情况、线程栈信息等。 下面是一个简单的 Java 代码示例,展示了如何使用 ManagementFactory 类来获取 JVM 的内存使用情况:

import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.lang.management.ManagementFactory;
public class JVMMonitor {
    public static void main(String[] args) {
        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage memoryUsage = memoryMXBean.getHeapMemoryUsage();
        System.out.println("Used Memory: " + memoryUsage.getUsed());
        System.out.println("Committed Memory: " + memoryUsage.getCommitted());
    }
}
4. JVMTI

JVMTI(Java Virtual Machine Tool Interface)是 JVM 提供的一套用于监控和控制 JVM 的接口。它允许开发人员创建自己的监控和分析工具,以定制的方式来监控 JVM。

监控原理
1. JVM 参数监控

JVM 参数监控的原理很简单,JVM 通过读取命令行参数或配置文件中的参数来改变其行为。当 JVM 启动时,它会解析这些参数,并相应地调整其内部状态和资源使用。

2. JConsole & VisualVM

JConsole 和 VisualVM 的工作原理是利用 JVMTI 接口来获取 JVM 的各种信息。它们通过 JVMTI 接口向 JVM 发送请求,以获取所需的监控数据,并将其以可视化的方式呈现给用户。

3. JVMTI

JVMTI(Java 虚拟机工具接口、Java Virtual Machine Tool Interface)是JVM提供了一种编程接口,JVMTI 允许软件开发人员创建软件代理以监视和控制 Java 编程语言应用程序。

JVMTI 是 Java 2 Software Development Kit (SDK), Standard Edition, 版本 1.5.0 中的一种新增功能。JVMTI 它取代了 Java Virtual Machine Profiling Interface (JVMPI),从版本 1.1 起即作为 Java 2 SDK 的一种实验功能包括在内。在 JSR-163 中对 JVMTI 进行了有关说明。

JVMTI 的原理是,在 JVM 启动时,JVMTI 驱动程序会加载到 JVM 中,并创建一个 JVMTI 接口的实例。开发人员可以使用 JVMTI 接口提供的方法来获取 JVM 的各种信息,如垃圾回收数据、线程栈信息等。

4. JMX

JMX 全称为 Java Management Extensions,翻译过来就是 Java 管理扩展,用来管理和监测 Java 程序。

最常用到的就是对于 JVM 的监测和管理,比如 JVM 内存、CPU 使用率、线程数、垃圾收集情况等等。另外,还可以用作日志级别的动态修改,比如 log4j 就支持 JMX 方式动态修改线上服务的日志级别。

最主要的还是被用来做各种监控工具,常用的  Spring Boot Actuator、JConsole、VisualVM 等JVM监控工具,就是通过 JMX 来实现。Java 代码监控的原理是利用 ManagementFactory 类提供的 API 来获取 JVMTI 数据。ManagementFactory 类是 JVMTI 的一层包装,它将 JVMTI 的复杂性隐藏在简单的 Java 接口后面。

JMX 既是 Java 管理系统的一个标准,一个规范,也是一个接口,一个框架。有标准、有规范是为了让开发者可以定制开发自己的扩展功能,而且作为一个框架来讲,JDK 已经帮我们实现了常用的功能,尤其是对 JVM 的监控和管理。

grafana+prometheus+jmx

访问Grafana查看JVM面板,获得如下效果:

图片

JVM 面板

总结

监控 JVM 的方法有很多,每种方法都有其优缺点。

开发人员应根据具体的应用场景和需求来选择合适的监控方法。在实际开发中,通常会结合多种方法来监控 JVM,以获取更全面和准确的 JVM 状态和性能信息。

13、如何保证开发过程中使用正确的 nacos 内容?

三种方法:

(1)服务端 nacos 隔离

(2)服务端 用户 隔离

(3)客户端 配置 隔离

(1)服务端 nacos 隔离

为dev,test,prod 启动 不同的 nacos 实例,然后通过 环境变量 获取nacos 的 地址。

(2)服务端 用户 隔离

dev,test,prod 环境 共用nacos。为dev,test,prod 启动 不同的 nacos 用户,然后通过 不同的用户, 获取不同环境的 配置。

(3)客户端 配置  隔离

客户端 可以采用以下方法:

  1. 配置环境变量

    为每个环境创建特定的环境变量,并确保在运行应用程序时使用正确的环境变量。例如,你可以为 devtest 和 prod 环境创建名为 NACOS_SERVER_ADDRESS 的环境变量,分别指向各自的环境地址。

  2. 使用 Java System Properties

    你可以使用 Java 的 System.setProperty() 方法来为不同的环境设置不同的 Nacos 地址。这需要你在运行应用程序时传递相应的参数。

  3. 创建不同的配置文件

    为每个环境创建单独的配置文件,例如 application-dev.propertiesapplication-test.properties 和 application-prod.properties。这些文件可以包含 Nacos 的地址和其他相关配置。在运行应用程序时,使用 spring.config.location 参数来指定相应的配置文件。

  4. 使用 Spring Profiles

    Spring Profiles 允许你为不同的环境创建不同的配置。可以在 application.yml 或 application.properties 文件中使用 spring.profiles.active 属性来选择要激活的配置。

下面是使用 Java System Properties 的示例代码:

import java.util.Properties;
public class NacosConfig {
    public static void main(String[] args) {
        // 根据环境设置 Nacos 地址
        String nacosAddress;
        if (System.getProperty("env") == null || System.getProperty("env").equals("dev")) {
            nacosAddress = "localhost:8848";
        } else if (System.getProperty("env").equals("test")) {
            nacosAddress = "test-nacos-server:8848";
        } else if (System.getProperty("env").equals("prod")) {
            nacosAddress = "prod-nacos-server:8848";
        } else {
            throw new RuntimeException("Invalid environment");
        }
        // 使用 NacosAddress
        System.out.println("Nacos server address: " + nacosAddress);
    }
}

在运行应用程序时,你可以使用 -Denv=dev-Denv=test 或 -Denv=prod 参数来选择要使用的环境。

上述方法可以确保你在本地开发环境中使用正确的 Nacos 内容,而不会影响到其他环境。当然,具体实现可能会根据你的项目结构和使用的工具而有所不同。

14、nginx+keepalive是如何设计的?

Nginx 和 Keepalive 是两个不同的软件,但它们可以一起使用,以提供高可用性、负载均衡和缓存等优势。下面,我将分别简要介绍这两个软件,然后探讨它们是如何一起设计的。

Nginx

什么是 Nginx?

Nginx 是一个高性能的 HTTP 和反向代理服务器。它用于 HTTP 服务器、反向代理、负载均衡和 HTTP 缓存。

Nginx 的优势

  • 高性能:相对于 Apache,Nginx 能处理更多的并发连接。

  • 轻量级:相对于 Apache,Nginx 更轻量,资源占用更少。

  • 模块化:Nginx 的模块化架构使其易于扩展和定制。

  • 安全性:Nginx 注重安全性,提供诸如 SSL/TLS 加密、防止 SQL 注入等安全功能。

Keepalive

什么是 Keepalive?Keepalive 是一个支持集群的高可用性解决方案,主要用于数据库、队列服务和消息服务。

Keepalive 的优势

  • 高可用性:Keepalive 确保即使一个或多个服务器出现故障,服务仍然可以继续运行。

  • 负载均衡:Keepalive 可以通过多个服务器之间的负载均衡来提高性能。

  • 简化管理:Keepalive 简化了复杂的服务器集群管理。

Nginx + Keepalive

设计目的 Nginx 和 Keepalive 的结合旨在提供高可用性、高性能的 Web 应用程序。Nginx 用于处理 HTTP 请求,而 Keepalive 用于确保服务器的可靠性。

工作原理

  • 当客户端发起一个 HTTP 请求时,该请求首先到达 Nginx

  • Nginx 接收请求,并根据负载均衡策略,将请求分发给后端服务器。

  • 后端服务器处理请求,并将响应返回给 Nginx

  • Nginx 接收响应,并将其返回给客户端。

  • 如果某个后端服务器出现故障,Keepalive 会自动将其从服务列表中移除,并尝试重启它。

代理软件及硬件

在网络安全和性能优化领域,除了 Nginx 之外,还有许多其他代理软件和硬件设备。以下是一些常见的代理软件和硬件:

  1. Apache:Apache 是一款高性能的 HTTP 服务器,与 Nginx 类似,它也可以用作反向代理、负载均衡和 HTTP 缓存。Apache 支持多种模块,可以满足各种需求。

  2. Caddy:Caddy 是一个现代的、开源的 HTTP 服务器,专为安全和性能而设计。它支持 SSL/TLS 证书管理、反向代理、缓存等功能。

  3. Varnish:Varnish 是一个高性能的 HTTP 缓存服务器,可用于提高网站性能。它与 Nginx 类似,支持 HTTP/2 和 SSL/TLS。

  4. squid:squid 是一个开源的 HTTP 缓存服务器,可用于提高网站性能。它支持 HTTP/1.1 和 HTTP/2,以及 SSL/TLS。

  5. WebAccelerator:WebAccelerator 是一款专为提高 Web 应用程序性能而设计的硬件设备。它支持 HTTP 缓存、压缩、负载均衡等功能。

  6. F5 Networks:F5 Networks 是一家知名的网络设备制造商,其产品包括硬件负载均衡器、SSL/TLS 加密设备等。F5 的产品广泛应用于企业网络、数据中心等场景。

  7. Citrix:Citrix 是一家提供虚拟化和云计算解决方案的公司,其产品包括硬件代理、负载均衡器和 SSL/TLS 设备。Citrix 的产品适用于企业级用户,可提高应用程序性能和安全性。

  8. A10 Networks:A10 Networks 是一家提供网络解决方案的公司,其产品包括硬件负载均衡器、SSL/TLS 加密设备和防火墙等。A10 的产品适用于各种规模的企业和数据中心。

  9. Radware:Radware 是一家提供网络安全和应用交付解决方案的公司,其产品包括硬件代理、负载均衡器和防御设备。Radware 的产品适用于保护企业网络免受攻击,提高应用程序性能。

  10. Arbor Networks:Arbor Networks 是一家提供网络安全和流量管理解决方案的公司,其产品包括硬件代理、负载均衡器和防御设备。Arbor 的产品适用于大型企业和数据中心,可确保网络安全和高效运行。

这些代理软件和硬件设备根据其特点和功能,在不同的场景中发挥作用。在实际应用中,可以根据需求和预算选择合适的代理软件或硬件设备。同时,为了确保数据安全和高可用性,还可以将这些代理与其他安全措施(如身份验证、访问控制等)结合使用。

15、加密算法了解吗?使用过哪些加密算法?

在计算机科学中,加密算法是用于将数据转换为一种秘密形式的数学函数。它使得只有授权方能够访问原始数据,从而保护数据的机密性和安全性。

常见的加密算法包括:

1. 对称加密算法
  • AES:AES 适用于各种场景,如加密敏感数据、保护网络通信、加密存储等。例如,在电子支付、数据传输和数据库安全方面,AES 可以确保数据在传输过程中的安全性。AES 支持 128 位、192 位和 256 位密钥长度,密钥长度越长,安全性越高。

  • DES:DES 主要用于加密敏感数据,例如在金融和保险领域。但由于其安全性较低(已被破解),现在已经逐渐被 AES 取代。DES 支持 56 位密钥长度。

  • 3DES:3DES 适用于需要高安全性的场景,例如加密网络通信、安全认证等。但由于其性能较差,在许多场景下已被 AES 取代。3DES 是对 DES 的改进,采用 112 位或 168 位密钥长度。

2. 非对称加密算法
  • RSA:RSA 广泛应用于公钥基础设施(PKI)领域,如数字签名、加密电子邮件、安全认证等。RSA 适用于加密对称加密密钥,以防止密钥泄露。RSA 支持不同长度的密钥,但通常使用 1024 位或 2048 位以保证安全性。

  • DSA:DSA 主要用于数字签名,适用于安全认证、电子商务等场景。与 RSA 相比,DSA 在相同的安全级别下具有较小的密钥长度,但性能较差。DSA 支持 1024 位密钥长度。

  • ECC:ECC 适用于高安全性场景,如加密数字货币、安全认证等。与 RSA 相比,ECC 提供了相同的安全性,但具有更好的性能和较短的密钥长度。ECC 使用的密钥长度取决于所选曲线,通常为 256 位或 512 位。

3. 散列算法
  • SHA-1:SHA-1 适用于数字签名、文件完整性验证等场景。但由于哈希碰撞问题,SHA-1 已被认为是不安全的。在实际应用中,建议使用 SHA-256 或其他更安全的散列算法。

  • SHA-256:SHA-256 是 SHA-1 的一个变种,提供更高的安全性。它被用于数字签名、文件完整性验证等场景。SHA-256 具有 256 位的散列值,相较于 SHA-1,其安全性更高。

  • HMAC:HMAC 用于提供消息的完整性和身份验证。它适用于保护 Web 应用程序、网络设备等。HMAC 可以与多种散列函数(如 SHA-256)结合使用,以实现更高的安全性。

这些加密算法根据其特点和安全性在不同的场景中发挥作用。在实际应用中,通常会根据需求和安全性要求选择合适的加密算法。同时,为了确保数据安全,还需要结合其他安全措施,如身份验证、访问控制等。

非对称和对称完美结合

非对称加密既然也有缺陷,那我们就将对称加密,非对称加密两者结合起来,取其精华、去其糟粕,发挥两者的各自的优势:

图片

这是个非常非常经典的数据传输过程,也是Https传输协议里面最经典的部分。也是把对称加密和非对称加密的作用发挥到了很好的地方。

在https传输的过程中,如果单独只用对称加密,或者单独使用非对称加密都会出现问题。

16、如何处理应用频繁发生fullgc,cpu使用100%?

处理方式

临时处理

  • 首先,尝试通过JVM参数来调整垃圾回收策略和优化垃圾回收器的行为。例如,可以尝试增加堆内存大小,调整年轻代和老年代的比例,或者尝试其他垃圾回收器(如G1、Parallel等)。

  • 增加JVM的堆内存大小。使用-Xms-Xmx参数设置堆内存的大小。

  • 调整年轻代和老年代的大小。使用-XX:NewRatio-XX:SurvivorRatio参数。

  • 尝试使用其他垃圾回收器。使用-XX:+UseG1GC-XX:+UseParallelGC参数。

长久处理

  • 优化代码。这包括:

    • 减少不必要的对象创建。

    • 重用对象而不是每次都创建新的对象。

    • 使用对象池来重用对象。

    • 避免使用全局变量和静态变量。

    • 使用弱引用、软引用和幻象引用。

  • 监控和分析内存使用情况。使用Java内置的工具(如jconsolejvisualvm等)或第三方工具(如YourKit、MAT等)来分析内存使用情况。

  • 考虑使用分布式缓存系统(如Redis、Memcached等)来减轻应用服务器的负担。

定位问题

查看日志

  • 查看Java虚拟机(JVM)的日志,看是否有关于垃圾回收的错误或警告信息。

  • 查看应用服务器的日志,看是否有关于响应时间、错误率等与性能相关的信息。

分析内存

  • 使用内存分析工具(如MAT)来检查内存使用情况。查看哪些对象占据了大量的内存,以及是否存在内存泄漏。

分析线程

  • 检查是否有死锁或其他线程相关的问题。使用线程分析工具(如VisualVM、YourKit等)来检查线程栈和线程状态。

分析CPU

  • 使用CPU分析工具(如VisualVM、YourKit等)来检查CPU的使用情况。查看哪些线程或方法占据了大量的CPU时间。

解决办法

代码优化

  • 优化代码以减少不必要的对象创建和长时间的对象引用。

  • 使用对象池来重用对象。

  • 减少全局变量和静态变量的使用。

  • 使用弱引用、软引用和幻象引用。

调整JVM参数

  • 增加堆内存大小。

  • 调整年轻代和老年代的大小。

  • 尝试使用其他垃圾回收器。

使用分布式缓存

  • 考虑使用分布式缓存系统(如Redis、Memcached等)来减轻应用服务器的负担。

监控和分析

  • 通过promethus监控内存、CPU和线程的使用情况。使用Java内置的工具或第三方工具来分析服务器性能。

微服务如何拆分?原则是什么?

微服务设计的规范和原则

单体架构往往以烟筒式方式发展,往往存在两个主要问题:中心化高耦合

所谓中心化,就是数据集中存储在单个数据库中,业务系统集中部署在单台服务器上,通过集群部署方式提供服务能力,然而中心化的问题,也就是单点问题。

所谓高耦合,主要是指其中一个功能模块升级,其它的模块都得一起升级。模块依赖度高的本质是架构腐烂。因为本来架构可能就没有设计好,但是,在实际场景中,随着快速迭代开发,研发换了一波又一波,产品走了一茬又一茬,难免系统架构腐化严重。

如果解决单体架构的问题呢?方案很多,主要有:SOA、微服务架构。

SOA(Service-Oriented Architecture,面向服务的架构)是一种高层级的架构设计理念,可通过在网络上使用基于通用通信语言的服务接口,让软件组件可重复使用。SOA 集成了独立部署和维护的服务,并允许它们相互通信和协同工作,以构建一个跨不同系统的软件应用。

微服务(Microservices)是一种软件架构风格,它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模块化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic)的API集相互通信。

2014年,Martin Fowler 与 James Lewis 共同提出了微服务的概念,定义了微服务是由以单一应用程序构成的小服务,自己拥有自己的进程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用HTTP API通信。同时服务会使用最小的规模的集中管理 (例如 Docker) 能力,服务可以用不同的编程语言与数据库等组件实现。

如何 做单体架构 到微服务架构的升级呢 ?

1.准备好微服务治理基础设施

微服务首先需要有微服务基础设施,没有微服务基础设施,实践微服务就是一场灾难。

图片

2.单一责任原则(SRP)

SRP是微服务架构重要的原则。

每个微服务都应该负责一个单一的业务,并确保做好这个业务,这个业务粒度的大小取决于你对业务和架构综合考虑。

SRP能够确保微服务职责单一性、功能完整性拆分, 这样,就便于维护、测试和部署。

图片

3.松耦合原则

什么是松耦合:松耦合是指每个微服务都应该是独立的,并通过API与其他服务进行通信。

松耦合的优势:可以降低 级联故障 的风险,也可以提高服务可扩展性,提高微服务的可复用性。

尽量做彻底解耦,包含数据库层的解耦:

  • 数据库层的解耦,就是避免一个微服务与其他微服务共享数据库,因为这可能会导致数据不一致,并且会使故障排查变得非常困难。

  • 每个微服务也都应该只管理自己的数据,每个微服务都有自己的数据库来存储数据,以确保可扩展性和可靠性。

在设计微服务时,应该专注于创建小型、松散耦合和高度内聚的服务。

图片

4.领域驱动原则,不数据驱动原则,也不是界面驱动原则

DDD是一种软件设计方法,它专注于特定业务领域的软件设计。

微服务架构、微服务设计非常适合采用DDD,为什么?因为每个服务都可以设计为特定业务领域的具体实现。

图片

领域驱动设计,首先应建立领域模型,确定领域限界上下文,然后才进行微服务拆分,如果是 数据驱动原则/界面驱动原则 ,那么,是一上来就定义数据库表结构,就去调整领域逻辑代码。

领域模型和领域服务应具有高度通用性、稳定性,通过接口层和应用层屏蔽外部变化对业务逻辑的影响,保证核心业务功能的稳定性。

基于领域模型进行拆分,围绕业务领域按职责单一性、功能完整性拆分。

5.架构分层职责明确,严守调用规范,规避 “微服务小泥球”

老的单体架构, 常常被成为大泥球。

“大泥球”单体,主要的问题

  • 代码腐化:业务代码经过长时间的迭代,有很多重复代码。比如一个功能可能有 3,4 种实现。

  • 业务逻辑交织:业务应用经过长时间发展,功能变多,业务功能里的逻辑代码可能相互引用,交织不清。

  • 代码复杂:功能多,业务逻辑复杂,只有少数员工能理解。这也是代码腐化一种。

  • 维护性变差:修改 bug 或增加新功能时,牵一发而动全身。一个 bug 没修好可能导致整个软件不可用。

  • 扩展性变差:增加新功能时,牵一发而动全身。

  • 编译发布变长:软件代码量大,编译时长变长,导致发布时长也变长。

化解“大泥球”单体的措施,是微服务架构。微服务架构最基本的一个点:分而治之,由大化小。

  • 松耦合:划分为一个一个小的微服务,代码之间逻辑交织降低。

  • 独立部署:每个划分的微服务都是一个独立的项目,可以独立部署。

  • 编译发布改善:划分为独立的小项目,编译时长变短,发布时长相应变短。

  • 故障隔离:由于划分为一个一个微服务,故障仅发生在独立的微服内。

  • 可扩展性:每个服务可以独立横向扩展,也可以从应用程序中提出独立功能变成服务,扩展变强。

  • 职责单一团队:每个小的微服务都由一个小型的高度专注的团队负责。

  • 技术异构:每个团队可以选择适合该业务的技术。

微服务架构目的就是把一个大单体划分为各种微服务,松耦合,独立自治。微服务架构把一个大泥球,变成了很多个小而美的颗粒。每个小颗粒职责单一,边界明确,可以通过简单组装完成大的功能,自然就比之前的大泥球好处理得多

但是,迭代过程中出现了一种奇怪现象,微服务内部没有进行内部的模块划分,代码耦合严重,调用关系混乱,就像一个小泥球。

在腾讯视频的DDD重构案例中,在老的微服务架构中,存在分层不明确,下层服务过于理解业务逻辑,存在下层调用上层的问题,出现了代码耦合严重,调用关系混乱的 “微服务小泥球”。

具体请参考《DDD落地:从腾讯视频DDD重构之路,看DDD极大价值

在腾讯视频的DDD重构的过程中,明确架构分层,降低模块间不必要的耦合;

严格遵守分层架构原则,上层服务可调用下层服务,下层服务不涉及业务逻辑。如上下层服务需交互,可通过逻辑解耦方式实现,如消息队列或中转。

如果规避 “微服务小泥球”?各层职能定位清晰,只能上层调用下层,也就是说只能从外层调用内层服务,下层服务通过封装、组合或编排对上层暴露,服务粒度由细到粗。

微服中,各层职能定位清晰:

  • 基础层为各层提供资源服务

  • 领域层负责领域业务逻辑的实现

  • 应用层负责服务的编排和组合

  • 接口层对外进行服务暴露

图片

6.进行全方位的监控、记录

监控和日志记录对于微服务架构的安全、维护和调优都至关重要。

在拥有数百个微服务的项目中开发的主要困难之一是调试非常困难,因为服务分散、日志分散,很难找到失败的原因。

因此,每个服务都应该有日志记录和监控措施,以跟踪其性能并检测错误。

图片

7.通过CI/CD实现devops (开发运维一体化)

CI/CD是一种软件开发运维过程实践,打通开发和运维环节,实现应用程序的构建、测试和部署自动化。任何微服务都应该是可持续部署的,实现微服务的快速高效部署,缩短了微服务上线时间。

图片

总之,采用微服务架构开发有许多优势,但要确保为微服务系统成功实施就需要遵循一些设计原则。

包括但不限于上面介绍的几个基础原则,在此基础上还需要有与所在领域或者行业的做定制化优化实践。

8.基于业务需求变化频率进行微服务拆分

这是一条扩展原则,是一条针对特定场景的的微服务拆分原则。

分离变和不变, 是设计领域遵守的一条  核心的原则。

识别变动频繁的业务需求和领域模型,考虑业务变更频率与相关度,将业务需求变动较高和功能相对稳定的业务进行分离。经常性变动必然会导致代码的频繁修改和版本发布,分离变和不变,可以有效降低频繁变动业务对稳态业务的影响。

9.基于吞吐量进行微服务拆分

这是一条扩展原则,是一条针对特定场景的的微服务拆分原则。

识别领域模型中性能压力较大、高吞吐量的功能,并且进行解耦,解除独立的微服务。

两个好处:

  • 避免高吞吐服务,拖累整个系统的性能, 由于局部的性能拖累整体性能。

  • 解耦之后,可以对高吞吐量服务定制个性化的 扩容缩容策略、熔断保护策略、限流策略。

10.基于技术异构因素进行微服务拆分

这是一条扩展原则,是一条针对特定场景的的微服务拆分原则。

领域模型中有些功能虽然在同一个业务域内,但在技术实现时可能会存在较大的差异,也就是说领域模型内部不同的功能存在技术异构的问题。

比如,在 go+java 双语言云原生架构实操中, 有的可能用go,有的则是 Java,当然,还有的微服务用大数据架构。

所以,对于这些存在技术异构的功能,可以考虑按照技术边界进行拆分。

参考文献:

https://www.51cto.com/article/769421.html

https://blog.csdn.net/inaoen/article/details/121107764

https://www.jianshu.com/p/1c16195e9a1b

MySQL为何需要4M来双写?为什么redo不双写?

InnoDB 存储引擎

InnoDB 存储引擎最早由 Innobase Oy 公司开发(属第三方存储引擎)。

从 MySQL 5.5 版本开始作为表的默认存储引擎。该存储引擎是第一个完整支持 ACID 事务的 MySQL 存储引擎,特点是行锁设计、支持 MVCC、支持外键、提供一致性非锁定读,非常适合 OLTP 场景的应用使用。

目前也是应用最广泛的存储引擎。InnoDB 存储引擎架构包含内存结构和磁盘结构两大部分

MySQL  8.0 版本,总体架构图如下:

图片

MySQL  5.5 版本,总体架构图如下:

图片

InnoDB 存储结构

1、磁盘结构

1.1 表空间 Tablespaces

InnoDB 存储引擎的逻辑存储结构是将所有的数据都被逻辑地放在了一个空间中,这个空间中的文件就是实际存在的物理文件(.ibd 文件),即表空间。

默认情况下,一个数据库表占用一个表空间,表空间可以看做是 InnoDB 存储引擎逻辑结构的最高层,所以的数据都存放在表空间中,例如:表对应的数据、索引、insert buffer bitmap undo 信息、insert buffer 索引页、double write buffer files 等都是放在共享表空间中的。

表空间分为:

  • 系统表空间 (ibdata1 文件)(共享表空间)

  • 临时表空间

  • 常规表空间

  • Undo 表空间

  • file-per-table 表空间 (独立表空间)。

系统表空间又包括双写缓冲区 (Doublewrite buffer)、Change Buffer 等

系统表空间 System Tablespace

系统表空间可以对应文件系统上一个或多个实际的文件,默认情况下, InnoDB 会在数据目录下创建一个名为.ibdata1的文件,大小为 12M

这个.ibdata1文件就是对应的系统表空间在文件系统上的表示。

.ibdata1这个文件是可以自扩展的,当不够用的时候它会自己增加文件大小。

需要注意的一点是,在一个 MySQL 服务器中,系统表空间只有一份。

从 MySQL5.5.7 到 MySQL5.6.6 之间的各个版本中,我们表中的数据都会被默认存储到这个系统表空间。

show variables like '%innodb_data_file_path%'

图片

独立表空间

在 MySQL5.6.6 以及之后的版本中, InnoDB 并不会默认的把各个表的数据存储到系统表空间中,而是为每一个表建立一个独立表空间,也就是说我们创建了多少个表,就有多少个独立表空间。

使用独立表空间来存储表数据的话,会在该表所属数据库对应的子目录下创建一个表示该独立表空间的文件,文件名和表名相同,只不过添加了一个.ibd 的扩展名而已。

show variables like '%innodb_file_per_table%'

图片

独立表空间只是存放数据、索引和插入缓冲 Bitmap 页,其他类的数据如回滚(undo)信息、插入缓冲索引页、系统事务信息、二次写缓冲等还是存放在原来的系统表空间。

其他类型的表空间

随着 MySQL 的发展,除了上述两种表空间之外,现在还新提出了一些不同类型的表空间,比如通用表空间 (general tablespace)、undo 表空间 (undo tablespace)、临时表空间 (temporary tablespace) 等。

表空间结构

表空间又由段 (segment)、区 ( extent)、页 (page) 组成,页是 InnoDB 磁盘管理的最小单位。

在我们执行 sql 时,不论是查询还是修改,mysql 总会把数据从磁盘读取内内存中,而且在读取数据时,不会单独加在一条数据,而是直接加载数据所在的数据页到内存中。

表空间本质上就是一个存放各种页的页面池。

图片

「page页」是 InnoDB 管理存储空间的基本单位,也是内存和磁盘交互的基本单位。

也就是说,哪怕你需要 1 字节的数据,InnoDB 也会读取整个页的数据,InnoDB 有很多类型的页,它们的用处也各不相同。

比如:有存放 undo 日志的页、有存放 INODE 信息的页、有存放 Change Buffer 信息的页、存放用户记录数据的页(索引页)等等。

InnoDB 默认的页大小是 16KB,在初始化表空间之前可以在配置文件中进行配置,一旦数据库初始化完成就不可再变更了。

SHOW VARIABLES LIKE 'innodb_page_size'

图片

1.2 重写日志 redo log 文件

redo log 记录数据库的变更,数据库崩溃后,会从 redo log 获取事务信息,进行系统恢复。

redo log 在磁盘上表现为 ib_logfile0 和 ib_logfile1 两个文件。

MySQL 会在事务的提交前将 redo 日志刷新回磁盘。

在同一时间提交的事务,会采用组提交(group commit)的方式一次性刷新回磁盘。从而避免一个事务刷新一次磁盘,提高性能。

1.3 Double Write Files 双写缓冲文件

double write 是保障 InnoDB 存储引擎操作数据页的可靠性。

double write 分为两部分组成,一部分在内存中的 double write buffer, 大小为 2MB,另一部分是物理磁盘上共享表空间中连续的 128 个数据页,即 2 个区大小(同样是 2MB)。

图片

InnoDB存储引擎doublewrite架构

2、内存结构

InnoDB 存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理,因此可将其视为基于磁盘的数据库系统(Disk-base Database)。

在数据库中 CPU 速度与磁盘速度是有很大差距的,所以,基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。结构如图所示:

图片

2.1 缓存池 Buffer Pool

Buffer Pool 是 InnoDB 内存中的一块占比较大的区域,通过内存的速度来弥补磁盘速度慢对数据库性能的影响。

在数据库中进行读取页的操作,首先将从磁盘读到的页放在缓冲池中,这个过程称为将页”FIX” 在缓冲池中,下次再读到相同的页时,首先判断该页是否在缓冲池中,若在缓冲池中,直接读取该页,否则读取磁盘上的页。

对于数据库中的页的修改操作,首先修改在缓冲池中的页,然后再以一定频率刷新到磁盘上,这里需要注意的是,页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为 Checkpoint 的机制刷新回磁盘。

缓存区缓存的数据页类型有:索引页,数据页,undo 页,插入缓冲(change buffer),自适应哈希索引(adaptive hash index),InnoDB 存储锁信息(lock info),数据字典信息(data dictionary)。数据页和索引页占据了缓冲池很大部分。

2.2 写缓冲 Change Buffer

在 MySQL5.5 之前,叫插入缓冲(Insert Buffer),只针对 INSERT 做了优化;

现在对 DELETE 和 UPDATE 也有效,叫做写缓冲(Change Buffer)。

它是一种应用在非唯一普通索引页(non-unique secondary index page)不在缓冲池中,对页进行了写操作,并不会立刻将磁盘页加载到缓冲池,而仅仅记录缓冲变更(Buffer Changes),等未来数据被读取时,再将数据合并(Merge)恢复到缓冲池中的技术。写缓冲的目的是降低写操作的磁盘 IO,提升数据库性能。

2.3 自适应散列索引 Adaptive Hash Index

自适应哈希索引用于优化对 BP 数据的查询。

InnoDB 存储引擎会监控对二级索引数据的查找,如果观察到建立哈希索引可以带来速度的提升 (最近连续被访问三次的数据),则建立哈希索引,自适应哈希索引通过缓冲池的 B + 树构造而来,因此建立的速度很快。

InnoDB 存储引擎会自动根据访问的频率和模式来为某些页建立哈希索引。(在高负载系统下 AHI 容易产生资源的争用,进而引起一些 bug 导致系统受影响甚至崩溃,故建议关闭该功能)

2.4 重做日志缓冲区 rodo Log Buffer

重做日志缓冲区,当在 MySQL 中对 InnoDB 表进行数据更改时,这些更改首先存储在 InnoDB 日志缓冲区的内存中,然后再写入重做日志(redo logs)的 InnoDB 日志磁盘文件中。

rodo Log Buffer让 MySQL 在崩溃的时候具有了恢复数据的能力,即在数据库发生意外的时候,可以进行数据恢复;日志缓冲区 log buffer 是内存存储区域,用于保存要写入磁盘上的日志文件的数据。日志缓冲区大小由 innodb_log_buffer_size 变量定义,默认大小为 16MB。日志缓冲区的内容定期刷新到磁盘。较大的日志缓冲区可以运行大型事务,而无需在事务提交之前将重做日志数据写入磁盘。

因此,如果有更新,插入或删除许多行的事务,则增加日志缓冲区的大小可以节省磁盘 I/O。

回到问题:为什么需要redo log

先用一个表对redo log 、undo log、binlog三种日志进行对比 介绍,方便大家理解,  下表涉及到redo log 、undo log、binlog三种日志使用场景和文件等

图片

undolog、redolog都是InnoDB引擎中的日志,都是在Buffer Pool中,而binlog在Server层中,位于每条线程中,

redo log 、undo log、binlog三大日志在磁盘中的的归档方式和文件都是不一样的,之间的区别如下图。

图片

在InnoDB存储引擎中,大部分redo log记录的是物理日志,具体来说,就是记录某个数据页做了什么修改。

前面讲到, 所有数据页的读写操作都要通过buffer pool进行

  • innodb的读操作:先从buffer pool中查看数据的数据页是否存在,如果不存在,则将数据页先从磁盘读到buffer pool中然后在读

  • innodb的写操作:先把数据和日志写入buffer pool和log buffer,再由后台线程以一定的频率将buffer中的内存刷到磁盘。

buffer pool是一块内存区域,是一种“降低磁盘访问机制”,buffer pool缓存数据表和索引数据,把磁盘上的数据加载到缓冲池,避免每次访问都进行磁盘IO,起到加速访问的作用。

buffer pool也是以页为存储单位(与磁盘中数据页索引页单位一样,默认大小16K),buffer pool底层采用链表的数据结构管理page。

写操作的事务持久性由redo log落盘保证,buffer pool只是为了提高读写效率。

当buffer pool中某页的数据更改后,这个页就变成了脏页(因为mysql为了提升性能,避免频繁的随机IO,每次更改数据不是第一时间去更改磁盘数据,这就导致了buffer pool中的数据和磁盘数据不一致)

实际上, redo log日志主要包括两部分:

  • 一是在内存中重做日志缓冲(redo log Buffer)易丢失,在内存中

  • 二是重做日志文件(redo log file),保存在磁盘中。

为了保证脏页落盘的安全性,防止断电丢失等,会 WAL 预写redo log Buffer 中的日志到磁盘中。

WAL机制是什么?

WAL,全称是Write-Ahead Logging, 预写日志系统。指的是 MySQL 的写操作并不是立刻更新到磁盘上,而是先记录在日志上,然后在合适的时间再更新到磁盘上。

MySQL真正使用WAL的原因是:磁盘的写操作是随机IO,比较耗性能,所以如果把每一次的更新操作都先写入log中,那么就成了顺序写操作,实际更新操作由后台线程再根据log异步写入。

通过WAL,这样对于client端,延迟就降低了。

并且,由于顺序写入大概率是在一个磁盘块内,这样产生的IO次数也大大降低。

所以WAL的核心在于将随机写转变为了顺序写,降低了客户端的延迟,提升了吞吐量。

使用redo log的好处主要2点:

  • 降低刷盘频率:变更日志先行存入redo log buffer,脏页刷盘和redo log buffer刷盘时机可控;

  • 日志占用空间小:存储表空间ID、页号、偏移量和需要更新的值。

图片

为什么写数据需要Double write Buffer

MySQL程序是跑在Linux操作系统上的,理所当然要跟操作系统交互,一般来说,MySQL中一页数据是16kb,操作系统一个页是 4kb,所以,mysql page 刷到磁盘,要写4个文件系统里的页。

如图所示:

图片

需要注意的是,这个刷页的操作并非原子操作,比如我操作系统写到第二个页的时候,Linux机器断电了,这时候就会出现问题了。

上面的问题,也叫「Partial Page Write(部分页写入)」问题,或者叫做「页数据损坏」。并且这种页数据损坏靠 redo日志是无法修复的。

为啥 redo日志是无法修复  部分页写入问题呢?

具体来说,和 redo 的记录内容有关。redo log 的内容包括  存储表空间ID、页号、偏移量和需要更新的值。

也就是说,redo log日志中记录的是对页的物理操作,而不是页面的全量记录,当发生「Partial Page Write(部分页写入)」问题时,如果出现问题的数据是未修改过的数据,此时redo日志无能为力

Doublewrite Buffer 就是为了解决「页数据损坏」 问题的。

Doublewrite Buffer  提供了数据页的可靠性,虽然名字带了Buffer,但实际上Doublewrite Buffer是「内存+磁盘」的结构。

Doublewrite Buffer  包括两个部分:

  • 内存结构:Doublewrite Buffer内存结构由128个页(Page)构成,大小是2MB。

  • 磁盘结构:Doublewrite Buffer磁盘结构在系统表空间上是128个页(2个区,extend1和extend2),大小是2MB。

Doublewrite Buffer的原理是,再把数据页写到数据文件之前,InnoDB先把它们写到一个叫「doublewrite buffer(双写缓冲区)」的共享表空间内,在写doublewrite buffer完成后,InnoDB才会把页写到数据文件适当的位置。

如果在写页的过程中发生意外崩溃,InnoDB会在doublewrite buffer中找到完好的page副本用于恢复。

Doublewrite Buffer原理

图片

如上图所示,当有数据页要刷盘时:

  1. 页数据先通过memcpy函数拷贝至内存中的Doublewrite Buffer中。

  2. Doublewrite Buffer的内存里的数据页,会fsync刷到Doublewrite Buffer的磁盘上,分两次写入磁盘共享表空间中(连续存储,顺序写,性能很高),每次写1MB。

  3. Doublewrite Buffer的内存里的数据页,再刷到数据磁盘存储.ibd文件上(离散写)。

所以在正常的情况下,MySQL写数据页时,会写两遍到磁盘上,

  • 第一遍是写到doublewrite buffer,

  • 第二遍是写到真正的数据文件中,这便是「Doublewrite」的由来。

如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的Double write中找到该页的一个副本,将其复制到表空间文件,再应用redo日志。

Doublewrite Buffer相关参数

以下是一些与Doublewrite Buffer相关的参数及其含义:

  • innodb_doublewrite:这个参数用于启用或禁用双写缓冲区。设置为1时启用,设置为0时禁用, 默认值为1。

  • innodb_doublewrite_files:这个参数定义了多少个双写文件被使用。默认值为2,有效范围从2到127。

  • innodb_doublewrite_dir:这个参数指定了存储双写缓冲文件的目录的路径。默认为空字符串,表示将文件存储在数据目录中。

  • innodb_doublewrite_batch_size: 这个参数定义了每次批处理操作写入的字节数。默认值为0,表示InnoDB会选择最佳的批量大小。

  • innodb_doublewrite_pages:这个参数定义了每个双写文件包含多少页面。默认值为128。

Doublewrite Buffer和redo log

在MySQL的InnoDB存储引擎中,Redo log和Doublewrite Buffer共同工作以确保数据的持久性和恢复能力。

  1. 首先wal架构:当有一个DML(如INSERT、UPDATE)操作发生时, InnoDB会首先将这个操作写入redo log(内存)。这些日志被称为未检查点(uncheckpointed)的redo日志。

  2. 然后,在修改内存中相应的数据页之后,需要将这些更改记录在磁盘上。

    但是直接把这些修改的页写到其真正的位置可能会因发生故障导致页部分更新,从而导致数据不一致。因此,InnoDB的做法是先将这些修改的页按顺序写入doublewrite buffer。

    这就是为什么叫做 "doublewrite" —— 数据实际上被写了两次,先在doublewrite buffer,然后在它们真正的位置。

  3. 一旦这些页被安全地写入doublewrite buffer,它们就可以按原始的顺序写回到文件系统中。

    即使这个过程在写回数据时发生故障,我们仍然可以从doublewrite buffer中恢复数据。

  4. 最后,当事务提交时,相关联的redo log会被写入磁盘。这样即使系统崩溃,redo log也可以用来重播(replay)事务并恢复数据库。

在系统恢复期间,InnoDB会检查doublewrite buffer,并尝试从中恢复损坏的数据页。

如果doublewrite buffer中的数据是完整的,那么InnoDB就会用doublewrite buffer中的数据来更新损坏的页。

否则,如果doublewrite buffer中的数据不完整,InnoDB也有可能丢弃buffer内容,重新执行那条redo log以尝试恢复数据。

所以,Redo log和Doublewrite Buffer的协作可以确保数据的完整性和持久性。如果在写入过程中发生故障,我们可以从doublewrite buffer中恢复数据,并通过redo log来进行事务的重播。

总结

Doublewrite Buffer是InnoDB的一个重要特性,用于保证MySQL数据的可靠性和一致性。

它的实现原理是通过将要写入磁盘的数据先写入到Doublewrite Buffer中的内存缓存区域,然后再写入到磁盘的两个不同位置,来避免由于磁盘损坏等因素导致数据丢失或不一致的问题。

总的来说,Doublewrite Buffer对于改善数据库性能和数据完整性起着至关重要的作用。尽管其引入了一些开销,但在大多数情况下,这些成本都被其提供的安全性和可靠性所抵消。

说说MySQL一条SQL语句的执行过程?

MySQL一条SQL语句的执行过程

MySQL一条SQL语句的执行过程可以大致分为以下几个步骤:

1. 连接器(Connection Phase)

首先,客户端与MySQL服务器建立连接,这一阶段由连接器负责处理,包括进行身份验证和权限确认等。

主要包括以下任务:

  • 进行身份验证:验证客户端连接的身份,如用户名和密码。

  • 权限确认:确认客户端是否有权限访问特定的数据库和表。

  • 配置设置:设置连接参数,如字符集、事务处理模式等。

2. 查询缓存(Query Cache)

MySQL会检查查询缓存,如果所需查询的结果已经存在于缓存中,便直接返回缓存结果,从而省略后续的查询过程。查询缓存可以提高查询效率,特别是对于重复执行的查询。

3. 解析器和预处理器(Parser and Preprocessor)

当查询未命中缓存时,MySQL会运用解析器和预处理器对查询语句进行解析,验证语法正确性,并将查询语句转换为内部数据结构。

具体包括以下步骤:

  • 词法分析:将SQL语句分解为关键字、表名、列名、运算符等词汇。

  • 语法分析:检查分解后的词汇是否符合SQL语法规则,生成语法树。

  • 预处理:根据语法树,对查询语句进行预处理,如替换表名、检查约束条件等。

4. 优化器(Optimizer)

MySQL优化器根据查询语句的结构、表的统计信息等因素,生成多个可能的执行计划,并通过成本估算器挑选出最优的执行计划。

优化器根据以下因素生成多个可能的执行计划:

  • 查询语句的结构:如单表查询、多表连接、聚合操作等。

  • 表的统计信息:如表的大小、索引的使用情况等。

  • 系统配置:如缓存大小、CPU核心数等。

优化器还会使用成本估算器(Cost-based Optimizer)挑选出最优的执行计划。

5. 执行器(Executor)

执行器按照优化器选定的执行计划,调用存储引擎的API来执行查询,获取所需数据。

具体包括以下步骤:

  • 生成执行计划:根据优化器生成的执行计划,生成具体的执行步骤。

  • 调用存储引擎:根据执行步骤,调用存储引擎的API读取或写入数据。

  • 返回结果:将查询结果返回给客户端。

6. 存储引擎(Storage Engine)

存储引擎负责实际的数据存储和检索,根据执行器的请求,读取或写入数据。

根据执行器的请求,执行以下任务:

  • 读取数据:从磁盘或其他存储介质中读取数据。

  • 写入数据:将数据写入磁盘或其他存储介质。

  • 数据处理:对数据进行计算、排序等操作。

需要指出的是,MySQL采用分层架构,其中连接器、查询缓存、解析器、优化器、执行器等功能属于Server层,而存储引擎主要负责数据存储和检索。

MySQL支持多种存储引擎,例如InnoDB、MyISAM等,各种存储引擎在数据存储和检索方面可能有差异。

总之,这是MySQL执行一条SQL语句的大致过程。但具体执行过程可能受到查询语句复杂性、表的大小、索引使用等因素的影响。

举个例子

举个例子,MySQL执行一条简单的查询语句select * from users where age='18' and name='tom'的执行流程如下:

  1. 首先,通过连接器,客户端与MySQL服务器建立连接,并完成身份认证和权限验证过程。在此过程中,客户端需要提供用户名和密码以证明其合法性,服务器则会对这些信息进行核对。

  2. 检查是否开启缓存(Mysql8.0之前),开启了 Query Cache 且命中完全相同的 SQL 语句,则将查询结果直接返回给客户端;

  3. MySQL的解析器会对查询语句进行解析,检查语法是否正确,并将查询语句转换为内部数据结构。如查询是select、表名users、条件是age='18' and name='tom',预处理器则会根据MySQL的规则进一步检查解析树是否合法,如检查数据表或数据列是否存在等。

  4. 优化器会根据查询语句的结构、表的统计信息等因素,生成多个可能的执行计划,并通过成本估算器选出最优的执行计划。这一步旨在提高查询效率,降低资源消耗。

  5. 执行器按照优化器选择的执行计划,调用存储引擎的API来执行查询。存储引擎负责实际的数据存储和检索,根据执行器的请求,读取或写入数据。

  6. 存储引擎负责实际的数据存储和检索工作,根据执行器的请求,读取或写入数据。

  7. 若开启了 Query Cache,则缓存,否则直接返回。

什么是数据库事务?InnoDB一次事务的执行过程?

什么是数据库事务?

数据库事务( transaction) 是指作为一个逻辑工作单元执行的一系列数据库操作,这些操作要么全部成功,要么全部失败,是一个不可分割的工作单元。

事务是在事务开始和事务结束之间执行的一系列数据库操作。

事务的目的是确保数据库操作的一致性和完整性,同时提供对并发访问的隔离性和恢复能力。

图片

事务具有四个核心特性,也被称为ACID特性:

原子性(Atomicity):事务中的操作要么全部成功,要么全部失败,不存在部分成功的情况。例如,在转账操作中,要么全部完成,要么全部取消,以确保资金的一致性。

如果事务失败,系统会回滚到事务开始前的状态,保证数据的一致性。回滚操作会将数据库恢复至事务开始前的状态,以消除任何可能的不一致性。

一致性(Consistency):事务执行前后,数据库必须保持一致性状态。

这意味着事务要将数据库从一个一致性状态转移到另一个一致性状态。一致性确保了数据始终处于有效且符合业务规则的状态。如果事务成功提交,数据库将处于有效状态;若事务失败,数据将回滚至事务开始前的状态。一致性是数据库事务的核心目标,因为它确保了数据在事务执行过程中的正确性。

例如,在转账操作中,转账前后的账户余额总和应该保持不变。

隔离性(Isolation):并发执行的多个事务之间应该相互隔离,互不干扰。

隔离性确保每个事务都是独立的,不受其他事务的影响。每个事务都应该感知不到其他事务的存在,以避免数据的不一致和并发访问的问题。隔离级别可以控制事务之间的隔离程度,如读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)等。

持久性(Durability):一旦事务提交成功,对数据库的修改应该永久保存,即使系统发生故障或重启。

持久性确保了事务的最终一致性,即使系统在执行过程中出现故障,事务提交后的数据也应保持不变。系统应该能够恢复到事务提交后的状态,保证数据的持久性。

这四个特性被称为ACID特性,是保证数据库事务正确执行的基本要素。通过这些特性,确保了数据库操作的一致性、完整性和可靠性。在此基础上,事务可以更好地满足并发控制、数据稳定和故障恢复等方面的需求。

图片

如何理解ACID?

在计算机科学中,ACID(原子性、一致性、隔离性、持久性)是数据库事务处理的基本特性,它在保证数据正确性和完整性方面起着至关重要的作用。我们可以通过一个常见的例子——银行转账来解释ACID的概念。

假设有一个银行数据库,其中包含两个表:信用卡表和储蓄表。

现在,用户A要将信用卡中的100元转移到储蓄账户中。这个转账过程需要遵循ACID特性。

1. 原子性

转账过程中的操作要么全部成功,要么全部失败。

以银行转账为例,如果用户A要将信用卡中的100元转移到储蓄账户中,那么转账过程中的所有操作(包括扣款和存款)要么全部成功,要么全部失败。如果其中一个操作失败,例如扣款成功但存款失败,那么系统会回滚到事务开始前的状态,之前的所有操作都将回滚,以保证数据的一致性。

2. 一致性

转账过程中,数据库必须保持一致性状态。这意味着转账前后的账户余额总和应该保持不变。

例如在转账之前,信用卡和储蓄账户中共有500+500=1000元钱。

如果转账成功,信用卡余额减少100元,储蓄账户余额增加100元,在转账之后,两个账户中共有400+600=1000元。保持了数据的一致性。

3. 隔离性

在转账过程中,多个用户可能同时进行转账操作。

隔离性确保每个用户的转账操作互不干扰,彼此独立进行。

例如,如果用户B同时也要将100元从信用卡转移到储蓄账户,他的转账操作应该与用户A的转账操作相互隔离,互不干扰。

4. 持久性

一旦转账操作成功提交(即事务提交),两个账户中的金额就会真正发生变化,并将数据写入数据库以进行持久化保存。

系统应该能够恢复到转账提交后的状态,保证数据的持久性。

通过遵循ACID特性,银行可以确保转账操作的正确执行,保证数据的一致性和完整性。总之,事务处理在需要保证数据一致性和完整性的场景中具有广泛的应用,如银行转账、在线支付等。使用事务可以简化应用程序的开发和维护,提高数据的可靠性和安全性。

图片

InnoDB的一次更新事务是怎么实现的?

InnoDB的一次更新事务涉及到多个组件和步骤,包括Buffer Pool、BinLog、UndoLog、RedoLog以及物理磁盘。

下面是一次完整的事务更新操作过程:

1. 加载数据到缓存中(Buffer Pool)

在进行数据更新时,InnoDB首先在缓冲池(Buffer Pool)中查找待更新记录是否已经在内存中。若记录不在内存中,InnoDB会将记录从磁盘文件读取到缓冲池(Buffer Pool)中。

缓冲池是InnoDB存储引擎提供的临时存储区,用于提升数据读取和修改的速度。将数据加载到缓冲池后,后续的更新操作均在缓冲池内进行。这样可以减少磁盘I/O操作,从而提高事务处理速度。缓冲池在内存中存储数据,可以降低磁盘I/O的开销,提高数据读取和写入的速度,从而优化事务处理性能。

2. 写入Undo Log

在更新数据之前,InnoDB会将原始数据的副本写入Undo Log(回滚日志)。

Undo Log是保证事务回滚和并发控制的关键部分,也是确保事务原子性和一致性的重要机制。Undo Log记录了事务开始前的数据状态,以便在需要回滚时进行数据恢复。通过记录撤销日志,InnoDB能够实现事务的滚动回滚,提高事务处理的灵活性。撤销日志在事务处理过程中起到了关键作用,它记录了事务的修改过程,使得事务能够在需要时回滚到之前的状态,保证数据的一致性和完整性。

3. 更新内存数据

接下来,InnoDB会在缓冲池中更新数据。

这意味着,当执行update语句时,InnoDB会先更新已经读取到Buffer Pool中的数据,修改操作会直接在内存中进行,而不是立即写入磁盘。

此时,缓冲池中的数据被标记为"脏页",表示与磁盘上的数据不一致。脏页是缓冲池中已经被修改但尚未写入磁盘的数据页,它需要后续的处理才能将修改同步到磁盘,保证数据的持久性。

4. 写入Redo Log

为了保证事务的持久性,InnoDB在Buffer Pool中记录修改操作的同时,InnoDB会先将更新操作写入Redo Log(重做日志)。

Redo Log是一种物理日志,它记录了事务对数据库的修改操作。通过Redo Log,即使系统发生故障,也可以通过重做日志来恢复事务修改后的状态。这一机制保证了事务的可靠性,降低了系统故障带来的风险。重做日志是保证数据持久性和恢复性的关键,它记录了事务的修改过程,使得事务的修改能够在故障恢复后得到恢复。

5. 提交事务

当事务完成所有的更新操作后,事务被提交。在提交事务时,InnoDB会将事务标记为"准备提交"状态。

此时,事务的修改操作仍然在缓冲池中,尚未写入磁盘。事务提交是事务处理的重要环节,它标志着事务处理完毕,可以进行后续的提交操作。在提交之前,事务的修改操作需要得到处理,保证数据的完整性和一致性。

6. 写入BinLog

在事务提交之后,InnoDB会将事务的修改操作写入BinLog(归档日志)。

BinLog是MySQL的二进制日志,用于记录数据库的所有修改操作。在归档日志中记录的信息包括:事务开始的时间、数据库名、表名、事务ID、SQL语句等。它可以用于数据恢复、主从复制、数据分析和同步等场景。归档日志在数据库中起到了关键作用,它记录了数据库的修改过程,使得数据库的修改能够在故障恢复后得到恢复。

7. 刷新脏页到磁盘

最后,在提交过程完成后,InnoDB会将缓冲池(Buffer Pool)中的脏页刷新到物理磁盘上的数据文件中。

这个过程称为"刷脏"。通过刷脏操作,将缓冲池中的修改操作同步到磁盘,确保数据的持久性。

然而,这个写入过程并非立即执行,而是由后台线程异步执行的,因此可能会有一定的延迟。总而言之,MySQL会在适当的时机选择将数据写入磁盘以进行持久化。

综上所述

InnoDB的数据更新事务涉及多个关键环节和步骤,包括数据预处理、记录撤销日志、更新内存数据、记录重做日志、事务提交、记录归档日志以及刷新脏页到磁盘。这些步骤确保事务具备原子性、一致性、隔离性和持久性,保障数据库操作正确执行和数据完整性。在实际应用中,了解这些细节有助于更好地优化数据库性能和事务处理。

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值