Redis面试八股文-基础数据结构实战(String,Hash,List,Set,SortedSet)

Redis是干嘛用的?为什么要选用Redis?

Redis一个开源的,是高性能的 key-value 数据库,单线程,所有操作均为原子操作。可以用来进行高性能的内存缓存,例如高度重复的数据库查询、秒杀活动中的库存防超卖、消息队列、数据基数统计、距离数据索引/查询等等。本文先从基本的数据结构入手,进行实际应用。

Redis有几种数据类型或结构?

Redis有如下几种数据类型:
* String
* Hash
* List
* Set
* Sorted Set
* Pub/Sub
* Geo
* HyperLogLog
* Bitmap
* Bitfields
* Stream (Redis 5.0以后)

什么?其他博文上为啥都说只有5种数据类型?严格意义上来说也没错,Redis最初的基础数据类型就是只有前5种,但是后面陆续添加了几种数据类型,严格意义来讲pub/sub并不是一种数据类型,而是 Redis 的一种消息传递模式,这里不过多赘述,在后续写Redis消息队列的时候详细说明。本文先就前5种数据类型的实际应用进行详细说明。

本文所有实例代码均基于golang和go-redis(v9)库

String(字符串)

String是Redis中最简单、最常用的数据类型,它是一个二进制安全的字符串,可以包含任何数据,比如jpg图片或序列化的对象等。实际上Redis种所有直接存储的数据都需要被转化为字节数组存储为String类型或直接存储为String类型。
String类型的特点是单个键值对的最大存储空间为512MB,可以通过set、get等命令进行读写操作。

String类型在实际应用中的常见使用场景有:

缓存:

缓存常用数据,如用户信息、文章内容等,以提高访问速度。

简单的字符串缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func testString() {
    // 测试string类型
    stringKey := "UserInfo_709394"
    result, err := redisClient.Set(
        context.Background(),
        stringKey,
        "{\"user_id\": \"709394\", \"user_name\": \"尼古拉斯-赵四\", \"age\": 48}",
        time.Second*2, // 缓存2秒
    ).Result()

    if err != nil {
        log.Fatalln(err.Error())
    }
    fmt.Println("Set result:", result)

    result, err = redisClient.Get(context.Background(), stringKey).Result()
    if err != nil {
        fmt.Println(err.Error())
    }
    fmt.Println("Get result:", result)

    time.Sleep(time.Second * 3) // 等待缓存过期

    result, err = redisClient.Get(context.Background(), stringKey).Result()
    if err != nil {
        fmt.Println("Get error:", err.Error())
    }
    fmt.Println("Get result:", result)
}

执行结果:

1
2
3
4
Set result: OK
Get result: {"user_id": "709394", "user_name": "尼古拉斯-赵四", "age": 48}
Get error: redis: nil
Get result:

字节数组存储(例如golang中的struct):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// PackageListOutPut 门店套餐列表返回值
type PackageListOutPut struct {
    PackageID      string `json:"package_id"                orm:"column(package_id)"`
    UniqueMark     string `json:"unique_mark"               orm:"column(unique_mark)"`
    PackageSubject string `json:"package_subject"           orm:"column(package_subject)"`
    PackageSummary string `json:"package_summary"           orm:"column(package_summary)"`
    SortMark       string `json:"sort_mark"                 orm:"column(sort_mark)"`
    CreateTime     string `json:"create_time"               orm:"column(create_time)"`
    ModifyTime     string `json:"modify_time"               orm:"column(modify_time)"`
    CoverImage     string `json:"cover_image"               orm:"column(cover_image)"`
    FixedPrice     string `json:"fixed_price"               orm:"column(fixed_price)"`
    EstimatedPrice string `json:"estimated_price"           orm:"column(estimated_price)"`
    AttachAction   string `json:"attach_action"             orm:"column(attach_action)"`
    PackageType    string `json:"package_type"              orm:"column(package_type)"`
}

// 实现 encoding.BinaryMarshaler 接口
func (m *PackageListOutPut) UnmarshalBinary(data []byte) error {
    return json.Unmarshal(data, m)
}

func (m *PackageListOutPut) MarshalBinary() (data []byte, err error) {
    return json.Marshal(m)
}

func testStruct() {
    structStoreKey := "PackageListOutPut_store_id_100001"

    toStoreStruct := PackageListOutPut{
        PackageID:      "709394",
        UniqueMark:     "bedee0e6-834d-4593-997c-aea9b58d0286",
        PackageSubject: "美容洗护套餐",
        PackageSummary: "深层清洁,进制提拉",
        SortMark:       "100",
        CreateTime:     "1672599845000",
        ModifyTime:     "1672599845000",
        CoverImage:     "https://gjh.me/wp-content/uploads/2015/12/IMG_0003.jpg",
        FixedPrice:     "168.00",
        EstimatedPrice: "299.0",
        AttachAction:   "1",
        PackageType:    "3",
    }

    result, err := redisClient.Set(
        context.Background(),
        structStoreKey,
        &toStoreStruct,
        time.Second*30,
    ).Result()

    if err != nil {
        log.Fatalln(err.Error())
    }
    fmt.Println("Set result:", result)

    var structFromRedis PackageListOutPut
    err = redisClient.Get(context.Background(), structStoreKey).Scan(&structFromRedis)
    if err != nil {
        fmt.Println(err.Error())
    }
    fmt.Println("Get result:", structFromRedis)
}

输出结果:

1
2
Set result: OK
Get result: {709394 bedee0e6-834d-4593-997c-aea9b58d0286 美容洗护套餐 深层清洁,进制提拉 100 1672599845000 1672599845000 https://gjh.me/wp-content/uploads/2015/12/IMG_0003.jpg 168.00 299.0 1 3}

这里需要注意的是几乎所有的数据都能够转换为字节数组存储到Redis中,然后读取后也可以很方便的将字节数组序列化到原始的数据格式中。例如golang中实现 encoding.BinaryMarshaler 接口即可。

缓存数字,计数器:

通过incr/decr命令实现自增/自减操作,如网站访问量统计。
通过incr/decr命令还能利用Redis的原子+单线程特性实现秒杀库存的精确减少,从而避免超卖,后续写分布式锁再详细讨论。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func testNumber() {
    // 测试数字
    numberKey := "TodayPV"
    result, err := redisClient.Set(context.Background(), numberKey, 0, time.Second*86400).Result()
    if err != nil {
        log.Fatalln(err.Error())
    }
    fmt.Println(result)

    wg := sync.WaitGroup{}

    for i := 0; i < 10_086; i++ {
        wg.Add(1)
        clickCount := int64(i%5 + 1)
        go func(count int64) { // 模拟并发访问的时候增加PV
            _, err := redisClient.IncrBy(context.Background(), numberKey, count).Result()
            if err != nil {
                log.Fatalln(err.Error())
            }
            wg.Done()
        }(clickCount)
    }

    wg.Wait()

    numberResult, err := redisClient.Get(context.Background(), numberKey).Int64()
    if err != nil {
        fmt.Println(err.Error())
    }
    fmt.Println(numberResult)
}

输出结果:

1
2
OK
30256

Hash(哈希表)

Hash是Redis中存储键值对的数据类型,与String类型不同的是,Hash类型可以存储多个属性和对应的值。Hash类型的特点是支持动态增加属性,单个键值对最多可以存储2^32-1个键值对,可以通过hset、hget等命令进行读写操作。
Hash类型在实际应用中的常见使用场景有:

用户信息存储:存储用户信息,如用户名、密码、邮箱等属性。
商品信息存储:存储商品信息,如商品名称、价格、库存等属性。
数据库缓存:缓存数据库中的数据,如查询结果、数据表等。

用户信息存储:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
func testHash() {
    // 测试hash
    hashKey := "UserInfo_id_10086"

    // 如果key存在,则只是覆盖参数中的key和value
    addKeyCount, err := redisClient.HSet(
        context.Background(),
        hashKey,
        "user_name",
        "尼古拉斯-赵四",
        "user_id",
        "10086",
        "like_count",
        "101",
    ).Result()
    if err != nil {
        log.Fatalln(err.Error())
    }
    fmt.Println("Add", addKeyCount, "new key")

    addKeyCount, err = redisClient.HSet(
        context.Background(),
        hashKey,
        "height",
        "183",
        "birthdate",
        -25862400000,
        "user_name",
        "尼古拉斯-凯奇",
    ).Result()
    if err != nil {
        log.Fatalln(err.Error())
    }
    fmt.Println("Add", addKeyCount, "new key")

    // 查看更改后的user_name
    result, err := redisClient.HGet(context.Background(), hashKey, "user_name").Result()
    if err != nil {
        log.Fatalln(err.Error())
    }
    fmt.Println(result)

    // 增加喜欢数key的值
    _, err = redisClient.HIncrBy(context.Background(), hashKey, "like_count", 66).Result()
    if err != nil {
        log.Fatalln(err.Error())
    }

    // 获取所有的键值对数据
    mapResult, err := redisClient.HGetAll(context.Background(), hashKey).Result()
    if err != nil {
        log.Fatalln(err.Error())
    }
    fmt.Println(mapResult)

    // 默认永不过期,如有需求,可手动设置过期时间
    succeed, err := redisClient.Expire(context.Background(), hashKey, time.Second*20).Result()
    if err != nil {
        log.Fatalln(err.Error())
    }
    fmt.Println(succeed)
}

输出结果:

1
2
3
4
5
Add 3 new key
Add 2 new key
尼古拉斯-凯奇
map[birthdate:-25862400000 height:183 like_count:167 user_id:10086 user_name:尼古拉斯-凯奇]
true

List(列表)

List是Redis中存储有序列表的数据类型,它的特点是可以按照插入顺序存储多个元素,每个元素都有一个索引值。List类型的特点是支持在列表头部或尾部插入或删除元素,可以通过lpush、rpush等命令进行读写操作。

List类型在实际应用中的常见使用场景有:

消息队列:通过lpush/rpop命令实现消息队列,如任务队列、事件队列等。
最新消息存储:存储最新的消息,如最新的微博、动态等。
用户行为记录:记录用户的操作历史,如最近浏览过的商品、最近听的歌曲等。

实现一个简单的消息队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
type ActionMessage struct {
    MessageID   string `json:"message_id"`
    MessageType string `json:"message_type"`
}

// 实现 encoding.BinaryMarshaler 接口
func (m *ActionMessage) UnmarshalBinary(data []byte) error {
    return json.Unmarshal(data, m)
}

func (m *ActionMessage) MarshalBinary() (data []byte, err error) {
    return json.Marshal(m)
}

func testList() {
    // 测试list
    listKey := "ActionMessageQueue"

    go func() {
        readList(listKey)
    }()

    go func() {
        for i := 0; i < 100; i++ {
            messageItem := ActionMessage{
                MessageID:   fmt.Sprintf("%d", i+1),
                MessageType: fmt.Sprintf("%d", i%5),
            }

            _, err := redisClient.LPush(context.Background(), listKey, &messageItem).Result()
            if err != nil {
                log.Fatalln(err.Error())
            }
            time.Sleep(time.Millisecond * 100)
        }
    }()
}

func readList(key string) {
    var messageItem ActionMessage
    err := redisClient.RPop(context.Background(), key).Scan(&messageItem)
    if err != nil {
        // 出现错误,等待500毫秒继续读取
        time.Sleep(time.Millisecond * 500)
        fmt.Println("sleep")
        readList(key)
        return
    }

    fmt.Println(messageItem)
    readList(key)
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sleep
{1 0}
{2 1}
{3 2}
{4 3}
{5 4}
sleep
{6 0}
{7 1}
{8 2}
{9 3}
{10 4}
 …………
sleep
sleep
sleep
 …………

Set(集合)

Set是Redis中的一种集合结构,它可以存储多个元素,并且每个元素都是唯一的。常用的命令包括SADD、SREM、SISMEMBER、SMEMBERS等。Set类型常用于存储用户的关注列表、标签等场景。
其使用场景还可以扩展到跟多的需要进行数据去重的场景。

使用Set记录关注当前用户的ID列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
func testSet() {
    setKey := "FollowUserIds_user_10086"

    // 添加重复元素
    for i := 0; i < 1000; i++ {
        count, err := redisClient.SAdd(
            context.Background(),
            setKey,
            i%11, // 仅11个不重复元素,注意这里是int类型
        ).Result()
        if err != nil {
            log.Fatalln(err.Error())
        } else {
            fmt.Println(count)
        }
    }

    // 继续添加重复元素,注意这里是string类型
    count, err := redisClient.SAdd(
        context.Background(),
        setKey,
        "1",
        "2",
        "2",
        "3",
        "3",
        "3",
        "3",
        "3",
        "3",
        "3",
        "4",
        "4",
        "4",
    ).Result()

    if err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println(count)
    }

    // 添加不重复的元素
    count, err = redisClient.SAdd(context.Background(), setKey, "33").Result()

    if err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println(count)
    }

    // 获取不重复元素个数
    count, err = redisClient.SCard(context.Background(), setKey).Result()
    if err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println(count)
    }

    // 获取最终结果
    results, err := redisClient.SMembers(context.Background(), setKey).Result()
    if err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println(results)
    }
}

输出结果:

1
2
12
[0 1 2 3 4 5 6 7 8 9 10 33]

Sorted Set(有序集合)

Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。

有序集合的成员是唯一的,但分数(score)却可以重复。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 232 – 1 (4294967295, 每个集合可存储40多亿个成员)。

我们可以用Sorted Set实现诸如商品推荐,用户推荐等场景下的数据缓存,而且在仅考虑单个数据推荐指数的情况下不用担心出现数据重复。

简单的用户推荐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
func testSortedSet() {
    sortedSetKey := "RecommendUsersKey"

    // 添加用户ID 1-1000,且赋予一个0-500的随机分数
    for i := 0; i < 1000; i++ {
        count, err := redisClient.ZAdd(context.Background(), sortedSetKey, redis.Z{
            Score:  randFloats(0, 500.0, 1)[0],
            Member: i + 1,
        }).Result()
        if err != nil {
            log.Fatalln(err.Error())
        } else {
            fmt.Println(count)
        }
    }

    // 用户1001设置分数为10086,一定为最大
    count, err := redisClient.ZAdd(context.Background(), sortedSetKey, redis.Z{
        Score:  10086,
        Member: "1001",
    }).Result()

    if err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println(count)
    }

    // 获取分数最高的20个用户作为推荐结果
    results, err := redisClient.ZRevRange(context.Background(), sortedSetKey, 0, 20).Result()
    if err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println(results)
    }

    // 删除后900名用户
    count, err = redisClient.ZRemRangeByRank(context.Background(), sortedSetKey, 0, 899).Result()
    if err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println(count)
    }
}

输出结果:

1
2
[1001 572 159 731 355 667 827 424 953 157 435 743 167 365 524 854 818 881 134 747 773]
900

总结

1. String是Redis所有基础类型的基础,其他类型中存储的value最终都是转化为二进制安全的字节数组以string的形式存储。
2. 利用incrby,decrby等命令可以结合Redis的单线程特性方便的进行数据统计。
3. 利用Hash类型可以方便的进行使用key对数据进行读取,从而免去复杂的数据库查询,减少数据库压力。
4. List类型可以很方便的进行列表数据缓存,甚至能实现简单的消息队列。
5. Set和Sorted Set都能方便的对数据进行去重,也可以陈本极低的实现推荐数据存储。
6. 需要注意的是,存储非基础类型的数据是,需要将数据转换为字节数组,需要考虑不同变成语言数据序列化时候的差异。

程序猿老龚(龚杰洪)原创,版权所有,转载请注明出处.

发表回复

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