淘淘网络商城

前言

该项目是 2017 年参加学院组织的实训项目期间同小组成员开发的,是一个简化版的电商项目。

涉及到的技术

  1. 掌握高并发站点的系统架构方法。
  2. 熟悉 linux 开发环境,掌握 tomcat、mysql 服务器集群的安装与配置。
  3. 理解 web 服务器的负载均衡原理,掌握配置方法。
  4. 掌握数据库主从复制,读写分离技术。
  5. 理解缓存原理,掌握缓存在程序开发的原理。
  6. 掌握 web 站点的动静分离技术。

项目分析

基础知识总结

  1. Java 基础
    课程刚开始复习了许多 Java 基础知识,虽然开发主要依赖框架,但是磨刀不误砍柴工,只有熟练掌握基础知识,在使用框架中遇到问题时才能游刃有余。
    老师的讲解主要分两个阶段,首先讲解了属性文件、注解和反射等的使用方法,这些 java 功能是实现框架的基础,老师也一再强调通用性,而通用也正是框架存在的理由,因此老师第一步带领我们开发了一个简易的数据库访问框架,将常用的增删改查操作抽象出来,使得数据库访问操作的代码量大大减少,我们也得以对一些常见的数据库访问框架的工作原理有了一个较为深刻的理解,同时也为之后 MyBatis 框架的学习做了铺垫。
    然后讲解了数据读写方法、多线程操作、网络方面的知识,比较可贵的是老师并不是停留在基础用法上,而是做了很多有益的拓展,比如对数据读写,不仅讲了传统的 Java IO 方法,而且还讲了 Java1.5 引入的 NIO(New IO);在多线程方面,讲解了一些新的线程创建方法和解决死锁的方法,并且结合理论知识给出了解决生产者-消费者等经典问题的代码;网络编程方面,讲解了使用阻塞式和非阻塞式进行网络访问的方法,并且用这些知识实现了一个简易的聊天室程序。
  2. MyBatis 框架
    接下来老师为我们比较了 Hibernate 和 MyBatis 两种框架,并仔细介绍了 MyBatis 框架的使用方法,这个框架主要功能是访问数据库,访问数据库的关键是做好实体类和数据库表之间的映射,如果数据库到应用程序间的抽象做得越好、功能越通用,写出的代码鲁棒性也能越高。
    对于数据库映射,老师首先讲解了 Mybatis 的配置文件映射,其主要功能是将数据库操作代码和应用程序代码分开。我开始时非常不解,因为这样并没有简化反而使得程序更复杂了,但之后明白了,通过将主要业务逻辑和数据库操作代码分开,真正使得代码解耦、写活了。但我也发现这样的一个缺陷——因为这部分代码文件多了一倍导致程序也变得更加难以管理。所幸老师之后还介绍了注解映射,这种方式使得配置文件大大减少,实现了零配置。
  3. Spring 框架
    接下来老师介绍了 Spring 框架,Spring 框架的核心是控制反转,它是通过将对接口的实例化过程转移到配置文件中来解决对实现类的依赖,达到把代码写活的目的。Spring 并不只是一个实体类的工厂,由于集成了许多组件——比如 SpringAOP、SpringMVC 等,Spring 可以很方便地应用于许多场景下。为了详细讲解 Spring 的概念,老师还不厌其烦地为我们讲解 Spring 的部分源码,达到了深入浅出的程度。
  4. Maven 包管理工具
    另外,不管是在老师演示还是自己操作时,我发现出现的最多的错误是包依赖产生的错误,因为程序中集成了不少的框架,于是在大到一定程度后,就会出现依赖特别难以管理的情况,为此老师提出了使用 maven 来管理依赖,解决了这个难题,因此我也感悟到工程实践的特点——它是一个不断简化的过程。更多关于实训项目相关的经历我会在下面进一步阐述。

项目介绍

  1. 项目介绍
    ebuy 网上商城是一个综合性的 B2C 平台,类似京东商城、天猫商城。会员可以登录商城浏览商品、下订单,管理员可以登录后台对商品信息、商城内容进行管理。
  2. 个人职责
    本人在该项目开发当中的主要职责是搜索模块,包括网页端的 AJAX 访问逻辑、middle 项目中对搜索结果添加缓存逻辑,及 Solr 搜索服务器的搭建。
  3. 相关技术
    (1) 项目开发方法
    该项目的开发应用了增量模型,在各个阶段并不交付一个可运行的完整产品,而是交付满足客户需求的一个子集的可运行产品。整个项目由多个子模块构成,由小组内不同成员负责开发,逐个构建地交付产品,并且最终使用版本管理工具整合进最终的产品中,这样的好处是软件开发可以较好地适应变化,客户可以不断地看到所开发的软件。
  4. 开发环境和工具
    Ubuntu 16.04
    IDEA、Maven 3.5.0、Tomcat 7.0.53、JDK 1.8、MySQL 5.6、Nginx 1.8.0、Redis 3.0.0、SVN
  5. 主要技术
    JSP、JSTL、jQuery、jQuery plugin、EasyUI、KindEditor
    Spring、SpringMVC、Solr、HttpClient、Nginx
    MyBatis、MySQL、Redis

系统分析

  1. 项目总体介绍
    Ebuy 购物网站的主要目标是使用 MVC 框架实现一个 B2C 电商平台网站,实现的功能包括后台管理、前台、会员管理、订单管理、商品搜索、单点登录等。考虑到电商系统的普遍要求,如高并发、高可用等,使用到了动静分离、redis 集群等技术。
  2. Search 子系统分析
    ① 业务分析
    任意用户都可以在前台首页发起搜索请求,点击搜索按钮后会跳转到一个搜索结果显示页面,结果将以分页列表显示,在这个页面上用户也可以再次发起搜索。如果搜索字段为空,系统将以错误状态码响应。
    ② 功能分析
    搜索子系统主要包含更新索引和搜索两个功能,如图 2 所示,用户参与 Search 模块的商品搜索用例,而商品搜索用例需要包含 Solr 模块搜索用例的执行,Create Index 用例扩展了 Intem Search 用例,当有新的商品数据被添加到数据库时执行。
    X
    图 3 是 Search 用例的活动图,表示用户进行搜索时的大致执行过程,用户可以在首页编辑发送搜索请求,此时前台会发送请求获取搜索结果,渲染页面并将结果显示给用户。

系统设计

  1. 总体设计
    Portal 模块(门户),主要由静态的页面数据组成,网页中动态数据是由 AJAX 远程调用其他模块异步生成的;
    Manager 模块(后台管理),主要负责对整个网站的商品的管理和网站内容的管理;
    Middle 模块(数据中间件),主要目的是从数据库或者其他服务器获取数据,再使用 JSON 格式返回给 Portal;
    Search 模块(搜索),为商品建立索引,然后在每次调用搜索接口后将搜索结果包装为 JSON 字符串传回;
    Order 模块(订单),主要使用 Cookie 来保存订单,当要下单时将购物车中的记录保存到数据库;
    SSO 模块(单点登录模块),使用一个 Redis 集群保存登录 token,实现单点登录。
  2. 详细设计
    ① 功能模块设计
    搜索子系统主要功能模块为搜索和更新索引,其功能划分如图 2 所示,其中搜索功能需要首先在 Solr 服务器上给每个商品添加索引,然后在用户访问前台的搜索框时提供商品列表,更新索引功能可以在每次将商品添加到数据库中的同时在 Solr 服务器上更新该商品的索引,从而可以在后台添加商品后用户就能马上在前台找到对应的商品记录。
    ② 类和对象设计
    以 Search 功能主要涉及的类为例,如图 3 所示,这个系统是按照 MVC 框架设计的,其中前台模块的 ItemController 负责接收请求,具体的搜索任务交给 SearchService 执行,SearchService 调用了搜索模块的相应接口 ItemSearchController 的方法,搜索参数配置在 ItemSearchService 中进行,而具体的搜索功能由 ItemSearchDao 调用 SolrServer 的查询方法执行。
    X
    ③ 动态模型设计
    Search 子系统中搜索功能的时序图如图 4 所示,,首先用户在首页搜索框中输入搜索条件,点击搜索按钮,此时预设的 AJAX 程序会以搜索条件为参数调用前台的接口,前台模拟 Http 请求调用 Search 模块的搜索服务,进行格式化和分页处理,以及添加一些参数,调用 Solr 服务器的搜索服务获取结果,再由 Portal 渲染到页面中,完成一次搜索过程。
    X
    ④ 算法设计
    Search 子系统的主要功能是搜索,以下是调用 Solr 搜索服务的算法实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public SearchResult searchItem(SolrQuery solrQuery) throws SolrServerException {
    //根据查询条件搜索索引库
    QueryResponse response = solrServer.query(solrQuery);
    //取商品列表
    SolrDocumentList documentList = response.getResults();
    //商品列表
    List<Item> itemList = new ArrayList<>();
    for (SolrDocument solrDocument : documentList) {
    Item item = new Item();
    item.setId(Long.parseLong((String) solrDocument.get("id")));
    //取高亮显示
    Map<String, Map<String, List<String>>> highlighting = response.getHighlighting();
    List<String> list = highlighting.get(solrDocument.get("id")).get("item_title");
    String title = "";
    if (null != list && !list.isEmpty()) {
    title = list.get(0);
    } else {
    title = (String) solrDocument.get("item_title");
    }
    item.setTitle(title);
    item.setPrice((Long) solrDocument.get("item_price"));
    item.setSell_point((String) solrDocument.get("item_sell_point"));
    item.setImage((String) solrDocument.get("item_image"));
    item.setCategory_name((String) solrDocument.get("item_category_name"));

    itemList.add(item);plain
    }
    SearchResult result = new SearchResult();
    //商品列表
    result.setItemList(itemList);
    //总记录数据
    result.setRecordCount(documentList.getNumFound());
    System.out.println(result.getRecordCount());
    return result;
    }
    ⑤ 数据库设计
    Search 子系统主要使用到的表的 ER 图如图 7 所示。其中最重要的表是 tb_item 表,它的每条记录表示一个商品,search 模块需要使用该表的 id、item_title、item_sell_point 等属性建立索引。
    X

系统实现

  1. Search 模块的运行过程
    ① 首先由 portal 模块的 ItemController 接收到请求,将请求关键字转码并调用 SearchService 的方法

    1
    2
    queryString = new String(queryString.getBytes("ISO8859-1"), "UTF-8");
    SearchResult searchResult = searchService.searchItemList(queryString, page);

    ② 然后在 portal 模块的 SearchService 中构造参数列表并将请求发送给 search 模块处理

    1
    2
    3
    4
    5
    6
    //查询参数
    Map<String, String> param = new HashMap<>();
    param.put("kw", queryString);
    param.put("page", page==null?"1":page.toString());
    //调用 search 提供的搜索服务
    String resultString = MHttpClientUtil.mdoGet(SEARCH_BASE_URL, param);

    ③ search 模块的 ItemSearchController 同样先检验传递过来的参数是否有效,再调用 ItemSearchService 的服务

    1
    2
    3
    4
    if (StringUtils.isBlank(queryString)) {
    return MResult.build(400, "查询条件是必须的参数");
    }
    SearchResult result = itemSearchService.searchItem(queryString, page);

    ④ 在 search 模块的 ItemSearchService 中,需要为 Solr 查询构造查询对象,包括设置一些查询参数和分页设置等,最后调用 ItemSearchDao 的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    //创建一个查询对象
    SolrQuery solrQuery = new SolrQuery();
    //查询条件
    if(StringUtils.isBlank(queryString)) {
    solrQuery.setQuery("*:*");
    } else {
    solrQuery.setQuery(queryString);
    }
    //分页条件
    if (page == null) {
    page = 1;
    }
    solrQuery.setStart((page -1) * PAGE_SIZE);
    solrQuery.setRows(PAGE_SIZE);
    //高亮显示
    solrQuery.setHighlight(true);
    //设置高亮显示的域
    solrQuery.addHighlightField("item_title");
    //高亮显示前缀
    solrQuery.setHighlightSimplePre("<em style=\"color:red\">");
    //后缀
    solrQuery.setHighlightSimplePost("</em>");
    //设置默认搜索域
    solrQuery.set("df", "item_keywords");
    //执行查询
    SearchResult result = itemSearchDao.searchItem(solrQuery);

    ⑤ ItemSearchDao 封装了对 Solr 服务器的访问逻辑,主要过程是发送查询请求,获取 Solr 文档对象,从中解析出结果集合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    //根据查询条件搜索索引库
    QueryResponse response = solrServer.query(solrQuery);
    //取商品列表
    SolrDocumentList documentList = response.getResults();
    //商品列表
    List<Item> itemList = new ArrayList<>();
    for (SolrDocument solrDocument : documentList) {
    Item item = new Item();
    item.setId(Long.parseLong((String) solrDocument.get("id")));
    //取高亮显示
    Map<String, Map<String, List<String>>> highlighting = response.getHighlighting();
    List<String> list = highlighting.get(solrDocument.get("id")).get("item_title");
    String title = "";
    if (null != list && !list.isEmpty()) {
    title = list.get(0);
    } else {
    title = (String) solrDocument.get("item_title");
    }
    item.setTitle(title);
    item.setPrice((Long) solrDocument.get("item_price"));
    item.setSell_point((String) solrDocument.get("item_sell_point"));
    item.setImage((String) solrDocument.get("item_image"));
    item.setCategory_name((String) solrDocument.get("item_category_name"));

    itemList.add(item);plain
    }
    SearchResult result = new SearchResult();
    //商品列表
    result.setItemList(itemList);
    //总记录数据
    result.setRecordCount(documentList.getNumFound());
    System.out.println(result.getRecordCount());
    return result;

    ⑥ 返回,按上面的顺序,将结果返回到 portal 模块的控制器 ItemController 中去
    ⑦ 最后 portal 模块的 ItemController 将结果渲染到相应的页面中去

    1
    2
    3
    4
    5
    6
    model.addAttribute("itemList", searchResult.getItemList());
    model.addAttribute("query", queryString);
    model.addAttribute("totalPages", searchResult.getPageCount());
    model.addAttribute("page", searchResult.getCurPage());
    model.addAttribute("pages", searchResult.getPageCount());
    return "search";
  2. 初步成果
    首页搜索框
    X
    X

系统测试

  1. 测试方案设计
    对该搜索子系统的测试主要集中在功能测试和压力测试两方面,即确保正常情况下内容的正确显示及高并发下的可用性。
  2. 测试用例设计
    ① 使用英文和中文进行搜索,判断搜索结果是否正确
    ② 使用 100 个线程分别对该搜索功能进行访问,判断网站是否出现崩溃的现象。
  3. 测试结果及评价
    X
    Search 子模块能够有效显示结果,并且如图在 1000 个线程并发访问的情况下能正常工作。

体会

通过这次实训,我收获了很多,一方面学习到了许多以前没学过的专业知识与应用,另一方面还提高了自己动手做项目的能力。本次实训,是对我能力的进一步锻炼,也是一种考验。从中获得的诸多收获,也是很可贵的,是非常有意义的。
在实训中我学到了许多新的知识。是一个让我把书本上的理论知识运用于实践中的好机会,原来,学的时候感叹学的内容太难懂,现在想来,有些其实并不难,关键在于理解,而且如果能够在实践中把握知识,情况就会好很多。
在这次实训中还锻炼了我许多其他方面的能力,提高了我的综合素质。首先,它锻炼了我与小组成员交流的能力,通过沟通我们才能够很好地分工,努力地为了目标而各尽其力;同时也锻炼了自己吃苦耐劳的精神,因为某些任务具有一定的难度,甚至需要加班加点完成,如果轻言放弃就没有现在最终的成果。