0%

SQLite锁机制

SQLite锁的粒度到哪里?

SQLite锁的是整个数据库文件,不支持页锁、表锁和行锁,粒度较粗。

当一个连接要写数据库文件时,所有其他的连接都会阻塞,直到写数据的连接事务结束。

SQLite 3.7.0 新增 Write-Ahead Log 机制改变了事务行为,读写可以并发。

为什么SQLite不支持表锁和行锁呢?

支持高度的写并发会带来很大的复杂性,这将使SQLite的简单性无法保持。

同时复杂性增大会增加运行设备电量的损害,对于手机这类这种电量敏感的设备比较重要。

SQLite锁机制是怎样的?

有多个数据库连接同时访问同一个数据库,就产生了资源竞争,需要锁机制保证资源竞争的正确性

数据库操作无非就是读和写,按道理普通的读写锁就可以实现,但高并发下读写锁仍然有优化的空间,为了提高整体的吞吐量,SQLite使用了锁逐步提升的机制

SQLite共有5种锁状态:

  1. 无锁(unlocked)
  2. 共享(shared)
  3. 预留(reserved)
  4. 未决(pending)
  5. 排它(exclusive)

每个数据库连接同一时刻只能处于其中一个状态

无锁:

  1. 即使事务已经开始,在没有读写数据库前,都是无锁状态

共享:

  1. 读数据库必须获得共享锁
  2. 多个数据库连接可以同时获得共享锁,即允许多个连接同时读数据库
  3. 写数据库前必须要释放所有的共享锁

预留:

  1. 写数据需要先获取到预留锁
  2. 持有预留锁后就可以立即把数据写入缓存中,而不用竞争数据库文件,把可以不占用的数据库文件可以完成的事情提前完成,这样最大程度的减少独占数据库文件的时间
  3. 预留锁可以与共享锁共存
  4. 预留锁不影响其他已经持有共享锁的连接继续读数据库
  5. 预留锁不阻止其他连接获得新的共享锁,因为并不确定是否要立即写入数据库文件,可能之后还要修改数据库内容,提交事务的时候才知道后面不会再修改数据库了
  6. 一个数据库同时只能有一个预留锁存在

未决:

  1. 当连接提交事务时,就要把缓存里的数据往数据库文件中写入,以保证事务的持久性
  2. 对数据库文件写数据前,需要保证没有连接再持有共享锁
  3. 但此时可能还有其他连接持有共享锁,需要等待这些连接释放共享锁后,才能写数据
  4. 同时要阻止新的连接获取共享锁,否则写数据一直得不到执行
  5. 所以从预留锁要转变为未决锁,来阻止新的连接获取共享锁

排它:

  1. 所有共享锁释放后,未决锁提升为排他锁
  2. 排它锁和未决锁一样,会阻止新的连接获取共享锁,阻止读数据
  3. 获得排它锁后,就可以把缓存中的数据写入数据库文件

参考资料:

《SQLite权威指南(第2版)》116页 第4章 SQLite中的高级SQL - 数据库锁

SQLite锁为什么搞这么多状态?

  • 由于写数据时是排他的,其他连接无法读数据库,所以要尽可能的减少写数据时的耗时,才能提高系统整体的吞吐量。
  • 所以写数据时先获取预留锁,把可以在不占用数据库文件的事情提前做好。
  • 那些需要占用数据库文件才能做的事情留到获取到排他锁时再做。
    • 获取排它锁前需要保证没有连接再获取共享锁。
    • 所以先从预留锁提升到未决锁,用来阻止新的共享锁的获取。
    • 等待已经获取的共享锁的连接执行完后释放了所有的共享锁,最后获取到排它锁写数据库文件。

锁的状态转移过程是怎样的?

为什么事务的开始都要先获取未决锁,再获取共享锁?

因为如果已经获得了未决锁,说明要占用数据库文件进行写操作,就不允许读了,而读数据库需要获得共享锁,这里就阻止别的连接获取共享锁。

有了预留锁,为什么还需要一个未决锁?未决锁存在的作用是什么?没有未决锁会发生什么?

获得预留锁时,读写可以并发执行,但是真正写数据库需要阻止共享锁获取。

这样做可以提升系统整体的吞吐量。

SQLite锁机制需要注意什么?

注意死锁的发生

SQLite什么情况下会发生死锁?

SQLite发生死锁时会抛出database is locked的异常信息。

参考《SQL权威指南(第2版)》117页 SQLite中的高级SQL - 死锁

如何避免死锁?

用正确的事务类型来开启事务

存在并发写数据,就开启immediate或exclusive事务,提供了同步机制

SQLite有三种不同的事务类型:

  1. DEFERRED(推迟)
  2. MMEDIATE(立即)
  3. 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开启事务有两个方法:

  1. beginTransaction()开启exclusive事务类型
  2. beginTransactionNonExclusive()开启immediate事务类型

锁的状态存储在哪里?

数据库文件是独立于进程的,多个进程可以访问同一个文件,所以数据库锁是存在数据库文件中的。

参考《SQL权威指南(第2版)》159页 第5章 SQLite设计与概念 - 写事务