精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

SQLite在多線程環(huán)境下的應(yīng)用

移動開發(fā) iOS
這幾天研究了一下SQLite這個嵌入式數(shù)據(jù)庫在多線程環(huán)境下的應(yīng)用,感覺里面的學(xué)問還挺多,于是就在此分享一下。

先說下初衷吧,實際上我經(jīng)常看到有人抱怨SQLite不支持多線程。而在iOS開發(fā)時,為了不阻塞主線程,數(shù)據(jù)庫訪問必須移到子線程中。為了解決這個矛盾,很有必要對此一探究竟。

關(guān)于這個問題,最權(quán)威的解答當(dāng)然是SQLite官網(wǎng)上的“Is SQLite threadsafe?”這個問答。

簡單來說,從3.3.1版本開始,它就是線程安全的了。而iOS的SQLite版本沒有低于這個版本的:

3.4.0 - iPhone OS 2.2.1

3.6.12 - iPhone OS 3.0 / 3.1

3.6.22 - iPhone OS 4.0

3.6.23.2 - iOS 4.1 / 4.2

3.7.2 - iOS 4.3

3.7.7 - iOS 5.0

當(dāng)然,你也可以自己編譯最新版本。只是我發(fā)現(xiàn)自己編譯出來的3.7.8居然比iOS 4.3.3內(nèi)置的3.7.2慢了一半,不知道蘋果做了什么優(yōu)化。發(fā)現(xiàn)是我編譯成了debug版本,改成release后性能比內(nèi)置版本高5%左右,不過構(gòu)建出來的app會大420k左右。

不過這個線程安全仍然是有限制的,在這篇《Is SQLite thread-safe?》里有詳細(xì)的解釋。

另一篇重要的文檔就是《SQLite And Multiple Threads》。它指出SQLite支持3種線程模式:

單線程:禁用所有的mutex鎖,并發(fā)使用時會出錯。當(dāng)SQLite編譯時加了SQLITE_THREADSAFE=0參數(shù),或者在初始化SQLite前調(diào)用sqlite3_config(SQLITE_CONFIG_SINGLETHREAD)時啟用。

多線程:只要一個數(shù)據(jù)庫連接不被多個線程同時使用就是安全的。源碼中是啟用bCoreMutex,禁用bFullMutex。實際上就是禁用數(shù)據(jù)庫連接和prepared statement(準(zhǔn)備好的語句)上的鎖,因此不能在多個線程中并發(fā)使用同一個數(shù)據(jù)庫連接或prepared statement。當(dāng)SQLite編譯時加了SQLITE_THREADSAFE=2參數(shù)時默認(rèn)啟用。若SQLITE_THREADSAFE不為0,可以在初始化SQLite前,調(diào)用sqlite3_config(SQLITE_CONFIG_MULTITHREAD)啟用;或者在創(chuàng)建數(shù)據(jù)庫連接時,設(shè)置SQLITE_OPEN_NOMUTEX flag。

串行:啟用所有的鎖,包括bCoreMutex和bFullMutex。因為數(shù)據(jù)庫連接和prepared statement都已加鎖,所以多線程使用這些對象時沒法并發(fā),也就變成串行了。當(dāng)SQLite編譯時加了SQLITE_THREADSAFE=1參數(shù)時默認(rèn)啟用。若SQLITE_THREADSAFE不為0,可以在初始化SQLite前,調(diào)用sqlite3_config(SQLITE_CONFIG_SERIALIZED)啟用;或者在創(chuàng)建數(shù)據(jù)庫連接時,設(shè)置SQLITE_OPEN_FULLMUTEX flag。

而這里所說的初始化是指調(diào)用sqlite3_initialize()函數(shù),這個函數(shù)在調(diào)用sqlite3_open()時會自動調(diào)用,且只有第一次調(diào)用是有效的。

另一個要說明的是prepared statement,它是由數(shù)據(jù)庫連接(的pager)來管理的,使用它也可看成使用這個數(shù)據(jù)庫連接。因此在多線程模式下,并發(fā)對同一個數(shù)據(jù)庫連接調(diào)用sqlite3_prepare_v2()來創(chuàng)建prepared statement,或者對同一個數(shù)據(jù)庫連接的任何prepared statement并發(fā)調(diào)用sqlite3_bind_*()和sqlite3_step()等函數(shù)都會出錯(在iOS上,該線程會出現(xiàn)EXC_BAD_ACCESS而中止)。這種錯誤無關(guān)讀寫,就是只讀也會出錯。文檔中給出的安全使用規(guī)則是:沒有事務(wù)正在等待執(zhí)行,所有prepared statement都被finalized。

順帶一提,調(diào)用sqlite3_threadsafe()可以獲得編譯期的SQLITE_THREADSAFE參數(shù)。標(biāo)準(zhǔn)發(fā)行版是1,也就是串行模式;而iOS上是2,也就是多線程模式;Python的sqlite3模塊也默認(rèn)使用串行模式,可以用sqlite3.threadsafety來配置。但是默認(rèn)情況下,一個線程只能使用當(dāng)前線程打開的數(shù)據(jù)庫連接,除非在連接時設(shè)置了check_same_thread=False參數(shù)。

現(xiàn)在3種模式都有所了解了,清楚SQLite并不是對多線程無能為力后,接下來就了解下事務(wù)吧。

數(shù)據(jù)庫只有在事務(wù)中才能被更改。所有更改數(shù)據(jù)庫的命令(除SELECT以外的所有SQL命令)都會自動開啟一個新事務(wù),并且當(dāng)最后一個查詢完成時自動提交。

而BEGIN命令可以手動開始事務(wù),并關(guān)閉自動提交。當(dāng)下一條COMMIT命令執(zhí)行時,自動提交再次打開,事務(wù)中所做的更改也被寫入數(shù)據(jù)庫。當(dāng)COMMIT失敗時,自動提交仍然關(guān)閉,以便讓用戶嘗試再次提交。若執(zhí)行的是ROLLBACK命令,則也打開自動提交,但不保存事務(wù)中的更改。關(guān)閉數(shù)據(jù)庫或遇到錯誤時,也會自動回滾事務(wù)。

經(jīng)常有人抱怨SQLite的插入太慢,實際上它可以做到每秒插入幾萬次,但是每秒只能提交幾十次事務(wù)。因此在插入大批數(shù)據(jù)時,可以通過禁用自動提交來提速。

事務(wù)在改寫數(shù)據(jù)庫文件時,會先生成一個rollback journal(回滾日志),記錄初始狀態(tài)(其實就是備份),所有改動都是在數(shù)據(jù)庫文件上進(jìn)行的。當(dāng)事務(wù)需要回滾時,可以將備份文件的內(nèi)容還原到數(shù)據(jù)庫文件;提交成功時,默認(rèn)的delete模式下會直接刪除這個日志。這個日志也可以幫助解決事務(wù)執(zhí)行過程中斷電,導(dǎo)致數(shù)據(jù)庫文件損壞的問題。但如果操作系統(tǒng)或文件系統(tǒng)有bug,或是磁盤損壞,則仍有可能無法恢復(fù)。

而從3.7.0版本(對應(yīng)iOS 4.3)開始,SQLite還提供了Write-Ahead Logging模式。與delete模式相比,WAL模式在大部分情況下更快,并發(fā)性更好,讀和寫之間互不阻塞;而其缺點對于iPhone這種嵌入式設(shè)備來說可以忽略,只需注意不要以只讀方式打開WAL模式的數(shù)據(jù)庫即可。

使用WAL模式時,改寫操是附加(append)到WAL文件,而不改動數(shù)據(jù)庫文件,因此數(shù)據(jù)庫文件可以被同時讀取。當(dāng)執(zhí)行checkpoint操作時,WAL文件的內(nèi)容會被寫回數(shù)據(jù)庫文件。當(dāng)WAL文件達(dá)到SQLITE_DEFAULT_WAL_AUTOCHECKPOINT(默認(rèn)值是1000)頁(默認(rèn)大小是1KB)時,會自動使用當(dāng)前COMMIT的線程來執(zhí)行checkpoint操作。也可以關(guān)閉自動checkpoint,改為手動定期checkpoint。

為了避免讀取的數(shù)據(jù)不一致,查詢時也需要讀取WAL文件,并記錄一個結(jié)尾標(biāo)記(end mark)。這樣的代價就是讀取會變得稍慢,但是寫入會變快很多。要提高查詢性能的話,可以減小WAL文件的大小,但寫入性能也會降低。

需要注意的是,低版本的SQLite不能讀取高版本的SQLite生成的WAL文件,但是數(shù)據(jù)庫文件是通用的。這種情況在用戶進(jìn)行iOS降級時可能會出現(xiàn),可以把模式改成delete,再改回WAL來修復(fù)。

要對一個數(shù)據(jù)庫連接啟用WAL模式,需要執(zhí)行“PRAGMA journal_mode=WAL;”這條命令,它的默認(rèn)值是“journal_mode=DELETE”。執(zhí)行后會返回新的journal_mode字符串值,即成功時為"wal",失敗時為之前的模式(例如"delete")。一旦啟用WAL模式后,數(shù)據(jù)庫會保持這個模式,這樣下次打開數(shù)據(jù)庫時仍然是WAL模式。

要停止自動checkpoint,可以使用wal_autocheckpoint指令或sqlite3_wal_checkpoint()函數(shù)。手動執(zhí)行checkpoint可以使用wal_checkpoint指令或sqlite3_wal_checkpoint()函數(shù)。

還有一個很重要的知識點需要強(qiáng)調(diào):事務(wù)是和數(shù)據(jù)庫連接相關(guān)的,每個數(shù)據(jù)庫連接(使用pager來)維護(hù)自己的事務(wù),且同時只能有一個事務(wù)(但是可以用SAVEPOINT來實現(xiàn)內(nèi)嵌事務(wù))。

也就是說,事務(wù)與線程無關(guān),一個線程里可以同時用多個數(shù)據(jù)庫連接來完成多個事務(wù),而多個線程也可以同時(非并發(fā))使用一個數(shù)據(jù)庫連接來共同完成一個事務(wù)。

下面用Python來演示一下:

  1. # -*- coding: utf-8 -*- 
  2.  
  3. import sqlite3 
  4.  
  5. import threading 
  6.  
  7. def f(): 
  8.  
  9. con.rollback() 
  10.  
  11. con = sqlite3.connect('test.db', check_same_thread=False# 允許在其他線程中使用這個連接 
  12.  
  13. cu = con.cursor() 
  14.  
  15. cu.execute('CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY)'
  16.  
  17. print cu.execute('SELECT count(*) FROM test').fetchone()[0# 0 
  18.  
  19. cu.execute('INSERT INTO test VALUES (NULL)'
  20.  
  21. print cu.execute('SELECT count(*) FROM test').fetchone()[0# 1 
  22.  
  23. thread = threading.Thread(target=f) 
  24.  
  25. thread.start() 
  26.  
  27. thread.join() 
  28.  
  29. print cu.execute('SELECT count(*) FROM test').fetchone()[0# 0 
  30.  
  31. cu.close() 
  32.  
  33. con.close() 

在這個例子中,雖然是在子線程中執(zhí)行rollback,但由于和主線程用的是同一個數(shù)據(jù)庫連接,所以主線程所做的更改也被回滾了。

而如果是用不同的數(shù)據(jù)庫連接,每個連接都不能讀取其他連接中未提交的數(shù)據(jù),除非使用read-uncommitted模式。

而要實現(xiàn)事務(wù),就不得不用到鎖。

一個SQLite數(shù)據(jù)庫文件有5種鎖的狀態(tài):

UNLOCKED:表示數(shù)據(jù)庫此時并未被讀寫。

SHARED:表示數(shù)據(jù)庫可以被讀取。SHARED鎖可以同時被多個線程擁有。一旦某個線程持有SHARED鎖,就沒有任何線程可以進(jìn)行寫操作。

RESERVED:表示準(zhǔn)備寫入數(shù)據(jù)庫。RESERVED鎖最多只能被一個線程擁有,此后它可以進(jìn)入PENDING狀態(tài)。

PENDING:表示即將寫入數(shù)據(jù)庫,正在等待其他讀線程釋放SHARED鎖。一旦某個線程持有PENDING鎖,其他線程就不能獲取SHARED鎖。這樣一來,只要等所有讀線程完成,釋放SHARED鎖后,它就可以進(jìn)入EXCLUSIVE狀態(tài)了。

EXCLUSIVE:表示它可以寫入數(shù)據(jù)庫了。進(jìn)入這個狀態(tài)后,其他任何線程都不能訪問數(shù)據(jù)庫文件。因此為了并發(fā)性,它的持有時間越短越好。

一個線程只有在擁有低級別的鎖的時候,才能獲取更高一級的鎖。SQLite就是靠這5種類型的鎖,巧妙地實現(xiàn)了讀寫線程的互斥。同時也可看出,寫操作必須進(jìn)入EXCLUSIVE狀態(tài),此時并發(fā)數(shù)被降到1,這也是SQLite被認(rèn)為并發(fā)插入性能不好的原因。

另外,read-uncommitted和WAL模式會影響這個鎖的機(jī)制。在這2種模式下,讀線程不會被寫線程阻塞,即使寫線程持有PENDING或EXCLUSIVE鎖。

提到鎖就不得不說到死鎖的問題,而SQLite也可能出現(xiàn)死鎖。

下面舉個例子:

連接1:BEGIN (UNLOCKED)

連接1:SELECT ... (SHARED)

連接1:INSERT ... (RESERVED)

連接2:BEGIN (UNLOCKED)

連接2:SELECT ... (SHARED)

連接1:COMMIT (PENDING,嘗試獲取EXCLUSIVE鎖,但還有SHARED鎖未釋放,返回SQLITE_BUSY)

連接2:INSERT ... (嘗試獲取RESERVED鎖,但已有PENDING鎖未釋放,返回SQLITE_BUSY)

現(xiàn)在2個連接都在等待對方釋放鎖,于是就死鎖了。當(dāng)然,實際情況并沒那么糟糕,任何一方選擇不繼續(xù)等待,回滾事務(wù)就行了。

不過要更好地解決這個問題,就必須更深入地了解事務(wù)了。

實際上BEGIN語句可以有3種起始狀態(tài):

DEFERRED:默認(rèn)值,開始事務(wù)時不獲取任何鎖。進(jìn)行第一次讀操作時獲取SHARED鎖,進(jìn)行第一次寫操作時獲取RESERVED鎖。

IMMEDIATE:開始事務(wù)時獲取RESERVED鎖。

EXCLUSIVE:開始事務(wù)時獲取EXCLUSIVE鎖。

現(xiàn)在考慮2個事務(wù)在開始時都使用IMMEDIATE方式:

連接1:BEGIN IMMEDIATE (RESERVED)

連接1:SELECT ... (RESERVED)

連接1:INSERT ... (RESERVED)

連接2:BEGIN IMMEDIATE (嘗試獲取RESERVED鎖,但已有RESERVED鎖未釋放,因此事務(wù)開始失敗,返回SQLITE_BUSY,等待用戶重試)

連接1:COMMIT (EXCLUSIVE,寫入完成后釋放)

連接2:BEGIN IMMEDIATE (RESERVED)

連接2:SELECT ... (RESERVED)

連接2:INSERT ... (RESERVED)

連接2:COMMIT (EXCLUSIVE,寫入完成后釋放)

這樣死鎖就被避免了。

而EXCLUSIVE方式則更為嚴(yán)苛,即使其他連接以DEFERRED方式開啟事務(wù)也不會死鎖:

連接1:BEGIN EXCLUSIVE (EXCLUSIVE)

連接1:SELECT ... (EXCLUSIVE)

連接1:INSERT ... (EXCLUSIVE)

連接2:BEGIN (UNLOCKED)

連接2:SELECT ... (嘗試獲取SHARED鎖,但已有EXCLUSIVE鎖未釋放,返回SQLITE_BUSY,等待用戶重試)

連接1:COMMIT (EXCLUSIVE,寫入完成后釋放)

連接2:SELECT ... (SHARED)

連接2:INSERT ... (RESERVED)

連接2:COMMIT (EXCLUSIVE,寫入完成后釋放)

不過在并非很高的情況下,直接獲取EXCLUSIVE鎖的難度比較大;而且為了避免EXCLUSIVE狀態(tài)長期阻塞其他請求,最好的方式還是讓所有寫事務(wù)都以IMMEDIATE方式開始。

順帶一提,要實現(xiàn)重試的話,可以使用sqlite3_busy_timeout()或sqlite3_busy_handler()函數(shù)。

由此可見,要想保證線程安全的話,可以有這4種方式:

SQLite使用單線程模式,用一個專門的線程訪問數(shù)據(jù)庫。

SQLite使用單線程模式,用一個線程隊列來訪問數(shù)據(jù)庫,隊列一次只允許一個線程執(zhí)行,隊列里的線程共用一個數(shù)據(jù)庫連接。

SQLite使用多線程模式,每個線程創(chuàng)建自己的數(shù)據(jù)庫連接。

SQLite使用串行模式,所有線程共用全局的數(shù)據(jù)庫連接。

接下來就一一測試這幾種方式在iPhone 4(iOS 4.3.3,SQLite 3.7.2)上的性能表現(xiàn)。

第一種方式太過麻煩,需要線程間通信,這里我就忽略了。

第二種方式可以用dispatch_queue_create()來創(chuàng)建一個serial queue,或者用一個maxConcurrentOperationCount為1的NSOperationQueue來實現(xiàn)。

這種方式的缺點就是事務(wù)必須在一個block或operation里完成,否則會亂序;而耗時較長的事務(wù)會阻塞隊列。另外,沒法利用多核CPU的優(yōu)勢。

先初始化數(shù)據(jù)庫:

  1. #import 
  2.  
  3. static char dbPath[200]; 
  4.  
  5. static sqlite3 *database
  6.  
  7. static sqlite3 *openDb() { 
  8.  
  9. if (sqlite3_open(dbPath, &database) != SQLITE_OK) { 
  10.  
  11. sqlite3_close(database); 
  12.  
  13. NSLog(@"Failed to open database: %s", sqlite3_errmsg(database)); 
  14.  
  15.  
  16. return database
  17.  
  18.  
  19. - (void)viewDidLoad { 
  20.  
  21. [super viewDidLoad]; 
  22.  
  23. sqlite3_config(SQLITE_CONFIG_SINGLETHREAD); 
  24.  
  25. NSLog(@"%d", sqlite3_threadsafe()); 
  26.  
  27. NSLog(@"%s", sqlite3_libversion()); 
  28.  
  29. NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 
  30.  
  31. NSString *documentsDirectory = [paths objectAtIndex:0]; 
  32.  
  33. strcpy(dbPath, [[documentsDirectory stringByAppendingPathComponent:@"data.sqlite3"] UTF8String]); 
  34.  
  35. database = openDb(); 
  36.  
  37. char *errorMsg; 
  38.  
  39. if (sqlite3_exec(database"CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY AUTOINCREMENT, value INTEGER);"NULLNULL, &errorMsg) != SQLITE_OK) { 
  40.  
  41. NSLog(@"Failed to create table: %s", errorMsg); 
  42.  
  43.  

再插入1000條測試數(shù)據(jù):

  1. static void insertData() { 
  2.  
  3. char *errorMsg; 
  4.  
  5. if (sqlite3_exec(database"BEGIN TRANSACTION"NULLNULL, &errorMsg) != SQLITE_OK) { 
  6.  
  7. NSLog(@"Failed to begin transaction: %s", errorMsg); 
  8.  
  9.  
  10. static const char *insert = "INSERT INTO test VALUES (NULL, ?);"
  11.  
  12. sqlite3_stmt *stmt; 
  13.  
  14. if (sqlite3_prepare_v2(databaseinsert, -1, &stmt, NULL) == SQLITE_OK) { 
  15.  
  16. for (int i = 0; i < 1000; ++i) { 
  17.  
  18. sqlite3_bind_int(stmt, 1, arc4random()); 
  19.  
  20. if (sqlite3_step(stmt) != SQLITE_DONE) { 
  21.  
  22. --i; 
  23.  
  24. NSLog(@"Error inserting table: %s", sqlite3_errmsg(database)); 
  25.  
  26.  
  27. sqlite3_reset(stmt); 
  28.  
  29.  
  30. sqlite3_finalize(stmt); 
  31.  
  32.  
  33. if (sqlite3_exec(database"COMMIT TRANSACTION"NULLNULL, &errorMsg) != SQLITE_OK) { 
  34.  
  35. NSLog(@"Failed to commit transaction: %s", errorMsg); 
  36.  
  37.  
  38. static const char *query = "SELECT count(*) FROM test;"
  39.  
  40. if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) { 
  41.  
  42. if (sqlite3_step(stmt) == SQLITE_ROW) { 
  43.  
  44. NSLog(@"Table size: %d", sqlite3_column_int(stmt, 0)); 
  45.  
  46. else { 
  47.  
  48. NSLog(@"Failed to read table: %s", sqlite3_errmsg(database)); 
  49.  
  50.  
  51. sqlite3_finalize(stmt); 
  52.  
  53.  

然后創(chuàng)建一個串行隊列:

  1. static dispatch_queue_t queue; 
  2.  
  3. - (void)viewDidLoad { 
  4.  
  5. // ... 
  6.  
  7. queue = dispatch_queue_create("net.keakon.db"NULL); 
  8.  

再設(shè)置一個計數(shù)器,每秒執(zhí)行一次:

  1. static int lastReadCount = 0; 
  2.  
  3. static int readCount = 0; 
  4.  
  5. static int lastWriteCount = 0; 
  6.  
  7. static int writeCount = 0; 
  8.  
  9. - (void)count { 
  10.  
  11. int lastRead = lastReadCount; 
  12.  
  13. int lastWrite = lastWriteCount; 
  14.  
  15. lastReadCount = readCount; 
  16.  
  17. lastWriteCount = writeCount; 
  18.  
  19. NSLog(@"%d, %d", lastReadCount - lastRead, lastWriteCount - lastWrite); 
  20.  
  21.  
  22. - (void)viewDidLoad { 
  23.  
  24. // ... 
  25.  
  26. [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(count) userInfo:nil repeats:YES]; 
  27.  

這樣就可以開始測試select和update了:

  1. static void readData() { 
  2.  
  3. static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;"
  4.  
  5. void (^ __block readBlock)() = Block_copy(^{ 
  6.  
  7. sqlite3_stmt *stmt; 
  8.  
  9. if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) { 
  10.  
  11. sqlite3_bind_int(stmt, 1, arc4random()); 
  12.  
  13. int returnCode = sqlite3_step(stmt); 
  14.  
  15. if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) { 
  16.  
  17. ++readCount; 
  18.  
  19.  
  20. sqlite3_finalize(stmt); 
  21.  
  22. else { 
  23.  
  24. NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); 
  25.  
  26.  
  27. dispatch_async(queue, readBlock); 
  28.  
  29. }); 
  30.  
  31. dispatch_async(queue, readBlock); 
  32.  
  33.  
  34. static void writeData() { 
  35.  
  36. static const char *update = "UPDATE test SET value = ? WHERE id = ?;"
  37.  
  38. void (^ __block writeBlock)() = Block_copy(^{ 
  39.  
  40. sqlite3_stmt *stmt; 
  41.  
  42. if (sqlite3_prepare_v2(databaseupdate, -1, &stmt, NULL) == SQLITE_OK) { 
  43.  
  44. sqlite3_bind_int(stmt, 1, arc4random()); 
  45.  
  46. sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1); 
  47.  
  48. if (sqlite3_step(stmt) == SQLITE_DONE) { 
  49.  
  50. ++writeCount; 
  51.  
  52.  
  53. sqlite3_finalize(stmt); 
  54.  
  55. else { 
  56.  
  57. NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); 
  58.  
  59.  
  60. dispatch_async(queue, writeBlock); 
  61.  
  62. }); 
  63.  
  64. dispatch_async(queue, writeBlock); 
  65.  

這里是用dispatch_async()來異步地遞歸調(diào)用block。

因為block是在棧里生成的,異步執(zhí)行時已經(jīng)被銷毀,所以需要copy到堆。因為需要一直執(zhí)行,所以我就沒release了。

此外,光copy的話還是無法正常執(zhí)行,但是把block本身的存儲類型設(shè)為__block后就正常了,原因我也不清楚。

測試結(jié)果為只讀時平均每秒165次,只寫時每秒68次,同時讀寫時每秒各47次。換成多線程或串行模式時,效率也差不多。

接著試試WAL模式:

  1. if (sqlite3_exec(database"PRAGMA journal_mode=WAL;"NULLNULL, &errorMsg) != SQLITE_OK) { 
  2.  
  3. NSLog(@"Failed to set WAL mode: %s", errorMsg); 
  4.  

sqlite3_wal_checkpoint(database, NULL); // 每次測試前先checkpoint,避免WAL文件過大而影響性能

測試結(jié)果為只讀時平均每秒166次,只寫時每秒244次,同時讀寫時每秒各97次。并發(fā)性增加了1倍有木有!更夸張的是寫入比讀取還快了。

在自編譯的3.7.8版中,同時讀寫為每秒各102次,加上SQLITE_THREADSAFE=0參數(shù)后為每秒各104次,性能稍有提升。

第三種方式需要打開和關(guān)閉數(shù)據(jù)庫連接,所以會額外消耗一些時間。此外還要維持各個連接間的互斥,事務(wù)也比較容易沖突,但能確保事務(wù)正確執(zhí)行。

首先需要移除全局的database變量,并修改openDb()函數(shù):

  1. static sqlite3 *openDb() { 
  2.  
  3. sqlite3 *database = NULL
  4.  
  5. if (sqlite3_open(dbPath, &database) != SQLITE_OK) { 
  6.  
  7. sqlite3_close(database); 
  8.  
  9. NSLog(@"Failed to open database: %s", sqlite3_errmsg(database)); 
  10.  
  11.  
  12. return database
  13.  

再配置成多線程模式:

  1. sqlite3_config(SQLITE_CONFIG_MULTITHREAD); 

隊列改成可以亂序執(zhí)行的:

  1. queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); 

然后是訪問數(shù)據(jù)庫:

  1. static void readData() { 
  2.  
  3. static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;"
  4.  
  5. dispatch_async(queue, ^{ 
  6.  
  7. sqlite3 *database = openDb(); 
  8.  
  9. sqlite3_stmt *stmt; 
  10.  
  11. if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) { 
  12.  
  13. while (YES) { 
  14.  
  15. sqlite3_bind_int(stmt, 1, arc4random()); 
  16.  
  17. int returnCode = sqlite3_step(stmt); 
  18.  
  19. if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) { 
  20.  
  21. ++readCount; 
  22.  
  23.  
  24. sqlite3_reset(stmt); 
  25.  
  26.  
  27. sqlite3_finalize(stmt); 
  28.  
  29. else { 
  30.  
  31. NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); 
  32.  
  33.  
  34. sqlite3_close(database); 
  35.  
  36. }); 
  37.  
  38.  
  39. static void writeData() { 
  40.  
  41. static const char *update = "UPDATE test SET value = ? WHERE id = ?;"
  42.  
  43. dispatch_async(queue, ^{ 
  44.  
  45. sqlite3 *database = openDb(); 
  46.  
  47. sqlite3_stmt *stmt; 
  48.  
  49. if (sqlite3_prepare_v2(databaseupdate, -1, &stmt, nil) == SQLITE_OK) { 
  50.  
  51. while (YES) { 
  52.  
  53. sqlite3_bind_int(stmt, 1, arc4random()); 
  54.  
  55. sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1); 
  56.  
  57. if (sqlite3_step(stmt) == SQLITE_DONE) { 
  58.  
  59. ++writeCount; 
  60.  
  61.  
  62. sqlite3_reset(stmt); 
  63.  
  64.  
  65. sqlite3_finalize(stmt); 
  66.  
  67. else { 
  68.  
  69. NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); 
  70.  
  71.  
  72. sqlite3_close(database); 
  73.  
  74. }); 
  75.  

這里就無需遞歸調(diào)用了,直接在子線程中循環(huán)即可。

測試結(jié)果為只讀時平均每秒164次,只寫時每秒68次,同時讀寫時分別為每秒14和30次(波動很大)。此外,這種方式因為最初啟動的幾個線程持續(xù)訪問數(shù)據(jù)庫,后加入的線程會滯后幾秒才啟動,且很難打開數(shù)據(jù)庫連接或創(chuàng)建prepare statement。調(diào)試時發(fā)現(xiàn)只會啟用2個線程,但是隨隊列中block數(shù)目的增加,讀性能增高,寫性能降低。讀寫各3個block時分別為每秒35和14次。

WAL模式下甚至連初始時啟動2個線程都會被lock,因此只能改成不斷重試:

  1. static void readData() { 
  2.  
  3. static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;"
  4.  
  5. dispatch_async(queue, ^{ 
  6.  
  7. sqlite3 *database = openDb(); 
  8.  
  9. sqlite3_stmt *stmt; 
  10.  
  11. while (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) != SQLITE_OK); 
  12.  
  13. while (YES) { 
  14.  
  15. sqlite3_bind_int(stmt, 1, arc4random()); 
  16.  
  17. int returnCode = sqlite3_step(stmt); 
  18.  
  19. if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) { 
  20.  
  21. ++readCount; 
  22.  
  23.  
  24. sqlite3_reset(stmt); 
  25.  
  26.  
  27. sqlite3_finalize(stmt); 
  28.  
  29. sqlite3_close(database); 
  30.  
  31. }); 
  32.  
  33.  
  34. static void writeData() { 
  35.  
  36. static const char *update = "UPDATE test SET value = ? WHERE id = ?;"
  37.  
  38. dispatch_async(queue, ^{ 
  39.  
  40. sqlite3 *database = openDb(); 
  41.  
  42. sqlite3_stmt *stmt; 
  43.  
  44. while (sqlite3_prepare_v2(databaseupdate, -1, &stmt, nil) != SQLITE_OK); 
  45.  
  46. while (YES) { 
  47.  
  48. sqlite3_bind_int(stmt, 1, arc4random()); 
  49.  
  50. sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1); 
  51.  
  52. if (sqlite3_step(stmt) == SQLITE_DONE) { 
  53.  
  54. ++writeCount; 
  55.  
  56.  
  57. sqlite3_reset(stmt); 
  58.  
  59.  
  60. sqlite3_finalize(stmt); 
  61.  
  62. sqlite3_close(database); 
  63.  
  64. }); 
  65.  

結(jié)果為只讀時平均每秒169次,只寫時每秒246次,同時讀寫時每秒分別為90和57次(波動較大)。并發(fā)效率有了顯著提升,但仍不及第二種方式。

第四種方式相當(dāng)于讓SQLite來維護(hù)隊列,只不過SQL的執(zhí)行是亂序的,因此無法保證事務(wù)性。

先恢復(fù)全局的database變量,然后配置成串行模式:

sqlite3_config(SQLITE_CONFIG_SERIALIZED);

再是訪問數(shù)據(jù)庫:

  1. static void readData() { 
  2.  
  3. static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;"
  4.  
  5. dispatch_async(queue, ^{ 
  6.  
  7. sqlite3_stmt *stmt; 
  8.  
  9. if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) { 
  10.  
  11. while (YES) { 
  12.  
  13. sqlite3_bind_int(stmt, 1, arc4random()); 
  14.  
  15. int returnCode = sqlite3_step(stmt); 
  16.  
  17. if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) { 
  18.  
  19. ++readCount; 
  20.  
  21.  
  22. sqlite3_reset(stmt); 
  23.  
  24.  
  25. sqlite3_finalize(stmt); 
  26.  
  27. else { 
  28.  
  29. NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); 
  30.  
  31.  
  32. }); 
  33.  
  34.  
  35. static void writeData() { 
  36.  
  37. static const char *update = "UPDATE test SET value = ? WHERE id = ?;"
  38.  
  39. dispatch_async(queue, ^{ 
  40.  
  41. sqlite3_stmt *stmt; 
  42.  
  43. if (sqlite3_prepare_v2(databaseupdate, -1, &stmt, NULL) == SQLITE_OK) { 
  44.  
  45. while (YES) { 
  46.  
  47. sqlite3_bind_int(stmt, 1, arc4random()); 
  48.  
  49. sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1); 
  50.  
  51. if (sqlite3_step(stmt) == SQLITE_DONE) { 
  52.  
  53. ++writeCount; 
  54.  
  55.  
  56. sqlite3_reset(stmt); 
  57.  
  58.  
  59. sqlite3_finalize(stmt); 
  60.  
  61. else { 
  62.  
  63. NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); 
  64.  
  65.  
  66. }); 
  67.  

測試結(jié)果為只讀時平均每秒164次,只寫時每秒68次,同時讀寫時每秒分別為57和43次。讀線程比寫線程的速率更高,而且新線程的加入不需要等待。

WAL模式下,只讀時平均每秒176次,只寫時每秒254次,同時讀寫時每秒分別為109和85次。

由此可見,要獲得最好的性能的話,WAL模式是必須啟用的,為此也有必要自己編譯SQLite 3.7.0以上的版本(除非不支持iOS 4.2及以下版本)。

而在測試過的后3種方式中:第3種是效率最低的,不建議使用;第4種讀取性能更高,適合無需使用事務(wù)的場合;第2種適用范圍更廣,效率也足夠優(yōu)秀,一般應(yīng)采用這種方式。

不過要注意的是,第2種方式在測試時的邏輯是完全與數(shù)據(jù)庫相關(guān)的。實際中可能要做計算或IO訪問等工作,在此期間其他線程都是被阻塞的,這樣就會大大降低效率了。因此只建議把訪問數(shù)據(jù)庫的邏輯放入隊列,其余工作在其他線程里完成。

剛才洗澡時我又想到一點,既然第2種方式不能并行,第4種方式不能保證事務(wù)性,那么能否將各自的優(yōu)點結(jié)合起來呢?

于是一個新的實現(xiàn)方案又浮出水面了:使用2個串行隊列,分別負(fù)責(zé)讀和寫,每個隊列各使用一個數(shù)據(jù)庫連接,線程模式可以采用多線程或串行模式。

代碼拿方式2稍做修改就行了,這里就不列出了。測試結(jié)果波動比較大(估計是checkpoint的影響),多線程模式下平均約為89和73次,串行模式下為91和86次。

但在iPad 2這種雙核的機(jī)型上,多線程明顯要比單隊列更具優(yōu)勢:方式2的成績是每秒各85次,方式3是94和124次(寫波動較大),方式4是95和72次,而新方案在多線程模式下是104和168次(寫波動很大,40~280之間),串行模式下為108和177次(寫波動很大)。

因此極端的優(yōu)化情況下,可以根據(jù)CPU核心數(shù)來創(chuàng)建隊列數(shù),然后把數(shù)據(jù)庫訪問線程隨機(jī)分配到某個隊列中。不過考慮到iOS設(shè)備這種嵌入式平臺并不需要密集地訪問數(shù)據(jù)庫,而且除數(shù)據(jù)庫線程以外還有其他事要做,如果沒遇到瓶頸的話,簡單的方案2其實也夠用了。

責(zé)任編輯:佚名 來源: keakon的Blog
相關(guān)推薦

2024-11-27 15:58:49

2023-10-19 08:30:58

線程源碼thread

2024-02-21 20:46:48

C++編程volatile

2011-08-10 10:18:22

iPhone多線程線程

2011-03-22 10:22:18

Windows環(huán)境Oracle

2020-11-18 09:48:09

Synchronize多線程Java

2021-09-12 07:57:06

多線程

2012-12-18 13:34:37

IBMdW

2009-05-26 17:20:51

多線程J2METhread

2025-02-27 08:15:28

2010-02-01 17:18:23

Python多線程環(huán)境

2024-10-24 16:38:30

測試線程

2010-03-10 19:25:04

python多線程

2011-04-01 16:56:57

NetBeansBlackBerry BlackBerry

2025-01-21 00:00:00

HashMap死循環(huán)數(shù)據(jù)損壞

2018-04-02 14:50:22

Java多線程應(yīng)用場景

2018-06-28 13:38:59

云計算云服務(wù)云安全

2009-08-31 14:45:15

C#.NET多線程應(yīng)用

2009-09-01 17:15:42

C#多線程應(yīng)用

2023-12-14 15:05:08

volatile代碼C++
點贊
收藏

51CTO技術(shù)棧公眾號

日本一区影院| 丰满肥臀噗嗤啊x99av| 亚洲精品白浆高清| 欧美视频中文一区二区三区在线观看| 日韩中文一区| 999久久久久久| 99日韩精品| 一区二区三区视频免费| 日韩av加勒比| 国产一二在线播放| 久久久久久免费毛片精品| 国产精品一香蕉国产线看观看| 国产又粗又长又黄的视频| 日韩精品一区二区三区中文在线| 亚洲va欧美va国产va天堂影院| 日日噜噜噜噜夜夜爽亚洲精品| 国产精品无码在线播放| 国产精品女主播一区二区三区| 在线观看国产成人av片| 俄罗斯女人裸体性做爰| 日韩在线影院| 一区二区三区欧美激情| 日本精品免费| 亚洲免费视频网| 美女视频黄免费的久久| 久久久久久国产精品三级玉女聊斋| 六十路息与子猛烈交尾| www.成人在线.com| 在线亚洲一区观看| 青草青青在线视频| 欧美13一16娇小xxxx| 91色porny在线视频| 亚洲一区二区三区视频播放| 天堂网免费视频| 欧美日韩免费观看一区=区三区| 亚洲图片在区色| 成熟妇人a片免费看网站| 日韩黄色三级| 欧美在线视频你懂得| 免费看又黄又无码的网站| 免费在线观看黄色| 国产午夜精品一区二区三区嫩草 | 国产亚洲精品美女| 中文字幕乱码在线| 成人动态视频| 日韩欧美中文一区| 6080国产精品| 日韩一区二区三免费高清在线观看| 欧美日韩午夜剧场| 亚洲精品久久久久久久蜜桃臀| 麻豆av免费在线观看| 国产丝袜美腿一区二区三区| 久久久免费看| 天天色综合久久| 岛国一区二区在线观看| 亚洲最大成人在线| 国产模特av私拍大尺度| 久草在线在线精品观看| 国产精品嫩草影院一区二区| 男人天堂视频网| 丝瓜av网站精品一区二区 | 日本三级黄色大片| 欧美激情日韩| 欧美激情精品久久久久久黑人| 在线看的片片片免费| 久久久久蜜桃| 久久天天躁狠狠躁夜夜躁| 婷婷社区五月天| 91亚洲国产高清| 久久九九热免费视频| 特一级黄色录像| 欧美~级网站不卡| 欧美高清视频免费观看| 久久在线视频精品| 亚洲日本欧美| 91福利视频网| 免费视频网站在线观看入口| 日韩av电影免费观看高清完整版| 国产精品吹潮在线观看| 亚洲一卡二卡在线| 精品影视av免费| 97在线电影| 性感美女福利视频| 国产精品三级电影| 成人在线观看www| 国产精品69xx| 色视频欧美一区二区三区| 日韩一级理论片| 亚洲网站免费| 精品国产凹凸成av人网站| 少妇特黄一区二区三区| 欧美色图激情小说| 美女福利视频一区| 国产又色又爽又黄的| 日韩在线卡一卡二| 91免费精品视频| 欧美熟妇交换久久久久久分类| 久久蜜臀精品av| 综合操久久久| 日韩欧美精品一区二区三区| 欧美视频三区在线播放| 欧美一区二区三区影院| 国产99精品| 欧美成人性色生活仑片| 天堂中文在线网| 视频一区二区欧美| 国产激情一区二区三区在线观看 | 最近2019中文字幕大全第二页| 免费在线黄色网| 新67194成人永久网站| 国产拍精品一二三| 天堂在线观看视频| 日韩理论片网站| 人妻有码中文字幕| 亚洲1区在线观看| 一个人看的www久久| 久久国产一级片| 免费观看日韩av| 韩国成人一区| 国产精品va在线观看视色 | 99热成人精品热久久66| 懂色av色香蕉一区二区蜜桃| 亚洲精品少妇网址| 久草视频在线资源站| 人妖欧美一区二区| 久久99精品久久久久久三级| 羞羞网站在线看| 欧美三级日韩三级国产三级| 亚洲 欧美 日韩在线| 91亚洲国产成人久久精品| 欧美有码在线观看| 六月丁香色婷婷| 亚洲精选一二三| 欧美午夜aaaaaa免费视频| 嫩草国产精品入口| 久久久久久成人精品| 99热这里只有精品在线观看| 中文字幕av不卡| 日韩精品一区二区三区不卡| 精品视频在线你懂得| 欧美成人剧情片在线观看| 国产精品51麻豆cm传媒 | 后进极品白嫩翘臀在线播放| 欧美精品 国产精品| 日韩不卡av在线| 久久一区激情| 欧美另类高清视频在线| 女人高潮被爽到呻吟在线观看| 欧美不卡激情三级在线观看| 国产免费一区二区三区四区| 蜜臀久久久99精品久久久久久| 鲁鲁狠狠狠7777一区二区| 理论不卡电影大全神| 亚洲福利视频二区| xxxxxx国产| bt欧美亚洲午夜电影天堂| 国产一线二线三线女| 99ri日韩精品视频| 久久全国免费视频| 熟妇人妻一区二区三区四区| 亚洲二区在线观看| 182在线视频| 亚洲一卡久久| 青娱乐一区二区| 91久久久久久白丝白浆欲热蜜臀| 国产小视频91| 97精品人妻一区二区三区香蕉| 中文字幕电影一区| 91丨九色丨蝌蚪| 欧美在线免费| 极品日韩久久| 日韩高清成人| 日韩小视频在线| 午夜精品在线播放| 精品久久久久久久久久久久久| 国产特黄级aaaaa片免| 日韩中文字幕不卡| 黄瓜视频免费观看在线观看www| 国模大尺度视频一区二区| 欧美福利视频在线| 天堂在线免费av| 欧美亚洲国产一区二区三区| 精品少妇一区二区三区密爱| 国产经典欧美精品| 国产免费黄视频| 欧美一级精品片在线看| 亚洲a在线观看| 国产无遮挡裸体视频在线观看| 亚洲一区二区福利| a级片在线播放| 欧美日韩免费在线观看| 国产精品久久久久久成人| 国产一区二三区| 一女被多男玩喷潮视频| 日韩欧美精品一区| 国产99午夜精品一区二区三区 | 蝌蚪视频在线播放| 欧美精品xxxxbbbb| 日韩人妻无码一区二区三区99 | 奇米888四色在线精品| 欧美一级黄色录像片| 亚洲毛片免费看| 91免费欧美精品| 欧美18av| 欧美大秀在线观看| 高清在线观看av| 亚洲а∨天堂久久精品喷水| 九九热最新视频| 亚洲一区二区高清| 一区二区三区在线播放视频| 99久久婷婷国产综合精品电影| 日本黄色的视频| 国产精品日韩久久久| 国产成人三级视频| 欧美日韩有码| 开心色怡人综合网站| 亚洲精品aⅴ| 国产欧美一区二区三区视频| 手机在线理论片| 欧美激情奇米色| 求av网址在线观看| 亚洲另类激情图| 国产综合视频在线| 欧美日韩国产bt| 亚洲国产成人无码av在线| 亚洲香蕉伊在人在线观| 五月天精品在线| 久久综合久久综合亚洲| 精品伦一区二区三区| 国产在线视频一区二区三区| 无码日韩人妻精品久久蜜桃| 夜夜嗨网站十八久久| 隔壁人妻偷人bd中字| 久久久久国产精品| 亚洲一区三区| 欧美亚洲国产激情| 欧洲久久久久久| 全国精品免费看| 国产日韩二区| 国产精品久久久久av蜜臀| 亚洲一区亚洲二区亚洲三区| 九七电影院97理论片久久tvb| 国产成人jvid在线播放| 久草在线资源福利站| 97高清免费视频| 91av久久| 亚州成人av在线| 激情国产在线| 97国产精品视频人人做人人爱| 手机在线免费看av| 欧美日韩成人在线观看| 亚洲第一图区| 欧美激情亚洲视频| 国产精品186在线观看在线播放| 欧美激情va永久在线播放| 欧美极品少妇videossex| 久久999免费视频| 黄视频在线免费看| 久久久久久久国产精品视频| 91九色国产在线播放| 国内精品久久影院| 国产美女高潮在线观看| 欧美影院久久久| 成人四虎影院| 成人激情在线观看| 综合激情网...| 国产欧美日韩一区| 夜夜春成人影院| 色噜噜狠狠一区二区三区| 欧美综合在线视频观看| 亚洲免费在线精品一区| 希岛爱理av一区二区三区| 成人黄色片免费| 亚洲国产午夜| 亚洲乱码中文字幕久久孕妇黑人| 日韩成人精品在线观看| 成人黄色一级大片| 成人午夜电影网站| 中文精品在线观看| 国产精品妹子av| 欧美日韩在线观看成人| 午夜不卡av在线| 一级久久久久久| 91精品欧美一区二区三区综合在 | 亚洲女人天堂av| 成年人在线看| 欧美福利视频在线观看| 欧美大片1688| 91美女片黄在线观| 私拍精品福利视频在线一区| 亚洲国产精品久久久久久女王| 欧美成人中文| 黄色片久久久久| 国产自产高清不卡| 国产精品久久久久久久无码| 国产精品视频免费看| 久久这里只有精品国产| 91福利精品视频| www.国产.com| 国产亚洲人成网站在线观看| 在线观看wwwxxxx| 国产97在线亚洲| 2023国产精华国产精品| 青青草原亚洲| 99成人在线| 欧美日韩久久婷婷| 久久影院午夜论| 免费人成年激情视频在线观看| 色综合天天综合色综合av | 欧美日韩123区| 99国产盗摄| 精品日韩毛片| 日韩欧美一区二| 国产一区欧美二区| 亚洲一区视频在线播放| 亚洲国产视频一区二区| 一级特黄色大片| 日韩精品视频在线| 污的网站在线观看| 国产日韩欧美电影在线观看| 神马久久影院| 日韩xxxx视频| 国产一区二区三区观看| 久久久视频6r| 精品欧美一区二区三区| 午夜美女福利视频| www欧美日韩| 国模一区二区| 蜜桃999成人看片在线观看| 国产精品hd| 久久久精品视频国产| 国产精品嫩草影院com| 亚洲欧美一区二区三区在线观看 | 国产欧美在线观看| 国产成人一区| 久久国产成人精品国产成人亚洲| 高清不卡一区二区| 四虎永久免费在线| 欧美嫩在线观看| aaa在线观看| 国产精品老牛影院在线观看| 国产精品videossex| 久久久99精品视频| 国产精品一区二区不卡| 欧美视频www| 91精品中文字幕一区二区三区| 最近高清中文在线字幕在线观看| 国产精品va在线| 欧美色婷婷久久99精品红桃| 激情综合网俺也去| 国产欧美一区二区三区鸳鸯浴 | 日本欧美一区二区三区不卡视频| 精品久久久久久久中文字幕| 天堂网av在线播放| 97成人精品区在线播放| 丝袜美腿一区二区三区动态图| 国产精品va无码一区二区| 99热国产精品| 国产精品国产三级国产专区52| 日韩国产高清视频在线| 中文字幕一区久| 日本一区二区精品视频| 欧美aⅴ一区二区三区视频| 久久久久久国产免费a片| 欧美日韩精品欧美日韩精品| 国产激情在线| 国产精品日韩欧美一区二区| 99在线|亚洲一区二区| 草草影院第一页| 欧美在线不卡一区| 麻豆视频网站在线观看| 亚洲一区二区三区毛片| 亚洲一级黄色| 日本高清www| 在线观看视频一区二区欧美日韩| 91成人高清| 99在线观看| 99精品视频免费观看视频| 级毛片内射视频| 91精品欧美一区二区三区综合在 | 国产精品高潮视频| 国产精品福利在线观看播放| 在线观看免费看片| 黄色成人av网| seseavlu视频在线| 2020国产精品久久精品不卡| 一区二区三区精品视频在线观看| 日本性高潮视频| 日韩一区二区三区观看| 亚洲美女炮图| 在线视频一区观看| 成人a区在线观看| 最近中文字幕免费观看| 欧美人与物videos| 国产亚洲一区二区三区不卡| 91pony九色| 岛国av一区二区| 国产一二区在线| 久久久久网址| 国产成人精品1024|