Redis

一、Redis基础

1. 概述

NoSQL(NoSQL = Not Only SQL),意思是不仅仅是SQL,泛指非关系型的数据库。

1.1 CAP理论

  • C:Consistency(强一致性)
  • A:Availability(可用性)
  • P:Partion tolerance(分区容错性)

CAP理论图的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,最多只能同时较好的满足两个。因此,根据CAP原理将NoSQL数据库分成了满足CA原则、满足CP原则和满足AP原则三大类:

CA: 传统Oracle数据库
AP: 大多数网站架构的选择
CP: Redis、Mongodb

1.3 Redis特点

  • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
  • Redis支持数据的备份,即master-slave模式的数据备份。

2. Redis五大数据类型

操作指令参考: http://redisdoc.com/
所有的key都为String类型,讨论数据类型是说的value的类型

2.1 字符串(String)

//=====================基本操作=======================//
//设置String
set key value
mset key1 value1 key2 value2...
//设置生命周期
setex key seconds value 

//得到String
get key 
mget key1 key2...

//删除String
del key

//向字符串的后面追加字符,如果有就补在后面,如果没有就新建
append key value

//=================String作为数值的操作===================//
//增长指令,只有当value为数字时才能增长
incr key  
incrby key increment  
incrbyfloat key increment 

//减少指令,有当value为数字时才能减少
decr key  
decrby key increment
  • string在redis内部存储默认就是一个字符串,当遇到增减类操作incr,decr时会转成数值型进行计算。
  • redis所有的操作都是原子性的,采用单线程处理所有业务,命令是一个一个执行的,因此无需考虑并发带来的数据影响。

指定生命周期

//设置数据的生命周期,单位 秒
setex key seconds value
//设置数据的生命周期,单位 毫秒
psetex key milliseconds value

2.2 列表(List)

  • 元素有序且可重
  • List中保存的数据都是string类型的
//=====================基本操作=======================//
//添加修改数据,lpush为从左边添加,rpush为从右边添加
lpush key value1 value2 value3...
rpush key value1 value2 value3...

//查看数据, 从左边开始向右查看. 如果不知道list有多少个元素,end的值可以为-1,代表倒数第一个元素
//lpush先进的元素放在最后,rpush先进的元素放在最前面
lrange key start end
//得到长度
llen key
//取出对应索引的元素
lindex key index

//获取并移除元素(从list左边或者右边移除)
lpop key
rpop key


//规定时间内获取并移除数据,b=block,给定一个时间,如果在指定时间内放入了元素,就移除
blpop key1 key2... timeout
brpop key1 key2... timeout

//移除指定元素 count:移除的个数 value:移除的值。 移除多个相同元素时,从左边开始移除
lrem key count value

2.3 集合(Set)

  • 不重复且无序
  • string类型的无序集合。它是通过HashTable实现的
//添加元素
sadd key member1 member2...

//查看元素
smembers key

//移除元素
srem key member

//查看元素个数
scard key

//查看某个元素是否存在
sismember key member
//从set中任意选出count个元素
srandmember key count

//从set中任意选出count个元素并移除
spop key count

//求两个集合的交集、并集、差集
sinter key1 key2...
sunion key1 key2...
sdiff key1 key2...

//求两个set的交集、并集、差集,并放入另一个set中
sinterstore destination key1 key2...
sunionstore destination key1 key2...
sdiffstore destination key1 key2...

//求指定元素从原集合放入目标集合中
smove source destination key

2.4 哈希(Hash)

  • 类似java里面的Map<String,Object>
  • hash类型下的value只能存储字符串,不允许存储其他数据类型
//插入(如果已存在同名的field,会被覆盖)
hset key field value
hmset key field1 value1 field2 value2...
//插入(如果已存在同名的field,不会被覆盖)
hsetnx key field value

//取出
hget key field
hgetall key

//删除
hdel key field1 field2...

//获取field数量
hlen key

//查看是否存在
hexists key field

//获取哈希表中所有的字段名或字段值 
hkeys key
hvals key

//设置指定字段的数值数据增加指定范围的值 
hincrby key field increment 
hdecrby key field increment

2.5 有序集合Zset(sorted set)

  • 有序不重复
  • zset和set一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数
  • 可进行排序

//插入元素, 需要指定score(用于排序)
zadd key score1 member1 score2 member2

//查看元素(score升序), 当末尾添加withscore时,会将元素的score一起打印出来
zrange key start end (withscore)
//查看元素(score降序), 当末尾添加withscore时,会将元素的score一起打印出来
zrevrange key start end (withscore)

//移除元素
zrem key member1 member2...

//按条件获取数据, 其中offset为索引开始位置,count为获取的数目
zrangebyscore key min max [withscore] [limit offset count]
zrevrangebyscore key max min [withscore] [limit offset count]

//按条件移除元素
zremrangebyrank key start end
zremrangebysocre key min max
//按照从大到小的顺序移除count个值
zpopmax key [count]
//按照从小到大的顺序移除count个值
zpopmin key [count]

//获得元素个数
zcard key

//获得元素在范围内的个数
zcount min max

//求交集、并集并放入destination中, 其中numkey1为要去交集或并集集合的数目
zinterstore destination numkeys key1 key2...
zunionstore destination numkeys key1 key2...
//查看某个元素的索引(排名)
zrank key member
zrevrank key member

//查看某个元素索引的值
zscore key member
//增加某个元素索引的值
zincrby key increment member

3. Redis持久化

3.1 简介

什么是持久化

利用永久性存储介质将数据进行保存,在特定的时间将保存的数据进行恢复的工作机制称为持久化。

为什么要持久化

防止数据的意外丢失,确保数据安全性

持久化过程保存什么

  • 将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单,关注点在数据
  • 将数据的操作过程进行保存,日志形式,存储操作过程,存储格式复杂,关注点在数据的操作过程

3.2 RDB

RDB的触发有两种方式:

  • 手动(使用命令save)
  • 自动(save m n)

自动: 默认的RDB快照触发机制(可以在redis.conf中的SNAPSHOTTING中查看)

1分钟内改动了1万次
5分钟内改动了10万次
15分钟内改动了1次

默认的名字为:dump.rdb

手动: 命令save或者是bgsave
如果出现某个数据非常重要,无法等到自动备份,这时候我们可以通过这
个命令来进行手动备份

save:save时只管保存,其它全部阻塞
bgsave:Redis会在后台异步进行快照操作

如何恢复RDB

将备份文件 (dump.rdb) 移动到启动Redis服务的目录即可

优缺点

优势

  • 适合大规模的数据恢复
  • 对数据完整性和一致性要求不高

劣势

  • 在一定间隔时间做一次备份,所以如果Redis服务器意外down掉的话,就会丢失最后一次快照后的所有修改
  • Fork的时候,内存中的数据被克隆了一份,会占用更多的内存空间

停止RDB

使用下面的命令:
CONFIG SET save ""

小结

  • RDB是一个非常紧凑的文件。
  • RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他I0操作,所以RDB持久化方式可以最大化redis的性能。
  • 与AOF相比,在恢复大的数据集的时候,RDB方式会更快一一些。
    数据丢失风险大。
  • RDB需要经常fork子进程来保存数据集到硬盘上,当数据集比较大的时候fork的过程是非常耗时间的,可能会导致Redis在一些毫秒级不能回应客户端请求

3.3 AOF

AOF概念

  • AOF(append only file)持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中命令,以达到恢复数据的目的。与RDB相比可以简单描述为改记录数据为记录数据产生的过程
  • AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式

AOF写数据三种策略

  • always: 每次写入操作均同步到AOF文件中,数据零误差,性能较低,不建议使用
  • everysec: 每秒将缓冲区中的指令同步到AOF文件中,数据准确性较高,性能较高 ,默认配置,在系统突然宕机的情况下丢失1秒内的数据
  • no: 由操作系统控制每次同步到AOF文件的周期,整体过程不可控

AOF功能开启:

appendonly yes|no
appendfsync always|everysec|no

AOF自动重写

  • 自动重写触发条件设置
//触发重写的最小大小
auto-aof-rewrite-min-size size 
//触发重写须达到的最小百分比
auto-aof-rewrite-percentage percent
  • 自动重写触发比对参数( 运行指令info Persistence获取具体信息 )
//当前.aof的文件大小
aof_current_size 
//基础文件大小
aof_base_size

4. Redis事务

基于特定条件的事务执行

  • 对 key 添加监视锁,在执行exec前如果key发生了变化,终止事务执行

    watch key1, key2....
    
  • 取消对所有key的监视

    unwatch
    

分布式锁

  • 使用 setnx 设置一个公共锁

    //上锁
    setnx lock-key value
    //释放锁
    del lock-key
    
    • 利用setnx命令的返回值特征,有值(被上锁了)则返回设置失败,无值(没被上锁)则返回设置成功
    • 操作完毕通过del操作释放锁

注意:上述解决方案是一种设计概念,依赖规范保障,具有风险性

分布式锁加强

  • 使用 expire 为锁key添加时间限定,到时不释放,放弃锁

    expire lock-key seconds
    pexpire lock-key milliseconds
    
  • 由于操作通常都是微秒或毫秒级,因此该锁定时间不宜设置过大。具体时间需要业务测试后确认。
    -- 例如:持有锁的操作最长执行时间127ms,最短执行时间7ms。
    -- 测试百万次最长执行时间对应命令的最大耗时,测试百万次网络延迟平均耗时
    -- 锁时间设定推荐:最大耗时120%+平均网络延迟110%
    -- 如果业务最大耗时<<网络平均延迟,通常为2个数量级,取其中单个耗时较长即可

二、Redis应用

1. Redis分布式锁

商品超卖问题:

在多线程情况下会出现并发问题导致商品超卖问题。(多个线程同时获取到stock数量)

1)解决多线程并发问题

加锁解决多线程并发问题:

上述代码,在分布式环境情况下,每个环境的jvm不同(不同进程),synchronized锁不是同一把锁,此时还是会有并发问题。

2)解决多台服务部署应用导致并非问题

使用redis分布式锁解决,使用setnx + 超时时间:

其中:

  • 使用try finally是为了在异常情况下依然可以释放锁;
  • 设置锁的过期时间,防止程序运行期间jvm挂了导致finally块中代码也无法执行去释放锁;
  • 第29行替换掉27、28行是为了原子操作。

上述代码还是有问题:
假设有3个客户端来请求这段代码,其中客户端A需要15秒钟才执行完,在执行第10秒的时候因为到了过期时间自动释放锁,此时客户端B拿到锁进来了,假设执行了5秒中B还正在执行中,此时客户端A执行到第43行去删除锁,此时删除的客户端拿的锁。那么,客户端C自然就可以再次拿锁进入。。。依次循环等于锁一直处于失效状态。

3)解决锁的误释放问题

问题的根本原因是自己设置的锁被别人释放掉了,可以设置一个客户端id来表示自己的锁。

4)释放锁的原子操作问题

当某一客户端a执行到46行时,卡了一下导致到了过期时间,客户端释放了锁,此时客户端B设置了锁进入程序,此时客户端A恢复,此时客户端执行47行删除的锁其实是客户端B的锁。

使用看门狗+锁续命来解决:弄一个监控线程轮训主线程是否还在执行,如果还在执行就给锁续命增加过期时间。

使用redisson框架来解决:

Redisson分布式锁实现原理:

Redlock实现原理(解决主从切换时锁失效的情况):

其实,分布式锁是和高并发是违背的,上述代码等同于串行执行。----分段加锁处理。

2. 缓存数据库双写不一致问题

情况1:

情况2:

解决方案:使用分布式锁,使得每个线程操作串行执行。

3. redis为什么这么快

1)内存数据库
2)单线程
3)底层的数据结构
4)全局维护了一个hash表。


三、Redis五大数据类型应用场景

1. String应用场景

2. Hash应用场景

3. List应用场景

比如:微信公共号的消息流。

4. Set应用场景

集合操作可实现共同关注模型、推荐模型(可能认识的人)。

5. Zset应用场景

四、Redis 底层的数据结构

Redis 底层的数据结构一共有 6 种,和数据类型对应关系如下图:

1.SDS

字符串在 Redis 中是很常用的,键值对中的键是字符串,值有时也是字符串。

Redis 使用 C 语言实现的,但是它没有直接使用 C 语言的 char* 字符数组来实现字符串,而是自己封装了一个名为简单动态字符串(simple dynamic string,SDS) 的数据结构来表示字符串。

1.1 C 语言字符串的缺陷

C 语言的字符串其实就是一个字符数组,即数组中每个元素是字符串中的一个字符。

比如,下图就是字符串“xiaolin”的 char* 字符数组的结构:

在 C 语言里,对字符串操作时,char * 指针只是指向字符数组的起始位置,而字符数组的结尾位置就用“\0”表示,意思是指字符串的结束

因此,C 语言标准库中字符串的操作函数,就通过判断字符是不是“\0”,如果不是说明字符串还没结束,可以继续操作,如果是则说明字符串结束了,停止操作。

举个例子,C 语言获取字符串长度的函数 strlen,就是通过字符数组中的每一个字符,并进行计数,等遇到字符为“\0”后,就会停止遍历,然后返回已经统计到的字符个数,即为字符串长度。下图显示了 strlen 函数的执行流程:

很明显,C 语言获取字符串长度操作的时间复杂度是 O(N)(*****这是一个可以改进的地方*)

C 语言的字符串用 “\0” 字符作为结尾标记有个缺陷。假设有个字符串中有个 “\0” 字符,这时在操作这个字符串时就会提早结束,比如 “xiao\0lin” 字符串,计算字符串长度的时候则会是 4。

还有,除了字符串中不能 “\0” 字符外,用 char* 字符串中的字符必须符合某种编码(比如ASCII)。

这些限制使得 C 语言的字符串只能保存文本数据,不能保存像图片、音频、视频文化这样的二进制数据(*****这也是一个可以改进的地方*****)

C 语言标准库中字符串的操作函数是很不安全的,对程序员很不友好,稍微一不注意,就会导致缓冲区溢出。

举个例子,strcat 函数是可以将两个字符串拼接在一起的。

c //将 src 字符串拼接到 dest 字符串后面 char *strcat(char dest, const char src);

C 语言的字符串是不会记录自身的缓冲区大小的,所以 strcat 函数假定程序员在执行这个函数时,已经为 dest 分配了足够多的内存,可以容纳 src 字符串中的所有内容,而一旦这个假定不成立,就会发生缓冲区溢出将可能会造成程序运行终止,(这是一个可以改进的地方)。

而且,strcat 函数和 strlen 函数类似,时间复杂度也很高,也都需要先通过遍历字符串才能得到目标字符串的末尾。然后对于 strcat 函数来说,还要再遍历源字符串才能完成追加,对字符串的操作效率不高。

通过以上的分析,可以得知 C 语言的字符串 不足之处以及可以改进的地方:

  • 获取字符串长度的时间复杂度为 O(N);
  • 字符串的结尾是以 “\0” 字符标识,而且字符必须符合某种编码(比如ASCII),只能保存文本数据,不能保存二进制数据;
  • 字符串操作函数不高效且不安全,比如可能会发生缓冲区溢出,从而造成程序运行终止;

Redis 实现的 SDS 的结构就把上面这些问题解决了,接下来我们一起看看 Redis 是如何解决的。

1.2 SDS 结构设计

SDS 的数据结构:

结构中的每个成员变量分别介绍下:

  • len,SDS 所保存的字符串长度。这样获取字符串长度的时候,只需要返回这个变量值就行,时间复杂度只需要 O(1)。
  • alloc,分配给字符数组的空间长度。这样在修改字符串的时候,可以通过 alloc - len 计算 出剩余的空间大小,然后用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区益处的问题。
  • flags,SDS 类型,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后面在说明区别之处。
  • buf[],字节数组,用来保存实际数据。不需要用 “\0” 字符来标识字符串结尾了,而是直接将其作为二进制数据处理,可以用来保存图片等二进制数据。它即可以保存文本数据,也可以保存二进制数据,所以叫字节数组会更好点。

总的来说,Redis 的 SDS 结构在原本字符数组之上,增加了三个元数据:len、alloc、flags,用来解决 C 语言字符串的缺陷。

1.3 节省内存空间

SDS 结构中有个 flags 成员变量,表示的是 SDS 类型。

Redos 一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。

这 5 种类型的主要区别就在于,它们数据结构中的 len 和 alloc 成员变量的数据类型不同之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间

2. 压缩列表

压缩列表是 Redis 数据类型为 list 和 hash 的底层实现之一。

  • 当一个列表键(list)只包含少量的列表项,并且每个列表项都是小整数值,或者长度比较短的字符串,那么 Redis 就会使用压缩列表作为列表键(list)的底层实现。
  • 当一个哈希键(hash)只包含少量键值对,并且每个键值对的键和值都是小整数值,或者长度比较短的字符串,那么 Redis 就会使用压缩列表作为哈希键(hash)的底层实现。

压缩列表结构设计

压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。不作过多讲解。

在有序集合zset的实现里面,跳跃表和压缩列表都有用到。
根据上面都描述,压缩列表的使用场景一般是短字符串,且元素个数不能太多。所以在zset里面有几个配置:
zset-max-ziplist-entries: 默认值128, 当元素个数小于这个值都时候使用压缩列表。否则使用跳跃表。
zset-max-ziplist-value:默认值64,当每个元素的字符串长度小于这个值的时候使用压缩列表,否则使用跳跃表。
满足这两个条件中的任意一个条件,就会转换到跳跃表。而转换到跳跃表之后,即便把元素删除,也不好回退到压缩列表。

3. 跳跃表

什么是跳跃表 ?

对于一个单链表来说, 即便链表中存储的数据是有序的, 我们想要随机查找一个数据,那么也只能从头到尾遍历链表节点, 这样查找的效率就会很低,是件复杂度为 O(n)

如果我们想要提高其查找效率, 可以考虑在链表上建索引的方式。每2个节点提取一个节点到上一级,我们把抽出来的那一级叫作索引。

image

可以看出, 加了一层索引以后, 查找一个节点需要遍历的节点数少了, 也就是说查找效率提升了,同理可以再加一层索引。

从图中我们可以看出,查找效率又有提升。在例子中我们的数据很少,当有大量的数据时,我们可以增加多级索引,其查找效率可以得到明显提升。

像这种链表加多级索引的结构,就是跳跃表!

Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时, Redis就会使用跳跃表来作为有序集合健的底层实现。

这里我们需要思考一个问题——为什么元素数量比较多或者成员是比较长的字符串的时候Redis要使用跳跃表来实现?
从上面我们可以知道,跳跃表在链表的基础上增加了多级索引以提升查找的效率,但其是一个空间换时间的方案,必然会带来一个问题——索引是占内存的。原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势必然会被放大,而缺点则可以忽略

Redis中跳跃表的实现

Redis的跳跃表由zskiplistNode和skiplist两个结构定义,其中 zskiplistNode结构用于表示跳跃表节点,而 zskiplist结构则用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等

  • header:指向跳跃表的表头节点,通过这个指针程序定位表头节点的时间复杂度就为O(1)

  • tail:指向跳跃表的表尾节点,通过这个指针程序定位表尾节点的时间复杂度就为O(1)

  • level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内),通过这个属性可以再O(1)的时间复杂度内获取层高最好的节点的层数。

  • length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内),通过这个属性,程序可以再O(1)的时间复杂度内返回跳跃表的长度。

结构右方的是四个 zskiplistNode结构,该结构包含以下属性:

  • 层(level):
    节点中用1、2、L3等字样标记节点的各个层,L1代表第一层,L代表第二层,以此类推。
    每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离(跨度越大、距离越远)。在上图中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。
    每次创建一个新跳跃表节点的时候,程序都根据幂次定律(powerlaw,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。

  • 后退(backward)指针:
    节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。与前进指针所不同的是每个节点只有一个后退指针,因此每次只能后退一个节点。

  • 分值(score):
    各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。

  • 成员对象(oj):
    各个节点中的o1、o2和o3是节点所保存的成员对象。在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的:分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排在后面(靠近表尾的方向)
    在比较的时候 如果分值相同则会比较保存的对象。

五、Redis常见问题

1. 缓存穿透、缓存击穿、缓存雪崩 如何解决?

缓存穿透

当缓存与DB中都不存在该数据时,如果此时发起恶意大量请求,会导致这个不存在的数据每次请求都会查询DB,导致DB压力过大,直接挂掉。

解决方案:
(1)对空值缓存,并设置较短的过期时间。
(2)设置访问白名单,使用布隆过滤器,分别通过多个哈希函数生成多个哈希值,然后将这些哈希值存到一个足够大的bitmap中,此时一个一定不存在的数据就会被这个bitmap拦截,从而减少了数据库的查询压力。
(3)业务过滤。

缓存击穿

某个热点key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案:
(1)设置热点数据永远不过期。
(2)使用分布式锁。

缓存雪崩

缓存中大批量的数据集中过期,从而导致查询数据量巨大,引起数据库压力过大甚至down机。

解决方案:
(1)将缓存失效时间分散开
(2)如果缓存数据库是分布式部署,将热点数据均匀分布在不同缓存中。
(3)设置热点数据永远不过期。

2. Redis为什么这么快?

(1)内存存储,没有磁盘IO上的开销
(2)单线程实现,避免了多个线程之间线程切换和锁资源争用的开销
(3)优化的数据结构
(4)非阻塞IO,Redis使用多路复用IO技术,在poll,epool,kqueue选择最优IO实现

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

推荐阅读更多精彩内容