You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
}
问题原因
  1. gin.Context的Get/Set操作并非并发安全,多个goroutine同时访问时会出现竞态:第一个goroutine还未完成数据库查询并设置缓存时,其他goroutine已经判断缓存不存在,继而重复执行SQL查询。
  2. 未针对并发场景做“单次查询控制”,同一个缓存key会被多个goroutine同时触发数据库查询。
解决方案

方案1:基于gin.Context的并发安全请求级缓存

先通过中间件给每个请求初始化一个并发安全的缓存实例,再修改查询逻辑:

  1. 新增请求缓存中间件:
func RequestCacheMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 初始化sync.Map作为请求内的并发安全缓存
        c.Set("request_cache", &sync.Map{})
        c.Next()
    }
}

记得在Gin路由中注册该中间件,确保每个请求都能获取到缓存实例。

  1. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 01:54:51