查
- 如果在read map里面找到了就直接返回
- 如果没找到而且read map的amended标志为false 说明此时read和dirty刚刚经过置换,里面条目一致,因此dirty中也不存在这个条目,直接返回空
- 如果没找到而且read map的amended标志为true则代表dirty中有额外的条目,因此需要加锁进入下一步
- 加锁后做double check 如果还是没有找到且read map的amended标志为true则读dirty并记录一次read miss
- 如果此时miss标记大于等于dirty中条目数,说明此时穿透read map的几率已经很大了,有大量的条目是不存在与read中的,此时就会直接将dirty复制给read(dirty中有全量的条目),并将amended置为false(实际上是换了个新的read only结构体,初始化为false)
- 标记小于dirty条目则返回
删
- 在read中查找该条目,如果找到了就直接通过entry的delete方法删除,这个删除是CAS值nil的标记删除而不是内置函数delete()的删除方式,因为要避免并发读写map并且标记删除可以保证读不穿透到dirty中(有点像缓存不存在的元素以防止非法请求不存在元素时请求穿透到数据库中去)
- 如果没找到而且read没被修改过则此时dirty中也不可能有该条目,直接返回
- 如果没找到并且read已经修改过了则加锁然后先double check一下,然后到dirty中使用内置函数delete()进行删除
改/增
-
如果存在而且未被标记为expunged(消除)则直接通过指针更新条目数据
-
加锁并double check
-
如果read存在或者已经被标记删除(expunged)
- 如果是被标记删除则将条目直接加入dirty并更新它的值为新的value
- 如果没有被标记删除则直接更新值为value
-
如果read中没有但dirty中有则直接更新值为value
-
如果都没有则检查read和dirty是否相等(即是否刚做了置换)
- 如果相等则将read中未被标记删除的条目全部加入dirty,注意加入的时候会把已经删除的条目(值为nil)标记为expunged,并且它们不会被添加进dirty,加入完毕后read和dirty内条目一致
- 如果不相等,则直接将该条目写入dirty
注: read中有的条目并且没有被标记为expunged说明它一定不存在于dirty,因此需要先添加到dirty中去,这样保证了后续dirty赋值给read时dirty拥有全量的条目,不会造成条目丢失
两次转换
- miss大于dirty中条目数量时,此时为了避免过量的穿透直接将dirty赋值给read,因为dirty中一定有全部的条目(不可能直接在read中增添新条目,新条目只会添加到dirty中; 如果想要更新read中的条目也是通过指针的形式,这会使得dirty也可以读取到最新的值)
- 当需要添加新条目时,此时如果read和dirty相等而且其中都没有新条目则会把read中没有被标记删除(nil)的条目全部加入到dirty中去,然后再把新条目加入dirty并标记此时read和dirty已经不相等了
原则
- 使用无锁的fast path避免加锁读的消耗,但代价是double check(这是对多读少写的工作负载来说是可以接受的)
- 使用数据指针的方式来实现标记删除(nil)并且避免在更新read中条目值时发生并发读写
- 新增条目只会添加到dirty中
- dirty永远有全量的条目(除非amended为false,此时dirty为nil但这是无所谓的,因为但凡要添加新元素时必须将read中未被删除的条目添加到dirty中,此时dirty也就有了全量的条目)