如何通过SQLite连接使用Diesel并避免出现“数据库已锁定”类型的错误

时间:2019-07-20 09:35:12

标签: database multithreading sqlite rust locking

在我的Rust应用程序中,我正在使用DieselSQLite数据库进行交互。我有多个线程可以同时查询数据库,并且正在使用板条箱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错误?另外,如果由于某种原因无法避免这些错误,该如何解锁数据库?

2 个答案:

答案 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模式

您可能要使用的第二个变量是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();