背景
在Soul的IM上線后故俐,初始時用戶本地消息量不大的情況下疮蹦,數(shù)據(jù)庫讀寫良好窖剑,不容易發(fā)現(xiàn)問題辕宏。
但隨著產(chǎn)品用的時間越來越近痊远,有些用戶本地聊天數(shù)據(jù)達到500萬條以上時,數(shù)據(jù)庫性能瓶頸逐漸體現(xiàn)出來刚陡。
1.讀寫數(shù)據(jù)較慢惩妇。
2.讀寫在同一線程里,當大量數(shù)據(jù)寫入時筐乳,遲遲讀不出數(shù)據(jù)歌殃,體驗較差
所以數(shù)據(jù)庫這塊急需優(yōu)化
優(yōu)化之前的方案
之前im用的是著名三方數(shù)據(jù)庫FMDB,它只是簡單的對sqlite進行了封裝蝙云,線程安全方案是把所有的數(shù)據(jù)庫操作放到了一個串行隊列里去同步執(zhí)行氓皱。
因為sqlite是不能多個操作同時訪問一個連接,不然會crash。
這樣做的好處是確保了數(shù)據(jù)庫連接數(shù)據(jù)讀寫安全波材,缺點是無法進行線程并發(fā)操作股淡。
對于IM這種對海量數(shù)據(jù)存儲性能要求較高的項目,F(xiàn)MDB不能滿足需求廷区。
優(yōu)化思考
我們所要做的是支持多線程讀寫的數(shù)據(jù)庫唯灵。
這里介紹下WAL模式:SQLite引入了 WAL 模式,即 Write-Ahead Log隙轻。在這種模式下埠帕,所有的修改會寫入一個單獨的 WAL 文件內(nèi)。這種模式下玖绿,寫操作甚至可以不去操作數(shù)據(jù)庫敛瓷,這使得所有的讀操作可以在 "寫的同時" 直接對數(shù)據(jù)庫文件進行操作,得到更好的并發(fā)性能斑匪。
這樣實現(xiàn)了讀和寫的并發(fā)呐籽。
同時sqlite支持三種線程模式:
單線程模型 這種模型下,所有互斥鎖都被禁用蚀瘸,同一時間只能由一個線程訪問狡蝶。
多線程模型 這種模型下,一個連接在同一時間內(nèi)只有一個線程使用就是安全的苍姜。
串行模型 開啟所有鎖,可以隨意訪問悬包。
優(yōu)化嘗試
前提條件:首先串行模式pass掉衙猪,只考慮單線程和多線程模式。
1.建表:
create table if not exists ChatModelDB (localId INTEGER primary key AUTOINCREMENT,
CommonId varchar(256),
text1 varchar(256),
text2 varchar(256),
text3 varchar(256),
text4 varchar(256),
text5 Long Long,
text6 INTEGER),
CommonId建立索引
2.已在數(shù)據(jù)庫里插入100萬條數(shù)據(jù)布近,每一列都是隨機字符垫释。
1.單線程模式 +無wal,也是FMDB模式
插入1萬條根據(jù)CommonId讀取20條
- (void)insert {
dispatch_async(queue, ^{
[db beginTransaction];
for (int j = 0;j<10000;j++) {
NString *insertSql = @"insert *****;
BOOL result = [db executeUpdate:insertSql];
}
[db commit];
}];
});
}
- (void)select{
dispatch_async(queue, ^{
NSArray *array = [db getRowsForQuery:selectSql];
});
}
混合讀寫
for (i = 0;i<100;i++) {
[self insert];
[self select];
}
寫消耗:1237ms,
讀消耗:10-20ms
混合讀寫:72185
2.單線程模式+wal
if (sqlite3_exec(_wdb, "PRAGMA journal_mode=WAL;", NULL, NULL, &error) != SQLITE_OK) {
NSLog(@"Failed to set WAL mode: %s", error);
}
寫消耗:480ms
讀消耗:10-15ms
混合讀寫:76205ms
會發(fā)現(xiàn)撑瞧,打開wal模式后棵譬,寫性能近乎提示了3倍,讀性能差別不大预伺。
3.多線程模式+多線程連接+wal
dispatch_async(globlequeue, ^{
sqlite3_open(db);
[self insert];
sqlite3_close(db);
});
dispatch_async(globlequeue, ^{
sqlite3_open(db);
[self select];
sqlite3_close(db);
});
混合讀寫:157301ms
會發(fā)現(xiàn)混合性能很差订咸,真正開啟所謂的多線程操作時,實際上是每個線程持有一個數(shù)據(jù)庫句柄酬诀,每做一個操作前脏嚷,要重新打開一個db句柄,執(zhí)行完后再close db句柄瞒御,這樣頻繁的打開數(shù)據(jù)是很消耗性能的父叙,一味地追求多線程并不會達到很好的性能,并且操作不好還容易crash。
在移動端這種微操作系統(tǒng)對數(shù)據(jù)庫的依賴遠沒有后端那般沉重趾唱,所以接下來考慮用單句柄+隊列的方式進行測試涌乳。
4.多線程模式+wal+異步單隊列
queue = dispatch_queue_create([[self queueName] UTF8String], DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
[self insert];
});
dispatch_asyncqueue, ^{
[self select];
});
讀寫性能和2相差不大
混合讀寫:68300ms
混合讀寫性能優(yōu)于2,在大型app場景下性能尤為明顯甜癞,本次測試是在無其他業(yè)務(wù)線程影響下夕晓,等于測試示例獨自cpu資源,和線上場景誤差交大带欢。
5.多線程模式+wal+同步單隊列
queue = dispatch_queue_create([[selfqueueName] UTF8String], DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
[self insert];
});
dispatch_syncqueue, ^{
[self select];
});
混合讀寫:66454ms
性能略遜于4运授,相差微乎其微,考慮切線程影響
考慮乔煞,多線程模式+wal模式下吁朦,讀和讀并發(fā),讀和寫并發(fā)渡贾,寫和寫不并發(fā)逗宜,
而讀速度遠大于寫速度,因為經(jīng)檢驗空骚,在500萬數(shù)據(jù)量下纺讲,讀速度在20ms以內(nèi)速度較快,所以讀并發(fā)暫不考慮囤屹,只給數(shù)據(jù)庫開啟兩條連接熬甚,讀連接和寫連接, 只做讀和寫之間的并發(fā)肋坚,為此乡括,設(shè)置兩條隊列:讀隊列和寫隊列,彼此異步執(zhí)行智厌,測試示例6诲泌。
6.多線程模式+wal+讀寫隊列分離
readqueue = dispatch_queue_create([[self queueName] UTF8String], DISPATCH_QUEUE_SERIAL);
writequeue = dispatch_queue_create([[self queueName] UTF8String], DISPATCH_QUEUE_SERIAL);
dispatch_async(writequeue, ^{
[self insert];
});
dispatch_async(readqueue, ^{
[self select];
});
混合讀寫:73112ms
這里會發(fā)現(xiàn),雙隊列的讀寫速度居然略慢于單隊列讀寫铣鹏。
分析敷扫,因為數(shù)據(jù)是在demo上跑,demo上無其他任務(wù)在執(zhí)行诚卸,等于cpu滿負荷只為測試調(diào)用葵第,而理論上頻繁的線程切換也需要資源開銷,單隊列模式下的線程切換減少合溺,性能略優(yōu)于雙隊列羹幸。
但在實際的項目中,app場景復雜辫愉,cpu無時不在執(zhí)行做一些復雜的功能栅受,所以在demo上播放一個長視頻進行測試,并且同時異步執(zhí)行一個while循環(huán)空轉(zhuǎn),測試cpu調(diào)度頻繁被其他業(yè)務(wù)影響時數(shù)據(jù)庫操作的性能屏镊。
經(jīng)測試混合讀寫:1.多線程模式+wal+異步單隊列 7800ms左右
2.多線程模式+wal+同步單隊列 73960ms左右
3. 多線程模式+wal+讀寫隊列分離 60471ms左右
在cpu調(diào)度頻繁的場景下依疼,多線程的優(yōu)勢體現(xiàn)了出來,在wal模式下而芥,寫性能提升幾倍律罢,讀和寫并發(fā),讀和讀并發(fā)棍丐,寫和寫串行误辑,達到效率最大化。
所以決定選用模式多線程模式+wal+讀寫隊列分離模式歌逢。