Go 戈朗一次试验条件

Go 戈朗一次试验条件,go,testing,concurrency,Go,Testing,Concurrency,我有一个模块,它依赖于通过调用外部服务填充缓存,如下所示: func (provider *Cache) GetItem(productId string, skuId string, itemType string) (*Item, error) { // First, create the key we'll use to uniquely identify the item key := fmt.Sprintf("%s:%s", productId, skuId)

我有一个模块,它依赖于通过调用外部服务填充缓存,如下所示:

func (provider *Cache) GetItem(productId string, skuId string, itemType string) (*Item, error) {

    // First, create the key we'll use to uniquely identify the item
    key := fmt.Sprintf("%s:%s", productId, skuId)

    // Now, attempt to get the concurrency control associated with the item key
    // If we couldn't find it then create one and add it to the map
    var once *sync.Once
    if entry, ok := provider.lockMap.Load(key); ok {
        once = entry.(*sync.Once)
    } else {
        once = &sync.Once{}
        provider.lockMap.Store(key, once)
    }

    // Now, use the concurrency control to attempt to request the item
    // but only once. Channel any errors that occur
    cErr := make(chan error, 1)
    once.Do(func() {

        // We didn't find the item in the cache so we'll have to get it from the partner-center
        item, err := provider.client.GetItem(productId, skuId)
        if err != nil {
            cErr <- err
            return
        }

        // Add the item to the cache
        provider.cache.Store(key, &item)
    })

    // Attempt to read an error from the channel; if we get one then return it
    // Otherwise, pull the item out of the cache. We have to use the select here because this is
    // the only way to attempt to read from a channel without it blocking
    var sku interface{}
    select {
    case err, ok := <-cErr:
        if ok {
            return nil, err
        }
    default:
        item, _ = provider.cache.Load(key)
    }

    // Now, pull out a reference to the item and return it
    return item.(*Item), nil
}

我遇到的问题是,偶尔这个测试会失败,因为在注释行,请求计数是2,而预期是1。这个失败是不一致的,我不太确定如何调试这个问题。任何帮助都将不胜感激。

我认为您的测试有时会失败,因为您的缓存无法保证它只获取一次项目,您很幸运,测试发现了这一点

如果一个项不在其中,并且两个并发goroutine同时调用
Cache.GetItem()
,则可能会发生
lockMap.Load()
将在这两个goroutine中报告密钥不在映射中,这两个goroutine创建一个
同步。一旦
,它们都将在映射中存储自己的实例(很明显,只有一个——后者——将保留在地图中,但您的缓存不会对此进行检查)

然后这两个goroutine都将调用
client.GetItem()
,因为两个单独的
sync.Once
不提供同步。只有当使用相同的
sync.Once
实例时,才保证传递给
Once.Do()
的函数只执行一次

我认为一个
sync.Mutex
会更容易,也更适合避免在这里创建和使用2
sync.one

或者,由于您已经在使用
sync.Map
,您可以使用以下方法:创建
sync.Once
,并将其传递给
Map.LoadOrStore()
。如果密钥已在映射中,请使用返回的
sync.Once
。如果密钥不在映射中,您的
sync.Once
将存储在其中,因此您可以使用它。这将确保多个并发goroutine不能在其中存储多个
sync.Once
实例

大概是这样的:

var once *sync.Once
if entry, loaded := provider.lockMap.LoadOrStore(key, once); loaded {
    // Was already in the map, use the loaded Once
    once = entry.(*sync.Once)
}
这个解决方案仍然不完美:如果2个goroutine调用
Cache.GetItem()
同时,只有一个goroutine会尝试从客户端获取项目,但如果失败,则只有此goroutine会报告错误,另一个goroutine不会尝试从客户端获取项目,但会从映射中加载项目,您不会检查加载是否成功。您应该,如果它不在映射中,则意味着另一个,并发尝试无法获取它。因此,您应该报告错误(并清除
同步。一次


正如你所看到的,它变得越来越复杂。我坚持我以前的建议:在这里使用
sync.Mutex
会更容易。

我在这里考虑了
sync.Mutex
,但我需要为每个唯一键调用
cache.GetItem
一次,我不确定在那种情况下如何使用互斥锁。我真的希望能举一个例子f您不介意花费额外的时间。@Woody1193“最佳”解决方案取决于您的其他未列出的需求。例如,当从客户端提取项目时,是否可以使用不同的键阻止其他
缓存.GetItem()
调用?如果提取项目,则其他
缓存.GetItem()
具有相同密钥的调用应该等待?或者可能返回错误?如果等待,则如果从客户端获取失败,所有调用都应该返回相同的错误?如果出现错误,则另一个
缓存.GetItem()会怎么样
发出了吗?它应该重试吗?还是一次又一次地返回相同的旧错误?我想要的是,对于使用该方法调用的键的每个唯一值,
client.GetItem
只调用一次。使用相同键的任何其他并发调用都应该等到
sync.one.Do
调用完成。如果该错误发生了呢?所有并发请求都应该返回相同的错误?并且将来调用相同的键?如果
client.GetItems
失败,那么我希望它返回一个错误,但其他任何操作都应该返回nil。
var once *sync.Once
if entry, loaded := provider.lockMap.LoadOrStore(key, once); loaded {
    // Was already in the map, use the loaded Once
    once = entry.(*sync.Once)
}