1. 最短路徑測試代碼
下面主要是對Spark圖計算框架GraphX中的單源點最短路徑的源碼進行解析。
test("shortPaths") {
// 測試的真實結果,後面用於對比
val shortestPaths = Set(
(1, Map(1 -> 0, 4 -> 2)), (2, Map(1 -> 1, 4 -> 2)), (3, Map(1 -> 2, 4 -> 1)),
(4, Map(1 -> 2, 4 -> 0)), (5, Map(1 -> 1, 4 -> 1)), (6, Map(1 -> 3, 4 -> 1)))
// 構造有向圖的邊序列
val edgeSeq = Seq((1, 2), (1, 5), (2, 3), (2, 5), (3, 4), (4, 5), (4, 6)).flatMap {
case e => Seq(e, e.swap)
}
// 構造有向圖
val edges = sc.parallelize(edgeSeq).map { case (v1, v2) => (v1.toLong, v2.toLong) }
val graph = Graph.fromEdgeTuples(edges, 1)
// 要求最短路徑的點集合
val landmarks = Seq(1, 4).map(_.toLong)
// 計算最短路徑
val results = ShortestPaths.run(graph, landmarks).vertices.collect.map {
case (v, spMap) => (v, spMap.mapValues(i => i))
}
// 與真實結果對比
assert(results.toSet === shortestPaths)
}
2. Graphx底層實現代碼
package org.apache.spark.graphx.lib
import org.apache.spark.graphx._
import scala.reflect.ClassTag
object ShortestPaths {
// 定義一個Map[VertexId,Int]類型的Map函數,別名爲SPMap,函數的屬性Key爲VertexId類型,
// 其實也就是scala中的Long類型,它在圖中的別名是VertexId,還有Int類型的路徑的長度。
type SPMap = Map[VertexId, Int]
// 初始化圖的屬性信息
private def makeMap(x: (VertexId, Int)*) = Map(x: _*)
// 主要用於將自身的屬性值(即源頂點屬性值)中路徑的長度加1(這裏說明該最短路徑模型只能應用與非帶權圖,即權值都相等的圖),然後和目標定點的屬性值比較
private def incrementMap(spmap: SPMap): SPMap = spmap.map { case (v, d) => v -> (d + 1) }
// 比較源頂點屬性和發送信息過來頂點的屬性取最小值。
private def addMaps(spmap1: SPMap, spmap2: SPMap): SPMap =
// 先將兩個集合spmap1和spma2的頂點整合要一起,這裏用了一個++來處理
// 再形成一個新的k->v的map
// 其中v是兩個消息中值最小的一個
(spmap1.keySet ++ spmap2.keySet).map {
k => k -> math.min(spmap1.getOrElse(k, Int.MaxValue), spmap2.getOrElse(k, Int.MaxValue))
}.toMap
// 計算給定了起始和終點序列的最短路徑
// ED是邊的屬性值,計算過程中不會被使用
// graph是要計算最短路徑的圖
// landmarks是要求最短路徑頂點id的集合,最短路徑會計算每一個landmark
// 返回的是一個圖,每個頂點的屬性就是landmark點間的最短路徑
def run[VD, ED: ClassTag](graph: Graph[VD, ED], landmarks: Seq[VertexId]): Graph[SPMap, ED] = {
val spGraph = graph.mapVertices { (vid, attr) =>
// 如果landmark只有一個點1
// 將landmarks中的頂點初始化爲Map(1-> 0),即自身到自身的距離爲0,其餘的頂點屬性初始化爲Map()。
if (landmarks.contains(vid)) makeMap(vid -> 0) else makeMap()
}
// 定義一個initMessage它的值爲Map()
// 作用是在Pregel第一次運行的時候,所有圖中的頂點都會接收到initMessage。
val initialMessage = makeMap()
// 用戶定義的頂點程序運行在每一個頂點中,負責接收進來的信息,和計算新的頂點值。
// 在第一次迭代的時候,所有的頂點程序將會被默認的defaultMessage調用,在次輪迭代中,頂點程序只有接收到message纔會被調用。
def vertexProgram(id: VertexId, attr: SPMap, msg: SPMap): SPMap = {
addMaps(attr, msg)
}
// 該函數應用於鄰居頂點在當前迭代中接收message
// 一旦收到通知,相對於發送該消息的點,就是目的節點,相對於收到消息的點就是源節點
// 這個地方從源節點考慮
def sendMessage(edge: EdgeTriplet[SPMap, _]): Iterator[(VertexId, SPMap)] = {
// 對所有目的節點值加1
val newAttr = incrementMap(edge.dstAttr)
// 求得最短路徑,將源節點的值發送給所有所有的源節點,其實這裏源節點就是相鄰點的意思,換成目的節點應該也是可以的
if (edge.srcAttr != addMaps(newAttr, edge.srcAttr)) Iterator((edge.srcId, newAttr))
else Iterator.empty
}
// 調用pregel函數
// 第一個參數列表包含配置參數初始消息、最大迭代數、發送消息的邊的方向(默認是沿邊方向出)
// 第二個參數列表包含用戶 自定義的函數用來接收消息(vprog)、計算消息(sendMsg)、合併消息(mergeMsg)
Pregel(spGraph, initialMessage)(vertexProgram, sendMessage, addMaps)
}
}
GraphX最短路徑求解中使用了Pregel模型,這是一個非常高效的圖計算模型。但目前最短路徑有如下限制:
- 只能用於非帶權圖(權值相等);
- 利用的算法是迪傑斯特拉求解最短路徑。
【完】