SQLite Lock Management
1. 锁机制的目的
SQLite为了顺序化的处理transaction而使用了锁机制, 这样能有效地管理事务请求数据库的顺序. SQLite的锁机制有一条原则: SQLite的锁是database级别的. 上锁和解锁都只针对database本身, 不针对某个row或page.
2. 五种锁状态
- NOLOCK
该状态的transaction不拥有database的锁. 这种状态下该transaction不能读或写该database. 而其他transaction可在其锁状态允许的条件下读或写database. 当一个transaction刚被创建时, NOLOCK就是它的初始状态. - SHARED
该锁仅允许读取database. 同一时间可有多个transaction拥有某个database的shared lock, 因此会database file会被同时访问. 当database file上有shared lock时, 不允许transaction写入该file. - Reserved
该锁允许读取database. 这意味着transaction将会在未来写入database file. 同一时间只能有一个reserved lock在某个database上. 但该锁可以和任意数量的shared lock共存. 其他transaction可添加shared lock到该database上. - PENDING
该锁允许读取database. 这意味着transaction急切地想要写入database. 该锁会等待该database上的所有shared lock被清除, 并获取exclusive lock. 该锁依然可以和shared lock共存, 但不能允许其他transaction获取新的shared lock. - EXCLUSIVE
该锁允许写入(也可以读取)database. 只有该锁允许transaction写入database file, 并且不能和其他exclusive lock共存.
3. 锁机制流程
3.1 状态转换
通常情况下, 锁状态的转换从NO LOCK到SHARED LOCK到RESERVED LOCK到PENDING LOCK, 最后到EXCLUSIVE LOCK. 如果有journal需要回滚, 则SHARED LOCK会直接变为PENDING LOCK.
- read transaction:
- NOLOCK到PENDING: 获取PENDING LOCK是获取SHARED LOCK的第一步. 如果有其他transaction获取PENDING LOCK, 那么会导致本transaction无法获取PENDING LOCK.
- 如果成功获取PENDING LOCK, transaction会继续获取SHARED LOCK, 并释放PENDING LOCK.
- write transaction
- 和read transaction一样, 获取SHARED LOCK.
- 获取RESERVED LOCK
- 为获取EXCLUSIVE LOCK, 必须先获取PENDING LOCK, 以阻止其他transaction获取SHARED LOCK.
- 获得PENDING LOCK后, 继续获得EXCLUSIVE LOCK. 如果成功获取EXCLUSIVE LOCK, 则进行写磁盘操作.
3.2 引发的问题
锁机制虽然解决了并发控制问题, 但也引入了其他问题: 两个SHRED LOCK都想获取RESERVED LOCK, 其中一个变为RESERVED LOCK后, 另外一个转为等待. RESERVED LOCK想要获取EXCLUSIVE LOCK, 但由于另一个SHARED LOCK不肯被清理, 所以就导致了两个锁陷入死锁(DEADLOCK).
对于DEADLOCK, 有两种解决方法:
- 预防
- 检测并消除
SQLite采用了第一种方法: 预防. 锁的获取是非阻塞模式, 当某个请求锁的行为失败时, 会重试一定次数. 如果全部失败, 则返回SQLITE_BUSY错误码给application. Application可以重新请求锁或放弃这个transaction. 因此, SQLite没有死锁的风险.
4. Linux和Windows的锁系统
SQLite通过使用原生系统的锁系统来实现自己的锁系统.
4.1 Linux锁系统
以下为Linux锁系统的分类:
- 建议锁(advisory)
- 锁文件(lock files)
- 记录锁(record locking)
- 强制锁(mandatory)
4.1.1 建议锁
建议锁不由内核强制执行, 也就是说, 如果其他进程想往加了锁的文件访问并写入数据, 那么内核不会阻拦. 因此建议锁不能阻止进程对文件的访问, 只能依赖于各个进程访问文件之前检查文件是否加锁.
- 锁文件
每个需要加锁的数据文件都要有一个锁文件(lock file). 当某个数据文件的锁文件存在时, 则认为该数据文件已经被加锁, 别的进程不应访问. 当锁不存在时, 则可以创建一个锁文件并访问数据文件. - 记录锁
记录锁克服了锁文件的缺点, 可以对文件的任何一部分加锁. 并且记录锁由内核持有, 这样拥有记录锁的进程消失时, 记录锁也会被释放. 当记录锁依然是建议性的, 有读锁(read lock)和写锁(write lock)两种. 这两种锁有以下几点特性:- 多个读锁可存在于一个文件的同一块区域
- 只有一个thread能拥有某块区域的写锁
- 读锁和写锁可存在于同一文件, 但不能是同一区域
- 如果某一thread在某一文件区域已经加锁, 但施加了新锁, 那么之前的锁会被新锁替代
4.1.2 强制锁
强制锁由内核执行, 当文件被上锁后, 则内核会阻止其他进程对其进行读写操作. 采用强制锁对性能影响很大, 因为每次读写操作都必须检查是否有锁存在.
4.2 Windows锁系统
Windows的锁都是强制锁(mandatory locks), 相关API函数:
- LockFile(): 加锁区域不能重叠, 只有Exclusive Lock.
- LockFileEx(): 可对文件加Shared Lock和Exclusive Lock. Shared Lock可与Shared Lock重叠, 但Exclusive Lock不能与任何锁重叠.
- 通过字节范围锁(byte range locks)可对文件某一部分进行读写操作.
- Windows不允许正在执行的文件被打开用来读写操作.
4.3 SQLite锁状态与原生系统锁系统
- SHARED LOCK通过文件上部分范围的read lock来实现
- EXCLUSIVE LOCK通过文件上所有字节的write lock来实现
- RESERVED LOCK通过文件上单个字节的write lock来实现
- PENDING LOCK通过文件上另一个位置上单个字节的write lock来实现
5. 锁状态具体内容
5.1 锁的偏移
以下为SQLite各个锁状态在代码中的体现:
- SHARED LOCK: 在SHARED_FIRST区域加read-lock(使用LockFileEx()), 即获取SHARED LOCK
- RESERVED LOCK: 在RESERVED_BYTE字节处加write-lock, 即获取REVERSED LOCK
- PENDING LOCK: 在PENDING_BYTE字节处加write-lock, 即获取PENDING LOCK
- EXCLUSIVE LOCK: 由于EXCLUSIVE LOCK不允许其他进程拥有SHARED LOCK, 所以EXCLUSIVE LOCK和SHARED LOCK使用相同的文件区域, 并添加write-lock.
5.2 锁的定义
随着锁级别的升高, 其类型值也在变大
5.3 锁页(lock-type lock)
SQLite把这512字节(PENDING占1byte, REVERSED占1byte, SHARED占510bytes)作为锁页, 该页是原生系统的加锁区域, 用于实现文件的并发访问.
锁页位于1073741824字节和1073742335字节之间, 即1G的位置, 小于1G的数据库文件则没有此页. 由于SQLite并不对锁页进行读写, 而原生系统可以对不存在的文件区域进行加锁, 所以SQLite把锁页的位置定在了0X40000000, 这样可以省去512字节空间. Windows中被上锁的空间将被操作系统保留, 由于这个原因, SQLite不能把数据存在被上锁的空间中.
以下是如何利用Linux系统锁来实现SQLite锁状态: