大家好,我是说写文章但一拖再拖的Asher,说要写文章,一定不能懒惰。
(嗨,大家好,首先本文并非本人撰写,为友人投稿,其次我是谢顶道人 --- 老李,本人擅鸽且不勤政。下周呢我和我之前的老板原上草来接着和大家一起唠唠日志或者Trace之类的,其实主要是原上草,他对于系统可观测性方面比较牛逼或者说过于牛逼,下周一定!)
背景
在很久很久之前,遇到了一件特别奇怪的事,一直以来我们的数据都是这样色的,顺序流畅,没有任何问题(图只是为了表达数据的流畅)
但突然有一天发生了神奇的情况,数据不流畅了:
在一段时间后又恢复了原状。。。
本着有问题一定要解决,绝不给自己留麻烦的原则,于是开始了追踪溯源之路。
分析流程,剥丝抽茧,定位产生问题数据的地方,分析其流程,如图所示,大概流程是一堆goroutine往map中写数据,一个goroutine从map中拿数据,然后把数据写到redis中。不管在读、写map的时候,都要先获取锁:sync.Mutex
结合实际现象和背后逻辑,可以发现一段时间内没有的数据是redis中缺失了,其缺失的原因是高并发情况下,写redis的goroutine拿到锁的概率相对来说小了。
解决问题
知其然,知其所以然
go map
go map本质是哈希表,使用时需注意:
- 使用前要初始化
// 未初始化报错
var m map[string]string
m["a"] = "a"
// 初始化
m := make(map[string]string)
- 并发读写,go内建的map是非线程安全的
并发安全的map
在文章开始的情景中其实还是并发情况的map,使用了锁,造成大量的锁争抢。要实现并发安全的map,大概也许可能有以下几种方法
加读写锁
直接加锁后,在高并发情况下,会造成大量的锁竞争,造成性能下降。
代码语言:javascript复制type RWMap struct {
sync.RWMutex
safeMap map[string]string
}
func NewRWMap(n int) *RWMap {
return &RWMap{
safeMap: make(map[string]string, n),
}
}
func (m *RWMap) Get(key string) (string, bool) {
m.RLock()
defer m.RUnlock()
val, exists := m.safeMap[key]
return val, exists
}
func (m *RWMap) Set(key,val string) {
m.Lock()
defer m.Unlock()
m.safeMap[key] = val
}
func (m *RWMap) Del(key string) {
m.Lock()
defer m.Unlock()
delete(m.safeMap,key)
}
sync.Map
sync.Map是go官方提供的线程安全的map,根据官方文档,其更偏向于指定场景的应用:
代码语言:javascript复制The Map type is optimized for two common use cases:
(1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or
(2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex.
大意是:
- 当一个key只写一次,有大量读
- 多个goroutine对不相交的key进行读、写、覆盖时
在这两种情况下,相比map
使用Mutex
和RWMutex
,能显著减少锁竞争
分片锁
一般来说,我们要尽可能的减少锁的使用,但是在并发编程中,锁的使用是避免不了的。在这种情况下,我们要做的就是尽量减小锁的粒度以及锁的持有时间。
分片就是把锁分成多个:我们要进一间教室,只需要拿教室对应的锁,而不是拿楼门的锁。
concurrent map是go比较知名的分片并发map:
代码语言:javascript复制https://github.com/orcaman/concurrent-map
其核心原理是:构建了指定数量分片的map,然后根据key计算出分片值,将key对应的值存入当前分片值对应的map
大概原理
1.准备好32分片的map
代码语言:javascript复制var SHARD_COUNT = 32
// A "thread" safe map of type string:Anything.
// To avoid lock bottlenecks this map is dived to several (SHARD_COUNT) map shards.
type ConcurrentMap []*ConcurrentMapShared
// A "thread" safe string to anything map.
type ConcurrentMapShared struct {
items map[string]interface{}
sync.RWMutex // Read Write mutex, guards access to internal map.
}
// Creates a new concurrent map.
func New() ConcurrentMap {
m := make(ConcurrentMap, SHARD_COUNT)
for i := 0; i < SHARD_COUNT; i {
m[i] = &ConcurrentMapShared{items: make(map[string]interface{})}
}
return m
}
2.当增、删、查的时候,先根据key计算分片值,然后根据分片值找对应的分片,找到后进行相应的处理
代码语言:javascript复制// GetShard returns shard under given key
func (m ConcurrentMap) GetShard(key string) *ConcurrentMapShared {
return m[uint(fnv32(key))%uint(SHARD_COUNT)]
}
// Sets the given value under the specified key.
func (m ConcurrentMap) Set(key string, value interface{}) {
// Get map shard.
shard := m.GetShard(key)
shard.Lock()
shard.items[key] = value
shard.Unlock()
}
解决问题
发现问题后,解决方案是将原始的Mutex map更换为concurrent map channel,完美解决问题
PS:这篇文章让我想起来我去年挖的坑,今年必须得找时间填完,就是多线程相关系列,因为也该到锁了,我正在考虑如何更好地表达锁...况且掌握多线程对于协程的应用会更加得心应手。
一万个进程的鬼故事 --- 多线程系列(三) 线程在线猛干,老李落泪回忆 --- 多线程系列(二) 传统功夫这叫化劲儿 --- 多线程系列(一)
PS:我觉得本文是一篇很好地循序渐进地适当结合原理并有效改善应用工程的文章,相对于只去埋头苦命卷底层原理而对应用工程无法做到改善的眼高手低,「结合原理并有效改善应用工程」应该才是更正确更好的选择,大家日常工作中要与这样的人为伍。另,这文章格式还有配图,整的是真不错,比我强多了,比如下图... ...