精华内容
下载资源
问答
  • 最近在工作时,有一位同事遇到了一个查询优化问题,过来向我请教。 问题大致是这样:要给前端提供一个查询的接口,而在此接口中要去调用远端10几个ESB接口,由于要调用远端ESB接口数量过多导致此接口响应...

    最近在工作时,有一位同事遇到了一个查询优化的问题,过来向我请教。

           问题大致是这样的:要给前端提供一个查询的接口,而在此接口中要去调用远端的10几个ESB接口,由于要调用的远端ESB接口数量过多导致此接口响应异常缓慢,甚至于达到了十几秒钟。现在要想办法去优化这个接口。这十几个远端的ESB接口不用关心调用顺序,不需要互相依赖。

           针对于这个问题,我其实第一反应想到的是采用线程池,实现多线程调用,但同事告诉我说组长要求,先不要考虑使用线程池,因为线程池涉及到线程回收、维护等各种情况可能会在长时间运行后出现不可控的结果。

           第二反应我想到的是采用非阻塞的方式掉用这10几个远端的ESB接口,但是了解后发现非阻塞的在java中主要支持的是NIO和socket通讯,所以这种方式也不行。

           最后我想到的是,既然首选不能是线程池,那么能不能简单粗暴的采用多线程的模式?这样线程执行完毕后会自我销毁,就不用考虑回收维护等这些负责的情况了。开启多个线程让不同的线程去调用不同的远端ESB接口,在所有的线程都执行完毕之后再进行最终查询结果的整合。答案是可行的,直接采用Java自带的CountDownLatch + 多线程 去实现。具体实现过程大致如下(只是一个模拟过程,公司代码有规定不能外泄):

    public class ThreadTest {
    
        //模拟不同的线程 返回不同的处理结果
        private Map map1 = new HashMap();
        private Map map2 = new HashMap();
        private Map map3 = new HashMap();
    
        /**
         * 主线程
         */
        @Test
        public void test() {
    
            //开启线程个数
            int threadCount = 3;
            //所有线程阻塞,然后统一开始
            CountDownLatch begin = new CountDownLatch(1);
            //主线程阻塞,直到所有分线程执行完毕
            CountDownLatch end = new CountDownLatch(threadCount);
            //开始多线程
            begin.countDown();
            for (Integer i = 0; i < threadCount; i++) {
                if (i == 0){
                    Runnable runnable = dealSomeThing1(i, end,map1);
                    new Thread(runnable).start();
                }
                if (i == 1){
                    Runnable runnable = dealSomeThing2(i, end,map2);
                    new Thread(runnable).start();
                }
                if (i == 2){
                    Runnable runnable = dealSomeThing3(i, end,map3);
                    new Thread(runnable).start();
                }
            }
    
            //多个线程都执行结束
            try {
                end.await();  //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
                System.out.println("多个线程都执行结束,可以做自己的事情了");
                String result1 = (String) map1.get("test");
                System.out.println(result1);
                String result2 = (String) map2.get("test");
                System.out.println(result2);
                String result3 = (String) map3.get("test");
                System.out.println(result3);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("多线程执行中出错了,凉凉了!!!");
            }
        }
    
    
        /**
         * 工作线程
         * 本方法  是在构造多线程要做的事情
         * <p>
         * =====================可以做的事===================
         * 当然可以传入ConcurrentHashMap之类的线程安全的 类
         * 来记录线程中的处理结果之类的
         * 最后 在多线程都执行完了以后 就可以对处理结果进行操作了
         * ==================================================
         *
         * @param threadNum 当前线程编号
         * @param end
         * @return
         */
        private Runnable dealSomeThing1(int threadNum, CountDownLatch end,Map map) {
            Runnable runnable = new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    System.out.println("线程" + threadNum + ":--------------------->开始工作");
                    Thread.sleep(3000);//模拟调用目标ESB接口
                    map.put("test","testResult1");
                    end.countDown();  //执行完成后将count值减1
                }
            };
            return runnable;
        }
    
        private Runnable dealSomeThing2(int threadNum, CountDownLatch end,Map map2) {
            Runnable runnable = new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    System.out.println("线程" + threadNum + ":--------------------->开始工作");
                    Thread.sleep(3000);//模拟调用目标ESB接口
                    map2.put("test","testResult2");
                    end.countDown();  //执行完成后将count值减1
                }
            };
            return runnable;
        }
    
        private Runnable dealSomeThing3(int threadNum, CountDownLatch end,Map map3) {
            Runnable runnable = new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    System.out.println("线程" + threadNum + ":--------------------->开始工作");
                    Thread.sleep(3000);//模拟调用目标ESB接口
                    map3.put("test","testResult3");
                    end.countDown();  //执行完成后将count值减1
                }
            };
            return runnable;
        }
    }
    

    对CountDownLatch类的简单介绍如下:

        1、CountDownLatch是在java1.5被引入,跟它一起被引入的工具类还有CyclicBarrier、Semaphore、concurrentHashMap和BlockingQueue。

        2、存在于java.util.concurrent并发包下(所以线程也是安全的)。

        3、CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。

        4、CountDownLatch是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

        5、CountDownLatch类有三个主要的方法:

           //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行

           public void await() throws InterruptedException { };  

           //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行

           public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; 

           //将count值减1

           public void countDown() { }; 

    对于以上模拟的代码,开启了三个线程分别去调用了三个方法dealSomeThing1、dealSomeThing2、dealSomeThing3, 在每个方法执行完毕之后采用end.countDown()方法使count值减1,end.await()方控制当count值为0时(同时也表示所有的子线程已经执行完毕)开始对每个多线程的执行结果进行整合。按照往常的方式调用耗时至少是9秒钟。而按照这种方式耗时大约只有3到4秒钟,时间节约了至少一半。成功解决了此问题。

    运行结果如下:

    展开全文
  • 多线程高并发下,将每个线程处理的数据分别存入redis,线程全部执行完毕再依次从redis取出数据 场景:多线程从数据库中查询数据,每个线程在处理完数据后将数据存入redis;线程全部执行结束后从redis中取出数据,...

    多线程高并发下,将每个线程处理的数据分别存入redis,线程全部执行完毕再依次从redis取出数据

    场景:多线程从数据库中查询数据,每个线程在处理完数据后将数据存入redis;线程全部执行结束后从redis中取出数据,持久化到数据库。
    遇到的问题:高并发下,每个线程的数据在存入redis中时,会遇到数据重复插入以及覆盖的问题
    解决过程:
    1.每个线程的数据存入redis的同一个键中,尝试用加锁的方式解决重复插入和覆盖的问题,失败;
    2.而后尝试每个线程的数据分别存入不同的键中,如何保证每个线程都能单独存入不同的键呢?利用队列即可解决,单个线程处理完,缓存数据到redis时,从队列中取出一个值用作redis的键;下面贴出部分代码:

    创建线程池

            // 拆分成'THREAD_SIZE'个List,每个线程处理一个
            List<List<String>> callableList = ListUtil.averageAssign(tableList, THREAD_SIZE);
            // 简单创建线程池
            ExecutorService executorService = Executors.newFixedThreadPool(THREAD_SIZE);
            FutureTask<String> futureTask = null;
            // 创建一个List,存放futureTask
            List<FutureTask<String>> futureList = new ArrayList<>();
            //创建一个队列,根据线程数量的多少存入值,后续用作redis的键
            LinkedBlockingDeque<Integer> tableQueue = new LinkedBlockingDeque<>();
            for (int i = 1; i <= callableList.size(); i++) {
                tableQueue.add(i);
            }
            // 每个List交由一个线程进行处理
            for (List<String> tables : callableList) {
                // Lambda表达式处理
                futureTask = new FutureTask<>(() -> {
                    return exploreTask(tables,tableQueue);
                });
                executorService.submit(futureTask);
                futureList.add(futureTask);
            }
    

    代码截图
    单个线程的处理逻辑

    		//处理数据的代码,略.......
            //得到处理结果 tableMetadataList
            
            //从队列中弹出一个元素
            Integer redisKey = linkedBlockingDeque.pop();
            //结果数据存入redis中,用队列中弹出的元素作为键
            redisCache(redisKey + "_tableMetadataList", JSON.toJSONString(tableMetadataList));
    

    在这里插入图片描述
    线程全部执行完毕,从redis中取出数据

    		//创建List,存放redis中取出的数据
            List<TableMetadata> tableMetadataList = new ArrayList<>();
            for (int i = 1; i <= THREAD_SIZE; i++) {
                //从redis中取出数据
                List<TableMetadata> temp = JSON.parseArray(redisService.get(i + "_tableMetadataList"), TableMetadata.class);
                if (temp != null) {
                    tableMetadataList.addAll(temp);
                }
            }
    

    在这里插入图片描述
    以上,就是整个场景的处理过程,仅供参考。

    展开全文
  • 背景:最近一直在做DCM相关编程工作,以前项目使用C++居多,所以使用DCMTK开源库,而目前团队使用C#居多,所以需要转向使用fo-dicom库,由于前一篇专栏文章DICOM医学图像处理:利用fo-dicom发送C-Find查询Worklist...

    一、背景:

            最近一直在做DCM相关的编程工作,以前项目使用C++居多,所以使用DCMTK开源库,而目前团队使用C#居多,所以需要转向使用fo-dicom库,由于前一篇专栏文章DICOM医学图像处理:利用fo-dicom发送C-Find查询Worklist在调试过程中需要对DIMSE信息进行手动保存,偶然间发现了dcmtk开源库与fo-dicom开源库在保存dcm文件时使用的方式差异很大,因此决定研究一下,期望通过对比分析来看一下孰优孰劣。

    二、dcmtk与fo-dicom保存文件函数的源码剖析:

    1)dcmtk中DcmFileFormat的saveFile

            saveFile中将文件写入状态划分为四种,即ERW_init 、ERW_ready、ERW_inWork、ERW_notInitialized四个状态。针对不同的状态进行不同的处理,因此可以认为dcmtk中的文件保存使用的是“状态机”方式,这有点类似于我以前用C++写过的一个自解析C++代码的程序,也是通过判别当前的环境来设定不同的状态,从而跳转到不同的操作中 。状态机(State Machine)这种方式在数字电路、编译原理中是很常见的(http://blog.chinaunix.net/uid-14880649-id-3011358.html),只不过dcmtk在文件写入过程中用到的是最直观的状态机而已(http://blog.csdn.net/xgbing/article/details/2784127)。


            状态机可以简单的理解为“特定状态,针对输入字符,发生状态改变,没有额外的行为”,在DCMTK中实现的状态机可能并不是十分符合原始状态机的定义,因为各个状态的跳转并非是按照当前输入来决定的,而是根据DCM文件当前写入的程度(是否可以理解为事件?)来设定各个状态(即ERW_XXX四个状态),不同的阶段分别需要完成格式检查、写入准备、写入、写入完成。

            状态机的编程写法有两种,竖写法——即针对每一种状态下发生的种种事件来进行分类处理;横写法——即针对每一种事件所能引起的状态改变来分类处理(详情参见http://blog.csdn.net/tomsen00/article/details/4932789),dcmtk的文件保存函数中使用的就是“竖写法”,因为各个状态之间有先后顺序,并非无序的跳转,必须先完成前缀(Preamble),然后才能写信息元(MetaInfo)、最后是真正的数据体(Dataset)。另外比较特殊的是:由于DCM文件本身的自包含性,即MetaInfo与Dataset两部分的互嵌套性(可参照我以前的博文http://blog.csdn.net/zssureqh/article/details/9275271),针对于DCM文件的不同部分又分别利用“状态机”来控制各部分的写入确保顺利进行。

    2)fo-dicom中Dicom的Save


            DicomFileWriter类中的同步函数Write内直接发起了异步调用请求,代码如下:

    public void Write(IByteTarget target, DicomFileMetaInformation fileMetaInfo, DicomDataset dataset) {

    EndWrite(BeginWrite(target, fileMetaInfo, dataset, null, null));

    }

            从上图中可以看出这是C#中较早期提出的一种异步编程模型——APM,针对于dcm文件的文件头、文件元信息、文件数据体三个不同的部分,通过异步回调的方式依次进行。这与dcmtk中采用的“状态机”实现的结果是一样的,对于各个状态的有序控制也是相同的,那么两者到底有何区别?fo-dicom既然是对dcmtk的重新封装,这是否就意味着异步回调方式优于状态机方式呢?

    三、dcmtk与fo-dicom保存文件实际检测:

            既然从原理上目前还并未找到很好的解释,或者说还没有完全理解两种方式的本质区别,那么我们先直接从两者的运行结果来实际体验一下两者的区别。

    1)ProcessExplorer

            简单的构造C++控制台程序和C#的控制台程序,分别利用dcmtk的saveFile和fo-dicom的Save,具体代码如下:

    // dcmtk-save-test.cpp : 定义控制台应用程序的入口点。 
    //
    #include "stdafx.h" 
    #include "dcmtk/config/osconfig.h" 
    #include "dcmtk/dcmdata/dctk.h" 
    #include "dcmtk/dcmdata/dcpxitem.h" 
    #include "dcmtk/dcmjpeg/djdecode.h" 
    #include "dcmtk/dcmjpeg/djencode.h" 
    #include "dcmtk/dcmjpeg/djcodece.h" 
    #include "dcmtk/dcmjpeg/djrplol.h"
    int _tmain(int argc, _TCHAR* argv[]) 
    { 
    Sleep(15000); 
    DcmFileFormat mDcm; 
    mDcm.loadFile("d:\\DcmData\\test2.dcm"); 
    mDcm.saveFile("d:\\DcmData\\dcmtk-test2.dcm"); 
    return 0; 
    }


    using System; 
    using System.Collections.Generic; 
    using System.Linq; 
    using System.Text; 
    using System.Threading.Tasks;
    using Dicom; 
    using Dicom.Network; 
    using Dicom.Log; 
    using System.Threading;
    namespace FindSCU1 
    { 
    
    class Program 
    {
    static void Main(string[] args) 
    {
    Thread.Sleep(1000); 
    DicomFile mDcm = DicomFile.Open(@"D:\DcmData\test2.dcm"); 
    mDcm.Save(@"d:\DcmData\fo-test2.dcm"); 
    Console.Read(); 
    } 
    } 
    }

            利用ProcessExplorer观察两种库在保存文件时的实际性能。

            第一是dcmtk开源库的saveFile函数的实际性能分析,


            第二是fo-dicom的Save函数的性能分析,下图中给出了多线程的创建、挂起和终止。



            由此可以看出fo-dicom的Save函数使用了多线程异步编程的模式来完成dcm文件的写入,从性能分析中可以直观的看出来多线程的fo-dicom版本在时间上有所提高,但是在其他的消耗方面有所增加(当然这里的分析不够科学,因为C#的基础运行环境要比C++复杂的多,所以不能简单的归结为fo-dicom采用了APM的结果)

    2)vshost.exe插曲:

            第一次利用ProcessExplorer查看两个模拟程序的性能时,可以正常的看到fo-dicom版本的多个线程(如上面的图所示),但是在第二次调试过程中一直未在ProcessExplorer工具中找到我自己的工程。


            后来发现是由于启动的是vshost.exe调试容器,我们的程序是在该容器中运行的。通过右键打开工程属性,选择“调试”选项,将“启用Visual Studio承载进程”选项去掉,重新编译启动后,即可在ProcessExplorer中顺利看到我们的FINDSCU1.exe进程。


    四、C#中的异步编程模式:

            说实话刚转向C#领域不久,对于内部出现的众多委托和事件绑定,以及复杂的跳转流程还摸不着头绪,主要的是对内部的机制和所产生的性能影响没有一个更清晰的认识,单纯的学习使用委托、事件,以及异步编程的众多模式不难,但是更应该、更难的是要了解微软为什么要给C#增加这么多的新特性?如是,必然有他的理由,要从根本上了解引入该机制的原因才能更好的使用C#的众多新特性。下面简单的介绍一下C#的几种异步编程方式,由于目前还未真正搞清楚模式的本质区别和性能方面的差异,所以就不详细分析了,有兴趣的可以参考下一节中我提出的一些疑问,或许会有一些启发。

            这里引用他人的一句话:所谓模式,其实是一种方法,就如同设计模式一样,是从工程实践中总结出来的解决相似或特定问题的一种惯用手段。常见的异步模式包括:

    1)APM模式:BeginXXX/EndXXX,IAsyncResult

            这就是我们上述提到的fo-dicom版的Save函数中使用的模式,该模式是C#中很早提出的异步模型,比较直接易懂。

    2)EAP模式:基于事件的异步模式,常见的有Windows Form、MethodNameAsync、Event

            该部分模式主要源于C#中提出的新的“事件”的概念,这不同于现实生活中的具体的发生在某一时间和某一空间的事件,其实是对Windows操作系统中消息的再封装(博文http://blog.csdn.net/fan158/article/details/6178392对C#中的事件有一个详细的分析)消息可以理解为操作系统记录运行过程中所发生的真实事件的抽象,可在各个应用程序之间进行传递,而C#的Event对消息的封装使得消息可以与其对应的响应函数绑定在一起,为开发者提供了更大的便利,但是其本质是没有改变的,即当某一事件发生时会触发与事件相绑定的处理函数(这也是计算机底层经常用到的一种实现机制,如中断向量表)。

    EAP模式是针对于Windows From编程而提出的,是对APM模式的改进,使得编程人员操作更方便,且内部也是采用的APM模式来实现的。

    3)TAP模式:基于任务的异步模式,常见的有MethodNameAsync、Task、Task<Result>

    4)C#5.0中的async和await引入的异步编程模式

            对于后两种异步模式接触不多,但是本质的原理是相同的,都需要利用线程池(也可以说多线程)和事件(消息)来配合完成异步操作,后续会进一步的学习。

            最后谈一下自己对异步编程的理解,为什么会出现异步编程?为什么我们要异步?原因只有一个——“效率”,高效利用计算机的各项资源。打个比方,你去饭馆吃饭,点菜的肯定跟做菜的不是同一个人(当然也有这种极端情况哈),为什么要将这两种工作分开呢?原因就是完成两个事情所需要的时间和精力完全不同,点菜简单快速,要求就是准确理解客户要求,而做菜复杂缓慢,要求就是要做出好的口味。如果让做菜的人也负责点菜,只有两种方式,一种就是客户需要等菜做完才能点菜,而这就浪费了客户很长的时间,影响了客户的体验;一种就是做菜的过程中出来听客户点菜,而这就打断了炒菜的节奏,自然菜就没法吃了。无论哪种情况发生,长此以往估计饭馆也离倒闭不远了。这个情景在计算机内部也经常发生,用户的各种操作、各种需求也存在着差异,有的需要进行长时间的运算(垃圾回收、碎片整理),有的则需要快速的响应(例如UI界面),那么在计算机领域,大牛们是怎么解决的呢?像Intel大牛们就致力于提升计算机本身的性能,期望提供无穷的资源来满足用户的各种需求,这就好比饭馆请更多的厨师,当厨师数量永远大于客户数量的时候问题自然就解决啦(当然这种情况真实世界是不会存在的)——基于这种情况,就衍生出了真正的并行编程、并行计算;而像微软的大牛们则致力于发展新的使用模式,期望更高效更合理的利用有限的资源,这就好比先让厨师抽一段时间、一段很短的时间集中接受客人的点菜,然后再拿出更长的时间来做菜,只要时间分配合理,也会使大多数客户满意,再进步一点的话就是花很少的成本请一名专门负责点菜的服务员,负责给客户点菜,如是厨师就可以安心在厨房炒菜啦——针对这种情况,就出现了我们提到的众多异步编程模式,完成端口就很像我所说的饭馆的情况:创建完成端口后,系统会提前开辟部分线程,这与CPU的核心数有关,基本规律是:线程数=2*CPU核心数+1(这就好比请了多名厨师),同时给完成端口分配一个消息队列(这就好比点菜的笔记本);随后系统用一个轻量级的线程快速的接受用户的响应并负责添加到消息队列中(这就好比专门请一名服务员点菜),当消息队列非空时,线程池的线程就会取出其中的消息进行相应的处理操作(这就好比多名厨师按照时间先后顺序来处理菜单)。这样就可以高效的满足用户的大多需求,当然上述两种情况只是真实世界中各种情况的两个极端例子而已,因此在真实的世界中为了提高效率会有更多的方式,例如厨师可以按照菜的种类来分别炒,服务员也可以按照座位的容量来合理安排点菜的顺序,等等……

            然而当厨师数量、服务员数量急剧扩大的时候,彼此之间工作的配合就显得越来越重要,这就好比异步编程中的同步概念,需要协调各个异步运行的线程之间的协作,尤其是访问共享资源时,或者当一个事情的核心步骤需要有序进行的时候,例如fo-dicom的Save函数对dcm的Preamble、MetaInfo和Dataset的保存。为了提高开发者的效率,使得现有的有效的处理事务的模式可以方便快捷的使用,就有了C#中的APM、EAP、Task和async+await等种种异步编程模式。

    五、思考

            从C++转到C#不久,一直对C++情有独钟,要想C++中的完成端口、是否与C#中的线程池有异曲同工之妙,C++的文件流、I/O流与C#的文件流、I/O流有何大的区别,后续要深入分析一下。

    1)线程池与完成端口

            在《CLR via C# 第三版》中指出了,在CLR内部就是通过使用完成端口技术来实现的“线程池”,以前我在进行DCM文件异步传输时专门分析过完成端口的优良性能(参见博文:完成端口学习笔记(一):完成端口+控制台 实现文件拷贝完成端口学习笔记(二):完成端口实现机制的仿真


            完成端口是Windows编程中极力提倡的一种编程方式,通过使用线程池和消息队列来配合,可以高效的实现计算密集型和IO密集型操作。在我的前两篇博文中对“完成端口”有一个详细的剖析,也可参照上一节中列举的例子来简单的了解一下。

    2)异步编程与状态机

            直接给出几篇关于异步编程和状态机的精彩博文:

    http://msdn.microsoft.com/zh-cn/magazine/hh456403.aspx

    http://blog.chinaunix.net/uid-14880649-id-3011358.html

    http://blog.csdn.net/tomsen00/article/details/4932789

            C#5.0引入的async和await新版异步编程模式,其在编译器内部就是将异步编程用状态机来模拟实现。正如上所述,所谓的异步编程模式仅仅就是一种经验的总结,一种惯用手法,更夸张点来说C#从APM到如今的async和await的改进其实都是对经验的总结和以前模式的改进,当然这就需要操作系统和C#语言底层的内嵌支持才能实现,才能给开发者的工作带来方便。

            在此简单的总结一下目前自己的体会,dcmtk单线程的saveFile函数利用状态机来完成使得程序流程清晰、简洁,对于后续代码的维护也提供了便利;fo-dicom的APM异步编程模式,通过启动线程池的方式在实现效率上有了一定的提升,另外也将dcm文件保存划分成了独立而又相关的几个部分,能够分别的检测和捕捉异常,对于程序的安全运行也有一定的帮助。可以说fo-dicom是使用APM异步编程模型来模拟了dcm文件的同步写入,这其中又利用了C#的FileStream流的异步特性而已。关于FileStream类在《CLR via c#》中27.8.8小节中有详细的介绍,大家可以体会一下。

    六、参考资料:

            上面的分析并未真正找到我想要的答案,因此后续还要继续阅读相关的书籍和资料,争取尽快找到想要的结果,写这篇博文的时候主要参考的书籍如下,推荐看先看英文版,随后再看中文版,那样理解可能会更透彻,因为翻译的毕竟不是第一手资料嘛。

    《CLR via C#》(中文第三版)

    《C# 5.0 in a Nutshell》(英文版)


    后续专栏博文介绍:

    1)AETitle在C-FIND和C-MOVE请求中的设置问题

    2)Dicom中的MPPS服务介绍

    3)C#的异步编程模式在fo-dicom中的应用

    作者:zssure@163.com

    时间:2014-09-22

    展开全文
  • 目的:公司最近要求建立对数据库内多表索引文件,来实现对数据库数据处理的全文检索。 表有多个,内容风别是不同表内字段。选用了IK分词和Lucene实现。 多线程创建索引 多区域联合查询索引

    目的:公司最近要求建立对数据库内多表的索引文件,来实现对数据库数据处理的全文检索
    表有多个,内容风别是不同表内的字段。选用了IK分词和Lucene实现。中间封装方法很简单,所以就很简单的掠过了
    步骤基本为:1.数据库提取数据转为已实例化的对象。
    2.将实例化的对象,存索引。此处,是一个字段即为一个Field .基本为:document.add(new StringField(“这里实例化对象的属性(即字段名)”, “字段值”, Store.YES));这里的StringField,即在新版LUC内是只存储不对其分词但将其作为索引的建立索引方式。而TextField则是存储索引,且对其分词且索引。还有StoreField则是只存储不索引不分词。
    3.写提交indexWriter每1000次提交一次。
    先上代码。(很抱歉不大会用这个写博客,就先这么写上了。改天再研究)

    /**
         * 循环多线程创建索引
         */
        @Override
        public void CreateIndexFullSearch() {
            ExecutorService executor = Executors.newCachedThreadPool();
            // 获得所有源表名
            Set<String> tableSourceList = FieldsTranslator.signInfoMap.keySet();
         //因为此处是自己创建的一个翻译类内的内容,MAp(“数据库内的表名(也可以称之为数据库转存对象的对象名)”,“中文翻译,例如基本信息”)
         //此处的遍历循环创建索引,采用线程池,加快其建立索引的速度。
            for (String tablename : tableSourceList) {
                CreateIndexSearch myThread = new CreateIndexSearch(tablename);
                executor.execute(myThread);
            }
            executor.shutdown();
        }
    
    
        public class CreateIndexSearch extends Thread {
    //因为在创建所以的时候需要传表名来获取该表内容的存储位置及该表内字段,但在线程重写的Run方法内是无参的,所以我们需要在内部类创建的时候就传入这个在内部可调用的参。
            private String tableSource;
            /**
             * @param tableSoure
             */
            public CreateIndexSearch(String tableSource) {
                super();
                this.tableSource = tableSource;
            }
            /**
             * @return the tableSource
             */
            public String getTableSource() {
                return tableSource;
            }
            /**
             * @param tableSource
             *            the tableSource to set
             */
            public void setTableSource(String tableSource) {
                this.tableSource = tableSource;
            }
    //上面这部分的构造其实只要有类构造方法就行,但下部的G/S方法涉及到下面的值获取。
            /**
             * 用于创建给定来源表的索引
             * 
             * @param tableSource
             * @throws IOException
             */
            public void run() {
                // 存放路径 此处的存放路径也保存在翻译类的静态map,需要通过表名去获取到这个地址。
                String INDEX_DIR = FieldsTranslator.IndexDirMap.get(tableSource);(“D://Lucnen/不同的表有不同的存储位置”)在不同的分布上采用不同的获取。
                // 分词器 选择IK分词器 中文分词一般更偏向ANSJ,但ANS级对含英文的数字串符的分词对项目而言并不理想。所以在实验了多个分词器后,IK虽然对名字的分词超乎我想象的渣,但正向最大切分还是比较适合项目。
                IKAnalyzer analyzerT = new IKAnalyzer(true);
                // 表内数据  这里是一个封装的方法,旨在获取表内数据的List,这个List一般形式为List<obj> 
                List<?> indexList = getIndexList(tableSource);
                // 表内字段 也是方法,其实是从静态map<"字段名(name)","字段名的中文(姓名)">获取到字段的名字。
                Set<String> propertyName = getpropertyName(tableSource);
                // 标记字段 这是我用来获取表名的翻译maP<"表名(User)","表的中文(用户表)">
                String signInfo = FieldsTranslator.signInfoMap.get(tableSource);
                Directory dir = null;
                try {
                    dir = FSDirectory.open(new File(INDEX_DIR));
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_47,
                        analyzerT);
                IndexWriter indexWriter = null;
                try {
                    indexWriter = new IndexWriter(dir, config);
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                for (int i = 0; i < indexList.size(); i++) {
                    Document document = new Document();
                    document.add(new StringField("归属类", tableSource, Store.YES));
                    document.add(new TextField("标注", signInfo, Store.YES));
                    for (String key : propertyName) {
                        // 判断该搜索域是否需要索引,默认不索引 
                        if (getsearchRegion(key)) {
    //这里的这个是翻译类,就是你表内存的数据可能用一些英文或者数字来标记,比如0和1来表示男和女,这里是将它们转为中文。
                            // 修改需要索引的就诊类型的表达字符,将该值改为汉字表达
                            String Str = getFieldValueByName(key, indexList.get(i))
                                    .replaceAll(" ", "");
                            document.add(new StringField(key, ValueTranslator
                                    .TranslterStr(key, Str), Store.YES));
                        } else {
                            // 修改需要索引的就诊类型的表达字符,将该值改为汉字表达
                            String Str = getFieldValueByName(key, indexList.get(i))
                                    .replaceAll(" ", "");
                            document.add(new TextField(key, ValueTranslator
                                    .TranslterStr(key, Str), Store.YES));
                        }
                    }
                    try {
                        indexWriter.addDocument(document);
    //这里其实是利用IndexWrite的锁机制,让他开闭锁的频率变为每1000个Doc再上提。
                        if (i % 1000 == 0) {
                            indexWriter.commit();
                        }
                    } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
    
    
                }
                try {
                    indexWriter.commit();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                try {
                    log.info(signInfo + "索引文件创建完毕!");
                    indexWriter.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
    
    
            /**
             * 该方法用以获取表内信息并输出
             * 
             * @param tableSource来源表名
             */
            private List<?> getIndexList(String tableSource) {
                List<?> indexList = new ArrayList<>();
                switch (tableSource) {
                case "ClinicalDocumentVo":
                    // 临床信息表内数据
                    indexList = createIndexClinicalDocumentVoDao
                            .getClinicalDocumentVo();
                    break;
                        ****省略
                case "PatientInspectionVoB":
                    // 影像表内数据
                    indexList = createIndexVoBDao.getPatientInspectionVoB();
                    break;
    
                default:
                    log.info("获取" + tableSource + "表内数据时失败,请检查。");
                    break;
                }
                return indexList;
            }
    
    
            /**
             * 私有方法:用于通过属性得到属性值
             * 自己写着玩的一个方法,实例化的一个对象,通过对象属性来获取到这个对象的属性值
             * @param fieldName
             * @param o
             * @return
             */
            private String getFieldValueByName(String fieldName《属性名》, Object o《对象名》) {
                String firstLetter = fieldName.substring(0, 1).toUpperCase();
                String getter = "get" + firstLetter + fieldName.substring(1);
                Method method = null;
                try {
                    method = o.getClass().getMethod(getter, new Class[] {});
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (SecurityException e) {
                    e.printStackTrace();
                }
                Object value = null;
                try {
                    if (method != null) {
                        value = method.invoke(o, new Object[] {});
                    } else {
                        log.info("通过属性名获取属性值时发生错误");
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (IllegalArgumentException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
                return value + "";
            }
    
    
            /**
             * 该方法用于获取表内字段
             *这方法就   ****略
             * @param tableSource
             * @return
             */
    
    
            /**
             * 判断该域是否需要只存储不索引
             * 略****
             * @param
             * @return
             */
            ******
        }

    创建索引就差不多这个样子。上面我注释大概应该算作很详细了。
    下面是索引的搜索部分。

         public ScoreDoc[] FullSearchAll(String text) throws IOException,
                ParseException {
            ScoreDoc[] hitAll = null;
            IKAnalyzer analyzerT = new IKAnalyzer(true);
            // 此处域项暂未添加更多功能 因为索引存在不同的域 我们使用multiReader来同步读取  此处类似分布式索引。
    /*confirmRegion(“”);这个方法就是传出一个打开所有DIR的readerj集合   我在想法曝出,只是因为太多所以就重写了*/
            IndexReader[] reader = confirmRegion(“”);
    //MultiReader 其实以前常用的一般是MultiSearch 自带线程的p**MultiSearch(忘了叫啥来着,请自行百度),但是4.0之后就被干掉了。不过这里的MultiReader其实在速度上得到了很大提升,在处理千万级别索引时,查询速度大概在1秒不到这个样子。这里的分布读取是我需要的。
            MultiReader multiReader = new MultiReader(reader);
            String[] searchRegion = getsearchRegion();
            IndexSearcher isearcher = new IndexSearcher(multiReader);
    //这里的Search其实也有很多,只是比较Luc近期的方法,感觉IndexSearch更适合。
    //这里是多字段多内容的联合查找方式。 
            MultiFieldQueryParser parser = new MultiFieldQueryParser(
                    Version.LUCENE_47, searchRegion, analyzerT);
    //原先是准备在这边运用booleanQuery等不同类型的Query做一个不同程度的索引 ,其实也是已经作出了包括OCCMust及OCCShould等方式的维度查询,还有RangeQuery的时间节点筛选和值筛。但都被客户一句话干掉了(哦发火 谢特) 然后这里就是阉割版的。
            Query queryShould = parser.parse(text.replaceAll("[\\pZ]|[\\pP]", ""));
            hitAll = isearcher.search(queryShould, null, 1000).scoreDocs;
            return hitAll;
        }
    
    
        /**
         * 优先匹配基本信息的全文检索
         * 
         * @param 关键字
         *            页码 每页显示数目
         */
        @SuppressWarnings("unchecked")
        public List<FullSearchInfo> FullSearch(String text, String pageNum,
                String pageSize) throws IOException, ParseException {
            List<FullSearchInfo> tableinfo = new ArrayList<FullSearchInfo>();
            IndexReader[] reader = confirmRegion(allPath);
            MultiReader multiReader = new MultiReader(reader);
            IndexSearcher isearcher = new IndexSearcher(multiReader);
            ScoreDoc[] b = FullSearchAll(text);
            ScoreDoc[] hitAll = null;
            hitAll = b;
            // 查询分页 分页这部分其实我并不喜欢整在外头,但是,客户说,我这一段太长了。你特么闲着没事看啥源码? 出于。。。。我在获取查询总条数时加了一个浪费小效率的二次查询的小改动。等二版改效率再见。
            int start = (Integer.parseInt(pageNum) - 1)
                    * Integer.parseInt(pageSize);
            int end = Integer.parseInt(pageNum) * Integer.parseInt(pageSize);
            for (int i = start; i < end; i++) {
            *******后台输出****因为涉及到部分东西,然后就不显示了。
        }

    以上,其实已经差不多了吧?
    嗯,其实Luc经过这么多次改版,其方法改变了很多,也是花了几个晚上统统试了一遍,当然也有部分方法看着API都没能搞掂,也是醉的不行。
    可能理解有些浅显,一想对初学的人可以是一个小参考。所以还是写了,当然,相较于打字更喜欢动笔的我写的很少,不懂就私信吧。
    QS568356@163.com 回复邮件分时间。着急可以红名

    展开全文
  • Redis单线程与慢查询

    千次阅读 2019-02-26 13:19:31
    同时需要注意的是单个Redis实例的16个数据库的操作也都是共享这个单线程的,所以在设计时,如果16个数据库或者个都要存放数据并且读写较频繁,则推荐采用独立的Redis实例来保存各个数据库的数据,即使用不同的端口...
  • 多线程socket编程2B设计

    千次阅读 2012-06-04 16:57:12
    最近做一个local socket(domain socket)服务器,来进行进程间通信,使用线程池管理线程不同的客户端连接... 在设计QUERY_CAPABILITY(能力查询)的时候,犯了一个2B的错误... 现在假设我有两个客户端C1, C2, ...
  • 多线程之创建工作者线程和用户界面线程区别  1、工作者线程倾向于琐碎的处理,与它不同的是,用户界面线程具有自己的界面而且实际上类似于运行其他应用程序。创建线程而不是其他应用程序的好处是线程可与...
  • 建立一个多线程服务器端,能够处理不同客户端连接,以查询客户端发送指令内容。假设客户端可以向服务器端查询一人向另一人发送短消息,发送指令格式如*#MSG#*from|to,则服务器端实现:import java.io....
  • 2.部分需要支持多线程的service类已增加@scope("prototype"),从debug可以看到service获取到的已是不同的对象(service是线程类,获取方式通过spring上下文获取)。 3.service中注解的mapper是单例的,运行过程中...
  • 同时需要注意的是单个Redis实例的16个数据库的操作也都是共享这个单线程的,所以在设计时,如果16个数据库或者个都要存放数据并且读写较频繁,则推荐采用独立的Redis实例来保存各个数据库的数据,即使用不同的端口...
  • 1、关于登录图片验证码,在非docker环境,共享session情况,服务器存储的是request中的param中,docker环境下,每次请求可能被不同的服务器处理,导致错误。最终使用通过本地cookie存储key_,然后存储/查询redis中的...
  • 三者相依而生,但是侧重点不同。 分布式:(物理资源角度) ...垂直拆分:,前端有多种需求查询时,一台机器不够用,可以将不同的需求分发到不同的机器上,比如A机器处理余票查询,B机器可以处理支付...
  • 分布式、高并发与多线程分布式实现形式高并发多线程总结 分布式 解决单个物理服务器容量和性能瓶颈问题而采用优化手段。 涉及:分布式文件系统、分布式缓存、分布式数据库、分布式计算等,一些名词如Hadoop、...
  • 程序从properties中读取参数后启动线程去数据库查询任务表,任务表里分几类,这个进程只针对某一类任务执行,而这个进程里的线程是分块处理这一类任务。这个程序可以针对不同的任务类去配置不同的properties...
  • 1、单引导进程,单个备用进程 ...特点:更好均衡使用工作进程,但是不能控制不同事务之间顺序,只能把要求顺序事务封装到一个事务中, 4、轮流分派工作进程 给工作进程分配序号队列,依次分配工
  •   ...所以我就用了多线程查询。然后就出现了每个子线程运行时间递增情况,单个子线程运行时间大概是240毫秒左右,不知道是什么原因导致。</strong></p>  //下面是我代码 ...
  • JMH 可以进行代码性能测试。进行方法吞吐量测试。 QPS,TPS QPS:是Queries Per Second缩写,意思是每秒查询率(最大吞吐能力) TPS:是Transactions Per Second缩写,也就是事务数/...已以往队列结构不同,使...
  • DM8线程

    2020-12-30 15:37:15
    DM数据库实例在运行时由各种内存数据结构和一系列的线程组成,不同类型的线程完成不同的任务。线程通过一定的同步机制对数据结构进行并发访问和处理,以完成客户提交的各种任务。 DM 数据库服务器是共享的服务器,...
  • 1.分布式 分布式,是为了解决单个物理服务器容量和性能瓶颈问题而采用的优化手段。...  **垂直拆分:**前台有多重查询需求时,一台机器无法处理,可以将不同的查询需求分发到不同的机器上,比如A机器处
  • 物理cpu数量,在Linux上查看/proc/cpuinfo,其中的physical id就是每个物理CPU的id,有几个不同的physical id就有几个物理CPU。 grep 'physical id' /proc/cpuinfo|sort|uniq|wc -l cpu核数: 每颗物理CPU可以有1...
  • 多核处理器遵循以LLC(Last Level Cache,最后一级cache)大小为中心优化技术,而众核处理器,如Phi、GPU协处理器,则采用较小cache并以更多的硬件级线程来掩盖内存访问延迟设计。随着处理核心数量增长,...
  • 这个一句话说明就是:定时器并不是多线程的. 2,while循环加sleep延时作用效果和定时器是否一样,它们有什么区别; 一般很少将Sleep与while写在一起,Sleep作用是线程暂停,此时线程得不到处理时间.处于休眠状态....
  • Java早已经为我们提供了多线程的API,但是实现方式略微麻烦,今天我们就来看看Java8在这方面提供改善。 假设场景 现在你需要为在线教育平台提供一个查询用户详情API,该接口需要返回用户基本信息,标签信息,...
  • 问题背景: ... 同一个客户不同订单并发处理时,两个线程可能同时获取到余额都是10000,而更新余额时后面更新操作会覆盖前面更新操作,即实际只扣除后一笔记录费用,而前一笔记录扣费标...
  • 和Node环境中不同的 Event Loop。 目录 <ul><li>不同环境中,这段代码的运行结果相同吗?</a></li><li><a href="#single-thread">记住,JS是单线程的</a></li> <h4><a href="#browsing-context...

空空如也

空空如也

1 2 3 4 5 ... 14
收藏数 277
精华内容 110
关键字:

多线程处理不同的查询