专业编程基础技术教程

网站首页 > 基础教程 正文

IT技术栈:Redis的9种数据类型原来是这样!

ccvgpt 2024-10-12 14:58:18 基础教程 541 ℃

整理不易,不要收藏吃灰,还要点赞支持!

字符串

Redis的字符串类型底层使用的是简单动态字符串(Simple Dynamic String,SDS)作为其数据结构。SDS是Redis自己实现的一种字符串表示,它具有一些传统C语言字符串不具备的特性,例如预分配、自动扩展和内存重用等。

IT技术栈:Redis的9种数据类型原来是这样!

在SDS中,字符串被存储在一个字节数组中,数组的头部包含了一个指向字符串内容的指针,以及一些其他元数据,如字符串长度、空闲空间等。当字符串增长时,SDS会自动分配更多的内存,并将指针指向新的内存位置。此外,SDS还支持释放未使用的空闲空间,以避免内存浪费。

这种数据结构的设计使得Redis字符串在处理大量数据时能够保持高效和快速。例如,对于包含大量元素的列表和集合,Redis可以使用SDS来存储和操作这些数据,避免了传统字符串处理中可能出现的内存分配和复制问题。

Golang模拟演示

package main

import (
"fmt"
"unsafe"
)

type RedisSDS struct {
	len  int
	free int
	buf  []byte
}

func NewRedisSDS(s string) *RedisSDS {
	sds := &RedisSDS{}
	sds.init(len(s))
	copy(sds.buf, s)
	return sds
}

func (sds *RedisSDS) init(size int) {
	sds.len = size
	sds.free = 0
	sds.buf = make([]byte, size+1)
}

func (sds *RedisSDS) Free() int {
	return sds.free
}

func (sds *RedisSDS) Len() int {
	return sds.len
}

func (sds *RedisSDS) Append(s string) {
	sds.len += len(s)
	sds.free -= len(s)
	copy(sds.buf[sds.len:], s)
}

func main() {
	sds := NewRedisSDS("Hello")
	fmt.Printf("sds: %v\n", sds)
	fmt.Printf("sds.Len(): %v\n", sds.Len())
	fmt.Printf("sds.Free(): %v\n", sds.Free())
	sds.Append(" World")
	fmt.Printf("sds: %v\n", sds)
	fmt.Printf("sds.Len(): %v\n", sds.Len())
	fmt.Printf("sds.Free(): %v\n", sds.Free())
}

特点

  1. 内存分配:Redis SDS使用预分配和自动扩展的策略,避免了频繁的内存分配和复制操作。在创建字符串时,会预先分配一定大小的内存空间,并将字符串内容存储在其中。当字符串长度增加时,会自动扩展内存空间,避免了频繁的内存分配和释放操作,提高了性能。
  2. 内存重用:Redis SDS还支持内存重用,当字符串长度减少时,会释放多余的内存空间,以便后续重新使用。这种内存重用的策略可以有效减少内存浪费,提高内存利用率。
  3. 高效操作:由于Redis SDS结构的设计,使得对字符串的操作更加高效。通过直接操作字节数组,可以避免传统字符串处理中的复制和拼接操作,提高了字符串操作的性能。
  4. 方便扩展:Redis SDS结构的设计使得其可以方便地扩展其他功能。例如,可以通过在结构中添加额外的字段来支持字符串的编码和解码操作,或者支持字符串的子串查找等功能。这种可扩展性使得Redis的字符串类型具有更强的适应性和灵活性。

列表

Redis的列表类型(List)底层使用的是双端队列(Double-Ended Queue,也称为Deque)作为其数据结构。在Redis中,双端队列被实现为两个双向链表,一个用于存储数据,另一个用于指向队列的头部和尾部。

Golang模拟演示

package main  
  
import "fmt"  
  
type RedisList struct {  
    data []interface{}  
}  
  
func (l *RedisList) LeftPush(value interface{}) {  
    l.data = append(l.data, value)  
}  
  
func (l *RedisList) RightPush(value interface{}) {  
    l.data = append(l.data, value)  
}  
  
func (l *RedisList) LeftPop() interface{} {  
    if len(l.data) == 0 {  
        return nil  
    }  
    value := l.data[0]  
    l.data = l.data[1:]  
    return value  
}  
  
func (l *RedisList) RightPop() interface{} {  
    if len(l.data) == 0 {  
        return nil  
    }  
    value := l.data[len(l.data)-1]  
    l.data = l.data[:len(l.data)-1]  
    return value  
}  
  
func main() {  
    list := RedisList{}  
    list.LeftPush("foo")  
    list.RightPush("bar")  
    fmt.Println(list) // Output: [bar foo]  
    fmt.Println(list.LeftPop()) // Output: foo  
    fmt.Println(list.RightPop()) // Output: bar  
}

特点

  1. 高效插入和删除:双端队列支持在队列的头部和尾部高效地插入和删除元素。这使得Redis列表可以快速地在两端添加或删除元素。
  2. 灵活的操作:使用双端队列,Redis可以实现从列表的两端进行添加和删除操作。这使得Redis列表适用于需要从两端处理数据的场景,如聊天室的消息队列、缓存等。
  3. 内存使用效率高:双端队列通过使用链表实现了空间的复用。当删除元素时,只需要调整指针指向,而不需要移动其他元素。这使得Redis列表在处理大量数据时能够更高效地利用内存资源。

使用场景

  1. 发布/订阅模型:Redis的发布/订阅功能可以用于实现消息的广播和监听。发布者可以向一个或多个频道发布消息,而订阅者则可以订阅一个或多个频道来接收消息。这种模型可以用于实现实时通信、消息推送等应用。
  2. 任务队列:Redis可以作为一个简单的任务队列使用。任务队列通常用于将任务添加到一个队列中,然后由后台工作线程或进程执行。这种模型可以用于实现异步任务处理、后台任务调度等应用。
  3. 缓存队列:Redis可以作为一个缓存队列使用,将数据暂时存储在队列中,然后由其他系统进行消费和处理。这种模型可以用于减轻数据库压力、加速数据访问等应用。
  4. 日志处理:Redis可以作为一个高速的日志处理平台,将日志数据添加到队列中,然后由其他系统进行实时分析和处理。这种模型可以用于实现实时监控、异常检测等应用。
  5. 事件驱动架构:Redis可以作为一个事件驱动架构的组成部分,实现事件发布和订阅、事件处理等环节。这种模型可以用于实现响应速度快、可扩展性强的事件驱动应用。

集合

Redis的集合类型(Set)底层使用的是哈希表(Hash Table)作为其数据结构。

Golang模拟实现

package main  
  
import "fmt"  
  
type RedisSet struct {  
    hashTable map[string]bool  
}  
  
func NewRedisSet() *RedisSet {  
    return &RedisSet{  
        hashTable: make(map[string]bool),  
    }  
}  
  
func (set *RedisSet) Add(member string) {  
    set.hashTable[member] = true  
}  
  
func (set *RedisSet) Remove(member string) {  
    delete(set.hashTable, member)  
}  
  
func (set *RedisSet) Contains(member string) bool {  
    return set.hashTable[member]  
}  
  
func (set *RedisSet) Size() int {  
    count := 0  
    for key := range set.hashTable {  
        count++  
    }  
    return count  
}  
  
func main() {  
    set := NewRedisSet()  
    set.Add("member1")  
    set.Add("member2")  
    set.Add("member3")  
    fmt.Println("Set size:", set.Size())  
    fmt.Println("Contains member1:", set.Contains("member1"))  
    set.Remove("member1")  
    fmt.Println("Set size after removing member1:", set.Size())  
}

特点

  1. 快速查找:哈希表在平均情况下具有接近O(1)的查找复杂度,这意味着无论集合中有多少成员,查找成员的操作都可以在几乎恒定的时间内完成。这使得Redis的集合类型非常适合于需要快速查找操作的数据。
  2. 动态扩容:哈希表可以动态地分配内存空间,根据需要自动扩展或收缩。当集合中成员数量增加时,哈希表可以自动扩展以容纳更多成员;当集合中成员数量减少时,哈希表可以自动收缩以释放内存。这种动态扩容的能力使得Redis的集合类型可以高效地处理数据的变化。
  3. 空间利用率高:哈希表的空间利用率通常较高,因为它们可以很好地利用内存空间,使得每个槽位(bucket)都尽可能地被使用。这有助于减少内存浪费,提高集合类型的空间效率。

使用场景

  1. 存储一些集合性的数据,例如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。
  2. 利用Redis提供的集合数据结构进行求交集、并集、差集等操作,可以非常方便地实现如共同关注、共同喜好、二度好友等功能。

有序集合

Redis的有序集合(Sorted Set)底层使用的是一种称为跳跃列表(Skip List)的数据结构来实现。跳跃列表是一个随机化的数据结构,它允许在O(log N)的时间复杂度内实现元素的插入、删除和查找操作。

跳跃列表是一个随机化的数据结构,它通过多级索引实现高效的插入、删除和查找操作。每个节点包含一个值、一个分数(Score)和一个指向下一层节点的指针。通过这种分层结构,跳跃列表可以在log(N)的时间复杂度内完成元素的插入、删除和查找操作。

在Redis的有序集合中,每个节点还包含一个分数(Score)字段,用于对元素进行排序。跳跃列表的每个节点都有一个指向下一层相同值节点的指针,形成一个双向链表结构,这样可以方便地进行遍历和排序操作。

使用跳跃列表作为底层数据结构,Redis的有序集合可以高效地处理大量有序的元素,并提供类似ZADD、ZRANGE等操作来对集合进行排序和查找。跳跃列表还支持反向查找功能,可以通过给定的值和指针反向查找上一层中具有相同值的节点。

此外,Redis有序集合还支持一些常用的操作,如ZREM、ZINCRBY等,用于删除元素、增加元素的分数等。这些操作的时间复杂度通常是O(log N),其中N是有序集合中元素的数量。

Golang模拟演示

package main

import (
	"fmt"
	"math/rand"
	"time"
)

// SkipListNode 表示跳跃列表中的节点  
type SkipListNode struct {
	Value     string   // 节点值  
	Score     float64   // 节点分数  
	Right     *SkipListNode // 右指针  
	Left      *SkipListNode // 左指针  
	Down      *SkipListNode // 下指针  
	Level     int        // 节点所在的层数  
}

// SkipList 表示跳跃列表  
type SkipList struct {
	Head *SkipListNode // 头节点  
}

// NewSkipList 创建一个新的跳跃列表  
func NewSkipList() *SkipList {
	return &SkipList{Head: &SkipListNode{}}
}

// Insert 插入一个元素到跳跃列表中  
func (sl *SkipList) Insert(value string, score float64) {
	level := sl.Head.Level + 1 // 计算层数  
	for level > 0 {
		// 随机决定向左或向右插入节点  
		if rand.Float64() < 0.5 {
			right := sl.Head.Right
			if right == nil || right.Level < level-1 {
				newNode := &SkipListNode{Value: value, Score: score, Right: nil, Left: nil, Down: sl.Head, Level: level}
				right = newNode
			} else {
				for right != nil && right.Level == level-1 {
					right = right.Right
				}
				right = &SkipListNode{Value: value, Score: score, Right: nil, Left: nil, Down: sl.Head, Level: level}
			}
			right.Right = newNode
			newNode.Left = right
			newNode.Down = newNode.Right.Down
			newNode.Right.Down = newNode.Left.Down
			sl.Head.Right = newNode.Right.Right
		} else {
			left := sl.Head.Left
			if left == nil || left.Level < level-1 {
				newNode := &SkipListNode{Value: value, Score: score, Right: nil, Left: nil, Down: sl.Head, Level: level}
				left = newNode
			} else {
				for left != nil && left.Level == level-1 {
					left = left.Left
				}
				left = &SkipListNode{Value: value, Score: score, Right: nil, Left: nil, Down: sl.Head, Level: level}
			}
			left.Left = newNode
			newNode.Right = left
			newNode.Down = newNode.Left.Down
			newNode.Left.Down = newNode.Right.Down
			sl.Head.Left = newNode.Left.Left
		}
		level-- // 层数递减,继续向下插入节点直到到达顶层或无法插入为止  
	}
}

特点

  1. 有序:底层数据结构是有序的,每个节点都包含一个分数(Score)字段,用于对元素进行排序。
  2. 多层:跳跃列表是一个分层结构,每个节点都有一个指向下一层相同值节点的指针,形成一个双向链表结构。
  3. 去重:底层数据结构中的每个元素都是唯一的,不会出现重复的情况。
  4. 构成有序集合的底层结构(ZSET):有序集合是跳跃列表的一种应用,它底层的数据结构就是跳跃列表。
  5. 查询效率高:由于跳跃列表的分层结构和每个节点包含指向下一层相同值节点的指针,使得它可以在log(N)的时间复杂度内完成元素的插入、删除和查找操作。
  6. 支持反向查找:跳跃列表还支持反向查找功能,可以通过给定的值和指针反向查找上一层中具有相同值的节点。
  7. 压缩列表:Redis为了节省内存,自行设计了一种数据结构——压缩列表,它是对于数组的每一个元素,都带有前一元素占用内存的大小,从而减少内存开销。

使用场景

  1. 带有权重的元素:例如,一个游戏的用户得分排行榜。
  2. 比较复杂的数据结构:一般用到的场景不算太多,和Sets相比,Sorted Sets是将Set中的元素增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
  3. 带权重的消息队列:Sorted Set还可以用来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务,让重要的任务优先执行。
  4. 共同好友、共同喜好、二度好友等场景:可以利用Sorted Set统计网站访问IP(利用唯一性,统计访问网站的所有独立IP)以及进行好友推荐(好友推荐时,根据tag求交集,大于某个阈值就可以推荐)。

Hash

Redis的Hash类型底层使用的是哈希表作为其数据结构。在Redis中,哈希表用于存储键值对,其中键用于唯一标识每个值,而值可以是任何类型的数据。

哈希类型常用的方法包括:HSET、HGET、HDEL等。这些方法用于设置、获取和删除哈希表中的键值对。使用场景包括但不限于存储用户信息、缓存对象等。时间复杂度通常是O(1),即对单个键值对的操作是常数时间复杂度。

Golang模拟演示

package main

import "fmt"

// Hash 表示Redis的哈希表  
type Hash map[string]interface{}

// NewHash 创建一个新的哈希表  
func NewHash() Hash {
	return make(map[string]interface{})
}

// Get 获取哈希表中指定键的值  
func (h Hash) Get(key string) interface{} {
	value, ok := h[key]
	if ok {
		return value
	}
	return nil
}

// Set 设置哈希表中键值对的值  
func (h Hash) Set(key string, value interface{}) {
	h[key] = value
}

// Delete 删除哈希表中指定的键值对  
func (h Hash) Delete(key string) {
	delete(h, key)
}

// Keys 返回哈希表中所有键的列表  
func (h Hash) Keys() []string {
	keys := make([]string, 0, len(h))
	for key := range h {
		keys = append(keys, key)
	}
	return keys
}

// Values 返回哈希表中所有值的列表  
func (h Hash) Values() []interface{} {
	values := make([]interface{}, 0, len(h))
	for _, value := range h {
		values = append(values, value)
	}
	return values
}

特点

  1. 快速访问:哈希表提供快速的键值查找操作,使得在Redis中访问哈希类型的元素非常高效。
  2. 动态增长:哈希表可以动态地增长以适应存储的数据量,无需预先分配固定数量的内存空间。
  3. 内存高效:哈希表在内存中以紧凑的形式存储键值对,使得Redis能够高效地利用内存资源。

使用场景

  1. 数据库:Redis的哈希类型可以用于实现数据库的底层结构,例如实现表和列的映射关系,或者作为数据库中的数据项的存储结构。
  2. 缓存:哈希类型可以用于缓存数据库查询结果或其他计算结果,以提供快速访问。
  3. 会话管理:哈希类型可以用于存储和管理用户会话信息,包括用户ID、登录状态等信息。
  4. 配置管理:Redis的哈希类型可以用于存储和管理应用程序的配置信息,例如配置参数、环境变量等。
  5. 对象存储:哈希类型可以用于存储对象,例如文件、图片或其他二进制数据。
  6. 计数器:哈希类型可以用于实现计数器功能,例如统计网站访问量、用户行为等。

其他

Redis支持的不仅仅是五种数据类型。除了五种基本数据类型(string、hash、list、set、zset)之外,Redis还支持其他四种特殊的数据类型。因此,Redis总共支持九种数据类型。

  1. Stream:用于实现高级消息队列,可以用于处理实时数据流。
  2. HyperLogLog:用于估计数据集的基数,可以在不存储完整数据集的情况下进行基数统计。
  3. Geospatial:用于存储地理空间数据,可以用于地理位置的搜索和距离计算。
  4. Pub/Sub:用于实现发布/订阅模型,可以用于实时通信和消息传递。

特殊的这4类,目前没有用过,欢迎大家补充!

【申明:部分图片来源于网络,侵权,联系,删除】

Tags:

最近发表
标签列表