1

我正在使用Microsoft.Data.Sqlite.CorewithSQLitePCLRaw.lib.e_sqlcipher访问加密的 SQLite 数据库。我需要解决一些挑战:

  • 我必须确保同一个实例Microsoft.Data.Sqlite.SqliteConnection不在多个线程之间共享(SQLite 连接不是线程安全的);
  • 打开 SQLCipher 加密数据库的成本很高,因此我必须将发生的次数限制在最低限度。

我想出了解决方案,但我不确定它是如何证明失败的,所以我希望有人能在其中戳一些漏洞。

4

2 回答 2

1

我面临与您相同的认识,并且将不得不做类似的事情。我认为您的代码没有问题。

我将尝试使用 ConcurrentDictionary 进行一些优化。它应该允许我在更新时避免锁定读者。

如果您继续使用 Dictionary,您可能希望将 ContainsKey() 调用更改为 TryGetValue(),因为文档表明如果使用不存在的键可能会更有效(我们经常会看到新线程?)

如果其他人开车经过,这里是我对这个问题的背景研究:Microsoft.Data.Sqlite 派生自 ADO.NET 对象,例如 DbConnection,根据设计,它不是线程安全的。ADO.NET 的设计者在高性能的祭坛上牺牲了线程安全。出于这个原因,任何使用从 ADO.NET 派生的任何东西的代码最终都需要进入这个兔子洞,以确保不会发生真正奇怪的事情。

至于 Microsoft.Data.Sqlite 我的问题是 SqliteCommand.Dispose 内部遇到 null 并崩溃。这是框架在不同线程上进行大量并行调用的时候。

您可能还注意到 sqlite 本身具有多线程设置,并相信这个兔子洞是适合您的。不幸的是,摆弄这些设置,尽管它们可能是有益的,但并没有改变 ADO.NET 对象设计,因此只要您使用 Microsoft.Data.Sqlite 访问 sqlite,问题就仍然存在。

感谢您发布您的代码!希望它能够成功地投入生产:-)

/尼克

发布后添加:持有对 Thread 和 SqliteConnection 的引用的字典将对资源产生影响,因为只要引用存在,它就会停止垃圾收集回收它们的资源。SqliteConnections 也是如此。可能不是一个真正的问题,但人们可能还想考虑某种清理算法,该算法将在一段时间后删除陈旧的线程引用。SqliteConnections 如果移过来并单独存储,仍然可以重复使用。

/尼克

于 2021-04-16T07:46:44.473 回答
0

这是我的解决方案,如果/哪里搞砸了,请告诉我:

public class SqliteConnectionPool
{
    private readonly object lockObject_ = new object();

    private readonly string connectionString_;
    private readonly Dictionary<Thread, SqliteConnection> pool_;

    public SqliteConnectionPool(string connectionString)
    {
        new SqliteConnectionStringBuilder
        {
            ConnectionString = connectionString // throws if connection string is invalid
        };

        connectionString_ = connectionString;
        pool_ = new Dictionary<Thread, SqliteConnection>();
    }

    public SqliteConnection GetConnection()
    {
        lock (lockObject_)
        {
            Thread currentThread = Thread.CurrentThread;

            // If this thread owns a connection, just retrieve it.
            if (pool_.ContainsKey(currentThread))
            {
                return pool_[currentThread];
            }

            // Looking for a thread that doesn't need its connection anymore.
            (Thread inactiveThread, SqliteConnection availableConnection) = pool_.Where(p => p.Key.ThreadState != ThreadState.Running)
                .Select(p => (p.Key, p.Value))
                .FirstOrDefault();

            // If all existing connections are being used, create a new one.
            if (availableConnection is null)
            {
                var connection = new SqliteConnection(connectionString_);
                pool_[currentThread] = connection;
                return connection;
            }

            // Otherwise, just use the existing free connection.
            pool_.Remove(inactiveThread);
            pool_[currentThread] = availableConnection;
            return availableConnection;
        }
    }
}
于 2020-10-02T09:17:53.793 回答