后端服務(wù)緩存總結(jié)

后端服務(wù)緩存總結(jié)

背景

最近再思考之前做的一個項目的時候, 有遇到一個問題, 那就是多級緩存的一致性問題.

之前在更新策略里面提到了DB和緩存的一些更新時的操作, 本文探討存在并發(fā)場景下緩/多級緩存可能存在的問題.

并發(fā)場景下的集中緩存

在絕大多數(shù)以HTTP為核心的請求情況下, 由于不同的負載均衡策略, 可能會導(dǎo)致有不同的請求落在集中緩存中.

<span style="font-weight: bold;" class="bold">讀</span>

一個讀緩存的請求結(jié)果可能有以下三種:

  1. 緩存命中, 且數(shù)據(jù)有效 (可以直接使用數(shù)據(jù))
  2. 緩存命中, 但數(shù)據(jù)無效 (使用兜底處理 / 請求DB )
  3. 緩存未命中 (讀DB -> 寫緩存)

筆者的技術(shù)有限, 在這里提供幾種方式

分布式鎖

分布式鎖的主要目的是為了控制到達DB的流量

在緩存失效的情況下, 多個進程同時請求DB, 在極端情況下會導(dǎo)致 緩存擊穿, 此時使用一個分布式鎖來控制并發(fā)是一個不錯的選擇

考慮到這個場景下, 對于分布式鎖的要求是能夠過濾大多數(shù)并發(fā)請求即可, 無需追求etcd帶來的強一致性控制.

如果能夠獲取到鎖, 則請求DB并返回. 如果無法獲取到鎖, 則使用 (本地緩存 -> 兜底返回 -> 錯誤返回)

問題

  1. <span style="font-weight: bold;" class="bold">在這種策略下, 可以保證對于DB的流量控制, 但是可能會導(dǎo)致出現(xiàn)同時出現(xiàn)較多的無效返回, 可能帶來一些不好的體驗?</span>

    如果我們的需求是盡可能的<span style="font-weight: bold;" class="bold">返回數(shù)據(jù)</span>而不是<span style="font-weight: bold;" class="bold">返回有效數(shù)據(jù),</span> 可以添加一個本地緩存(一定要注意<span style="font-weight: bold;" class="bold">本地緩存的TTL</span>)

  2. <span style="font-weight: bold;" class="bold">如果存在非常大的流量, 這種策略會不會有問題?</span>

    如果我們的分布式緩存都沒法承擔(dān)這種熱點, 這意味著我們還需要一個本地的鎖來控制

    即保證在同一段時間內(nèi), 本地只有一個請求去嘗試獲取分布式鎖.

    實現(xiàn)可以是通過 <span style="font-weight: bold;" class="bold">實例Hash</span> 獲取本地鎖. 如果能夠獲取到鎖, 則嘗試請求分布式鎖, 如果獲取不到, 則按照 (本地緩存 -> 兜底返回 -> 錯誤返回)

  3. <span style="font-weight: bold;" class="bold">本地緩存TTL應(yīng)該怎么設(shè)置?</span>

    個人建議是選擇 30% - 50%的集中緩存的TTL, 在加上一些隨機的偏移值(本地緩存的 10% 以內(nèi))

NO TTL

即然造成失效的源頭是緩存失效, 那么只要不失效就好了

對于一些活動類需求, 設(shè)計一套完美的方案是非常浪費時間的, 因為可能你的緩存TTL都比活動時間長

在這種情況下, 我建議你直接做緩存預(yù)熱 + NOTTL

在活動結(jié)束后手動刪除.

問題

  1. <span style="font-weight: bold;" class="bold">怎么刪除?</span>

    如果能夠單拎出緩存實例最好, 如果不能, 那就設(shè)置一些共同的前綴, 然后再業(yè)務(wù)的低峰進行刪除.

  2. <span style="font-weight: bold;" class="bold">怎么不對其他業(yè)務(wù)產(chǎn)生影響?</span>

    不要使用*Keys*命令, 這會直接導(dǎo)致線上問題!

    不要使用<span style="font-weight: bold;" class="bold">***Keys***</span>命令, 這會直接導(dǎo)致線上問題!**

    不要使用<span style="font-weight: bold;" class="bold">***Keys***</span>命令, 這會直接導(dǎo)致線上問題!<span style="font-weight: bold;" class="bold">

    你可以寫一個腳本 使用</span>SCAN + UNLINK的<span style="font-weight: bold;" class="bold">方式來處理

    這種方案的好處是能夠比較好的保證不阻塞主業(yè)務(wù)邏輯 以及 不會因為大量刪除Key 導(dǎo)致出現(xiàn)Slots Rebalance.

    package main
    
    import (
        "context"
    
        "github.com/redis/go-redis/v9"
    )
    
    var c redis.Client
    
    func main() {
        ctx := context.Background()
        var cursor uint64 = 0
        var keys []string
        for {
            var err error
            keys, cursor, err = c.Scan(ctx, cursor, "biz_prefix*", 100).Result()
            if err != nil {
                break
            }
            if len(keys) == 0 {
                break
            }
            c.Unlink(ctx, keys...)
        }
    }
    
    

</span>寫 / 更新<span style="font-weight: bold;" class="bold">

這兩種操作帶來的問題其實會比較多, 原因就出在這個并發(fā)環(huán)境以及一致性保證上了.

為什么? 歸根到底只有一點, 怎么保證并發(fā)環(huán)境下的一致性?

我們需要確定這個幾個基本的規(guī)則:

  1. </span>如果DB中沒有寫入, 那么必不能從緩存中讀取出.<span style="font-weight: bold;" class="bold">
  2. </span>盡可能的返回及時的內(nèi)容.<span style="font-weight: bold;" class="bold">
  3. </span>保證對底層DB有較小的讀負載.<span style="font-weight: bold;" class="bold">

有幾種方案, 我們挨個分析下

更緩存 -> 更DB

</span>我根本不推薦你使用這種方案, 因為不出現(xiàn)問題還好, 一旦出現(xiàn)問題就是巨大的麻煩.<span style="font-weight: bold;" class="bold">

這種方案的優(yōu)點是很快, 進程A更新后, 后續(xù)的進程可以無感知的直接使用新的緩存.

能較好的保證規(guī)則2, 規(guī)則3

但是問題是, 如果更DB失敗了呢?

那么導(dǎo)致的結(jié)果會非常嚴重: 持久化出現(xiàn)問題 </span>相比給出一個過時的返回, 給一個錯誤的返回可能造成更大的風(fēng)險.<span style="font-weight: bold;" class="bold">

請一定慎重!!!

刪緩存 -> 更DB

這個方案聽起來好像沒有什么問題, 但是請不要忽略了一件事情, 在并發(fā)環(huán)境下, 一切皆有可能, 溝槽的并發(fā).

在正常情況下, 我們的想法是刪除緩存, 更新DB, 然后其他的請求過來, 通過上述的</span>讀緩存操作<span style="font-weight: bold;" class="bold">獲得了最新的DB, 然后DB緩存也一致, 完美!

sequenceDiagram
    A ->> Cache: Delte Cache
    Cache -->> A: Delte OK
    A ->> DB: Update
    DB ->> A: OK
  
    B --x Cache: Load
    B ->> DB: Load
    DB ->> B: OK
    B ->> Cache: Set Cache

但是實際情況不然

sequenceDiagram
    participant A
    participant Cache
    participant DB
    participant B
  
    A ->> Cache: Delte Cache
    Cache -->> A: Delte OK

    B --x Cache: Load
    B ->> DB: Load
    DB ->> B: OK

    A ->> DB: Update
    DB ->> A: OK
    B ->> Cache: Set Cache

在這種情況下, 刪完緩存, 有另外一個B直接從DB獲取給刷進去了, 那么就壞事兒了.

更DB -> 刪緩存

這個方案是一個比較可用的方案, 先更新DB, 然后再刪除緩存, 這樣不就能保證一致性了嗎?

正常處理如下

sequenceDiagram
    participant A
    participant Cache
    participant DB
    participant B
  
    A ->> DB: Update
    DB ->> A: OK
  
    A ->> Cache: Delte Cache
    Cache -->> A: Delte OK

    B --x Cache: Load
    B ->> DB: Load
    DB ->> B: OK

    B ->> Cache: Set Cache

但是實際可能出現(xiàn)一些特殊的情況: 如果刪緩存失敗了呢?

sequenceDiagram
    participant A
    participant Cache
    participant DB
    participant B
  
    A ->> DB: Update
    DB ->> A: OK
  
    A ->> Cache: Delte Cache
    Cache -X A: Delte Error!

    B ->> Cache: Load
    Cache ->> B: OK

問題

  1. </span>如果沒法刪除緩存呢?<span style="font-weight: bold;" class="bold">

    那么后果就是要等到一個緩存TTL, 才能獲取到最新的數(shù)據(jù).

  2. </span>怎么改善?<span style="font-weight: bold;" class="bold">

    其實緩存了一個老的數(shù)據(jù)并不是一個無法接受的事情.

    我們可以通過降低TTL的方式來提高一致性, 刪緩存失敗的出現(xiàn)頻率有多高呢?

    另外就是如果第一次刪緩存失敗, 等待重試能夠解決大部分問題.

延遲雙刪

刪緩存, 更DB, 再刪緩存.

示意圖:

sequenceDiagram
    participant A as Client A
    participant Cache as Cache
    participant DB as Database
    participant B as Client B

    A->>Cache: Delete Cache
    Cache-->>A: Delete OK
    A->>DB: Update Data
    DB-->>A: Update OK

    A->>+Cache: Delayed Delete Cache (after some time)
    Cache-->>-A: Delete OK

    B--xCache: Load Data
    B->> DB: Load Data
    DB ->>B: Return Data
    B->>Cache: Set Cache

改善了 刪緩存 -> 更DB的影響范圍, 我們前面提到了, 如果先刪除緩存在更新DB, 有可能在更新DB前, 被其他的進程給寫了老的緩存.

在延遲雙刪的情況下:

sequenceDiagram
    participant A as Client A
    participant Cache as Cache
    participant DB as Database
    participant B as Client B

    A->>Cache: Delete Cache
    Cache-->>A: Delete OK

    A->>DB: Update Data
    DB-->>+A: Update OK
    B--xCache: Load Data
    B->> DB: Load Data
    DB ->>B: Return Data
    B->>Cache: Set Cache

    A->>-Cache: Delayed Delete Cache (after some time)
    Cache-->>A: Delete OK

看起來一切都很美好, 但是在極端的情況下可能出現(xiàn)

sequenceDiagram
    participant A as Client A
    participant Cache as Cache
    participant DB as Database
    participant B as Client B

    A->>Cache: Delete Cache
    Cache-->>A: Delete OK

    B--xCache: Load Data
    B->> DB: Load Data
    DB ->>B: Return Data
    B->>Cache: Set Cache

    A->>DB: Update Data
    DB-->>+A: Update OK
    A-x-Cache: Delayed Delete Cache (after some time)

即在最壞的情況下, 可能會回退到 更DB -> 刪緩存 的情況.

思考

  1. </span>延遲雙刪改進了什么?<span style="font-weight: bold;" class="bold">

    比較 刪緩存 -> 更DB, 解決了中間請求導(dǎo)致的不一致問題

    比較 更DB -> 刪緩存, 因為第一次刪除的存在, 保證在 (Delete緩存, Update DB] 中間只要沒有請求就能保證一致性

    只有在存在中間請求, 且最后一次刪除失敗的情況下才會出現(xiàn)回退到到 </span>更DB -> 刪緩存<span style="font-weight: bold;" class="bold">的策略.

  2. </span>延遲雙刪的延遲改怎么選擇?**

    我的建議是選擇業(yè)務(wù) 2 * P99, 這只是一個經(jīng)驗之談, 請盡量考慮自己的業(yè)務(wù)場景以及對不一致請求數(shù)量容忍性來考慮

    延遲設(shè)置的越高, 整體一致性越好, 同時中間請求數(shù)量越多

    延遲設(shè)置的越低, 中間請求數(shù)量越少, 同時越有可能出現(xiàn)中間請求last SetCache的情況, 進而導(dǎo)致出現(xiàn)數(shù)據(jù)不一致情況, 一致性破壞可能性越高.

事件驅(qū)動的緩存同步

事件驅(qū)動的緩存同步是指使用事件來觸發(fā)和協(xié)調(diào)緩存與數(shù)據(jù)源之間的數(shù)據(jù)同步。

這種模式特別適合于大規(guī)模、高性能的系統(tǒng)架構(gòu),因為它能夠減少系統(tǒng)組件之間的耦合,提高數(shù)據(jù)處理的效率。

實現(xiàn)

事件驅(qū)動的緩存同步通常涉及以下組件:

  1. <span style="font-weight: bold;" class="bold">事件生產(chǎn)者</span>:一般是基于業(yè)務(wù)操作產(chǎn)生事件的服務(wù). 通常來說是MongoDB oplog, MySQL binlog (row模式)
  2. <span style="font-weight: bold;" class="bold">事件傳遞系統(tǒng)</span>:負責(zé)事件的傳輸,常用的有消息隊列系統(tǒng),如 Kafka、RabbitMQ、Redis pub/sub等。
  3. <span style="font-weight: bold;" class="bold">事件消費者</span>:訂閱事件并執(zhí)行緩存同步操作的服務(wù)。

實現(xiàn)步驟:

  1. <span style="font-weight: bold;" class="bold">捕獲數(shù)據(jù)變更事件</span>:當(dāng)數(shù)據(jù)源中的數(shù)據(jù)發(fā)生變更時,要生成一個事件。(通常使用Debezium)
  2. <span style="font-weight: bold;" class="bold">發(fā)布事件</span>:將捕獲的事件發(fā)布到消息隊列系統(tǒng)中。這樣可以解耦生產(chǎn)者和消費者,提高系統(tǒng)的伸縮性和容錯能力。
  3. <span style="font-weight: bold;" class="bold">處理事件</span>:實現(xiàn)一個或多個事件消費者,它們從消息隊列中訂閱并接收事件,然后根據(jù)事件內(nèi)容來更新緩存中的數(shù)據(jù)。

優(yōu)劣

<span style="font-weight: bold;" class="bold">優(yōu)點:</span>

  • <span style="font-weight: bold;" class="bold">低耦合</span>:生產(chǎn)者和消費者之間通過事件解耦,降低了系統(tǒng)間的直接依賴。
  • <span style="font-weight: bold;" class="bold">高可擴展性</span>:系統(tǒng)各部分可以獨立擴展,滿足不同的性能要求。
  • <span style="font-weight: bold;" class="bold">實時性</span>:可以實現(xiàn)接近實時的緩存更新,提高系統(tǒng)響應(yīng)速度。
  • <span style="font-weight: bold;" class="bold">容錯性</span>:消息隊列系統(tǒng)通常具有容錯機制,可以在某個組件失敗時繼續(xù)保持系統(tǒng)的穩(wěn)定。

<span style="font-weight: bold;" class="bold">缺點:</span>

  • <span style="font-weight: bold;" class="bold">復(fù)雜性</span>:引入了額外的組件和系統(tǒng)復(fù)雜性,需要更多的維護工作。
  • <span style="font-weight: bold;" class="bold">一致性挑戰(zhàn)</span>:可能會出現(xiàn)數(shù)據(jù)不一致的情況,特別是在分布式系統(tǒng)中。
  • <span style="font-weight: bold;" class="bold">消息積壓</span>:在高負載情況下,如果處理不及時,可能會導(dǎo)致消息堆積。
  • <span style="font-weight: bold;" class="bold">延遲波動</span>:系統(tǒng)的性能可能會受到網(wǎng)絡(luò)延遲和消息隊列性能的影響。

并發(fā)情況下的多級緩存

以下內(nèi)容不適用

通常來說是不需要多級緩存的, 為什么?

在經(jīng)典的后端場景下, 底層DB的典型QPS在1W, 緩存中間件的QPS在10W

通常來說, 很少有能夠觸碰到10W QPS的場景.

但是在極少數(shù)情況下, 例如鑒權(quán)/用戶信息等等接口, 可能出現(xiàn)一些超高請求量的情況, 我個人是比較建議使用 緩存Cluster 來解決這個問題, 如果經(jīng)濟實力允許的話, 能夠最高支撐到 200 * 10W, 但是那個時候基礎(chǔ)設(shè)施的建設(shè)已經(jīng)超出我的能力理解范圍外了.

本章節(jié)指代的多級緩存是由 LocalCache -> Redis Cache -> DB 的三級, <span style="font-weight: bold;" class="bold">高性能的集中緩存, 有一層就夠了.</span>

什么場景需要?

  1. 能夠保證同一個Key(用戶/IP)的請求都能落在一個服務(wù)實例上, 比如配置的IP 負載均衡, 或者是使用長連接的情況下, 如果一個Key是隨機的落在任意實例上的, 那么此時的本地緩存是沒有什么效果的.

  2. 超他媽的高的流量, 我的Redis頂不住了

    OK, 你可以試一試, 但是我沒有遇到過, 所以你就聽一聽就好了.

總之, 保證你的請求的內(nèi)聚性.

主要問題

引入了本地緩存之后, 主要的問題就是一致性問題, 即本地緩存 -> 集中緩存 -> DB的一致性.

但是細細想來, 其實也沒有那么復(fù)雜.

解決方案

集中緩存 -> DB的一致性

這里的解決方案和章節(jié)2所示的一模一樣, 沒有任何改動

本地緩存的一致性

讀: 與集中緩存的一致性

看到這個標(biāo)題, 你可能會有疑問, 為什么是本地緩存和集中緩存的一致性?

原因很簡單, 因為我們通常認為本地緩存的TTL要遠小于集中緩存, 如果頻繁的請求DB, 則可以直接取消掉集中緩存的存在.

<span style="font-weight: bold;" class="bold">為什么?</span>

因為每個服務(wù)實例都存在一個本地緩存, 如果你設(shè)置了一個長的TTL, 你還需要集中緩存嗎?

如果TTL過期了, 直接請求DB, 你還需要集中緩存嗎?

OK, 總之, 我們確定了一點:

本地緩存的更新應(yīng)該總是按照: 集中緩存 -> DB這樣的邏輯來處理.

寫: 與DB的一致性

看到這個標(biāo)題, 你可能又有疑問, 為什么是本地緩存和DB的一致性? 不應(yīng)該是本地緩存 -> 集中緩存(Redis)的一致性嗎?

因為集中緩存不論如何都是有可能出現(xiàn)不一致的情況的, 這一點在上面的標(biāo)題也提到了.

最準(zhǔn)確的結(jié)果應(yīng)該總是從DB中獲取.

請不要忘記了一件事情, 如果是寫 或 更新的情況下, 我們一定可以知道最新的數(shù)據(jù)情況, 那么此時直接繞過Redis進行更新, 會有什么問題?

答案: 不存在任何問題.

因為你的本地緩存一定是最新的內(nèi)容, 本地緩存和DB的一致性一定要比 集中緩存和DB的一致性要高.

所以, 這種情況下, 你可以為寫/更新 導(dǎo)致的緩存更新設(shè)置一個比較高的TTL.

我的建議是, 設(shè)置為讀更新TTL的兩倍, 具體視你的業(yè)務(wù)場景來處理

如果是一個寫頻率稍高的場景, 可以將TTL設(shè)置的稍小一些. 反之, 就可以設(shè)置的大一點.

總結(jié)

其實看到這兒, 你也明白了, 本地緩存的引入其實會帶來非常多的問題.

維護兩個緩存其實是一件不那么容易的事情, 我的建議是能不用本地緩存, 就不用本地緩存, 99.9%情況下,你不需要本地緩存.

當(dāng)然, 如果基本不寫入, 是一些比較靜態(tài)的數(shù)據(jù), 那么本地緩存還是非常推薦的.

一個真實的場景

背景

  1. 一個IM即時通信的場景, C/S維持長連接, 一個需求是用戶的每個消息, 需要查看之前的歷史記錄.
  2. 用戶發(fā)送消息的頻率還是比較高的, 所以可以認為是一個讀多寫多的場景.
  3. 有其他的服務(wù)需要查看歷史記錄, 但是對一致性要求不高.

設(shè)計

  1. DB存儲使用MongoDB, 每一個請求都會落MongoDB, 進行持久化處理
  2. Server端為每個Client維護一個 context, 在context中維護一個List, 用來存放歷史記錄
  3. 消息持久化之后, 異步的放入Redis List中.

分析

這就是典型的可以使用本地緩存的場景, 為什么?

因為在這中情況下, 可以最高效率的利用本地緩存, 幾乎所有的本地緩存都可以使用到,而且除非斷線重連.

如果不是因為其他的服務(wù)需要一個快速的而且允許不一致的數(shù)據(jù)訪問方式, Redis可以直接去掉的.

總結(jié)

在多級緩存架構(gòu)中,一致性問題成為了一個關(guān)鍵的挑戰(zhàn),尤其是在高并發(fā)場景下。一致性問題主要來源于緩存與數(shù)據(jù)庫狀態(tài)不同步的問題,以及不同級別的緩存之間狀態(tài)不一致的問題。處理這些問題的策略主要圍繞數(shù)據(jù)更新路徑的設(shè)計,以及緩存失效和更新機制。

單級緩存一致性的解決方案

讀操作

  • <span style="font-weight: bold;" class="bold">分布式鎖</span>:用于控制并發(fā)下對數(shù)據(jù)庫的訪問,以避免緩存擊穿。但需注意,分布式鎖可能導(dǎo)致大量的無效返回和流量沖擊。
  • <span style="font-weight: bold;" class="bold">NO TTL</span>:對于短期的高流量活動,可以采用緩存預(yù)熱加上不設(shè)置TTL的方法,活動結(jié)束后手動刪除緩存。

寫操作

  • <span style="font-weight: bold;" class="bold">更緩存 -> 更DB</span>:不推薦,因為一旦更新數(shù)據(jù)庫失敗,緩存中的數(shù)據(jù)將是陳舊的,造成數(shù)據(jù)不一致.
  • <span style="font-weight: bold;" class="bold">刪緩存 -> 更DB</span>:可能會導(dǎo)致在刪除緩存和更新數(shù)據(jù)庫之間的窗口期內(nèi),其他請求將老數(shù)據(jù)寫回緩存,造成不一致.
  • <span style="font-weight: bold;" class="bold">更DB -> 刪緩存</span>:更為安全的方法,即使刪除緩存失敗,也只會導(dǎo)致數(shù)據(jù)短暫的不一致.
  • <span style="font-weight: bold;" class="bold">延遲雙刪</span>:結(jié)合刪緩存 -> 更DB 和 更DB -> 刪緩存 的方法,通過延遲刪除來盡量減少不一致窗口期的大小.
  • <span style="font-weight: bold;" class="bold">事件驅(qū)動:</span> 性能強, 侵入低, 但是引入了新的組件, 可能會降低可維護性.

多級緩存一致性問題

在某些極端高流量情況下,可能需要采用多級緩存來分擔(dān)集中緩存的壓力。這時,除了要處理集中緩存與數(shù)據(jù)庫之間的一致性問題外,還要處理本地緩存與集中緩存之間的一致性問題。

集中緩存和DB的一致性

對于集中緩存和數(shù)據(jù)庫的一致性問題,解決方案與單級緩存一致性的解決方案相同。

本地緩存的一致性

  • <span style="font-weight: bold;" class="bold">本地緩存與集中緩存</span>:本地緩存的TTL應(yīng)該小于集中緩存的TTL,確保數(shù)據(jù)的更新能夠及時反映到本地緩存中。
  • <span style="font-weight: bold;" class="bold">本地緩存與DB</span>:寫操作時,可以直接更新本地緩存與數(shù)據(jù)庫,保證本地緩存的數(shù)據(jù)總是最新的。設(shè)置合理的TTL以確保數(shù)據(jù)一致性。

最終總結(jié)

多級緩存一致性問題的處理是一個復(fù)雜的挑戰(zhàn),特別是在高并發(fā)、讀寫頻繁的背景下。每一種緩存一致性策略都有其適用場景和潛在風(fēng)險。

在設(shè)計緩存更新機制時,需要全面考慮業(yè)務(wù)需求、性能和一致性的平衡。

在大多數(shù)情況下,單級緩存足矣。而在特定高流量場景下,引入本地緩存是必要的,但這需要仔細設(shè)計以確保數(shù)據(jù)一致性,并盡量減少復(fù)雜性。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容