20190404_ARTS_W00

2019第一篇arts, 果然學(xué)習(xí)都是反人性的

**Algorithm**
每周至少做一個(gè)leetcode算法題

**Review**
閱讀并點(diǎn)評(píng)至少一篇英文技術(shù)文章
(英文論文文獻(xiàn))

**Tip**
至少學(xué)習(xí)一個(gè)技術(shù)技巧

**Share**
分享一篇有觀點(diǎn)和思考的技術(shù)文章

Algorithm

看了下數(shù)據(jù)結(jié)構(gòu)與算法之美的A*搜索算法:是一種基于Dijkstra算法優(yōu)化的次優(yōu)解算法,意在減少搜索時(shí)間,不保證是最優(yōu)解

自己按照思路簡(jiǎn)單畫(huà)了個(gè)導(dǎo)圖如下:


49-A星搜索算法實(shí)現(xiàn)游戲中的尋路功能.png

大概實(shí)現(xiàn)了下,還好之前有Dijkstra算法的基礎(chǔ),不然一下午根本搞不定:

  1. 基本數(shù)據(jù)結(jié)構(gòu)
//邊
public class Edge {

    int start;
    int end;
    int weight;
    public Edge(int start, int end,int weight){
        this.start = start;
        this.end = end;
        this.weight = weight;
    }
}

//節(jié)點(diǎn)坐標(biāo)信息類
public class Vertx {

    int nodeValue; //圖中節(jié)點(diǎn)值
    int distance;  //節(jié)點(diǎn)距離起點(diǎn)的最小值

    int x,y; //節(jié)點(diǎn)在xy坐標(biāo)系中的坐標(biāo)

    int f; //f=distance+節(jié)點(diǎn)距離終點(diǎn)的曼哈頓距離(|x1-x2|+|y1-y2|)

    public Vertx(int nodeValue, int x, int y){
        this.nodeValue = nodeValue;
        this.x = x;
        this.y = y;
    }

}

//相應(yīng)的圖結(jié)構(gòu),包括鄰接表,各個(gè)頂點(diǎn)的坐標(biāo)數(shù)組等
public class AStarGraph {

    int v;
    LinkedList<Edge>[] adj; //鄰接表
    Vertx[] vertxes;
    public AStarGraph(int v){
        this.v = v;
        adj = new LinkedList[v];
        for(int i=0;i<v;i++){
            adj[i] = new LinkedList<>();
        }

        vertxes = new Vertx[v];
    }

    //s->t,權(quán)重為weight
    public void addEdge(int s, int t, int weight){
        adj[s].add(new Edge(s,t,weight));
    }

    //添加頂點(diǎn)的坐標(biāo)
    public void addVertx(int id, int x, int y){
        vertxes[id] = new Vertx(id,x,y);
    }
}
  1. 優(yōu)先級(jí)隊(duì)列: 根據(jù)Vertex.f構(gòu)建小頂堆
package com.zxb.structurealgo.AStarAlgo49;

/**
 * @ClassName AStarPriorityQueue
 * @Description 優(yōu)先級(jí)隊(duì)列,根據(jù)Vertex.f構(gòu)建小頂堆
 * @Author xuery
 * @Date 2019/3/21 20:43
 * @Version 1.0
 */
public class AStarPriorityQueue {

    Vertx[] nodes; //堆數(shù)組
    int count;     //當(dāng)前隊(duì)列實(shí)際存在多少個(gè)值

    public AStarPriorityQueue(int v){
        /**
         * 多加1,從下標(biāo)為1的位置開(kāi)始存數(shù)據(jù),方便計(jì)算堆的節(jié)點(diǎn)的左右子樹(shù)
         * 下標(biāo)為i的左右子樹(shù)為:2*i,2*i+1,父節(jié)點(diǎn)為i/2
         */
        nodes = new Vertx[v+1];
        count = 0; //專欄里面應(yīng)該是寫(xiě)錯(cuò)了
    }

    public Vertx poll(){
        if(count == 0){
            return null;
        }
        /**
         * 下標(biāo)為1的元素就是要出隊(duì)列的元素,
         * 利用小頂堆性質(zhì):將下標(biāo)為1的元素與最后一個(gè)元素交換,最后一個(gè)元素出隊(duì)列并將count減1,之后從第一個(gè)元素開(kāi)始向下堆化
         * 假設(shè)堆化時(shí)有n個(gè)元素,下標(biāo)1...n,則只需要堆化到n/2即可,因?yàn)楹罄m(xù)的都是葉子節(jié)點(diǎn),舉個(gè)例子就知道了
         */
        Vertx pollVertx = nodes[1];
        swap(nodes, 1, count);
        count--;
        //從1-count/2開(kāi)始堆化
        int i=1;
        while(count > 1 && i <= count/2){
            //如果i比它的左右葉子節(jié)點(diǎn)2*i,2*i+1大則繼續(xù)調(diào)整
            int f = nodes[i].f;
            int swapIndex = -1;
            if(f > nodes[2*i].f){
                f = nodes[2*i].f;
                swapIndex = 2*i;
            }
            if(2*i+1 <= count && f > nodes[2*i+1].f){
                swapIndex = 2*i+1;
            }
            if(swapIndex != -1){
                swap(nodes,i,swapIndex);
                i = swapIndex;
            } else {
                break;
            }
        }
        return pollVertx;
    }

    private void swap(Vertx[] nodes, int i, int j){
        Vertx tmp = nodes[i];
        nodes[i] = nodes[j];
        nodes[j] = tmp;
    }

    public void add(Vertx vertx){
        /**
         * 小頂堆插入數(shù)據(jù),count加1,先將其插入末尾
         * 然后從末尾開(kāi)始往上按照小頂堆堆化
         */
        count++;
        nodes[count] = vertx;
        int i = count; //i的父節(jié)點(diǎn)為i/2
        while(i/2 >=1){
            if(nodes[i].f < nodes[i/2].f){
                swap(nodes,i,i/2);
                i = i/2;
            } else {
                break;
            }
        }
    }

    public void update(Vertx vertx){
        /**
         * 先根據(jù)vertex.nodeValue找到其在堆數(shù)組中的位置并更新它的f
         * 之后從該點(diǎn)開(kāi)始,在這里之所以更新它是因?yàn)檎业搅烁〉膄,
         * 所以從該點(diǎn)開(kāi)始向上按小頂堆堆化即可,畫(huà)圖舉例就知道了
         *
         * 這個(gè)查找復(fù)雜度為O(n),有點(diǎn)高,這里其實(shí)可以通過(guò)構(gòu)造map來(lái)實(shí)現(xiàn)快速查找<nodeValue,在數(shù)組中的位置>
         */
        int updateIndex = -1;
        for(int i=1;i<=count;i++){
            if(nodes[i].nodeValue == vertx.nodeValue){
                updateIndex = i;
                break;
            }
        }
        if(updateIndex != -1){
            nodes[updateIndex].f = vertx.f;
        }
        int i = updateIndex;//從updateIndex開(kāi)始向上堆化
        while(i/2 >=1){
            if(nodes[i].f < nodes[i/2].f){
                swap(nodes,i,i/2);
                i = i/2;
            } else {
                break;
            }
        }

    }

    public boolean isEmpty(){
        return count == 0;
    }

    public void clear(){
        count = 0;
    }
}

  1. 算法具體實(shí)現(xiàn)類
package com.zxb.structurealgo.AStarAlgo49;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * @ClassName AStarAlgo
 * @Description A*搜索算法,快速找出一條次優(yōu)路徑解,參考49講
 *
 * 與Dijkstra算法的區(qū)別:
 * 1. 節(jié)點(diǎn)引入(x,y)屬性,出隊(duì)列順序要綜合頂點(diǎn)離起點(diǎn)的距離+頂點(diǎn)離終點(diǎn)的距離=f
 * 2. 距離等于1中兩者之和,新增屬性f=g+h;
 * 3. 遍歷到終點(diǎn)就結(jié)束,所以不保證最優(yōu)解
 * @Author xuery
 * @Date 2019/4/9 15:27
 * @Version 1.0
 */
public class AStarAlgo {

    public static void main(String[] args) {

        AStarGraph graph = AStarGraphGenUtil.aStarGraphGen2();

        AStarAlgo aStarAlgo = new AStarAlgo();
        List<Integer> result = aStarAlgo.aStarAlgo(graph,2,5);
        System.out.println(result);
    }

    public List<Integer> aStarAlgo(AStarGraph graph, int start, int end){

        //優(yōu)先級(jí)隊(duì)列,用于每次取距離最短的哪個(gè)節(jié)點(diǎn),這里java自帶的優(yōu)先級(jí)隊(duì)列沒(méi)有提供update接口,只能自己實(shí)現(xiàn)一個(gè)了
        int v = graph.v;
        LinkedList<Edge>[] adj = graph.adj;
        Vertx[] vertxes = graph.vertxes;
        AStarPriorityQueue queue = new AStarPriorityQueue(v);

        //preNode數(shù)組,這里節(jié)點(diǎn)取值是0-v-1, 所以直接用數(shù)組就可以了;如果不是的話就要用map了
        int[] preNodeArr = new int[v];
        boolean[] visited = new boolean[v];
        int[] currMinDisArr = new int[v];

        //start作為第一個(gè)點(diǎn),然后根據(jù)當(dāng)前節(jié)點(diǎn)廣度優(yōu)先遍歷其后繼節(jié)點(diǎn)
        Vertx vertx0 = vertxes[start];
        queue.add(vertx0);
        preNodeArr[start] = -1;
        visited[start] = true;
        while(!queue.isEmpty()){
            Vertx currMinFVertx = queue.poll();
            //看它的后繼節(jié)點(diǎn)
            LinkedList<Edge> nextEdges = adj[currMinFVertx.nodeValue];
            for(int i=0;i<nextEdges.size();i++){
                Edge nextEdge = nextEdges.get(i);
                if(!visited[nextEdge.end]){
                    //該節(jié)點(diǎn)之前沒(méi)有進(jìn)入過(guò)隊(duì)列,說(shuō)明是第一次遍歷到它
                    Vertx vertx = vertxes[nextEdge.end];
                    vertx.distance = currMinFVertx.distance + nextEdge.weight;
                    vertx.f = vertx.distance + Math.abs(vertx.x-vertxes[end].x)+Math.abs(vertx.y-vertxes[end].y);
                    queue.add(vertx);

                    preNodeArr[nextEdge.end] = currMinFVertx.nodeValue;
                    visited[nextEdge.end] = true;
                    currMinDisArr[nextEdge.end] = vertx.distance;
                } else {
                    //非第一次遍歷到它,需要比較下currMinFVertx.distance+nextEdge.weight與nextEdge.end對(duì)應(yīng)的distance
                    //這里不能每次都去隊(duì)列中查對(duì)應(yīng)的數(shù)據(jù)比較,太慢了,構(gòu)建一個(gè)輔助數(shù)組
                    int currDis = currMinFVertx.distance+nextEdge.weight;
                    if(currMinDisArr[nextEdge.end] > currDis){
                        //需要更新一波
                        Vertx vertx = vertxes[nextEdge.end];
                        vertx.distance = currDis;
                        vertx.f = vertx.distance + Math.abs(vertx.x-vertxes[end].x)+Math.abs(vertx.y-vertxes[end].y);
                        queue.update(vertx);

                        preNodeArr[nextEdge.end] = currMinFVertx.nodeValue;
                        currMinDisArr[nextEdge.end] = currDis;
                    }
                }

                //截止條件:遍歷到end節(jié)點(diǎn)就結(jié)束
                if(nextEdge.end == end){
                    queue.clear();
                    break;
                }

            }
        }

        //根據(jù)preNodeArr回溯
        List<Integer> resultList = new ArrayList<>();
        int index = end;
        while(index != -1){
            resultList.add(index);
            index = preNodeArr[index];
        }
        return resultList;
    }
}

  1. 簡(jiǎn)單的圖產(chǎn)生測(cè)試用例
package com.zxb.structurealgo.AStarAlgo49;


/**
 * @ClassName AStarGraphGenUtil
 * @Description 拓?fù)鋱D生成類
 * @Author xuery
 * @Date 2019/3/21 10:56
 * @Version 1.0
 */
public class AStarGraphGenUtil {


    /**
     *
     * @return
     */
    public static AStarGraph aStarGraphGen1(){

        AStarGraph graph = new AStarGraph(6);

        graph.addEdge(0,1,1);
        graph.addEdge(0,2,3);
        graph.addEdge(1,2,1);
        graph.addEdge(1,3,2);
        graph.addEdge(2,3,1);
        graph.addEdge(3,4,2);
        graph.addEdge(3,5,5);
        graph.addEdge(4,5,2);

        graph.addVertx(0,0,0);
        graph.addVertx(1,0,0);
        graph.addVertx(2,0,0);
        graph.addVertx(3,0,0);
        graph.addVertx(4,0,0);
        graph.addVertx(5,0,0);

        return graph;
    }

    public static AStarGraph aStarGraphGen2(){

        AStarGraph graph = new AStarGraph(6);

        graph.addEdge(2,0,4);
        graph.addEdge(2,1,2);
        graph.addEdge(1,0,1);
        graph.addEdge(0,5,2);
        graph.addEdge(2,3,4);
        graph.addEdge(2,4,5);
        graph.addEdge(3,5,6);
        graph.addEdge(4,5,7);

        graph.addVertx(0,0,0);
        graph.addVertx(1,1,1);
        graph.addVertx(2,2,2);
        graph.addVertx(3,3,3);
        graph.addVertx(4,4,4);
        graph.addVertx(5,5,5);

        return graph;
    }
}

Review

Raft算法
今天大概看了下Raft算法,理解了一些基礎(chǔ)的東西,做下總結(jié),希望自己之后可以更深入的理解

1. Raft算法出現(xiàn)的目的

因?yàn)镻AXOS算法過(guò)于晦澀,常人難以理解,且落地困難。想要找到一種這樣的一致性算法:可實(shí)現(xiàn)性強(qiáng),減少開(kāi)發(fā)的工作量;必須安全且大部分情況下是可用的;大部分操作必須是高效的;可理解性強(qiáng),Raft一致性算法應(yīng)運(yùn)而生。

2. Raft算法的核心
領(lǐng)導(dǎo)選舉

集群中的每個(gè)節(jié)點(diǎn)有三種狀態(tài):Follower(跟隨者),Candidate(參與選舉者),Leader(領(lǐng)導(dǎo)者);
所有節(jié)點(diǎn)的初始狀態(tài)為Follower, 每個(gè)節(jié)點(diǎn)都有一個(gè)隨機(jī)時(shí)間,超過(guò)隨機(jī)時(shí)間時(shí),如果相應(yīng)的Follower沒(méi)有收到其他節(jié)點(diǎn)的選舉信息,則會(huì)變成Candidate(先會(huì)給自己投一票), 再向其余的節(jié)點(diǎn)發(fā)送參與選舉領(lǐng)導(dǎo)者請(qǐng)求并接收其余節(jié)點(diǎn)的投票,如果投票數(shù)超過(guò)一半則節(jié)點(diǎn)由Candidate變成Leader, 選舉完畢,保證每次最多只會(huì)選出一個(gè)領(lǐng)導(dǎo)者。

有兩個(gè)timeout設(shè)置來(lái)保證領(lǐng)導(dǎo)選舉的順利進(jìn)行:一個(gè)是election timeout,它就是上面所說(shuō)的每個(gè)Follower有一個(gè)隨機(jī)的election timeout, 超過(guò)這個(gè)時(shí)間則節(jié)點(diǎn)由Follower變成Candidate狀態(tài),一般設(shè)置為150ms-300ms, 這里每個(gè)節(jié)點(diǎn)的election timeout可能都是不一樣的,可以保證盡快的選出領(lǐng)導(dǎo)(可以想想如果都是一個(gè)確定的值,每個(gè)節(jié)點(diǎn)都同時(shí)由Follower變成Candidate,那樣選出領(lǐng)導(dǎo)的速度肯定會(huì)變慢的);一個(gè)是heartbeats timeout, 一旦確定領(lǐng)導(dǎo)者,領(lǐng)導(dǎo)者會(huì)定時(shí)向Follower發(fā)送心跳,來(lái)保證領(lǐng)導(dǎo)者正常的情況下,其他Follower不會(huì)變成Candidate

當(dāng)領(lǐng)導(dǎo)者掛了之后,重新開(kāi)始選舉流程:由于heartbeats timeout了,所以會(huì)有節(jié)點(diǎn)從Follower變成Candidate, 然后跟之前一樣選舉出領(lǐng)導(dǎo)者即可

當(dāng)多個(gè)Follower同時(shí)變成Candidate,且投票結(jié)果Candidate都沒(méi)有超過(guò)半數(shù)以上,則等待下一輪超時(shí)重新選舉,直到選舉出leader.(注:每次選舉的時(shí)候term會(huì)加1)

日志復(fù)制

選好領(lǐng)導(dǎo)者之后開(kāi)始日志復(fù)制,就是領(lǐng)導(dǎo)者將更新通知到Follower, 要保證所有節(jié)點(diǎn)的更新數(shù)據(jù)的一致性,主要流程如下:當(dāng)客戶端發(fā)起一個(gè)更新請(qǐng)求會(huì)先給到leader,leader會(huì)將更新記錄追加到它的日志末尾,然后leader會(huì)將更新發(fā)給其他的Follower, 同理Follower也會(huì)記錄日志到其對(duì)應(yīng)的日志末尾并ack一個(gè)信號(hào)給leader, 當(dāng)leader收到超過(guò)半數(shù)的ack之后,會(huì)commit更新并通知Follower也commit這個(gè)更新。(有點(diǎn)類似于2PC,先prepare再commit)

當(dāng)發(fā)生腦裂時(shí),raft算法還可能可以保證一致性,這是由于要半數(shù)以上ack所決定的,即使不返回?cái)?shù)據(jù),也不會(huì)返回錯(cuò)誤數(shù)據(jù)。

參考:http://thesecretlivesofdata.com/raft/

Tip

項(xiàng)目中一個(gè)接口列表慢查詢優(yōu)化實(shí)踐

背景:項(xiàng)目開(kāi)發(fā)中提供給外部系統(tǒng)一個(gè)列表查詢接口,在測(cè)試環(huán)境驗(yàn)證響應(yīng)時(shí)間1s左右(現(xiàn)在看來(lái)也挺慢的_),感覺(jué)問(wèn)題不大,上了生產(chǎn)之后調(diào)用方反饋接口響應(yīng)時(shí)間太長(zhǎng)了,有些時(shí)候甚至超過(guò)5s,顯然這是不能忍受的,已經(jīng)超過(guò)http的超時(shí)時(shí)間了,通過(guò)響應(yīng)參數(shù)發(fā)現(xiàn),這個(gè)列表查詢每次能返回100多條數(shù)據(jù),之前完全沒(méi)預(yù)料到,于是乎開(kāi)始優(yōu)化

代碼大概是下面這樣的:

List<RouteRes> result = new ArrayList<>();
//根據(jù)條件查詢出符合條件的task列表
List<Task> tasks = queryTaskService.getTaskByCondition(map);
//將task列表按照routeId分組
Map<String> taskMap = tasks.stream.collect(Collectors.groupingBy(event -> event.getRouteId));
taskMap.forEach((routeId, tasks) -> {
    //根據(jù)routeId查詢對(duì)應(yīng)的Route信息,這里面會(huì)查緩存,帶Biz后綴的類都會(huì)加事務(wù)
   long t1 = System.System.currentTimeMillis();
    Route route = routeServiceBiz.getRouteByRouteId(routeId);
    long t2 = System.System.currentTimeMillis();
    //組裝RouteRes
    RouteRes routeRes = routeServiceBiz.buildRouteRes(route);
   result.add(routeRes);
});
return result;
  1. 首先之前在taskMap.forEach((routeId, tasks)循環(huán)里面,調(diào)了一個(gè)不應(yīng)該調(diào)的rpc接口(復(fù)用原來(lái)的接口導(dǎo)致的嗚嗚嗚),代碼里沒(méi)有列出來(lái),每次rpc大概20-30ms, 100多次循環(huán)就是2-3s,去除掉基本就節(jié)約一半時(shí)間了
  2. 還是需要2s多,不正常,再來(lái);分析來(lái)分析去只有循環(huán)里面查緩存比較耗時(shí),按經(jīng)驗(yàn)緩存查詢一次最多5ms,100多次也就500ms,跟2s相差還是很多??;那就把在routeServiceBiz.getRouteByRouteId里面把查緩存的時(shí)間全部打印出來(lái),結(jié)果發(fā)現(xiàn)一次緩存大概2-3ms,100多次就是200-300ms,那時(shí)間究竟花在哪里了呢?
  3. 百思不得奇解,最后加了上面的t1,t2,計(jì)算出所有循環(huán)t2-t1的累計(jì)值發(fā)現(xiàn)比查緩存的累積值要大很多;再想想,外層只是通過(guò)方法調(diào)用了一下而已??;不對(duì)每次方法調(diào)用都會(huì)開(kāi)啟一次事務(wù),基本可以確定了循環(huán)100多次就有100多次事務(wù),主要時(shí)間花在了事務(wù)上
  4. 解決方法:去除掉事務(wù),查詢根本不需要事務(wù),之后重新查詢發(fā)現(xiàn)200ms左右,優(yōu)化完畢

總結(jié):循環(huán)中最好不要循環(huán)加事務(wù),當(dāng)循環(huán)次數(shù)多的時(shí)候,事務(wù)占用的時(shí)間非常多;造成這樣的最終原因還是因?yàn)椴捎玫目蚣?,Biz結(jié)尾的service所有方法都會(huì)默認(rèn)加上事務(wù),所以對(duì)于項(xiàng)目中的查詢方法不要寫(xiě)在Biz結(jié)尾的service層,應(yīng)該重新建一個(gè)不帶Biz結(jié)尾的service,將相關(guān)的查詢方法寫(xiě)到里面去

Share

其實(shí)買(mǎi)極客時(shí)間的專欄,如果你能認(rèn)真的去看完并做筆記和思考,收獲還是蠻多的,當(dāng)然專欄只是把主要的知識(shí)點(diǎn)羅列出來(lái),如果真的想要深入理解的話,可以借助專欄的思路去看相關(guān)的書(shū)籍和英文官方文檔,一步一步拓展,那你習(xí)得的知識(shí)將會(huì)比大部分人都要深。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容