1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        緩存和數(shù)據(jù)庫一致性問題,看這篇就夠了

        共 6961字,需瀏覽 14分鐘

         ·

        2021-09-25 19:10

        閱讀本文大約需要 10 分鐘。

        如何保證緩存和數(shù)據(jù)庫一致性,這是一個老生常談的話題了。

        但很多人對這個問題,依舊有很多疑惑:

        • 到底是更新緩存還是刪緩存?
        • 到底選擇先更新數(shù)據(jù)庫,再刪除緩存,還是先刪除緩存,再更新數(shù)據(jù)庫?
        • 為什么要引入消息隊列保證一致性?
        • 延遲雙刪會有什么問題?到底要不要用?
        • ...

        這篇文章,我們就來把這些問題講清楚。

        這篇文章干貨很多,希望你可以耐心讀完。

        引入緩存提高性能

        我們從最簡單的場景開始講起。

        如果你的業(yè)務(wù)處于起步階段,流量非常小,那無論是讀請求還是寫請求,直接操作數(shù)據(jù)庫即可,這時你的架構(gòu)模型是這樣的:

        但隨著業(yè)務(wù)量的增長,你的項目請求量越來越大,這時如果每次都從數(shù)據(jù)庫中讀數(shù)據(jù),那肯定會有性能問題。

        這個階段通常的做法是,引入「緩存」來提高讀性能,架構(gòu)模型就變成了這樣:

        當下優(yōu)秀的緩存中間件,當屬 Redis 莫屬,它不僅性能非常高,還提供了很多友好的數(shù)據(jù)類型,可以很好地滿足我們的業(yè)務(wù)需求。

        但引入緩存之后,你就會面臨一個問題:之前數(shù)據(jù)只存在數(shù)據(jù)庫中,現(xiàn)在要放到緩存中讀取,具體要怎么存呢?

        最簡單直接的方案是「全量數(shù)據(jù)刷到緩存中」:

        • 數(shù)據(jù)庫的數(shù)據(jù),全量刷入緩存(不設(shè)置失效時間)
        • 寫請求只更新數(shù)據(jù)庫,不更新緩存
        • 啟動一個定時任務(wù),定時把數(shù)據(jù)庫的數(shù)據(jù),更新到緩存中

        這個方案的優(yōu)點是,所有讀請求都可以直接「命中」緩存,不需要再查數(shù)據(jù)庫,性能非常高。

        但缺點也很明顯,有 2 個問題:

        1. 緩存利用率低:不經(jīng)常訪問的數(shù)據(jù),還一直留在緩存中
        2. 數(shù)據(jù)不一致:因為是「定時」刷新緩存,緩存和數(shù)據(jù)庫存在不一致(取決于定時任務(wù)的執(zhí)行頻率)

        所以,這種方案一般更適合業(yè)務(wù)「體量小」,且對數(shù)據(jù)一致性要求不高的業(yè)務(wù)場景。

        那如果我們的業(yè)務(wù)體量很大,怎么解決這 2 個問題呢?

        緩存利用率和一致性問題

        先來看第一個問題,如何提高緩存利用率?

        想要緩存利用率「最大化」,我們很容易想到的方案是,緩存中只保留最近訪問的「熱數(shù)據(jù)」。但具體要怎么做呢?

        我們可以這樣優(yōu)化:

        • 寫請求依舊只寫數(shù)據(jù)庫
        • 讀請求先讀緩存,如果緩存不存在,則從數(shù)據(jù)庫讀取,并重建緩存
        • 同時,寫入緩存中的數(shù)據(jù),都設(shè)置失效時間

        這樣一來,緩存中不經(jīng)常訪問的數(shù)據(jù),隨著時間的推移,都會逐漸「過期」淘汰掉,最終緩存中保留的,都是經(jīng)常被訪問的「熱數(shù)據(jù)」,緩存利用率得以最大化。

        再來看數(shù)據(jù)一致性問題。

        要想保證緩存和數(shù)據(jù)庫「實時」一致,那就不能再用定時任務(wù)刷新緩存了。

        所以,當數(shù)據(jù)發(fā)生更新時,我們不僅要操作數(shù)據(jù)庫,還要一并操作緩存。具體操作就是,修改一條數(shù)據(jù)時,不僅要更新數(shù)據(jù)庫,也要連帶緩存一起更新。

        但數(shù)據(jù)庫和緩存都更新,又存在先后問題,那對應(yīng)的方案就有 2 個:

        1. 先更新緩存,后更新數(shù)據(jù)庫
        2. 先更新數(shù)據(jù)庫,后更新緩存

        哪個方案更好呢?

        先不考慮并發(fā)問題,正常情況下,無論誰先誰后,都可以讓兩者保持一致,但現(xiàn)在我們需要重點考慮「異?!骨闆r。

        因為操作分為兩步,那么就很有可能存在「第一步成功、第二步失敗」的情況發(fā)生。

        這 2 種方案我們一個個來分析。

        1) 先更新緩存,后更新數(shù)據(jù)庫

        如果緩存更新成功了,但數(shù)據(jù)庫更新失敗,那么此時緩存中是最新值,但數(shù)據(jù)庫中是「舊值」。

        雖然此時讀請求可以命中緩存,拿到正確的值,但是,一旦緩存「失效」,就會從數(shù)據(jù)庫中讀取到「舊值」,重建緩存也是這個舊值。

        這時用戶會發(fā)現(xiàn)自己之前修改的數(shù)據(jù)又「變回去」了,對業(yè)務(wù)造成影響。

        2) 先更新數(shù)據(jù)庫,后更新緩存

        如果數(shù)據(jù)庫更新成功了,但緩存更新失敗,那么此時數(shù)據(jù)庫中是最新值,緩存中是「舊值」。

        之后的讀請求讀到的都是舊數(shù)據(jù),只有當緩存「失效」后,才能從數(shù)據(jù)庫中得到正確的值。

        這時用戶會發(fā)現(xiàn),自己剛剛修改了數(shù)據(jù),但卻看不到變更,一段時間過后,數(shù)據(jù)才變更過來,對業(yè)務(wù)也會有影響。

        可見,無論誰先誰后,但凡后者發(fā)生異常,就會對業(yè)務(wù)造成影響。那怎么解決這個問題呢?

        別急,后面我會詳細給出對應(yīng)的解決方案。

        我們繼續(xù)分析,除了操作失敗問題,還有什么場景會影響數(shù)據(jù)一致性?

        這里我們還需要重點關(guān)注:并發(fā)問題

        并發(fā)引發(fā)的一致性問題

        假設(shè)我們采用「先更新數(shù)據(jù)庫,再更新緩存」的方案,并且兩步都可以「成功執(zhí)行」的前提下,如果存在并發(fā),情況會是怎樣的呢?

        有線程 A 和線程 B 兩個線程,需要更新「同一條」數(shù)據(jù),會發(fā)生這樣的場景:

        1. 線程 A 更新數(shù)據(jù)庫(X = 1)
        2. 線程 B 更新數(shù)據(jù)庫(X = 2)
        3. 線程 B 更新緩存(X = 2)
        4. 線程 A 更新緩存(X = 1)

        最終 X 的值在緩存中是 1,在數(shù)據(jù)庫中是 2,發(fā)生不一致。

        也就是說,A 雖然先于 B 發(fā)生,但 B 操作數(shù)據(jù)庫和緩存的時間,卻要比 A 的時間短,執(zhí)行時序發(fā)生「錯亂」,最終這條數(shù)據(jù)結(jié)果是不符合預(yù)期的。

        同樣地,采用「先更新緩存,再更新數(shù)據(jù)庫」的方案,也會有類似問題,這里不再詳述。

        除此之外,我們從「緩存利用率」的角度來評估這個方案,也是不太推薦的。

        這是因為每次數(shù)據(jù)發(fā)生變更,都「無腦」更新緩存,但是緩存中的數(shù)據(jù)不一定會被「馬上讀取」,這就會導(dǎo)致緩存中可能存放了很多不常訪問的數(shù)據(jù),浪費緩存資源。

        而且很多情況下,寫到緩存中的值,并不是與數(shù)據(jù)庫中的值一一對應(yīng)的,很有可能是先查詢數(shù)據(jù)庫,再經(jīng)過一系列「計算」得出一個值,才把這個值才寫到緩存中。

        由此可見,這種「更新數(shù)據(jù)庫 + 更新緩存」的方案,不僅緩存利用率不高,還會造成機器性能的浪費。

        所以此時我們需要考慮另外一種方案:刪除緩存。

        刪除緩存可以保證一致性嗎?

        刪除緩存對應(yīng)的方案也有 2 種:

        1. 先刪除緩存,后更新數(shù)據(jù)庫
        2. 先更新數(shù)據(jù)庫,后刪除緩存

        經(jīng)過前面的分析我們已經(jīng)得知,但凡「第二步」操作失敗,都會導(dǎo)致數(shù)據(jù)不一致。

        這里我不再詳述具體場景,你可以按照前面的思路推演一下,就可以看到依舊存在數(shù)據(jù)不一致的情況。

        這里我們重點來看「并發(fā)」問題。

        1) 先刪除緩存,后更新數(shù)據(jù)庫

        如果有 2 個線程要并發(fā)「讀寫」數(shù)據(jù),可能會發(fā)生以下場景:

        1. 線程 A 要更新 X = 2(原值 X = 1)
        2. 線程 A 先刪除緩存
        3. 線程 B 讀緩存,發(fā)現(xiàn)不存在,從數(shù)據(jù)庫中讀取到舊值(X = 1)
        4. 線程 A 將新值寫入數(shù)據(jù)庫(X = 2)
        5. 線程 B 將舊值寫入緩存(X = 1)

        最終 X 的值在緩存中是 1(舊值),在數(shù)據(jù)庫中是 2(新值),發(fā)生不一致。

        可見,先刪除緩存,后更新數(shù)據(jù)庫,當發(fā)生「讀+寫」并發(fā)時,還是存在數(shù)據(jù)不一致的情況。

        2) 先更新數(shù)據(jù)庫,后刪除緩存

        依舊是 2 個線程并發(fā)「讀寫」數(shù)據(jù):

        1. 緩存中 X 不存在(數(shù)據(jù)庫 X = 1)
        2. 線程 A 讀取數(shù)據(jù)庫,得到舊值(X = 1)
        3. 線程 B 更新數(shù)據(jù)庫(X = 2)
        4. 線程 B 刪除緩存
        5. 線程 A 將舊值寫入緩存(X = 1)

        最終 X 的值在緩存中是 1(舊值),在數(shù)據(jù)庫中是 2(新值),也發(fā)生不一致。

        這種情況「理論」來說是可能發(fā)生的,但實際真的有可能發(fā)生嗎?

        其實概率「很低」,這是因為它必須滿足 3 個條件:

        1. 緩存剛好已失效
        2. 讀請求 + 寫請求并發(fā)
        3. 更新數(shù)據(jù)庫 + 刪除緩存的時間(步驟 3-4),要比讀數(shù)據(jù)庫 + 寫緩存時間短(步驟 2 和 5)

        仔細想一下,條件 3 發(fā)生的概率其實是非常低的。

        因為寫數(shù)據(jù)庫一般會先「加鎖」,所以寫數(shù)據(jù)庫,通常是要比讀數(shù)據(jù)庫的時間更長的。

        這么來看,「先更新數(shù)據(jù)庫 + 再刪除緩存」的方案,是可以保證數(shù)據(jù)一致性的。

        所以,我們應(yīng)該采用這種方案,來操作數(shù)據(jù)庫和緩存。

        好,解決了并發(fā)問題,我們繼續(xù)來看前面遺留的,第二步執(zhí)行「失敗」導(dǎo)致數(shù)據(jù)不一致的問題。

        如何保證兩步都執(zhí)行成功?

        前面我們分析到,無論是更新緩存還是刪除緩存,只要第二步發(fā)生失敗,那么就會導(dǎo)致數(shù)據(jù)庫和緩存不一致。

        保證第二步成功執(zhí)行,就是解決問題的關(guān)鍵。

        想一下,程序在執(zhí)行過程中發(fā)生異常,最簡單的解決辦法是什么?

        答案是:重試

        是的,其實這里我們也可以這樣做。

        無論是先操作緩存,還是先操作數(shù)據(jù)庫,但凡后者執(zhí)行失敗了,我們就可以發(fā)起重試,盡可能地去做「補償」。

        那這是不是意味著,只要執(zhí)行失敗,我們「無腦重試」就可以了呢?

        答案是否定的?,F(xiàn)實情況往往沒有想的這么簡單,失敗后立即重試的問題在于:

        • 立即重試很大概率「還會失敗」
        • 「重試次數(shù)」設(shè)置多少才合理?
        • 重試會一直「占用」這個線程資源,無法服務(wù)其它客戶端請求

        看到了么,雖然我們想通過重試的方式解決問題,但這種「同步」重試的方案依舊不嚴謹。

        那更好的方案應(yīng)該怎么做?

        答案是:異步重試。什么是異步重試?

        其實就是把重試請求寫到「消息隊列」中,然后由專門的消費者來重試,直到成功。

        或者更直接的做法,為了避免第二步執(zhí)行失敗,我們可以把操作緩存這一步,直接放到消息隊列中,由消費者來操作緩存。

        到這里你可能會問,寫消息隊列也有可能會失敗啊?而且,引入消息隊列,這又增加了更多的維護成本,這樣做值得嗎?

        這個問題很好,但我們思考這樣一個問題:如果在執(zhí)行失敗的線程中一直重試,還沒等執(zhí)行成功,此時如果項目「重啟」了,那這次重試請求也就「丟失」了,那這條數(shù)據(jù)就一直不一致了。

        所以,這里我們必須把重試或第二步操作放到另一個「服務(wù)」中,這個服務(wù)用「消息隊列」最為合適。這是因為消息隊列的特性,正好符合我們的需求:

        • 消息隊列保證可靠性:寫到隊列中的消息,成功消費之前不會丟失(重啟項目也不擔心)
        • 消息隊列保證消息成功投遞:下游從隊列拉取消息,成功消費后才會刪除消息,否則還會繼續(xù)投遞消息給消費者(符合我們重試的場景)

        至于寫隊列失敗和消息隊列的維護成本問題:

        • 寫隊列失敗:操作緩存和寫消息隊列,「同時失敗」的概率其實是很小的
        • 維護成本:我們項目中一般都會用到消息隊列,維護成本并沒有新增很多

        所以,引入消息隊列來解決這個問題,是比較合適的。這時架構(gòu)模型就變成了這樣:

        那如果你確實不想在應(yīng)用中去寫消息隊列,是否有更簡單的方案,同時又可以保證一致性呢?

        方案還是有的,這就是近幾年比較流行的解決方案:訂閱數(shù)據(jù)庫變更日志,再操作緩存。

        具體來講就是,我們的業(yè)務(wù)應(yīng)用在修改數(shù)據(jù)時,「只需」修改數(shù)據(jù)庫,無需操作緩存。

        那什么時候操作緩存呢?這就和數(shù)據(jù)庫的「變更日志」有關(guān)了。

        拿 MySQL 舉例,當一條數(shù)據(jù)發(fā)生修改時,MySQL 就會產(chǎn)生一條變更日志(Binlog),我們可以訂閱這個日志,拿到具體操作的數(shù)據(jù),然后再根據(jù)這條數(shù)據(jù),去刪除對應(yīng)的緩存。

        訂閱變更日志,目前也有了比較成熟的開源中間件,例如阿里的 canal,使用這種方案的優(yōu)點在于:

        • 無需考慮寫消息隊列失敗情況:只要寫 MySQL 成功,Binlog 肯定會有
        • 自動投遞到下游隊列:canal 自動把數(shù)據(jù)庫變更日志「投遞」給下游的消息隊列

        當然,與此同時,我們需要投入精力去維護 canal 的高可用和穩(wěn)定性。

        如果你有留意觀察很多數(shù)據(jù)庫的特性,就會發(fā)現(xiàn)其實很多數(shù)據(jù)庫都逐漸開始提供「訂閱變更日志」的功能了,相信不遠的將來,我們就不用通過中間件來拉取日志,自己寫程序就可以訂閱變更日志了,這樣可以進一步簡化流程。

        至此,我們可以得出結(jié)論,想要保證數(shù)據(jù)庫和緩存一致性,推薦采用「先更新數(shù)據(jù)庫,再刪除緩存」方案,并配合「消息隊列」或「訂閱變更日志」的方式來做。

        主從庫延遲和延遲雙刪問題

        到這里,還有 2 個問題,是我們沒有重點分析過的。

        第一個問題,還記得前面講到的「先刪除緩存,再更新數(shù)據(jù)庫」方案,導(dǎo)致不一致的場景么?

        這里我再把例子拿過來讓你復(fù)習(xí)一下:

        2 個線程要并發(fā)「讀寫」數(shù)據(jù),可能會發(fā)生以下場景:

        1. 線程 A 要更新 X = 2(原值 X = 1)
        2. 線程 A 先刪除緩存
        3. 線程 B 讀緩存,發(fā)現(xiàn)不存在,從數(shù)據(jù)庫中讀取到舊值(X = 1)
        4. 線程 A 將新值寫入數(shù)據(jù)庫(X = 2)
        5. 線程 B 將舊值寫入緩存(X = 1)

        最終 X 的值在緩存中是 1(舊值),在數(shù)據(jù)庫中是 2(新值),發(fā)生不一致。

        第二個問題:是關(guān)于「讀寫分離 + 主從復(fù)制延遲」情況下,緩存和數(shù)據(jù)庫一致性問題。

        在「先更新數(shù)據(jù)庫,再刪除緩存」方案下,「讀寫分離 + 主從庫延遲」其實也會導(dǎo)致不一致:

        1. 線程 A 更新主庫 X = 2(原值 X = 1)
        2. 線程 A 刪除緩存
        3. 線程 B 查詢緩存,沒有命中,查詢「從庫」得到舊值(從庫 X = 1)
        4. 從庫「同步」完成(主從庫 X = 2)
        5. 線程 B 將「舊值」寫入緩存(X = 1)

        最終 X 的值在緩存中是 1(舊值),在主從庫中是 2(新值),也發(fā)生不一致。

        看到了么?這 2 個問題的核心在于:緩存都被回種了「舊值」。

        那怎么解決這類問題呢?

        最有效的辦法就是,把緩存刪掉。

        但是,不能立即刪,而是需要「延遲刪」,這就是業(yè)界給出的方案:緩存延遲雙刪策略。

        按照延時雙刪策略,這 2 個問題的解決方案是這樣的:

        解決第一個問題:在線程 A 刪除緩存、更新完數(shù)據(jù)庫之后,先「休眠一會」,再「刪除」一次緩存。

        解決第二個問題:線程 A 可以生成一條「延時消息」,寫到消息隊列中,消費者延時「刪除」緩存。

        這兩個方案的目的,都是為了把緩存清掉,這樣一來,下次就可以從數(shù)據(jù)庫讀取到最新值,寫入緩存。

        但問題來了,這個「延遲刪除」緩存,延遲時間到底設(shè)置要多久呢?

        • 問題1:延遲時間要大于「主從復(fù)制」的延遲時間
        • 問題2:延遲時間要大于線程 B 讀取數(shù)據(jù)庫 + 寫入緩存的時間

        但是,這個時間在分布式和高并發(fā)場景下,其實是很難評估的。

        很多時候,我們都是憑借經(jīng)驗大致估算這個延遲時間,例如延遲 1-5s,只能盡可能地降低不一致的概率。

        所以你看,采用這種方案,也只是盡可能保證一致性而已,極端情況下,還是有可能發(fā)生不一致。

        所以實際使用中,我還是建議你采用「先更新數(shù)據(jù)庫,再刪除緩存」的方案,同時,要盡可能地保證「主從復(fù)制」不要有太大延遲,降低出問題的概率。

        可以做到強一致嗎?

        看到這里你可能會想,這些方案還是不夠完美,我就想讓緩存和數(shù)據(jù)庫「強一致」,到底能不能做到呢?

        其實很難。

        要想做到強一致,最常見的方案是 2PC、3PC、Paxos、Raft 這類一致性協(xié)議,但它們的性能往往比較差,而且這些方案也比較復(fù)雜,還要考慮各種容錯問題。

        相反,這時我們換個角度思考一下,我們引入緩存的目的是什么?

        沒錯,性能。

        一旦我們決定使用緩存,那必然要面臨一致性問題。性能和一致性就像天平的兩端,無法做到都滿足要求。

        而且,就拿我們前面講到的方案來說,當操作數(shù)據(jù)庫和緩存完成之前,只要有其它請求可以進來,都有可能查到「中間狀態(tài)」的數(shù)據(jù)。

        所以如果非要追求強一致,那必須要求所有更新操作完成之前期間,不能有「任何請求」進來。

        雖然我們可以通過加「分布鎖」的方式來實現(xiàn),但我們要付出的代價,很可能會超過引入緩存帶來的性能提升。

        所以,既然決定使用緩存,就必須容忍「一致性」問題,我們只能盡可能地去降低問題出現(xiàn)的概率。

        同時我們也要知道,緩存都是有「失效時間」的,就算在這期間存在短期不一致,我們依舊有失效時間來兜底,這樣也能達到最終一致。

        總結(jié)

        好了,總結(jié)一下這篇文章的重點。

        1、想要提高應(yīng)用的性能,可以引入「緩存」來解決

        2、引入緩存后,需要考慮緩存和數(shù)據(jù)庫一致性問題,可選的方案有:「更新數(shù)據(jù)庫 + 更新緩存」、「更新數(shù)據(jù)庫 + 刪除緩存」

        3、更新數(shù)據(jù)庫 + 更新緩存方案,在「并發(fā)」場景下無法保證緩存和數(shù)據(jù)一致性,且存在「緩存資源浪費」和「機器性能浪費」的情況發(fā)生

        4、在更新數(shù)據(jù)庫 + 刪除緩存的方案中,「先刪除緩存,再更新數(shù)據(jù)庫」在「并發(fā)」場景下依舊有數(shù)據(jù)不一致問題,解決方案是「延遲雙刪」,但這個延遲時間很難評估,所以推薦用「先更新數(shù)據(jù)庫,再刪除緩存」的方案

        5、在「先更新數(shù)據(jù)庫,再刪除緩存」方案下,為了保證兩步都成功執(zhí)行,需配合「消息隊列」或「訂閱變更日志」的方案來做,本質(zhì)是通過「重試」的方式保證數(shù)據(jù)一致性

        6、在「先更新數(shù)據(jù)庫,再刪除緩存」方案下,「讀寫分離 + 主從庫延遲」也會導(dǎo)致緩存和數(shù)據(jù)庫不一致,緩解此問題的方案是「延遲雙刪」,憑借經(jīng)驗發(fā)送「延遲消息」到隊列中,延遲刪除緩存,同時也要控制主從庫延遲,盡可能降低不一致發(fā)生的概率

        后記

        本以為這個老生常談的話題,寫起來很好寫,沒想到在寫的過程中,還是挖到了很多之前沒有深度思考過的細節(jié)。

        在這里我也分享 4 點心得給你:

        1、性能和一致性不能同時滿足,為了性能考慮,通常會采用「最終一致性」的方案

        2、掌握緩存和數(shù)據(jù)庫一致性問題,核心問題有 3 點:緩存利用率、并發(fā)、緩存 + 數(shù)據(jù)庫一起成功問題

        3、失敗場景下要保證一致性,常見手段就是「重試」,同步重試會影響吞吐量,所以通常會采用異步重試的方案

        4、訂閱變更日志的思想,本質(zhì)是把權(quán)威數(shù)據(jù)源(例如 MySQL)當做 leader 副本,讓其它異質(zhì)系統(tǒng)(例如 Redis / Elasticsearch)成為它的 follower 副本,通過同步變更日志的方式,保證 leader 和 follower 之間保持一致

        很多一致性問題,都會采用這些方案來解決,希望我的這些心得對你有所啟發(fā)。



        如果我的文章對你有所幫助,還請幫忙點贊、在看、轉(zhuǎn)發(fā)一下,你的支持會激勵我輸出更高質(zhì)量的文章,非常感謝!
        瀏覽 28
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            奶大灬嗯灬啊灬一进一出在线视频 | 欧美日大香蕉 | 日韩在线视频看看 | 岳扒开下面让我舔是什么意思 | 大岛优香av | 成人免费网站www污污污在线看 | 人操人碰| 豆花视频福利网站 | 免费性爱视频在线观看 | 欲乱合集二免费视频 |