• 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

    solr配置:http://www.cnblogs.com/mangyang/p/5500852.html

    solr5中文分词:http://www.cnblogs.com/mangyang/p/5502773.html

  • 相关阅读:
    [工作札记]01: CS系统中分页控件的制作
    【非技术】试占新型肺炎的情况与发展趋势
    给培训学校讲解ORM框架的课件
    SAAS云平台搭建札记: (二) Linux Ubutu下.Net Core整套运行环境的搭建
    SAAS云平台搭建札记: (一) 浅论SAAS多租户自助云服务平台的产品、服务和订单
    开源三维地球GIS引擎Cesium常用功能的开发
    Asp.net管理信息系统中数据统计功能的实现
    [Thrift]学习使用Thrift
    使用Netty实现一下简单RPC
    [Redis]Redis做简单的分布式限流
  • 原文地址:https://www.cnblogs.com/mangyang/p/5509646.html
Copyright © 2020-2023  润新知