Redis学习笔记

. 简述:

Redis由Salvatore Sanfilippo使用C语言编写的一种支持网络、可基于内存亦可持久化的日志型、Key-Value数据库。

2. 特点:

2.1 优点:

2.1.1 redis的支持数据持久化

可以将内存中的数据保存在磁盘中,重启时通过再次加载,这样数据不会丢失;

2.1.2 Redis支持数据类型丰富

支持String,List,Set,Zset(sorted sets),Hash数据结构的存储;

2.1.3 Redis支持master-slave模式的数据备份;
2.1.4 Redis的操作支持原子性

Redis提供了简单的事务功能, 将一组需要一起执行的命令放到multi和exec两个命令之间(其中 multi命令代表事务开始, exec命令代表事务结束),它们之间的命令是原子顺序执行的。

2.1.5 性能极高

读写数据每秒可达10万次左右。

2.1.6 字符串能够存储的最大值是512M,几乎能够满足所有常见业务场景。

缺点:

2.2.1 容量收到物理内存的限制,适合小数据量级别的高性能读写操作,大数据量可以选择淘宝的Tair
2.2.2 不具有自动容错和恢复能力;
2.2.3 主机宕机之后,如果有部分没有同步到从机的数据,那么切到从机之后,会出现数据不一致的情况;
2.2.3 redis事务不支持回滚。

3.Redis的数据持久化

Redis提供了RDB(Redis DataBase)和AOF(Append Only File)两种持久化方式。

3.1 RDB是指在一定的时间间隔将数据快照存储到磁盘。

当到达数据备份时间时,Redis会启动一个线程,该线程将数据保存到一个临时文件,以替换上个时间周期备份的临时文件。优点:数据恢复快,缺点:最后一次持久化后的数据有可能会丢失。

3.2 AOF是以日志的形式记录每次对Redis的写操作

以redis协议追加保存每次写的操作到文件末尾,当Redis重启的时,重新执行这些命令来恢复原始的数据。优点:数据实时同步,缺点:AOF文件持续增长而过大(Redis通过设置文件大小的阀值来触发AOF文件的压缩)。

注:当同时开启RDB和AOF时,系统优先使用AOF。

4.主从复制

4.1 主从复制的延迟

主从复制可以通过slaveof命令配置实现,默认情况下Redis都是主节点,每个从节点只能有一个主节点,而主节点可以有多个从节点,故复制的数据流是单向的,即只能从主节点复制到从节点,因此主节点无法感知从节点的修改,所以一般从节点提供只读模式。

由于主从部署在不同的机器上,Redis提供了repl-disable-tcp-nodelay参数用于控制是否关闭

TCP_NODELAY, 默认关闭。
a.当关闭时, 主节点产生的命令数据大小都会及时地发送给从节点, 优点:延迟会变小, 缺点:网络带宽的消耗增加。 适用于同机架或同机房部署等网络环境好的场景。

b.当开启时, 主节点会合并较小的数据包发送到从节点,一般时间间隔设置为30-50毫秒。优点:节省带宽,缺点:增大主从之间的延迟。 适用于主从网络环境较差的环境, 如跨机房部署等。

注意:从节点更换主节点后从节点会清空之前所有的数据, 所以执行该操作时一定要在正确的主节点和从节点上。

4.2 Redis拓扑结构

4.2.1 一主一从

4.2.2 一主多从

4.2.3 树状主从

4.3 主从复制过程

1.从节点保存主节点信息

即执行slaveof后,从节点保存主节点的地址信息。

2.主从建立Socket连接

从节点通过执行定时任务(即每秒执行一次)发现slaveof新的主节点后,会尝试建立Socket网络连接。如果连接失败,从节点会通过定时任务来无限重试。

3.发送ping命令

Socket连接成功后,从节点发送ping请求进行首次通信,主要目的是:检测主从之间网络套接字是否可用,并检测主节点当前是否可接受处理命令。

4.权限验证

如果主节点设置了requirepass参数, 则需要密码验证,从节点必须配置masterauth参数保证与主节点相同的密码才能通过验证。

5.同步数据集

主从复制连接正常后, 对于首次建立复制的场景, 主节点会把持有的数据全部发送给从节点,

6.命令持续复制

主节点会持续地把写命令发送给从节点, 从而保证主从数据一致。

4.4 数据延迟

Redis主从数据的延迟由于异步复制特性导致的,是无法避免的, 延迟受到网络带宽和命令阻塞情况的影响, 如果业务不允许短时间内的数据延迟, 可以编码实现监控程序,监听主从节点的复制偏移量, 当延迟较大时触发报警或通知客户端避免读取延迟过大的从节点,从而避免读不到数据的情况发生。

4.5 过期数据的处理

4.5.1 惰性删除

主节点每次处理读取命令时,除了读取key-value表,还会读取Redis超时时间表,以检查key是否超时, 如果超时则删除key对象, 之后del命令也会异步发送给从节点,从节点再执行del。

4.5.2 定时删除

Redis主节点在内部定时任务会循环读取一定数量的Key, 当发现读取的Key过期时执行del命令,之后del命令也会异步发送给从节点,从节点再执行del。

注意:为了保证复制数据的一致性, 从节点自身不会主动删除超时数据。

4.6 Redis读写分离的考量

考虑到数据延迟,过期数据等,以及Redis的高性能,因此在考虑读写分离之前,一定要在主节点上做好充分优化,如解决慢查询,持久化阻塞和合理的数据结构等,当仍然不满足时,可以先考虑Redis Cluster等分布式方案,再考虑读写分离方案。

5.Redis的阻塞问题

由于Redis是典型的单线程架构,所有的读写操作都在一条主线程中完成,因此如果主线程拥塞了,那将是业务系统的噩梦。
造成阻塞的原因如下:

5.1 API或数据结构使用不合理

问题定位:1.通过slowlog get { n } 获取最近n条慢查询(Redis默认对于执行超过10毫秒的命令都会记录到一个定长队列(队列默认长度为128)中),发现慢查询后,可以通过使用低算法复杂度的命令或避免大对象数据来优化。

5.2 CPU饱和的问题

CPU饱和是指Redis把单核CPU(由于Redis是单线程,只能使用一个CPU)使用率跑到接近100%。可以使用top命令查看Redis线程的CPU使用率,可参考:https://www.jianshu.com/p/6d7571d82304。对于Redis的使用情况,可通过redis-cli-h{ip}-p{port}--stat命令每秒输出Redis的统计信息。如果此时发现Redis的请求量比较低,那说明使用了高复杂度算法的命令;如果此时发现Redis的请求量已经很高了,而且应用的优化有限,那就需要使用新的Redis来分摊请求和CPU的压力。

5.3 持久化相关的阻塞

对于开启了持久化功能的Redis节点,要检查是否是fork阻塞,AOF刷盘阻塞和HugePage写操作阻塞导致的CPU使用率过高。具体可参考官网:http://www.redis.io/topics/latency

5.4 CPU竞争

当Redis和其他多核CPU密集型服务部署在一起时,其他进程过度消耗CPU时, 将严重影响Redis吞吐量。可以将Redis绑定到一个CPU上,避免CPU切换的开销。

5.5 内存交换

如果系统把Redis的部分内存中的数据换出并序列化到磁盘后,如果请求的数据不在内存中,需要内存交换,那么Redis性能严重下降。解决方式:扩大内存,设置Redis的最大可用内存,降低系统内存交换执行的优先级。

5.6 网络问题

解决方式:增加带宽,同机房部署,机房部署使用专线,改善机器的物理拓扑(同物理机>同机架>跨机架>同机房>同城机房>异地机房,注意:容灾性相反)

6. Redis高性能的原因:

6.1.纯内存

Redis将所有数据放在内存中,而内存的相应时长在100纳秒左右。

6.2.非阻塞I/O

Redis使用了epoll多路复用技术,另外Redis自身的事件处理模型将epoll中的连接、 读写、关闭都转换为事件。

6.3.没有线程切换(Redis 6已经支持了多线程)

Redis采用了单线程架构,避免了线程切换、竞态和锁等产生的自我消耗。

7.Redis的Pipeline流水线

Redis提供了批量操作命令(如mget、 mset、hmset等),有效的节约了多次访问的往返时间,但是有些命令没有对应的批量命令(如hgetall没有对应的mhgetall),如果n次调用,就会消耗n次往返时间。Redis针对这一问题提供了Pipeline流水线机制,Redis将一组Redis命令组装到一起,通过一次调用Redis,再将执行结果按照请求顺序返回客户端。

8 Redis内存优化:

info memory命令可以显示内存的相关指标,主要有used_memory(Redis内部存储所有数据内存占用量),used_memory_ssr(操作系统显示Redis进程占用的物理内存)以及他们的比值mem_fragmentation_ratio。
当mem_fragmentation_ratio>1时,说明有部分内存没有用于数据存储,而是被内存碎片消耗,因此要降低mem_fragmentation_ratio的值,即降低碎片率。
当mem_fragmentation_ratio<1时,是因为操作系统把Redis的内存数据交换到磁盘,因此used_memory_ssr才会大于used_memory,由于硬盘的读写速度和内存差距很大,此时Redis的性能会很差。

8.2.内存的使用划分

8.2.1 Redis的进程自身的内存

这部分数据在几MB级别,很小可以忽略不计。

8.2.2 对象内存

即所有key-value数据使用的内存,包括所有key的消耗和所有value的消耗。

8.2.3 缓存内存

主要包括客户端缓存,复制积压区缓存,AOF缓存。

8.2.3.1 客户端缓存:

指的是所有接入到Redis服务器TCP连接的输入输出缓冲。输入缓冲无法控制(最大空间为1G), 如果超过将断开连接。 输出缓冲通过参数控制(默认为1G)
a.客户端:可以通过设置最大客户端连接数(特别是有慢连接时)来限制大量客户端接入造成的内存消耗
b.主从复制客户端:当主从复制延迟较高或从节点挂载从节点数量过多时,内存消耗将增加。可通过改善网络环境和减少主节点挂载的从节点数来解决。
c.订阅客户端:使用发布/订阅时, 连接客户端使用独立的输出缓冲区,对于订阅消息短时间内量比较大时,输出缓冲区会产生积压溢出。
d 复制积压缓存区
复制积压缓存区主要用来实现主从复制功能,一个主节点只有一个复制积压缓存区,所有从节点共享该缓存区。
e AOF缓冲区
AOF缓冲区用于在Redis重写期间保存最近的写入命令,AOF缓存区的消耗大小由AOF重写时间和写入命令量决定,这部分空间占用量比较小。

8.2.3.2 内存碎片

Redis提供了jemalloc(默认),glibc和tcmalloc三种内存分配器。当存储的数据长短差异较大时,做频繁的更新,删除大量key。主要解决碎片的方法有:
a.数据对齐,尽量采用数字类型或者固定长度字符串等;
b.Redis重启,内存会重新整理。

8.2.3.3 子进程内存消耗

子进程内存消耗主要指执行AOF/RDB重写时Redis创建的子进程内存消耗。

8.3 内存管理

8.3.1 控制内存上限

maxmemory参数设置最大可用内存,一方面方式所用内存超过物理内存,另一方面用于缓存,超过上限时,触发内存回收,以便释放空间。

8.3.2 内存回收

a.删除过期key对象
参考上文
b.内存溢出控制策略
内存达到maxmemory上限时会触发溢出控制策略,Redis提供了6种溢出控制策略:
1)noeviction:即拒绝写只响应读(默认);
2) volatile-lru:根据LRU(Least Recently Used,即最近最少使用),删除过期的key数据,以腾出空间。如果没有可删除的过期key数据,回退到noeviction;
3)allkeys-lru:不考虑过期key数据的LRU算法;
4)allkeys-random:随机删除所有键;
5)volatile-random:随机删除过期键;
6)volatile-ttl:根据键值对象的ttl属性, 删除将要过期的key数据。 如果没有可删除的数据, 回退到noeviction策略。

8.3.3 手动内存回收

可以使用scan+object idletime命令批量查询哪些key长时间未被访问, 然后对key进行清理, 可降低内存占用。

8.4 内存优化

8.4.1 缩减key(key值越短越好)和value的值的大小

主要方法有把key和value序列化成二进制数组数据(可以通过protostuff,kryo等高效序列化工具)或者序列化成json,xml压缩(如GZIP,Snappy压缩工具)再放入Redis,去掉value中不必要的字段。

8.4.2 共享对象池

共享对象池是指Redis内部维护[0-9999]的整数对象池,避免Redis封装RedisObject(Redis存储的数据都使用redisObject来封装)内存消耗。

8.4.3 字符串优化

a.Redis针对字符串采用了预分配机制,防止更新操作需要不断重分配内存和字节数据拷贝,但是同事造成了内存浪费。因此尽量减少字符串频繁修改操作(如append、setrange),直接使用set修改字符串。

8.4.4 字符串重构

避免每份数据作为字符串整体存储, 像json这样的数据可以使用hash结构, 使用二级结构存储能够节省内存。 可以使用hmget、 hmset命令支持字段的部分读取修改, 而不用每次整体存取。

8.4.5 编码优化

Redis通过不同编码实现效率和空间的平衡,编码类型转换在Redis写入数据时自动完成, 这个转换过程是不可逆的, 转换规则只能从小内存编码向大内存编码转换。针对性能要求较高的场景使用ziplist

8.4.6 控制key的数量

利用Redis的数据结构降低外层键的数量,比如把大量键分组映射到多个hash(用field记录副key)结构中降低键的数量。

9.Redis的经典问题

9.1 缓存穿透

缓存穿透:指查询的数据,在数据库不存在的情况。这样程序会先访问Redis,没有找到数据,再查询数据库,也没有找到,然后返回空。如果恶意大量的访问数据库中不存在的数据,造成资源浪费,对数据库造成压力,甚至会导致业务不可用,甚至压垮数据库。
解决方法:空值缓存,即如果数据库查询的时候,没有查询到数据,也将空值缓存到Redis中,并设置过期时间(相对普通的数据,过期时间设置较短,避免占用过大的存储空间),这样就避免造成对数据库的压力。

9.2 缓存雪崩

缓存雪崩: 指在某一时间段内,缓存数据集中过期失效。比如CRM系统,工作日早上9点左右,会迎来大量的数据访问,加入缓存是2个小时,那么到了11点,很多缓存数据就会过期,很多数据的访问都会落到数据库上,导致此时的数据库访问量大增,会对数据库产生周期性的压力。另外,Redis集群的某些节点宕机,也会导致缓存雪崩,而且是不可预知的,此时DB的访问量大增,甚至压垮DB。
解决方法: 针对不同的类型数据,如客户,销售订单等,采用不同的缓存过期时间;在同一个对象类中,采取随机因子的方式,设置不同的过期时间。

9.3 缓存击穿

缓存击穿: 指一个cache中的key并发访问量非常大,形成了热点。当这个key过期后,持续的大并发就会跳过缓存,直接访问DB,造成DB的压力。
解决方法: 对热点的key程序实现自动转为永不过期缓存,可以在value中设置超时时间,程序内部校验是否过期。如果过期,异步发起一个线程更新缓存(可以使用锁控制并发)。

10.哨兵(Sentinel)

哨兵是Redis的高可用解决方案,它由一个或者多个Sentinel实例组成Sentinel系统,Sentinel系统监视n个服务器以及对应的从服务器,当监视到主服务器线下时,自动将主服务器对应的某个从服务器升级为新的主服务器,代替主服务器继续处理请求。
哨兵系统架构:


Sentinel .jpg

10.1 定时监控

a.每个哨兵实例会间隔10秒向主节点和从节点发送info命令,获取最新的Redis的拓扑结构;
b.每隔2秒每个哨兵节点会向Redis数据节点的指定频道上发送 该节点对主节点的判断以及自身的信息,同时也从该频道上获取其他哨兵节点的发送的信息;
c.每隔1秒,每个哨兵节点会向主节点、从节点、其他哨兵节点发送ping命令,做心跳检测,来判断节点的可达性。

10.2 节点下线

主观下线
如果哨兵节点发送心跳检测,超过down_after_milliseconds后,仍然没有得到回复,哨兵节点会认为该节点为失效节点,称为主观下线。
客观下线
当一个哨兵节点主观下线的节点是Master时,该哨兵节点会向其他哨兵节点询问当前Master节点的状态,当超过quorum指定个数时,该哨兵节点认为Master节点确实失效,然后做出客观下线。

10.3 选举哨兵主节点

故障转移有一个哨兵节点负责,当哨兵系统有多个哨兵节点时,需要进行哨兵领导者选举:
当一个哨兵节点最先完成客观下线后,会向其他哨兵节点发送"我要成为领导者",其他哨兵节点如果没有同意过其他哨兵节点的请求,则返回同意该请求,否则拒绝。当该节点获得的同意票大于等于max(quorum,num(sentinels)/2 +1)时,该哨兵节点成为领导者;否则进入下一轮选举。实际选举过程非常快,基本谁先完成客观下线,谁就是领导者。

10.4 故障转移

哨兵领导者负责故障转移,具体过程如下:
1.选取slave节点作为Master节点;
 1.1 过滤失效的slave节点;
 1.2 如果有slave优先级,则根据slave节点的优先级返回slave节点,否则继续;
 1.3 选择复制偏移量最大(这样节省复制时间)的slave节点,如果没有则继续;
 1.4 选择runid最小的slave节点;
2.哨兵领导者根据第一步选举出的slave节点执行slaveof on one命令,使其成为Master;
3.哨兵领导者向剩余的slave节点发送命令,让他们成为新Master节点的从节点;
4.哨兵节点集合将旧的Master节点更改为Slave节点,并持续监控,当其恢复可用后,让它copy新的Master节点。

当前很少用哨兵,目前推荐使用Redis集群,这个的话就避免了单个主节点的问题。

11.Redis跳表

可参考文章,写的很详细。
https://www.cnblogs.com/Elliott-Su-Faith-change-our-life/p/7545940.html

跳表的优点:

1. 存储空间小
改变关于节点具有给定级别的概率的参数将使其比b树更少的内存占用
2. 范围查找
红黑树结构在范围查找时,要进行中序遍历;
跳表查询范围非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
3. 插入删除效率
红黑树的插入和删除会导致树的旋转,跳表的插入和删除只需要修改相邻节点的指针,简单又快速。

12.Redis的字典

Redis数据库使用字典作为底层的实现,利用字典提供CRUD操作。

12.1 Redis的Hash表

Redis使用的Hash表dict.h/dictht,具体格式如下:

typedef struct dictht {

    dictEntry **table;  // Hash表数组
    unsigned long size;// Hash表的大小
    unsigned long sizemasky;// Hash表的大小的掩码,大小总是等于 size-1,用于计算索引值
    unsigned long size;  // table中已有节点的数量

    
} dictht;

typedef struct dictEntry {
      // key
     void *key;

     // value,可以是指针*val、整数u64和整数s64的其中之一
     union {
       void *val;
       uint64_t u64;
       int64_t s64;
       } v;

struct dictEntry *next;  // 指向下一个Hash节点,形成链表,解决Hash冲突

}   dictEntry;


数据保存格式


Redis字典的Hash表存储结构.jpg

12.2 Redis字典

Redis的字典由dict.h/dict 结构:

typedef struct dict {
     // 类型特定函数,指向一簇用于操作特定类型key-value的函数,主要函数有hash值计算(HashFunction)、复制key(keyDup)、复制value(valDup)、对比key大小(keyCompare)、删除key(keyDestructor)和删除value(valDestructor)的函数等。
      dictType *type;
      // 私有数据,保存了传给dictType的参数
      void *private;
      // Hash表,字典只使用ht[0],ht[1] 只会在对ht[0]进行rehash时使用
      dictht ht[2];
      // rehash索引,记录当前rehash的进度,当rehash不在进行中,值为-1
      in trehashidx;

} dict;

没有进行rehash的字典数据结构:


没有进行rehash的字典数据结构.jpg

Hash值的计算:
hash = dict - > type -> hashFunction(key);
Redis使用MurmurHash2作为其Hash函数。

index的计算:
index = hash & dict - > ht[i].sizemask;
//字典一般只使用ht[0],ht[1] 只会在对ht[0]进行rehash时使用

冲突解决:
Redis使用链表方法来解决hash值的冲突,很类似JDK1.7的HashMap实现。

12.3 Redis的ReHash

针对Hash表保存的key-value的增加和减少,Redis提供了对hash表进行扩展或者收缩的功能,以让hash表的负载因子维持在一个合理的范围之内。
具体步骤如下:
1.为ht[1] Hash表分配空间:
如果是扩展(负载因子大于1或者负载因子大于5(如果正在执行BGSAVE或者BGREWRIGEAOF时)),ht[1]的大小为第一个大于等于ht[0].used * 2的2^n;
如果是收缩(负载因子小于0.1时),ht[1]的大小为第一个大于等于ht[0].used 的 2的2^n.
2.将ht[0]中的所有key-value 重新散列到ht[1] 上,需要重新计算hash值和索引值,因为ht[1]的长度和ht[0]不一样;
3.迁移完成后,释放ht[0]的空间,交换ht[0]和ht[1],为下一次rehash准备。

负载因子load_facotr = ht[0].used / ht[0].size;

13.Redis Cluster

Redis 3.0之后,Redis推出了Redis集群,解决了单点的问题,每个节点之间是去中心化的,并且提供了sharding(数据分片)、replication(主从复制)、failover(故障转移)等解决方案。Redis Cluster的最小配置节点个数为6个(即3主3从,主节点提供读写,从节点不提供请求服务),最大16384个节点,采用虚拟槽分区的原理,即根据key的Hash值映射到0~16383个整数槽内。另外,Redis集群具有感知主备的能力。

注意Redis Cluster和哨兵的却别,而且并不是功能重叠,也不是相互替换的。

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