緩存--參考go-cache自己實(shí)現(xiàn)緩存

目錄

鏈接地址

思考

  • 如何控制緩存過(guò)期時(shí)間
  • 如何控制緩存內(nèi)存大小
  • 如何提高內(nèi)存的利用率

1.前言

緩存在業(yè)務(wù)中是不可避免的一個(gè)模塊须教,大體上分成兩類(lèi)

  • 本地緩存烁涌,如前端的localstorage
  • 分布式緩存,如redise/memcache

同時(shí)墓贿,通常我們?yōu)榱吮苊鈽I(yè)務(wù)層直接操作redis,會(huì)引入一個(gè)緩存模塊术吝,即再次封裝淋样,最典型的例子就是go-cache

2.go-cache走讀

2.1API概覽與快速上手

image.png

go-cache的API設(shè)計(jì)主要分成兩部分

  • 單個(gè)操作:Set/Get
  • 針對(duì)數(shù)字的加減: Increment
func Test_go_cache(t *testing.T) {
    c := cache.New(time.Minute, 10*time.Minute)
    c.Set("name", "zjb", cache.DefaultExpiration)
    res, ok := c.Get("name")
    if !ok {
        t.Fatal("not found")
    }
    name := res.(string)
    t.Log(name)
}

2.2主要數(shù)據(jù)結(jié)構(gòu)

New方法返回Cache結(jié)構(gòu)體

func New(defaultExpiration, cleanupInterval time.Duration) *Cache {
    items := make(map[string]Item)
    return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)
}

type Item struct {
    Object     interface{}
    Expiration int64 // 過(guò)期時(shí)間:設(shè)置時(shí)間+緩存時(shí)長(zhǎng)
}

我們可以大致猜到這個(gè)Item就是實(shí)際存儲(chǔ)KV的本地緩存胳螟,實(shí)際上就是如此,后續(xù)的Set/get等就是并發(fā)操作map接校;同時(shí)我們也意識(shí)到map屬于hash一類(lèi)的數(shù)據(jù)結(jié)構(gòu)猛频,內(nèi)存利用率并不高(但從這角度講,替換成數(shù)組是否更好?)

那問(wèn)題來(lái)了伦乔,為什么我們不直接使用map還要套一層娃呢厉亏?因?yàn)閙ap并發(fā)會(huì)沖突,擴(kuò)展性也差

type Cache struct {
    *cache
    // If this is confusing, see the comment at the bottom of New()
}

type cache struct {
    defaultExpiration time.Duration
    items             map[string]Item
    mu                sync.RWMutex
    onEvicted         func(string, interface{})
    janitor           *janitor
}

type janitor struct {
    Interval time.Duration // 多長(zhǎng)時(shí)間掃描一次緩存
    stop     chan bool // 是否需要停止
}
  • onEvicted 是刪除Key時(shí)執(zhí)行的回調(diào)函數(shù)
  • mu 內(nèi)部使用鎖來(lái)處理并發(fā)
  • defaultxxxx 默認(rèn)的超時(shí)時(shí)間New函數(shù)傳入
  • janitor 負(fù)責(zé)定時(shí)清空緩存

2.3清空緩存操作

newCacheWithJanitor方法初始化janitor

    C := &Cache{c}
    if ci > 0 {
        runJanitor(c, ci)
        runtime.SetFinalizer(C, stopJanitor)
    }

內(nèi)部啟動(dòng)一個(gè)協(xié)程烈和,監(jiān)聽(tīng)計(jì)時(shí)器和信號(hào)通道(cache的刪除操作是 for range 遍歷全部)

func runJanitor(c *cache, ci time.Duration) {
    j := &janitor{
        Interval: ci,
        stop:     make(chan bool),
    }
    c.janitor = j
    go j.Run(c)
}

func (j *janitor) Run(c *cache) {
    ticker := time.NewTicker(j.Interval)
    for {
        select {
        case <-ticker.C:
            c.DeleteExpired()
        case <-j.stop:
            ticker.Stop()
            return
        }
    }
}

那么問(wèn)題來(lái)了這個(gè)終止的信號(hào)量由誰(shuí)輸入爱只?runtime.SetFinalizer

runtime.SetFinalizer 是Go提供對(duì)象被GC回收時(shí)的一個(gè)注冊(cè)函數(shù),可以在對(duì)象被回收的時(shí)候回掉函數(shù)

2.4擴(kuò)展思考

  • go-cache是鎖住全部map招刹,鎖的粒度是否可以更刑袷浴?
  • 使用sync.Map代替map?
  • SetFinalizer函數(shù)使用有什么需要注意點(diǎn)疯暑?

其他緩存庫(kù)參考:

  • freecache
  • groupcache
  • bigcache

3.嘗試自己實(shí)現(xiàn)一個(gè)高可用的緩存

假設(shè)我們自己的redis緩存API設(shè)計(jì)如下

type Cache interface {
    Get(ctx context.Context, key string) (any, error)
    Set(ctx context.Context, key string, value any, t time.Duration) error
    Delete(ctx context.Context, key string) error
}

實(shí)現(xiàn)結(jié)構(gòu)體為

type RedisCache struct {
    t   time.Duration
    cmd redis.Cmdable
}

set和get就不貼出了训柴,具體就是操作cmd

3.1緩存的過(guò)期機(jī)制如何設(shè)計(jì)

緩存如何處理過(guò)期Key?

  • 定期刪除:學(xué)go-cache開(kāi)一個(gè)協(xié)程定時(shí)輪詢,time.Ticker時(shí)間一到就檢測(cè)一遍妇拯。那么時(shí)間間隔多久合適幻馁,或者直接讓用戶傳入?遍歷數(shù)量是全部還是100個(gè)越锈,1000個(gè)仗嗦?(時(shí)間不精準(zhǔn),對(duì)象過(guò)期只有等到下一個(gè)甚至更久的循環(huán)才能被刪除)
  • 定時(shí)刪除:每個(gè)key開(kāi)一個(gè)協(xié)程盯著(time.Afterfunc)甘凭?雖然可行但很離譜,這些協(xié)程大部分時(shí)間在阻塞稀拐,浪費(fèi)資源(定時(shí)器本身開(kāi)銷(xiāo)就很大,時(shí)間重置要先取消原本定時(shí)器丹弱,再重啟)
  • 懶惰刪除:學(xué)mysql懶刪除德撬,即用戶調(diào)用Get訪問(wèn)key的時(shí)候檢查過(guò)期,通常會(huì)與定期刪除一起使用躲胳,比如redis(對(duì)象過(guò)期后究竟什么時(shí)候刪除不確定蜓洪,內(nèi)存浪費(fèi))
  • 延遲隊(duì)列:把對(duì)象扔到一個(gè)延遲隊(duì)列里面,當(dāng)從隊(duì)列里取出時(shí)表示已經(jīng)過(guò)期(需要額外的內(nèi)存開(kāi)銷(xiāo)泛鸟,而且過(guò)期時(shí)間被重置時(shí)需要調(diào)整在隊(duì)列中的位置蝠咆,計(jì)算的時(shí)間復(fù)雜度增加)

3.1.1 參考redis的過(guò)期刪除策略

redis是追求高性能的中間件,因此不會(huì)選擇使用延遲隊(duì)列和定時(shí)刪除的方式北滥,選擇懶加載和定期的方式
同時(shí)redis的定期刪除會(huì)在每一個(gè)循環(huán)中遍歷DB,如果當(dāng)次定期沒(méi)有遍歷全部DB闸翅,那么下一次循環(huán)從下一個(gè)DB開(kāi)始遍歷再芋,對(duì)于每個(gè)DB:

  • 如果DB的key都沒(méi)設(shè)置過(guò)期時(shí)間就遍歷下一個(gè)DB
  • 設(shè)置了就抽一批調(diào)查,默認(rèn)25個(gè)坚冀,檢查過(guò)期济赎,執(zhí)行刪除
  • 每遍歷16個(gè)key就檢測(cè)執(zhí)行時(shí)間,如果超過(guò)閾值就中斷本次循環(huán)
  • 如果這批過(guò)期的key比例超過(guò)一個(gè)閾值,就繼續(xù)抽取下一批(比例太低就直接去遍歷下一個(gè)DB)

redis通過(guò)hz和dynamic-hz的值來(lái)控制定期刪除的頻率

特變注意redis還有一個(gè)主從分布的問(wèn)題司训,3.2之前若key過(guò)期主庫(kù)會(huì)執(zhí)行刪除操作构捡,從庫(kù)不會(huì),因此仍可以從從庫(kù)中獲取key

補(bǔ)充一點(diǎn)redis的持久化文件RDB在生成時(shí)會(huì)忽略已經(jīng)過(guò)期的key壳猜,AOF是無(wú)論是定期刪除還是懶刪除都會(huì)記錄DEL命令

3.1.2 過(guò)期時(shí)間如何確定和優(yōu)化

最完美的情況就是容量無(wú)限大勾徽,設(shè)置永不過(guò)期,但實(shí)際怎么可能统扳,基本都是通過(guò)緩存命中率來(lái)確定過(guò)期時(shí)間

  • 從緩存命中率角度出發(fā)(命中緩存的次數(shù)/過(guò)期時(shí)間)喘帚,可以選擇調(diào)大過(guò)期時(shí)間,但代價(jià)就是緩存了更多key咒钟,占用更多內(nèi)存吹由,類(lèi)似的業(yè)務(wù)就是登錄的狀態(tài)(緩存命中率越高,就越需要更多的緩存容量朱嘴,越長(zhǎng)的過(guò)期時(shí)間)
  • 減少過(guò)期時(shí)間倾鲫,這是根據(jù)業(yè)務(wù)特征出發(fā)的,如果有些業(yè)務(wù)根本用不了一定的時(shí)間萍嬉,可以選擇降低過(guò)期時(shí)間乌昔,比如當(dāng)某個(gè)數(shù)據(jù)被查出來(lái)后,大概率在三十分鐘內(nèi)再次使用這個(gè)對(duì)象帚湘,就可以減少過(guò)期時(shí)間到30分鐘

問(wèn)題又來(lái)了如何確定緩存命中率玫荣,這個(gè)實(shí)際要看用戶體驗(yàn)確定,或者公司規(guī)定的平均響應(yīng)時(shí)間來(lái)推算大诸,比如要求平均響應(yīng)時(shí)間是300ms捅厂,實(shí)際命中緩存時(shí)間100ms,沒(méi)命中1000ms资柔,假設(shè)命中率=p 則p需要滿足
命中的時(shí)間p+沒(méi)命中的時(shí)間(1-p)=300

100 x p + 1000 x (1-p) = 300
p=0.78
  • 從數(shù)據(jù)特征出發(fā)焙贷,熱點(diǎn)數(shù)據(jù)越熱,過(guò)期時(shí)間越長(zhǎng),這也就是動(dòng)態(tài)確定過(guò)期時(shí)間贿堰,比如根據(jù)請(qǐng)求特征辙芍、計(jì)算時(shí)間、重要性羹与、優(yōu)先級(jí)等

3.1.3 擴(kuò)展-緩存的預(yù)加載和超短過(guò)期時(shí)間

簡(jiǎn)單來(lái)說(shuō)就是預(yù)料用戶的行為--空間換響應(yīng)時(shí)間故硅,在訪問(wèn)a數(shù)據(jù)時(shí),大概率會(huì)訪問(wèn)B纵搁,獲取A數(shù)據(jù)時(shí)提前緩存B數(shù)據(jù)吃衅,此時(shí)這個(gè)用戶不管看不看B數(shù)據(jù),別人都用不上腾誉,所以可以把B數(shù)據(jù)的過(guò)期時(shí)間設(shè)置很短徘层,典型的就是B站的視頻下方的視頻推薦列表峻呕,朋友圈的九宮格圖片,點(diǎn)開(kāi)第一張后半預(yù)加載第二張等趣效,總結(jié)就是預(yù)期用戶行為

3.2緩存的內(nèi)存如何控制

為了防止內(nèi)存無(wú)止盡的擴(kuò)展通常需要我們做緩存控制瘦癌,go-cache里沒(méi)實(shí)現(xiàn)改功能

  • 控制鍵值對(duì)數(shù)量
  • 控制內(nèi)存大小

思路都是在set時(shí)檢測(cè)是否超過(guò)閾值,在刪除時(shí)恢復(fù)當(dāng)前余量

3.2.1擴(kuò)展閱讀-淘汰策略

  • LRU最近最少策略跷敬,緩存容量不足時(shí)從所有key挑一個(gè)最近最長(zhǎng)未使用的key淘汰(經(jīng)典的實(shí)現(xiàn)方式是額外使用一個(gè)隊(duì)列讯私,最近使用過(guò)的放到隊(duì)首,隊(duì)尾就是長(zhǎng)時(shí)間未使用過(guò)的)
  • LFU最不經(jīng)常使用干花,根據(jù)使用頻率刪除(可以在隊(duì)列實(shí)現(xiàn)的基礎(chǔ)上妄帘,增加一個(gè)表示訪問(wèn)次數(shù)的字段,每次訪問(wèn)+1)

redis支持多種淘汰算法池凄,可以根據(jù)maxmemory設(shè)置內(nèi)存用量和maxmemory_policy設(shè)置淘汰算法

  • 還有從業(yè)務(wù)角度出發(fā)抡驼,區(qū)分VIP用戶和普通用戶的數(shù)據(jù),優(yōu)先淘汰普通用戶
  • 從數(shù)據(jù)角度出發(fā)肿仑,可以選擇先淘汰大對(duì)象或小對(duì)象致盟,熱點(diǎn)數(shù)據(jù)或冷點(diǎn)數(shù)據(jù)

3.3緩存模式

緩存常與數(shù)據(jù)庫(kù)DB一起使用(最經(jīng)典的場(chǎng)景就是redis查不到,再去DB里面查)尤慰,這也涉及到我們常用的一些緩存模式

3.3.1 Cache Aside

就是Cache和DB的更新策略全部由開(kāi)發(fā)者自己決定(把緩存看作是一個(gè)獨(dú)立的數(shù)據(jù)源)馏锡,即上面的業(yè)務(wù)層(寫(xiě)在業(yè)務(wù)代碼里)查cache查不到,去db里查再去更新cache

image.png

cache-aside常在并發(fā)場(chǎng)景下的數(shù)據(jù)不一致問(wèn)題
image.png

擴(kuò)展閱讀

3.3.2 讀穿透read-through/寫(xiě)穿透write-through

cache可以做決策
read-through:

  • 業(yè)務(wù)代碼只需要從cache中讀取數(shù)據(jù)伟端,cache會(huì)在緩存不命中的時(shí)候讀取數(shù)據(jù)(我們可以主動(dòng)給結(jié)構(gòu)體設(shè)置一個(gè)接收讀DB的方法成員)
  • 寫(xiě)數(shù)據(jù)的時(shí)候杯道,業(yè)務(wù)代碼需要自己寫(xiě)DB和cache

write-through反之,當(dāng)然這兩者也不能解決并發(fā)情況下责蝠,對(duì)同一數(shù)據(jù)使用的數(shù)據(jù)一致性問(wèn)題

3.3.3 write back

  • 寫(xiě)操作直接寫(xiě)緩存党巾,不會(huì)更新DB,讀也是一樣
  • 緩存過(guò)期了再寫(xiě)回?cái)?shù)據(jù)庫(kù)
    image.png

類(lèi)似go-cache里的onEvicted函數(shù)霜医,缺點(diǎn)就是緩存奔潰時(shí)齿拂,會(huì)造成數(shù)據(jù)丟失的情況,優(yōu)點(diǎn)就是緩解了數(shù)據(jù)一致性的問(wèn)題
image.png

(使用redis的Setnx回寫(xiě)命令能解決)

3.3.4 refresh ahead

即在DB和Redis中間再加一層肴敛,監(jiān)聽(tīng)DB變更自動(dòng)更新cache
image.png

這種模式也有緩存一致性問(wèn)題署海,出現(xiàn)在緩存未命中時(shí)(使用redis的Setnx回寫(xiě)命令能解決)

3.3.5刪除緩存

顧名思義,在寫(xiě)操作時(shí)医男,直接寫(xiě)數(shù)據(jù)庫(kù)刪除緩存砸狞,類(lèi)似的就是延遲雙刪(在第一次刪除后設(shè)置一個(gè)定時(shí)器再次刪除,為防止刪除時(shí)再來(lái)一個(gè)寫(xiě)操作镀梭,實(shí)際就是二次檢查)

3.3.6 緩存一致性問(wèn)題

實(shí)際就是并發(fā)下的數(shù)據(jù)一致性問(wèn)題(操作部分失敗也會(huì)造成不一致我問(wèn)題)趾代,

  • 業(yè)務(wù)層面:常用的解決思路就是二次檢查double-check:在獲取數(shù)據(jù)時(shí)加鎖做一次檢查,在執(zhí)行操作前再加鎖做一次檢查

  • 使用消息隊(duì)列丰辣,確保同一時(shí)刻只有一個(gè)線程在更新數(shù)據(jù)

  • 引入數(shù)據(jù)版本號(hào)撒强,每一次更新版本號(hào)+1,低版本數(shù)據(jù)不能覆蓋高版本數(shù)據(jù)笙什,缺點(diǎn)就是難以維護(hù)

  • 多級(jí)緩存:本地+redis+mysql結(jié)合

  • 分布式鎖方案

3.4緩存異常

3.4.1緩存穿透

請(qǐng)求的數(shù)據(jù)根本不在(DB和Cache都沒(méi)有)飘哨,在cache-aside模式下就會(huì)造成多次請(qǐng)求全部打到數(shù)據(jù)庫(kù)上(典型如黑客非法攻擊)

那么我們是否可以在數(shù)據(jù)庫(kù)都沒(méi)有的情況下,往緩存里回寫(xiě)一個(gè)特殊值琐凭,標(biāo)記數(shù)據(jù)不存在芽隆,那么下次查詢過(guò)來(lái)不就防止了(但是如果攻擊者每次都用不同key就會(huì)造成資源浪費(fèi))

3.4.2 緩存擊穿

緩存沒(méi)有對(duì)應(yīng)的key,但是在某個(gè)時(shí)間該key并發(fā)訪問(wèn)量巨大统屈,請(qǐng)求就會(huì)都落在數(shù)據(jù)庫(kù)上

3.4.3 緩存雪崩

同一時(shí)刻大量key過(guò)期

3.4.4 解決思路-singleflight

  • 穿透:同一ip的大量請(qǐng)求落在數(shù)據(jù)庫(kù)上
  • 擊穿:不同ip的大量請(qǐng)求落在數(shù)據(jù)庫(kù)上
  • 雪崩:大量key同時(shí)過(guò)期導(dǎo)致大量請(qǐng)求落在數(shù)據(jù)庫(kù)上

綜上胚吁,解決思路就是如何在緩存出問(wèn)題時(shí)如何讓請(qǐng)求不落在數(shù)據(jù)庫(kù)上?或者說(shuō)我們?cè)谝?guī)定時(shí)間內(nèi)愁憔,多個(gè)協(xié)程訪問(wèn)同一個(gè)key時(shí)腕扶,只允許一個(gè)通過(guò),

singleflight就是上面的思路吨掌,擊穿都能解決半抱,但是穿透效果不好,因?yàn)閿?shù)據(jù)庫(kù)里面也是沒(méi)有膜宋,那么我們可以使用布隆過(guò)濾器或者bit array窿侈,cache未命中時(shí)再問(wèn)一下這些數(shù)據(jù)結(jié)構(gòu)

image.png
;最后緩存雪崩秋茫,最好的辦法就是給每個(gè)key再加一個(gè)隨機(jī)的時(shí)間偏移量防止同時(shí)過(guò)期

最后別忘了最關(guān)鍵的限流

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末史简,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子肛著,更是在濱河造成了極大的恐慌圆兵,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件策泣,死亡現(xiàn)場(chǎng)離奇詭異衙傀,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)萨咕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)统抬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人危队,你說(shuō)我怎么就攤上這事聪建。” “怎么了茫陆?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵金麸,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我簿盅,道長(zhǎng)挥下,這世上最難降的妖魔是什么揍魂? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮棚瘟,結(jié)果婚禮上现斋,老公的妹妹穿的比我還像新娘。我一直安慰自己偎蘸,他們只是感情好庄蹋,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著迷雪,像睡著了一般限书。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上章咧,一...
    開(kāi)封第一講書(shū)人閱讀 49,792評(píng)論 1 290
  • 那天倦西,我揣著相機(jī)與錄音,去河邊找鬼慧邮。 笑死调限,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的误澳。 我是一名探鬼主播耻矮,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼忆谓!你這毒婦竟也來(lái)了裆装?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤倡缠,失蹤者是張志新(化名)和其女友劉穎哨免,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體昙沦,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡琢唾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盾饮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片采桃。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖丘损,靈堂內(nèi)的尸體忽然破棺而出普办,到底是詐尸還是另有隱情,我是刑警寧澤徘钥,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布衔蹲,位于F島的核電站,受9級(jí)特大地震影響呈础,放射性物質(zhì)發(fā)生泄漏舆驶。R本人自食惡果不足惜橱健,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贞远。 院中可真熱鬧畴博,春花似錦、人聲如沸蓝仲。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春桐愉,著一層夾襖步出監(jiān)牢的瞬間而克,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工维费, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留果元,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓犀盟,卻偏偏與公主長(zhǎng)得像而晒,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子阅畴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容