SQLite在连接关闭后仍会保持数据库锁定状态。

43
我正在一个ASP.NET应用程序(框架4.0)中使用System.Data.SQLite提供程序。我的问题是,当我向SQLite数据库的表中插入数据时,该数据库被锁定,并且即使连接已释放,锁也没有被释放。
在尝试访问文件时,会出现错误:“由于正被另一个进程使用,因此无法访问文件'catalog.sqlite'。”
我的代码很简单,我打开连接,从SQLServer数据库读取一些数据,通过SQLiteDataAdapter将该数据插入SQLite,然后关闭连接并释放所有内容,只是为了保险起见。但是,当我尝试在数据填充后压缩文件时,仍然会收到该错误。
我在StackOverflow上阅读了各种建议,但是没有任何一种方法能够解决这个问题(关闭防病毒软件、更改事务模型、在压缩文件之前等待几秒钟、将所有插入调用包装到事务中等等)。可能与ASP.NET有关(多线程是问题吗?尽管我正在测试它的开发机器上只有一个对该函数的调用,没有并发?)
附带说明一下,我尝试避免使用DataTable和SQLiteDataAdapter,而是直接使用SQLiteCommand,这样它就可以完美地工作。当然,我可以继续构建我的查询字符串而不是使用数据适配器,但是当有一个构建来做这件事时,我觉得这有点尴尬。

2
你是否在using语句中包装命令和连接? - Arran
这个锁是由你的应用程序控制的吗?也就是说,当应用程序退出时,这个锁会消失吗? - CL.
@Arran 我没有使用 using 来包装连接。但是,即使是那个不需要锁定的版本(使用命令而不是数据适配器的版本),我也没有包装它。 - Valerio Santinelli
@CL。是的,锁定是由我的应用程序引起的。如果我停止开发Web服务器,文件就会解锁。 - Valerio Santinelli
1
请展示源代码,至少说明您如何创建、关闭/释放所有对象。 - CL.
18个回答

36
我曾经遇到同样的问题,使用与 System.Data.Sqlite.dll 版本 1.0.82.0 一起提供的设计器生成的数据集/表适配器后,在关闭连接后,我们无法使用 System.IO.FileStream 读取数据库文件。 我正确地处理了连接和表适配器的释放,并且没有使用连接池。
根据我的首次搜索(例如 thisthis thread),似乎这是库本身的问题,可能是对象未正确释放和/或池问题(我不使用)。
阅读您的问题后,我尝试使用仅 SQLiteCommand 对象来复制问题,发现当您不处理它们时,问题会出现。{Update 2012-11-27 19:37 UTC}:这个票证进一步证实了这一点,对于System.Data.SQLite,其中一个开发人员解释说,“与连接相关的所有SQLiteCommand和SQLiteDataReader对象都应该正确处理”。
然后,我重新启用了生成的TableAdapters,并发现没有实现Dispose方法--因此,创建的命令没有被处理。 我实现了它,注意处理所有命令,就没有问题了。
以下是C#代码,希望能帮到您。请注意,代码是从Visual Basic中的原始代码转换而来,因此可能会有一些转换错误。
//In Table Adapter    
protected override void Dispose(bool disposing)
{
   base.Dispose(disposing);

    Common.DisposeTableAdapter(disposing, _adapter, _commandCollection);
}

public static class Common
{
    /// <summary>
    /// Disposes a TableAdapter generated by SQLite Designer
    /// </summary>
    /// <param name="disposing"></param>
    /// <param name="adapter"></param>
    /// <param name="commandCollection"></param>
    /// <remarks>You must dispose all the command,
    /// otherwise the file remains locked and cannot be accessed
    /// (for example, for reading or deletion)</remarks>
    public static void DisposeTableAdapter(
        bool disposing,
        System.Data.SQLite.SQLiteDataAdapter adapter,
        IEnumerable<System.Data.SQLite.SQLiteCommand> commandCollection)
    {
        if (disposing) {
            DisposeSQLiteTableAdapter(adapter);

            foreach (object currentCommand_loopVariable in commandCollection)
            {
                currentCommand = currentCommand_loopVariable;
                currentCommand.Dispose();
            }
        }
    }

    public static void DisposeSQLiteTableAdapter(
            System.Data.SQLite.SQLiteDataAdapter adapter)
    {
        if (adapter != null) {
            DisposeSQLiteTableAdapterCommands(adapter);

            adapter.Dispose();
        }
    }

    public static void DisposeSQLiteTableAdapterCommands(
            System.Data.SQLite.SQLiteDataAdapter adapter)
    {
        foreach (object currentCommand_loopVariable in {
            adapter.UpdateCommand,
            adapter.InsertCommand,
            adapter.DeleteCommand,
            adapter.SelectCommand})
        {
            currentCommand = currentCommand_loopVariable;
            if (currentCommand != null) {
                currentCommand.Dispose();
            }
        }
    }
}

更新时间 2013-07-05 17:36 UTC gorogm 的回答 强调了两个重要的事情:

  • according to the changelog on the official site of System.Data.SQLite, starting from version 1.0.84.0 the above code should not be needed, since the library takes care of this. I haven't tested this, but in the worst case you only need this snippet:

    //In Table Adapter    
    protected override void Dispose(bool disposing)
    {
      base.Dispose(disposing);
    
      this.Adapter.Dispose();
    }
    
  • about the implementation of the Dispose call of the TableAdapter: it is is better to put this in a partial class, so that a dataset regeneration does not affected this code (and any additional code you may need to add).


我发现在使用Entity Framework和最新的1.82.0 SQLite时存在非常大的内存泄漏问题。您认为这是问题所在吗? - Peter
可能是因为(我认为,我在EF方面经验不足),与SQLiteCommand相关的非托管资源正在等待释放。这个SO线程这个票似乎证实了你的假设。为了避免泄漏,您可以尝试禁用EF中的“多个活动结果集”,或者尝试使用像C#-SQLite这样的托管库。希望这有所帮助。 - edymtt

29

我也遇到了同样的问题。我的情况是,在获取SQLite数据库文件中的数据后,我想删除该文件,但总是会抛出错误“…被其他进程使用”。即使我已经处理了SqliteConnection或SqliteCommand,错误仍然发生。我通过调用GC.Collect()修复了这个错误。

代码片段

public void DisposeSQLite()
{
    SQLiteConnection.Dispose();
    SQLiteCommand.Dispose();

    GC.Collect();
}

希望这有所帮助。


我也遇到了同样的问题,不得不使用GC.Collect();参考: http://system.data.sqlite.org/index.html/tktview/6434e23a0f63b440?plaintext希望下一个Sqlite版本能够解决这个问题。 - 27k1
7
处理你提到的问题的一种更简洁的方法是调用System.Data.SQLite.SQLiteConnection.ClearAllPools();而不是进行两次Dispose调用+GC.Collect。 - Aaron Hudon
2
在某些情况下,需要等待GC任务完成后才能使用带有GC.WaitForPendingFinalizers()的sqlite文件。 - Fiambre

15

以下方法对我有效:

MySQLiteConnection.Close();
SQLite.SQLiteConnection.ClearAllPools()

请注意,调用 ClearAllPools 会清除所有池。这可能会在多线程情况下引起(性能)问题。请参阅 https://dev59.com/y2cs5IYBdhLWcg3w1ndq#74960203 以清除一个特定的池。 - René Sackers

10

在我的情况下,我创建了SQLiteCommand对象但没有显式地将其释放。

var command = connection.CreateCommand();
command.CommandText = commandText;
value = command.ExecuteScalar();

我在using语句中包装了我的命令,这解决了我的问题。

static public class SqliteExtensions
{
    public static object ExecuteScalar(this SQLiteConnection connection, string commandText)
    {
        // Added using
        using (var command = connection.CreateCommand())
        {
            command.CommandText = commandText;
            return command.ExecuteScalar();
        }
    }
}

然后您可以像这样使用它

connection.ExecuteScalar(commandText);

3
我强烈建议不要忽略这样的异常情况! - Tom McKearney
这个程序是如何处理异常的?当我使用它时,如果命令失败,在 using 语句结束时会抛出一个异常。 - Miebster
我删除了吞噬异常的代码,因此它不再这样做。如果您真的想要吞噬异常,可以在使用周围的代码中添加try-catch块 - Nate

2

如前所述,SQLite对象必须被销毁。但是,有一个奇怪的行为:在调用Dispose命令时,连接必须保持打开状态。例如:

using(var connection = new SqliteConnection("source.db"))
{
    connection.Open();
    using(var command = connection.CreateCommand("select..."))
    {
        command.Execute...
    }
}

正常工作,但是:
using(var connection = new SqliteConnection("source.db"))
{
    connection.Open();
    using(var command = connection.CreateCommand("select..."))
    {
        command.Execute...
        connection.Close();
    }
}

给出相同的文件锁。

2
在大多数情况下,如果您没有正确处理读取器和命令,则会出现问题。有一种情况是命令和读取器无法正确处理。
场景1:如果您正在运行一个布尔函数,在达到结果之前,finally块中的代码不会执行。如果您在执行代码时评估函数isDataExists的结果,并根据结果来检查是否适合,那么这是一个大问题。
    if(isDataExists){
        // execute some code
    }

正在评估的函数。
    public bool isDataExists(string sql)
    {
        try
        {
            OpenConnection();
            SQLiteCommand cmd = new SQLiteCommand(sql, connection);
            reader = cmd.ExecuteReader();
            if (reader != null && reader.Read())
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        catch (Exception expMsg)
        {
            //Exception
        }
        finally
        {
            if (reader != null)
            {
                reader.Dispose();
            }
            CloseConnection();
        }
        return true;
    }

解决方案:请按照以下方式在try块内处理您的reader和command

            OpenConnection();
            SQLiteCommand cmd = new SQLiteCommand(sql, connection);
            reader = cmd.ExecuteReader();
            if (reader != null && reader.Read())
            {
                cmd.Dispose();
                CloseConnection();
                return true;
            }
            else
            {
                cmd.Dispose();
                CloseConnection();
                return false;
            }

最后,为防止出现问题,请处理读取器和命令。请注意保留HTML标记,但无需解释内容。
        finally
        {
            if (reader != null)
            {
                reader.Dispose();
            }
            CloseConnection();
        }

如果您在try块中处理命令,那么在ExecuteReader()期间发生的异常将导致该命令未被处理。您应该使用using块,或者如果您喜欢自己编写代码,可以嵌套多个try/finally块。 - C.Evenhuis

1
我发现edymtt的答案是正确的,他指责TableAdapters / Datasets,但是不需要修改每次重新生成的TableAdapter代码文件,我找到了另一种解决方案:手动调用TableAdapter的子元素上的.Dispose方法。(在.NET 4.5和最新的SQLite 1.0.86中)
using (var db = new testDataSet())
{
    using (testDataSetTableAdapters.UsersTableAdapter t = new testDataSetTableAdapters.UsersTableAdapter())
    {
        t.Fill(db.Users);
        //One of the following two is enough
        t.Connection.Dispose(); //THIS OR
        t.Adapter.Dispose();    //THIS LINE MAKES THE DB FREE
    }
    Console.WriteLine((from x in db.Users select x.Username).Count());
}

当我在设计器中更改数据集时,我不必修改TableAdapters - 我已经使用partial classes实现了Dispose。感谢指出在最新版本的System.Data.SQLite中,我的代码已不再必要(请参见更改日志,发布1.0.84)。 - edymtt

1

虽然这是一个老问题,但以下是在 .NET 7 中使用 EntityFrameworkCore 的一个示例。解决方法确实是使用 SqliteConnection.ClearPool(...) 来清除连接池。

我有一个 Web 服务,用户上传 SQLite 数据库文件,服务会下载该文件,并读取其中的一个表格,之后立即删除已下载的数据库文件。

var dbFilePath = "Database.db";
var connectionString = $"Data Source={dbFilePath};";

var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
optionsBuilder.UseSqlite(connectionString);
await using var context = new MyDbContext(optionsBuilder.Options);

var retrievedData = await context.MyTable.ToListAsync();
SqliteConnection.ClearPool((SqliteConnection) context.Database.GetDbConnection());
await context.DisposeAsync();

// File.Delete(dbFilePath); // <- works immediately, file is not locked

这是一个简单的输入输出示例。 它加载SQLite数据库,查询表,然后从连接池中清除(特定的)连接。与其他一些调用 ClearAllPools 的答案相反,这可能会在多线程情况下引起问题。

1

我曾经遇到同样的问题,只有在using语句中释放DbCommand才能解决问题,但是开启Pooling = true后,我的问题得到了解决!

                SQLiteConnectionStringBuilder builder = new SQLiteConnectionStringBuilder
                {
                    Pooling = true
                };

0

遇到了同样的问题。 通过使用NuGet安装SQLite 1.0.111来简单解决它。

我不需要更改任何代码,只需更新到那个版本即可。之前我使用的是1.0.85,在那个版本中文件被锁定,尽管连接已关闭和释放资源。


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