在我的Rust应用程序中,我正在使用Diesel
与SQLite
数据库进行交互。我有多个线程可以同时查询数据库,并且正在使用板条箱r2d2
创建连接池。
我看到的问题是我无法同时查询数据库。如果尝试这样做,我总是会收到错误database is locked
,该错误是无法恢复的(即使只有一个线程正在查询,任何后续请求都将因相同的错误而失败)。
以下代码重现了该问题。
# Cargo.toml
[dependencies]
crossbeam = { version = "0.7.1" }
diesel = { version = "1.4.2", features = ["sqlite", "r2d2"] }
-- The database table
CREATE TABLE users (
name TEXT PRIMARY KEY NOT NULL
);
#[macro_use]
extern crate diesel;
mod schema;
use crate::schema::*;
use crossbeam;
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::RunQueryDsl;
use diesel::{ExpressionMethods, SqliteConnection};
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "users"]
struct User {
name: String,
}
fn main() {
let db_url = "test.sqlite3";
let pool = Pool::builder()
.build(ConnectionManager::<SqliteConnection>::new(db_url))
.unwrap();
crossbeam::scope(|scope| {
let pool2 = pool.clone();
scope.spawn(move |_| {
let conn = pool2.get().unwrap();
for i in 0..100 {
let name = format!("John{}", i);
diesel::delete(users::table)
.filter(users::name.eq(&name))
.execute(&conn)
.unwrap();
}
});
let conn = pool.get().unwrap();
for i in 0..100 {
let name = format!("John{}", i);
diesel::insert_into(users::table)
.values(User { name })
.execute(&conn)
.unwrap();
}
})
.unwrap();
}
这是应用程序出现紧急情况时显示的错误:
thread '<unnamed>' panicked at 'called `Result::unwrap()` on an `Err` value: DatabaseError(__Unknown, "database is locked")'
AFAIK,我应该能够将连接池与多个线程一起使用(即,用于多个线程的多个连接),如r2d2_sqlite
板条箱example所示。
此外,我已在系统中安装的sqlite3库支持序列化线程模型,该模型来自here:
在序列化模式下,SQLite可以被多个线程安全地使用, 没有限制。
如何避免出现database is locked
错误?另外,如果由于某种原因无法避免这些错误,该如何解锁数据库?
答案 0 :(得分:1)
最近我也偶然发现了这个问题。这就是我发现的东西。
SQLite 不不支持多个编写器。
摘自文档:
当SQLite尝试访问被另一个进程锁定的文件时,默认行为是返回SQLITE_BUSY。
线程模型是关于在多个线程之间使用单个连接。 在您的示例中,序列化无效。您正在使用多个连接,并且每个连接同时在单个线程上使用。
那么如何解决这些限制?我看到了两种解决方案。
您可以多次重试查询,直到获得锁为止。 实际上,SQLite提供了built-in mechanism。 您可以指示SQLite尝试多次锁定数据库。
现在,您唯一需要做的就是以某种方式将此编译指示传递给SQLite。
不幸的是,diesel::r2d2
并没有为通过新建立的连接传递初始设置的简便方法。
但是快速的解决方法是在SqliteConnection
上做自己的包装,然后重新实现Connection::establish
:
struct SqliteConnectionWrap(SqliteConnection);
impl SimpleConnection for SqliteConnectionWrap {
fn batch_execute(&self, query: &str) -> QueryResult<()> {
self.0.batch_execute(query)
}
}
impl Connection for SqliteConnectionWrap {
type Backend = <SqliteConnection as Connection>::Backend;
type TransactionManager = <SqliteConnection as Connection>::TransactionManager;
fn establish(database_url: &str) -> ConnectionResult<Self> {
let c = SqliteConnection::establish(database_url)?;
c.batch_execute("PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 60000;")
.unwrap();
Ok(Self(c))
}
fn execute(&self, query: &str) -> QueryResult<usize> {
self.execute(query)
}
fn query_by_index<T, U>(&self, source: T) -> QueryResult<Vec<U>>
where
T: AsQuery,
T::Query: QueryFragment<Self::Backend> + QueryId,
Self::Backend: HasSqlType<T::SqlType>,
U: Queryable<T::SqlType, Self::Backend>,
{
self.query_by_index(source)
}
fn query_by_name<T, U>(&self, source: &T) -> QueryResult<Vec<U>>
where
T: QueryFragment<Self::Backend> + QueryId,
U: QueryableByName<Self::Backend>,
{
self.query_by_name(source)
}
fn execute_returning_count<T>(&self, source: &T) -> QueryResult<usize>
where
T: QueryFragment<Self::Backend> + QueryId,
{
self.execute_returning_count(source)
}
fn transaction_manager(&self) -> &Self::TransactionManager {
self.transaction_manager()
}
}
您可能要使用的第二个变量是WAL模式。通过允许读者和作家同时工作来提高并发性。 但是请注意,繁忙的超时still required才能使所有这些正常工作。
请记住,即使在WAL模式下,也只有一个线程可以写入SQLite数据库。 WAL还有一个收获。 在WAL模式下,SQLite创建必须打开才能写入的其他文件。这将使用WAL的连接数限制为one active connection:
编写者仅将新内容附加到WAL文件的末尾。 因为作家不做任何会干扰读者行为的事情,所以作家和读者可以同时运行。 但是,由于只有一个WAL文件,所以一次只能有一个写入器。
但是WAL模式仍然比默认日志模式更快。我的策略很简单-在互斥体后面的多个线程之间共享一个编写器连接。读者使用r2d2
和我们刚刚实现的包装器。如果在您的用例中经常进行写操作,那么这就是方法。
let conn = SqliteConnection::establish(db_url).unwrap();
conn.batch_execute(
"PRAGMA synchronous = NORMAL; PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;",
)
.unwrap();
答案 1 :(得分:1)
我发现设置r2d2::Pool::builder().max_size(1)
解决了此问题,但是您需要小心连接管理,不要问是否已经拥有一个,例如:
fn create(pool: &DbPool, data: User) {
let conn = pool.get().unwrap(); // One connection
if !exist(pool, data) { // Two connection
diesel::insert_into(users::table)
.values(User { name: data.name })
.execute(&conn)
.unwrap();
}
}
fn exist(pool: &DbPool, data: User) -> bool {
let conn = pool.get().unwrap();
Ok(
select(exists(users::table.filter(col_user_name.eq(data.name))))
.get_result(&conn)
.unwrap(),
)
}
我删除了所有let conn = pool.get().unwrap();
,仅尝试将其放入请求中。
diesel::insert_into(users::table)
.values(User { name })
.execute(&pool.get().unwrap())
.unwrap();