redis 命令大全

公用命令

可以删除任意数据结构的key

del key 

查看所有key

keys *

根据给定的选项,对输入列表、集合或者有序集合进行排序,然后返回或者存储排序的结果。

sort source-key [by pattern] [limit offset count] [get pattern [get pattern ...]] [asc|desc] [alpha] [store dest-key] 

查看数据库有多少个key

dbsize

清除所有库的数据

flushall

清除当前库的所有数据

flushdb

查看reids内存信息等

info

监控所有客户发送的命令(redis编译执行成功的命令)

monitor

处理时间

移除键的过期时间

persist key 

查看给定键距离过期还有多少秒

ttl key 

给定键在指定的秒数后过期

expire key seconds 

将给定键的过期时间设置为给定的unix时间戳

expireat key timestamp

查看给定键距离过期时间还有多少毫秒,这个命令在reids2.6以上版本可用。

pttl key 

让给定键在指定的毫秒数之后过期,这个命令在redis2.6以上版本可用。

pexpire key milliseconds 

将一个毫秒级精度的unix 时间戳设置为给定键的过期时间,这个命令在redis2.6以上版本可用。

pexpireat key timestamp-milliseconds 

自增和自减

将键存储的值+1

incr key 

将键存储的值-1

decr key 

将键存储的值加上整数amount

incrby key amount 

将键存储的值减去整数amount

decrby key amount 

将键存储的值加上浮点数amount,这个命令在Redis2.6以上版本可用

incrbyfloat key amount 

字符串 String 可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB

创建一个键值对 set key value

127.0.0.1:6379> set a 1234567890
OK

获取key存储的值 get key

127.0.0.1:6379> get a
"1234567890"

将值value追加到给定 key 当前存储的值的末尾,追加成功的发返回值为这个字符串的长度(length) append key value

127.0.0.1:6379> append a a
(integer) 11
127.0.0.1:6379> get a
"1234567890a"

获取一个由偏移量start至end范围内的所有字符组成的字串,包括start和end在内。getrange key start end

127.0.0.1:6379> getrange a 0 3
"1234"

将从start偏移量开始的字串设置为给定值 setrange key offset value

127.0.0.1:6379> setrange a 3 bcde
(integer) 11
127.0.0.1:6379> get a
"123bcde890a"

列表 list 用来存储多个有序的字符串,列表中的每个字符串称为元素(element),一个列表最多可以存储2^32 -1个元素

将一个或多个值推入列表的右端 rpush key value[...]

127.0.0.1:6379> LRANGE b 0 -1
1) "5"
2) "6"
127.0.0.1:6379> RPUSH b 7 8
(integer) 4
127.0.0.1:6379> LRANGE b 0 -1
1) "5"
2) "6"
3) "7"
4) "8"

获取 list 元素数量 llen

127.0.0.1:6379[1]> llen 32021420001:90000300009999:10002:1601198414621
(integer) 2

lpush key value[...] -> 将一个或多个值推入列表的左端

127.0.0.1:6379> LPUSH b 1 2
(integer) 6
127.0.0.1:6379> LRANGE b 0 -1
1) "2"
2) "1"
3) "5"
4) "6"
5) "7"
6) "8"

rpop key -> 移除并返回列表最右端的元素

127.0.0.1:6379> rpop b
"8"
127.0.0.1:6379> LRANGE b 0 -1
1) "2"
2) "1"
3) "5"
4) "6"
5) "7"

lpop key -> 移除并返回列表最左端的元素

127.0.0.1:6379> lpop b
"2"
127.0.0.1:6379> LRANGE b 0 -1
1) "1"
2) "5"
3) "6"
4) "7"

lindex key offset -> 返回列表中偏移量为offset的元素(下标从0开始)

127.0.0.1:6379> lindex b 3
"7"

lrange key start end -> 返回列表从start偏移量到end偏移量范围的所有元素。包含start 和 end 本身。lrange key 0 -1 为查询所有

127.0.0.1:6379> LRANGE b 0 2
1) "1"
2) "5"
3) "6"

ltrim key start end -> 对列表进行修键,只保留从start至end偏移量的元素,包含start 和 end。

127.0.0.1:6379> LTRIM b 1 2
OK
127.0.0.1:6379> LRANGE b 0 -1
1) "5"
2) "6"

blpop key[...] timeout -> 从第一个非空列表中弹出位于最左端的元素,或者在timeout秒之内阻塞并等待可弹出的元素出现(一下测试没有元素所以nil)

127.0.0.1:6379> BLPOP c 10
(nil)
(10.03s)

brpop key[...] timeout -> 从第一个非空列表中弹出位于最右端的元素,或者在timeout秒之内阻塞并等待可弹出的元素出现(一下测试没有元素所以nil)

127.0.0.1:6379> brpop c 10
(nil)
(10.07s)

rpoplpush source-key dest-key -> 从source-key 列表中弹出位于最右端的元素,然后将这个元素推入dest-key 列表的最左端,并向用户返回这个元素

brpoplpush source-key dest-key timeout -> 从source-key 列表中弹出位于最右端的元素,然后将这个元素推入dest-key 列表的最左端,并向用户返回这个元素;如果source-key为空,那么在timeout秒之内阻塞并等待可弹出的元素出现。

集合 set 用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素

sadd key item[...] -> 将一个或多个元素添加到集合里面,并返回被添加元素当中原本并不存在于集合里面的元素数量。

srem key item[...] -> 从集合里面删除一个或多个元素,并返回被移除元素的数量。

sismember key item -> 检查元素item是否存在于集合key里面。

scard key -> 返回集合包含的元素数量。

smembers key -> 返回集合包含的所有元素。

srandmember key [count] -> 从集合里随机的返回一个或多个元素,当count 为正整数时,命令返回的随机元素不会重复;当count为负数时,命令返回的随机元素可能会出现重复。

spop key -> 随机的移除集合中的一个元素,并返回被移除的元素。

smove source-key dest-key item -> 如果集合source-key包含元素item,那么从集合source-key 里面移除y元素item,并将元素item添加到集合dest-key中;如果item成功移除,那么命令返回1,否则返回0;

sdiff key[...] -> 返回那些存在于第一个集合、但不存在于其他集合中的元素(数学上的差集运算)

sdiffstore dest-key key[...] -> 将那些存在于第一个集合但不存在于其他集合的元素(数学上的差集运算)存储到dest-key 键里面

sinter key[...] -> 返回那些同时存在于所有集合中的元素(数学上的交集运算)

sinter dest-key key[...] -> 那些同时存在于所有集合中的元素(数学上的交集运算)存储到dest-key键里面。

sunion key[...] -> 返回那些至少存在于一个集合中的元素(数学上讲的并集运算)

sunionstore dest-key key[...] -> 那些至少存在于一个集合中的元素(数学上讲的并集运算)存储到dest-key键里面。

散列 hash 哈希类型是指键值本身又是一个键值对结构,value={{field1,value1},…{fieldN,valueN}}

hset key-name key value -> 创建一个散列并赋值

hget key-name key -> 返回指定散列键的值

hgetall key-name -> 获取散列包含的所有键值对

hmset key-name key value[key value ...] -> 为散列里面的一个或多个键设置值

hmget key-name key[...] -> 从散列里面获取一个或多个键的值

hlen key-name -> 返回散列包含的键值对数量

hdel key-name key[...] -> 删除散列里面的一个或多个键值对,返回成功找到并删除的键值对数量。

hexists key-name key -> 检查给定的键是否存在于散列中

hkeys key-name -> 获取散列包含的所有键

hvals key-name -> 获取散列包含的所有值

hincrby key-name key increment -> 将键key存储的值加上整数increment

hincrbyfloat key-name key increment -> 将键key存储的值加上浮点数increment

有序集合 zset 不能有重复的元素,而且还可以排序,它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据(默认升序)

zadd key score member [score member ...] -> 将带有给定分值的成员添加到有序集合里面

zrem key member[...] -> 从有序集合里面移除给定的成员,并返回被移除成员的数量

zcard key -> 返回有序集合包含的成员数量

zincrby key increment member -> 将member成员的分值加上increment

zcount key min max -> 返回分值介于min 和max 之间的成员数量

zrank key member -> 返回成员member在有序集合中的排名

zscore key member -> 返回成员member的分值

zrange key start stop [withscores] -> 返回有序集合中排名介于start 和 stop 之间的成员,如果给定了可选的 withscores 选项,那么会将成员的分值一并返回。zrange key 0 -1 查询所有成员

zrevrank key member -> 返回有序集合里成员member的排名,成员按照分值从大到小排列

zrevrange key start stop [withscores] -> 返回有序集合给定排名范围内的成员,按照分值从大到小排列

zrangebyscore key min max [withscores] [left offset count] -> 返回有序集合中,分值介于min和max之间的所有成员

zrevrangebyscore key max min [withscores] [left offset count] -> 获取有序集合中分值介于min和max之间的所有成员,并按照分值从大到小的顺序来返回他们。

zremrangebyrank key start top -> 移除有序集合中排名介于start 和 stop 之间的成员。

zremrangebyscore key min max -> 移除有序集合中分值介于min和max 之间的成员。

zinterstore dest-key key-count key[...] [weights weight[...] ] [aggregate sum|min|max] -> 对给定的有序集合执行类似于集合的交集运算

zunionstore dest-key key-count key[...] [weights weight[...] ] [aggregate sum|min|max] -> 对指定的有序集合执行类似于集合的并集运算

订阅/发布

subscribe channel[...] -> 订阅给定的一个或多个频道

unsubscribe channel[...] -> 退订给定的一个或多个频道,如果执行时没有给定任何频道,那么退订所有频道

publish channel message -> 向给定的频道发送消息

psubscribe pattern[...] -> 订阅与给定模式相匹配的所有频道

punsubscribe pattern[...] -> 退订给定的模式,如果执行时没有给定任何模式,那么退订所有模式。

Redis5 之 Streams 数据类型

Redis 5.0 全新的数据类型:streams,官方把它定义为:以更抽象的方式建模日志的数据结构。Redis的streams主要是一个append only的数据结构,至少在概念上它是一种在内存中表示的抽象数据类型,只不过它们实现了更强大的操作,以克服日志文件本身的限制。

如果你了解MQ,那么可以把streams当做MQ。如果你还了解kafka,那么甚至可以把streams当做kafka。

另外,这个功能有点类似于redis以前的Pub/Sub,但是也有基本的不同:

  • streams支持多个客户端(消费者)等待数据(Linux环境开多个窗口执行XREAD即可模拟),并且每个客户端得到的是完全相同的数据。

  • Pub/Sub是发送忘记的方式,并且不存储任何数据;而streams模式下,所有消息被无限期追加在streams中,除非用于显示执行删除(XDEL)。

  • streams的Consumer Groups也是Pub/Sub无法实现的控制方式。

streams数据结构

streams数据结构本身非常简单,但是streams依然是Redis到目前为止最复杂的类型,其原因是实现的一些额外的功能:一系列的阻塞操作允许消费者等待生产者加入到streams的新数据。另外还有一个称为Consumer Groups(消费组)的概念,这个概念最先由kafka提出,Redis有一个类似实现,和kafka的Consumer Groups的目的是一样的:允许一组客户端协调消费相同的信息流!

streams基础

为了理解streams的目的,以及如何使用它,我们先忽略掉所有高级特性,只把注意力放在数据结构本身,以及那些操作和访问streams的命令。这基本上也是大多数其他Redis数据类型共有的部分,例如Lists,Sets,Sorted Sets等。然而需要注意的是,Lists也有一个更复杂的阻塞式的API,例如BLPOP,BRPOP等。streams这方便的API也没什么不同,只是更复杂,更强大(更牛逼,哈)!

streams命令

废话不多说,先上手玩玩这个全新的数据类型。streams这个数据类型对应有如下13个操作命令,所有命令都以"X"开头:
XADD

  • 用法:XADD key ID field string [field string …]

  • 正如其名,这个命令就是用来添加的,给streams追加(append,前面提到过:streams主要是一个append only的数据结构)一个新的entry(和Java里的Map类似,Redis里的streams中的数据也称为entry)。

  • key:的含义就是同一类型streams的名称;

  • ID: streams中entry的唯一标识符,如果执行XADD命令时,传入星号(*),那么,ID会自动生成,且自动生成的ID会在执行XADD后返回,默认生成的ID格式为millisecondsTime+sequenceNumber,即当前毫秒级别的时间戳加上一个自增序号值,例如"1540013735401-0"。并且执行XADD时,不接受少于或等于上一次执行XADD的ID,否则会报错:ERR The ID specified in XADD is equal or smaller than the target stream top item;

  • field&string:接下来就是若干组field string。可以把它理解为表示属性的json中的key-value。例如,某一streams的key命名为userInfo,且某个用户信息为{"username":"afei", "password":"123456"},那么执行XADD命令如下:

127.0.0.1:6379> xadd userInfo * name afei password 123456
"1594456377383-0"
127.0.0.1:6379> 

由于命令中ID字段的值是 * ,所以自定生成ID,1594456377383-0 就是自动生成的ID。 XADD命令也支持显示指定ID,例如:XADD key 0-2 field string。

需要注意的是,ID的时间戳部分是部署Redis服务器的本地时间,如果发生时钟回拨会怎么样?如果发生时钟回拨,生成的ID的时间戳部分就是回拨后的时间,然后加上这个时间的递增序列号。例如当前时间戳1540014082060,然后这时候发生了时钟回拨,且回拨5ms,那么时间戳就是1540014082055。假设以前已经生成了1540014082055-0,1540014082055-1,那么这次由于时钟回拨,生成的ID就是1540014082055-2。所以允许自动生成的ID在发生时钟回拨时少于上次的ID,但是不允许显示指定一个少于上次的ID。

如果我们以相同的 key 插入一遍,他不会像普通的key-value一样做替换,而是在相同key维护这一个列表。

xdel

  • 用法:XDEL key ID [ID …]
    和XADD相反,这是命令用来从streams中删除若干个entry,并且会返回实际删除数,这个删除数可能和参数ID个数不等,因为某些ID表示的消息可能不存在。执行命令如下,第二个参数ID是不存在的,所以XDEL的返回结果是1:
127.0.0.1:6379> xdel userInfo 1594456377383-0
(integer) 1

xlen

  • 用法:XLEN key
    很好理解,这个命令就是用来返回相同 key 的数量,上面又说相同key是不会覆盖的,是维护了一个列表,所以这个是相同key 列表的size。执行如下:
127.0.0.1:6379> xadd user * name zhangsan
"1594457206541-0"
127.0.0.1:6379> xlen user
(integer) 1
127.0.0.1:6379> xadd user * name zhangsan sex 0 
"1594457274424-0"
127.0.0.1:6379> xlen user
(integer) 2
127.0.0.1:6379> xadd user * name zhangsan sex 0 password 123
"1594457287981-0"
127.0.0.1:6379> xlen user
(integer) 3
127.0.0.1:6379> xdel user 1594457287981-0
(integer) 1
127.0.0.1:6379> xlen user
(integer) 2
127.0.0.1:6379> 
streams三种查询模式

redis提供了三种查询streams数据的模式:

  • 范围查询:因为streams的每个entry,其默认生成的ID是基于时间且递增的;

  • 监听模式:类比linux中的tailf命令,实时接收新增加到streams中的entry(也有点像一个消息系统,事实上笔者认为它就是借鉴了kafka);

  • 消费者组:即Consumer Groups,特殊的监听模式。从一个消费者的角度来看streams,一个streams能被分区到多个处理消息的消费者,对于任意一条消息,同一个消费者组中只有一个消费者可以处理(和kafka的消费者组完全一样)。这样还能够横向扩容消费者,从而提升处理消息的能力,而不需要只让把让一个消费者处理所有消息。

xrange

  • 用法:XRANGE key start end [COUNT count]
    这个命令属于第1种模式,即基于范围查询。这个命令用来返回streams某个顺序范围下的元素,start参数是更小的ID,end参数是更大的ID。有两个特殊的ID用符号"-"和"+"表示,符号"-"表示最小的ID,符号"+"表示最大的ID:
127.0.0.1:6379> XRANGE user 1540014096298-0 1594457274424-0
1) 1) "1594457206541-0"
   2) 1) "name"
      2) "zhangsan"
2) 1) "1594457274424-0"
   2) 1) "name"
      2) "zhangsan"
      3) "sex"
      4) "0"
127.0.0.1:6379> xrange user - +
1) 1) "1594457206541-0"
   2) 1) "name"
      2) "zhangsan"
2) 1) "1594457274424-0"
   2) 1) "name"
      2) "zhangsan"
      3) "sex"
      4) "0"
3) 1) "1594458356220-0"
   2) 1) "name"
      2) "wu"

XRANGE还能实现遍历某个范围区间的功能,例如我想遍历2018-10-20号新增的用户信息。首先得到2018-10-20 00:00:00对应的时间戳为1539964800000,再得到2018-10-20 23:59:59对应的时间戳为1540051199000,然后执行如下命令:

127.0.0.1:6379> xrange user 1594457206541-0 1594458356220-0 count 1
1) 1) "1594457206541-0"
   2) 1) "name"
      2) "zhangsan"
127.0.0.1:6379> xrange user 1594457206541-0 1594458356220-0 count 2
1) 1) "1594457206541-0"
   2) 1) "name"
      2) "zhangsan"
2) 1) "1594457274424-0"
   2) 1) "name"
      2) "zhangsan"
      3) "sex"
      4) "0"

xrevrange

  • 用法:XREVRANGE key end start [COUNT count]
    这个命令也属于第1种模式,且和XRANGE相反,返回一个逆序范围。end参数是更大的ID,start参数是更小的ID

xread

  • 用法:XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key …] ID [ID …]
    很明显,这个命令就是用来实现第2个模式,即监听模式。其作用是返回streams中从来没有读取的,且比参数ID更大的元素。
    这里我开了两个窗口测试,且第一个没有做新增,第二个做了新增.
127.0.0.1:6379> XREAD COUNT 10 BLOCK 60000 STREAMS user 1594458356220-0



(nil)
(60.02s)
127.0.0.1:6379> XREAD COUNT 10 BLOCK 60000 STREAMS user 1594458356220-0
1) 1) "user"
   2) 1) 1) "1594458985980-0"
         2) 1) "name"
            2) "ceshi"
(13.37s)
127.0.0.1:6379> 

使用xread 会阻塞特定时间,等待比我ID大的数据,如果使用最小ID进行测试,会跟xrange user - + 效果是一样的。

注意:BLOCK为0表示一致等待知道有新的数据,否则永远不会超时。并且ID的值我们用特殊字符$表示,这个特殊字符表示我们只获取最新添加的消息。

127.0.0.1:6379> xread count 10 block 0 streams user $


xread 还支持同时监听多个streams

127.0.0.1:6379> XREAD BLOCK 0 STREAMS user_01 user_02 user_03 user_04  $ $ $ $



XREAD除了COUNT和BLOCK,没有其他选项了。所有XREAD是一个非常基本的命令。更多高级特性可以往下看接下来要介绍的XREADGROUP。

XREADGROUP

  • 用法:XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] STREAMS key [key …] ID [ID …]
    很明显,这就是第三种模式:消费者组模式。
  • 如果你了解kafka的消费者组,那么你就也了解了streams的消费者组。如果不了解也没关系,笔者简单解释一下,假设有三个消费者C1,C2,C3。在streams中总计有7条消息:1, 2, 3, 4, 5, 6, 7,那么消费关系如下所示:
    1 -> C1
    2 -> C2
    3 -> C3
    4 -> C1
    5 -> C2
    6 -> C3
    7 -> C1
  • 消费者组具备如下几个特点:
    • 同一个消息不会被投递到一个消费者组下的多个消费者,只可能是一个消费者。

    • 同一个消费者组下,每个消费者都是唯一的,通过大小写敏感的名字区分。

    • 消费者组中的消费者请求的消息,一定是新的,从来没有投递过的消息。

    • 消费一个消息后,需要用命令(XACK)确认,意思是说:这条消息已经给成功处理。正因为如此,当访问streams的历史消息时,每个消费者只能看到投递给它自己的消息。

XACK

  • 用法:XACK key group ID [ID …]
  • 这是消费者组相关的另一个重要的命令。标记一个处理中的消息为已被正确处理,如此一来,这条消息就会被从消费者组的pending消息集合中删除,类似MQ中的ack。

XGROUP

  • 用法:xgroup create key groupname id
  • 这也是消费者组的一个重要命令,这个命令用来管理消费者组,例如创建,删除等。
    XREADGROUP,XACK,XGROUP三种命令构成了消费者组相关的操作命令。
    目前XGROUP CREATE的streams必须是一个存在的streams,否则会报错。
    创建一个消费组
127.0.0.1:6379> XGROUP CREATE user GRP-AFEI $
OK

文中内容关于 Redis5 之 Streams 数据类型 来自 阿飞的博客

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