sqlite锁机制和greenDAQ多线程
锁
Android和iOS都是采用的sqlite作为默认数据库。在有并发业务的场景下,数据库需要提供锁机制来保证数据一致。sqlite3提供了五种级别的锁:未加锁(UNLOCKED)、共享 (SHARED)、保留 (RESERVED)、未 决(PENDING) 和排它(EXCLUSIVE)。SQLite 使用锁逐步上升机制,为了写数据库,连接需要逐级地获得排它锁,以最大限度的保证并发性。
事务
事务是与锁紧密关联的概念。SQLite有三种不同的事务,DEFERRED、 MMEDIATE和EXCLUSIVE,使用不同的锁状态。 事务类型在BEGIN 命令中指定: BEGIN [ DEFERRED | IMMEDIATE | EXCLUSIVE ] TRANSACTION。
一个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 事务保证没有其它的连接,所以就可对数据库进行读写操作了。
BEGIN IMMEDIATE 和BEGIN EXCLUSIVE 通常被写事务使用。就像同步机制一样,它防止了死锁的产生。
基本的准则是:如果你正在使用的数据库没有其它的连接,用BEGIN 就足够了。但是,如果你使用的数据库有其它的连接也会对数据库进行写操作,就得使用 BEGIN IMMEDIATE或 BEGIN EXCLUSIVE开始你的事务。
默认情况下,SQLite的每条语句就是一条事务,比如执行一条INSERT语句,执行后,INSERT语句就生效,提交到了数据库中。
我们可以分别看看读操作和写操作的锁变化流程。
读操作锁变化
读操作的目的是获取共享锁shared从而来访问数据。在获得共享锁(SHARED)之前,首先检查是否有排它锁(EXCLUSIVE):如果有,则说明sqlite正在进行写入操作,为保障数据一致,所以无法获取共享锁(SHARED)。
如果没有,再检查是否有未决锁(PENDING)如果有,表示当前有准备进行的写操作并阻止共享锁(SHARED)的获取。如果检测不到上述两个锁,将获得共享锁(SHARED),读取数据,然后释放共享锁。
写操作锁变化
首先,检查数据库是否有保留锁(RESERVED)与排它锁(EXCLUSIVE),如果有,则说明在此次写操作之前还准备有或者正在进行一次写操作。此时如法获取排它锁(EXCLUSIVE),如果没有,则获取未决锁(PENDING) 当未决锁(PENDING)获得时,将无法再获取到共享锁(SHARED),也就是说sqlite此时已经不再处理读请求。在持有未决锁(PENDING)期间,将会不断询问内部是否还有共享锁(SHARED),当等待所有共享锁(SHARED)消失,当所有共享锁(SHARED)消失时,此时锁状态将由未决锁(PENDING)切换至排它锁(EXCLUSIVE)并写入数据,当排它锁(EXCLUSIVE)**时,阻止任何类型的其它锁获取,直至写入完毕并释放排它锁(EXCLUSIVE)。
greenDAO并发及事务控制
以上是对sqlite事务和锁级别的简单总结。下面再来看一看orm框架greenDAO是如何操作sqlite数据库的(greenDAO实际上已经停止维护了,不推荐使用,只是因为旧项目之中使用了greenDAO,所以这里对一些关键点做一个简单的总结)。
greenDAO使用forCurrentThread()方法来实现多线程同步。它的源码是这样的:
Q forCurrentThread() {
// Process.myTid() seems to have issues on some devices (see Github #376) and Robolectric (#171):
// We use currentThread().getId() instead (unfortunately return a long, can not use SparseArray).
// PS.: thread ID may be reused, which should be fine because old thread will be gone anyway.
long threadId = Thread.currentThread().getId();
synchronized (queriesForThreads) {
WeakReference<Q> queryRef = queriesForThreads.get(threadId);
Q query = queryRef != null ? queryRef.get() : null;
if (query == null) {
gc();
query = createQuery();
queriesForThreads.put(threadId, new WeakReference<Q>(query));
} else {
System.arraycopy(initialValues, 0, query.parameters, 0, initialValues.length);
}
return query;
}
该方法将返回本线程内的Query实例,每次调用该方法时,参数均会被重置为最初创建时的一样。其中,queriesForThreads是一个Map,key是线程id,value是当前线程对应的Query实例。final Map<Long, WeakReference<Q>> queriesForThreads;
在多线程场景下,如果使用的Query实例不是当前线程所有的,将会引发“No Session found for current thread”异常。
forCurrentThread()只针对Query操作,对于Insert/Delete/Update是不需要的。因为如前所述,数据库支持多路并发读,但是同时只能有一路写。
Greendao所有批量操作都增加了事务处理,保证了数据的一致性。下面,我们分别以insert(T entity)
(插入单个实体)和insertOrReplaceInTx(Iterable<T> entities)
方法,来分析一下greenDAO是怎么定义和执行sql事务的。
首先我们分析一下insert(T entity)
的源码,它是通过调用executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach)
实现的:
private long executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach) {
long rowId;
if (db.isDbLockedByCurrentThread()) {
rowId = insertInsideTx(entity, stmt);
} else {
// Do TX to acquire a connection before locking the stmt to avoid deadlocks
db.beginTransaction();
try {
rowId = insertInsideTx(entity, stmt);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
if (setKeyAndAttach) {
updateKeyAfterInsertAndAttach(entity, rowId, true);
}
return rowId;
}
executeInsert方法首先要检查database有没有被当前线程锁定,如果当前线程获取了数据库锁,那么直接调用insertInsideTx(T entity, DatabaseStatement stmt)
执行写入操作(为节省篇幅,这里几句不再详细分析insertInsideTx的源码了,有兴趣的同学可以下载greenDAO源码自行查看,比较简单)。如果当前线程还没有获取数据库锁,那么,开启一个事务,然后尝试去调用insertInsideTx(T entity, DatabaseStatement stmt)
执行写入操作。isDbLockedByCurrentThread
和beginTransaction
都是SQLiteDatabase中的方法,Android开发的事务是immediate或者exclusive类型,有兴趣的同学可以查看SQLiteDatabase的源码。
对于批量插入操作insertOrReplaceInTx(Iterable<T> entities)
方法,最终是通过executeInsertInTx(DatabaseStatement stmt, Iterable<T> entities, boolean setPrimaryKey)
实现。它的源码如下:
private void executeInsertInTx(DatabaseStatement stmt, Iterable<T> entities, boolean setPrimaryKey) {
db.beginTransaction();
try {
synchronized (stmt) {
if (identityScope != null) {
identityScope.lock();
}
try {
if (isStandardSQLite) {
SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement();
for (T entity : entities) {
bindValues(rawStmt, entity);
if (setPrimaryKey) {
long rowId = rawStmt.executeInsert();
updateKeyAfterInsertAndAttach(entity, rowId, false);
} else {
rawStmt.execute();
}
}
} else {
for (T entity : entities) {
bindValues(stmt, entity);
if (setPrimaryKey) {
long rowId = stmt.executeInsert();
updateKeyAfterInsertAndAttach(entity, rowId, false);
} else {
stmt.execute();
}
}
}
} finally {
if (identityScope != null) {
identityScope.unlock();
}
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
如前所述,对于这种批量操作,greenDAO都是开启事务进行处理,以保证数据的一致性。
关于AsyncSessiion
最后,简单介绍一下greenDAO自带的AsyncSession。这个类实际是通过ExecutorService来实现了多线程并发操作,以最大限度的利用数据库的性能。但是这个类本身并没有对并发场景下的数据安全性提供保证。
总结
sqlite本身提供了五级锁以及三种事务,以支持并发场景下的数据同步和一致性,完整性。greenDAO通过支持事务和线程校验,来保证数据操作的正确。
下一篇: 7.1 2- 多线程的几种方式、锁机制