Categories: 技术原创

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. 需要注意的是,存储非基础类型的数据是,需要将数据转换为字节数组,需要考虑不同变成语言数据序列化时候的差异。

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

龚杰洪

Recent Posts

GOLANG面试八股文-并发控制

背景 协程A执行过程中需要创建…

1 年 ago

MYSQL面试八股文-常见面试问题和答案整理二

索引B+树的理解和坑 MYSQ…

1 年 ago

MYSQL面试八股文-InnoDB的MVCC实现机制

背景 什么是MVCC? MVC…

1 年 ago

MYSQL面试八股文-索引类型和使用相关总结

什么是索引? 索引是一种用于加…

1 年 ago

MYSQL面试八股文-索引优化之全文索引(解决文本搜索问题)

背景:为什么要有全文索引 在当…

1 年 ago