使用Redis解決「樹」形數據的複雜查詢

使用Redis解決「樹」形數據的複雜查詢

最近因業務須要,研究了一下樹數據結果的存儲及查詢解決方案。 最初的想法是使用neo4j,但是在網上看了一下開源的不支持集羣,感受用的人很少。node

網上也查了一些 樹形結構數據存儲方案 但每種實現方案都有它的必定侷限性。redis

想了一短期後,想出了下面的方案:數據庫

1、 由於複雜的查詢都由Redis來處理,因此數據庫表的設計就變得很是簡單:tree 表json

字段名稱 數據類型 備註說明
id int 主鍵
parent_id int 上級節點ID

2、Redis的數據存儲方案:測試

把表的數據存儲到一個Hash表中,使用表中的id值作爲此hash表的key, value值爲:優化

{
   id: 10,
   parentId: 9,
   childIds: [11]
}

代碼實現

爲了簡化測試,這裏只演示Redis相關的操做lua

  1. Tree 類定義spa

    public class Tree {
         private Integer id;
         private String name;
         private Integer parentId;
         private List<Integer> childIds;
     }
  2. 往Redis中添加測試數據:.net

    [@Test](https://my.oschina.net/azibug)
     public void addTestData() throws Exception {
         String key = "tree-test-key";
         Tree tree = new Tree();
         List<Integer> childIds = new ArrayList<>();
         int max = 100000
         tree.setChildIds(childIds);
         for (int i = 0; i < max; i++) {
             tree.setId(i);
             tree.setName("tree" + i);
             if (i > 0) {
                 tree.setParentId(i - 1);
             }
             childIds.clear();
             if(i < (max - 1)){
                 childIds.add(i + 1);
             }
             redis.setHash(key, "" + i, JsonUtil.toJson(tree));
         }
     }
  3. Lua 代碼的實現設計

在Lua中使用遞歸時,須要使用「尾調用」來優化代碼。關於尾調用的知識,你們能夠上網去搜索。

獲取全部子節點 get-tree-childs.lua

local treeKey = KEYS[1]
local fnodeId  = ARGV[1]

local function getTreeChild(currentnode, t, res)
  if currentnode == nil or t == nil  then
    return res
  end

  local nextNode = nil
  local nextType = nil
  if t == "id" and (type(currentnode) == "number" or type(currentnode) == "string") then
    local treeNode = redis.call("HGET", treeKey, currentnode)
    if treeNode then
      local node = cjson.decode(treeNode)
      table.insert(res, treeNode)
      if node and node.childIds then
        nextNode = node.childIds
        nextType = "childIds"
      end
    end
  elseif t == "childIds" then
    nextNode = {}
    nextType = "childIds"
    local treeNode  = nil
    local node = nil
    local cnt = 0
    for _, val in ipairs(currentnode) do
      treeNode = redis.call("HGET", treeKey, tostring(val))
      if treeNode then
        node = cjson.decode(treeNode)
        table.insert(res, treeNode)
        if node and node.childIds then
          for _, val2 in ipairs(node.childIds) do
            table.insert(nextNode, val2)
            cnt = cnt + 1
          end
        end
      end
    end
    if cnt == 0 then
      nextNode = nil
      nextType = nil
    end
  end
  return getTreeChild(nextNode, nextType, res)
end


if treeKey and fnodeId then
  return getTreeChild(fnodeId, "id", {})
end

return {}

獲取全部子節點數目 get-tree-childs-cnt.lua

local treeKey = KEYS[1]
local fnodeId  = ARGV[1]

local function getTreeChildCnt(currentnode, t, res)
  if currentnode == nil or t == nil  then
    return res
  end

  local nextNode = nil
  local nextType = nil
  if t == "id" and (type(currentnode) == "number" or type(currentnode) == "string") then
    local treeNode = redis.call("HGET", treeKey, currentnode)
    if treeNode then
      local node = cjson.decode(treeNode)
      res = res + 1
      if node and node.childIds then
        nextNode = node.childIds
        nextType = "childIds"
      end
    end
  elseif t == "childIds" then
    nextNode = {}
    nextType = "childIds"
    local treeNode  = nil
    local cnt = 0
    for _, val in ipairs(currentnode) do
      treeNode = redis.call("HGET", treeKey, tostring(val))
      if treeNode then
        local node = cjson.decode(treeNode)
        res = res + 1
        if node and node.childIds then
          for _, val2 in ipairs(node.childIds) do
            table.insert(nextNode, val2)
            cnt = cnt + 1
          end
        end
      end
    end
    if cnt == 0 then
      nextNode = nil
      nextType = nil
    end
  end
  return getTreeChildCnt(nextNode, nextType, res)
end


if treeKey and fnodeId then
  return getTreeChildCnt(fnodeId, "id", 0)
end

return 0

獲取全部子節點數目 get-tree-parent.lua

local treeKey = KEYS[1]
local nodeId  = ARGV[1]

local function getTreeParent(treeKey, res, nodeId)
  if nodeId == nil or not (type(nodeId) == "number" or type(nodeId) == "string") then
    return res
  end
  local treeNode = redis.call("HGET", treeKey, nodeId)
  local nextNodeId = nil
  if treeNode then
    local node = cjson.decode(treeNode)
    table.insert(res, treeNode)
    if node then
      nextNodeId = node.parentId
    end
  end
  return getTreeParent(treeKey, res, nextNodeId)
end


if treeKey and nodeId then
  return getTreeParent(treeKey, {}, nodeId)
end

return {}

獲取全部子節點數目 get-tree-parent-cnt.lua

local treeKey = KEYS[1]
local nodeId  = ARGV[1]

local function getTreeParentCnt(treeKey, nodeId, res)
  if nodeId == nil or not (type(nodeId) == "number" or type(nodeId) == "string") then
    return res
  end
  local treeNode = redis.call("HGET", treeKey, nodeId)
  local nextNodeId = nil
  if treeNode then
    local node = cjson.decode(treeNode)
    res = res + 1
    if node then
      nextNodeId = node.parentId
    end
  end
  return getTreeParentCnt(treeKey, nextNodeId, res)
end


if treeKey and nodeId then
  return getTreeParentCnt(treeKey, nodeId, 0)
end

return 0

以上代碼由於使用了「尾調用」,因此變得相對比較複雜

總結

此方案相對比較靈活,能支持相對比較大量的數據。

缺點:過於依賴Redis。數據同步會麻煩些,好在操做不是很複雜。

相關文章
相關標籤/搜索