Talk is Cheap, Show me the Code! <<网站首页文章列表

没有符合条件的文章,为您推荐如下内容
  • 搜索引擎入门——Solr查询参数详解以及如何使用Java完成对接

    零、前言经过文章[《搜索引擎入门——启动第一个Solr应用 》](https://oomabc.com/articledetail?atclidf9b37293ec184ab6ad4d672327057dd7)的介绍,我们已经成功搭建了一个搜索引擎服务。通过使用Solr提供的rest接口,我们已经能够完成索引重建、更新以及查询的功能。本章主要介绍以下几个方面的内容:+ 查询界面的基本使用:基本关键字查询、二次查询、排序、分页、默认查询字段、返回字段设置等等。+ 查询语句的调试:主要是进行关键字查询得分的调试,可以清楚的看到每一个结果所匹配的条件以及详细得分计算情况。+ 关键字高亮与摘要:搜索引擎常见的功能,对命中的关键字进行高亮展示。+ 高级自定义查询:主要是借助solr封装的一些函数进行字段的简单计算,最终干预文档得分。------ 一、基本查询![图片](https://oomabc.com/staticsrc/img/201907/24/15639473826215ea242bf30ce49149aad6480e98c91a3.jpg)上图是我们项目中使用的一个职位索引的查询页面,这是Solr启动之后我们所要重点关注的查询页面。 普通查询语句 q这个参数可以直接输入关键字,那么它查询的字段就是schema.xml中配置的默认查询字段keyword。关于schema.xml的相关配置可以参考另一篇文章[《搜索引擎入门——聊聊schema.xml配置》](https://oomabc.com/articledetail?atclidadb7b81f31ca4b45b1315b3af89a9c23)。那么,qjava的查询结果与qkeyword:java的查询结果是一样的。这个q里面可以设置任意多组查询条件,比如qpositiontitle:java cityname:上海 functionname:技术,就是搜索上海的技术职能下的java岗位。多组条件可以用AND符号连接,如果都是AND可以用空格代替,系统自动使用defaultOperator配置的连接符。当然我们也可以进行更复杂的查询,类似mySQL一样:q(positiontitle:java AND cityname:上海) OR (positiontitle:php AND cityname:杭州),搜索上海的java或者杭州的php。设置权重q(positiontitle:java AND cityname:上海^100) OR (positiontitle:php AND cityname:杭州^50),通过^符号来指定查询条件的权重,这个语句就表示上海的职位权重是100,杭州的职位权重是50,上海的职位就排在杭州的职位之前了。设置跨度(我暂时先这么叫,具体原理目前还在研究)如果我要搜索关键字“java上海”,使用qkeyword:java上海,我们测试环境的数据结果只有4条。但是在我印象中,即使是测试环境也绝不可能只有这么点数据。原来此时的查询语句默认要求java和上海两个关键字是要在一起的,也就是连起来的。而我们大部分职位java是属于标题或者职位描述,上海是属于工作地点字段,虽然这些字段都copy到keyword,但并不一定是连着的。所以,应该这么查qkeyword:"java上海"~100",将关键字用双引号引起来,然后通过字符~来指定分词结果的跨度。这里大概的意思就是,查询keyword同时包含java和上海的职位,而且java和上海两个关键字的距离不超过100。这个参数是必传的,如果实在没有查询条件,请使用q:。 过滤查询 fqfq是Filter Query的缩写,从字面意思可以看出来,主要的功能就是filter,也就是过滤。fq的查询结果会被缓存起来,下次使用相同的fq时,会直接命中缓存,所以性能很好。但是fq只负责过滤结果集,而无法使自身命中结果参与文档得分的计算。fq可以设置多组,所有fq之间是AND关系;当然也可以像q一样,通过AND和OR来拼接多组条件,然后设置在一个fq上。如果我们要查询上海的java或者杭州的php,但必须是“高级”的,我们这可以这样q((positiontitle:java AND cityname:上海) OR (positiontitle:php AND cityname:杭州)) AND positiontitle:高级。这样查的时候,positiontitle:高级^100可以设置条件权重为100。当然,我们还可以通过q与fq的组合进行查询:q(positiontitle:java AND cityname:上海) OR (positiontitle:php AND cityname:杭州),fqpositiontitle:高级。最终出来的结果和上面仅使用q的时候一样,只是这里的高级不能设置权重(设置了无效)。所以建议,需要反映在文档得分的字段的查询设置在q中,对于那些仅仅需要筛选的字段,都放在fq上。例如我只要看上海的职位,那么就将cityname:上海设置到fq上。fq还有另一个很有用的地方,就是与facet配合使用,来动态生成筛选器。从这个配合使用看了,fq确实是在结果集之上进行了二次过滤。所以多个fq可以对应多个facet,产生多组facet统计结果。关于fact的使用,可以参考另一篇文章[《搜索引擎入门——solr筛选器facet的应用》](https://oomabc.com/articledetail?atclid833939e86efd4c98be5af1e007add093)。 sort这个简单,顾名思义就是排序,也支持多个字段的排序,类似mySQL。sortsalary DESC,headcount ACS表示先按照薪资降序,在薪资一致的情况下按照招聘人数升序。 分页这个类似于mySQL的LIMIT first, limit,Solr通过start和rows两个字段来实现分页,分别对应first和limit。start默认值是0,表示从第一条数据开始,rows表示本次查询要求返回几条数据。 flField List缩写,用于设置需要返回的字段,多个字段之间用英文逗号分隔,预定义字段文档得分用score指定。很多时候我们的document字段很多,达到几百是很常见的,如果我们一次返回50条数据,那么返回的数据量也是很客观的,尤其是有很多字段是大文本的情况下。那么我可以通过fl参数来指定返回哪些字段,只返回需要的字段是一个明智的选择。我通常指返回主键。但是我们如果要返回所有字段,再加上score,总不能将所有字段都列出来吧,有点麻烦。其实你只要这样fl,score,也就是说,这里的字段支持通配符。 dfDefault Field,也就是默认查询字段,本身是定义在schema.xml中的keyword。所以在qjava时,查询的是keyword字段。配合这个参数使用,可以指定默认查询字段,比如qjava&dfpositiontitle,那其查询结果就与qpositiontitle:java一致了。------ 二、查询语句调试我们在搜索功能开发过程中经常会遇到以下两种问题:1. 关键字搜索多个字段,且每个字段的权重不同,但是查询结果的排序与预想的顺序不同。2. 某些关键字查询,明明应该有结果,但事实上的查询结果却是0。上面这两种情况都需要借助Solr的debugQuery功能,来进行调试。 debugQuery设置debugQuerytrue,查询结果中会额外返回本次查询使用的query分析结果(query重新各个阶段的条件),如果有结果返回,还会返回每个结果文档的得分细节。![图片](https://oomabc.com/staticsrc/img/201907/24/156395324956596f08d851d5a41909982b699f1a58174.jpg)上图就是条件qkeyword:java^11 address:杭州^343执行后返回的文档得分信息。name就是文档主键,这里就是positionid的值。后面就是这个文档的命中关键字后的得分计算过程,粗略看可以发现keyword得分11,address得分343,总分354。其中的terFreq、fieldWeight、idf、tf等值就是在相似度计算类中计算的,这里就是HunterOnSimilarity,在schema.xml中有配置。更详细的得分计算过程将会另开一篇文章进行讲解,它涉及到Lucene的相似度(文档与查询关键字的相似度)计算模型——空间向量模型,也就是我们学过的向量夹角计算。----- 三、关键字高亮![图片](https://oomabc.com/staticsrc/img/201907/24/15639542132407e692f14e6a64a39b3ba572dc7d97f90.jpg)那些红色的关键字就是关键字高亮实现的功能,作为全文搜索引擎,Solr当然也提供了这个功能。 highlight模块XML true ((positiontitle:java AND cityname:上海) OR (positiontitle:php AND cityname:杭州)) positiontitle xml true 2上面是Solr关键字高亮查询之后,系统返回的参数。主要的高亮参数如下:+ hltrue,打开高亮功能+ hl.flpositiontitle,设置高亮字段,这个字段必须是分词字段,多个字段用英文逗号分隔。在不在q的查询条件中不重要,只要返回的文档有命中关键字,就有高亮;但是q中必须有关键字查询。+ hl.simple.pre,设置高亮关键字标签的前半部分。+ hl.simple.post,设置高亮关键字标签的后半部分。hl.simple.pre和hl.simple.post参数主要是配合前端展示使用,一般配置为html元素,然后通过class样式进行高亮展示。![图片](https://oomabc.com/staticsrc/img/201907/24/15639548939893af39a82c21347e3b20569a1d7144afe.jpg)----- 四、高级查询下面将通过一个例子来进行edismax的讲解,例子不一定非常合适,但作为功能说明是没有问题的。假设我们职位索引上有以下几个字段:+ cityname:职位工作城市。+ annualsalary:职位平均年薪,单位:元。+ maxshowannualSalary:职位年薪范围的最大值,单位:元。+ minshowannualSalary:职位年薪范围的最小值,单位:元。如果我们要查询上海的职位,那么无论是qcityname:上海还是fqcityname:上海都可以实现。如果我要将结果集中,年薪在50000(包含)到100000(不包含)的职位排序在最前面呢?那么需要在cityname:上海的条件下增加条件薪资条件,但是无论把annualsalary:[50000 TO 100000}^100查询条件放置在q或者fq,都无法实现这个效果,因为两者都是只返回了上海且年薪在50000~100000之间的结果。那要怎么设置查询条件,才能保持cityname:上海结果集范围不变,同时将年薪在50000~100000之间的职位排在最前面呢?这个时候就要借助edismax功能了。 edismaxdismax是Solr提供的一种高级查询功能,帮助用户通过简单的参数实现多字段多权重多场景的复杂查询。而edismax(Extended DisMax)则是在dismax基础上的扩展。本节主要介绍edismax中的bq和bf参数。bq它是在用户主查询的基础上,增加的一个额外的、可选的子查询语句,它只会影响文档的得分,而不会影响文档的范围。所以我们只要将annualsalary:[50000 TO 100000}^100放置在bq参数上即可。如果同时要将1000000以上的职位排在第二档,那么这样设置就行了edismaxtrue&bqannualsalary:[50000 TO 100000}^100 OR annualsalary:[1000000 TO ]^50。bf如果要在上海的结果集中,将maxshowannualSalary大于minshowannualSalary的职位排在前面,因为很多职位这两个字段都是一样的。这是两个字段做比较啊,一般的查询都是key-value的,不存在key-key的使用情况。这个时候我就需要借助bf了,bf也是在主查询之上增加的额外的、可选的子查询,也只会影响文档得分。不过它使用的不是key-value方式,而是Solr自定义的一些数学函数,比如div、map、min、max等。所以这个需求这样设置bfdiv(maxshowannualSalary,minshowannualSalary)^123,它会将minshowannualSalary0的排在最前面,其它maxshowannualSalary大于minshowannualSalary的紧随其后。一般通过bq和bf就能实现大部分的排序需求了。----- 五、Java客户端使用如果我们要实现工程化,就不能只使用Solr提供的管理界面,也不建议自己对rest接口进行封装使用。Solr提供了很多语言的客户端包,java有其对应的客户端包——solr-solrj-4.7.2.jar。代码比较简单:Javapublic static void main(String[] args) throws MalformedURLException, SolrServerException, UnsupportedEncodingException { String solrHost "http://192.168.50.35:8080/solr/position"; //创建连接对象 LBHttpSolrServer solrServer new LBHttpSolrServer(solrHost); //查询条件对象 SolrQuery query new SolrQuery(); StringBuilder queryBuilder new StringBuilder(); queryBuilder.append("cityname:\"上海\"~20^50"); // 城市 上海 queryBuilder.append(" AND annualsalary:{0 TO ]"); // 年薪大于0 query.setQuery(queryBuilder.toString()); query.setFilterQueries("headcount:[2 TO 5]"); // 通过fq设置headcount值在2到5 query.setFields("positionid,positiontitle,score,annualsalary,maxshowannualSalary,minshowannualSalary"); // 设置返回字段 query.setStart(0).setRows(15); // 设置分页// query.setSort("annualsalary", ORDER.desc); // 年薪降序;这里开启了排序,则后面的bf、bq等权重不会生效 // 开启关键字高亮 query.setHighlight(true); query.addHighlightField("cityname"); // 设置高亮字段 query.addHighlightField("address"); // 以下两个方法主要是在高亮的关键字前后加上html代码 query.setHighlightSimplePre(""); query.setHighlightSimplePost(""); // 设置edismax query.setParam("defType", "edismax"); query.add("bq", "annualsalary:[50000 TO 60000}^100 OR annualsalary:[100000 TO 110000]^50"); query.add("bf", "div(maxshowannualSalary,minshowannualSalary)^123"); // 从bf和bq分析,预测排序: // max/min的商,权重最高,所以商越大,排序越靠前; // 如果商相同,则annualsalary在5到6万(不含6w)的排序靠前,其次是10到11w的 // 打印查询条件,通常用于调试 System.out.println("query string " + URLDecoder.decode(query.toString(), "UTF-8")); // 调用接口 QueryResponse response solrServer.query(query, METHOD.POST); // 这里使用了post方式,如果参数很多可以使用post int queryCost response.getQTime(); // 查询耗时 int status response.getStatus(); // 状态码 System.out.println("status " + status + ", cost " + queryCost + "ms"); // 获得结果集 SolrDocumentList documentList response.getResults(); long total documentList.getNumFound(); // 结果总数,用于分页计算 System.out.println("total " + total); // 遍历文档,取出字段:positionid,positiontitle,score,annualsalary,maxshowannualSalary,minshowannualSalary for(SolrDocument doc : documentList) { // 取出字段,默认Object类型,如有需要自行转换 Object positionId doc.get("positionid"); Object title doc.get("positiontitle"); Object score doc.get("score"); Object annualSalary doc.get("annualsalary"); Object max doc.get("maxshowannualSalary"); Object min doc.get("minshowannualSalary"); System.out.println(score + " " + positionId + " / " + title + " " + annualSalary + " : " + max + " / " + min + " " + ((int)(Float.parseFloat(max.toString()) / Float.parseFloat(min.toString())))); }}输出结果:bashquery string qcityname:\"上海\"~20^50 AND annualsalary:{0 TO ]&fqheadcount:[2 TO 5]&flpositionid,positiontitle,score,annualsalary,maxshowannualSalary,minshowannualSalary&start0&rows15&hltrue&hl.flcityname&hl.fladdress&hl.simple.pre&hl.simple.post&defTypeedismax&bqannualsalary:[50000 TO 60000}^100 OR annualsalary:[100000 TO 110000]^50&bfdiv(maxshowannualSalary,minshowannualSalary)^123status 0, cost 21mstotal 6310766.0 42078 / 基金销售 50000.0 : 200000.0 / 40000.0 5716.0 73716 / P6-盒马鲜生-品类采销专家-上海 100000.0 : 500000.0 / 100000.0 5716.0 73900 / P6-盒马鲜生-高级Java开发工程师-上海 100000.0 : 500000.0 / 100000.0 5716.0 66437 / 大数据算法工程师 100000.0 : 500000.0 / 100000.0 5716.0 66439 / 大数据数据仓库开发 工程师 100000.0 : 500000.0 / 100000.0 5666.0 41663 / 客户经理 60000.0 : 300000.0 / 60000.0 5666.0 69337 / 运营管理中心副总/总监 300000.0 : 1500000.0 / 300000.0 5643.0 71928 / 销售专员 50000.0 : 200000.0 / 50000.0 4636.7143 50987 / 房源拓展经理/主管/专员 42000.0 : 200000.0 / 42000.0 4593.0 72425 / Java 100000.0 : 400000.0 / 100000.0 4578.1428 71633 / 销售经理 70000.0 : 300000.0 / 70000.0 4578.1428 66557 / 外汇经纪人 高薪招聘 42000.0 : 180000.0 / 42000.0 4563.5 58909 / 云计算工程师 120000.0 : 500000.0 / 120000.0 4563.5 77355 / 小额房贷资深销售总监(北京) 120000.0 : 500000.0 / 120000.0 4563.5 74745 / 房贷客户经理(上海) 120000.0 : 500000.0 / 120000.0 4从输出结果可以确定之前推测的排序方式是正确的: 从bf和bq分析,预测排序: max/min的商,权重最高,所以商越大,排序越靠前; 如果商相同,则annualsalary在5到6万(不含6w)的排序靠前,其次是10到11w的好了,关于Solr的查询参数介绍,以及Java客户端使用就介绍到这了。----

    Java   搜索引擎   edismax   查询条件   solrj   2019-07-24 浏览(475) 阅读原文>>
  • 搜索引擎入门——聊聊schema.xml配置

    前言Lucene中一个很重要的概念就是文档(Document),它代表一条建立索引的独立且完整的数据。可以对标到我们关系数据库的一条记录。一个Document包含很多个域(Field),对标数据库的字段Column。Field的一些属性配置对标字段的属性。本身Lucene对Document的Field是开放式的,不同Field的Document可以索引到一起,有点类似于noSQL的概念,属于schema-free的。但是这种开放式结构会造成“开发一时爽,维护骂爹娘”的情况,所以Solr在封装Lucene的时候通过schema.xml文件来规范Document的Field定义。类似于MongoDB的一些ORM框架(Morephia、spring-data-mongo)做的事,其实就是定义一个标准、做个存根,方便排查。所以,schema.xml配置的内容就出来了:+ Field Type 定义:定义了字段类型,string、int、double、text等等,名字是自取的。+ Field 定义:字段,比如positionid、name、age等等。------ schema.xml的大致结构XML -- -- -- positionid keyword 从实际的配置文件也可以发现,schema中确实主要包含两个内容,types和fields。+ types:定义Field类型的元数据。+ fields:表示这个索引的每条数据包含哪些字段,分别是什么类型。+ copyField:表示将哪些字段(source)的值复制到某个字段(dest)。 比如我们的职位索引包含了职位标题、职位描述、任职要求、职位公司、工作地点等字段,如果想要关键字搜索包含这些字段,那么需要每个字段都拼接查询条件,然后通过OR连接。这样显得比较麻烦。 那么,我们可以建立一个名为keyword的综合字段,将以上需要关键字搜索的所有字段通过StringBuilder拼接好,然后设置到keyword中。 通过字段拼接虽然灵活,但是麻烦;这时候就用到copyField了。我们配置多个的copyField,source分别是那些字段,dest就是keyword。+ similarity:配置了似度类,这个类必须是继承了lucene-core.4.7.2.jar中的org.apache.lucene.search.similarities.Similarity。+ uniqueKey:定义了索引的主键,类似mySQL的唯一主键。+ defaultSearchField:查询参数q默认的查询域,当用户输入qjava时,默认搜索的就是这个参数指定的字段。当然用户可以通过qtitle:java来指定查询域为title。+ solrQueryParser:指定Solr参数q的默认查询逻辑。当用户输入qtitle:java age:[25 TO 30] city:上海,此时有三个查询条件,且用户未指定查询逻辑,那么就按照这里配置的默认的AND逻辑。如果要全量设置查询逻辑关键字,可以通过参数q.opOR来指定,也可以直接拼接在查询语句中qtitle:java AND age:[25 TO 30] AND city:上海。 types定义XML -- -- -- name就是一个唯一标识符,在后面fields定义时通过属性type指定。 class定义此类型实际的行为类。这个属性的值都是以solr.开头的,例如上面solr.StrField。solr对应标准的Solr目录org.apache.solr.schema。关于StrField、solr.BoolField等实际类型,可以去源码查看定义和解释。StrField字段是不会进行分词的,只会索引和存储。 precisionStep(很重要)对于TrieIntField, TrieFloatField, TrieLongField, TrieDoubleField这些继承与TrieField的数值类型字段,所指定的一个精度值,这也是一个数字,默认值是8。它主要是用于数值字段的范围查询,前提是Field的indexed属性必须是true。在TrieField.java的初始化方法中可以看到,其默认值是precisionStepArg,为8。如果配置值是小于等于0或者大于等于64,则配置值会重置为Integer.MAXVALUE。相当于没有启用这一配置,此时该字段的范围查询将会很慢,与普通的分词查询一样。Java@Overrideprotected void init(IndexSchema schema, Map args) { super.init(schema, args); String p args.remove("precisionStep"); if (p ! null) { precisionStepArg Integer.parseInt(p); } // normalize the precisionStep precisionStep precisionStepArg; if (precisionStep64) precisionStepInteger.MAXVALUE; String t args.remove("type"); if (t ! null) { try { type TrieTypes.valueOf(t.toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException e) { throw new SolrException(SolrException.ErrorCode.SERVERERROR, "Invalid type specified in schema.xml for field: " + args.get("name"), e); } }}在TrieField中使用的NumericRangeQuery对该参数有比较详细的介绍: 该值越小,在索引阶段产生的token将会越多,索引的大小也会越大,但是会提升范围查询性能。这里建议的合理值范围是1~8,一般可以设置为4。一个字段值在索引是产生的token数公式是indexedTermsPerValue ceil(bitsPerValue / precisionStep)。 合适的precisionStep值通常由具体数据类型和使用场景决定的: + 当未指定时,所有数值类型的precisionStep默认值都是4(TrieField中设置了默认值是8)。 + 64位数据类型,比如long、double,理想的precisionStep值是6或者8。 + 32位数据类型,比如int、float,理想的precisionStep值是4。 + 如果某个字段的数值基数比较小,那么将precisionStep设置大一些会比较好。比如,数值都小于100,可以直接选择Integer.MAXVALUE作为precisionStep值。 + 由上面那个公式可以看出来,precisionStep值大于等于数据类型位数时(long、double类型,precisionStep64;int、float类型,precisionStep32),每个字段值在索引时都只会产生一个token,因此在查询的时候会非常慢,与普通的TermRangeQuery差不多。 + 如果你的数值字段只要求排序,不要求范围查询,那么通过将其配置为0或者大于64的任意int值即可。这样设置时,虽然查询效率几乎和纯文本字段一致,但是它的索引效率和排序效率却是远高于纯文本(StrField)类型的数字字段(将数字存储到文本字段)。 Comparisons of the different types of RangeQueries on an index with about 500,000 docs showed that TermRangeQuery in boolean rewrite mode (with raised BooleanQuery clause count) took about 30-40 secs to complete, TermRangeQuery in constant score filter rewrite mode took 5 secs and executing this class took -- -- -- solr.TextField看过前面的介绍就不会找错这个类,因为TextField类有很多,Lucene源码中也有org.apache.lucene.document.TextField。但这里配置的是org.apache.solr.schema.TextField。与上面的其它type一样,它也是通过org.apache.solr.schema.FieldTypePluginLoader.create(SolrResourceLoader, String, String, Node)方法来进行FieldType初始化的。Java@Overrideprotected FieldType create( SolrResourceLoader loader, String name, String className, Node node ) throws Exception { // 普通类型,通过类加载器来初始对象实例 FieldType ft loader.newInstance(className, FieldType.class); ft.setTypeName(name); // 加载查询阶段使用的分词器,typequery // 注:1 String expression "./analyzer[@type'query']"; Node anode (Node)xpath.evaluate(expression, node, XPathConstants.NODE); Analyzer queryAnalyzer readAnalyzer(anode); // 加载多term分词器:模糊查询、正则匹配、占位符查询等 expression "./analyzer[@type'multiterm']"; anode (Node)xpath.evaluate(expression, node, XPathConstants.NODE); Analyzer multiAnalyzer readAnalyzer(anode); // 指定typeindex或者未指定type的,都是索引阶段使用的分词器 // 注:2 expression "./analyzer[not(@type)] ./analyzer[@type'index']"; anode (Node)xpath.evaluate(expression, node, XPathConstants.NODE); Analyzer analyzer readAnalyzer(anode); // 加载用户指定的相似度计算类 // 注:3 expression "./similarity"; anode (Node)xpath.evaluate(expression, node, XPathConstants.NODE); //这是一个solr封装的工厂类,里面持有了lucene定义的Similarity类对象 SimilarityFactory simFactory IndexSchema.readSimilarity(loader, anode); if (null ! simFactory) { ft.setSimilarity(simFactory); } // 如果为定义query阶段的分词器,则用index阶段的分词器代替,并设置,query分词器是否明确定义为false if (null queryAnalyzer) { queryAnalyzer analyzer; ft.setIsExplicitQueryAnalyzer(false); } else { ft.setIsExplicitQueryAnalyzer(true); } // 如果index阶段分词器未定义,则用query阶段分词器代替,也可能两者都为空 if (null analyzer) { analyzer queryAnalyzer; ft.setIsExplicitAnalyzer(false); } else { ft.setIsExplicitAnalyzer(true); } // index、query两个阶段,至少定义了一个分词器,则进入下面的赋值流程 if (null ! analyzer) { ft.setAnalyzer(analyzer); ft.setQueryAnalyzer(queryAnalyzer); if (ft instanceof TextField) { // 这里的text对应类型是TextField if (null multiAnalyzer) { // 如果未定义多term分词器,则用query分词器代替 multiAnalyzer constructMultiTermAnalyzer(queryAnalyzer); ((TextField)ft).setIsExplicitMultiTermAnalyzer(false); } else { ((TextField)ft).setIsExplicitMultiTermAnalyzer(true); } ((TextField)ft).setMultiTermAnalyzer(multiAnalyzer); } } if (ft instanceof SchemaAware){ schemaAware.add((SchemaAware) ft); } return ft;}注1:通过来指定query(查询)阶段所使用的分词器,并进行相关配置。注2:通过或者来指定index(所以)阶段所使用的分词器,并进行相关配置。注3:这里的相似度类定义与上面介绍的配置在最外层的similarity标签一样。可以配置在里面,也可以配置在里面。class可以配置两种类型:1. org.apache.lucene.search.similarities.Similarity的子类;初始化之后会将其通过org.apache.solr.schema.SimilarityFactory匿名类的方式进行包装。2. org.apache.solr.schema.SimilarityFactory的子类,要实现public abstract Similarity getSimilarity()方法,次方法返回的还是org.apache.lucene.search.similarities.Similarity的子类。Javastatic SimilarityFactory readSimilarity(SolrResourceLoader loader, Node node) { if (nodenull) { return null; } else { SimilarityFactory similarityFactory; final String classArg ((Element) node).getAttribute(SimilarityFactory.CLASSNAME); final Object obj loader.newInstance(classArg, Object.class, "search.similarities."); if (obj instanceof SimilarityFactory) { final NamedList namedList DOMUtil.childNodesToNamedList(node); namedList.add(SimilarityFactory.CLASSNAME, classArg); SolrParams params SolrParams.toSolrParams(namedList); similarityFactory (SimilarityFactory)obj; similarityFactory.init(params); } else { // 这里会有类型强转的风险 similarityFactory new SimilarityFactory() { @Override public Similarity getSimilarity() { return (Similarity) obj; } }; } return similarityFactory; }} analyzer.index我在Solr中使用的分词器是IK分词器,所以index和query阶段的配置方式都是基于IK的配置。XML -- 指定的class值是IK分词器实现类的完整路径,它继承了org.apache.lucene.analysis.util.TokenizerFactory。我在index阶段设置了useSmartfalse,这是为了在索引阶段分出更多、更全面的关键词,主要是提高查全率。不过查准率会有所影响,因为出现了很多歧义分词。被注释掉的那一个filter是自己实现的,用于过滤单字切词的。我默认认为,单个字是不存在全文意义的,类似于停用词中的你、的,对与这些关键字的搜索,我直接返回无结果,因为无法分词,所以不会存在单字索引。但是招聘行业也有单字是有意义的,比如C表示C语言,所以这个filter中做了动态例外。后面三个filter是lucene预定义的,其作用分别是过滤停用词、转小写、关键字标记。关于分词器扩展以及filter链扩展,包括同义词扩展,可以参考我的另一篇文章[《搜索引擎进阶——IK分词器扩展》](https://oomabc.com/articledetail?atclide738d22188194d3fac7577d3c38a2219) analyzer.queryXML 在query阶段,分词器配置与index阶段很相似,主要有处不同:1. useSmart设置为true,目的是尽量保持用户关键字的原意,所以采用了智能分词,其实大致就是最大分词。2. 增加了两个filter。同义词过滤转换org.wltea.analyzer.lucene.IKSynonymFilterFactory和重复关键字过滤solr.RemoveDuplicatesTokenFilterFactory。关于分词设置useSmart的区别,可以参考另一篇文章[《搜索引擎开发——关键字预处理模块》](https://oomabc.com/articledetail?atclidcecf525bb9184a10a7f22ef619c4544f)的前言模块。------ fields这一部分定义了当前索引Core的文档结构,也就是包含哪些字段。需要注意的是,从Solr4.+之后的版本,必须要包含至少两个field定义:1. 主键字段:也就是在uniqueKey标签定义过的字段,例子中的是。2. version字段:。这是Solr自己会赋值的字段。 name指定字段名,正常情况下的命名规范是由小写英文、下划线、数字组成。英文开头,多个单词用下划线分隔。 type这就是在types中定义的所有type,值就是type的name。 indexed当前字段是否索引。false表示字段不会进行索引;true表示会进行索引,可以在该字段上进行筛选、排序。如果未配置,会从对应type定义上继承该配置值,默认值是false。 stored当前字段是否存储。 multiValued是否是多值字段。 required是否是必填字段。true表示必填,类似mySQL的NOT NULL。 dynamicField动态字段,字段名可以动态设置。一般来说,我会预留一些动态字段,以应对将来的字段新增。索引一旦超过10G,每次修改schema并重启会很费时,而且有一定风险。所以针对添加新字段的修改,通常通过预留的动态字段来实现。预留字段的原则是,每个类型各留一个单值动态字段、多值动态字段。XML关于schema.xml的相关配置就聊到这了。------

    Java   搜索引擎   schema   分词器   2019-07-24 浏览(245) 阅读原文>>
  • 君子性非异也,善假于物物也——功能强大的Postman

    君子性非异也,善假于物物也!相信大部分开发人员,尤其是后端开发,都听过postman这款堪称神器的软件吧。它的一切都是为API而生的:+ 管理API:创建、更新、删除、执行我们开发的接口+ 分享API:可以将我们编写的接口共享给小组其他成员,比如将写好的并经过良好自测的接口提供给前端同学+ 控制API版本:通过版本号,我们可以同时管理接口的多个版本+ API的自动化测试:通过postman,我们可以很简单的就是实现接口的断言测试下面我们就先从入门使用开始介绍postman的常用功能和技巧吧。------ 基础我使用的是postman的mac版本,所以后续的介绍都是基于这个版本的。当我们打开postman之后,在Collections菜单中默认是没有内容的,不过官网已经为我们准备了一个[完整的学习样例](https://docs.postman-echo.com/)。打开这个网站之后,你会看到如下页面:![blockimg](https://oomabc.com/staticsrc/img/201909/21/15690767925025a5fef3bbd3b4eabacd8de6ef572638b.jpg)这是学习样例的共享API页面,也就是接口文档页面,我们直接点击右上角的Run in Postman按钮,浏览器会打开我们本地的Postman软件然后倒入整个学习样例接口:![blockimg](https://oomabc.com/staticsrc/img/201909/21/1569077253599adc167d2da9b4c429033da442d85c76d.jpg) 集合(Collection)+文件夹(Folder)左边Collections中,Postman Echo就是一个API集合,而里面的Request Methods或者Headers就是这个集合下的细分文件夹。所以,我们针对一个独立的项目可以创建一个Collections,然后为不同的模块创建不同的文件夹。你可以随意运行学习样例中的几个Request,有个初步的感觉。 接口文档当我们完成了一些接口的开发及自测之后,就可以将这些API先分享给同事了。![图片](https://oomabc.com/staticsrc/img/201909/21/1569078270545e2ea087aaed345e48d977e4fbf34bf04.jpg)点击Postman Echo 右侧上面菜单 'View in web'。![blockimg](https://oomabc.com/staticsrc/img/201909/21/15690786086202abaeeb7bc0647dc9809cba8803f1dba.jpg)然后,将这个文档地址发给他们即可。以后你在这个Colleciton中做所的更新、新增接口,他们刷新页面就能看到最新内容。------ 入门当我们在本地开发完接口并自测通过之后,接下来就是发布测试环境。在测试环境通过之后发布克隆环境,最后就是发布生产环境。 多环境(Environment)本地、测试环境、克隆环境、生产环境一般是每个上线流程会涉及到的四个环境。不同环境需要测试的接口都是一致的,区别就是API地址与Cookie不同。所以按环境定义API地址与Cookie等信息就很实用,恰好Postmant就给我们提供了这个功能。![blockimg](https://oomabc.com/staticsrc/img/201909/23/15692039769875b72f19ea650440eb6fc80227a479186.jpg)当然这里的环境参数配置你可以分享(就是按钮Share)给同一个工作组(Workspace)的其他同事。点击其中的本地环境,可以看下我配置的参数,都是不同应用的本地地址,还包含了其它的常用参数,比如Cookie等。![blockimg](https://oomabc.com/staticsrc/img/201909/23/1569204346689f126bb714bb94701b4dd9c463a28c2ba.jpg) 全局参数设置(Globals)我们可以将每个环境的参数看成是局部参数,所以自然就有对应的全局参数Globals。入口就在环境配置页面上,下方最左侧有一个Globals按钮:![blockimg](https://oomabc.com/staticsrc/img/201909/23/1569206051548e028b04def6b466f95d5ca8ece72040b.jpg)点击按钮进入全局参数定义页面,设置方式与环境参数一致,仅仅是生命周期不同。看到这里可能会有疑问,如果全局参数和局部参数的参数名相同时,取值优先级如何?截止发文,我测试过这个优先级,Postman是没有进行优先级处理的。比如,本地和全局都有同一个参数testHdToken,那么当你在输入参数名的时候,Postman会给你选择,让你自己选择E还是G。E表示Environment,G表示Globals。![图片](https://oomabc.com/staticsrc/img/201909/23/156920660838162c7a14cc20b40e995533b382c5bd62a.jpg) API分享前面介绍过,我们可以将自测通过的接口,直接以接口文档的形式分享给其他同事。如果,我想将接口的测试配置分享给同组的其它开发呢?Postman早已经考虑到,还记得上面提到的Workspace吗?我们可以将整个集合中的所有接口都分享给其他开发同事,他们就不用进行重复设置了。![blockimg](https://oomabc.com/staticsrc/img/201909/23/1569207056982b2d3924c444349ee89aea48b10660118.jpg)不过,免费账号的分享额度比较低。------ 进阶有些系统用的认证方式是自定义的,比如我上面的一些项目,都是根据账号密码换取Token,然后通过将Token信息设置在Cookie中进行认证判断的。不过也有许多项目是标准的认证方式,比如OAtuh2.0,SpringSecurity就比较方便的支持这种认证。Postman提供了很多种认证方式的快捷配置。![blockimg](https://oomabc.com/staticsrc/img/201909/23/1569208296935e5350d279caf4431abda46c3e1f3883b.jpg)由于我这个项目中,/oauth/token接口获取token时需要设置Headers参数,而Postman的OAtuh2认证模块默认不支持设置Headers(可能是我没发现)。所以,我先通过Post请求,根据认证参数要求,先获得token。然后在Auth菜单下选择Bearer Token的认证模式,在Token参数上,选择之前获取的token内容。![图片](https://oomabc.com/staticsrc/img/201909/23/1569208864286d68b0bc7f8f44eed86bfbced207c0679.jpg) 公共认证(Authorization)不过这个项目中,几乎所有的接口都要进行token验证才能方法,我们总不能每个Request请求都进行Auth配置吧。当然,Postman为我们提供了Collections、Folder和Request三个级别的Authorization配置。Request级别的认证配置上面已经介绍过了,下面看下Collections和Folder的配置。Collections和Folder两个级别的认证配置方式是一样的,通过其右侧菜单中的Edit按钮:![blockimg](https://oomabc.com/staticsrc/img/201909/23/1569209140344ce4db75ea8cb4eb2aab674985dda3786.jpg)进入配置页面,后续的设置也与Request级别的设置一样:![图片](https://oomabc.com/staticsrc/img/201909/23/156920920389976fc21b6fbee4c9d8bbc85d883427bcd.jpg) 断言测试(Tests)在设置Collections和Folder级别的认证界面,可以看到边上有一个子菜单是Tests,这就是针对接口Response的断言测试。针对这一块内容,我也不甚了解,所以只做简单的功能介绍,具体的断言语法可以自行查阅相关资料。![blockimg](https://oomabc.com/staticsrc/img/201909/23/1569209466319658edef4a9b245b29cd20f90a71fef22.jpg)断言参考文章:[官方TestScripts介绍](https://learning.getpostman.com/docs/postman/scripts/testscripts/)、[https://www.cnblogs.com/CyLee/p/9749767.html](https://www.cnblogs.com/CyLee/p/9749767.html)

    API   Postman   2019-10-12 浏览(296) 阅读原文>>
  • 搜索引擎入门——启动第一个Solr应用

    零、关于Solr摘自维基百科: Solr(读作“solar”)是[Apache Lucene](https://zh.wikipedia.org/wiki/Lucene)项目的开源企业搜索平台。其主要功能包括全文检索、命中标示、分面搜索、动态聚类、数据库集成,以及富文本(如Word、PDF)的处理。Solr是高度可扩展的,并提供了分布式搜索和索引复制。Solr是最流行的企业级搜索引擎,Solr 4还增加了NoSQL支持。 Solr是用Java编写、运行在Servlet容器(如[Apache Tomcat](https://zh.wikipedia.org/wiki/ApacheTomcat)或Jetty)的一个独立的全文搜索服务器。 Solr采用了[Lucene](https://zh.wikipedia.org/wiki/Lucene) Java搜索库为核心的全文索引和搜索,并具有类似REST的HTTP/XML和JSON的API。 Solr强大的外部配置功能使得无需进行Java编码,便可对其进行调整以适应多种类型的应用程序。Solr有一个插件架构,以支持更多的高级定制。 因为2010年Apache Lucene和Apache Solr项目合并,两个项目是由同一个[Apache软件基金会](https://zh.wikipedia.org/wiki/Apache%E8%BD%AF%E4%BB%B6%E5%9F%BA%E9%87%91%E4%BC%9A)开发团队制作实现的。提到技术或产品时,Lucene/Solr或Solr/Lucene是一样的。 Solr的历史2004年,Solr作为CNET Networks为公司网站添加搜索功能的一个内部项目,由Yonik Seeley创建。 后来Yonik Seeley随Grant Ingersoll和Erik Hatcher创建了LucidWorks(原名Lucid Imagination),公司提供商业支持、咨询和Apache Solr搜索技术的培训。2006年1月,CNET Networks决定捐赠其到Apache软件基金会顶级项目Lucene,公开发布其源代码。像在Apache软件基金会的任何新项目一样,其进入了一个潜伏期,以助于解决组织、法律和金融问题。2007年1月,Solr结束孵化状态,稳步成长,累积功能,从而形成聚集了用户、参与者和提交者的强大社区。即便作为一个非常新的开源项目,Solr已被应用于一些流量很高的网站。2008年9月,Solr 1.3发布了许多增强功能,包括分布式搜索功能和性能增强等。2009年11月,Solr 1.4发布。此版本对索引、搜索和分面做了增强,并有许多其它改进,例如富文本(PDF、Word和HTML)的处理,基于Carrot 2的搜索结果聚簇,与数据库集成的改进。该版本还提供了许多插件。2010年3月,Lucene和Solr项目合并。产品现在由同一组参与者共同开发。在2011年,Solr改变了版本编号方案,以便与Lucene的匹配。为了使Solr和Lucene有相同的版本号,Solr 1.4的下一版本号为3.1。2012年10月,Solr 4.0版本发布,包括新的SolrCloud功能。以上Solr的历史也是摘自维基百科,目前Solr的稳定版本已经升级到了8.\。而且官方提示: (( (( (( dtextlatestfirstpositiontitle:java^15 ) OR ( dtextlatestfirstpositiontitle:"java"~3^7 ) OR ( dtextlatestfirstcompanyname:java^10 ) OR ( dtextlatestfirstcompanyname:"java"~4^5 ) OR ( dtextlatestsecondpositiontitle:java^5 ) OR ( dtextlatestsecondpositiontitle:"java"~3^2 ) OR ( dtextlatestsecondcompanyname:java^5 ) OR ( dtextlatestsecondcompanyname:"java"~4^2 ) OR ( dmtextotherpositiontitles:java^3 ) OR ( dmtextothercompanynames:java^3 ) OR ( dtexttalenteducationschool:java^15 ) OR ( dtexttalenteducationschool:"java"~2^7 ) OR ( dtexttalenteducationprofessional:java^15 ) OR ( dtexttalenteducationprofessional:"java"~5^7 ) OR ( dtextnostoreworkprojectdetail:"java"~5^1 )) ) AND ( (( expectcityid:33102 )) )) )) 关键字查询字段包括最近一段经历的职位、公司;最近第二段经历的职位、公司;其他经历的职位、公司;毕业院校、专业、全文工作项目经历、简历更新时间、期望工作地点等十多个字段,每个字段的权重都是不同的情况。+ 开始全新的技术选型,开发全新的搜索服务。分布式搜索系统包括ES和Solr-cloud并不能很好的提升查询性能(相比于Solr单机版),分布式应对的主要是索引数据量的激增所带来的问题。但是分布式会比单机模式带来更多的集群管理问题。--- 一、开始后面整个系列的文章都是基于Solr4.7.2版本,大伙参考文章的思想、思路、方法即可。Solr4.7.2版本,启动方式包括jetty和tomcat,由于习惯问题以及频繁修改Solr源码和增加扩展的需要,我选择了tomcat启动方式,这里不做两者优劣对比。 下载旧版本Solr4.7.2Solr官方当然不会建议我们使用如此之旧的EOL版本,所以旧版本入口在官网不是很好找,这里直接给出[下载Solr4.7.2](https://archive.apache.org/dist/lucene/solr/4.7.2/)的链接。![图片](https://oomabc.com/staticsrc/img/201907/23/15638612295586c8e3a8c980b403ba866d620566f51a7.jpg)我们只要下载solr-4.7.2.zip包就行。压缩包解压之后的目录大致如下:![图片](https://oomabc.com/staticsrc/img/201907/23/1563862633349958600e5f42e4d31ae25ed11d6dc0a7a.jpg)大概说一下相关目录的用途:+ contexts:里面只有一个solr-jetty-context.xml文件,顾名思义,就是配置jetty启动时上下文中的solr参数。例如jetty的项目路径和项目访问根路径。+ etc:这里存的是和jetty相关其它配置。例如jetty的访问端口、连接池等等。+ example-DIH:solr的一个数据导入模块的用例,DIH就是data import handler的意思。它的插件支持从数据库、xml文件、word文本等数据源导入数据到Solr索引中。不过我一般不使用,可以了解下。真实应用场景会比较复杂,一个完整的索引数据源可能由多个数据库、多个数据服务甚至多个数据仓库接口组合而成。+ example-schemaless:是一个Solr最小单元的定义举例,也就是Core,可以理解为一个有完整意义的表。比如电商搜索中的商品索引就是一个Core。+ exampledocs:也是一些样例的数据文件,主要是配合example-DIH使用的一些数据源文件。+ lib:就是存放jetty启动时用的一些jar包。+ multicore:主要是支持Solr-cloud模式下的配置。但是4.7.2的cloud模式与当前8.\版本的cloud配置方式完全不同了,这里就不详细说明了。+ solr:Solr正式启动之后使用的目录,包括所有Core配置以及相关的索引数据。每个Collection的结构是一致的,代表一个独立的索引。+ webapps:默认配置下,是Solr的web目录,与tomcat的webapps目录一样。+ start.jar:官方提供的jetty启动脚本。 jetty启动官方提供的默认启动方式就是jetty,因此在你安装了jdk环境之后,启动是很方便的:bash13:57 wjyuian@wjyuianMacBookPro /Users/wjyuian/software/example% java -jar start.jar+ 在/contexts/solr-jetty-context.xml文件中配置Solr基本信息:XML/webapps/solr+ 在/etc/jetty.xml配置项目访问端口:XML 50000 1500 false 或者启动时指定端口号java -jar start.jar -Djetty.port8985。+ 在/webapps/solr/WEB-INF/web.XML中,配置solr索引的配置和数据存储目录:XML solr/home java.lang.String /Users/wjyuian/software/example/solr tomcat启动配置tomcat/conf目录下的server.xml文件:XML -------- 二、solr索引的配置和数据存储目录下面详细说一下solr的索引配置和数据目录。这个目录下存放的是一个个Core,也就是一个个独立的索引。例如在招聘行业就是,职位索引、订单索引、简历索引等等的配置。![图片](https://oomabc.com/staticsrc/img/201907/23/15638646661213869806210e34594a0e332e1d8edd338.jpg)例如上图所示项目的索引配置目录,包含了大约27个Core,其中就包括职位索引、订单所以、简历索引。 position我们进入到其中某一个Core,可以看到里面有两个子目录:conf和data。conf存放的就是结构化配置,data存放的就是实际索引数据。 solr.xml在索引配置的根目录下,还有一个solr.xml文件。XML --- 三、测试访问当我们使用jetty或者tomcat启动Solr之后,通过链接http://localhost:8985/solr//就可以访问到Solr的管理页面。![图片](https://oomabc.com/staticsrc/img/201907/23/1563865193760480984ec2aa64d7eb48987c56b43e0e7.jpg)在Core Selector下拉列表可以选择上面solr.xml中配置的所有Core。当选择了一个Core,点击Query菜单,然后Execute Query可以看到:![图片](https://oomabc.com/staticsrc/img/201907/23/15638653353393be045304a2e4a3eb592c2de7c4b9574.jpg) Core菜单介绍+ Overview:当前Core的总览数据,包括索引更新时间、文档数量、堆内存、数据版本、索引文件Segment数、主从同步状态、索引大小、Core实例的相关路径等等。+ Analysis:Core配置的分词器测试,比如Index阶段的分词、Query阶段的分词。+ Dataimport:数据导入配置,我这里没有配置。+ Documents:主要是通过接口进行索引文档的修改,一般不建议使用。+ Files:索引配置文件。需要重点关注的是schema.xml、solrconfig.xml、solrcore.properties。+ Ping:检查Core的存活情况。+ Plugins/Stats:显示Solr使用的相关插件信息、扩展信息。比如Solr的缓存配置,用到了哪些Cache;Core使用的search数据;高亮插件;查询语句分析器、转换器;数据更新插件等等。+ Query:进行Solr查询,这是最常用的页面,主要是用于索引数据查询和数据调试。+ Replication:进行索引的主从管理。显示了上次同步时间、下次同步时间、同步的数据量、同步状态、主的地址;以及手动执行同步和关闭同步操作。+ Schema Browser:对schema.xml配置进行更为全面的查看,包括静态配置数据和实时数据。下一章将会详细介绍schema.xml的内容和配置。---

    Solr   Java   搜索引擎   2019-07-24 浏览(286) 阅读原文>>
  • 择其善而从之——我为什么开始学习ElasticSearch

    人生天地之间,若白驹之过隙,忽然而已。 感慨刚毕业与同学合租打网游的场景仿佛还在眼前,现在参加工作都已经九年了,这时间流逝的速度一点不亚于“过隙白驹”啊!九年时间,我工作过三家公司,包括现在这家,三家公司的工作时间分别是一年、四年、4年。像我这个跳槽频率,应该可以算低了,特别是我们这一行。而且,其中有八年时间我都专注于Java领域的搜索引擎开发;往细节说,就是专注于垂直领域的基于Solr的搜索引擎开发。所以,在“Solr应用于垂直搜索”这个领域,我应该可以算得上专家了,至少时间上差不多够到专家门槛了。来现在这家公司之前,面试过几个公司,对我的技术和业务能力还算认可,不过他们都要求我转ElasticSearch。关于这个ES,我也是听说过,只是没用过。所以我问他们,为什么选这个框架而不是Solr(之所以这么问,有一部原因是我一直使用的Solr居然被人看不起)?是基于数据量考量还是基于功能考量。大部分人都没有颇具说服力的理由,主要的原因有以下几条:1. ElasticSearch 是一个分布式搜索引擎框架,分布式又是互联网热门词汇2. ElasticSearch 实时性比较好3. ElasticSearch 支持的数据量更大4. ElasticSearch 查询性能更高,尤其是大数据量的时候但是一问具体有测试报告或者自己做过测试没有,都说没有。问号脸???没有调查就么有发言权。当时针对这几个理由,我也无力反驳,谁叫我不懂ElasticSearch呢。----- 理性分析不过话说回来,为什么我遇到的公司的技术负责人选技术这么随意呢?ElasticSearch 能搭上分布式的船就选它?分布式自带大数据属性,所以就是一个互联网公司了?而且部署了支持大数据的分布式搜索引擎的互联网公司?暂且不纠结是不是面试官不屑于跟我说明具体原因,下面让我来分析分析上面的几条原因。 分布式这没什么好说的,ElasticSearch天生就是分布式的。而Solr是以单机版出道的;后来出了Solr-cloud,需要手动维护节点;现在Solr也学习了ElasticSearch的优点,cloud版本就是天然分布式,部署和维护也更方便了。 实时性这是Lucene的能力,而不是ElasticSearch或者Solr的。在我的知识范畴里面,Lucene(4.7.2)本身是没有索引更新能力的,都是先删除再插入,这应该是跟倒排链表数据结构有关。官方文档这样说的: In either case, documents are added with addDocument and removed with deleteDocuments(Term) or deleteDocuments(Query). A document can be updated with updateDocument (which just deletes and then adds the entire document). When finished adding, deleting and updating documents, close should be called.要说实时性,必须先简单介绍下Lucene搜索工具包支持的能力和概念。 Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎 工具包 ,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎。 Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。Lucene提供了一个简单却强大的应用程式接口,能够做全文 索引 和 查询 。 在Java开发环境里Lucene是一个成熟的免费开源工具。就其本身而言,Lucene是当前以及最近几年最受欢迎的免费Java信息检索程序库。人们经常提到信息检索程序库,虽然与搜索引擎有关,但不应该将信息检索程序库与搜索引擎相混淆。索引和查询就是全文搜索引擎两个最重要的组成模块,在Lucene中分别提供了IndexWriter和IndexReader两个类,让我们可以轻松实现这两个模块的读写。 IndexWriter它负责一个索引的创建和维护,所以它会打开一个(这里的一个指一个完整的索引包含的全部索引文件,而不是单独一个索引文件)已有的索引文件,或者创建一个新的索引。具体有三种模式、创建(CREATE)、更新(APPEND)、创建或更新(CREATEORAPPEND),具体定义见枚举OpenMode(org.apache.lucene.index.IndexWriterConfig.OpenMode)。文档中还提到,创建一个IndexWriter(打开一个索引,就是将索引加载到内存)时会给索引文件目录创建一个lock文件,也就是上锁,此时如果尝试在同一个目录创建另一个IndexWriter对象时,将会抛LockObtainFailedException错。所以Solr配置多个Core的时候是分文件夹的。所以,IndexWriter负责的是将索引数据持久化到磁盘。 IndexReader这是一个虚拟类,有很多不同功能的子类,主要分为两大派系:AtomicReader和CompositeReader。看类名,应该就知道AtomicReader是负责对索引进行原子操作的类,负责访问实际索引文件内容的,比如存储的字段定义、文档值以及term信息。CompositeReader是一个复合类,会包含多个不同功能的Reader,进行组合使用,本身是不能直接访问索引文件内容的。比如,CompositeReader持有多个AtomicReader实例,通过这些AtomicReader实例来进行索引文件的读取。 Writer和Reader的关系当一个索引正在被IndexReader使用时,也可以同时被IndexWriter打开。不过IndexReader持有的索引版本是打开时的快照版本,如果此时IndexWriter对索引进行了更新(先删除再新增,下同),IndexReader也不会看到这部分更新的数据,除非IndexReader重新打开这个新的索引。所以,一个索引更新(IndexWriter写入)想马上被IndexReader发现(说通俗点就是被用户感知),就必须将索引文件重新加载到内存,而加载一次索引的开销非常大(IO开销),具体耗时由索引文件的大小决定。为了在加载索引文件过程中,系统还能提供查询服务,所以必须同时保持之前的IndexReader对象,直到新所以文件加载完成并替换掉旧的IndexReader对象。 IndexReader支持实时性简单说说第一种情况,称为过程A:1. 初始化加载索引文件,生成一个IndexReader对象记为R12. 此时发生数据更新,那么先生成一个IndexWrter对象,记为W1,然后由W1进行数据更新操作(如果是需要删除的话,会同步从R1中进行删除),生成新的索引文件3. 系统重新加载索引文件,包含刚刚W1生成的新文件,记为R2;在R2完全加载成功之前,内存中必须同时存在R1和R2,由R1继续提供查询服务4. 当R2加载成功,则会替换掉R1,向外提供查询服务,那么此时对于用户来说就能感知到W1更新的数据了不过过程A有个很大的问题就是,如果R2加载时间比较长,那么从W1更新数据到R2加载完成并替换掉R1的这段时间内,用户依旧感知不到数据更新,那么就谈不上实时性了,所以有了过程B:1. 初始化加载索引文件,生成一个IndexReader对象记为R12. 此时发生数据更新,那么先生成一个IndexWrter对象,记为W1,然后由W1进行数据更新操作,生成新的索引文件3. 同时在内存中生成一个只包含更新数据的R2,后续新增的数据都会同步在R2中进行维护。此时由R1和R2共同向外提供查询服务,两者的并集就是完整的数据4. 系统重新加载完整的索引文件,包含刚刚W1生成的新文件,记为R3(内存中的R1和R2无法合并,必须通过重新加载)。在R3完全加载成功之前,内存中必须同时存在R1、R2和R3,且由R1和R1共同提供查询服务5. 当R3加载成功,则会替换掉R1,系统同时丢弃R2,最终R3向外提供查询服务,那么此时对于用户来说就能感知到W1更新的数据了整个过程B中,查询服务提供者变化是:R1 --- (R1 + R2) --- R3其中从R2+R1过度到R3的过程我们可以称之为“搜索预热”,内存占用率其实会很高,最高的情况将占用一个完整索引所占用内存的两倍大小。再极限的情况,如果上一个搜索预热过程没有完成,又发生了下一个预热,系统将会开始下一个搜索预热过程,依次循环。所以,内存中会有多个过程B存在,内存占用率可想而知。这就是近实时搜索(Near Real Time Search)的代价。 Solr中的实时性在Solr的solrconfig.xml配置文件中,就有一项针对近实时搜索内存占用率的参数配置:xml2这个参数值指的就是内存中同时存在过程B的数量上限,一旦超过就会报错,内存中丢失一部分数据,磁盘中的索引数据不受影响。关于Solr的实时搜索触发、配置、自动软提交、硬提交等内容,将会在另一篇文章中进行详细介绍。所以,ElasticSearch的实时性与Solr的实时性搜索是一样的,并没有优劣之分,它们都是继承自Lucene,而且代价都很大。 数据量更大分布式的ElasticSearch支持的数据量确实更大,毫无疑问。但这是相对于单机版Solr来说的,分布式Solr-cloud数据量也是一样的支持。我说一下个人的经验数据吧。在使用单机版Solr时,我目前达到的最大数据量在千万级,具体是960W,而且单索引占用内存的大小为40GB。所以,在千万级数据量且索引大小比较大的情况下,单机Solr完全能支持,而且搜索性能也是比较高。这一点,我在另一篇搜索文章[《搜索引擎入门——启动第一个Solr应用》](https://oomabc.com/articledetail?atclidf9b37293ec184ab6ad4d672327057dd7)中关于“关于版本的选择”章节有介绍。所以,根据应用场景的当前数据量以及未来可期的数据量,可以自行评估,进行合理的选择。毕竟单机Solr的可维护性是远远高于分布式版本的Solr和ElasticSearch的。 查询性能脱离场景和数据量谈性能就是耍流氓。 择其善者而从之作为开源搜索引擎的明星框架,ElasticSearch必须是尤其优势的地方。除了更方便的集群管理,它的管道(aggregate)数据统计功能是非常非常非常强大。注意,我用了三个非常。Solr的facet功能与之相比,简直小巫见大巫,不过场景不同,本身facet就是很消耗性能的一个功能。新版本Solr应该也有代替方案了。之前使用过MongoDB的group和aggregate功能,虽然也很好用,但是其性能是远远不及ElasticSearch的aggregate的。所以,我准备从零开始,学习下ElasticSearch的使用。学习方面主要有:+ 基础查询语法+ 统计aggregate功能+ 集群部署、管理和维护+ 分词器接入,词库动态加载+ 同义词模块---

    ElasticSearch   ES   Solr   搜索引擎   2019-10-12 浏览(186) 阅读原文>>
  • 实现一个关于队列的伪需求是一种怎样的体验

    最近花了一天的时间,在实现一个关于队列扩展的伪需求。就是当队列消息有积累的时候,如果对队列中的消息进行去重,或者说在一定范围内去重。 场景比如,有一个用于通知搜索引擎进行职位索引更新的消息队列,消息内容就是职位主键positionId,当职位数据更新频繁的时候,在队列中积累了100个消息,其中有30个消息都是关于同一个职位A的。那么,我的需求就是如何在这些消息被消费前,将其根据职位主键进行去重,也就是说,职位A的索引更新,我只想执行一次,而不是30次。我之所以把这个需求称为伪需求,因为队列本身就是为了有序进行任务的一个数据结构,即先进先出。而经过去重,本质上就是对于同一个主键,都只执行一次,因此顺序是不能严格保证的,不过在主键上还是保留了大方向上的有序性。即使是伪需求,对于我们目前的情况来说,还是很有必要的。 需求针对的数据范围从目前的索引更新日志分析看来,任务高峰时期,在几十毫秒内会有十来个相同的消息(每个消息都是一批职位主键)连续从队列中被消费。我自己定义的数据范围的概念就是:当队列中消息积累的某个时刻,针对这些积累的数据进行去重,这些积累的数据就是数据范围,这是一个动态的数据范围。每当进来一个消息,就会针对当前的数据范围进行去重,保证当前数据范围不会存在重复数据。发生这种重复的现实原因就是,索引更新队列的通知服务是开放的,公司内部很多其他服务都会通知搜索引擎进行数据更新。比如职位服务在发布、更新职位时,算法服务在进行职位匹配后,数据统计服务在统计职位数据后等等。而且很多时候,由于功能的先后接入以及缺乏相关良好的规划,甚至会出现一些重复通知的情况,在一个调用链中,上下游可能会重复通知索引更新队列很多次。综上所述,在现阶段的实际情况下,实现前面提出的伪需求,应该是解决这一问题的最快速有效的方法。 思考内事不决问媳妇,外事不决问百度。遇到这种情况,我的第一选择当然是去搜索,看看java车库中有没有类似功能的轮子。不知是我搜索的关键字不对还是这个需求过于奇葩,百度、谷歌一圈,居然没有发现轮子。怎么办?既然没有搜到轮子,当然就撸起袖子,问老铁了。 加同步锁我问的第一位老铁就是,有着30年交情的发小,某IT互联网公司技术大拿。上至九十九,下到刚会走......啊不,上至前端交互java、go,下到网络运维DBA,乃至世界上最好的语言PHP,无一不是精通。当我想他描述了现状,表达了诉求,果然,他立马给出方案:针对职位主键级别添加同步锁。通过redis实现分布式锁,锁竞争失败就忽略这条消息。好像这确实解决了并发的问题,因为锁是职位主键级别,所以不管几个线程还是几个服务器都没关系,永远都不存在同一时间对同一个职位执行多次更新服务。不过,我思考了下,似乎还是存在以下问题:1. 虽然通过同步锁控制了同一职位发并发更新,但是会带来脏数据问题。比如A、B两个消息是针对同一个职位,当A获得锁并进行数据更新的时候,B消息还未产生,或者说B消息对应的DB更新还未发生。当A对应的更新完成了数据查询阶段,B消息进入队列并被消费,但是由于无法获得同步锁,所以B的数据修改被搜索引擎忽略。当A消费结束之后,搜索引擎的数据是A对应版本,DB的数据可能是B或者之后的版本,因此造成数据不一致问题。2. 实际上并没有实现我想要的效果,就是在消费前进行去重,压根就不让重复数据在一定的范围内被重复消费。 需求的本质回过头来再看看这个需求。在数据范围内去重,这个数据范围中的数据代表什么?他们代表DB已持久化完毕,所以在数据范围内去重是不会有数据一致性问题的。加同步锁会有一致性问题是因为A和B不一定是一个数据范围。所以,要实现这个需求,就必须在这个数据范围内按规则去重,即使这个数据范围是动态的,也要时刻保持在某个时间点的内是去重后的。 分析源码有了思路,接下来就是先了解目前队列的工作过程,至少要了解消息到达本地以及消息推送给消费者线程的过程。很简单,看下我们自己封装的mq相关配置文件,发现使用的消息监听器是org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer。这个类主要封装了队列消费者相关配置,比如并发消费者数量concurrentConsumers、消息预取数量prefetchCount、ack模式acknowledgeMode等。不过了解这些内容并不是我们此行的目的,所以略过,我们要找到消息是如何到达本地以及如何推送给消费者的。打开编辑器的Outline信息框,忽略那些public方法,直接关注protected或者private方法。![图片](https://oomabc.com/staticsrc/img/201909/12/156826017836332e0121a614d425eb108a5d3a2973242.jpg)发现里面有个方法很可疑doReceiveAndExecute,点进去一看,没错,就是它了:javaprivate boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Throwable { Channel channel consumer.getChannel(); for (int i 0; i 这是一个内部专用的封装了与服务器连接细节的消费者,它有自己的生命周期,比如开始与结束。 BlockingQueueConsumer既然说到BlockingQueueConsumer,那么先来看看这个类在SimpleMessageListenerContainer中是做什么用的。前面有提到在构建SimpleMessageListenerContainer对象时,有一个参数是concurrentConsumers,它是指定队列的最小并发消费者数量。这个参数指定的是consumer数量,这个consumer指的应该是BlockingQueueConsumer的数量没错了。那去看下这个参数注入方法setConcurrentConsumers(final int concurrentConsumers)。Javapublic void setConcurrentConsumers(final int concurrentConsumers) { Assert.isTrue(concurrentConsumers 0, "'concurrentConsumers' value must be at least 1 (one)"); // 限制:独占模式下,消费者并发数只能是1 Assert.isTrue(!this.exclusive concurrentConsumers 1, "When the consumer is exclusive, the concurrency must be 1"); if (this.maxConcurrentConsumers ! null) { Assert.isTrue(concurrentConsumers 0) { // consumers参数记录了当前,已经创建的consumer以及他们的状态 // 到这里应该就可以确认,concurrentConsumers代表的就是BlockingQueueConsumer实例的数量 Iterator entryIterator consumers.entrySet() .iterator(); while (entryIterator.hasNext() && delta 0) { Entry entry entryIterator.next(); if (entry.getValue()) { BlockingQueueConsumer consumer entry.getKey(); consumer.basicCancel(); // 设置为无效 this.consumers.put(consumer, false); delta--; } } } else { // 如果是负数,则表示要增加并发,也就是BlockingQueueConsumer实例数 addAndStartConsumers(-delta); } } }}接下来看下addAndStartConsumers(int delta)是如何创建BlockingQueueConsumer实例的:javaprotected void addAndStartConsumers(int delta) { synchronized (this.consumersMonitor) { if (this.consumers ! null) { for (int i 0; i SimpleMessageListenerContainer.this.consecutiveActiveTrigger) { considerAddingAConsumer(); consecutiveMessages 0; } } } else { // 否则会考虑,是否需要空置消费者实例来节省资源 consecutiveMessages 0; if (consecutiveIdles++ SimpleMessageListenerContainer.this.consecutiveIdleTrigger) { considerStoppingAConsumer(this.consumer); consecutiveIdles 0; } } } } } // ...... 省略一些错误处理}上面标注的重点3,对应的实际执行的方法就是一开始提到的doReceiveAndExecute(BlockingQueueConsumer consumer)。所以可以看出来:+ 重点1负责创建消费者createBlockingQueueConsumer实例+ 重点2创建消费者实例的包装类,负责启动异步线程进行消费者状态维护+ 重点3负责从消费者队列中获取消息,然后进行消费现在再来看重点1,就是负责初始化消费者实例对象的:javaprotected BlockingQueueConsumer createBlockingQueueConsumer() { BlockingQueueConsumer consumer; String[] queues getRequiredQueueNames(); // 由于一轮消费的数量是txSize指定的,所以预取数量必须要大于txSize才有效,所以实际的预取数量取了一个较大值 int actualPrefetchCount prefetchCount txSize ? prefetchCount : txSize; // 创建一个 BlockingQueueConsumer 实例对象 consumer new BlockingQueueConsumer(getConnectionFactory(), this.messagePropertiesConverter, cancellationLock, getAcknowledgeMode(), isChannelTransacted(), actualPrefetchCount, this.defaultRequeueRejected, this.consumerArgs, this.exclusive, queues); // 设置相关参数,后面关于伪需求的扩展,需要在下面进行设置 if (this.declarationRetries ! null) { consumer.setDeclarationRetries(this.declarationRetries); } if (this.failedDeclarationRetryInterval ! null) { consumer.setFailedDeclarationRetryInterval(this.failedDeclarationRetryInterval); } if (this.retryDeclarationInterval ! null) { consumer.setRetryDeclarationInterval(this.retryDeclarationInterval); } return consumer;}最后再来看看这个BlockingQueueConsumer类,其实从这个类的命名上也能发现些什么,它似乎与java中的阻塞队BlockingQueue列命名很像。没错,它持有的属性中最重要的一个就是BlockingQueue类型的变量queue,不过这个queue的实际上是有序的阻塞队列LinkedBlockingQueue类型的。queue负责存储服务端推送过来的消息,然后给重点3中的consumer.nextMessage方法提供返回值。 实现我们再回头看看“需求的本质”那一节提到的内容: 所以,要实现这个需求,就必须在这个数据范围内按规则去重,即使这个数据范围是动态的,也要时刻保持在某个时间点的内是去重后的。划重点就是:1. 在数据范围内去重2. 时刻保持数据范围内是动态去重的经过源码分析可以确定,这个数据范围就是重点1中提到的BlockingQueueConsumer实例,准确的说是它持有的queue对象中的数据。因为这是服务器推送的消息的第一个落脚点。要保证数据范围的动态去重,只要保证队列queue的入队列(在重点2提到的consumer的内部类InternalConsumer中)和出队列(重点3)都进行去重操作即可。 编码扩展根据上面的讨论,发现需要修改的类主要有BlockingQueueConsumer和SimpleMessageListenerContainer两个。考虑一下几点实际情况: 1. 这两个都是spring封装的类,我不宜做修改,万一出什么问题,影响太大。 2. 类中有许多final的属性和方法,也不适合进行继承来实现。 3. 为这个伪需求提供可选择性,因为很多场景并没有需要这个功能。 4. 为这个伪需求提供可扩展性,不同类型的消费队列去重的逻辑都不同,所以交由扩展的接口实现来决定。所以,我直接复制了这两个类,重新命名为HanldedBlockingQueueConsumer和HandledMessageListenerContainer。并提供了两个接口IHandledMessageBodyConsumer和IBlockingQueueDeliveryFilter。当用户自定义的队列消费者需要使用这个过滤功能时,只要实现IHandledMessageBodyConsumer接口即可。系统在加载mq配置文件并注入队列消费者的时候,根据其是否实现IHandledMessageBodyConsumer接口来判断创建SimpleMessageListenerContainer实例还是HandledMessageListenerContainer实例。 扩展接口下面说说这个两个接口的定义。IHandledMessageBodyConsumer.java,它负责标记是否需要这个伪需求,如果需要,则实现这个接口,返回扩展的container,这个container持有去重逻辑的处理类IBlockingQueueDeliveryFilter的实现:javapublic interface IHandledMessageBodyConsumer { // 实现这个接口,需要返回HandledMessageListenerContainer实例,这个实例持有一个 IBlockingQueueDeliveryFilter 接口的实现类 AbstractMessageListenerContainer getHandledMessageListernerContainer();}重点说说IBlockingQueueDeliveryFilter这个接口类。javapublic interface IBlockingQueueDeliveryFilter { / 在消息数据进入本地阻塞队列之前,进行body数据分析、过滤、去重, 这个body就是消息实体对象的二进制数组; 我们对body数组进行发序列化,然后根据instanceof判断类型,根据具体需求,进行对象转换 获得生产者那边的对象之后,在根据具体属性,比如说是我前面提到的职位ID,进行记录, 只要创建一个JVM级别的静态且线程安全的Set即可 这个方法不能返回null,因为body数组是null的消息会被过滤;我们根据Set返回新增的职位ID, 然后更新对象内容,重新序列化并返回。 / byte[] beforeIntoBlockingQueue(byte[] body); // 当这个消息被取出之后,被消费之前,进行数据操作,过滤、去重 // 还是对body进行反序列化并对象转换,将即将消费的职位ID,从Set移除,从而保证Set的正确性 void afterTakeFromBlockingQueue(byte[] body);}我在HandledMessageListenerContainer中新增了一个构造方法,用于:javapublic HandledMessageListenerContainer(IBlockingQueueDeliveryFilter filter) { this.filter filter;}这个类持有IBlockingQueueDeliveryFilter对象的目的,是在初始化(重点1)BlockingQueueConsumer的时候,设置其过滤方法:javaprotected HanldedBlockingQueueConsumer createBlockingQueueConsumer() { HanldedBlockingQueueConsumer consumer; String[] queues getRequiredQueueNames(); int actualPrefetchCount prefetchCount txSize ? prefetchCount : txSize; // 创建了我们自定义的 HanldedBlockingQueueConsumer 实例对象 consumer new HanldedBlockingQueueConsumer(getConnectionFactory(), this.messagePropertiesConverter, cancellationLock, getAcknowledgeMode(), isChannelTransacted(), actualPrefetchCount, this.defaultRequeueRejected, this.consumerArgs, this.exclusive, queues); // 。。。。。。省略了常规设置 // 进行filter设置 if(this.filter ! null) { consumer.setFilter(filter); } return consumer;}接下来看下HanldedBlockingQueueConsumer中创建InternalConsumer的地方,将这个filter传递给InternalConsumer实例,因为它(handleDelivery方法)负责从服务器接收消息并put到阻塞队列:java// HanldedBlockingQueueConsumer.start() 方法中:this.consumer new InternalConsumer(channel);// 设置filterthis.consumer.setDeliveryFilter(filter);以及InternalConsumer.handleDelivery方法:java@Overridepublic void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Storing delivery for " + HanldedBlockingQueueConsumer.this); } try { if(deliveryFilter ! null) { body deliveryFilter.beforeIntoBlockingQueue(body); // 这里只做过滤,不做拦截 } queue.put(new Delivery(consumerTag, envelope, properties, body)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }}再看看从队列中返回消息的地方,我们增加一个后置处理方法,调用com.hunteron.mqkit.consumer.handled.filter.IBlockingQueueDeliveryFilter.afterTakeFromBlockingQueue(byte[])方法:javapublic Message nextMessage() throws InterruptedException, ShutdownSignalException { logger.trace("Retrieving delivery for " + this); Delivery delivery queue.take(); processAfter(delivery); return handle(delivery);}public Message nextMessage(long timeout) throws InterruptedException, ShutdownSignalException { if (logger.isDebugEnabled()) { logger.debug("Retrieving delivery for " + this); } checkShutdown(); if (this.missingQueues.size() 0) { checkMissingQueues(); } Delivery delivery queue.poll(timeout, TimeUnit.MILLISECONDS); processAfter(delivery); Message message handle(delivery); if (message null && cancelReceived.get()) { throw new ConsumerCancelledException(); } return message;}private void processAfter(Delivery delivery) { if(delivery null filter null) { return ; } try { // 当消息从queue中获取之后,进行处理 filter.afterTakeFromBlockingQueue(delivery.getBody()); } catch (Exception e) { e.printStackTrace(); }} 如何使用首先,按照业务逻辑,实现一下过滤器:java// 我的需求还是,基于职位ID进行 数据范围 内去重public class DefaultBlockingQueueDeliveryFilter implements IBlockingQueueDeliveryFilter { private static final Set filter Sets.newConcurrentHashSet(); @Override public byte[] beforeIntoBlockingQueue(byte[] body) { try { // 将消息数据进行目标对象反序列化 MessageBody msg createFromBody(body); // 转换失败,则原路返回 if(msg null) { return body; } IndexParameter parameter msg.getBody(); if(!isPositionIndex(parameter)) { return body; } // 去重复 List after filterAndReturnNotExistIds(parameter.getIds()); // 更新参数值 parameter.setIds(after); // 序列化之后返回 return SerializationUtils.serialize(msg); } catch (Exception e) { return body; } } @SuppressWarnings("unchecked") private MessageBody createFromBody(byte[] body) { if(body null body.length 0) { return null; } Object object SerializationUtils.deserialize(body); try { MessageBody msg (MessageBody) object; return msg; } catch (Exception e) { return null; } } @Override public void afterTakeFromBlockingQueue(byte[] body) { try { MessageBody messageBody createFromBody(body); if(messageBody null) { return ; } IndexParameter parameter messageBody.getBody(); if(parameter null) { return ; } List ids parameter.getIds(); // 从Set中删除将会被消费的职位ID clearIds(ids); } catch (Exception e) { } } private boolean isPositionIndex(IndexParameter p) { // ...... 省略代码;判断是否是需要去重的消息 return true; } // 进行ID过滤 private List filterAndReturnNotExistIds(List ids) { if(CollectionUtils.isEmpty(ids)) { return ids; } List rs new ArrayList(ids.size() / 2); for(String id : ids) { if(filter.contains(id)) { continue; } rs.add(id); filter.add(id); } return rs; } private void clearIds(List ids) { if(CollectionUtils.isEmpty(ids)) { return ; } filter.removeAll(ids); }}然后,让队列消费者实现我们的IHandledMessageBodyConsumer接口:java@Service("searchIndexConsumer")public class SearchIndexConsumer implements IHandledMessageBodyConsumer, IMessageBodyConsumer { // 伪需求对应的container private AbstractMessageListenerContainer container new HandledMessageListenerContainer(new DefaultBlockingQueueDeliveryFilter()); // 消费者入口 @Override public Object handle(MessageBody messageBody) { // ...... 省略消费者具体业务代码 return null; } @Override public AbstractMessageListenerContainer getHandledMessageListernerContainer() { // 返回 container return container; }} 总结至此,我们的这轮伪需求已经成功实现了。不过它是JVM中,同一个消费者级别的去重,所以我配置了少量的并发,较多的fetechCount,力求在控制并发的前提下,增大我们的数据范围,以便获得较好的去重效果,尤其是消息积累的时候。不过,我觉得其中可能会存在一些隐患或者纰漏,只不过是我没发现。

    队列   rabbitmq   Spring   过滤器   2019-10-18 浏览(141) 阅读原文>>
  • SpringBoot2(四)Docker+Consul+Cloud+Feign

    前言距离上一篇SpringBoot2的文章,已经过去很久了,今天终于有时间继续写一篇基于SpringBoot2+SpringCloud+consul+feign的入门文章。 Spring Cloud Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。[SpringCloud](https://springcloud.cc/) Spring Cloud Feign Feign是Netflix开发的声明式、模板化的HTTP客户端, Feign可以帮助我们更快捷、优雅地调用HTTP API。Spring Cloud对Feign进行了增强,使Feign支持了Spring MVC注解,并整合了Ribbon和Eureka,从而让Feign的使用更加方便。通过Feign,Java项目可以轻松实现跨语言的基于Http的服务接口。[Fegin](https://github.com/OpenFeign/feign) Consul Consul是一个服务发现与配置工具,与Docker容器可以无缝集成,同时它自身也是分布式的、高可用的、可扩展的。+ 服务发现:通过Consul,服务提供方可以轻而易举的将自己的服务进行注册,同时可以通过DNS或者HTTP接口来发现其他服务。SaaS等其他服务同样可以注册上来。+ 健康检查:当Consul集群发生任何异常时,健康检查功能可以迅速将异常信息通知给其它节点。它可以防止请求被路由到非健康节点,同时可以提供服务级别的熔断。+ 基于键值的存储:类似Zookeeper,Consul也可以提供键值存储。基于此功能,我们通过简单的HTTP接口,就可以实现分布式配置中心、分布式任务协调、leader选举等诸多分布式能力。+ 多数据中心:进过简单的配置,Consul就可以实现异地多数据中心的能力。+ 服务分区:通过TLS(传输层安全协议)的加密以及基于令牌的权限验证,Consul提供了可靠的端(服务)到端通信。------ Consul安装(单机版)这里的例子中是基于Docker的consul的安装,所以需要先安装Docker环境。参考:[Testing a Consul cluster on a single host](https://hub.docker.com/r/progrium/consul/) 零、首先启动一台Consul实例bash$ docker run -d --name node1 -h node1 progrium/consul -server -bootstrap-expect 3这里通过参数-bootstrap-expect 3指定了本集群有三台Server实例。所以直到这个集群中有另外两台Server角色加入的时候,它才会开始初始化,并且整个集群才是可用状态。 一、准备启动另外两台Server在启动另外两台Server之前,我们需要先获得第一台Server对应的容器IP,并将它设置到docker环境变量中。bash$ JOINIP"$(docker inspect -f '{{.NetworkSettings.IPAddress}}' node1)"接下来通过docker来启动另外两台Server即可:bash$ docker run -d --name node2 -h node2 progrium/consul -server -join $JOINIPbash$ docker run -d --name node3 -h node3 progrium/consul -server -join $JOINIP注:通过docker inspect -f '{{.NetworkSettings.IPAddress}}' 容器name或者ID命令,就可以看到对应容器的IP地址。 二、Client暴露服务至此,我们已经安装好有三个Server节点的Consul集群了。不过此时,我们并没有为这个集群暴露可访问端口。所以我们需要通过第四个节点以Client(去掉-server参数)身份加入到集群中。Client节点不会参与集群管理,只会参与通信,同时也不会将数据持久化。bash$ docker run -d -p 8400:8400 -p 8500:8500 -p 8600:53/udp --name node4 -h node4 progrium/consul -join $JOINIP此时我们暴露了几个端(8400 (RPC)、8500 (HTTP)、8600 (DNS) )口以实现不同协议的通信。 三、多服务集群参考文档 [Running a real Consul cluster in a production environment](https://hub.docker.com/r/progrium/consul)可以通过以下命令生成启动命令:bashdocker run --rm progrium/consul cmd:run 10.0.1.1 -ddocker run --rm progrium/consul cmd:run 10.0.1.1::10.0.1.2 -d -v /mnt:/data在Consul集群安装成功之后,我们可以访问web页面查看集群状态。我这里是将集群安装在192.168.100.185单机上,访问地址http://192.168.100.185:8500/ui//dc1/nodes/node4?condensedfalse。出现如下页面,表示安装成功。![blockimg](https://oomabc.com/staticsrc/img/201904/30/15565605850603471e01059d04eafb4dc44e484a5c551.jpg)接下来在SpringBoot项目中增加Consul配置。------ 添加SpringCloud、Consul配置 零、在pom.xml文件增加如下配置xml org.springframework.cloud spring-cloud-consul-dependencies 2.0.0.M7 pom import org.springframework.cloud spring-cloud-openfeign 2.0.0.RC1 pom import org.springframework.cloud spring-cloud-starter-consul-discovery org.springframework.boot spring-boot-starter-actuator compile org.springframework.cloud spring-cloud-starter-openfeign 一、配置SpringCloud、Consul在application.yml配置文件中增加cosul配置bashspring : application : name : test-springcloud 注册springcloud时候的服务名称,在下面serviceName使用 cloud: consul: consul集群相关配置 host: 192.168.100.185 port: 8500 discovery: 服务发现配置 register: true 注册服务打开 serviceName: ${spring.application.name} 服务名称 healthCheckPath: /actuator/health 开启服务健康检查 healthCheckInterval: 15s 检查间隔 tags: urlprefix-/${spring.application.name} instanceId: ${spring.application.name}:aa356dsfe21312fafd00asdf 服务唯一标识在启动类上增加注解:java//服务注册、服务发现@EnableDiscoveryClient@EnableFeignClients@SpringBootApplication//myBatis的mapper扫描路径public class Springboot2demoApplication { public static void main(String[] args) { SpringApplication.run(Springboot2demoApplication.class, args); }}启动项目,我们可以在之前的consul界面看到服务注册成功了。图片如下:![blockimg](https://oomabc.com/staticsrc/img/201904/30/1556560619868f8d96113bd3d4e6a8390cc2072c3bd3b.jpg) 二、增加服务发现测试代码我们写一个Controller,来测试服务发现功能:java@RestControllerpublic class CloudConsulController { @Autowired private DiscoveryClient discoveryClient; / 获取所有服务 / @RequestMapping("/services") public Object services() { return discoveryClient.getServices(); } @RequestMapping("/services/list") public Object servicesList() { return discoveryClient.getInstances("test-springcloud"); }}重新启动项目之后,通过链接http://localhost:8081/services/list可以看到发现的服务:js// 20190426111730// http://localhost:8081/services/list[ { "serviceId": "test-springcloud", "host": "10.10.1.102", "port": 8081, "secure": false, "metadata": { "urlprefix-/test-springcloud": "urlprefix-/test-springcloud" }, "uri": "http://10.10.1.102:8081", "scheme": null }]------ 消费者feign对接服务 Spring Cloud Feign帮助我们定义和实现依赖服务接口的定义。在Spring Cloud feign的实现下,只需要创建一个接口并用注解方式配置它,即可完成服务提供方的接口绑定,简化了在使用Spring Cloud Ribbon时自行封装服务调用客户端的开发量。所以我们新建一个接口,然后自定义接口以及相关注解,即可对接已提供的服务。java//cloud注册到consul的application-name@FeignClient("test-springcloud")public interface FeignService { //服务对应的url @RequestMapping(value "/springboot2/mysql") String hello(@RequestParam(name "value") String v); //这里对接了一个参数名为value的参数}这里配置的RequestMapping的value值,就是我们再controller中提供的mvc接口。java@RestControllerpublic class FirstController { //........... @RequestMapping("/springboot2/mysql") public String mysql(@RequestParam(defaultValue "defaultV", value "value") String v ,@RequestParam(defaultValue "defaultV", value "value2") String v2) { TbSearchManagerUserExample example new TbSearchManagerUserExample(); int count tbSearchManagerUserMapper.countByExample(example); System.out.println(count + " " + v); example.createCriteria().andUserIdEqualTo(2L); TbSearchManagerUser record new TbSearchManagerUser(); record.setNickName("SpringBoot"); int rs tbSearchManagerUserMapper.updateByExampleSelective(record, example); System.out.println("searchManagerUser count : " + rs); return "count : " + count + " " + v; } //...........}然后我们在测试Controller里面对这个Feign客户端进行测试:java@RestControllerpublic class CloudConsulController { //......... @Autowired private FeignService feignService; @RequestMapping(value "/feign/mysql", method RequestMethod.GET) public Object consumerCloud() { return feignService.hello("testcloud"); }}这个项目把服务提供者和消费者整合在了一起,重启启动项目。然后访问./feign/mysql对应的链接http://localhost:8081/feign/mysql,返回结果count : 45 testcloud。这个结果与我们直接访问服务端mvc接口http://localhost:8081/springboot2/mysql?valuetestcloud的结果,是一致的。至此,SpringBoot2+SpringCloud+Consul+Feign的整合,已经初步完成了。

    SpringCloud   Consul   Feign   Docker   SpringBoot   2019-09-22 浏览(752) 阅读原文>>
  • Docker学习——创建一个JDK+Tomcat的Solr服务镜像

    前言这里没有前言,我发现网上关于Docker的介绍文章一大堆,我写的肯定没他们好。本想去官网翻一下相关重点内容,看了文档,发现自己4.5级的英文实在难以准确翻译,大伙还是直接去[Docker官网](https://www.docker.com/)吧。 Docker安装以下是Mac版本Docker安装,完全按照[官网](https://www.docker.com/get-started)指示行事。 创建Docker账号![图片](https://oomabc.com/staticsrc/img/201905/06/1557146057796cb47a1da2da6449e8eac420fa04ad2bb.jpg)它会让你创建一个Docker账号,如果有就点击“Sign In”直接登录。![图片](https://oomabc.com/staticsrc/img/201905/06/155714614472691abe3c0ffa44bfea656932230b61072.jpg) 下载Docker.dmg登录之后点击"Get started with Docker Desktop"去下载dmg文件即可安装。![图片](https://oomabc.com/staticsrc/img/201905/06/1557146634809daaf31bd3c6942b38eeb522b02d07fc6.jpg)![图片](https://oomabc.com/staticsrc/img/201905/06/1557146681391b33ae0b1104d4c1782e4657da4bc27e4.jpg)下载页面已经告诉你后续步骤了:1. 下载并安装Docker2. 克隆一个样例git3. build自己第一个镜像4. 运行这个镜像5. 推送镜像到远程仓库---- 样例测试安装好Docker之后,我们从第2个步骤开始 克隆doodle.gitbash% git clone https://github.com/docker/doodle.git正克隆到 'doodle'...remote: Enumerating objects: 36, done.remote: Counting objects: 100% (36/36), done.remote: Compressing objects: 100% (27/27), done.remote: Total 36 (delta 13), reused 27 (delta 7), pack-reused 0展开对象中: 100% (36/36), 完成.20:49 wjyuian@wjyuianMacBookPro /Users/wjyuian/dockerl% lltotal 0drwxr-xr-x 6 wjyuian staff 192 5 6 20:49 ./drwxr-xr-x+ 66 wjyuian staff 2112 5 6 20:50 ../-rw-r--r-- 1 wjyuian staff 0 4 22 09:33 bashprofiledrwxr-xr-x 5 wjyuian staff 160 5 6 20:49 doodle/drwxr-xr-x 3 wjyuian staff 96 4 22 10:01 mynginx/drwxr-xr-x 4 wjyuian staff 128 4 25 17:55 search1/ build对Docker容器来说,镜像是一个独立的文件系统。它给容器的运行提供了所有必须的文件或者代码。我们编写一个Dockerfile脚本,然后通过build命令来创建Docker镜像。当然我们自己build的镜像只会存在于我们本地仓库。---- 运行一个Mysql镜像从远程仓库查询mysql镜像信息bash% docker search mysql NAME DESCRIPTION STARS OFFICIAL AUTOMATEDmysql MySQL is a widely used, open-source relation… 8109 [OK] mariadb MariaDB is a community-developed fork of MyS… 2750 [OK] mysql/mysql-server Optimized MySQL Server Docker images. Create… 607 [OK]拉取第一个官方镜像,我这里指定了版本5.7。因为我之前执行过,所以部分信息是已存在的。bash% docker pull mysql:5.75.7: Pulling from library/mysql27833a3ba0a5: Already exists864c283b3c4b: Already existscea281b2278b: Already exists8f856c14f5af: Already exists9c4f38c23b6f: Already exists1b810e1751b3: Already exists5479aaef3d30: Already exists0f1430d39d4f: Pull complete2bc64c824b3f: Pull completeb64ec62ca852: Pull complete42323e351ef3: Pull completeDigest: sha256:069c2bc62a2b1f84827b96c36870c45be5458f1438c27a41535ba5197aa2e26cStatus: Downloaded newer image for mysql:5.7运行这个镜像bash% docker run --name localmysql -e MYSQLROOTPASSWORD123456 -p 3306:3306 -d mysql:5.7localmysql是容器的别名,以后通过docker start localmysql启动即可。通过-e指定环境变量MYSQLROOTPASSWORD123456,设置mysql启动之后的root密码。-p指定端口映射。+ docker ps查看正在运行的容器+ docker ps -a查看所有容器------------- DockerfileDockerfile是一个文本文件,我们把创建并组织镜像的命令行写在里面,Docker通过连续读取并执行其中的命令行来自动创建镜像。docker build命令是由Docker主程序(daemon)执行的。事实上docker build命令在创建镜像的是会使用到Dockerfile文件和上下文。这里的上下文(context)指的是存放文件的一个具体路径,可以是本地文件系统的路径(PATH),也可以Git仓库路径(URL)。每一个命令行都是单独执行,而且都会创建一个新的镜像。因此任何一条命令都不会催下一条命令产生干扰。但是,Docker会利用之前已经产生的镜像缓存来提高buidl命令的性能。上下文通常会被递归处理,例如PATH对应的目录还有子目录,URL对应的仓库路径里面包含了其它子模块目录。Docker命令通过.来表示当前目录:bash20:30 wjyuian@wjyuianMacBookPro /Users/wjyuian/dockerl/search1% docker build -t hosearch:v2 .Sending build context to Docker daemon 89.14MBStep 1/7 : FROM openjdk:8-jdk-slim --- e2581abdea18Step 2/7 : ADD apache-tomcat-7.0.73 /opt/apache-tomcat-7.0.73/ --- Using cache --- 9a526b90b3ebStep 3/7 : WORKDIR /opt/ --- Using cache --- 938f73366ec8Step 4/7 : VOLUME /opt/ --- Using cache --- 5747fd8e7a3fStep 5/7 : EXPOSE 8080 --- Using cache --- 23c2d151c6c5Step 6/7 : RUN cp /etc/hosts /etc/hosts.tmp && sed -i "s/.$(hostname)/$DOCKERIP $(hostname)/" /etc/hosts.tmp && cat /etc/hosts.tmp /etc/hosts && more /etc/hosts --- Using cache --- 2699ea1b0594Step 7/7 : CMD ["/opt/apache-tomcat-7.0.73/bin/catalina.sh","run"] --- Using cache --- a630dbd129b6Successfully built a630dbd129b6Successfully tagged hosearch:v220:30 wjyuian@wjyuianMacBookPro /Users/wjyuian/dockerl/search1简单说明下:0. Sending build context to Docker daemon 89.14MB表示,将当前目录作为上下文,而且把上下文的内容关联到Docker主程序上下文。1. docker build -t hosearch:v2 .命令中最后一个.很重要,它表示上下文的根目录是当前目录/Users/wjyuian/dockerl/search1。2. 在当前目录执行docker build,默认读取当前目录的Dockerfile文件;还可以通过docker build -f /Users/wjyuian/dockerl/search2 .来指定Dockerfile目录,但是上下文还是当前目录。2. 通过-t参数来指定build成功之后的镜像路径、名字和tag信息。当然我们可以多次使用-t参数来使得本次创建的镜像保存为多个版本。docker build -t hosearch:v2 -t wjyuian/search:v1 .3. Using cache表示这一个命令行使用了之前的镜像缓存。这里的镜像缓存必须是之build时创建的本地镜像,或者通过docker load命令加载过的镜像。不过可以通过--cache-from参数在build的时候动态指定镜像缓存。 Dockerfile命令格式命令格式很简单:bash 指令 参数INSTRUCTION arguments指令本身不是大小写敏感的,不过通常建议将指令大写以方便与参数区分开来。Docker会依次执行Dockerfile中的指令,而且Dockerfile的第一条指令必须是From指令。因为我们创建一个镜像必须有一个基础镜像(Docker有个特殊镜像,叫做scratch,表示空白镜像。)以开头的就是注释行,出现在其他位置都会被当做参数:bash 这一行是注释;下面那一行的就会被当做参数内容。RUN echo 'we are running some of cool things' 第一个Dockerfile我的目标是为运行在tomcat容器中的一个搜索项目创建一个docker镜像。那么这个镜像需要包含以下几部分内容:1. java运行环境JDK。所以FROM镜像选择了openjdk:8-jdk-slim,首先因为它比较小而且是open官方开源的,其次是因为Oracle的jdk需要账号验证,比较烦。2. web容器tomcat。我直接在根目录放置了一个tomcat7版本。3. 项目war包。直接将war包放置到tomcat的webapps目录。简单的Dockerfile文件内容如下:bash 选择open官方的jdk镜像作为我的基础镜像FROM openjdk:8-jdk-slim 将上下文根目录的tomcat复制到镜像对应的目录;这里要注意的是镜像中的目录名字必须和源目录名一致ADD apache-tomcat-7.0.73 /opt/apache-tomcat-7.0.73/ 设置工作目录WORKDIR /opt/ 指定镜像内部的挂载点,可以通过docker run的-d参数映射到宿主机 可以指定多个,VOLUME ["/opt/data1", "/opt/data2"]VOLUME /opt/ 这里指定了镜像容器的暴露端口;通常只是提示将哪个端口作为服务;正常情况下并不能访问宿主机的该端口,在使用时还是需要通过参数-p参数来指定暴露和映射EXPOSE 8080 最后一个命令行可以是RUN,CMD或者ENTRYPOINTCMD ["/opt/apache-tomcat-7.0.73/bin/catalina.sh","run"]关于RUN、CMD、ENTRYPOINT命令的区别,参考[网友文章](https://www.cnblogs.com/klvchen/p/9238410.html)。 创建并运行镜像执行之前介绍过的命令docker build -t hosearch:v2 .。运行镜像:bashdocker run -d -p 8081:8080 -v /Users/wjyuian/mnt/search/:/opt/apache-tomcat-7.0.73/logs/ --name hosearch hosearch:v2+ docker run :运行镜像。+ -d :后台运行。+ -p 8081:8080 :将容器暴露的8080端口与宿主机的8081端口进行映射,我们直接访问宿主机IP:8081就相当于访问容器:8080。+ -v :将容器的挂载点目录或者子目录挂载带宿主机目录,这里是将容器目录/opt/apache-tomcat-7.0.73/logs/挂载到宿主机的/Users/wjyuian/mnt/search/。+ --name :指定容器的名字;之后可以通过docker stop 容器名字或者docker start 容器名字来停止或者运行容器。+ hosearch:v2 :是需要运行的镜像以及版本。至此我已经成功创建一个搜索服务的docker镜像,并且运行成功,也可以正常访问系统页面。----- 安装本地Docker仓库我这已有的Linux服务器安装的是CentOS6.\版本,无法直接通过常规命令安装docker。 失败发方法直接执行yum安装命令:bash[root@master ~] yum install dockerLoaded plugins: fastestmirrorSetting up Install ProcessDetermining fastest mirrors base: mirrors.cn99.com extras: mirrors.163.com updates: mirrors.cn99.combase 3.7 kB 00:00 extras 3.4 kB 00:00 extras/primarydb 29 kB 00:00 updates 3.4 kB 00:00 updates/primarydb 3.7 MB 00:00 No package docker available.Error: Nothing to do[root@master ~]执行失败,原因是yum源上无法找到docker,运行命令更新epel第三方软件库:bashsudo yum install epel-release然后再执行yum install dockerbash[root@branch-test ~] yum install dockerLoaded plugins: fastestmirrorSetting up Install ProcessLoading mirror speeds from cached hostfile base: mirrors.cn99.com epel: mirror.premi.st extras: mirrors.aliyun.com updates: ap.stykers.moeResolving Dependencies-- Running transaction check--- Package docker.x8664 0:1.5-5.el6 will be installed-- Finished Dependency ResolutionDependencies Resolved Package Arch Version Repository SizeInstalling: docker x8664 1.5-5.el6 epel 19 kTransaction SummaryInstall 1 Package(s)Total download size: 19 kInstalled size: 37 kIs this ok [y/N]: yDownloading Packages:docker-1.5-5.el6.x8664.rpm 19 kB 00:00 Running rpmcheckdebugRunning Transaction TestTransaction Test SucceededRunning Transaction Installing : docker-1.5-5.el6.x8664 1/1 Verifying : docker-1.5-5.el6.x8664 1/1Installed: docker.x8664 0:1.5-5.el6 Complete![root@branch-test ~] dockerSegmentation Fault or Critical Error encountered. Dumping core and aborting.Aborted[root@branch-test ~]虽然感觉安装成功了,但是执行docker命令时报错。后来查询资料发现说需要先yum remove docker再执行yum install docker-io,很遗憾,我这还是失败了。 终于找到成功的方法[参考文章](https://www.cnblogs.com/maodot/p/7654918.html)直接指定安装软件包:bashyum install https://get.docker.com/rpm/1.7.1/centos-6/RPMS/x8664/docker-engine-1.7.1-1.el6.x8664.rpm记得执行前先删除之前安装过的docker包,命令docker remove docker。出现以下内容表示安装成功:bashInstalled: docker-engine.x8664 0:1.7.1-1.el6 Dependency Installed: libcgroup.x8664 0:0.40.rc1-27.el610 xz.x8664 0:4.999.9-0.5.beta.20091007git.el6 Complete![root@branch-test ~] docker这个时候执行docker命令会显示docker命令对应的参数,表示成功。不过此时执行docker search mysql还是不行,因为Docker服务并没有启动:bash[root@branch-test ~] service docker startStarting cgconfig service: [ OK ]Starting docker: [ OK ][root@branch-test ~]至此,在CentOS6.\上安装docker环境成功。 通过镜像安装仓库从docker官方拉取仓库镜像:bash[root@branch-test ~] docker search registryNAME DESCRIPTION STARS OFFICIAL AUTOMATEDregistry The Docker Registry 2.0 implementation for... 2552 [OK] [root@branch-test ~] docker pull registrylatest: Pulling from registry3ade34601851: Pull complete5051fe558ac9: Pull completeee207260066f: Pull complete1a6638ea4ae6: Pull completed0f44026bcfd: Pull completed33041cd4577: Pull completea2f981cd64ba: Pull complete0065ebdad215: Pull completee003ce632a6d: Pull completef99738109191: Pull completeDigest: sha256:db8e07b1da92e1774458798a018512d71d869887d80b13cf126acda20122e41eStatus: Downloaded newer image for registry:latest[root@branch-test ~] docker imagesREPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZEregistry latest f99738109191 9 weeks ago 25.76 MB[root@branch-test ~]运行仓库:bash[root@branch-test mnt] docker run -d -v /mnt/dockers:/var/lib/registry -p 5000:5000 --restartalways --privilegedtrue --name registry registry:latesta016f2a8c50a2f7939aed9ee6c8b663ecde0dff794fbab74f529bd5a1fe43105[root@branch-test mnt] docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESa016f2a8c50a registry:latest \"/entrypoint.sh /etc 2 seconds ago Up 2 seconds 0.0.0.0:5000-5000/tcp registry [root@branch-test mnt] 连接仓库的HTTPS问题 Linux https问题当我们从私有仓库pull镜像时,会提示https错误:bash[root@branch-test ~] docker pull 192.168.100.185:5000/hosearch:v1Error response from daemon: invalid registry endpoint https://192.168.100.185:5000/v0/: unable to ping registry endpoint https://192.168.100.185:5000/v0/v2 ping attempt failed with error: Get https://192.168.100.185:5000/v2/: tls: oversized record received with length 20527 v1 ping attempt failed with error: Get https://192.168.100.185:5000/v1/ping: tls: oversized record received with length 20527. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add --insecure-registry 192.168.100.185:5000 to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/192.168.100.185:5000/ca.crt[root@branch-test ~]因为我自己搭建的私有仓库不支持HTTPS,而新版本Docker会默认要求通过HTTPS连接。所以需要修改Docker的启动参数。需要修改/etc/sysconfig/docker文件,在otherargs参数值里面添加我们的仓库地址和端口,并重启Docker服务:bash /etc/sysconfig/docker Other arguments to pass to the docker daemon process These will be parsed by the sysv initscript and appended to the arguments list passed to docker -dotherargs"--insecure-registry 192.168.100.185:5000"bash[root@branch-test ~] service docker restartStopping docker: [ OK ]Starting docker: [ OK ][root@branch-test ~] Mac下https问题直接在Docker的设置中增加http的仓库地址:![图片](https://oomabc.com/staticsrc/img/201905/10/15574620040917e458464155c47fb8590f46dcaa7dba3.jpg)---- Docker容器中dubbo注册地址问题前面自己创建的一个搜索镜像虽然可以成功运行,但是后来发现,dubbo注册到zk的地址是容器地址,而不是宿主机地址。在docker run的时候可以通过-p 20880:20881来进行dubbo端口映射,但是默认无法进行IP映射。所以可以手动通过宿主机IP:20880访问dubbo,但是消费者无法通过zk获得的IP访问该服务。容器IP是无法在局域网通信的。网上找了一圈,解决方案是挺多的,但是比较好的就是使用docker-compose来启动。问题又来了,CentOS6.\安装容器编排也遇到了许多问题,无奈放弃。后来看了dubbo的[github官网内容](https://github.com/apache/incubator-dubbo-samples/tree/master/dubbo-samples-docker),发现可以在执行docker run时指定环境参数,而且dubbo在2.6以及之后的版本新增了这个支持(较旧的版本没有DUBBOIPTOREGISTRY等相关参数)。bashdocker run -d -v /mnt/logs/search1/:/opt/apache-tomcat-7.0.73/logs/ -e DUBBOIPTOREGISTRY192.168.100.209 -e DUBBOPORTTOREGISTRY20881 -p 192.168.100.209:20881:20880 -p 8081:8080 --name hosearch1 hosearch:v1 主要是通过-e参数来指定容器内环境变量,dubbo在注册时会优先从环境变量中通过DUBBOIPTOREGISTRY来获取注册IP。

    Docker   Solr   Dubbo   2019-05-10 浏览(568) 阅读原文>>
  • blogTest
    分享文章
     
    使用APP的"扫一扫"功能,扫描左边的二维码,即可将网页分享给别人。
    你也可以扫描右边本博客的小程序二维码,实时关注最新文章。