一,缓存穿透
原因:一个请求来访问某个数据,发现缓存中没有,直接去DB中访问。此种情况就是穿透。(正常情况下缓存跟数据库中数据都是存在,异常情况下会导致)
特点:因传递了非法的key,导致缓存跟数据库中都无法查询
方法:
1.添加参数校验,校验传递的值是否合法,再决定是否处理该请求。
2.设置缓存空值,为访问的key缓存中设置对应的nil值,这样当访问过来的发现key的value是空值值,就直接返回nil了。并设置过期时间。
3,布隆过滤器,就是利用高效的数据结构,在请求跟缓存之间设置一个布隆过滤器,请求来的时候,直接判断当前的key是否存在DB中。也能很好防止发生缓存穿透。不存在直接返回,存在的话,直接访问数据库,在刷新缓存即可。
package mainimport ("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 int64Username string `gorm:"column:username"`Password string `gorm:"column:password"`// 时间戳CreateTime int64 `gorm:"column:createtime"`
}// 数据库操作
var DB *sql.DBfunc 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 = dbreturn nil
}// redis
var client *redis.Clientfunc 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")// 连接 mysqldb, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {panic("数据库连接失败, error" + err.Error())}OrmDB = db
}// 布隆过滤器,高可用访问kv
var userMap sync.Mapfunc 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 + "_" + nameret := 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,避免后续请求大量访问dbfmt.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会将数据刷新到缓存中)就直接访问缓存了。但是该方法会导致性能,从而降低了吞吐量。所有要结合业务场景思考权衡利弊来做处理。