【轉(zhuǎn)】分布式全局唯一ID生成策略

為什么分布式系統(tǒng)需要用到ID生成系統(tǒng)

在復(fù)雜分布式系統(tǒng)中,往往需要對大量的數(shù)據(jù)和消息進(jìn)行唯一標(biāo)識魄健。如在美團(tuán)點(diǎn)評的金融赋铝、支付、餐飲沽瘦、酒店革骨、貓眼電影等產(chǎn)品的系統(tǒng)中,數(shù)據(jù)日漸增長析恋,對數(shù)據(jù)庫的分庫分表后需要有一個(gè)唯一ID來標(biāo)識一條數(shù)據(jù)或消息良哲,數(shù)據(jù)庫的自增ID顯然不能滿足需求;特別一點(diǎn)的如訂單助隧、騎手筑凫、優(yōu)惠券也都需要有唯一ID做標(biāo)識。此時(shí)一個(gè)能夠生成全局唯一ID的系統(tǒng)是非常必要的并村。

概括下來巍实,業(yè)務(wù)系統(tǒng)對ID號的要求有哪些呢?

ID生成系統(tǒng)的需求

1.全局唯一性:不能出現(xiàn)重復(fù)的ID哩牍,最基本的要求棚潦。
2.趨勢遞增:MySQL InnoDB引擎使用的是聚集索引,由于多數(shù)RDBMS使用B-tree的數(shù)據(jù)結(jié)構(gòu)來存儲索引數(shù)據(jù)姐叁,在主鍵的選擇上面我們應(yīng)盡量使用有序的主鍵保證寫入性能瓦盛。
3.單調(diào)遞增:保證下一個(gè)ID一定大于上一個(gè)ID。
4.信息安全:如果ID是連續(xù)遞增的外潜,惡意用戶就可以很容易的窺見訂單號的規(guī)則原环,從而猜出下一個(gè)訂單號,如果是競爭對手处窥,就可以直接知道我們一天的訂單量嘱吗。所以在某些場景下,需要ID無規(guī)則滔驾。

第3谒麦、4兩個(gè)需求是互斥的,無法同時(shí)滿足哆致。

同時(shí)绕德,在大型分布式網(wǎng)站架構(gòu)中,除了需要滿足ID生成自身的需求外摊阀,還需要ID生成系統(tǒng)可用性極高耻蛇。想象以下踪蹬,如果ID生成系統(tǒng)癱瘓,那么整個(gè)業(yè)務(wù)無法進(jìn)行下去臣咖,那將是一次災(zāi)難跃捣。
因此,總結(jié)ID生成系統(tǒng)還需要滿足如下的需求:
1.高可用夺蛇,可用性達(dá)到5個(gè)9或4個(gè)9疚漆。
2.高QPS,性能不能太差刁赦,否則容易造成線程堵塞娶聘。
3.平均延遲和TP999(保證99.9%的請求都能成功的最低延遲)延遲都要盡可能低。

ID生成系統(tǒng)的類型

UUID

UUID是指在一臺機(jī)器在同一時(shí)間中生成的數(shù)字在所有機(jī)器中都是唯一的截型。按照開放軟件基金會(huì)(OSF)制定的標(biāo)準(zhǔn)計(jì)算趴荸,用到了以太網(wǎng)卡地址、納秒級時(shí)間宦焦、芯片ID碼和許多可能的數(shù)字
UUID由以下幾部分的組合:
(1)當(dāng)前日期和時(shí)間发钝。
(2)時(shí)鐘序列。
(3)全局唯一的IEEE機(jī)器識別號波闹,如果有網(wǎng)卡酝豪,從網(wǎng)卡MAC地址獲得,沒有網(wǎng)卡以其他方式獲得精堕。
標(biāo)準(zhǔn)的UUID格式為:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12)孵淘,以連字號分為五段形式的36個(gè)字符,示例:550e8400-e29b-41d4-a716-446655440000
Java標(biāo)準(zhǔn)類庫中已經(jīng)提供了UUID的API歹篓。

UUID.randomUUID()

優(yōu)點(diǎn)

  • 性能非常高:本地生成瘫证,沒有網(wǎng)絡(luò)消耗。

缺點(diǎn)

  • 不易存儲:UUID太長庄撮,16字節(jié)128位背捌,通常以36長度的字符串表示,很多場景不適用洞斯。
  • 信息不安全:基于MAC地址生成UUID的算法可能會(huì)造成MAC地址泄露毡庆,這個(gè)漏洞曾被用于尋找梅麗莎病毒的制作者位置。
  • ID作為主鍵時(shí)在特定的環(huán)境會(huì)存在一些問題烙如,比如做DB主鍵的場景下么抗,UUID就非常不適用。

SnowFlake雪花算法

雪花ID生成的是一個(gè)64位的二進(jìn)制正整數(shù)亚铁,然后轉(zhuǎn)換成10進(jìn)制的數(shù)蝇刀。64位二進(jìn)制數(shù)由如下部分組成:

snowflake id生成規(guī)則
  • 1位標(biāo)識符:始終是0,由于long基本類型在Java中是帶符號的徘溢,最高位是符號位熊泵,正數(shù)是0仰迁,負(fù)數(shù)是1甸昏,所以id一般是正數(shù)顽分,最高位是0。
  • 41位時(shí)間戳:41位時(shí)間截不是存儲當(dāng)前時(shí)間的時(shí)間截施蜜,而是存儲時(shí)間截的差值(當(dāng)前時(shí)間截 - 開始時(shí)間截 )得到的值卒蘸,這里的的開始時(shí)間截,一般是我們的id生成器開始使用的時(shí)間翻默,由我們程序來指定的缸沃。
  • 10位機(jī)器標(biāo)識碼:可以部署在1024個(gè)節(jié)點(diǎn),如果機(jī)器分機(jī)房(IDC)部署修械,這10位可以由 5位機(jī)房ID + 5位機(jī)器ID 組成趾牧。
  • 12位序列:毫秒內(nèi)的計(jì)數(shù),12位的計(jì)數(shù)順序號支持每個(gè)節(jié)點(diǎn)每毫秒(同一機(jī)器肯污,同一時(shí)間截)產(chǎn)生4096個(gè)ID序號

優(yōu)點(diǎn)

  • 簡單高效翘单,生成速度快。
  • 時(shí)間戳在高位蹦渣,自增序列在低位哄芜,整個(gè)ID是趨勢遞增的,按照時(shí)間有序遞增柬唯。
  • 靈活度高认臊,可以根據(jù)業(yè)務(wù)需求,調(diào)整bit位的劃分锄奢,滿足不同的需求失晴。

缺點(diǎn)

  • 依賴機(jī)器的時(shí)鐘,如果服務(wù)器時(shí)鐘回?fù)芫醒耄瑫?huì)導(dǎo)致重復(fù)ID生成涂屁。
  • 在分布式環(huán)境上,每個(gè)服務(wù)器的時(shí)鐘不可能完全同步堪滨,有時(shí)會(huì)出現(xiàn)不是全局遞增的情況胯陋。

snowflake Java實(shí)現(xiàn)

/**
 * Twitter_Snowflake<br>
 * SnowFlake的結(jié)構(gòu)如下(每部分用-分開):<br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
 * 1位標(biāo)識,由于long基本類型在Java中是帶符號的袱箱,最高位是符號位遏乔,正數(shù)是0,負(fù)數(shù)是1发笔,所以id一般是正數(shù)盟萨,最高位是0<br>
 * 41位時(shí)間截(毫秒級),注意了讨,41位時(shí)間截不是存儲當(dāng)前時(shí)間的時(shí)間截捻激,而是存儲時(shí)間截的差值(當(dāng)前時(shí)間截 - 開始時(shí)間截)
 * 得到的值)制轰,這里的的開始時(shí)間截,一般是我們的id生成器開始使用的時(shí)間胞谭,由我們程序來指定的(如下下面程序IdWorker類的startTime屬性)垃杖。
 * 41位的時(shí)間截,可以使用69年丈屹,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 * 10位的數(shù)據(jù)機(jī)器位调俘,可以部署在1024個(gè)節(jié)點(diǎn),包括5位datacenterId和5位workerId<br>
 * 12位序列旺垒,毫秒內(nèi)的計(jì)數(shù)彩库,12位的計(jì)數(shù)順序號支持每個(gè)節(jié)點(diǎn)每毫秒(同一機(jī)器,同一時(shí)間截)產(chǎn)生4096個(gè)ID序號<br>
 * 加起來剛好64位先蒋,為一個(gè)Long型骇钦。<br>
 * SnowFlake的優(yōu)點(diǎn)是,整體上按照時(shí)間自增排序竞漾,并且整個(gè)分布式系統(tǒng)內(nèi)不會(huì)產(chǎn)生ID碰撞(由數(shù)據(jù)中心ID和機(jī)器ID作區(qū)分)眯搭,并且效率較高,
 * 經(jīng)測試畴蹭,SnowFlake每秒能夠產(chǎn)生26萬ID左右坦仍。
 */
public class SnowflakeIdWorker {

    // ==============================Fields===========================================
    /** 開始時(shí)間截 (2015-01-01) */
    private final long twepoch = 1420041600000L;

    /** 機(jī)器id所占的位數(shù) */
    private final long workerIdBits = 5L;

    /** 數(shù)據(jù)標(biāo)識id所占的位數(shù) */
    private final long datacenterIdBits = 5L;

    /** 支持的最大機(jī)器id,結(jié)果是31 (這個(gè)移位算法可以很快的計(jì)算出幾位二進(jìn)制數(shù)所能表示的最大十進(jìn)制數(shù)) */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /** 支持的最大數(shù)據(jù)標(biāo)識id叨襟,結(jié)果是31 */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    /** 序列在id中占的位數(shù) */
    private final long sequenceBits = 12L;

    /** 機(jī)器ID向左移12位 */
    private final long workerIdShift = sequenceBits;

    /** 數(shù)據(jù)標(biāo)識id向左移17位(12+5) */
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    /** 時(shí)間截向左移22位(5+5+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    /** 生成序列的掩碼繁扎,這里為4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /** 工作機(jī)器ID(0~31) */
    private long workerId;

    /** 數(shù)據(jù)中心ID(0~31) */
    private long datacenterId;

    /** 毫秒內(nèi)序列(0~4095) */
    private long sequence = 0L;

    /** 上次生成ID的時(shí)間截 */
    private long lastTimestamp = -1L;

    //==============================Constructors=====================================
    /**
     * 構(gòu)造函數(shù)
     * @param workerId 工作ID (0~31)
     * @param datacenterId 數(shù)據(jù)中心ID (0~31)
     */
    public SnowflakeIdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    // ==============================Methods==========================================
    /**
     * 獲得下一個(gè)ID (該方法是線程安全的)
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();

        //如果當(dāng)前時(shí)間小于上一次ID生成的時(shí)間戳,說明系統(tǒng)時(shí)鐘回退過這個(gè)時(shí)候應(yīng)當(dāng)拋出異常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //如果是同一時(shí)間生成的糊闽,則進(jìn)行毫秒內(nèi)序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒內(nèi)序列溢出
            if (sequence == 0) {
                //阻塞到下一個(gè)毫秒,獲得新的時(shí)間戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //時(shí)間戳改變梳玫,毫秒內(nèi)序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的時(shí)間截
        lastTimestamp = timestamp;

        //移位并通過或運(yùn)算拼到一起組成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //
                | (datacenterId << datacenterIdShift) //
                | (workerId << workerIdShift) //
                | sequence;
    }

    /**
     * 阻塞到下一個(gè)毫秒,直到獲得新的時(shí)間戳
     * @param lastTimestamp 上次生成ID的時(shí)間截
     * @return 當(dāng)前時(shí)間戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒為單位的當(dāng)前時(shí)間
     * @return 當(dāng)前時(shí)間(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }

    //==============================Test=============================================
    /** 測試 */
    public static void main(String[] args) {
        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
        for (int i = 0; i < 1000; i++) {
            long id = idWorker.nextId();
            System.out.println(Long.toBinaryString(id));
            System.out.println(id);
        }
    }
}

數(shù)據(jù)庫自增ID機(jī)制

主要思路是采用數(shù)據(jù)庫自增ID + replace_into實(shí)現(xiàn)唯一ID的獲取右犹。

create table t_global_id(
    id bigint(20) unsigned not null auto_increment,
    stub char(1) not null default '',
    primary key (id),
    unique key stub (stub)
) engine=MyISAM;

# 每次業(yè)務(wù)可以使用以下SQL讀寫MySQL得到ID號
replace into t_golbal_id(stub) values('a');
select last_insert_id();

replace into跟insert功能類似提澎,不同點(diǎn)在于:replace into首先嘗試插入數(shù)據(jù)列表中,如果發(fā)現(xiàn)表中已經(jīng)有此行數(shù)據(jù)(根據(jù)主鍵或唯一索引判斷)則先刪除念链,再插入盼忌。否則直接插入新數(shù)據(jù)。
當(dāng)然為了避免數(shù)據(jù)庫的單點(diǎn)故障掂墓,最少需要兩個(gè)數(shù)據(jù)庫實(shí)例谦纱,通過區(qū)分auto_increment的起始值和步長來生成奇偶數(shù)的ID。如下:

Server1:
auto-increment-increment = 2
auto-increment-offset = 1

Server2:
auto-increment-increment = 2
auto-increment-offset = 2

優(yōu)點(diǎn)

  • 簡單君编。充分借助數(shù)據(jù)庫的自增ID機(jī)制跨嘉,可靠性高,生成有序的ID吃嘿。

缺點(diǎn)

  • ID生成依賴數(shù)據(jù)庫單機(jī)的讀寫性能祠乃。
  • 依賴數(shù)據(jù)庫梦重,當(dāng)數(shù)據(jù)庫異常時(shí)整個(gè)系統(tǒng)不可用。

對于MySQL的性能問題亮瓷,可以用如下方案解決
在分布式環(huán)境中琴拧,我們可以部署N臺數(shù)據(jù)庫實(shí)例,每臺設(shè)置成不同的初始值寺庄,自增步長為機(jī)器的臺數(shù)艾蓝。每臺的初始值分別為1,2,3...N,步長為N斗塘。

MySQL數(shù)據(jù)庫自增ID多機(jī)圖

以上方案雖然解決了性能問題,但是也存在很大的局限性:

  • 系統(tǒng)水平擴(kuò)容困難:系統(tǒng)定義好步長之后亮靴,增加機(jī)器之后調(diào)整步長困難馍盟。如果要添加機(jī)器怎么辦?假設(shè)現(xiàn)在只有一臺機(jī)器發(fā)號是1,2,3,4,5(步長是1)茧吊,這個(gè)時(shí)候需要擴(kuò)容機(jī)器一臺贞岭。可以這樣做:把第二臺機(jī)器的初始值設(shè)置得比第一臺超過很多搓侄,比如14(假設(shè)在擴(kuò)容時(shí)間之內(nèi)第一臺不可能發(fā)到14)瞄桨,同時(shí)設(shè)置步長為2,那么這臺機(jī)器下發(fā)的號碼都是14以后的偶數(shù)讶踪。然后摘掉第一臺芯侥,把ID值保留為奇數(shù),比如7乳讥,然后修改第一臺的步長為2柱查。讓它符合我們定義的號段標(biāo)準(zhǔn),對于這個(gè)例子來說就是讓第一臺以后只能產(chǎn)生奇數(shù)云石。擴(kuò)容方案看起來復(fù)雜嗎唉工?貌似還好,現(xiàn)在想象一下如果我們線上有100臺機(jī)器汹忠,這個(gè)時(shí)候要擴(kuò)容該怎么做淋硝?簡直是噩夢。
  • 數(shù)據(jù)庫壓力大:每次獲取一個(gè)ID都必須讀寫一次數(shù)據(jù)庫宽菜。當(dāng)然對于這種問題谣膳,也有相應(yīng)的解決方案,就是每次獲取ID時(shí)都批量獲取一個(gè)區(qū)間的號段到內(nèi)存中赋焕,用完之后再來獲取参歹。數(shù)據(jù)庫的性能提高了幾個(gè)量級。

第三方軟件生成(Redis)

Redis實(shí)現(xiàn)了一個(gè)原子操作INCR和INCRBY實(shí)現(xiàn)遞增的操作隆判。當(dāng)使用數(shù)據(jù)庫性能不夠時(shí)犬庇,可以采用Redis來代替僧界,同時(shí)使用Redis集群來提高吞吐量〕敉欤可以初始化每臺Redis的初始值為1,2,3,4,5捂襟,然后步長為5。各個(gè)Redis生成的ID為:

A:1欢峰,6葬荷,11,16纽帖,21
B:2宠漩,7,12懊直,17扒吁,22
C:3,8室囊,13雕崩,18,23
D:4融撞,9盼铁,14,19尝偎,24
E:5饶火,10,15冬念,20趁窃,25

優(yōu)點(diǎn)

  • 不依賴于數(shù)據(jù)庫,靈活方便急前,且性能優(yōu)于數(shù)據(jù)庫醒陆。
  • 數(shù)字ID天然排序,對分頁或者需要排序的結(jié)果很有幫助裆针。

缺點(diǎn):

  • 如果系統(tǒng)中沒有Redis刨摩,還需要引入新的組件,增加系統(tǒng)復(fù)雜度世吨。
  • 需要編碼和配置的工作量比較大澡刹。這個(gè)都不是最大的問題。

關(guān)于分布式全局唯一ID的生成耘婚,各個(gè)互聯(lián)網(wǎng)公司有很多實(shí)現(xiàn)方案罢浇,比如美團(tuán)點(diǎn)評的Leaf-snowflake,用zookeeper解決了各個(gè)服務(wù)器時(shí)鐘回?fù)艿膯栴},弱依賴zookeeper嚷闭。以及Leaf-segment類似上面數(shù)據(jù)庫批量ID獲取的方案攒岛。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市胞锰,隨后出現(xiàn)的幾起案子灾锯,更是在濱河造成了極大的恐慌,老刑警劉巖嗅榕,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件顺饮,死亡現(xiàn)場離奇詭異,居然都是意外死亡凌那,警方通過查閱死者的電腦和手機(jī)兼雄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來案怯,“玉大人君旦,你說我怎么就攤上這事〕凹睿” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵局蚀,是天一觀的道長麦锯。 經(jīng)常有香客問我,道長琅绅,這世上最難降的妖魔是什么扶欣? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮千扶,結(jié)果婚禮上料祠,老公的妹妹穿的比我還像新娘。我一直安慰自己澎羞,他們只是感情好髓绽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著妆绞,像睡著了一般顺呕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上括饶,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天株茶,我揣著相機(jī)與錄音,去河邊找鬼图焰。 笑死启盛,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播僵闯,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼卧抗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了棍厂?” 一聲冷哼從身側(cè)響起颗味,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎牺弹,沒想到半個(gè)月后浦马,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡张漂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年晶默,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片航攒。...
    茶點(diǎn)故事閱讀 39,902評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡磺陡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出漠畜,到底是詐尸還是另有隱情币他,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布憔狞,位于F島的核電站蝴悉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏瘾敢。R本人自食惡果不足惜拍冠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望簇抵。 院中可真熱鬧庆杜,春花似錦、人聲如沸碟摆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽焦履。三九已至拓劝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嘉裤,已是汗流浹背郑临。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留屑宠,地道東北人厢洞。 一個(gè)月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親躺翻。 傳聞我的和親對象是個(gè)殘疾皇子丧叽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評論 2 354

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