sync.Map是一个并发安全的map,它是通过双层的数据来存储的,第一层read,可以实现无锁的读取,因此sync.Map适合用于读多写少的场景,结构如下
个人理解sync.Map的结构有点像常用的缓存设计,read就是缓存层,它可以进行无锁的读取;dirty就是数据库层,它存储了所有的数据,需要加锁进行操作。dirty在一定情况下会提升到read,同时dirty数据会从read中进行拷贝
dirty和read的转换
read可以理解为缓存,可以无锁进行操作,所以很快,dirty即为全量数据
- dirty提升为read
Map中有一个变量为misses,它记录了读取read没命中的次数,当misses次数超过了dirty中数据个数的时候,就会将dirty提升为read,同时dirty置为nil。每次读取read失败的时候都会进行判断是否需要将dirty提升到read,升级结果如下图所示。其实就是将dirty赋值到read.m
- 从read复制数据到dirty
在写入数据的时候,如果read未命中,会判断dirty是否为nil,如果是,则会从read中复制数据到dirty中。注意这里expunged就会产生作用了,在复制的过程中,如果read中某个entry存储的value的数据为nil,说明这个数据被删除了,次数dirty不会复制这个entry,同时会将这个entry.p置为expunged。如下图所示。至于key4和key6为什么为nil,可以看删除操作
样例图
读取操作
由此可以发现,sync.Map的读取很简单,首先无锁读取read,如果没有再去dirty中读取,其中包括了double check,即加锁后再check一边read的数据
写入操作
对于写入,需要保证read和dirty数据一致性,这里有两种情况,一种是read中有相应的key,一种是read中没有相应key
如果read中没有相应的键,只需要在dirty中写入相应的key/value就行了,当然这里需要加锁进行写入即可。例如,如果我们此时需要插入key5,此时read中并没有key5,则加锁后往dirty中设置key5即可,同样的,key3也是这种情况
如果read中有相应的键,这里需要对key对应的entry中保存的指针进行判断:
- 如果entry.p不为expunged,可以将其直接指向value即可,因为dirty和read.m的结构都是map[interface{}]*entry,它们都是指针结构,因此只需要替换掉entry中的指针,就可以实现dirty和read同时更新。对应上图中key1和key4情况
- 如果entry.p为expunged,那说明这个key在read中存在,但是在dirty中不存在。这种情况就不能直接更新entry.p了,这样会导致数据不同步。这种情况下,就需要加锁更新dirty和read了,对应上图中key2的情况
删除操作
对于删除操作,也需要保证数据一致性,同样分为read中有相应的key和read中没有相应的key两种情况
如果read中没有相应的key,则直接加锁后从dirty中删除key即可
如果read中有相应的key,则通过将entry.p置为nil即可