深入SQLite,一网打尽“危险操作”

简述

Sqlite Database是一个比较稳定轻量的小型数据库,不像是mysql、oracle等数据库一样具有单独的服务进程。基于sqlite的读写都是读写原始的磁盘文件,也就是说sqlite的操作完全是磁盘的IO操作。SQLite操作存在一定的异常情况,在客户端也难以监控,在用户基数比较大的情况下,往往对极少数的用户的影响也是致命的。了解sqlite便于我们规范的去使用它。

通常的异常情况都包含在以下情况:

  • 文件错写
  • 文件锁 bug
  • 文件 sync 失败
  • 设备损坏
  • 内存覆盖
  • 操作系统 bug
  • SQLite bug

官方的文档也有介绍到

How To Corrupt An SQLite Database File

鉴于操作系统和Sqlite bug等不可控方面我们更多需要做到能准确的监控,如内存不足,类似于微信的做法就是监控到内存不足,弹出提示框告知用户去清理内存。下面就来探讨下由于操作不当引起的异常。

SQLiteDatabaseLockedException

android.database.sqlite.SQLiteDatabaseLockedException DB引擎在执行job的时候发现获取不到数据库的锁就会抛出这个异常。数据库锁用于防止多线程同时写入数据从而导致DB损坏。所以一般发生在多线程的事务操作上。针对同一个事务代码段,如果一个事务尚未结束提交,另外一个线程便开始介入执行就会导致冲突抛出此异常。

所以出现上述异常,请检查你的代码。建议的操作是:

  1. 只使用一个SQLOpenHelper来访问和操作database,单例你的SQLOpenHelper,避免多个OpenHelper多次open db并写入数据。
  2. 当不再需要继续操作db的时候,关闭所有database helper实例
  3. 做事务操作的时候一定要保证事务完成调用了endTransaction() 或者transaction Successful相关方法。下一个事务操作会在entransaction结束后请求锁。

文件锁

系统文件锁问题SQLite依赖于底层的文件系统对文件锁的实现。SQLite 默认锁是协同锁。假设有A、B线程同时访问数据库并写入数据。这个时候来个C线程不是使用SQLite API的方式来操作数据的话(如直接的IO操作),那么数据库锁就会被自动取消,A、B线程在没有锁保护的状态同时操作db就可能导致DB的损坏。

建议:

  1. 尽量使用SQLite API来操作数据库的读写。
  2. 如果有IO线程直接操作数据库,代码逻辑上保证线程间的同步顺序。

文件sync失败

批量写入比较大的数据我们可能想要更快的速度,网上很多会建议你设置PRAGMA synchronous=OFF, Android对应的设置方式就是:mDatabase.execSQL("PRAGMA synchronous=OFF"); 等同于在说:我期待更快的写入速度,磁盘驱动器为了达到目标,就耍了个小聪明,忽略开始SQLite的同步操作,先通知到系统我写入完了。实际上不一定数据完全写入了,假如这个时候出现了断电等意外,就会导致真实数据写入失败从而引发DB损坏。

建议:

尽量不要设置PRAGMA synchronous=OFF 数据比较大的写入等操作从sql语句优化和性能的优化入手。

多db操作

很多较为复杂的应用可能一个应用程序不止有一个数据库。这里列举一个场景,应用包含两个DB。 DatabaseA:预置数据的数据库,已经设计好表并提前插入了数据。不需要跟随应用动态的去创建DB。用户第一次安装的时候把准备好的DB通过IO的方式拷贝到应用包路径下面的databases路径提供给应用访问,此DB一般只用读,很少或者完全不写入数据,是静态的。 DatabaseB: 跟随应用的安装和升级动态的创建和升级,作为应用数据的持久化方式之一。读写操作均比较频繁。

DababaseA会在应用的启动的时候IO迁移拷贝。DatabaseB动态Create后到处可能都有使用到(包括启动)

Danger 1:文件覆盖

DatabaseB的DBOpenHelperB正在读写DatabaseA的表,同时DatabaseA正在被IO线程拷贝写入文件覆盖。DBOpenHelperB建立连接可能依然是旧的数据库,读写自然也是到旧的数据库。部分文件系统甚至引发IO拷贝被中断。

Danger 2:交叉读写

一般有多个DB会对应多个DBOpenHelper进行读写,如过DBOpenHelper做了跨DB的读写,就会导致重复的DB Open或Close,影响到了其他DBOpenHelper的正常读写。直接的导致锁异常或者DB损坏等问题。

多DB建议:

  1. 非SQLite API线程IO数据库请保持IO完全结束后再建立与数据库的读写连接或读写操作 管理好你的DBHelper
  2. DBHelper避免跨DB的操作,一个DB对应一个DBHelper实例

综上多数情况都是代码上操作不当引发的问题,了解好原理才能对症下药,避开危险区

补充: 减少多进程或多线程操作,尽可能单线程写; 减少事务操作,减小事务复杂度。

猜你喜欢

转载自juejin.im/post/5b3e14006fb9a04fea5878ae