(4) 事务处理
sqlite 是支持事务处理的。如果你知道你要同步删除很多数据,不仿把它们做成一个统一的事务。@H_502_11@
通常一次 sqlite3_exec 就是一次事务,如果你要删除1万条数据,sqlite就做了1万次:开始新事务->删除一条数据->提交事务->开始新事务->… 的过程。这个操作是很慢的。因为时间都花在了开始事务、提交事务上。@H_502_11@
你可以把这些同类操作做成一个事务,这样如果操作错误,还能够回滚事务。@H_502_11@
事务的操作没有特别的接口函数,它就是一个普通的 sql 语句而已:@H_502_11@
分别如下:@H_502_11@
int result; @H_502_11@
result = sqlite3_exec( db,"begin transaction",&zErrorMsg ); //开始一个事务@H_502_11@
result = sqlite3_exec( db,"commit transaction",&zErrorMsg ); //提交事务@H_502_11@
result = sqlite3_exec( db,"rollback transaction",&zErrorMsg ); //回滚事务@H_502_11@
@H_502_11@
一、 给数据库加密
前面所说的内容网上已经有很多资料,虽然比较零散,但是花点时间也还是可以找到的。现在要说的这个——数据库加密,资料就很难找。也可能是我操作水平不够,找不到对应资料。但不管这样,我还是通过网上能找到的很有限的资料,探索出了给sqlite数据库加密的完整步骤。@H_502_11@
这里要提一下,虽然 sqlite 很好用,速度快、体积小巧。但是它保存的文件却是明文的。若不信可以用 NotePad 打开数据库文件瞧瞧,里面 insert 的内容几乎一览无余。这样赤裸裸的展现自己,可不是我们的初衷。当然,如果你在嵌入式系统、智能手机上使用 sqlite,最好是不加密,因为这些系统运算能力有限,你做为一个新功能提供者,不能把用户有限的运算能力全部花掉。@H_502_11@
sqlite为了速度而诞生。因此sqlite本身不对数据库加密,要知道,如果你选择标准AES算法加密,那么一定有接近50%的时间消耗在加解密算法上,甚至更多(性能主要取决于你算法编写水平以及你是否能使用cpu提供的底层运算能力,比如MMX或sse系列指令可以大幅度提升运算速度)。@H_502_11@
sqlite免费版本是不提供加密功能的,当然你也可以选择他们的收费版本,那你得支付2000块钱,而且是USD。我这里也不是说支付钱不好,如果只为了数据库加密就去支付2000块,我觉得划不来。因为下面我将要告诉你如何为免费的sqlite扩展出加密模块——自己动手扩展,这是sqlite允许,也是它提倡的。@H_502_11@
那么,就让我们一起开始为 sqlite3.c 文件扩展出加密模块。@H_502_11@
@H_502_11@
i.1 必要的宏@H_502_11@
通过阅读 sqlite 代码(当然没有全部阅读完,6万多行代码,没有一行是我习惯的风格,我可没那么多眼神去看),我搞清楚了两件事:@H_502_11@
需要 #define 一个宏才能使用加密扩展。@H_502_11@
这个宏就是 sqlITE_HAS_CODEC。@H_502_11@
你在代码最前面(也可以在 sqlite3.h 文件第一行)定义:@H_502_11@
#ifndef sqlITE_HAS_CODEC@H_502_11@
#define sqlITE_HAS_CODEC@H_502_11@
#endif@H_502_11@
@H_502_11@
如果你在代码里定义了此宏,但是还能够正常编译,那么应该是操作没有成功。因为你应该会被编译器提示有一些函数无法链接才对。如果你用的是 VC 2003,你可以在“解决方案”里右键点击你的工程,然后选“属性”,找到“C/C++”,再找到“命令行”,在里面手工添加“/D "sqlITE_HAS_CODEC"”。@H_502_11@
定义了这个宏,一些被 sqlite 故意屏蔽掉的代码就被使用了。这些代码就是加解密的接口。@H_502_11@
尝试编译,vc会提示你有一些函数无法链接,因为找不到他们的实现。@H_502_11@
如果你也用的是VC2003,那么会得到下面的提示:@H_502_11@
error LNK2019: 无法解析的外部符号 _sqlite3CodecGetKey ,该符号在函数 _attachFunc 中被引用@H_502_11@
error LNK2019: 无法解析的外部符号 _sqlite3CodecAttach ,该符号在函数 _attachFunc 中被引用@H_502_11@
error LNK2019: 无法解析的外部符号 _sqlite3_activate_see ,该符号在函数 _sqlite3Pragma 中被引用@H_502_11@
error LNK2019: 无法解析的外部符号 _sqlite3_key ,该符号在函数 _sqlite3Pragma 中被引用@H_502_11@
fatal error LNK1120: 4 个无法解析的外部命令@H_502_11@
@H_502_11@
这是正常的,因为sqlite只留了接口而已,并没有给出实现。@H_502_11@
下面就让我来实现这些接口。@H_502_11@
@H_502_11@
如果真要我从一份 www.sqlite.org 网上down下来的 sqlite3.c 文件,直接摸索出这些接口的实现,我认为我还没有这个能力。@H_502_11@
好在网上还有一些代码已经实现了这个功能。通过参照他们的代码以及不断编译中vc给出的错误提示,最终我把整个接口整理出来。@H_502_11@
实现这些预留接口不是那么容易,要重头说一次怎么回事很困难。我把代码都写好了,直接把他们按我下面的说明拷贝到 sqlite3.c 文件对应地方即可。我在下面也提供了sqlite3.c 文件,可以直接参考或取下来使用。@H_502_11@
@H_502_11@
这里要说一点的是,我另外新建了两个文件:crypt.c和crypt.h。@H_502_11@
其中crypt.h如此定义:@H_502_11@
#ifndef DCG_sqlITE_CRYPT_FUNC_@H_502_11@
#define DCG_sqlITE_CRYPT_FUNC_@H_502_11@
/***********@H_502_11@
董淳光写的 sqlITE 加密关键函数库@H_502_11@
***********/@H_502_11@
@H_502_11@
/***********@H_502_11@
***********/@H_502_11@
int My_Encrypt_Func( unsigned char * pData,unsigned int data_len,const char * key,unsigned int len_of_key );@H_502_11@
@H_502_11@
/***********@H_502_11@
***********/@H_502_11@
int My_DeEncrypt_Func( unsigned char * pData,unsigned int len_of_key );@H_502_11@
@H_502_11@
#endif@H_502_11@
@H_502_11@
@H_502_11@
其中的 crypt.c 如此定义:@H_502_11@
#include "./crypt.h"@H_502_11@
#include "memory.h"@H_502_11@
/***********@H_502_11@
***********/@H_502_11@
int My_Encrypt_Func( unsigned char * pData,unsigned int len_of_key )@H_502_11@
{@H_502_11@
return 0;@H_502_11@
}@H_502_11@
@H_502_11@
/***********@H_502_11@
***********/@H_502_11@
int My_DeEncrypt_Func( unsigned char * pData,unsigned int len_of_key )@H_502_11@
{@H_502_11@
return 0;@H_502_11@
}@H_502_11@
@H_502_11@
这个文件很容易看,就两函数,一个加密一个解密。传进来的参数分别是待处理的数据、数据长度、密钥、密钥长度。@H_502_11@
处理时直接把结果作用于 pData 指针指向的内容。@H_502_11@
你需要定义自己的加解密过程,就改动这两个函数,其它部分不用动。扩展起来很简单。@H_502_11@
这里有个特点,data_len 一般总是 1024 字节。正因为如此,你可以在你的算法里使用一些特定长度的加密算法,比如AES要求被加密数据一定是128位(16字节)长。这个1024不是碰巧,而是 sqlite 的页定义是1024字节,在sqlite3.c文件里有定义:@H_502_11@
# define sqlITE_DEFAULT_PAGE_SIZE 1024@H_502_11@
你可以改动这个值,不过还是建议没有必要不要去改它。@H_502_11@
@H_502_11@
上面写了两个扩展函数,如何把扩展函数跟 sqlite 挂接起来,这个过程说起来比较麻烦。我直接贴代码。@H_502_11@
分3个步骤。@H_502_11@
首先,在 sqlite3.c 文件顶部,添加下面内容:@H_502_11@
@H_502_11@
#ifdef sqlITE_HAS_CODEC@H_502_11@
#include "./crypt.h"@H_502_11@
/***********@H_502_11@
用于在 sqlite3 最后关闭时释放一些内存@H_502_11@
***********/@H_502_11@
void sqlite3pager_free_codecarg(void *pArg);@H_502_11@
#endif@H_502_11@
这个函数之所以要在 sqlite3.c 开头声明,是因为下面在 sqlite3.c 里面某些函数里要插入这个函数调用。所以要提前声明。@H_502_11@
@H_502_11@
其次,在sqlite3.c文件里搜索“sqlite3PagerClose”函数,要找到它的实现代码(而不是声明代码)。@H_502_11@
#ifdef sqlITE_ENABLE_MEMORY_MANAGEMENT@H_502_11@
/* A malloc() cannot fail in sqlite3ThreadData() as one or more calls to @H_502_11@
** malloc() must have already been made by this thread before it gets@H_502_11@
** to this point. This means the ThreadData must have been allocated already@H_502_11@
** so that ThreadData.nAlloc can be set.@H_502_11@
*/@H_502_11@
ThreadData *pTsd = sqlite3ThreadData();@H_502_11@
assert( pPager );@H_502_11@
assert( pTsd && pTsd->nAlloc );@H_502_11@
#endif@H_502_11@
@H_502_11@
需要在这部分后面紧接着插入:@H_502_11@
@H_502_11@
#ifdef sqlITE_HAS_CODEC@H_502_11@
sqlite3pager_free_codecarg(pPager->pCodecArg);@H_502_11@
#endif@H_502_11@
@H_502_11@
这里要注意,sqlite3PagerClose 函数大概也是 3.3.17版本左右才改名的,以前版本里是叫 “sqlite3pager_close”。因此你在老版本sqlite代码里搜索“sqlite3PagerClose”是搜不到的。@H_502_11@
类似的还有“sqlite3pager_get”、“sqlite3pager_unref”、“sqlite3pager_write”、“sqlite3pager_pagecount”等都是老版本函数,它们在 pager.h 文件里定义。新版本对应函数是在 sqlite3.h 里定义(因为都合并到 sqlite3.c和sqlite3.h两文件了)。所以,如果你在使用老版本的sqlite,先看看 pager.h 文件,这些函数不是消失了,也不是新蹦出来的,而是老版本函数改名得到的。@H_502_11@
@H_502_11@
最后,往sqlite3.c 文件下找。找到最后一行:@H_502_11@
@H_502_11@
/************** End of main.c ************************************************/@H_502_11@
@H_502_11@
这些代码很长,我不再解释,直接接上去就得了。@H_502_11@
唯一要提的是 DeriveKey 函数。这个函数是对密钥的扩展。比如,你要求密钥是128位,即是16字节,但是如果用户只输入 1个字节呢?2个字节呢?或输入50个字节呢?你得对密钥进行扩展,使之符合16字节的要求。@H_502_11@
DeriveKey 函数就是做这个扩展的。有人把接收到的密钥求md5,这也是一个办法,因为md5运算结果固定16字节,不论你有多少字符,最后就是16字节。这是md5算法的特点。但是我不想用md5,因为还得为它添加包含一些 md5 的.c或.cpp文件。我不想这么做。我自己写了一个算法来扩展密钥,很简单的算法。当然,你也可以使用你的扩展方法,也而可以使用 md5 算法。只要修改 DeriveKey 函数就可以了。@H_502_11@
在 DeriveKey 函数里,只管申请空间构造所需要的密钥,不需要释放,因为在另一个函数里有释放过程,而那个函数会在数据库关闭时被调用。参考我的 DeriveKey 函数来申请内存。@H_502_11@
@H_502_11@
这里我给出我已经修改好的 sqlite3.c 和 sqlite3.h 文件。@H_502_11@
如果太懒,就直接使用这两个文件,编译肯定能通过,运行也正常。当然,你必须按我前面提的,新建 crypt.h 和 crypt.c 文件,而且函数要按我前面定义的要求来做。@H_502_11@
@H_502_11@
@H_502_11@
你要把加密功能给用上,除了改 sqlite3.c 文件、给你工程添加 sqlITE_HAS_CODEC 宏,还得修改你的数据库调用函数。@H_502_11@
前面提到过,要开始一个数据库操作,必须先 sqlite3_open 。@H_502_11@
加解密过程就在 sqlite3_open 后面操作。@H_502_11@
假设你已经 sqlite3_open 成功了,紧接着写下面的代码:@H_502_11@
int i;@H_502_11@
i = sqlite3_key( db,"dcg",3 );@H_502_11@
i = sqlite3_rekey( db,0 );@H_502_11@
@H_502_11@
用 sqlite3_key 函数来提交密码。@H_502_11@
第1个参数是 sqlite3 * 类型变量,代表着用 sqlite3_open 打开的数据库(或新建数据库)。@H_502_11@
第2个参数是密钥。@H_502_11@
第3个参数是密钥长度。@H_502_11@
用 sqlite3_rekey 来修改密码。参数含义同 sqlite3_key。@H_502_11@
@H_502_11@
实际上,你可以在sqlite3_open函数之后,到 sqlite3_close 函数之前任意位置调用 sqlite3_key 来设置密码。@H_502_11@
但是如果你没有设置密码,而数据库之前是有密码的,那么你做任何操作都会得到一个返回值:sqlITE_NOTADB,并且得到错误提示:“file is encrypted or is not a database”。@H_502_11@
只有当你用 sqlite3_key 设置了正确的密码,数据库才会正常工作。@H_502_11@
如果你要修改密码,前提是你必须先 sqlite3_open 打开数据库成功,然后 sqlite3_key 设置密钥成功,之后才能用 sqlite3_rekey 来修改密码。@H_502_11@
如果数据库有密码,但你没有用 sqlite3_key 设置密码,那么当你尝试用 sqlite3_rekey 来修改密码时会得到 sqlITE_NOTADB 返回值。@H_502_11@
如果你需要清空密码,可以使用:@H_502_11@
i = sqlite3_rekey( db,NULL,0 );@H_502_11@
@H_502_11@
@H_502_11@
i.4 sqlite3.c 最后添加代码段@H_502_11@
@H_502_11@
@H_502_11@
/***@H_502_11@
***/@H_502_11@
#ifdef sqlITE_HAS_CODEC@H_502_11@
@H_502_11@
/***@H_502_11@
加密结构@H_502_11@
***/@H_502_11@
#define CRYPT_OFFSET 8@H_502_11@
typedef struct _CryptBlock@H_502_11@
{@H_502_11@
BYTE* ReadKey; // 读数据库和写入事务的密钥@H_502_11@
BYTE* WriteKey; // 写入数据库的密钥@H_502_11@
int PageSize; // 页的大小@H_502_11@
BYTE* Data;@H_502_11@
} CryptBlock,*LPCryptBlock;@H_502_11@
@H_502_11@
#ifndef DB_KEY_LENGTH_BYTE /*密钥长度*/@H_502_11@
#define DB_KEY_LENGTH_BYTE 16 /*密钥长度*/@H_502_11@
#endif@H_502_11@
@H_502_11@
#ifndef DB_KEY_PADDING /*密钥位数不足时补充的字符*/@H_502_11@
#define DB_KEY_PADDING 0x33 /*密钥位数不足时补充的字符*/@H_502_11@
#endif@H_502_11@