sync map 作为解决 map 并发读写问题的补充,用法上其实不复杂,有些惋惜的是,不支持 len 统计数量的方法。map 并发读写算得上一个非常严重的问题,会导致服务宕机,为了避免 map 的并发读写,一种解决办法是直接使用类似 mutex 的加解锁方式,另一种就是使用 sync map 来替换。
我的想法
map 并发写会导致 fatal error,并发读写会导致 fatal error,但并发读并不会导致 panic。所以,我们是不是可以考虑将 map 拆分成 2 个map,一个 map 负责存储只读的数据,不需要进行加锁,另一个负责读写的数据部分,使用 RWMutex 进行加锁。
通过这样的读写拆分,如果是一个读多写少的场景,读完全不需要加锁,性能杠杠的。写正常加锁,因为写的场景少,性能也可以接受。但这种内部数据隔离机制该怎么实现呢,如果来区分不同的场景呢?
我觉得可以实现一套切换机制,我们称左边的为 read map,不需要加锁,只负责读取。右边称为 write map,只负责写入。我们封装一层代理层,读取的时候读取 read map,写入的时候,异步加锁写入 write map,写入完成后,将 read map 和 write map 互换角色,并异步保持两者的数据同步。
write map 的写入都是异步加锁进行的,切换为 read map 的角色之后,两个角色之间数据的一致性也是异步加锁进行的,感觉好像也没有什么问题。但如果两个 map 切换过程有写入操作的话,该如何处理比较好呢?
Sharding 的思路
既然本质上需要通过加锁来解决,那么如何低成本地使用锁就是关键,而细粒度拆分就是我们的切入点。Sharding 最常见的应用场景就是分库分表,关于分库分表也是一块值得认真思考的问题。
Go 中 Sharding 的思路其实也分成明显,比如 PGM 的设计,程序设计上,每个 P 上都有自己的本地 G 队列,也有本地的内存申请空间,用来 Sharding 全局 G 队列的锁开销。忘记补充一点,还有 sync pool 的缓存对象,也是和 P 挂钩。
综上所述,我们可以设计一个 Sharding 的 map,就好比是一个包含 32 个分片的 redis 集群,每个 map 对应一个独立的锁,这样当访问数据时,首先计算 key 所在的分片 map,然后通过加解锁获取值。
官方的设计
上面写的那些都不作数,关键还是得看看官方的设计,看看官方是如何解决我们遇到的问题的。官方的设计也是底层实现了两个 map,只不过一个叫 read,另外一个叫 ditry。read 作为只读变量,访问是不需要加锁的。ditry 作为写对象,访问是需要解锁的。
和我们之前的想法特别相近,如果是读多写少的场景,利用 read 不加锁的特性来提高访问的性能。这里的处理思路本质上是:空间换时间,但实际的空间也并不是假想的 2 倍,主要是因为 read 和 ditry 两个结构底层使用的类型是指针。
那么 read 和 ditry 两个对象是如何交互的呢?只要得从几个关键的路径上去分析。①如果在 read 中获取不到数据,是否要去 ditry 中尝试获取;②如果向 ditry 中写入/更新/删除了新的数据,read 如何知道 ditry 变更了新的数据。