之前出了一篇解决无限级分类子集取值的文章: 《使用BFS(广度优先搜索)算法解决无限级分类子集取值 》
在使用过程中发现部门的数据一旦多起来,且层级嵌套比较深的时候,使用常规递归算法来取部门的嵌套结果集会效率很低,因为每次递归取下一级数据的时候,都会将所有数据都循环遍历一次,导致执行时间拖得很长。
假设有 1000 多个部门,部门嵌套层级达到 4 层,接口逻辑实际执行时间竟然要 26 秒,必须考虑进一步优化组装逻辑。
首先递归算法可通过层级深度进行优化,假设层级深度的字段为 depth :
`depth` smallint unsigned NOT NULL DEFAULT '1' COMMENT '层级深度'
先组装每层的列表数据:
// 声明一个字典,储存每层的列表数据
list := make(map[int][]models.Departments, 4)
// 构造每层的迭代数据
for _, item := range departments {
list[item.Depth] = append(works[item.Depth], item)
}
然后递归的时候只循环下一层级的数据,执行时间就可以大大缩减了:
func GetChildrenDepartments(companyId int, pid int, depth int, list map[int][]models.Departments) []Departments {
var result []Departments
for _, item := range list[depth] {
if item.CompanyId != companyId {
continue
}
if pid != item.Pid {
continue
}
// 使用递归循环组装子集
children := GetChildrenDepartments(companyId, item.Id, item.Depth+1, list)
data := Departments{
Id: item.Id,
Name: item.Name,
Children: children,
}
result = append(result, data)
}
return result
}
优化后再看执行时间,已经缩减到 12 秒了,效率提高了一倍多。
接着还可以用 go 的协程去处理子集的组装,这里可以使用 chan 去收集每个顶级部门的层级关系。
我们知道,go 的 chan 实际上是一个指针变量,意味着可以在函数内部处理中提前返回,配合调用方 for range 时可以通过关闭 chan 来控制执行流程(不关闭 chan 则 for range 永远无法停止下来,这里需要注意),所以这里还可以进一步优化。
首先组装顶级部门数据的 chan:
func getTopDepartmentsChan(list map[int][]models.Departments) <-chan Departments {
topDepartmentsChan := make(chan Departments, len(list[1]))
// 使用协程处理顶级部门数据组装
go func() {
for _, item := range list[2] {
topDepartmentsChan <- Departments{
Id: item.Id,
Name: item.Name,
companyId: item.CompanyId, // 这里用结构体私有属性进行缓存
depth: 1, // 这里用结构体私有属性进行处理
}
}
// 处理完成必须关闭 chan,否则使用 for range 循环该 chan 时会导致死锁
close(topDepartmentsChan)
}()
return topDepartmentsChan
}
再接着通过顶级部门数据 chan 并发处理层级关系:
func getDepartmentsChan(topDepartmentsChan <-chan Departments, list map[int][]models.Departments) <-chan Departments {
// 使用锁处理数据收集
var wg sync.WaitGroup
departmentsChan := make(chan Departments, len(topDepartmentsChan))
output := func(department Departments) {
department.Children = GetChildrenDepartments(department.companyId, department.Id, department.depth+1, list)
departmentsChan <- department
wg.Done()
}
// 使用协程处理顶级部门数据的层级关系
for department := range topDepartmentsChan {
wg.Add(1)
go output(department)
}
// 同时注意挂起锁及关闭结果 chan,防止执行逻辑出错及外层 for range 死锁
go func() {
wg.Wait()
close(departmentsChan)
}()
return departmentsChan
}
然后在之前构造每层的迭代数据后进行调用:
topDepartmentsChan := getTopDepartmentsChan(list)
departmentsChan := getDepartmentsChan(topDepartmentsChan, list)
for item := range departmentsChan {
response = append(response, item)
}
// 再将 response 数据返回给调用方
看一下执行时间我们就知道,执行效率又提高了一倍。
考虑到组织架构的部门数据应该属于不常变动的数据,可将结果集丢入缓存提高接口访问效率。
但使用 go-redis 进行 set 操作时,因为结果集是结构体无法直接存入 redis ,需要进行 json 处理转换,经过阅读源码看到,在 go-redis 内,有这么一段 redis 协议的写入值的方法,里面通过判断写入值类型来进行编码。go-redis 已经支持了所有的 go 基本数据类型。
func (w *Writer) writeArg(v interface{}) error {
switch v := v.(type) {
case nil:
return w.string("")
case string:
return w.string(v)
case []byte:
return w.bytes(v)
case int:
return w.int(int64(v))
case int8:
return w.int(int64(v))
case int16:
return w.int(int64(v))
case int32:
return w.int(int64(v))
case int64:
return w.int(v)
case uint:
return w.uint(uint64(v))
case uint8:
return w.uint(uint64(v))
case uint16:
return w.uint(uint64(v))
case uint32:
return w.uint(uint64(v))
case uint64:
return w.uint(v)
case float32:
return w.float(float64(v))
case float64:
return w.float(v)
case bool:
if v {
return w.int(1)
} else {
return w.int(0)
}
case encoding.BinaryMarshaler:
b, err := v.MarshalBinary()
if err != nil {
return err
}
return w.bytes(b)
default:
return fmt.Errorf(
"redis: can't marshal %T (implement encoding.BinaryMarshaler)", v)
}
}
通过上面的源码我们可以知道,可以通过实现 encoding.BinaryMarshaler 来进行结构体存储。先将结果集的类型单独定义,并增加 MarshalBinary 和 UnmarshalBinary 方法:
type DepartmentsResult []Departments
func (result *DepartmentsResult) MarshalBinary() (data []byte, err error) {
return json.Marshal(result)
}
func (result *DepartmentsResult) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, result)
}
最后将结果集直接丢入 set 方法里就可自动处理:
for item := range departmentsChan {
response = append(response, item)
}
client.Set("Departments", &response, 0)