MongoDB的连接复用,从连接池说到事务

888 阅读13分钟

mongodb中复用连接

在做后端开发过程中,在数据库连接方面,我们为了减少网络io,会使用连接复用,那么mongodb中有复用连接的功能吗?

MongoDB 的客户端库通常支持连接池,允许在多个操作之间复用连接,从而提高性能并减少连接建立和关闭的开销。以下是如何在 Node.js 中使用 mongodb 官方驱动来实现连接池和复用连接的示例。

使用 mongodb 驱动的连接池

mongodb 驱动默认支持连接池,连接池的大小和其他设置可以在创建 MongoClient 实例时配置。

安装 mongodb 驱动

首先,确保你已经安装了 mongodb 驱动:

npm install mongodb

配置连接池

创建一个连接池并复用连接的示例代码如下:

const { MongoClient } = require('mongodb');

async function main() {
  const url = 'mongodb://localhost:27017';
  const dbName = 'mydatabase';

  // 创建 MongoClient 实例,配置连接池大小
  const client = new MongoClient(url, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    poolSize: 10, // 配置连接池大小,例如设置为 10
  });

  try {
    // 连接到 MongoDB
    await client.connect();
    console.log('Connected to database');

    // 获取数据库和集合
    const db = client.db(dbName);
    const booksCollection = db.collection('books');

    // 执行数据库操作
    const books = await booksCollection.find().toArray();
    console.log('Books:', books);

    // 这里可以执行更多的数据库操作,复用同一个连接
  } finally {
    // 关闭连接
    await client.close();
  }
}

main().catch(console.error);

在这个示例中,我们创建了一个连接池,设置 poolSize 选项来配置连接池的大小。连接池将自动管理连接的复用,以处理并发请求。

使用 mongoose 连接池

如果你使用的是 mongoose,它也支持连接池,并且可以通过连接选项进行配置。

安装 mongoose

首先,确保你已经安装了 mongoose

npm install mongoose

配置连接池

配置 mongoose 使用连接池的示例代码如下:

const mongoose = require('mongoose');

async function main() {
  const url = 'mongodb://localhost:27017/mydatabase';

  // 配置连接池大小和其他选项
  await mongoose.connect(url, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    poolSize: 10, // 配置连接池大小,例如设置为 10
  });

  console.log('Connected to database');

  // 定义一个模型
  const bookSchema = new mongoose.Schema({
    title: String,
    author: String,
  });

  const Book = mongoose.model('Book', bookSchema);

  // 执行数据库操作
  const books = await Book.find();
  console.log('Books:', books);

  // 这里可以执行更多的数据库操作,复用同一个连接

  // 关闭连接
  await mongoose.disconnect();
}

main().catch(console.error);

在这个示例中,我们使用 mongoose.connect 方法并配置连接池选项 poolSize 来创建一个连接池。与 mongodb 驱动类似,连接池将自动管理连接的复用,以处理并发请求。

连接池的优缺点

使用连接池的主要优点包括:

  1. 减少连接建立和关闭的开销:连接池允许多个请求复用现有的连接,从而减少了频繁建立和关闭连接的开销。
  2. 提高性能:通过复用连接,可以减少延迟并提高数据库操作的性能。
  3. 更好地管理资源:连接池可以限制同时打开的连接数量,从而更好地管理数据库服务器的资源。

总结来说,无论是使用 mongodb 驱动还是 mongoose,连接池都是提升性能和资源管理的重要机制。通过适当配置连接池,你可以确保应用程序在高并发场景下仍然表现良好。

连接池还有一个特点就是,当连接个数大于之前设置的最大池子连接数时,其他请求将进入队列等待,直到有空闲连接,这似乎没有每次重新建立连接有优势,但其实,使用连接池可以在一定程度上限制高并发请求对数据库服务的压力,同时也在连接容量不足时难以处理高并发请求数据库;避免过多的连接创建和销毁操作,同时连接池的管理和维护也会占用一定的系统资源。

连接池通常会限制同时打开的连接数,这样可以避免因为大量连接同时访问数据库而导致数据库性能下降甚至崩溃的情况。另外,连接池还可以复用已经建立的连接,减少连接的建立和关闭时间,降低系统开销。

但是,连接池也有一些限制和缺点。例如,连接池的大小是有限的,如果连接池的容量不足以处理高并发请求,仍然会出现连接阻塞的情况。此外,连接池的管理和维护也会占用一定的系统资源,需要根据实际情况合理配置连接池参数。

这种机制可以避免数据库连接过多而导致的性能问题,但也需要合理设置连接池的大小,以免过大或过小影响系统性能。

在使用连接池时,通常可以设置以下参数来控制连接池的行为:

  1. 最大连接数(max connections):连接池中允许的最大连接数,超过这个数目的连接请求将被阻塞,满足高并发下减小数据库压力。
  2. 最小连接数(min connections):连接池中保持的最小连接数,避免连接空闲时频繁创建和释放连接,满足减少低并发下连接的创建和销毁操作。
  3. 连接超时(connection timeout):连接池在获取连接时等待的最长时间,超过这个时间将抛出超时异常。

通过合理设置这些参数,可以更好地管理连接池,提高系统的稳定性和性能。

总的来说,连接池可以有效地管理数据库连接,提高系统的性能和稳定性,但需要根据实际情况进行调优和管理,避免出现连接池满或连接阻塞等问题。

复用一个连接

上面的连接池,只是提前建立几个连接,然后使用的时候再去拿已经创建好的连接,前后任然是两个连接,能否有类似redis管道一样的功能,在一个连接中传递多个命令呢?

在 MongoDB 中,类似于 Redis 管道(pipeline)功能的机制称为批量操作(bulk operations)。批量操作允许你在一个批处理中执行多条操作(例如插入、更新、删除),从而减少网络往返次数,提高性能。

MongoDB 提供了两种类型的批量操作:

  1. Unordered Bulk Operations:这些操作可以并行执行,并且即使其中一部分操作失败,其他操作也会继续执行。
  2. Ordered Bulk Operations:这些操作按顺序执行,如果其中一个操作失败,后续操作将不会执行。

示例代码

以下是使用 MongoDB 原生驱动和 Mongoose 进行批量操作的示例:

使用 MongoDB 原生驱动

const { MongoClient } = require('mongodb');

async function main() {
  const url = 'mongodb://localhost:27017';
  const dbName = 'mydatabase';

  // 创建 MongoClient 实例
  const client = new MongoClient(url, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  });

  try {
    // 连接到 MongoDB
    await client.connect();
    console.log('Connected to database');

    // 获取数据库和集合
    const db = client.db(dbName);
    const booksCollection = db.collection('books');

    // 创建批量操作对象
    const bulk = booksCollection.initializeUnorderedBulkOp();

    // 添加多条操作到批量操作对象中
    bulk.insert({ title: 'Book 1', author: 'Author 1' });
    bulk.insert({ title: 'Book 2', author: 'Author 2' });
    bulk.find({ title: 'Book 1' }).updateOne({ $set: { author: 'Updated Author' } });
    bulk.find({ title: 'Book 2' }).deleteOne();

    // 执行批量操作
    const result = await bulk.execute();
    console.log('Bulk operation completed:', result);
  } finally {
    // 关闭连接
    await client.close();
  }
}

main().catch(console.error);

使用 Mongoose

Mongoose 不直接支持批量操作接口,但你可以使用 MongoDB 原生驱动提供的批量操作方法。以下是一个使用 Mongoose 的示例:

const mongoose = require('mongoose');

async function main() {
  const url = 'mongodb://localhost:27017/mydatabase';

  // 连接到 MongoDB
  await mongoose.connect(url, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  });

  console.log('Connected to database');

  // 定义一个模型
  const bookSchema = new mongoose.Schema({
    title: String,
    author: String,
  });

  const Book = mongoose.model('Book', bookSchema);

  // 获取原生 MongoDB 集合对象
  const booksCollection = mongoose.connection.collection('books');

  // 创建批量操作对象
  const bulk = booksCollection.initializeUnorderedBulkOp();

  // 添加多条操作到批量操作对象中
  bulk.insert({ title: 'Book 1', author: 'Author 1' });
  bulk.insert({ title: 'Book 2', author: 'Author 2' });
  bulk.find({ title: 'Book 1' }).updateOne({ $set: { author: 'Updated Author' } });
  bulk.find({ title: 'Book 2' }).deleteOne();

  // 执行批量操作
  const result = await bulk.execute();
  console.log('Bulk operation completed:', result);

  // 关闭连接
  await mongoose.disconnect();
}

main().catch(console.error);

通过使用 MongoDB 的批量操作功能,你可以在一个批处理中执行多个操作,从而减少网络往返次数,提高性能。批量操作有两种类型:无序批量操作和有序批量操作,你可以根据需要选择适合的类型。如果使用 Mongoose,可以通过获取原生 MongoDB 集合对象来进行批量操作。

批处理有一些限制:第一个是只能对一个集合进行批处理,第二个是批处理只能用于数据的命令,不支持聚合查询

批处理和连接池的区别

MongoDB 的批量操作(bulk operations)是在一个连接中执行多条命令。批量操作的设计目的是为了减少网络往返,从而提高性能,这与连接池管理是两个不同的概念和机制。

连接池和批量操作的区别

  1. 连接池

    • 功能:连接池是用来管理多个到 MongoDB 服务器的连接,以便客户端可以复用这些连接。连接池有助于减少每次操作都重新建立连接的开销。
    • 工作方式:当客户端需要进行数据库操作时,它从连接池中获取一个连接。当操作完成后,连接被返回到连接池中,以便下次使用。
    • 配置:通过 poolSize 配置选项来设置连接池中最大连接数。例如,poolSize: 10 表示连接池中最多可以有 10 个连接。
  2. 批量操作

    • 功能:批量操作允许在一次请求中执行多条数据库命令(如插入、更新、删除)。这减少了客户端与服务器之间的网络往返次数,从而提高性能。
    • 工作方式:批量操作会将多个数据库命令合并成一个批次,然后通过一个连接发送到 MongoDB 服务器。服务器会按顺序执行这些命令,并返回结果。
    • 类型:有无序批量操作(unordered bulk operations)和有序批量操作(ordered bulk operations)。

连接池与批量操作的结合

批量操作是在一个连接中执行的,这个连接可以是从连接池中获取的。因此,批量操作本身并不会影响连接池的管理。以下是两者结合使用时的工作流程:

  1. 从连接池获取连接:当你准备执行批量操作时,客户端从连接池中获取一个连接。
  2. 执行批量操作:使用这个连接执行批量操作。批量操作中的所有命令都会在这个连接中按顺序执行。
  3. 返回连接到连接池:批量操作完成后,连接会被返回到连接池中,以供后续操作复用。

示例代码

以下是一个结合使用连接池和批量操作的示例:

const { MongoClient } = require('mongodb');

async function main() {
  const url = 'mongodb://localhost:27017';
  const dbName = 'mydatabase';

  // 创建 MongoClient 实例,并设置连接池大小
  const client = new MongoClient(url, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    poolSize: 10, // 设置连接池大小为 10
  });

  try {
    // 连接到 MongoDB
    await client.connect();
    console.log('Connected to database');

    // 获取数据库和集合
    const db = client.db(dbName);
    const booksCollection = db.collection('books');

    // 创建批量操作对象
    const bulk = booksCollection.initializeUnorderedBulkOp();

    // 添加多条操作到批量操作对象中
    bulk.insert({ title: 'Book 1', author: 'Author 1' });
    bulk.insert({ title: 'Book 2', author: 'Author 2' });
    bulk.find({ title: 'Book 1' }).updateOne({ $set: { author: 'Updated Author' } });
    bulk.find({ title: 'Book 2' }).deleteOne();

    // 执行批量操作
    const result = await bulk.execute();
    console.log('Bulk operation completed:', result);
  } finally {
    // 关闭连接
    await client.close();
  }
}

main().catch(console.error);

在这个示例中,我们设置了连接池的大小,并在批量操作中复用了从连接池获取的连接。批量操作在获取的连接上执行所有命令,完成后连接返回到连接池。

批量操作和连接池是两个独立的机制。批量操作是在一个连接中执行多个数据库命令,而连接池是管理和复用多个连接的机制。结合使用这两者,可以有效提高 MongoDB 操作的性能和效率。

批处理的缺陷

MongoDB 的批处理操作(Bulk Operations)只能针对一个集合进行。在批量操作中,你可以对一个集合执行多个插入、更新、删除操作,但这些操作必须在同一个集合中进行。

如果你需要对多个集合执行批量操作,可以使用事务(transactions),但这超出了单个批处理操作的范围。事务允许你在多个集合或文档上执行一组操作,并确保这些操作要么全部成功,要么全部回滚,以保证原子性。

使用事务示例

以下是一个使用事务在多个集合中执行操作的示例:

const { MongoClient } = require('mongodb');

async function main() {
  const url = 'mongodb://localhost:27017';
  const dbName = 'mydatabase';

  const client = new MongoClient(url, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    poolSize: 10, // 设置连接池大小为 10
  });

  try {
    await client.connect();
    console.log('Connected to database');

    const session = client.startSession();

    session.startTransaction();

    try {
      const db = client.db(dbName);
      const booksCollection = db.collection('books');
      const authorsCollection = db.collection('authors');

      // 在事务中执行多个集合的操作
      await booksCollection.insertOne({ title: 'Book 1', author: 'Author 1' }, { session });
      await authorsCollection.insertOne({ name: 'Author 1', books: ['Book 1'] }, { session });

      // 提交事务
      await session.commitTransaction();
      console.log('Transaction committed.');
    } catch (error) {
      // 发生错误时回滚事务
      await session.abortTransaction();
      console.error('Transaction aborted. Error:', error);
    } finally {
      session.endSession();
    }
  } finally {
    await client.close();
  }
}

main().catch(console.error);

在这个示例中,我们在事务中对 booksauthors 集合执行了插入操作。如果任何一个操作失败,事务将回滚,确保数据库的一致性。

MongoDB 的批处理操作只能针对单个集合进行。如果需要对多个集合执行操作,可以使用事务来保证操作的原子性和一致性。事务适用于需要在多个集合或文档上执行一组操作,并确保这些操作要么全部成功,要么全部回滚的场景。

事务的连接

MongoDB的事务是多个连接内进行的。当事务内发生失败时,数据可以回滚,以确保数据库的一致性和完整性。

由于mongodb的事务是在多个连接中进行并发出多个请求,在一个事务的执行过程中,其他并发连接也是可以请求数据库的,这样数据库怎么知道哪些请求是事务任务的一个部分呢?这个时候就得说到会话(session)的功能,我们需要在事务任务的请求中加上会话(session),这样数据库的事务处理器才知道那个请求属于事务任务。

因此,即使某些操作如果是在同一个连接中发起的,如果没有传递会话,它们也会被视为独立的请求,不具备事务的特性。

MongoDB 事务特性

  1. 会话

    • 事务在单个会话(session)内进行,该会话由一个客户端连接创建。
    • 在事务期间,所有的操作都在这个连接上执行,以确保事务的原子性。
  2. 原子性

    • 事务中的所有操作要么全部成功,要么全部回滚。这样可以确保数据库不会处于不一致的状态。
    • 如果事务内的某个操作失败,事务会回滚所有已执行的操作。

事务回滚示例

以下是一个在MongoDB中使用事务的示例,包括在事务内发生失败时的回滚:

const { MongoClient } = require('mongodb');

async function main() {
  const url = 'mongodb://localhost:27017';
  const dbName = 'mydatabase';

  const client = new MongoClient(url, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    poolSize: 10,
  });

  try {
    await client.connect();
    console.log('Connected to database');

    const session = client.startSession();

    // 开始事务
    session.startTransaction();

    try {
      const db = client.db(dbName);
      const booksCollection = db.collection('books');
      const authorsCollection = db.collection('authors');

      // 执行多集合操作
      await booksCollection.insertOne({ title: 'Book 1', author: 'Author 1' }, { session });//传递session 很重要
      await authorsCollection.insertOne({ name: 'Author 1', books: ['Book 1'] }, { session });//传递session 很重要

      // 这里引入一个错误来演示回滚
      await authorsCollection.insertOne({ _id: 1, name: 'Author 2', books: ['Book 2'] }, { session });//传递session 很重要
      await authorsCollection.insertOne({ _id: 1, name: 'Author 3', books: ['Book 3'] }, { session }); // 这将导致重复键错误

      // 提交事务
      await session.commitTransaction();
      console.log('Transaction committed.');
    } catch (error) {
      // 发生错误时回滚事务
      await session.abortTransaction();
      console.error('Transaction aborted. Error:', error);
    } finally {
      session.endSession();
    }
  } finally {
    await client.close();
  }
}

main().catch(console.error);

在上述示例中,第二个插入操作会导致重复键错误,触发事务回滚。所有在此事务中进行的操作(包括第一个插入操作)都会被回滚,不会对数据库产生任何影响。

注意事项

  • 事务支持:事务功能在MongoDB 4.0及以上版本中引入,最初仅支持复制集。在MongoDB 4.2及以上版本中,事务扩展到分片集群。
  • 性能开销:事务可能会引入额外的性能开销,特别是在高并发写入场景中,因此应谨慎使用。