前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >源码解读 sync.Map 实现原理

源码解读 sync.Map 实现原理

作者头像
张凯强
发布于 2020-03-25 13:34:06
发布于 2020-03-25 13:34:06
98400
代码可运行
举报
文章被收录于专栏:面向人生编程面向人生编程
运行总次数:0
代码可运行

简介

Go 的内建 map 是不支持并发写操作的,原因是 map 写操作不是并发安全的,当你尝试多个 Goroutine 操作同一个 map,会产生报错:fatal error: concurrent map writes

因此官方另外引入了 sync.Map 来满足并发编程中的应用。

sync.Map 的实现原理可概括为:

•通过 read 和 dirty 两个字段将读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上•读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty•读取 read 并不需要加锁,而读或写 dirty 都需要加锁•另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据同步到 read 上•对于删除数据则直接通过标记来延迟删除

数据结构

sync.Map数据结构如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Map struct {    // 加锁作用,保护 dirty 字段    mu Mutex    // 只读的数据,实际数据类型为 readOnly    read atomic.Value    // 最新写入的数据    dirty map[interface{}]*entry    // 计数器,每次需要读 dirty 则 +1    misses int}

其中 readOnly 的数据结构为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type readOnly struct {    // 内建 map    m  map[interface{}]*entry    amended bool  // 表示 dirty 里存在 read 里没有的 key,通过该字段决定是否加锁读 dirty}

entry 数据结构则用于存储值的指针:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type entry struct {    p unsafe.Pointer  // 等同于 *interface{}}

属性 p 有三种状态:

p == nil: 键值已经被删除,且 m.dirty == nilp == expunged: 键值已经被删除,但 m.dirty!=nilm.dirty 不存在该键值(expunged 实际是空接口指针)•除以上情况,则键值对存在,存在于 m.read.m 中,如果 m.dirty!=nil 则也存在于 m.dirty

Map 常用的有以下方法:

Load:读取指定 key 返回 value•Store: 存储(增或改)key-value•Delete: 删除指定 key

源码解析

Load

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {    // 首先尝试从 read 中读取 readOnly 对象    read, _ := m.read.Load().(readOnly)    e, ok := read.m[key]
    // 如果不存在则尝试从 dirty 中获取    if !ok && read.amended {        m.mu.Lock()        // 由于上面 read 获取没有加锁,为了安全再检查一次        read, _ = m.read.Load().(readOnly)        e, ok = read.m[key]
        // 确实不存在则从 dirty 获取        if !ok && read.amended {            e, ok = m.dirty[key]            // 调用 miss 的逻辑            m.missLocked()        }        m.mu.Unlock()    }
    if !ok {        return nil, false    }    // 从 entry.p 读取值    return e.load()}
func (m *Map) missLocked() {    m.misses++    if m.misses < len(m.dirty) {        return    }    // 当 miss 积累过多,会将 dirty 存入 read,然后 将 amended = false,且 m.dirty = nil    m.read.Store(readOnly{m: m.dirty})    m.dirty = nil    m.misses = 0}

Store

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (m *Map) Store(key, value interface{}) {    read, _ := m.read.Load().(readOnly)    // 如果 read 里存在,则尝试存到 entry 里    if e, ok := read.m[key]; ok && e.tryStore(&value) {        return    }
    // 如果上一步没执行成功,则要分情况处理    m.mu.Lock()    read, _ = m.read.Load().(readOnly)    // 和 Load 一样,重新从 read 获取一次    if e, ok := read.m[key]; ok {        // 情况 1:read 里存在        if e.unexpungeLocked() {            // 如果 p == expunged,则需要先将 entry 赋值给 dirty(因为 expunged 数据不会留在 dirty)            m.dirty[key] = e        }        // 用值更新 entry        e.storeLocked(&value)    } else if e, ok := m.dirty[key]; ok {        // 情况 2:read 里不存在,但 dirty 里存在,则用值更新 entry        e.storeLocked(&value)    } else {        // 情况 3:read 和 dirty 里都不存在        if !read.amended {            // 如果 amended == false,则调用 dirtyLocked 将 read 拷贝到 dirty(除了被标记删除的数据)            m.dirtyLocked()            // 然后将 amended 改为 true            m.read.Store(readOnly{m: read.m, amended: true})        }        // 将新的键值存入 dirty        m.dirty[key] = newEntry(value)    }    m.mu.Unlock()}
func (e *entry) tryStore(i *interface{}) bool {    for {        p := atomic.LoadPointer(&e.p)        if p == expunged {            return false        }        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {            return true        }    }}
func (e *entry) unexpungeLocked() (wasExpunged bool) {    return atomic.CompareAndSwapPointer(&e.p, expunged, nil)}
func (e *entry) storeLocked(i *interface{}) {    atomic.StorePointer(&e.p, unsafe.Pointer(i))}
func (m *Map) dirtyLocked() {    if m.dirty != nil {        return    }
    read, _ := m.read.Load().(readOnly)    m.dirty = make(map[interface{}]*entry, len(read.m))    for k, e := range read.m {        // 判断 entry 是否被删除,否则就存到 dirty 中        if !e.tryExpungeLocked() {            m.dirty[k] = e        }    }}
func (e *entry) tryExpungeLocked() (isExpunged bool) {    p := atomic.LoadPointer(&e.p)    for p == nil {        // 如果有 p == nil(即键值对被 delete),则会在这个时机被置为 expunged        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {            return true        }        p = atomic.LoadPointer(&e.p)    }    return p == expunged}

Delete

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (m *Map) Delete(key interface{}) {    m.LoadAndDelete(key)}
// LoadAndDelete 作用等同于 Delete,并且会返回值与是否存在func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {    // 获取逻辑和 Load 类似,read 不存在则查询 dirty    read, _ := m.read.Load().(readOnly)    e, ok := read.m[key]    if !ok && read.amended {        m.mu.Lock()        read, _ = m.read.Load().(readOnly)        e, ok = read.m[key]        if !ok && read.amended {            e, ok = m.dirty[key]            m.missLocked()        }        m.mu.Unlock()    }    // 查询到 entry 后执行删除    if ok {        // 将 entry.p 标记为 nil,数据并没有实际删除        // 真正删除数据并被被置为 expunged,是在 Store 的 tryExpungeLocked 中        return e.delete()    }    return nil, false}

总结

可见,通过这种读写分离的设计,解决了并发情况的写入安全,又使读取速度在大部分情况可以接近内建 map,非常适合读多写少的情况。

sync.Map 还有一些其他方法:

Range:遍历所有键值对,参数是回调函数•LoadOrStore:读取数据,若不存在则保存再读取

这里就不再详解了,可参见 源码[1]。

References

[1] 源码: https://github.com/golang/go/blob/2e8dbae85ce88d02f651e53338984288057f14cb/src/sync/map.go

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

本文分享自 面向人生编程 微信公众号,前往查看

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

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

评论
登录后参与评论
暂无评论
推荐阅读
ES日志报错赏析-- allow delete
当磁盘使用率超过85%,或者达到100%,会导致 Elasticsearch 集群或 Kibana 无法正常提供服务,可能会出现以下几种问题场景:
ES小助理
2022/07/07
1.2K0
ES集群搭建详细步骤[通俗易懂]
@系统:*Centos6**** ES版本:6.4.0 服务器三台 172.16.0.8 172.16.0.6 172.16.0.22
全栈程序员站长
2022/11/04
4.4K1
Elasticsearch 悬挂索引解析与管理指南
在 Elasticsearch 的实战中,悬挂索引是一个既常见又容易引起困扰的概念。
铭毅天下
2024/03/25
2500
Elasticsearch 悬挂索引解析与管理指南
腾讯云ES分批融合迁移方案
本文描述问题及解决方法同样适用于 腾讯云 Elasticsearch Service(ES)。
岳涛
2023/07/25
8251
腾讯云ES分批融合迁移方案
Elasticsearch 索引生命周期管理
在 Elasticsearch的日常管理中,有很多如系统日志,行为数据等方面的应用场景,这些场景的特点是数据量非常大,并且随着时间的增长索引的数量也会持续增长,然而这些场景基本上只有最近一段时间的数据有使用价值或者会被经常使用(热数据),而历史数据几乎没有作用或者很少会被使用(冷数据),这个时候就需要对索引进行一定策略的维护管理甚至是删除清理,否则随着数据量越来越多除了浪费磁盘与内存空间之外,还会严重影响 Elasticsearch 的性能。
Se7en258
2021/05/18
8180
Elasticsearch 索引生命周期管理
【ES三周年】集群半数以上master节点掉线解决方法
1.es集群有元数据(clusterstate)包含cluster、index、shard级别的元数据,持久化保存在master-eligible节点
用户8496852
2023/02/06
1.2K0
ElasticSearch悬挂索引的处理
Dangling indices(悬空索引)指数据存储在一个或多个节点磁盘上但当前集群的clusterMetaData中并不包含这些索引信息。
保持热爱奔赴山海
2024/08/13
2110
ES系列(五):获取单条数据get处理过程实现
前面讲的都是些比较大的东西,即框架层面的东西。今天咱们来个轻松点的,只讲一个点:如题,get单条记录的es查询实现。
烂猪皮
2021/06/10
1.3K0
ES系列(五):获取单条数据get处理过程实现
ELK批量删除索引及集群相关操作记录-运维笔记
线上部署了ELK+Redis日志分析平台环境, 随着各类日志数据源源不断的收集, 发现过了一段时间之后, ELK查看会原来越慢, 重启elasticsearch服务器节点之前同步时间也会很长,  这是因为长期以来ELK收集的索引没有删除引起的! 以下是ELK批量删除索引的操作记录:
洗尽了浮华
2018/12/14
4.2K0
Elasticsearch索引分片损坏该怎么办?
那么这种情况发生的原因是什么呢?我们要知道,索引分片是不可能无故发生损坏的,分片所在的节点一定发生过异常。
周银辉
2024/08/27
3600
【腾讯云ES】Elasticsearch 分布式架构剖析及扩展性优化
Elasticsearch 是一个实时的分布式搜索分析引擎,简称 ES。一个集群由多个节点组成,节点的角色可以根据用户的使用场景自由配置,集群可以以节点为单位自由扩缩容,数据以索引、分片的形式散列在各个节点上。本文介绍 ES 分布式架构基础原理,剖析分布式元数据管理模型,并介绍腾讯云 ES 在分布式扩展性层面相关的优化,解析代码基于 8.5 版本。
黄华
2022/11/18
3.7K1
【腾讯云ES】Elasticsearch 分布式架构剖析及扩展性优化
深入了解Elasitcsearch存储
本文我们将研究Elasticsearch各功能模块写入数据目录中的文件。我们将分别从节点层面,索引层面和分片层面进行了解,并简单解释他们的内容,以帮助大家了解Elasticsearch写入磁盘的数据。
michelmu
2019/07/05
10.2K0
深入了解Elasitcsearch存储
Kubernetes 下部署 Jmeter 集群
可以从 master 节点启动测试,master 节点把对应的测试脚本发送到对应的 slaves 节点,slave 节点的 pod/nodes 主要作用即发压。
高楼Zee
2021/04/01
3K0
Kubernetes 下部署 Jmeter 集群
Elasticsearch索引分片损坏该怎么办?(一)
本文描述问题及解决方法同样适用于 腾讯云 Elasticsearch Service(ES)。
岳涛
2021/09/30
5.1K2
Elasticsearch索引分片损坏该怎么办?(一)
Elasticsearch基于nfs备份环境搭建
从事DBA工作多年,一直没有给大家留下东西。昨天看了铭毅天下老师的文章,感觉我也要写点东西。所以立志每天写一篇文章,在社区留下点痕迹。本文主要分两部分,一部分为nfs环境的搭建,下次会介绍下hdfs环境的搭建;第二部分是关于索引和全库备份方案,详细请查看下文。
雨夜v1
2021/03/12
9410
Elasticsearch基于nfs备份环境搭建
ES维护常见问题(持续更新)
1 存在未分片索引 1)找出未分片的索引 curl xxx/_cat/shards?v | grep UNASSIGNED 2)查看未分配的原因 curl -XGET 'http://xxx/_clu
YG
2018/05/23
3.5K0
Elasticsearch探索:Index lifecycle policy
如果你要处理时间序列数据,则不想将所有内容连续转储到单个索引中。 取而代之的是,您可以定期将数据滚动到新索引,以防止数据过大而又缓慢又昂贵。 随着索引的老化和查询频率的降低,您可能会将其转移到价格较低的硬件上,并减少分片和副本的数量。
HLee
2021/01/12
4.4K0
Elasticsearch探索:Index lifecycle policy
ELK运维文档
用于查看Node级别的基本信息,选参数为pipelines、os和jvm,如下查看基本的os和jvm信息:
charlieroro
2024/01/26
8740
ELK运维文档
OpenShift 3.11 离线安装
环境描述 介绍 两个节点,一个master节点,另一个当做compute和infra节点,使用的操作系统为rhel 7.4,没有安装EFK、service broker、service catalog、metric,promethues在3.11正式GA,默认就会安装。因为本人有红帽的订阅账号,所以可以从红帽的源进行yum安装,需要提醒的是,从3.11开始,红帽官方的镜像仓库从registry.access.redhat.com变为registry.redhat.io,且拉取镜像也需要红帽的订阅账号了。 主
DevOps云学堂
2019/10/18
1.8K0
ES集群如何做到高可用
ES集群的高可用可分为读高可用、写高可用与发生改变(集群状态改变)时高可用。其实这么说不是很准确,因为部分集群状态的改变会影响读和写的高可用。 读高可用指的是多个副本情况下,某个副本出问题时不影响整个系统的读。 写高可用指的是多个副本情况下,某个副本出问题时不影响整个系统的写,通过translog来确保数据不会丢失。 集群状态的改变的高可用包含自动处理节点的加入和离开,自动同步改变的集群状态,当集群发生故障时自动切换主副shard等等来保持集群的高可用。 读和写的高可用这里不再描述,下面将通过三个部
YG
2018/05/23
3.2K0
推荐阅读
相关推荐
ES日志报错赏析-- allow delete
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档