ElasticSearch

基本概念

Near Realtime(NRT进实时)

Elasticsearch是一个进实时的搜索平台,这意味着您从索引一个文档开始知道它可以被查询时会有轻微的延迟时间(通常为一秒)。

Cluster(集群)

cluster是一个或者多个节点的集合,他们一起保存数据并且提供所有的节点联合索引以及搜索功能。集群存在一个唯一的名字身份且默认为“elasticsearch”。这个名字非常重要,因为如果节点安装时通过他自己的名字加入到一个集群中的话,那么一个节点只能是一个集群中的一部分。

Node(节点)

node(节点)是一个单独的服务器,他是集群中的一部分,存储数据,参与集群中的索引和搜索的功能。想一个集群一样,一个节点通过一个在他启动是默认分配的uuid(通用唯一标识符)名称来识别。如果不想使用默认的名称,您也可以自定义任务节点的名称。这个名字是要识别网络中的服务器对应,这在集群的节点管理的目的是很重要的。

Index(索引)

动词,相当于MySQL中的insert;

名词,相当于MySQL中的Database

Type(类型)

在 Index(索引)中,可以定义一个或多个类型。

类似于MySQL中的Table;每一种类型的数据放在一起;

Document(文档)

保存在某个索引(Index)下,某种类型(Type)的一个数据(Document),文档是JSON格式的,Document就像是MySQL中的某个Table里面的内容

Shards&Replicas(分片&副本)

索引可以存储大量数据,可以超过单个节点的硬件限制。例如,十亿个文档占用了 1TB 的磁盘空间的单个索引可能不适合放在单个节点的磁盘上,并且从单个节点服务请求会变得很慢。

为了解决这个问题,Elasticsearch 提供了把 Index(索引)拆分到多个 Shard(分片)中的能力。在创建索引时,您可以简单的定义 Shard(分片)的数量。每个 Shard本身就是一个 fully-functional(全功能的)和独立的 “Index(索引)”,(Shard)它可以存储在集群中的任何节点上。

Sharding(分片)非常重要两个理由是 :

1)水平的拆分/扩展。

2)分布式和并行跨 Shard 操作(可能在多个节点),从而提高了性能/吞吐量。

每个索引可以被拆分成多个分片,一个索引可以设置 0 个(没有副本)或多个副本。开启副本后,每个索引将有主分片(被复制的原始分片)和副本分片(主分片的副本)。分片和副本的数量在索引被创建时都能够被指定。在创建索引后,您也可以在任何时候动态的改变副本的数量,但是不能够改变分片数量。


初步检索

_cat

GET /_cat/nodes: 查看所有的节点

GET /_cat/health:查看es健康状况

GET /_cat/master:查看主节点

GET /_cat/indeices:查看所有的索引

索引一个文档

PUT customer/external/1
{
"name":"John Doe"
}

PUT和POST都可以

POST只是新增

PUT可以新增可以修改

查询文档

GET customer/external/1

更新文档

POST customer/external/1/_update
{
  "doc":{
     "name": "John Doew"
  }
}
或者
PUT customer/external/1
{
  "name": "John Doe"
}
或者
POST customer/external/1
{
  "name": "John Doe2"
}
POST PUT
不同点 post操作会对比源文档,如果相同不会有什么操作,文档的version不会增加 PUT操作不会对比源文档,总是会将数据重新保存并增加version版本

更新的同时增加属性

POST customer/external/1/_update
{
  "doc": { "name": "Jane Doe", "age": 20 }
}

简单脚本更新

POST customer/external/1/_update
{
  "script" : "ctx._source.age += 5"
}

删除文档&索引

DELETE customer/external/1
DELETE customer 

bulk批量API

POST customer/external/_bulk
{"index":{"_id":"1"}}
{"name": "John Doe" }
{"index":{"_id":"2"}}
{"name": "Jane Doe" }
语法格式:
{ action: { metadata }}\n
{ request body        }\n

{ action: { metadata }}\n
{ request body        }\n

bulk API以此顺序执行所有的action(动作)如果一个单个的动作因为任何原因失败,它将继续处理他后面的剩余的动作。当bulk API返回时,它将提供每个动作的状态(与放松的顺序相同),所以您可以检查是否一个指定的动作是不是失败了。

样本测试数据

官方提供的样本

POST bank/account/_bulk

测试数据

进阶检索

检索信息

一切检索从_search开始

响应结果解释

名词 解释
took- Elasticsearch 执行搜索的时间(毫秒)
time-out 搜索是否超时
_shards 多少个分片被搜索了,以及统计了成功/失败的搜索分片
hits 搜索的结果
hits.total 搜索的结果
hits.hits 实际的搜索结果数组(默认为前10的文档)
sort 结果的排序key(键)
score和max_score 相关性得分和最高得分(全文检索的时候使用)

uri+请求体进行搜索

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "account_number": {
        "order": "desc"
      }
    }
  ]
}

我们post一个json风格的查询到_search API。需要了解,一旦搜索结果被返回,es就完成了这次请求,并且不会维护任何服务端的资源或者结果的cursor(游标)

Query DSL

基本语法结构

es提供了一个可以执行查询的JSON分割的DSL(domain-specific language领域特定语言)。这个被称为Query DSL。该查询语句非常全面,并且刚开始的时候会感觉有点复杂,真正学好他的方法是从一些基础的示例开始的。

  • 一个查询语句的典型结构
{
    QUERY_NAME: {
        ARGUMENT: VALUE,
        ARGUMENT: VALUE,...
    }
}
  • 如果是针对某个字段,那么他的结构如下
{
    QUERY_NAME: {
        FIELD_NAME: {
            ARGUMENT: VALUE,
            ARGUMENT: VALUE,...
        }
    }
}
GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0,
  "size": 5,
  "sort": [
    {
      "account_number": {
        "order": "desc"
      }
    }
  ]
}
  • query定义如何查询
  • match_all查询类型[代表查询所有],es中可以在query中组合非常多的查询类型完成复杂的查询
  • 除了query参数之外,我们也可以传递其他的参数以改变查询的结果,如sort,size
  • from+size限定,完成分页功能
  • sort排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准。

返回部分字段

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0,
  "size": 5,
  "_source": ["age","balance"]
}

match【匹配查询】

  • 基本类型(非字符串),精确匹配
GET bank/_search
{
  "query": {
    "match": {
      "account_number": "20"
    }
  }
}
match返回account_number=20的
  • 字符串,全文检索
GET bank/_search
{
  "query": {
    "match": {
      "address": "mill"
    }
  }
}

最终查询出address中包含mill单词的所有记录
match当搜索字符串类型的时候,会进行全文检索,并且每条记录都有相关性得分

  • 字符串,多个单词(分词+全文检索)
GET bank/_search
{
  "query": {
    "match": {
      "address": "mill road"
    }
  }
}

最终查询出address中包含mill或者road或者mill road的所有记录,并给出相关性得分

match_phrase【短语匹配】

将需要匹配的值当成一个整体单词(不分词)进行检索

GET bank/_search
{
  "query": {
    "match_phrase": {
      "address": "mill road"
    }
  }
}

查出address中包含mill road的所有记录,并给出相关性得分

multi_match【多字段匹配】

GET bank/_search
{
  "query": {
    "multi_match": {
      "query": "mill",
      "fields": ["state","address"]
    }
  }
}

state或者address包含mill

bool【复合查询】

bool用来做复合查询:

复合语句可以合并任何的查询语句,包括复合语句,这意味着复合语句可以嵌套,可以表达非常复杂的逻辑。


must: 必须达到must列举的所有的条件

GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "address": "mill" } },
        { "match": { "gender": "M" } }
      ]
    }
  }
}

should:应该达到should列举的条件,如果达到会增加评分,不会改变查询的结果。如果query中只有should且只有一种匹配规则,那么should的条件就会被作为默认的匹配条件而去改变查询的结果。

GET bank/_search
{
  "query": {
    "bool": {
      "must": [ 
{ "match": { "address": "mill" } },
        { "match": { "gender": "M" } }
      ],
      "should": [
        {"match": { "address": "lane" }}
      ]
    }
  }
}

must_not必须不是指定的情况

GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "address": "mill" } },
        { "match": { "gender": "M" } }
      ],
      "should": [
        {"match": { "address": "lane" }}
      ],
      "must_not": [
        {"match": { "email": "baluba.com" }}
      ]
    }
  }
}

address包含mill,并且gender是M,如果address里面有lane最好不过,但是email必须不包含baluba.com

filter【结果过滤】

并不是所有的查询都需要产生分数,特别是哪些仅仅用于"filtering"(过滤)的文档。为了不计算分数,es会自动检查场景并且优化查询的执行。

GET bank/_search
{
  "query": {
     "bool": {
       "must": [
         {"match": { "address": "mill"}}
       ],
       "filter": {
         "range": {
           "balance": {
             "gte": 10000,
             "lte": 20000
           }
         }
       }
     }
  }
}

aggregations(执行聚合)

聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于SQL中的分组和sql中的聚合函数。

  • 在es中,您有执行搜索返回命中结果,并且同时返回聚合的结果,把一个响应中所有命中结果分隔开的能力。
  • 这是非常强大且有效的,您可以执行查询和多个聚合,并且在一次使用中得到各自的返回结果,使用一次简介和简化的API来避免网络往返。

搜索address中包含mill的所有人的年龄分布以及平均年龄,但不显示这些人的详情

GET bank/_search
{
  "query": {
    "match": {
      "address": "mill"
    }
  },
  "aggs": {
    "group_by_state": {
      "terms": {
        "field": "age"
      }
    },
    "avg_age": {
      "avg": {
        "field": "age"
      }
    }
  },
  "size": 0
}
size:0  不显示搜索数据
aggs:执行聚合。聚合语法如下
"aggs": {
    "aggs_name这次聚合的名字,方便展示在结果集中": {
      "AGG_TYPE聚合的类型(avg,term,terms)": {}
    }
  }

复杂:

按照年龄聚合,并且请求这些年龄段的这些人的平均薪资

GET bank/account/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "age_avg": {
      "terms": {
        "field": "age",
        "size": 1000
      },
      "aggs": {
        "banlances_avg": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  }
  ,
  "size": 1000
}

Mapping

字段类型

  • 核心类型

  • 字符串(String)

    • text
    • keyword
  • 数字类型

    • long
    • Integer
    • short
    • byte
    • double
    • float
    • ...
  • 日期类型

    • date
  • 布尔类型

    • boolean
  • 二进制类型

    • binary
  • 复合类型

    • 数组类型
      • Array支持不针对特定的类型
    • 对象类型
      • Object用于单JSON对象
    • 嵌套类型
      • nested 用于JSON对象数组
  • 地理类型

    • 地理坐标
      • geo_point用于描述经纬度坐标
    • 地理图形
      • geo_shape用于描述复杂形状
  • 特定类型

    • IP类型
      • ip用于描述ipv4和ipv6地址
    • 补全类型
      • completion提供自动完成提示
    • 令牌计数类型
      • token_count用于统计字符串中的词条数量
    • 附件类型
      • 支持附件如office
    • 抽取类型
      • 支持特定领域语言的查询

映射

Mapping是用来定义一个文档,以及他所包含的属性如何存储和索引的

  • 哪些字符串应该被看做全文本属性

  • 哪些属性包含数字,日期或者地理位置

  • 文档中的所有属性是否都能被索引

  • 日期的格式

  • 自定义映射规则来执行动态添加属性

查询mapping信息

GET bank/_mapping

修改mapping信息

需要在创建索引的时候指定映射
https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html 
PUT my_index
{
  "mappings": {
    "user": {
      "_all":       { "enabled": false  },
      "properties": {
        "title":    { "type": "text"  },
        "name":     { "type": "text"  },
        "age":      { "type": "integer" } 
      }
    },
    "blogpost": {
      "_all":       { "enabled": false  },
      "properties": {
        "title":    { "type": "text"  },
        "body":     { "type": "text"  },
        "user_id":  {
          "type":   "keyword"
        },
        "created":  {
          "type":   "date",
          "format": "strict_date_optional_time||epoch_millis"
        }
      }
    }
  }
}

分词

一个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立的单词),然后输出 tokens 流。

倒排索引

安装ik分词器

测试分词器

集群原理

集群安装

es1配置文件
# 开启跨域,为了让es-head可以访问
http.cors.enabled: true
http.cors.allow-origin: "*"

# 集群的名称
cluster.name: elasticsearch
# 节点的名称
node.name: es1
# 指定该节点是否有资格被选举成为master节点,默认是true,es是默认集群中的第一台机器为master,如果这台机挂了就会重新选举master
node.master: true
# 允许该节点存储数据(默认开启)
node.data: true
# 允许任何ip访问
network.host: 0.0.0.0
# 通过这个ip列表进行节点发现,我这里配置的是各个容器的ip
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300","192.168.192.133:9301","192.168.192.133:9302","192.168.192.133:9301"]
#如果没有这种设置,遭受网络故障的集群就有可能将集群分成两个独立的集群 – 导致脑裂 - 这将导致数据丢失
discovery.zen.minimum_master_nodes: 2
es2-es3配置文件
-----------------------es2-----------------------
http.cors.enabled: true
http.cors.allow-origin: "*"
cluster.name: elasticsearch
node.name: es2

network.host: 0.0.0.0
node.master: true
node.data: true

discovery.zen.ping.unicast.hosts: ["127.0.0.1","172.17.0.2","172.17.0.4","172.17.0.5"]
discovery.zen.minimum_master_nodes: 2

-----------------------es3-----------------------
http.cors.enabled: true
http.cors.allow-origin: "*"
cluster.name: elasticsearch
node.name: es3

network.host: 0.0.0.0
node.master: true
node.data: true

discovery.zen.ping.unicast.hosts: ["127.0.0.1","172.17.0.2","172.17.0.4","172.17.0.5"]
discovery.zen.minimum_master_nodes: 2
集群化实例启动
实例一:
docker run --name es1 -p 9200:9200 -p 9300:9300 \
-e ES_JAVA_OPTS="-Xms256m -Xmx256m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data -d elasticsearch:5.6.11

实例二:
docker run --name es2 -p 9201:9200 -p 9301:9300 \
-e ES_JAVA_OPTS="-Xms256m -Xmx256m" \
-v /mydata/elasticsearch2/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch2/data:/usr/share/elasticsearch/data -d elasticsearch:5.6.11

实例三:
docker run --name es3 -p 9202:9200 -p 9302:9300 \
-e ES_JAVA_OPTS="-Xms256m -Xmx256m" \
-v /mydata/elasticsearch3/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch3/data:/usr/share/elasticsearch/data -d elasticsearch:5.6.11


需要修改linux的进程数限制
vi /etc/sysctl.conf
vm.max_map_count=655360
sysctl -p

集群、分片原理

单节点

一个运行中的 Elasticsearch 实例称为一个 节点

集群

由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力

  • 有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。
  • 当一个节点被选举成为 主 节点时, 它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。
  • 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。
  • 任何节点都可以成为主节点。
  • 作为用户,我们可以将请求发送到 集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。

集群健康

  • green(所有的主分片和副本分片都正常运行)
  • yellow(所有的主分片都正常运行,单不是所有的副本分片都正常运行)
  • red(有主分片没有能正常运行)

添加故障转移

  1. 当集群中只有一个节点在运行时,意味着会有一个单点故障问题--没有冗余。只需要再启动一个节点即可防止数据丢失。
  2. 当第二个节点加入到集群后,3个副本分片将会分配到这个节点上,当集群中的任何一个节点出现问题的时候,数据都完好无损。
  3. 所有新近被索引的文档都将会保存到主分片上,然后被并行复制到对应的副本分片上,这就保证了我们既可以从主分片又可以从副本分片获得文档。

水平扩容

拥有三个节点的集群--为了分散负载而对分片进行重新分配

  • 读操作,可以同时被主分片或者副本分片所处理,所有当你拥有越多的副本分片时,也将拥有越高的吞吐量
  • 运行中的集群上是可以动态调整副本分片的数目的,我们可以按需伸缩集群。可以把副本数从默认的1增加到2
PUT /blogs/_settings
{
   "number_of_replicas" : 2
}

应对故障

  1. 我们关闭的节点是一个主节点。而集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点
  2. 在我们关闭Node1的同时也失去了主分片1和2,并且在缺失主分片的时候索引页不能正常工作
  3. 幸运的是,在其他节点上存在着这两个主分片的完整副本,所以新的主节点立即将其他节点上对应的副本分片提升为主分片,这个提升的过程是瞬间发生的

脑裂问题

脑裂问题是什么?

同一个集群中的不同节点,对于集群的状态,有了不一样的理解。集群中不同的节点对于master的选择出现了分歧,出现了多个master竞争,导致主分片和副本的识别也发生了分歧,对一些分歧中的分片标识为了坏片。

可能的原因

  • 网络问题:集群间的网络延迟导致一些节点访问不到master,认为master挂掉了从而选举出新的master,并对master上的分片和副本标红,分配新的主分片
  • 节点负载:主节点的角色既为master又为data,访问量较大时可能会导致ES停止响应造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
  • 内存回收:data节点上的ES进程占用的内存较大,引发JVM的大规模内存回收,造成ES进程失去响应。

解决方案

  • 减少误判 :调大节点的响应时间,默认为3秒,调至6秒

  • 选举触发 discovery.zen.minimum_master_nodes:(默认是1)

    这个参数控制的是,一个节点需要看到的具有master节点资格的最小数量,然后才能在集群中做操作。官方的推荐值是(N/2)+1,其中N是具有master资格的节点的数量(我们的情况是3,因此这个参数设置为2,但对于只有2个节点的情况,设置为2就有些问题了,一个节点DOWN掉后,你肯定连不上2台服务器了,这点需要注意)。

    增大该参数,当该值为2时,我们可以设置master的数量为3,这样,挂掉一台,其他两台都认为主节点挂掉了,才进行主节点选举。

  • 角色分离:即master节点与data节点分离,限制角色

主节点配置

node.master: true ##作为master节点

node.data: false ##不作为存储数据节点

从节点配置为:

node.master: false

node.data: true

实际的解决方案

三台物理机上搭建六个ES节点,三个data节点,三个master节点

分析

  1. 角色分离后,当集群中某一台节点的master进程意外挂掉了,或者因负载过高停止响应,终止掉的master进程很大程度上不会影响到同一台机器上的data进程,即减小了数据丢失的可能性。
  2. discovery.zen.minimum_master_nodes设置成了2(3/2+1)当集群中两台机器都挂了或者并没有挂掉而是处于高负载的假死状态时,仅剩一台备选master节点,小于2无法触发选举行为,集群无法使用,不会造成分片混乱的情况。

以上的解决方法只能是减缓这种现象的发生,并没有从根本上杜绝,但是毕竟是有帮助的

JestClient

SpringBoot整合

<!-- https://mvnrepository.com/artifact/io.searchbox/jest -->
<dependency>
    <groupId>io.searchbox</groupId>
    <artifactId>jest</artifactId>
    <version>5.3.4</version>
</dependency> 
<!-- https://mvnrepository.com/artifact/org.elasticsearch/elasticsearch -->
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>5.6.11</version>
</dependency>

配置

spring:
  elasticsearch:
    jest:
      uris: http://192.168.159.130:9200, http://192.168.159.130:9201
      username: elastic
      password: 123456
      read-timeout: 20000 #读取超时    
      connection-timeout: 20000 #连接超时

使用

@Autowired
    JestClient jestClient;

    public void search(){
       //先定义一个action(增删改查),例如search       
       Search search = new Search.Builder(query).addIndex(index).build();
       //执行action,返回结果              
       SearchResult result = jestClient.execute(search);
       //处理结果集
       ...........省略
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,200评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,526评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,321评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,601评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,446评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,345评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,753评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,405评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,712评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,743评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,529评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,369评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,770评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,026评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,301评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,732评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,927评论 2 336

推荐阅读更多精彩内容