Consistent hash學(xué)習(xí)與總結(jié)

近期在學(xué)習(xí)分布式存儲(chǔ)相關(guān)的知識(shí),了解和熟悉一些數(shù)據(jù)分布相關(guān)的算法船惨,本文總結(jié)一下最近的學(xué)習(xí)成果整以。

本文目標(biāo)

  • 熟悉普通取模算法進(jìn)行數(shù)據(jù)分布的優(yōu)勢(shì)劣勢(shì)
  • 熟悉一致性hash的原理,以及該算法的優(yōu)勢(shì)劣勢(shì)

本文使用軟件環(huán)境

  • Java8

本文進(jìn)度簡(jiǎn)介

  • 數(shù)據(jù)分布接口定義
  • 通過(guò)取模算法實(shí)現(xiàn)上述數(shù)據(jù)分布接口定義
  • 通過(guò)一致性hash算法實(shí)現(xiàn)上述數(shù)據(jù)分布接口定義

Note:

本文通過(guò)Java來(lái)實(shí)現(xiàn)這兩個(gè)算法确垫。

一弓颈、數(shù)據(jù)分布接口定義

概述

在分布式環(huán)境下面,我們經(jīng)常會(huì)通過(guò)一定的規(guī)則來(lái)進(jìn)行數(shù)據(jù)分布的定義删掀,比如用戶1的數(shù)據(jù)存儲(chǔ)到數(shù)據(jù)庫(kù)1翔冀、用戶2的數(shù)據(jù)存儲(chǔ)到數(shù)據(jù)庫(kù)2......

一般來(lái)說(shuō),有這么幾種常用的方式:
1披泪、有一個(gè)分布式環(huán)境中唯一的中心分發(fā)節(jié)點(diǎn)纤子,每次在數(shù)據(jù)存儲(chǔ)的時(shí)候,都會(huì)詢問(wèn)這個(gè)中心節(jié)點(diǎn)這個(gè)數(shù)據(jù)該去哪兒款票,這個(gè)分發(fā)節(jié)點(diǎn)明確告訴這個(gè)數(shù)據(jù)該去哪兒控硼。
2、通過(guò)一定規(guī)則產(chǎn)生一個(gè)key艾少,對(duì)這個(gè)key進(jìn)行一定規(guī)則的運(yùn)算卡乾,得出這個(gè)數(shù)據(jù)該去哪兒。本文描述的取模算法和一致性hash缚够,就是這樣一種方式

接口定義

/**
* 數(shù)據(jù)分布hash算法接口定義
* @author xingchuan.qxc
*
*/
public interface HashNodeService {

/**
* 集群增加一個(gè)數(shù)據(jù)存儲(chǔ)節(jié)點(diǎn)
* @param node
*/
public void addNode(Node node);

/**
* 數(shù)據(jù)存儲(chǔ)時(shí)查找具體使用哪個(gè)節(jié)點(diǎn)來(lái)存儲(chǔ)
* @param key
* @return
*/
public Node lookupNode(String key);

/**
* hash的算法
* @param key
* @return
*/
public Long hash(String key);

/**
* 模擬意外情況斷掉一個(gè)節(jié)點(diǎn)幔妨,用于測(cè)試緩存命中率
* @param node
*/
public void removeNodeUnexpected(Node node);
}

二、數(shù)據(jù)分布算法實(shí)現(xiàn)——取模算法

概述

取模算法的應(yīng)用場(chǎng)景描述如下:
需要在集群中實(shí)現(xiàn)一個(gè)用戶數(shù)據(jù)存儲(chǔ)的負(fù)載均衡谍椅,集群中有n個(gè)存儲(chǔ)節(jié)點(diǎn)误堡,如何均勻的把各個(gè)數(shù)據(jù)分布到這n個(gè)節(jié)點(diǎn)呢?
實(shí)現(xiàn)步驟大概分成兩步:
1毯辅、通過(guò)用戶的key來(lái)取一個(gè)hash值
2埂伦、通過(guò)這個(gè)hash值來(lái)對(duì)存儲(chǔ)節(jié)點(diǎn)數(shù)n進(jìn)行取模煞额,得出一個(gè)index
3思恐、上面這個(gè)index就是待存儲(chǔ)的節(jié)點(diǎn)標(biāo)識(shí)

Note:

本文例子我生成hash值的方式,我采用CRC32的方式膊毁。

代碼實(shí)現(xiàn):

/**
* 取模數(shù)據(jù)分布算法實(shí)現(xiàn)
* @author xingchuan.qxc
*
*/
public class NormalHashNodeServiceImpl implements HashNodeService{

/**
* 存儲(chǔ)節(jié)點(diǎn)列表
*/
private List<Node> nodes = new ArrayList<>();

@Override
public void addNode(Node node) {
this.nodes.add(node);
}
@Override
public Node lookupNode(String key) {
long k = hash(key);
int index = (int) (k % nodes.size());
return nodes.get(index);
}
@Override
public Long hash(String key) {
CRC32 crc32 = new CRC32();
crc32.update(key.getBytes());
return crc32.getValue();
}
@Override
public void removeNodeUnexpected(Node node) {
nodes.remove(node);
}
}

通過(guò)上述例子我們可以看到胀莹,lokkupNode的時(shí)候,是要先去取這個(gè)key的CRC32的值婚温,然后對(duì)集群中節(jié)點(diǎn)數(shù)進(jìn)行取模得到r描焰,最后返回下標(biāo)為r的Node。
測(cè)試代碼如下:

HashNodeService nodeService = new NormalHashNodeServiceImpl();
Node addNode1 = new Node("xingchuan.node1", "192.168.0.11");
Node addNode2 = new Node("xingchuan.node2", "192.168.0.12");
Node addNode3 = new Node("xingchuan.node3", "192.168.0.13");
Node addNode4 = new Node("xingchuan.node4", "192.168.0.14");
Node addNode5 = new Node("xingchuan.node5", "192.168.0.15");
Node addNode6 = new Node("xingchuan.node6", "192.168.0.16");
Node addNode7 = new Node("xingchuan.node7", "192.168.0.17");
Node addNode8 = new Node("xingchuan.node8", "192.168.0.18");
nodeService.addNode(addNode1);
nodeService.addNode(addNode2);
nodeService.addNode(addNode3);
nodeService.addNode(addNode4);
nodeService.addNode(addNode5);
nodeService.addNode(addNode6);
nodeService.addNode(addNode7);
nodeService.addNode(addNode8);

//用于檢查數(shù)據(jù)分布情況
Map<String, Integer> countmap = new HashMap<>();
Node node = null;
for (int i = 1; i <= 100000; i++) {
String key = String.valueOf(i);
node = nodeService.lookupNode(key);
node.cacheString(key, "TEST_VALUE");
String k = node.getIp();
Integer count = countmap.get(k);
if (count == null) {
count = 1;
countmap.put(k, count);
} else {
count++;
countmap.put(k, count);
}

}
System.out.println("初始化數(shù)據(jù)分布情況:" + countmap);

運(yùn)行結(jié)果如下:

初始化數(shù)據(jù)分布情況:{192.168.0.11=12499, 192.168.0.12=12498, 192.168.0.13=12500, 192.168.0.14=12503, 192.168.0.15=12500, 192.168.0.16=12502, 192.168.0.17=12499, 192.168.0.18=12499}

可以看到栅螟,每個(gè)節(jié)點(diǎn)的存儲(chǔ)分布數(shù)量是大致一樣的荆秦。

缺點(diǎn)

我們可以很清楚的看到,取模算法是通過(guò)數(shù)據(jù)存儲(chǔ)節(jié)點(diǎn)個(gè)數(shù)來(lái)進(jìn)行運(yùn)算的力图,所以步绸,當(dāng)存儲(chǔ)節(jié)點(diǎn)個(gè)數(shù)變化了,就會(huì)造成災(zāi)難性的緩存失效吃媒。
舉例:
初始集群里面只有4個(gè)存儲(chǔ)節(jié)點(diǎn)(Node0,Node1,Node2,Node3)瓤介,這時(shí)候我要存儲(chǔ)id為1~10的用戶吕喘,我可以通過(guò)id % 4來(lái)運(yùn)算得出各個(gè)ID的分布節(jié)點(diǎn)

id 取余數(shù)結(jié)果 StoreNode
1 1 Node1
2 2 Node2
3 3 Node3
4 0 Node0
5 1 Node1
6 2 Node2
7 3 Node3
8 0 Node0
9 1 Node1
10 2 Node2

這時(shí)候,如果集群新增一個(gè)存儲(chǔ)節(jié)點(diǎn)Node4刑桑,會(huì)發(fā)生什么呢氯质?

id 取余數(shù)結(jié)果 原存儲(chǔ)節(jié)點(diǎn) 新存儲(chǔ)節(jié)點(diǎn) 是否有差異
1 1 Node1 Node1
2 2 Node2 Node2
3 3 Node3 Node3
4 4 Node0 Node4 有差異
5 0 Node1 Node0 有差異
6 1 Node2 Node1 有差異
7 2 Node3 Node2 有差異
8 3 Node0 Node3 有差異
9 4 Node1 Node4 有差異
10 0 Node2 Node0 有差異

這里我們會(huì)發(fā)現(xiàn),大量的存儲(chǔ)節(jié)點(diǎn)的key和原先的對(duì)應(yīng)不上了祠斧,這時(shí)候我們?nèi)绻谏a(chǎn)環(huán)境闻察,就需要做大量的數(shù)據(jù)遷移。
刪除一個(gè)節(jié)點(diǎn)琢锋,原理同上蜓陌,不再贅述。

代碼模擬一個(gè)分布式緩存存儲(chǔ)吩蔑,使用取模的方式钮热,新增一個(gè)節(jié)點(diǎn)帶來(lái)的問(wèn)題。測(cè)試代碼如下:

HashNodeService nodeService = new NormalHashNodeServiceImpl();
Node addNode1 = new Node("xingchuan.node1", "192.168.0.11");
Node addNode2 = new Node("xingchuan.node2", "192.168.0.12");
Node addNode3 = new Node("xingchuan.node3", "192.168.0.13");
Node addNode4 = new Node("xingchuan.node4", "192.168.0.14");
Node addNode5 = new Node("xingchuan.node5", "192.168.0.15");
Node addNode6 = new Node("xingchuan.node6", "192.168.0.16");
Node addNode7 = new Node("xingchuan.node7", "192.168.0.17");
Node addNode8 = new Node("xingchuan.node8", "192.168.0.18");
nodeService.addNode(addNode1);
nodeService.addNode(addNode2);
nodeService.addNode(addNode3);
nodeService.addNode(addNode4);
nodeService.addNode(addNode5);
nodeService.addNode(addNode6);
nodeService.addNode(addNode7);
nodeService.addNode(addNode8);

//用于檢查數(shù)據(jù)分布情況
Map<String, Integer> countmap = new HashMap<>();
Node node = null;
for (int i = 1; i <= 100000; i++) {
String key = String.valueOf(i);
node = nodeService.lookupNode(key);
node.cacheString(key, "TEST_VALUE");
String k = node.getIp();
Integer count = countmap.get(k);
if (count == null) {
count = 1;
countmap.put(k, count);
} else {
count++;
countmap.put(k, count);
}

}
System.out.println("初始化數(shù)據(jù)分布情況:" + countmap);
// 正常情況下的去獲取數(shù)據(jù)烛芬,命中率
int hitcount = 0;
for (int i = 1; i <= 100000; i++) {
String key = String.valueOf(i);
node = nodeService.lookupNode(key);
if (node != null) {
String value = node.getCacheValue(key);
if (value != null) {
hitcount++;
}
}
}
double h = Double.parseDouble(String.valueOf(hitcount))/ Double.parseDouble(String.valueOf(100000));
System.out.println("初始化緩存命中率:"+ h);
// 移除一個(gè)節(jié)點(diǎn)
Node addNode9 = new Node("xingchuan.node0", "192.168.0.19");
nodeService.addNode(addNode9);
hitcount = 0;
for (int i = 1; i <= 100000; i++) {
String key = String.valueOf(i);
node = nodeService.lookupNode(key);
if (node != null) {
String value = node.getCacheValue(key);
if (value != null) {
hitcount++;
}
}
}
h = Double.parseDouble(String.valueOf(hitcount))/ Double.parseDouble(String.valueOf(100000));
System.out.println("增加一個(gè)節(jié)點(diǎn)后緩存命中率:"+ h);

運(yùn)行結(jié)果如下:

初始化數(shù)據(jù)分布情況:{192.168.0.11=12499, 192.168.0.12=12498, 192.168.0.13=12500, 192.168.0.14=12503, 192.168.0.15=12500, 192.168.0.16=12502, 192.168.0.17=12499, 192.168.0.18=12499}
初始化緩存命中率:1.0
增加一個(gè)節(jié)點(diǎn)后緩存命中率:0.11012

三隧期、分布式數(shù)據(jù)分布算法——一致性Hash

概述

取模算法的劣勢(shì)很明顯,當(dāng)新增節(jié)點(diǎn)和刪除節(jié)點(diǎn)的時(shí)候赘娄,會(huì)涉及大量的數(shù)據(jù)遷移問(wèn)題仆潮。為了解決這一問(wèn)題,引入了一致性Hash遣臼。
一致性Hash算法的原理很簡(jiǎn)單性置,描述如下:
1、想象有一個(gè)巨大的環(huán)揍堰,比如這個(gè)環(huán)的值的分布可以是 0 ~ 4294967296

Paste_Image.png

2鹏浅、還是在取模算法中的那個(gè)例子,這時(shí)候我們假定我們的4個(gè)節(jié)點(diǎn)通過(guò)一些key的hash屏歹,分布在了這個(gè)巨大的環(huán)上面
3隐砸、用戶數(shù)據(jù)來(lái)了,需要存儲(chǔ)到哪個(gè)節(jié)點(diǎn)呢蝙眶?通過(guò)key的hash季希,得出一個(gè)值r,順時(shí)針找到最近的一個(gè)Node節(jié)點(diǎn)對(duì)應(yīng)的hash值nodeHash幽纷,這次用戶數(shù)據(jù)也就存儲(chǔ)在對(duì)應(yīng)的這個(gè)Node上式塌。

那么問(wèn)題來(lái)了,如果只有4個(gè)節(jié)點(diǎn)友浸,可能會(huì)造成數(shù)據(jù)分布不均勻的情況峰尝,舉個(gè)例子,上圖中的Node3和Node4離的很近尾菇,這時(shí)候境析,Node1的壓力就會(huì)很大了囚枪。如何解決這個(gè)問(wèn)題呢?虛擬節(jié)點(diǎn)能解決這個(gè)問(wèn)題劳淆。

什么是虛擬節(jié)點(diǎn)链沼?
簡(jiǎn)單說(shuō),就是在環(huán)上模擬很多個(gè)不存在的節(jié)點(diǎn)沛鸵,這時(shí)候這些節(jié)點(diǎn)是可以盡可能均勻分布在環(huán)上的括勺,在key的hash后,順時(shí)針找最近的存儲(chǔ)節(jié)點(diǎn)曲掰,存儲(chǔ)完成之后疾捍,集群中的數(shù)據(jù)基本上就分配均勻了。唯一要做的栏妖,必須要維護(hù)一個(gè)虛擬節(jié)點(diǎn)到真實(shí)節(jié)點(diǎn)的關(guān)系乱豆。

一致性Hash的實(shí)現(xiàn)

下面,我們就來(lái)通過(guò)兩個(gè)進(jìn)階吊趾,實(shí)現(xiàn)一個(gè)一致性Hash宛裕。
進(jìn)階一我們不引入虛擬節(jié)點(diǎn),進(jìn)階二我們引入虛擬節(jié)點(diǎn)

一致性Hash實(shí)現(xiàn)论泛,進(jìn)階一揩尸,關(guān)鍵代碼如下

@Override
public void addNode(Node node) {
nodeList.add(node);
long crcKey = hash(node.getIp());
nodeMap.put(crcKey, node);
}

@Override
public Node lookupNode(String key) {
long crcKey = hash(key);
Node node = findValidNode(crcKey);
if(node == null){
return findValidNode(0);
}
return node;
}

/**
* @param crcKey
* @param entrySet
*/
private Node findValidNode(long crcKey) {
Set<Map.Entry<Long, Node>> entrySet = nodeMap.entrySet();
//順時(shí)針找到最近的一個(gè)節(jié)點(diǎn)
for(Map.Entry<Long, Node> entry : entrySet){
long k = entry.getKey();
if(crcKey <= k){
return entry.getValue();
}
}
return null;
}

@Override
public Long hash(String key) {
CRC32 crc = new CRC32();
crc.update(key.getBytes());
return crc.getValue();
}

這里我們發(fā)現(xiàn),計(jì)算key的hash的算法和取模算法例子里是一樣的屁奏,這不是重點(diǎn)岩榆,重點(diǎn)是,在addNode的時(shí)候坟瓢,我們通過(guò)ip地址來(lái)進(jìn)行一次hash勇边,并且丟到了一個(gè)TreeMap里面,key是一個(gè)Long载绿,是可以自動(dòng)排序的粥诫。
在lookupNode的時(shí)候油航,我們是順時(shí)針去找最近的一個(gè)節(jié)點(diǎn)崭庸,如果沒(méi)有找到,數(shù)據(jù)就會(huì)存在環(huán)上順時(shí)針數(shù)第一個(gè)節(jié)點(diǎn)谊囚。

測(cè)試代碼如下:

和取模算法的一樣怕享,唯一不同的,就是把算法實(shí)現(xiàn)的那一行改掉
HashNodeService nodeService = new ConsistentHashNodeServiceImpl();

運(yùn)行結(jié)果如下:

初始化數(shù)據(jù)分布情況:{192.168.0.11=2495, 192.168.0.12=16732, 192.168.0.13=1849, 192.168.0.14=32116, 192.168.0.15=2729, 192.168.0.16=1965, 192.168.0.17=38413, 192.168.0.18=3701}
初始化緩存命中率:1.0
增加一個(gè)節(jié)點(diǎn)后緩存命中率:0.97022

這里我們可以看到镰踏,數(shù)據(jù)分布是不均勻的函筋,同時(shí)我們也發(fā)現(xiàn),某一個(gè)節(jié)點(diǎn)失效了奠伪,對(duì)于緩存命中率的影響跌帐,要比取模算法的場(chǎng)景首懈,要好得多。

一致性Hash的實(shí)現(xiàn)谨敛,進(jìn)階2究履,引入虛擬節(jié)點(diǎn),代碼如圖:

一致性hash添加虛擬節(jié)點(diǎn)

我們?cè)谛略龉?jié)點(diǎn)的時(shí)候块请,每個(gè)真實(shí)節(jié)點(diǎn)對(duì)應(yīng)128個(gè)虛擬節(jié)點(diǎn)

刪除節(jié)點(diǎn)的代碼如下硝逢,對(duì)應(yīng)的虛擬節(jié)點(diǎn)也一并刪掉仑乌。

刪除節(jié)點(diǎn)也刪除虛擬節(jié)點(diǎn)

再次測(cè)試數(shù)據(jù)分布和緩存命中率

測(cè)試代碼不變,運(yùn)行結(jié)果如下:

初始化數(shù)據(jù)分布情況:{192.168.0.11=11610, 192.168.0.12=14600, 192.168.0.13=13472, 192.168.0.14=11345, 192.168.0.15=11166, 192.168.0.16=12462, 192.168.0.17=14477, 192.168.0.18=10868}
初始化緩存命中率:1.0
增加一個(gè)節(jié)點(diǎn)后緩存命中率:0.91204

這時(shí)泥彤,我們發(fā)現(xiàn)數(shù)據(jù)分布的情況已經(jīng)比上面沒(méi)有引入虛擬節(jié)點(diǎn)的情況好太多了。

總結(jié)

我理解一致性Hash就是為了解決在分布式存儲(chǔ)擴(kuò)容的時(shí)候涉及到的數(shù)據(jù)遷移的問(wèn)題卿啡。
但是吟吝,一致性hash中如果每個(gè)節(jié)點(diǎn)的數(shù)據(jù)都很平均,每個(gè)都是熱點(diǎn)颈娜,在數(shù)據(jù)遷移的時(shí)候爸黄,還是會(huì)有比較大數(shù)據(jù)量遷移。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末揭鳞,一起剝皮案震驚了整個(gè)濱河市炕贵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌野崇,老刑警劉巖称开,帶你破解...
    沈念sama閱讀 222,627評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異乓梨,居然都是意外死亡鳖轰,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門扶镀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蕴侣,“玉大人,你說(shuō)我怎么就攤上這事臭觉±ト福” “怎么了?”我有些...
    開封第一講書人閱讀 169,346評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵蝠筑,是天一觀的道長(zhǎng)狞膘。 經(jīng)常有香客問(wèn)我,道長(zhǎng)什乙,這世上最難降的妖魔是什么挽封? 我笑而不...
    開封第一講書人閱讀 60,097評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮臣镣,結(jié)果婚禮上辅愿,老公的妹妹穿的比我還像新娘智亮。我一直安慰自己,他們只是感情好点待,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評(píng)論 6 398
  • 文/花漫 我一把揭開白布鸽素。 她就那樣靜靜地躺著,像睡著了一般亦鳞。 火紅的嫁衣襯著肌膚如雪馍忽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,696評(píng)論 1 312
  • 那天燕差,我揣著相機(jī)與錄音遭笋,去河邊找鬼。 笑死徒探,一個(gè)胖子當(dāng)著我的面吹牛瓦呼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播测暗,決...
    沈念sama閱讀 41,165評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼央串,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了碗啄?” 一聲冷哼從身側(cè)響起质和,我...
    開封第一講書人閱讀 40,108評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎稚字,沒(méi)想到半個(gè)月后饲宿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,646評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡胆描,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評(píng)論 3 342
  • 正文 我和宋清朗相戀三年瘫想,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昌讲。...
    茶點(diǎn)故事閱讀 40,861評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡国夜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出短绸,到底是詐尸還是另有隱情车吹,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布鸠按,位于F島的核電站礼搁,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏目尖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評(píng)論 3 336
  • 文/蒙蒙 一扎运、第九天 我趴在偏房一處隱蔽的房頂上張望瑟曲。 院中可真熱鬧饮戳,春花似錦、人聲如沸洞拨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)烦衣。三九已至歹河,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間花吟,已是汗流浹背秸歧。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留衅澈,地道東北人键菱。 一個(gè)月前我還...
    沈念sama閱讀 49,287評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像今布,于是被迫代替她去往敵國(guó)和親经备。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評(píng)論 2 361

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