多线程导入 数据分片 并发访问

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/huyunqiang111/article/details/95478901
1.队列与阻塞队列

    阻塞队列可以阻塞,非阻塞队列不能阻塞,只能使用队列wait(),notify()进行队列消息传送。而阻塞队列当队列里面没有值时,会阻塞直到有值输入。输入也一样,当队列满的时候,会阻塞,直到队列不为空

                                      

当阻塞队列是空时,从队列中获取元素的操作将会被阻塞,当阻塞队列是满时,往队列中添加元素的操作将会被阻塞.

2.使用阻塞队列的好处与作用
     使用非阻塞队列的时候有一个很大问题就是:它不会对当前线程产生阻塞,那么在面对类似消费者-生产者的模型时,就必须额外地实现同步策略以及线程间唤醒策略,这个实现起来就非常麻烦。但是有了阻塞队列就不一样了,它会对当前线程产生阻塞,比如一个线程从一个空的阻塞队列中取元素,此时线程会被阻塞直到阻塞队列中有了元素。当队列中有元素后,被阻塞的线程会自动被唤醒(不需要我们编写代码去唤醒)。这样提供了极大的方便性。
3.三高demo系统解读
     三高系统是一个利用多线程来进行导入 ,查询 等操作的一个系统,虽然只有简单的单元测试但是相比较一些小的demo的例子而言,它很完整的展示了在大数据量的面前我们如何能做到更好的使用多线程,是一个进阶系统,利用写的一些示例来完成对一些比较复杂的多线程的方法的使用! 希望大家能够多多参加进来,完善此系统!
 1.数据如何分片?

     分片方法有很多种,这里面我就介绍下,我的分片方法,当然你可以用你的分片方法实现此功能,
     这都无所谓!有兴趣的可以联系我!
     已 point 表为例,取point表的id 最大值按照每个范围区间进行取余预算,然后id从0开始,进行 区间+
     例如区间为1000 则数据为: 0-1000 , 1000-2000 , 2000-3000 最后一位的数字如果有余数则多加1
     详情请看三高系统 queryPointSteps 

 2.表的设计与思考?
 
     因为考虑数据量大,如果利用多线程那么当其中一个发生错误则可能全军覆没,所以想设计成批次,
     如果一个数据发生错误那么直影响这个一批次的数据量
     id为3的发生错误则,只影响0-1000其他批次继续
     表分为: import_data_task 记录任务 import_data_step 记录任务批次 import_xxx_日期 记录导入数据 详情见表sql

 3.如何实现高可用?
 
     在三高系统中有重试机制,因为每次定时任务每次查询的为未开始和发生错误的数据,
     那么无论多少次数据都是可以不断的去查询去重试,
     任务结束后,又发生错误的数据会继续重试,但是只会重试当前批次,不会影响其他批次,实现了高可用!
     
 4.如何实现高性能?
     利用批次导入,再多线程的情况下会比单线程导入数据更加快速,提升性能大概几倍左右,系统给出了利用多线程的countdowmlatch,线程池,future ....来进行导入 让大家理解更透彻

 5.如何实现高可靠?
     错误批次发生错误会不断重试,不会影响其他批次数据,在发生异常后,会有报警系统,让系统管理者即时处理!
 6.如何自定义线程池以及使用与导入 如何利用CountDownLatch使用与导入
@Service
public class TimerRunner2 {

    public static final Logger logger = LoggerFactory.getLogger(TimerRunner2.class);
    @Autowired
    private HighImportDataService highImportDataService;

    @Autowired
    private ImportDataTaskDao importDataTaskDao;

    @Autowired
    private ImportDataStepDao importDataStepDao;

    private static final int IMPORT_TASK_AND_STEP_RETRY_TIME = 5;


    /**
     * ThreadFactoryBuilder是一个Builder设计模式的应用,可以设置守护进程、错误处理器、线程名字
     * <p>
     * guava
     */
    private ExecutorService executorService = new ThreadPoolExecutor(2, 2,
            10L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(),
            new ThreadFactoryBuilder().setNameFormat("高可用改造hu专用线程池-%d").build());

    /**
     * 跑任务
     */
    public void timeGo() {


        logger.info("billNewRunnerImport start 导入step  and task 数据开始 2 !");
        long start = System.currentTimeMillis();
        Date date = new Date();
        String day = DateUtil.format(date, DateUtil.DATE_FORMAT_DAY_SHORT);
        /**
         * ======= 如果 今天的数据 存在 则无需重复导入 重试 5次 ====
         */
        boolean flagImport = false;
        int importTaskAndStepTime = 0;
        while (importTaskAndStepTime < IMPORT_TASK_AND_STEP_RETRY_TIME && !flagImport) {
            boolean todayIsTryImport = importDataTaskDao.queryTodayTaskIsImport(day) > 0;
            if (todayIsTryImport) {
                logger.info("今日{}数据已导入importdataTask 和 importdataStep 表 2", day);
            } else {
                highImportDataService.recordHandle(day);
            }
            flagImport = todayIsTryImport;
            importTaskAndStepTime++;
        }
        if (flagImport) {
            boolean recordHandleImport = false;
            int recordHandleImportStepTime = 0;
            /**
             * ========  如果 没有未导入与导入失败的则无需重试 2=======
             */
            while (recordHandleImportStepTime < IMPORT_TASK_AND_STEP_RETRY_TIME && !recordHandleImport) {
                recordHandleImport = importDataStepDao.queryTodayStepIsImportSuccess(day) == 0;
                if (recordHandleImport) {
                    logger.info("import_x_x_x 该数据已导入完毕2 ====!");
                } else {
//                    highImportDataService.recordHandleImport(day);
                    List<ImportDataTask> importDataTasks = importDataTaskDao.queryTaskDatas(Constant.IMPORTTYPE.point.name(), day);
                    logger.info("每天需要处理 importDataTasks size : {}", importDataTasks.size());

                    final CountDownLatch latch = new CountDownLatch(importDataTasks.size());
                    //执行的任务数量也就是并发的线程数
                    for (ImportDataTask importDataTask : importDataTasks) {

                        executorService.execute(new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    /**
                                     * 返回结果失败结果重试10次
                                     */
                                    final Integer RetryTime = 10;
                                    int handleTime = 0;
                                    while (handleTime < RetryTime) {
                                        List<ImportDataStep> steps = importDataStepDao.queryAllStepNotDealAndFail(importDataTask.getId(),
                                                day, Constant.IMPORTTYPE.point.name());
                                        if (CollectionUtils.isEmpty(steps)) {
                                            break;
                                        }
                                        logger.info("point handletime 2 重试!,{}", handleTime);
                                        highImportDataService.insertPointTaskStep(steps);
                                        handleTime++;
                                    }
                                } finally {
                                    latch.countDown();
                                }

                            }
                        });
                    }
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        //被打断的可能性很小
                        logger.error("当前线程被打断!", e);
                    }
                }
            }
        } else {
            logger.info("task and step 未导入!");
            return;
        }
        long end = System.currentTimeMillis();
        logger.info("导入生成全部 task .step import_x_x_x 需要时间 !time  = {}", end - start);
    }


}
//这个方法是保证数据的一致性 
@Override
    public void recordHandle(String day) {

        System.out.println("111111");
        ImportDataTask pointTask = this.queryPointTask(day);
        //将point 表中的数据分片
        List<ImportDataStep> pointSteps = this.queryPointSteps(pointTask);

        /**
         * 保证数据一致性
         */
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        TransactionStatus transaction = transactionManager.getTransaction(definition);
        try {
            /**
             * 插入 task表
             */
            importDataTaskDao.insert(pointTask);

            /**
             * 插入step 表
             */
            for (ImportDataStep stat : pointSteps) {
                stat.setTaskId(pointTask.getId());
            }
            importDataStepDao.insertBatch(pointSteps);
            transactionManager.commit(transaction);
        } catch (Exception e) {
            logger.error("数据插入失败!!回滚!", e);
            transactionManager.rollback(transaction);
        }
    }
    /**
     * ==============================================
     * 批量导入
     * @param steps
     */
    @Override
    public void insertPointTaskStep(List<ImportDataStep> steps) {
        logger.info("批量导入++++++++++++++++++++++++++++++++=");
        //返回结果
        for (ImportDataStep step : steps) {
            try {
                logger.info("start  账单 point 批量插入处理! " + step.getRangeStart() + " end :" + step.getRangeEnd());
                List<Point> points = pointDao.queryAllByPointId(step.getRangeStart(), step.getRangeEnd());
                String yMonth = DateUtil.formatCompactMonthDate(DateUtil.parse(step.getDay()));
                /**
                 * 事务保证批次和失败成功状态对应上
                 */
                DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
                TransactionStatus transaction = transactionManager.getTransaction(definition);
                try {
                    if (!CollectionUtils.isEmpty(points)) {
                        logger.info("start insert batch !");
                        int flag = importPointDao.insertBatch(points, "import_point_" +
                                yMonth, step.getDay(), new Date());
                        if (flag > 0) {
                            importDataStepDao.updateByStepId(step.getId(), Constant.IMPORT_SUCCESS, new Date());
                            importDataTaskDao.updateByTaskId(step.getTaskId(), Constant.IMPORT_SUCCESS, new Date());
                        } else {
                            importDataStepDao.updateByStepId(step.getId(), Constant.IMPORT_FAIL, new Date());
                            importDataTaskDao.updateByTaskId(step.getTaskId(), Constant.IMPORT_FAIL, new Date());
                        }
                    } else {
                        importDataStepDao.updateByStepId(step.getId(), Constant.IMPORT_RANGE_NO_DATA, new Date());
                        importDataTaskDao.updateByTaskId(step.getTaskId(), Constant.IMPORT_RANGE_NO_DATA, new Date());
                        logger.info("point  start" + step.getRangeStart() + "end :" + step.getRangeEnd() + "+ 范围无数据!!");
                    }
                    transactionManager.commit(transaction);
                } catch (Exception e) {
                    logger.error("数据插入失败!!回滚!", e);
                    transactionManager.rollback(transaction);
                }
                logger.info("start  " + step.getRangeStart() + " end :" + step.getRangeEnd());
            } catch (Exception e) {
                logger.error("数据插入失败!!回滚!此次批次失败的start! {},{},继续下一批次start ", step.getRangeStart(), e);
            }
        }
    }
//将point 表中的数据分片    
        public List<ImportDataStep> queryPointSteps(ImportDataTask importDataTask) {
        long maxPointId = pointDao.getMaxPointId();
        logger.info("start -------->> pointSteps拼接开始!");
        long index;
        if (maxPointId % Constant.limitNew != 0) {
            index = maxPointId / Constant.limitNew + 1;
        } else {
            index = maxPointId / Constant.limitNew;
        }
        List<ImportDataStep> lists = new ArrayList<>();
        for (int i = 0; i < index; i++) {
            ImportDataStep step = new ImportDataStep();
            step.setCreateTime(new Date());
            step.setDay(importDataTask.getDay());
            //mysql between 去除 重复数据
            if (i == 0) {
                step.setRangeStart((long) i * Constant.limitNew);
            } else {
                step.setRangeStart((long) i * Constant.limitNew + 1);
            }
            //获取 最后一条
            if (i == index - 1) {
                step.setRangeEnd(maxPointId);
            } else {
                step.setRangeEnd((long) (i + 1) * Constant.limitNew);
            }
            step.setMsg("point 表数据 index :" + index + "起始位置" + step.getRangeStart() + "结束位置" + step.getRangeEnd() + "天数" + importDataTask.getDay());
            step.setTaskId(importDataTask.getId());
            step.setVersion(1);
            step.setUpdateTime(new Date());
            step.setStatus(Constant.IMPORT_NOT_DEAL);
            step.setType(Constant.IMPORTTYPE.point.name());
            lists.add(step);
        }
        return lists;
    }
  1. 并发包中常用的类

CountDownLatch

  CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。主要应用利用CountDownLatch和线程池来进行导出功能。

图 CountDownLatch运行流程图

假设计数器的值为3,线程A调用await()方法之后,当前线程就进入了等待状态, 之后在其他线程中执行countDown(),计数器就会 - 1 ,该操作线程继续执行,当计数器从3变成0之后,线程A继续执行。

CountDownLatch这个类可以阻塞线程,保证线程在某种特定的条件下,继续执行。

Semaphore

图 Semaphore概念流程

  Semaphore翻译成字面意思为 信号量,Semaphore可以阻塞进程并且控制同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

  CountDownLatch与Semaphore在使用时,通常会与线程池配合使用

  Semaphore适合控制并发数,CountDownLatch比较适合保证线程执行完后再执行其他处理,因此模拟并发时,使用两者结合起来是最好的。

同步组件概览

  • CountDownLatch:是闭锁,通过一个计数来保证线程是否需要一直阻塞
  • Semaphore:控制同一时间,并发线程的数目
  • CyclicBarrier:和CountDwonLatch相似,能阻塞线程
  • ReentrantLock
  • Condition:使用时需要ReentrantLock
  • FutureTask

CountDownLatch还提供在指定时间内完成的条件(超出时间没有完成,完成多少算多少),如果等待时间没有完成,则继续执行。通过countDownLatch.await(int timeout,TimeUnit timeUnit);设置,第一个参数没超时时间,第二个参数为时间单位

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    test(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await(10, TimeUnit.MILLISECONDS);
        //线程未完成,就可以输出以下信息
        log.info("finish");
        //执行关闭线程池,内部先把所有正在工作的线程完成后,再关闭
        exec.shutdown();
    }

CyclicBarrier也是一个同步辅助类,它允许一组线程相互等待, 直到到达某个公共的屏障点(common barrier point ),也称之为栅栏点。通过它可以完成多个线程之间相互等待,只有当每个线程都准备就绪后,才能各自继续进行后面的操作。它和CountDownLatch有相似的地方,都是通过计数器实现。当某个线程调用await()方法之后,该线程就进入等待状态,而且计数器是执行加一操作,当计数器值达到初始值(设定的值),因为调用await()方法进入等待的线程,会被唤醒,继续执行他们后续的操作。由于CyclicBarrier在等待线程释放之后,可以进行重用,所以称之为循环屏障。它非常适用于一组线程之间必需经常互相等待的情况。

SimpleDateFormat 与 JodaTime

CopyOnWriteArrayList

    CopyOnWriteArrayList相比于ArrayList是线程安全的,从字面意思理解,即为写操作时复制。CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。

CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。 这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。

public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 添加元素的操作
    public boolean add(E e) {
        // 获得锁
        final ReentrantLock lock = this.lock;
        //上锁
        lock.lock();
        try {
            Object[] elements = getArray();//获得当前的数组
            int len = elements.length;//获取数组长度
            //进行数组的复制
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //添加新元素
            newElements[len] = e;
            //引用指向更改
            setArray(newElements);
            return true;
        } finally {
            //解锁
            lock.unlock();
        }
    }

}

 CopyOnWriteArraySet

 ConcurrentHashMap        

AtomicInteger
AtomicLong
LongAdder
    /**
     * AtomicReference 所提供的某些方法可以进行原子性操作,如compareAndSet、getAndSet,
     * 这仅仅是对引用进行原子性操作
     * AtomicReference  不能保证对象中若存在属性值修改是线程安全的,如假设引用对象是person,
     * 修改person中name和age,多个线程同时从引用中获得对象,并进行修改,会出现线程不安全情况。
     * 下面我们通过代码来验证一下这条结论。
     *  在AtomicReferenceTest 中测试了在高并发的情况下不是源自操作

     */

    private static AtomicReference<Integer> count = new AtomicReference<>(0);

    public static void main(String[] args) {
        count.compareAndSet(0, 2); // 2
//        count.compareAndSet(0, 1); // no
//        count.compareAndSet(1, 3); // no
        count.compareAndSet(2, 4); // 4
//        count.compareAndSet(3, 5); // no
        log.info("count:{}", count.get());
    }
//AtomicIntegerFieldUpdater 原子性的更新某一个类的实例的指定的某一个字段
//并且该字段由volatile进行修饰同时不能被static修饰
//有些网上说而且不能被private修饰?下文将进行验证

/*
 *  AtomicLong是作用是对长整形进行原子操作。而AtomicLongArray的作用则是对"
 *  长整形数组"进行原子操作,根据索引,对数据中的指定位置的数据进行院子性的更新
 */

synchronized 和 lock 区别 

    1.synchronized 是关键字属于JVM层面 
     Java -c monitorenter (底层是通过monitor对象来完成的 
     其实wait和notify等方法也依赖于monitor对象只有在同步块或方法中才能调用wait和notify等方法)  
     monitorenter 
    
    lock是具体类java.util.concurrent.locks.lock 是 api层面的锁
    
    2.synchronized 不需要用户去手动释放锁,当synchronized 代码执行完后系统会自动让线程释放对锁的占用
      ReentrantLock 则需要用户去手动释放锁若没有主动释放锁就有可能导致出现死锁现象 需要lock和unlock tryfinally 语句块来完成
      
    3.等待是否可中断
    
    synchronized不可中断除非抛出异常或者正常运行完成
    ReentrantLock 可中断 1.设置超时时间 trylock(long timeout,TimeUnit unit)
                        2.lockInterruptibly() 放代码块中调用interrupt()方法可中断
      
    4.加锁是否公平
    
      synchronized 非公平锁
      ReentrantLock 两者都可以,默认非公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁
      
    5.锁绑定多个条件condition
    
    synchronized 没有
    ReentrantLock 用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个要么全部唤醒

 

猜你喜欢

转载自blog.csdn.net/huyunqiang111/article/details/95478901
今日推荐