Go sync.Map:从高效使用到底层实现原理详解

Go标准库中的mapmap的使用及其底层实现原理可参考 Go数据结构之:映射map)并不是并发安全的,通常需要使用互斥锁(sync.Mutexsync.RWMutex)或者改用sync.Mapsync.Map底层借助sync.Mutex、读写分离的设计,减少加锁时机与粒度,非常适用于读多写少的场景,本文将从sync.Map的使用、底层实现两个角度对其进行详细介绍(基于Go 1.16版本)。

1 基础知识

1.1 基本使用

// sync.Map主要对外提供的API
func (m *Map) Store(key, value interface{})
func (m *Map) Load(key interface{}) (value interface{}, ok bool)
func (m *Map) Delete(key interface{})
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
func (m *Map) Range(f func(key, value interface{}) bool)
  • Store:用于存储键值对,如果key已经存在,则更新其对应的值。

  • Load:加载key对应的值,

    • 如果key存在,返回的oktruevaluekey对应的值;

    • 如果key不存在,则okfalsevaluenil

  • Delete:删除键值对,如果key不存在,则不执行任何操作。

  • LoadAndDelete:尝试加载并删除键值对,

    • 如果key存在,loadedtrue,删除该键值对并且返回其原来的值;

    • 如果key不存在,loadedfalse,不执行任何操作,valuenil

  • LoadOrStore:尝试加载key

    • 如果key存在,loadedtrue,返回已经存在的值

    • 如果key不存在,loadedfalse,存储给定的键值对,并返回这个vlaue

  • Range:遍历sync.Map中所有有效的键值对,对每一个键值对调用传入的函数f,一旦调用f返回了false,整个遍历过程后离开停止。需要注意的是,此处的遍历其实只是键值对集合的某一个一致性快照,遍历过程中发生的StoreDelete操作可能被反映在这次遍历中,也可能不被反映(即遍历可能包含新写入的键值对,也可能不包含刚删除的键值对,因为遍历的对象是read map)。

func main() {
    // 直接声明变量后即可使用
    var sm sync.Map

    // 存储,如果最先调用的是Store函数,则底层会先初始化dirty map
    sm.Store("name", "Alice")
    sm.Store("age", 30)

    // 加载
    if name, ok := sm.Load("name"); ok {
        fmt.Println("Name:", name) // 输出: Name: Alice
    }

    // LoadOrStore (存在)
    if actual, loaded := sm.LoadOrStore("name", "Bob"); loaded {
        fmt.Println("Name already exists:", actual) // 输出: Name already exists: Alice
    }

    // LoadOrStore (不存在)
    if actual, loaded := sm.LoadOrStore("job", "Engineer"); !loaded {
        fmt.Println("Stored new job:", actual) // 输出: Stored new job: Engineer
    }

    // LoadAndDelete
    if age, loaded := sm.LoadAndDelete("age"); loaded {
        fmt.Println("Deleted age:", age) // 输出: Deleted age: 30
    }

    // 遍历 (注意快照特性)
    sm.Range(func(k, v interface{}) bool {
        fmt.Printf("Key: %v, Value: %v\n", k, v)
        // 可能输出:
        // Key: name, Value: Alice
        // Key: job, Value: Engineer
        return true // 继续遍历
    })
}

1.2 性能测试

针对实际开发中常用的三种map并发安全方案:

  1. 原生map + sync.Mutex

  2. sync.Map

  3. 原生map + sync.RWMutex(针对读多写少优化)

编写测试代码,进行如下测试场景包括:

  1. 纯读:100%读,0%写
  2. 读写比例9:1:90%读,10%写
  3. 读写比例1:1:50%读,50%写
  4. 纯写:0%读,100%写
package main

import (
	"math/rand"
	"sync"
	"testing"
)

const (
	operationCount = 1000000 // 每个测试的总操作数
	keySpaceSize   = 10000   // 键空间大小
)

// 方案1: 原生 map + sync.Mutex
type MutexMap struct {
	mu sync.Mutex
	m  map[int]int
}

func NewMutexMap() *MutexMap {
	return &MutexMap{m: make(map[int]int)}
}

func (mm *MutexMap) Store(k, v int) {
	mm.mu.Lock()
	defer mm.mu.Unlock()
	mm.m[k] = v
}

func (mm *MutexMap) Load(k int) (int, bool) {
	mm.mu.Lock()
	defer mm.mu.Unlock()
	v, ok := mm.m[k]
	return v, ok
}

// 方案2: 原生 map + sync.RWMutex (读写分离)
type RWMutexMap struct {
	mu sync.RWMutex
	m  map[int]int
}

func NewRWMutexMap() *RWMutexMap {
	return &RWMutexMap{m: make(map[int]int)}
}

func (rw *RWMutexMap) Store(k, v int) {
	rw.mu.Lock()
	defer rw.mu.Unlock()
	rw.m[k] = v
}

func (rw *RWMutexMap) Load(k int) (int, bool) {
	rw.mu.RLock()
	defer rw.mu.RUnlock()
	v, ok := rw.m[k]
	return v, ok
}

// 方案3: sync.Map (标准库并发安全Map)
type SyncMap struct {
	m sync.Map
}

func NewSyncMap() *SyncMap {
	return &SyncMap{}
}

func (sm *SyncMap) Store(k, v int) {
	sm.m.Store(k, v)
}

func (sm *SyncMap) Load(k int) (int, bool) {
	v, ok := sm.m.Load(k)
	if !ok {
		return 0, false
	}
	return v.(int), true
}

// 初始化测试数据
func initTestData(initSize int) []int {
	keys := make([]int, initSize)
	for i := 0; i < initSize; i++ {
		keys[i] = i
	}
	rand.Shuffle(initSize, func(i, j int) {
		keys[i], keys[j] = keys[j], keys[i]
	})
	return keys
}

// 基准测试函数
func benchmarkMap(b *testing.B, readPercent int, setupFunc func() interface {
	Store(k, v int)
	Load(k int) (int, bool)
}) {
	keys := initTestData(keySpaceSize)

	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		// 每个goroutine的本地随机源,避免锁竞争
		r := rand.New(rand.NewSource(rand.Int63()))
		mapInstance := setupFunc()

		for pb.Next() {
			op := r.Intn(100)
			key := keys[r.Intn(len(keys))]

			if op < readPercent {
				// 读操作
				mapInstance.Load(key)
			} else {
				// 写操作
				mapInstance.Store(key, r.Intn(1000))
			}
		}
	})
}

// 纯读场景 (100%读)
func BenchmarkMutexMap_Read100(b *testing.B) {
	benchmarkMap(b, 100, func() interface {
		Store(k, v int)
		Load(k int) (int, bool)
	} {
		m := NewMutexMap()
		// 预填充数据
		for i := 0; i < keySpaceSize; i++ {
			m.Store(i, i)
		}
		return m
	})
}

func BenchmarkRWMutexMap_Read100(b *testing.B) {
	benchmarkMap(b, 100, func() interface {
		Store(k, v int)
		Load(k int) (int, bool)
	} {
		m := NewRWMutexMap()
		for i := 0; i < keySpaceSize; i++ {
			m.Store(i, i)
		}
		return m
	})
}

func BenchmarkSyncMap_Read100(b *testing.B) {
	benchmarkMap(b, 100, func() interface {
		Store(k, v int)
		Load(k int) (int, bool)
	} {
		m := NewSyncMap()
		for i := 0; i < keySpaceSize; i++ {
			m.Store(i, i)
		}
		return m
	})
}

// 读写比例 9:1 (90%读, 10%写)
func BenchmarkMutexMap_Read90(b *testing.B) {
	benchmarkMap(b, 90, func() interface {
		Store(k, v int)
		Load(k int) (int, bool)
	} {
		return NewMutexMap()
	})
}

func BenchmarkRWMutexMap_Read90(b *testing.B) {
	benchmarkMap(b, 90, func() interface {
		Store(k, v int)
		Load(k int) (int, bool)
	} {
		return NewRWMutexMap()
	})
}

func BenchmarkSyncMap_Read90(b *testing.B) {
	benchmarkMap(b, 90, func() interface {
		Store(k, v int)
		Load(k int) (int, bool)
	} {
		return NewSyncMap()
	})
}

// 读写比例 1:1 (50%读, 50%写)
func BenchmarkMutexMap_Read50(b *testing.B) {
	benchmarkMap(b, 50, func() interface {
		Store(k, v int)
		Load(k int) (int, bool)
	} {
		return NewMutexMap()
	})
}

func BenchmarkRWMutexMap_Read50(b *testing.B) {
	benchmarkMap(b, 50, func() interface {
		Store(k, v int)
		Load(k int) (int, bool)
	} {
		return NewRWMutexMap()
	})
}

func BenchmarkSyncMap_Read50(b *testing.B) {
	benchmarkMap(b, 50, func() interface {
		Store(k, v int)
		Load(k int) (int, bool)
	} {
		return NewSyncMap()
	})
}

// 纯写场景 (0%读, 100%写)
func BenchmarkMutexMap_Write100(b *testing.B) {
	benchmarkMap(b, 0, func() interface {
		Store(k, v int)
		Load(k int) (int, bool)
	} {
		return NewMutexMap()
	})
}

func BenchmarkRWMutexMap_Write100(b *testing.B) {
	benchmarkMap(b, 0, func() interface {
		Store(k, v int)
		Load(k int) (int, bool)
	} {
		return NewRWMutexMap()
	})
}

func BenchmarkSyncMap_Write100(b *testing.B) {
	benchmarkMap(b, 0, func() interface {
		Store(k, v int)
		Load(k int) (int, bool)
	} {
		return NewSyncMap()
	})
}

 从上述测试代码的基准测试结果来看,随着写比例的增加,sync.Map的性能急剧下降,因为读操作较多的情况下,首先是通过无锁化进行取值操作;而随着写操作的增加,read map与dirty map之间的转换频率增加,由此导致使用性能下降。

go test -bench=. -benchmem tt_test.go
goos: darwin
goarch: arm64
BenchmarkMutexMap_Read100-8             122276505                9.637 ns/op           0 B/op          0 allocs/op
BenchmarkRWMutexMap_Read100-8           124338636                9.669 ns/op           0 B/op          0 allocs/op
BenchmarkSyncMap_Read100-8              89099803                13.42 ns/op            0 B/op          0 allocs/op
BenchmarkMutexMap_Read90-8              121638187               10.08 ns/op            0 B/op          0 allocs/op
BenchmarkRWMutexMap_Read90-8            120444948               10.00 ns/op            0 B/op          0 allocs/op
BenchmarkSyncMap_Read90-8               66396577                17.28 ns/op            3 B/op          0 allocs/op
BenchmarkMutexMap_Read50-8              100000000               10.55 ns/op            0 B/op          0 allocs/op
BenchmarkRWMutexMap_Read50-8            100000000               10.74 ns/op            0 B/op          0 allocs/op
BenchmarkSyncMap_Read50-8               46445742                25.65 ns/op           15 B/op          1 allocs/op
BenchmarkMutexMap_Write100-8            100000000               10.40 ns/op            0 B/op          0 allocs/op
BenchmarkRWMutexMap_Write100-8          112851788               10.74 ns/op            0 B/op          0 allocs/op
BenchmarkSyncMap_Write100-8             27792708                36.73 ns/op           30 B/op          2 allocs/op

2 底层实现

2.1 sync.Map 数据结构

sync.Map的底层数据结构包含

  • mu,该互斥量只有在操作dirty map时才需要上锁,对于read map的操作一般只涉及到原子操作。

  • read map,读取操作优先从该map中进行判断,该map中,值可能出现expungednil0x...三种类型,其中expunged表示上一次进行read ===> dirty转换时,已经被删除(nil ===> expunged),这类键值对就不会被写入到dirty中,等待下一次dirty ===> read时,就会被彻底删除。

  • dirty map,包含Map全部的键值对,该map中的值只会出现nil0x...两种。这里需要注意的是,readdirty两个map底层value指向的内存地址其实是同一个(expunged类型除外),也就是说通过任意一个map修改地址的value值,两个map都能感知到。

  • misses,用于read中获取失败次数的计数,如果misses值大于等于dirty的长度,则会触发dirty ===> read 的转换。

type Map struct {
   // 互斥锁,用于保证map的并发安全
   mu Mutex

   read atomic.Value // readOnly

   // 包含read map(除了value为expunged)+ 新插入的key-value
   dirty map[interface{}]*entry 

   // 用于控制是否需要将dirty ==> read
   misses int
}

// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
	m       map[interface{}]*entry
    // 标记是否有新的键值对存在于dirty map 而不存在于 readOnly.m
	amended bool // true if the dirty map contains some key not in m.
}

// An entry is a slot in the map corresponding to a particular key.
type entry struct {
   // p 代表的是执行value存储的指针,该变量一共有三个值
   // p == nil,标记该entry已经被删除,只是还没进行read ==> dirty 同步
   // p == expunged,表示正在进行read==>dirty转换,此时会将read中p = nil的键值对转换为 
   // expunged,新的ditry中不会在出现该key,等下一次dirty ==> read,之前被删除的key就会彻底被移除
   // p == 0x...,表示键值对有效,该指针执行具体的内存地址
   p unsafe.Pointer // *interface{}
}

2.2 Store函数

  • 第一步(乐观 - 无锁):尝试从 read 中加载 readOnly 并查找 key 对应的 entry

    • 如果找到 e 并且 e.p 不是 expunged,尝试原子地更新 entry.p 指向新的值。如果 CAS 成功,直接返回(因为非expunged的键值对同时存在于read、dirty)。这是更新已存在 key 的高效路径(无锁)!

    • 如果找到 e 但 e.p 是 expunged,说明这个 key 之前在 read 中被标记为完全删除(dirty 里也没有了),不能直接更新(如果直接更新,dirty中并没有这个key,违背了dirty包含全部生效的键值对原则),需要进入慢路径。

    • 如果没找到,且 readOnly.amended 为 false,说明 dirty 是空的(或者还没初始化),也进入慢路径。

  • 第二步(加锁 - 慢路径):获取 mu 锁(在操作结束后及时释放)。

    • 再次检查 read (Double-Checking):避免在加锁期间 read 已经被更新了。如果这次在 read 中找到了 e

      • 如果 e.p 是 expunged:需要将这个 key 重新“复活”到 dirty 中。将 e.p 设置为 nil(临时状态),然后向 dirty map 添加 key: e。接着就可以更新 e.p 指向新值了。

      • 如果 e.p 不是 expunged(可能被其他 goroutine 更新了):直接用 CAS 更新 e.p(和第一步的快速更新类似)。

    • 如果在 read 中没找到 key

      • 如果 dirty 为 nil,需要初始化 dirty:创建一个新的 map,并将当前 read.m 中所有 p != nil && p != expunged (nil、expunged表示该键值对已经被删除了,如果为nil,此时需要将其在read中的状态修改为expunged)的 entry 浅拷贝到新 dirty 中(值是指针拷贝,不是值拷贝)。同时设置 readOnly.amended = true

      • 如果 dirty 非 nil,且 readOnly.amended 已经是 true,直接操作 dirty

      • 将 key: newEntry(value) 添加到 dirty map 中(如果 key 已存在则更新对应的 entry)。

      • 更新 read 中对应的 entry(如果存在且是 expunged 状态需要先复活)或者确保 dirty 包含了这个 key,并设置 readOnly.amended = true

    • 更新或创建 entry,设置 entry.p 指向新值。

// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
   // 如果key存在于read中,且并没有被标记为expunged,
   // 即同时存在于read和dirty中,则直接使用原子操作进行更新(避免的对dirty修改所需的加锁操作)
   read, _ := m.read.Load().(readOnly)
   if e, ok := read.m[key]; ok && e.tryStore(&value) {
      return
   }

   // 为防止上一次m.read.Load()与m.mu.Lock()之间,有其他并发操作,需要再一次判断m.read
   m.mu.Lock()
   read, _ = m.read.Load().(readOnly)
   // 1. key 存在于read中
   // 1.1 key仅存在于read中,既 p == expunged 的情况
   // 1.2 key同时存在于read、dirty中
   if e, ok := read.m[key]; ok {
      // key仅存在于read中,则需要将其加入到dirty中
      if e.unexpungeLocked() {
         // The entry was previously expunged, which implies that there is a
         // non-nil dirty map and this entry is not in it.
         m.dirty[key] = e
      }
      e.storeLocked(&value)
   } else if e, ok := m.dirty[key]; ok {
      // 2. key仅存在于dirty
      e.storeLocked(&value)
   } else {
      // 3. key不存在于read、dirty,需要重新插入
      if !read.amended {
         // We're adding the first new key to the dirty map.
         // Make sure it is allocated and mark the read-only map as incomplete.
         // dirty == nil,且新加入的key不存在于read中,则将read ==> dirty
         m.dirtyLocked()
         m.read.Store(readOnly{m: read.m, amended: true})
      }
      m.dirty[key] = newEntry(value)
   }
   m.mu.Unlock()
}

2.3 Load函数

  • 第一步(无锁):首先原子地加载 read 指向的 readOnly 结构 (readOnly 本身是只读的,所以并发读安全)。尝试从 readOnly.m 这个 map 中通过 key 查找对应的 *entry

    • 如果找到 e,并且 e.p 不是 nil 也不是 expunged(即有效状态),直接返回 e.p 指向的值;e.p 是 nil 或者 expunged(即无效状态),直接返回 nil。这是最快速、无锁的路径!

    • 如果在 readOnly.m 没找到 key ,并且 readOnly.amended 为 true(表示 dirty 中可能有新数据),则进入慢路径。

  • 第二步(加锁 - 慢路径):获取 mu 锁。

    • 再次检查 read (Double-Checking):避免在加锁期间 read 已经被更新了。如果这次在 read 中找到了有效的 entry(可能其他 goroutine 刚写入或提升了),解锁并返回。

    • 检查 dirty:如果read 中还是没有找到且dirty 非 nil,尝试从 dirty map 中找 key

      • 记录一次 missm.misses++),如果 misses 计数 >= len(dirty),触发 dirty 提升为新的 readread 指向新的 readOnly,其 m 设置为当前的 dirtyamended 设置为 false),然后将 dirty 置为 nilmisses 重置为 0。最后解锁并返回 dirty 中找到的值。

      • 如果 dirty 中也没找到,解锁并返回 nil, false

// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
   // 先不加锁,从read中查找
   read, _ := m.read.Load().(readOnly)
   e, ok := read.m[key]
   
   // read中不存在,且dirty中存在新增的值
   if !ok && read.amended {
      m.mu.Lock()
      // Avoid reporting a spurious miss if m.dirty got promoted while we were
      // blocked on m.mu. (If further loads of the same key will not miss, it's
      // not worth copying the dirty map for this key.)
      // 避免并发时,在上一次读 m.read.Load() 和 m.mu.Lock() 的时间区间内发生 dirty=>read 的升级
      read, _ = m.read.Load().(readOnly)
      e, ok = read.m[key]
      if !ok && read.amended { // 只有 read.amended 为true时,m.dirty才不为nil
         e, ok = m.dirty[key]
         // Regardless of whether the entry was present, record a miss: this key
         // will take the slow path until the dirty map is promoted to the read
         // map.
         // 判断是否需要将dirty ==> read
         m.missLocked()
      }
      m.mu.Unlock()
   }
   if !ok {
      return nil, false
   }
   return e.load()
}

2.4 Delete/LoadAndDelete函数

  • 类似 Load,先尝试从 read 中无锁查找:

    • 如果找到 e 且 e.p 非 nil 且非 expunged,尝试用 CAS 将 e.p 设置为 nil(标记为逻辑删除)。如果成功,返回(对于 LoadAndDelete 返回原值)。

    • 如果 e.p 是 nil 或 expunged,说明它已经被删除了,返回nil,flase

  • 如果快速路径失败,进入加锁慢路径:

    • 加锁后再次检查 read,如果在 read 中找到 e

      • 如果 e.p 非 nil 且非 expunged,将其设置为 nil(逻辑删除)。

      • 如果 e.p 是 nil 或 expunged,说明它已经被删除了,返回nil,flase

    • 如果在 read 没找到且 amended 为 true,且 dirty 非 nil,则直接从 dirty 中删除 key(如果存在的话,直接进行的是物理删除)并返回对应的值(对于 LoadAndDelete)。

// LoadAndDelete deletes the value for a key, returning the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
	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]
            // 对于仅存在于dirty map中的key执行的是物理删除
			delete(m.dirty, key)
			// Regardless of whether the entry was present, record a miss: this key
			// will take the slow path until the dirty map is promoted to the read
			// map.
			m.missLocked()
		}
		m.mu.Unlock()
	}
    // 存在于read map的的key执行的是逻辑删除,既将value值修改为nil
	if ok {
		return e.delete()
	}
	return nil, false
}

2.5 Range函数 

  1. 首先,检查read map中是否有数据(m.read.Load() 获取readOnly结构)。如果read map已经包含了当前的所有键(即dirtynil),那么我们可以安全地遍历read map,因为read map是原子值且只读,遍历过程中不会发生并发问题。

  2. 如果dirty不为nil,说明可能存在read map中没有的新键,此时需要加锁,然后将dirty提升为read(将m.dirty赋值给m.read,并将m.dirty置为nil,同时更新m.misses为0)。

  3. 然后遍历新的read map(即原来的dirty map),并对其中的每个键值对调用函数f。注意:在遍历过程中,如果f返回false,则立即终止遍历。在遍历过程中,由于read map是无锁操作,可能会出现键被删除的情况(表现为值被标记为expungednil),这类在遍历过程中被删除的键值对,如果在获取到该值之前被标记,则会被忽略;如果在获取到该值之后被标记,则会正常被读取。

需要注意的是,在遍历过程中,如果有新的键值对被添加,由于在开始遍历时dirty已经被提升为read,新的写入会进入新的dirty(尚未创建),而当前的read在遍历期间不会变化(只读),所以不会影响当前遍历。但在遍历开始之后新加入的键值对将不会出现在本次遍历中。另外,在遍历过程中,如果遇到已经被删除的键(即值被标记为expunged)或者nil,则跳过。

func (m *Map) Range(f func(key, value any) bool) {
   // 首先,获取当前的read map(只读)
   read := m.loadReadOnly()
   // 如果dirty中有read中没有的键,则需要将dirty提升为read,然后遍历新的read
   if read.amended {
       // 加锁,确保并发安全
       m.mu.Lock()
       // 再次获取read,因为锁之前可能read已经被更新
       read = m.loadReadOnly()
       if read.amended {
           // 将dirty提升为read
           read = readOnly{m: m.dirty}
           m.read.Store(read)
           m.dirty = nil
           m.misses = 0
       }
       m.mu.Unlock()
   }

   // 遍历read map(只读结构)
   for k, e := range read.m {
       v, ok := e.load()
       if !ok {
           // 如果该entry已经被删除,则跳过
           continue
       }
       // 调用f函数,如果f返回false,则终止遍历
       if !f(k, v) {
           break
       }
   }
}

2.6 read与dirty之间的转换

dirty 提升为 read(提升操作)​​:

  • ​时机​​:
    • 当执行 Range 操作并且 amended 为 true(表示 dirty 包含 read 中没有的键)时,需要先提升 dirty 为 read 以获取最新数据的快照。
    • 当 Load 操作在 read 中未找到键(miss)并且需要访问 dirty 时,如果 misses 计数超过了 dirty 的长度(len(m.dirty)),也会触发提升操作。这确保在多次未命中后,最新的 dirty 数据被提升为 read,减少后续的锁争用。
  • ​目的​​:提升后,dirty 被置为 nilmisses 被重置为0。这样新的读操作可以直接在 read 中查找而不需要加锁,直到新的写操作再次创建 dirty

read 重建为 dirty(重建操作)​​:

  • ​时机​​:当需要写(Store)一个不在 dirty中的新键,且此时 dirty 为 nil 时(通常是上次提升后)。
  • ​过程​​:
    • 将 read 中未被标记为删除(nilexpunged)的键值对复制到新创建的 dirty 映射。
    • 在复制过程中,若 read 中的某些项被删除(nilexpunged,nil更新成expunged),则跳过;否则将其标记为正常状态(非删除状态),然后复制到 dirty
  • ​目的​​:重建后,新写入的键值对可以安全地添加到 dirty 中。同时,标记 amended=true 表示 dirty 包含 read 中没有的额外数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值