前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一次生产环境高效利用Go Concurrent Map的记录

一次生产环境高效利用Go Concurrent Map的记录

作者头像
老李秀
发布2022-08-31 10:38:40
4240
发布2022-08-31 10:38:40
举报

大家好,我是说写文章但一拖再拖的Asher,说要写文章,一定不能懒惰。

(嗨,大家好,首先本文并非本人撰写,为友人投稿,其次我是谢顶道人 --- 老李,本人擅鸽且不勤政。下周呢我和我之前的老板原上草来接着和大家一起唠唠日志或者Trace之类的,其实主要是原上草,他对于系统可观测性方面比较牛逼或者说过于牛逼,下周一定!)

背景

在很久很久之前,遇到了一件特别奇怪的事,一直以来我们的数据都是这样色的,顺序流畅,没有任何问题(图只是为了表达数据的流畅)

但突然有一天发生了神奇的情况,数据不流畅了:

在一段时间后又恢复了原状。。。

本着有问题一定要解决,绝不给自己留麻烦的原则,于是开始了追踪溯源之路。

分析流程,剥丝抽茧,定位产生问题数据的地方,分析其流程,如图所示,大概流程是一堆goroutine往map中写数据,一个goroutine从map中拿数据,然后把数据写到redis中。不管在读、写map的时候,都要先获取锁:sync.Mutex

结合实际现象和背后逻辑,可以发现一段时间内没有的数据是redis中缺失了,其缺失的原因是高并发情况下,写redis的goroutine拿到锁的概率相对来说小了。

解决问题

知其然,知其所以然

go map

go map本质是哈希表,使用时需注意:

  • 使用前要初始化
代码语言:javascript
复制
// 未初始化报错
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使用MutexRWMutex,能显著减少锁竞争

分片锁

一般来说,我们要尽可能的减少锁的使用,但是在并发编程中,锁的使用是避免不了的。在这种情况下,我们要做的就是尽量减小锁的粒度以及锁的持有时间。

分片就是把锁分成多个:我们要进一间教室,只需要拿教室对应的锁,而不是拿楼门的锁。

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:我觉得本文是一篇很好地循序渐进地适当结合原理并有效改善应用工程的文章,相对于只去埋头苦命卷底层原理而对应用工程无法做到改善的眼高手低,「结合原理并有效改善应用工程」应该才是更正确更好的选择,大家日常工作中要与这样的人为伍。另,这文章格式还有配图,整的是真不错,比我强多了,比如下图... ...

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能API社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 大家好,我是说写文章但一拖再拖的Asher,说要写文章,一定不能懒惰。
    • 背景
      • 解决问题
        • go map
        • 并发安全的map
      • 解决问题
      相关产品与服务
      云数据库 Redis
      腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档