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
会更容易,也更适合避免在这里创建和使用2sync.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)
}