一、布景

因为咱们现在保护的是一款IM类的运用,需求在本地存储音讯、好友、群、群成员等数据,这就涉及到运用数据库。因为之前是以开发工具类运用为主,数据库方面运用的较少,自18年开始运用数据库以来也走了不少弯路。

所以接下来计划利用几篇文章介绍咱们项目怎样运用数据库,首要讲复杂事务中数据库的运用,以及过程中遇到了哪些问题和咱们都是怎样处理的。这儿尽或许从实际运用动身,处理实际生产过程中的问题。

当然这是站在一个Android APP 运用开发者的角度,描绘在Android APP上运用数据遇到的问题,请路过的数据库大佬嘴下留情~

二、数据库ORM选型

在18年做项目之初,咱们首要调研了两款ORM框架,分别是GreenDao以及Room。

GreenDao地址:github.com/greenrobot/…

Room介绍地址:developer.android.com/training/da…

终究咱们确定运用ROOM,原因也很简略ROOM作为官方开发的数据库ORM框架尽管刚刚推出不久(2018年初调研的),但后边更新保护有保证。一起还能够与 LiveData 合作运用,用于观察某一张表的数据改变。所以果断决定运用了Room。

三、Room怎样运用

1、Room的运用

Room 包含三个首要组件:

  • 数据库类
  • 数据实体
  • 数据访问目标 (DAO)

具体运用办法在官网有具体的介绍,这儿我就不赘述了。

官网地址:developer.android.com/training/da…

官网中首要介绍的是Room框架的运用,这儿我想要点聊的是,复杂事务中数据库要怎样规划。

2、分库

在咱们的项目中,大致能够分为以下几个模块:IM模块、日历模块、音视频模块、小程序、其他。

其中IM是重度依赖数据库存储数据的,音讯、联系人、群、群成员等各类信息都需求持久化到本地。日历模块涉及到日程信息、日历本的存储。
咱们为什么要将数据库拆分呢?

起初时,因为经验不足咱们是将以上一切信息都存储在同一库中的,也便是整个APP只要一个数据库。不过很快咱们就发现数据库的功能比较差。

咱们都知道Android SQLite是存在衔接池概念的,衔接池由一个主链接+多个非主链接构成。SQLite为了保证数据的一致性与完整性,规划为单写多读形式。它们会被序列化,即一个接一个地履行,以防止写抵触。因而,尽管存在多个读衔接是或许的,可是关于写操作,就算在WAL形式下,也只能有一个主衔接在任意时刻对数据库履行写操作。

当一切的数据存储在同一个库中,经过事务一起修正数据就需求阻塞等待了。咱们的项目中作为核心的IM模块收发音讯十分频繁,需求不断的操作数据库,这就影响了其他事务模块。

依据此咱们将数据库从头划分为:

  • 音讯库:用于存储IM模块音讯
  • IM信息库:用于存储音讯外其他装备信息,如联系人、群、群成员、各类装备
  • 日历库:用于存储日程等信息
  • 搜索库:用于存储需求全文检索的数据,削减关于事务库的影响

3、分表

当一个SQLite数据库表中的数据变得庞大时,它或许会导致功能问题,包含查询速度慢和数据刺进功率低下。在咱们的事务中大部分的数据表都是不需求考虑分表的,只要音讯表需求考虑该问题。依据线上埋点核算,部分高频的用户,一天能够发送+接纳音讯数量超越5万条(首要是接纳来自机器人的音讯)。

常见的分表战略大致有:

水平分表

将一张表的数据依照某种规则涣散到多张结构相同的表中。

  • 优势:涣散IO压力,进步查询功能
  • 下风:添加数据管理的复杂性,跨表操作愈加复杂

笔直分表

将一张表按列分割成多张表,每张表存储部分列

  • 优势:削减单次查询的数据量,进步查询功率
  • 下风:添加了JOIN操作的复杂性和本钱,数据不在同一个表中,或许会影响数据一致性的保护

依据时刻分表

依据时刻将数据分配到不同的表中,例如按年、月、日来分表

  • 优势:能够依据数据的时效性采取不同的备份战略
  • 下风:数据跨多个表,复杂查询或许需求跨表JOIN,复杂度添加

依据事务分表

依据事务特性将数据分配到不同的表中

  • 优势:不同的事务表能够独立优化和扩展;更好的契合事务逻辑
  • 下风:跨表查询;分表战略或许会随着事务开展而变得不适用

咱们事务中方式

在咱们的事务中,针对音讯表一起采取了水平分表以及笔直分表的方式。

水平分表

首要依据音讯地点的会话类型进行区别。不同的会话类型地点的表不同。这儿咱们首要针对联系人、群进行分表。一起依据联系人ID以及群ID核算哈希值,依据哈希值分红各十张表。即存在10张联系人音讯表,10张群音讯表,这样水平方向就存在20张表。

不过这样拆分也带来了一个很大的问题,便是在Room中,每张表都要写对应的Dao类,那么就存在20个Dao类。在Dao中除了对应的表名称不同,其他都是相同的。在后续开发中,每次修正都需求一起修正20个类。初期咱们都是硬着头皮来修正,后边针对此咱们开发一个简略AS插件,每次仅需求操作其中一类,AS插件会主动匹配其他19个类动态修正。

一起以上一切核算哈希获取不同表Dao的办法都被咱们封装在了底层,上层事务则不需求关心该获取那张表,只需求将ID传入给底层逻辑就能够了。

笔直分表

笔直方向上,咱们将音讯相关字段做了拆分。一部分作为基础字段放入基础表中,一部分作为扩展字段放入到可扩展的扩展表中。这样在笔直方向就将一张音讯表拆分红了两张表。所以终究在音讯库中共有40张表存储音讯数据。

4、关于WAL

WAL 的全称是 Write Ahead Logging(预写日志),它是很多数据库中用于进步并发功能完成原子事务的一种机制。SQLite 在 3.7.0 版别引进该特性,在此之前 SQLite 完成原子提交和回滚的办法是 rollback journal(回滚日志)。

在WAL形式下,一切的写操作(如INSERT、UPDATE、DELETE)不是直接修正数据库文件,而是首要写入一个单独的日志文件,称为WAL文件(通常是数据库文件名后跟”-wal”的文件)。这些变更在事务提交时被记载,并终究在恰当的时刻被“检查点”(checkpoint)操作合并到主数据库文件。

在这种形式下,读取操作能够在写入操作进行时并行进行。因为读取操作读取的是主数据库文件,在写入期间,主数据库文件不会发生改变。这允许多个读取操作和一个写入操作一起发生,从而进步了并发功能。

关于WAL的介绍来自于:www.sqlite.org/wal.html

在工程中,咱们要保证WAL处于敞开状态,这样才能够将进步数据库的功能。

Room中判断是否敞开WAL代码如下:

JournalMode resolve(Context context) {
  if (this != AUTOMATIC) {
    return this;
  }
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
    ActivityManager manager = (ActivityManager)
        context.getSystemService(Context.ACTIVITY_SERVICE);
    if (manager != null && !isLowRamDevice(manager)) {
      return WRITE_AHEAD_LOGGING;
    }
  }
  return TRUNCATE;
}
private static boolean isLowRamDevice(@NonNull ActivityManager activityManager) {
  if (Build.VERSION.SDK_INT >= 19) {
    return SupportSQLiteCompat.Api19Impl.isLowRamDevice(activityManager);
  }
  return false;
}

能够看到当API大于16(4.1)时 或API大于19(4.4)且非低RAM设备,就会敞开WAL形式。也能够手动调用接口敞开WAL形式。

四、切换到WCDB

在后边的调研中,咱们了解到微信开源了他们的数据库:WCDB。一起WCDB提供了依据Room的封装,所以咱们决定将数据库切换到WCDB。

WCDB是WeChat Database的缩写,顾名思义便是微信开源的数据库,GitHub的wiki如下:
github.com/Tencent/wcd…

因为WCDB支持运用Room ORM 与数据绑定,所以关于咱们工程而言切换十分简略。核心代码:

SQLiteCipherSpec cipherSpec = new SQLiteCipherSpec() // 指定加密方式,运用默许加密能够省掉
    .setPageSize(4096)
    .setKDFIteration(64000);
WCDBOpenHelperFactory factory = new WCDBOpenHelperFactory()
    .passphrase("passphrase".getBytes()) // 指定加密DB密钥,非加密DB去掉此行
    .cipherSpec(cipherSpec)        // 指定加密方式,运用默许加密能够省掉
    .writeAheadLoggingEnabled(true)    // 翻开WAL以及读写并发,能够省掉让Room决定是否要翻开
    .asyncCheckpointEnabled(true);    // 翻开异步Checkpoint优化,不需求能够省掉
AppDatabase db = Room.databaseBuilder(this, AppDatabase.class, "app-db")
        //.allowMainThreadQueries()  // 允许主线程履行DB操作,一般不引荐
        .openHelperFactory(factory)  // 重要:运用WCDB翻开Room
        .build();

代码来自:github.com/Tencent/wcd…

1、WCDB敞开WAL

咱们能够在构建WCDBOpenHelperFactory时,调用writeAheadLoggingEnabled()办法,敞开WAL形式。

对应源码:

public WCDBOpenHelperFactory writeAheadLoggingEnabled(boolean wal) {
  mWALMode = wal;
  return this;
}
@Override
public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
  WCDBOpenHelper result = new WCDBOpenHelper(configuration.context, configuration.name,
      mPassphrase, mCipherSpec, configuration.callback);
  result.setWriteAheadLoggingEnabled(mWALMode);
  result.setAsyncCheckpointEnabled(mAsyncCheckpoint);
  return result;
}

2、遭遇大坑:参数超越999

就在咱们感叹切换是如此简略时,实际就给了一个大闷棍,在灰度期间出现很多的溃散,溃散信息是:too many SQL variables。

经过排查,WCDB有针对SQL参数做约束,最大是999。而在咱们的工程历史代码中有SQL语句运用【x IN list】的用法。这种写法导致SQL的参数具有不确定性,很或许会超越999。所以咱们针对这种情况兵分两路,进行优化。

一路针对咱们的工程进行排查,看看哪些事务逻辑存在这种写法。当list十分大时对查询功能、内存等都有影响。所以咱们倾向于将这类SQL语句结合事务场景将其优化掉。假如不能改,那么就将List集合进行拆分,分批查询。封装分批操作底层工具类,提供给事务逻辑运用。

一路查看WCDB源码,从源码角度看看能否调整其参数约束。终究团队小伙伴经过翻WCDB代码找到了对应的位置:

#ifndef SQLITE_MAX_VARIABLE_NUMBER
# define SQLITE_MAX_VARIABLE_NUMBER 999
#endif
assert( aHardLimit[SQLITE_LIMIT_VARIABLE_NUMBER]==SQLITE_MAX_VARIABLE_NUMBER);
if( x>db->aLimit[SQLITE_LIMIT_VARIABLE_NUMBER] ){
 sqlite3ErrorMsg(pParse, "too many SQL variables");
}

后边咱们将999参数调整更大,然后从头编译了一个包集成到工程中。

五、预告

在后边的文章中,会介绍在咱们的事务中,咱们又遇到了哪些与数据库相关的问题,以及咱们是怎样优化的。首要有以下几点:

1、WCDB全文检索的引进

2、SQL治理与数据库相关溃散处理

3、衔接池忙碌与WCDB源码

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。