ForkJoin实际中应用

1,747 阅读8分钟

前言

前两篇文章讲了ForkJoin的基本原理和源码的简单讲解。也将ForkJoin做了简单的应用。今天这篇文章讲一讲在解决实际问题时对ForkJoin的应用。

实际中的问题

在做项目的时候,遇到了这样一个需求:需要把远端数据源中的数据全量导入到我们项目中的库中,也就是远端库数据同步更新到本地库。远端数据库中的数据表量比较庞大。由于是zf项目,对方只给我们提供这一种数据获取方式,而且对方的服务器也不给开放任何权限,只能链接他们的数据库。所以,在这些限制条件下,只能使用最笨的链接数据库的方式进行数据获取。

分析

经过远端数据库的数据梳理,发现虽然表的量比较庞大,但是表中的数据最多只达到了百万级,并不算多。而且有些表数据是以增量的形式变化的。这样,对于这些表,第一次数据同步可以看作全量同步外,以后的同步数量级就很小了。

既然是同步数据,那每次数据同步就包含有:新增|更新|删除的数据。每次同步数据可以分批获取远端数据,再对分批数据进行解析,得到新增数据更新数据删除数据。得到这些数据后,再针对不同数据对本地表做相应的操作。

思路

通过上面的分析,那数据同步思路大概就是:

  1. 每张表分批查询,获取分批数据。
  2. 对分批数据进行解析,得到新增、更新、删除数据。
  3. 对于新增的数据直接进行插表,对于更新的数据对其进行更新,对于删除的数据则进行删除。
  4. 重复以上3个步骤,扫描全部数据表。

image.png

数据分批获取

数据分批获取这一块,我直接用的是最简单粗暴的方法:分页查询。由于项目框架使用的是Hibernate,我这里使用的是@Query加手写sql。得到的数据结构为:List<Map<String, Object>>,再通过反射得到自己想要的数据。由于数据表数量庞大,这里用到了泛型。

样例代码贴在下面:

    /**
     * 分页查询
     *
     */
public <T> Map getSrcDataFromRemote(Class<T> clazz, DataSrcSyncPj param, PgRepository pgRepository) throws Exception {
        Map resMap = new HashMap();
        Integer pageNum = param.getPageNum();
        Integer pageSize = param.getPageSize();
        FixedPageData pageData = new FixedPageData(pageNum, pageSize);

        /**
         * 数据同步时间
         */
        Date syncDate = param.getSyncDatetime();
        String syncDateStr = "";
        if (syncDate != null) {
            syncDateStr = DateUtils.getDateTime(syncDate);
        }

        Integer size = Math.toIntExact(pageData.getSize());
        Integer offset = Math.toIntExact(pageData.getOffset());
        Integer unSyncNum = param.getUnSyncNum();
        //总数
        Integer total = 0;
        if (unSyncNum != null) {
            total = unSyncNum;
        } else {
            PageNum pgN = pgRepository.findPageTotal(syncDateStr);
            if (pgN != null) {
                total = pgN.getTotal();
            }
        }

        //查询数据
        List<Map<String, Object>> pageObjs = pgRepository.findPageData(syncDateStr, offset, size);
        //反射
        List<T> remoteData = EntityUtils.reflectMap2PojoList(pageObjs, clazz);

        resMap.put("total", total);
        resMap.put("remoteData", remoteData);
        log.info("{} getSrcDataFromRemote >>>> QUERY COMPLETED, UnSync Data Total: {}", this.getClass().getSimpleName(), total);
        return resMap;
    }

数据 增|删|改

项目中用到的框架是Hibernate,所以就涉及到entityManager,queryFactory这些东西。

数据新增这里,我这块直接用到了entityManager做的批量插入。先persist()添加批量数据,然后再做flush()刷新。

    /**
     * 批量新增
     *
     * @param var1
     * @param <T>
     */
    @Transactional(value = "transactionManagerMysql", rollbackFor = Exception.class)
    public <T> void batchInsert(List<T> var1) {
        Iterator<T> iterator = var1.iterator();
        int index = 0;
        while (iterator.hasNext()) {
            T itD = iterator.next();

            entityManager.persist(itD);
            index++;

            //过临界值或达到集合边界刷新缓存,数据入库
            if (index % BATCH_SIZE == 0 || index == var1.size()) {
                try {
                    entityManager.flush();
                } catch (Exception e) {
                    printExceptionLog(this, "batchInsert", "Insert EXCEPTION", e);
                    e.printStackTrace();
                } finally {
                    entityManager.clear();
                }
            }
        }
    }

数据的更新,直接可以用merge()来处理,最后flush()刷新。

/**
     * 批量修改
     *
     * @param var1
     * @param <T>
     */
    @Transactional(value = "transactionManagerMysql", rollbackFor = Exception.class)
    public <T> void batchUpdate(List<T> var1) {
        Iterator<T> iterator = var1.iterator();
        int index = 0;
        while (iterator.hasNext()) {
            T itD = iterator.next();

            entityManager.merge(itD);
            index++;

            //过临界值或达到集合边界刷新缓存,数据入库
            if (index % BATCH_SIZE == 0 || index == var1.size()) {
                try {
                    entityManager.flush();
                } catch (Exception e) {
                    printExceptionLog(this, "batchUpdate", "update EXCEPTION", e);
                    e.printStackTrace();
                } finally {
                    entityManager.clear();
                }
            }
        }
    }

删除这里,我直接用的是临时表和本地表的leftjoin来进行过滤。在数据进行分批解析和入库的过程中,将远端数据的id存入本地的临时表中。在数据新增或更新完后,就可以得到远端表和本地表的所有最新数据。再通过leftjoin的筛选,就可以得到需要删除的数据。

只不过在删除的时候,需要注意。在用到delete from table in (xxxx)的时候,in的数据不能过大,否则会有异常的风险。我这里将in的数量控制在了1000

/**
     * 删除数据
     *
     * @param rIds
     * @param repos
     * @return
     */
    private Integer deleteData(List<String> rIds, BaseRepository repos) {
        int total = 0;
        if (!rIds.isEmpty()) {
            List<List<String>> parts = Lists.partition(rIds, 1000);
            int num = 0;
            for (List<String> ids : parts) {
                num = repos.deleteByIds(ids);
                total += num;
            }
        }
        return total;
    }

ForkJoin应用

从上面的思路来看,可以引入ForkJoin的地方,是在对数据进行插入、更新、删除的时候。由于ForkJoin的基本原理是工作窃取和fork/join。那么就可以在对分批数据进行解析的时候引入,将分批数据化整为零,各自处理。如下图:

forkJoin.png

线程安全

既然使用到了ForkJoin,又是涉及数据的处理,必然涉及到线程安全的问题。对于这里的线程安全,其实我一开始是用synchronized做的处理,但是在后续的测试中发现,被synchronized的线程并不安全,还是会出现数据不一致的问题。后来,我用到了ReentrantLock 可重入锁,ReentrantLock可以更有效的处理线程安全问题,而且比synchronized使用更加灵活。

ReentrantLock

ReentrantLock属于乐观锁。一般使用lock获取锁,unlock释放锁。每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而持有该锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

相比于synchronizedReentrantLock在功能上更加丰富,它具有可重入、可中断、可限时、公平锁等特点。

ForkJoinTask compute()

compute()是ForkJoinTask的核心,需要复写来实现自己的业务功能。这里的主要逻辑就是数据的fork和结果的join。由于需要获取新增|更新数据量,我这里用到了ConcurrentMap来存放自己想要的数据。

样例代码:

protected ConcurrentMap<String, Object> compute() {
            try {
                //上锁
                lock.lock();
                ConcurrentMap<String, Object> map = new ConcurrentHashMap<>();
                ConcurrentMap<String, Object> mapIds = new ConcurrentHashMap<>();
                if (this.remoteList.size() > part) {
                    List<ForkJoinTask> tasks = new ArrayList<>();
                    List<List<OrclEntity>> partList = Lists.partition(this.remoteList, part);

                    Date date = this.syncDate;
                    boolean locate = this.locate;
                    BaseRepository repos = this.bRepos;
                    Class<T> clazz = this.clazzT;
                    
                    //任务分割
                    partList.stream().forEach(it -> {
                        SyncOrclTaskN task = new SyncOrclTaskN(clazz,
                        Collections.synchronizedList(it), date, repos, locate);
                        tasks.add(task.fork());
                    });


                    AtomicInteger idsA = new AtomicInteger(0);
                    AtomicInteger addA = new AtomicInteger(0);
                    AtomicInteger updA = new AtomicInteger(0);

                    ConcurrentMap<String, Object> finalMap = map;
                    //结果合并
                    tasks.stream().forEach(it -> {
                ConcurrentMap<String, Object> itMAP = (ConcurrentMap<String, Object>)it.join();
                        idsA.addAndGet((int) itMAP.get("ids"));
                        addA.addAndGet((int) itMAP.get("add"));
                        updA.addAndGet((int) itMAP.get("upd"));
                        finalMap.put("ids", idsA);
                        finalMap.put("add", addA);
                        finalMap.put("upd", updA);
                    });
                    return finalMap;
                } else {
                    //数据解析
                    transformSrcData();

                    mapIds.put("ids", 0);
                    if (this.overAll) {
                        mapIds = locateIds();
                    }

                    map.put("add", 0);
                    map.put("upd", 0);
                    if (this.locate) {
                        map = addOrUpdate();
                    }

                }
                map.putAll(mapIds);
                return map;
            } catch (Exception e) {
                log.error("{} compute >>>> TASK compute EXCEPTION: {}", this.getClass().getSimpleName(), e.toString());
                log.error("{} compute >>>> TASK compute EXCEPTION StackTrace: {}", this.getClass().getSimpleName(), e.getStackTrace());
                e.printStackTrace();
            } finally {
                //解锁
                lock.unlock();
            }
            return null;
        }

数据解析

数据解析这块,主要就是为了分离新增和更新数据。思路很简单,就是在本地表存在的数据,就是更新数据,不存在的就是新增数据。这里还是用到了泛型。

样例代码:

        /**
         * @Author zhou
         * @Description 远端数据解析
         * @Date 2021/7/12 16:19
         * @Param []
         * @return void
         **/
        public void transformSrcData() {

            List<OrclEntity> unUpdData = new ArrayList<>();
            List<OrclEntity> newData = new ArrayList<>();

            if (remoteList.size() > 0 && remoteList != null) {
                //获取id
                List<String> remoteIds = this.remoteList.stream().map(OrclEntity::getId).collect(Collectors.toList());

                if (this.overAll) {//全表扫描
                    this.idList = remoteIds;
                }

                //查询本地库存在的数据
                List<ExistsId> existIds = new ArrayList<>();

                existIds = bRepos.findExistIds(remoteIds);


                //分离新增数据和更新数据
                Date date = new Date();
                List<ExistsId> finalExistIds = existIds;
                remoteList.stream().forEach(item -> {
                    String pId = item.getId();
                    if (finalExistIds != null && finalExistIds.size() > 0) {
                        //新增标识
                        boolean flag = false;
                        for (ExistsId itId : finalExistIds) {
                            String localId = itId.getLocalId();
                            if (StringUtils.equals(pId, localId)) {
                                flag = true;
                                item.setId(localId);
                                item.setSyncDatetime(date);
                                unUpdData.add(item);
                            }
                        }

                        if (!flag) {
                            newData.add(item);
                        }
                    } else {
                        newData.add(item);
                    }
                });
                //转换为json存放
                String jsonAdd = JSON.toJSONString(newData);
                String jsonUpd = JSON.toJSONString(unUpdData);
                //转换数据
                this.addList = JSON.parseArray(jsonAdd, this.clazzT);
                this.updList = JSON.parseArray(jsonUpd, this.clazzT);
                this.existIds = finalExistIds;
            }
        }

多数据表 泛型处理

一张表的数据同步过程已经捋清楚,而远端数据表数量比较庞大,不可能每一张表的数据同步都重复写一遍。那么就可以将数据表向上提炼,提炼出一个父类,父类表包含有子类表的共有属性,比如 id, syncDateTime

/**
 * @ProjectName xxxx
 * @ClassName OrclEntity
 * @Description 父类
 * @Author zhou
 * @Date 2021/6/17 14:02
 * @Version 1.0
 */
public abstract class OrclEntity {
    //提到父类的公共方法
    public abstract String getId();

    public abstract void setId(String id);

    public abstract void setSyncDatetime(Date date);
}

这样在ForkJoinTask的基础上,增加了泛型以后,对应的构造函数和Task类做了一些改变。

class SyncOrclTaskN<T> extends RecursiveTask<ConcurrentMap<String, Object>> {
        //单个任务处理数据量
        private static final int part = 10;
        //远端数据
        private List<OrclEntity> remoteList;
        //新增数据
        private List<T> addList;
        //更新数据
        private List<T> updList;
        //ids
        private List<String> idList;
        //存在数据id
        private List<ExistsId> existIds;
        //同步时间
        private Date syncDate;
        //全表扫描
        private Boolean overAll = false;
        //是否开启数据同步
        private Boolean locate;
        //dao
        private BaseRepository bRepos;
        //泛型
        private Class<T> clazzT;
        //锁
        final ReentrantLock lock = new ReentrantLock();

public SyncOrclTaskN(Class<T> clazz, List<OrclEntity> remotes, Date date, BaseRepository repository, boolean locate) {
            /**
             * 初始化参数
             */
            this.syncDate = date;
            this.locate = locate;
            if (this.syncDate == null) {
                //时间为空,全表扫描
                this.overAll = true;
            }
            this.remoteList = Collections.synchronizedList(remotes);
            this.bRepos = repository;
            this.clazzT = clazz;
        }
}

至此,整个数据同步的逻辑与过程就梳理完了,剩下的就是在此基础上,增加多数据源了。关于多数据源的引入就不在这里展开了。

参考与引用

blog.csdn.net/SunStaday/a…