相关接口说明:
sqlite3_open()
通常这个函数被第一个调用,这个操作打开一个数据库文件的链接,输出一个database connection对象,database connection对象在调用之后的接口时使用。
函数原型:
int sqlite3_open(
const char *filename, /*Database filename (UTF-8) */
sqlite3 **ppDb /* OUT:SQLite db handle */
);
int sqlite3_open16(
const void *filename, /*Database filename (UTF-16) */
sqlite3 **ppDb /* OUT:SQLite db handle */
);
int sqlite3_open_v2(
constchar *filename, /* Database filename(UTF-8) */
sqlite3 **ppDb, /* OUT:SQLite db handle */
intflags, /* Flags */
const char *zVfs /* Name ofVFS module to use */
);
通过filename参数指定要打开的数据库,sqlite3_open()和sqlite3_open_v2()的filename参数是utf8编码格式,sqlite3_open16()的filename参数是UTF-16编码。
如果成功,sqlite3*被创建,返回值SQLITE_OK,如果失败,返回错误码,调用sqlite3_errmsg() 或 sqlite3_errmsg16()将得到错误信息。
无论成功与否, database connection句柄资源已经被分配,在不使用时应该将数据库连接句柄传递给sqlite3_close()释放资源。
sqlite3_open_v2的第三个参数,有3个值和其他的值(SQLITE_OPEN_NOMUTEX, SQLITE_OPEN_FULLMUTEX, SQLITE_OPEN_SHAREDCACHE,SQLITE_OPEN_PRIVATECACHE, and/or SQLITE_OPEN)组合:
SQLITE_OPEN_READONLY
SQLITE_OPEN_READWRITE
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE
如何不是用上面的3个值之一和其他值组合,其行为是未定义的。
sqlite3_prepare()
这个操作第一个参数输入database connection对象,第二个参数输入sql语句,该接口将输入的sql语句转变成prepared statement对象,输出prepared statement对象,在调用之后的接口时使用,记住,这个函数不执行sql语句,只为执行sql做准备。
函数原型:
int sqlite3_prepare(
sqlite3 *db, /*Database handle */
const char *zSql, /* SQLstatement, UTF-8 encoded */
intnByte, /* Maximum length ofzSql in bytes. */
sqlite3_stmt **ppStmt, /* OUT:Statement handle */
const char **pzTail /* OUT:Pointer to unused portion of zSql */
);
int sqlite3_prepare_v2(
sqlite3 *db, /*Database handle */
const char *zSql, /* SQLstatement, UTF-8 encoded */
intnByte, /* Maximum length of zSql in bytes. */
sqlite3_stmt **ppStmt, /* OUT:Statement handle */
const char **pzTail /* OUT:Pointer to unused portion of zSql */
);
int sqlite3_prepare16(
sqlite3 *db, /*Database handle */
const void *zSql, /* SQLstatement, UTF-16 encoded */
intnByte, /* Maximum length ofzSql in bytes. */
sqlite3_stmt **ppStmt, /* OUT:Statement handle */
const void **pzTail /* OUT:Pointer to unused portion of zSql */
);
int sqlite3_prepare16_v2(
sqlite3 *db, /*Database handle */
const void *zSql, /* SQLstatement, UTF-16 encoded */
intnByte, /* Maximum length ofzSql in bytes. */
sqlite3_stmt **ppStmt, /* OUT:Statement handle */
const void **pzTail /* OUT: Pointer to unused portion of zSql*/
);
sqlite3_step()
这个操作执行sqlite3_prepare()接口返回的prepared statement,如果是SELECT操作,结果集的第一行将被返回,如果想得到第二行,还须再调用一次该函数,也就是每调用一次返回一行,直到完成。
有些操作仅需调用1次该函数,如:INSERT, UPDATE, DELETE语句。
使用老接口得到的statement,执行sqlite3_step返回SQLITE_BUSY, SQLITE_DONE, SQLITE_ROW, SQLITE_ERROR, or SQLITE_MISUSE;
使用v2接口得到的statement,执行sqlite3_step返回任意的result code或extended result codes。
sqlite3_column()
sqlite3接口并没有sqlite3_column()函数,这里所说的是一组函数:
sqlite3_column_blob()
sqlite3_column_bytes()
sqlite3_column_bytes16()
sqlite3_column_count()
sqlite3_column_double()
sqlite3_column_int()
sqlite3_column_int64()
sqlite3_column_text()
sqlite3_column_text16()
sqlite3_column_type()
sqlite3_column_value()
这个操作输入执行过sqlite3_step()的prepared statement对象,返回单列的结果,要得到一行中全部列的值需要调用多次这个函数。
sqlite3_finalize()
销毁之前调用sqlite3_prepare()创建的prepared statement,prepared statement必须由该函数销毁,否则会导致内存泄露。
sqlite3_close()
关闭之前调用sqlite3_open()创建的database connection,在调用该函数之前,必须保证所有的prepared statements都被finalized。
开发SQLite应用一般的流程:
首先用sqlite3_open()创建一个数据库连接,该函数根据输入参数可以打开一个已存在的数据库,也可以创建一个新的数据库文件。不用数据库的时候记得调用 sqlite3_close() 关闭数据库连接。
执行一个SQL statement,有以下步骤:
1. 调用sqlite3_prepare()创建 preparedstatement。
2. 调用sqlite3_step()一次或多次执行preparedstatement。
3. 对于查询操作,在两次sqlite3_step()之间调用sqlite3_column()获取结果。
4. 调用sqlite3_finalize()销毁preparedstatement。
在调用 sqlite3_step()之后,可以调用sqlite3_reset()初始化prepared statement。
很多时候,每次调用的sql语句都是相似的,比如使用INSERT语句,每次执行只是插入不同的值,为了适应这种灵活性,SQLite允许SQL statements带参数,在执行statements前给这些参数绑定一个值,绑定的值在 prepared statement第二次执行的时候还可以改变。
获得错误信息的函数:
int sqlite3_errcode(sqlite3 *db);
int sqlite3_extended_errcode(sqlite3 *db);
const char *sqlite3_errmsg(sqlite3*);
const void *sqlite3_errmsg16(sqlite3*);
三.SQLite3的线程模式
sqlite支持3种不同的线程模式:
Single-thread:这种模式下,所有的互斥被禁用,多线程使用sqlite是不安全的。
Multi-thread:这种模式下,sqlite可以安全地用于多线程,但多个线程不能共享一个database connect。
Serialized:这种模式下,sqlite可以安全地用于多线程,无限制。
sqlite线程模式的选择可以在编译时(当SQLitelibrary 源码被编译时)或启动时(使用SQLite的应用初始化时)或运行时(新的 database connection被创建时)。
一般来说,运行时覆盖启动时,启动时覆盖编译时,不过,Single-thread模式一旦被选择了,就不能被重改。
编译时设置线程模式:
用SQLITE_THREADSAFE选择线程模式,
如果SQLITE_THREADSAFE没有被设置或设置了-DSQLITE_THREADSAF=1,则是Serialized模式;
如果设置了-DSQLITE_THREADSAF=0,线程模式是Single-thread;
如果设置了-DSQLITE_THREADSAF=2,线程模式是Multi-thread.
启动时设置线程模式:
假设编译时线程模式被设置为非Single-thread模式,在初始化时可以调用sqlite3_config()改变线程模式,参数可以为SQLITE_CONFIG_SINGLETHREA,SQLITE_CONFIG_MULTITH,SQLITE_CONFIG_SERIALIZED.
运行时设置线程模式:
如果Single-thread模式在编译时和启动时都没被设置,则可以在database connections被创建时设置Multi-thread或Serialized模式,但不可以降级设置为Single-thread模式,也不可以升级编译时或启动时设置的Single-thread模式。
sqlite3_open_v2()的第三个参数决定单个databaseconnection的线程模式,SQLITE_OPEN_NOMUTEX使database connection为Multi-thread模式,SQLITE_OPEN_FULLMUTE使database connection为Serialized模式。
如果不指定模式或使用sqlite3_open()或sqlite3_open16()接口,线程模式为编译时或启动时设置的模式。
在IOS上的示例:
先说下初衷吧,实际上我经常看到有人抱怨SQLite不支持多线程。而在iOS开发时,为了不阻塞主线程,数据库访问必须移到子线程中。为了解决这个矛盾,很有必要对此一探究竟。
关于这个问题,最权威的解答当然是SQLite官网上的“Is SQLite threadsafe?”这个问答。
简单来说,从3.3.1版本开始,它就是线程安全的了。而iOS的SQLite版本没有低于这个版本的:
3.4.0 - iPhone OS 2.2.1当然,你也可以自己编译最新版本。只是我发现自己编译出来的3.7.8居然比iOS 4.3.3内置的3.7.2慢了一半,不知道苹果做了什么优化。 发现是我编译成了debug版本,改成release后性能比内置版本高5%左右,不过构建出来的app会大420k左右。
3.6.12 - iPhone OS 3.0 / 3.1
3.6.22 - iPhone OS 4.0
3.6.23.2 - iPhone OS 4.1 / 4.2
3.7.2 - iPhone OS 4.3
3.7.7 - iPhone OS 5.0
# -*- coding: utf-8 -*- import sqlite3 import threading def f(): con.rollback() con = sqlite3.connect('test.db', check_same_thread=False) # 允许在其他线程中使用这个连接 cu = con.cursor() cu.execute('CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY)') print cu.execute('SELECT count(*) FROM test').fetchone()[0] # 0 cu.execute('INSERT INTO test VALUES (NULL)') print cu.execute('SELECT count(*) FROM test').fetchone()[0] # 1 thread = threading.Thread(target=f) thread.start() thread.join() print cu.execute('SELECT count(*) FROM test').fetchone()[0] # 0 cu.close() con.close()
在这个例子中,虽然是在子线程中执行rollback,但由于和主线程用的是同一个数据库连接,所以主线程所做的更改也被回滚了。连接1:BEGIN (UNLOCKED)现在2个连接都在等待对方释放锁,于是就死锁了。当然,实际情况并没那么糟糕,任何一方选择不继续等待,回滚事务就行了。
连接1:SELECT ... (SHARED)
连接1:INSERT ... (RESERVED)
连接2:BEGIN (UNLOCKED)
连接2:SELECT ... (SHARED)
连接1:COMMIT (PENDING,尝试获取EXCLUSIVE锁,但还有SHARED锁未释放,返回SQLITE_BUSY)
连接2:INSERT ... (尝试获取RESERVED锁,但已有PENDING锁未释放,返回SQLITE_BUSY)
连接1:BEGIN IMMEDIATE (RESERVED)这样死锁就被避免了。
连接1:SELECT ... (RESERVED)
连接1:INSERT ... (RESERVED)
连接2:BEGIN IMMEDIATE (尝试获取RESERVED锁,但已有RESERVED锁未释放,因此事务开始失败,返回SQLITE_BUSY,等待用户重试)
连接1:COMMIT (EXCLUSIVE,写入完成后释放)
连接2:BEGIN IMMEDIATE (RESERVED)
连接2:SELECT ... (RESERVED)
连接2:INSERT ... (RESERVED)
连接2:COMMIT (EXCLUSIVE,写入完成后释放)
连接1:BEGIN EXCLUSIVE (EXCLUSIVE)不过在并发很高的情况下,直接获取EXCLUSIVE锁的难度比较大;而且为了避免EXCLUSIVE状态长期阻塞其他请求,最好的方式还是让所有写事务都以IMMEDIATE方式开始。
连接1:SELECT ... (EXCLUSIVE)
连接1:INSERT ... (EXCLUSIVE)
连接2:BEGIN (UNLOCKED)
连接2:SELECT ... (尝试获取SHARED锁,但已有EXCLUSIVE锁未释放,返回SQLITE_BUSY,等待用户重试)
连接1:COMMIT (EXCLUSIVE,写入完成后释放)
连接2:SELECT ... (SHARED)
连接2:INSERT ... (RESERVED)
连接2:COMMIT (EXCLUSIVE,写入完成后释放)
#import <sqlite3.h> static char dbPath[200]; static sqlite3 *database; static sqlite3 *openDb() { if (sqlite3_open(dbPath, &database) != SQLITE_OK) { sqlite3_close(database); NSLog(@"Failed to open database: %s", sqlite3_errmsg(database)); } return database; } - (void)viewDidLoad { [super viewDidLoad]; sqlite3_config(SQLITE_CONFIG_SINGLETHREAD); NSLog(@"%d", sqlite3_threadsafe()); NSLog(@"%s", sqlite3_libversion()); NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; strcpy(dbPath, [[documentsDirectory stringByAppendingPathComponent:@"data.sqlite3"] UTF8String]); database = openDb(); char *errorMsg; if (sqlite3_exec(database, "CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY AUTOINCREMENT, value INTEGER);", NULL, NULL, &errorMsg) != SQLITE_OK) { NSLog(@"Failed to create table: %s", errorMsg); } }
static void insertData() { char *errorMsg; if (sqlite3_exec(database, "BEGIN TRANSACTION", NULL, NULL, &errorMsg) != SQLITE_OK) { NSLog(@"Failed to begin transaction: %s", errorMsg); } static const char *insert = "INSERT INTO test VALUES (NULL, ?);"; sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, insert, -1, &stmt, NULL) == SQLITE_OK) { for (int i = 0; i < 1000; ++i) { sqlite3_bind_int(stmt, 1, arc4random()); if (sqlite3_step(stmt) != SQLITE_DONE) { --i; NSLog(@"Error inserting table: %s", sqlite3_errmsg(database)); } sqlite3_reset(stmt); } sqlite3_finalize(stmt); } if (sqlite3_exec(database, "COMMIT TRANSACTION", NULL, NULL, &errorMsg) != SQLITE_OK) { NSLog(@"Failed to commit transaction: %s", errorMsg); } static const char *query = "SELECT count(*) FROM test;"; if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { NSLog(@"Table size: %d", sqlite3_column_int(stmt, 0)); } else { NSLog(@"Failed to read table: %s", sqlite3_errmsg(database)); } sqlite3_finalize(stmt); } }
static dispatch_queue_t queue; - (void)viewDidLoad { // ... queue = dispatch_queue_create("net.keakon.db", NULL); }
static int lastReadCount = 0; static int readCount = 0; static int lastWriteCount = 0; static int writeCount = 0; - (void)count { int lastRead = lastReadCount; int lastWrite = lastWriteCount; lastReadCount = readCount; lastWriteCount = writeCount; NSLog(@"%d, %d", lastReadCount - lastRead, lastWriteCount - lastWrite); } - (void)viewDidLoad { // ... [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(count) userInfo:nil repeats:YES]; }
static void readData() { static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;"; void (^ __block readBlock)() = Block_copy(^{ sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) { sqlite3_bind_int(stmt, 1, arc4random()); int returnCode = sqlite3_step(stmt); if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) { ++readCount; } sqlite3_finalize(stmt); } else { NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); } dispatch_async(queue, readBlock); }); dispatch_async(queue, readBlock); } static void writeData() { static const char *update = "UPDATE test SET value = ? WHERE id = ?;"; void (^ __block writeBlock)() = Block_copy(^{ sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, update, -1, &stmt, NULL) == SQLITE_OK) { sqlite3_bind_int(stmt, 1, arc4random()); sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1); if (sqlite3_step(stmt) == SQLITE_DONE) { ++writeCount; } sqlite3_finalize(stmt); } else { NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); } dispatch_async(queue, writeBlock); }); dispatch_async(queue, writeBlock); }
这里是用dispatch_async()来异步地递归调用block。if (sqlite3_exec(database, "PRAGMA journal_mode=WAL;", NULL, NULL, &errorMsg) != SQLITE_OK) { NSLog(@"Failed to set WAL mode: %s", errorMsg); } sqlite3_wal_checkpoint(database, NULL); // 每次测试前先checkpoint,避免WAL文件过大而影响性能
测试结果为只读时平均每秒166次,只写时每秒244次,同时读写时每秒各97次。并发性增加了1倍有木有!更夸张的是写入比读取还快了。static sqlite3 *openDb() { sqlite3 *database = NULL; if (sqlite3_open(dbPath, &database) != SQLITE_OK) { sqlite3_close(database); NSLog(@"Failed to open database: %s", sqlite3_errmsg(database)); } return database; }
sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
static void readData() { static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;"; dispatch_async(queue, ^{ sqlite3 *database = openDb(); sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) { while (YES) { sqlite3_bind_int(stmt, 1, arc4random()); int returnCode = sqlite3_step(stmt); if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) { ++readCount; } sqlite3_reset(stmt); } sqlite3_finalize(stmt); } else { NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); } sqlite3_close(database); }); } static void writeData() { static const char *update = "UPDATE test SET value = ? WHERE id = ?;"; dispatch_async(queue, ^{ sqlite3 *database = openDb(); sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, update, -1, &stmt, nil) == SQLITE_OK) { while (YES) { sqlite3_bind_int(stmt, 1, arc4random()); sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1); if (sqlite3_step(stmt) == SQLITE_DONE) { ++writeCount; } sqlite3_reset(stmt); } sqlite3_finalize(stmt); } else { NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); } sqlite3_close(database); }); }
这里就无需递归调用了,直接在子线程中循环即可。static void readData() { static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;"; dispatch_async(queue, ^{ sqlite3 *database = openDb(); sqlite3_stmt *stmt; while (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) != SQLITE_OK); while (YES) { sqlite3_bind_int(stmt, 1, arc4random()); int returnCode = sqlite3_step(stmt); if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) { ++readCount; } sqlite3_reset(stmt); } sqlite3_finalize(stmt); sqlite3_close(database); }); } static void writeData() { static const char *update = "UPDATE test SET value = ? WHERE id = ?;"; dispatch_async(queue, ^{ sqlite3 *database = openDb(); sqlite3_stmt *stmt; while (sqlite3_prepare_v2(database, update, -1, &stmt, nil) != SQLITE_OK); while (YES) { sqlite3_bind_int(stmt, 1, arc4random()); sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1); if (sqlite3_step(stmt) == SQLITE_DONE) { ++writeCount; } sqlite3_reset(stmt); } sqlite3_finalize(stmt); sqlite3_close(database); }); }
结果为只读时平均每秒169次,只写时每秒246次,同时读写时每秒分别为90和57次(波动较大)。并发效率有了显著提升,但仍不及第二种方式。sqlite3_config(SQLITE_CONFIG_SERIALIZED);
static void readData() { static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;"; dispatch_async(queue, ^{ sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) { while (YES) { sqlite3_bind_int(stmt, 1, arc4random()); int returnCode = sqlite3_step(stmt); if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) { ++readCount; } sqlite3_reset(stmt); } sqlite3_finalize(stmt); } else { NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); } }); } static void writeData() { static const char *update = "UPDATE test SET value = ? WHERE id = ?;"; dispatch_async(queue, ^{ sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, update, -1, &stmt, NULL) == SQLITE_OK) { while (YES) { sqlite3_bind_int(stmt, 1, arc4random()); sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1); if (sqlite3_step(stmt) == SQLITE_DONE) { ++writeCount; } sqlite3_reset(stmt); } sqlite3_finalize(stmt); } else { NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); } }); }
测试结果为只读时平均每秒164次,只写时每秒68次,同时读写时每秒分别为57和43次。读线程比写线程的速率更高,而且新线程的加入不需要等待。最后总结:
1、如果是SQLITE_OPEN_FULLMUTEX,也就是串行化方式,则对于连接时互斥的,只有一个连接关闭,另外一个连接才能读写
2、如果是SQLITE_OPEN_NOMUTEX,则是多线程模式,对于写是互斥的,但是如果一个连接持续写,另外一个连接是无法写入的,只能是错误或者超时返回。不过一个连接写,多个连接读,是没问题的。windows版本模式是SQLITE_OPEN_NOMUTEX
3、如果要多线程写并发,只有一个办法,就是连接->写->关闭连接,而且需要开启超时sqlite3_busy_timeout,不过这样效率很低,因为一条一条写入的话,利用不上事务
4、sqlite通过事务插入效率还可以,大约就是1000条/秒
5、sqlite的数据类型操作非常灵活,可以写入任意自定义类型
6、如果要断电等意外也完全保证数据完整性,PRAGMA synchronous=FULL,对于大数据量提交,性能和synchronous=OFF相差很小。
7、对于大数据量写入,例如一次提交100MB以上的事务,设置cache_size很有必要,默认是例如:PRAGMA cache_size=400000,有时候提交速度会成倍提升。
8、sqlite事务的insert或者update等是很快的,但是commit是很慢的,例如提交200MB的事务,在win7+酷睿T9300+4GB内存+7200转磁盘+10GB的数据库上,commit会花费5分钟左右,在usb3.0的上U盘上,花费大约15分钟,在USB2.0的U盘上,1个半小时后还没有完成(没有等到结果,因为是NTFS格式,造成U盘写入太频繁,产生了坏道,尝试了两个U盘都是如此,格式化为exFAT就没问题)
9、对于commit意外退出,大数据量的时候,例如200MB,下次再次进入的时候,哪怕只是一个select,为了保证数据完整性,sqlite都要经过很长时间(和commit完成时间差不多)的rollback才能返回select结果,如果自作主张删除临时的journal文件,则会造成数据库崩溃。