如何保证 数据库/文件 之间的同步

问题描述

我们的项目中, 产品需要增加接口 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 做恢复. 但是我们认为 不值得去 做.

posted @ 2016-02-29 18:03  brayden  阅读(1539)  评论(0编辑  收藏  举报