说起搜索,大家心里边都有一些概念,我们平时使用的百度和谷歌,其实就是给我们拱了搜索的能力,当我们输入一个关键字点击搜索后,搜索引擎会返回给我们一大堆相关和不相关的内容。
当然谷歌和百度搜索引擎的实现原理和知识推荐远比我们想象的复杂的多,也不在此篇的讨论范畴,我们就聊一下基于业务场景的通用搜索。首先明确一个概念,一个成熟的系统或者网站都会有很人性化的用户导航能力,但是有些场景操作链路比较长,会对一些用户造成困扰和时间上的浪费,当然他们会选择一个快捷的搜索入口,然后检索到自己关注的内容进行操作。例如:一个医药管理系统,用户使用的时候寻找一个类目或者详情可能会在左侧导航栏点击几次才能找到,那如果在首页显眼的地方放置一个通用搜索框,用户输入关键字的时候系统查询出相似度比较高的相关类目,会很大程度上提高使用者的操作效率;再比如 在一个电商平台,用户输入关键字后会返回相关的店铺和产品。
也就是说,对于一个优秀的系统或者网站,通用搜索是一个必备的能力。接下来我们聊一下使用java自带线程池实现通用搜索功能。
背景描述
在一个售后服务平台,会员信息、退款信息和订单信息是用户最关心也是查看最多的功能,传统的实现方案是在左侧设置不同的菜单,针对不同的类目展示相应的信息列表,然后输入查询条件检索到具体的数据,最后点击详情查看到详细的信息。这种实现方案没有问题,但是假如我很明确想查看一笔订单的详细内容,那么我至少需要操作三步才能看到详情信息:
①找到类目对应的菜单点进去
②输入查询条件查询到相关数据
③点击详情查看详细信息
确实操作链路有点长,对于内部系统来说可能影响不大,如果是一个大型网站,那么很可能因为操作过于复杂导致用户量流失。
解决方案
我们编写系统和网站的最终目的是提供方便,提高效率和降低损耗,那么从一定程度上来说,能够让程序完成的操作,千万不要让用户去操作。对于上述的问题是将繁杂的检索操作交给程序完成,并且把三个步骤合成一个,也就是提供一个能力让用户输入一个关键信息由系统做分析检索并返回结果推荐给用户,具体要做什么操作由用户自己决定:
在上图用例交互很清晰的表达了,系统需要在页面提供一个通用搜索框,后台实现搜索能力,然后将结果返回给用户,具体查看哪些信息由用户自己选择。也即是在系统首页提供搜索框如下图:
技术实现
既然叫做通用搜索能力,从体验角度来说,无法忍受响应时间过长;也就是说从技术实现角度,串行查询然后返回结果是不可行的,也就是说如果不引入外部插件,我们要考虑使用多线程并行查询然后由程序调用处同意收口返回。jdk1.5引入了线程池,对并发提供了很好的支持,所以此处我们使用jdk自带线程池来实现并发查询,具体技术方案如下图:
核心代码结构图如下:
1.控制器层
@Controller
@RequestMapping("common")
public class SearchController extends BaseController {
@Autowired
private SearchManager searchManager;
/**
* 通用查询
* @param request
* @param response
*/
@RequestMapping(value = "/get_search",method = RequestMethod.GET)
public void commonSearch(HttpServletRequest request, HttpServletResponse response) {
String key = request.getParameter("key");
Operator operator = getOperator(request);
try {
List<SearchResultDto> list = this.searchManager.search(key,operator);
if(null == list || list.isEmpty()) {
this.putFailtResult(response,"没有查到结果");
return;
}
this.putSuccessResult(response,list);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.适配器层
/**
* 查询管理器
*
*/
@Service
public class SearchManager {
@Autowired
private List<ISearch> iSearchList;
ExecutorService executorService = Executors.newFixedThreadPool(10);
public List<SearchResultDto> search(String key, Operator operator) {
List<SearchResultDto> list = new ArrayList<>();
if(null == iSearchList || iSearchList.isEmpty()) {
return list;
}
List<Future<List<SearchResultDto>>> futureList = new ArrayList<>(iSearchList.size());
iSearchList.forEach(iSearch -> {
futureList.add(this.executorService.submit(new SearchThread(key,operator,iSearch)));
});
futureList.forEach(future -> {
try {
List<SearchResultDto>results = future.get();
if(null != results && !results.isEmpty()) {
list.addAll(results);
}
} catch (Exception e) {
e.printStackTrace();
}
});
return list;
}
}
3.实现层
ISearch:
/**
* 查询接口
*/
public interface ISearch {
/**
* 查询
*
* @param key
* @param operator
* @return
*/
List<SearchResultDto> search(String key, Operator operator);
}
AbstractSearch:
/**
* 通用搜索抽象类
*
*/
public abstract class AbstractSearch implements ISearch {
@Override
public List<SearchResultDto> search(String key, Operator operator) {
if(isParameterIllegal(key,operator)) {
return null;
}
return bizSearch(key,operator);
}
/**
* 具体业务查询
*
* @param key
* @param operator
* @return
*/
public abstract List<SearchResultDto> bizSearch(String key, Operator operator);
/**
* 检查入参是否非法
*
* @param key
* @param operator
* @return
*/
protected boolean isParameterIllegal(String key, Operator operator) {
if(null == key || key.isEmpty()) {
return true;
}
if(null == operator) {
return true;
}
return false;
}
}
UserSearchImpl:
/**
* 用户查询实现
*
*/
@Service
public class UserSearchImpl extends AbstractSearch {
@Autowired
private UserService userService;
@Override
public List<SearchResultDto> bizSearch(String key, Operator operator) {
if(!StringUtils.isNumeric(key)) {
return null;
}
List<SearchResultDto> list = new ArrayList<>();
User user = this.userService.queryByPK(Long.valueOf(key));
if(null == user) {
return list;
}
list.add(ConvertUtils.convertSearchResultDto(user));
return list;
}
}
OrderSearchImpl:
/**
* 订单查询实现
*
*/
@Service
public class OrderSearchImpl extends AbstractSearch {
@Autowired
private OrderService orderService;
@Override
public List<SearchResultDto> bizSearch(String key, Operator operator) {
if(!StringUtils.isNumeric(key)) {
return null;
}
List<SearchResultDto> list = new ArrayList<>();
Order order = this.orderService.queryByOrderId(Long.valueOf(key));
if(null == order) {
return list;
}
list.add(ConvertUtils.convertSearchResultDto(order));
return list;
}
}
RefundSearchImpl:
/**
* 退款查询实现
*
*/
@Service
public class RefundSearchImpl extends AbstractSearch {
@Autowired
private RefundService refundService;
@Override
public List<SearchResultDto> bizSearch(String key, Operator operator) {
if(!StringUtils.isNumeric(key)) {
return null;
}
List<SearchResultDto> list = new ArrayList<>();
Refund refund = this.refundService.queryByRefundId(Long.valueOf(key));
if(null == refund) {
return list;
}
list.add(ConvertUtils.convertSearchResultDto(refund));
return list;
}
}
测试验证
启动应用,我们通过debug的方式验证执行步骤和结果:
进入控制器层:
进入适配器层:
此步骤中可以看到ISearch有三个实现,并发查到两条结果。看到页面响应:
格式化后:
可以看到返回两条条数据,类型分别是退款和订单,根据关键字没有查到会员数据,也就验证实现了通用搜索能力。
总结
通过上述一系列描述,我们根据真实的业务场实现了通用搜索的能力,解决了用户对系统的操作复杂度,从而也就实现了我们开发系统体校降本的目的,并且该实现是一种插件式编程,如果业务上要支持一种新的业务查询,我们直接对ISearch增加一个实现类即可,完全不用其他任何配置,但是每一种方案和技术实现都是一把双刃剑,我们使用jdk自带线程池实现通用搜索能力的最大弊端是,如果并发比较大可能导致系统启用的线程特别多引起内存问题和仍旧会部分用户查询出现排队延迟现象,具体使用过程中可以预先评估业务量来设置线程池大小以及调整jvm内存参数。
希望通过此篇的描述加深各位看官对多线程以及jdk自带线程池的理解和认识,并且能够在实际开发过程中有比较好的应用和体验。