Go微服务实战
上QQ阅读APP看书,第一时间看更新

8.2.6 sync.Map

如果要缓存一部分体量不大的数据,完全可以考虑使用sync.Map。Go语言在1.9以上的版本的标准包就提供了该功能。

在1.6版本以前,Go语言自带的标准的map类型是并发读安全的,但是并发写不安全。所以1.6版本以前需要自己写一个并发读写安全的map,不过,实现这个map也非常简单,只需要定义一个带有RWMutex锁和map类型的结构体即可,示例如下:


var safeMap = struct{
    sync.RWMutex
    m map[string]int
}{m:make(map[string]int)}

上面的代码非常简便地定义了变量safeMap,使用的时候也非常简单,如下所示:


safeMap.RLock()
n := safeMap.m["key"]
safeMap.RUnlock()

上面的代码实现了map的读安全,而下面的代码实现了写安全:


safeMap.Lock()
safeMap.m["key"]=1
safeMap.Unlock()

可见之前的用法也不算复杂,但是这种实现方式在大并发写的情况下会有性能问题。也就是说在写操作时,上述实现方式相当于串行写,非常容易发生性能问题。所以,Go语言提供了标准的sync.Map。

sync.Map在并发安全性上的实现是典型的空间换时间的思想,其提供了read和dirty这两个数据结构,用来降低加锁对性能的负面影响。sync.Map的结构体定义如下:


type Map struct{
    mu Mutex //dirty操作使用该锁
    read atomic.Value //只读数据结构
    dirty map[interface{}]*entry //包含当前Map的entry
    misses int //read内没有而dirty内有的entry数
}

read变量是只读的,因此不存在并发安全的问题。实际上,read的结构体内是包含entry的,且entry是会更新的。如果entry在Map内没有被删除则不需要加锁,反之则需要加锁,以便更新dirty的数据。

dirty含有最新的写入数据,并且在写操作执行时会把read中未删除的数据复制到dirty中,dirty的操作需要mu锁。

从read内读数据的时候,如果read内没有数据,会到dirty内读取,然后misses会加1。待misses的数值等于dirty的长度时,会把dirty数据提升到read内。

上面对于原理的介绍可能还是有读者不能透彻理解,因为篇幅原因本书中不做详细的源码分析,请有兴趣的读者分析sync.Map的源码,结合上述介绍,就可以理解其实现。

sync.Map提供的常用方法有如下五个。

▪Load(key interface{}) (value interface{},ok bool):通过参数key查询对应的value,如果不存在则返回nil;ok表示是否找到对应的值。

▪Store(key,value interface{}):该方法相当于对sync.Map的更新或新增,参数是键值对。

▪LoadOrStore(key,value interface{}) (actual interface{},loaded bool):该方法的参数为key和value。该方法会先根据参数key查找对应的value,如果找到则不修改原来的值并通过actual返回,并且loaded为true;如果通过key无法查找到对应的value,则存储key-value并且将存储的value通过actual返回,loaded为false。

▪Delete(key interface{}):通过key删除键值对。

▪Range(f func(key,value interface{}) bool):遍历sync.Map的元素,注意for...range map是对内置map类型的用法,sync.Map需要使用单独的Range方法。

下面写一段简单的示例代码,介绍sync.Map的使用方法:


book/ch08/8.2/map/map.go
1. package main
2.
3. import (
4.     "fmt"
5.     "sync"
6. )
7.
8. var m sync.Map
9.
10. func main() {
11.     //新增
12.     m.Store(1,"one")
13.     m.Store(2,"two")
14.     //LoadOrStore key不存在
15.     v,ok := m.LoadOrStore(3,"three")
16.     fmt.Println(v,ok)//three false
17.     //LoadOrStore key存在
18.     v,ok = m.LoadOrStore(1,"thisOne")
19.     fmt.Println(v,ok)//one true
20.     //Load
21.     v,ok = m.Load(1)
22.     if ok{
23.         fmt.Println("key is existed,and value is:",v)
24.     }else{
25.         fmt.Println("key is not existed")
26.     }
27.     //Range
28.     //先为Range准备一个传入的函数,该函数的声明格式是固定的,只有内部代码
29.     //可以自定义
30.     f := func(k,v interface{}) bool{
31.         fmt.Println(k,v)
32.         return true
33.     }
34.     //执行Range
35.     m.Range(f)
36.
37.     //Delete
38.     m.Delete(2)
39.     fmt.Println(m.Load(2)) //nil false
40. }

示例代码非常简单,而且也加了注释,此处就不再做详细介绍了。

sync.Map在实战中经常用到。在存储的内存不是很多的情况下,完全可以使用sync.Map,性能非常优异,而且并非所有的场景都优先考虑redis(一款常用的key-value远程存储服务)。