Golang + Redis解决缓存击穿(双层缓存)

Golang + Redis解决缓存穿透(双层缓存)

代码地址:

https://github.com/ziyifast/ziyifast-code_instruction/tree/main/redis_demo/cache_breakdown

1 概念

目前主流方案是在数据库前加一层缓存。类似于DB的防弹衣。

缓存击穿:Redis热点key过期,查询Redis不存在,高并发情况下大量请求打到DB。

拓展:

概念 产生原因 解决思路
缓存穿透 恶意请求不存在的Key 缓存空对象、布隆过滤器等
缓存雪崩 Redis大面积key过期 快速失败熔断、主从模式、集群模式。过期时间尽量随机
缓存击穿 热点Key过期 互斥更新、随机退避、差异失效时间(两份缓存cacheA、cacheB,差异过期时间)

1.1 缓存击穿:热点key过期

如淘宝天猫聚优选页面,经常会有最火HOT排行,这个排行定时会更新,那么在更新时,我们会去删除之前Redis的缓存,然后从数据库加载新的数据到Redis。

  • 删除时,我们可以理解为key过期了,因为浏览器缓存如果此时有大量用户在页面点击Redis中已经过期的商品,就会有大量请求打到数据库。

1.2 解决方案:双层缓存

解决方案:双层缓存(cacheA、cacheB)

  • 互斥更新、随机退避、差异失效时间(两份缓存,差异过期时间)
  • 我们可以将热点Key在Redis中存两份(两份过期时间不同=》差异过期时间),cacheA过期时,保证在cacheA加载新数据完成前,用户能查询到cacheB中的数据,不至于大量请求直接打到数据库。
  • 因为热点key的数据较少,因此缓存两份不会对内存有太多开销。

2 代码

2.1 模拟缓存击穿

1 演示
  • 案例解析:
  1. Redis中存热点数据20个,然后提供一个接口返回topK的热点数据
  2. 8s后刷新redis缓存,模拟key过期
  3. 重新加载数据sleep 6秒,模拟数据库及IO耗时

①热点key过期前,请求到了redis就返回了

②当热点key过期时,请求直接打到数据库

2 代码
go 复制代码
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/go-redis/redis/v8"
	"github.com/kataras/iris/v12"
	context2 "github.com/kataras/iris/v12/context"
	"github.com/ziyifast/log"
	"time"
)

var RedisCli *redis.Client

func init() {
	RedisCli = redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
		DB:   0,
	})
	_, err := RedisCli.Del(context.TODO(), GoodsKeyCacheA).Result()
	if err != nil {
		panic(err)
	}
}

type Goods struct {
	Id   int    `json:"id"`
	Name string `json:"name"`
}

var (
	GoodsKeyCacheA = "goodsA"
	GoodsKeyCacheB = "goodsB"
)

func InitDb(s, count int, cacheKey string) {
	for i := s; i < s+count; i++ {
		g := &Goods{
			Id:   i + 1,
			Name: fmt.Sprintf("good-%d", i+1),
		}
		marshal, err := json.Marshal(g)
		if err != nil {
			panic(err)
		}
		_, err = RedisCli.RPush(context.TODO(), cacheKey, string(marshal)).Result()
		if err != nil {
			panic(err)
		}
	}
}

func main() {
	InitDb(0, 20, GoodsKeyCacheA)
	app := iris.New()
	app.Get("/goods/top/{offset}/{pageSize}", func(c *context2.Context) {
		offset, err := c.Params().GetInt64("offset")
		if err != nil {
			panic(err)
		}
		pageSize, err := c.Params().GetInt64("pageSize")
		if err != nil {
			panic(err)
		}
		start := (offset - 1) * pageSize
		end := offset*pageSize - 1
		err = c.JSON(QueryForData(start, end))
		if err != nil {
			panic(err)
		}
	})
	//set the expire time
	_, err := RedisCli.Expire(context.TODO(), GoodsKeyCacheA, time.Second*8).Result()
	if err != nil {
		panic(err)
	}
	go ReloadNewGoods()
	app.Listen(":9999", nil)
}

func QueryForData(start, end int64) []string {
	val := RedisCli.LRange(context.TODO(), GoodsKeyCacheA, start, end).Val()
	log.Infof("query redis of cache A")
	if len(val) == 0 {
		log.Infof("redis is not exist, query db.....no!!!!")
	}
	return val
}

func ReloadNewGoods() {
	time.Sleep(time.Second * 15)
	log.Infof("start ReloadNewGoods......")
	InitDb(2000, 20, GoodsKeyCacheA)
	log.Infof("ReloadNewGoods......DONE")
}

2.2 双层缓存解决问题:热点key存两份(cacheA、cacheB)

在Redis中将热点key缓存两份。且cacheA、cacheB过期时间需要不同,保证在新热点数据加载到Redis之前,CacheB能提供服务。因为热点key数据不会存在太多,所以一般缓存两份对我们内存开销影响不会很大。(当然具体情况具体分析,并非绝对)

  1. 热点数据存两份一样的:cacheA、cacheB(两份缓存key的过期时间需要不同)
  2. 到点了,需要更新热点数据,cacheA过期,重新读取加载热点数据到Redis,更新cacheA的数据为最新。此时对外提供服务的是cacheB。防止请求直接打到DB。
  3. cacheA加载最新数据完成后,cacheA对外提供服务。更新cacheB中的数据到最新。(保证数据一致)

注意:service层查询时,先查cacheA有没有,如果有直接返回,如果没有查cacheB的数据。

1 演示

①获取TopK数据

②当热点key更新时(cacheA过期时),保证cacheB有数据,避免数据打到数据库

2 代码
go 复制代码
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/go-redis/redis/v8"
	"github.com/kataras/iris/v12"
	context2 "github.com/kataras/iris/v12/context"
	"github.com/ziyifast/log"
	"time"
)

var RedisCli *redis.Client

func init() {
	RedisCli = redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
		DB:   0,
	})
	_, err := RedisCli.Del(context.TODO(), GoodsKeyCacheA).Result()
	if err != nil {
		panic(err)
	}
}

type Goods struct {
	Id   int    `json:"id"`
	Name string `json:"name"`
}

var (
	GoodsKeyCacheA = "goodsA"
	GoodsKeyCacheB = "goodsB"
)

func InitDb(s, count int, cacheKey string) {
	for i := s; i < s+count; i++ {
		g := &Goods{
			Id:   i + 1,
			Name: fmt.Sprintf("good-%d", i+1),
		}
		marshal, err := json.Marshal(g)
		if err != nil {
			panic(err)
		}
		_, err = RedisCli.RPush(context.TODO(), cacheKey, string(marshal)).Result()
		if err != nil {
			panic(err)
		}
	}
}

func main() {
	InitDb(0, 20, GoodsKeyCacheA)
	app := iris.New()
	app.Get("/goods/top/{offset}/{pageSize}", func(c *context2.Context) {
		offset, err := c.Params().GetInt64("offset")
		if err != nil {
			panic(err)
		}
		pageSize, err := c.Params().GetInt64("pageSize")
		if err != nil {
			panic(err)
		}
		start := (offset - 1) * pageSize
		end := offset*pageSize - 1
		err = c.JSON(QueryForData(start, end))
		if err != nil {
			panic(err)
		}
	})
	//set the expire time
	_, err := RedisCli.Expire(context.TODO(), GoodsKeyCacheA, time.Second*8).Result()
	if err != nil {
		panic(err)
	}
	InitDb(0, 20, GoodsKeyCacheB)
	//add cacheB, expire time is different from cacheA (make sure new goods will be added to cacheA)
	_, err = RedisCli.Expire(context.TODO(), GoodsKeyCacheB, time.Second*20).Result()
	if err != nil {
		panic(err)
	}
	go ReloadNewGoods()
	app.Listen(":9999", nil)
}

func QueryForData(start, end int64) []string {
	val := RedisCli.LRange(context.TODO(), GoodsKeyCacheA, start, end).Val()
	log.Infof("query redis of cache A")
	if len(val) == 0 {
		log.Infof("cacheA is not exist, query redis of cache B")
		val = RedisCli.LRange(context.TODO(), GoodsKeyCacheB, start, end).Val()
		if len(val) == 0 {
			log.Infof("cacheB is not exist, query db, no!!!")
			return val
		}
	}
	return val
}

func ReloadNewGoods() {
	time.Sleep(time.Second * 15)
	log.Infof("start ReloadNewGoods......")
	InitDb(2000, 20, GoodsKeyCacheA)
	//set the expire time of cacheA
	log.Infof("ReloadNewGoods......DONE")
	//reload cacheB....
	//set the expire time of cacheB
}
相关推荐
RickeyBoy3 小时前
SwiftUI 如何实现 Infinite Scroll?
ios·面试
YDS8295 小时前
黑马点评 —— 分布式锁详解加源码剖析
java·spring boot·redis·分布式
KD5 小时前
阿里云服务迁移实战(二)——网关迁移与前后端分离配置
后端
小江的记录本6 小时前
【Redis】Redis全方位知识体系(附《Redis常用命令速查表(完整版)》)
java·数据库·redis·后端·python·spring·缓存
颜酱6 小时前
回溯算法实战练习(3)
javascript·后端·算法
前端摸鱼匠6 小时前
【AI大模型春招面试题12】Scaling Laws揭示了模型性能、数据量、计算量之间的什么关系?
人工智能·ai·语言模型·面试·大模型
zihao_tom6 小时前
Spring Boot(快速上手)
java·spring boot·后端
Bear on Toilet7 小时前
基于Deepseek(C++)的SSE协议流式响应实现方案
chrome·后端·deepseek接入
didiplus7 小时前
Python 入门第三课:让程序"开口说话":90% 新手都忽略的输入输出技巧
后端
明月_清风7 小时前
宿命的对决:深度对比 JavaScript 与 Python 的异步进化论
后端·python