美文网首页
缓存穿透:缓存空键+封杀ip

缓存穿透:缓存空键+封杀ip

作者: 快感炮神 | 来源:发表于2020-10-23 21:49 被阅读0次

    1.缓存空键,防止缓存被穿透

    缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,比如发起id为“-1”或id超级大这些不存在的数据,这是时缓存没有命中,请求会全部打在数据库上,造成数据库压力过大,甚至崩溃。解决方案一般有两个:

    1. 接口层增加校验,如鉴权、id范围校验、访问频率限制等等
    2. 缓存空键,即给不存在的数据设置默认缓存
    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拉黑的步骤

    1. 增加两个黑名单的函数
    // 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) // 记录在黑名单时长(防止永远拉黑)
    }
    
    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也会浪费很多资源。

    1. 增加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()
    }
    
    1. 增加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的禁用时间都可以自由控制

    相关文章

      网友评论

          本文标题:缓存穿透:缓存空键+封杀ip

          本文链接:https://www.haomeiwen.com/subject/zlzvmktx.html