sqlite锁机制和greenDAQ多线程

16 篇文章 0 订阅
6 篇文章 0 订阅

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)执行写入操作。isDbLockedByCurrentThreadbeginTransaction都是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通过支持事务和线程校验,来保证数据操作的正确。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值