最近寫了一個簡單的客戶端康嘉,用來模擬服務(wù)化框架的客戶端調(diào)用,功能如下:
- 隨機調(diào)用服務(wù)
- 打印服務(wù)結(jié)果
- 10%的幾率較少訪問量(假設(shè)1個并發(fā))财松,10%幾率高訪問量(假設(shè)100個并發(fā))菜秦,80%幾率正常訪問量(假設(shè)10個并發(fā))
- 打印各個訪問量情況下的服務(wù)調(diào)用總時間
分別嘗試了Java和Clojure實現(xiàn)鲫售,在實現(xiàn)過程中,兩者的思路完全不同衷模!
面向?qū)ο?面向過程語言思路
邏輯很簡單,基本不涉及面向?qū)ο蟾拍钭叽ィ饕€是面向過程語言的思路晦譬!
如果使用Java來實現(xiàn),那么大致的思路是這樣的:
- 首先需要一個隨機數(shù)生成器互广,基于這個隨機數(shù)生成器來構(gòu)建隨機調(diào)用邏輯
- 隨機調(diào)用服務(wù)就是判斷隨機數(shù)大小敛腌,例如:0~1的隨機數(shù)范圍,大于0.5訪問服務(wù)A,否則訪問服務(wù)B
- 并發(fā)量判定則可以依據(jù)0~10的隨機數(shù)范圍惫皱,小于等于1時并發(fā)為1像樊,大于等于9時并發(fā)為100,否則并發(fā)為10
- 在每個服務(wù)調(diào)用完成后旅敷,統(tǒng)計執(zhí)行時間生棍,然后匯總就可以了
下面是Java實現(xiàn)的代碼:
public class RandomCall {
private static ExecutorService executorService = Executors.newFixedThreadPool(4);
public static void main(String[] args) throws Exception {
while (true) {
int rand = (int)(Math.random() * 10);
if (rand >= 9) {
call(100);
} else if (rand <= 1) {
call(1);
} else {
call(10);
}
Thread.sleep(1000L);
}
}
private static void call(int n) throws Exception {
final AtomicLong total = new AtomicLong(0);
final CountDownLatch latch = new CountDownLatch(n);
for (int i = 0; i < n; i++) {
executorService.execute(new Runnable() {
public void run() {
long start = System.currentTimeMillis();
if ((int)(Math.random() * 2) > 1) {
System.out.println(callServiceA());
} else {
System.out.println(callServiceB());
}
total.addAndGet(System.currentTimeMillis() - start);
latch.countDown();
}
});
}
latch.await();
System.out.println("Invoke " + n + ":" + total + " ms");
}
}
代碼沒什么好說的,對ExecutorService,Executors以及CountDownLatch不熟悉的請自行Google媳谁。
函數(shù)式語言思路
函數(shù)式語言的思路和上面的思路差異很大涂滴!
函數(shù)式語言通過提供大量的函數(shù)來操作少量的數(shù)據(jù)結(jié)構(gòu)來完成邏輯。
所以其大致思路就是構(gòu)建相應(yīng)的數(shù)據(jù)結(jié)構(gòu)晴音,然后使用提供的操作函數(shù)來對其進行操作柔纵。
對于“隨機調(diào)用服務(wù)”這個需求,我們可以把它看成是有一個序列段多,隨機排列著需要調(diào)用的服務(wù)首量!
;定義一個包含了所調(diào)用服務(wù)的Vector
(def fns [call-serviceA call-serviceB])
;依據(jù)上面的Vector構(gòu)建一個隨機排列的服務(wù)序列
(def rand-infinite-fns (repeatedly #(get fns (->> fns count rand int))))
簡單解釋下最后一行代碼:
;;->>是個宏,是為了方便代碼閱讀
(->> fns count rand int)
;;等價于
(int (rand (count fns)))
;;從最里面那個括號往外讀:
;;1 獲取fns這個Vector的長度
;;2 以這個長度為隨機數(shù)范圍(0,length)產(chǎn)生隨機數(shù)
;;3 并取整
#(get fns ...)
;#(...)表示匿名函數(shù)进苍,是個語法糖加缘,等價于
(fn [] (get fns ...))
(get fns ...)
;就是依據(jù)上面的隨機數(shù),從fns這個Vector中獲取元素
(repeatedly ...)
;就是字面意思觉啊,不停的重復(fù)拣宏,結(jié)果構(gòu)建成一個LazySeq
;LazySeq就是在需要時才執(zhí)行
理解了上面的代碼,我們是不是可以以同樣的邏輯杠人,構(gòu)建一個隨機1勋乾、10、100的LazySeq來實現(xiàn)隨機并發(fā)的邏輯呢嗡善?
(defn arr [1 10 100])
(def rand-infinite-arr (repeatedly #(get arr (->> arr count rand int))))
很簡單吧辑莫?那么問題來了:
- 目前這個LazySeq是平均概率的分布著1、10罩引、100各吨,和需求不符合
- 第二行代碼和前面的邏輯一模一樣,是不是可以重構(gòu)成函數(shù)
第一個問題有思路嗎袁铐?可以先想想揭蜒。
其實很簡單
(def arr [1 10 10 10 10 10 10 10 10 100])
(def rand-infinite-arr (repeatedly #(get arr (->> arr count rand int))))
這樣是不是就符合要求了横浑?
再進一步對第二行代碼重構(gòu)為函數(shù):
(defn rand-infinite [vec]
(repeatedly #(get vec (->> vec count rand int))))
(def fns [call-serviceA call-serviceB])
(def arr [1 10 10 10 10 10 10 10 10 100])
(def rand-infinite-fns (rand-infinite fns))
(def rand-infinite-arr (rand-infinite arr))
- rand-infinite-fns中是隨機調(diào)用的函數(shù)
- rand-infinite-arr中是隨機的并發(fā)數(shù)
現(xiàn)在我們只要從rand-infinite-arr中依次取出元素,然后根據(jù)元素的值來構(gòu)建相同數(shù)量的線程來進行調(diào)用就可以了屉更!由于是無限序列徙融,所以間接實現(xiàn)了死循環(huán)!
舉個例子:
;;假設(shè)現(xiàn)在rand-infinite-fns中元素如下
[call-serviceA call-serviceA call-serviceB call-serviceA ...]
;;rand-infinite-arr中元素如下
[10 1 10 100 10 10 1 ...]
;;rand-infinite-arr的第一個元素是10,
;;則從rand-infinite-fns中取10個元素瑰谜,構(gòu)建10個線程去調(diào)用
;;rand-infinite-arr的第二個元素是1,
;;則從rand-infinite-fns的第11個函數(shù)開始欺冀,去一個函數(shù)去調(diào)用
;;以此類推
第一印象是遞歸,Clojure代碼實現(xiàn)如下:
(loop [rand-fns (rand-infinite fns)
nums (rand-infinite arr)]
...
(recur (drop (first nums) rand-fns)
(drop 1 nums)))
最后就是構(gòu)建線程進行函數(shù)調(diào)用
(time (println (pmap #(%) (take (first nums) rand-fns)))
;;pmap就是將#(% obj)這個函數(shù)依次應(yīng)用到后面的序列上,并且是并發(fā)的
;;time函數(shù)打印出執(zhí)行所需要的時間
;;(take (first nums) rand-fns)就是依據(jù)nums元素的大小似舵,獲取相應(yīng)數(shù)量的rand-fns的元素
;;rand-fns中的元素是函數(shù)脚猾,直接放在括號里的第一個元素就可以執(zhí)行了,這里是替換了那個%
實際上可以更進一步砚哗,上面的流程龙助,相當(dāng)于遍歷下面這個鏈表:
[[call-serviceA] [call-serviceA call-serviceB ...] [call-serviceB] [call-serviceB call-serviceA ...] ...]
所以只需要構(gòu)建類似上面的鏈表結(jié)構(gòu)就可以了,Clojure里很簡單:
(let [rand-fns (rand-infinite fns)
rand-arr (rand-infinite arr)
group-rand-fns (map #(take % rand-fns) rand-arr)]
...))
;;group-rand-fns就是我們需要的鏈表結(jié)構(gòu)
最后只要遍歷這個鏈表就可以了:
(defn invoke [fns]
(Thread/sleep 1000)
(time (println (pmap #(%) fns))))
(defn -main [& args]
(let [rand-fns (rand-infinite fns)
rand-arr (rand-infinite arr)
group-rand-fns (map #(take % rand-fns) rand-arr)]
(doall (map invoke group-rand-fns))))
;;doall表示立即執(zhí)行蛛芥,因為map出來的鏈表是lazySeq,這里的map相當(dāng)于外層循環(huán)提鸟,對每個內(nèi)部鏈表應(yīng)用invoke函數(shù)
;;invoke內(nèi)部是內(nèi)層循環(huán),每隔1秒就并發(fā)調(diào)用鏈表中的函數(shù)
完整代碼如下:
(defn rand-infinite [vec]
(repeatedly #(get vec (->> vec count rand int))))
(def fns [call-serviceA call-serviceB])
(def arr [1 10 10 10 10 10 10 10 10 100])
(defn invoke [fns]
(Thread/sleep 1000)
(time (println (pmap #(%) fns))))
(defn -main [& args]
(let [rand-fns (rand-infinite fns)
rand-arr (rand-infinite arr)
group-rand-fns (map #(take % rand-fns) rand-arr)]
(doall (map invoke group-rand-fns))))
Java8實現(xiàn)
Java8提供了lambda表達式等功能仅淑,支持函數(shù)式編程称勋,下面使用Java8實現(xiàn),直接貼代碼:
public class RandomCall {
public static void main(String[] args) throws Exception {
Function<Supplier<String>,Long> func = sup -> {
long start = System.currentTimeMillis();
sup.get();
return System.currentTimeMillis() - start;
};
Supplier<String> serviceASup = () -> callServiceA();
Supplier<String> serviceBSup = () -> callServiceB();
List<Supplier<String>> fns = Arrays.asList(serviceASup,serviceBSup);
List<Integer> arr = Arrays.asList(1,10,10,10,10,10,10,10,10,100);
Stream.generate(() -> (int) (Math.random() * 10)).map(arr::get)
.forEach(n -> {
Thread.sleep(1000L);
System.out.println(Stream.generate(() -> (int) (Math.random() * 2)).map(fns::get).limit(n)
.parallel().mapToLong(func::apply).sum());
});
}
}
總結(jié)
- 最終代碼涯竟,Clojure明顯少于Java赡鲜、略少于Java8。在代碼表現(xiàn)力上Clojure > Java8 > Java
- 函數(shù)式思路和過程式思路差異很大
- 在編寫Clojure代碼時庐船,明顯是偏腦力的勞動银酬。而在編寫Java的時候,明顯是偏體力的勞動
- 編寫Clojure代碼筐钟,如果不多思考揩瞪,則寫出來的代碼將比Java要難讀得多
- Java8代碼比Clojure代碼可讀性上感覺更差(可能自己對Java8的函數(shù)式思路還不太了解)