http://www.ox-holdings.com

在移动端的数据持久化方式总体可以分为以下两类,我们可以提示用户进行空间清理

摘要微信开发团队宣布将微信自用的移动端数据库组件 WCDB(WeChat Database)正式对外开源。前言微信团队两周前预告即将开源WCDB后(详见当时资讯《[资讯] 微信移动端数据库组件WCDB即将开源!》),于2017年06月09日正式开源了微信自用的移动端数据库组件 WCDB(WeChat Database)。WCDB简介WCDB是一个高效、完整、易用的移动数据库框架,基于 SQLCipher,支持 iOS、macOS 和 Android。微信高级工程师何俊伟表示:“开源只是故事的开始,我们仍会持续对 WCDB 做改进,包括更易用的接口、更好的性能、更高的可靠性。这些改进最终也会原封不动地在微信使用。”WCDB(WeChatDataBase)是微信官方的移动端数据库组件,致力于提供一个高效、易用、完整的移动端存储方案。它包含三个模块:WCDB-iOS/Mac;WCDB-Android;数据库损坏修复工具WCDBRepair。WCDB的开发背景对于iOS开发者来说,数据库的技术选型一直是个令人头痛的问题。由于Apple提供的CoreData框架差强人意,使得开发者们纷纷将目光投向开源社区,寻找更好的存储方案。 对于微信也是如此。数据库是微信内最基础的组件之一,消息收发、联系人、朋友圈等等业务都离不开数据库的支持。为了满足需求,我们也对现有方案做了对比研究。目前移动端数据库方案按其实现可分为两类:关系型数据库,代表有CoreData、FMDB等。CoreData 它是苹果内建框架,和Xcode深度结合,可以很方便进行ORM;但其上手学习成本较高,不容易掌握。稳定性也堪忧,很容易crash;多线程的支持也比较鸡肋。FMDB 它基于SQLite封装,对于有SQLite和ObjC基础的开发者来说,简单易懂,可以直接上手;而缺点也正是在此,FMDB只是将SQLite的C接口封装成了ObjC接口,没有做太多别的优化,即所谓的胶水代码(Glue

前沿介绍

腾讯开源微信数据库框架WCDB,他是一个高效、完整、易用的移动数据库框架,基于SQLCipher,支持iOS, macOS和Android。

你好,WCDB

WCDB是一个高效、完整、易用的移动数据库框架,基于SQLCipher,支持iOS, macOS和Android。

一、数据库介绍

SQLite 3 比较常见不予赘述。

Realm 是由Y Combinator孵化的创业团队开源出来的一款可以用于iOS(同样适用于Swift&Objective-C)和Android的跨平台移动数据库。目前最新版是Realm 2.0.2,支持的平台包括Java,Objective-C,Swift,React Native,Xamarin。

优势:兼顾iOS和Android两个平台;简单易用,学习成本低;提供了一个轻量级的数据库查看工具,开发者可以查看数据库当中的内容,执行简单的插入和删除数据的操作。

Realm支持事务,满足ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。

WCDB 是微信推出的一个高效、完整、易用的移动数据库框架,基于SQLCipher(an SQLite extension that provides 256 bit AES encryption of database files.),支持iOS, macOS和Android。易用,支持事务,可加密、损坏修复。

了解移动端的数据持久化方式和对应的使用场景,提供相关技术选型做技术储备。

Code)。使用过程需要用大量的代码拼接SQL、拼装Object,并不方便。key-value数据库,代表有Realm、LevelDB、RocksDB等。Realm因其在各平台封装、优化的优势,比较受移动开发者的欢迎。对于iOS开发者,key-value的实现直接易懂,可以像使用NSDictionary一样使用Realm。并且ORM彻底,省去了拼装Object的过程。但其对代码侵入性很强,Realm要求类继承RLMObject的基类。这对于单继承的ObjC,意味着不能再继承其他自定义的子类。同时,key-value数据库对较为复杂的查询场景也比较无力。可见,各个方案都有其独特的优势及劣势,没有最好的,只有最适合的。而对于微信来说,我们所期望的数据库应满足:高效;增删改查的高效是数据库最基本的要求。除此之外,我们还希望能够支持多个线程高并发地操作数据库,以应对微信频繁收发消息的场景。易用;这是微信开源的原则,也是WCDB的原则。SQLite本不是一个易用的组件:为了完成一个查询,往往我们需要写很多拼接字符串、组装Object的胶水代码。这些代码冗长繁杂,而且容易出错,我们希望组件能统一完成这些任务。完整;数据库操作是一个复杂的场景,我们希望数据库组件能完整覆盖各种场景。包括数据库损坏、监控统计、复杂的查询、反注入等。显然,上述各个方案都不能完全满足微信的需求。于是,我们造了这个“轮子”

WCDB-iOS/Mac。WCDB通过ORM和WINQ,体现了其易用性上的优势,使得数据库操作不再繁杂。同时,通过链式调用,开发者也能够方便地获取数据库操作的耗时等性能信息。而高级用法则扩展了WCDB的功能和用法。详情请见:《微信移动端数据库组件WCDB系列(一)-iOS基础篇》WCDB源码下载WCDB源码托管地址:

便捷地定义表、索引、约束,并进行增删改查操作

项目演示效果如下:

图片 1

Markdown

1 基本特性

  • 易用,WCDB支持一句代码即可将数据取出并组合为object。

  • WINQ(WCDB语言集成查询):通过WINQ,开发者无须为了拼接SQL的字符串而写一大坨胶水代码。

  • ORM(Object Relational Mapping):WCDB支持灵活、易用的ORM。开发者可以很便捷地定义表、索引、约束,并进行增删改查操作。

[database getObjectsOfClass:WCTSampleConvenient.class
                    fromTable:tableName
                        where:WCTSampleConvenient.intValue>=10
                        limit:20];                      
  • 高效,WCDB通过框架层和sqlcipher源码优化,使其更高效的表现。

  • 多线程高并发:WCDB支持多线程读与读、读与写并发执行,写与写串行执行。

  • 批量写操作性能测试

    图片 2

更多关于WCDB的性能数据,请参考benchmark。
  • 完整,WCDB覆盖了数据库相关各种场景的所需功能。

  • 加密:WCDB提供基于SQLCipher的数据库加密。

  • 损坏修复:WCDB内建了Repair Kit用于修复损坏的数据库。

  • 反注入:WCDB内建了对SQL注入的保护。

二、测试数据表结构

Student表。
字段:ID、name、age、money。

图片 3

image.png

其中age为0~100随机数字,money为每一万条数据中,0~10000各个数字只出现一次。

  1. 快速展示,提升体验
    • 已经加载过的数据,用户下次查看时,不需要再次从网络加载,直接展示给用户
  2. 节省用户流量
    • 对于较大的资源数据进行缓存,下次展示无需下载消耗流量
    • 同时降低了服务器的访问次数,节约服务器资源。
  3. 离线使用。
    • 用户浏览过的数据无需联网,可以再次查看。
    • 部分功能使用解除对网络的依赖。(百度离线地图、图书阅读器)
    • 无网络时,允许用户进行操作,等到下次联网时同步到服务端。
  4. 记录用户操作
    • 草稿:对于用户需要花费较大成本进行的操作,对用户的每个步骤进行缓存,用户中断操作后,下次用户操作时直接继续上次的操作。
    • 已读内容标记缓存,帮助用户识别哪些已读。
    • 搜索记录缓存...

微信 即时通讯软件

  • 微信(英文名:wechat)是腾讯公司于2011年1月21日推出的一个为智能终端提供即时通讯服务的免费应用程序

  • 微信支持跨通信运营商、跨操作系统平台通过网络快速发送免费语音短信、视频、图片和文字

  • 同时,也可以使用通过共享流媒体内容的资料和基于位置的社交插件“摇一摇”、“漂流瓶”、“朋友圈”、”公众平台“、”语音记事本“等服务插件。

  • 腾讯于6月9日在 GMTC 全球移动技术大会上正式宣布, WCDB(WeChat Database)作为微信的一个开源组件正式对外开源

2 数据库修复方案

通过收集到的大量案例和日志,分析出实际上移动端数据库损坏的真正原因其实就3个:

  • 空间不足
  • 设备断电
  • 文件 sync 失败

我们需要针对这些原因一一进行优化

三、测试数据

对于以下测试数据,只是给出一次测试后的具体数值供参考,经过反复测试后的,基本都在这个时间量级上。

这里测试用的是纯SQLite,没有用FMDB。

SQLite3:

  • 9万条数据基础上连续单条插入一万条数据耗时:1462ms。
  • 已经建立索引,需要注意的是,如果是检索有大量重复数据的字段,不适合建立索引,反而会导致检索速度变慢,因为扫描索引节点的速度比全表扫描要慢。比如当我对age这个经常重复的数据建立索引再对其检索后,反而比不建立索引查询要慢一倍多。
  • 已经设置WAL模式。
  • 简单查询一万次耗时:331ms
  • dispatch 100个block来查询一万次耗时:150ms

realm:

  • 9万条数据基础上连续单条插入一万条数据耗时:32851ms。
  • 注意,Realm似乎必须通过事务来插入,所谓的单条插入即是每次都开关一次事务,耗时很多,如果在一次事务中插入一万条,耗时735ms。
  • 已经建立索引。
  • 简单查询一万次耗时:699ms。
  • dispatch 100个block来查询一万次耗时:205ms。

WCDB:

  • 9万条数据基础上连续单条插入一万条数据耗时:750ms。
  • 此为不用事务操作的时间,如果用事务统一操作,耗时667ms。
  • 已经建立索引。
  • 简单查询一万次耗时:690ms。
  • dispatch 100个block来查询一万次耗时:199ms。

三者对比:

图片 4

image.png

由于Realm单次事务操作一万次耗时过长,图表中显示起来也就没有了意义,因此下面图中Realm的耗时是按照事务批量操作耗时来记录的,实际上WCDB的插入操作是优于Realm的。

图片 5

image

图片 6

image

从结果来看,Realm似乎必须用事务,单条插入的性能会差很多,但是用事务来批量操作就会好一些。按照参考资料[3]中的测试结果,Realm在插入速度上比SQLite慢,比用FMDB快,而查询是比SQLite快的。

而WCDB的表现很让人惊喜,其插入速度非常快,以至于比SQLite都快了一个量级,要知道WCDB也是基于SQLite扩展的。WCDB的查询速度也还可以接受,这个结果其实跟其官方给出的结果差不多:读操作基本等于FMDB速度,写操作比FMDB快很多。

在移动端的数据持久化方式总体可以分为以下两类:

基本功能

图片 7

Markdown

  • 基于SQLCipher的数据库加密

  • 使用连接池实现并发读写

  • 内建 Repair Kit 可用于修复损坏数据库

  • 针对占用空间大小优化的数据库备份/恢复功能

  • 日志输出重定向以及性能跟踪接口

  • 内建用于全文搜索的 mmicu FTS3/4 分词器
    入门

2.1 优化空间占用

  • 业务文件先申请后使用,如果某个文件没有申请就使用了,会被自动扫描出来并删除;
  • 每个业务文件都要申明有效期,是一天、一个星期、一个月还是永久存储;
  • 过期文件会被自动清理。

对于微信之外的空间占用,例如相册、视频、其他App的空间占用,微信本身是做不了什么事情的,我们可以提示用户进行空间清理

四、Realm优缺点

优点:

  • Realm在使用上和Core Data有点像,直接建立我们平常的对象Model类就是建立一个表了,确定主键、建立索引也在Model类里操作,几行代码就可以搞定,在操作上也可以很方便地增删改查,不同于SQLite的SQL语句(即使用FMDB封装的操作依然有点麻烦),Realm在日常使用上非常简单,起码在这次测试的例子中两个数据库同样的一些操作,Realm的代码只有SQLite的一半。
  • 其实Realm的“表”之间也可以建立关系,对一、对多关系都可以通过创建属性来解决。
  • 在.m方法中给“表”确定主键、属性默认值、加索引的字段等。
  • 修改数据时,可以直接丢进去一条数据,Realm会根据主键判断是否有这个数据,有则更新,没有则添加。
  • 查询操作太简单了,一行代码根据查询目的来获取查询结果的数组。
  • 支持KVC和KVO。
  • 支出数据库加密。
  • 支持通知。
  • 方便进行数据库变更(版本迭代时可能发生表的新增、删除、结构变化),Realm会自行监测新增加和需要移除的属性,然后更新硬盘上的数据库架构,Realm可以配置数据库版本,进行判断。
  • 一般来说Realm比SQLite在硬盘上占用的空间更少。

缺点:

  • Realm也有一些限制,需要考虑是否会影响。
  • 类名长度最大57个UTF8字符。
  • 属性名长度最大63个UTF8字符。
  • NSData及NSString属性不能保存超过16M数据,如果有大的可以分块。
  • 对字符串进行排序以及不区分大小写查询只支持“基础拉丁字符集”、“拉丁字符补充集”、“拉丁文扩展字符集 A” 以及”拉丁文扩展字符集 B“(UTF-8 的范围在 0~591 之间)。
  • 多线程访问时需要新建新的Realm对象。
  • Realm没有自增属性。。也就是说对于我们习惯的自增主键,如果确实需要,我们要自己去赋值,如果只要求独一无二, 那么可以设为[[NSUUID UUID] UUIDString],如果还要求用来判断插入的顺序,那么可以用Date。
  • Realm支持以下的属性类型:BOOL、bool、int、NSInteger、long、long long、float、double、NSString、NSDate、NSData以及 被特殊类型标记的NSNumber,注意,不支持集合类型,只有一个集合RLMArray,如果服务器传来的有数组,那么需要我们自己取数据进行转换存储。
  • 定义

    对于使用频率比较高的数据,从网络或者磁盘加载数据到内存以后,使用后并不马上销毁,下次使用时直接从内存加载。

  • 案例

    • iOS系统图片加载——[UIImage imageNamed:@"imageName"]
    • 网络图片加载三方库:SDWebImage

官方介绍如下:

WCDB 是一个高效、完整、易用的移动数据库框架,基于 SQLCipher,支持 iOS、macOS 和 Android。

2.2 优化文件 sync

五、WCDB优缺点

优点:

实际体验后,WCDB的代码体验非常好,代码量基本等于Realm,都是SQLite的一半,在风格上比Realm更接近于OC原本的风格,基本已经感受不到是在写数据库的SQL操作。并且其查询语句WINQ也写的很符合逻辑,基本都可以一看就懂,甚至不需要你了解SQL语句。整个开发流程下来非常流畅,除了配置环境时出了问题并且没有资料参考只能自己猜着解决外,代码基本是一气呵成写完完美运行的。

缺点:

最明显的缺点是其相关资料太少了,毕竟6月初才正式开源,大家可能还在体验阶段,不敢随便上项目,不过其提供了QQ群答疑,而且看了一下代码提交记录,更新很频繁,对于腾讯内部使用来说应该有问题会得到更快解决。

贴一份评论:

图片 8

image

WCDB的好处

  • WINQ(WCDB语言集成查询): 通过WINQ,开发者无须为了拼接SQL的字符串而写一大坨胶水代码。

  • ORM(Object Relational Mapping): WCDB支持灵活、易用的ORM。开发者可以很便捷地定义表、索引、约束,并进行增删改查操作。

  • 多线程高并发: WCDB支持多线程读与读、读与写并发执行,写与写串行执行。

  • 加密:WCDB提供基于SQLCipher的数据库加密。

  • 损坏修复: WCDB内建了Repair Kit用于修复损坏的数据库。

  • 反注入: WCDB内建了对SQL注入的保护。

  • 基于SQLCipher的数据库加密

  • 使用连接池实现并发读写

  • 内建 Repair Kit 可用于修复损坏数据库

  • 针对占用空间大小优化的数据库备份/恢复功能

  • 日志输出重定向以及性能跟踪接口

  • 内建用于全文搜索的 mmicu FTS3/4 分词器

相关代码(SQLiteOpenHelper类)

    /**
     * 类功能描述:</br>
     * 新数据处理帮助类
     * @author 于亚豪
     *  博客地址: http://blog.csdn.net/androidstarjack
     * 公众号: 终端研发部
     * @version 1.0 </p> 修改时间:</br> 修改备注:</br>
     */
    public class EncryptedDBHelper extends SQLiteOpenHelper {

        private static final String TAG = "EncryptedDBHelper";

        private static final String DATABASE_NAME = "encrypted.db";
        private static final String OLD_DATABASE_NAME = "plain-text.db";
        private static final int DATABASE_VERSION = 2;

        private Context mContext;
        private String mPassphrase;

        public EncryptedDBHelper(Context context, String passphrase) {

            // 调用“加密”版本的超类构造函数。
            super(context, DATABASE_NAME, passphrase.getBytes(), null, null, DATABASE_VERSION,
                    null);
            // 保存上下文对象供以后使用。
            mContext = context;
            mPassphrase = passphrase;
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            // 检查数据库plain-text.db是否存在 ,存在 如果是这样,将其导出到新的加密库中的。
            File oldDbFile = mContext.getDatabasePath(OLD_DATABASE_NAME);
            if (oldDbFile.exists()) {

                Log.i(TAG, "Migrating plain-text database to encrypted one.");

                //SQLiteOpenHelper在调用onCreate()之前开始一个事务。 我们必须结束事务才能附加一个新的数据库。
                db.endTransaction();

                // 将旧数据库附加到新创建的加密数据库。
                String sql = String.format("ATTACH DATABASE %s AS old KEY '';",
                        DatabaseUtils.sqlEscapeString(oldDbFile.getPath()));
                db.execSQL(sql);

                // 导出旧数据库。
                db.beginTransaction();
                DatabaseUtils.stringForQuery(db, "SELECT sqlcipher_export('main', 'old');", null);
                db.setTransactionSuccessful();
                db.endTransaction();

                // 获取旧的数据库版本供以后升级。
                int oldVersion = (int) DatabaseUtils.longForQuery(db, "PRAGMA old.user_version;", null);

                // 分离旧数据库并输入新的事务。
                db.execSQL("DETACH DATABASE old;");

                // 旧数据库现在可以删除。
                oldDbFile.delete();

                // 在进一步的操作之前,还原事务。
                db.beginTransaction();

                // 检查我们是否需要升级架构。
                if (oldVersion > DATABASE_VERSION) {
                    onDowngrade(db, oldVersion, DATABASE_VERSION);
                } else if (oldVersion < DATABASE_VERSION) {
                    onUpgrade(db, oldVersion, DATABASE_VERSION);
                }
            } else {
                Log.i(TAG, "Creating new encrypted database.");

                // 如果旧数据库不存在,请进行真正的初始化。
                db.execSQL("CREATE TABLE message (content TEXT, "
                        + "sender TEXT);");
            }

            // 损坏恢复的备份主信息。
            RepairKit.MasterInfo.save(db, db.getPath() + "-mbak", mPassphrase.getBytes());
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

            Log.i(TAG, String.format("Upgrading database from version %d to version %d.",
                    oldVersion, newVersion));

            //将新列添加到数据库升级的消息表中。
            db.execSQL("ALTER TABLE message ADD COLUMN sender TEXT;");

            //损坏恢复的备份主信息
            RepairKit.MasterInfo.save(db, db.getPath() + "-mbak", mPassphrase.getBytes());
        }
    }

WCDB如何查询数据库的相关示例:

new AsyncTask<Void, Void, Cursor>() {
            @Override
            protected void onPreExecute() {
                mAdapter.notifyDataSetChanged();
            }
            @Override
            protected Cursor doInBackground(Void... params) {
                if (mDB == null || !mDB.isOpen())
                    return null;

                String message = "Message inserted on " + DATE_FORMAT.format(new Date());

                if (mDBVersion == 1) {
                    mDB.execSQL("INSERT INTO message VALUES (?);",
                            new Object[]{"yyh"});
                    return mDB.rawQuery("SELECT rowid as _id, content, '???' as sender FROM message;",
                            null);
                } else {
                    mDB.execSQL("INSERT INTO message VALUES (?, ?);",
                            new Object[]{"yyh", "男"});
                    return mDB.rawQuery("SELECT rowid as _id, content, sender FROM message;",
                            null);
                }
            }
            @Override
            protected void onPostExecute(Cursor cursor) {
                if (cursor == null)
                    return;
                list = getAllStudent(cursor);
                mAdapter.changeCursor(list);
            }
        }.execute();

WWCDB如何插入表的相关示例:

final DateFormat DATE_FORMAT = SimpleDateFormat.getDateTimeInstance();
        new AsyncTask<Void, Void, Cursor>() {
            @Override
            protected void onPreExecute() {
                mAdapter.notifyDataSetChanged();
            }
            @Override
            protected Cursor doInBackground(Void... params) {
                if (mDB == null || !mDB.isOpen())
                    return null;

                String message = "Message inserted on " + DATE_FORMAT.format(new Date());

                if (mDBVersion == 1) {
                    mDB.execSQL("INSERT INTO message VALUES (?);",
                            new Object[]{"yyh"});
                    return mDB.rawQuery("SELECT rowid as _id, content, '???' as sender FROM message;",
                            null);
                } else {
                    mDB.execSQL("INSERT INTO message VALUES (?, ?);",
                            new Object[]{"yyh", "男"});
                    return mDB.rawQuery("SELECT rowid as _id, content, sender FROM message;",
                            null);
                }
            }
            @Override
            protected void onPostExecute(Cursor cursor) {
                if (cursor == null)
                    return;
                list = getAllStudent(cursor);
                mAdapter.changeCursor(list);
            }
        }.execute();

WCDB如何删除表的相关示例:

if (mDB == null || !mDB.isOpen()){
        return  ;

    }
    mDB.execSQL("DELETE FROM message WHERE content"+"=?",new Object[]{"yyh"});
    com.tencent.wcdb.Cursor cursor =  mDB.rawQuery("SELECT rowid as _id, content, sender FROM message;",null);
    list = getAllStudent(cursor);
    mAdapter.changeCursor(list);

完整demo下载地址:

相关项目下载地址(github):

https://github.com/androidstarjack/MyWCDBStudy

关于WCDB还有很多的地方要去学习,该demo中只是演示的其中的冰山一角,接下来我们还有很多要探寻的。

WCDB官方地址:

WCDB官方地址

https://github.com/Tencent/wcdb/wiki

2.2.1 synchronous = FULL

设置SQLite的文件同步机制为全同步,亦即要求每个事物的写操作是真的flush到文件里去。

六、结

测试过后,感觉还是比用FMDB方便很多,其中又以WCDB更为推崇,Realm其实也不错,如果是一些新创建的中小型工程,也可以尝试,WCDB刚开源不久,可能还会有一些坑,不过毕竟微信那边出品的,你问我支持不支持我当然是支持的。

需要注意的是如果是老工程想换新数据库,那么需要注意一些数据库迁移的问题,这中间必然存在一些阵痛,此外,Realm和WCDB都会用到自有的Model类来作为表结构。

刚刚上手,如果有哪里有问题或者疏漏,请多多指教。

  • 定义

    将从网络加载的、用户操作产生的数据写入到磁盘,用户下次查看、继续操作时,直接从磁盘加载使用。

  • 案例

    • 用户输入内容草稿缓存(如:评论、文本编辑)
    • 网络图片加载三方库:SDWebImage
    • 搜索历史缓存

相信自己,没有做不到的,只有想不到的

如果你觉得此文对您有所帮助,欢迎入群 QQ交流群 :232203809
微信公众号:终端研发部

图片 9

Markdown

(这里 学到的不仅仅是技术)

2.2.2 fullfsync = 1

通过与苹果工程师的交流,我们发现在 iOS 平台下还有 fullfsync 这个选项,可以严格保证写入顺序跟提交顺序一致。设备开发商为了测评数据好看,往往会对提交的数据进行重排,再统一写入,亦即写入顺序跟App提交的顺序不一致。在某些情况下,例如断电,就可能导致写入文件不一致的情况,导致文件损坏。

参考资料

[1] Realm数据库 从入门到“放弃”: http://www.jianshu.com/p/50e0efb66bdf

[2] Realm中文官方文档:https://realm.io/cn/docs/objc/latest/#section

[3] 移动端数据库新王者:realm(可以看看这篇博客的评论部分,看看坑) http://www.jianshu.com/p/2b4388cf2a2d

[4] realm之于iOS https://zhuanlan.zhihu.com/p/23556740

[5] Core Data, FMDB, Realm 性能测试 http://suree.org/2015/09/29/DatabaseThink/

[6] WCDB 官方说明 https://github.com/Tencent/wcdb/wiki

[7] WCDB 官方iOS使用说明 https://github.com/Tencent/wcdb/wiki/iOS+macOS使用教程

[8] WCDB 官方与FMDB性能对比 https://github.com/Tencent/wcdb/wiki/性能数据与Benchmark


查看作者首页

在缓存设计中,由于硬件设备的存储空间不是无限的,我们期望存储空间不要占用过多,仅能缓存有限的数据,但是我们希望获得更高的命中率。想达到这一目的。通常需要借助缓存算法来实现。

2.3 SQLite 修复逻辑优化

官方修复算法是这样一个流程:从 master 表中读出一个个表的信息,根据根节点地址和创表语句来 select 出表里的数据,能 select 多少是多少,然后插入到一个新 DB 中。要注意的是 master 表他本身也是一个 B+树 形式的普通表,DB 第0页就是他的根节点。那么只要 master 表某个节点损坏,这个节点下面记录的表就都恢复不了。更坏的情况是 DB 第0页损坏,那么整个 master 表都读不出来,就导致整个DB都恢复失败。这就是官方修复算法成功率这么低的原因,太依赖 master 表了。

实现原理:

2.3.1 解析B-tree恢复方案(RepairKit)

正常情况下,SQLite 引擎打开DB后首次使用,需要先遍历sqlite_master,并将里面保存的SQL语句再解析一遍, 保存在内存中供后续编译SQL语句时使用。假如sqlite_master损坏了无法解析,“Dump恢复”这种走正常SQLite 流程的方法,自然会卡在第一步了。为了让sqlite_master受损的DB也能打开,需要想办法绕过SQLite引擎的逻辑。 由于SQLite引擎初始化逻辑比较复杂,为了避免副作用,没有采用hack的方式复用其逻辑,而是决定仿造一个只可以 读取数据的最小化系统

sqlite_master信息量比较小,而且只有改变了表结构的时候(例如执行了CREATE TABLE、ALTER TABLE等语句)才会改变,因此对它进行备份成本是非常低的,一般手机典型只需要几毫秒到数十毫秒即可完成,一致性也容易保证, 只需要执行了上述语句的时候重新备份一次即可。有了备份,我们的逻辑可以在读取DB自带的sqlite_master失败的时候 使用备份的信息来代替。

DB初始化的问题除了文件头和sqlite_master完整性外,还有加密。SQLCipher加密数据库,对应的恢复逻辑还需要加上 解密逻辑。按照SQLCipher的实现,加密DB 是按page 进行包括头部的完整加密,所用的密钥是根据用户输入的原始密码和 创建DB 时随机生成的 salt 运算后得出的。可以猜想得到,如果保存salt错了,将没有办法得出之前加密用的密钥, 导致所有page都无法读出了。由于salt 是创建DB时随机生成,后续不再修改,将它纳入到备份的范围内即可

到此,初始化必须的数据就保证了,可以仿造读取逻辑了。我们常规使用的读取DB的方法(包括dump方式恢复), 都是通过执行SQL语句实现的,这牵涉到SQLite系统最复杂的子系统——SQL执行引擎。我们的恢复任务只需要遍历B-tree所有节点, 读出数据即可完成,不需要复杂的查询逻辑,因此最复杂的SQL引擎可以省略。同时,因为我们的系统是只读的, 写入恢复数据到新 DB 只要直接调用 SQLite 接口即可,因而可以省略同样比较复杂的B-tree平衡、Journal和同步等逻辑。 最后恢复用的最小系统只需要:

  • VFS读取部分的接口(Open/Read/Close),或者直接用stdio的fopen/fread、Posix的open/read也可以

  • SQLCipher的解密逻辑

  • B-tree解析逻辑

即可实现

图片 10

B-tree解析好处是准备成本较低,不需要经常更新备份,对大部分表比较少的应用备份开销也小到几乎可以忽略, 成功恢复后能还原损坏时最新的数据,不受备份时限影响。 坏处是,和Dump一样,如果损坏到表的中间部分,比如非叶子节点,将导致后续数据无法读出。

使用 Repair Kit 可以直接从损坏的数据库里尽量读出未损坏的数据,不需要事先准备, 但是先备份 Master 信息可以大大增加恢复成功率。 如果有意使用 Repair Kit 恢复数据库, 建议备份 Master 信息

FIFO 先进先出的核心思想如果一个数据最先进入缓存中,则应该最早淘汰掉。类似实现一个按照时间先后顺序的队列来管理缓存,将淘汰最早访问的数据缓存。

2.3.2 备份方案

主要的方案有:

  • 拷贝: 不能再直白的方式。由于SQLite DB本身是文件(主DB + journal 或 WAL), 直接把文件复制就能达到备份的目的。

  • Dump: 上一个恢复方案用到的命令的本来目的。在DB完好的时候执行.dump, 把 DB所有内容输出为 SQL语句,达到备份目的,恢复的时候执行SQL即可。

  • Backup API: SQLite自身提供的一套备份机制,按 Page 为单位复制到新 DB, 支持热备份。

对以上方案做简单测试后,备份方案也就基本定下了。测试用的DB大小约 50MB, 数据条目数大约为 10万条:

图片 11

微信在Dump + gzip方案上再加以优化,由于格式化SQL语句输出耗时较长,因此使用了自定义 的二进制格式承载Dump输出。第二耗时的压缩操作则放到别的线程同时进行,在双核以上的环境 基本可以做到无额外时间消耗。由于数据保密需要,二进制Dump数据也做了加密处理。 采用自定义二进制格式还有一个好处是,恢复的时候不需要重复的编译SQL语句,编译一次就可以 插入整个表的数据了,恢复性能也有一定提升。优化后的方案比原始的Dump + 压缩, 每秒备份行数提升了 150%,每秒恢复行数也提升了 40%

示意图:

2.3.3 不同方案的组合

由于解析B-tree恢复原理和备份恢复不同,失败场景也有差别,可以两种手段混合使用覆盖更多损坏场景。 微信的数据库中,有部分数据是临时或者可从服务端拉取的,这部分数据可以选择不修复,有些数据是不可恢复或者 恢复成本高的,就需要修复了。

如果修复过程一路都是成功的,那无疑使用B-tree解析修复效果要好于备份恢复。备份恢复由于存在 时效性,总有部分最新的记录会丢掉,解析修复由于直接基于损坏DB来操作,不存在时效性问题。 假如损坏部分位于不需要修复的部分,解析修复有可能不发生任何错误而完成。

若修复过程遇到错误,则很可能是需要修复的B-tree损坏了,这会导致需要修复的表发生部分或全部缺失。 这个时候再使用备份修复,能挽救一些缺失的部分。

最早的Dump修复,场景已经基本被B-tree解析修复覆盖了,若B-tree修复不成功,Dump恢复也很有可能不会成功。 即便如此,假如上面的所有尝试都失败,最后还是会尝试Dump恢复。

图片 12

注:了解到iOS端恢复方式只提供Repair Kit, 且所有的备份和恢复操作都需要开发人员自己调用相应的接口

图片 13image

3 SQLite源文件优化

问题:

郑重声明:本文版权归新匍京a奥门-最全网站手机版app官方下载所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。