2019第一篇arts, 果然学习都是反人性的
**Algorithm**
每周至少做一个leetcode算法题
**Review**
阅读并点评至少一篇英文技术文章
(英文论文文献)
**Tip**
至少学习一个技术技巧
**Share**
分享一篇有观点和思考的技术文章
Algorithm
看了下数据结构与算法之美的A*搜索算法:是一种基于Dijkstra算法优化的次优解算法,意在减少搜索时间,不保证是最优解
自己按照思路简单画了个导图如下:
大概实现了下,还好之前有Dijkstra算法的基础,不然一下午根本搞不定:
- 基本数据结构
//边
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);
}
}
- 优先级队列: 根据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;
}
}
- 算法具体实现类
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;
}
}
- 简单的图产生测试用例
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;
- 首先之前在taskMap.forEach((routeId, tasks)循环里面,调了一个不应该调的rpc接口(复用原来的接口导致的呜呜呜),代码里没有列出来,每次rpc大概20-30ms, 100多次循环就是2-3s,去除掉基本就节约一半时间了
- 还是需要2s多,不正常,再来;分析来分析去只有循环里面查缓存比较耗时,按经验缓存查询一次最多5ms,100多次也就500ms,跟2s相差还是很多啊;那就把在routeServiceBiz.getRouteByRouteId里面把查缓存的时间全部打印出来,结果发现一次缓存大概2-3ms,100多次就是200-300ms,那时间究竟花在哪里了呢?
- 百思不得奇解,最后加了上面的t1,t2,计算出所有循环t2-t1的累计值发现比查缓存的累积值要大很多;再想想,外层只是通过方法调用了一下而已啊;不对每次方法调用都会开启一次事务,基本可以确定了循环100多次就有100多次事务,主要时间花在了事务上
- 解决方法:去除掉事务,查询根本不需要事务,之后重新查询发现200ms左右,优化完毕
总结:循环中最好不要循环加事务,当循环次数多的时候,事务占用的时间非常多;造成这样的最终原因还是因为采用的框架,Biz结尾的service所有方法都会默认加上事务,所以对于项目中的查询方法不要写在Biz结尾的service层,应该重新建一个不带Biz结尾的service,将相关的查询方法写到里面去
Share
其实买极客时间的专栏,如果你能认真的去看完并做笔记和思考,收获还是蛮多的,当然专栏只是把主要的知识点罗列出来,如果真的想要深入理解的话,可以借助专栏的思路去看相关的书籍和英文官方文档,一步一步拓展,那你习得的知识将会比大部分人都要深。