SQLite锁的粒度到哪里?
SQLite锁的是整个数据库文件,不支持页锁、表锁和行锁,粒度较粗。
当一个连接要写数据库文件时,所有其他的连接都会阻塞,直到写数据的连接事务结束。
SQLite 3.7.0 新增 Write-Ahead Log 机制改变了事务行为,读写可以并发。
为什么SQLite不支持表锁和行锁呢?
支持高度的写并发会带来很大的复杂性,这将使SQLite的简单性无法保持。
同时复杂性增大会增加运行设备电量的损害,对于手机这类这种电量敏感的设备比较重要。
SQLite锁机制是怎样的?
有多个数据库连接同时访问同一个数据库,就产生了资源竞争,需要锁机制保证资源竞争的正确性
数据库操作无非就是读和写,按道理普通的读写锁就可以实现,但高并发下读写锁仍然有优化的空间,为了提高整体的吞吐量,SQLite使用了锁逐步提升的机制
SQLite共有5种锁状态:
- 无锁(unlocked)
- 共享(shared)
- 预留(reserved)
- 未决(pending)
- 排它(exclusive)
每个数据库连接同一时刻只能处于其中一个状态
无锁:
- 即使事务已经开始,在没有读写数据库前,都是无锁状态
共享:
- 读数据库必须获得共享锁
- 多个数据库连接可以同时获得共享锁,即允许多个连接同时读数据库
- 写数据库前必须要释放所有的共享锁
预留:
- 写数据需要先获取到预留锁
- 持有预留锁后就可以立即把数据写入缓存中,而不用竞争数据库文件,把可以不占用的数据库文件可以完成的事情提前完成,这样最大程度的减少独占数据库文件的时间
- 预留锁可以与共享锁共存
- 预留锁不影响其他已经持有共享锁的连接继续读数据库
- 预留锁不阻止其他连接获得新的共享锁,因为并不确定是否要立即写入数据库文件,可能之后还要修改数据库内容,提交事务的时候才知道后面不会再修改数据库了
- 一个数据库同时只能有一个预留锁存在
未决:
- 当连接提交事务时,就要把缓存里的数据往数据库文件中写入,以保证事务的持久性
- 对数据库文件写数据前,需要保证没有连接再持有共享锁
- 但此时可能还有其他连接持有共享锁,需要等待这些连接释放共享锁后,才能写数据
- 同时要阻止新的连接获取共享锁,否则写数据一直得不到执行
- 所以从预留锁要转变为未决锁,来阻止新的连接获取共享锁
排它:
- 所有共享锁释放后,未决锁提升为排他锁
- 排它锁和未决锁一样,会阻止新的连接获取共享锁,阻止读数据
- 获得排它锁后,就可以把缓存中的数据写入数据库文件
参考资料:
《SQLite权威指南(第2版)》116页 第4章 SQLite中的高级SQL - 数据库锁
SQLite锁为什么搞这么多状态?
- 由于写数据时是排他的,其他连接无法读数据库,所以要尽可能的减少写数据时的耗时,才能提高系统整体的吞吐量。
- 所以写数据时先获取预留锁,把可以在不占用数据库文件的事情提前做好。
- 那些需要占用数据库文件才能做的事情留到获取到排他锁时再做。
- 获取排它锁前需要保证没有连接再获取共享锁。
- 所以先从预留锁提升到未决锁,用来阻止新的共享锁的获取。
- 等待已经获取的共享锁的连接执行完后释放了所有的共享锁,最后获取到排它锁写数据库文件。
锁的状态转移过程是怎样的?
为什么事务的开始都要先获取未决锁,再获取共享锁?
因为如果已经获得了未决锁,说明要占用数据库文件进行写操作,就不允许读了,而读数据库需要获得共享锁,这里就阻止别的连接获取共享锁。
有了预留锁,为什么还需要一个未决锁?未决锁存在的作用是什么?没有未决锁会发生什么?
获得预留锁时,读写可以并发执行,但是真正写数据库需要阻止共享锁获取。
这样做可以提升系统整体的吞吐量。
SQLite锁机制需要注意什么?
注意死锁的发生
SQLite什么情况下会发生死锁?
SQLite发生死锁时会抛出database is locked的异常信息。
参考《SQL权威指南(第2版)》117页 SQLite中的高级SQL - 死锁
如何避免死锁?
用正确的事务类型来开启事务
存在并发写数据,就开启immediate或exclusive事务,提供了同步机制
SQLite有三种不同的事务类型:
- DEFERRED(推迟)
- MMEDIATE(立即)
- EXCLUSIVE(排它)
事务类型在BEGIN命令中指定。
一个deferred事务不获取任何锁,直到它需要锁的时候。而且BEGIN语句本身也不会做什么事情——它开始于UNLOCK状态;默认情况下是这样的。如果仅仅用BEGIN开始一个事务,那么事务就是DEFERRED的,同时它不会获取任何锁,当对数据库进行第一次读操作时,它会获取SHARED LOCK;同样,当进行第一次写操作时,它会获取RESERVED LOCK。
由BEGIN开始的Immediate事务会试着获取RESERVED LOCK。如果成功,BEGIN IMMEDIATE保证没有别的连接可以写数据库。但是,别的连接可以对数据库进行读操作,但是RESERVED LOCK会阻止其它的连接BEGIN IMMEDIATE或者BEGIN EXCLUSIVE命令,SQLite会返回SQLITE_BUSY错误。这时你就可以对数据库进行修改操作,但是你不能提交,当你COMMIT时,会返回SQLITE_BUSY错误,这意味着还有其它的读事务没有完成,得等它们执行完后才能提交事务。
Exclusive事务会试着获取对数据库的EXCLUSIVE锁。这与IMMEDIATE类似,但是一旦成功,EXCLUSIVE事务保证没有其它的连接,所以就可对数据库进行读写操作了。 上面那个例子的问题在于两个连接最终都想写数据库,但是他们都没有放弃各自原来的锁,最终,shared 锁导致了问题的出现。如果两个连接都以BEGIN IMMEDIATE开始事务,那么死锁就不会发生。在这种情况下,在同一时刻只能有一个连接进入BEGIN IMMEDIATE,其它的连接就得等待。BEGIN IMMEDIATE和BEGIN EXCLUSIVE通常被写事务使用。就像同步机制一样,它防止了死锁的产生。 基本的准则是:如果你在使用的数据库没有其它的连接,用BEGIN就足够了。但是,如果你使用的数据库在其它的连接也要对数据库进行写操作,就得使用BEGIN IMMEDIATE或BEGIN EXCLUSIVE开始你的事务。
Android中对SQLite开启的哪一种事务类型?
SQLiteDatabase开启事务有两个方法:
- beginTransaction()开启exclusive事务类型
- beginTransactionNonExclusive()开启immediate事务类型
锁的状态存储在哪里?
数据库文件是独立于进程的,多个进程可以访问同一个文件,所以数据库锁是存在数据库文件中的。
参考《SQL权威指南(第2版)》159页 第5章 SQLite设计与概念 - 写事务