最近负责的系统要接入solr,原因嘛,就是那些前辈们以前对数据库的操作,比如全表扫描,循环全表扫描等,随着库中数据的增大,导致系统动不动就罢工,全部改动需要时间,紧急解决方案就是用solr部分替代原数据库。。。
先说说什么是solr,我从百度上复制了一点定义:
Solr是一个独立的企业级搜索应用服务器,它对外提供类似于Web-service的API接口。用户可以通过http请求,向搜索引擎服务器提交一定格式的XML文件,生成索引;也可以通过Http Get操作提出查找请求,并得到XML格式的返回,Solr是一个高性能,采用Java5开发,基于Lucene的全文搜索服务器。同时对其进行了扩展,提供了比Lucene更为丰富的查询语言,同时实现了可配置、可扩展并对查询性能进行了优化,并且提供了一个完善的功能管理界面,是一款非常优秀的全文搜索引擎。
多了我就不复制了,大家可以网上搜搜看,我这里把我如何用到solr,已经用到了solr中的哪些地方和如何使用展示出来,solr的功能非常强大,但是我用到的到不是很多。
下面我按照需求-思路-实现来讲解solr的使用,这是我平生第一次使用solr,不到之处还望多多指正:
需求原因:系统数据量太大,根据系统业务逻辑需要,sql已经没有多少优化空间,并发量太大和过大的数据量导致查询响应速度缓慢,页面加载缓慢,用户体验非常不好。
需求:系统原来的逻辑是:redis->DB,修改为redis->solr->DB
这样设计的原因:
1、首先同等并发量和数据量的情况下,solr检索出结果集的速度远大于关系型数据库(具体原因涉及到检索排序算法<solr是基于倒排序算法的> 等多种原因)
2、从solr服务器中读取数据,就避免了访问访问DB,除非solr服务器出现问题,这个概率不是很大
当然主要原因还是第一个,查询速度快,用户体验会上升
开发思路:既然系统要接入solr,自己首先得弄清楚原理,为什么solr可以当作数据库来使用。
solr是企业级搜索服务器,提供对数据的存储,数据索引创建等一系列功能。比如对一条数据创建索引的时候,同时将这个数据保存到solr服务器,就可以根据索引数据查询到整条数据,知道solr可以当作容器一样存放数据,除此之外,solr还提供了跟DB类似的功能-对数据的增删改查,知道这些,就知道为什么可以“替代”DB了吧。
代码开发阶段:
一、开发环境的搭建:
solr是独立的服务器,所以我们需要搭建环境,我使用的应用服务器是tomcat,将solr接到tomcat服务器中即可,至于怎么配置的,我这里不想详细说明了,网上配置这个东西的就像配置JDK环境变量一样多,大家自己动手,丰衣足食,我会上传一个我配置好solr功能的tomcat服务器,大家可以直接下载,地址:http://download.csdn.net/detail/duyunduzai/7286411
二、代码开发:
我代码中是将solr封装成一个接口,只需要调用对索引增删改查的接口即可。使用的是solrj系类功能,下面会针对代码详细讲述。
1、创建接口
/**
*
* 前面文章中说过了,是对系统的优化,假如之前数据库中保存的数据是用户的信息
* User:username,age,email,custNo;
*
*/
public interface ISolrUserService {
/**
* 一句话功能描述:创建用户索引数据
*/
public boolean createUserIndex(User user);
/**
* 一句话功能描述:批量创建用户索引数据
*/
public boolean createUserIndex(List<User> userLists);
/**
* 一句话功能描述:批量删除用户索引数据
*/
public boolean deleteUserIndex(List<String> custNos);
/**
* 一句话功能描述:查询用户数据
*/
public QueryResult<SearchUserDTO> queryUser(SearchPage page);
}
上面的代码是一个接口,其中包含的是对用户数据索引的创建、删除和查询,User类是用户信息类,包含四个属性:username,age,email,custNo,创建对应的实体类:
public class User {
private String custNo;
private String username;
private String email;
private int age;
// 省略get和set方法
}
/**
*
* 功能描述: 查询结果基类
*/
public class QueryResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
private List<T> datas;
private Boolean isLastPage;
private Integer totalDataCount;
private int pageNumber = 1;
private int pageSize = 10;
private Integer pageCount;
private int indexNumber;
/**
* @param totalDataCount 总数据件数
* @param pageSize 每页显示条数
* @param pageNumber 当前的页数
*/
public QueryResult(int totalDataCount, int pageSize, int pageNumber) {
super();
this.totalDataCount = totalDataCount;
this.pageSize = pageSize;
this.pageNumber = pageNumber;
if (this.pageNumber < 1) {
this.pageNumber = 1;
}
if (this.totalDataCount <= 0) {
return;
}
// 如果查询页数大于总页数,则取最后一页
if (this.totalDataCount <= (this.pageNumber - 1) * this.pageSize) {
this.pageNumber = (this.totalDataCount + this.pageSize - 1) / this.pageSize;
}
this.indexNumber = (this.pageNumber - 1) * this.pageSize;
// 总页数
this.pageCount = (this.totalDataCount + this.pageSize - 1) / this.pageSize;
// 是否为最后一页
this.isLastPage = (this.pageNumber == this.pageCount ? true : false);
}
public QueryResult() {
super();
}
/**
* 返回的数据集
* @return the datas
*/
public List<T> getDatas() {
return datas;
}
/**
* @param datas the datas to set
*/
public void setDatas(List<T> datas) {
this.datas = datas;
}
/**
* 满足查询条件的总记录数, null 意味着未知。注:只在查询第一页时返回正确的总记录数,其它页码时,返回-1
* @return the totalDataCount
*/
public Integer getTotalDataCount() {
return totalDataCount;
}
/**
* @param totalDataCount the totalDataCount to set
*/
public void setTotalDataCount(Integer totalDataCount) {
this.totalDataCount = totalDataCount;
}
/**
* 页码,从1开始
* @return the pageNumber
*/
public int getPageNumber() {
return pageNumber;
}
/**
* @param pageNumber the pageNumber to set
*/
public void setPageNumber(int pageNumber) {
this.pageNumber = pageNumber;
}
/**
* 满足查询条件的总页数, null 意味着未知。注:只在查询第一页时返回正确的总记录数,其它页码时,返回-1
*
* @return the pageCount
*/
public Integer getPageCount() {
return pageCount;
}
/**
* @param pageCount the pageCount to set
*/
public void setPageCount(Integer pageCount) {
this.pageCount = pageCount;
}
/**
* 每页大小,缺省为10条记录/页
* @return the pageSize
*/
public int getPageSize() {
return pageSize;
}
/**
* @param pageSize the pageSize to set
*/
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
/**
* 标志是否最后一页,True: 是最后一页,False: 不是,null:未知
* @return the lastPage
*/
public Boolean getIsLastPage() {
return isLastPage;
}
/**
* @param lastPage the lastPage to set
*/
public void setIsLastPage(Boolean lastPage) {
this.isLastPage = lastPage;
}
/**
* 计算开始数
* @return the lastPage
*/
public int getIndexNumber() {
return indexNumber;
}
}
public class SearchPage implements Serializable {
private static final long serialVersionUID = 1L;
/** 页码 */
private int pageNumber = 1;
/** 每页记录数 */
private int pageSize = 10;
/** 总记录数 */
private int totalCount;
/** 排序字段 */
private String[] orderType;
/**
* 查询关键字
*/
private String keyword;
/** 多条件查询 */
private String selectParam;
/** 默认查询字段 */
private String field;
private int Start = 0;
// 省略get和set
}
接下来是对isolrUserService接口的实现类,在实现类中就是具体的实现如何对数据创建索引,删除索引等操作
@Service
public class SolrUserServiceImpl implements IsolrUserService {
private static final Logger LOGGER = LoggerFactory.getLogger(SolrUserServiceImpl.class);
private static HttpSolrServer httpSolrServer;
/** httpServer 是用来连接solr服务器,这里采用单例模式设计 */
private static HttpSolrServer getHttpSolrServer() {
if (httpSolrServer == null) {
/** 用户(User)数据solr服务地址 */
httpSolrServer = new HttpSolrServer("http://127.0.0.1:8983/solr/user");
/** 设置solr查询超时时间 */
httpSolrServer.setSoTimeout(1000);
/** 设置solr连接超时时间 */
httpSolrServer.setConnectionTimeout(1000);
/** solr最大连接数 */
httpSolrServer.setDefaultMaxConnectionsPerHost(1000);
/** solr最大重试次数 */
httpSolrServer.setMaxRetries(1);
/** solr所有最大连接数 */
httpSolrServer.setMaxTotalConnections(100);
/** solr是否允许压缩 */
httpSolrServer.setAllowCompression(false);
/** solr是否followRedirects */
httpSolrServer.setFollowRedirects(true);
}
return httpSolrServer;
}
@Override
public boolean createUserIndex(User user) {
// 获取solr服务
SolrServer solrServer = getHttpSolrServer();
try {
// 创建索引,因为solr创建索引的时候,在参数类中的属性上面需要注解@Field,
//所以,要将user类转换成可以创建索引的类,我单独创建了一个类,对应User,
// SearchUserDTO.java,跟User类属性一样,只是在各个属性上面添加@Field注解
solrServer.addBean(toSearchUser(user));
// 提交创建。就相当于DB中的commit
solrServer.commit();
return true;
} catch (IOException e) {
LOGGER.error("", e);
} catch (SolrServerException e) {
LOGGER.error("", e);
}
return false;
}
private SearchUserDTO toSearchUser(User user) {
SearchUserDTO userDTO = new SearchUserDTO();
// 此方法是将user中属性的值复制到userDTO属性,这个方法是复制类中属性名一样的属性值
BeanUtils.copyProperties(user, userDTO);
return userDTO;
}
@Override
public boolean createUserIndex(List<User> users) {
if (users != null && users.size() > 0) {
List<SearchUserDTO> datas = new ArrayList<SearchUserDTO>(users.size());
for (User user : users) {
datas.add(toSearchUser(user));
}
SolrServer solrServer = getHttpSolrServer();
try {
// 批量创建评价回复索引数据
solrServer.addBeans(datas);
solrServer.commit();
return true;
} catch (IOException e) {
// 如果创建失败的话,可以回滚
// solrServer.collback();
LOGGER.error("", e);
} catch (SolrServerException e) {
LOGGER.error("", e);
}
}
return false;
}
@Override
public boolean deleteUserIndex(List<String> custNos) {
SolrServer solrServer = getHttpSolrServer();
try {
// 根据唯一性标识删除索引
solrServer.deleteById(custNos);
// 删除该核下所有索引
// solrServer.delete("*:*");
solrServer.commit();
return true;
} catch (IOException e) {
LOGGER.error("", e);
} catch (SolrServerException e) {
LOGGER.error("", e);
}
return false;
}
@Override
public QueryResult<SearchUserDTO> queryUser(SearchPage pager) {
QueryResult<SearchUserDTO> queryResult = new QueryResult<SearchUserDTO>();
QueryResponse response = null;
// 设置默认查询条件,格式为:field:keyword,比如:"custNo:1234"
String searchParam = pager.getField() + ":" + pager.getKeyword();
SolrServer server = getHttpSolrServer();
SolrQuery query = new SolrQuery(searchParam);
// 设置限制条件查询,假如同时查询username为zhangsan的用户,这里查询条件格式我就暂不多说了,下面会和配置文件一起来说一下
// query.setFilterQueries("username:zhangsan");
query.setFilterQueries(pager.getSelectParam());
query.setStart(pager.getStart()); // 起始位置,用于分页,solrj中默认是每页10条数据
query.setRows(pager.getPageSize()); // 每页文档数
try {
response = server.query(query);
} catch (SolrServerException e) {
LOGGER.error("查询索引出现问题", e);
}
if (response != null) {
SolrDocumentList list = response.getResults();
List<SearchUserDTO> datas = new ArrayList<SearchUserDTO>();
setSearchUserDTOData(datas, list);
queryResult.setTotalDataCount(new Long(list.getNumFound()).intValue());
queryResult.setPageNumber(pager.getPageNumber());
queryResult.setPageSize(pager.getPageSize());
queryResult.setDatas(datas);
}
return queryResult;
}
private void setSearchUserDTOData(List<SearchUserDTO> datas, SolrDocumentList list) {
for (SolrDocument solrDocument : list) {
SearchUserDTO userDTO = new SearchUserDTO();
// 根据属性名称从返回结果中取得数据,并且封装到返回对象中
SearchUserDTO.setUsername(solrDocument.getFieldValue("username").toString());
SearchUserDTO.setEmail(solrDocument.getFieldValue("email").toString());
SearchUserDTO.setCustNo(solrDocument.getFieldValue("custNo").toString());
SearchUserDTO.setAger((Integer)solrDocument.getFieldValue("age"));
datas.add(SearchUserDTO);
}
}
}
solr服务支持单核和多核,假如只是对一张表进行创建索引数据,只使用单核就可以了,但是如果你对用户表创建索引数据,同时又对商品信息表创建索引数据,就需要对这两个表的索引数据放在不同 的地址下面,这里就利用到了solr多核,我上传到tomcat-solr服务器就是多核配置
解压tomcat-solr,在../apache-tomcat-7.0.26-master\webapps\solr\conf\multicore\下面有一个solr.xml,这里配置的是多核信息,在user、productInfo表示两个核,这里以user为例,user文件夹下有两个文件夹,conf和data,其中data中存放的是索引文件,这个就不多说了,索引文件的格式,内容等是solrj创建索引的时候写到里面去的,说一下conf问价夹,conf下面有三个文件:solrconfig.xml,scheam.xml和dataimport.properties,其中solrconfig.xml和dataimport.properties是可以配置很多功能的,但是我这个例子中只需要用到schema.xml配置,其余两个用默认就行了,重点讲下schema.xml:
<?xml version="1.0" ?>
<schema name="example core user" version="1.1">
<types>
<fieldtype name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/>
<!--add IKAnalyzer configuration-->
<fieldType name="textik" class="solr.TextField" >
<analyzer type="index">
<tokenizer class="org.wltea.analyzer.solr.IKTokenizerFactory" isMaxWordLength="false" useSmart="false"/>
</analyzer>
<analyzer type="query">
<tokenizer class="org.wltea.analyzer.solr.IKTokenizerFactory" isMaxWordLength="false" useSmart="false"/>
</analyzer>
</fieldType>
<fieldType name="int" class="solr.TrieIntField" precisionStep="0" positionIncrementGap="0"/>
</types>
<fields>
<field name="username" type="textik" indexed="true" stored="true" multiValued="false" required="true"/>
<field name="custNo" type="string" indexed="true" stored="true" multiValued="false" required="true" />
<field name="email" type="string" indexed="true" stored="true" multiValued="false" />
<field name="age" type="int" indexed="false" stored="true" multiValued="false" />
</fields>
<!-- field to use to determine and enforce document uniqueness. -->
<uniqueKey>custNo</uniqueKey>
<!-- field for the QueryParser to use when an explicit fieldname is absent -->
<defaultSearchField>username</defaultSearchField>
<!-- SolrQueryParser configuration: defaultOperator="AND|OR" -->
<solrQueryParser defaultOperator="OR"/>
</schema>
types:这里面配置的是字段类型,就像java中的int,long,String等,用fieldType标签表示,以String为例:
<fieldtype name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/>
name:FieldType的名称
class:指向org.apache.solr.analysis包里面对应的class名称,用来定义这个类型的行为
sortMissingLast:设置成true没有该field的数据排在有该field的数据之后,而不管请求时的排序规则, 默认是设置成false。 sortMissingFirst 跟上面倒过来呗
omitNorms:true 则字段检索时被省略相关的规范
fields:
name:字段名称
type:类型,此处写的类型必须在fieldType中声明,假如你type=long,在fieldType中就要有对应的long的声明(<fieldtype name="long" class="solr.LongField" omitNorms="true"/>),不然启动服务时会报错
indexed:是否创建索引,true表示创建,默认false,加入你username的indexed=false,那么你用username来查询索引是查询不到的
stored:是否保存数据,如果false的话,就查询返回结果中该字段数据就没有,这个属性在设计的时候是要注意的,有写字段不需要的话可以设为false,可以减小索引文件的大小
multiValued:是否多值,true的话就是多值,假如username的multiValued=true,在索引文件中查出的结果就是username['zhangsan','lisi'..]
required:是否必须,假如username的required=true,那么在创建索引的时候,username就必须有值,否则创建不成功
当然,field还包含其他很多属性,比如默认值defaule,是否压缩compressed等就不多写了,因为我没用到。
uniqueKey:唯一性主键
defaultSearchField:默认使用该字段查询,但是我们往往查询的时候是根据自己需要查询的,比如我们查询条件为:username:zhangsan,如果直接写成zhangsan的话,查询条件就是:"defauleSearchField:zhangsan"
solrQueryParser:设置默认操作,OR还是AND,加入我们设置<solrQueryParser defaultOperator="OR"/>,我们查询username:zhangsan lisi ,就相当于username=zhangsan OR username=lisi,如果这个地方我们设置成and的话,就是username=zhagnsan and username=lisi
配置文件这里就讲完了,现在大家应该懂了吧,下面加一点查询扩展
/*设置查询条件
*查询所有:*:*
*按照名称查询:username:zhangsan
*按照email查询:email:1234@sina.cn
*我们假设按照名称查询
*/
SolrQuery query = new SolrQuery("username:zhangsan");
/*
*设置限制级查询条件
*假如我们同时查出email是1234@sina.cn的
*
*这里面也可以多条件,假如email是1234@sina.cn同时年龄是21
*就应该写成:email:1234@sina.cn AND age:21(注意这里面的AND和OR必须大写)
*/
query.setFilterQueries("email:1234@sina.cn AND age:21");
/*
*设置排序
*假如要求按照名称降序
*/
query.addSortField("username", ORDER.desc);
query.addSortField(username, order);
query.setStart(pager.getStart()); // 起始位置,用于分页
query.setRows(pager.getPageSize()); // 每页文档数
response = server.query(query);
// 概括一下上面的查询条件就是:username=zhangsan,age=21 email=1234@sina.cn,按照username降序排列(上面age的indexed=false,应该改为true,才能按照age索引)