今天介绍下Android数据库锁机制、性能优化的点,之前去面试的时候面试官问我能同时在同个数据库里写两个记录么,对两个不同的表写数据可以么?我知道肯定是不行的,但说不出个所以然,只能浅显地说数据库整体是一个文件,保证并发性的话不能同时对一个文件做写操作哈。那接下来主要会对SQL锁机制做个介绍。

性能优化

锁机制

1.性能优化

1.1数据库insert、query、update、delete的API与execSQL、rawQuery函数执行插入、查询、更新、删除语句操作花费时间的对比。

(1)批量执行1000条,则SqliteDatabase提供的insert、query、update、delete函数和直接execSQL、rawQuery函数的效率差不多。

(2)批量执行10w条,SqliteDatabase提供的insert、query、update、delete函数相对效率比较低。execSQL省去了拼接sql语句的步骤,当数据库越大,差别就越大。

1.2批量操作效率

在数据库中插入大量数据,如果使用遍历insert操作会导致应用响应缓慢,每一次insert操作都会开启一个事务,这样都会执行一次磁盘操作。我们可以通过显示添加事务来提高效率

db.beginTransaction();
try {
for(int i = 0; i < 10000; i++) {
insert(db, c);
}
db.setTransactionSuccessful();
} catch(Exception ex) {
} finally {
db.endTransaction();
}

这样会将全部要执行的sql语句先缓存在内存当中,然后等到Commit的时候一次性写入数据库,保证数据库文件只被打开关闭了一次。

2.锁机制

SQLite是基于锁来实现并发控制的,SQLite的锁是粗粒度的,所以是轻量级的,当一个连接要写数据库时,所有其他的连接都被锁住,直到写连接结束它的事务。

2.1锁的状态

SQLite数据库连接有5种状态

image.png

未加锁:

未和数据库建立连接、已建立连接但还没访问数据库、已用BEGIN开始了一个事务但未开始读写数据库,处于这些情形时是未加锁状态。

共享锁:

连接需要从数据库中读取数据时,需要申请获得一个共享锁,如果获取成功,则进入共享状态。

预留锁:

连接需要写数据至数据库时,首先申请一个预留锁,一个数据库同时只能有一个预留锁,预留锁可以与共享锁共存。获得预留锁后会进入预留状态,这时会先在缓存区中进行写操作,操作后的结果依然保存在缓存区中,未真正写入数据库。

未决锁:

连接从预留升级为排它前,需要先升级为未决锁,这时其他的连接就不能获取到共享锁了,但已经拥有共享锁的连接仍然正常读数据库,此时,拥有未决锁的连接等待其他拥有共享锁连接完成工作并释放其共享锁后,才能提升到排它锁。

排它锁:

连接需要提交修改时,需要将预留锁升级为排它锁,这时候其他链接都无法获取任何锁,直到当前连接的排它状态结束。

一个连接读数据库的流程如下:

image.png

一个连接写数据库的流程如下:

image.png

但在实际中可能会出现死锁的问题,例如在使用事务的情况下。

image.png

执行顺序1:连接A获取一个未加锁

执行顺序2:连接B获取一个未加锁

执行顺序3:连接B要执行写操作,获得预留锁(数据库连接只能一次有一个预留锁)

执行顺序4:连接A要执行读操作,获取共享锁

执行顺序5:连接B要升级为排它锁前,在执行提交数据库的操作前,先升级成未决锁(这时候不能有新的共享锁了)

执行顺序6:连接B要升级为排它锁,但可能存在4的操作耗时久,共享锁未被释放,连接B需等待

执行顺序7:连接A要执行写操作,需要获取预留锁

执行顺序8:连接A获取预留锁失败,必须先等连接B释放未决锁

于是连接A和连接B相互等待对方,发生死锁。

那么,如何避免这种死锁情况呢?就要谈谈事务类型了。

2.2事务类型

DEFERRED

不获取任何锁,这种是正常执行流程从上往下的,但对数据库进行连接时它本身是不会获取任何锁的,当对数据库进行读操作时,会获取共享锁,当对写操作时,会获取预留锁。

IMMEDIATE

在BEGIN时事务会尝试获取预留锁。如果获取成功,能保证没有其他可以对数据库可以进行写操作,但可以进行读操作。通俗而言就是当前事务在没有结束之前,任何其他线程或进程都无法对数据库进行写操作。

EXCLUSIVE

事务会尝试获取排它锁。一旦获取成功,保证没有额外的连接,这样就能保证对数据库进行读写操作。通俗而言就是当前事务在没有结束之前,任何其他线程或进程都无法对数据库进行读写操作。

其实上面死锁的情况主要是由于一个共享锁未释放导致两个连接互相等待对方。那么如果两个连接都是以IMMEDIATE的方式开启事务的话,那么当下只能有一个连接获取预留锁,其他的连接就得等它操作完。

可能说的有点悬乎,那么我们看下安卓开启事务的代码。

image.png

beginTransaction():使用EXCLUSIVE这种方式的。

beginTransactionNonExclusive():使用IMMEDIATE这种方式的。

具体内部逻辑如下:

image.png

所以说默认情况下使用事务时,只允许一个连接进行数据库读写,这样保证了并发性。