# 8.4 Redis缓存实战分析

## 8.4.1 缓存不存在的键,以防穿透

代码已经实现.见[8.3.4 模拟缓存穿透现象](https://github.com/rayallen20/StudyRedisBaseOnDocker/blob/master/note/%E7%AC%AC8%E7%AB%A0%20GO%E6%95%B4%E5%90%88MySQL%E4%B8%8ERedis/8.3%20Redis%E4%B8%8EMySQL%E7%9A%84%E6%95%B4%E5%90%88.md#834-%E6%A8%A1%E6%8B%9F%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F%E7%8E%B0%E8%B1%A1)

## 8.4.2 合理设置超时时间,以防内存溢出

代码已经实现.见[8.3.5 模拟内存使用不当的场景](https://github.com/rayallen20/StudyRedisBaseOnDocker/blob/master/note/%E7%AC%AC8%E7%AB%A0%20GO%E6%95%B4%E5%90%88MySQL%E4%B8%8ERedis/8.3%20Redis%E4%B8%8EMySQL%E7%9A%84%E6%95%B4%E5%90%88.md#835-%E6%A8%A1%E6%8B%9F%E5%86%85%E5%AD%98%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%BD%93%E7%9A%84%E5%9C%BA%E6%99%AF)

## 8.4.3 超时时间外加随机数,以防穿透

我们在之前设置的所有超时时间均为1小时(3600秒).假设我们在某一时刻批量添加了几千个缓存数据,那么按照之前程序设置的超时时间,在1小时之后,这几千个key会同时失效.那么对这批数据的请求会被同时发送到MySQL(当然我们是假定会有对这几千个key的请求),那么MySQL同样有可能崩溃.

解决方法:**设置超时时间的数值采用`整数 + 随机数`的方式**

在8.3小节的代码基础上修改:

工程结构如下:

```
(base) yanglei@yuanhong mysqlAndRedis % tree ./
./
├── biz
│   └── student.go
├── cache
│   ├── conf.go
│   ├── conn.go
│   ├── genExpireSecond.go
│   └── student.go
├── controller
│   └── student.go
├── db
│   ├── conf.go
│   ├── conn.go
│   └── student.go
├── go.mod
├── go.sum
├── lib
│   └── randInt.go
├── main.go
├── request
│   └── student
│       └── getStudentById.go
└── resp
    └── response.go

8 directories, 15 files
```

`lib/randInt.go`:

```go
package lib

import (
	"math/rand"
	"time"
)

func GenRandInt(ceiling int) int {
	rand.Seed(time.Now().UnixNano())
	return rand.Intn(ceiling)
}
```

`cache/genExpireSecond.go`:

```go
package cache

import "mysqlAndRedis/lib"

const ExpireSecond = 3600

const CeilSecond = 60

func genExpireSecond() int {
	return ExpireSecond + lib.GenRandInt(CeilSecond)
}
```

`cache/student.go`:

```go
package cache

import (
	"github.com/gomodule/redigo/redis"
	"strconv"
)

const StudentKeyPrefix = "Stu"

type Student struct {
	Id    int
	Name  string
	Age   int
	Score float64
	Exist bool
}

func (s *Student) FindById(id int) (err error) {
	// 判断键是否存在
	s.Exist, err = s.exists(id)
	if err != nil {
		return err
	}

	// 键不存在则直接返回
	if !s.Exist {
		return nil
	}

	// 确认键存在,则从redis中读取(有可能读到的是一个0值)
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	reply, err := redis.Values(Conn.Do("LRANGE", key, 0, -1))
	if err != nil {
		return err
	}

	s.Id, err = redis.Int(reply[0], nil)
	if err != nil {
		return err
	}

	s.Name, err = redis.String(reply[1], nil)
	if err != nil {
		return err
	}

	s.Age, err = redis.Int(reply[2], nil)
	if err != nil {
		return err
	}

	s.Score, err = redis.Float64(reply[3], nil)
	if err != nil {
		return err
	}

	// 若读取了该key 则重置过期时间
	// 此处忽略错误
	_ = s.setExpireTime(id)

	return nil
}

func (s *Student) SaveById(id int) error {
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	_, err := Conn.Do("RPUSH", key, s.Id, s.Name, s.Age, s.Score)
	if err != nil {
		return err
	}

	err = s.setExpireTime(id)
	if err != nil {
		return err
	}

	return nil
}

func (s *Student) exists(id int) (bool, error) {
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	reply, err := redis.Int(Conn.Do("EXISTS", key))
	if err != nil {
		return false, err
	}

	return reply == 1, nil
}

func (s *Student) setExpireTime(id int) error {
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	_, err := Conn.Do("EXPIRE", key, genExpireSecond())
	return err
}
```

运行后查询`id = 1`和`id = 2`的数据:

```
(base) yanglei@yuanhong ~ % redis-cli
127.0.0.1:6379> AUTH redis_user redis_password
OK
127.0.0.1:6379> TTL Stu1
(integer) 3610
127.0.0.1:6379> TTL Stu2
(integer) 3643
```

可以看到,各个键的生存时间都有所区别了


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://redis.sai.show/di-8-zhang-go-zheng-he-mysql-yu-redis/8.4-redis-huan-cun-shi-zhan-fen-xi.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
