Redis存储总是心里没底?你大概漏了这些数据结构原理

一、Redis内存数据结构与编码

想要弄清楚Redis内部如何支持5种数据类型,也就是要弄清Redis到底是使用什么样的数据结构来存储、查找我们设置在内存中的数据。

虽然我们使用5种数据类型来缓存数据,但是Redis会根据我们存储数据的不同而选用不同的数据结构和编码。

这些数据类型在内存中的数据结构和编码有很多种,随着我们存储的数据类型的不同、数据量的大小不同都会引起内存数据结构的动态调整。

本文只是做数据结构和编码的一般性介绍,不做过多细节讨论,一方面是关于Redis源码分析的资料网上有很多,还有一个原因就是Redis每一个版本的实现有很大差异,一旦展开细节讨论,每一个点每一个数据结构都会很复杂,所以我们这里就不展开讨论这些,只是起到抛砖引玉作用。

1、OBJECT encoding key、DEBUG OBJECT key

我们知道使用type命令可以查看某个key是否是5种数据类型之一,但是当我们想查看某个key底层是使用哪种数据结构和编码来存储的时候,可以使用OBJECT encoding命令。

同样一个key,由于我们设置的值不同,Redis选用了不同的内存数据结构和编码。虽然Redis提供String数据类型,但是Redis会自动识别我们cache的数据类型是int还是String。

如果我们设置的是字符串,且这个字符串长度不大于39字节,那么将使用embstr来编码;如果大于39字节将使用raw来编码。Redis4.0将这个阀值扩大了45个字节。

除了使用OBJECT encoding命令外,我们还可以使用DEBUG OBJECT命令来查看更多详细信息。

DEBUG OBJECT能看到这个对象的refcount引用计数、serializedlength长度、lru_seconds_idle时间,这些信息决定了这个key缓存清除策略。

2、简单动态字符串(simple dynamic String)

简单动态字符串简称SDS,在Redis中所有涉及到字符串的地方都是使用SDS实现,当然这里不包括字面量。SDS与传统C字符串的区别就是SDS是结构化的,它可以高效的处理分配、回收、长度计算等问题。

structsdshdr {unsignedintlen;unsignedintfree;charbuf[]; };

这是Redis3.0版本的sds.h头文件定义,3.0.0之后变化比较大。len表示字符串长度,free表示空间长度,buf数组表示字符串。

SDS有很多优点,比如,获取长度的时间复杂度O(1),不需要遍历所有charbuf[]组数,直接返回len值。

staticinline size_t sdslen(constsds s) {structsdshdr*sh = (void*)(s-(sizeof(structsdshdr)));returnsh->len; }

当然还有空间分配检查、空间预分配、空间惰性释放等,这些都是SDS结构化字符串带来的强大的扩展能力。

3、链表(linked list)

链表数据结构我们是比较熟悉的,最大的特点就是节点的增、删非常灵活。Redis List数据类型底层就是基于链表来实现。这是Redis3.0实现。

typedefstructlist{     listNode *head;     listNode *tail;void*(*dup)(void*ptr);void(*free)(void*ptr);int(*match)(void*ptr,void*key);unsignedlonglen; }list;typedefstructlistNode {structlistNode *prev;structlistNode *next;void*value; } listNode;

在Redis3.2.0版本的时候引入了quicklist链表结构,结合了linkedlist和ziplist的优势。

typedefstructquicklist {     quicklistNode *head;     quicklistNode *tail;unsignedlongcount;/* total count of all entries in all ziplists */unsignedintlen;/* number of quicklistNodes */intfill :16;/* fill factor for individual nodes */unsignedintcompress :16;/* depth of end nodes not to compress;0=off */} quicklist;typedefstructquicklistNode {structquicklistNode *prev;structquicklistNode *next;unsignedchar*zl;unsignedintsz;/* ziplist size in bytes */unsignedintcount :16;/* count of items in ziplist */unsignedintencoding :2;/* RAW==1 or LZF==2 */unsignedintcontainer :2;/* NONE==1 or ZIPLIST==2 */unsignedintrecompress :1;/* was this node previous compressed? */unsignedintattempted_compress :1;/* node can't compress; too small */unsignedintextra :10;/* more bits to steal for future usage */} quicklistNode;

quicklist提供了灵活性同时也兼顾了ziplist的压缩能力,quicklist->encoding指定了两种压缩算法。quickList->compress表示我们可以进行quicklist node的深度压缩能力。Redis提供了两个有关于压缩的配置:

List-max-zipList-size:zipList长度控制;

List-compress-depth:控制链表两端节点的压缩个数,越是靠近两端的节点被访问的机率越大,所以可以将访问机率大的节点不压缩,其他节点进行压缩。

对比Redis3.2的quicklist与Redis3.0,很明显quicklist提供了更加丰富的压缩功能。Redis3.0的版本是每个listnode直接缓存值,而quicklistnode还有强大的有关于压缩能力。

LPUSHlist:products:mall100200300(integer)3OBJECT encodinglist:products:mall"quicklist"

4、字典(dict)

dict字典是基于Hash算法来实现,是Hash数据类型的底层存储数据结构。我们来看下Redis3.0.0版本的dict.h头文件定义:

typedefstructdict {     dictType *type;void*privdata;     dictht ht[2];longrehashidx;intiterators;  } dict;typedefstructdictht {     dictEntry **table;unsignedlongsize;unsignedlongsizemask;unsignedlongused; } dictht;typedefstructdictEntry {void*key;union{void*val;uint64_tu64;int64_ts64;doubled;     } v;structdictEntry *next; } dictEntry;

说到Hash table有两个东西是我们经常会碰到的,首先就是Hash碰撞问题,Redis dict是采用链地址法来解决,dictEntry->next就是指向下个冲突key的节点。

还有一个经常碰到的就是rehash的问题,提到rehash我们还是有点担心性能的。那么Redis实现是非常巧妙的,采用惰性渐进式rehash算法。

在dict struct里有一个ht[2]组数,还有一个rehashidx索引。Redis进行rehash的大致算法是这样的:

首先会开辟一个新的dictht空间,放在ht[2]索引上,此时将rehashidx设置为0,表示开始进入rehash阶段,这个阶段可能会持续很长时间,rehashidx表示dictEntry个数。每次当有对某个ht[1]索引中的key进行访问时,获取、删除、更新,Redis都会将当前dictEntry索引中的所有key rehash到ht[2]字典中。一旦rehashidx=-1表示rehash结束。

5、跳表(skip list)

skip list是Zset的底层数据结构,有着高性能的查找排序能力。

我们都知道一般用来实现带有排序的查找都是用Tree实现,不管是各种变体的B Tree还是B+Tree,本质都是用来做顺序查找。

skip list实现起来简单,性能也与B Tree相接近。

typedefstructzskiplistNode{     robj *obj;     double score;structzskiplistNode*backward;structzskiplistLevel{structzskiplistNode*forward;         unsignedintspan;     } level[]; } zskiplistNode;   typedefstructzskiplist{structzskiplistNode*header, *tail;     unsigned long length;intlevel; } zskiplist;

zskiplistNode->zskiplistLevel->span这个值记录了当前节点距离下个节点的跨度。每一个节点会有最大不超过zskiplist->level节点个数,分别用来表示不同跨度与节点的距离。

每个节点会有多个forward向前指针,只有一个backward指针。每个节点会有对象__*obj__和score分值,每个分值都会按照顺序排列。

6、整数集合(int set)

int set整数集合是set数据类型的底层实现数据结构,它的特点和使用场景很明显,只要我们使用的集合都是整数且在一定的范围之内,都会使用整数集合编码。

SADDset:userid100200300(integer)3OBJECTencodingset:userid"intset"

int set使用一块连续的内存来存储集合数据,它是数组结构不是链表结构。

typedefstructintset {uint32_tencoding;uint32_tlength;int8_tcontents[]; } intset;

intset->encoding用来确定contents[]是什么类型的整数编码,以下三种值之一:

#define INTSET_ENC_INT16 (sizeof(int16_t)) #define INTSET_ENC_INT32 (sizeof(int32_t)) #define INTSET_ENC_INT64 (sizeof(int64_t))

Redis会根据我们设置的值类型动态sizeof出一个对应的空间大小。如果我们集合原来是int16,然后往集合里添加了int32整数将触发升级,一旦升级成功不会触发降级操作。

7、压缩表(zip list)

zip list压缩表是List、Zset、Hash数据类型的底层数据结构之一。它是为了节省内存通过压缩数据存储在一块连续的内存空间中。

typedefstructzlentry {unsignedintprevrawlensize, prevrawlen;unsignedintlensize, len;unsignedintheadersize;unsignedcharencoding;unsignedchar*p; } zlentry;

它最大的优点就是压缩空间,空间利用率很高。缺点就是一旦出现更新可能就是连锁更新,因为数据在内容空间中都是连续的,最极端情况下就是可能出现顺序连锁扩张。

压缩列表会由多个zlentry节点组成,每一个zlentry记录上一个节点长度和大小,当前节点长度lensize和大小len包括编码encoding。

这取决于业务场景,Redis提供了一组配置,专门用来针对不同的场景进行阈值控制:

hash-max-ziplist-entries512hash-max-ziplist-value64list-max-ziplist-entries512list-max-ziplist-value64zset-max-ziplist-entries128zset-max-ziplist-value64

上述配置分别用来配置ziplist作为Hash、List、Zset数据类型的底层压缩阈值控制。

8、Redis Object类型与映射

Redis内部每一种数据类型都是对象化的,也就是我们所说的5种数据类型其实内部都会对应到Redis Object对象,然后再由Redis Object来包装具体的存储数据结构和编码。

typedefstructredisObject {unsignedtype:4;unsignedencoding:4;unsignedlru:REDIS_LRU_BITS;intrefcount;void*ptr; } robj;

这是一个很OO的设计,redisObject->type是5种数据类型之一,redisObject->encoding是这个数据类型所使用的数据结构和编码。

我们看下Redis提供的5种数据类型与每一种数据类型对应的存储数据结构和编码。

/* Object types */#defineREDIS_STRING 0#defineREDIS_LIST 1#defineREDIS_SET 2#defineREDIS_ZSET 3#defineREDIS_HASH 4#defineREDIS_ENCODING_RAW 0#defineREDIS_ENCODING_INT 1#defineREDIS_ENCODING_HT 2#defineREDIS_ENCODING_ZIPMAP 3#defineREDIS_ENCODING_LINKEDLIST 4#defineREDIS_ENCODING_ZIPLIST 5#defineREDIS_ENCODING_INTSET 6#defineREDIS_ENCODING_SKIPLIST 7#defineREDIS_ENCODING_EMBSTR 8

REDIS_ENCODING_ZIPMAP 3这个编码可以忽略了,在特定的情况下有性能问题,在Redis 2.6版本之后已经废弃,为了兼容性保留。

图是Redis 5种数据类型与底层数据结构和编码的对应关系:

但是这种对应关系在每一个版本中都会有可能发生变化,这也是Redis Object的灵活性所在,有着OO的这种多态性。

redisObject->refcount表示当前对象的引用计数,在Redis内部为了节省内存采用了共享对象的方法,当某个对象被引用的时候这个refcount会加1,释放的时候会减1。

redisObject->lru表示当前对象的空转时长,也就是idle time,这个时间会是Redis lru算法用来释放对象的时间依据。可以通过OBJECT idletime命令查看某个key的空转时长lru时间。

二、Redis内存管理策略

Redis在服务端分别为不同的db index维护一个dict,这个dict称为key space键空间。每一个RedisClient只能属于一个db index,在Redis服务端会维护每一个链接的RedisClient。

typedefstructredisClient {uint64_tid;intfd;     redisDb *db; } redisClient;

在服务端每一个Redis客户端都会有一个指向redisDb的指针。

typedefstructredisDb {     dict *dict;     dict *expires;     dict *blocking_keys;     dict *ready_keys;     dict *watched_keys;structevictionPoolEntry *eviction_pool;intid;longlongavg_ttl; } redisDb;

key space键空间就是这里的redisDb->dict。redisDb->expires是维护所有键空间的每一个key的过期时间。

1、键过期时间、生存时间

对于一个key我们可以设置它多少秒、毫秒之后过期,也可以设置它在某个具体的时间点过期,后者是一个时间戳。例如:

EXPIRE命令可以设置某个key多少秒之后过期;

PEXPIRE命令可以设置某个key多少毫秒之后过期;

EXPIREAT命令可以设置某个key在多少秒时间戳之后过期;

PEXPIREAT命令可以设置某个key在多少毫秒时间戳之后过期;

PERSIST命令可以移除键的过期时间。

其实上述命令最终都会被转换成对PEXPIREAT命令。在redisDb->expires指向的key字典中维护着一个到期的毫秒时间戳。

TTL、PTTL可以通过这两个命令查看某个key的过期秒、毫秒数。

Redis内部有一个事件循环,这个事件循环会检查键的过期时间是否小于当前时间,如果小于则会删除这个键。

2、过期键删除策略

在使用Redis的时候我们最关心的就是键是如何被删除的,如何高效准时地删除某个键。其实Redis提供了两个方案来完成这件事情:惰性删除、定期删除双重删除策略。

惰性删除:当我们访问某个key的时候,Redis会检查它是否过期。

robj *lookupKeyRead(redisDb *db, robj *key) {     robj *val;      expireIfNeeded(db,key);val= lookupKey(db,key);if(val== NULL)         server.stat_keyspace_misses++;elseserver.stat_keyspace_hits++;returnval; }  int expireIfNeeded(redisDb *db, robj *key) {     mstime_twhen= getExpire(db,key);     mstime_t now;if(when<0)return0;/* No expire for this key */if(server.loading)return0;      now = server.lua_caller ? server.lua_time_start : mstime();if(server.masterhost != NULL)returnnow >when;/* Return when this key has not expired */if(now <=when)return0;/* Delete the key */server.stat_expiredkeys++;     propagateExpire(db,key);     notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,"expired",key,db->id);returndbDelete(db,key); }

定期删除:Redis通过事件循环,周期性地执行key的过期删除动作。

intserverCron(structaeEventLoop *eventLoop,longlongid,void*clientData){/* Handle background operations on Redis databases. */databasesCron(); }voiddatabasesCron(void){/* Expire keys by random sampling. Not required for slaves 

     * as master will synthesize DELs for us. */if(server.active_expire_enabled && server.masterhost ==NULL)         activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW); }

要注意的是:

惰性删除是每次只要有读取、写入都会触发惰性删除代码;

定期删除是由Redis EventLoop来触发的。Redis内部很多维护性工作都是基于EventLoop。

3、AOF、RDB处理过期键策略

既然键会随时存在过期问题,那么涉及到持久化Redis是如何帮我们处理的?

当Redis使用RDB方式持久化时,每次持久化的时候就会检查这些即将被持久化的key是否已经过期,如果过期将直接忽略,持久化那些没有过期的键。

当Redis作为master主服务器启动的时候,载入rdb持久化键时也会检查这些键是否过期,将忽略过期的键,只载入没过期的键。

当Redis使用AOF方式持久化时,每次遇到过期的key Redis会追加一条DEL命令到AOF文件,也就是说只要我们顺序载入执行AOF命令文件就会删除过期的键。

如果Redis作为从服务器启动的话,它一旦与master主服务器建立链接就会清空所有数据进行完整同步。当然新版本的Redis支持SYCN2的半同步,如果是已经建立了master/slave主从同步之后,主服务器会发送DEL命令给所有从服务器执行删除操作。

4、Redis LRU算法

在使用Redis的时候我们会设置maxmemory选项,64位的默认是0不限制。线上的服务器必须要设置的,要不然很有可能导致Redis宿主服务器直接内存耗尽,最后链接都上不去。

所以基本要设置两个配置:

maxmemory最大内存阈值;

maxmemory-policy到达阈值的执行策略。

可以通过CONFIG GET maxmemory/maxmemory-policy分别查看这两个配置值,也可以通过CONFIG SET去分别配置。

maxmemory-policy有一组配置,可以用在很多场景下:

noeviction:客户端尝试执行会让更多内存被使用的命令直接报错;

allkeys-lru:在所有key里执行lru算法;

volatile-lru:在所有已经过期的key里执行lru算法;

allkeys-random:在所有key里随机回收;

volatile-random:在已经过期的key里随机回收;

volatile-ttl:回收已经过期的key,并且优先回收存活时间(TTL)较短的键。

关于cache的命中率可以通过info命令查看键空间的命中率和未命中率。

# Stats keyspace_hits:33keyspace_misses:5

maxmemory在到达阈值的时候会采用一定的策略去释放内存,这些策略我们可以根据自己的业务场景来选择,默认是noeviction 。

Redis LRU算法有一个取样的优化机制,可以通过一定的取样因子来加强回收的key的准确度。CONFIG GET maxmemory-samples查看取样配置,具体可以参考更加详细的文章。

三、Redis持久化方式

Redis本身提供持久化功能,有两种持久化机制,一种是数据持久化RDB,一种是命令持久化AOF,这两种持久化方式各有优缺点,也可以组合使用。一旦组合使用,Redis在载入数据的时候会优先载入aof文件,只有当AOF持久化关闭的时候才会载入rdb文件。

1、RDB(Redis DataBase)

RDB是Redis数据库,Redis会根据一个配置来触发持久化:

#save<seconds><changes>save9001save30010save6010000CONFIG GET save1)"save"2)"3600 1 300 100 60 10000"

表示在多少秒之类的变化次数,一旦达到这个触发条件Redis将触发持久化动作。

Redis在执行持久化的时候有两种模式BGSAVE、SAVE:

BGSAVE是后台保存,Redis会fork出一个子进程来处理持久化,不会block用户的执行请求;

SAVE则会block用户执行请求。

structredisServer {longlongdirty;/* Changes to DB from the last save */time_tlastsave;/* Unix time of last successful save */longlongdirty_before_bgsave;pid_trdb_child_pid;/* PID of RDB saving child */structsaveparam *saveparams;/* Save points array for RDB */}structsaveparam {time_tseconds;intchanges; };

RedisServer包含的信息很多,其中就包含了有关于RDB持久化的信息。

redisServer->dirty至上次save到目前为止的change数。redisServer->lastsave上次save时间。

saveparam struct保存了我们通过save命令设置的参数,time_t是个long时间戳。

typedef__darwin_time_ttime_t;typedeflong__darwin_time_t;/* time() */intserverCron(structaeEventLoop *eventLoop,longlongid,void*clientData){for(j =0; j < server.saveparamslen; j++) {structsaveparam *sp = server.saveparams+j;if(server.dirty >= sp->changes &&                 server.unixtime-server.lastsave > sp->seconds &&                 (server.unixtime-server.lastbgsave_try >                  REDIS_BGSAVE_RETRY_DELAY ||                  server.lastbgsave_status == REDIS_OK))             {                 redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...",                     sp->changes, (int)sp->seconds);                 rdbSaveBackground(server.rdb_filename);break;             }          } }

Redis事件循环会周期性的执行serverCron方法,这段代码会循环遍历server.saveparams参数链表。

如果server.dirty大于等于我们参数里配置的变化并且server.unixtime-server.lastsave大于参数里配置的时间,并且server.unixtime-server.lastbgsave_try减去bgsave重试延迟时间,或者当前server.lastbgsave_status== REDIS_OK则执行rdbSaveBackground方法。

2、AOF(Append-only file)

AOF持久化是采用对文件进行追加对方式进行,每次追加都是Redis处理的命令。有点类似command sourcing命令溯源的模式,只要我们可以将所有的命令按照执行顺序在重放一遍就可以还原最终的Redis内存状态。

AOF持久化最大的优势是可以缩短数据丢失的间隔,做到秒级的丢失率。RDB会丢失上一个保存周期到目前的所有数据,只要没有触发save命令设置的save seconds changes阈值数据就会一直不被持久化。

structredisServer {/* AOF buffer, written before entering the event loop */sds aof_buf;  }structsdshdr {unsignedintlen;unsignedintfree;charbuf[]; };

aof_buf是命令缓存区,采用sds结构缓存,每次当有命令被执行当时候都会写一次到aof_buf中。有几个配置用来控制AOF持久化的机制。

appendonlynoappendfilename"appendonly.aof"

appendonly用来控制是否开启AOF持久化,appendfilename用来设置aof文件名。

appendfsyncalways appendfsync everysec appendfsyncno

appendfsync用来控制命令刷盘机制。现在操作系统都有文件cache/buffer的概念,所有的写入和读取都会走cache/buffer,并不会每次都同步刷盘,因为这样性能一定会受影响。所以Redis也提供了这个选项让我们来自己根据业务场景控制。

always:每次将aof_buf命令写入aof文件并且执行实时刷盘。

everysec:每次将aof_buf命令写入aof文件,但是每隔一秒执行一次刷盘。

no:每次将aof_buf命令写入aof文件不执行刷盘,由操作系统来自行控制。

AOF也是采用后台子进程的方式进行,与主进程共享数据空间也就是aof_buf,但是只要开始了AOF子进程之后Redis事件循环文件事件处理器会将之后的命令写入另外一个aof_buf,这样就可以做到平滑的切换。

AOF会不断的追加命令进aof文件,随着时间和并发量的加大aof文件会极速膨胀,所以有必要对这个文件大小进行优化。Redis基于rewrite重写对文件进行压缩。

no-appendfsync-on-rewriteno/yes auto-aof-rewrite-percentage100auto-aof-rewrite-min-size64mb

no-appendfsync-on-rewrite控制是否在bgrewriteaof的时候还需要进行命令追加,如果追加可能会出现磁盘IO跑高现象。

上面说过,当AOF进程在执行的时候原来的事件循环还会正常的追加命令进aof文件,同时还会追加命令进另外一个aof_buf,用来做新aof文件的重写。这是两条并行的动作,如果我们设置成yes就不追加原来的aof_buf 因为新的aof文件已经包含了之后进来的命令。

auto-aof-rewrite-percentage和auto-aof-rewrite-min -size64mb这两个配置前者是文件增长百分比来进行rewrite,后者是按照文件大小增长进行rewrite

欢迎欢迎学Java的朋友们加入java架构交流: 855835163

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

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

推荐阅读更多精彩内容