Gin框架单请求内Goroutine间本地缓存失效问题求助
问题描述
基于Golang Gin框架开发的应用中,单个请求内启动多个goroutine执行SQL查询时出现重复查询问题。尝试用gin.Context实现请求内本地缓存,但未生效——单次请求日志中多次出现相同的SQL语句:
SELECT * FROM `core_website` WHERE website_id = 1510 ORDER BY `core_website`.`website_id` LIMIT 1
相关代码如下:
type BaseRepository struct { BaseRepositoryInterface Db *gorm.DB Context *gin.Context } type CoreWebsiteRepository struct { *BaseRepository } func (repository *CoreWebsiteRepository) GetResourceCenterCityId(websiteId int) (int, error) { if websiteId == 0 { return 0, fmt.Errorf("website_id is empty") } cacheKeyId := fmt.Sprintf("resource_center_city_id_by_website_id_%d", websiteId) value, exists := repository.Context.Get(cacheKeyId) if exists { return value.(int), nil } var data model.CoreWebsite err := repository.Db.Model(&model.CoreWebsite{}). Where("website_id = ?", websiteId). First(&data). Error if err != nil { return 0, err } repository.Context.Set(cacheKeyId, data.ResourceCenterCityId) return data.ResourceCenterCityId, nil }
问题原因
gin.Context的Get/Set操作并非并发安全,多个goroutine同时访问时会出现竞态:第一个goroutine还未完成数据库查询并设置缓存时,其他goroutine已经判断缓存不存在,继而重复执行SQL查询。- 未针对并发场景做“单次查询控制”,同一个缓存key会被多个goroutine同时触发数据库查询。
解决方案
方案1:基于gin.Context的并发安全请求级缓存
先通过中间件给每个请求初始化一个并发安全的缓存实例,再修改查询逻辑:
- 新增请求缓存中间件:
func RequestCacheMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // 初始化sync.Map作为请求内的并发安全缓存 c.Set("request_cache", &sync.Map{}) c.Next() } }
记得在Gin路由中注册该中间件,确保每个请求都能获取到缓存实例。
- 修改Repository的查询逻辑:
func (repository *CoreWebsiteRepository) GetResourceCenterCityId(websiteId int) (int, error) { if websiteId == 0 { return 0, fmt.Errorf("website_id is empty") } cacheKeyId := fmt.Sprintf("resource_center_city_id_by_website_id_%d", websiteId) // 从Context中获取并发安全缓存 cache, ok := repository.Context.Get("request_cache") if !ok { return 0, fmt.Errorf("request cache not initialized") } requestCache := cache.(*sync.Map) // 尝试从缓存读取 if value, exists := requestCache.Load(cacheKeyId); exists { return value.(int), nil } // 用sync.Once确保同一个key只执行一次数据库查询 var once sync.Once var result int var err error once.Do(func() { var data model.CoreWebsite err = repository.Db.Model(&model.CoreWebsite{}). Where("website_id = ?", websiteId). First(&data). Error if err == nil { result = data.ResourceCenterCityId requestCache.Store(cacheKeyId, result) } }) if err != nil { return 0, err } return result, nil }
方案2:给Repository添加独立的并发安全缓存
如果不想依赖gin.Context的缓存,可以直接在Repository中内置sync.Map作为请求级缓存(注意每个请求要创建独立的Repository实例,避免跨请求数据污染):
type BaseRepository struct { BaseRepositoryInterface Db *gorm.DB Context *gin.Context cache *sync.Map // 并发安全的请求内缓存 } // 初始化BaseRepository时创建新的sync.Map实例 func NewBaseRepository(db *gorm.DB, c *gin.Context) *BaseRepository { return &BaseRepository{ Db: db, Context: c, cache: &sync.Map{}, } } type CoreWebsiteRepository struct { *BaseRepository } func (repository *CoreWebsiteRepository) GetResourceCenterCityId(websiteId int) (int, error) { if websiteId == 0 { return 0, fmt.Errorf("website_id is empty") } cacheKeyId := fmt.Sprintf("resource_center_city_id_by_website_id_%d", websiteId) // 从Repository内置缓存读取 if value, exists := repository.cache.Load(cacheKeyId); exists { return value.(int), nil } var once sync.Once var result int var err error once.Do(func() { var data model.CoreWebsite err = repository.Db.Model(&model.CoreWebsite{}). Where("website_id = ?", websiteId). First(&data). Error if err == nil { result = data.ResourceCenterCityId repository.cache.Store(cacheKeyId, result) } }) if err != nil { return 0, err } return result, nil }
关键说明
- 使用
sync.Map保证缓存操作的并发安全性,避免多goroutine下的竞态问题。 - 配合
sync.Once确保同一个缓存key对应的数据库查询只会执行一次,彻底解决重复查询问题。 - 确保缓存是请求级别的:每个请求都有独立的缓存实例,不会出现跨请求的数据污染。
内容的提问来源于stack exchange,提问作者Smirnov
相关产品推荐
相关产品推荐

