mycat的介绍及使用

mycat介绍

  1. 什么是mycat

    mycat是Java语言开发的目前比较主流的数据库中间件之一,是mycat团队在阿里开源的 Cobar 产品基础上而研发的,也一直在维护。mycat实现了 MySQL 协议的服务器,前端用户可以把它看作是一个数据库代理,用 MySQL 客户端工具和命令行访问,而其后端可以用 MySQL 原生协议与多个 MySQL 服务器通信,也可以用 JDBC 协议与大多数主流数据库服务器通信。

    mycat不单只可以做MySQL的代理,它的后端可以支持MySQL、SQL Server、Oracle、DB2、PostgreSQL 等主流数据库,也支持 MongoDB 这种新型NoSQL 方式的存储,未来还会支持更多类型的存储。

  2. mycat的实现原理

    mycat主要是通过对SQL的拦截,然后经过一定规则的分片解析、路由分析、读写分离分析、缓存分析等,然后将SQL发给后端真实的数据块,并将返回的结果做适当处理返回给客户端。

  3. mycat的应用场景

    • 数据库读写分离,此时配置最为简单,支持读写分离,主从切换
    • 数据库分库分表,这也是mycat的核心功能之一,可以解决数据库分库分表之后的数据插入和查询问题
    • 多租户应用,每个应用一个数据库,但程序只需连接mycat,程序不改变,实现多租户化
    • 海量数据的存储及实时查询,使用mycat是一种简单有效的方案
    • 替代Hbase,分析大数据

mycat安装

官网地址:http://www.mycat.org.cn/

安装包下载地址:http://dl.mycat.org.cn/

mycat有windows版和Linux版,这里只说Linux的安装

PS:使用mycat前必须安装Java的JDK环境,so easy,这里不讲了,一定记得提前安装!!!

wget http://dl.mycat.org.cn/1.6.7.6/20210730131311/Mycat-server-1.6.7.6-release-20210730131311-linux.tar.gz
tar -C /usr/local -zxvf Mycat-server-1.6.7.6-release-20210730131311-linux.tar.gz
cd /usr/local/mycat/
#./bin/mycat { console | start | stop | restart | status | dump }
./bin/mycat start

连接测试(mycat默认端口是8066,默认密码是123456)

root@f15fc642fedf:/# mysql -uroot -p123456 -P8066 -h192.168.241.140
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.6.29-mycat-1.6.7.6-release-20210730131311 MyCat Server (OpenCloudDB)

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

mycat核心配置

mycat的配置文件在 conf 文件夹下,最核心的配置文件有三个:schema.xml、server.xml、rule.xml

一、schema.xml

该配置文件主要是用于配置逻辑库、逻辑表等相关信息

  1. schema标签

    定义mycat实例中的逻辑库,mycat可以有多个逻辑库,每个逻辑库都有自己的相关配置。可以使用schema标签来划分这些不同的逻辑库。如果不配置schema标签,所有表的配置会属于同一个默认的逻辑库。

    逻辑库的概念和MySQL的database的概念一样,在查询两个不同逻辑库中的表的时候,需要切换到该逻辑库下进行查询。示例:

    <schema name="TESTDB" checkSQLschema="true" sqlMaxLimit="100" randomDataNode="dn1"></schema>
    
    • name:定义逻辑库名,必须唯一不能重复
    • checkSQLschema:检查发给mycat的SQL是否含有库名。当该值为true时,例如执行语句 select * from TESTDB.company mycat会把语句修改为 select * from company 去掉库名 TESTDB
    • sqlMaxLimit:限制返回结果集的行数。该值设置为某个数值时,每条执行的sql语句,如果没有加上limit参数,mycat会自动加上这个属性的值。不写的话,默认返回所有的值。需要注意的是,如果运行的schema为非拆分库的,那么该属性不会生效。需要自己sql语句加limit。
    • randomDataNode:这是新版mycat新加入的属性,定义将一些随机语句发送到该数据节点中
  2. table 标签

    定义mycat中的逻辑表,所有需要拆分的表都需要在这个标签中定义。示例:

    <table name="customer" primaryKey="id" dataNode="dn1,dn2" rule="sharding-by-intfile" autoIncrement="true" fetchStoreNodeByJdbc="true">
     <childTable name="customer_addr" primaryKey="id" joinKey="customer_id" parentKey="id"> </childTable>
    </table>
    
    • name:定义逻辑表名,同一个schema中逻辑表名必须唯一
    • primaryKey:指定逻辑表中的主键,也是需要与物理表的主键一致
    • dataNode:指定物理表所在数据节点的名称,该属性的值需要和dataNode标签中的name值相对应。配置多个以逗号隔开,或者使用 dn1$0-100 方式
    • rule:指定逻辑表使用的分片规则名称,规则名称在rule.xml中定义,必须与tableRule标签中name属性对应
    • autoIncrement:是否自增,mycat提供了自增长主键功能,但是对应的mysql节点上数据表,没有auto_increment,那么在mycat层调用last_insert_id()也是不会返回结果的
    • fetchStoreNodeByJdbc:是否启用ER表使用JDBC方式获取DataNode

    2.1)childTable标签:用于定义 ER 分片的子表。通过标签上的属性与父表进行关联

    • name:子表名称
    • primaryKey:主键
    • joinKey:子表中字段的名称
    • parentKey:父表中字段名称
  3. dataNode标签

    定义mycat中的数据节点,也就是我们所说的数据分片。一个dataNode标签就是一个独立的数据分片。数据节点指向的是存储逻辑表的物理数据库。示例:

    <dataNode name="dn1" dataHost="localhost1" database="db1" />
    <dataNode name="dn2" dataHost="localhost1" database="db2" />
    <dataNode name="dn3" dataHost="localhost1" database="db3" />
    
    • name:定义数据节点的名字,这个名字需要唯一。上边在table标签上用这个名字来建立表与分片对应的关系
    • dataHost:定义该分片属于哪个数据库实例,属性与dataHost标签上定义的name对应
    • database:定义该分片属于数据库实例上的具体库
  4. dataHost标签

    这个标签直接定义了具体数据库实例,读写分离配置和心跳语句。示例:

    <dataHost name="localhost1" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1"  slaveThreshold="100">
     <heartbeat>select user()</heartbeat>
     <writeHost host="hostM1" url="jdbc:mysql://localhost:3306" user="root" password="root">
         <readHost host="hostS1" url="jdbc:mysql://localhost:3306" user="root" password="123456"/>
     </writeHost>
    </dataHost>
    
    • name:唯一标示dataHost标签,供上层使用,必须唯一

    • maxCon:指定每个读写实例连接池的最大连接

    • minCon:指定每个读写实例连接池的最小连接,初始化连接池的大小

    • balance:指定读写分离的负载均衡类型,目前有4种类型

      0:代表不开启读写分离机制,所有读操作都发送到当前可用的writeHost上

      1:代表全部的readHost与stand by writeHost参与select语句的负载均衡,简单的说,当双主双从模式(M1-S1,M2-S2 并且M1 M2互为主备),正常情况下,M2,S1,S2都参与select语句的负载均衡。

      2:所有读操作都随机的在writeHost、readHost上分发

      3:所有读请求随机的分发到writeHost对应的readHost执行,writeHost不负担读压力(1.4 新增)

    • writeType:指定写实例的负载均衡类型,目前有4种类型

      -1:表示不自动切换

      0:所有写操作发送到配置的第一个writeHost,第一个挂了切到还生存的第二个writeHost。重新启动后以切换后的为准,切换记录在配置文件中:dnindex.properties

      1:所有写操作都随机的发送到配置的writeHost,1.5 以后废弃不推荐使用

      2:基于MySQL主从同步的状态决定是否切换(1.4 新增)

    • dbType:指定后端连接的数据库类型

    • dbDriver:指定连接后端数据库使用的驱动,目前可选的值有 native 和 jdbc

    • switchType:指定主从切换的方式

      -1:表示不自动切换

      1:默认值,自动切换

      2:基于MySQL主从同步的状态决定是否切换,心跳检测语句为:show slave status

      3:基于MySQL galary cluster 的切换机制(适合集群,1.4.1新增),心跳检测语句为:show status like 'wsrep%'

    • slaveThreshold:定义主从复制延时阈值,当 Seconds_Behind_Master > slaveThreshold 时,读写分离筛选器会过滤掉此Slave机器,防止读到很久之前的旧数据。该属性是用于配合writeType属性实现根据主从延时来进行主从切换的

    4.1)heartbeat 标签:指明用于和后端数据库进行心跳检查的语句。例如,MySQL可以使用select user(),Oracle可以使用select 1 from dual

    4.2)writehost 标签及 readHost 标签:这两个标签属性相同,这里一起介绍

    • host:标识不同实例名称,一般writeHost名称使用M1作为后缀,readHost则使用S1作为后缀
    • url:配置数据库的连接地址,如果dbDriver属性是 native,则一般为address:port这种形式。如果是jdbc或其他的,则需要特殊指定。
    • user:配置数据库用户名
    • password:配置数据库密码
    • weight:配置某个数据库在 readHost 中作为读节点的权重
    • usingDecrypt:指定是否对密码加密,默认为0, 若需要开启则配置为1
二、server.xml

该配置文件主要是用于配置系统参数、用户信息、访问权限及SQL防火墙和SQL拦截功能等

  1. system标签

    用于配置mycat的系统配置参数。常见的配置参数示例:

    <system>
        <!-- mycat 服务连接端口 -->
        <property name="serverPort">8066</property>
        <!-- mycat 服务管理端口 -->
        <property name="managerPort">9066</property>
        <!-- mycat 服务监听的ip -->
        <property name="bindIp">0.0.0.0</property>
        <!-- 0为需要密码登陆、1为不需要密码登陆;默认为0,设置为1则需要指定默认账户-->
        <property name="nonePasswordLogin">0</property>
        <!-- 前端连接的写队列大小 -->
        <property name="frontWriteQueueSize">2048</property>
        <!-- 设置字符集编码 -->
        <property name="charset">utf8</property>
        <!-- mycat 的进程数量 -->
        <property name="processors">8</property>
        <!-- 闲置连接超时时间,单位:毫秒 -->
        <property name="idleTimeout">1800000</property>
        <!-- 默认最大返回的数据集大小 -->
        <property name="defaultMaxLimit">100</property>
        <!-- 允许的最大包大小 -->
        <property name="maxPacketSize">104857600</property>
        <!-- 0遇上没有实现的报文(Unknown command:),就会报错、1为忽略该报文,返回ok报文。
    在某些mysql客户端存在客户端已经登录的时候还会继续发送登录报文,mycat会报错,该设置可以绕过这个错误-->
        <property name="ignoreUnknownCommand">0</property>
        <property name="useHandshakeV10">1</property>
        <property name="removeGraveAccent">1</property>
        <!-- 1为开启实时统计、0为关闭 -->
        <property name="useSqlStat">0</property>
        <!-- 1为开启全加班一致性检测、0为关闭 -->
        <property name="useGlobleTableCheck">0</property>
        <!-- SQL 执行超时 单位:秒-->
        <property name="sqlExecuteTimeout">300</property>
        <property name="sequnceHandlerType">1</property>
        <!--必须带有MYCATSEQ_或者 mycatseq_进入序列匹配流程 注意MYCATSEQ_有空格的情况-->
        <property name="sequnceHandlerPattern">(?:(\s*next\s+value\s+for\s*MYCATSEQ_(\w+))(,|\)|\s)*)+</property>
        <!-- 子查询中存在关联查询的情况下,检查关联字段中是否有分片字段 .默认 false -->
        <property name="subqueryRelationshipCheck">false</property>
        <property name="sequenceHanlderClass">io.mycat.route.sequence.handler.HttpIncrSequenceHandler</property>
        <!--默认为type 0: DirectByteBufferPool | type 1 ByteBufferArena | type 2 NettyBufferPool -->
        <property name="processorBufferPoolType">0</property>
        <!--分布式事务开关,0为不过滤分布式事务,1为过滤分布式事务(如果分布式事务内只涉及全局表,则不过滤),2为不过滤分布式事务,但是记录分布式事务日志-->
        <property name="handleDistributedTransactions">0</property>
        <!-- off heap for merge/order/group/limit  1开启;0关闭 -->
        <property name="useOffHeapForMerge">0</property>
        <!--是否采用zookeeper协调切换  -->
        <property name="useZKSwitch">false</property>
        <!--如果为 true的话 严格遵守隔离级别,不会在仅仅只有select语句的时候在事务中切换连接-->
        <property name="strictTxIsolation">false</property>
        <!-- Mycat连接数据库时使用的隔离级别
             1 - 读未提交
             2 - 读已提交
             3 - 可重复读
             4 - 串行化
         -->
        <property name="txIsolation">2</property>
        <property name="useZKSwitch">true</property>
        <!--如果为0的话,涉及多个DataNode的catlet任务不会跨线程执行-->
        <property name="parallExecute">0</property>
    </system>
    
  2. user标签

    用于配置mycat的访问用户及权限。示例:

    <!-- 用户名,defaultAccount属性是指定当前账户是否为默认账户 -->
    <user name="root" defaultAccount="true">
        <!-- 密码 -->
        <property name="password">123456</property>
        <!-- 允许该用户访问的逻辑库 -->
        <property name="schemas">TESTDB</property>
        <!-- 可配置多个允许访问的逻辑库,使用逗号分隔 -->
        <!-- <property name="schemas">db1,db2</property>  -->
        <!-- 是否只读 -->
        <property name="readOnly">false</property>
        <!-- 表级 DML 权限配置,check属性表示是否开启该配置 -->
        <privileges check="true">
            <!-- 特别权限应用的逻辑库 -->
            <schema name="TESTDB" dml="0110">
                <!-- 
                    配置用户对该表的访问权限,dml属性用于指定权限位,
                    如果table标签没有配置该属性的话,默认取schema标签的dml属性值,
                    剩余没有配置的其他表默认也是取schema标签的dml属性值
                -->
                <table name="tb01" dml="0000"></table>
                <table name="tb02" dml="1111"></table>
            </schema>
        </privileges>
    </user>
    

    dml属性配置的数字是权限位,分别对应着 insert,update,select,delete 四种权限。例如,当dml的值为0110时,表示拥有 update 和 select 权限,不具有 insert 和 delete 权限。所以权限位为1时代表拥有对应的操作权限,为0时代表没有该操作权限。

三、rule.xml

该配置文件主要用于配置切分规则的。数据库水平拆分之后,就需要用这个配置文件定义分片算法规则

配置文件中主要有两种标签,tableRule 和 function ,这两个是一一对应的,示例:

<!-- name属性指定分片规则的名称,必须在 rule.xml 文件中是唯一的 -->
<tableRule name="mod-long">
    <rule>
        <!-- 指定使用表中的哪个列(字段)进行分片 -->
        <columns>id</columns>
        <!-- 指定表的分片算法,取值为<function>标签的name属性 -->
        <algorithm>mod-long</algorithm>
    </rule>
</tableRule>
<!-- name属性指定分片算法的名称,同样需要是唯一的;class属性指定该算法的具体实现类 -->
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
    <!-- 要分片的数据库节点数量,必须指定,否则没法分片 -->
    <property name="count">3</property>
</function>

mycat默认已经内置了很多分片算法,除了使用这些外也可以根据自己的实际需求自定义算法规则。

mycat内置的常用分片算法有:

  • PartitionByMod:简单取模,直接通过列值进行取模得出分片位置。适用于整数类型的列,不能用于非整型的列,且不需要人工干预
  • PartitionByHashMod:哈希取模,先将列值进行hash运算之后再取模得出分片位置。适用于非整型的列,且不需要人工干预
  • PartitionByFileMap:分片枚举,根据枚举值对数据进行分片。适用于需要人工指定某些数据到哪个分片下。例如在异地多活的场景中通过地区id进行数据分片的场景。
  • PartitionByPrefixPattern:字符串范围取模,根据长字符串的前面几位进行取模分片
四、加密明文密码(可选)

在mycat中,不论是 server.xml 配置文件中的mycat登录密码,还是 schema.xml 配置文件的writeHost和readHost标签的连接密码,都是以明文显示的,只要查看这两个配置文件,就可以知道密码,这种方式可能会存在安全隐患。

所以mycat提供了一个工具用于加密明文密码,该工具在一个jar包内,可使用如下命令对密码进行加密,执行成功后会得到一个加密后的字符串,然后替换配置文件中的明文密码即可

java -cp lib/Mycat-server-1.6.7.6-release.jar io.mycat.util.DecryptUtil 0:root:123456

命令解释:

0:user:password 是加密字符串,有两种格式,示例

  • 0:root:123456:代表对mycat用户名root明文密码123456的账户进行加密,修改配置文件示例

    <user name="root" defaultAccount="true">
      <property name="usingDecrypt">1</property>
      <property name="password">d6D+pOmkuUoY09p4/aivwMsScLa7zfjIwAxvkEhr3v7en06mEXoX9DTTjQNug5CfvGf7Wy9oLcthYI3yLMSjIg==</property>
      <property name="schemas">TESTDB</property>
    </user>
    
  • 1:hostM1:root:123456 :代表对 schema.xml 配置文件中dataHost标签下数据库连接加密,修改示例

    <writeHost host="hostM1" url="localhost:3306" user="root" password="BpkNIjF7LfzS1C76HT7B1bJgmGIDtPihqIvHBlC92L1IFqsMfoJEMk1EkxSzjasWB4GWoUcODYO4AaJstdAp5w==" usingDecrypt="1">
    </writeHost>
    

实例演示

以商城为例,假使现在的需求是:一个订单表数据量太大,需要进行水平拆分。

假使解决方案是:使用三台服务器部署mysql,实现一主二从读写分离。数据库名为shop,订单表拆分成64张表,表名为orders0-63。使用雪花算法实现分布式唯一id,由于id是整型,所以切片规则选择PartitionByMod算法,也就是直接对id进行取模,这样可以保证数据分布均匀且易扩展,将算法中分片数设置为64

按照以上方案,直接贴出相关配置文件中的核心代码

schema.xml

<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
    <schema name="TESTDB" checkSQLschema="true" sqlMaxLimit="100" randomDataNode="dn1">
         <table name="orders" primaryKey="id" subTables="orders_$0-63" dataNode="dn1" rule="mod-long" />
    </schema>
    <dataNode name="dn1" dataHost="shop" database="shop" />
    <dataHost name="shop" maxCon="1000" minCon="10" balance="3" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
        <heartbeat>select user()</heartbeat>
        <writeHost host="hostM1" url="192.168.241.140:3306" user="root" password="root">
            <readHost host="hostS1" url="192.168.241.141:3306" user="root" password="root"/>
            <readHost host="hostS2" url="192.168.241.142:3306" user="root" password="root"/>
        </writeHost>
    </dataHost>
</mycat:schema>

rule.xml

<tableRule name="mod-long">
    <rule>
        <columns>id</columns>
        <algorithm>mod-long</algorithm>
    </rule>
</tableRule>
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
    <property name="count">64</property>
</function>

测试

mysql> explain insert into orders (id,no) values(1,'1111111');
+-----------+------------------------------------------------------------+
| DATA_NODE | SQL                                                        |
+-----------+------------------------------------------------------------+
| dn1       | INSERT INTO orders_1 (id, no) VALUES ('1', '1111111') |
+-----------+------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> insert into orders (id,no) values(1,'1111111');
Query OK, 1 row affected (0.01 sec)

mysql> explain select id,no from orders where id in (1);
+-----------+---------------------------------------------------------+
| DATA_NODE | SQL                                                     |
+-----------+---------------------------------------------------------+
| dn1       | SELECT id, no FROM orders_1 orders WHERE id IN (1) |
+-----------+---------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select id,no from orders where id in (1);
+----+---------+
| id | no      |
+----+---------+
|  1 | 1111111 |
+----+---------+
1 row in set (0.00 sec)

mysql> show variables like "server_id";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| server_id     | 2     |
+---------------+-------+
1 row in set (0.00 sec)

mysql> show variables like "server_id";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| server_id     | 3     |
+---------------+-------+
1 row in set (0.01 sec)

mysql>

通过上述可观察到,数据在插入和查询时mycat会自动取模计算找到对应的表。查询 server-id 参数发现在变,证明确实在两个从库中进行负载轮询。到此说明整个操作成功

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

推荐阅读更多精彩内容