Map的源码解析,还是有点绕的
主要两个字段:read和dirty
我的理解是read相当于redis,dirty相当于db: 主要涉及Load()、Store()、Delete()
读:
- 读都从read中读取,不加锁,很快
- 如果read中没有,那就从 dirty 中读取,这时就会加锁读取了
- 如果dirty中有值,返回值,要不要更新read呢?看misses统计数量,只有达到某个阈值misses > len(dirty),才会更新read(正常我们从dirty中读取之后会重新设置到read中,但是作者选择搞个阈值来控制,这样减少拷贝值,因为拷贝值也是比较耗时的事情)
写:
- 写的时候,优先更新read中的key,因为这时候不需要加锁,通过原子操作CompareAndSwap(),如果能更新成功,那就赚到了
- 如果read中没有key,那就开始加锁了,因为可能要操作dirty了
- 这时候还是会先看read中有没有值,有就更新read里的数据(双重校验read,因为可能其他线程把read给更新了,这时候read中又有值了,捡漏)
- 如果read还是没有key,那就要看这个key在不在dirty中了,如果在dirty中,直接更新dirty中的值
- 如果都不在,那就是新key,插入到dirty中,read这时候并没有新key,所以read的amend要为true,表示read和dirty数据不一致
删:
- 删除也是先判断read中有没有该key,如果有的话,直接将该key对应的entry复制为nil,标记该key删除了(虽然该key还在read中,但是entry=nil)
- 如果read中没有,且read和dirty数据不一致,那就要加锁操作了
- 同样的双重校验,判断key在不在read中,如果在的话,同1标记该key为删除(entry=nil)
- 如果还是不在,那就从dirty中读取该key,如果key在dirty中存在,同样将该key的entry=nil,同时将dirty中的key真正删除(要保证dirty的数据都是未被删除的数据),同时misses+1
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
m := &MapSync{}
k := "a"
v := "hello"
k1 := "b"
v1 := "world"
m.Store(k, v) // dirty中有k,read中没有,read的amend=true
fmt.Printf("m=%+v\n", m)
fmt.Println("------")
m.Load(k) // misses=1
m.Load(k) // misses=2, misses > len(dirty)了, 把dirty的值copy给read,read中就有值k了,dirty为nil
// 这时候read的amend=false,表示read和dirty是一致的
m.Store(k1, v1) // !amend条件成立,把read未删除的k同步给dirty,这时候dirty有k和k1,read只有k,amend=true
r, _ := m.dirty[k].load()
r1, _ := m.loadReadOnly().m[k].load() // 从read中读取的
//r2, _ := m.loadReadOnly().m[k1].load() // 从read中读取的, read中没有,一定会报错
r2 := m.loadReadOnly().m[k1] // 这里一定是nil
fmt.Printf("r=%+v\n", r) // hello
fmt.Printf("r1=%+v\n", r1) // hello
fmt.Printf("r2=%+v\n", r2) // nil
m.Load("c") // misses=1
m.Load("c") // misses=2
m.Load("c") // misses=3 misses条件达到了,把dirty的值赋值给read了,这时候read就有k和k1了,dirty=nil
r1, _ = m.loadReadOnly().m[k].load() // 从read中读取的
r3, _ := m.loadReadOnly().m[k1].load() // 从read中读取的
fmt.Printf("r1=%+v\n", r1) // hello
fmt.Printf("r2=%+v\n", r3) // world
}
type MapSyncInterface interface {
Load(key any) (value any, ok bool)
Store(key, value any) (previous any, ok bool)
Delete(key any) (old any, ok bool)
}
// new 返回的是一个指针,说明expunged只是一个指针变量,用于标记某个元素是否被删除
var expunged = new(any)
type entry struct {
p atomic.Pointer[any]
}
func newEntry(i any) *entry {
e := &entry{}
e.p.Store(&i)
return e
}
// load 获取map中value的值
func (e *entry) load() (value any, ok bool) {
p := e.p.Load()
if p == nil || p == expunged {
return nil, false
}
return *p, true
}
// trySwap 尝试更新read中的值
func (e *entry) trySwap(value *any) (*any, bool) {
// 这里搞个死循环,那就是如果值没标记删除,那就是要必更新成功的节奏啊
for {
p := e.p.Load()
// 如果read中的值已经是标记为删除状态,那该值也不会存在于dirty中
// 所以不能直接更新,不然会出现read中有值,但是dirty中没有
if p == expunged {
return nil, false
}
// 直到更新成功
if ok := e.p.CompareAndSwap(p, value); ok {
return p, true
}
}
}
// delete 标记删除
func (e *entry) delete() (old any, ok bool) {
// 循环更新
for {
p := e.p.Load()
// 如果entry的值已经删除或被标记为删除了,就不存在删除了
if p == nil || p == expunged {
return nil, false
}
// 标记删除,设置为nil
if e.p.CompareAndSwap(p, nil) {
return *p, true
}
}
}
// unexpungeLocked 如果旧的值被标记为删除了,重新修改为nil
func (e *entry) unexpungeLocked() bool {
return e.p.CompareAndSwap(expunged, nil)
}
// swapLocked 设置新值,同时把旧值返回
func (e *entry) swapLocked(value *any) *any {
return e.p.Swap(value)
}
type readOnly struct {
m map[any]*entry // read中存储key-value
amend bool // 如果为true,那说明read中的数据和dirty的不一致
}
// MapSync 并发安全Map
type MapSync struct {
mu sync.Mutex
// 读内容,类似于redis的作用,用于快速查询数据,不需要加锁
// 如果读中没有,那就需要加锁去dirty中查询
// 往往会进行双重读,第一遍不加锁去读数据,如果数据不存在,再加锁去取dirty数据之前,再从read中取一次
read atomic.Pointer[readOnly]
// 类似于DB的作用,记录写的内容,往往都是最新最准确的数据
// 新增的key都是写到dirty中
dirty map[any]*entry
// misses, 指read和dirty中数据不一致的次数
// 如果misses数大于等于dirty的长度,则表示穿透到dirty的情况比较多,
// 这时候需要把dirty中的数据放到read中,方便后续只需要从read中读取,减少加减锁的性能
misses int
}
// loadReadOnly 加载readOnly
func (m *MapSync) loadReadOnly() readOnly {
if p := m.read.Load(); p != nil {
return *p
}
return readOnly{}
}
// Load 根据key查询value值
func (m *MapSync) Load(key any) (value any, ok bool) {
// 读取read
read := m.loadReadOnly()
// 先从read中获取key是否存在
e, ok := read.m[key]
// 如果key不存在read中,且read和dirty中的数据不一致了,再去dirty中读取
// 不然如果read和dirty一致,read中没有,dirty一样没有,也没必要去dirty中读取了
if !ok && read.amend {
// 准备从dirty中读取,先锁住
m.mu.Lock()
// 双重校验
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amend {
// 双重校验还是没有,且read和dirty不一样,那就从dirty中读取
e, ok = m.dirty[key]
m.missLocked()
}
m.mu.Unlock()
}
// 最后检查一次,不管是从read中,还是从dirty中,如果能读到,ok都有值
if !ok {
return nil, false
}
// 返回map中value的值
return e.load()
}
// Store 更新/插入新的值
func (m *MapSync) Store(key, value any) (previous any, loaded bool) {
// 先从read中查询是否存在该key
read := m.loadReadOnly()
if e, ok := read.m[key]; ok {
// read中有数据,那就尝试在read中更新该值,
// 为啥尝试在read中更新?通过原子操作减少加锁带来的消耗
// 因为read中和dirty中指向的底层是一样,read中更新了,dirty中的值也会相应更新,如果dirty有该key的话
if v, ok := e.trySwap(&value); ok {
// 旧值是nil,说明旧值被删除了
// 这里其实已经更新成功了,只是旧值被删除了,这里返回nil和无法获取旧值
if v == nil {
return nil, false
}
// 返回旧值的值和标识旧值存在
return *v, true
}
}
// 如果read中没有数据,那就需要加锁写入了
m.mu.Lock()
// 双重校验
read = m.loadReadOnly()
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
// unexpungeLocked方法会把expunged状态变更为nil状态,如果进入该分支,说明是被标记为删除状态,那说明发生了read中的未被删除的值重新写到了dirty中,且该key就不会存在于dirty中
// 那就需要把该key,写入到dirty中, 因为下面会更新该key为非删除状态,保证dirty中一定有read中未被删除的值
m.dirty[key] = e
}
// 把read中的值更新为新值,这一步和上面的trySwap不同
// 上面的是没加锁的,所以需要不断重试更新
// 这里前面已经加锁了,所以只有一个线程在处理,直接更新值即可
// 到这一步其实就更新成功了,这个v != nil的判断只是判断返回旧值有没有值
// 这一步更新成功,对应dirty中e的值也成功了,因为e的底层是一样的
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
} else if e, ok := m.dirty[key]; ok { // read中没有值,如果dirty中有,那就更新dirty的值
// 什么情况会进入这个方法,那就是插入之后接着更新的情况,这时候read是没有值的
// 下面这个方法是比成功的,因为加锁了,v != nil判断只是判断返回旧值有没有值
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
} else { // 如果read 和 dirty中都没有,那就是新的key插入场景
// 如果read和dirty中是一致的,那就需要更新read的amend值为true,标记为不一致
// 什么情况下是一致的呢?
// 1、初始化的时候,第一次插入数据的时候,amend=false
// 2、misses数量超过了阈值,重新把dirty赋值给read,这时候amend是false
if !read.amend {
// 同时如果read中有未删除的值,也同步给dirty
m.dirtyLocked()
// 因为只写到dirty,不写read,所以两者一定是不一致的,amend=true
m.read.Store(&readOnly{m: read.m, amend: true})
}
// 直接写到dirty中,这时read中是没有该key的,因为新插入的key都是在dirty中
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
return previous, loaded
}
// Delete 删除某个key
// 标记删除法
func (m *MapSync) Delete(key any) (value any, loaded bool) {
// 从read总读取该值
read := m.loadReadOnly()
e, ok := read.m[key]
if !ok && read.amend {
m.mu.Lock()
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amend {
// 从dirty获取
e, ok = m.dirty[key]
// 直接删除,这个方法,如果key不存在,并不会真正删除
// 这个方法是真正删除,因为要保证dirty的数据都是未被删除的正常的数据
delete(m.dirty, key)
// 记一次穿透
m.missLocked()
}
m.mu.Unlock()
}
if ok {
return e.delete() // 赋值为nil,表示删除了
}
return nil, false
}
// missLocked 对misses计数,超过一定阈值后,把dirty的值赋值到read中
func (m *MapSync) missLocked() {
m.misses++
// 这里为啥是dirty长度呢?
// 因为dirty代表写入的数据,等于dirty长度,相当于允许每个新key都穿透一次,如果超过了,那就说明穿透概率很高了
if m.misses < len(m.dirty) {
return
}
// 超过阈值了, 把dirty给到read,把dirty设置为nil
// 为啥要把dirty给read,因为进入到这个方法,就是read中没有,才到dirty中的,都已经穿透了
// 所以就把read的换成dirty的,这样就减少穿透了
m.read.Store(&readOnly{m: m.dirty})
// 为啥要把dirty设置为nil呢?
// 我的理解是,既然把dirty给到read了,那大部分的读在read中就能获取到了,就不需要到dirty中查询了
// 对于新插入的key,是写到dirty中的,但是写入dirty之前,会把read中的值同步回dirty
m.dirty = nil
m.misses = 0
}
func (m *MapSync) dirtyLocked() {
// 如果dirty不为空,直接返回
if m.dirty != nil {
return
}
// 什么情况下dirty是nil?
// 1、初始化的时候,第一次插入数据的时候,dirty是nil
// 2、misses数量超过了阈值,重新把dirty赋值给read,这时候amend是false,dirty是nil
read := m.loadReadOnly()
// 把read中位nil的标记为已删除的同时,把未删除的数据赋值给dirty
m.dirty = make(map[any]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
// 能不能不同步read的数据给dirty呢?
// 我理解是不能的,因为如果不同步read的数据给dirty,后续misses超过了,dirty直接覆盖read的值
// dirty变为nil,那之前read中的那部分数据是不是就丢了?
m.dirty[k] = e
}
}
}
// tryExpungeLocked 标记为删除
// delete方法只是把entry设置为nil
// 这个方法就是把标记为nil的entry标记为expunged
func (e *entry) tryExpungeLocked() bool {
p := e.p.Load()
// 不断循环,直到标记成功为止
// 这个方法就是把标记为nil的entry标记为expunged,所以一定是为nil的才会进入循环处理
for p == nil {
// 标记为删除
if e.p.CompareAndSwap(nil, expunged) {
return true
}
p = e.p.Load()
}
// 如果p不为nil,就不会进入上面的循环,那就要判断是否已被标记为删除
return p == expunged
}