需求背景

之前做了个需求,当时搞了一个基于树结构的算法,这里记录一下。需求是在任意地图层级下,实时统计当前屏幕中分布的点的数量,数据放在mongodb的单个表中,总量是一亿多。地图范围是某个省份。

在任意地图层级下查询,意味着当前屏幕中分布的点的数量可以是0~1亿+,这样的话,直接从数据库查询很难做到实时返回;当时测试了一下,直接在内存中计数1000w也要1s左右。所以,考虑自己实现一个计数算法,该算法有一定的误差。

算法介绍

算法也比较简单,主要思路有两点:

  • 把数据放在内存中
  • 提前计数。

算法详情

  1. 构造一颗多叉树,后面称为统计树;
  2. 根据内存使用量误差容忍度可以自定义树的深度分叉数量
  3. 树的每个节点存放的是一个范围,与该范围内的点的数量
  • 首先,做出全省边界的最小矩形框(由4个经纬度坐标点构成), 即java JTS里面的Envelope,作为根节点的范围,这里称为E0,0;
  • 其次,将E0均分为n个相同的矩形,例如n=16,分别是E1,0~E1,15,作为根节点的子节点的范围;
  • 然后,再将E1,0~E1,15中的每个矩形继续分割,根据内存使用量和误差容忍度确定树的深度。
  • 最后,遍历所有数据,计算出树中的每个节点中范围内的点数,存在树的节点中。
    深度是3的树,如下图:
    范围分割效果

查询:

例如查询范围是下图红色部分S
查询范围

首先,判断S的范围与根节点的范围E0,0的关系:
如果S包含E0,0,则S中点位的数量就是提前统计的E0,0范围的点数;这里显然,S是在E0,0范围内,两者是相交关系。

那就遍历E0,0的所有子节点,再次判断S与每个E1,n(0<=n<=15)的关系:
如果S包含E1,k(0<=k<=15),则查询结果就加上E1,k所在节点的统计数量;否则,再遍历E1,k的子节点,直到到达树的叶子节点;如果S与某个叶子节点Ep,q相交,计算两个面交集的面积,将最后的统计结果加上 Ep,q的统计数量*交集的面积/Ep,q的面积

由于最后的叶子节点没有子节点,因此用估计量代替实际数量,这里简单地使用了面积的百分比作为对数量的估计。该算法的误差就来源于此。可以看出,每个叶子节点的范围越小,最后的误差也越小,这也意味着树的分叉数量和深度的增加,即增加了内存使用量和查询时间。

新增和删除

为了能够新增和删除点位后,树内的统计量也随之变化,需要在预先的统计中为每条数据设置一个字段进行标识,标记该数据已经在树中。

新增数据:

  • 判断该数据节点在哪一个叶子节点的范围内,将统计数量加1;
  • 然后往上遍历所有父节点,把每个父节点的统计数量都加1;
  • 最后在数据库中标识该条记录已经加入到统计树。

对于删除数据,与新增数据类似:

  • 判断该数据节点在哪一个叶子节点的范围内,将统计数量减1;
  • 然后往上遍历所有父节点,把每个父节点的统计数量都减1;
  • 最后在数据库中删除统计树标识。

更新

更新操作(数据点位的位置发送变化)就是新增和删除的组合,对位置更新之前执行上述删除操作,对位置更新之后执行上述新增操作。

序列化和反序列化

预先遍历数据表生成统计树是一个很耗时的工作,做好了树之后,不可能每次应用重启都要重新做一遍,因此,可以将做好的统计树进行序列化,每次重启时,再反序列化出来。还可以开启定时任务,周期性地执行序列化操作,每次重启时,反序列化最新的那个序列化文件。

算法性能

  • 树的高度和分叉数量需要做一个权衡,一般来说,CRUD都很快,都是毫秒级的;
  • 内存占用情况:对于32个分叉、深度是6层的树,节点数量是一百万多点,对于java,假设每个节点的范围由4个double类型的数据表示,统计量是一个int类型,每个节点就是36个字节,统计树的大小应该不超过100MB。
  • 查询的范围越大,相对误差越小;小范围查询可以考虑用其它方案,例如直接查数据库。不严格的测试结果:对于32个分叉、深度是6层的统计树,总数是1亿的数据量,超过百万点位的查询平均误差小于1%。

顺便说一下,在生成统计树的时候很耗时(上面已经提过了),有可能发生意外(网络问题、数据库问题)导致中断的情况,因此可以在树节点内添加额外字段,标识该节点是否已经统计完成。这样,当程序发生异常时,在捕获异常中进行序列化操作,这样下次就可以继续统计,而不是重头开始。

还有不太重要的一点是,统计之前,可以先大致估计统计树的内存占用量,一般其实也不需要很大的内存空间。

最后一点,可以考虑将多个统计量放到同一个统计对象里面,例如人口点位数量、poi点位数量,免得维护多个树结构。

源码

下面是java源码实现,主要的依赖就是JTS和序列化包msgpack,写的比较潦草,后来也没有再优化,欢迎指出问题。

//统计树的节点对象
@Message
public class Cnode implements Serializable {
    // 树的深度, 根节点是1
    private int level;
    private Envelope envelope;
    // 这里存放统计量,统计标识
    private StatisticBo statisticBo;
    // 子节点
    private Cnode[][] subNodes;

    Cnode() {
    }

    Cnode(Envelope envelope, Ctree ctree) {
        this.envelope = envelope;
        computeLevel(ctree);
    }

    private void computeLevel(Ctree ctree) {
        if (ctree.getRoot() == null) {
            this.level = 1;
        } else {
            int times = new Long(Math.round(ctree.getRoot().envelope.getWidth() / this.envelope.getWidth())).intValue();
            int count = 1;
            while (times != 1) {
                count++;
                times = times / ctree.getWidthSize();
            }
            this.level = count;
        }
    }

    public void updateStatic(StatisticBo statisticBo) {
        if (this.statisticBo == null) {
            this.statisticBo = statisticBo;
        } else {
            this.statisticBo.plus(statisticBo);
        }
    }

   // ...省略getter、setter
}

//统计量
@Message
public class StatisticBo implements Serializable {

    private int personCount;
    //节点统计完成标识
    private boolean personStatisticFinish;

    private int poiCount;
    //节点统计完成标识
    private boolean poiStatisticFinish;

    public StatisticBo plus(StatisticBo statisticBo) {
        if (statisticBo.personStatisticFinish) {
            this.personCount = statisticBo.personCount;
        }
        if (statisticBo.poiStatisticFinish) {
            this.poiCount = statisticBo.poiCount;
        }
        
        this.personCount += statisticBo.getPersonCount();
        this.poiCount += statisticBo.getPoiCount();
        

        return this;
    }

    public com.tuyun.mapserver.bo.StatisticBo multiply(double factor) {
        this.personCount *= factor;
        this.poiCount *= factor;
        
        return this;
    }
   
    // ...省略getter、setter
}

// 统计树对象
@Message
public class Ctree implements Serializable {
    private static final Logger logger = LoggerFactory.getLogger(Ctree.class);
    /**
     * 树的根节点
     */
    private Cnode root;
    /**
     * envelop横向分隔的数量
     */
    private int widthSize;
    /**
     * envelop纵向分隔的数量
     */
    private int heightSize;
    /**
     * 默认envelop分为16个子方格
     */
    private final int default_widthSize = 4;
    private final int defaust_heightSize = 4;


    public Ctree() {

    }

    /**
     * 初始化树,设置树的深度,所有节点的envelope
     *
     * @param envelope
     * @param depth
     */
    public void init(Envelope envelope, int widthSize, int heightSize, int depth) {
        if (widthSize < 1 || heightSize < 1) {
            logger.warn("envelop分隔的个数不能小于1: [widthSize: {}], [heightSize: {}]", widthSize, heightSize);
            logger.warn("使用默认值: [widthSize: {}], [heightSize: {}]", default_widthSize, defaust_heightSize);
            this.widthSize = default_widthSize;
            this.heightSize = defaust_heightSize;
        } else {
            this.widthSize = widthSize;
            this.heightSize = heightSize;
        }
        if (depth < 1) {
            throw new IllegalArgumentException("树的深度不能小于1: [ " + depth + "]");
        }
        this.root = new Cnode(envelope, this);
        if (depth == 1) {
            return;
        }
        createSubNodes(this.root);
        for (int i = 0; i < depth - 2; i++) {
            moreDeep(this.root.getSubNodes());
        }

    }

    private void createSubNodes(Cnode cnode) {
        Envelope envelope = cnode.getEnvelope();
        Cnode[][] subNodes = new Cnode[widthSize][heightSize];
        double minx = envelope.getMinX();
        double miny = envelope.getMinY();
        for (int i = 0; i < widthSize; i++) {
            for (int j = 0; j < heightSize; j++) {
                Envelope envelope1 = new Envelope(minx + envelope.getWidth() / widthSize * i,
                        minx + envelope.getWidth() / widthSize * (i + 1),
                        miny + envelope.getHeight() / heightSize * j,
                        miny + envelope.getHeight() / heightSize * (j + 1));
                subNodes[i][j] = new Cnode(envelope1, this);
            }
        }
        cnode.setSubNodes(subNodes);
    }

    /**
     * 增加树的深度
     *
     * @param cnodes
     */
    private void moreDeep(Cnode[][] cnodes) {
        for (int m = 0; m < widthSize; m++) {
            for (int n = 0; n < widthSize; n++) {
                if (cnodes[m][n].getSubNodes() == null) {
                    createSubNodes(cnodes[m][n]);
                } else {
                    moreDeep(cnodes[m][n].getSubNodes());
                }
            }
        }
    }

    /**
     * 获取树的深度
     *
     * @return
     */
    public int getDepth() {
        if (root == null) {
            return 0;
        }
        int depth = 1;
        Cnode[][] subNodes = root.getSubNodes();
        while (subNodes != null) {
            depth++;
            subNodes = subNodes[0][0].getSubNodes();
        }
        return depth;
    }

    public int getCnodeCount() {
        if (this.root == null) {
            return 0;
        }
        return getCnodeCount(this.root);
    }

    private int getCnodeCount(Cnode cnode) {
        int count = 1;
        if (cnode.getSubNodes() == null) {
            return count;
        }
        Cnode[][] subNodes = cnode.getSubNodes();
        for (int i = 0; i < subNodes.length; i++) {
            for (int j = 0; j < subNodes[0].length; j++) {
                Cnode cnode1 = subNodes[i][j];
                int count1 = getCnodeCount(cnode1);
                count += count1;
            }

        }
        return count;
    }

    // ...省略getter、setter
}


//程序入口
public class CtreeMain {
    public static Ctree ctree;

    private static final String PATH = "E:\\ctree.msgpack";
    
    public static void main(String[] args) throws Exception {
        try {
            File file = new File(PATH);
            if (file.exists()) {
                try {
                    //如果序列化文件存在, 反序列化获取统计树
                    deserializer(PATH);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {
                Envelope rootEnvelop = new Envelope(Minx, Maxx, Miny, Maxy);
                //构造树(创建节点, 设置范围)
                ctree = buildCtree(rootEnvelop, 4, 4, 6);
            }
            // 填充树(设置统计量)
            fillCtree(ctree);
            // 统计完成后,再序列化一次
            serializer(ctree,PATH);
        } catch (Exception e) {
            e.printStackTrace();
            if (ctree != null && ctree.getRoot() != null) {
                serializer(PATH);
            }
        }
    }
}

// 反序列化
public void deserializer(String path) throws IOException {
    MessagePack messagePack = new MessagePack();
    messagePack.register(Envelope.class);
    ctree = messagePack.read(new BufferedInputStream(new FileInputStream(new File(path))), Ctree.class);
}

// 构造树
public Ctree buildCtree(Envelope envelope, int widthSize, int heightSize, int depth) {
    Ctree ctree = new Ctree();
    ctree.init(envelope, widthSize, heightSize, depth);
    return ctree;
}

//填充树,这个方法的实现是用于统计单个统计量的,如果要同时统计多个统计量,需要改动/或者 多次调用
public StatisticBo fillCnode(Cnode cnode) {
    //TODO 统计不同的数据时,这里要改动
    //已经统计过了
    if (cnode.getStatisticBo() != null && cnode.getStatisticBo().isPersonStatisticFinish()) {
        return cnode.getStatisticBo();
    }
    StatisticBo statisticBo = new StatisticBo();
    if (cnode.getSubNodes() == null) {
        Envelope envelope = cnode.getEnvelope();
        //TODO 统计不同的数据时,这里要改动
        //根据范围查数据库计算数量,这里不是遍历数据库所有记录,而是根据每个范围查询的
        statisticBo = staticPerson(envelope);
//statisticBo = staticPoi(envelope);
        cnode.updateStatic(statisticBo);
        return statisticBo;
    }

    Cnode[][] subNodes = cnode.getSubNodes();
    for (int i = 0; i < subNodes.length; i++) {
        for (int j = 0; j < subNodes[0].length; j++) {
            Cnode cnode1 = subNodes[i][j];
            StatisticBo statisticBo1 = fillCnode(cnode1);
            statisticBo.plus(statisticBo1);
        }
    }
    cnode.updateStatic(statisticBo);
    return statisticBo;
}

//序列化树
public void serializer(String path) throws IOException {
    MessagePack messagePack = new MessagePack();
    messagePack.register(Envelope.class);
    byte[] bytes = messagePack.write(ctree);
    ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
    Files.newOutputStream(Paths.get(path)).write(byteBuffer.array());
}

//数量查询
public static StatisticBo queryCount(Geometry geometry) {
    Cnode root = ctree.getRoot();
    if (geometry.contains(geometryFactory.toGeometry(root.getEnvelope())) || root.getSubNodes() == null) {
        return root.getStatisticBo() != null ? root.getStatisticBo() : new StatisticBo();
    }
    return queryCount(geometry, root.getSubNodes());
}

//递归调用
private static StatisticBo queryCount(Geometry geometry, Cnode[][] subNodes) {
    StatisticBo statisticBo = new StatisticBo();
    if (subNodes != null) {
        for (int i = 0; i < subNodes.length; i++) {
            for (int j = 0; j < subNodes[0].length; j++) {
                Cnode cNode = subNodes[i][j];
                Geometry envelopeGeometry = geometryFactory.toGeometry(cNode.getEnvelope());
                if (geometry.contains(envelopeGeometry)) {
                    statisticBo.plus(cNode.getStatisticBo());
                } else if (envelopeGeometry.contains(geometry) || envelopeGeometry.intersects(geometry)) {
                    Cnode[][] cNodes = cNode.getSubNodes();
                    if (cNodes == null) {
                        statisticBo.plus(cNode.getStatisticBo().multiply(Math.random()));
                    } else {
                        statisticBo.plus(queryCount(geometry, cNodes));
                    }
                }
            }
        }
    }
    return statisticBo;
}

转发请注明原作者和出处!


海棠依旧
1 声望0 粉丝