事务是什么?
事务定义了一组数据库操作的边界,这组操作要么全部执行,要么全部不执行,这也是事务的原子性特征
为什么要发明事务?事务存在的价值是什么?
在操作数据库的情况下,需要考虑的特别的场景处理特别多,例如并发操作、系统崩溃等,发明事务这个模型,就可以用来简化讨论,把复杂的场景归类为少数的几个特定类型的场景,降低处理成本。
数据库操作例如增删改查、建立索引、建立约束等。
事务有哪些特性?
ACID
Atomicity 原子性
事务内的操作要么全部执行,要么全部不执行,不可再拆分
Consistency 一致性
保证数据库从一个正确的状态转变到另一个正确的状态,正确的状态指的是当前数据库中的数据都满足预定的约束条件。
AID是保证一致性的必要条件,但不是充分条件,因为数据库作为通用的技术,不可能知道具体业务场景的正确逻辑,所以正确的逻辑需要由用户决定应该使用怎样的约束
Isolation 隔离性
事务的执行不受其他事务的干扰,事务之间的操作只能串行的执行,保证任何事务都不可能读取到其他任何事务内部执行的中间状态,否则会产生数据混乱
Durability 持久性
事务一旦提交,它对数据库中的数据改变就是永久性的,接下来的其他操作或故障都不会影响本次事务提交的结果
事务的原子性是如何实现的?
SQLite事务的实现是依赖于名为rollback journal文件,借助这个临时文件来完成原子操作和回滚功能。
回滚日志文件,用于实现数据库的原子提交和回滚。 此文件和数据库文件总是在同一个目录,并且有相同的文件名,但是在文件名中添加了一个-journal 字符串。此文件一般在transaction开始时创建,transaction结束时删除。
如果系统crash,Rollback journals文件将被保留,下次打开数据库文件时,系统会检查有没有Rollback journals文件存在,如果有就用它来恢复数据库。
创建回滚日志详细过程是怎样的?
- 写数据库前获取保留锁
- 创建回滚日志文件,将要修改的页的原始数据写到缓存中
- 在用户空间修改页数据
- 回滚日志的缓存刷盘到回滚日志文件中
- 获取未决锁,等待共享锁都释放后,提升为排它锁
- 将用户空间修改过的数据写入数据库文件,会先写到操作系统的缓存中,再刷盘到磁盘上
- 删除rollback journal文件
- 释放排它锁
创建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的事务分为
- deferred
- immediate
- exclusive
begin [ deferred | immediate | exclusive ] transaction
deferred
- 未指定事务模式时默认的选择
- 开始事务时处于未锁定状态
- 实际访问数据库时才试图加锁
- 第一个读数据库操作试图获取共享锁
- 第一个写数据库操作试图获取预留锁
immediate
- 事务开始时试图获取预留锁
- 获取预留锁成功后,其他连接中已执行的事务无法再获取到预留锁
- 新的连接开始immediate和exclusive的事务也会失败,并返回SQLITE_BUSY错误
- 最后提交事务时,预留锁会提升到未决锁
- 等待其他连接中的事务释放共享锁
- 阻止新的连接获取共享锁
- 如果其他事务一直没释放共享锁,会返回SQLITE_BUSY错误
exclusive
- 开始事务时尝试获取排它锁
- 获取成功后阻止所有的读写操作
- 在事务内可以对数据库进行任意的读写
参考《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的优缺点
- WAL is significantly faster in most scenarios.
- WAL provides more concurrency as readers do not block writers and a writer does not block readers. Reading and writing can proceed concurrently.
- Disk I/O operations tends to be more sequential using WAL.
- 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的缺点是什么?
- 所有的数据库操作必须都在同一台机器上进行,并且该机器的操作系统需要支持 VFS 特性。
- 当连接处于 WAL 模式中时我们无法修改页大小
- 为满足 Wal 和相关共享内存的需要,使用 WAL 引入了两个额外的半持久性文件 -wal 和 -shm 该文件需要占用一定的存储空间。
- 数据库读性能会比 rollback journal 模式略差 (大概慢 1% ~ 2% ),另外写操作也会间歇性的性能下降。
- 读操作的性能会比 rollback journal 模式出现部分下降,因为它需要额外对 -wal 文件进行一次检索,而且 Checkpoint 本身就比较耗时且会对读操作进行阻塞。
- 频繁 Checkpoint 变得频繁又会影响写操作的性能指标,而且频繁的同步操作也会增加数据库损坏的概率
参考:《SQLite权威指南(第2版)》第11章 sqlite内部机制及新特性 - wal的优缺点
- All processes using a database must be on the same host computer; WAL does not work over a network filesystem.
- Transactions that involve changes against multiple ATTACHed databases are atomic for each individual database, but are not atomic across all databases as a set.
- 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.
- 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。