参考链接:https://www.sqlite.org/lockingv3.html#rollback
1、sqlite 版本3.0.0 为了降低版本2 带来的写饥饿的问题引入了新的锁和回滚机制。新的机制同时允许多个数据库的原子事务提交。
2、sqlite 内的pager module 负责sqlite的ACID,同时对磁盘文件一些内容的内存缓存。pager模块不会去关注b-tree,编码,索引等。在pager看来,sqlite数据库是由统一大小的块组成的单个文件。每个块我们称之为''页’‘,每个页通常是1024byte大小。这些个页从数字1开始编排,比如 page 1,page 2 ...。
3、锁状态分类
无锁 : 默认状态,对数据库无读写。
共享锁 : 只有读无写,同一时刻可以有任何的读,但是不允许存在写。
保留锁 : 计划在未来的某个时刻写,但目前仅是在读。同一时刻只会有1个活动的保留锁,尽管多个共享锁可以同时和1个保留锁存在。与意向锁不同,当存在保留锁,还可以获取新的共享锁。
意向锁 : 表示想尽可能快的写入数据库,正在等待所有当前共享锁释放,以使自身能获取到排他锁。如果意向锁激活,将无法再获得共享锁,已存在的共享锁允许继续。是个中间状态。
排他锁 : 写入锁。排它锁持有时不允许有其他锁存在。为了最大化并发,sqlite 最小化了排它锁持有时间。
4、回滚日志 文件
在更新之前,数据库首先会记录改变之前的数据库内容到回滚日志文件中.回滚文件和数据库文件在同一目录下,并以-journal 结尾。日志文件同时会记录原始库的大小以便在回滚时将数据库撤回原始大小。
attach命令时,操作多个库将会产生一个聚合的journal文件,叫super-journal。super-journal文件不会包含将要作为回滚变化的页数据,相反只包含每个库的日志文件的名称。每个库的日志文件同时也会包含该super-journal文件的名称。如果没有attached别的库,就不会有super-journal文件,单是相应库的日志文件还是会在其存在super-journal名称的位置留空。
在操作数据库前,sqlite会先处理日志文件,以保证数据一致性,处理步骤如下:
- 现获得数据库共享锁. 未获得,失败,立即返回sqlite_busy.
- 检查是否有journal文件,如果没有直接返回,完成。如果有按接下来的步骤执行。
- 获取意向锁然后排它锁,失败表明有其他线程在处理此过程。此时释放所有锁,关闭数据,返回sqlite_busy。
- 读取journal文件,回滚改变。
- 等待回滚完成,保证完整性。
- 删除日志文件。
- 删除super-jounal文件,如果有。
- 释放排他,意向锁,保持共享锁。
5、删除坏的super-journals文件
如果没有相应数据库的journal文件指向super-journal文件,则认为super-journal文件无用。判断super-journal是坏的,先读取super-jounal文件获取包含的相应库的journal文件名称,检查这个名称的journal文件是否存在,或者这个journal文件的回指是否是当前super-journal文件。如果有异常则可以进行删除。
6、数据写过程
先获取共享锁,再获取保留锁。保留锁表示在未来的某个时刻就写入数据。同一时刻只能有1个保留锁。但是其他读可以继续。如果获取不到保留锁,表明已被其他人获取,返回sqlite_busy。
先写创建的journal文件,文件头初始化为 数据库文件大小。同时留空间个super-journal文件名。
先将要写入的那页数据备份到journal文件。然后将该页数据的变更写入内存而不是磁盘。此时源库未改变,其他人还是可以读。
接下来,当内存缓存充满或者事务提交,准备写入磁盘。
-
确保jouranl文件已经实实在在写入
-
获取意向锁 然后排他锁。如果其他人持有共享锁,写入必须等待,直到能获取到排他锁。
-
写入所有内存缓存当中持有的页数据到源库。
如果写入到数据库文件的原因是因为cache已满,那么写入进程将不会立刻提交,而是继续对其它页进行修改。但是在后续的修改被写入到数据库文件之前,回滚日志必须被再一次刷新到磁盘中。还要注意的是,写入进程获取的排他锁必须被一直持有,直到所有的更改被提交为止。这意味着从数据第一次被刷新到磁盘文件开始,直到事务被提交之前,其它的进程不能访问该数据库
接下来,写入准备提交事务,步骤如下:
-
获取排他锁,确保所有内存数据变化按照1-3步骤刷入磁盘
-
将所有修改刷入磁盘
-
删除日志文件
-
释放排他,意向锁
如果一个事务涉及多个库,则写入更为复杂:
- 确保每个库都有1个排他锁和1个有效的日志文件.
- 创建主数据库日志文件,文件名随机,同时将每个数据库的回滚日志文件名写入主数据库日志文件,刷入磁盘。
- 将主数据库日志文件名写入每个数据库日志文件,刷入磁盘。
- 将所有数据库的变化持久化到磁盘。
- 删除主日志文件,如果在删除之前出现系统故障,进程在下一次打开该数据库时仍将基于该HOT日志进行恢复操作。因此只有在成功删除主日志文件之后,我们才可以认为该事务成功完成
- 删除每个数据库的日志文件
- 释放所有库的排他锁,意向锁
-
写饥饿的处理
在版本2中,如果多个进程正在从数据库中读取数据,也就是说该数据库始终都有读操作发生,即在每一时刻该数据库都持有至少一把共享锁,这样将会导致没有任何进程可以执行写操作,因为在数据库持有读锁的时候是无法获取写锁的,我们将这种情形称为“写饥饿”。
在版本3中,通过使用PENDING锁则有效的避免了“写饥饿”情形的发生。当某一进程持有PENDING锁时,已经存在的读操作可以继续进行,直到其正常结束,但是新的读操作将不会再被SQLite接受,所以在已有的读操作全部结束后,持有PENDING锁的进程就可以被激活并试图进一步获取排他锁以完成数据的修改操作。
-
sql级别的事物控制
在缺省情况下,版本 3会将所有的SQL操作置于antocommit模式下,这样所有针对数据库的修改操作都会在SQL命令执行结束后被自动提交。在SQLite中,SQL命令"BEGIN TRANSACTION"(其中TRANSACTION关键字可选)用于显式地声明一个事务,禁用autocommit模式,即其后的SQL语句在执行后都不会自动提交,而是需要等到SQL命令"COMMIT"或"ROLLBACK"被执行时,才考虑提交还是回滚。
注意BEGIN命令并不获得任何类型的锁,在BEGIN之后,当执行第一个SELECT语句时才得到一个共享锁,当执行第一个DML语句(INSERT、UPDATE或DELETE)时才获得一个保留锁。至于排它锁,只有在数据从内存写入磁盘时开始后,直到事务提交或回滚之前才能持有排它锁。
SQL命令COMMIT命令并不实际提交更改到磁盘,它只是重新打开autocommit模式。然后,在命令结束时,正式的自动提交逻辑才实际提交更改到磁盘。SQL命令ROLLBACK也是打开autocommit模式,但是它设置一标志,以告诉自动提交逻辑执行回滚,而不是提交。如果另外有进程持有共享锁,自动提交逻辑提交更改失败,则autocommit模式会自动关闭。这允许用户在共享锁释放之后重新COMMIT。
如果多个SQL命令在同一个时刻同一个数据库连接中被执行,autocommit将会被延迟执行,直到最后一个命令完成。比如,如果一个SELECT语句正在被执行,在这个命令执行期间,需要返回所有检索出来的行记录,如果此时处理结果集的线程因为业务逻辑的需要被暂时挂起并处于等待状态,而其它的线程此时或许正在该连接上对该数据库执行INSERT、UPDATE或DELETE命令,那么所有这些命令作出的数据修改都必须等到SELECT检索结束后才能被提交。