问题描述
我们的项目中, 产品需要增加接口 query/add/update/delete, 在其中访问/修改 sqlite数据库中的一个表和 一个 文件.
表里的数据和文件的数据 是 一条一条对应相关的, 需要保持一致, 否则系统会无法使用. 而且同一个数据库/文件 会被多线程/多进程访问.
从最简单的开始:
query() { // 查文件 // 查数据库 add/update/delete { // 增/改/删数据库记录 // 增/改/删文件记录
那么问题来了, 如何保证数据库/文件 记录的正确性? 如何保证 数据库/文件 数据的一致性?
解决方案
问题1: 数据库本身的 一致性:
由dbms保证.
问题2. 文件本身的正确性:
这个可以对文件做checksum. 略...
问题3. 数据库/文件 数据的一致性
问题3.1 多进程同时修改文件, 进程a对文件的修改可能会覆盖了进程b的修改
我们需要某种锁机制;
比较合适的 锁选择有: 文件锁(flock) 和 锁文件(.lck). 最终我们用了 锁文件. 即
lock: 进程每次用 CREATE | EXCL 去打开.lck文件, 如果EEXIST, 则等待-重试;
unlock: 删除.lck文件.
优点在于实现简单; 缺点是 如果进程中途死掉, 则.lck需要手动清除.
问题3.2 如何保证对数据库/文件记录 修改的原子性, 即 要么 数据库/文件都被修改, 要么都没有修改.
这个问题进一步分为两个子问题:
问题3.2.1 更新数据库操作失败, 则不会对文件进行操作
需要检查数据库上一条sql的执行情况.
仅当检查sql执行返回值 为成功, 并且sqlite3_changes(db) != 0时更新文件.
这是因为 update/delete 如果对应的记录不存在, 则sql返回成功, 但实际没有更新数据库.
注: sqlite3_changes返回某个 db connection的上一条修改记录数. 而sqlite要求 一个connection只在一个线程中使用.
问题3.2.2 更新文件失败, 对数据库的操作要回滚
需要 数据库事务. 数据库操作前打开 事务, 仅当更新文件成功, 提交事务; 否则回滚.
这里遇到的问题是, 因为我们是一个接口(remember?). 所以在用户程序调我们的接口前, 可能已经打开了事务. 就是说我们提交的时候 会把外部事物提交.
解决1. 一开始考虑用savepoint. 这是错误的. savepoint类似bdb中的子事务. release时并没有真正提交, 如果父事务回滚, 则此savepoint也回滚.
解决2. 禁止在外部事物中调用我们的 接口. 查db->autoCommit, 如果false, 则外部事物打开. (注: 这是sqlite内部机制, 一般情况下我们应该尽可能用sqlite外部接口, 这里算一个hack).
问题3.4 对文件的修改是否需要多条 write() 系统调用? 如果在中间失败, 文件是否变为不可用?
做类似copy-on-write. 需要修改文件时, 先拷贝到临时文件, 在拷贝文件上修改, 然后rename回来. 这里OS保证rename为原子操作,要么失败, 文件没有改动, 我们回滚数据库; 要么成功, yeah!
遗留问题
最后, 我们的实现还不是完全确保 数据库/文件的一致性. 考虑如下情况:
修改文件, rename成功, 在事务还没有提交的时候, 程序挂了. 文件被修改了, 程序重启后, 未提交事务 被回滚, 则数据库不会改变. rename到事务提交成功之间, 这个window有多大? 在我们的实现, 已经尽量在缩短这个window, 但是 txn commit 可能需要 flush log文件, 这个耗时操作是绕不过去的.
可能的解决方案, 象数据库一样, 写log 做恢复. 但是我们认为 不值得去 做.