solr5项目实战详解,分布式缓存,全文检索
说一下大体思路,电商类网站,由于老项目数据库设计很不合理,一些查询涉及的表过多,导致查询速度异常缓慢,在不修改架构设计和源码上,做了一下处理。
solr+eh ,使用eh缓存关联数据,再用solr查询速度,文章偏向小白文,大神见笑。很多设计不完善,实现功能为主。
一、配置缓存功能
结合我之前博文的eh文章配置,让项目启动后自动拉取数据存入缓存。
首先建立一个监听类,实现项目启动后调用。
StartupListener.java
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> { @Override public void onApplicationEvent(ContextRefreshedEvent event) { //缓存数据 }
公共容器xml配置添加
<bean id="startupListener" class="StartupListener类路径"></bean>
封装了EH工具类
EHUtil.java
import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Element; public class EHUtil { private static final CacheManager manager = new CacheManager("src/main/resources/ehcache.xml"); public static void setCache(String cacheName,String key,Object value){ Cache cache = manager.getCache(cacheName); Element element = new Element(key,value); cache.put(element); } public static Object getCache(String cacheName,String key){ Cache cache = manager.getCache(cacheName); Element element = cache.get(key); if (element != null) { return element.getValue(); } return null; } public static boolean removeCache(String cacheName,String key){ Cache cache = manager.getCache(cacheName); return cache.remove(key); } public static int getSize(String cacheName){ Cache cache = manager.getCache(cacheName); return cache.getSize(); } public static long getMemoryStoreSize(String cacheName){ Cache cache = manager.getCache(cacheName); return cache.getMemoryStoreSize(); } }
定义 getCache 返回类型为Object 如果需要返回javaBean,需要实现序列化
然后在项目Service内调用,保证操作dao层数据修改同步到EH缓存里,具体根据业务编写。
二、solr检索配置
创建solr工具类
SolrUti.java
import java.util.ArrayList; import java.util.List; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.HttpSolrClient; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.client.solrj.response.UpdateResponse; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.params.ModifiableSolrParams; import com.penuel.mythopoetms.model.SolrCode; public class SolrUtil { //填写你得solr服务器的core地址,例如: http://XXX/solr/db private static final String URL = Constants.get("SOLR_URL"); private static HttpSolrClient server = null; static{ server = new HttpSolrClient(URL); } public static void addDoc(SolrInputDocument doc){ try { UpdateResponse response = server.add(doc); // 提交 server.commit(); // System.out.println("########## Query Time :" + response.getQTime()); // System.out.println("########## Elapsed Time :" + response.getElapsedTime()); // System.out.println("########## Status :" + response.getStatus()); } catch (Exception e) { e.printStackTrace(); } } public static void addDocs(Object key,List<Object> objs){ List<SolrInputDocument> docs = new ArrayList<SolrInputDocument>(); for(Object obj:objs ){ SolrInputDocument doc = new SolrInputDocument(); doc.addField(SolrCode.id.code, key); doc.addField(SolrCode.title.code, obj); docs.add(doc); } try { server.add(docs); // 提交 server.commit(); } catch (Exception e) { e.printStackTrace(); } } public static void byAddDocs(List<SolrInputDocument> docs){ try { server.add(docs); // 提交 server.commit(); } catch (Exception e) { e.printStackTrace(); } } public static void removeDoc(String key){ try { server.deleteById(key); server.commit(); } catch (Exception e) { e.printStackTrace(); } } public static void removeQuery(String key){ try { server.deleteByQuery(key); server.commit(); } catch (Exception e) { e.printStackTrace(); } } public static SolrDocumentList queryPage(ModifiableSolrParams params){ try { QueryResponse response = server.query(params); SolrDocumentList list = response.getResults(); // System.out.println("########### 总共 : " + list.getNumFound() + "条记录"); // System.out.println("########### 总共 : " + response.getQTime()+ "毫秒"); // // for (SolrDocument doc : list) { // System.out.println("######### id : " + doc.get("id") + " title : " + doc.get("title")); // } return list; } catch (SolrServerException e) { e.printStackTrace(); } return null; } public static SolrDocument queryById(ModifiableSolrParams params){ try { QueryResponse response = server.query(params); SolrDocumentList list = response.getResults(); if(list.size()>0){ return list.get(0); } } catch (SolrServerException e) { e.printStackTrace(); } return null; } public static String getIds(SolrDocumentList list){ StringBuffer sb = new StringBuffer(); for(SolrDocument doc:list){ String sss = ((String) doc.get("id")).replaceAll("\\D+", ""); if(sb.length()==0){ sb.append(sss); continue; } sb.append(","+sss); } return sb.toString(); } }
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
配置完成,现在理顺一下项目业务,
例:获取一个商品列表,商品名和设计师名,两个字段需要检索,然后提供根据更新时间排序,并实现分页。
分析:前面说了,数据库设计结构为 (A表 B表 AB表)一但这种表多起来, 查询会很慢。
数据库设计解决:A: 商品表,B:设计师表,C:分类,D:品牌。 可见设计库设计的不合理,在不修改原架构上进行处理。
1.建立缓存。把 AB,AC,AD,B,C,D 等表 建立缓存。
配置统一标准规则key来主键查询缓存,如(XXX+A表的id)这样就可以根据A表id直接提取缓存数据。
创建枚举类
CacheCode.java
public enum CacheCode { Cache("myCache"),designer("designerKEY"),brand("brandKEY") ,designerI("designerIKEY"),brandI("brandIKEY") ,cate("cateKEY"),cateI("cateIKEY"); public String code; private CacheCode(String code){ this.code=code; } }
创建好后按照规则 启动拉取数据存入缓存,修改之前的监听类
StartupListener.java
import java.util.ArrayList; import java.util.List; import org.apache.solr.common.SolrInputDocument; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Service; import com.penuel.mythopoetms.model.Brand; import com.penuel.mythopoetms.model.CacheCode; import com.penuel.mythopoetms.model.Category; import com.penuel.mythopoetms.model.Designer; import com.penuel.mythopoetms.model.Item; import com.penuel.mythopoetms.model.ItemBrand; import com.penuel.mythopoetms.model.ItemCate; import com.penuel.mythopoetms.model.ItemDesigner; import com.penuel.mythopoetms.model.ItemOther; import com.penuel.mythopoetms.service.BrandService; import com.penuel.mythopoetms.service.CategoryService; import com.penuel.mythopoetms.service.DesignerService; import com.penuel.mythopoetms.service.ItemBrandService; import com.penuel.mythopoetms.service.ItemCateService; import com.penuel.mythopoetms.service.ItemDesignerService; import com.penuel.mythopoetms.service.ItemHelpService; import com.penuel.mythopoetms.service.ItemOtherService; import com.penuel.mythopoetms.service.ItemService; @Service public class StartupListener implements ApplicationListener<ContextRefreshedEvent> { @Autowired private DesignerService designerService; @Autowired private BrandService brandService; @Autowired private ItemDesignerService itemDesignerService; @Autowired private ItemBrandService itemBrandService; @Autowired private CategoryService categoryService; @Autowired private ItemCateService itemCateService; @Autowired private ItemService itemService; @Override public void onApplicationEvent(ContextRefreshedEvent event) { try { //设计师关联表 List<ItemDesigner> idList = itemDesignerService.list(); if(idList!=null&&idList.size()>0){ for(ItemDesigner idr:idList){ EHUtil.setCache(CacheCode.Cache.code, CacheCode.designerI.code+idr.getItemId(), idr.getDesignerId()); } } //设计师 List<Designer> dlist = designerService.listDesigners(); if(dlist!=null&&dlist.size()>0){ for(Designer designer:dlist){ EHUtil.setCache(CacheCode.Cache.code, CacheCode.designer.code+designer.getId(), designer); } } //品牌关联表 List<ItemBrand> ibList = itemBrandService.list(); if(ibList!=null&&ibList.size()>0){ for(ItemBrand ib:ibList){ EHUtil.setCache(CacheCode.Cache.code, CacheCode.brandI.code+ib.getItemId(), ib.getBrandId()); } } //品牌 List<Brand> blist = brandService.listBrands(); if(blist!=null&&blist.size()>0){ for(Brand brand:blist){ EHUtil.setCache(CacheCode.Cache.code, CacheCode.brand.code+brand.getId(), brand); } } //标签关联 List<ItemCate> icList = itemCateService.list(); if(icList!=null&&icList.size()>0){ for(ItemCate ic:icList){ EHUtil.setCache(CacheCode.Cache.code, CacheCode.cateI.code+ic.getItemId(), ic.getCateId()); } } //标签 List<Category> cList = categoryService.listCategories(); if(cList!=null&&cList.size()>0){ for(Category category:cList){ EHUtil.setCache(CacheCode.Cache.code, CacheCode.cate.code+category.getId(), category); } } } }
Service自行添加编写,不做描述。
下面开始solr相关配置
2.编写业务工具类,用于增删改查 solr 索引,
首先,设计好solr索引库要存的字段。前面讲过:获取一个商品列表,商品名和设计师名,两个字段需要检索,然后提供根据更新时间排序,并实现分页
所以solr存储数据的格式应该是这样的
['ltime:'更新时间',id:'itemId+商品ID','title:'商品名称','name':'设计师名字']
其中id为 唯一,相同id会覆盖操作,这里不懂solr配置的可看我前面的文章配置,包括中文分词。
先从建立索引开始入手。同样建立solr的枚举类统一key规则
SolrCode.java
public enum SolrCode { id("id"),title("title"),ltime("last_modified"),name("name"); public String code; private SolrCode(String code){ this.code=code; } }
编写业务类建立索引
ItemHelpService.java
import java.io.IOException; import java.io.StringReader; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.params.ModifiableSolrParams; import org.wltea.analyzer.core.IKSegmenter; import org.wltea.analyzer.core.Lexeme; import com.penuel.mythopoetms.model.CacheCode; import com.penuel.mythopoetms.model.Designer; import com.penuel.mythopoetms.model.Item; import com.penuel.mythopoetms.model.SolrCode; import com.penuel.mythopoetms.utils.EHUtil; import com.penuel.mythopoetms.utils.SolrUtil; public class ItemHelpService { /** * @param date 时间 * @param strs 多个参数 * @return * @throws ParseException */ public static SolrInputDocument addDocsHelp(Date date,String ...strs) throws ParseException{ SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); SolrInputDocument doc = new SolrInputDocument(); doc.addField(SolrCode.ltime.code,date); doc.addField(SolrCode.id.code, strs[0]); doc.addField(SolrCode.title.code, strs[1]); doc.addField(SolrCode.name.code, strs[2]); return doc; } }
编写完成后 修改 StartupListener 启动项目并建立索引
StartupListener.java
import java.util.ArrayList; import java.util.List; import org.apache.solr.common.SolrInputDocument; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Service; import com.penuel.mythopoetms.model.Brand; import com.penuel.mythopoetms.model.CacheCode; import com.penuel.mythopoetms.model.Category; import com.penuel.mythopoetms.model.Designer; import com.penuel.mythopoetms.model.Item; import com.penuel.mythopoetms.model.ItemBrand; import com.penuel.mythopoetms.model.ItemCate; import com.penuel.mythopoetms.model.ItemDesigner; import com.penuel.mythopoetms.model.ItemOther; import com.penuel.mythopoetms.service.BrandService; import com.penuel.mythopoetms.service.CategoryService; import com.penuel.mythopoetms.service.DesignerService; import com.penuel.mythopoetms.service.ItemBrandService; import com.penuel.mythopoetms.service.ItemCateService; import com.penuel.mythopoetms.service.ItemDesignerService; import com.penuel.mythopoetms.service.ItemHelpService; import com.penuel.mythopoetms.service.ItemOtherService; import com.penuel.mythopoetms.service.ItemService; @Service public class StartupListener implements ApplicationListener<ContextRefreshedEvent> { @Autowired private DesignerService designerService; @Autowired private BrandService brandService; @Autowired private ItemDesignerService itemDesignerService; @Autowired private ItemBrandService itemBrandService; @Autowired private CategoryService categoryService; @Autowired private ItemCateService itemCateService; @Autowired private ItemOtherService itemOtherService; @Autowired private ItemService itemService; @Override public void onApplicationEvent(ContextRefreshedEvent event) { try { //设计师关联表 List<ItemDesigner> idList = itemDesignerService.list(); if(idList!=null&&idList.size()>0){ for(ItemDesigner idr:idList){ EHUtil.setCache(CacheCode.Cache.code, CacheCode.designerI.code+idr.getItemId(), idr.getDesignerId()); } } //设计师 List<Designer> dlist = designerService.listDesigners(); if(dlist!=null&&dlist.size()>0){ for(Designer designer:dlist){ EHUtil.setCache(CacheCode.Cache.code, CacheCode.designer.code+designer.getId(), designer); } } //品牌关联表 List<ItemBrand> ibList = itemBrandService.list(); if(ibList!=null&&ibList.size()>0){ for(ItemBrand ib:ibList){ EHUtil.setCache(CacheCode.Cache.code, CacheCode.brandI.code+ib.getItemId(), ib.getBrandId()); } } //品牌 List<Brand> blist = brandService.listBrands(); if(blist!=null&&blist.size()>0){ for(Brand brand:blist){ EHUtil.setCache(CacheCode.Cache.code, CacheCode.brand.code+brand.getId(), brand); } } //标签关联 List<ItemCate> icList = itemCateService.list(); if(icList!=null&&icList.size()>0){ for(ItemCate ic:icList){ EHUtil.setCache(CacheCode.Cache.code, CacheCode.cateI.code+ic.getItemId(), ic.getCateId()); } } //标签 List<Category> cList = categoryService.listCategories(); if(cList!=null&&cList.size()>0){ for(Category category:cList){ EHUtil.setCache(CacheCode.Cache.code, CacheCode.cate.code+category.getId(), category); } } //商品url List<ItemOther> ioList = itemOtherService.list(); if(ioList!=null&&ioList.size()>0){ for(ItemOther io:ioList){ EHUtil.setCache(CacheCode.Cache.code, CacheCode.otherI.code+io.getItemId(), io.getUrlDetail()); } } //商品or设计师索引 //['ltime:'更新时间',id:'itemId+商品ID','title:'商品名称','name':'设计师名字'] List<SolrInputDocument> docs = new ArrayList<SolrInputDocument>(); List<Item> iList = itemService.listItems(); for(Item item:iList){ Designer dir =(Designer)EHUtil.getCache(CacheCode.Cache.code,CacheCode.designer.code+ EHUtil.getCache(CacheCode.Cache.code, CacheCode.designerI.code+item.getId())); if(dir==null) continue; docs.add(ItemHelpService.addDocsHelp(item.getUtime(),"itemId"+item.getId(),item.getName(), dir.getName())); } SolrUtil.byAddDocs(docs); } catch (Exception e) { e.printStackTrace(); } } }
Service 主要为数据拉取,可自行编写。
现在索引建立完成,可以开始进行搜索了。
这里需要中文分词器,配合solr的查询功能。方法如下。
ItemHelpService.java
import java.io.IOException; import java.io.StringReader; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.params.ModifiableSolrParams; import org.wltea.analyzer.core.IKSegmenter; import org.wltea.analyzer.core.Lexeme; import com.penuel.mythopoetms.model.CacheCode; import com.penuel.mythopoetms.model.Designer; import com.penuel.mythopoetms.model.Item; import com.penuel.mythopoetms.model.SolrCode; import com.penuel.mythopoetms.utils.EHUtil; import com.penuel.mythopoetms.utils.SolrUtil; public class ItemHelpService { /** * @param date 时间 * @param strs 多个参数 * @return * @throws ParseException */ public static SolrInputDocument addDocsHelp(Date date,String ...strs) throws ParseException{ SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); SolrInputDocument doc = new SolrInputDocument(); doc.addField(SolrCode.ltime.code,date); doc.addField(SolrCode.id.code, strs[0]); doc.addField(SolrCode.title.code, strs[1]); doc.addField(SolrCode.name.code, strs[2]); return doc; } /** * @param key 商品名 * @param key2 设计师 * @param sort 排序 * @param start 起始位 * @param rows 显示页数 * @return */ public static String[] queryCount(String key,String key2,String sort,int start,int rows){ ModifiableSolrParams params = new ModifiableSolrParams(); if(StringUtils.isBlank(key)){ if(StringUtils.isBlank(key2)) params.set("q", "*:*"); else params.set("q", "name:"+key2+"*"); }else{ if(key.length()<2) params.set("q", "title:"+key+"*"); else params.set("q", "title:"+key); params.set("fq", toDismantle(key,key2)); } params.set("sort", sort); params.set("start", start); params.set("rows", rows); String [] str = new String[2]; SolrDocumentList list = SolrUtil.queryPage(params); str[0]=SolrUtil.getIds(list); str[1]=Long.toString(list.getNumFound()); return str; } /** * @param text 商品名 * @param text2 设计师名 * @return */ public static String[] toDismantle(String text,String text2){ List<String> list = new ArrayList<String>(); if(text.length()<2){ list.add("title:"+text); }else{ StringReader sr=new StringReader(text); IKSegmenter ik=new IKSegmenter(sr, true); Lexeme lex=null; ModifiableSolrParams params = new ModifiableSolrParams(); params.set("q", "title:"+text); try { while((lex=ik.next())!=null){ list.add("title:"+lex.getLexemeText()+"*"); } } catch (IOException e) { e.printStackTrace(); } } if(StringUtils.isNotBlank(text2)){ list.add("name:"+text2+"*"); } String[] arr = (String[])list.toArray(new String[list.size()]); return arr; } }
这里可能看不太懂,
现讲一下 queryCount方法
key=商品名称,key2=设计师名称,sort=指定字段排序,start=起始位,rows=搜索数量
key默认值为 空串即"", key2 默认值为 空串即 ""
toDismantle方法
主要针对 key 值 做中文分词(IK2012版本),设计师不需要分词,因为设计师需要单字匹配,但是商品不可以,举一个例子 就会明白
例如,
查询: ("q","title:男士灰色")
不做分词处理的话,数据就会是这样的
输出 "男士灰色XXXX","女士灰色XXXX"
因为 不做分词处理。灰色为同性词,很多不相关的数据会掺杂进来。
做分词处理的效果:
查询: ("q","title:男士灰色")
分词处理并添加条件 ("fq",["title:男士*",title:灰色*])
数据就会过滤掉 不相关的词,比如"女士"等,
还有一处为查询字段长度判断,
if(key.length()<2) params.set("q", "title:"+key+"*");
如果用户搜索单个词。
例如
查询("q","title:男")
solr的分词器 会自动匹配 "男" 的索引 而不是模糊查询 所以要在后边加上* 来匹配模糊查询。
sort值为排序用,书写规则为 "字段 desc" 或者 "字段 asc"
start,rows。 最常见的分页功能,
start 值为:(当前页- 1) * 显示数量
rows 值为:显示数量
queryCount方法,是一个数组,
[0] 储存的是 查询出来的所有商品ID
[1]储存的是 商品条数
得到这些数据
,使用 sql select * from A表 where id in (数组[0]) 来获取商品列表数据。
然后遍历商品列表,根据其ID值 自动匹配 缓存里存储的 B表 C表 D表 等等数据。
最终效率从最开始的 3秒 到 现在的首次查询仅0.3秒左右,之后,平均查询占0.02秒。
讲到这里基本上是结束了,集体索引的更新操作和缓存操作基本一样,保持缓存 索引和 数据库同步即可。
配置方面可查询之前文章
EH集群:http://www.cnblogs.com/mangyang/p/5481713.html