数据结构

常用的五种类型

  • String
    • 常用命令
      • get、set、incr、decr、mget
    • 使用场景
      • key-value缓存
      • 计数
  • Hash
    • redis hash 是一个String类型的fieldvalue映射表。
    • 常用命令
      • hget、hset、hgetall
    • 使用场景
      • 用户对象信息
  • List
    • list 是简单的字符串列表,我们可以在列表的头部或尾部插入元素。
    • 实现方式
      • 双向链表,redis的list的每个元素都是String类型的双向链表
    • 常用命令
      • lpush(左进)、rpop(右出)、rpush(右进)、lpop(左出)、lrange(获取列表片段lrange key start end
    • 使用场景
      • 粉丝列表
      • 消息队列
  • Set
    • set 是String类型的无序集合,集合是通过hashtable实现的。与数学中的集合类似,redis 中的集合可以取交集、并集、差集等。因为set中的元素是没有顺序的,所以对 set 的添加、删除、查找的复杂度都是O(1)
    • 实现方式
      • Set内部实现是一个 value 永远为 null 的HashMap
    • 常用命令
      • sadd、spop、smembers、sunion
    • 使用场景
      • List结构一样,Set可以提供列表功能,特殊之处在于Set 可以自动去重。当需要存储一个列表数据,又不希望出现重复数据时,Set是一个很好的选择。
    • 案例
      • 新浪微博使用Set存储用户关注和粉丝数据,可以使用Set的求交集、并集、差集等操作实现比如互相关注、共同喜好、二度好友等功能。
  • SortedSet(zset)
    • zset 和 set 一样也是String元素的集合,且不允许重复的元素,zset 需要在集合中给每个元素设置对应的score。通过score就可以保证元素在集合中的顺序。
    • 实现方式
      • zset 内部使用HashMapSkipList(跳表)保证数据的存储和有序。HashMap里存的是集合中成员和score的映射,跳表里存的是所有成员。
    • 常用命令
      • zadd、zrange、zrem、zcard
    • 使用场景
      • 有序列表/排名
      • 微博 feed 流
        • zadd feed_uid 1620104824 content_id 1620104824 = createtime
      • 延迟队列
        • 将需要执行的时间戳作为score写入集合,程序指定周期取指定时间段的数据消费
      • 排行榜

除了以上五种还支持以下类型

  • HyperLogLog
  • Geo
  • Pub/Sub

SkipList(跳表)在 Redis 中的使用

参考文章:Redis 为什么用跳表而不用平衡树?

布隆过滤

todo

管道(pipeline)

todo

缓存问题

缓存雪崩

原因

同一时间,大面积缓存失效,大量流量直接打到DB

解决

  1. 设置每个key的失效时间都带一个随机值,保证数据不会在统一时间大面积失效。
  2. 对于热点数据(比如:首页数据)设置永不过期,当管理后台有操作时,主动刷一下缓存。

缓存穿透

原因

用户请求的数据,在系统CacheDB中都没有(比如:我们用户表主键id是从1开始自增,有人拿-1的uid疯狂刷接口),当这种请求量很大时,会导致数据库压力过大。

解决

  1. 参数校验,参数大小限制,用户鉴权
  2. 设置DB中无数据的key的缓存为null
  3. 布隆过滤

缓存击穿

原因

热点key失效瞬间,大量请求涌入DB

解决

  1. 热点key不过期
  2. 互斥锁
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package main

import (
"context"
"errors"
"fmt"
"sync"
"time"

"github.com/go-redis/redis/v8"
)

var rdb *redis.Client

func main() {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()

err := initClient(ctx)

if err != nil {
fmt.Printf("initClient.error:%+v\n", err)
return
}

key := "aa"
num := 1000

var r []string
var wg sync.WaitGroup
wg.Add(num)

for i := 0; i < num; i++ {
time.Sleep(time.Millisecond * 10)

go func() {

defer wg.Done()
val, err := getData(ctx, key)
if err != nil {
fmt.Printf("getData.error:%+v\n", err)
return
}
r = append(r, val)

}()
}

wg.Wait()
fmt.Printf("result.len:%+v", len(r))
}

func initClient(ctx context.Context) (err error) {

rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
PoolSize: 1000,
})

if rdb == nil {
return errors.New("redis.connected.faild")
}

_, err = rdb.Ping(ctx).Result()

return err
}

// mockGetDataFromDB
func mockGetDataFromDB(ctx context.Context, key string) (string, error) {

fmt.Println("cache.miss")
time.Sleep(time.Second * 2)
return "hello", nil
}

// get Data
func getData(ctx context.Context, key string) (string, error) {

val, err := rdb.Get(ctx, key).Result()

if err == nil {
return val, nil
}

if err.Error() == redis.Nil.Error() { // 处理 cache miss
// get lock
if ok, err := rdb.SetNX(ctx, fmt.Sprintf("%s_lock", key), 1, time.Second*5).Result(); err == nil && ok {

val, err := mockGetDataFromDB(ctx, key)
if err != nil {
return "", err
}
// cache
_, err = rdb.SetEX(ctx, key, val, time.Second*5).Result()

// del lock
rdb.Del(ctx, fmt.Sprintf("%s_lock", key))

if err == nil {
return val, nil
}

return "", err

} else {
// sleep 1s
time.Sleep(time.Second)
return getData(ctx, key)
}

} else {
return "", err
}

}