1、SQLite介绍自几十年前出现的商业应用程序以来,数据库就成为软件应用程序的主要组成部分。正与数据库管理系统非常关键一样,它们也变得非常庞大,并占用了相当多的系统资源,增加了管理的复杂性。随着软件应用程序逐渐模块模块化,一种新型数据库会比大型复杂的传统数据库管理系统更适应。嵌入式数据库直接在应用程序进程中运行,提供了零配置(zero-configuration)运行模式,并且资源占用非常少。SQLite是一个开源的嵌入式关系数据库,它在2000年由D.RichardHipp发布,它的减少应用程序管理数据的开销,SQLite可移植性好,很容易使用,很小,高效而且可靠。SQLite嵌入到使用它的应用程序中,它们共用相同的进程空间,而不是单独的一个进程。从外部看,它并不像一个RDBMS,但在进程内部,它却是完整的,自包含的数据库引擎。嵌入式数据库的一大好处就是在你的程序内部不需要网络配置,也不需要管理。因为客户端和服务器在同一进程空间运行。SQLite的数据库权限只依赖于文件系统,没有用户帐户的概念。SQLite有数据库级锁定,没有网络服务器。它需要的内存,其它开销很小,适合用于嵌入式设备。你需要做的仅仅是把它正确的编译到你的程序。2、架构(architecture)SQLite采用了模块的设计,它由三个子系统,包括8个独立的模块构成。2.1、接口(Interface)接口由SQLiteCAPI组成,也就是说不管是程序、脚本语言还是库文件,最终都是通过它与SQLite交互的(我们通常用得较多的ODBC/JDBC最后也会转化为相应CAPI的调用)。2.2、编译器(Compiler)在编译器中,分词器(Tokenizer)和分析器(Parser)对SQL进行语法检查,然后把它转化为底层能更方便处理的分层的数据结构---语法树,然后把语法树传给代码生成器(codegenerator)进行处理。而代码生成器根据它生成一种针对SQLite的汇编代码,最后由虚拟机(VirtualMachine)执行。2.3、虚拟机(VirtualMachine)架构中最核心的部分是虚拟机,或者叫做虚拟数据库引擎(VirtualDatabaseEngine,VDBE)。它和Java虚拟机相似,解释执行字节代码。VDBE的字节代码由128个操作码(opcodes)构成,它们主要集中在数据库操作。它的每一条指令都用来完成特定的数据库操作(比如打开一个表的游标)或者为这些操作栈空间的准备(比如压入参数)。总之,所有的这些指令都是为了满足SQL命令的要求(关于VM,后面会做详细介绍)。2.4、后端(Back-End)后端由B-树(B-tree),页缓存(pagecache,pager)和操作系统接口(即系统调用)构成。B-tree和pagecache共同对数据进行管理。B-tree的主要功能就是索引,它维护着各个页面之间的复杂的关系,便于快速找到所需数据。而pager的主要作用就是通过OS接口在B-tree和Disk之间传递页面。3、SQLite的特点(SQLite’sFeaturesandPhilosophy)3.1、零配置(ZeroConfiguration)3.2、可移植(Portability):它是运行在Windows,Linux,BSD,MacOSX和一些商用Unix系统,比如Sun的Solaris,IBM的AIX,同样,它也可以工作在许多嵌入式操作系统下,比如QNX,VxWorks,PalmOS,Symbin和WindowsCE。3.3、Compactness:SQLite是被设计成轻量级,自包含的。oneheaderfile,onelibrary,andyou’rerelational,noexternaldatabaseserverrequired3.4、简单(Simplicity)3.5、灵活(Flexibility)3.6、可靠(Reliability):SQLite的核心大约有3万行标准C代码,这些代码都是模块化的,很容易阅读。主要参考:TheDefinitiveGuidetoSQLite第二节设计与概念
1、API由两部分组成:核心API(coreAPI)和扩展API(extensionAPI)核心API的函数实现基本的数据库操作:连接数据库,处理SQL,遍历结果集。它也包括一些实用函数,比如字符串转换,操作控制,调试和错误处理。扩展API通过创建你自定义的SQL函数去扩展SQLite。1.1、SQLiteVersion3的一些新特点:(1)SQLite的API全部重新设计,由第二版的15个函数增加到88个函数。这些函数包括支持UTF-8和UTF-16编码的功能函数。(2)改进并发性能。加锁子系统引进一种锁升级模型(lockescalationmodel),解决了第二版的写进程饿死的问题(该问题是任何一个DBMS必须面对的问题)。这种模型保证写进程按照先来先服务的算法得到排斥锁(ExclusiveLock)。甚至,写进程通过把结果写入临时缓冲区(TemporaryBuffer),可以在得到排斥锁之前就能开始工作。这对于写要求较高的应用,性能可提高400%(引自参考文献)。(3)改进的B-树。对于表采用B+树,大大提高查询效率。(4)SQLite3最重要的改变是它的存储模型。由第二版只支持文本模型,扩展到支持5种本地数据类型。总之,SQLiteVersion3与SQLiteVertion2有很大的不同,在灵活性,特点和性能方面有很大的改进。1.2、主要的数据结构(ThePrincipalDataStructures)SQLite由很多部分组成-parser,tokenize,virtualmachine等等。但是从程序员的角度,最需要知道的是:connection,statements,B-tree和pager。它们之间的关系如下:上图告诉我们在编程需要知道的三个主要方面:API,事务(Transaction)和锁(Locks)。从技术上来说,B-tree和pager不是API的一部分。但是它们却在事务和锁上起着关键作用(稍后将讨论)。1.3、Connections和StatementsConnection和statement是执行SQL命令涉及的两个主要数据结构,几乎所有通过API进行的操作都要用到它们。一个连接(Connection)代表在一个独立的事务环境下的一个连接A(connectionrepresentsasingleconnectiontoadatabaseaswellasasingletransactioncontext)。每一个statement都和一个connection关联,它通常表示一个编译过的SQL语句,在内部,它以VDBE字节码表示。Statement包括执行一个命令所需要一切,包括保存VDBE程序执行状态所需的资源,指向硬盘记录的B-树游标,以及参数等等。1.4、B-tree和pager一个connection可以有多个database对象---一个主要的数据库以及附加的数据库,每一个数据库对象有一个B-tree对象,一个B-tree有一个pager对象(这里的对象不是面向对象的“对象”,只是为了说清楚问题)。Statement最终都是通过connection的B-tree和pager从数据库读或者写数据,通过B-tree的游标(cursor)遍历存储在页面(page)中的记录。游标在访问页面之前要把数所从disk加载到内存,而这就是pager的任务。任何时候,如果B-tree需要页面,它都会请求pager从disk读取数据,然后把页面(page)加载到页面缓冲区(pagecache),之后,B-tree和与之关联的游标就可以访问位于page中的记录了。如果cursor改变了page,为了防止事务回滚,pager必须采取特殊的方式保存原来的page。总的来说,pager负责读写数据库,管理内存缓存和页面(page),以及管理事务,锁和崩溃恢复(这些在事务一节会详细介绍)。总之,关于connection和transaction,你必须知道两件事:(1)对数据库的任何操作,一个连接存在于一个事务下。(2)一个连接决不会同时存在多个事务下。wheneveraconnectiondoesanythingwithadatabase,italwaysoperatesunderexactlyonetransaction,nomore,noless.1.5、核心API核心API主要与执行SQL命令有关,本质上有两种方法执行SQL语句:preparedquery和wrappedquery。Preparedquery由三个阶段构成:preparation,execution和finalization。其实wrappedquery只是对preparedquery的三个过程包装而已,最终也会转化为preparedquery的执行。1.5.1、连接的生命周期(TheConnectionLifecycle)和大多数据库连接相同,由三个过程构成:(1)连接数据库(Connecttothedatabase):每一个SQLite数据库都存储在单独的操作系统文件中,连接,打开数据库的CAPI为:sqlite3_open(),它的实现位于main.c文件中,如下:intsqlite3_open(constchar*zFilename,sqlite3**ppDb){returnopenDatabase(zFilename,ppDb,SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE,0);}当连接一个在磁盘上的数据库,如果数据库文件存在,SQLite打开一个文件;如果不存在,SQLite会假定你想创建一个新的数据库。在这种情况下,SQLite不会立即在磁盘上创建一个文件,只有当你向数据库写入数据时才会创建文件,比如:创建表、视图或者其它数据库对象。如果你打开一个数据,不做任何事,然后关闭它,SQLite会创建一个文件,只是一个空文件而已。另外一个不立即创建一个新文件的原因是,一些数据库的参数,比如:编码,页面大小等,只在在数据库创建前设置。默认情况下,页面大小为1024字节,但是你可以选择512-32768字节之间为2幂数的数字。有些时候,较大的页面能更有效的处理大量的数据。(2)执行事务(Performtransactions):allcommandsareexecutedwithintransactions。默认情况下,事务自动提交,也就是每一个SQL语句都在一个独立的事务下运行。当然也可以通过使用BEGIN..COMMIT手动提交事务。(3)断开连接(Disconnectfromthedatabase):主要是关闭数据库的文件。1.5.2、执行PreparedQuery前面提到,预处理查询(PreparedQuery)是SQLite执行所有SQL命令的方式,包括以下三个过程:(1)PreparedQuery:分析器(parser),分词器(tokenizer)和代码生成器(codegenerator)把SQLStatement编译成VDBE字节码,编译器会创建一个statement句柄(sqlite3_stmt),它包括字节码以及其它执行命令和遍历结果集的所有资源。相应的CAPI为sqlite3_prepare(),位于prepare.c文件中,如下:intsqlite3_prepare(sqlite3*db,constchar*zSql,intnBytes,sqlite3_stmt**ppStmt,constchar**pzTail){intrc;rc=sqlite3LockAndPrepare(db,zSql,nBytes,0,ppStmt,pzTail);assert(rc==SQLITE_OK||ppStmt==0||*ppStmt==0);returnrc;}(2)Execution:虚拟机执行字节码,执行过程是一个步进(stepwise)的过程,每一步(step)由sqlite3_step()启动,并由VDBE执行一段字节码。由sqlite3_prepare编译字节代码,并由sqlite3_step()启动虚拟机执行。在遍历结果集的过程中,它返回SQLITE_ROW,当到达结果末尾时,返回SQLITE_DONE。(3)Finalization:VDBE关闭statement,释放资源。相应的CAPI为sqlite3_finalize()。通过下图可以更容易理解该过程:最后以一个具体的例子结束本节,下节讨论事务。#include 本节通过一个具体的例子来分析SQLite原子提交的实现(基于Version3.3.6的代码)。CREATETABLEepisodes(idintegerprimarykey,nametext,cidint);插入一条记录:insertintoepisodes(name,cid)values(\"cat\;它经过编译器处理后生成的虚拟机代码如下:sqlite>explaininsertintoepisodes(name,cid)values(\"cat\0|Trace|0|0|0|explaininsertintoepisodes(name,cid)values(\"cat\1|Goto|0|12|0||00|2|SetNumColumns|0|3|0||00|3|OpenWrite|0|2|0||00|4|NewRowid|0|2|0||00|5|Null|0|3|0||00|6|String8|0|4|0|cat|00|7|Integer|1|5|0||00|8|MakeRecord|3|3|6|dad|00|9|Insert|0|6|2|episodes|0b|10|Close|0|0|0||00|11|Halt|0|0|0||00|12|Transaction|0|1|0||00|13|VerifyCookie|0|1|0||00|14|Transaction|1|1|0||00|15|VerifyCookie|1|0|0||00|16|TableLock|0|2|1|episodes|00|17|Goto|0|2|0||00|1、初始状态(InitialState)当一个数据库连接第一次打开时,状态如图所示。图中最右边(“Disk”标注)表示保存在存储设备中的内容。每个方框代表一个扇区。蓝色的块表示这个扇区保存了原始数据。图中中间区域是操作系统的磁盘缓冲区。开始的时候,这些缓存是还没有被使用,因此这些方框是空白的。图中左边区域显示SQLite用户进程的内存。因为这个数据库连接刚刚打开,所以还没有任何数据记录被读入,所以这些内存也是空的。2、获取读锁(AcquiringAReadLock)在SQLite写数据库之前,它必须先从数据库中读取相关信息。比如,在插入新的数据时,SQLite会先从sqlite_master表中读取数据库模式(相当于数据字典),以便编译器对INSERT语句进行分析,确定数据插入的位置。在进行读操作之前,必须先获取数据库的共享锁(sharedlock),共享锁允许两个或更多的连接在同一时刻读取数据库。但是共享锁不允许其它连接对数据库进行写操作。sharedlock存在于操作系统磁盘缓存,而不是磁盘本身。文件锁的本质只是操作系统的内核数据结构,当操作系统崩溃或掉电时,这些内核数据也会随之消失。3、读取数据一旦得到sharedlock,就可以进行读操作。如图所示,数据先由OS从磁盘读取到OS缓存,然后再由OS移到用户进程空间。一般来说,数据库文件分为很多页,而一次读操作只读取一小部分页面。如图,从8个页面读取3个页面。4、获取ReservedLock在对数据进行修改操作之前,先要获取数据库文件的ReservedLock,ReservedLock和sharedlock的相似之处在于,它们都允许其它进程对数据库文件进行读操作。ReservedLock和SharedLock可以共存,但是只能是一个ReservedLock和多个SharedLock——多个ReservedLock不能共存。所以,在同一时刻,只能进行一个写操作。ReservedLock意味着当前进程(连接)想修改数据库文件,但是还没开始修改操作,所以其它的进程可以读数据库,但不能写数据库。5、创建恢复日志(CreatingARollbackJournalFile)在对数据库进行写操作之前,SQLite先要创建一个单独的日志文件,然后把要修改的页面的原始数据写入日志。回滚日志包含一个日志头(图中的绿色)——记录数据库文件的原始大小。所以即使数据库文件大小改变了,我们仍知道数据库的原始大小。从OS的角度来看,当一个文件创建时,大多数OS(Windows,Linux,MacOSX)不会向磁盘写入数据,新创建的文件此时位于磁盘缓存中,之后才会真正写入磁盘。如图,日志文件位于OS磁盘缓存中,而不是位于磁盘。上面5步的代码的实现:Code其实现过程如下图所示:主要参考:http://www.sqlite.org/atomiccommit.htmlPageCache之事务处理(2)个人认为pager层是SQLite实现最为核心的模块,它具有四大功能:I/O,页面缓存,并发控制和日志恢复。而这些功能不仅是上层Btree的基础,而且对系统的性能和健壮性有关至关重要的影响。其中并发控制和日志恢复是事务处理实现的基础。SQLite并发控制的机制非常简单——封锁机制;别外,它的查询优化机制也非常简单——基于索引。这一切使得整个SQLite的实现变得简单,SQLite变得很小,运行速度也非常快,所以,特别适合嵌入式设备。好了,接下来讨论事务的剩余部分。6、修改位于用户进程空间的页面(ChangingDatabasePagesInUserSpace)页面的原始数据写入日志之后,就可以修改页面了——位于用户进程空间。每个数据库连接都有自己私有的空间,所以页面的变化只对该连接可见,而对其它连接的数据仍然是磁盘缓存中的数据。从这里可以明白一件事:一个进程在修改页面数据的同时,其它进程可以继续进行读操作。图中的红色表示修改的页面。7、日志文件刷入磁盘(FlushingTheRollbackJournalFileToMassStorage)接下来把日志文件的内容刷入磁盘,这对于数据库从意外中恢复来说是至关重要的一步。而且这通常也是一个耗时的操作,因为磁盘I/O速度很慢。这个步骤不只把日志文件刷入磁盘那么简单,它的实现实际上分成两步:首先把日志文件的内容刷入磁盘(即页面数据);然后把日志文件中页面的数目写入日志文件头,再把header刷入磁盘(这一过程在代码中清晰可见)。代码如下:Code8、获取排斥锁(ObtainingAnExclusiveLock)在对数据库文件进行修改之前(注:这里不是内存中的页面),我们必须得到数据库文件的排斥锁(ExclusiveLock)。得到排斥锁的过程可分为两步:首先得到Pendinglock;然后Pendinglock升级到exclusivelock。Pendinglock允许其它已经存在的Sharedlock继续读数据库文件,但是不允许产生新的sharedlock,这样做目的是为了防止写操作发生饿死情况。一旦所有的sharedlock完成操作,则pendinglock升级到exclusivelock。9、修改的页面写入文件(WritingChangesToTheDatabaseFile)一旦得到exclusivelock,其它的进程就不能进行读操作,此时就可以把修改的页面写回数据库文件,但是通常OS都把结果暂时保存到磁盘缓存中,直到某个时刻才会真正把结果写入磁盘。以上两步的实现代码:Code10、修改结果刷入存储设备(FlushingChangesToMassStorage)为了保证修改结果真正写入磁盘,这一步必不要少。对于数据库存的完整性,这一步也是关键的一步。由于要进行实际的I/O操作,所以和第7步一样,将花费较多的时间。最后来看看这几步是如何实现的:其实以上以上几步是在函数sqlite3BtreeSync()---btree.c中调用的(而关于该函数的调用后面再讲)。代码如下:Code下图可以进一步解释该过程:11、删除日志文件(DeletingTheRollbackJournal)一旦更改写入设备,日志文件将会被删除,这是事务真正提交的时刻。如果在这之前系统发生崩溃,就会进行恢复处理,使得数据库和没发生改变一样;如果在这之后系统发生崩溃,表明所有的更改都已经写入磁盘。SQLite就是根据日志存在情况决定是否对数据库进行恢复处理。删除文件本质上不是一个原子操作,但是从用户进程的角度来看是一个原子操作,所以一个事务看起来是一个原子操作。在许多系统中,删除文件也是一个高代价的操作。作为优化,SQLite可以配置成把日志文件的长度截为0或者把日志文件头清零。12、释放锁(ReleasingTheLock)作为原子提交的最后一步,释放排斥锁使得其它进程可以开始访问数据库了。下图中,我们指明了当锁被释放的时候用户空间所拥有的信息已经被清空了.对于老版本的SQLite你可这么认为。但最新的SQLite会保存些用户空间的缓存不会被清空—万一下一个事务开始的时候,这些数据刚好可以用上呢。重新利用这些内存要比再次从操作系统磁盘缓存或者硬盘中读取要来得轻松与快捷得多,何乐而不为呢?在再次使用这些数据之前,我们必须先取得一个共享锁,同时我们还不得不去检查一下,保证还没有其他进程在我们拥有共享锁之前对数据库文件进行了修改。数据库文件的第一页中有一个计数器,数据库文件每做一次修改,这个计数器就会增长一下。我们可以通过检查这个计数器就可得知是否有其他进程修改过数据库文件。如果数据库文件已经被修改过了,那么用户内存空间的缓存就不得不清空,并重新读入。大多数情况下,这种情况不大会发生,因此用户空间的内存缓存将是有效的,这对于性能提高来说作用是显著的。以上两步是在sqlite3BtreeCommit()---btree.c函数中实现的。代码如下:Code下图可进一步描述该过程:最后来看看sqlite3BtreeSync()和sqlite3BtreeCommit()是如何被调用的。一般来说,事务提交方式为自动提交的话,在虚拟机中的OP_Halt指令实现提交事务,相关代码如下://虚拟机停机指令caseOP_Halt:{/*no-push*/p->pTos=pTos;p->rc=pOp->p1;p->pc=pc;p->errorAction=pOp->p2;if(pOp->p3){sqlite3SetString(&p->zErrMsg,pOp->p3,(char*)0);}//设置虚拟机状态SQLITE_MAGIC_RUN为SQLITE_MAGIC_HALT,//并提交事务rc=sqlite3VdbeHalt(p);assert(rc==SQLITE_BUSY||rc==SQLITE_OK);if(rc==SQLITE_BUSY){p->rc=SQLITE_BUSY;returnSQLITE_BUSY;}returnp->rc?SQLITE_ERROR:SQLITE_DONE;}//当虚拟机要停机时,调用该函数,如果VDBE改变了数据库且为自动//提交模式,则提交这些改变intsqlite3VdbeHalt(Vdbe*p){sqlite3*db=p->db;inti;int(*xFunc)(Btree*pBt)=0;/*Functiontocalloneachbtreebackend*/intisSpecialError;/*SettotrueifSQLITE_NOMEMorIOERR*//*Thisfunctioncontainsthelogicthatdeterminesifastatementor**transactionwillbecommittedorrolledbackasaresultofthe**executionofthisvirtualmachine.****Specialerrors:****IfanSQLITE_NOMEMerrorhasoccuredinastatementthatwritesto**thedatabase,theneitherastatementortransactionmustberolled**backtoensurethetree-structuresareinaconsistentstate.A**statementtransactionisrolledbackifoneisopen,otherwisethe**entiretransactionmustberolledback.****IfanSQLITE_IOERRerrorhasoccuredinastatementthatwritesto**thedatabase,thentheentiretransactionmustberolledback.The**I/Oerrormayhavecausedgarbagetobewrittentothejournal**file.Werethetransactiontocontinueandeventuallyberolled**backthatgarbagemightendupinthedatabasefile.****Inbothoftheabovecases,theVdbe.errorActionvariableis**ignored.Ifthesqlite3.autoCommitflagisfalseandatransaction**isrolledback,itwillbesettotrue.****Othererrors:****Noerror:***/if(sqlite3MallocFailed()){p->rc=SQLITE_NOMEM;}if(p->magic!=VDBE_MAGIC_RUN){/*Alreadyhalted.Nothingtodo.*/assert(p->magic==VDBE_MAGIC_HALT);returnSQLITE_OK;}//释放虚拟机中所有的游标closeAllCursors(p);checkActiveVdbeCnt(db);/*Nocommitorrollbackneedediftheprogramneverstarted*/if(p->pc>=0){/*Checkforoneofthespecialerrors-SQLITE_NOMEMorSQLITE_IOERR*/isSpecialError=((p->rc==SQLITE_NOMEM||p->rc==SQLITE_IOERR)?1:0);if(isSpecialError){/*Thisloopdoesstaticanalysisofthequerytoseewhichofthe**followingthreecategoriesitfallsinto:****Read-only**Querywithstatementjournal**Querywithoutstatementjournal****Wecoulddosomethingmoreelegantthanthisstaticanalysis(i.e.**storethetypeofqueryaspartofthecompliationphase),but**handlingmalloc()orIOfailureisafairlyobscureedgecaseso**thisisprobablyeasier.Todo:Mightbeanopportunitytoreduce**codesizeaverysmallamountthough*/intisReadOnly=1;intisStatement=0;assert(p->aOp||p->nOp==0);for(i=0;i 本节主要谈谈SQLite的锁机制,SQLite是基于锁来实现并发控制的,所以本节的内容实际上是属于事务处理的,但是SQLite的锁机制实现非常的简单而巧妙,所以在这里单独讨论一下。如果真正理解了它,对整个事务的实现也就理解了。而要真正理解SQLite的锁机制,最好方法就是阅读SQLite的源码,所以在阅读本文时,最好能结合源码。SQLite的锁机制很巧妙,尽管在本节中的源码中,我写了很多注释,也是我个人在研究时的一点心得,但是我发现仅仅用言语,似乎不能把问题说清楚,只有通过体会,才能真正理解SQLite的锁机制。好了,下面进入正题。SQLite的并发控制机制是采用加锁的方式,实现非常简单,但也非常的巧妙,本节将对其进行一个详细的解剖。请仔细阅读下图,它可以帮助更好的理解下面的内容。1、RESERVEDLOCKRESERVED锁意味着进程将要对数据库进行写操作。某一时刻只能有一个RESERVEDLock,但是RESERVED锁和SHARED锁可以共存,而且可以对数据库加新的SHARED锁。为什么要用RESERVED锁?主要是出于并发性的考虑。由于SQLite只有库级排斥锁(EXCLUSIVELOCK),如果写事务一开始就上EXCLUSIVE锁,然后再进行实际的数据更新,写磁盘操作,这会使得并发性大大降低。而SQLite一旦得到数据库的RESERVED锁,就可以对缓存中的数据进行修改,而与此同时,其它进程可以继续进行读操作。直到真正需要写磁盘时才对数据库加EXCLUSIVE锁。2、PENDINGLOCKPENDINGLOCK意味着进程已经完成缓存中的数据修改,并想立即将更新写入磁盘。它将等待此时已经存在的读锁事务完成,但是不允许对数据库加新的SHAREDLOCK(这与RESERVEDLOCK相区别)。为什么要有PENDINGLOCK?主要是为了防止出现写饿死的情况。由于写事务先要获取RESERVEDLOCK,所以可能一直产生新的SHAREDLOCK,使得写事务发生饿死的情况。3、加锁机制的具体实现SQLite在pager层获取锁的函数如下:CodeWindows下具体的实现如下:Code在几个关键的部位标记数字。(I)对于一个读事务会的完整经过:语句序列:(1)——>(2)——>(6)相应的状态真正的变化过程为:UNLOCKED→PENDING(1)→PENDING、SHARED(2)→SHARED(6)→UNLOCKED(II)对于一个写事务完整经过:第一阶段:语句序列:(1)——>(2)——>(6)状态变化:UNLOCKED→PENDING(1)→PENDING、SHARED(2)→SHARED(6)。此时事务获得SHAREDLOCK。第二个阶段:语句序列:(3)此时事务获得RESERVEDLOCK。第三个阶段:事务执行修改操作。第四个阶段:语句序列:(1)——>(4)——>(5)状态变化为:RESERVED→RESERVED、PENDING(1)→PENDING(4)→EXCLUSIVE(5)。此时事务获得排斥锁,就可以进行写磁盘操作了。注:在上面的过程中,由于(1)的执行,使得某些时刻SQLite处于两种状态,但它持续的时间很短,从某种程度上来说可以忽略,但是为了把问题说清楚,在这里描述了这一微妙而巧妙的过程。4、SQLite的死锁问题SQLite的加锁机制会不会出现死锁?这是一个很有意思的问题,对于任何采取加锁作为并发控制机制的DBMS都得考虑这个问题。有两种方式处理死锁问题:(1)死锁预防(deadlockprevention)(2)死锁检测(deadlockdetection)与死锁恢复(deadlockrecovery)。SQLite采取了第一种方式,如果一个事务不能获取锁,它会重试有限次(这个重试次数可以由应用程序运行预先设置,默认为1次)——这实际上是基本锁超时的机制。如果还是不能获取锁,SQLite返回SQLITE_BUSY错误给应用程序,应用程序此时应该中断,之后再重试;或者中止当前事务。虽然基于锁超时的机制简单,容易实现,但是它的缺点也是明显的——资源浪费。5、事务类型(TransactionTypes)既然SQLite采取了这种机制,所以应用程序得处理SQLITE_BUSY错误,先来看一个会产生SQLITE_BUSY错误的例子:所以应用程序应该尽量避免产生死锁,那么应用程序如何做可以避免死锁的产生呢?答案就是为你的程序选择正确合适的事务类型。SQLite有三种不同的事务类型,这不同于锁的状态。事务可以从DEFERRED,IMMEDIATE或者EXCLUSIVE,一个事务的类型在BEGIN命令中指定:BEGIN[DEFERRED|IMMEDIATE|EXCLUSIVE]TRANSACTION;一个deferred事务不获取任何锁,直到它需要锁的时候,而且BEGIN语句本身也不会做什么事情——它开始于UNLOCK状态;默认情况下是这样的。如果仅仅用BEGIN开始一个事务,那么事务就是DEFERRED的,同时它不会获取任何锁,当对数据库进行第一次读操作时,它会获取SHAREDLOCK;同样,当进行第一次写操作时,它会获取RESERVEDLOCK。由BEGIN开始的Immediate事务会试着获取RESERVEDLOCK。如果成功,BEGINIMMEDIATE保证没有别的连接可以写数据库。但是,别的连接可以对数据库进行读操作,但是RESERVEDLOCK会阻止其它的连接BEGINIMMEDIATE或者BEGINEXCLUSIVE命令,SQLite会返回SQLITE_BUSY错误。这时你就可以对数据库进行修改操作,但是你不能提交,当你COMMIT时,会返回SQLITE_BUSY错误,这意味着还有其它的读事务没有完成,得等它们执行完后才能提交事务。Exclusive事务会试着获取对数据库的EXCLUSIVE锁。这与IMMEDIATE类似,但是一旦成功,EXCLUSIVE事务保证没有其它的连接,所以就可对数据库进行读写操作了。上面那个例子的问题在于两个连接最终都想写数据库,但是他们都没有放弃各自原来的锁,最终,shared锁导致了问题的出现。如果两个连接都以BEGINIMMEDIATE开始事务,那么死锁就不会发生。在这种情况下,在同一时刻只能有一个连接进入BEGINIMMEDIATE,其它的连接就得等待。BEGINIMMEDIATE和BEGINEXCLUSIVE通常被写事务使用。就像同步机制一样,它防止了死锁的产生。基本的准则是:如果你在使用的数据库没有其它的连接,用BEGIN就足够了。但是,如果你使用的数据库在其它的连接也要对数据库进行写操作,就得使用BEGINIMMEDIATE或BEGINEXCLUSIVE开始你的事务。第六节再谈SQLite的锁 SQLite封锁机制的实现需要底层文件系统的支持,不管是Linux,还是Windows,都提供了文件锁的机制,而这为SQLite提供了强大的支持。本节就来谈谈SQLite使用到的文件锁——主要基于Linux和Windows平台。Linux的文件锁Linux支持的文件锁技术主要包括建议锁(advisorylock)和强制锁(mandatorylock)这两种。此外,Linux中还引入了两种强制锁的变种形式:共享模式强制锁(share-modemandatorylock)和租借锁(lease)。在这里,主要讨论建议锁(advisorylock)。建议锁并不由内核强制实行,也就是说如果有进程不遵守“游戏规则”,不检查目标文件是否已经由别的进程加了锁就往其中写入数据,那么内核是不会加以阻拦的。因此,建议锁并不能阻止进程对文件的访问,而只能依靠各个进程在访问文件之前检查该文件是否已经被其他进程加锁来实现并发控制。进程需要事先对锁的状态做一个约定,并根据锁的当前状态和相互关系来确定其他进程是否能对文件执行指定的操作。而强制锁是由内核强制采用的文件锁——由于内核对每个read()和write()操作都会检查相应的锁,所以会降低系统性能。对于建议锁,Linux提供两种实现方式:锁文件(lockfiles)和记录锁(recordlocking)。(1)锁文件(lockfiles)锁文件是最简单的对文件加锁的方法,每个需要加锁的数据文件都有一个锁文件(lockfile)。当锁文件存在时,就认为该数据文件已经被加锁,别的进程不应该访问(但是你非要访问,Linux也不会阻止)。当锁不存在,进程就可以创建一个锁文件,然后访问相应的数据文件。只要创建锁的过程是原子的,就能保证某一时刻只有一个进程拥有该锁,这种方法保证某一时刻只有一个进程访问文件。这种想法很简单,当一个进程想访问文件时,可以按如下方式对文件加锁:fd=open(\"somefile.lck\",O_RDONLY,0644);if(fd>=0){close(fd);printf(\"thefileisalreadylocked\");return1;}else{/*thelockfiledoesnotexist,wecanlockitandaccessit*/fd=open(\"somefile.lck\",O_CREAT|O_WRONLY,0644\");if(fd<0){perror(\"errorcreatinglockfile\");return1;}/*wecouldwriteourpidtothefile*/close(fd);}当一个进程处理完文件后,就可以调用unlink(\"somefile.lck\")释放锁了——本质上是删除somefile.lck文件。上面这段代码实际上存在竞争情况,原因在于if语句块不是原子性的,进入if语句块,内核可能调度别的进程运行。更好的方式如下:fd=open(\"somefile.lck\",O_WRONLY|O_CREAT|O_EXCL,0644);if(fd<0&&errno==EEXIST){printf(\"thefileisalreadylocked\");return1;}elseif(fd<0){perror(\"unexpectederrorcheckinglock\");return1;}/*wecouldwriteourpidtothefile*/close(fd);O_EXCL标志保证open()创建锁文件的过程是原子性的。注意以下几点:1、任何时刻只有一个进程可以拥有锁。2、O_EXCL标志只对本志文件系统是可靠的,对于网络文件系统并不能很好的支持。3、锁仅仅只是建议性的。4、如果一个持有锁的进程不正常结束,锁文件仍然存在。如果加锁进程的pid存储在锁文件中,其它进程可以检查锁进程是否存在,当它结束时就可以删除锁。但是,在检查的时候,如果pid被其它进程使用了,此时就无能为力了。(2)记录锁(RecordLocking)为了克服锁文件的缺点,SystemV和BSD4.3引入了记录锁,相应的系统调用为lockf()和flock()。而POSIX对于记录锁提供了另外一种机制,其系统调用为fcntl()。Linux提供三种接口,在这里仅讨论POSIX的接口。记录锁和锁文件有两个很重要的区别:首先,记录锁可以对文件的任何一部分加锁——这对于DBMS这样的应用程序,有极大的帮助,SQLite当然没有放过这样的好处。其次,记录锁的另一个优点就是它由内核持有,而不是文件系统持有。当进程结束时,所有的锁也随之释放。和锁文件一样,POSIX锁也是建议性的。记录锁有两种锁:读锁(readlocks)和写锁(writelocks)。读锁也就是共享锁(sharedlock),写锁也就是排它锁(exclusivelock)。对于一个记录,只能有一个进程持有写锁,读锁不能存在。对于一个进程本身而言,多个锁绝不会冲突。如果一个进程对文件的200-250字节持有读锁,然后对200-225字节数据加写锁,是会成功的。此时,200-225为写锁,而226-250字节数据为读锁,该规则主要是防止进程本身发生死锁(尽管多进程之间仍然可能发生死锁)。POSIX锁通过fcntl()系统来实现,如下:#include 1.0总览 SQLite3是SQLite一个全新的版本,它虽然是在SQLite2.8.13的代码基础之上开发的,但是使用了和之前的版本不兼容的数据库格式和API.SQLite3是为了满足以下的需求而开发的:支持UTF-16编码.用户自定义的文本排序方法.可以对BLOBs字段建立索引.因此为了支持这些特性我改变了数据库的格式,建立了一个与之前版本不兼容的3.0版.至于其他的兼容性的改变,例如全新的API等等,都将在理论介绍之后向你说明,这样可以使你最快的一次性摆脱兼容性问题.3.0版的和2.X版的API非常相似,但是有一些重要的改变需要注意.所有API接口函数和数据结构的前缀都由\"sqlite_\"改为了\"sqlite3_\".这是为了避免同时使用SQLite2.X和SQLite3.0这两个版本的时候发生链接冲突.由于对于C语言应该用什么数据类型来存放UTF-16编码的字符串并没有一致的规范.因此SQLite使用了普通的void*类型来指向UTF-16编码的字符串.客户端使用过程中可以把void*映射成适合他们的系统的任何数据类型.2.0C/C++接口 SQLite3.0一共有83个API函数,此外还有一些数据结构和预定义(#defines).(完整的API介绍请参看另一份文档.)不过你们可以放心,这些接口使用起来不会像它的数量所暗示的那么复杂.最简单的程序仍然使用三个函数就可以完成:sqlite3_open(),sqlite3_exec(),和sqlite3_close().要是想更好的控制数据库引擎的执行,可以使用提供的sqlite3_prepare()函数把SQL语句编译成字节码,然后在使用sqlite3_step()函数来执行编译后的字节码.以sqlite3_column_开头的一组API函数用来获取查询结果集中的信息.许多接口函数都是成对出现的,同时有UTF-8和UTF-16两个版本.并且提供了一组函数用来执行用户自定义的SQL函数和文本排序函数.2.1如何打开关闭数据库typedefstructsqlite3sqlite3;intsqlite3_open(constchar*,sqlite3**);intsqlite3_open16(constvoid*,sqlite3**);intsqlite3_close(sqlite3*);constchar*sqlite3_errmsg(sqlite3*);constvoid*sqlite3_errmsg16(sqlite3*);intsqlite3_errcode(sqlite3*);sqlite3_open()函数返回一个整数错误代码,而不是像第二版中一样返回一个指向sqlite3结构体的指针.sqlite3_open()和sqlite3_open16()的不同之处在于sqlite3_open16()使用UTF-16编码(使用本地主机字节顺序)传递数据库文件名.如果要创建新数据库,sqlite3_open16()将内部文本转换为UTF-16编码,反之sqlite3_open()将文本转换为UTF-8编码.打开或者创建数据库的命令会被缓存,直到这个数据库真正被调用的时候才会被执行.而且允许使用PRAGMA声明来设置如本地文本编码或默认内存页面大小等选项和参数.sqlite3_errcode()通常用来获取最近调用的API接口返回的错误代码.sqlite3_errmsg()则用来得到这些错误代码所对应的文字说明.这些错误信息将以UTF-8的编码返回,并且在下一次调用任何SQLiteAPI函数的时候被清除.sqlite3_errmsg16()和sqlite3_errmsg()大体上相同,除了返回的错误信息将以UTF-16本机字节顺序编码.SQLite3的错误代码相比SQLite2没有任何的改变,它们分别是:#defineSQLITE_OK0/*Successfulresult*/#defineSQLITE_ERROR1/*SQLerrorormissingdatabase*/#defineSQLITE_INTERNAL2/*AninternallogicerrorinSQLite*/#defineSQLITE_PERM3/*Accesspermissiondenied*/#defineSQLITE_ABORT4/*Callbackroutinerequestedanabort*/#defineSQLITE_BUSY5/*Thedatabasefileislocked*/#defineSQLITE_LOCKED6/*Atableinthedatabaseislocked*/#defineSQLITE_NOMEM7/*Amalloc()failed*/#defineSQLITE_READONLY8/*Attempttowriteareadonlydatabase*/#defineSQLITE_INTERRUPT9/*Operationterminatedbysqlite_interrupt()*/#defineSQLITE_IOERR10/*SomekindofdiskI/Oerroroccurred*/#defineSQLITE_CORRUPT11/*Thedatabasediskimageismalformed*/#defineSQLITE_NOTFOUND12/*(InternalOnly)Tableorrecordnotfound*/#defineSQLITE_FULL13/*Insertionfailedbecausedatabaseisfull*/#defineSQLITE_CANTOPEN14/*Unabletoopenthedatabasefile*/#defineSQLITE_PROTOCOL15/*Databaselockprotocolerror*/#defineSQLITE_EMPTY16/*(InternalOnly)Databasetableisempty*/#defineSQLITE_SCHEMA17/*Thedatabaseschemachanged*/#defineSQLITE_TOOBIG18/*Toomuchdataforonerowofatable*/#defineSQLITE_CONSTRAINT19/*Abortduetocontraintviolation*/#defineSQLITE_MISMATCH20/*Datatypemismatch*/#defineSQLITE_MISUSE21/*Libraryusedincorrectly*/#defineSQLITE_NOLFS22/*UsesOSfeaturesnotsupportedonhost*/#defineSQLITE_AUTH#defineSQLITE_ROW#defineSQLITE_DONE23/*Authorizationdenied*/100/*sqlite_step()hasanotherrowready*/101/*sqlite_step()hasfinishedexecuting*/SQLite3C/C++开发接口简介(API函数)二 2.2执行SQL语句typedefint(*sqlite_callback)(void*,int,char**,char**);intsqlite3_exec(sqlite3*,constchar*sql,sqlite_callback,void*,char**);sqlite3_exec函数依然像它在SQLite2中一样承担着很多的工作.该函数的第二个参数中可以编译和执行零个或多个SQL语句.查询的结果返回给回调函数.更多地信息可以查看API参考.在SQLite3里,sqlite3_exec一般是被准备SQL语句接口封装起来使用的.typedefstructsqlite3_stmtsqlite3_stmt;intsqlite3_prepare(sqlite3*,constchar*,int,sqlite3_stmt**,constchar**);intsqlite3_prepare16(sqlite3*,constvoid*,int,sqlite3_stmt**,constvoid**);intsqlite3_finalize(sqlite3_stmt*);intsqlite3_reset(sqlite3_stmt*);sqlite3_prepare接口把一条SQL语句编译成字节码留给后面的执行函数.使用该接口访问数据库是当前比较好的的一种方法.sqlite3_prepare()处理的SQL语句应该是UTF-8编码的.而sqlite3_prepare16()则要求是UTF-16编码的.输入的参数中只有第一个SQL语句会被编译.第四个参数则用来指向输入参数中下一个需要编译的SQL语句存放的SQLitestatement对象的指针,任何时候如果调用sqlite3_finalize()将销毁一个准备好的SQL声明.在数据库关闭之前,所有准备好的声明都必须被释放销毁.sqlite3_reset()函数用来重置一个SQL声明的状态,使得它可以被再次执行.SQL声明可以包含一些型如\"?\"或\"?nnn\"或\":aaa\"的标记,其中\"nnn\"是一个整数,\"aaa\"是一个字符串.这些标记代表一些不确定的字符值(或者说是通配符),可以在后面用sqlite3_bind接口来填充这些值.每一个通配符都被分配了一个编号(由它在SQL声明中的位置决定,从1开始),此外也可以用\"nnn\"来表示\"?nnn\"这种情况.允许相同的通配符在同一个SQL声明中出现多次,在这种情况下所有相同的通配符都会被替换成相同的值.没有被绑定的通配符将自动取NULL值.intsqlite3_bind_blob(sqlite3_stmt*,int,constvoid*,intn,void(*)(void*));intsqlite3_bind_double(sqlite3_stmt*,int,double);intsqlite3_bind_int(sqlite3_stmt*,int,int);intsqlite3_bind_int64(sqlite3_stmt*,int,longlongint);intsqlite3_bind_null(sqlite3_stmt*,int);intsqlite3_bind_text(sqlite3_stmt*,int,constchar*,intn,void(*)(void*));intsqlite3_bind_text16(sqlite3_stmt*,int,constvoid*,intn,void(*)(void*));intsqlite3_bind_value(sqlite3_stmt*,int,constsqlite3_value*);以上是sqlite3_bind所包含的全部接口,它们是用来给SQL声明中的通配符赋值的.没有绑定的通配符则被认为是空值.绑定上的值不会被sqlite3_reset()函数重置.但是在调用了sqlite3_reset()之后所有的通配符都可以被重新赋值.在SQL声明准备好之后(其中绑定的步骤是可选的),需要调用以下的方法来执行:intsqlite3_step(sqlite3_stmt*);如果SQL返回了一个单行结果集,sqlite3_step()函数将返回SQLITE_ROW,如果SQL语句执行成功或者正常将返回SQLITE_DONE,否则将返回错误代码.如果不能打开数据库文件则会返回SQLITE_BUSY.如果函数的返回值是SQLITE_ROW,那么下边的这些方法可以用来获得记录集行中的数据:constvoid*sqlite3_column_blob(sqlite3_stmt*,intiCol);intsqlite3_column_bytes(sqlite3_stmt*,intiCol);intsqlite3_column_bytes16(sqlite3_stmt*,intiCol);intsqlite3_column_count(sqlite3_stmt*);constchar*sqlite3_column_decltype(sqlite3_stmt*,intiCol);constvoid*sqlite3_column_decltype16(sqlite3_stmt*,intiCol);doublesqlite3_column_double(sqlite3_stmt*,intiCol);intsqlite3_column_int(sqlite3_stmt*,intiCol);longlongintsqlite3_column_int64(sqlite3_stmt*,intiCol);constchar*sqlite3_column_name(sqlite3_stmt*,intiCol);constvoid*sqlite3_column_name16(sqlite3_stmt*,intiCol);constunsignedchar*sqlite3_column_text(sqlite3_stmt*,intiCol);constvoid*sqlite3_column_text16(sqlite3_stmt*,intiCol);intsqlite3_column_type(sqlite3_stmt*,intiCol);sqlite3_column_count()函数返回结果集中包含的列数.sqlite3_column_count()可以在执行了sqlite3_prepare()之后的任何时刻调用.sqlite3_data_count()除了必需要在sqlite3_step()之后调用之外,其他跟sqlite3_column_count()大同小异.如果调用sqlite3_step()返回值是SQLITE_DONE或者一个错误代码,则此时调用sqlite3_data_count()将返回0,然而sqlite3_column_count()仍然会返回结果集中包含的列数.返回的记录集通过使用其它的几个sqlite3_column_***()函数来提取,所有的这些函数都把列的编号作为第二个参数.列编号从左到右以零起始.请注意它和之前那些从1起始的参数的不同.sqlite3_column_type()函数返回第N列的值的数据类型.具体的返回值如下:#defineSQLITE_INTEGER1#defineSQLITE_FLOAT2#defineSQLITE_TEXT3#defineSQLITE_BLOB4#defineSQLITE_NULL5sqlite3_column_decltype()则用来返回该列在CREATETABLE语句中声明的类型.它可以用在当返回类型是空字符串的时候.sqlite3_column_name()返回第N列的字段名.sqlite3_column_bytes()用来返回UTF-8编码的BLOBs列的字节数或者TEXT字符串的字节数.sqlite3_column_bytes16()对于BLOBs列返回同样的结果,但是对于TEXT字符串则按UTF-16的编码来计算字节数.sqlite3_column_blob()返回BLOB数据.sqlite3_column_text()返回UTF-8编码的TEXT数据.sqlite3_column_text16()返回UTF-16编码的TEXT数据.sqlite3_column_int()以本地主机的整数格式返回一个整数值.sqlite3_column_int64()返回一个64位的整数.最后,sqlite3_column_double()返回浮点数.不一定非要按照sqlite3_column_type()接口返回的数据类型来获取数据.数据类型不同时软件将自动转换.SQLite3C/C++开发接口简介(API函数)三 2.3用户自定义函数可以使用以下的方法来创建用户自定义的SQL函数:typedefstructsqlite3_valuesqlite3_value;intsqlite3_create_function(sqlite3*,constchar*zFunctionName,intnArg,inteTextRep,void*,void(*xFunc)(sqlite3_context*,int,sqlite3_value**),void(*xStep)(sqlite3_context*,int,sqlite3_value**),void(*xFinal)(sqlite3_context*));intsqlite3_create_function16(sqlite3*,constvoid*zFunctionName,intnArg,inteTextRep,void*,void(*xFunc)(sqlite3_context*,int,sqlite3_value**),void(*xStep)(sqlite3_context*,int,sqlite3_value**),void(*xFinal)(sqlite3_context*));#defineSQLITE_UTF81#defineSQLITE_UTF162#defineSQLITE_UTF16BE3#defineSQLITE_UTF16LE4#defineSQLITE_ANY5nArg参数用来表明自定义函数的参数个数.如果参数值为0,则表示接受任意个数的参数.用eTextRep参数来表明传入参数的编码形式.参数值可以是上面的五种预定义值.SQLite3允许同一个自定义函数有多种不同的编码参数的版本.数据库引擎会自动选择转换参数编码个数最少的版本使用.普通的函数只需要设置xFunc参数,而把xStep和xFinal设为NULL.聚合函数则需要设置xStep和xFinal参数,然后把xFunc设为NULL.该方法和使用sqlite3_create_aggregate()API一样.sqlite3_create_function16()和sqlite_create_function()的不同就在于自定义的函数名一个要求是UTF-16编码,而另一个则要求是UTF-8.请注意自定函数的参数目前使用了sqlite3_value结构体指针替代了SQLiteversion2.X中的字符串指针.下面的函数用来从sqlite3_value结构体中提取数据:constvoid*sqlite3_value_blob(sqlite3_value*);intsqlite3_value_bytes(sqlite3_value*);intsqlite3_value_bytes16(sqlite3_value*);doublesqlite3_value_double(sqlite3_value*);intsqlite3_value_int(sqlite3_value*);longlongintsqlite3_value_int64(sqlite3_value*);constunsignedchar*sqlite3_value_text(sqlite3_value*);constvoid*sqlite3_value_text16(sqlite3_value*);intsqlite3_value_type(sqlite3_value*);上面的函数调用以下的API来获得上下文内容和返回结果:void*sqlite3_aggregate_context(sqlite3_context*,intnbyte);void*sqlite3_user_data(sqlite3_context*);voidsqlite3_result_blob(sqlite3_context*,constvoid*,intn,void(*)(void*));voidsqlite3_result_double(sqlite3_context*,double);voidsqlite3_result_error(sqlite3_context*,constchar*,int);voidsqlite3_result_error16(sqlite3_context*,constvoid*,int);voidsqlite3_result_int(sqlite3_context*,int);voidsqlite3_result_int64(sqlite3_context*,longlongint);voidsqlite3_result_null(sqlite3_context*);voidsqlite3_result_text(sqlite3_context*,constchar*,intn,void(*)(void*));voidsqlite3_result_text16(sqlite3_context*,constvoid*,intn,void(*)(void*));voidsqlite3_result_value(sqlite3_context*,sqlite3_value*);void*sqlite3_get_auxdata(sqlite3_context*,int);voidsqlite3_set_auxdata(sqlite3_context*,int,void*,void(*)(void*));2.4用户自定义排序规则下面的函数用来实现用户自定义的排序规则:sqlite3_create_collation(sqlite3*,constchar*zName,inteTextRep,void*,int(*xCompare)(void*,int,constvoid*,int,constvoid*));sqlite3_create_collation16(sqlite3*,constvoid*zName,inteTextRep,void*,int(*xCompare)(void*,int,constvoid*,int,constvoid*));sqlite3_collation_needed(sqlite3*,void*,void(*)(void*,sqlite3*,inteTextRep,constchar*));sqlite3_collation_needed16(sqlite3*,void*,void(*)(void*,sqlite3*,inteTextRep,constvoid*));sqlite3_create_collation()函数用来声明一个排序序列和实现它的比较函数.比较函数只能用来做文本的比较.eTextRep参数可以取如下的预定义值SQLITE_UTF8,SQLITE_UTF16LE,SQLITE_UTF16BE,SQLITE_ANY,用来表示比较函数所处理的文本的编码方式.同一个自定义的排序规则的同一个比较函数可以有UTF-8,UTF-16LE和UTF-16BE等多个编码的版本.sqlite3_create_collation16()和sqlite3_create_collation()的区别也仅仅在于排序名称的编码是UTF-16还是UTF-8.可以使用sqlite3_collation_needed()函数来注册一个回调函数,当数据库引擎遇到未知的排序规则时会自动调用该函数.在回调函数中可以查找一个相似的比较函数,并激活相应的sqlite_3_create_collation()函数.回调函数的第四个参数是排序规则的名称,同样sqlite3_collation_needed采用UTF-8编码.sqlite3_collation_need16()采用UTF-16编码.SQLite数据库加密的一种解决方案 SQLite是一个非常小巧的跨平台嵌入式数据库,它的数据库以文件的形式存放在本地磁盘上,但是在其开源的免费版中它却缺少了一个数据库中几乎是必备的功能,那就是对于数据库的加密。SQLite的数据库文件可以被任何的文本编辑工具打开,从而获取到其中的数据,这一点令很多开发者感到不安。但是其实SQLite是支持数据库加密的,前些天看到了网友arris的帖子,具体如下:sqlite的源代码中原本就考虑了加密的实现,并且保留了接口sqlite3_key和sqlite3_rekey,只是这两个函数在free版本中没有实现,但幸运的是,sqlite的源代码的代码是开放并允许修改,我们可以很方便的增加加密的实现。在http://www.sqlite.com.cn/POParticle/3/216.Html链接的的代码包中就包含有可加密sqlite的源代码的实现,我根据这个包编译了一个可加密的sqlite。这个包加密实现调用了windowsAPI的加密函数,所以只能在windows中使用。这个可加密的版本是在一个ADO.NET2.0SQLiteDataProvider的基础上改过来的(http://www.sqlite.com.cn/POParticle/3/216.Html),据原作者声称效率损失在千分之一以下。原始工程是基于VS2005的,但是考虑到其普及性还不是很广,所以重新建立了一个居于VC2003的工程。其实SQLite的两个加密函数使用起来非常的简单,下面分情况说明:①给一个未加密的数据库添加密码:如果想要添加密码,则可以在打开数据库文件之后,关闭数据库文件之前的任何时刻调用sqlite3_key函数即可,该函数有三个参数,其中第一个参数为数据库对象,第二个参数是要设定的密码,第三个是密码的长度。例如:sqlite3_key(db,\"1q2w3e4r\//给数据库设定密码1q2w3e4r②读取一个加密数据库中的数据:完成这个任务依然十分简单,你只需要在打开数据库之后,再次调用一下sqlite3_key函数即可,例如,但数据库密码是123456时,你只需要在代码中加入sqlite3_key(db,\"123456\①更改数据库密码:首先你需要使用当前的密码正确的打开数据库,之后你可以调用sqlite3_rekey(db,\"112233\来更改数据库密码。②删除密码:也就是把数据库恢复到明文状态。这时你仍然只需要调用sqlite3_rekey函数,并且把该函数的第二个参数置为NULL或者\"\0。加密后数据库文件显示为乱码:为此我建立了一个简单的示例:sqlite3*db;sqlite3_stmt*stat;char*zErrMsg=0;chartemp[256],FileRoot[256];charbuffer2[1024]=\"0\";sprintf(temp,_T(\"%s\"),_T(\"utf.db\"));CCodingConv::GB2312_2_UTF8(FileRoot,256,temp,0);sqlite3_open(FileRoot,&db);if(db==NULL){return-1;}sqlite3_key(db,\"1q2w3e4r\sqlite3_exec(db,\"CREATETABLElist(flienamevarchar(128)UNIQUE,fziptext);\0,0,&zErrMsg);sqlite3_prepare(db,\"insertintolistvalues('中文GB2312编码',?);\-1,&stat,0);strcpy(temp,\"测试数据UTF-8的支持情况\");intlen=(int)strlen(temp);sqlite3_bind_text(stat,1,temp,len,NULL);sqlite3_step(stat);sqlite3_prepare(db,\"select*fromlist;\-1,&stat,0);sqlite3_step(stat);constunsignedchar*test=sqlite3_column_text(stat,1);intsize=sqlite3_column_bytes(stat,1);printf(\"%sest);sqlite3_finalize(stat);//sqlite3_rekey(db,\"\sqlite3_close(db);具体的源代码如下:SQLite3.3.7加密版源代码(VC2003)SQLite3.3.7加密版源代码(VC2005)例子1,例子2SQLite的类型亲和性分析 一、类型亲和性介绍SQLite不强制数据类型约束。任何数据都可以插入任何列。你可以向一个整型列中插入任意长度的字符串,向布尔型列中插入浮点数,或者向字符型列中插入日期型值。在CreateTABLE中所指定的数据类型不会限制在该列中插入任何数据。任何列均可接受任意长度的字符串(只有一种情况除外:标志为INTEGERPRIMARYKEY的列只能存储64位整数,当向这种列中插数据除整数以外的数据时,将会产生错误。)但SQLite确实使用声明的列类型来指示你所期望的格式。所以,例如你向一个整型列中插入字符串时,SQLite会试图将该字符串转换成一个整数。如果可以转换,它将插入该整数;否则,将插入字符串。这是一个特性,而不是一个bug。这种特性被称为类型或列亲和性(typeorcolumnaffinity).二、类型亲和性总结(优点):1提高和其它DBMS的兼容性,让用户就像是在用一般的DBMS一样而使用它,提高了容错能力。2SQLite支持的数据类型只有五种,而其它的大型DBMS支持的数据类型有几十种,那么如果要将其它的数据转换成SQLite下的数据就根本不能实现,所以就将它的数据类型设计为亲和性的,数据类型种类少了系统实现会简单很多,整个系统也就不会太庞大,因为如果有太多的数据类型限制的话,本身系统在实现方面也会困难些。然而,虽然它支持的类型虽然只有五种,可是实际上任何类型都支持了,这就是SQLite数据类型亲和性的巧妙之处。由此我个人认为这也就是将数据类型设计成为亲和性的初衷。3在插入数据的时候只要做一些检查和转换即可,实现容易三、数据类型亲和性(缺点):1.在对表中数据进行统计方面如果有不一致的数据存在则运算比较混乱,其实也就是放宽政策为的是让更多人去维护。不过它自己是有处理方法的,如果在运算时出现不同类型的数据时就忽略不计等(我认为这点也是很牵强,因为如果跳过就会得到一些不合乎人期望的结果,但我认为一般情况下,对于一列数据来说,基本上会是一致的,因为如果在很大程序上不一致的话就没什么意义的)。2.还有在数据比较方面也存在同样的问题,不过也有相应的补救措施,自己规定了比较准则:a)一个具有空存储类型的值被认为小于任何值(包括另外一个具有空存储类型的值)。b)一个整数值或实数值小于任何文本值和BLOB值。当一个整数或实数和另一个整数或实数相比较的时候,则按照实际数值来比较。c)一个文本值小于BLOB值。当两个文本值相比较的时候,则用C语言类库中的memcmp()函数来比较。然而,有时候也不是这样的,比如在下面所描述的“用户定义的整理顺序”情况下。d)当两个BLOB文本被比较的时候,结果决定于memcmp()函数。SQLite的查询优化 SQLite是个典型的嵌入式DBMS,它有很多优点,它是轻量级的,在编译之后很小,其中一个原因就是在查询优化方面比较简单,它只是运用索引机制来进行优化的,经过对SQLite的查询优化的分析以及对源代码的研究,我将SQLite的查询优总结如下:一、影响查询性能的因素:1.对表中行的检索数目,越小越好2.排序与否。3.是否要对一个索引。4.查询语句的形式二、几个查询优化的转换1.对于单个表的单个列而言,如果都有形如T.C=expr这样的子句,并且都是用OR操作符连接起来,形如:x=expr1ORexpr2=xORx=expr3此时由于对于OR,在SQLite中不能利用索引来优化,所以可以将它转换成带有IN操作符的子句:xIN(expr1,expr2,expr3)这样就可以用索引进行优化,效果很明显,但是如果在都没有索引的情况下OR语句执行效率会稍优于IN语句的效率。2.如果一个子句的操作符是BETWEEN,在SQLite中同样不能用索引进行优化,所以也要进行相应的等价转换:如:aBETWEENbANDc可以转换成:(aBETWEENbANDc)AND(a>=b)AND(a<=c)。在上面这个子句中,(a>=b)AND(a<=c)将被设为dynamic且是(aBETWEENbANDc)的子句,那么如果BETWEEN语句已经编码,那么子句就忽略不计,如果存在可利用的index使得子句已经满足条件,那么父句则被忽略。3.如果一个单元的操作符是LIKE,那么将做下面的转换:xLIKE‘abc%’,转换成:x>=‘abc’ANDx<‘abd’。因为在SQLite中的LIKE是不能用索引进行优化的,所以如果存在索引的话,则转换后和不转换相差很远,因为对LIKE不起作用,但如果不存在索引,那么LIKE在效率方面也还是比不上转换后的效率的。三、几种查询语句的处理(复合查询)1.查询语句为: 上次讲到了SQLite的查询优化代码中的具体实现,现在来看一下它的几个实例:1#include\"stdio.h\"2#include\"sqlite3.h\"3#include 因篇幅问题不能全部显示,请点此查看更多更全内容