背景
在Soul的IM上線后,初始時(shí)用戶本地消息量不大的情況下,數(shù)據(jù)庫讀寫良好,不容易發(fā)現(xiàn)問題。
但隨著產(chǎn)品用的時(shí)間越來越近,有些用戶本地聊天數(shù)據(jù)達(dá)到500萬條以上時(shí),數(shù)據(jù)庫性能瓶頸逐漸體現(xiàn)出來。
1.讀寫數(shù)據(jù)較慢。
2.讀寫在同一線程里,當(dāng)大量數(shù)據(jù)寫入時(shí),遲遲讀不出數(shù)據(jù),體驗(yàn)較差
所以數(shù)據(jù)庫這塊急需優(yōu)化
優(yōu)化之前的方案
之前im用的是著名三方數(shù)據(jù)庫FMDB,它只是簡(jiǎn)單的對(duì)sqlite進(jìn)行了封裝,線程安全方案是把所有的數(shù)據(jù)庫操作放到了一個(gè)串行隊(duì)列里去同步執(zhí)行。
因?yàn)閟qlite是不能多個(gè)操作同時(shí)訪問一個(gè)連接,不然會(huì)crash。
這樣做的好處是確保了數(shù)據(jù)庫連接數(shù)據(jù)讀寫安全,缺點(diǎn)是無法進(jìn)行線程并發(fā)操作。
對(duì)于IM這種對(duì)海量數(shù)據(jù)存儲(chǔ)性能要求較高的項(xiàng)目,F(xiàn)MDB不能滿足需求。
優(yōu)化思考
我們所要做的是支持多線程讀寫的數(shù)據(jù)庫。
這里介紹下WAL模式:SQLite引入了 WAL 模式,即 Write-Ahead Log。在這種模式下,所有的修改會(huì)寫入一個(gè)單獨(dú)的 WAL 文件內(nèi)。這種模式下,寫操作甚至可以不去操作數(shù)據(jù)庫,這使得所有的讀操作可以在 "寫的同時(shí)" 直接對(duì)數(shù)據(jù)庫文件進(jìn)行操作,得到更好的并發(fā)性能。
這樣實(shí)現(xiàn)了讀和寫的并發(fā)。
同時(shí)sqlite支持三種線程模式:
單線程模型 這種模型下,所有互斥鎖都被禁用,同一時(shí)間只能由一個(gè)線程訪問。
多線程模型 這種模型下,一個(gè)連接在同一時(shí)間內(nèi)只有一個(gè)線程使用就是安全的。
串行模型 開啟所有鎖,可以隨意訪問。
優(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ù),每一列都是隨機(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
會(huì)發(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
會(huì)發(fā)現(xiàn)混合性能很差,真正開啟所謂的多線程操作時(shí),實(shí)際上是每個(gè)線程持有一個(gè)數(shù)據(jù)庫句柄,每做一個(gè)操作前,要重新打開一個(gè)db句柄,執(zhí)行完后再close db句柄,這樣頻繁的打開數(shù)據(jù)是很消耗性能的,一味地追求多線程并不會(huì)達(dá)到很好的性能,并且操作不好還容易crash。
在移動(dòng)端這種微操作系統(tǒng)對(duì)數(shù)據(jù)庫的依賴遠(yuǎn)沒有后端那般沉重,所以接下來考慮用單句柄+隊(duì)列的方式進(jìn)行測(cè)試。
4.多線程模式+wal+異步單隊(duì)列
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場(chǎng)景下性能尤為明顯,本次測(cè)試是在無其他業(yè)務(wù)線程影響下,等于測(cè)試示例獨(dú)自cpu資源,和線上場(chǎng)景誤差交大。
5.多線程模式+wal+同步單隊(duì)列
queue = dispatch_queue_create([[selfqueueName] UTF8String], DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
[self insert];
});
dispatch_syncqueue, ^{
[self select];
});
混合讀寫:66454ms
性能略遜于4,相差微乎其微,考慮切線程影響
考慮,多線程模式+wal模式下,讀和讀并發(fā),讀和寫并發(fā),寫和寫不并發(fā),
而讀速度遠(yuǎn)大于寫速度,因?yàn)榻?jīng)檢驗(yàn),在500萬數(shù)據(jù)量下,讀速度在20ms以內(nèi)速度較快,所以讀并發(fā)暫不考慮,只給數(shù)據(jù)庫開啟兩條連接,讀連接和寫連接, 只做讀和寫之間的并發(fā),為此,設(shè)置兩條隊(duì)列:讀隊(duì)列和寫隊(duì)列,彼此異步執(zhí)行,測(cè)試示例6。
6.多線程模式+wal+讀寫隊(duì)列分離
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
這里會(huì)發(fā)現(xiàn),雙隊(duì)列的讀寫速度居然略慢于單隊(duì)列讀寫。
分析,因?yàn)閿?shù)據(jù)是在demo上跑,demo上無其他任務(wù)在執(zhí)行,等于cpu滿負(fù)荷只為測(cè)試調(diào)用,而理論上頻繁的線程切換也需要資源開銷,單隊(duì)列模式下的線程切換減少,性能略優(yōu)于雙隊(duì)列。
但在實(shí)際的項(xiàng)目中,app場(chǎng)景復(fù)雜,cpu無時(shí)不在執(zhí)行做一些復(fù)雜的功能,所以在demo上播放一個(gè)長視頻進(jìn)行測(cè)試,并且同時(shí)異步執(zhí)行一個(gè)while循環(huán)空轉(zhuǎn),測(cè)試cpu調(diào)度頻繁被其他業(yè)務(wù)影響時(shí)數(shù)據(jù)庫操作的性能。
經(jīng)測(cè)試混合讀寫:1.多線程模式+wal+異步單隊(duì)列 7800ms左右
2.多線程模式+wal+同步單隊(duì)列 73960ms左右
3. 多線程模式+wal+讀寫隊(duì)列分離 60471ms左右
在cpu調(diào)度頻繁的場(chǎng)景下,多線程的優(yōu)勢(shì)體現(xiàn)了出來,在wal模式下,寫性能提升幾倍,讀和寫并發(fā),讀和讀并發(fā),寫和寫串行,達(dá)到效率最大化。
所以決定選用模式多線程模式+wal+讀寫隊(duì)列分離模式。