NoSQL: MongoDB (文档存储)

简介

  • NoSQL : 全名为Not Only SQL, 指的是非关系型的数据库
  • 随着访问量上升, 网站的数据库性能出现了问题, 于是nosql被设计出来
  • 优点 / 缺点
    • 优点:
      • 高可扩展性
      • 分布式计算
      • 低成本
      • 架构的灵活性, 半结构化数据
      • 没有复杂的数据
    • 缺点:
      • 没有标准化
      • 有限的查询功能
      • 最终一致是不直观的程序

Mongo DB

基本操作
  • 一) 简介:

    • MongoDB是一个基于分布式文件存储的NoSQL数据库
    • 由C++语言编写, 运行稳定, 性能高
    • 旨在为WEB应用提供可扩展的高性能数据存储解决方案
    • Mongo DB特点:
        1. 模式自由: 可以把不同结构的文件存储在同一个数据库里
        1. 面向集合的存储: 适合存储JSON风格文件的形式
        1. 完整的索引支持: 对任何属性可索引
        1. 复制和高可用性: 支持服务器之间的数据复制, 支持主-从模式及服务器之间的相互复制. 复制的只要目的是提供用于及自动故障转移
        1. 自动分片: 支持云级别的伸缩性: 自动分片功能支持水平的数据库集群, 可动态添加额外的机器
        1. 丰富的查询: 支持丰富的查询表达方式, 查询指令使用JSON形式的标记, 可轻易查询文档中的内嵌的对象及数组
        1. 快速就地更新: 查询优化器会分析查询表达式, 并生成一个高效的查询计划
        1. 高效的传统存储方式: 支持二进制数据机大型对象(照片)
  • 二) 基本操作:

    • 三元素 : 数据库, 集合, 文档
      • 集合就是关系数据库中的表
      • 文档对应着关系数据库中的行
    • 文档 : 就是一个对象, 由键值对构成, 是json的扩展Bson形式
    • 集合 : 类似于关系数据库中的表, 储存多个文档, 结构不固定, 如可以存储文档在集合中
    • 数据库 : 是一个集合的物理容器, 一个数据库中可以包含多个文档
    • 一个服务器通常有多个数据库
  • 三) 环境安装(Ubuntu)

    • 1> 下载mongodb的版本(偶数稳定版, 奇数开发版)
    • 2> 解压: tar -zxvf mongodb-linux-x86_64-ubuntu1604-3.4.0.tgz
    • 3> 移动到/usr/local目录下面: sudo mv - rmongodb-linux-x86_64-ubuntu1604-3.4.0/ /usr/local/mongodb
    • 4> 将可执行文件添加到PATH路径中: export PATH=/usr/local/mongo/bin:$PATH
  • 四) 服务端maogod

    • 配置文件在/etc/mongd.conf
    • 默认端口27017
    • 启动: sudo service mongod start
    • 停止: sudo service mongod stop
    • 重启: sudo service mongod restart
  • 五) 集合操作

    • 1> 集合创建
      • 语法: db.createCollection(name, options)
        • name : 是要创建的集合名称
        • options : 是一个文档, 用于指定集合的配置
        • 选项参数是可选的, 所以只要指定的集合名称
        • 例子 :
          • db.createCollection("stu")
          • db.createCollection("stu", { capped : true, size : 10})
            • capped : 默认值为false表示不设置上限, 值为true表示设置上限
            • size : 当capped值为true的时候, 需要指定此参数, 表示上限大小, 当文档达到上限时, 会将之前的数据覆盖, 单位为字节
    • 2> 查看当前数据库的集合
      • 语法 : show collections
    • 3> 删除
      • 语法 : db.集合名称.drop()
  • 六) 数据类型

    • MongoDB中差用的集中数据类型:

      • ObjectID : 文档ID
      • String : 字符串, 最常用, 必须是有效的UTF-8
      • Boolean : 存储一个布尔值, true/false
      • Integer : 整数可以是32位或64位, 这取决于服务器
      • Double : 存储浮点值
      • Arrays : 数组或列表, 多个值存储到一个键
      • Object : 用于嵌入式文档, 即一个职为一个文档
      • Null : 存储Null值
      • Date : 存储当前日期或时间的UNIX时间格式
    • object id :

      • 每个文档都有一个属性为: _id, 保证每个文档的唯一性
      • 可以自己去设置_id插入文档
      • 如果没有提供, 那么MongoDB为每个文档提供了一个独特的_id, 类型为objectID
      • objectID是一个12字节的十六进制数
        • 前4个字节为当前时间戳
        • 接下来3个字节的机器ID
        • 接下来的2个字节中MongoDB的服务进程id
        • 最后3个字节是简单的增量值
  • 七) 数据操作

  • 1> 插入

    • 语法 : db.集合名称.insert(document)
    • 插入文档时, 如果不指定_id参数, MongoDB会为文档分配一个唯一的ObjectId
      • db.stu.insert({name:'jack', gender:1})
      • s1={_id:'20170202',name:'hr'}
        s1.gender=0
        db.stu.insert(s1)
        
  • 2> 简单查询

    • 语法 : db.集合名称.find()
  • 3> 更新

    • 语法 :
      db.集合名称.update(
          <query>,
          <update>,
          {mulit: <boolean>}
      )
      
      • 参数query : 查询条件, 类似于sql语句update中where部分
      • 参数update : 更新操作符, 类似于sql语句update中set部分
      • 参数mulit : 可选, 默认是false, 表示只更新找到的第一条记录, 值为true表示把满足条件的文档全部更新
      • 例子 :
        • 全文档更新(之前的数据全部删除): db.stu.update({name : 'hr'}, {name : 'erik'})
        • 指定属性更新, 通过操作符 $set
            db.stu.insert({name: 'jack', gender:0})
            db.stu.update({name: 'tom'}, {$set:{name: 'lucky'}})
          
        • 修改多条匹配到的数据: db.stu.update({}, {$set:{gender:0}}, {multi:true})
  • 4> 保存

    • 语法 : db.集合名称.save(document)
    • 如果文档_id已经存在则修改, 如果文档_id不存在则添加
    • 例子 :
      • db.stusave({_id:'20170202', 'name':'leo', gender:1})
      • db.stusave({_id:'20170202', 'name':'leo'})
  • 5> 删除

    • 语法 :
      db.集合名称.remove(
          <query>,
          {
              justOne: <boolean>
          }
      )
      
      • 参数query : 可选, 删除的文档的条件
      • 参数justOne : 可选, 如果设为true或1, 则只删除一条, 默认false, 表示删除多条
      • 例子 :
        • 只删除匹配到的第一条: db.stu.remove({gender:0}, {justOne:true})
        • 全部删除 : db.stu.remove({})
    • 6> 关于size的示例
      • 当超过size的时候, 会覆盖最老的数据
  • 七) 数据查询

    • 基本查询

      • 方法find() : 查询 db.集合名称.find({条件文档})
      • 方法findOne() : 查询, 只返回第一个 db.集合名称.findOne({条件文档})
      • 方法pretty() : 将结果格式化 db.集合名称.find({条件文档}).pretty()
    • 比较运算符

      • 小于 : $lt
      • 小于或等于 : $lte
      • 大于 : $gt
      • 大于或等于 : $gte
      • 不等于 : $ne
      • 例子 :
        • 查询name等于old的学生 : db.stu.find({name: 'old'})
        • 查询年龄大于或等于18的学生 : db.stu.find({age: {$gte:18}})
    • 逻辑运算符

      • 逻辑与 : 默认是逻辑与的关系
      • 逻辑或 : 使用$for
        • 查询年龄大于18, 或性别为0的学生 : `` db.stu.find({$for:[{age:{$gt:18}}, {gender:1}]})
      • or 和 and 一起使用:
      • 查询年龄大于18, 或性别为0的学生, 并且学生的姓名为old : `` db.stu.find({$for:[{age:{$gt:18}}, {gender:1}], name:'old'})
    • 范围运算符

      • 使用$in, $nin 判断是否在某个范围内
      • 查询年龄为18, 28的学生 : db.stu.find({age:{$in:[18,28]}})
    • 支持正则表达式

      • 使用//或者$regex编写正则表达式
      • 例子 : 查询姓黄的学生
          db.stu.find({name:/^黄/})
          db.stu.find({name:{name: {$regex:'/^黄'}}})
        
    • 自定义查询

      • 使用$where后面写一个函数, 返回满足条件的数据
      • 查询年龄大于30的学生: db.stu.find({$where:function(){return this.age>20}})
  • 八) 高级查询

    • limit() : 用于读取指定数量的文档

      • 语法: db.集合名称.find().limit(NUMBER)
      • 参数NUMBER: 表示要获取文档的条数
      • 如果没有制定参数则显示集合中的所有文档
      • 例子: 查询2条学生信息 db.stu.find().limit(2)
    • skip() : 用于跳过指定数量的文档

      • 语法 : db.集合名称.find().skip(NUMBER)
      • 参数NUMBER: 表示跳过的记录条数, 默认值为0
      • 例子 : 查询从第三条开始的学生信息 db.stu.find().skip(2)
    • 投影:

      • 在查询的返回结果中, 只选择必要的字段, 而不是选择一个文档的整个字段
      • 语法 : db.集合名称.find({}, {字段名称:1, ...}) -> 参数为字段与值, 值为1表示显示, 为0表示不显示
      • 特殊: 对于_id列默认是显示的, 如果不显示需要明确设置为0
    • 排序:

      • 方法sort(): 用于对结果集进行排序
      • 语法 : db.集合名称.find().sort({字段:1,...}) -> 参数1为升序, -1为降序
      • 例子 : db.stu.find().sort({gender:-1,age:1})
    • 统计个数:

      • 方法count(): 用于统计结果集中文档条数
      • 语法: db.集合名称.find({条件}).count()
      • 例子 :
        • 统计男生人数: db.stu.find({gender:1}).count()
        • 统计年龄大于20的男生人数: ``db.stu.find({age{$gt:20}, gender:1})
    • 消除重复:

      • 方法distinct() : 对数据进行去重
      • 语法 : db.集合名称.distinct('去重字段', {条件})
      • 例子 : 查找年龄大于18的性别(去重) -> db.stu.distinct('gender', {age:{$gt;20}})
高级操作

聚合, 主从复制, 分片, 备份与恢复, MR

  • 一) 聚合(aggregate)

    • 聚合主要用于计算数据, 类似sql中的sum(), avg()

    • 语法: db.集合名称.aggregate([{管道: {表达式}}])

    • 管道

      • 管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的输入 : ps ajx | grep mongo
      • 在mongodb中, 管道具有同样的作用, 文档处理完毕后, 通过管道进行下一次处理
      • 常用管道
        • $group : 将集合中的文档分组, 可用于统计结果
        • $match : 过滤数据, 只输出符合条件的文档
        • $project : 修改输入文档的结构, 如重命名, 增加, 删除字段, 创建计算结果
        • $sort : 将输入文档排序后输出
        • $limit : 限制聚合管道返回的文档数
        • $skip : 跳过制定数量的文档, 并返回余下的文档
        • $unwind : 将数组类型的字段进行拆分
    • 表达式

      • 处理输入文档并输出
      • 语法 : 表达式:'$列名'
      • 常用表达式 :
        • $sum : 计算综合, $sum:1同count表示技术
        • $avg : 计算平均值
        • $max : 获取最大值
        • $push : 在结果文档中插入值到一个数组中
        • $first : 根据资源文档的排序获取第一个文档数据
        • $last : 根据资源文档的排序获取最后一个文档数据
    1. $group:
    • 1> group

      • 将集合中的文档分组, 可用于统计结果
      • _id表示分组的依据, 使用某个字段的格式为'$字段'
      • 例子 : 统计男生, 女生的总人数
        db.stu.aggregate({
          {$group:
            {
              _id : '$gender',
              counter : {$sum:1}
            }
          }
        })
        
    • 2> Group by null

      • 将集合中所有文档分为一组
      • 例子 : 求学生总人数, 平均年龄
        db.stu.aggregate({
          {$group:
            {
              _id : null,
              counter : {$sum:1}
            }
          }
        })
        
    • 3> 透视数据($push)

      • 例子 : 统计学生性别及学生姓名
        db.stu.aggregate({
          {$group:
            {
              _id : '$gender',
              name : {$push:'$name'}
            }
          }
        })
        
      • 使用$$ROOT可以将文档内容加入到结果集的数组中, 代码如下:
        db.stu.aggregate({
          {$group:
            {
              _id : '$gender',
              name : {$push:'$$ROOT'}
            }
          }
        })
        
    1. $match:
    • 用于过滤数据, 只输出符合条件的文档
    • 使用MongoDB的标准查询操作
    • 例子:
      • 查询年龄大于20的学生
          db.stu.aggregate([
            {$match:{age:{$gt:20}}}
          ])
        
      • 查询年龄大于20的男生, 女生人数
          db.stu.aggregate([
            {$match:{age:{$gt:20}}},
            {$group:{_id:'gender', counter:{$sum:1}}}
          ])
        
    1. $project:
    • 修改输入文档的结构, 如重命名, 增加, 删除字段, 创建计算结果
    • 例子:
      • 查询学生的年龄, 姓名
          db.stu.aggregate([
            {$project:{_id:0, name:1, age:1}}
          ])
        
      • 查询男生, 女生人数, 输出人数
          db.stu.aggregate([
            {$group:{_id:'$gender', counter:{$sum:1}}},
            {$project:{_id:0, counter:1}}
          ])
        
    1. $sort
    • 将输入文档排序后输出
    • 例子:
      • 查询学生信息, 按年龄升序
          db.stu.aggregate([
            {$sort:{age:1}}
          ])
        
      • 查询男生, 女生人数, 按人数降序
          db.stu.aggregate([
            {$group:{_id:'$gender', counter:{$sum:1}}},
            {$sort:{counter:-1}}
          ])
        
    1. $limit
    • 限制聚合管道返回的文档数
    • 例子: 查询2条学生信息 -> db.stu.aggregate([{$limit:2}])
    1. $skip
    • 跳过制定数量的文档, 并返回余下的文档
    • 例子:
      • 查询从第三条开始的学生信息 -> db.stu.aggregate([{$skip:2}])
      • 查询男生, 女生人数, 按人数升序, 取第二条数据(先写skip, 后limit)
          db.stu.aggregate([
            {$group:{_id:'$gender', counter:{$sum:1}}},
            {$sort:{counter:-1}},
            {$skip:1}
            {$limit:1}
          ])
        
    1. $unwind
    • 将文档中的某一个数组类型字段拆分成多条, 每条包含数组中的一个值

    • 语法1

      • 对某字段值进行拆分 : db.集合名称.aggregate([{$unwind:'$字段名称'}])
      • 构造数据 : db.t2.insert({_id:1, item:'t-shirt', size:['S', 'M', 'L']})
      • 查询 : db.t2.aggregate([{$unwind:'$size'}])
    • 语法2

      • 对某字段值进行拆分

      • 处理空数组, 非数组, 无字段, null情况

          db.inventory.aggregate([
            {$unwind:{
                path:'$字段名称',
                preserveNullAndEmptyArrays:<boolean>#防止数据丢失
            }
          ])
        
      • 构造数据

          db.t3.insert([
            {_id:1, item:'a', size:['S', 'M', 'L']},
            {_id:2, item:'b', size:[]},
            {_id:3, item:'c', size:'M'},
            {_id:4, item:'d'},
            {_id:5, item:'t', size:null}
            }
          ])
        
      • 使用语法1查询 : db.t3.aggregate([{$unwind:'$size'}])

        • 查看查询结果, 发现对于空数组, 无字段, null的文档, 都被丢弃了
        • 如何防止数据不丢失 :
          db.t3.aggregate([
            {$unwind:{path:'$size', preserveNullAndEmptyArrays:true}}
          ])
        
  • 二) 索引

    • 1> 创建大量的数据

      • 向集合中插入10万条文档
        for(i=0;i<100000;i++){
            db.t1.insert({name: 'test'+i,age:i})
        }
      
    • 2> 数据查找性能分析

      • 查找姓名为'test10000'的文档 : db.t1.find({name:'test10000'})
      • 使用explain()命令进行查询性能分析 : db.t1.find({name:'test10000'}).explain('executionStats')
        • 其中的executionStats下的executionTimeMillis表示整体查询时间, 单位是毫秒
    • 3> 建立索引

      • 创建索引 : 1->表示升序, -1->表示降序
          # db.集合.ensureIndex({属性:1})
          db.t1.ensureIndex({name:1})
        
    • 4> 对索引属性查询

      • 执行上面的同样的查询, 并进行查询性能分析 : db.t1.find({name:'test10000'}).explain('excutionStats')
    • 索引的命令
      • 建立唯一索引, 实现唯一约束的功能 : ``db.t1.ensureIndex({'name':1}, {'unique':true})
      • 联合索引, 对多个属性建立一个索引, 按照find()出现的顺序 : db.t1.ensureIndex({name:1,age:1})
      • 查看文档所有索引 : db.t1.getIndexes()
      • 删除索引 : db.t1.dropIndexes('索引名称')
  • 三) 安全性

    • 超级管理员
      • 为了更安全的访问mongodb, 需要访问者提供用户名和密码, 于是需要在mongodb中创建用户
      • 采用了角色-用户-数据库的安全管理方式
      • 常用系统角色如下:
        • root : 只在admin数据库中可用, 超级账号, 超级权限
        • Read : 允许用户读取制定数据库
        • readWrite : 允许用户读写指定数据库
      • 创建超级管理用户
        use admin
        db.create({
              user:'admin',
              pwd:'123',
              roles:[{role:'root', db:'admin'}]
        })
        
    • 启用安全认证
      • 修改配置文件 : sudo vi /etc/mongod.conf
      • 启用身份验证(注意: keys和values之间一定要加空格, 否则解析会报错)
        security:
            authorization: enabled
        
      • 重启服务 : sudo service mongod restart
      • 终端连接 : mongo -u admin -p 123 --authenticationDatabase admin
      • 切换数据库, 执行命令查看结果
      • 修改用户 : 可以修改pwd, roles属性 -> db.updateUser('t1':[pwd:'456'])
  • 四) 复制(副本集)

    • 什么是复制

      • 复制提供了数据的冗余备份, 并在多个服务器上存储数据副本, 提高了数据的可用性, 并可以保证数据的安全性
      • 复制还允许从硬件故障和服务中断中恢复数据
    • 为什么要复制

      • 数据备份
      • 数据灾难恢复
      • 读写分离
      • 高(24*7)数据可用性
      • 无宕(dang)机维修
      • 副本集对应用程序是透明
    • 复制的工作原理

      • 复制至少需要两个节点A, B...
      • A是主节点, 负责处理客户端请求
      • 其余的都是从节点, 负责复制主节点上的数据
      • 节点常见的搭配方式为: 一主一从, 一主多从
      • 主节点记录在其上的所有操作, 从节点定期轮询主节点获取这些操作, 然后对自己的数据副本执行这些操作, 从而保证从节点的数据与主节点一致
      • 主节点与从节点进行数据交互保障数据的一致性
    • 复制的特点

      • N个节点在集群
      • 任何节点可作为主节点
      • 所有写入操作都在主节点上
      • 自动故障转移
      • 自动恢复
    • 设置复制节点

      • 接下来的操作需要打开多个终端窗口, 而且可能会连接多台ubuntu主机, 会显得有些乱, 建议在xshell中实现
      • 1> 创建数据库目录t1, t2
        mkdir t1
        mkdir t2
        
      • 2> 使用如下格式启动mongod, 注意replSet的名称是一致的
        mongod --bind_ip 192.168.196.128 --port 27017 --dbpath ~/Desktop/t1 --replSet rs0
        mongod --bind_ip 192.168.196.128 --port 27018 --dbpath ~/Desktop/t1 --replSet rs0
        
      • 3> 连接主服务器, 此处设置192.168.196.128:27017为主服务器 : mongo --host 192.168.196.128 --port 27017
      • 4> 初始化 : rs.initiate()
      • 5> 查看当前状态 : rs.status()
      • 6> 添加复本集 : rs.add('192.168.196.128:27018')
      • 7> 连接第二个mongo服务 :
      • 8> 向主服务器中插入数据
        use test1
        for(i=0;i<10;i++){db.t1.insert({_id:i})}
        
      • 9> 在从服务器中查询(如果在从服务器上进行读操作, 需要设置rs.slaveOk())
        rs.slaveOk()
        db.t1.find()
        
    • 其他说明

      • 删除从节点 : rs.remove('192.168.196.128:27018')
      • 关闭主服务器后, 再重新启动, 会发现原来的主服务器变为了从服务器, 新启动的服务器(原来的从服务器)变为了从服务器(主从自动切换)
  • 五) 备份与恢复

    • 备份

      • 语法: mongodump -h - dbhost -d dbname -o dbdirectory
        • h : 服务器地址, 也可以指定端口号
        • d : 需要备份的数据库名称
        • o : 备份的数据存放位置, 此目录中存放着备份出来的数据
        • 例子 :
                sudo mkdir test1bak
                sudo mongodump -h 192.168.196.128:27017  -d test1 -o ~/Desktop/test1bak
          
    • 恢复

      • 语法: mongorestore -h dbhost -d dbname --dir dbdirectory
        • h : 服务器地址
        • d : 需要恢复的数据库实例
        • --dir : 备份的数据所在位置
        • 例子 :
                mongorestore -h 192.168.196.128:27017  -d test2 --dir ~/Desktop/test1bak/test1
          
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容