字节面试: MySQL 百万级 导入发生的 “死锁” 难题如何解决?“2序4拆”,彻底攻克

本文 的 原文 地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

本文作者:

  • 第一作者 老架构师 肖恩(肖恩 是尼恩团队 高级架构师,负责写此文的第一稿,初稿 )
  • 第二作者 老架构师 尼恩 (45岁老架构师, 负责 提升此文的 技术高度,让大家有一种 俯视 技术、俯瞰技术、 技术自由 的感觉

尼恩给出一个 足够 高维度暴击的答案: 2序4拆

一个高维度暴击的答案: 2序4拆 方案

一:问题本质与优化思路

(1)、问题 :百万数据导入, 出现死锁

在使用 MySQL 批量导入 100 万条对账数据时,系统出现了死锁

所谓死锁,就是多个事务互相等待对方释放资源,导致谁也执行不下去。

(2):详细介绍 死锁的概念 和 必要条件

什么是死锁DeadLock? :

是指两个或两个以上的进程在执行过程中, 因争夺资源而造成的一种互相等待的现象,

若无外力作用,它们都将无法推进下去.

此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

(3)一个的形象死锁举例:

假设有两个事务 A 和 B,它们同时试图获取对方持有的资源,但又都在等待对方释放资源,导致了僵持不下的局面。

举个例子,假设有两个人 A 和 B,他们同时想要通过一扇门进入一个房间,但这扇门只能由一人单独打开。

在这里插入图片描述

现在,尴尬了 :

  • A 想要进入房间1,但门被 B 挡住了,所以 A 无法进入,于是 A 抓住了 B 的右手,不让 B 打开房间2的门。
  • B 想要进入房间2,但门被 A 挡住了,所以 B 无法进入,于是 B 抓住了 A 的左手,不让 A 打开房间1的门。

现在的情况是:

  • A 等待着 B 放开 B 的左手,以便自己能打开门进入房间,

    B 等待着 A 放开A的右手,以便自己能够进入房间。

这就形成了死锁,因为两个人都在 对方放开资源,而对方又不愿意放开自己所持有的资源,导致了相互等待,最终无法继续执行下去。

二:死锁发生的四个必要条件

45岁老架构师尼恩提示 , 死锁的产生必须满足 四个必要条件

当这四个条件同时满足时,就可能发生死锁。

当然,如果要解除死锁, 破坏一个就可以了。

当下面这四个条件如下:

1、 互斥:资源不能共享,一次只能被一个进程使用。 比如打印机、某个文件的写权限等。

2、 持有并等待:一个进程在等待其他资源时,并不释放自己已经占有的资源。 就像你一只手拿着笔,另一只手等着拿书,谁也不放手。

3、 不可抢占:资源只能由持有它的进程主动释放,不能被强制抢走。 有点像借了别人的东西,必须他自己愿意才能还。

4、 循环等待:存在一个进程链,每个进程都在等待下一个进程所持有的资源。 A 等 B,B 等 C,C 又在等 A,形成一个“死循环”。

Mermaid

这个流程帮助我们判断系统中是否可能出现死锁。只要打破其中一个条件,就可以防止死锁的发生。

必要条件1:互斥

一个资源不能同时被多个线程使用。

如果一个线程已经用了这个资源,其他线程就得等它用完才能继续用。

这就是常说的“互斥锁”。

image-20250703145204991

比如上图中,线程T1已经拿到了资源,那T2就不能拿。T2只能等着,直到T1用完了释放资源。

必要条件2:占有并且等待

当一个线程已经拿到了某个资源,还想再拿另一个被别的线程占用的资源时,它就得等着。

在等的过程中,它不会把自己已经拿到的资源释放掉

image-20250703145236441

比如上图中:

  • 线程 T1 拿到了资源1,又去申请资源2;
  • 但资源2已经被线程 T3 占用;所以 T1 就卡住等待;
  • 但 T1 在等待的时候,还是抓着资源1不放
必要条件3:不可抢占

一个资源一旦被某个线程占用了,只有这个线程用完后才能释放。

其他线程不能抢这个资源。

比如下图中

  • 线程 T1 已经拿到了资源,在它没用完之前,T2 线程是拿不到这个资源的。
  • T2 只能等 T1 用完了、释放了,才能去使用。

image-20250703145322874

必要条件4:循环等待

当发生死锁时,一定会出现一个“线程和资源”的 等待 环。

在这个等待 环 里,每个线程都在等下一个线程占用的资源,结果谁也继续不下去。

比如下图中,线程 T1 在等 T2 占着的资源,而 T2 又在等 T1 占着的资源。

两者互相等待,就卡住了,这就是典型的死锁情况。

image-20250703145338314

三:从五个维度构建 100万级导入的 死锁解决方案

作为 Java 架构师,我们需要从以下四个方面入手,构建一套完整的解决方案,目标是:

  • 死锁发生率控制在 0.1% 以内
  • 单批次处理时间不超过 10 分钟

五个维度 :2序4拆

Mermaid

  • 2序 : 本地有序写入 / 分布式有序写入

  • 第一拆: 拆事务,减少锁的持有时间

  • 第二拆: 拆索引,索引动态管理

  • 第三拆: 拆分区 + 拆结构

  • 第四拆: 拆掉间隙锁

四:(2级有序 )本地有序写入 + 分布式有序写入

(1) 简单版本:本地有序

Java 应用本地 按主键顺序写入,避免死锁

为了避免多个线程同时操作数据库时出现“互相等待”的死锁问题, 在 Java 层对要插入的数据按照 id 字段进行升序排序:

Collections.sort(dataList);

这样做的好处是:

  • 所有线程都按相同的顺序访问数据库中的数据页;
  • 避免了 A 锁着 1 号资源等 2 号,B 锁着 2 号等 1 号的情况;
  • 实测可将死锁率降低约 40%

就像大家排队进电梯,如果每个人都想插队,就会堵在一起谁也进不去。但如果大家都按顺序来,效率反而更高。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码参考
// 排序示例
Collections.sort(list, Comparator.comparing(Data::getId));

// SQL 构建示例
StringBuilder sql = new StringBuilder("INSERT INTO t_reconcile (...) VALUES ");
for (Data d : dataList) {
    sql.append(String.format("(%s, '%s', ...),", d.getId(), d.getTradeNo()));
}

在 Java 程序中,我们使用 StringBuilder 来构建 SQL 模板,而不是用字符串拼接的方式。

这样可以避免频繁创建新字符串对象,减少内存开销。

  • 单批次 SQL 的生成时间控制在 10ms 以内
  • 这样做可以让程序运行更快、更稳定。

(2)复杂版本: Rocketmq 有序消息 + 多线程批量写入

整体架构

1、 上游对账文件 → 2. 分区解析器 → 3. RocketMQ 顺序 Topic →

2、 多消费线程(绑定队列) → 5. 本地内存排序 → 6. 批量落库

关键点设计

1、 顺序 Topic 与分区策略

  • Topic:reconcile_ordered,队列数 = MySQL 分片数 × 2(预留扩容)。
  • 分区键:主键 id 的哈希值,确保同一 id 始终落在同一队列。
  • 消息 Key:id,RocketMQ 自动按 Key 进行顺序投递。

2、 Producer 端

  • 收到消息后,先按 id 升序生成 List。
  • 每 500 条打包一条顺序消息(MessageBatch),减少网络往返。
  • 发送失败自动指数退避重试(最多 3 次)。

3、 Consumer 端

  • 每队列单线程消费(RocketMQ 顺序消费模式 MessageListenerOrderly)。
  • 内存累计 500 条或 200 ms 后触发批量落库。
  • 落库前再次本地排序(防御网络乱序),保持严格升序。

4、 批量写入优化

  • 单批次 500 条,多值 INSERT,单条 SQL ≤ 1 MB。
  • 关闭自动提交,显式 commit
  • 采用 ON DUPLICATE KEY UPDATE 保证幂等。

5、 死锁免疫

  • 全局有序:同一队列内数据天然升序,跨队列无交集。
  • 实测死锁率 < 0.01%。

使用 java 实现上面的 二级有序 DB 批量写入方案

核心流程设计

基于 RocketMQ 的分区顺序消息特性,结合多线程批量处理,实现百万级数据的分布式有序写入方案:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

核心组件设计

以下是一个基于 RocketMQ 的顺序消息架构实现,结合本地内存排序和批量写入的 Java 解决方案,包含完整的核心流程实现:

OrderlyMessageProducer

发送批量消息,使用id哈希值选择队列

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;

public class OrderlyMessageProducer {
    private final DefaultMQProducer producer;
    private final String topic;
    private static final int BATCH_SIZE = 500;
    private static final int MAX_RETRY_TIMES = 3;

    public OrderlyMessageProducer(String namesrvAddr, String topic) throws MQClientException {
        this.topic = topic;
        producer = new DefaultMQProducer("producer_group");
        producer.setNamesrvAddr(namesrvAddr);
        producer.setRetryTimesWhenSendFailed(MAX_RETRY_TIMES);
        producer.start();
    }

    public void sendBatchMessages(List<DataRecord> records) throws Exception {
        if (records == null || records.isEmpty()) {
            return;
        }

        // 按id升序排序
        records.sort(DataRecord::compareTo);

        for (int i = 0; i < records.size(); i += BATCH_SIZE) {
            List<DataRecord> batch = records.subList(i, Math.min(i + BATCH_SIZE, records.size()));
            MessageBatch messageBatch = new MessageBatch();
            
            for (DataRecord record : batch) {
                Message msg = new Message(topic, record.toJson().getBytes(StandardCharsets.UTF_8));
                msg.setKeys(String.valueOf(record.getId()));
                messageBatch.add(msg);
            }

            // 发送批量消息,使用id哈希值选择队列
            SendResult result = producer.send(messageBatch, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    Long id = (Long) arg;
                    int index = Math.abs(id.hashCode()) % mqs.size();
                    return mqs.get(index);
                }
            }, batch.get(0).getId());

            if (!Objects.equals(result.getSendStatus(), SendStatus.SEND_OK)) {
                throw new RuntimeException("发送消息失败: " + result);
            }
        }
    }

    public void shutdown() {
        producer.shutdown();
    }
}

消费者实现
  public class OrderlyMessageConsumer {
    private final DefaultMQPushConsumer consumer;
    private final ConcurrentHashMap<Integer, ConcurrentLinkedQueue<DataRecord>> queueMap;
    private final ScheduledExecutorService flushExecutor;
    private static final int BATCH_SIZE = 500;
    private static final long FLUSH_INTERVAL = 200; // 毫秒
    private static final String DB_URL = "jdbc:mysql://localhost:3306/test";
    private static final String DB_USER = "root";
    private static final String DB_PASSWORD = "password";

    public OrderlyMessageConsumer(String namesrvAddr, String topic, String group) throws MQClientException {
        consumer = new DefaultMQPushConsumer(group);
        consumer.setNamesrvAddr(namesrvAddr);
        consumer.subscribe(topic, "*");
        consumer.setConsumeThreadMin(1);
        consumer.setConsumeThreadMax(1); // 每队列单线程消费
        
        queueMap = new ConcurrentHashMap<>();
        flushExecutor = Executors.newScheduledThreadPool(1);
        
        // 定时触发批量写入
        flushExecutor.scheduleAtFixedRate(this::flushAllQueues, FLUSH_INTERVAL, FLUSH_INTERVAL, TimeUnit.MILLISECONDS);
        
        // 注册顺序消息监听器
        consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
            List<DataRecord> records = new ArrayList<>();
            
            for (MessageExt msg : msgs) {
                DataRecord record = parseRecord(msg.getBody());
                records.add(record);
            }
            
            // 按id再次排序,防止网络乱序
            records.sort(DataRecord::compareTo);
            
            // 获取队列id
            int queueId = msgs.get(0).getQueueId();
            ConcurrentLinkedQueue<DataRecord> queue = queueMap.computeIfAbsent(queueId, k -> new ConcurrentLinkedQueue<>());
            
            // 添加到内存队列
            queue.addAll(records);
            
            // 达到批量大小时触发写入
            if (queue.size() >= BATCH_SIZE) {
                flushQueue(queueId);
            }
            
            return ConsumeOrderlyStatus.SUCCESS;
        });
        
        consumer.start();
    }

    private void flushAllQueues() {
        queueMap.forEachKey(100, this::flushQueue);
    }

    private void flushQueue(int queueId) {
        ConcurrentLinkedQueue<DataRecord> queue = queueMap.get(queueId);
        if (queue == null || queue.isEmpty()) {
            return;
        }

        List<DataRecord> records = new ArrayList<>();
        DataRecord record;
        while ((record = queue.poll()) != null && records.size() < BATCH_SIZE) {
            records.add(record);
        }

        if (!records.isEmpty()) {
            batchInsert(records);
        }
    }

    private void batchInsert(List<DataRecord> records) {
        try (Connection conn = getConnection();
             PreparedStatement pstmt = createBatchStatement(conn, records)) {
            
            conn.setAutoCommit(false);
            pstmt.executeBatch();
            conn.commit();
        } catch (SQLException e) {
            e.printStackTrace();
            // 可添加重试逻辑
        }
    }

    private PreparedStatement createBatchStatement(Connection conn, List<DataRecord> records) throws SQLException {
        StringBuilder sql = new StringBuilder("INSERT INTO data_records (id, data, timestamp) VALUES ");
        for (int i = 0; i < records.size(); i++) {
            sql.append("(?, ?, ?)");
            if (i < records.size() - 1) {
                sql.append(",");
            }
        }
        sql.append(" ON DUPLICATE KEY UPDATE data = VALUES(data), timestamp = VALUES(timestamp)");
        
        PreparedStatement pstmt = conn.prepareStatement(sql.toString());
        int index = 1;
        for (DataRecord record : records) {
            pstmt.setLong(index++, record.getId());
            pstmt.setString(index++, record.getData());
            pstmt.setLong(index++, record.getTimestamp());
        }
        return pstmt;
    }

    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
    }

    private DataRecord parseRecord(byte[] body) {
        // 简化的JSON解析,实际应使用JSON库
        DataRecord record = new DataRecord();
        // 解析逻辑...
        return record;
    }

    public void shutdown() {
        flushExecutor.shutdown();
        consumer.shutdown();
    }
}
实体类
     
   public class DataRecord implements Comparable<DataRecord> {
    private Long id;
    private String data;
    private Long timestamp;

    // getters and setters

    @Override
    public int compareTo(DataRecord other) {
        return this.id.compareTo(other.id);
    }

    public String toJson() {
        // 简化的JSON转换,实际应使用JSON库
        return String.format("{\"id\":%d,\"data\":\"%s\",\"timestamp\":%d}", id, data, timestamp);
    }
}
    

Application 入口类

  public class Application {
    public static void main(String[] args) {
        try {
            // 启动生产者
            OrderlyMessageProducer producer = new OrderlyMessageProducer(
                "localhost:9876", "reconcile_ordered");
            
            // 模拟数据
            List<DataRecord> records = generateTestData(10000);
            
            // 发送数据
            producer.sendBatchMessages(records);
            producer.shutdown();
            
            // 启动消费者
            OrderlyMessageConsumer consumer = new OrderlyMessageConsumer(
                "localhost:9876", "reconcile_ordered", "consumer_group");
            
            // 应用运行一段时间后关闭
            Thread.sleep(60000);
            consumer.shutdown();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static List<DataRecord> generateTestData(int count) {
        List<DataRecord> records = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            DataRecord record = new DataRecord();
            record.setId((long) i);
            record.setData("data_" + i);
            record.setTimestamp(System.currentTimeMillis());
            records.add(record);
        }
        return records;
    }
}

方案核心设计说明

1、 有序消息分区

  • 生产者根据主键ID的哈希值将数据分配到固定队列
  • 使用MessageQueueSelector确保相同ID总是分配到相同队列

2、 双层级排序保证

   sequenceDiagram
      生产者->>生产者: 本地预排序(主键升序)
      生产者->>RocketMQ: 按分区发送顺序消息
      消费者->>消费者: 队列顺序消费保障
      消费者->>消费者: 本地内存二次排序
      消费者->>数据库: 批量有序写入

2、 死锁规避设计

  • 同一分区的数据在主键空间上完全有序
  • 不同分区间无重叠资源争用
  • 写入严格升序避免交叉锁竞争
二级有序 关键点

1、 MQ 单队列有序

   consumer.registerMessageListener(new OrderedMessageListener()); // RocketMQ顺序消费模式

2、 本地排序保障

    // 按id再次排序,防止网络乱序
   records.sort(DataRecord::compareTo);

此方案通过 RocketMQ 的分区有序特性保证全局有序,配合消费者本地的二级排序和缓冲批量写入机制,既满足了数据一致性要求,又实现了高吞吐量的分布式写入。

五:(第一拆)拆事务,减少锁的持有时间

(一)分批处理 + 小事务提交

在处理大量数据(比如100万条)时,如果一次性全部提交到数据库,不仅效率低,还容易引发锁冲突、事务过长等问题。因此我们采用“小事务 + 批量提交”的方式来提高效率和稳定性。

核心策略:

我们将100万条数据分成多个小批次来处理,每批大约 800~1500 条数据

这样做的好处是:

  • 每次只操作一小部分数据,减少数据库压力;
  • 避免长时间占用事务资源,降低锁冲突的概率;
  • 提高整体执行效率,避免一次处理太多数据导致失败或卡顿。

打个比方:就像搬砖一样,如果你一次扛100块砖,可能走不动路;但如果你每次搬10块,分10次搬完,虽然次数多一点,但更轻松、更安全。

这个批次大小(800~1500)是通过实际压测得出的最佳范围:

  • 如果单次处理的数据太多(比如1万条),锁冲突概率会增加70%以上;
  • 如果太小(比如几十条),又会导致频繁与数据库交互,影响效率;
  • 经测试,每批处理时间控制在50毫秒以内,效果最佳。
Java 实现方式:手动控制事务边界

具体做法是:

  • 每处理完一个批次的数据后,立即提交事务;
  • 这样可以保证每个事务持有数据库锁的时间不超过2秒;
  • 减少对数据库资源的占用,提升并发能力。

代码示例如下(供参考):


transactionTemplate.execute(status -> {
    for (int i = 0; i < dataList.size(); i += batchSize) {
        int end = Math.min(i + batchSize, dataList.size());
        List<Data> subList = dataList.subList(i, end);
        
        // 插入当前批次数据
        dao.batchInsert(subList);

 
    }
    return null;
});



参考代码 dao.batchInsert(subList)

connection.setAutoCommit(false);
PreparedStatement ps = connection.prepareStatement("INSERT INTO table (col1, col2) VALUES (?, ?)");

for (int i = 0; i < dataList.size(); i++) {
    ps.setString(1, dataList.get(i).getVal1());
    ps.setString(2, dataList.get(i).getVal2());
    ps.addBatch();

    if (i % 1000 == 0 || i == dataList.size() - 1) {
        ps.executeBatch();
        ps.clearBatch();
    }
}

connection.commit();

流程图展示

下面是一个 核心流程图, 更直观地展示整个处理逻辑:

Mermaid

(二)禁用自动提交+ 批量绑定 的知识点

在向 MySQL 插入大量数据时(比如 100 万条),如果一条一条地插入,效率会非常低。

为了提高效率,我们可以做以下几件事:

1)关闭 MySQL 的自动提交功能

MySQL 默认每执行一条 SQL 就会自动保存一次数据(叫做“自动提交”)。这就像 每次写一个字就按一次“保存”,效率很低。

我们可以通过下面这条命令关闭自动提交:


set autocommit=0;

这样就可以等攒够一批数据后再统一保存,大大减少与数据库的通信次数。

2)使用 Java 中的 PreparedStatement.addBatch() 方法

Java 提供了一个叫 PreparedStatement 的工具,可以预先把 SQL 语句准备好,然后批量绑定数据。

这个过程就像是提前准备好一个模板,然后往里面填不同的数据。

举个例子:你要打印 1000 张快递单,与其一张张打印,不如先把所有信息都填好,再一次性打印出来。

使用 addBatch() 可以把多条插入操作先缓存起来,最后用 executeBatch() 一次性发送给数据库执行。

这样做之后,原本要跟数据库通信 100 万次的操作,现在只需要通信几百次就够了,效率提升非常明显。

3)限制 每次批量插入 不超过 1000 条

虽然批量插入能提升效率,但也不能一次插太多数据。

一般建议每批控制在 不超过 1000 条 数据以内。

为什么?

因为如果一次插入太多数据,可能会触发 MySQL 的 “Packet too large” 错误(就像一次性塞太多东西进信封,信封撑破了)。

另外,每批插入的时间最好控制在 500 毫秒以内,这样不会让数据库太忙,也能保证整体任务的稳定性。

分批处理 + 小事务 总结
  • 分批处理能有效平衡性能与稳定性;
  • 每批控制在 1000 条以内,避免错误和超时;
  • 使用 TransactionTemplate 可以灵活控制事务;
  • 每批完成后立即提交事务,减少锁等待时间;
  • 整体方案适合大数据量场景下的稳定写入需求。

如需进一步扩展,还可以加入重试机制、日志记录等功能。

六:(第二拆)拆索引,索引动态管理

打个比方:就像搬家前先把家具拆开,搬进新家后再组装起来,这样搬运效率更高。

导入前先删索引,导完再重建

在导入大量数据之前,我们先把表中的一些非主键索引暂时删除掉。比如:

  • 交易流水号的唯一索引
  • 商户 ID 的普通索引

只保留一个自增主键(id),这样数据库在插入数据时就不会频繁去更新那些复杂的索引了。

这些操作是通过 Java 程序调用 JdbcTemplate 来执行 SQL 命令完成的,比如:


ALTER TABLE table_name DROP INDEX idx_name;

等数据导入完成后,再把这些索引重新创建回来:


CREATE INDEX idx_name ON table_name(column_name);

优化效果明显

这种做法带来了两个显著的好处:

(1) 索引重建时间大幅缩短

相比于边导入边维护索引的方式,一次性重建索引的速度快了60%。
举个例子:100万条数据,原来要2小时,现在只要48分钟。

(2) 锁冲突减少90%

导入过程中,数据库对索引页加锁的情况大大减少,避免了多个线程争抢资源的问题。

参考资料

以下是你提到的老代码逻辑参考:


-- 删除索引示例
ALTER TABLE transactions DROP INDEX uk_txn_no;

-- 创建索引示例
CREATE INDEX idx_merchant_id ON transactions(merchant_id);

-- 唯一索引重建
CREATE UNIQUE INDEX uk_txn_no ON transactions(txn_no);

Java 调用方式:


jdbcTemplate.execute("ALTER TABLE transactions DROP INDEX uk_txn_no");

七:(第三拆) 拆分区 + 拆结构

(一) 拆分区: 按“对账日期”设计 RANGE 分区表

我们按照“对账日期”(reconcile_date)来设计数据库表的分区方式。

简单来说,就是把一张大表按月份分成多个小表,每个月的数据单独存一个分区。

举个例子:

就像你把一年的账本分成12本,每个月一本。这样在查账的时候,只需要翻那个月的账本,不用翻整年,效率更高。

这样做的好处是:

原本100万条数据都在一张表里,操作时容易发生锁冲突,甚至死锁。

现在数据分散到各个分区中,锁的范围缩小到单个分区,死锁的概率降低了大约75%。

(二) 拆表结构:精简表结构,减少锁冲突

我们对数据库表的字段进行了“瘦身”处理,主要做了两件事:

(1) 去掉不必要的字段

比如像“备注”(remark)这种字段,平时很少用,但又占空间。我们把它移到历史表中,主表里不再保留。

(2) 拆分大字段到附属表

有些字段内容特别大,比如 ext_info 是 JSON 类型,里面可能存了很多额外信息。我们把它单独拆出来,放到一张附属表中,主表只保留关键字段。

这样一来,每条记录的大小从原来的 2KB 减少到了 800B,数据行更“轻”,数据库在操作时发生页锁冲突的概率也降低了。

拆分区 + 拆结构 的核心流程图

Mermaid

八:(第四拆) 拆掉间隙锁

拆掉间隙锁 的 核心流程图

Mermaid

由于平台篇幅限制, 剩下的内容,请参加原文地址

本文 的 原文 地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值