去年做過一個項目,需要每日對上千個Android內存泄漏(OOM)時core dump出的hprof文件進行分析,希望借助海量數據來快速定位內存泄漏的原因。最終的分析結果是一個類森林,因為時隔較遠,只找到下面這個截圖了。
點擊打開折疊的項目,會看到該類的每個屬性,類有多少個實例,占用的大小等等信息,樹的深度可以達到10^2級別。重點是項目需要實時,每個hprof文件解析出來的節點達到5w+,千萬級節點已經由mapreduce進行過一次匯聚計算才出庫,在展示時,依然需要一次實時計算,當點擊項目時,需要快速將該類下所有的子孫節點占用的字節大小累加到該節點,因此對森林要有很高的查詢效率。
好的查詢效率取決於好的存儲機構,眾所周知,多級目錄樹有如下三種存儲方法,這里主要講解這三種方式,並對其做了一些修改。這里使用同一個森林為模型(字母為節點名稱,數字為節點權重)
鄰接列表
這種方式最為開發人員熟知,每一個節點持有父節點的引用。為了更好的處理森林,抽象一個不存在的0節點,森林中所有樹掛在改節點下,將森林轉換為一顆樹來處理。
改方法的SQL如下
1 CREATE TABLE node1 ( 2 id INT AUTO_INCREMENT PRIMARY KEY , 3 name VARCHAR(12) NOT NULL, 4 num INT NOT NULL DEFAULT 0 COMMENT '節點下葉子的數量、節點權重(可認為分類下產品數量)', 5 p_id INT NOT NULL DEFAULT 0 COMMENT '0表示根節點' 6 );
此方法結構簡單,更新也簡單,但是在查詢子孫節點時,效率低下,不能滿足項目需求,因為這種方式過於簡單,這里就不寫該結構的查詢更新刪除SQL了。
進階鄰接列表
該方法僅僅需要在鄰接列表的基礎上,添加path_key(search_key)字段,該字段存儲從根節點到節點的標識路徑,這里依然抽象一個不存在的0節點。
該結構SQL表示如下:
1 CREATE TABLE node2 ( 2 id INT AUTO_INCREMENT PRIMARY KEY , 3 name VARCHAR(12) NOT NULL , 4 num INT NOT NULL DEFAULT 0 COMMENT '節點下葉子的數量、節點權重(可認為分類下產品數量)', 5 p_id INT NOT NULL DEFAULT 0 COMMENT '0表示根節點', 6 search_key VARCHAR(128) DEFAULT '' COMMENT '用來快速搜索子孫的key,存儲根節點到該節點的路徑', 7 level INT DEFAULT 0 COMMENT '層級' 8 );
重點在於search_key字段
插入測試數據
1 INSERT INTO node2(id,name, num, p_id,search_key) VALUES 2 (1,'A',10,0,'0-1'), 3 (2,'B',7,1,'0-1-2'), 4 (3,'C',3,1,'0-1-3'), 5 (4,'D',1,3,'0-1-3-4'), 6 (5,'E',2,3,'0-1-3-5'), 7 (6,'F',2,0,'0-6'), 8 (7,'G',2,6,'0-6-7');
查詢森林中的根節點
1 # 查詢森林的根節點 2 SELECT * FROM node2 WHERE p_id = 0 AND search_key LIKE '0-%' AND level = 0;
查詢節點A的所有子孫節點
1 # SELECT * FROM node2 WHERE search_key LIKE '{A.search_key}%'; 2 SELECT * FROM node2 WHERE search_key LIKE '0-1-%';
更新某個節點的權值,只需要一次select與一次update操作
1 # 例如,更新節點C的權重 2 UPDATE node2,( SELECT sum(num) AS sum FROM node2 WHERE search_key LIKE '0-1-3-%') rt SET num = rt.sum WHERE id=3;
有節點權重累加時,將所有父輩權重再加1,只需要將該節點的search_key以'-' 切分,得到的就是所有父輩的id(0除外)。例如,將節點D的權重+1,這里使用where locate,實際更好是先將search_key split之后使用where in查詢
1 UPDATE node2,(SELECT search_key FROM node2 WHERE id = 4) rt SET num=num+1 WHERE locate(id,rt.search_key);
刪除某個節點,比如刪除B節點
假設刪除節點子孫全部清理
1 DELETE FROM node2 WHERE search_key LIKE '0-1-2%';
假設子節點不清除 ,將子孫節點掛到父輩節點下,則需要更新兒子節點的search_key、p_id、level字段
1 # UPDATE node2, SET p_id = {B.p_id} 2 UPDATE node2 SET p_id = 1 AND search_key = concat('0-1-',id); 3 # 刪除 4 DELETE FROM node2 WHERE id=2;
方式2僅僅添加了一個路徑字段,使得查詢變的簡單,並且更新也容易,在節點深度有限的情況下,個人認為第二種方式是比較優的選擇。
先序樹結構
先序樹即按照先序遍歷的方式,給節點分配左右值,第一次到達該節點時,設置左值,第二次到達該節點,設置右值,每走一步,序號加1。這里以一段php代碼來生成第一張圖片中的森林的先序樹結構
1 <?php 2 /** 3 * Created by PhpStorm. 4 * User: samlv 5 * Date: 2017/2/23 6 * Time: 16:44 7 */ 8 9 $forest = array( 10 array( 11 'name' => 'A', 12 'num' => 10, 13 'childs' => array( 14 array( 15 'name' => 'B', 16 'num' => 7, 17 'childs' => array() 18 ), 19 array( 20 'name' => 'C', 21 'num' => 3, 22 'childs' => array( 23 array( 24 'name' => 'D', 25 'num' => 1, 26 'childs' => array() 27 ), 28 array( 29 'name' => 'E', 30 'num' => 2, 31 'childs' => array() 32 ) 33 ) 34 ) 35 ) 36 ), 37 array( 38 'name' => 'F', 39 'num' => 2, 40 'childs' => array( 41 array( 42 'name' => 'G', 43 'num' => 2, 44 'childs' => array() 45 ) 46 ) 47 ) 48 ); 49 50 function pre_order(& $forset,$level){ 51 static $i = 1; 52 static $tree_id = 1; 53 foreach($forset as & $node){ 54 $node['lft'] = $i ++ ; 55 if(!empty($node['childs'])){ 56 pre_order($node['childs'],$level + 1); 57 } 58 $node['rgt'] = $i ++ ; 59 echo "{$node['lft']} | {$node['name']} | {$node['rgt']} \n"; 60 //echo "insert into node3 (tree_id, name, num, lft, rgt, level) VALUE ($tree_id,'{$node['name']}',{$node['num']},{$node['lft']},{$node['rgt']},$level); \n"; 61 if($node['lft'] === 1){ 62 // 遍歷新的樹 63 $i = 1; 64 $tree_id ++; 65 } 66 } 67 } 68 69 pre_order($forest,1);
運行結果
C:\xampp\php\php.exe D:\www\php-all\sp.php
2 | B | 3
5 | D | 6
7 | E | 8
4 | C | 9
1 | A | 10
2 | G | 3
1 | F | 4
將結果解析成圖片如下
我后續以這段代碼生成了SQL insert代碼。用來存儲該森林的SQL如下
1 CREATE TABLE node3 ( 2 id INT AUTO_INCREMENT PRIMARY KEY , 3 tree_id INT NOT NULL COMMENT '為保證對某一棵的操作不影響森林中的其他書', 4 name VARCHAR(12) NOT NULL , 5 num INT NOT NULL DEFAULT 0 COMMENT '節點下葉子的數量、節點權重(可認為分類下產品數量)', 6 lft INT NOT NULL , 7 rgt INT NOT NULL , 8 level INT DEFAULT 0 9 ); 10 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (1,'B',7,2,3,2); 11 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (1,'D',1,5,6,3); 12 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (1,'E',2,7,8,3); 13 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (1,'C',3,4,9,2); 14 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (1,'A',10,1,10,1); 15 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (2,'G',2,2,3,2); 16 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (2,'F',2,1,4,1);
這里加入了一個tree_id字段,用來保證對一棵樹內的更新操作,不會影響到別的樹,有利於提高效率。
對該結構的操作可以非常復雜,這里說兩種基本的單元操作。
append操作,待加入節點不帶子節點
remove操作,待刪除節點沒有子節點
首先來看append操作,我想在節點C下添加一個M節點,如圖
仔細看可以發現,在已有一個節點下append一個節點M的話,M的左右值應該連續的,按照先序遍歷的順序,只需要將走在其后的節點的左右值分別+2,並且M節點的父節點的右值必然也要+2。下面以一個mysql function來實現append過程
1 DROP FUNCTION IF EXISTS append_node; 2 CREATE FUNCTION append_node(param_name VARCHAR(12), param_num INT, param_p_id INT, param_tree_id INT) 3 returns INT 4 BEGIN 5 DECLARE p_lft INT; 6 DECLARE p_rgt INT; 7 DECLARE p_level INT; 8 DECLARE ret INT; 9 10 SELECT lft,rgt,level INTO p_lft,p_rgt,p_level FROM node3 WHERE tree_id = param_tree_id AND id=param_p_id ; 11 # 比前一個節點左值大的需要加2 12 UPDATE node3 SET lft = lft + 2 WHERE tree_id = param_tree_id AND lft > p_lft; 13 # 按照先序遍歷規則,在一個節點M下添加節點之后,節點M的右值必然也要加2 14 UPDATE node3 SET rgt = rgt + 2 WHERE tree_id = param_tree_id AND rgt >= p_rgt; 15 INSERT INTO node3 (tree_id,name, num, lft, rgt, level) 16 VALUE (param_tree_id,param_name,param_num,p_lft + 1,p_rgt + 1,p_level + 1); 17 SELECT LAST_INSERT_ID() INTO ret; 18 RETURN ret; 19 END; 20 21 # 在節點C下添加節點M,已知節點C的id為4,tree_id為1。 22 select append_node('M', 7, 4, 1);
除了append操作之外,還可以有insert操作,如下圖
其實insert操作可以看做是append 與 delete或者update結合的操作,不一定要一步到位,可以由單元操作來組成。
至於remove操作,則恰好是append操作的反向操作,需要將被刪除節點后面的節點的左右值-2。最后進行delete操作。另外,刪除要分兩種情況,就是子孫丟棄與子孫不丟棄。