目录什么是 Redis前置知识String介绍常用命令使用场景底层实现SDS 结构体List介绍常用命令使用场景底层实现ZipListQuickListHash介绍常用命令使用场景底层实现DictDict 的 rehas
Redis 是基于内存的 K-V 数据库,常用于缓存、消息队列,分布式锁等场景,并且提供了常见的数据结构:字符串、哈希、列表、集合、带排序的集合
Redis中的任意数据类型的键和值都会被封装为一个 RedisObject
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
} robj;
type
:指具体的对象类型
encoding
:底层编码方式
lru
:记录最后一次被访问的时间
refcount
:对象引用计数器,计数器为 0 时表示没有被引用,可以回收
ptr
:实际存储的值
五种数据结构
Redis中会根据存储的数据类型不同,选择不同的编码方式。每种数据类型的使用的编码方式如下:
数据类型 | 编码方式 |
---|---|
OBJ_STRING | int、embstr、raw |
OBJ_LIST | LinkedList和ZipList(3.2以前)、QuickList(3.2以后) |
OBJ_SET | intset、HT |
OBJ_ZSET | ZipList、HT、SkipList |
OBJ_HASH | ZipList、HT |
Redis 的编码方式
Redis 中会根据存储的数据类型不同,选择不同的编码方式,共包含 11 种不同类型:
编号 | 编码方式 | 说明 |
---|---|---|
0 | OBJ_ENCODING_RAW | raw编码动态字符串 |
1 | OBJ_ENCODING_INT | long类型的整数的字符串 |
2 | OBJ_ENCODING_HT | hash表(字典dict) |
3 | OBJ_ENCODING_ZIPMAP | 已废弃 |
4 | OBJ_ENCODING_LINKEDLIST | 双端链表 |
5 | OBJ_ENCODING_ZIPLIST | 压缩列表 |
6 | OBJ_ENCODING_INTSET | 整数集合 |
7 | OBJ_ENCODING_SKIPLIST | 跳表 |
8 | OBJ_ENCODING_EMBSTR | embstr的动态字符串 |
9 | OBJ_ENCODING_QUICKLIST | 快速列表 |
10 | OBJ_ENCODING_STREAM | Stream流 |
字符串 (string) 其实是最简单的数据结构,不仅可以存储字符串,还可以存储整数、浮点数、二进制数据等;
set key value
:添加一个 string 类型的键值对
get key
:获取 key 对应的 value
mset
:批量设置对个键值对
mget
:批量获取多个键值对
incr
:让一个整形自增 1
setnx
:添加一个 string 类型的字符串,前提是这个 key 不存在
缓存对象
直接缓存整个 JSON 字符串对象 SET user:1 '{"name":"ezreal", "age":18}'
缓存更新策略
分布式锁
使用 setnx 指令可以实现分布式锁,若设置值成功则表示获取锁成功,设置失败则获取锁失败,注意要设置该 key 的过期时间,防止一直抢占该锁
常规计数
可以通过 incr
指令来统计数量,前提设置的 value 需要是整形
共享 session
我们可以把多台服务器上的 session 统一放到 redis 中,无论向集群哪台服务器发送消息,都保证去 redis 中获取 session 的信息
redis 的底层实现是 SDS 字符串
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len;
uint8_t alloc;
unsigned char flags;
char buf[];
};
len
:该字符串的 长度
alloc
:buf 申请的 总字节数
buf
:实际存储数据的位置
flag
:SDS 头类型,用来控制SDS头大小 :SDS_TYPE_5
,SDS_TYPE_8
,SDS_TYPE_16
,SDS_TYPE_32
,SDS_TYPE_64
整形的存储
如果存储的是一个整形,则则直接存储在 RedisObject 中即可;
SDS 动态扩容
假如现在在 name 字符串后加入 ezreal,形成新的字符串 'nameezreal'
列表是一个有序的字符串集合,可以通过下标索引来访问指定位置的字符串,还可以在列表头部或者尾部来添加或删除元素
LPUSH KEY
:向列表左侧插入元素
RPUSH KEY
:向列表右侧插入元素
LPOP KEY
:向列表左侧弹出元素
RPOP KEY
:向列表右侧弹出元素
LRANGE KEY star end
:获取指定范围内的 key
BLPOP / BRPOP
:与 LPOP / RPOP 类似,只不过在没有元素时会等待
消息队列
消息队列实现的三大要素:消息保序,处理重复的消息,保证消息的可靠性
我们可以基于 LPUSH / RPOP 的方法来实现一个队列,消息先进先出,保证消息的顺序;
当有一个缺点,消费者只能不断的轮询该队列是否有消息,因为消费者新增一条消息后是不能通知消费者的,所以消费者要不断轮询 while(1)
,不断的占用 CPU。我们可以使用 BRPOP
来优化,消费者没有获取到消息时,会自动阻塞,直到生产者放入新的消息。
我们可以为每个消息生成一个 唯一 的消息 ID,每次消费消息时,从数据库中判断该消息是否已经被处理了,防止消息的重复消息。
当消息被消费者获取后,没有处理完消息,机器就宕机了,这样不仅消息没有完全被消费,List 中的消息也丢失了,从而导致这个任务永远丢失了;
我们可以使用 Redis 中的BRPOPLPUSH
命令,作用是把该消息从 List 中拿出来,同时还会把该消息放到另一个 List 中作留存。
在 Redis 3.2 之前,List 的底层实现是压缩列表 ( ZipList
) 或 双向链表
在 Redis 3.2 之后,List 的底层实现是 QuickList
ZipList 又叫压缩列表,类似 "双向链表",但其不是链表。它由一段连续的内存块组成,可以在列表头部或者尾部添加或者删除元素。
zlbytes
:整个 ZipList 的 内存字节数,占用 4 字节
zltail
:从 ZipList 的起始地址到最后一个 entry 的内存字节数,占用 4 字节
zllen
:ZipList 中 entry 的个数,占用 2 字节
zlend
:结束标识 0xff
,占用 1 字节
ZipEntry结构
previous_entry_length
:上一个 entry 的长度,占用 1个 或 5个字节
encoding
:记录 content 的类型 [ 整形 or 字符串 ] 和长度,占用 1个,2个 或者 5个字节
content
:实际的内容
Encoding 编码
Entry 中的 encoding 编码分为字符串和整数两种:
字符串:如果 encoding 是以“00”“01”或者“10”开头,则证明 content 是字符串
整数:如果encoding是以“11”开始,则证明content是整数,且encoding固定只占用1个字节
编码 | 编码长度 | 整数类型 |
---|---|---|
11000000 | 1 | int16_t(2 bytes) |
11010000 | 1 | int32_t(4 bytes) |
11100000 | 1 | int64_t(8 bytes) |
11110000 | 1 | 24位有符整数(3 bytes) |
11111110 | 1 | 8位有符整数(1 bytes) |
1111xxxx | 1 | 直接在xxxx位置保存数值,范围从0001~1101,减1后结果为实际值 |
可见,ZipList 通过记录上一个entry 的长度 和 自身entry 的长度(length + encoding + content),从而可以快速得到上一个 entry 和下一个 entry ,从而实现从头到尾 或 从尾到头的遍历。
QuickList 是一个双向链表,它的每一个节点都是 ZipList
为了避免 QuickList 中的每个 ZipList 中 entry 过多,Redis 提供了一个配置项:list-max-ziplist-size
来限制。
如果值为正,则代表 ZipList 的允许的 entry 个数的最大值;
如果值为负,则代表ZipList的最大内存大小,分5种情况:
其默认值为 -2:
以下是 QuickList 的和 QuickListnode 的结构源码:
QuickList
typedef struct quicklist {
quicklistNode *head; // 头结点指针
quicklistNode *tail; // 尾结点指针
unsigned long count;
unsigned long len;
int fill : QL_FILL_BITS;
unsigned int compress : QL_COMP_BITS;
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
QuickListNode
typedef struct quicklistNode {
struct quicklistNode *prev; // 前一个指针
struct quicklistNode *next; // 后一个指针
unsigned char *zl; // 当前结点的ziplist指针
unsigned int sz;
unsigned int count : 16;
unsigned int encoding : 2;
unsigned int container : 2;
unsigned int recompress : 1;
unsigned int attempted_compress : 1;
unsigned int extra : 10;
} quicklistNode;
整体结构
Hash 是一个键值对的集合,形如:key:{field1: value1,field1: value1}
,非常适合存储一个对象
# 存储一个哈希表key的键值
HSET key field value
# 获取哈希表key对应的field键值
HGET key field
# 在一个哈希表key中存储多个键值对
HMSET key field value [field value...]
# 批量获取哈希表key中多个field键值
HMGET key field [field ...]
# 删除哈希表key中的field键值
HDEL key field [field ...]
# 返回哈希表key中field的数量
HLEN key
# 返回哈希表key中所有的键值
HGETALL key
# 为哈希表key中field键的值加上增量n
HINCRBY key field n
缓存对象
例如下图,对于使用 string + json
还是用 hash
来存储,建议优先选择 string + json
,对于某些字段经常改变的话,可以考虑使用 hash
购物车
以用户 id 为 key,商品 id 为 field,商品数量为 value,恰好构成了购物车的 3 个要素,如下图所示。
涉及的命令如下:
当前仅仅是将商品 ID 存储到了 Redis 中,在回显商品具体信息的时候,还需要拿着商品 id 查询一次数据库,获取完整的商品的信息。
Hash 的底层实现可以是 ZipList 或者 Dict
ZipList
编码,用以节省内存。 ZipList
中相邻的两个 entry
分别保存 field
和 value
Dict 中维护一个 哈希表 DitHashtable
,里面有一个 DictEntry 数组,用来存储实际的 key-value
,每个槽位下的 DictEntry 都会形成一个链表,处理哈希冲突
向 Dict 插入一个元素时,先会根据 key 计算出一个哈希值 h,然后计算 h & sizemask 得到数组下标,最后加入该 DictEntry 即可
数据结构体
字典(Dict)
typedef struct dict {
dictType *type; // dict 类型,不同的哈希函数
void *privdata; // 私有数据,做特殊hash的时候用
dictht ht[2]; // 包含两个hash表,一个是当前数据,另个为空,rehash的时候用
long rehashidx; // rehash的进度
int16_t pauserehash; // rehash是否暂停
} dict;
哈希表(DictHashTable)
typedef struct dictht {
dictEntry **table; // 指向entry 的数组
unsigned long size; // 哈希表大小
unsigned long sizemask; // 掩码:size - 1
unsigned long used; // entry 的个数
} dictht;
哈希节点(DictEntry)
typedef struct dictEntry {
void *key; // 键
uNIOn {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;// 值
struct dictEntry *next;// 下一个结点
} dictEntry;
不管是扩容还是收缩,必定会创建新的哈希表,导致哈希表的 size 和 sizemask 变化,而 key 的查询与 sizemask 有关。因此必须对哈希表中的每一个 key 重新计算索引,插入新的哈希表,这个过程称为 rehash。过程是这样的:
dict.ht[0].used + 1
的2^ndict.ht[0].used
的2^n (不得小于4)Set 是 Redis 中的无序集合,可以保证元素的唯一性,但不一定保证元素的有序性,可以对 set 取交集,并集等操作
Set 常用操作
# 往集合key中存入元素,元素存在则忽略,若key不存在则新建
SADD key member [member ...]
# 从集合key中删除元素
SREM key member [member ...]
# 获取集合key中所有元素
SMEMBERS key
# 获取集合key中的元素个数
SCARD key
# 判断member元素是否存在于集合key中
SISMEMBER key member
# 从集合key中随机选出count个元素,元素不从key中删除
SRANDMEMBER key [count]
# 从集合key中随机选出count个元素,元素从key中删除
SPOP key [count]
Set 集合操作
# 交集运算
SINTER key [key ...]
# 将交集结果存入新集合destination中
SINTERSTORE destination key [key ...]
# 并集运算
SUNION key [key ...]
# 将并集结果存入新集合destination中
SUNIONSTORE destination key [key ...]
# 差集运算
SDIFF key [key ...]
# 将差集结果存入新集合destination中
SDIFFSTORE destination key [key ...]
点赞
使用 set 集合来存储,key 为文章 id,value为 用户 id 即可,获取该文章的点赞数量取该 set 中 id 的总数即可
关注
使用 set 集合来存储,key 为用户 id,value 为该用户关注的用户的 id,可以对两个 set 取并集,就可以找出它们的共同关注
抽奖
使用 set 集合来存储,key 为活动 id,value 为参见该活动的用户
SPOP key [count]
来随机获取 count
个用户,并从 set 中移除SRANDMEMBER key [count]
来随机获取 count 个用户,但不会从 set 中移除Set 底层是通过 IntSet(整数集合)或者 Dict 来实现的
set-max-intset-entries
时,会使用 intset
来存储数据Dict
来存储Intset 是 Redis 中的一种实现模式,基于整数数组来实现,具备长度可变,有序的特征
结构体
typedef struct intset {
uint32_t encoding; // 编码方式
uint32_t length; // 长度
int8_t contents[]; // 存储位置
} intset;
encoding
:表示存储整数的大小
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
length
:数组的长度
contents
:存储实际的内容
encoding
:4字节
length
:4字节
contents
:2字节 * 3 = 6 字节
扩容过程
我们向该其中添加一个数字:50000,这个数字超出了 int16_t 的范围,intset 会自动升级编码方式到合适的大小。
以当前案例来说流程如下:
INTSET_ENC_INT32
, 每个整数占 4 字节,并按照新的编码方式及元素个数扩容数组INTSET_ENC_INT32
,将 length
属性改为 4小结:
ZSet 是一个有序集合类型,比 Set 多出了一个 score 字段,该字段用来对 value 值进行排序,保证 ZSet 中的值是有序的
所以每一个元素都有两个值,一个 score,用来排序,一个 value 存储实际的值
ZSet 常用操作
# 往有序集合key中加入带分值元素
ZADD key score member [[score member]...]
# 往有序集合key中删除元素
ZREM key member [member...]
# 返回有序集合key中元素member的分值
ZSCORE key member
# 返回有序集合key中元素个数
ZCARD key
# 为有序集合key中元素member的分值加上increment
ZINCRBY key increment member
# 正序获取有序集合key从start下标到stop下标的元素
ZRANGE key start stop [WITHSCORES]
# 倒序获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES]
# 返回有序集合中指定分数区间内的成员,分数由低到高排序。
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
# 返回指定成员区间内的成员,按字典正序排列, 分数必须相同。
ZRANGEBYLEX key min max [LIMIT offset count]
# 返回指定成员区间内的成员,按字典倒序排列, 分数必须相同
ZREVRANGEBYLEX key max min [LIMIT offset count]
ZSet 运算操作
# 并集计算(相同元素分值相加),numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积
ZUNIONSTORE desTKEy numberkeys key [key...]
# 交集计算(相同元素分值相加),numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积
ZINTERSTORE destkey numberkeys key [key...]
排行榜
以文章排行榜为例子,使用 article:rank 作为 key,点赞数量作为 score,文章的 id 作为 value 即可,根据 score 从大到小进行排序,则可以得到一个 文章热度的排行榜。
ZSet 使用 压缩列表 或者 跳表 + 字典来实现
Ziplist 本身是没有排序功能的,而且没有键值对的概念,需要通过编码来实现:
当同时满足一下条件时,会选择使用 Ziplist
zset_max_ziplist_entries
,默认是 256zset_max_ziplist_value
字节,默认是 64 KB否则使用 跳表 +Dict
使用 Dict 存储 value 和 score 的映射关系;
使用 跳表 来有序的存储 score 和 value 值,便于范围查找
跳表是一个并联多个有序链表的数据结构,它可以在 O(logn) 的时间复杂度内完成插入、删除、增加的操作;
特点
跳表代码
public class SkipList {
private final int MAX_LEVEL = 32;
private final double factor = 0.25;
private int level = 1;
private final Node head = new Node(null, MAX_LEVEL);
static class Node {
Integer value;
Node[] next;
int size;
public Node(Integer value, int size) {
this.value = value;
this.size = size;
this.next = new Node[size];
}
}
public boolean search(int value) {
Node p = head;
for (int i = level - 1; i >= 0; i--) {
while (p.next[i] != null && p.next[i].value <= value) {
if (p.next[i].value == value) {
return true;
}
p = p.next[i];
}
}
return false;
}
public void insert(int value) {
int randomLevel = randomLevel();
Node newNode = new Node(value, randomLevel);
Node p = head;
for (int i = level - 1; i >= 0; i--) {
while (p.next[i] != null && p.next[i].value <= value) {
p = p.next[i];
}
if (p.next[i] == null) {
p.next[i] = newNode;
} else {
newNode.next[i] = p.next[i];
p.next[i] = newNode;
}
}
if (randomLevel > level) {
for (int i = level ; i < randomLevel ; i++) {
head.next[i] = newNode;
}
level = randomLevel;
}
}
public boolean delete(int value) {
boolean contain = search(value);
Node p = head;
Node h = null;
if (contain) {
for (int i = level - 1; i >= 0; i--) {
while (p.next[i] != null && p.next[i].value <= value) {
h = p;
p = p.next[i];
}
h.next[i] = p.next[i];
}
}
return contain;
}
private int randomLevel() {
int level = 1;
Random random = new Random();
while (random.nextDouble() <= factor && level <= MAX_LEVEL) {
level++;
}
return level;
}
}
以上就是Redis五种数据类型详解的详细内容,更多关于Redis数据类型的资料请关注我们其它相关文章!
--结束END--
本文标题: Redis五种数据类型详解
本文链接: https://lsjlt.com/news/201844.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-10-23
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0