Redis 学习教程·三 —— Redis 的五大基本类型操作与底层实现

概述

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作 数据库、缓存和消息中间件

它支持多种类型的数据结构,如 字符串(strings)散列(hashes)列表(lists)集合(sets)有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。

Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。


String 类型详解

基本的数据类型,字符串

127.0.0.1:6379> set key1 v1  # 设置值
OK
127.0.0.1:6379> get key1  # 获取值
"v1"
127.0.0.1:6379> keys *   # 获取所有的 key
1) "key1"
2) "age"
127.0.0.1:6379> EXISTS key1  # 判断一个 key 是否存在
(integer) 1
127.0.0.1:6379> APPEND key1 "hello"  # 追加字符串,如果当前 key 不存在,就相当于 set key
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> APPEND key1 "zzzzkkkk"
(integer) 15
127.0.0.1:6379> STRLEN key1   # 获取字符串的长度
(integer) 15
127.0.0.1:6379> get key1
"v1hellozzzzkkkk"
# 设置值
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views  # 自加 1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views   # 自减 1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> INCRBY views 10  # 可以设置增加的步长
(integer) 12
127.0.0.1:6379> INCRBY views 10
(integer) 22
127.0.0.1:6379> DECRBY views 5  # 设置减少的步长
(integer) 17
127.0.0.1:6379> get views
"17"
# 字符串范围 range
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set key1 "Hello World"
OK
127.0.0.1:6379> get key1
"Hello World"
127.0.0.1:6379> GETRANGE key1 0 3     # 截取字符串 [0, 3]
"Hell"
127.0.0.1:6379> GETRANGE key1 0 -1  # 获取全部字符串 == get
"Hello World"

# 替换
127.0.0.1:6379> set key2 ahuoffdewe
OK
127.0.0.1:6379> get key2
"ahuoffdewe"
127.0.0.1:6379> SETRANGE key2 1 xx  # 替换指定位置开始的字符串
(integer) 10
127.0.0.1:6379> get key2
"axxoffdewe"
# setex   (set with expire)  # 设置过期时间
# setnx   (set with exist)   # 不存在设置(在分布式锁中常常使用)
127.0.0.1:6379> setex key3 60 "hello"  # 设置 key3 ,60s后过期
OK
127.0.0.1:6379> ttl key3
(integer) 55
127.0.0.1:6379> get key3
"hello"
127.0.0.1:6379> setnx mykey "redis"  # mykey 如果不存在,则设置
(integer) 1
127.0.0.1:6379> keys *
1) "key3"
2) "key1"
3) "mykey"
4) "key2"
127.0.0.1:6379> keys *
1) "key1"
2) "mykey"
3) "key2"
127.0.0.1:6379> ttl key3
(integer) -2
127.0.0.1:6379> setnx mykey "MongoDB"  # 修改失败,返回值 0
(integer) 0
127.0.0.1:6379> get mykey
"redis"
#  批量设置
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3  # 同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3  # 同时获取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4  # msetnx 是一个原子性的操作,要么一起失败,要么一起成功
(integer) 0
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"

# 对象
set user:1:{name:zhangsan, age:3}  # 设置一个 user:1 对象  值为 json 字符来保存一个对象
# user:{id}:{field}  是可以的
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"
#  getset  先 get 再 set
127.0.0.1:6379> getset db redis   # 如果不存在值,就返回 nil
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mongodb  # 如果存在值,获取原来的值,并设置新的值
"redis"
127.0.0.1:6379> get db
"mongodb"

String 数据类型的底层实现

Redis 中所有场景中出现的字符串,基本都是由 SDS (动态字符串)来实现的;
SDS 是 “simple dynamic string” 的缩写。

SDS 的源码
// 3.0及以前
struct sdshdr {
    // 记录buf数组中已使用字节数量
    unsigned int len;
    // 记录buf数组中未使用的字节数量
    unsigned int free;
    // 字节数组,存储字符串
    char buf[];
};

// >=3.2
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};


//在3.2以后的版本,redis 的SDS分为了5种数据结构,分别应对不同长度的字符串需求,具体的类型选择如下。
static inline char sdsReqType(size_t string_size) { // 获取类型
    if (string_size < 1<<5)     // 32
        return SDS_TYPE_5;
    if (string_size < 1<<8)     // 256
        return SDS_TYPE_8;
    if (string_size < 1<<16)    // 65536 64k
        return SDS_TYPE_16;
    if (string_size < 1ll<<32)  // 4294967296 4GB
        return SDS_TYPE_32;
    return SDS_TYPE_64;
}

SDS 有两种存储形式:一种是 raw,一种是 embstr

所有的 Redis 对象都有一个 Redis 对象头结构体

struct RedisObject { // 一共占用16字节
    int4 type; // 4bits  类型
    int4 encoding; // 4bits 存储格式
    int24 lru; // 24bits 记录LRU信息
    int32 refcount; // 4bytes 
    void *ptr; // 8bytes,64-bit system 
} robj;

raw 在分配内存时,RedisObject 和 SDS 各分配一块内存,需要两次分配内存;两个对象头在内存地址一般是不连续的。

而 embstr 是 RedisObject 和 SDS 同处于一块内存,分配内存一次就分配就实现了; RedisObject 对象头和 SDS 对象头是连续存在一起的

  • 空间预分配
    为减少修改字符串带来的内存重分配次数,SDS 采用了“一次管够”的策略;
    若修改之后,SDS 长度小于 1MB,则多分配现有 len 长度的空间;
    若修改之后,SDS 长度大于等于 1MB,则扩充除了满足修改之后的长度外,额外多 1MB 空间

  • 惰性空间释放
    为避免缩短字符串时候的内存重分配操作,SDS 在数据减少时,并不立刻释放空间。

List 类型详解

基本的数据类型,列表

在 Redis 中,可以把 list 当作栈、队列、阻塞队列使用

Redis 的命令不区分大小写

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> LPUSH list one   # 将一个值或者多个值插入到 列表的头部(左)
(integer) 1
127.0.0.1:6379> LPUSH list two
(integer) 2
127.0.0.1:6379> LPUSH list three
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1  # 获取 list 中的值 
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> LRANGE list 0 1  # 获取值
1) "three"
2) "two"
127.0.0.1:6379> RPUSH list four  # 将一个值或者多个值插入到 列表的头部(右)
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
127.0.0.1:6379> Lpop list  # 移除第一个元素
"three"
127.0.0.1:6379> Rpop list   # 移除最后一个元素
"four"
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lindex list 1  # 通过下标获取值
"one"
127.0.0.1:6379> lindex list 0
"two"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> Lpush list one
(integer) 1
127.0.0.1:6379> Lpush list two
(integer) 2
127.0.0.1:6379> Lpush list three
(integer) 3
127.0.0.1:6379> Lpush list four
(integer) 4
127.0.0.1:6379> llen list  # 获取 list 长度
(integer) 4
127.0.0.1:6379> lrem list 1 one  # 移除 list 集合中指定个数的 value,精确匹配
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1
1) "four"
2) "three"
3) "two"
127.0.0.1:6379> Lpush list three
(integer) 4
127.0.0.1:6379> Lpush list three
(integer) 5
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "three"
3) "four"
4) "three"
5) "two"
127.0.0.1:6379> lrem list 2 three
(integer) 2
127.0.0.1:6379> LRANGE list 0 -1
1) "four"
2) "three"
3) "two"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> Rpush mylist "hello"
(integer) 1
127.0.0.1:6379> Rpush mylist "hello1"
(integer) 2
127.0.0.1:6379> Rpush mylist "hello12"
(integer) 3
127.0.0.1:6379> Rpush mylist "hello123"
(integer) 4
127.0.0.1:6379> ltrim mylist 1 2  # 通过下标截取指定的长度,这个list 已经被改变了,只剩下截取后的不放呢
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello12"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> rpush mylist "hello"
(integer) 1
127.0.0.1:6379> rpush mylist "hello1"
(integer) 2
127.0.0.1:6379> rpush mylist "hello12"
(integer) 3
127.0.0.1:6379> rpush mylist "hello123"
(integer) 4
127.0.0.1:6379> rpoplpush mylist myother   # 移除列表的最后一个元素,并移动到新的列表
"hello123"
127.0.0.1:6379> lrange mylist 0 -1  # 查看原来列表
1) "hello"
2) "hello1"
3) "hello12"
127.0.0.1:6379> lrange myother 0 -1  # 查看新的列表
1) "hello123"
127.0.0.1:6379> exists list   # 判断list 是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item   # 如果不存在列表,则会报错
(error) ERR no such key
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> lrange list 0 0
1) "value1"
127.0.0.1:6379> lset list 0 item  # 如果存在,更新当前下标的值
OK
127.0.0.1:6379> lrange list 0 0  # 如果下标不存在,则会报错
1) "item"
# insert   将某个具体的 value 插入到列表中 某个元素的前面或者后面
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> rpush mylist "hello"
(integer) 1
127.0.0.1:6379> rpush mylist zzz
(integer) 2
127.0.0.1:6379> linsert mylist before "zzz" "other"
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "other"
3) "zzz"
127.0.0.1:6379> linsert mylist after "zzz" "new"
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "other"
3) "zzz"
4) "new"

List 的底层实现

在 Redis 3.2 版本之前,List 使用 ziplist 或者 linkedlist 实现,如下图1

但是,在之后的版本中,List 使用 quicklist 实现

List 的底层实现1
List 的底层实现2

  • ziplist
    压缩列表,适合用于长度较小的值;

    由连续空间组成(会保存每个值得长度信息,可以依次找到各个值)

    存取效率高,内存占用小,但是由于是连续内存,修改操作需要重新分配内存

  • linkedlist
    双向链表,修改效率高,但是由于需要保存前后指针,占用内存比较多

  • quicklist
    可以看作是一种混合结构,quicklistziplistlinkedlist 的混合体

    linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串连起来

    既满足了快速的插入删除功能,又不会出现太大的空间冗余

Set 类型详解

set 是一个 无序不重复 集合,set 中的值不能重复

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> sadd myset "hello"  # 往 set 中添加元素
(integer) 1
127.0.0.1:6379> sadd myset "zzz"
(integer) 1
127.0.0.1:6379> sadd myset "love"
(integer) 1
127.0.0.1:6379> smembers myset   # 查看指定 set 的所有值
1) "love"
2) "zzz"
3) "hello"
127.0.0.1:6379> sismember myset hello  # 判断某一个值是否在 set 集合中,存在
(integer) 1
127.0.0.1:6379> sismember myset hello1  # 不存在
(integer) 0
127.0.0.1:6379> scard myset   # 查看set 集合中元素的个数
(integer) 3
127.0.0.1:6379> srem myset hello  # 移除 set 集合中的指定元素
(integer) 1
127.0.0.1:6379> scard myset
(integer) 2
127.0.0.1:6379> smembers myset
1) "love"
2) "zzz"
127.0.0.1:6379> sadd myset "hello"
(integer) 1
127.0.0.1:6379> sadd myset "world"
(integer) 1
127.0.0.1:6379> sadd myset "earth"
(integer) 1
127.0.0.1:6379> smembers myset
1) "love"
2) "zzz"
3) "world"
4) "hello"
5) "earth"
127.0.0.1:6379> srandmember myset  # 随机抽取一个元素
"world"
127.0.0.1:6379> srandmember myset
"hello"
127.0.0.1:6379> srandmember myset 2  # 随机抽取指定个数的元素
1) "zzz"
2) "hello"
127.0.0.1:6379> srandmember myset 2
1) "zzz"
2) "hello"
127.0.0.1:6379> srandmember myset 2
1) "world"
2) "earth"
127.0.0.1:6379> smembers myset
1) "hello"
2) "zzz"
3) "love"
4) "world"
5) "earth"
127.0.0.1:6379> spop myset  # 随机删除一些 set 集合中的元素
"hello"
127.0.0.1:6379> spop myset
"earth"
127.0.0.1:6379> smembers myset
1) "zzz"
2) "love"
3) "world"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> sadd myset "hello"
(integer) 1
127.0.0.1:6379> sadd myset "world"
(integer) 1
127.0.0.1:6379> sadd myset "earth"
(integer) 1
127.0.0.1:6379> sadd myset2 "water"
(integer) 1
127.0.0.1:6379> smove myset myset2 "earth"  # 将一个指定的值,移动到另外一个 set 集合中
(integer) 1
127.0.0.1:6379> smembers myset
1) "world"
2) "hello"
127.0.0.1:6379> smembers myset2
1) "water"
2) "earth"
127.0.0.1:6379> smembers myset
1) "world"
2) "hello"
127.0.0.1:6379> smembers myset2
1) "water"
2) "earth"
127.0.0.1:6379> sadd myset "earth"
(integer) 1
127.0.0.1:6379> sdiff myset myset2   # 差集
1) "world"
2) "hello"
127.0.0.1:6379> sinter myset myset2  # 交集
1) "earth"
127.0.0.1:6379> sunion myset myset2  # 并集
1) "water"
2) "world"
3) "earth"
4) "hello"

Set 的底层实现

Set 的底层使用 intset 或者 hashtable 实现

当集合中的数据都是整数时,且数量不超过 512 个,使用 intset 方式;

intset 是有序不重复的连续空间,可以节约内存,但是也由于是连续的空间存储,修改效率不高

Set 使用 hashtable 实现:其中 value 使用 NULL 填充

哈希表的制作方法一般有两种,一种是开放寻址法,一种是拉链法。Redis 的哈希表的制作使用的是拉链法。

Set 的底层实现

Hash 类型详解

可以等同于一个 Map 集合,还是以 key-value 的形式存在,key-map 此时的值是一个集合

127.0.0.1:6379> hset myhash field1 zzz  # set 一个具体的 key-value
(integer) 1
127.0.0.1:6379> hget myhash field1  # 获取一个字段值
"zzz"
127.0.0.1:6379> hmset myhash field2 hello field3 world  # set 多个 key-value
OK
127.0.0.1:6379> hmget myhash field1 field2 field3  # 获取多个字段值
1) "zzz"
2) "hello"
3) "world"
127.0.0.1:6379> hgetall myhash   # 获取全部数据
1) "field1"
2) "zzz"
3) "field2"
4) "hello"
5) "field3"
6) "world"
127.0.0.1:6379> hdel myhash field1  # 删除 hash 指定的 key 字段,对应的 value 也就消失
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "hello"
3) "field3"
4) "world"
127.0.0.1:6379> hgetall myhash
1) "field1"
2) "hello"
3) "field2"
4) "world"
5) "field3"
6) "hello"
7) "field4"
8) "earth"
127.0.0.1:6379> hlen myhash  # 获取hash 表的字段数量
(integer) 4
127.0.0.1:6379> hexists myhash field2  # 判断 hash 中指定字段是否存在
(integer) 1
127.0.0.1:6379> hexists myhash field5  # 不存在
(integer) 0
127.0.0.1:6379> hkeys myhash   # 只获得所有的 field
1) "field1"
2) "field2"
3) "field3"
4) "field4"
127.0.0.1:6379> hvals myhash  # 只获得所有的 value
1) "hello"
2) "world"
3) "hello"
4) "earth"
127.0.0.1:6379> hset myhash field5 5  # 指定增量
(integer) 1
127.0.0.1:6379> hincrby myhash field5 1   # 加 1
(integer) 6
127.0.0.1:6379> hget myhash field5
"6"
127.0.0.1:6379> hincrby myhash field5 -1  # 减 1
(integer) 5
127.0.0.1:6379> hget myhash field5
"5"
127.0.0.1:6379> hsetnx myhash field6 lll   # 如果不存在则设置
(integer) 1
127.0.0.1:6379> hsetnx myhash field6 ooo  # 如果存在,则不可以设置
(integer) 0

hash 的应用:

  • 存储变更的数据,尤其是用户的信息等经常变动的信息
  • hash 更适合对象的存储,而 string 更适合字符串的存储

hash 的底层实现

Hash 的底层实现

Zset 类型详解

Zset 是一个有序集合

127.0.0.1:6379> zadd myset 1 one
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three
(integer) 2
127.0.0.1:6379> zrange myset 0 -1
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> zadd salary 2500 xiaoh   # 添加用户
(integer) 1
127.0.0.1:6379> zadd salary 3000 zhangss
(integer) 1
127.0.0.1:6379> zadd salary 200 kuahshen
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf +inf  # 显示所有用户 从小到大
1) "kuahshen"
2) "xiaoh"
3) "zhangss"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores # 显示所有用户 从小到大,附带成绩
1) "kuahshen"
2) "200"
3) "xiaoh"
4) "2500"
5) "zhangss"
6) "3000"
127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores # 显示所有用户 从小到大,salary 小于 2500
1) "kuahshen"
2) "200"
3) "xiaoh"
4) "2500"
127.0.0.1:6379> zrevrange salary 0 -1  # 从大到小 排序
1) "zhangss"
2) "kuahshen"
127.0.0.1:6379> zrange salary 0 -1
1) "kuahshen"
2) "xiaoh"
3) "zhangss"
127.0.0.1:6379> zrem salary xiaoh  # 移除 有序集合中的指定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "kuahshen"
2) "zhangss"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> zadd myset 1 hello
(integer) 1
127.0.0.1:6379> zadd myset 2 world 3 zzz
(integer) 2
127.0.0.1:6379> zcount myset 1 3   # 获取指定区间的成员数量
(integer) 3
127.0.0.1:6379> zcount myset 1 2
(integer) 2

Zset 的底层实现

Zset 的底层存储结构使用 ziplist(压缩双向链表) 或者 skiplist(跳表)

如果 Zset 储存的数据中,存储元素数量超过 128 个,每个节点最大存储字节数超过 64 字节,则从 ziplist 转换到 skiplist

对于零散数据,数据量比较少,使用 ziplist 可以节省内存的使用

ziplist 作为 Zset 的底层存储结构时候,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值

skiplist 作为 Zset 的底层存储结构的时候,使用 skiplist 按序保存元素及分值,使用 dict 来保存元素和分值的映射关系

Zset 的底层实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值