0%

SQLite事务、回滚日志、WAL

事务是什么?

事务定义了一组数据库操作的边界,这组操作要么全部执行,要么全部不执行,这也是事务的原子性特征

为什么要发明事务?事务存在的价值是什么?

在操作数据库的情况下,需要考虑的特别的场景处理特别多,例如并发操作、系统崩溃等,发明事务这个模型,就可以用来简化讨论,把复杂的场景归类为少数的几个特定类型的场景,降低处理成本。

数据库操作例如增删改查、建立索引、建立约束等。

事务有哪些特性?

ACID

Atomicity 原子性

事务内的操作要么全部执行,要么全部不执行,不可再拆分

Consistency 一致性

保证数据库从一个正确的状态转变到另一个正确的状态,正确的状态指的是当前数据库中的数据都满足预定的约束条件。

AID是保证一致性的必要条件,但不是充分条件,因为数据库作为通用的技术,不可能知道具体业务场景的正确逻辑,所以正确的逻辑需要由用户决定应该使用怎样的约束

Isolation 隔离性

事务的执行不受其他事务的干扰,事务之间的操作只能串行的执行,保证任何事务都不可能读取到其他任何事务内部执行的中间状态,否则会产生数据混乱

Durability 持久性

事务一旦提交,它对数据库中的数据改变就是永久性的,接下来的其他操作或故障都不会影响本次事务提交的结果

事务的原子性是如何实现的?

SQLite事务的实现是依赖于名为rollback journal文件,借助这个临时文件来完成原子操作和回滚功能。

回滚日志文件,用于实现数据库的原子提交和回滚。 此文件和数据库文件总是在同一个目录,并且有相同的文件名,但是在文件名中添加了一个-journal 字符串。此文件一般在transaction开始时创建,transaction结束时删除。

如果系统crash,Rollback journals文件将被保留,下次打开数据库文件时,系统会检查有没有Rollback journals文件存在,如果有就用它来恢复数据库。

创建回滚日志详细过程是怎样的?

  1. 写数据库前获取保留锁
  2. 创建回滚日志文件,将要修改的页的原始数据写到缓存中
  3. 在用户空间修改页数据
  4. 回滚日志的缓存刷盘到回滚日志文件中
  5. 获取未决锁,等待共享锁都释放后,提升为排它锁
  6. 将用户空间修改过的数据写入数据库文件,会先写到操作系统的缓存中,再刷盘到磁盘上
  7. 删除rollback journal文件
  8. 释放排它锁

创建rollback journal file

将要修改的pages的原始数据写入rollback journal file,以page为单位写入。

rollback journal file中绿色的部分是header,header中会包括数据库文件的原始大小(即包括多少个page)。每一个page保存到rollback journal中时前边四字节会保存该page的page number。

  • rollback journal创建之后并没有实际落盘,只是保存在操作系统的缓存中
  • rollback journal以page为单位,但每个page前四字节会有一个page number的记录,后四字节有一个checksum

在用户空间修改数据库文件

修改用户进程空间内的数据库文件,注意不同的用户进程有自己的私有内存空间,因此此时的修改并不影响其他进程的读取操作。

将rollback journal落盘

该步骤是保持原子性很关键的一步,保证即使掉电或者操作系统crash,Sqlite也能恢复到原来的状态。

(进行到该步也能看出reserved lock的作用,这种锁是一个中间状态,既能为即将写入做准备,又不影响其他进程的读取操作,提高并行度)。

该步需要刷两次盘,第一次将rollback journal的内容刷到磁盘,第二次在header中记录第一步中刷到磁盘的page个数,然后将header刷盘。

获取排他锁

在实际写入数据库文件之前,需要获取一个排他锁.获取过程分两步
• 获取一个pending lock
• 将pending lock升级为排他锁
获取到pending lock之后,在数据库文件上已经获取到共享锁的进程可以继续读取,但不允许其他进程继续获取共享锁.该锁存在的意义在于防止write starvation(即有大量的读取连接时,一直有新的共享锁产生,导致获取不到排他锁).当所有已经存在的共享锁都释放后,此时该pending lock即可以升级为排他锁。

写入数据库文件

获取到排他锁后,说明已经没有其他进程在读取该数据库文件.此时可以安全的写入数据库文件.注意也只是写入操作系统的缓存中,并没有落盘。

删除rollback journal

因为数据库文件已经安全落盘,此时可以删除掉rollback journal.若删除之前系统crash或者掉电,则重启后会恢复到事务开始前的状态,如果删除之后系统crash或者掉电,因为数据库文件已经落盘,相当于事务已经执行完成.(那么会不会在删除一半时系统crash或者掉电呢?注意上文中关于硬件的一些假设,删除操作必须是原子性的,即不会发生这种情况)

因为删除一个文件也是一个耗时的操作,因此Sqlite提供了两种方式减少删除过程的耗时.

  • 将一个文件truncate为0
  • 将journal file header清0.清0操作并不是原子性的,但只要header中有一个byte被清0,该文件就会被识别为无效的格式

参考:

热日志是什么意思?

每次打开数据库文件,或者从数据库读取页时,都要简单的做一致性检查。

如果发现存在回滚日志文件,但是数据库中没有保留锁,那么创建日志文件的肯定崩溃了或者系统死机了,因为正常情况下,获得保留锁后才会创建回滚日志。

这种情况下日志被称为“热日志”,数据库可能处于不一致的状态(不满足约束条件),要让一切正常需要回滚日志,将数据库还原到初始的被中断的事务之前的状态。

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

SQLite中事务为什么还要分不同的类型?分别有什么作用?

是为了解决死锁

SQLite的事务分为

  1. deferred
  2. immediate
  3. exclusive

begin [ deferred | immediate | exclusive ] transaction

deferred

  1. 未指定事务模式时默认的选择
  2. 开始事务时处于未锁定状态
  3. 实际访问数据库时才试图加锁
  4. 第一个读数据库操作试图获取共享锁
  5. 第一个写数据库操作试图获取预留锁

immediate

  1. 事务开始时试图获取预留锁
  2. 获取预留锁成功后,其他连接中已执行的事务无法再获取到预留锁
  3. 新的连接开始immediate和exclusive的事务也会失败,并返回SQLITE_BUSY错误
  4. 最后提交事务时,预留锁会提升到未决锁
  5. 等待其他连接中的事务释放共享锁
  6. 阻止新的连接获取共享锁
  7. 如果其他事务一直没释放共享锁,会返回SQLITE_BUSY错误

exclusive

  1. 开始事务时尝试获取排它锁
  2. 获取成功后阻止所有的读写操作
  3. 在事务内可以对数据库进行任意的读写

参考《SQLite权威指南(第2版)》118页 第4章 SQLite中的高级SQL - 事务的类型

SQLite WAL是什么?

SQLite在3.7.0开始引入了WAL技术

Write-Ahead Log

预先写日志

默认的 rollback journal 模式工作原理大致为:写操作进行前进行数据库文件拷贝,然后对数据库进行写操作。如果发生 Crash 或者 rallback 则将日志中的原始内容回滚到数据库中进行恢复操作,否则在 Commit 完成时删除日志文件。

WAL模式采用了相反的的做法,

在准备写数据库文件前,会先复制数据库文件的中的原始数据到日志文件,写操作也只更新日志文件,原有数据库文件不变动;

如果事务失败,WAL中的数据被忽略;

如果事务成功,在随后的某个时间点,将WAL中的数据写回到数据库文件,这个时间点称为checkpoint。

WAL使用检查点将修改写回数据库,默认情况下,当WAL文件发现有1000页修改时,将自动调用检查点。这个页数大小可以自行配置。

读数据时,SQLite在WAL中搜索,找到最后一个写入点,并记录,读时忽略这个写入点后面的信息,如果读取在WAL中读取不到数据,则去数据库文件中读取;写数据时继续往记录点后追加写入数据,这样保证了读和写可以同时进行;由于同一时刻只能有一个连接写数据库,写与写之间是互斥的,所以不会产生写入点后面写数据时指针冲突。

由于每个读操作都会搜索整个WAL文件,所以在共享内存加建立了一个wal-index索引,加速查找,避免扫描整个WAL文件。

WAL提升了哪方面的性能?优点是什么?解决了什么问题?

参考:《SQLite权威指南(第2版)》第11章 sqlite内部机制及新特性 - wal的优缺点


  1. WAL is significantly faster in most scenarios.
  2. WAL provides more concurrency as readers do not block writers and a writer does not block readers. Reading and writing can proceed concurrently.
  3. Disk I/O operations tends to be more sequential using WAL.
  4. WAL uses many fewer fsync() operations and is thus less vulnerable to problems on systems where the fsync() system call is broken.

参考:https://sqlite.org/wal.html

WAL的缺点是什么?

  1. 所有的数据库操作必须都在同一台机器上进行,并且该机器的操作系统需要支持 VFS 特性。
  2. 当连接处于 WAL 模式中时我们无法修改页大小
  3. 为满足 Wal 和相关共享内存的需要,使用 WAL 引入了两个额外的半持久性文件 -wal 和 -shm 该文件需要占用一定的存储空间。
  4. 数据库读性能会比 rollback journal 模式略差 (大概慢 1% ~ 2% ),另外写操作也会间歇性的性能下降。
  5. 读操作的性能会比 rollback journal 模式出现部分下降,因为它需要额外对 -wal 文件进行一次检索,而且 Checkpoint 本身就比较耗时且会对读操作进行阻塞。
  6. 频繁 Checkpoint 变得频繁又会影响写操作的性能指标,而且频繁的同步操作也会增加数据库损坏的概率


参考:《SQLite权威指南(第2版)》第11章 sqlite内部机制及新特性 - wal的优缺点


  1. All processes using a database must be on the same host computer; WAL does not work over a network filesystem.
  2. Transactions that involve changes against multiple ATTACHed databases are atomic for each individual database, but are not atomic across all databases as a set.
  3. WAL might be very slightly slower (perhaps 1% or 2% slower) than the traditional rollback-journal approach in applications that do mostly reads and seldom write.
  4. There is an additional quasi-persistent “-wal” file and “-shm” shared memory file associated with each database, which can make SQLite less appealing for use as an application file-format.

参考:https://sqlite.org/wal.html

Android中如何开启WAL?

SQLiteDatabase.enableWriteAheadLogging()

根据注释描述,Android默认没有开启WAL。