一、布景
因为咱们现在保护的是一款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();
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源码