一,缓存穿透
原因:一个请求来访问某个数据,发现缓存中没有,直接去DB中访问。此种情况就是穿透。(正常情况下缓存跟数据库中数据都是存在,异常情况下会导致)
特点:因传递了非法的key,导致缓存跟数据库中都无法查询
方法:
1.添加参数校验,校验传递的值是否合法,再决定是否处理该请求。
2.设置缓存空值,为访问的key缓存中设置对应的nil值,这样当访问过来的发现key的value是空值值,就直接返回nil了。并设置过期时间。
3,布隆过滤器,就是利用高效的数据结构,在请求跟缓存之间设置一个布隆过滤器,请求来的时候,直接判断当前的key是否存在DB中。也能很好防止发生缓存穿透。不存在直接返回,存在的话,直接访问数据库,在刷新缓存即可。
package main
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis"
_ "github.com/go-sql-driver/mysql"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type User struct {
ID int64
Username string `gorm:"column:username"`
Password string `gorm:"column:password"`
// 时间戳
CreateTime int64 `gorm:"column:createtime"`
}
// 数据库操作
var DB *sql.DB
func InitDB() error {
// 初始化数据库
db, err := sql.Open("mysql",
"root:password@tcp(127.0.0.1:3306)/my_sql?charset=utf8")
if err != nil {
log.Fatal(err.Error())
return err
}
DB = db
return nil
}
// redis
var client *redis.Client
func InitRedis() {
cl := redis.NewClient(
&redis.Options{
Addr: "127.0.0.1:6379",
Password: "password",
})
client = cl
}
var OrmDB *gorm.DB
// gorm
func InitOrmDB() {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local",
"root", "password", "127.0.0.1", 3306, "my_sql")
// 连接 mysql
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("数据库连接失败, error" + err.Error())
}
OrmDB = db
}
// 布隆过滤器,高可用访问kv
var userMap sync.Map
func main() {
InitOrmDB()
InitRedis()
r := gin.Default()
router := r.Group("/user")
router.POST("/add", addUser)
router.GET("/get", getUser)
fmt.Println("Server is running ...")
r.Run(":8080")
}
// add user
func addUser(c *gin.Context) {
name := c.Request.FormValue("name")
password := c.Request.FormValue("password")
fmt.Println("++=", name, password)
// 检查表是是否存在
if !OrmDB.Migrator().HasTable("users") {
fmt.Println("user is not exist")
OrmDB.Migrator().CreateTable(&User{})
}
// 构建数据
ur := &User{
Username: name,
Password: password,
CreateTime: time.Now().Unix(),
}
if err := OrmDB.Create(ur).Error; err != nil {
fmt.Println("插入失败", err)
return
}
// 将数据添加到布隆过滤器
userMap.Store(name, ur)
c.JSON(200, gin.H{"message": "success to add a user info"})
}
func getUser(c *gin.Context) {
// 缓存中查询
id := c.Request.FormValue("id")
name := c.Request.FormValue("name")
// 添加一个布隆过滤器
if _, ok := userMap.Load(name); !ok {
// 不存在该用户,说明非法参数,直接返回,可以避免缓存穿透
fmt.Println("illlage user", name)
return
}
// 缓存穿透,从数据库中查询,因为使用了非法参数,一般情况下,缓存跟数据库都是有数据的
key := id + "_" + name
ret := client.Get(key)
if ret.Err() == nil {
fmt.Println("cache get")
c.JSON(200, gin.H{"user": ret.String()})
return
}
fmt.Println("cache is not data,will go to db find ")
// 数据库查询
us := &User{}
result := OrmDB.Where("username = ? AND id = ?", name, id).First(&us)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// 为当前的非法key设置nil,直接返回,或者使用布隆过滤器
client.Set(key, nil, time.Second*10) // 临时设置一个nil,避免后续请求大量访问db
fmt.Println("找不到记录")
return
} else {
retUser, _ := json.Marshal(us)
// 将数据重新写入到缓存
retSet := client.Set(key, retUser, 0) // 设置都是不过期,仅仅测试使用
if retSet.Err() != nil {
fmt.Println("set cache is failed")
return
}
c.JSON(200, gin.H{"user": retUser})
}
}
二,缓存雪崩
原因:因大量的数据在同一时间内因为过期失效了,与此同时大量的请求到来,发现缓存中没有数据,都奔向DB,结果导致DB承受不了大规模的请求处理导致服务崩溃。(当然还有可能是缓存宕机)
特点:大量不同数据在缓存同一时间同时失效,并被大量请求访问至DB导致
方法:按照具体的实际情况来解决处理。
a,缓存宕机导致,那么就采用集群模式,并设置主从 + 哨兵模式,确保容灾的发生能及时供给提供服务。另外再开启持久化,宕机服务重启之后重新将数据加载出来。
b,数据过期导致,那么就采用在设置数据过期时间每个key添加随机数,确保不会再某一时间,大量的key同时失效。在这针对热数据,可以设置永不过期处理,有更新进行更新处理就可。
c.如果事先没考虑到雪崩问题,但是已经发生了。
处理方法:可采取限流 + 本地缓存进行补救,但是还得看具体情况具体对待。
三,缓存击穿
原因:因某个数据过期了,刚好此时大量请求都请求到DB上情况导致。
特点:某一数据因过期,导致访问缓存拿不到数据,击穿缓存去DB拿取。
该种情景跟雪崩有点类似。但是也有差异,雪崩是大面积缓存失效导致。击穿是一个热key在不停的被访问(刚好失效)。就会造成某一时候数据库的压力过大。
方法:
a.可以根据具体情况设置热数据不过期,定期刷新数据即可。
b,访问数据库加锁,第一个请求访问加锁,其余来访问,会被阻塞,一直到锁被释放,当后面的请求访问时,发现已经有缓存了,(因为前面访问过DB会将数据刷新到缓存中)就直接访问缓存了。但是该方法会导致性能,从而降低了吞吐量。所有要结合业务场景思考权衡利弊来做处理。