通过System.Data.SQLite和c#实现对单个SQLite数据库文件的多次访问

11
据我从SQLite FAQ中了解到,它支持多个进程读取(SELECT),但在任何时候只支持一个进程写入(INSERT、UPDATE、DELETE)数据库:
  

SQLite使用读写锁来控制对数据库的访问。   当任何进程想要写入时,它必须在其更新期间锁定整个数据库文件。   但是通常只需要几毫秒钟。其他进程只需等待编写者完成,然后继续他们的业务

我正在使用通过c#调用的System.Data.SQLite适配器。

请问有人能够准确地向我解释这个过程吗?

这个过程是否会自动工作,如果已经有另一个写入 SQLiteCommand 正在执行相同的数据库,那么写入 SQLiteCommand 将简单地等待?

还是它会抛出异常?是什么样的异常?

很抱歉,但我没有找到关于这个机制的信息:)

谢谢。

更新:

我发现一篇帖子说将引发异常并带有特定的错误代码。

这个陈述正确吗?


1
还没有 :) 我更喜欢在编码之前阅读教程 - 以获得优化和正确的解决方案... - bairog
1
如果您更喜欢“优化和正确的解决方案”,请不要将SQLite用于其未经优化的用途;它是一个很棒的数据库,但并不是一个很好的多用户数据库。 - Tim
3
我需要一个免费且轻量级(零配置和无服务器)的数据库用于我的项目。因此,大多数“多用户优化”的数据库对我来说不太合适 :) - bairog
1个回答

19

我已经自己进行了调查:

我创建了一个样例SQLite数据库c:\123.db,其中包含一个名为Categories的表,该表包含两个字段:ID(uniqueidentifier)和Name(nvarchar)。

然后,我编写了一些多线程代码来模拟对数据库的多个写访问(如果您使用此代码,请不要忘记将System.Data.SQLite引用添加到您的项目中):

using System;
using System.Data.SQLite;
using System.Threading.Tasks;

namespace SQLiteTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var tasks = new Task[100];

            for (int i = 0; i < 100; i++)
            {
                tasks[i] = new Task(new Program().WriteToDB);
                tasks[i].Start();
            }

            foreach (var task in tasks)
                task.Wait();
        }

        public void WriteToDB()
        {
            try
            {
                using (SQLiteConnection myconnection = new SQLiteConnection(@"Data Source=c:\123.db"))
                {
                    myconnection.Open();
                    using (SQLiteTransaction mytransaction = myconnection.BeginTransaction())
                    {
                        using (SQLiteCommand mycommand = new SQLiteCommand(myconnection))
                        {
                            Guid id = Guid.NewGuid();

                            mycommand.CommandText = "INSERT INTO Categories(ID, Name) VALUES ('" + id.ToString() + "', '111')";
                            mycommand.ExecuteNonQuery();

                            mycommand.CommandText = "UPDATE Categories SET Name='222' WHERE ID='" + id.ToString() + "'";
                            mycommand.ExecuteNonQuery();

                            mycommand.CommandText = "DELETE FROM Categories WHERE ID='" + id.ToString() + "'";
                            mycommand.ExecuteNonQuery();
                        }
                        mytransaction.Commit();
                    }
                }
            }
            catch (SQLiteException ex)
            {
                if (ex.ReturnCode == SQLiteErrorCode.Busy)
                    Console.WriteLine("Database is locked by another process!");
            }
        }
    }
}

在我的Core2Duo E7500上的测试结果是:异常从未被触发!

看起来SQLite已经为我的需求做了足够的优化(根据SQLite FAQ所说,锁定/解锁非常快并且通常只需要几毫秒)- 很棒!

请注意,对于SQLiteException,没有必要检索整数ErrorCode - 您可以使用特殊的枚举ReturnCode字段。所有代码都在这里描述。

希望这些信息能够帮助到某些人。


2
你的代码证明了单个进程内的多个线程可以无缝地访问SQLite数据库。你能否测试多个不同的进程是否可以访问同一个数据库? - pmont
首先我想到的是将代码编译成 Test1.exe,然后在 Main 的第一行添加 **Process.Start("Test1.exe")**,再重新编译成 Test2.exe。看起来应该可以工作,但我从未测试过... - bairog
3
好主意!我刚刚尝试了一下那种方法,它起作用了。使用SQLite时,多进程访问完全正常。 - pmont
只是想補充一下。我正在撰寫一個基於 NLog / SignalR 的“保證交付”日誌服務,使用 System.Data.SQLite 寫入本地數據庫以便後台線程將數據發送到 LogStash,並且我有 40 個線程將日誌消息發送到本地 SignalR 中心。當該單個進程正在處理其他進程的其他線程的消息時,它肯定會拋出 SQLite error (5): database is locked 異常。 - James Eby
2
@JamesEby SQlite使用内部日志记录,因此当某个块状态或其他原因导致失败时,它会重试直到完成。因此,这不是内部编码问题,当然您的插入操作也不会丢失。实际上,如果您看到消息显示出现了错误,则稍后会进行重试。当您使用线程进行插入时,这种“锁定”错误很常见。除此之外,如果您从线程中读取大量数据,则可以使用WAL日志模型,以便将写入和读取分开处理,因此来自线程的写入不会影响可能存在的数千个并发读取操作。 - m3nda
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接