Tecky‘s Blog

你拍一、我拍一,喝着茅台吹牛逼
  首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

SQLite做为本地缓存的应用需要注意的地方

Posted on 2011-02-17 19:04  Tecky Li  阅读(9074)  评论(5编辑  收藏  举报

今天看到了园友陆敏计的一篇文章<<C#数据本地存储方案之SQLite>>, 写到了SQLite的诸多优点,尤其适应于本地数据缓存和应用程序。

转自陆兄的内容,来夸夸Sqlite:

SQLite官方网站: http://www.sqlite. org/ 时第一眼看到关于SQLite的特性。

1. ACID事务

2. 零配置 – 无需安装和管理配置

3. 储存在单一磁盘文件中的一个完整的数据库

4. 数据库文件可以在不同字节顺序的机器间自由的共享

5. 支持数据库大小至2TB

6. 足够小, 大致3万行C代码, 250K

7. 比一些流行的数据库在大部分普通数据库操作要快

8. 简单, 轻松的API

9. 包含TCL绑定, 同时通过Wrapper支持其他语言的绑定

10. 良好注释的源代码, 并且有着90%以上的测试覆盖率

11. 独立: 没有额外依赖

12. Source完全的Open, 你可以用于任何用途, 包括出售它

13. 支持多种开发语言,C, PHP, Perl, Java, ASP .NET,Python

正好前一段时间我做了这方面的应用,我就结合陆兄的这篇文章,谈谈我在Sqlite本地缓存业务数据时的经验,给大家借鉴一下。我开发时比较仓促,很多地方请大家多提意见。

解决的问题

首先介绍我用Sqlite解决的实际问题是什么?

问题1:某个功能的数据需要连接一个远程数据库查询速度很慢,查一次数据不容易,希望能够重复利用之前查过的数据集。

问题2:非常大的数据量比如几千万甚至几亿条数据,一次性读取到DataTable中,会内存溢出的,所以在第一次分析时就是通过Reader的方式,分析完一条后并不在内存中保存,但是紧接着用户的第二次分析、第三次分析还是要用到的第一次分析的数据,如果我们重新查询一次远程服务器,效率可想而知啊。

结合上面的2个问题,为了解决效率问题和数据重复利用度,减少数据库服务器的压力,我才用Sqlite缓存数据(当然这不是唯一也不是最好的解决方案) 。

优化SQLiteHelper

陆兄的SQLiteHelper类我增加了几个有用的方法:

第一个方法是GetSchema,得到某个表的表结构。

        /// <summary>   
        /// 查询数据库中的所有数据类型信息   
        /// </summary>   
        /// <returns></returns>   
        public DataTable GetSchema()
        {
            using (SQLiteConnection connection = new SQLiteConnection(connectionString))
            {
                connection.Open();
                DataTable data = connection.GetSchema("TABLES");
                connection.Close();
                //foreach (DataColumn column in data.Columns)   
                //{   
                //    Console.WriteLine(column.ColumnName);   
                //}   
                return data;
            }
        }

第二个方法是IsTableExist,判断SQLite数据库重某个表是否存在 。

        /// <summary>   
        /// 判断SQLite数据库表是否存在  
        /// </summary>   
        /// <param name="dbPath">要创建的SQLite数据库文件路径</param>   
        public bool IsTableExist(string tableName)
        {
            using (SQLiteConnection connection = new SQLiteConnection(connectionString))
            {
                connection.Open();
                using (SQLiteCommand command = new SQLiteCommand(connection))
                {

                    command.CommandText = "SELECT COUNT(*) FROM sqlite_master where type='table' and name='" + tableName + "'";
                    int iaaa = Convert.ToInt32(command.ExecuteScalar());
                    if (Convert.ToInt32(command.ExecuteScalar()) == 0)
                    {
                        return false;
                    }
                    else
                    {
                        return true;
                    }
                }
            }
        }

第三个方法是Query,执行查询语句,返回DataSet

        /// <summary>
        /// 执行查询语句,返回DataSet
        /// </summary>
        /// <param name="SQLString">查询语句</param>
        /// <returns>DataSet</returns>
        public DataSet Query(string SQLString)
        {
            using (SQLiteConnection connection = new SQLiteConnection(connectionString))
            {
                DataSet ds = new DataSet();
                try
                {
                    connection.Open();
                    SQLiteDataAdapter command = new SQLiteDataAdapter(SQLString, connection);
                    command.Fill(ds, "ds");
                }
                catch (System.Data.SQLite.SQLiteException ex)
                {
                    throw new Exception(ex.Message);
                }
                return ds;
            }
        }

构建缓存对象模型和缓存控制器

每一块缓存对象,在数据库中会产生一个表,而表名称是有缓存控制器自动生成的,访问缓存的工作全部交由缓存控制器完成,通过缓存项的ID和ModuleKey来访问。

在Sqlite中还需要一个系统表来维护每个缓存项和实际缓存存储表之间的对应关系,我们称之为配置表,它将在缓存控制器创建Sqlite缓存数据库文件时创建。

配置表共有以下几个字段,分别和缓存对象模型CdlCacheItem类映射:

列名称 说明
Id 缓存的唯一数字编号
ModuleKey 缓存模块名称,一个模块可以有多个缓存数据,ID可以区分。实际应用时,某个功能时会经常缓存数据的,所以通过ModuleKey就可以得到这个功能所有的缓存列表,然后选定其中的部分缓存来进行使用。
Comments 缓存说明
TableName 缓存数据存储的数据表名称
AddDate 缓存时间戳

创建数据库的方法如下:

        static void CreateDB()
        {
            //总共有ID、ModuleKey、Comments、AddDate这几列
            string sql = "CREATE TABLE SYSCDLTABLES(ID INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,MODULEKEY VARCHAR(200),COMMENTS VARCHAR(500),TABLENAME VARCHAR(100),ADDDATE DATETIME)";
            SQLiteDBHelper.CreateDB(CACHEFILEPATH, sql);
        }

每个缓存项(缓存对象模型)定义如下,和配置表对应:

    /// <summary>
    /// 缓存项对象
    /// </summary>
    /// <Author>Tecky Lee</Author>
    /// <Date>2011-1-11 15:11</Date>
    public class CdlCacheItem
    {
        int m_id;

        public int Id
        {
            get { return m_id; }
            set { m_id = value; }
        }
        string m_moduleKey;

        public string ModuleKey
        {
            get { return m_moduleKey; }
            set { m_moduleKey = value; }
        }
        string m_comments;

        public string Comments
        {
            get { return m_comments; }
            set { m_comments = value; }
        }
        string m_tableName;

        public string TableName
        {
            get { return m_tableName; }
            set { m_tableName = value; }
        }
        DateTime m_timestamp;

        public DateTime Timestamp
        {
            get { return m_timestamp; }
            set { m_timestamp = value; }
        }
    }

下面是控制器的接口定义:

public interface ICdlCacheController
    {
        void BeginLoadRow();
        void EndLoadRow();
        System.Collections.Generic.IList<CdlCacheItem> GetCdlCacheItems(string moduleKey);
        CdlCacheItem GetCdlCacheItems(int id);
        void LoadRow(System.Data.DataRow row, string tableName);
        void LoadRow(IEnumerable<object> row, string tableName);
        string LoadTable(System.Data.DataTable dt, string moduleKey, string comments);
        System.Data.Common.DbDataReader QueryCdlTableReader(CdlCacheItem item);
        System.Data.DataTable QueryCdlTables(CdlCacheItem item);
        System.Data.DataTable QueryCdlTables(string sql);
        void RemoveAllTables();
        void RemoveCdlTables(string moduleKey);
        void RemoveCdlTables(System.Collections.Generic.IList<CdlCacheItem> items);
        void RemoveCdlTables(CdlCacheItem item);
        void RemoveCdlTables(int id);
    }

上面的函数下面来做个说明:

1、BeginLoadRow、LoadRow和EndLoadRow,三个函数组为了在我们查询主数据库时使用Reader方式读取数据时,可以一条条将数据同时存放在缓存中。

2、RemoveAllTables和RemoveCdlTables是用来删除缓存项的。

3、GetCdlCacheItems,通过moduleKey得到多个缓存项。比如用户想基于这几天内保存的某个功能的数据做一次快速分析,那么我们就可以通过这个函数得到缓存列表,由用户选择列表中的一个来继续。

4、QueryCdlTableReader,得到某个缓存数据的Reader对象,这样可以一行行的分析,一次读出大数据量的数据到DataTable中,内存可能会溢出的。

5、QueryCdlTables,将某个缓存项查询并装载到DataTable中。

 

提高缓存数据写入效率

Sqlite在保存数据的时候,比如一次保存一个亿条的数据,一条条插入效率非常低下,网上也有人对其进行讨论。

效率低下的主要原因在于IO操作次数过于频繁,所以在LoadTable或者是使用BeginLoadRow·EndLoadRow的时候,使用了事务来减少数据提交的次数,结果保存的效率非常的高,我测试的结果是400万条数据查询,只需要几十秒钟,这点时间相对于重新查一次远程服务器那是可以忽略了。

下面给出BeginLoadRow和EndLoadRow的具体代码(只有在EndRow的时候才会提交一次数据):

        SQLiteConnection m_connection;
        SQLiteCommand m_command;
        DbTransaction m_transaction;
        public void BeginLoadRow()
        {
            m_connection = new SQLiteConnection("Data Source=" + CACHEFILEPATH);

            m_connection.Open();
            m_transaction = m_connection.BeginTransaction();
            m_command = new SQLiteCommand(m_connection);
        }
        public void EndLoadRow()
        {
            try
            {
                if (m_command != null)
                    m_command.Dispose();

                if (m_transaction != null)
                {
                    m_transaction.Commit();
                }

                if (m_connection != null)
                {
                    m_connection.Close();
                    m_connection.Dispose();
                }
            }
            catch (System.Exception ex)
            {
                LogHandle.Error(ex);
            }
        }

LoadTable函数内部也是调用BeginLoadRow·EndLoadRow模式来完成的。

 

数据库文件如何创建:

Sqlite数据库文件如果不存在,在执行sql语句的时候,会自动根据ConnetionString中指定的位置创建数据库文件,默认创建的空数据库只有4K。

其他有待讨论的问题:

1、我是将所有的缓存做到一个数据库文件中了,实际应用根据业务的不同,可以一份缓存数据一个文件也是很好管理的,维护也方便,资源管理器中就可以拷贝删除等。

2、当我们存储一亿条数据到Sqlite的时候,因为Sqlite没有压缩数据,结果数据库文件就可以会有好几个G(这也不一定,适合数据库字段的多少,字段类型有关的)。

文件太大就消耗了磁盘空间,而且用户或者程序如果不及时清理的,可能会耗尽磁盘空间。

这里就必须建立一个机制,检查sqlite的缓存并及时清理,或者设置缓存应用的上限,当达到上限后自动根据时间戳清理历史缓存。