多线程应用程序中的SQLite事务

时间:2014-12-18 16:56:16

标签: c# multithreading sqlite transactions

我做了一些搜索,我知道为了从同一个应用程序中的不同线程更新一个SQLite数据库文件,线程必须共享相同的SQLiteConnection对象。我已经在我的应用程序中采取措施来实现这一目标。

我的问题与交易有关。我需要每个线程在一个事务中执行其更新,并且我需要将每个事务与其他事务完全隔离。也就是说,在线程A中回滚或提交事务应该对线程B正在进行的工作没有影响。

我是否必须做一些特别的事情来实现这一点,或者在每个线程的操作开始时创建一个SQLiteTransaction并将其分配给SQLiteCommand's Transaction属性呢?

2 个答案:

答案 0 :(得分:3)

在玩完这个之后,我发现我之前关于需要跨线程共享单个连接的结论是错误的。每个线程现在:

  1. 使用名为ConnectionManager的特殊单例类创建SQLiteConnection个实例,打开它们,并配置每个连接打开后必须设置的常用设置。
  2. 在通过调用ConnectionManager类中的方法访问数据库之前创建新连接。
  3. 以通常的方式开始和管理任何交易。
  4. 以通常的方式关闭和处置SQLiteConnection
  5. ConnectionManager类允许所有需要连接到数据库的代码获取其连接,并以一致的方式设置所有数据库设置。我的代码需要设置的大多数属性实际上都在连接字符串中,但是有一个在那里无法指定,所以我需要另一种机制。这是ConnectionManager的样子:

    public class ConnectionManager {
    
        public int BusyTimeout { get; set; }
    
        public static ConnectionManager Instance {
            get {
                if ( iInstance == null ) {
                    lock ( instanceLock ) {
                        if ( iInstance == null )
                            iInstance = new ConnectionManager();
                    }
                }
                return iInstance;
            }
        }
        private static ConnectionManager iInstance = null;
    
        private static object instanceLock;
    
        private ConnectionManager() {
            BusyTimeout = Convert.ToInt32( TimeSpan.FromMinutes( 2 ).TotalMilliseconds );
        }
    
        static ConnectionManager() {
            instanceLock = new object();
        }
    
        public SQLiteConnection CreateConnection( string connectionString ) {
            SQLiteConnection connection = new SQLiteConnection( connectionString );
            connection.Open();
    
            using ( SQLiteCommand command = connection.CreateCommand() ) {
               command.CommandText = string.Format( "PRAGMA busy_timeout={0}", BusyTimeout );
               command.ExecuteNonQuery();
            }
            return connection;
        }
    }
    

    要使用ConnectionManager类,可以将实例变量或局部变量设置为单例实例的副本,如下所示:

    _connectionManager = ConnectionManager.Instance;
    

    要获取和使用数据库连接,每个线程都使用这样的代码:

    using ( SQLiteConnection connetion = _connectionManager.CreateConnection( connectionString ) {
        // Thread's database operation code here
    }
    

    事实证明,让它工作的真正技巧是将busy_timeout pragma设置为比默认值更长的值。 SQLite内部完全是线程安全的并且自己序列化请求,因此您的代码只需告诉SQLite等待任何当前正在执行的操作完成。我们的代码已经过结构化,因此数据库引擎中的任何故障都会导致在等待几秒后重试操作,因此这很有效。

    默认的2分钟等待时间足以让99.99%的所有操作完成。实际上,如果事情需要超过2分钟才能完成,我们需要重新审视该区域,并以任何方式加快速度。

答案 1 :(得分:0)

我可能会弄错,但是对于多线程应用程序,我认为您一次只需要与数据库建立一个连接。我看不到您的代码如何阻止2个线程试图同时连接到同一数据库。如果有2个线程尝试同时获取连接,则当您尝试打开另一个线程使用的连接时,最后一个尝试获取连接的线程将获得SQLITE_BUSY响应。您想像这样锁定连接的创建:

public class ConnectionManager
{

    public static int BusyTimeout { get; set; }

    public static object instanceLock;

    static ConnectionManager()
    {
        instanceLock = new object();
        BusyTimeout = Convert.ToInt32(TimeSpan.FromMinutes(2).TotalMilliseconds);
    }

    public static SQLiteConnection CreateConnection(string connectionString)
    {
        SQLiteConnection connection = new SQLiteConnection(connectionString);
        connection.Open();

        using (SQLiteCommand command = connection.CreateCommand())
        {
            command.CommandText = string.Format("PRAGMA busy_timeout={0}", BusyTimeout);
            command.ExecuteNonQuery();
        }
        return connection;
    }
}

这是使用连接管理器的数据访问对象的示例。

class UserDAO
{


    public void UpdateUser(User user)
    {
        int result = -1;
        string connectionString = "Data Source=C:\\Counter\\Counter.sqlite";
        lock (ConnectionManager.instanceLock)
        {
            using (SQLiteConnection conn = ConnectionManager.CreateConnection(connectionString))
            {
                using (SQLiteCommand cmd = new SQLiteCommand(conn))
                {
                    cmd.CommandText = "UPDATE counter_user " +
                        "SET runnerfirstname=@runnerfirstname, " +
                        "runnerlastname=@runnerlastname, " +
                        "parentlastname=@parentlastname, " +
                        "parentfirstname=@parentfirstname, " +
                        "runnergrade=@runnergrade, " +
                        "email=@email, " +
                        "laps=@laps, " +
                        "vestnumber=@vestnumber, " +
                        "tagid=@tagid " +
                        "WHERE id=@id";

                    cmd.Prepare();
                    cmd.Parameters.AddWithValue("@runnerfirstname", user.RunnerFirstName);
                    cmd.Parameters.AddWithValue("@runnerlastname", user.RunnerLastName);
                    cmd.Parameters.AddWithValue("@parentlastname", user.ParentLastName);
                    cmd.Parameters.AddWithValue("@parentfirstname", user.ParentFirstName);
                    cmd.Parameters.AddWithValue("@runnergrade", user.RunnerGrade);
                    cmd.Parameters.AddWithValue("@email", user.Email);
                    cmd.Parameters.AddWithValue("@laps", user.Laps);
                    cmd.Parameters.AddWithValue("@vestnumber", user.VestNumber);
                    cmd.Parameters.AddWithValue("@tagid", user.TagId);
                    cmd.Parameters.AddWithValue("@id", user.Id);

                    try
                    {
                        result = cmd.ExecuteNonQuery();
                    }
                    catch (SQLiteException e)
                    {
                        Console.WriteLine("test");
                    }
                }
                conn.Close();
            }
        }
    }