如何优雅避免自引用多对多递归关系中的无限循环?
优化角色循环关联检测的方案
这确实是个很典型的层级关系循环检测问题,你的递归思路方向是对的,但频繁发起SQL查询确实会拖慢性能,尤其是角色数量多的时候。这里有几个更优雅高效的优化方案,供你参考:
1. 使用SQL递归CTE(首推方案)
MySQL 8.0+和主流关系型数据库都支持递归公共表表达式(CTE),可以通过一次SQL查询直接获取所有需要排除的父级及祖先ID,彻底消除多次查询的开销。
示例SQL语句
WITH RECURSIVE excluded_roles AS ( -- 初始步骤:获取当前角色的直接父角色 SELECT role_id FROM role_roles WHERE child_id = ? UNION ALL -- 递归步骤:向上遍历所有父角色的父角色,直到无上层节点 SELECT rr.role_id FROM role_roles rr JOIN excluded_roles er ON rr.child_id = er.role_id ) SELECT DISTINCT role_id FROM excluded_roles;
Golang调用示例
把这个SQL整合到代码中,只需执行一次查询就能拿到所有需排除的ID:
func getExcludedRoleIDs(childID int) ([]int, error) { query := ` WITH RECURSIVE excluded_roles AS ( SELECT role_id FROM role_roles WHERE child_id = ? UNION ALL SELECT rr.role_id FROM role_roles rr JOIN excluded_roles er ON rr.child_id = er.role_id ) SELECT DISTINCT role_id FROM excluded_roles; ` rows, err := builder.GlobalBuilder.Query(query, childID) if err != nil { return nil, err } defer rows.Close() var excludedIDs []int for rows.Next() { var id int if err := rows.Scan(&id); err != nil { return nil, err } excludedIDs = append(excludedIDs, id) } return excludedIDs, nil }
这个方案把递归逻辑交给数据库处理,性能比多次调用SQL提升明显,代码也更简洁易维护。
2. 预加载全量角色关系到内存(适合小数据量场景)
如果你的角色数量不多,且角色关联关系不会频繁变更,可以一次性把所有role_roles数据加载到内存中,之后的循环检测都在内存中完成,彻底避免数据库查询。
实现思路
- 先查询所有关联数据,构建子角色ID到父角色ID列表的映射(比如
map[int][]int) - 在内存中递归遍历这个映射,收集所有需排除的ID
示例代码
// 预加载的父角色映射,可缓存起来避免重复查询 var parentMap map[int][]int // 并发场景下需要加锁保证线程安全 var mapMutex sync.RWMutex // 预加载全量角色关联关系 func loadParentMap() error { mapMutex.Lock() defer mapMutex.Unlock() rows, err := builder.GlobalBuilder.Select("role_roles").Columns("child_id", "role_id").All() if err != nil { return err } defer rows.Close() parentMap = make(map[int][]int) for rows.Next() { var childID, parentID int if err := rows.Scan(&childID, &parentID); err != nil { return err } parentMap[childID] = append(parentMap[childID], parentID) } return nil } // 内存中递归查询需排除的ID func getExcludedIDsInMemory(id int) []int { mapMutex.RLock() defer mapMutex.RUnlock() var result []int visited := make(map[int]bool) // 避免重复处理同一个角色ID var traverse func(int) traverse = func(currentID int) { if visited[currentID] { return } visited[currentID] = true parents, exists := parentMap[currentID] if !exists { return } for _, pID := range parents { result = append(result, pID) traverse(pID) } } traverse(id) return result }
这种方案适合角色数据量小的场景,预加载一次后后续查询性能极高。
3. 给递归函数加缓存(最小改动方案)
如果暂时不想大改现有代码,可以给递归函数加一个缓存层,避免重复查询同一个角色的父角色列表,减少SQL查询次数。
示例代码
// 缓存已查询过的角色排除ID列表 var idCache = make(map[int][]int) var cacheMutex sync.Mutex // 并发场景下加锁 func whereIDLoop(id int) ([]int, error) { cacheMutex.Lock() defer cacheMutex.Unlock() // 先查缓存,命中则直接返回 if cachedIDs, ok := idCache[id]; ok { return cachedIDs, nil } ids := []int{} b := builder.GlobalBuilder rows, err := b.Select("role_roles").Columns("role_id").Where("child_id = ?", id).All() if err != nil { return nil, err } defer rows.Close() for rows.Next() { var parentID int if err := rows.Scan(&parentID); err != nil { return nil, err } ids = append(ids, parentID) childIDs, err := whereIDLoop(parentID) if err != nil { return nil, err } ids = append(ids, childIDs...) } // 写入缓存,供后续查询复用 idCache[id] = ids return ids, nil }
这个方案在现有代码基础上改动最小,通过缓存减少重复查询,但性能提升幅度不如前两个方案,适合临时过渡使用。
内容的提问来源于stack exchange,提问作者Patrick
相关产品推荐
相关产品推荐

