MySQL重要知识点--圈住,面试要考

mysql数据库使用了Innodb存储引擎就有了事务特性。

什么是事务?

事务是逻辑上的一组操作,要么都执行,要么都不执行。

事务的四大特性:(ACID)

原子性:事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;

一致性:执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;

隔离性:并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;

持久性:一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

那么ACID实现的原理是怎么样的呢:

问题一:Mysql怎么保证一致性的

从数据库层面,数据库通过原子性、隔离性、持久性来保证一致性。也就是说ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段。数据库必须要实现AID三大特性,才有可能实现一致性。例如,原子性无法保证,显然一致性也无法保证。从应用层面,通过代码判断数据库数据是否有效,然后决定回滚还是提交数据。

问题二: Mysql怎么保证原子性的?

答,是利用Innodb的undo log。undo log名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的sql语句,他需要记录你要回滚的相应日志信息。

例如

(1)当你delete一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert这条旧数据

(2)当你update一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行update操作

(3)当年insert一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行delete操作

undo log记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。

问题三: Mysql怎么保证持久性的?

OK,是利用Innodb的redo log。

正如之前说的,Mysql是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上。如果此时突然宕机,内存中的数据就会丢失。

怎么解决这个问题?

简单啊,事务提交前直接把数据写入磁盘就行啊。

这么做有什么问题?

1.只修改一个页面里的一个字节,就要将整个页面刷入磁盘,太浪费资源了。毕竟一个页面16kb大小,你只改其中一点点东西,就要将16kb的内容刷入磁盘,听着也不合理。

2.毕竟一个事务里的SQL可能牵涉到多个数据页的修改,而这些数据页可能不是相邻的,也就是属于随机IO。显然操作随机IO,速度会比较慢。

于是,决定采用redo log解决上面的问题。当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据。

采用redo log的好处?

其实好处就是将redo log进行刷盘比对数据页刷盘效率高,具体表现如下 

1:redo log体积小,毕竟只记录了哪一页修改了啥,因此体积小,刷盘快。

2:redo log是一直往末尾进行追加,属于顺序IO。效率显然比随机IO来的快。

问题四: Mysql怎么保证隔离性的?

是利用的是锁和MVCC机制。MVCC,即多版本并发控制(Multi Version Concurrency Control),一个行记录数据有多个版本对快照数据,这些快照数据在undo log中。

如果一个事务读取的行正在做DELELE或者UPDATE操作,读取操作不会等行上的锁释放,而是读取该行的快照版本。由于MVCC机制在可重复读(Repeateable Read)和读已提交(Read Commited)的MVCC表现形式不同,就不赘述了。但是有一点说明一下,在事务隔离级别为读已提交(Read Commited)时,一个事务能够读到另一个事务已经提交的数据,是不满足隔离性的。但是当事务隔离级别为可重复读(Repeateable Read)中,是满足隔离性的。

mysql数据库在并发操作时会出现的问题:

*脏读(Dirty read): **当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。

**丢****失修改(Lost to modify): **指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。

**不可重复读(Unrepeatableread): **指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

**幻读(Phantom read): **幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复度和幻读区别:

不可重复读的重点是修改,幻读的重点在于新增或者删除。

例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导 致A再读自己的工资时工资变为 2000;这就是不可重复读。

例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读。

那如果面试官问你:你们项目Mysql默认的事务隔离级别是什么呀?是可重复读(Repeatable Read)么,那互联网项目中Mysql也是用默认隔离级别,不做修改么?OK,不是的,我们在项目中一般用读已提交(Read Commited)这个隔离级别!what!居然是读已提交,网上不是说这个隔离级别存在不可重复读和幻读问题么?不用管么?

首先从主从复制,主从是基于什么复制的?

是基于binlog复制的!这里不想去搬binlog的概念了,就简单理解为binlog是一个记录数据库更改的文件吧~

binlog有几种格式?

OK,三种,分别是

statement:记录的是修改SQL语句

row:记录的是每行实际数据的变更

mixed:statement和row模式的混合

那Mysql在5.0这个版本以前,binlog只支持STATEMENT这种格式!而这种格式在读已提交(Read Commited)这个隔离级别下主从复制是有bug的,因此Mysql将可重复读(Repeatable Read)作为默认的隔离级别!

有什么bug呢?

就是在master上执行的顺序为先删后插!而此时binlog为STATEMENT格式,它记录的顺序为先插后删!从(slave)同步的是binglog,因此从机执行的顺序和主机不一致!就会出现主从不一致!

如何解决?

解决方案有两种!

(1)隔离级别设为可重复读(Repeatable Read),在该隔离级别下引入间隙锁。当Session 1执行delete语句时,会锁住间隙。那么,Ssession 2执行插入语句就会阻塞住!

(2)将binglog的格式修改为row格式,此时是基于行的复制,自然就不会出现sql执行顺序不一样的问题!奈何这个格式在mysql5.1版本开始才引入。因此由于历史原因,mysql将默认的隔离级别设为可重复读(Repeatable Read),保证主从复制不出问题!

那么,当我们了解完mysql选可重复读(Repeatable Read)作为默认隔离级别的原因后,接下来我们将其和读已提交(Read Commited)进行对比,来说明为什么在互联网项目为什么将隔离级别设为读已提交(Read Commited)

首先说项目中是不用读未提交(Read UnCommitted)串行化(Serializable)两个隔离级别,原因有二

采用读未提交(Read UnCommitted),一个事务读到另一个事务未提交读数据,这个不用多说吧,从逻辑上都说不过去!

采用串行化(Serializable),每个次读操作都会加锁,快照读失效,一般是使用mysql自带分布式事务功能时才使用该隔离级别!(笔者从未用过mysql自带的这个功能,因为这是XA事务,是强一致性事务,性能不佳!互联网的分布式方案,多采用最终一致性的事务解决方案!)

究竟隔离级别是用读已经提交呢还是可重复读?

可重复读(Repeatable Read),简称为RR;

读已提交(Read Commited),简称为RC;

缘由一:在RR隔离级别下,存在间隙锁,导致出现死锁的几率比RC大的多!

缘由二:在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行

缘由三:在RC隔离级别下,半一致性读(semi-consistent)特性增加了update操作的并发性!

在5.1.15的时候,innodb引入了一个概念叫做“semi-consistent”,减少了更新同一行记录时的冲突,减少锁等待。

所谓半一致性读就是,一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本,由MySQL上层判断此版本是否满足update的where条件。若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)!

两个疑问

在RC级别下,不可重复读问题需要解决么?

不用解决,这个问题是可以接受的!毕竟你数据都已经提交了,读出来本身就没有太大问题!Oracle的默认隔离级别就是RC,你们改过Oracle的默认隔离级别么?

在RC级别下,主从复制用什么binlog格式?

OK,在该隔离级别下,用的binlog为row格式,是基于行的复制!Innodb的创始人也是建议binlog使用该格式!


Innodb存储引擎的索引原理:

主要分 聚簇索引和非聚簇索引

我们自己用语句创建的索引,如复合索引、前缀索引、唯一索引,都是属于非聚簇索引,在有的书籍中,又将其称为辅助索引(secondary index)。在后文中,我们称其为非聚簇索引,其数据结构为B+树。

在Innodb中,聚簇索引默认就是主键索引。在Mysql中是没有语句来另外生成的。在Innodb中,Mysql中的数据是按照主键的顺序来存放的。那么聚簇索引就是按照每张表的主键来构造一颗B+树,叶子节点存放的就是整张表的行数据。由于表里的数据只能按照一颗B+树排序,因此一张表只能有一个聚簇索引。

如果没有主键,则按照下列规则来建聚簇索引

1.没有主键时,会用一个唯一且不为空的索引列做为主键,成为此表的聚簇索引

2.如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚簇索引。

MySQL数据库会根据你的建立的索引字段生成一颗新的B+树。我们每加一个索引,就会增加表的体积, 占用磁盘存储空间。然而,注意看叶子节点,非聚簇索引的叶子节点并不是真实数据,它的叶子节点依然是索引节点,存放的是该索引字段的值以及对应的主键索引(聚簇索引)。再说个小知识:MySQL表 一个表最多可创建16个索引,最大索引长度256字节。如果索引太多,插入性能就会下降是因为多加一个索引,就会多生成一颗非聚簇索引树。这时可以考虑使用组合索引,利用前缀索引进行优化。

问题来了什么是最左前缀原则?

假设,我们对(a,b)字段建立了联合索引 ,他们是按照a来进行排序,在a相等的情况下,才按b来排序。

因此,我们可以看到a是有序的1,1,2,2,3,3。而b是一种全局无序,局部相对有序状态!

什么意思呢?

从全局来看,b的值为1,2,1,4,1,2,是无序的,因此直接执行b = 2这种查询条件没有办法利用索引。

从局部来看,当a的值确定的时候,b是有序的。例如a = 1时,b值为1,2是有序的状态。当a=2时候,b的值为1,4也是有序状态。因此,你执行a = 1 and b = 2是a,b字段能用到索引的。而你执行a > 1 and b = 2时,a字段能用到索引,b字段用不到索引。因为a的值此时是一个范围,不是固定的,在这个范围内b值不是有序的,因此b字段用不上索引。综上所示,最左匹配原则,在遇到范围查询的时候,就会停止匹配。

为什么mysql的索引要用B+树数据结构呢:

B+树和B树的特点,我们做一个总结

(1)B树的树内存储数据,因此查询单条数据的时候,B树的查询效率不固定,最好的情况是O(1)。我们可以认为在做单一数据查询的时候,使用B树平均性能更好。但是,由于B树中各节点之间没有指针相邻,因此B树不适合做一些数据遍历操作。

(2)B+树的数据只出现在叶子节点上,因此在查询单条数据的时候,查询速度非常稳定。因此,在做单一数据的查询上,其平均性能并不如B树。但是,B+树的叶子节点上有指针进行相连,因此在做数据遍历的时候,只需要对叶子节点进行遍历即可,这个特性使得B+树非常适合做范围查询。

因此,我们可以做一个推论:没准是Mysql中数据遍历操作比较多,所以用B+树作为索引结构。而Mongodb是做单一查询比较多,数据遍历操作比较少,所以用B树作为索引结构。

那么为什么Mysql做数据遍历操作多?而Mongodb做数据遍历操作少呢?

因为Mysql是关系型数据库,而Mongodb是非关系型数据。

由于关系型数据库和非关系型数据的设计方式上的不同。导致在关系型数据中,遍历操作比较常见,因此采用B+树作为索引,比较合适。而在非关系型数据库中,单一查询比较常见,因此采用B树作为索引,比较合适。

显示的sql语句加锁

(insert into select)复制表时候会用到,加锁

select *** lock in share mode乐观锁共享锁

select *** for update 悲观锁 排它锁

1. 明确指定主键,并且有此数据,row lock

2.明确指定主键,无此数据,不 lock

3.(无主键,table lock)

4.(明确指定索引,并且有此数据,row lock)

使用悲观锁要注意尽量别产生间隙锁,否则非常影响性能。 二级索引  非唯一索引

乐观锁一般会使用版本号机制或CAS算法实现。

1. 版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

2. CAS算法

compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

需要读写的内存值 V

进行比较的值 A

拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试

锁机制与InnoDB锁算法

InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁

表级锁和行级锁对比:

表级锁:Mysql中锁定粒度最大的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。

行级锁:Mysql中锁定粒度最小的一种锁,只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。

InnoDB存储引擎的锁的算法有三种:

Record lock:单个行记录上的锁

Gap lock:间隙锁,锁定一个范围,不包括记录本身

Next-key lock:record+gap 锁定一个范围,包含记录本身

相关知识点:

innodb对于行的查询使用next-key lock

Next-locking keying为了解决Phantom Problem幻读问题

当查询的索引含有唯一属性时,将next-key lock降级为record key

Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生

有两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) A. 将事务隔离级别设置为RC B. 将参数innodb_locks_unsafe_for_binlog设置为1

面试官问你如何进行数据库优化的?别慌:

第一阶段 优化sql和索引

(1)用慢查询日志定位执行效率低的SQL语句

(2)用explain分析SQL的执行计划

(3)确定问题,采取相应的优化措施,建立索引啊,等

我就不举例了,因为如何优化SQL的文章,一抓一大把,再贴过来,读者看着也累。

第二阶段 搭建缓存

在优化sql无法解决问题的情况下,才考虑搭建缓存。毕竟你使用缓存的目的,就是将复杂的、耗时的、不常变的执行结果缓存起来,降低数据库的资源消耗。

这里需要注意的是:搭建缓存后,系统的复杂性增加了。你需要考虑很多问题,比如:

缓存和数据库一致性问题?(比如是更缓存,还是删缓存),

缓存击穿、缓存穿透、缓存雪崩问题如何解决?是否有做缓存预热的必要。不过我猜,大部分中小公司应该都没考虑。

第三阶段 读写分离

缓存也搞不定的情况下,搞主从复制,上读写分离。在应用层,区分读写请求。或者利用现成的中间件mycat或者altas等做读写分离。

需要注意的是,只要你敢说你用了主从架构,有三个问题,你要准备:

(1)主从的好处?

回答:实现数据库备份,实现数据库负载均衡,提交数据库可用性

(2)主从的原理?

主库有一个log dump线程,将binlog传给从库

从库有两个线程,一个I/O线程,一个SQL线程,I/O线程读取主库传过来的binlog内容并写入到relay log,SQL线程从relay log里面读取内容,写入从库的数据库。

(3)如何解决主从一致性?

回答:这个问题,我不建议在数据库层面解决该问题。根据CAP定理,主从架构本来就是一种高可用架构,是无法满足一致性的

哪怕你采用同步复制模式或者半同步复制模式,都是弱一致性,并不是强一致性。所以,推荐还是利用缓存,来解决该问题。

步骤如下:

1、自己通过测试,计算主从延迟时间,建议mysql版本为5.7以后,因为mysql自5.7开始,多线程复制功能比较完善,一般能保证延迟在1s内。不过话说回来,mysql现在都出到8.x了,还有人用5.x的版本么。

2、数据库的写操作,先写数据库,再写cache,但是有效期很短,就比主从延时的时间稍微长一点。

3、读请求的时候,先读缓存,缓存不存在(这时主从同步已经完成),再读数据库。

第四阶段 利用分区表

说句实在话,你们面试的时候,其实可以略过这个阶段。因为很多互联网公司都不建议用分区表,我自己也不太建议用分区表,采用这个分区表,坑太多。

这里引用一下其他文章的回答:

什么是mysql的分区表?

回答:所有数据还在一个表中,但物理存储根据一定的规则放在不同的文件中。这个是mysql支持的功能,业务代码不需要改动,但是sql语句需要改动,sql条件需要带上分区的列。

缺点

(1)分区键设计不太灵活,如果不走分区键,很容易出现全表锁

(2)在分区表使用ALTER TABLE … ORDER BY,只能在每个分区内进行order by。

(3)分区表的分区键创建索引,那么这个索引也将被分区。分区键没有全局索引一说。

(4)自己分库分表,自己掌控业务场景与访问模式,可控。分区表,研发写了一个sql,都不确定该去哪个分区查,不太可控。

...不列举了,不推荐

第五阶段 垂直拆分

上面四个阶段都没搞定,就来垂直拆分了。垂直拆分的复杂度还是比水平拆分小的。将你的表,按模块拆分为不同的小表。大家应该都看过《大型网站架构演变之路》,这种类型的文章或者书籍,基本都有提到这一阶段。

如果你有幸能够在什么运营商、银行等公司上班,你会发现他们一个表,几百个字段都是很常见的事情。所以,应该要进行拆分,拆分原则一般是如下三点:

(1)把不常用的字段单独放在一张表。

(2)把常用的字段单独放一张表

(3)经常组合查询的列放在一张表中(联合索引)。

第六阶段 水平拆分

OK,水平拆分是最麻烦的一个阶段,拆分后会有很多的问题,我再强调一次,水平拆分一定是最最最最后的选择。从某种意义上,我觉得还不如垂直拆分。因为你用垂直拆分,分成不同模块后,发现单模块的压力过大,你完全可以给该模块单独做优化,例如提高该模块的机器配置等。如果是水平拆分,拆成两张表,代码需要变动,然后发现两张表还不行,再变代码,再拆成三张表的?水平拆分模块间耦合性太强,成本太大,不是特别推荐。

(全文参考烟哥MySQL数据库杂文拼接)

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