路由规划算法

需求分析

两张原始数据表如下:

station (站点表)

id name
1 上海
2 昆山
3 苏州
4 常州
5 镇江
6 南京
7 扬州
8 西安
9 郑州

站点表用于记录所有分拔与网点的信息

route (路径表)

station destination_station next_station status
1 6 2 1
2 6 3 1
3 6 4 1
4 6 5 1
5 6 6 1
7 6 6 1
1 8 2 1
2 8 3 1
3 8 4 1
4 8 5 1
5 8 6 1
7 8 6 1
6 8 9 1
9 8 8 1

路由信息的基础数据表,用于记录从当前网点(station)前往目前网点(destination_station)的下一网点(next_station)。以及这个路由的启动状态(status ,1,启动,2关闭)

要生成一条完整的路由。为了缓解数据库压力,加快查询速度。决定将原数据库查询更换成纯算法实现。

解决思跑路

以前实现是在数据库中查找,现在想所有数据加载到内存中,得用数据结构与算法,实现路由查找。实现起来也不复杂,比起最短路径或最优路径这个功能就很简单

数据结构

public class Station implements Serializable {
    
    private Serializable id;
    private String name;
    private T extra;
    private final Map pathMap = new HashMap<>();
    
    ....getter and setter...
    
    
       /**
     * 连接两个station
     * @param destination
     * @param next
     * @param extra
     * @param 
     */
    public    void linkTo(Station destination, Station next, E extra){


        Path p = pathMap.get(destination.getId());
        if(p==null){
            p = new Path<>(this);
        }else{
            detach(destination);
        }


        p.setExtra(extra);
        p.setDestination(destination);
        p.setNext(next);
        pathMap.put(destination.getId(),p);
        Path nextPath = next.getPath(destination);
        if(nextPath==null){
            nextPath = new Path(next);
            nextPath.setDestination(destination);
            next.pathMap.put(destination.getId(),nextPath);
        }
        nextPath.addPrevious(this);
    }

    /**
     * 获聚首当前路是径
     * @param des
     * @return
     */
    public Path getPath(Station des){
       return pathMap.get(des.getId());
    }



    public void detach(Station des){
        Path path = pathMap.get(des.getId());
        if(path!=null){
            path.removePrevious(this);
        }
        if(path.getPrevious().size()==0) {
            pathMap.remove(des.getId());
        }else{
            path.setNext(null);
        }
    }
}
@Data
public class Path {
    private T extra;
    private Station destination;
    private Station cur;
    private Station next;
    private List previous = new ArrayList<>();

    public Path(Station cur) {
        this.cur = cur;
    }

    public void addPrevious(Station station){
        if(!previous.contains(station)){
            previous.add(station);
        }
    }

    public void removePrevious(Station station){
        previous.remove(station);
    }

    @Override
    public String toString() {

        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for (Station station : previous) {
            sb.append(",");
            sb.append(station.getId());
        }
        sb.append("]");


        return "Path{" +
                " cur=" + cur.getId() +
                ", destination=" + (destination!=null?destination.getId():"") +
                ", next=" + (next!=null ? next.getId():"") +
                ", previous=" + sb.toString() +

                '}';
    }
}

解决并发问题加锁方案

PathFinder类主要是用来存储所有站点的集合,以及提供所有路径想着的操作(可能我要重新命名),因为业务中要根据网点id查询网点(或路由信息),所以用map来存储网点。路径信息可能随时会出变动,而每段路径的变动,都会影响到整个路由。所以我在此处用到了读写锁,每段路径的变动,只会影响到与其相同终点的路径,我让每个Station都持有一个读锁和写锁。在做读取或更改操作时,我获取目的结点上的锁,并对其进行加锁操作(这种不锁全部,只锁部门的思想在LongAdapter和ConcurrentMap中都有应用)。

@Service
public class PathFinder {
    ConcurrentSkipListMap> stationMap = new ConcurrentSkipListMap();
    
     public  void link(Station curt, Station des, Station next, E extra){
        if(curt == next){
            throw new IllegalStateException("当前网点与下一网点不可以相同");
        }

        ReentrantReadWriteLock.WriteLock lock = des.writeLock();
        lock.lock();
        try {
            /**
             * 验证是否存在死循环
             */
            List> route = findRoute(next, des);
            for (Path p : route) {
                if(p.getNext()==curt||p.getCur() == curt){
                    throw new IllegalStateException("网点存在死循环。");
                }
            }
            curt.linkTo(des, next, extra);
        }finally {
            lock.unlock();
        }
    }
    
 public List> findRoute(Station begin,Station des){
        int len = 0;

        List> list = new ArrayList<>();
        ReentrantReadWriteLock.ReadLock lock = des.readLock();
        lock.lock();
        try{
            Path p;
            Station station =begin;
          while((p=station.getPath(des))!=null && (station=p.getNext())!=null){
              list.add(p);
              if((len++)==100){
                  throw new IllegalStateException("链路太长,可能出现死路:"+list);
              }
          }
        }finally {
            lock.unlock();
        }
        return list;
    }
    
    
}


数据全加载时遇到的问题

原计划是在程序启动时,加载整张表数据。但测试之后,2000万条数据,加载速度很慢,之少要20分钟之久。

延迟加载及并行加载

数据全加载保证了用户访问时的查询速度,以及线程安全等问题。但确增长系统启动时间。

懒加载不影响系统启动时间。但可能会带来两个问题:

  1. 多线程下数据重复加载问题,比如有几个请求同时请求上海南京的结点,可能造成两个线程同时加载数据。
  2. 可能会降代查询速度,用户第一次请求查询某路径时,因为要从数据库加载数据,可能会出现延迟。
  • 状态控制及加锁

    为了避免数据重复加载,可以对每个接点添加一个状态,表示以此接点为目的接点的数据是否已加载,如果未加裁,可以使用tryLock试获此接点的锁,如果获取成功,则加载数据据,否则进行自旋,不断获取加载状态。一旦数据另载成功,便返回。

  • 并行加载
    懒加载是指在在需要数据时再加载数据(从数据库中读取所有目的网点的数据),在访问峰值时,会造成类似于缓存的雪崩现像。所以采用预加载与懒加载并行的方式,在程序启动地,另开一线程加载数据,这样不会影响到项目的启动时间。同时由于上面的状判断及懒加载的方案,也能在数据未能加载完成时,对外提供服务。

后继方案

目前2000个网点,生成的结点数所约500万条,得用jmap查看内存发现,总共占用内存不到一个G, 如果以后再增加网点数,可以考虑LRU策略进行淘汰,目前我存储网点用的的是ConcurrentSkipListMap,如果要实现LRU策略,可能要换成HashLinkedMap实现。当然我也可以用数据分片,不管是LRU或数据分片,都可以利用目的网点对数据进行分割。这要比传统的那种寻找最小路径算法简单多了(因为如果是传统的图,我想不到什么好的办法,对数据进行分割)。因为不想增加复杂性,我只对该服务进行单节点部署(多节点部署要考虑数据一致性问题,而且当前拥有有的服务器资源也不适做多接点部署),以后如果要做接点部署,可以考虑应用数据一致性算法,加乐观锁机制。

总结

算法很简单,应用的技术也不算太复杂,关键是选择合适的方案来解决当前的问题。

你可能感兴趣的:(路由规划算法)