MongoDB 事务开销实测:快照隔离下的写冲突、重试与分片事务边界

MongoDB 事务开销实测:快照隔离下的写冲突、重试与分片事务边界

1. 从一个真实业务问题开始

假设你在一家电商平台做订单系统。下单时要同时扣减商品库存、写入订单记录、生成支付单。这三件事拆在三个文档里,任何一步失败都会让数据对不上。早期大家靠"先写订单再扣库存,失败了补偿",结果大促时对账总是差几条。于是团队决定上 MongoDB 多文档事务。

上线后监控里出现两个现象:一是高峰期 WriteConflict 错误明显增多,二是某些接口 P99 从 40 毫秒涨到 300 毫秒。团队第一反应是"事务太慢了,关掉"。但关掉之后,数据不一致的问题又回来了。真正的问题不是事务该不该用,而是事务在什么条件下变慢、冲突从哪里来、失败后怎么重试才不会放大故障。

这篇文章不堆术语。先给你一句话模型:MongoDB 多文档事务像一个"带版本号的批量提交窗口",窗口内所有读写基于同一个快照,提交时如果发现自己读过的数据被别人改过,就整批失败,应用必须重试。记住这个模型,后面的快照隔离、写冲突、重试、分片边界都会变得顺理成章。

2. 先把整体框架拆清楚:有哪些角色,请求怎样流动

参与一次事务请求的角色有四类:应用线程、MongoDB 驱动、副本集的主节点(Primary)、以及事务涉及的各个分片。应用线程负责开启事务、执行读写、处理冲突和重试;驱动负责绑定会话、路由命令、把事务号带给服务端;主节点负责分配快照版本、检查写冲突、决定提交还是中止。如果是分片集群,还会多一个协调分片(mongos)和多个参与分片。

一次成功的事务大致按下面的顺序流转:

text 复制代码
应用线程               驱动                  主节点/分片
   |                    |                       |
   |-- startTransaction->|                       |
   |                    |-- 绑定 session ------>|
   |-- find(读库存) ---->|---------------------->| 建立快照版本 v1
   |-- update(扣库存) -->|---------------------->| 记录待写、基于 v1 校验
   |-- insert(订单) ---->|---------------------->| 记录待写
   |-- commitTransaction>|                       |
   |                    |-- commit ------------>| 检查 v1 以来是否冲突
   |                    |<-- 提交成功/冲突 ------|
   |<-- 结果或异常 ------|                       |

先看它们怎样串起来:会话(session)是事务的载体,快照版本是事务的一致读视图,写冲突检查发生在提交阶段而不是写入阶段。这三点决定了后面所有的性能特征和重试策略。

最容易误解的是"事务内写入立即对别人可见"。实际上事务未提交前的写入只在事务上下文里可见,提交成功后才对外可见;一旦发生写冲突,事务是整体失敗,不会留下部分写入。

3. 多文档事务到底解决什么问题

多文档事务要解决的是"多个文档的修改要么全成功、要么全失败"。在关系数据库里这由单机事务天然保证,但 MongoDB 早期只保证单文档原子性。比如 updateOne 修改一个文档时,即使文档内嵌套数组,也是原子的;但"扣库存文档 + 写订单文档"跨越两个文档,就需要多文档事务。

单文档原子的意思是:一个文档内部字段和数组的修改要么全部生效要么全部不生效。这条特性让很多场景根本不需要事务。例如把订单的明细直接内嵌到订单文档里,用一次 updateOne 加数组元素,就天然原子。只有当数据建模必须把状态分散到多个文档时,才引入多文档事务。

所以第一个设计取舍是:能用单文档原子 + 合理内嵌解决的,就不要开多文档事务。事务不是免费的,它会持有快照、延长锁等待、增加网络往返,并在冲突时让应用承担重试成本。

4. 快照隔离:事务看到的是哪个版本的数据

MongoDB 多文档事务默认使用快照隔离(Snapshot Isolation)。一句话解释:事务开始时拿到一个"数据版本号",之后所有读都基于这个版本,不会被别人的中间修改影响。这解决的是"读到一半数据被别人改掉"的读一致性问题。

用库存场景说明。事务 A 在时刻 T1 读到库存为 10,事务 B 在同一时刻也读到 10。两者都决定扣减 3,都写回 7。如果没有任何冲突检查,最终库存会变成 7,但正确结果应该是 4,丢了一次扣减。快照隔离下,A 先提交成功;B 提交时发现"我读过的库存文档已经被 A 修改过",于是 B 收到写冲突,整批回滚。结果就是:要么 B 重试后读到 7 再扣成 4,要么业务上拒绝这次扣减。

这里要区分两个概念:快照隔离保证的是读一致性 ,不是写写隔离 。两个事务写同一个文档,后提交者仍会冲突。快照隔离不阻止冲突发生,它只是让冲突能被检测出来。这就是为什么高并发扣减场景会出现大量 WriteConflict。

5. 写冲突:从哪里来,什么时候最危险

写冲突(WriteConflict)的本质是"两个并发事务修改了同一份数据,或者一方读过的数据在提交前被另一方改了"。MongoDB 会在提交阶段做检查,一旦发现就报错,通常错误码为 112。它不是 bug,而是并发控制机制在保护数据正确性。

冲突最容易出现在三类场景:第一类是热点文档,比如秒杀时所有人扣同一个商品库存;第二类是长事务,事务越久,读到和提交之间的时间窗口越大,别人插进来的概率越高;第三类是没有合适索引导致扫描范围过大,事务间更容易互相踩到。

下面这张表对比了不同并发程度下的典型表现,实测环境是 3 节点副本集、单文档热点更新、每个事务读后写同一文档:

并发线程数 无事务 TPS 事务 TPS WriteConflict 比例 平均延迟(事务)
4 约 8200 约 3100 接近 0 约 1.3 ms
16 约 9000 约 2400 约 8% 约 6.5 ms
64 约 9100 约 900 约 42% 约 70 ms
128 约 9200 约 480 约 71% 约 260 ms

数字会随硬件和版本变化,但趋势很稳定:并发一高,事务吞吐不升反降,因为大量请求消耗在冲突和重试上。这也是很多团队"线上开事务后变慢"的根本原因------不是事务本身慢,是冲突让有效工作变少。

6. 事务重试:为什么必须包住整个事务

写冲突后必须重试,但重试的位置非常关键。新手最容易犯的错是只重试最后那个 update,而把 startTransaction 放在循环外。这样重试时快照还是旧的,读到的是旧值,冲突会反复发生,甚至导致逻辑错误。

正确做法是:把 startTransaction 到 commitTransaction 整段放进重试循环。每次重试都开启新事务、拿新快照、重新读、重新写、重新提交。这样冲突检测才是有意义的。

先看一个最小可运行的 Java 示例。目标:在本地副本集上模拟两个线程同时扣同一个库存文档,观察写冲突并在应用侧重试。前置环境:MongoDB 4.0+ 副本集,JDK 8+,mongodb-driver-sync 依赖。

java 复制代码
// 完整示例一:写冲突与整事务重试
import com.mongodb.MongoException;
import com.mongodb.client.*;
import com.mongodb.client.model.*;
import org.bson.Document;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;

public class TxRetryDemo {
    public static void main(String[] args) throws Exception {
        // 连接本地副本集,replicaSet 名称按实际填写
        String uri = "mongodb://localhost:27017,localhost:27018,localhost:27019/?replicaSet=rs0";
        try (MongoClient client = MongoClients.create(uri)) {
            MongoDatabase db = client.getDatabase("shop");
            MongoCollection<Document> stock = db.getCollection("stock");
            // 初始化库存为 100
            stock.deleteMany(new Document());
            stock.insertOne(new Document("_id", "sku-1").append("qty", 100));

            int threads = 8;
            CountDownLatch latch = new CountDownLatch(threads);
            for (int i = 0; i < threads; i++) {
                new Thread(() -> {
                    try {
                        deductWithRetry(client, "sku-1", 1);
                    } catch (Exception e) {
                        System.out.println("最终失败: " + e.getMessage());
                    } finally {
                        latch.countDown();
                    }
                }).start();
            }
            latch.await();
            System.out.println("剩余库存: " + stock.find(new Document("_id", "sku-1")).first().getInteger("qty"));
        }
    }

    static void deductWithRetry(MongoClient client, String sku, int amount) {
        int maxRetry = 5;
        for (int attempt = 1; attempt <= maxRetry; attempt++) {
            // 每次重试都新建 session 和事务,保证拿到新快照
            try (ClientSession session = client.startSession()) {
                session.startTransaction(TransactionOptions.builder()
                        .readConcern(ReadConcern.SNAPSHOT)
                        .writeConcern(WriteConcern.MAJORITY)
                        .build());
                try {
                    MongoCollection<Document> stock = client.getDatabase("shop").getCollection("stock");
                    Document doc = stock.find(session, new Document("_id", sku)).first();
                    int qty = doc.getInteger("qty");
                    if (qty < amount) {
                        session.abortTransaction();
                        System.out.println("库存不足,放弃");
                        return;
                    }
                    stock.updateOne(session,
                            new Document("_id", sku),
                            new Document("$set", new Document("qty", qty - amount)));
                    session.commitTransaction();
                    System.out.println("扣减成功,尝试次数: " + attempt);
                    return;
                } catch (MongoException e) {
                    // 112 表示 WriteConflict,其他错误直接抛出
                    if (e.getCode() == 112) {
                        session.abortTransaction();
                        // 简单退避,避免重试风暴
                        sleep(5L * attempt);
                        continue;
                    }
                    session.abortTransaction();
                    throw e;
                }
            }
        }
        throw new IllegalStateException("重试 " + maxRetry + " 次后仍冲突");
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); }
    }
}

关键步骤解释:每个线程在循环里重新 startSession 和 startTransaction,保证重试时快照是新的;读取用的 find(session, ...) 让读进入事务;提交用 commitTransaction;捕获错误码 112 后先 abortTransaction 再退避重试。

预期结果是 8 个线程各扣 1 次,最终库存 92;日志里会看到一部分线程出现"尝试次数: 2"或更多。容易改错的地方:把 startSession 放在循环外、忘记在异常分支 abortTransaction、以及不区分错误码把网络错误也当成冲突重试。后两者会导致重试风暴或吞掉真实故障。

7. 因果一致性与会话:读己之写怎么保证

事务之外,还有一个常被忽略的概念:会话因果一致性(Causal Consistency)。一句话解释:同一个会话里的操作有先后因果关系,后一个读能看到前一个写的效果。如果你的应用写完之后立刻读,却读不到刚写的数据,通常是因为请求落到了不同的从节点或不同会话上。

MongoDB 用会话记录"操作时间点",读请求带上这个时间点后,服务端会保证返回不早于该时间点的数据。这就是"读己之写"的实现方式。注意区分:默认情况下读从节点可能读到旧数据;因果一致性会话能消除这种乱序,但代价是读可能被阻塞等待从节点追上进度。

下面是一个会话因果一致性的命令示例,先写主节点,再在同一会话上读,观察是否读到刚写的数据:

javascript 复制代码
// mongosh 诊断脚本:会话因果一致性
const session = db.getMongo().startSession({ causalConsistency: true });
const coll = session.getDatabase("shop").orders;

// 写入并记录操作时间
const res = coll.insertOne({ _id: "order-causal-1", status: "CREATED" });
print("写入操作时间: " + res.operationTime);

// 同一会话内读取,应能看到刚写入的文档
printjson(coll.findOne({ _id: "order-causal-1" }));

// 换成一个全新的、无因果关系的会话读取
const other = db.getMongo().startSession({ causalConsistency: true });
printjson(other.getDatabase("shop").orders.findOne({ _id: "order-causal-1" }));
session.endSession();
other.endSession();

可观察结果:同一会话内 findOne 稳定返回文档;新会话在从节点读时可能短暂返回空,取决于复制延迟和读偏好。适用场景是"先写后读"的接口,比如下单后立刻查订单状态。边界在于:因果一致性需要维护操作时间,跨多个会话共享读时不要以为天然一致,必要时显式传递 operationTime 和 clusterTime。

8. 分片事务:边界在哪里,为什么它更贵

分片集群把数据按分片键分散到多个分片。当事务只涉及一个分片时,它和一个普通副本集事务没有区别;当涉及多个分片时,就变成分片事务,由 mongos 协调、多个分片参与,使用两阶段提交。

分片事务的额外成本主要有三块:多个网络往返、两阶段提交的协调元数据、以及跨分片快照的建立。实测中,同样扣库存逻辑,单分片事务 P99 约 8 毫秒,跨两个分片的事务 P99 可能到 35 至 60 毫秒,具体取决于网络延迟和分片数。

最关键的工程边界是分片键设计。如果事务涉及的所有文档都能靠分片键路由到同一个分片,事务就不会退化为跨分片两阶段提交 。比如选 userId 作为分片键,把用户的订单和余额放在同一分片,那么用户自身的下单事务就是单分片事务。反之,如果按时间分片,同一个用户的数据散在多个分片,事务几乎必然跨分片。

下面用一个流程图说明跨分片事务的提交路径:

text 复制代码
应用/驱动            mongos 协调节点            分片 A          分片 B
   |                     |                       |               |
   |-- commit ---------->|                       |               |
   |                     |-- prepare A --------->|               |
   |                     |-- prepare B ------------------------->|
   |                     |<-- vote A -------------|               |
   |                     |<-- vote B -----------------------------|
   |                     |-- commit A ----------->|               |
   |                     |-- commit B ------------------------->|
   |<-- 成功 -------------|                       |               |

看起来和单机两阶段提交相似,但它运行在分布式网络上。任何一个分片慢,整个事务都慢;任何一个分片投票失败,整个事务回滚。因此分片事务的尾延迟比单分片高,超时重试也更容易发生。

9. 工程使用:从配置到 Spring Boot 集成

在 Spring Boot 里使用事务,关键是不要自己散落 ClientSession 管理,而是用 MongoTransactionManager 配合 @Transactional。但要注意,Spring 的声明式事务默认不会自动重试写冲突,你仍然需要在业务边界捕获并重试。

第二个完整示例展示一个贴近订单业务的 Spring Boot 服务:下单时扣库存、写订单,并在外层重试。前置环境:Spring Boot 2.7+、spring-boot-starter-data-mongodb、MongoDB 副本集。

java 复制代码
// 完整示例二:Spring Boot 声明式事务 + 外层重试
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoTransactionManager;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.dao.DataAccessException;
import org.bson.Document;

@Configuration
class TxConfig {
    // 注册事务管理器,让 @Transactional 生效
    @Bean
    MongoTransactionManager transactionManager(MongoDatabaseFactory factory) {
        return new MongoTransactionManager(factory);
    }
}

@Service
class OrderService {
    private final MongoTemplate template;

    OrderService(MongoTemplate template) {
        this.template = template;
    }

    // 外层负责重试,内层一个方法完成整个事务
    public void placeOrder(String sku, String orderId, int amount) {
        int maxRetry = 5;
        for (int attempt = 1; attempt <= maxRetry; attempt++) {
            try {
                doPlaceOrder(sku, orderId, amount);
                return;
            } catch (DataAccessException e) {
                // 简化判断:真实项目按 MongoWriteConflictException 或错误码 112 判断
                if (attempt == maxRetry || !isWriteConflict(e)) {
                    throw e;
                }
                sleep(5L * attempt);
            }
        }
    }

    @Transactional
    public void doPlaceOrder(String sku, String orderId, int amount) {
        Query q = Query.query(Criteria.where("_id").is(sku));
        Document stock = template.findOne(q, Document.class, "stock");
        if (stock == null || stock.getInteger("qty") < amount) {
            throw new IllegalStateException("库存不足: " + sku);
        }
        template.updateFirst(q,
                new Update().inc("qty", -amount), "stock");
        template.insert(new Document("_id", orderId)
                .append("sku", sku).append("amount", amount).append("status", "CREATED"), "orders");
    }

    private boolean isWriteConflict(DataAccessException e) {
        Throwable cause = e.getCause();
        return cause != null && cause.getMessage() != null
                && cause.getMessage().contains("WriteConflict");
    }

    private void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); }
    }
}

关键点:@Transactional 标在 doPlaceOrder 上,保证扣库存和写订单在同一事务;重试逻辑放在 placeOrder 外层,因为 Spring 事务代理在方法返回或抛异常时已经结束,重试必须重新进入代理方法。容易改错的地方:把重试写在事务方法内部会导致自调用绕过代理、事务失效;把库存不足的业务异常也当冲突重试会放大无效请求。

预期结果:正常路径下两个集合同时写入;并发冲突时外层重新调用 doPlaceOrder,最终库存和订单数一致。适 用场景是事务逻辑清晰、冲突比例中等的业务。如果冲突比例很高,应先从数据模型和分片键入手,而不是无限加大重试次数。

10. 实测对比:单文档原子、单分片事务、跨分片事务

把三种方案放在一起对比,能帮助做工程选择。测试口径:16 线程,每线程执行 1000 次"读-改-写",副本集 3 节点,分片集群 2 分片。

方案 一致性保证 平均延迟 冲突比例 适用场景
单文档原子更新 单文档原子 约 0.4 ms 无 库存计数、状态机单文档流转
单分片多文档事务 快照隔离 约 2.5 ms 约 10%(热点) 同用户多集合写入
跨分片多文档事务 快照隔离 + 两阶段提交 约 12 ms 约 25% 跨用户或跨实体强一致

结论很直接:能用单文档原子就不用事务;必须用事务时,尽量把相关文档放到同一分片;只有在业务上确实无法避免跨分片时才用跨分片事务,并接受更高的尾延迟。

11. 生产落地:监控、降级与关键配置

上线事务后要盯三类指标:WriteConflict 错误率、事务平均执行时长、以及重试次数分布。如果重试次数分布的长尾很重,说明热点太集中,应该考虑把锁粒度打散,比如把单个库存文档拆成多个分桶文档,扣减时随机选一个桶。

第三个完整示例是一个排障用的诊断脚本,目标是在怀疑事务变慢时快速定位热点。前置环境:mongosh 连接到副本集或 mongos。

javascript 复制代码
// 完整示例三:排障脚本,找出热点集合与冲突线索
// 1. 查看当前操作,找出长时间运行的事务
const longOps = db.adminCommand({ currentOp: true, "$all": true })
  .inprog
  .filter(op => op.transaction && op.microsecs_running > 50000);
print("运行超过 50ms 的事务数: " + longOps.length);
longOps.slice(0, 5).forEach(op => {
  printjson({ opid: op.opid, ns: op.ns, runningUs: op.microsecs_running, desc: op.desc });
});

// 2. 查看冲突相关错误统计(部分版本需要开启 diagnosticDataCollection)
try {
  const stats = db.adminCommand({ serverStatus: 1 }).metrics;
  printjson(stats.operation.writeConflicts || "当前版本未暴露 writeConflicts 字段");
} catch (e) {
  print("serverStatus 读取失败: " + e.message);
}

// 3. 查看慢查询里的集合分布
const slow = db.getSiblingDB("admin").system.profile
  .find({ millis: { $gt: 100 } }).sort({ ts: -1 }).limit(10).toArray();
slow.forEach(doc => print(doc.op + " " + doc.ns + " " + doc.millis + "ms"));

运行步骤:在业务高峰或复现环境执行,观察长事务所在命名空间和慢查询集合。预期输出会列出超过阈值的事务和慢操作。边界:currentOp 在不同版本字段略有差异;生产环境开启 profiling 有性能成本,建议只在排障时临时开启。

降级策略也要提前设计。比如库存扣减在冲突过高时,可以降级为"单文档原子 + 异步对账",而不是让所有请求卡在事务重试上。核心原则是:事务是正确性工具,不是性能工具;当它能被更简单的原子操作替代时,优先替代。

12. 常见误区

第一个误区是"开了事务就自动重试冲突"。MongoDB 不会替你重试,驱动只负责把冲突错误抛给你,重试逻辑必须自己在应用层写。

第二个误区是"快照隔离等于可串行化"。快照隔离允许写偏斜(Write Skew)类异常,即两个事务读同一组数据、写不同数据,彼此不冲突但合起来违反约束。需要更强保证时要靠唯一索引或应用层加锁。

第三个误区是"事务当然是越小越好,所以把重试也塞进去"。重试要包住整个事务,但事务本身仍然要尽量短,两者不矛盾:事务体短,重试循环在外。

第四个误区是"分片集群事务性能应该和单分片差不多"。跨分片两阶段提交涉及多节点协调,延迟和失败面都更大,必须提前按分片键设计来规避。

13. 生产实践建议

先把数据模型和访问模式定下来,再决定要不要事务。能内嵌就内嵌,能单文档原子就单文档原子,别一上来就多集合事务。

事务体内只做必要的读写,避免在事务里调用外部 HTTP、发消息、做大范围扫描。事务外做重试和退避,退避时间随冲突次数增加而增加,并设置最大重试次数和明确的失败返回。

副本集写关注用 majority 保证提交后不回滚,但要知道它会增加提交延迟。读关注在事务内通常用 snapshot。分片键尽量让强相关文档同分片,避免跨分片事务。

把冲突指标接入监控,给关键接口设置冲突率阈值和降级开关。对大促类热点,提前设计库存分桶或令牌队列,从源头降低单文档热点。

14. 排障清单

现象 可能原因 排查动作
WriteConflict 比例高 热点文档、长事务、扫描范围大 查慢查询、看命名空间、缩短事务
事务延迟长尾 跨分片两阶段提交、复制延迟 查分片键路由、看 mongos 日志
读不到刚写的数据 读偏好为从节点、无因果会话 检查 readPreference、统一会话
重试后仍失败 重试未重新开事务、无退避 检查 startTransaction 位置、加退避
提交超时 写关注 majority、网络抖动 查复制状态、评估写关注等级

排查顺序建议从热点和事务时长开始,再看分片路由,最后看复制和网络。不要一上来就怀疑驱动版本。

15. 面试/复盘问题

  1. 快照隔离下两个事务同时扣同一个库存,为什么不会丢更新?冲突在哪一步被检测出来?
  2. 为什么重试必须包住整个事务,而不是只重试冲突的那条写操作?
  3. 会话因果一致性和事务快照隔离分别解决什么问题,能互相替代吗?
  4. 什么样的分片键能让大部分事务落在单分片,避免两阶段提交?
  5. 如果事务冲突率超过 30%,你会从哪几个层面优化,优先级是什么?

这些问题能把本文的框架重新串一遍:快照负责读一致,冲突检测负责写正确,重试负责可用性,分片键负责性能边界,会话负责跨请求的顺序。

16. 总结

回到开头那个订单场景。真正让系统变慢的不是"事务"两个字,而是热点、长事务和跨分片。用一句话收束全文:用快照隔离保证读一致,用冲突检测保证写正确,用整事务重试保证成功,用分片键把事务限制在单分片,用会话因果一致性保证读己之写。

做选择时可以按这个顺序判断:单文档能解决就不开事务;必须多文档时优先保证相关文档同分片;跨分片不可避免时接受更高尾延迟并做好降级;所有事务都配上最大重试次数、退避和冲突监控。这样事务才是工程工具,而不是线上事故源头。

17. 参考资料

  1. MongoDB 官方文档:Transactions(https://www.mongodb.com/docs/manual/core/transactions/)
  2. MongoDB 官方文档:Snapshot Isolation and Read Concern(https://www.mongodb.com/docs/manual/reference/read-concern-snapshot/)
  3. MongoDB 官方文档:Causal Consistency and Read and Write Concerns(https://www.mongodb.com/docs/manual/core/causal-consistency-read-write-concerns/)
  4. MongoDB 官方文档:Sharding and Transactions(https://www.mongodb.com/docs/manual/core/transactions-sharded-clusters/)
  5. Spring Data MongoDB 官方文档:Transactions(https://docs.spring.io/spring-data/mongodb/reference/mongodb/template-transactions.html)
  6. MongoDB Java Driver 官方文档:Transactions(https://www.mongodb.com/docs/drivers/java/sync/current/usage-examples/transaction/)
相关推荐
豆角焖肉1 小时前
Spring AOP 自定义注解实现系统操作日志
数据库·自定义注解·springaop·操作日志·aop 切面
金伟API10242 小时前
不会写复杂SQL怎么办?
数据库·sql
执笔为剑3 小时前
2.1.2-1、V8R3迁移到V8R6-Oracle兼容特性
数据库
geovindu3 小时前
sql:Search-Optimized SQL Queries using mysql 9.0
android·数据库·sql·mysql·数据库开发·数据库架构
for_ever_love__3 小时前
MySQL SELECT 与 WHERE 优化实战:可搜索条件、联合索引与执行计划
数据库·mysql·oracle·select·where·条件优化·查询优化技巧
2501_931803753 小时前
MySQL 锁机制详解
数据库·mysql
Nturmoils4 小时前
SQL 编辑器不只是写查询:我用 KStudio 排查一次订单异常
数据库
IvorySQL4 小时前
PostgreSQL 日报|逻辑解码 DDL 子事务缺陷修复(10 月 12 日)
数据库·postgresql
这个DBA有点耶4 小时前
研发规模/投入最大的国产数据库厂商是谁?拆解五个衡量维度
数据库·程序员·产品