zoukankan      html  css  js  c++  java
  • 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-pcl1.7.335
    SQLiteCommand SQLiteCmd = SQLiteConnection.CreateCommand(sqlStr);    //sqlite-net-pcl1.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

  • 相关阅读:
    调用微信扫一扫接口
    Http错误代码解释
    php goto的用法
    Yii2.0-生成二维码实例
    飞鹅WiFi打印机配置,php调用接口
    HTML5 为什么只需要写 <!DOCTYPE HTML>?
    VueJs之 v-bind
    react 组件导出
    前端代码规范
    git与github的文件推送
  • 原文地址:https://www.cnblogs.com/peterYong/p/14416849.html
Copyright © 2011-2022 走看看