全局数据库连接池 (`DB`)
大约 5 分钟
全局数据库连接池 (DB
)
在我们的应用中,与数据库的交互是核心功能之一。为了实现高效、安全且易于管理的数据库访问,我们采用了一种基于 tokio::sync::OnceCell
的 全局异步单例数据库连接池 模式。
这个模式被封装成一个极其简洁的异步函数 DB()
,你可以在应用的任何 async
上下文中调用它来获取数据库连接。
核心理念
通过 let db = DB().await;
这一行代码,你可以安全地获取到一个全局共享、性能卓越的数据库连接池实例。
如何使用 DB()
在需要进行数据库操作的任何地方(通常是 Service 层或 Handler 层),只需调用 DB().await
即可。
1. 获取连接池实例
use crate::DB; // 确保通过 prelude 或直接 use 引入
async fn some_function() -> Result<()> {
// 只需这一行,即可获得一个 'static 生命周期的数据库连接池引用
let db = DB().await;
// ... 接下来可以使用 db 进行各种数据库操作
Ok(())
}
解释:
DB()
是一个异步函数,因此需要.await
。- 它返回一个
&'static DatabaseConnection
,这意味着你得到的是一个对 整个应用生命周期内都有效 的数据库连接池的引用。 DatabaseConnection
实际上是一个 连接池(由sea-orm
底层的sqlx::Pool
提供支持),而不是单个连接。
2. 在 SeaORM 中使用
获取到 db
实例后,你可以将其传递给 SeaORM 的各种查询和操作方法。
✅ 更新 ActiveModel
async fn update_user_avatar(user_id: i64, new_avatar: String) -> Result<()> {
let db = DB().await;
let mut user_active: user::ActiveModel = user::Entity::find_by_id(user_id)
.one(db)
.await?
.ok_or(Error::NotFound("用户不存在".to_string()))?
.into();
user_active.avatar = Set(Some(new_avatar));
// 将 db 传递给 update 方法
user_active.update(db).await?;
Ok(())
}
✅ 执行查询构造器 (Query Builder)
async fn change_username(id: i64, new_name: String) -> Result<()> {
let db = DB().await;
user::Entity::update_many()
.col_expr(user::Column::Username, Expr::value(new_name))
.filter(user::Column::Id.eq(id))
.exec(db) // 将 db 传递给 exec 方法
.await?;
Ok(())
}
为什么采用这种设计?
这种设计的背后有几个关键考量,旨在实现性能、安全和代码简洁性的最佳平衡。
1. tokio::sync::OnceCell
:安全的异步单例
OnceCell
是实现“单次初始化”的关键。DATABASE.get_or_init(db_conn).await
这行代码保证了:
db_conn()
函数在整个应用的生命周期中 只会被成功执行一次。- 即使有多个线程在应用启动时同时尝试调用
DB().await
,OnceCell
内部的异步锁机制也能确保只有一个线程会真正执行初始化,其他线程则会安全地等待初始化完成后获取结果。 - 这完美地解决了 异步场景下的懒加载单例 问题,避免了创建多个数据库连接池的风险。
2. 单例模式 (Singleton Pattern) 的优势
- 资源效率: 整个应用共享一个连接池,有效复用数据库连接,避免了频繁创建和销毁连接的巨大开销,也防止了耗尽数据库的最大连接数。
- 性能卓越: 从连接池中获取一个现有连接远比建立一个新连接快得多。
- 全局可访问: 无需在层层函数调用中手动传递数据库连接池实例(避免了所谓的“参数钻孔” Prop Drilling)。任何需要的地方都可以通过
DB().await
轻松获取。
3. 异步初始化
数据库连接是一个 I/O 密集型操作。将其设计为 async
,可以防止在应用启动时阻塞主线程,提高了应用的启动速度和响应能力。
核心实现解析
让我们深入代码,理解其工作原理。
// 1. 定义一个全局、静态的 OnceCell,用于存放数据库连接池
static DATABASE: OnceCell<DatabaseConnection> = OnceCell::const_new();
// 2. 数据库初始化函数,负责读取配置并建立连接
async fn db_conn() -> DatabaseConnection {
// 从全局配置中读取数据库设置
let config = APPCOFIG.database.clone();
let mut opt = ConnectOptions::new(&config.uri);
// 配置连接池参数...
opt.max_connections(config.max_connections)
.min_connections(config.min_connections)
// ...
// 异步连接数据库,如果失败则 panic,因为数据库是应用的核心依赖
let db = Database::connect(opt).await.expect("Database connected failed");
tracing::info!("Database connected successfully");
db
}
// 3. 公开的异步访问函数
pub async fn db() -> &'static DatabaseConnection {
// 核心:如果 DATABASE 未初始化,则调用 db_conn() 进行初始化,并返回其引用
// 如果已经初始化,则直接返回引用
DATABASE.get_or_init(db_conn).await
}
// 4. 在 prelude 中导出为 DB,简化调用
pub use db::db as DB;
最佳实践与注意事项
关键:如何处理数据库事务
DB().await
获取的是 连接池。如果你需要执行一个数据库事务(Transaction),你必须从连接池开始一个事务,然后在该事务中执行所有操作。
错误的做法: 在事务的多个步骤中分别调用 DB().await
。 正确的做法:
use sea_orm::TransactionTrait;
async fn transfer_funds(from_user: i64, to_user: i64, amount: Decimal) -> Result<()> {
let db = DB().await;
// 1. 从连接池开始一个事务
let txn = db.begin().await?;
// 2. 所有后续操作都使用 `&txn`,而不是 `db`
let sender_model = user::Entity::find_by_id(from_user).one(&txn).await?;
// ... 检查余额 ...
// ... 更新发送者余额 ...
sender_model.update(&txn).await?;
let receiver_model = user::Entity::find_by_id(to_user).one(&txn).await?;
// ... 更新接收者余额 ...
receiver_model.update(&txn).await?;
// 3. 提交事务
txn.commit().await?;
Ok(())
}
当 transfer_funds
函数返回 Err
时,txn
会被丢弃,事务将自动回滚(Rollback)。
- 不要在循环中重复调用: 在一个函数内部,通常只需要在开头调用一次
DB().await
,然后复用db
变量即可。OnceCell
保证了后续调用开销极低,但没必要重复写。 - 依赖注入 vs 全局访问: 这种全局访问模式极大地简化了代码。在 Rust 的所有权和生命周期保证下,这种模式是线程安全的。对于大型、复杂的应用,你也可以考虑使用依赖注入框架,但对于大多数项目而言,
DB()
模式是实用且高效的。