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

今天看到了园友陆敏计的一篇文章<<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的缓存并及时清理,或者设置缓存应用的上限,当达到上限后自动根据时间戳清理历史缓存。

SQLite做为本地缓存的应用需要注意的地方的更多相关文章

  1. 使用SQLite做本地数据缓存的思考

    前言 在一个分布式缓存遍地都是的环境下,还讲本地缓存,感觉有点out了啊!可能大家看到标题,就没有想继续看下去的欲望了吧.但是,本地缓存的重要性也是有的! 本地缓存相比分布式缓存确实是比较out和比较 ...

  2. Xamarin android使用Sqlite做本地存储数据库

    android使用Sqlite做本地存储非常常见(打个比方就像是浏览器要做本地存储使用LocalStorage,貌似不是很恰当,大概就是这个意思). SQLite 是一个软件库,实现了自给自足的.无服 ...

  3. spring boot:使用spring cache+caffeine做进程内缓存(本地缓存)(spring boot 2.3.1)

    一,为什么要使用caffeine做本地缓存? 1,spring boot默认集成的进程内缓存在1.x时代是guava cache 在2.x时代更新成了caffeine, 功能上差别不大,但后者在性能上 ...

  4. HTML5权威指南--Web Storage,本地数据库,本地缓存API,Web Sockets API,Geolocation API(简要学习笔记二)

    1.Web Storage HTML5除了Canvas元素之外,还有一个非常重要的功能那就是客户端本地保存数据的Web Storage功能. 以前都是用cookies保存用户名等简单信息.   但是c ...

  5. Java8简单的本地缓存实现

    原文出处:lukaseder         Java8简单的本地缓存实现 这里我将会给大家演示用ConcurrentHashMap类和lambda表达式实现一个本地缓存.因为Map有一个新的方法,在 ...

  6. Java学习之ConcurrentHashMap实现一个本地缓存

    ConcurrentHashMap融合了Hashtable和HashMap二者的优势. Hashtable是做了线程同步,HashMap未考虑同步.所以HashMap在单线程下效率较高,Hashtab ...

  7. iOS五种本地缓存数据方式

    iOS五种本地缓存数据方式   iOS本地缓存数据方式有五种:前言 1.直接写文件方式:可以存储的对象有NSString.NSArray.NSDictionary.NSData.NSNumber,数据 ...

  8. OutputCache 如何使用本地缓存 【转】

    注意!ASP.NET MVC 3 的一个 OutputCache 问题   在用 ASP.NET MVC 3 重写博客园网站首页时,特地留意了一下这个缓存问题,通过这篇博文分享一下. 在 ASP.NE ...

  9. 第七章 企业项目开发--本地缓存guava cache

    1.在实际项目开发中,会使用到很多缓存技术,而且数据库的设计一般也会依赖于有缓存的情况下设计. 常用的缓存分两种:本地缓存和分布式缓存. 常用的本地缓存是guava cache,本章主要介绍guava ...

随机推荐

  1. android问题及其解决-优化listView卡顿和怎样禁用ListView的fling

    问题解决-优化listView卡顿和怎样禁用ListView的fling 前戏非常长,转载请保留出处:http://blog.csdn.net/u012123160/article/details/4 ...

  2. 硬件——nrf51822第二篇,如何设置keil用来下载程序

    转自电子发烧友论坛 未完,待续...... 这里就是根据自己的项目了,并不一定是按照下面的图片去做.

  3. JS学习笔记 - 自定义右键菜单、文本框只能输入数字

    <script> // 事件总共有2个部分, //1.点击鼠标右键的表现 oncontextmenu 2.点击鼠标左键的表现(即普通点击onclick) // 点击右键,div位置定位到鼠 ...

  4. Web--CSS控制页面(link与import方式差别)

        先了解: [1]         "Table"和"DIV"这两个网页元素诞生的目的不同,首先Table诞生的目的是为了存储数据,而DIV诞生的目的就是 ...

  5. 10.8 android输入系统_实战_使用GlobalKey一键启动程序

    11. 实战_使用GlobalKey一键启动程序参考文章:Android 两种注册(动态注册和静态注册).发送广播的区别http://www.jianshu.com/p/ea5e233d9f43 [A ...

  6. 8.6 Android灯光系统_源码分析_背光灯

    Change system screen brightness, using android.provider.Settings.System.SCREEN_BRIGHTNESSandroid-er. ...

  7. 谁要的手机用KRKR2 Onscripter 资源打包工具

    本软件能够把你手机上指定文件夹打包为文字冒险游戏资源文件 支持打包 1.Onscripter 的NSA格式 2.吉里吉里2(KRKR2)的XP3.(分2.29曾经的旧版本号和2.30以后新版本号) 3 ...

  8. 【23.39%】【codeforces 558C】Amr and Chemistry

    time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...

  9. 使用DatagramSocket与DatagramPacket传输数据 分类: B1_JAVA 2013-10-12 13:00 1936人阅读 评论(0) 收藏

    参考传智播客毕向东JAVA视频. 注: DatagramSocket发送的每个包都需要指定地址,而Socket则是在首次创建时指定地址,以后所有数据的发送都通过此socket. A socket is ...

  10. 【Bash百宝箱】Linux shell学习

    shell特点-- Linux有多种shell能够使用,默认的为bash,bash有以下几个主要特点. 1.命令记忆能力 在命令行中按上下键能够找到一个前/后输入的命令.这些命令记录在-/.bash_ ...