HarmonyOS Next IM实战:数据库查询慢的优化过程分享_dataorm harmony
HarmonyOS Next IM实战:数据库查询慢的优化过程分享
1、背景介绍
在IMSDK开发中,客户端要使用关系型数据库存储会话、用户、消息等数据,最开始做C端应用一直没发现问题,今年开始有B端用户使用时反馈应用卡,消息延迟等,经过定位发现是B端用户的会话和消息更多,在数据库查询时更慢,由于之前都在主线程所以导致应用卡和慢。本文分享整个优化过程中的遇到问题、解决思路,最终效果等。
2、HarmonyOS Next关系型数据库介绍
HarmonyOS Next关系型数据库基于SQLite组件,适用于存储包含复杂关系数据的场景,比如一个用户的会话信息,需要包括会话名称、会话类型、会话ID等,又或者会话的聊天信息,需要包括消息id、消息类型、消息内容等,由于数据之间有较强的对应关系,复杂程度比键值型数据更高,此时需要使用关系型数据库来持久化保存数据。
关系型数据库对应用提供通用的操作接口,底层使用SQLite作为持久化存储引擎,支持SQLite具有的数据库特性,包括但不限于事务、索引、视图、触发器、外键、参数化查询和预编译SQL语句。
使用关系型数据库实现数据持久化,需要获取一个RdbStore,其中包括建库、建表、升降级等操作。示例代码如下所示:
import { relationalStore } from \'@kit.ArkData\'; // 导入模块import { UIAbility } from \'@kit.AbilityKit\';import { BusinessError } from \'@kit.BasicServicesKit\';import { window } from \'@kit.ArkUI\';// 此处示例在Ability中实现,使用者也可以在其他合理场景中使用class EntryAbility extends UIAbility { onWindowStageCreate(windowStage: window.WindowStage) { // 若希望使用分词器,可调用isStorageTypeSupported检查希望使用的分词器是否支持当前平台。 let tokenType = relationalStore.Tokenizer.ICU_TOKENIZER; let tokenTypeSupported = relationalStore.isTokenizerSupported(tokenType); if (!tokenTypeSupported) { console.error(`ICU_TOKENIZER is not supported on this platform.`); } const STORE_CONFIG: relationalStore.StoreConfig = { name: \'RdbTest.db\', // 数据库文件名 securityLevel: relationalStore.SecurityLevel.S3, // 数据库安全级别 encrypt: false, // 可选参数,指定数据库是否加密,默认不加密 customDir: \'customDir/subCustomDir\', // 可选参数,数据库自定义路径。数据库将在如下的目录结构中被创建:context.databaseDir + \'/rdb/\' + customDir,其中context.databaseDir是应用沙箱对应的路径,\'/rdb/\'表示创建的是关系型数据库,customDir表示自定义的路径。当此参数不填时,默认在本应用沙箱目录下创建RdbStore实例。 isReadOnly: false, // 可选参数,指定数据库是否以只读方式打开。该参数默认为false,表示数据库可读可写。该参数为true时,只允许从数据库读取数据,不允许对数据库进行写操作,否则会返回错误码801。 tokenizer: tokenType // 可选参数,指定用户在全文搜索场景(FTS)下使用哪种分词器。当此参数不填时,则在FTS下仅支持英文分词,不支持其他语言分词。 }; // 判断数据库版本,如果不匹配则需进行升降级操作 // 假设当前数据库版本为3,表结构:EMPLOYEE (NAME, AGE, SALARY, CODES, IDENTITY) const SQL_CREATE_TABLE = \'CREATE TABLE IF NOT EXISTS EMPLOYEE (ID INTEGER PRIMARY KEY AUTOINCREMENT, NAME TEXT NOT NULL, AGE INTEGER, SALARY REAL, CODES BLOB, IDENTITY UNLIMITED INT)\'; // 建表Sql语句, IDENTITY为bigint类型,sql中指定类型为UNLIMITED INT relationalStore.getRdbStore(this.context, STORE_CONFIG, (err, store) => { if (err) { console.error(`Failed to get RdbStore. Code:${err.code}, message:${err.message}`); return; } console.info(\'Succeeded in getting RdbStore.\'); // 当数据库创建时,数据库默认版本为0 if (store.version === 0) { store.executeSql(SQL_CREATE_TABLE) // 创建数据表,以便后续调用insert接口插入数据 .then(() => { // 设置数据库的版本,入参为大于0的整数 store.version = 3; }) .catch((err: BusinessError) => { console.error(`Failed to executeSql. Code:${err.code}, message:${err.message}`); }); } // 如果数据库版本不为0且和当前数据库版本不匹配,需要进行升降级操作 // 当数据库存在并假定版本为1时,例应用从某一版本升级到当前版本,数据库需要从1版本升级到2版本 if (store.version === 1) { // version = 1:表结构:EMPLOYEE (NAME, SALARY, CODES, ADDRESS) => version = 2:表结构:EMPLOYEE (NAME, AGE, SALARY, CODES, ADDRESS) store.executeSql(\'ALTER TABLE EMPLOYEE ADD COLUMN AGE INTEGER\') .then(() => { store.version = 2; }).catch((err: BusinessError) => { console.error(`Failed to executeSql. Code:${err.code}, message:${err.message}`); }); } // 当数据库存在并假定版本为2时,例应用从某一版本升级到当前版本,数据库需要从2版本升级到3版本 if (store.version === 2) { // version = 2:表结构:EMPLOYEE (NAME, AGE, SALARY, CODES, ADDRESS) => version = 3:表结构:EMPLOYEE (NAME, AGE, SALARY, CODES) store.executeSql(\'ALTER TABLE EMPLOYEE DROP COLUMN ADDRESS\') .then(() => { store.version = 3; }).catch((err: BusinessError) => { console.error(`Failed to executeSql. Code:${err.code}, message:${err.message}`); }); } // 请确保获取到RdbStore实例,完成数据表创建后,再进行数据库的增、删、改、查等操作 }); }}
HarmonyOS Next提供了relationalStore来对数据库进行操作,但是在增删改查中,我们一般会做一层封装,数据库结果集与实际使用的对象做转换。
3、ORM框架介绍
HarmonyOS Next提供了dataORM库做数据关系映射,dataORM 是一个轻量级 ORM(对象关系映射)库,用于简化本地数据库的操作。提供了高效的数据库访问性能和低内存消耗。dataORM 支持多线程操作、链式调用、备份、升级、缓存等特性等功能。其设计理念是轻量、快速且易于使用,帮助开发者快速构建高性能的应用程序。dataORM在使用时有一些限制,比如数据库表不支持联合主键等,现在官方已不再维护该库。
后面字节开源了rdbStore,rdbStore 以DTO对象形式来进行数据库操作,封装数据库创建和自动升级、数据库谓词构建、查询结果反序列化、品质调优等能力,实现简单高效地进行数据库操作。运维方面:完备单元测试、品质数据打点上报、全链路日志等。由于在数据库升级等方面遇到问题,所以现在切换到了rdbStore做数据关系映射。
4、多线程优化
大数据量场景下查询数据可能会导致耗时长甚至应用卡死,一般建议如下:
- 单次查询数据量不超过5000条。
- 在TaskPool中查询。
- 拼接SQL语句尽量简洁。
- 合理地分批次查询。
在用户会话数量达到以前后,由于设计到一个联表查询,展开后有50列,查询好耗费六七秒,导致应用很卡,甚至出现冻结异常,所以将复杂耗时的查询放到子线程。
@Concurrent async function getConvDetailObservableThread(context: Context, param: Param, convId: number): Promise { console.log(\'ConvImpl\', \"getConvDetailObservableThread,convId=\" + convId); //线程存储参数上下文等 await DBManager.getInstance().initDB(); const conv = await DBManager.getInstance() .getConvDaoHelper() .getConvById(convId); ...}const result = await taskpool.execute(getConvDetailObservableThread, context, param, convId);
线程间数据传递默认是拷贝方式,如果为了极致的效率体验可以改为Sendable方式。
5、数据查询慢优化
虽然经过上述优化后卡顿已经基本解决,但是查询仍然耗时长的问题没有解决,第一次进入会话列表要等七八秒,用户体验很差,而且还有个奇怪现象,Mac版本模拟器耗时几十毫秒,而真机特别慢需要六七秒。
逐步分析查询过程,由于这里涉及到联表查询,所以使用relationalStore接口直接做查询,执行sql很快,执行完查询语句,结果集resultSet.goToNextRow()的过程很耗时。
最开始遍历是按行按列读取,resultSet.getLong(resultSet.getColumnIndex(\'unreadMsgCount\'));
,官方在API12提供了geRow的接口,可以一次性直接读取一列,改为getRow方式后从七八秒优化到了七八百毫秒,速度快了十倍。
在API18中又增加了新的接口getRows,可以将指定的行直接全部读取出,效果更好。
总结
本文围绕IMSDK开发中B端用户因会话和消息数据量大导致数据库查询卡顿的问题,详细阐述了基于HarmonyOS Next关系型数据库的优化过程。首先介绍了HarmonyOS Next关系型数据库基于SQLite的特性,包括事务、索引等功能及RdbStore的使用方式;随后对比了dataORM与rdbStore两种ORM框架,因dataORM不再维护且升级存在问题,最终切换至更高效的rdbStore。 在性能优化方面,通过多线程方案将复杂查询移至子线程,避免主线程阻塞,同时遵循单次查询不超过5000条数据、简洁SQL拼接等原则;针对查询耗时问题,分析发现结果集遍历效率低下,通过使用HarmonyOS提供的getRow、getRows等新接口,将查询时间从七八秒优化至七八百毫秒,大幅提升了数据读取效率。此次优化不仅解决了应用卡顿和消息延迟问题,也为HarmonyOS平台下大数据量场景的数据库操作提供了可参考的实践经验。