缓存穿透:缓存空键+封杀ip
2020-10-23 本文已影响0人
快感炮神
1.缓存空键,防止缓存被穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,比如发起id为“-1”或id超级大这些不存在的数据,这是时缓存没有命中,请求会全部打在数据库上,造成数据库压力过大,甚至崩溃。解决方案一般有两个:
- 接口层增加校验,如鉴权、id范围校验、访问频率限制等等
- 缓存空键,即给不存在的数据设置默认缓存
const defaultCache = ""
router := gin.Default()
v1 := router.Group("v1")
{
v1.Handle(http.MethodGet, "/posts/:id", func(context *gin.Context) {
var (
err error
post = new(model.Post) // 数据库查询结果
jsonStr string // 缓存json
)
id := context.Param("id")
_id, _ := strconv.Atoi(id)
if id == "" {
context.JSON(http.StatusOK, gin.H{
"code": http.StatusBadRequest,
"msg": "id error",
})
}
// 从redis取
postKey := "post:" + id
jsonStr, err = lib.Cache().Get(context, postKey).Result()
// 如果缓存没有
if err != nil {
// 从数据库取
if post, err = post.Find(_id); err == nil {
bytes, _ := json.Marshal(post)
lib.Cache().Set(context, postKey, string(bytes), time.Minute*1) // 设置缓存
jsonStr = string(bytes)
} else {
lib.Cache().Set(context, postKey, defaultCache, time.Second*30) // 设置默认缓存
}
}
// 判断是否为默认缓存
var data interface{}
if jsonStr != defaultCache {
t, _ := lib.Cache().TTL(context, postKey).Result()
lib.Cache().Expire(context, postKey, t+time.Second*5) // +5s
post = new(model.Post)
_ = json.Unmarshal([]byte(jsonStr), post)
data = gin.H{
"code": 0,
"msg": "ok",
"data": post,
}
} else {
lib.Cache().Expire(context, postKey, t+time.Second*2) // +5s
data = gin.H{
"code": 0,
"msg": "ok",
}
}
context.JSON(http.StatusOK, data)
})
}
router.Run()
上述示例代码中实现了空键缓存,并且可以通过访问对命中缓存或者默认缓存来加减时间控制缓存时常
2.封杀IP
仅有上一步还是有些不足,因为每次有默认缓存生成都代表着背后有一次查库,如果任由恶意请求不断的访问还是会对数据库造成压力,所以我们增加一步将其ip拉黑的步骤
- 增加两个黑名单的函数
// forbiddenLimit 禁止上限
const forbiddenLimit = 10
// IsForbidden 是否被禁止
func IsForbidden(ctx *gin.Context) bool {
if frequency, err := lib.Cache().Get(ctx, "ip:"+ctx.ClientIP()).Result(); err == nil {
if i, err := strconv.Atoi(frequency); err == nil {
return i >= forbiddenLimit
}
}
return false
}
// IncrForbidden 给恶意IP记录罚分
func IncrForbidden(ctx *gin.Context, number int64) {
key := "ip:" + ctx.ClientIP()
lib.Cache().IncrBy(ctx, key, number) // 罚分
lib.Cache().Expire(ctx, key, time.Hour*1) // 记录在黑名单时长(防止永远拉黑)
}
- 修改第一步的代码,增加黑名单记录
v1 := router.Group("v1")
{
v1.Handle(http.MethodGet, "/posts/:id", func(context *gin.Context) {
if commpn.IsForbidden(context) {
context.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"code": http.StatusForbidden,
"msg": "滚~~~",
})
return
}
var (
err error
post = new(model.Post) // 数据库查询结果
jsonStr string // 缓存json
increase bool // IP计分开关
)
if err != nil {
// 从数据库取
if post, err = post.Find(_id); err == nil {
...
} else {
lib.Cache().Set(context, postKey, defaultCache, time.Second*30) // 设置默认缓存
commpn.IncrForbidden(context, 2) // 数据库没取到,则给指定IP加2分
increase = true // 防止下面再被加分
}
}
// 判断是否为默认缓存
var data interface{}
if jsonStr != defaultCache {
...
} else {
if !increase {
commpn.IncrForbidden(context, 1) // 命中默认缓存,则给指定IP加1分
}
...
}
})
}
当然可以把这些操作封装到中间件,不然业务代码看着有些乱。目前为止,就可以把恶意IP拦截了。
3.封杀IP段
有了上面的步骤后其实还是有不足,比如对方会更换ip来请求,这时候可以把整个ip段禁用,不然封大量ip也会浪费很多资源。
- 增加ip段处理
const (
// forbiddenLimit 禁止上限
ForbiddenLimit = 10
// forbiddenIPListLimit ip段内超过几个禁用ip就禁用全段
forbiddenIPListLimit = 3
)
// IsInForbiddenIPList IP段是否被禁
func IsInForbiddenIPList(ctx *gin.Context, key string) bool {
if length, err := lib.Cache().LLen(ctx, key).Result(); err != nil {
return false
} else {
return length >= forbiddenIPListLimit
}
}
// GetIpBlock 获取IP段
func GetIpBlock(ctx *gin.Context) string {
ip := ctx.ClientIP()
arr := strings.Split(ip, ".")
block := arr[:3]
return strings.Join(block, ".")
}
修改之前的记录罚分的函数,增加当前分数返回
// IncrForbidden 给恶意IP记录罚分
func IncrForbidden(ctx *gin.Context, number int64) (score int64) {
key := "ip:" + ctx.ClientIP()
score, _ = lib.Cache().IncrBy(ctx, key, number).Result() // 罚分
lib.Cache().Expire(ctx, key, time.Hour*1) // 记录在黑名单时长(防止永远拉黑)
return
}
// Tomorrow 明天凌晨
func Tomorrow() int64 {
now := time.Now().Format("2006-01-02")
t, _ := time.ParseInLocation("2006-01-02", now, time.Local)
return t.AddDate(0, 0, 1).Unix()
}
- 增加ip段判断
// ip段或单个ip被禁用
if commpn.IsInForbiddenIPList(context, block) || commpn.IsForbidden(context) {
context.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"code": http.StatusForbidden,
"msg": "滚~~~",
})
return
}
...
if score >= commpn.ForbiddenLimit {
lib.Cache().LPush(context, block, context.ClientIP())
lib.Cache().ExpireAt(context, block, time.Unix(common.Tomorrow(), 0))
}
context.JSON(http.StatusOK, data)
...
这样一来,只要某个段的ip超过两个被禁用,整个ip段都会被禁用,ip段和单个ip的禁用时间都可以自由控制