- 1. 课程上下线业务
- 1.1 业务描述
- 1.2 技术方案
- 1.2.1 技术方案一:数据库状态做判断-->不可取
- 1.2.2 技术方案二:全文检索服务器-->可取
- 2. 课程上下线实现
- 2.1 技术架构
- 2.2 实现+分析
- 2.2.1 搭建搜索服务
- 2.2.1.1 步骤分析
- 2.2.1.2 步骤实现
- 2.2.2 课程上下线处理
- 2.2.2.1 步骤分析
- 2.2.2.2 步骤实现
- 2.2.1 搭建搜索服务
- 3. 易错点总结
1. 课程上下线业务
1.1 业务描述
-
上线
在系统中,我们添加了一个课程,用户不能立即就搜索到,需要上线以后才行。
-
下线
当某个课程不想卖的时候,就要下线.当课程下线后,用户不能搜索到,但是数据库是还有的。
1.2 技术方案
1.2.1 技术方案一:数据库状态做判断-->不可取
上线后,修改状态为”上线”,用户搜索时只能搜索到上线状态的.如果不想卖了,执行下线时,修改状态下线.
--->垃圾(每次都要操作数据库)
1.2.2 技术方案二:全文检索服务器-->可取
上线时把课程数据同步到es,用户查询直接从es查询.也就意味着没有上线的课程用户查询不到,因为没有放到es库.
下线时把es库课程数据删除掉.用户就查询不到了.
--->牛B(以基于索引搜索代替数据查询)
优点:
- (1)降低数据库压力
- (2)提高了查询速度,增强用户体验-基于索引搜索,效率远远数据库搜索
2. 课程上下线实现
2.1 技术架构
//TODO 结构图以后自己画上补充
简单描述:
用户查询直接从ES库中查
- 管理员:
- (1)添加课程。管理员将课程添加到db
- (2)课程上线。把需要上线的课程查出来,同步到ES库。
- (3)删除或修改课程。同步操作ES库和DB库。
- (4)查询课程。这是后台的查询,就直接从数据库查询。
- 用户:用户查询直接从ES库中查
- 减少数据库压力
- 提高查询效率,用户体验更佳
2.2 实现+分析
课程服务调用搜索服务-服务内部调用feign
步骤分析:
- 搭建搜索服务
- 课程的上下线处理
2.2.1 搭建搜索服务
2.2.1.1 步骤分析
- 创建项目
- 导包
- 配置
- 入口类
- doc准备
- repository准备-service
- query准备-interface
- IESCourseService接口-service
- ESCourseServiceImpl(实现上面那个接口)-service
- ESCourseController-service
- client-interface
- 生成文档映射-test-service
- 测试
- 日志集成
- 网关集成
- 本项目swagger集成
- 网关swagger集成
- 启动测试网关、日志、swagger
2.2.1.2 步骤实现
-
创建项目
在二级子模块hrm_basic_parent下创建三级子模块hrm_basic_es_interface和三级子模块hrm_basic_es_service
导包
- hrm_basic_es_interface
cn.wangningbo.hrm
hrm_basic_util
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.data
spring-data-elasticsearch
3.0.10.RELEASE
- hrm_basic_es_service
cn.wangningbo.hrm
hrm_basic_es_interface
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
io.springfox
springfox-swagger2
2.9.2
io.springfox
springfox-swagger-ui
2.9.2
org.springframework.cloud
spring-cloud-starter-config
org.springframework.boot
spring-boot-starter-data-elasticsearch
- 配置(application.yml)-在service端
server:
port: 9004
spring:
application:
name: hrm-es
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 127.0.0.1:9300 #9200是图形界面端,9300代码端
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
prefer-ip-address: true
- 入口类-在service端
package cn.wangningbo.hrm;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class ElasticSearch9004Application {
public static void main(String[] args) {
SpringApplication.run(ElasticSearch9004Application.class, args);
}
}
-
doc准备-interface
根据业务和表设计这里的doc
package cn.wangningbo.hrm.doc;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.math.BigDecimal;
import java.util.Date;
@Document(indexName = "hrm", type = "course")
public class ESCourse {
@Id
private Long id;
private String name;
private String users;
private Long courseTypeId;
private String courseTypeName;
private Long gradeId;
private String gradeName;
private Integer status;
private Long tenantId;
private String tenantName;
private Long userId;
private String userName;
private Date startTime;
private Date endTime;
private String intro;
private String resources; //图片
private Date expires; //过期时间
private BigDecimal priceOld; //原价
private BigDecimal price; //原价
private String qq; //原价
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
private String all;
public String getAll() {
String tmp = name
+ " " + users
+ " " + courseTypeName
+ " " + gradeName
+ " " + tenantName
+ " " + userName
+ " " + intro;
return tmp;
}
public void setAll(String all) {
this.all = all;
}
//提供get、set和toString方法
}
-
repository准备-service
由于需要操作es,所需需要repository操作
package cn.wangningbo.hrm.repository;
import cn.wangningbo.hrm.doc.ESCourse;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface CourseRepository extends ElasticsearchRepository {
}
- query准备-interface
public class ESCourseQuery extends BaseQuery {
}
- IESCourseService接口-service
package cn.wangningbo.hrm.service;
import cn.wangningbo.hrm.doc.ESCourse;
import cn.wangningbo.hrm.query.ESCourseQuery;
import cn.wangningbo.hrm.util.PageList;
import java.util.List;
/**
* @author wangningbo
* @since 2019-09-06
*/
public interface IESCourseService {
//添加
void insert(ESCourse esCourse);
//修改
void updateById(ESCourse esCourse);
//删除
void deleteById(Long id);
//查询一个
ESCourse selectById(Long id);
//查询所有
List selectList(Object o);
//dsl高级查询
PageList selectListPage(ESCourseQuery query);
}
- ESCourseServiceImpl(实现上面那个接口)-service
@Service
public class ESCourseServiceImpl implements IESCourseService {
@Autowired
private CourseRepository courseRepository;
@Override
public void insert(ESCourse esCourse) {
courseRepository.save(esCourse);
}
@Override
public void updateById(ESCourse esCourse) {
courseRepository.save(esCourse);
}
@Override
public void deleteById(Long id) {
courseRepository.deleteById(id);
}
@Override
public ESCourse selectById(Long id) {
return courseRepository.findById(id).get();
}
@Override
public List selectList(Object o) {
Page page = (Page) courseRepository.findAll();
return page.getContent();
}
@Override
public PageList selectListPage(ESCourseQuery query) {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
BoolQueryBuilder bool = QueryBuilders.boolQuery();
//模糊查询 @TODO
bool.must(QueryBuilders.matchQuery("intro", "zhang"));
//精确过滤 @TODO
List filters = bool.filter();
filters.add(QueryBuilders.rangeQuery("age").gte(0).lte(200));
builder.withQuery(bool); //query bool must(filter)
//排序 @TODO
builder.withSort(SortBuilders.fieldSort("age").order(SortOrder.ASC));
//分页 当前页从0开始
builder.withPageable(PageRequest.of(query.getPage() - 1, query.getRows()));
//构造查询条件
NativeSearchQuery esQuery = builder.build();
//查询
Page page = courseRepository.search(esQuery);
return new PageList<>(page.getTotalElements(), page.getContent());
}
}
- ESCourseController-service
package cn.wangningbo.hrm.web.controller;
import cn.wangningbo.hrm.doc.ESCourse;
import cn.wangningbo.hrm.query.ESCourseQuery;
import cn.wangningbo.hrm.service.IESCourseService;
import cn.wangningbo.hrm.util.AjaxResult;
import cn.wangningbo.hrm.util.PageList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/esCourse")
public class ESCourseController {
@Autowired
public IESCourseService esCourseService;
/**
* 保存和修改公用的
*
* @param esCourse 传递的实体
* @return Ajaxresult转换结果
*/
@RequestMapping(value = "/save", method = RequestMethod.POST)
public AjaxResult save(@RequestBody ESCourse esCourse) {
try {
if (esCourse.getId() != null) {
esCourseService.updateById(esCourse);
} else {
esCourseService.insert(esCourse);
}
return AjaxResult.me();
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.me().setMessage("保存对象失败!" + e.getMessage());
}
}
/**
* 删除
*
* @param id
* @return
*/
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
public AjaxResult delete(@PathVariable("id") Long id) {
try {
esCourseService.deleteById(id);
return AjaxResult.me();
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.me().setMessage("删除对象失败!" + e.getMessage());
}
}
//获取用户
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ESCourse get(@PathVariable("id") Long id) {
return esCourseService.selectById(id);
}
/**
* 查看所有信息
*
* @return
*/
@RequestMapping(value = "/list", method = RequestMethod.GET)
public List list() {
return esCourseService.selectList(null);
}
/**
* 分页查询数据
*
* @param query 查询对象
* @return PageList 分页对象
*/
@RequestMapping(value = "/json", method = RequestMethod.POST)
public PageList json(@RequestBody ESCourseQuery query) {
return esCourseService.selectListPage(query);
}
}
-
client-interface
注意点:@FeignClient的value = "HRM-ES",自己服务端的名字,@RequestMapping("/esCourse")要与自己服务端controller的一样
package cn.wangningbo.hrm.client;
import cn.wangningbo.hrm.doc.ESCourse;
import cn.wangningbo.hrm.query.ESCourseQuery;
import cn.wangningbo.hrm.util.AjaxResult;
import cn.wangningbo.hrm.util.PageList;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@FeignClient(value = "HRM-ES", configuration = FeignClientsConfiguration.class,
fallbackFactory = EsCourseClientHystrixFallbackFactory.class)
@RequestMapping("/esCourse")
public interface ESCourseClient {
/**
* 保存和修改公用的
*
* @param esCourse 传递的实体
* @return Ajaxresult转换结果
*/
@RequestMapping(value = "/save", method = RequestMethod.POST)
AjaxResult save(ESCourse esCourse);
/**
* 删除
*
* @param id
* @return
*/
@RequestMapping(value = "/delete/{id}", method = RequestMethod.DELETE)
AjaxResult delete(@PathVariable("id") Integer id);
//获取用户
@RequestMapping("/{id}")
ESCourse get(@RequestParam(value = "id", required = true) Long id);
/**
* 查看所有信息
*
* @return
*/
@RequestMapping("/list")
public List list();
/**
* 分页查询数据
*
* @param query 查询对象
* @return PageList 分页对象
*/
@RequestMapping(value = "/json", method = RequestMethod.POST)
PageList json(@RequestBody ESCourseQuery query);
}
package cn.wangningbo.hrm.client;
import cn.wangningbo.hrm.doc.ESCourse;
import cn.wangningbo.hrm.query.ESCourseQuery;
import cn.wangningbo.hrm.util.AjaxResult;
import cn.wangningbo.hrm.util.PageList;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author wangningbo
* @date 2019/09/06
*/
@Component
public class EsCourseClientHystrixFallbackFactory implements FallbackFactory {
@Override
public ESCourseClient create(Throwable throwable) {
return new ESCourseClient() {
@Override
public AjaxResult save(ESCourse esCourse) {
return null;
}
@Override
public AjaxResult delete(Integer id) {
return null;
}
@Override
public ESCourse get(Long id) {
return null;
}
@Override
public List list() {
return null;
}
@Override
public PageList json(ESCourseQuery query) {
return null;
}
};
}
}
- 生成文档映射-test-service
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ElasticSearch9004Application.class)
public class IESCourseServiceTest {
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@Test
public void testInit() throws Exception {
elasticsearchTemplate.createIndex(ESCourse.class);
elasticsearchTemplate.putMapping(ESCourse.class);
}
}
-
测试
http://localhost:9004/esCourse/list
-
日志集成
resources下存放一个名字为logback-spring.xml的配置文件
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
${LOG_HOME}/${appName}/${appName}.log
${LOG_HOME}/${appName}/${appName}-%d{yyyy-MM-dd}-%i.log
365
100MB
%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n
-
网关集成
在网关的配置文件中zuul.routes中再新加两行,配置es
es.serviceId: hrm-es # 服务名
es.path: /es/** # 把es打头的所有请求都转发给hrm-es
- 本项目swagger集成
package cn.wangningbo.hrm.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class Swagger2 {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//对外暴露服务的包,以controller的方式暴露,所以就是controller的包.
.apis(RequestHandlerSelectors.basePackage("cn.wangningbo.hrm.web.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("分布式全文检索api")
.description("分布式全文检索接口文档说明")
.contact(new Contact("wangningbo", "", "[email protected]"))
.version("1.0")
.build();
}
}
-
网关swagger集成
在网关的DocumentationConfig配置类里面新加个
resources.add(swaggerResource("分布式全文检索", "/services/es/v2/api-docs", "2.0"));
- 启动测试网关、日志、swagger
2.2.2 课程上下线处理
架构分析图(后续补上)
2.2.2.1 步骤分析
- 改造商品的删除和修改(索引库也要进行对应的操作)
- 课程的上下线逻辑
- client
2.2.2.2 步骤实现
-
改造商品的删除和修改(索引库也要进行对应的操作)
由于要使用es,这里操作删除和修改的时候也要操作es库,所以要覆写删除和修改方法!
@Override
public boolean deleteById(Serializable id) {
//删除数据库的同时也要判断状态是否要删除es库
courseMapper.deleteById(id);
Course course = courseMapper.selectById(id);
if (course.getStatus() == 1)
esCourseClient.delete(Integer.valueOf(id.toString()));
return true;
}
// @TODO 不同服务,反3Fn设计冗余字段
// @TODO 相同服务,关联查询
//根据自己的需求和库中的表设计
private ESCourse course2EsCourse(Course course) {
ESCourse result = new ESCourse();
result.setId(course.getId());
result.setName(course.getName());
result.setUsers(course.getUsers());
result.setCourseTypeId(course.getCourseTypeId());
//type-同库
if (course.getCourseType() != null)
result.setCourseTypeName(course.getCourseType().getName());
//跨服务操作
result.setGradeId(course.getGrade());
result.setGradeName(null);
result.setStatus(course.getStatus());
result.setTenantId(course.getTenantId());
result.setTenantName(course.getTenantName());
result.setUserId(course.getUserId());
result.setUserName(course.getUserName());
result.setStartTime(course.getStartTime());
result.setEndTime(course.getEndTime());
//Detail
result.setIntro(null);
//resource
result.setResources(null);
//market
result.setExpires(null);
result.setPrice(null);
result.setPriceOld(null);
result.setQq(null);
return result;
}
@Override
public boolean updateById(Course entity) {
//修改数据库的时候也要根据状态判断是否操作修改es库
courseMapper.updateById(entity);
Course course = courseMapper.selectById(entity.getId());
if (course.getStatus() == 1)
esCourseClient.save(course2EsCourse(entity));
return true;
}
-
课程的上下线逻辑
简单逻辑:前端会发起上线或下线请求!到我这里的controller接口。我这里进行一层一层的实现逻辑!既要操作db库,也要操作es库!
controller层新增两个方法,上线和下线。我这里先实现上线,再写下线(下线比较简单)!
==(上线---------------------->)==
controller
//由于前端可能是批量操作,所以我这里使用数组接收参数
@PostMapping("/onLine")
public AjaxResult onLine(@RequestBody Long[] ids) {
try {
courseService.onLine(ids);
return AjaxResult.me();
} catch (Exception e) {
e.printStackTrace();
logger.error("online failed!"+e);
return AjaxResult.me().setSuccess(false)
.setMessage("上线失败!"+e.getMessage());
}
}
IService
void onLine(Long[] ids);
ServiceImpl
/**
* 商品课程上线
* @param ids
*/
@Override
public void onLine(Long[] ids) {
//批量操作数据库状态字段 //类似于这种update t_course set status = 1,start_time=xxx where id in (1,2,3)
ArrayList
上面这个serviceImpl主要是做2步操作,第一步是批量修改db库的状态字段为上线,第二步是操作es库,批量把上线的商品课程添加es库中。
①先说操作db库的字段修改为上线的逻辑步骤
Mapper.java
void batchOnline(ArrayList
Mapper.xml
UPDATE t_course set status =1,start_time=now() where id IN
#{item.id}
②再说操作es库,把上线的商品课程添加到es库
es-->client
//批量上线
@PostMapping("/online")
AjaxResult batchSave(List esCourseList);
es-->ClientHystrixFallbackFactory
@Override
public AjaxResult batchSave(List esCourseList) {
return null;
}
es-->controller
/**
* 批量保存到es库,批量上线
* @param esCourseList
* @return
*/
@PostMapping("/online")
AjaxResult batchSave(@RequestBody List esCourseList){
try {
esCourseService.batchSave(esCourseList);
return AjaxResult.me();
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage("批量添加失败!"+e.getMessage());
}
}
es-->IService
//批量保存
void batchSave(List ids);
es-->ServiceImpl
//批量保存
@Override
public void batchSave(List esCourseList) {
courseRepository.saveAll(esCourseList);
}
==注意:这时候调用es的那个模块的入口类就要打上注解@EnableFeignClients== 入口类获得feign支持
测试是否成功
==(下线---------------------->)==
下线的简单逻辑分析:管理员下线商品课程只需要做2步,第一步:修改db库中的商品课程字段为下线状态,第二步:删除es库中的商品课程
controller
/**
* 商品课程下线
*
* @param ids
* @return
*/
@PostMapping("/offLine")
public AjaxResult offLine(@RequestBody Long[] ids) {
try {
courseService.offLine(ids);
return AjaxResult.me();
} catch (Exception e) {
e.printStackTrace();
logger.error("offLine failed!"+e);
return AjaxResult.me().setSuccess(false)
.setMessage("下线失败!"+e.getMessage());
}
}
IService
void offLine(Long[] ids);
ServiceImpl
@Override
public void offLine(Long[] ids) {
//批量修改db库中商品课程的状态为下线状态
courseMapper.batchOffline(Arrays.asList(ids));
//批量删除es库中的商品课程 //时间方面是使用mysql的语法生成的
List courseList = courseMapper.selectBatchIds(Arrays.asList(ids));
List esCourseList = courseList2EsCourse(courseList);
esCourseClient.batchDel(esCourseList);
}
上面这个serviceImpl主要是做2步操作,第一步是批量修改db库的状态字段为下线状态,第二步是操作es库,批量把下线的商品课程从es库中删除掉。
①批量修改db库的状态字段为下线状态
Mapper.java
void batchOffline(List longs);
Mapper.xml
UPDATE t_course set status =0,end_time=now() where id IN
#{item}
es-->client
//批量下线
@PostMapping("/offline")
void batchDel(List esCourseList);
es-->ClientHystrixFallbackFactory
@Override
public void batchDel(List esCourseList) {
}
es-->controller
@PostMapping("/offline")
AjaxResult batchDel(@RequestBody List esCourseList){
try {
esCourseService.batchDel(esCourseList);
return AjaxResult.me();
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage("批量删除失败!"+e.getMessage());
}
}
es-->Iservice
//批量删除
void batchDel(List esCourseList);
es-->ServiceImpl
//批量删除
@Override
public void batchDel(List esCourseList) {
courseRepository.deleteAll(esCourseList);
}
测试是否成功
3. 易错点总结
- ESClient那里的注解@FeignClient的参数。value = "HRM-ES"的值指向自己注册到eureka的服务。以下面为例
@FeignClient(value = "HRM-ES",configuration = FeignClientsConfiguration.class,
fallbackFactory = EsCourseClientHystrixFallbackFactory.class)
- ESClient那里的注@RequestMapping的参数值要与controller的一致。以下面的为例
//client
@FeignClient(value = "HRM-ES",configuration = FeignClientsConfiguration.class,
fallbackFactory = EsCourseClientHystrixFallbackFactory.class)
@RequestMapping("/esCourse")
public interface EsCourseClient {}
//controller
@RestController
@RequestMapping("/esCourse")
public class EsCourseController {}
- 其他模块调用es的时候,作为es的客户端,在入口类上要加入feign的支持。也就是注解@EnableFeignClients。以下面为例
package cn.wangningbo.hrm;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@MapperScan("cn.wangningbo.hrm.mapper")
@EnableFeignClients
public class Course9002Application {
public static void main(String[] args) {
SpringApplication.run(Course9002Application.class, args);
}
}