物流系统路由查找解决方案

简介

我供职的在上家是一家物流公司,我主要从事架构方面的工作,但偶尔也会写一些跟算法相关代码。项目组曾一个查找全路径的需求,系统现在的方法是循环查找数据库,迭代出所有数据。但数据量过多,且方法调用过于频繁,给数据库造成了严重的负担。于是我便用简单的数据结构加算法重新实现了。

以下是系统中的数据表:

station (站点表)

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

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

route (路径表)

stationdestination_stationnext_stationstatus
1621
2631
3641
4651
5661
7661
1821
2831
3841
4851
5861
7861
6891
9881

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

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

解决思跑路

定义数据结构,及其算法。利用Map进行快速查找。

数据结构

public class Station<T> implements Serializable {
    
    private Serializable id;
    private String name;
    private T extra;
    private final Map<Serializable,Path> pathMap = new HashMap<>();
    
    ....getter and setter...
    
    
       /**
     * 连接两个station
     * @param destination
     * @param next
     * @param extra
     * @param <E>
     */
    public  <E>  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<T> {
    private T extra;
    private Station destination;
    private Station cur;
    private Station next;
    private List<Station> 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<T,E> {
    ConcurrentSkipListMap<Serializable,Station<T>> 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<Path<E>> route = findRoute(next, des);
            for (Path<E> p : route) {
                if(p.getNext()==curt||p.getCur() == curt){
                    throw new IllegalStateException("网点存在死循环。");
                }
            }
            curt.linkTo(des, next, extra);
        }finally {
            lock.unlock();
        }
    }
    
 public List<Path<E>> findRoute(Station begin,Station des){
        int len = 0;

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

总结

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值