20190404_ARTS_W00

2019第一篇arts, 果然学习都是反人性的

**Algorithm**
每周至少做一个leetcode算法题

**Review**
阅读并点评至少一篇英文技术文章
(英文论文文献)

**Tip**
至少学习一个技术技巧

**Share**
分享一篇有观点和思考的技术文章

Algorithm

看了下数据结构与算法之美的A*搜索算法:是一种基于Dijkstra算法优化的次优解算法,意在减少搜索时间,不保证是最优解

自己按照思路简单画了个导图如下:


49-A星搜索算法实现游戏中的寻路功能.png

大概实现了下,还好之前有Dijkstra算法的基础,不然一下午根本搞不定:

  1. 基本数据结构
//边
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;
    }
}

//节点坐标信息类
public class Vertx {

    int nodeValue; //图中节点值
    int distance;  //节点距离起点的最小值

    int x,y; //节点在xy坐标系中的坐标

    int f; //f=distance+节点距离终点的曼哈顿距离(|x1-x2|+|y1-y2|)

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

}

//相应的图结构,包括邻接表,各个顶点的坐标数组等
public class AStarGraph {

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

        vertxes = new Vertx[v];
    }

    //s->t,权重为weight
    public void addEdge(int s, int t, int weight){
        adj[s].add(new Edge(s,t,weight));
    }

    //添加顶点的坐标
    public void addVertx(int id, int x, int y){
        vertxes[id] = new Vertx(id,x,y);
    }
}
  1. 优先级队列: 根据Vertex.f构建小顶堆
package com.zxb.structurealgo.AStarAlgo49;

/**
 * @ClassName AStarPriorityQueue
 * @Description 优先级队列,根据Vertex.f构建小顶堆
 * @Author xuery
 * @Date 2019/3/21 20:43
 * @Version 1.0
 */
public class AStarPriorityQueue {

    Vertx[] nodes; //堆数组
    int count;     //当前队列实际存在多少个值

    public AStarPriorityQueue(int v){
        /**
         * 多加1,从下标为1的位置开始存数据,方便计算堆的节点的左右子树
         * 下标为i的左右子树为:2*i,2*i+1,父节点为i/2
         */
        nodes = new Vertx[v+1];
        count = 0; //专栏里面应该是写错了
    }

    public Vertx poll(){
        if(count == 0){
            return null;
        }
        /**
         * 下标为1的元素就是要出队列的元素,
         * 利用小顶堆性质:将下标为1的元素与最后一个元素交换,最后一个元素出队列并将count减1,之后从第一个元素开始向下堆化
         * 假设堆化时有n个元素,下标1...n,则只需要堆化到n/2即可,因为后续的都是叶子节点,举个例子就知道了
         */
        Vertx pollVertx = nodes[1];
        swap(nodes, 1, count);
        count--;
        //从1-count/2开始堆化
        int i=1;
        while(count > 1 && i <= count/2){
            //如果i比它的左右叶子节点2*i,2*i+1大则继续调整
            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){
        /**
         * 小顶堆插入数据,count加1,先将其插入末尾
         * 然后从末尾开始往上按照小顶堆堆化
         */
        count++;
        nodes[count] = vertx;
        int i = count; //i的父节点为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){
        /**
         * 先根据vertex.nodeValue找到其在堆数组中的位置并更新它的f
         * 之后从该点开始,在这里之所以更新它是因为找到了更小的f,
         * 所以从该点开始向上按小顶堆堆化即可,画图举例就知道了
         *
         * 这个查找复杂度为O(n),有点高,这里其实可以通过构造map来实现快速查找
         */
        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开始向上堆化
        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. 算法具体实现类
package com.zxb.structurealgo.AStarAlgo49;

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

/**
 * @ClassName AStarAlgo
 * @Description A*搜索算法,快速找出一条次优路径解,参考49讲
 *
 * 与Dijkstra算法的区别:
 * 1. 节点引入(x,y)属性,出队列顺序要综合顶点离起点的距离+顶点离终点的距离=f
 * 2. 距离等于1中两者之和,新增属性f=g+h;
 * 3. 遍历到终点就结束,所以不保证最优解
 * @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 result = aStarAlgo.aStarAlgo(graph,2,5);
        System.out.println(result);
    }

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

        //优先级队列,用于每次取距离最短的哪个节点,这里java自带的优先级队列没有提供update接口,只能自己实现一个了
        int v = graph.v;
        LinkedList[] adj = graph.adj;
        Vertx[] vertxes = graph.vertxes;
        AStarPriorityQueue queue = new AStarPriorityQueue(v);

        //preNode数组,这里节点取值是0-v-1, 所以直接用数组就可以了;如果不是的话就要用map了
        int[] preNodeArr = new int[v];
        boolean[] visited = new boolean[v];
        int[] currMinDisArr = new int[v];

        //start作为第一个点,然后根据当前节点广度优先遍历其后继节点
        Vertx vertx0 = vertxes[start];
        queue.add(vertx0);
        preNodeArr[start] = -1;
        visited[start] = true;
        while(!queue.isEmpty()){
            Vertx currMinFVertx = queue.poll();
            //看它的后继节点
            LinkedList nextEdges = adj[currMinFVertx.nodeValue];
            for(int i=0;i 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节点就结束
                if(nextEdge.end == end){
                    queue.clear();
                    break;
                }

            }
        }

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

  1. 简单的图产生测试用例
package com.zxb.structurealgo.AStarAlgo49;


/**
 * @ClassName AStarGraphGenUtil
 * @Description 拓扑图生成类
 * @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算法,理解了一些基础的东西,做下总结,希望自己之后可以更深入的理解

1. Raft算法出现的目的

因为PAXOS算法过于晦涩,常人难以理解,且落地困难。想要找到一种这样的一致性算法:可实现性强,减少开发的工作量;必须安全且大部分情况下是可用的;大部分操作必须是高效的;可理解性强,Raft一致性算法应运而生。

2. Raft算法的核心
领导选举

集群中的每个节点有三种状态:Follower(跟随者),Candidate(参与选举者),Leader(领导者);
所有节点的初始状态为Follower, 每个节点都有一个随机时间,超过随机时间时,如果相应的Follower没有收到其他节点的选举信息,则会变成Candidate(先会给自己投一票), 再向其余的节点发送参与选举领导者请求并接收其余节点的投票,如果投票数超过一半则节点由Candidate变成Leader, 选举完毕,保证每次最多只会选出一个领导者。

有两个timeout设置来保证领导选举的顺利进行:一个是election timeout,它就是上面所说的每个Follower有一个随机的election timeout, 超过这个时间则节点由Follower变成Candidate状态,一般设置为150ms-300ms, 这里每个节点的election timeout可能都是不一样的,可以保证尽快的选出领导(可以想想如果都是一个确定的值,每个节点都同时由Follower变成Candidate,那样选出领导的速度肯定会变慢的);一个是heartbeats timeout, 一旦确定领导者,领导者会定时向Follower发送心跳,来保证领导者正常的情况下,其他Follower不会变成Candidate

当领导者挂了之后,重新开始选举流程:由于heartbeats timeout了,所以会有节点从Follower变成Candidate, 然后跟之前一样选举出领导者即可

当多个Follower同时变成Candidate,且投票结果Candidate都没有超过半数以上,则等待下一轮超时重新选举,直到选举出leader.(注:每次选举的时候term会加1)

日志复制

选好领导者之后开始日志复制,就是领导者将更新通知到Follower, 要保证所有节点的更新数据的一致性,主要流程如下:当客户端发起一个更新请求会先给到leader,leader会将更新记录追加到它的日志末尾,然后leader会将更新发给其他的Follower, 同理Follower也会记录日志到其对应的日志末尾并ack一个信号给leader, 当leader收到超过半数的ack之后,会commit更新并通知Follower也commit这个更新。(有点类似于2PC,先prepare再commit)

当发生脑裂时,raft算法还可能可以保证一致性,这是由于要半数以上ack所决定的,即使不返回数据,也不会返回错误数据。

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

Tip

项目中一个接口列表慢查询优化实践

背景:项目开发中提供给外部系统一个列表查询接口,在测试环境验证响应时间1s左右(现在看来也挺慢的_),感觉问题不大,上了生产之后调用方反馈接口响应时间太长了,有些时候甚至超过5s,显然这是不能忍受的,已经超过http的超时时间了,通过响应参数发现,这个列表查询每次能返回100多条数据,之前完全没预料到,于是乎开始优化

代码大概是下面这样的:

List result = new ArrayList<>();
//根据条件查询出符合条件的task列表
List tasks = queryTaskService.getTaskByCondition(map);
//将task列表按照routeId分组
Map taskMap = tasks.stream.collect(Collectors.groupingBy(event -> event.getRouteId));
taskMap.forEach((routeId, tasks) -> {
    //根据routeId查询对应的Route信息,这里面会查缓存,带Biz后缀的类都会加事务
   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)循环里面,调了一个不应该调的rpc接口(复用原来的接口导致的呜呜呜),代码里没有列出来,每次rpc大概20-30ms, 100多次循环就是2-3s,去除掉基本就节约一半时间了
  2. 还是需要2s多,不正常,再来;分析来分析去只有循环里面查缓存比较耗时,按经验缓存查询一次最多5ms,100多次也就500ms,跟2s相差还是很多啊;那就把在routeServiceBiz.getRouteByRouteId里面把查缓存的时间全部打印出来,结果发现一次缓存大概2-3ms,100多次就是200-300ms,那时间究竟花在哪里了呢?
  3. 百思不得奇解,最后加了上面的t1,t2,计算出所有循环t2-t1的累计值发现比查缓存的累积值要大很多;再想想,外层只是通过方法调用了一下而已啊;不对每次方法调用都会开启一次事务,基本可以确定了循环100多次就有100多次事务,主要时间花在了事务上
  4. 解决方法:去除掉事务,查询根本不需要事务,之后重新查询发现200ms左右,优化完毕

总结:循环中最好不要循环加事务,当循环次数多的时候,事务占用的时间非常多;造成这样的最终原因还是因为采用的框架,Biz结尾的service所有方法都会默认加上事务,所以对于项目中的查询方法不要写在Biz结尾的service层,应该重新建一个不带Biz结尾的service,将相关的查询方法写到里面去

Share

其实买极客时间的专栏,如果你能认真的去看完并做笔记和思考,收获还是蛮多的,当然专栏只是把主要的知识点罗列出来,如果真的想要深入理解的话,可以借助专栏的思路去看相关的书籍和英文官方文档,一步一步拓展,那你习得的知识将会比大部分人都要深。

你可能感兴趣的:(20190404_ARTS_W00)