NestedMap:一个可以无限嵌套的Map

1.概述

NestedMap是在sync.Map基础上实现的无限嵌套(递归)的Map,由于sync.Map天生的协程安全,使用NestedMap时不用考虑锁的问题。NestedMap应用面还是比较广泛的,比如对大量的对象进行分类管理。以笔者从事的集群调度领域为例,一个综合性的scheduler管理多个类型的大量worker,一般两层(根节点除外)的Map:第一层是按照worker类型分类,第二层是每个分类的worker集群。

笔者在介绍concurrent.Map的文章中专门剖析过sync.Map,其中提到sync.Map在某些场景(比如读多写少)性能非常好,适用于高并发的场景。所以笔者将NestedMap也放到了concurrent包中。本文所有源码来自https://github.com/jindezgm/concurrent/blob/master/nested_map.go

2.设计

NestedMap沿用了sync.Map的接口定义,这样可以降低学习成本。NestedMap与sync.Map最大的不同是键的类型,NestedMap的键是一个slice(从根节点一直到叶子节点的路径),所以在接口设计上沿用了sync.Map的接口的同时做了一些修改,具体如下代码所示:

func (nm *NestedMap) Load(keys ...interface{}) (interface{}, error) 
  • 首先,NestedMap的键是...interface{}类型,而不是[]interface{}类型,这是因为前者在大部分应用场景更加方便;
  • 其次,如果使用者的keys已经是一个slice了,直接作为参数传进去即可,但是参数传递不能包含...;
  • 再者,NestedMap对于keys的类型没有任何约束,这让NestedMap的适应面更广;
  • 最后,返回值增加了错误代码,因为不再是有没有值的问题,可能keys也可能是错误的;

这里对上面提到的四点逐一进行解释,为什么说...interface{}更加方便?因为...interface{}可以有多种变化,如下代码实例:

nm := NestedMap{}
// 在一些场景,keys不是一个slice,而是由不同的变量组成,那么直接将不同的变量按照顺序传递即可
nm.Load(1, 2, 3)
// 在一些场景,keys已经是一个[]interface{},那么可以如下方式调用
keys := []interface{}{1, 2, 3}
nm.Load(keys...)
// 但是比较常见的场景的key是string,并且已经是[]string类型了,可以按照如下方式调用(注意没有...,否则编译会报错)
keys := []string{"1", "2", "3"}
nm.Load(keys)
// 所以下面的调用方式也是对的(注意没有...)
keys := []interface{}{1, 2, 3}
nm.Load(keys)

...interface{}可以适配多种场景,而[]interface{}则需要调用者将keys的类型做转换,比较常见的就是[]string转为[]interface{},而NestedMap将类转换留给了自己,方便了调用者。下面来看看NestedMap是如何实现各种类型的keys转换为[]interface{}的:

func (*NestedMap) convertKeys(keys ...interface{}) []interface{} {
	if len(keys) != 1 { // 只要keys不是一个,那么每个都视为一个key,即便某个key是slice类型
		return keys
	} else if t := reflect.TypeOf(keys[0]); t.Kind() != reflect.Slice { // 只有一个key,如果不是slice类型,则视为单key
		return keys
    } else if t.Elem().Kind() == reflect.Interface { // 如果是[]interface{}类型,则用类型断言方式转换
		return keys[0].([]interface{})
	}

    // 这里就是将[]T转为[]interface{}的过程了
	s := reflect.ValueOf(keys[0])
	r := make([]interface{}, s.Len())
	for i := 0; i < s.Len(); i++ {
		r[i] = s.Index(i).Interface()
	}

	return r
}

如果keys只有一个并且类型是slice怎么办?这种情况还是比较少见的,如果需要只能调用者自己做一次类型转换:[]interface{}{[]T}。

因为keys描述了一个查找路径,而keys可能会非法,比如某个key不是分支节点,也就不能再继续递归搜索了,只能返回错误。所以NestedMap很多接口都包含错误代码。至此,已经对上面提到的四点基本做了比较详细的解释。

了解了如何使用keys,那么也就会理解NestedMap为什么把keys作为接口的最后一个参数,而sync.Map接口都是把key作为第一个参数。此处对NestedMap的接口做简要说明:

// Load读取叶子节点,
func (nm *NestedMap) Load(keys ...interface{}) (interface{}, error)
// 写任意节点
func (nm *NestedMap) Store(value interface{}, keys ...interface{}) error
// 如果keys已经存在则不写入,无论是否写入都会把value返回,loaded表示
func (nm *NestedMap) LoadOrStore(value interface{}, keys ...interface{}) (actual interface{}, loaded bool, err error) 
// 删除任意节点
func (nm *NestedMap) Delete(keys ...interface{}) error
// 遍历keys下的所有节点,回调的keys是全路径的keys
func (nm *NestedMap) Range(f func(keys []interface{}, value interface{}) bool, keys ...interface{}) error

3.使用场景

笔者会将NestedMap在如下(但不限于)场景中使用:

  • 配置:复杂的配置不仅层级深,而且类型多变,同时写多读少;
  • worker管理:集群调度中需要根据类型、ID快速访问worker,而worker不会频繁的上、下线;

4.不足

NestedMap暂时不支持如下的调动方式:

nm.Load(1, []int{2, 3})

对于keys的约束是:1)一一罗列在参数中;2)一个slice。上面的代码中,NestedMap无法区分[]int{2, 3}是第二层的一个key还是第二层和第三层的两个key。

NestedMap.Store()权限很高,可以直接用一个叶子节点覆盖一个分支节点。举个栗子:

// 1.2.3.4=4
nm.Store(4, 1, 2, 3, 4)
// 1.2=2, 1.2.3及以后的value的都会被删除。是不是返回错误更合理?从安全的角度看更合理,但需要以损失性能为代价
nm.Store(2, 1, 2)

还有,NestedMap存在分支节点泄露问题,即已经创建的分支节点就不会再删除,即便这个分支节点下没有任何叶子节点。虽然一个分支节点只有sync.Map的大小,但是在大规模节点的场景中也是不小的开销。所以不推荐在频繁创建分支节点的场景中使用NestedMap,除非调用者可以手动删除无效分支节点,直接调用Delete接口即可。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值