SQLiteDatabase4 连接建立、写入DB —— Native简介

Basics

VFS

Virtual File System,虚拟文件系统通过Unix上标准的系统调用(Syscall)来读写位于不同物理介质上的不同文件系统,为各类文件系统提供了一套统一的API接口。

文件系统一般需要经历几个阶段来具备被操作系统读写的能力:

  • 创建:通过某种方式将磁盘(Disk)格式化并建立文件系统。在建立文件系统时还会让磁盘里写入文件系统的控制信息。
  • 注册:各文件系统声明自己能够被操作系统内核支持,使内核能够记录文件系统的有关控制信息。注册又分为静态注册动态注册。静态注册是指在编译内核时进行注册;动态注册指通过加载模块的方式进行注册。
  • 安装:对应的是平时的mount操作,将文件系统挂载到操作系统根文件系统的目录树上。在这时候,操作系统便可以正常读写文件系统。

在这之后,VFS接口便可以对该文件系统进行读写。
以 sys_read() 的层次为例,可以看到通过 vfs_read() 的转接,VFS向 sys_read() 屏蔽了具体文件系统读写方法的细节。以编程设计模式的角度来看,就像是在具体文件系统的 read() 上面加了一个 代理层(Proxy),代理还可以将读写操作转到不同的文件系统上。

BTree

引用自wikipedia的简介:

在计算机科学中,B树(英语:B-tree)是一种自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。B树,概括来说是一个一般化的二叉查找树(binary search tree),可以拥有多于2个子节点。与自平衡二叉查找树不同,B树为系统大块数据的读写操作做了优化。B树减少定位记录时所经历的中间过程,从而加快存取速度。B树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库和文件系统的实现上。

Pager

Pager 模块负责处理SQLite的并发操作控制。同时Pager 确保SQLite是”ACID” (Atomic 原子性的,Consistent 一致性的,Isolated 隔离的,Durable 持久性的),以便支持对事务(Transaction)的处理。我们对数据库文件的操作并不会直接作用到数据库文件本身,而是先与 Pager 打交道,再由 Pager 将修改写入到数据库文件。

结构

Pager 将SQLite的.db文件划分成尺寸大小和规格一致的块(Blocks),每一个块也叫做 “Page”,每一个块的大小一般是1024字节。

在Unix系统上,Pager 通过 os_unix.c来为操作系统提供抽象的操作接口。

多进程/多线程访问

通过以下一些机制,Pager 能够高效地控制多进程/多线程条件下对SQLite数据库文件的访问。

通过加锁,Pager 能够确保进程/线程对数据库文件进行正确地操作,锁有可能处于几个状态当中:

  • UNLOCKED: db没有加锁,这样的db不用从中读取数据也不用向其中写入数据,该db中的数据被认为是可疑的。
  • SHARED: 该db允许读取数据但不允许写入数据,同一时间可以有多个进程通过持有该锁而从db中读取数据。但是当db的 SHARED 锁被持有时,不允许同一进程中的其他线程或者其他进程对db进行写入操作。
  • RESERVED:该锁同时只能被一个进程所获取。当一个进程正在从db中读取数据,并打算在未来的某一个时刻对db进行写入时可以获取该锁。RESERVED 锁存在时获取其他进程获取 SHARED 锁以便对db进行读取。
  • PENDING:一个进程允许获取该锁以便准备接下来对db的写入;如果db的 PENDING 锁被持有,则该db不允许新的进程或者线程获取 SHARED 锁。获取 PENDING 锁的进程并不能立即写入db,而是必须等待其他早已经被获得的 SHARED 执行完毕,之后该进程会获得 EXCLUSIVE 锁进行真正的写入。
  • EXCLUSIVE:获取该锁后便可以进行真正地写入;只有 EXCLUSIVE 锁能够写入db;EXCLUSIVE 是唯一一个互斥的,当一个 EXCLUSIVE 存在时,不允许存在其他锁 (UNLOCKED、SHARED、RESERVED)。

写入数据库文件

Rollback 记录

Rollback Journal 机制保证了在数据库操作出错时数据库文件的完整性。

当Pager 修改数据库文件之前,会先保存一份当前数据库内容的记录,叫作 Rollback Journal 到当前数据库文件所在的目录下,文件名为 XXX(db name)-journal。除了保存数据库内容,还会保存当前数据库的文件大小,以便后面在写入数据库出错时能够正确恢复。

Pager 需要Rollback Journal来恢复数据库文件时,Rollback Journal会被认为处于 hot 状态。当进程/线程在对数据库文件进行修改过程中因为 进程或者系统Crash 或者 突然断电 等原因而终止,Pager 会创建一个类型为 hot journal 的记录文件来帮助恢复。
当SQLite想要尝试从数据库文件中读取数据时,会先检查对应的 hot journal是否存在。如存在,则在读取数据之前会先执行数据库文件的rollback恢复操作。

锁获取

对于一个想要写入数据库文件的进程,它必须按照以下顺序来获取锁:

1
SHARED --> RESERVED --> PENDING --> EXCLUSIVE

当一个进程/线程获取RESERVED锁之后,其他进程/线程无法获取RESERVED锁,其他进程/线程的数据库写入操作会执行失败,并返回SQLITE_BUSY错误。
在获取RESERVED锁之前,想要写入数据库文件的进程/线程会先创建一个 Rollback Journal,并将数据库中的内容都写入到 Rollback Journal 当中,此时其他进程/线程依旧可以从数据库文件中读取数据。

写入
  • 进程对 Page 的修改并不会立即写入到磁盘中而是保留在缓存上。只有当缓存中满了或者事务提交了commit之后才会写入到磁盘中的数据库文件中。此时进程/线程需要等待其他已经被获得的 SHARED锁 的读取执行完毕(获取 PENDING锁,等待获取EXCLUSIVE锁),并确保 Rollback Journal 已经完整创建。
  • 如果因为缓存满而需要写入数据库文件,进程/线程并不会立即commit事务,而是会继续执行对其他 Page 的修改。因此,进程/线程所获取的 EXCLUSIVE锁 的生命周期可能非常长,从一开始执行写入直到最后事务执行commit。
  • commit之后会立即删除 Rollback Journal(如果在写入过程中操作系统Crash会创建 Hot Journal)。当执行完将所有修改真正写入到磁盘中之后,便会释放EXCLUSIVEPENDING。释放PENDING锁之后其他进程/线程便可以尝试获取SHARED锁从而读取最新的数据库文件中的数据。

创建连接、写入数据库源码解析

SQLite暴露了诸多接口给Framework,从连接建立到读写再到事务操作。

1
2
3
4
5
6
7
8
9
10
// /framework/base/core/java/android/database/sqlite/SQLiteConnection.java
// 平时Framework对数据库操作最终都是由这些JNI接口转寄到SQLite中
private static native int nativeOpen(String path, int openFlags, String label,
boolean enableTrace, boolean enableProfile);
private static native void nativeClose(int connectionPtr);
...
private static native long nativeExecuteForLastInsertedRowId(
int connectionPtr, int statementPtr);
...
private static native void nativeResetCancel(int connectionPtr, boolean cancelable);

篇幅所限,这里只介绍连接建立(数据库连接的存在是Framework操作DB的基础)和写入DB(经历 Pager 锁的全部状态、Rollback Journal,相对读取更复杂)。

连接建立

nativeOpen(),对应的JNI接口实现在 /framework/base/core/jni/android_database_SQLiteConnection.cpp 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static jint nativeOpen(JNIEnv* env, jclass clazz, jstring pathStr, jint openFlags,
jstring labelStr, jboolean enableTrace, jboolean enableProfile) {
... //sqliteFlags 检查和字符转换
sqlite3* db;
//通过 sqlite3_open_v2 方式打开数据库,还有sqlite3_open、sqlite3_open16等方式
int err = sqlite3_open_v2(path.string(), &db, sqliteFlags, NULL);
if (err != SQLITE_OK) {
throw_sqlite3_exception_errcode(env, err, "Could not open database");
return 0;
}
...
//1. 如果请求的拥有读写功能数据库连接,则需要检查数据库对象是否真的可以读写
//2. 设置busy handler来处理SQLITE_BUSY
//3. 注册android function

//将数据库对象包装到数据库连接当中
SQLiteConnection* connection = new SQLiteConnection(db, openFlags, path, label);
... //设置 tracing、profiling 等功能
//返回
return reinterpret_cast<jint>(connection);
}

无论使用何种方式打开数据库,最后都会进入到openDatabase()
/external/sqlite/dist/sqlite3.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
static int openDatabase(
const char *zFilename, /* Database filename UTF-8 encoded */
sqlite3 **ppDb, /* OUT: Returned database handle */
unsigned int flags, /* Operational flags */
const char *zVfs /* Name of the VFS to use */
){
sqlite3 *db; /* Store allocated handle here */
int rc; /* Return code */
int isThreadsafe; /* True for threadsafe connections */
char *zOpen = 0; /* Filename argument to pass to BtreeOpen() */
char *zErrMsg = 0; /* Error message from sqlite3ParseUri() */
*ppDb = 0;

... //flag设置
// SQLite DB 分配内存
db = sqlite3MallocZero( sizeof(sqlite3) );
//分配失败则跳转到 opendb_out
if( db==0 ) goto opendb_out;
if( isThreadsafe ){ // 没有设置有关线程安全的标识符因此跟具体的SQLite配置有关
db->mutex = sqlite3MutexAlloc(SQLITE_MUTEX_RECURSIVE);
if( db->mutex==0 ){
sqlite3_free(db);
db = 0;
goto opendb_out;
}
}
//锁上db.mutex,再进入后面的执行
sqlite3_mutex_enter(db->mutex);
//两个db的backend, 第一个是主要操作的main, 第二个是temp,后面会看到
db->nDb = 2;
db->aDb = db->aDbStatic;
... //设置db一些参数
// 创建排序函数
createCollation(db, "BINARY", SQLITE_UTF8, 0, binCollFunc, 0);
createCollation(db, "BINARY", SQLITE_UTF16BE, 0, binCollFunc, 0);
createCollation(db, "BINARY", SQLITE_UTF16LE, 0, binCollFunc, 0);
createCollation(db, "RTRIM", SQLITE_UTF8, (void*)1, binCollFunc, 0);
if( db->mallocFailed ){ //分配内存失败,跳转到opendb_out
goto opendb_out;
}
// 默认的排序函数为 BINARY
db->pDfltColl = sqlite3FindCollSeq(db, SQLITE_UTF8, "BINARY", 0);
assert( db->pDfltColl!=0 );
// 创建UTF-8 大小写不敏感的排序函数
createCollation(db, "NOCASE", SQLITE_UTF8, 0, nocaseCollatingFunc, 0);
// 将数据库文件名、指定的VFS、flag 等信息转换成对应的 Uri
db->openFlags = flags;
rc = sqlite3ParseUri(zVfs, zFilename, &flags, &db->pVfs, &zOpen, &zErrMsg);
if( rc!=SQLITE_OK ){ //Uri parse 出错
if( rc==SQLITE_NOMEM ) db->mallocFailed = 1;
sqlite3Error(db, rc, zErrMsg ? "%s" : 0, zErrMsg);
sqlite3_free(zErrMsg);
goto opendb_out;
}
//打开数据库驱动并将其指向到 第一个database backend的pBt上
//这里是真实打开数据库文件的地方,使用BTree的方式打开数据库文件
//将创建的BTree 放到db->aDb[0].pBt当中。
rc = sqlite3BtreeOpen(db->pVfs, zOpen, db, &db->aDb[0].pBt, 0,
flags | SQLITE_OPEN_MAIN_DB);
if( rc!=SQLITE_OK ){
if( rc==SQLITE_IOERR_NOMEM ){
rc = SQLITE_NOMEM;
}
sqlite3Error(db, rc, 0);
goto opendb_out;
}
//获取database backend 对应的数据库模式
db->aDb[0].pSchema = sqlite3SchemaGet(db, db->aDb[0].pBt);
db->aDb[1].pSchema = sqlite3SchemaGet(db, 0);
db->aDb[0].zName = "main";
db->aDb[0].safety_level = 3;
db->aDb[1].zName = "temp";
db->aDb[1].safety_level = 1;
...
//注册内建函数到db中,这个时候还没有从db中读取内容,
//第一次读取内容将是在db对象第一次被访问的时候
sqlite3RegisterBuiltinFunctions(db);
...
// 如何退出
opendb_out:
sqlite3_free(zOpen);
if( db ){
assert( db->mutex!=0 || isThreadsafe==0 || sqlite3GlobalConfig.bFullMutex==0 );
// 有控制线程安全的锁则需要退出
sqlite3_mutex_leave(db->mutex);
}
rc = sqlite3_errcode(db);
assert( db!=0 || rc==SQLITE_NOMEM );
if( rc==SQLITE_NOMEM ){
// 没有空间创建数据库对象,执行数据库关闭,释放资源
sqlite3_close(db);
db = 0;
}else if( rc!=SQLITE_OK ){
db->magic = SQLITE_MAGIC_SICK;
}
//让ppDb指针指向db,供调用方使用。
*ppDb = db;
return sqlite3ApiExit(0, rc);
}

在SQLite3中,使用BTree 作为存储引擎来使用数据库,上面也使用sqlite3BtreeOpen() 的方法来打开数据库,下面看看在这其中都做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
SQLITE_PRIVATE int sqlite3BtreeOpen(
sqlite3_vfs *pVfs, /* VFS to use for this b-tree */
const char *zFilename, /* Name of the file containing the BTree database */
sqlite3 *db, /* Associated database handle */
Btree **ppBtree, /* Pointer to new Btree object written here */
int flags, /* Options */
int vfsFlags /* Flags passed through to sqlite3_vfs.xOpen() */
){
BtShared *pBt = 0; /* Shared part of btree structure */
Btree *p; /* Handle to return */
sqlite3_mutex *mutexOpen = 0; /* Prevents a race condition. Ticket #3537 */
int rc = SQLITE_OK; /* Result code from this function */
u8 nReserve; /* Byte of unused space on each page */
unsigned char zDbHeader[100]; /* Database header content */

...
if( isMemdb ){
//是否是 in-memory 的DB
flags |= BTREE_MEMORY;
}
if( (vfsFlags & SQLITE_OPEN_MAIN_DB)!=0 && (isMemdb || isTempDb) ){
vfsFlags = (vfsFlags & ~SQLITE_OPEN_MAIN_DB) | SQLITE_OPEN_TEMP_DB;
}
//分配内存
p = sqlite3MallocZero(sizeof(Btree));
if( !p ){
return SQLITE_NOMEM;
}
p->inTrans = TRANS_NONE;
p->db = db;
// 如果设置使用缓存则尝试寻找并使用缓存的BTree 共享内存,找到则可以直接返回,不再执行后续执行。
...

// pBt = 0, 共享的BTree存储对象为空
...
pBt = sqlite3MallocZero( sizeof(*pBt) );
if( pBt==0 ){
rc = SQLITE_NOMEM;
goto btree_open_out;
}
// 打开sqlite3 数据库文件
rc = sqlite3PagerOpen(pVfs, &pBt->pPager, zFilename,
EXTRA_SIZE, flags, vfsFlags, pageReinit);
if( rc==SQLITE_OK ){ //打开成功,则继续读取该文件的头
rc = sqlite3PagerReadFileheader(pBt->pPager,sizeof(zDbHeader),zDbHeader);
}
if( rc!=SQLITE_OK ){
goto btree_open_out;
}
pBt->openFlags = (u8)flags;
pBt->db = db;
// 注册数据库文件的 BusyHandler
sqlite3PagerSetBusyhandler(pBt->pPager, btreeInvokeBusyHandler, pBt);
p->pBt = pBt;

pBt->pCursor = 0;
pBt->pPage1 = 0;
// 判断sqlite3 是否以只读方法打开
if( sqlite3PagerIsreadonly(pBt->pPager) ) pBt->btsFlags |= BTS_READ_ONLY;
...
// 设置Pager Object的size
rc = sqlite3PagerSetPagesize(pBt->pPager, &pBt->pageSize, nReserve);
// 将共享的BTree 对象添加进全局的 linkedlist
// 将新的BTree 添加进同一个连接的共享Btree 对象链表中
// 如何退出
...
}

写入

以SQLiteConnection中的 executeForLastInsertedRowId JNI方法为例

1
2
3
4
5
6
7
8
9
10
static jlong nativeExecuteForLastInsertedRowId(JNIEnv* env, jclass clazz,
jint connectionPtr, jint statementPtr) {
// 将上层保存的int指针转换成SQLiteConnection指针
SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
// 转换Statement指针
sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
int err = executeNonQuery(env, connection, statement);
return err == SQLITE_DONE && sqlite3_changes(connection->db) > 0
? sqlite3_last_insert_rowid(connection->db) : -1;
}

转换到 executeNonQuery 执行

1
2
3
4
5
6
7
8
// 通过 sqlite3_step 到SQLite中执行
int err = sqlite3_step(statement);
if (err == SQLITE_ROW) {
throw_sqlite3_exception(env,
"Queries can be performed using SQLiteDatabase query or rawQuery methods only.");
} else if (err != SQLITE_DONE) {
throw_sqlite3_exception(env, connection->db);
}

然后转换到SQLite中执行
sqlite3.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
SQLITE_API int sqlite3_step(sqlite3_stmt *pStmt){
int rc = SQLITE_OK; /* Result from sqlite3Step() */
int rc2 = SQLITE_OK; /* Result from sqlite3Reprepare() */
Vdbe *v = (Vdbe*)pStmt; /* the prepared statement */
int cnt = 0; /* Counter to prevent infinite loop of reprepares */
sqlite3 *db; /* The database connection */

if( vdbeSafetyNotNull(v) ){
return SQLITE_MISUSE_BKPT;
}
db = v->db;
// 加锁
sqlite3_mutex_enter(db->mutex);
// 循环执行 sqlite3Step, 并最终转发到sqlite3VdbeExec() 中执行
while( (rc = sqlite3Step(v))==SQLITE_SCHEMA
&& cnt++ < SQLITE_MAX_SCHEMA_RETRY
&& (rc2 = rc = sqlite3Reprepare(v))==SQLITE_OK ){
// reset SQLiteStatement
sqlite3_reset(pStmt);
assert( v->expired==0 );
}
// 如果执行出错则进行清理阶段
if( rc2!=SQLITE_OK && ALWAYS(v->isPrepareV2) && ALWAYS(db->pErr) ){
const char *zErr = (const char *)sqlite3_value_text(db->pErr);
sqlite3DbFree(db, v->zErrMsg);
if( !db->mallocFailed ){
v->zErrMsg = sqlite3DbStrDup(db, zErr);
v->rc = rc2;
} else {
v->zErrMsg = 0;
v->rc = rc = SQLITE_NOMEM;
}
}
rc = sqlite3ApiExit(db, rc);
sqlite3_mutex_leave(db->mutex);
return rc;
}

最后转发到 sqlite3VdbeExec 中执行,在 sqlite3VdbeExec 中会转换成 SQLite VDBE Program。有关VDBE Program 的内容请参考官方文档 VDBE