SQLite的小研究

一、简介
主要缺点
•SQLite 只提供数据库级的锁定,所以不支持高并发。
•不支持存储过程。
•SQLite?没有用户帐户概念,而是根据文件系统确
二、事务与锁

1、SQLite的锁
SQLite采用粗放型的锁。当一个连接要写数据库,所有其它的连接被锁住,直到写连接结束了它的事务。SQLite使用锁逐步上升机制,为了写数据库,连接需要逐级地获得排它锁。SQLite有一个加锁表,记录数据库连接的锁状态。每个数据库连接在同一时刻只能处于其中一个锁状态。每种状态(UNLOCKED)都有一种锁与之对应。
· unlocked:未加锁,等级最低,权限最小
· shared:共享锁
· reserved:保留锁
· pending:未决锁
· exclusive:排他锁,等级最高,权限最大

未加锁:
未和数据库建立连接、已建立连接但是还没访问数据库、已用BEGIN开始了一个事务但未开始读写数据库,处于这些情形时是未加锁状态。
共享:
为了能够从数据库中读(不写)数据,连接必须首先进入共享状态,也就是说首先要获得一个共享锁。多个连接可以 同时获得并保持共享锁,也就是说多个连接可以同时从同一个数据库中读数据。但哪怕只有一个共享锁还没有释放,也不允许任何连接写数据库
预留:
连接想要写数据库,它必须首先获得一个保留锁。一个数据库上同时只能有一个保留锁。保留锁可以与共享锁共存,保留锁是写数据库的第1阶段。保留锁即不阻止其它拥有共享锁的连接继续读数据库,也不阻止其它连接获得新的共享锁。获得预留锁后进入预留状态,这时会先在缓冲区中进行需要的修改、更新操作,操作后的结果依然保存在缓冲区中,未真正写入数据库。
未决:
连接从预留升为排它前,需要先升为未决,这时其它连接就不能获得共享锁了,但已经拥有共享锁的连接仍然可以继续正常读数据库,此时,拥有未决锁的连接等待其它拥有共享锁的连接完成工作并释放其共享锁后,提成到排它锁。
排它:
连接需要提交修改时,需要将预留锁升为排它锁,这时其它连接都无法获得任何锁,直到当前连接的排它状态结束

例子:读数据库和写数据库的过程中锁的变化
读:数据库连接最初处于unlocked状态,并且在连接到数据库甚至使用begin开始一个事务时,连接还处于unlocked状态。但是为了读取数据库,要获取shared锁,使连接必须进入shared状态。多个连接可以同时获得并保持shared锁,即多个连接可以同时从一个数据库中读数据,SQLite支持并发读取数据

写:一个连接要想写数据库,必须首先获得一个reserved锁(一个数据库同时只能有一个reserved锁,reserved锁可以与shared锁共存,reserved锁既不阻止其他拥有shared锁的连接继续读数据,也不阻止其他连接获得新的shared锁)。一旦一个连接获得了reserved锁,它就可以将数据写入缓冲区,注意不是实际地写到磁盘中。当连接想要提交修改(或事物)时,需要获得pending锁,之后连接就不能再获得新的shared锁,但已经拥有shared锁的连接仍然可以继续正常读取数据库。当所有其它的shared锁都被释放时,拥有pending锁的连接就可以将它的锁提升至esclusive锁,此时就可以将以前对缓冲区所做的修改写到数据库文件。所以SQLite不支持并发写操作。

2、事务
(1)事务是类似于原子操作,事务定义了一组SQL命令,这组命令要么全部执行,要么不执行,默认情况下,SQLite单条SQL语句自成事务(自动提交)。它由3个命令控制:begin、commit和rollback控制
-BEGIN开始一个事务,之后的所有操作都可以取消。
-COMMIT使BEGIN后的所有命令得到确认;
-ROLLBACK还原BEGIN之后的所有操作。

(2)SQLite有三种不同的事务
1. DEFERRED(推迟)
2. MMEDIATE(立即)
3. EXCLUSIVE(排它)
—>一个DEFERRED事务不获取任何锁(直到它需要锁的时候),BEGIN语句本身也不会做什么事情——它开始于UNLOCK状态。默认情况下就是这样的,如果仅仅用BEGIN开始一个事务,那么事务就是DEFERRED的,同时它不会获取任何锁;当对数据库进行第一次读操作时,它会获取 SHARED锁;同样,当进行第一次写操作时,它会获取RESERVED锁。

—>由BEGIN开始的IMMEDIATE事务会尝试获取RESERVED锁。如果成功,BEGIN IMMEDIATE保证没有别的连接可以写数据库。但是,别的连接可以对数据库进行读操作;但是,RESERVED锁会阻止其它连接的BEGIN IMMEDIATE或者BEGIN EXCLUSIVE命令,当其它连接执行上述命令时,会返回SQLITE_BUSY错误。这时你就可以对数据库进行修改操作了,但是你还不能提交,当你 COMMIT时,会返回SQLITE_BUSY错误,这意味着还有其它的读事务没有完成,得等它们执行完后才能提交事务。

—>EXCLUSIVE事务会试着获取对数据库的EXCLUSIVE锁。这与IMMEDIATE类似,但是一旦成功,EXCLUSIVE事务保证没有其它的连接,所以就可对数据库进行读写操作了。

上节那个例子的问题在于两个连接最终都想写数据库,但是它们都没有放弃各自原来的锁,最终,SHARED锁导致了问题的出现。如果两个连接都以 BEGIN IMMEDIATE开始事务,那么死锁就不会发生。在这种情况下,在同一时刻只能有一个连接进入BEGIN IMMEDIATE,其它的连接就得等待。BEGIN IMMEDIATE和BEGIN EXCLUSIVE通常被写事务使用。就像同步机制一样,它防止了死锁的产生。
基本的准则是:如果你正在使用的数据库没有其它的连接,用BEGIN就足够了。但是,如果你使用的数据库有其它的连接也会对数据库进行写操作,就得使用BEGIN IMMEDIATE或BEGIN EXCLUSIVE开始你的事务。

注意:预防死锁
考虑下面表4-7所假设的情况。两个连接——A和B——同时但完全独立地工作于同一个数据库。A执行第1条命令,B执行第2、3条,等等。
表4-7 一个死锁的假设情况
A连接 B连接
sqlite> BEGIN;
sqlite> BEGIN;
sqlite> INSERT INTO foo VALUES(‘x’);
sqlite> SELECT * FROM foo;
sqlite> COMMIT;
SQL error: database is locked
sqlite> INSERT INTO foo VALUES (‘x’);
SQL error: database is locked
两个连接都在死锁中结束。B首先尝试写数据库,也就拥有了一个未决锁。A再试图写,但当其INSERT语句试图将共享锁提升为保留锁时失败。
为了讨论的方便,假设连接A和B都一直等待数据库可写。那么此时,其它的连接甚至都不能够再读数据库了,因为B拥有未决锁(它能阻止其它连接获得共享锁)。那么时此,不仅A和B不能工作,其它所有进程都不能再操作此数据库了。
如果避免此情况呢?当然不能让A和B通过谈判解决,因为它们甚至不知道彼此的存在。答案是采用正确的事务类型来完成工作。

上节那个例子的问题在于两个连接最终都想写数据库,但是它们都没有放弃各自原来的锁,最终,SHARED锁导致了问题的出现。如果两个连接都以 BEGIN IMMEDIATE开始事务,那么死锁就不会发生。在这种情况下,在同一时刻只能有一个连接进入BEGIN IMMEDIATE,其它的连接就得等待。BEGIN IMMEDIATE和BEGIN EXCLUSIVE通常被写事务使用。就像同步机制一样,它防止了死锁的产生。

基本的准则是:如果你正在使用的数据库没有其它的连接,用BEGIN就足够了。但是,如果你使用的数据库有其它的连接也会对数据库进行写操作,就得使用BEGIN IMMEDIATE或BEGIN EXCLUSIVE开始你的事务。

3、冲突解决
冲突解决 说明
replace 违反的记录被删除,以新记录代替之
ignore 违反的记录保持原貌,其它记录继续执行
fail 终止命令,违反之前执行的操作得到保存
abort 终止命令,恢复违反之前执行的修改
rollback 终止命令和事务,回滚整个事务

4、WAL技术
SQLite在3.7.0开始引入了WAL技术,全称叫Write Ahead Log(预写日志)。
其原理是:修改并不直接写入到数据库文件中,而是写入到另外一个称为WAL的文件中;如果事务失败,WAL中的记录会被忽略,撤销修改;如果事务成功,它将在随后的某个时间被写回到数据库文件中,提交修改。
WAL使用检查点将修改写回数据库,默认情况下,当WAL文件发现有1000页修改时,将自动调用检查点。这个页数大小可以自行配置。
WAL技术带来以下优点:
· 读写操作不再互相阻塞,一定程度上解决了SQLite在处理高并发上的性能瓶颈
· 大多数场景中,带来很大的性能提升
· 磁盘I/O行为更容易被预测
WAL也有一些缺点,不过在iOS开发中影响不大,除非数据达到GB级时,性能才会降低。
如何启用WAL:
如果是直接使用SQLite,需要设置如下编译指示:
1 PRAGMA journal_mode=WAL;
如果是使用Core Data,则已经默认开启了WAL模式。

另外,read-uncommitted和WAL模式会影响这个锁的机制。在这2种模式下,读线程不会被写线程阻塞,即使写线程持有PENDING或EXCLUSIVE锁。

当一条语句被sqlite3_prepare() 或其相关的函数预编译后, sqlite3_step()必须被调用一次或多次来评估该预编译语句.
该函数的详细行为依赖于由sqlite3_prepare()(或其相关的函数) 产生的是一条怎样的预编译语句.

函数将返回一个以下的结果来标识其执行结果:
SQLITE_BUSY:忙碌. 数据库引擎无法锁定数据去完成其工作. 但可以多次尝试.
SQLITE_DONE:完成. sql 语句已经被成功地执行. 在调用 sqlite_reset() 之前, 当前预编译的语句不应该被sqlite3_step() 再次调用.
SQLITE_ROW:查询时产生了结果. 此时可以通过相关的”数据访问函数(column accessfunctions)”来取得数据. sqlite3_step() 的再一次调用将取得下一条查询结果.
SQLITE_ERROR:发生了错误. 此时可以通过 sqlite3_errmmsg() 取得相关的错误信息.sqlite3_step() 不能被再次调用.
SQLITE_MISUSE:不正确的库的使用. 该函数使用不当.
其它:

1.  @try{  
2.    
3.  charchar *errorMsg;  
4.    
5.  if (sqlite3_exec(_database, "BEGIN", NULL, NULL, &errorMsg)==SQLITE_OK) {  
6.    
7.  NSLog(@”启动事务成功”);  
8.    
9.  sqlite3_free(errorMsg);  
10.   
11.        sqlite3_stmt *statement;  
12.   
13. if (sqlite3_prepare_v2(_database, [@"insert into persons(name) values(?);" UTF8String], -1, &statement, NULL)==SQLITE_OK) {  
14.   
15. //绑定参数  
16.   
17. const charchar *text=[@”张三” cStringUsingEncoding:NSUTF8StringEncoding];  
18.   
19. sqlite3_bind_text(statement, index, text, strlen(text), SQLITE_STATIC);  
20.   
21.   
22. if (sqlite3_step(statement)!=SQLITE_DONE) {  
23.   
24. sqlite3_finalize(statement);  
25.   
26. }  
27.   
28. }  
29.   
30.   
31. if (sqlite3_exec(_database, "COMMIT", NULL, NULL, &errorMsg)==SQLITE_OK) {  
32.   
33. NSLog(@”提交事务成功”);  
34.   
35. }  
36.   
37. sqlite3_free(errorMsg);  
38.   
39. }else{  
40.   
41. sqlite3_free(errorMsg);  
42.   
43. }  
44.   
45. }  
46.   
47. @catch(NSException *e){  
48.   
49. charchar *errorMsg;  
50.   
51. if (sqlite3_exec(_database, "ROLLBACK", NULL, NULL, &errorMsg)==SQLITE_OK) {  
52.   
53. NSLog(@”回滚事务成功”);  
54.   
55. }  
56.   
57. sqlite3_free(errorMsg);  
58.   
59. }  
60.   
61. @finally{  
62.   
63. }  

5、iOS上的多线程
1.单线程:禁用所有的mutex锁,并发使用时会出错。当SQLite编译时加了SQLITE_THREADSAFE=0参数,或者在初始化SQLite前调用sqlite3_config(SQLITE_CONFIG_SINGLETHREAD)时启用。
2.多线程:只要一个数据库连接不被多个线程同时使用就是安全的。源码中是启用bCoreMutex,禁用bFullMutex。实际上就是禁用数据库连接和prepared statement(准备好的语句)上的锁,因此不能在多个线程中并发使用同一个数据库连接或prepared statement。当SQLite编译时加了SQLITE_THREADSAFE=2参数时默认启用。若SQLITE_THREADSAFE不为0,可以在初始化SQLite前,调用sqlite3_config(SQLITE_CONFIG_MULTITHREAD)启用;或者在创建数据库连接时,设置SQLITE_OPEN_NOMUTEX flag。
3.串行:启用所有的锁,包括bCoreMutex和bFullMutex。因为数据库连接和prepared statement都已加锁,所以多线程使用这些对象时没法并发,也就变成串行了。当SQLite编译时加了SQLITE_THREADSAFE=1参数时默认启用。若SQLITE_THREADSAFE不为0,可以在初始化SQLite前,调用sqlite3_config(SQLITE_CONFIG_SERIALIZED)启用;或者在创建数据库连接时,设置SQLITE_OPEN_FULLMUTEX flag。

  在iOS上,默认使用的是第2种方式编译的,也就是只有一个线程能够打开数据库操作,其他线程要操作数据库必须等数据库关闭后才能打开操作。多线程时:每个线程独立打开数据库,操作数据库,操作完后关闭数据库。打开和关闭都比较费时间,而且要手动控制打开关闭锁,在每个线程操作不频率时可用该方法。
  如果多个线程频繁操作数据库,使用以上方法很容易造成系统崩溃,解决方案:开启第3种串行模式,使用一个类(单例方式)操作数据库。

-(BOOL)open
{
    sqlite3_shutdown();
    NSLog(@"sqlite3 lib version: %s", sqlite3_libversion());
    int err=sqlite3_config(SQLITE_CONFIG_SERIALIZED);
    if (err == SQLITE_OK) {
        NSLog(@"Can now use sqlite on multiple threads, using the same connection");
    } else {
        NSLog(@"setting sqlite thread safe mode to serialized failed!!! return code: %d", err);
    }
    err = sqlite3_open([VersionDBPath UTF8String], &hSqlite3DB);
    if(err != SQLITE_OK) {
        NSLog(@"datebase open error: %d", err);
        return NO;
    }
    return YES;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员的修养

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值