Redis 内存数据库(二)

目录
[隐藏]

上文: Redis 内存数据库  提到 Redis 提供了丰富的数据类型,本文将好好记录下这些数据类型。

1、对象

首先得了解一点:Redis 数据库里面的每个键值对(key-value pair)都是由对象(object)组成的

其中,数据键(key) 总是一个字符串对象(string object)
而数据键的值(value)可以是字符串对象、散列对象(hash object)、列表对象(list object)、集合对象(set object)以及有序集合对象(sorted set object)这五种对象中的一种。

上述这五种对象形成的对象系统直接实现了 Redis 的键值对数据库,这个对象系统在 Redis 底层,是通过一系列 数据结构 构建而成的,包括:简单动态字符串、字典、双端链表、压缩列表、整数集合 等等。

Redis 中每种对象的实现都至少使用了一种数据结构。而对于同一个对象,使用不同数据结构去实现,是为了优化对象在不同场景下的效率。

在 Redis 中每个对象都由一个 redisObject 结构表示,下面列出这个结构中的部分属性:

typedef struct redisObjec{

  unsigned type;         //对象类型

  unsigned encoding; //对象编码方式

  void *ptr;                    //指向底层数据结构(实际内容)的指针

  unsinged lru;              //记录最后访问时间

  ... ...

} robj;

type:对象类型是很容易理解的,对象类型的值是一系列常量,来指明对象的类型,见下表:

对象
 对象 type 属性的值    TYPE 命令的输出
字符串对象REDIS_STRING"string"
列表对象REDIS_LIST"list"
哈希对象REDIS_HASH"hash"
集合对象REDIS_SET"set"
有序集合对象REDIS_ZSET"zset"

encoding:对象编码方式是什么?其实就是上面提到的对象的底层实现数据结构。每种对象至少有两种编码方式,这里就不列出来了。可以通过 OBJECT ENCODING 命令获取这个属性的值。

ptr:这个也好理解,就是个指针而已

lru:这个属性记录了对象最后一次被命令程序访问的时间,用于在服务器启用了 maxmemory 功能的情况下, 空转时长较大的那些键可能会优先被服务器删除。可通过 OBJECT IDLETIME 命令获取这个属性的值。

2、字符串对象(String)

String 类型是 Redis 中最基础的类型,使用典型的 key-value 方式存放及查询数据。Redis 中所有字符串都是使用二进制保存的,而且 String 类型是二进制安全的,所以可把图片、视频等二进制文件保存在 String 中。何为 二进制安全 呢?

简单说就是:输入任何字节都能正确处理, 即使包含零值(\0)字节。

看个例子就懂了:

C 语言中的 strlen 函数就是非二进制安全的,因为它需要依靠“\0”来判断字符串结尾。那如果字符串中间含有 "\0",那就不能不会识别之后的字节了,也就没有正确处理输入的所有字节,所以是非二进制安全。

str = "1234\05678";  strlen(str) 的值是 4 。

而 PHP 的 strlen 函数是binary safe 的,因为它不会对任何字符(包括'\0')进行特殊解释,所以在php中,strlen(str)=8

那 Redis 是怎么保证 String 的二进制安全的呢?看 下Redis 中 String 的定义:

struct sdshdr{

  long len;    //记录字符串实际内容的长度

  long free;   //表示 buf 数组剩余空间

  char buf[];  //保存字符串的内容

}
因为有 len 和 free 字段记录字符串信息,所以不必使用特殊字符标记结尾,即使字符串中有"\0"等也能正确读取,从而实现了二进制安全。同时该结构也提高了求长度、拼接的速度,因为不需要遍历了。

实际上,字符串对象 的编码有三种:intembstrraw

  如果一个字符串对象保存的是整数,并且这个整数值可以用 long 类型 来表示,那么字符串对象会将整数值保存在字符串对象结构的 ptr 属性里面(将 void* 转换成 long),并将 encode 设置为 int

  如果一个字符串对象保存的是字符串值,并且这个字符串值得长度小于 39 字节,那么字符串对象就会使用embstr 编码方式来保存这个字符串值。

  如果一个字符串对象保存的是字符串值,并且这个字符串值得长度大于 39 字节,那么字符串对象就会使用简单动态字符串(SDS) 来保存这个字符串,并且设置编码为 raw

后面要提到的四种对象,都和字符串对象一样,至少有两种编码方式,在不同场景下,Redis 会自动选择使用或转化不同编码方式,后续的对象和这类似,就以这个为例,后面不再多说。

字符串三种编码方式的结构:
              int                                                                               raw

embstr 编码和 raw 类似,都是使用 sdshdr 来实现,不过 redisObject 和 sdshdr 是在连续地址段。

String 类型支持的命令如下:

命令描述
set key value设置 key 对应 string   的值,返回1成功,0失败
setnx key value当key存在时返回0,否则设置
get key获取 key 对应 string   的值,不存在返回 nil
getset key value先获取key的值,再设置   key,不存在返回 nil
mset k1 v1  ... k2 v2同时设置多个值,返回1都成功,返回0都未设置
mget k1 k2    kn同时获取多个 key 的值,不存在的返回   null
msetnx k1 v1 … k2 v2同时设置多个并且不会覆盖已经存在的key
incr key让   key 对应的值加 1 ,如果 value 不是int 则会返回错误,incr 一个不存在的 key 则会设置 key 为1.
decr key让 key 对应的值减 1,decr   一个不存在的key,会设置 key 为-1
incrby key integer让 key 对应的值加指定整数   integer
decrby key integer让 key 对应的值加减指定整数   integer

3、散列对象(Hash)

String 类型只能存储单一的键值对,比较适合存放数值统计。 Hash 类型则对 String 进行了水平扩展,改变了一个 Value 只能存放单条数据的形式,而是一个 Value 就是一个 Hash 表。Hash 类型是以虚拟表的形式存放的,适合存储对象。Hash 表结构如下图

哈希对象 的编码有:ziplisthashtable

  ziplist 编码的哈希对象使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾, 因此:

  •         保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
  •         先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。

  hashtable 编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存:

  •         字典的每个键都是一个字符串对象, 对象中保存了键值对的键;
  •         字典的每个值都是一个字符串对象, 对象中保存了键值对的值。

这两种编码方式对应的结构如下(前者ziplist,后者hashtable):

Hash 类型支持的命令:

命令描述
hset key field   value设置   key 对应的 Hash 对象中指定域的值。不存在 key 对应的Hash 则创建,存在则重写
hget key field获取   key 对应的 Hash 对象中指定域的值,不存在则返回 nil
hmget key   field1..fieldN获取   key 对应的 Hash 对象中多个域的值,不存在的返回 nil
hmset k1 f1 v1   … kN fN vN同时设置   key 对应的 Hash 对象中多个域的值,存在会被覆盖,不存在则创建
hincrby key   field integer同   incrby 命令
hexists key   field查看   field 域是否存在
hdel key field删除指定   域,不存在返回 0
hlen key返回   key 对应的 Hash 对象中有多少个域,key 不存在返回 0
hkeys key返回   key 对应的 Hash 对象中所有 field 名称
hvals key返回   key 对应的 Hash 对象中所有值
hgetall key返回   key 对应的 Hash 对象中所有 域 和 值,并且一个域后跟一个值

4、List 类型

List 类型是指 key 对应的是一个 双向链表 结构。所以 List 类型提供链表支持的所有操作。可以通过 lpush、lpop 对链表中的队列数据进行压入或弹出。 Redis 正式基于 List 链表实现  消息队列 或 堆栈 服务的。

列表对象的编码可以是 ziplist 或者 linkedlist

  ziplist 编码的列表对象使用压缩列表作为底层实现, 每个压缩列表节点(entry)保存了一个列表元素。

  linkedlist 编码的列表对象使用双端链表作为底层实现, 每个双端链表节点(node)都保存了一个字符串对象, 而每个字符串对象都保存了一个列表元素。

linkedlist对应的底层结构如下:

List 类型支持的命令如下:

命令描述
lpush key   string向   key 对应的 List 头部添加一个字符串元素,成功返回1,否则返回0
rpush key   string向   key 对应的 List 尾部添加一个字符串元素
llen key返回   key 对应的 List 的长度,key 不存在返回 ,如果key类型不对返回错误
lrange key   start end返回指定区间的元素,下标从   0 开始,end 为负值表示从尾部开始
ltrim key   start end截取指定区间元素,成功返回1
lset key index   value通过索引号设置值
linsert key   before/after o n在   key 对应的 List 中的某个元素 v 之前或之后插入 元素 n 
lrem key count   value批量删除相同元素,count   是数量,为正从上开始,为负从下开始
lpop key从   List 头部返回并删除元素
rpop key从   List 尾部返回并删除元素
lindex key   index根据索引返回值

5、Set 类型

Set 数据类型是一种无序集合,在Redis内部通过 HashTable 实现。查找和删除元素的时间是O(1).Set 类型可以快速查找元素是否存在,用于记录一些不能重复的数据。对于 Set,除了常规添加、查看、删除等操作,还具备集合操作命令包括 差集、并集、交集。具体这里不列出,有需要查手册。。。

集合对象的编码可以是 intset 或者 hashtable

intset 编码的集合对象使用整数集合作为底层实现, 集合对象包含的所有元素都被保存在整数集合里面

hashtable 编码的集合对象使用字典作为底层实现, 字典的每个键都是一个字符串对象, 每个字符串对象包含了一个集合元素, 而字典的值则全部被设置为 NULL

intset 结构如下图:

6、ZSet/Sorted Set 类型

该类型 是一个有序集合,通过一个double类型的整数 score 进行排序。

7、事务管理

Redis 支持不太完善的事务功能。multi 命令声明事务开始,exec 提交事务,discard 事务回滚。

如果事务中存在错误的命令,Redis 还是会执行其它命令,这是和常见事务机制有所差异的地方

8、数据持久化

Redis 是基于内存的数据库,为了避免数据的意外丢失。Redis 提供了两种持久化方式;内存快照(Snapshotting) 和 日志追加(Append-only file)。

内存快照:就是将内存中的数据以快照方式写入二进制文件,默认文件名为 dump.rdb。

Redis 每隔一段时间就会进行一次内存快照操作。客户端可以使用 save 或 bgsave 来告诉 Redis 做一次内存快照操作。不过执行 save 命令可能会阻塞其它客户端请求。另外,内存快照每次都把内存数据完整地写入硬盘,而不是只写增量数据,所以可能会影响性能。

内存快照 通过配置项来控制(同时可以有多个条件):
save 900 1  #表示 900 秒内有超过 1 条 key 被修改就执行快照
save 300 10   #同上
save 60 10000  #同上                 

日志追加:就是把增加、修改数据的命令通过 write 函数追加到文件尾部(默认为 appendonly.aof)。Redis 重启时读取 appendonly.aof 文件中的所有命令并且执行,从而把数据从硬盘写入内存。不过日志追加方式可能会因为操作系统 I/O 接口的缓存而不能立即写入文件,从而导致丢失数据。不过可以在配置文件中强制把缓存写入硬盘。配置项如下:

appendoly yes #启用aof 方式

#appendonfsync always   #每次收到增加或者修改命令就立刻强制写入磁盘

appendfsync everysec      #每秒强制写入磁盘一次

#appendfsync no             #是否写入磁盘完全依赖操作系统

虽然通过配置项强制写入,降低了数据丢失的风险,同时也带来了一个问题,即持久化文件 appendonly.aof 不断膨胀!


Redis 内容丰富,还有很多很多需要学习和掌握的。但一步步来吧,现在难以实战,所以稍微掌握一些基础,让后续将会面对的实际应用更加简单一些。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

To