对象

  • Redis并不会直接使用数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包括字符串对象,列表对象,哈希对象,集合对象,有序集合对象

使用对象的好处

  • 这5种不同类型的对象,Redis可以在执行命令之前,根据对象类型来判断对象是否可以执行给定的命令
  • 可以针对不同的场景来为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率
  • Redis对象系统实现了基于引用计数技术的内存回收机制,这个机制的好处是当程序不使用某个对象的时候,这个对象所占用的内存会被自动释放,另外这个技术还可以实现对象共享机制,让多个数据库键共享同一个对象来节约内存
  • Redis对象带有访问时间记录信息,这个信息可以用来计算数据库键的空转时长,服务器启用了maxmemory功能,空转时长较大的会优先被服务器删除

对象类型与编码

  • 在Redis中创建键值对,至少创建两个对象,一个键对象,一个值对象
typedef struct redisObject{
  //类型
  unsigned type:4;
  //编码
  unsigned encoding:4;
  //指向底层实现数据结构的指针
  void *ptr;
  //.....
}robj;

类型

  • 对象的type属性记录了对象的类型,Redis数据库来说,键总是一个字符串对象,值可以是字符串对象,列表对象,哈希对象,集合对象,有序集合对象
  • 当我们说一个数据库键“字符串键”,其实是说这个键的值是字符串对象,说一个数据库键“列表键”,其实说这个键对应的值为列表对象,type命令就是这样的,执行type命令返回的是这个键所对应值的对象类型
类型常量 对象的名称 type命令的输出
REDIS_STRING 字符串对象 "string"
REDIS_LIST 列表对象 "list"
REDIS_HASH 哈希对象 "hash"
REDIS_SET 集合对象 "set"
REDIS_ZSET 有序集合对象 "zset"

SET msg "hello world"
TYPE msg
string

RPUSH number 1 3 5
TYPE number
list

HMSET profile name tom age 23 career programmer
TYPE profile
hash

SADD fruits apple banana cherry
TYPE fruits
set

ZADD price 8.5 apple 5.0 banana 6.0 cherry
TYPE fruits
zset

编码和底层实现

  • ptr指针指向底层数据结构实现,使用哪种数据结构是由对象的encoding决定
  • 使用OBJECT ENCODING key命令可以查看一个数据库键的值对象的编码
对象所使用的底层数据结构编码 编码常量 OBJECT ENCODING命令输出
整数 REDIS_ENCODING_INT "int"
embstr编码的简单动态字符串 REDIS_ENCODING_EMBSTR "embstr"
简单动态字符串 REDIS_ENCODING_RAW "raw"
字典 REDIS_ENCODING_HT "hashtable"
双端链表 REDIS_ENCODING_LINKEDLIST "linkedlist"
压缩列表 REDIS_ENCODING_ZIPLIST "ziplist"
整数集合 REDIS_ENCODING_INTSET "intset"
跳跃表和字典 REDIS_ENCODING_SKIPLIST "skiplist"
  • 每种类型可以使用的编码
类型 编码 对象
REDIS_STRING REDIS_ENCODING_INT 使用整数值实现的字符串对象
REDIS_STRING REDIS_ENCODING_EMBSTR 使用embstr编码的简单动态字符串实现的字符串对象
REDIS_STRING REDIS_ENCODING_RAW 使用简单动态字符串实现的字符串对象
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用压缩列表实现的列表对象
REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用双端链表实现的列表对象
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用压缩列表实现的哈希对象
REDIS_HASH REDIS_ENCODING_HT 使用字典对象实现的哈希对象
REDIS_SET REDIS_ENCODING_INTSET 使用整数集合实现的集合对象
REDIS_SET REDIS_ENCODING_HT 使用字典实现的集合对象
REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象
REDIS_ZSET REDIS_ENCODING_SKIIPLIST 使用跳跃表和字典实现的有序集合对象
  • 通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一定的编码,提高了Redis的灵活性和效率,因为Redis可以根据不同的使用场景为一个对象设置不同的编码,从而优化对象在某一个场景下的效率
  • 比如,列表对象包含的元素较少的时候,底层使用压缩列表,因为这样更节约内存和更快被加载到缓冲中,当元素越来越多的时候,就使用双端列表了,因为这时候双端列表的功能更强,且压缩列表的优势没有了

字符串对象

  • 字符串对象的编码:int,raw,embstr
  • 如果一个字符串对象是整数,并且这个整数可以用long类型表示,字符串对象会将整数值保存到字符串对象结构的ptr属性中,将void*转换成long,将字符串编码设置为int
    SET msg 10086
    OBJECT ENCODING msg
    "int"
  • 如果字符串对象保存的是一个字符串值,并且这个字符串的长度大于32字节,那么将用SDS来保存这个值,编码设置为raw


    raw编码的字符串对象
  • 如果字符串对象保存的是一个字符串值,并且这个字符串的长度小于等于32字节,字符串对象将使用embstr编码方式来保存字符串值,embstr编码通过调用一次内存分配函数来分配一块连续的空间,空间一次包含redisObject和sdshdr两个结构


    embstr编码创建的内存块结构
  • 使用embstr的好处:
  • embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为1次
  • 释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象,需要调用两次内存是释放函数
  • 因为embstr编码的字符串对象的所有数据都保存到一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好的利用缓冲带来的优势
  • 使用long double类型表示的浮点数在Redis中也是作为字符串值来保存的,如果我们保存浮点数到字符串对象中,那么程序会先将这个浮点数转换为字符串值,然后在保存转换所得的字符串值

编码的转换

  • int,embstr类型的字符串对象在条件满足的情况下,被转化成为raw编码类型的字符串对象

SET number 10086
APPEND number " is a good number"
GET number
OBJECT ENCODING number

SET msg "hello world"
APPEND msg " again"
OBJECT ENCODING msg

字符串命令

命令 int编码的实现方式 embstr编码的实现方式 raw编码实现方式
SET 使用int编码保存值 使用embstr编码保存值 使用raw编码保存值
GET 拷贝对象锁保存的整数值,将这个拷贝转换成字符串值,然后向客户端返回这个字符串值 直接向客户端返回字符串值 直接向客户端返回字符串值
APPEND 将对象转换成为raw编码,然后按raw编码方式执行此操作 将对象转成raw编码,然后按raw编码方式执行此操作 调用sdscatlen函数,将给定字符串追加到现有字符串末尾
INCRBYFLOAT 取出整数值并将其转化成long double类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存 取出字符串值并尝试将其转化成为long double类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存,如果字符串值不能被转化成为浮点数,那么向客户端返回一个错误 取出字符串值并尝试将其转化成为long double类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存,如果字符串值不能被转化成为浮点数,那么向客户端返回一个错误
INCRBY 对整数值进行加法计算,得出的计算结果会作为整数被保存起来 embstr编码不能执行此操作命令,向客户端返回一个错误 raw编码不能执行此操作,向客户端返回错误
DECRBY 对整数值进行减法运算,得出的结果被作为整数被保存起来 embstr编码不能执行此命令,向客户端返回错误 raw编码不能执行此命令,向客户端返回错误
STRLEN 拷贝对象锁保存的整数值,将这个拷贝转换成为字符串值,计算并返回这个字符串的长度 调用sdslen函数,返回字符串的长度 调用sdslen函数,返回字符串的长度
SETRANGE 将对象转换成为raw编码,然后按raw编码的方式执行此命令 将对象转换成为raw编码,然后按raw编码的方式执行此命令 将字符串特定索引上的值设置为给定的字符
GETRANGE 拷贝对象锁保存的整数值,将这个拷贝转换成为字符串值,然后取出并返回字符串指定索引上的字符 直接取出并返回字符串指定索引上的字符 直接取出并返回指定索引上的字符

列表对象

  • 列表对象的编码可以是ziplist或者linkedlist
  • RPUSH numbers 1 'three' 5
  • 假设numbers使用ziplist编码,每个压缩列表节点都保存了一个列表元素


    ziplist编码的numbers列表对象
  • 假设numbers使用linkedlist编码,每个双端链表节点都保存了一个字符串对象,而每一个字符串对象都保存了一个列表元素,字符串对象是5中类型的对象中唯一一种会被其他四种嵌套的对象


    简化的字符串对象

    完整的字符串对象表示

    linkedlist编码的numbers列表对象

编码转换

  • 列表使用ziplist必须同时满足下面的两种条件,如果不满足就使用linkedlist编码,这两个条件的上限可以修改,list-max-ziplist-value和list-max-ziplist-entries
  • 列表对象保存的所有字符串元素的长度都小于 65字节
  • 列表对象保存的元素个数小于512个
  • 对于ziplist编码来说,如果所需的两个条件有一个不满足了,那么就会转变为linkedlist编码

列表命令的实现

命令 ziplist编码的实现方法 linkedlist编码的实现方法
LPUSH 调用ziplistPush函数,将新元素推入到压缩列表的表头 调用listAddNodeHead函数,将新元素推入到双端链表的表头
RPUSH 调用ziplistPush函数,将新元素推入到压缩列表的表尾 调用listAddNodeTail函数,将新元素 推入到双端链表的表尾
LPOP 调用ziplistIndex函数定位压缩列表的表头节点,在向用户返回节点所保存的元素之后,调用ziplistDelete函数删除表头节点 调用listFirst函数定位双端链表的表头节点,在向用户返回节点所保存的元素之后,调用listDelNode函数删除表头节点
RPOP 调用ziplistIndex函数定位压缩列表的表尾节点,在向用户返回节点所保存的元素之后,调用ziplistDelete函数删除表尾节点 调用listLast函数定位双端链表的表尾节点,在向用户返回节点所保存的元素之后,调用listDelNode函数删除表尾节点
LINDEX 调用ziplistIndex函数定位压缩列表中指定节点,然后返回节点所保存的元素 调用listIndex函数定位双端链表中指定节点,然后返回节点所保存的元素
LLEN 调用ziplistLen函数返回压缩列表的长度 调用listLength函数返回双端链表的长度
LINSERT 插入新节点到压缩列表的表土或表尾时,使用ziplistPush函数,插入新结点到压缩列表的其他位置,使用ziplistInsert函数 调用listInsertNode函数,将新结点插入到双端链表的指定位置
LREM 遍历压缩列表节点,并调用ziplistDelete函数删除包含了给定元素的节点 遍历双端链表节点,并调用listDelNode函数删除了包含给定元素的节点
LTRIM 调用ziplistDeleteRange函数,删除压缩列表中所有不在指定索引范围内的节点 遍历双端链表节点,并调用listDelNode函数删除链表中所有不在指定范围内的节点
LSET 调用ziplistDelete函数,先删除压缩列表指定索引上的现有节点,然后调用ziplistInsert函数,将一个包含给定元素的新节点插入到相同索引上面 调用listIndex函数,定位到双端链表指定索引上的节点,然后通过赋值操作更新节点的值

哈希对象

  • 哈希对象的编码可以是ziplist或者hashtable
  • ziplist编码,每当有新的键值对要加入到哈希对象的时候,程序会先将保存了键的压缩列表节点推入到压缩列表的表尾,然后在将保存了值的压缩列表节点推入到压缩列表的表尾,因此,保存了同一键值对的两个节点总是挨在一起,保存键的在前,值的在后,先添加到哈希对象中的键值对被放在压缩列表表头方向,后来的添加到表尾方向
  • HSET profile name "tom"
  • HSET profile age 25
  • HSET profile career "programmer"
  • 假设使用的是ziplist编码


    ziplist编码的profile哈希对象
  • 假设使用hashtable编码的底层实现,每一个键值对都用一个字典键值对来保存
  • 字典中每一个键和每一个值都是一个字符串对象


    hashtable编码实现的profile的哈希对象

编码转换

  • 当哈希对象使用ziplist编码,必须满足下面的两个条件,否则使用的是hashtable编码
  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
  • 哈希对象保存的键值对数量小于512个,
  • ziplist编码在不满足任意一个条件的时候会转换为hashtable编码

哈希命令的实现

命令 ziplist编码的实现方式 hashtable编码的实现方式
HSET 首先调用ziplistPush函数将键推入到压缩列表的表尾,然后调用ziplistPush将值推入到压缩列表的表尾 调用dictAdd函数,将新节点添加到字典中
HGET 首先调用ziplistFind函数,在压缩列表中查找指定键所对应的节点,然后调用ziplistNext函数,将指针移动到键节点旁边的值节点,最后返回值节点 调用dictFind函数,在字典中查找给定键,然后调用dictGetval函数,返回给键所对应的值
HEXISTS 调用ziplistFind函数,在压缩列表中查找指定键所对应的节点,如果找到说明键值对存在,否则不存在 调用dictFind函数,查找给定键,如果找到,说明存在,否则说明不存在
HDEL 调用ziplistFind函数,在压缩列表中查找指定键对应的节点,然后将相应的键节点,以及键节点旁边的值节点都删除 调用dictDelete函数,将指定键所对应的键值对删除
HLEN 调用ziplistLen函数,取得压缩列表包含节点的总数量,将这个数量除以2,得出结果就是压缩列表保存的键值对的数量 调用dictSize函数,返回的结果就是键值对的数量
HGETALL 遍历整个压缩列表,用ziplistGet函数返回所有键和值 遍历整个字典,dictGetKey返回键,dictGetval返回值

集合对象

  • 集合对象的编码可以是intset或者hashtable
  • SADD numbers 1 3 5
  • SADD fruits "apple" "banana" "cherry"
  • intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面
  • hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含一个集合对象,而字典的值则全部被设置为NULL
intset编码的numbers集合对象
hashtable编码的fruits集合对象

编码的转化

  • intset编码必须满足下面的两个条件,否则使用hashtable编码
  • 集合对象保存的所有元素都是整数值
  • 集合对象保存的元素数量不超过512个
  • 当使用intset编码的集合对象来说,两个条件有一个不满足就会转换为hashtable编码

集合命令的实现

命令 intset编码的实现方法 hashtable编码的实现方法
SADD 调用intsetAdd函数,将所有新元素添加到整数集合中 调用dictAdd,以新元素为键,NULL为值,将键值对添加到里面
SCARD 调用intsetLen函数,返回整数集合所包含的元素数量,这个数量就是集合对象锁包含的元素数量 调用dictSize函数,返回字典所包含的键值对数量,这个数量就是集合对象所包含的元素数量
SISMEMBER 调用intsetFind函数,在整数集合中查找给定的元素,如果找到了说明元素存在集合中,如果没找到说明元素不在集合中 调用dictFind函数,在字典的键中查找给定的元素,如果找到了说明元素存在于集合中,没找到说明不存在集合
SMEMBERS 遍历整个整数集合,使用intsetGet函数,返回集合元素 遍历整个字典,使用dictGetKey函数返回字典的键作为集合元素
SRANDMEMBER 调用intsetRandom函数,从整数集合中随机的取出一个元素 调用dictGetRandomKey函数,从字典中随机的返回一个字典键
SPOP 调用intsetRandom函数,从整数集合中随机的取出一个元素,在将这个随机元素返回给客户端之后,调用intsetRemove函数,将随机元素从整数集合中删除 调用dictGetRandomKey函数,从字典中随机的选出 一个字典键,在将这个随机字典键的值返回给客户端之后,调用dictDelete函数,从字典中删除随机字典键所对应的键值对
SREM 调用intsetRemove函数,从整数集合中删除所有给定的元素 调用dictDelete函数,从字典中删除所有键为给定元素的键值对

有序集合对象

  • 有序集合对象的编码可以是ziplist或者skiplist
  • ZADD price 8.5 apple 5.0 banana 6.0 cherry
  • 使用ziplist编码的有序集合对象,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存成员,第二个节点保存分值,按分值大小,从小到大排序


    ziplist编码的有序集合对象
  • skiplist编码的有序集合对象使用zset结构作为底层实现
typedef struct zset{
  zskiplist *zsl;
  dict *dict;
}zset;
  • zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存一个集合元素,跳跃表节点的object属性保存了元素的成员,跳跃表的score属性保存了分值,程序可以对有序集合进行范围型的操作,ZRANK,ZRANGE命令就是通过这一特性实现的
  • dict字典为有序集合创建了一个从成员到分值的映射,字典中每个键值对都保存了一个集合元素,键保存了元素成员,值保存了分值,查找给定元素的分值O(1),ZSCORE命令就是通过这个特性实现的
  • 有序集合的每个元素成员都是一个字符串对象,每个分值都是一个double型的浮点数
  • zset结构同时使用跳跃表和字典来保存有序集合,但这两种数据结构通过指针来共享相同元素的成员和分值,不会浪费额为的内存
  • 为什么同时使用跳跃表和字典来实现有序集合了?
  • 因为单使用字典,那么保存的元素不会排序,而进行排序O(nlogn),以及额为的O(N)内存空间,因为排序后需要有一个地方存放
  • 因为单使用跳跃表,那么根据成员查找分值O(logn)
  • 所以为了查找和范围型操作尽可能快的执行,所以同时使用了跳跃表和字典


    skiplist编码的有序集合对象

编码的转化

  • 当有序集合对象同时满足下面两个条件,对象使用的是ziplist编码,否则使用的是skiplist编码
  • 有序集合保存的元素数量小于128个
  • 有序集合保存的所有元素成员的长度都小于64字节
  • 对于ziplist编码的集合对象来说,有一个条件不满足,就会将所有集合元素被转移并保存到zset结构中,对象的编码也变为skiplist

有序集合命令的实现

命令 ziplist编码的实现 zset编码实现方法
ZADD 调用ziplistInsert函数,将成员和分值作为两个节点分别插入到压缩列表 先调用zslInsert函数,将新元素添加到跳跃表中,然后调用dictAdd函数,将新元素关联到字典
ZCARD 调用ziplistLen函数,获得压缩列表包含节点的数量,将这个数量除以2得出集合元素的数量 访问跳跃表数据结构的length属性,直接返回集合元素的数量
ZCOUNT 遍历压缩列表,统计分值在给定范围内的节点数量 遍历跳跃表,统计分值在给定范围内的节点数量
ZRANGE 从表头向表尾遍历压缩列表,返回给定索引范围内的所有元素 从表头向表尾遍历跳跃表,返回给定索引范围内的所有元素
ZREVRANGE 从表尾向表头遍历压缩列表,返回给定索引范围内的所有元素 从表尾向表头遍历跳跃表,返回给定索引范围内的所有元素
ZRANK 从表头向表尾遍历压缩列表查找给定成员,沿途记录经过节点的数量,当找到给定成员之后,途径节点数量就是该成员所对应元素的排名 从表头向表尾遍历跳跃表查找给定成员,沿途记录经过节点的数量,当找到给定成员之后,途径节点数量就是该成员所对应元素的排名
ZREVRANK 从表尾向表头遍历压缩列表查找给定成员,沿途记录经过节点的数量,当找到给定成员之后,途径节点数量就是该成员所对应元素的排名 从表尾向表头遍历跳跃表查找给定成员,沿途记录经过节点的数量,当找到给定成员之后,途径节点数量就是该成员所对应元素的排名
ZREM 遍历压缩列表,删除所有包含给定成员的节点,以及被删除成员节点旁边的分值节点 遍历跳跃表,删除所有包含了给定成员的跳跃表节点,并在字典中解除被删除元素的成员和分值的联系
ZSCORE 遍历压缩列表,查找包含了给定成员的节点,然后取出成员节点旁边的分值节点保存的元素分值 直接从字典中取出给定成员的分值

类型检查和命令多态

Redis的命令分为两类

  • 可以对任何类型的键执行的命令,DEL,EXPIRE,RENAME,TYPE,OBJECT等命令
  • 只能对特定类型的键执行的命令,
    SET,GET,APPEND,STRLEN只能对字符串键执行
    HDEL,HSET,HGET,HLEN只能对哈希键执行
    RPUSH,LPOP,LINSERT,LLEN只能对列表键执行
    SADD,SPOP,SINTER,SCARD只能对集合键执行
    ZADD,ZCARD,ZRANK,ZSCORE只能对有序集合键执行
  • 类型检查实现

在执行一个类型特定的命令之前,Redis会先检查输入键的类型是否正确,然后在决定是否执行这个命令,这个检查是通过redisObject对象的type属性来实现的,下面是对于LLEN命令来说的:


LLEN命令执行时的类型检查错误
  • 多态命令的实现

根据值对象的编码方式,选择正确的命令实现代码来执行命令,比如列表对象的编码,有两种,ziplist和linkedlist可用,如果是ziplist编码,使用ziplistLen返回列表的长度,如果是linkedlist,使用listLength返回列表的长度,这个体现了基于编码的多态——一个命令可以同时用于处理多种不同的编码

使用DEL,EXPIRE,TYPE等命令,也是多态命令,无论输入的键是什么类型,这些命令都可以正确的执行,基于类型的多态——一个命令可以同时用于处理多种不同类型的键

LLEN命令执行过程

内存回收

c语言并没有自动内存回收功能,Redis自己的对象系统中构建了通过引用计数技术来实现内存回收机制,引用计数信息通过redisObject对象的refcount属性来实现

typedef struct redisObject{
   //引用计数
  int refcount;
}robj;

对象的引用信息随着对象的使用状态而不断变化

  • 创建一个对象的时候,refcount=1
  • 当对象被一个新程序使用,refcount加1 ,通过incrRefCount函数实现
  • 当对象不再被一个程序使用,refcount减1,通过decrRefCount函数实现,当refcount=0的时候,释放对象
  • 当refcount=0的时候,对象占用的内存释放
  • resetRefCount函数将对象的refcount设置为0,但不释放对象,通常在需要重写设置refcount的时候使用

对象的生命周期,一般是创建对象,操作对象,释放对象

对象共享

  • 使用refcount这个属性来实现对象共享,如果键A创建一个100的字符串,而键B也要创建一个100的字符串,此时100这个字符串对象已经存在内存了,我们只要让键A和键B共享这个字符串对象,就可以节省内存空间
  • Redis让多个键共享同一个值对象步骤:
  1. 将数据库键的值指针指向同一个现有的值对象
  2. 将被共享的值对象的引用的计数加1


    未被共享的字符串对象

    被共享的字符串对象

Redis只包含整数值的字符串对象进行共享,因为这样共享对象和目标对象的验证的复杂度是O(1),如果是字符串值的字符串对象,O(N),如果是多个值对象,比如列表对象或哈希对象,O(n^2)

目前,Redis在初始化服务器的时候,创建一万个字符串对象,从0到9999的所有整数值,当服务器需要用这些值的时候,服务器就共享这些对象,而不是创建对象

创建共享字符串对象的数量可以通过修改redis.h/REDIS_SHARED_INTEGERS进行修改

OBJECT REFCOUNT A可以查看对象的引用计数

对象的空转时长

redisObject对象的lru属性记录对象最后一次被命令程序访问的时间

typedef struct redisObject{
  unsigned lru:22;
}robj;

OBJECT IDLETIME 键可以打印键的空转时长,就是当前时间减去值对象的lru时间得出,这个命令在访问键的值对象的时候,不会修改这个对象的lru属性

当服务器打开了maxmemory选项,并且服务器用于回收内存的算法时volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的值,空转时长较高的那部分键会优先被服务器释放,从而回收内存

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

推荐阅读更多精彩内容

  • 前言:Reids并没有直接使用这些数据结构来实现数据库的键值对,而是基于这些数据结构创建了一个对象系统,这个对象系...
    rookie_yuqi阅读 317评论 0 0
  • 对象 本篇文章着重介绍一下,redis中大家日常使用的对象实现原理,这里附上这一系列的数据结构讲解,有需要的请点击...
    伟大的华仔阅读 191评论 0 0
  • 前言 在上一篇关于redis的文章中,我们分析了redis用到的主要的数据结构,但是redis并没有直接使用这些数...
    绝色天龙阅读 295评论 0 1
  • 最近读了《Redis 设计与实现》,知道了Redis在存储对象中做了很多的优化,利用各种不同的存储结构实现, 减少...
    mecury阅读 427评论 0 0
  • Redis用到的主要数据结构,如简单动态字符串、双端链表、字典、压缩列表、整数集合等。Redis并没有直接使用这些...
    HRADPX阅读 337评论 0 0