SQLite使用中的幾個問題


一、Sqlite刪除記錄后數據庫文件大小不變

原因是:
sqlite采用的是變長紀錄存儲,當你從Sqlite刪除數據后,未使用的磁盤空間被添加到一個內在的”空閑列表”中用於存儲你下次插入的數據,用於提高效率,磁盤空間並沒有丟失,但也不向操作系統返回磁盤空間,這就導致刪除數據乃至清空整個數據庫后,數據文件大小還是沒有任何變化,還是很大。

解決方法:兩種

1、手動:

在數據刪除后,手動執行VACUUM命令,執行方式很簡單

sqlite> vacuum;

在Navicat中可以直接執行,eg:

DELETE FROM Company WHERE Age < 190;VACUUM;  

但是此語句在C#代碼中執行不生效,可以查看下文的方法。

2、自動:

在數據庫文件建成中,將auto_vacuum設置成“1”。

參考:

解決sqlite 刪除記錄后數據庫文件大小不變

壓縮Sqlite數據文件大小,解決數據刪除后占用空間不變的問題

擴展:C# 中如何解決?

方法也是兩種:

1、手動釋放空間

先寫一個執行sql語句的函數:

private void ExecuteSql(string sDbPath, string sqlStr)
        {
            using (SQLiteConnection conn = new SQLiteConnection("data source = " + sDbPath))
            {
                using (SQLiteCommand cmd = new SQLiteCommand())
                {
                    cmd.Connection = conn;

                    conn.Open();
                    cmd.CommandText = sqlStr;
                    cmd.ExecuteNonQuery();

                    conn.Close();
                }
            }
        }

在刪除數據表/大量數據后,調用上述函數(dbPath為數據庫的地址)。

ExecuteSql(dbPath, "VACUUM");

參考代碼:SQLiteRepository.cs 

以上代碼視sqlite-net.dll 的版本而改變,

SQLiteCommand cmd = new SQLiteCommand(SQLiteConnection);              //sqlite-net-pcl\1.7.335
SQLiteCommand SQLiteCmd = SQLiteConnection.CreateCommand(sqlStr);    //sqlite-net-pcl\1.4.118

 

2、設置數據庫為自動釋放空間

當數據庫中無數據表時,設置其屬性:

ExecuteSql(dbPath, "PRAGMA auto_vacuum = 1;");

測試了一下發現效果也挺好的。

比較兩種方法,自動更方便。但是需要注意的是,在進行頻繁的插入、更新、刪除操作時,數據庫會產生大量的內存碎片。自動釋放空間的方法只能釋放空閑數據頁,但是並不會對內存碎片進行整理,在這個過程中反而會產生額外的內存碎片;

而手動方式可以同時釋放空閑空間和整理內存碎片。

推薦使用第一種方式。

參考:C# 壓縮 SQLite 數據庫

 

二、Sqlite執行效率優化

參考:C# SQLite執行效率的優化教程

實踐:

/// <summary>
    /// 執行時間測試
    /// </summary>
    public class ExecuteTimeHelper
    {
        public static void Test()
        {
            SQLiteConnection connection = Run(() => new SQLiteConnection("Test.db"), "連接對象初始化並打開連接");
            Run(() => connection.CreateTable<Info>(), "創建數據表Info");

            SQLiteCommand command = Run(() => new SQLiteCommand(connection), "命令對象初始化");

            Run(() =>
            {
                command.CommandText = $"DELETE FROM Info;VACUUM;UPDATE sqlite_sequence SET seq ='0' where name ='Info';";
                command.ExecuteNonQuery();
            }, "執行DELETE命令及收縮數據庫");

            int count = 200;
            Run(() =>
            {
                for (int i = 0; i < count; i++)
                {
                    command.CommandText = $"INSERT INTO Info(Name, Age) VALUES ('A{i:000}','{i}')";
                    command.ExecuteNonQuery(); //將返回操作所影響的記錄條數
                }
                command.ExecuteScalar<object>();
            }, $"[---不使用事務---]事務執行INSERT命令,插入{count}條數據");
            Run(() =>
            {
                command.CommandText = $"DELETE FROM Info;VACUUM;UPDATE sqlite_sequence SET seq ='0' where name ='Info';";
                command.ExecuteNonQuery();
            }, "執行DELETE命令及收縮數據庫");


            Run(() => connection.BeginTransaction(), "開始事務");
            int count2 = 3000;
            Run(() =>
            {
                for (int i = 0; i < count2; i++)
                {
                    command.CommandText = $"INSERT INTO Info(Name, Age) VALUES ('A{i:000}','{i}')";
                    command.ExecuteNonQuery();
                }
                var result = command.ExecuteScalar<object>();
            }, $"[---使用事務---]執行INSERT命令,插入{count2}條數據");
            Run(() => connection.Commit(), "提交事務");

            //或者用事務方法 執行批量sql語句
            connection.RunInTransaction(() =>
            {
                int count2 = 3000;
                Run(() =>
                {
                    for (int i = 0; i < count2; i++)
                    {
                        command.CommandText = $"INSERT INTO Info(Name, Age) VALUES ('A{i:000}','{i}')";
                        command.ExecuteNonQuery();
                    }
                    var result = command.ExecuteScalar<object>();
                }, $"[---使用事務---]執行INSERT命令,插入{count2}條數據");
            });

            Run(() => connection.Close(), "關閉連接");
            Console.ReadKey();
        }

        public static void Run(Action action, string description)
        {
            Stopwatch sw = Stopwatch.StartNew();
            action();
            Console.WriteLine($"--> {description}: {sw.ElapsedMilliseconds}ms");
        }

        public static T Run<T>(Func<T> func, string description)
        {
            Stopwatch sw = Stopwatch.StartNew();
            T result = func();
            Console.WriteLine($"--> {description}: {sw.ElapsedMilliseconds}ms");
            return result;
        }
    }

    class Info
    {
        public long ID { set; get; }
        public string Name { set; get; }
        public long Age { set; get; }
    }
View Code

  •  無論是執行插入或查詢操作,使用事務比不使用事務快,尤其是在批量插入操作時,減少得時間非常明顯;比如在不使用事務的情況下插入3000條記錄,執行所花費的時間為17.252s,而使用事務,執行時間只用了0.057s,效果非常明顯,而SQL Server不存在這樣的問題。
  • 不能每次執行一條SQL語句前開始事務並在SQL語句執行之后提交事務,這樣的執行效率同樣是很慢,最好的情況下,是在開始事務后批量執行SQL語句,再提交事務,這樣的效率是最高的。

即使只插入一條數據也是有很大差異的:

 

三、SQLite.SQLiteException異常: database is locked、Busy

這就涉及到sqlite的並發讀寫問題:

1.sqlite3支持多線程同時讀操作,但不支持多線程同時寫操作。

2.同一時刻只能有一個線程去進行寫操作,並且在一個線程進行寫操作的時候,其他線程是不能進行讀操作的。

  當一個線程正在寫操作時,其他線程的讀寫都會返回操作失敗的錯誤,顯示數據庫文件被鎖住

1、sqlite3的鎖及事務類型

sqlite3總共有三種事務類型:BEGIN [DEFERRED /IMMEDIATE / EXCLUSIVE] TRANSCATION,
五種鎖,按鎖的級別依次是:UNLOCKED /SHARED /RESERVERD /PENDING /EXCLUSIVE。
  • UNLOCKED ,無鎖狀態。數據庫文件沒有被加鎖。
  • SHARED 共享狀態。數據庫文件被加了共享鎖。可以多線程執行讀操作,但不能進行寫操作。
  • RESERVED 保留狀態。數據庫文件被加保留鎖。表示數據庫將要進行寫操作。
  • PENDING 未決狀態。表示即將寫入數據庫,正在等待其他讀線程釋放 SHARED 鎖。一旦某個線程持有 PENDING 鎖,其他線程就不能獲取 SHARED 鎖。這樣一來,只要等所有讀線程完成,釋放 SHARED 鎖后,它就可以進入 EXCLUSIVE 狀態了。
  • EXCLUSIVE 獨占鎖。表示它可以寫入數據庫了。進入這個狀態后,其他任何線程都不能訪問數據庫文件。因此為了並發性,它的持有時間越短越好

說明:

  • 當執行select即讀操作時,需要獲取到SHARED鎖(共享鎖),
  • 當執行insert/update/delete操作(即內存寫操作時),需要進一步獲取到RESERVERD鎖(保留鎖),
  • 當進行commit操作(即磁盤寫操作時),需要進一步獲取到EXCLUSIVE鎖(排它鎖)。
  1. 對於RESERVERD鎖,sqlite3保證同一時間只有一個連接可以獲取到保留鎖,也就是同一時間只有一個連接可以寫數據庫(內存),但是其它連接仍然可以獲取SHARED鎖,也就是其它連接仍然可以進行讀操作(這里可以認為寫操作只是對磁盤數據的一份內存拷貝進行修改,並不影響讀操作)。
  2. 對於EXCLUSIVE鎖,是比保留鎖更為嚴格的一種鎖,在需要把修改寫入磁盤即commit時需要在保留鎖/未決鎖的基礎上進一步獲取到排他鎖,顧名思義,排他鎖排斥任何其它類型的鎖,即使是SHARED鎖也不行,所以,在一個連接進行commit時,其它連接是不能做任何操作的(包括讀)。
  3. PENDING鎖(即未決鎖),則是比較特殊的一種鎖,它可以允許已獲取到SHARED鎖的事務繼續進行,但不允許其它連接再獲取SHARED鎖,當已存在的SHARED鎖都被釋放后(事務執行完成),持有未決鎖的事務就可以獲得commit的機會了。sqlite3使用這種鎖來防止writer starvation(寫餓死)。

sqlite3只支持庫級鎖,庫級鎖意味着什么?

意味着同時只能允許一個寫操作,也就是說,即事務T1在A表插入一條數據,事務T2在B表中插入一條數據,這兩個操作不能同時進行,即使你的機器有100個CPU,也無法同時進行,而只能順序進行。表級都不能並行,更別說元組級了——這就是庫級鎖。

但是,SQLite盡量延遲申請X鎖,直到數據塊真正寫盤時才申請X鎖,這是非常巧妙而有效的。

2、死鎖的情況

死鎖的情況:
當兩個連接使用begin transaction開始事務時,第一個連接執行了一次select操作(已經獲取到SHARED鎖),第二個連接執行了一次insert操作(已經獲取到了RESERVERD鎖)。
此時第一個連接需要進行一次insert/update/delete(需要獲取到RESERVERD鎖),第二個連接則希望執行commit(需要獲取到EXCLUSIVE鎖)。
由於第二個連接已經獲取到了RESERVERD鎖,根據RESERVERD鎖同一時間只有一個連接可以獲取的特性,第一個連接獲取RESERVERD鎖的操作必定失敗;
而由於第一個連接已經獲取到SHARED鎖,第二個連接希望進一步獲取到EXCLUSIVE鎖的操作也必定失敗【因為排他鎖 排斥任何其他類型的鎖】。這就導致2個連接都在等待對方釋放鎖,於是就出現了事務死鎖。
 
要解決這個問題,就必須了解事務: 事務類型的使用原則
在用”begin transaction”顯式開啟一個事務時,默認的事務類型為DEFERRED,鎖的狀態為UNLOCKED,即不獲取任何鎖,如果在使用的數據庫沒有其它的連接,用begin就可以了。
如果有多個連接都需要對數據庫進行寫操作,那就得 使用BEGIN IMMEDIATE/EXCLUSIVE開始事務【解決死鎖的問題】。
使用事務的好處是:
1.一個事務的所有操作相當於一次原子操作,如果其中某一步失敗,可以通過回滾來撤銷之前所有的操作,只有當所有操作都成功時,才進行commit,保證了操作的原子特性;
2.對於多次的數據庫操作,如果我們希望提高數據查詢或更新的速度,可以在開始操作前顯式開啟一個事務,在執行完所有操作后,再通過一次commit來提交所有的修改或結束事務。
 
更多參考:

3、對SQLITE_BUSY的處理

當有多個連接同時對數據庫進行寫操作時,根據事務類型的使用原則,我們在每個連接中用BEGIN IMMEDIATE開始事務,即多個連接都嘗試取得保留鎖的情況,根據保留鎖同一時間只有一個連接可以獲取到的特性,其它連接都將獲取失敗,即事務開始失敗,這種情況下,sqlite3將返回一個SQLITE_BUSY的錯誤。
如果我們不希望操作就此失敗而返回,就必須處理SQLITE_BUSY的情況,讓其重試,sqlite3提供了sqlite3_busy_handler或sqlite3_busy_timeout來處理SQLITE_BUSY。
對於sqlite3_busy_handler,我們可以指定一個busy_handler來處理,並可以指定失敗重試的次數。
而sqlite3_busy_timeout則是由sqlite3自動進行sleep並重試,當sleep的累積時間超過指定的超時時間時,最終返回SQLITE_BUSY。
需要注意的是, 這兩個函數同時只能使用一個,后面的調用會覆蓋掉前次調用。從使用上來說,sqlite3_busy_timeout更易用一些,只需要指定一個總的超時時間,然后sqlite自己會決定多久進行重試以及重試的次數,直到達到總的超時時間最終返回SQLITE_BUSY。並且,這兩個函數一經調用,對其后的所有數據庫操作都有效,非常方便。

參考:SQLite 線程安全和並發

更多:Sqlite WAL原理

How to open SQLite connection in WAL mode

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM