【问题标题】:Database locked in WAL mode with only readers数据库锁定在 WAL 模式下,只有读卡器
【发布时间】:2013-06-11 12:15:38
【问题描述】:

在Write-Ahead Logging 模式下使用 System.Data.Sqlite 1.0.86.0(包括 SQLite 3.7.17)时,我在并发读取时遇到了数据库锁,如果我正确理解 WAL,则不会出现这种情况。我没有写或提交任何东西,ReadCommitted 事务隔离模式已正确使用以避免序列化读取。

SQLite DB (with WAL) locked when preparing a "select" statmement - why? 是一个类似的问题。唯一的答案是在每个sqlite3_step 之后调用sqlite3_reset,就我在源代码中看到的而言,这是由 System.Data.Sqlite 正确完成的。

完整复制:

internal static class Program {

    private const string DbFileName = "test.sqlite";
    private static readonly string _connectionString = BuildConnectionString(DbFileName);

    internal static void Main() {
        File.Delete(DbFileName);
        ExecuteSql("CREATE TABLE Test (Id INT NOT NULL, Name TEXT);", true);
        for (int i = 0; i < 10; i++)
            Task.Run(() => ExecuteSql("SELECT Id, Name FROM Test;", false));
        Console.ReadKey();
    }

    private static string BuildConnectionString(string fileName) {
        var builder = new SQLiteConnectionStringBuilder {
            DataSource = fileName,
            DateTimeFormat = SQLiteDateFormats.ISO8601,
            DefaultIsolationLevel = IsolationLevel.ReadCommitted,
            ForeignKeys = true,
            JournalMode = SQLiteJournalModeEnum.Wal,
            SyncMode = SynchronizationModes.Full
        };
        return builder.ToString();
    }

    private static void ExecuteSql(string sql, bool commit) {
        Stopwatch stopwatch = Stopwatch.StartNew();
        using (var connection = new SQLiteConnection(_connectionString)) {
            connection.Open();
            using (SQLiteTransaction transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted)) {
                using (SQLiteCommand command = connection.CreateCommand()) {
                    command.CommandText = sql;
                    command.ExecuteNonQuery();
                }
                if (commit)
                    transaction.Commit();
            }
        }
        stopwatch.Stop();
        Console.WriteLine("{0}: {1}", stopwatch.Elapsed, sql);
    }

}

输出:

00:00:00.1927492: CREATE TABLE Test (Id INT NOT NULL, Name TEXT);
00:00:00.0054247: SELECT Id, Name FROM Test;
00:00:00.0055334: SELECT Id, Name FROM Test;
00:00:00.0056022: SELECT Id, Name FROM Test;
00:00:00.0054860: SELECT Id, Name FROM Test;
00:00:00.0053894: SELECT Id, Name FROM Test;
00:00:00.0056843: SELECT Id, Name FROM Test;
00:00:00.0006604: SELECT Id, Name FROM Test;
00:00:00.0006758: SELECT Id, Name FROM Test;
00:00:00.0097950: SELECT Id, Name FROM Test;
00:00:00.0980008: SELECT Id, Name FROM Test;

你可以看到最后一个慢了一个数量级。如果在调试模式下执行,则根据运行情况在输出窗口中记录一次或多次:

SQLite 错误 (261):数据库被锁定

您知道如何避免这种锁定吗?当然,在这个示例中,WAL 可以简单地关闭,但在实际项目中我不能:即使正在进行长时间的读取事务,我也需要潜在的写入来立即成功。

【问题讨论】:

  • 总是第十个并发事务吗?如果您使用默认隔离级别,它会改变吗?数据库文件是在网络上还是在本地磁盘上?
  • 不,并不总是第十次,这取决于运行情况,通常在 10 个线程中发生 2-3 次。只有 3 个线程我得到了大约 50% 的时间(当然不是科学衡量标准)。数据库是本地的。 SQLite 的默认隔离级别是 Serializable,这意味着根本没有并发。
  • ...对于给定的交易(如果我得到文档)正确。
  • 您的 sqlite3 可能在没有 HAVE_USLEEP 的情况下编译,这会使并发线程在重试之前等待几秒钟。 beets.io/blog/sqlite-nightmare.html

标签: sqlite concurrency system.data.sqlite


【解决方案1】:

经过调查,锁定数据库的不是读取,而是打开连接。据我再次阅读 WAL 文档后的理解,即使是读者也必须具有对 WAL 文件的写入权限。打开连接的简单事实比非 WAL 模式的成本要高得多。此操作显然包括获取 WAL 文件的排他锁,即使是很短的时间。

一个简单的解决方案是启用池(连接字符串中的Pooling=True)。它在示例中没有任何影响,因为所有连接都是同时打开的,但在现实世界的应用程序中,由于现有连接被重用,因此不再有任何锁定。大多数简单查询从 5 毫秒缩短到不到 1 毫秒(在 SSD 上),并且“数据库已锁定”消息完全消失了。

【讨论】:

    猜你喜欢
    • 2016-08-26
    • 1970-01-01
    • 1970-01-01
    • 2019-09-24
    • 1970-01-01
    • 1970-01-01
    • 2014-12-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多