Elasticsearch+Dubbo+Spring实践

1. 需求

因为公司一直用的是阿里的dubbo作为业务工程的RPC通信框架,我现在的任务是在mentor的指导下尝试重构公司的NLP服务,想把NLP的资源检索,单轮对话,多轮对话这些功能模块拆分成各自独立的服务。第一个尝试是优化问答系统中信息检索的过程,所以拿到的需求是调研用Elasticsearch来代替已有的资源检索性能会不会更好。这样也就产生了Elasticsearch+Dubbo+Spring这样奇葩的组合方式,毕竟Elasticsearch提倡的是用RESTful API交互来简化对数据的操作,而我要硬生生地用RPC的方式去调用。


想想都刺激

文章结构

  1. 需求
  2. Dubbo框架搭建
  3. Elasticsearch功能添加
  4. 源码

2. Dubbo框架搭建

环境准备

  • Zookeeper注册中心安装,下载
解压
cd your_zookeeper_path/conf
cp zoo_sample.cfg zoo.cfg
vi zoo.cfg

根据自己的需要修改配置文件,参见,这里Zookeeper的端口号采用默认配置2181。

  • Zookeeper启动:
cd your_zookeeper_path
./bin/zkServer.sh start
  • Zookeeper停止:
cd your_zookeeper_path
./bin/zkServer.sh stop

服务提供者Demo

  1. 在项目中添加Spring,Dubbo,Zookeeper的依赖:
<dependencies>
        <!-- spring -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <classifier>jdk15</classifier>
            <version>2.4</version>
        </dependency>
        <!-- dubbo -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.5.3</version>
            <exclusions>
                <exclusion>
                    <artifactId>spring</artifactId>
                    <groupId>org.springframework</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- zkclient  -->
        <dependency>
            <groupId>com.github.sgroschupf</groupId>
            <artifactId>zkclient</artifactId>
            <version>0.1</version>
        </dependency>
        <!--  zookeeper -->
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.3.6</version>
        </dependency>
    </dependencies>
  1. 新建服务提供者对外暴露的接口类:
public interface SearchService {
    public String sayHello(String name);
}
  1. 实现服务的接口:
public class SearchServiceImpl implements SearchService{
    public String sayHello(String name) {
        System.out.println("received from remote: "+name);
        return "Hello " + name;
    }
}
  1. 用 Spring 配置声明暴露服务:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://code.alibabatech.com/schema/dubbo
        http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <!-- 提供方应用信息,用于计算依赖关系 -->
    <dubbo:application name="elastic-search"/>

    <dubbo:registry protocol="zookeeper" address="127.0.0.1:2181" />

    <!-- 用dubbo协议在20880端口暴露服务 -->
    <dubbo:protocol name="dubbo" port="20880"/>

    <!-- 声明需要暴露的服务接口 -->
    <dubbo:service interface="com.yyz.elasticsearch.SearchService" ref="demoService" />

    <!-- 和本地bean一样实现服务 -->
    <bean id="demoService" class="com.yyz.elasticsearch.SearchServiceImpl" />

</beans>
  1. 启动服务提供者:
public class Starter {
    public static void main(String[] args) throws IOException {
        System.setProperty("java.net.preferIPv4Stack", "true");
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"server.xml"});
        context.start();
        System.in.read(); 
    }
}

服务消费者Demo

  1. 在pom.xml中添加spring,dubbo,zookeeper的依赖:
<dependencies>
        <!-- spring -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>1.5.1.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <classifier>jdk15</classifier>
            <version>2.4</version>
        </dependency>
        <!-- dubbo -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.5.3</version>
            <exclusions>
                <exclusion>
                    <artifactId>spring</artifactId>
                    <groupId>org.springframework</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- zkclient  -->
        <dependency>
            <groupId>com.github.sgroschupf</groupId>
            <artifactId>zkclient</artifactId>
            <version>0.1</version>
        </dependency>
        <!--  zookeeper -->
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.3.6</version>
        </dependency>
    </dependencies>

2.通过 Spring 配置引用远程服务:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://code.alibabatech.com/schema/dubbo
        http://code.alibabatech.com/schema/dubbo/dubbo.xsd
        ">
    <!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
    <dubbo:application name="demo-consumer"/>

    <dubbo:registry protocol="zookeeper" address="127.0.0.1:2181" />
    
    <!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
    <dubbo:reference id="demoService" check="false" interface="com.yyz.elasticsearch.SearchService"/>

</beans>
  1. 加载Spring配置,并调用远程服务:
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Client {
    public static void main(String[] args) {
        String configName = "client.xml";
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{configName});
        context.start();
        SearchService searchService = (SearchService) context.getBean("demoService");

        while (true) {
            try {
                Thread.sleep(1000);
                // 执行远程方法
                String hello = searchService.sayHello("world");
                // 显示调用结果
                System.out.println( hello );
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }

    }
}

好啦,到这里用阿里的dubbo框架搭建的RPC通信的服务提供者和消费者已经完成,可以运行起来看看效果,记得先启动zookeeper注册中心。

3. Elasticsearch功能添加

下面我们往上面的Demo中添加操作Elasticsearch分布式搜索分析引擎的功能。

Elasticsearch安装

Elasticsearch是免安装的,下载后解压就好。关于下载版本的问题,我想发一个表情。


此处有好大一个坑。
sping data elasticsearch和elasticsearch的版本对应关系

因为一开始并不知道Spring data和elasticsearch之间有严格的版本对应关系,所以出了一堆莫名其妙的bug,这张对应表可以在这里看,虽然我下面用的是spring boot,但是同样有这个问题。
建议下载Elasticsearch2.4.0,下载后解压就算是安装完成了。
修改配置文件:

cd your_Elasticsearch
vi config/elasticsearch.yml

默认的配置,elasticsearch使用的HTTP端口是9200,TCP端口是9300.
启动Elasticsearch:

cd your_Elasticsearch/bin
./elasticsearch

Server Demo

  1. pom.xml中添加elasticsearch的依赖
        <!--  elasticsearch -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
            <version>2.0.1.RELEASE</version>
        </dependency>

这时会产生netty包的冲突,解决:

        <!-- dubbo -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.5.3</version>
            <exclusions>
                <exclusion>
                    <artifactId>spring</artifactId>
                    <groupId>org.springframework</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>netty</artifactId>
                    <groupId>org.jboss.netty</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--  zookeeper -->
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.3.6</version>
            <exclusions>
                <exclusion>
                    <artifactId>netty</artifactId>
                    <groupId>io.netty</groupId>
                </exclusion>
            </exclusions>
        </dependency>
  1. 新建实体类:
@Document(indexName = "dialog", type = "qa", shards = 1, replicas = 0)
public class QA {
    @Id
    private int id;

    private String Q;

    @Field(type = FieldType.Nested)
    private List<String> A;

    public QA() {
    }

    public QA(int id, String q, List<String> a) {
        this.id = id;
        Q = q;
        A = a;
    }

    public int getId() {
        return id;
    }

    public String getQ() {
        return Q;
    }

    public void setQ(String q) {
        Q = q;
    }

    public List<String> getA() {
        return A;
    }

    public void setA(List<String> a) {
        A = a;
    }

    @Override
    public String toString() {
        return  " QA { " +
                "   id = " + id + "," +
                "   Q = '" + Q + "'," +
                "   A = " + A +
                '}';
    }
}
  1. 新建Repository类:
public interface QARepository extends ElasticsearchRepository<QA,Long> {
}
  1. 修改server的接口类,添加操作elasticsearch的方法:
public interface SearchService {
    public String sayHello(String name);
    public String search(final String query);
}
  1. 修改service的实现,实现操作elasticsearch的方法:
@Service("demoService")
public class SearchServiceImpl implements SearchService {
    @Autowired
    private Client client;
    @Autowired
    private QARepository qARepository;
    public String sayHello(String name) {
        System.out.println("received from remote: "+name);
        return "Hello " + name;
    }

    /**
     * 插入一条数据
     * @param qa
     */
    public void addEntity(QA qa) {
        qARepository.save(qa);
    }

    /**
     * 查询
     * @param query
     * @return
     */
    public String search(final String query) {

        System.out.println("query: " + query);

        QueryBuilder queryBuilder = QueryBuilders.matchQuery("q", query);

        SearchResponse response = client.prepareSearch()
                .setQuery(queryBuilder)
                .addHighlightedField("Q")
                .execute().actionGet();

        SearchHit[] searchHitArr = response.getHits().getHits();
        /**
         * 遍历检索到的结果,这里可以自定义排序算法
         */
        for (int i = 0; i < searchHitArr.length; ++i) {
            SearchHit searchHit = searchHitArr[i];
            System.out.print("index:"+searchHit.index()+" type:"+searchHit.getType()+" id: "+searchHit.getId()+" q:"+(String)searchHit.getSource().get("q"));
            List l  = (List)searchHit.getSource().get("a");
            for (int j = 0;j<l.size();j++){
                System.out.print(" a: "+l.get(j));
            }
            System.out.println("");
        }
        /**
         * 返回第一个相关文档
         */
        String result = null;
        if(searchHitArr.length>0){
            SearchHit searchHit = searchHitArr[0];
            result = "index:"+searchHit.index()+" type:"+searchHit.getType()+" id: "+searchHit.getId()+" q:"+(String)searchHit.getSource().get("q");
            List l  = (List)searchHit.getSource().get("a");
            for (int j = 0;j<l.size();j++){
                result = result+ " a: "+l.get(j);
            }
        }
        return result;
    }
}

注意最上面的注解@service ,因为client和qARepository需要spring自动搜索注入。所以改成了注解的方式,不再在配置文件中显示的配置。

  1. spring配置文件的改变:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xmlns:elasticsearch="http://www.springframework.org/schema/data/elasticsearch"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://code.alibabatech.com/schema/dubbo
        http://code.alibabatech.com/schema/dubbo/dubbo.xsd
        http://www.springframework.org/schema/data/elasticsearch
         http://www.springframework.org/schema/data/elasticsearch/spring-elasticsearch-1.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"
>

    <!-- 提供方应用信息,用于计算依赖关系 -->
    <dubbo:application name="elastic-search"/>
    <!-- 配置注册中心 -->
    <dubbo:registry protocol="zookeeper" address="127.0.0.1:2181" />
    <!-- 用dubbo协议在20880端口暴露服务 -->
    <dubbo:protocol name="dubbo" port="20880"/>
    <!-- 配置elasticsearch 连接 -->
    <elasticsearch:transport-client id="client" cluster-nodes="127.0.0.1:9300" cluster-name="elasticsearch"/>
    <!-- spring data elasticsearch DAO 必须依赖 elasticsearchTemplate  -->
    <bean id="elasticsearchTemplate" class="org.springframework.data.elasticsearch.core.ElasticsearchTemplate">
        <constructor-arg name="client" ref="client" />
    </bean>
    <!-- 扫描DAO包 自动创建实现 -->
    <elasticsearch:repositories base-package="com.yyz.elasticsearch.dao" />
    <!-- 扫描Service包 -->
    <context:component-scan base-package="com.yyz.elasticsearch.service" />
    <!-- 声明需要暴露的服务接口 -->
    <dubbo:service interface="com.yyz.elasticsearch.SearchService" ref="demoService" />

</beans>

Client Demo

客户端需要改变的地方很小,只需要改变调用的远程方法即可。

public interface SearchService {
    public String sayHello(String name);
    public String search(final String query);
}
public class Client {
    public static void main(String[] args) {
        String configName = "client.xml";
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{configName});
        context.start();
        SearchService searchService = (SearchService) context.getBean("demoService");

        while (true) {
            try {
                Thread.sleep(1000);
                // 执行远程方法
                String hello = searchService.search("你怕黑吗?");
                // 显示调用结果
                System.out.println( hello );
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
    }
}

记得测试之前先启动Elasticsearch。

4. 源码

Demo已经上传到github,点击ElasticsearchDemo查看。
这篇文章没有详细介绍导入数据的部分,可直接在命令行用Elasticsearch的语法插入数据,也可以先用SearchServiceImpl的addEntity方法插入数据,下一篇文章会介绍Elasticsearch如何批量插入数据。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 0 准备 安装注册中心:Zookeeper、Dubbox自带的dubbo-registry-simple;安装Du...
    七寸知架构阅读 13,905评论 0 88
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,360评论 6 343
  • 一般和人告别我习惯用拜拜,很少用再见,因为我感觉,分别之后,可能就真的再难相见,为什么还要安慰自己说会再见哪?——...
    一杭oneline阅读 262评论 0 1
  • 物理工程学院2016级物理学王晓 我不敢说生命是什么,我只能说生命像什么。 生命,就像,一列路途遥远的班车。有人上...
    晓love阅读 308评论 0 0