Redis知识点总结

NoSQL介绍和redis介绍

not only SQL:非关系型数据库;

作用:应用于海量数据用户数据的前提下的数据处理问题。
特征:可扩容;可伸缩;大数据量下高性能;灵活的数据模型;高可用

redis:远程字典服务,键值对数据库

redis特征:
①数据之间没有必然的联系;
②单线程工作机制;
③高性能;
④多数据类型支持;
⑤持久化支持,数据灾难恢复。

Redis的基本数据类型

String类型

String对象底层是int、raw、embstr
1.如果一个String对象的内容可以转换为long类型,那么该字符串就会被转换为long类型,而且对象类型会用int类型表示
2.普通的字符串有两种,embstr和raw,在redis3.0之后
2.1.如果字符串对象的长度小于39个字节,会使用embstr,
2.2.否则是用raw对象

# set key value
# 设置key为name,value为hu的string类型

# get key 
# get key

# strlen key 
# key对应的字符的长度

# incr key 
# key对应的value值加一 

# incrby key increment 
# key对应的value值加increment

# incrbyfloat key increment 
# key对应的value值加increment(类型为float)

:类型不能转换或者超范围,会报错,范围是2^63 - 1

hash类型

哈希对象的底层实现可以是ziplist或者hashtable
ziplist中的哈希对象是按照key1,value1,key2,value2这样的顺序来存储的,元素不多的时候,效率很高
hashtable是由dict这个结构来实现的

# hset key field value 
# field是键,value是值

# hget key field 
# 获取field对应的value值

# hgetall key 
# 获取所有的键值对,奇数时field,偶数时value

# hdel key field1 [field2...] 
# 删除指定field

# hmset key field1 value1 [field2 value2] 
# 一次性设置多个键值对

# hmget key field1 [field2] 
# 获取多个field对应的value值

# hlen key 
# 获取hash的长度,一个键值对对应一个长度

# hexist key field 
# 确认key是否有指定field的键值对存在

# hkeys key 
# 获取所有的field

# hvals key 
# 获取所有的value

# hincrby key field increment 
# field对应的value值加increment

# hincrbyfloat key field increment 
# field对应的value值加类型为flaot的increment

:①hash中的value只能存储字符串;②每个hash类型最大存储2^31-1个键值对;③hgetall谨慎使用

list类型

list对象底层可以是 ziplist或者linkedlist
ziplist是一种压缩链表,节省空间,所存储的内容都是在连续的内存区域中的。
1.当list对象元素不大,每个元素也不大的时候,采用ziplist存储
当数据量过大时,ziplist不是那么好用,因为为了保证内存的连续性,此时的时间复杂度事O(N)
2.数据量过大是,使用linkedlist,是一个双向链表,插入方便

# lpush key value 
# 从key的左边添加元素

# rpush key value 
# 从key的右边添加元素

# lrange key start end 
# 获取从start到end之间的元素

# lindex key index 
# 获取指定下标下的值

# llen key 
# 获取key的长度

# lpop key 
# 从左边移除元素

# rpop key 
# 从右边移除元素

# blpop key timeout 
# 从左边在规定时间内部移除最左边元素

# brpop key timeout 
# 从右边在规定时间内部移除最右端元素

# lrem key count value 
# 移除值为value的元素count个

:①数据时string类型的,总长度为2^32-1;②具有索引的概念;③获取全部数据操作将结束索引设置为-1;④可以分页

set类型

set对象底层可以是intset或者hashtable
intset是一个正数集合,里面存放的时某种同一类型的整数
支持一下三种长度的整数
define INTSET_ENC_INT16 (sizeof(int16_t))
define INTSET_ENC_INT32 (sizeof(int32_t))
define INTSET_ENC_INT64 (sizeof(int64_t))

intset是一个有序集合,查找元素的时间复杂度时O(logN)
但是插入不一定是O(logN),因为可能涉及到升级的操作,
当set中是int16_t的整数,插入int32_t的整数,为了维护set中数据类型的一致,
所有的数据都会转换为int32_t,这个时候时间复杂度就是O(N),
而且set不支持降级操作

# sadd key member1 [member2]
# 添加元素

# smembers key 
# 获取所有的值

# srem key member1 [member2] 
# 删除元素

# scard key 
# 获取集合中的数据总数

# sismember key member 
# 是否有指定数据存在

# srandmember key [count] 
# 随机获取count个元素

# spop key 
# 随机获取集合中某个元素并将其移除

# sinter key1 key2 
# 获取两个集合的交集,存储在key1中

# sunion key1 key2 
# 获取两个集合的并集,存储在key1中

# sdiff key1 key2 
# 获取两个集合的差集,存储在key1中

# sinterstore destination key1 key2 
# 获取两个集合的交集,存储在指定集合中

# sunionstore destination key1 key2 
# 获取两个集合的并集,存储在指定集合中

# sdiffstore destination key1 key2 
# 获取两个集合的差集,存储在指定集合中

# smove source destination member 
# 将指定数据移动到指定集合

sorted_set

在set的基础上面做了排序,不同的是每个元素都会关联一个double类型的score;redis正是通过分数来为集合中的成员进行从小到大的排序。

有序集合可以是ziplist,也可以是skiplist和dict的结合
ziplist作为zset和作为hash是一样的,member和score顺序存放,
按照score从小到大顺序排列。
skiplist作为一种跳跃表,实现了zset的快速查找,大多数情况下他的速度和平衡树差不多

typedef struct zset {
// 字典
dict *dict;
// 跳跃表
zskiplist *zsl;
} zset;
// 使用两者相加的原因
// 1.单一的使用hashtable,可以快速地查找,添加,删除元素,但是没有办法保证有序性
// 2.单一的使用skiplist,查找太慢了,有序性可以保障
// 所以hashtable用于查找,skiplsit用于保证有序性

# zadd key score1 member1 [score2 member2] 
# 添加数据

# zrange key start stop 
# 获取数据

# zrevrange key start end 
# 逆序获取数据

# zrem key member1 [member2] 
# 移除元素

# zrangebuscore key min max [withscore] [limit] 
# 获取区间score之间的数据

# zrangebyscore key max min [withxcore] [limit] 
# 逆序获取

# zremrangebyrank key start end 
# 删除索引区间内部的元素

# zremrangebyscore key start end 
# 删除score区间内部的数据

# zcard key 
# 查询全部数量

# zcount min max 
# 查询指定范围的元素数量

# zinterstore destination numkeys key1 [key2] 
# 交集

# zunionstore destination numkeys key1 [key2] 
# 并集

# zrank key number 
# 获取数据的索引

# zrevrank key number 
# 获取数据的逆序索引

# zscore key member 
# 获取member的score

# zincrby key increment member 
# 对member的score操作

key的通用操作

# del key 
# 删除key

# exists key 
# key是否存在

# type key 
# key的类型

# expire key seconds 
# 设置key的有效时间(秒)

# pexpire key milliseconds 
# 设置key的有效时间(毫秒)

# ttl key 
# 查看key的有效时间(秒)

# pttl key 
# 查看key的有效时间(毫秒)
# 有效时间结束为-2,若时永久数据为-1

# keys * 
# 查看所有的key "*"匹配任意数量的任意字符;"?"匹配任意的一个字符;"[]"匹配一个指定的字符

# rename key newname 
# 重命名

# renamenx key newname 
# 如果存在则重命名

# sort list/set/sorted_set 
# 排序,只能是list/set/sorted_set

# select index 
# 切换数据库,默认为0,一共有16个(0-15)

# flushdb 
# 清除当前数据库

# flushall 
# 清除所有数据

Redis底层数据结构

持久化

为了数据的安全;持久化的方式:RDB、AOF

RDB

save指令,每次save都会保存一次数据,以二进制的数据报错

# save的相关配置
# save 60 1 
# 当60s发生一次数据改变,就自动save

# dbfilename dump.rdb
# 设置本地数据库的文件名,默认为dump.rdb

# dir
# 设置存储.rdb文件的路径

# rdbcompression yes
# 是否采用压缩格式,是的话采用LZF压缩

# rdbchecksum yes
# 是否开启文件校验,在读和写的时候均会进行

save指令的工作原理

执行save指令的时候会阻塞当前服务器,直到当前RDB过程执行完毕为止,有可能会造成长时间的阻塞。

bgsave指令的工作原理

①发送指令,返回消息”bgsaving started”;
②调用fork函数生成子进程;
③子进程来创建rdb文件;
④返回信息给redis服务端。

RDB的优缺点

优点 缺点
二进制存储,存储效率高 无法做到实时性持久化
比AOF的恢复速度快 bgsave需要创建子进程
数据备份,灾难恢复快 多个版本文件格式未进行统一

AOF

AOF(append only file):以独立日志的方式记录每条写命令,重启时执行存储的写命令达到恢复数据的目的。

主要作用:保持数据持久化的实时性

AOF写数据的三种策略

①always:每次写操作都会同步到AOF文件中,误差小,性能低
②everysec:每秒同步一次,误差较小,性能较高
③no:由系统控制,整体过程不可控

# 开启功能

# appendonly yes/no 
# 是否开启AOF持久化,默认不开启

# appendsync always/everysec/no 
# AOF的写策略

AOF重写

当数据不断写入,AOF文件会变得越来越大,为了解决此问题,redis引入AOF重写机制来压缩文件体积,对于一个数据的操作执行最后的指令,保存最终结果。

AOF重写的作用

①降低磁盘占用量,提高磁盘的利用率;
②提高持久化率,降低持久化写时间,提高IO性能;
③提高用时重写规则

重写规则

①已经超时的数据不会重写;
②忽略无效的数据;
③同一数据多条指令合并成一条指令

重写方式

①手动重写

# bgrewtireof -> 调用子进程

②自动重写

# auto_aof_rewrite_min_size size 
# 设置最小重写的指令数,超过就重写

# auto_aof_rewrite_percentage percentage
# 当前AOF的大小和上一次重写时AOF文件的大小

# aof_current_size
# 当前AOF文件的大小

# aof_base_size
# 基准文件的大小

# 触发条件
# aof_current_szie > auto_aof_rewrite_min_size
# (aof_current_size - aof_base_size) / aof_base_size >= auto_aof_rewrite_percentage

RDB和AOF的对比

持久化方式 RDB AOF
占用存储空间 小,二进制压缩文件 大,存储的是指令
存储速度
恢复速度
数据安全性 会丢失 依据写策略来决定
资源消耗
启动优先级

事务

基本操作

# multi
# 开启事务,设定事务的开启位置,从此处指令执行后,后续的所有指令都加入到队列中并且不执行

# exec
# 执行事务,设定事务的结束位置,同时执行事务,与multi成对出现,成对使用

# discard
# 终止事务,发生在multi和discard之间


①若是事务中有语法错误,则事务中所有的指令都不执行;
②若是指令格式正确,但是不能执行,则能执行的命令执行,不能执行的命令不执行,而且已经执行的任务不能回滚。

删除策略

过期数据:具有时效性的数据在expire之后还未被删除的数据。
所有时效性的数据在redis中一哈希表的形式存储,key是数据的地址,value是key的过期时间。

数据的删除策略

①定时删除:设置定时器,当过期时间达到时,由定时器任务立即执行对key的删除

优点:节约内存;
缺点:CPU压力大

②惰性删除:过期数据等到下次访问的时候再删除;expireIfNeeded()函数

优点:CPU压力小
缺点:耗费内存

③定期删除:Redis启动服务器初始化时,读取配置server.hz的值,默认每秒执行server.hz次ServerCron() -> databases() -> ActiveExpireCycle()函数,activeExpireCycle()函数对每个expire[*]逐一进行检测,每次检测250ms/server.hz.

对于某个expire[ * ] 检测时,随机的挑W个key进行检测:a.如果key已经超时,则删除key;b.如果在此轮删除的key数量大于W * 0.25,则循环;c.如果小于W * 0.25,检查下一个expire[*] 。

逐出算法

作用:用来解决空间不足的问题

Redis使用内存存储数据,执行每一个指令之前,会使用freeMemoryIfNeeded来检测内存空间是否充足,如果内存不足的话,redis会临时删除一些数据为当前指令清理空间,清理的过程称为逐出算法

:逐出算法可能不能够100%清理出足够的空间,不成功则反复执行,所有尝试之后还不能则返回OOM

影响逐出算法的相关配置:

# maxmemory 
# 最大可用内存,默认全部使用

# maxmemory-samples
# 每次选取的删除的数据个数

# maxmemory-policy
# 逐出策略

# 1.检查易失数据(可能会过期的数据集,server.db[i].expires)
# ① volatile-lru:最近最少使用的数据淘汰
# ② volatile-lfu:最近使用次数最少的数据淘汰
# ③ volatile-ttl:挑选即将过期的数据
# ④ volatile-random:随机挑选

# 2.检测全库数据(server.db[i].dict)
# ① allkeys-lru:最近最少使用
# ② allkeys-lfu:最近使用次数最少的数据
# ③ allkeys-random:随机挑选

# 3.放弃逐出策略
# ① no-enviction

高级数据类型

bitmaps

是对bit进行操作的数据类型

# setbit key offset value
# 设置key上的偏移量上的bit值为value,value只能为0 or 1

# getbit key offset 
# 获取指定key偏移量上的bit值

# bitcount key start end
# 统计key中指定长范围下为1的数量

# bitop op destkey key1 [key2...]
# 对指定的key进行操作,将结果存入到destkey中
# and/or/not/xor

HyperLogLog

统计不重复的数量,基数是数据中不重复的数量

# pfadd key element1 [element2]
# 添加数据

# pfcount key1 [key2]
# 统计数据

# pfmerge deskey sourcekey1 [sourcekey2]
# 合并数据

GEO

存储地理位置信息

# geoadd key longitude latitude member
# 添加数据

# geopos key member
# 获取数据的经纬度

# geodist member1 member2
# 计算两个地理位置的距离,默认为m

主从复制

多台服务器连接方案:
提供数据方:master -> 主节点,主服务器,主库
接受数据方:slave -> 从节点,从服务器,从库

主从复制:把master的数据即时的,有效的复制到slave中。一个master可有多个slave,一个slave只能由一个master

作用:
①读写分离,master写数据,slave读数据,提高服务器的读写负载均衡;
②负载均衡:基于主从复制结构,配合需求改变slave的数量,通过多个从节点分担数据获取负载,大大提高redis的服务器并发量和数据吞吐量;

建立连接阶段

①设置master的地址和端口,保存master信息
②简历socket连接
③发送ping
④身份认证
⑤发送slave端口信息

此时的状态
slave的状态:保存master的地址和端口;
master的状态:保存slave的端口;
两者之间有创建连接的socket

三种实现方式:
①在slave服务器上面加入 slaveof host port;
②在启动服务器时加入 如 redis-server ./conf/redis-6379.conf –slaveof host port;
③在配置文件中加入 slave of host port

断开连接:slave of no one

数据同步阶段

①请求同步数据(slave -> master)
②master执行bgsave指令,创建RDB文件并创建缓冲区;
③slave接受RDB文件,清空数据,执行RDB的恢复过程;
④请求部分数据(发送的时缓冲区中的指令,AOF),所以slave接收到数据之后使用bgrewriteaof来后台执行;
⑤恢复部分数据

注重点!!!:全量复制和部分复制

全量复制:在执行bgsave的时候,master将数据通过RDB的当时发送给slave

部分复制:在全量复制的过程中,将这个阶段的指令存入到缓冲区中,等全量复制结束之后,将缓冲区中的指令通过AOF的形式发送到slave中,通过bgrewriteaof来执行恢复

此时的状态
slave:有master的所有数据,包括RDB过程中接收到的数据;
master:保存了slave当前同步数据的位置

命令传播阶段

出现断网的情况:

①闪断闪联:忽略;②短时间中断:部分复制;③长时间中断:全量复制

部分复制的三个要素:

①服务器运行的id(Runid):每次运行,每个服务器都有一个Runid,40位16进制的随机数
②主服务器的复制缓冲区:队列,每次当master向slave发送时,会将记录存储在复制缓冲区中;

缓冲区包括两行,第一行时偏移值,第二行是具体的字节;

例如 set name hu先转换成AOF存储的形式3\r\nset\r\n4\r\nname\r\n$2\r\nhu\r\n,然后存储在缓存区中

偏移量 4321 4322 4323 4324
字节指令 $ 3 \r \n s e t \r \n $ 4 \r \n n a m r

③主从服务器的复制偏移量:如上所示

心跳机制

salve:

指令:REPLCONF ACK offset
周期:1s
作用:①汇报slave的偏移量,获取最新的数据变更指令;②判断master是否存活

master:

指令:ping
周期:10s as default
作用:判断slave是否在线

总结主从复制的完整过程

master slave
建立连接阶段
数据同步阶段(全量复制、部分复制)
①发送指令replconf ack offset(slave)
②接收到命令,判断offset(slave)是否在复制缓冲区中
③如果不在的话,全量复制; 如果在,且offset(slave) = offset(master),忽略; 如果 offset(slave) != offset(master),通过部分复制,把从offset(slave) 到offset(master)的数据传入到slave中
④收到offset(master),接受数据,执行完重新从①开始

Redis哨兵

Sentinel:是一个分布式系统,用于对主从结构中的每台服务器进行监测,当出现故障可以通过投票选出新的master并将slave连接到新的master中。

作用:
检测:检测master和slave是否能够正常运行;
通知:当被监控服务器出现问题时,向其他的哨兵发送通知;
自动故障转移:当master断开的时候,选取一个slave作为新的master,并和其他的slave相连接并告知客户端新的master地址。

配置哨兵

redis-sentinel sentinel-port.conf

工作原理

阶段一:监控,用于同步各个节点之间的状态
①获取各个sentinel的状态是否在线;
②获取master的状态:runid;role:master;以及该master各个slave的详细信息;
③获取master中所有slave的状态:runid;role:slave;master_host;master_host;offset

阶段二:通知阶段,哨兵集群之间的信息互通

阶段三:故障转移阶段

若sentinel向master发送信息,未接受到信息,则标记master为SRI_S_DOWN,并在sentinel内网中传播,使得其他sentinel都去向master求证是否真的下线,若超过半数的sentinel求证得到SDI_S_DOWN(主观下线),则标记master为SDI_O_DOWN(客观下线)并认为其下线。

若客观下线,则投票选出新的master,在sentinel内部,每个salve发送自己的被选择次数和runid,先进来的先被选择,若是有超过半数的slave,则选择成功,若没有,则进行下一轮的票选,此轮被选择的slave被选+1。

被选择的原则:①在线的slave;②响应快的slave;③与原来的master断开时间长的不选;④优先原则

当选中之后,向新的master发送slaveof no one,断开连接;②向其他的slave发送新master的runid,并连接新的master。

redis集群

cluster

集群:使用网络将若干计算机连接起来,提供一个统一的管理方式,使其对外呈现单机的效果。

作用:
①分散单台计算机的访问压力,实现负载均衡;
②分散单台计算机的存储压力,实现可扩展性;
③降低单机下线带来的损失。

哨兵故障转移期间redis不可用,所以使用cluster集群,内部实现哨兵的作用但是不需要哨兵。

redis-cli -c 来开启集群

数据存储设计:

①通过算法设计,计算key应该保存的位置;
②将所有的存储空间划分成16384份(slot,槽),每台主机保存一部分;通过哈希算法得到数据所在的slot。

③将key按照计算除的结果放到对应的存储空间。

一致性哈希原理:将所有的数据当作一个token环,token中的数据范围市0-2^32,
为每一个数据节点分配一个token范围值,这个节点就负责保存这个范围内的数据。

对么一个key进行hash运算,被哈希后的结果在那个token范围内,则按顺时针去寻找最近的节点,
这个key将会被保存到这个节点

内部通讯设计:
①各个数据库相互通信,保存各个库中槽的编号;
②一次命中,直接返回;
③一次未命中,被告知位置,二次命中。
若master宕机,slave变成master,旧master重新上线,变成新master的slave。

其他

缓存与数据库的一致性

不一致分为三种
①数据库中有数据,缓存中没有数据;
②数据库中有数据,缓存中也有数据,数据不一致;
③数据库中没有数据,缓存中有数据;

缓存策略:cache Aside Pattern
①首先尝试从缓存中读取数据,若是成功,则直接返回,若是不成功,则读数据库,并把数据写道缓存中;
②需要更新数据的时候,先更新数据库,然后把缓存中的数据失效掉。

缓存预热

在系统启动前,提前将相关的缓存数据直接加载到缓存系统中,避免在用户请求的时候,先查询数据库再将数据缓存的问题。用户直接实现查询预热的数据。

缓存穿透

查询大量一个不存在的key,服务器绕过Redis直接向数据库去查询,极大的增加了数据库的压力。

解决方案:布隆过滤器

布隆过滤器

布隆过滤器:基于布隆算法,来解决缓存穿透问题。

布隆算法:对一个key通过k个hash算法计算出k个hash值,将这些hash值在bit数组中对应的位置置为1,然后查询的时候直接查看这k个位置都为1的话,则判断该key存在。在Redis中,使用bitmaps来作bit数组,支持2^32大小。

布隆算法的错误率:当布隆算法判断一个key不存在,则一定不存在,若是判断一个key存在,则不一定存在,有可能出现误判的情况。出现的原因是hash碰撞导致的。

缓存击穿

一个key在过期的时候还是超热数据,服务器直接绕过Redis去数据库访问,增大压力

解决方案:第三方缓存;分布式锁

分布式锁

# setnx key value
# 设置分布式锁

# expire key time
# 设置时间防止死锁

缓存雪崩

①同一时间多个可以过期,但是还是热数据,服务器绕过Redis去访问数据库,增大压力;②Redis宕机

解决方案

针对①:给过期的key增加随机时间,让所有的key均匀的过期而不是集中的过期。

针对②:设置Redis集群

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