上文: 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"等也能正确读取,从而实现了二进制安全。同时该结构也提高了求长度、拼接的速度,因为不需要遍历了。
实际上,字符串对象 的编码有三种:int
、embstr
、raw
如果一个字符串对象保存的是整数,并且这个整数值可以用 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 表结构如下图
哈希对象 的编码有:ziplist
和 hashtable
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 内容丰富,还有很多很多需要学习和掌握的。但一步步来吧,现在难以实战,所以稍微掌握一些基础,让后续将会面对的实际应用更加简单一些。
发表评论
取消回复电子邮件地址不会被公开。 必填项已用*标注