当前位置:   article > 正文

第四章--学成在线--课程发布

学成在线

学习视频地址 : https://www.bilibili.com/video/BV1j8411N7Bm/?vd_source=08ac522c6603c56e243d5e129a309a60
学成在线学习网址链接
以上是引用地址,仅用作个人练习记录

a 需求分析

课程信息编辑完毕即可发布课程,发布课程相当于一个确认操作,课程发布后学习者在网站可以搜索到课程,然后查看课程的详细信息,进一步选课、支付、在线学习。
下边是课程编辑与发布的整体流程:
image.png
为了课程内容没有违规信息、课程内容安排合理,在课程发布之前运营方会进行课程审核,审核通过后课程方可发布。
作为课程制作方即教学机构,在课程发布前通过课程预览功能可以看到课程发布后的效果,哪里的课程信息存在问题方便查看,及时修改。
教学机构确认课程内容无误,提交审核,平台运营人员对课程内容审核,审核通过后教学机构人员发布课程成功。
课程发布模块共包括三块功能:
1、课程预览
2、课程审核
3、课程发布

业务流程

课程预览

1.教育机构用户在课程管理中可对该机构内所管理的课程进行检索。
2.点击某课程数据后的预览链接,即可对该课程进行预览,可以看到发布后的详情页面效果。
点击课程目录,显示课程计划,通过此界面去核实课程计划的信息是否存在问题。
点击课程目录中的具体章节,查看视频播放是否正常
image.png

课程审核

教学机构提交课程审核后,平台运营人员登录运营平台进行课程审核,课程审核包括程序自动审核和人工审核,程序会审核内容的完整性,人员通过课程预览进行审核。
1、首先查询待审核的记录。
2、课程审核
具体审核的过程与课程预览的过程类似,运营人员查看课程信息、课程视频等内容。
如果存在问题则审核不通过,并附上审核不通过的原因供教学机构人员查看。
如果课程内容没有违规信息且课程内容全面则审核通过。
课程审核通过后教学机构发布课程成功。

课程发布

1.教育机构用户在课程管理中可对机构内课程进行检索。
2.点击某课程数据后的 发布 链接(审核状态为通过),即可对该课程进行发布。
3、课程发布后可通过课程搜索查询到课程信息,并查看课程的详细信息。
4 点击课程搜索页中课程列表的某个课程,可进入课程详情页。

b 课程预览

课程预览就是把课程的相关信息进行整合,在课程详情界面进行展示,通过课程预览页面查看信息是否存在问题。
下图是课程预览的数据来源:
image.png
在课程预览页面点击"视频播放图片"打开视频播放页面,通过视频播放页面查看课程计划对应的视频是否存在问题。
课程预览的效果与最终课程发布后查看到的效果是一致的,所以课程预览时会通过网站门户域名地址进行预览,下图显示了整个课程预览的流程图:
image.png
说明如下:
1、点击课程预览,通过Nginx、后台服务网关请求内容管理服务进行课程预览。
2、内容管理服务查询课程相关信息进行整合,并通过模板引擎技术在服务端渲染生成页面,返回给浏览器。
3、通过课程预览页面点击”马上学习“打开视频播放页面。
4、视频播放页面通过Nginx请求后台服务网关,查询课程信息展示课程计划目录,请求媒资服务查询课程计划绑定的视频文件地址,在线浏览播放视频。

模板引擎⭐

项目采用模板引擎技术实现课程预览界面。什么是模板引擎?
早期我们采用的jsp技术就是一种模板引擎技术,如下图:
image.png
1、浏览器请求web服务器
2、服务器渲染页面,渲染的过程就是向jsp页面(模板)内填充数据(模型)。
3、服务器将渲染生成的页面返回给浏览器。
所以模板引擎就是:模板+数据=输出,Jsp页面就是模板,页面中嵌入的jsp标签就是数据,两者相结合输出html网页。
常用的java模板引擎还有哪些?
Jsp、Freemarker、Thymeleaf 、Velocity 等。
本项目采用Freemarker作为模板引擎技术。
Freemarker官方地址:http://freemarker.foofun.cn/
FreeMarker 是一款 模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。FreeMarker 是 免费的,基于Apache许可证2.0版本发布。

Freemarker快速入门

内容管理接口工层添加Freemarker与SpringBoot的整合包

| XML

org.springframework.boot spring-boot-starter-freemarker | | --- |

在nacos为内容管理接口层配置freemarker,公用配置组新加一个freemarker-config-dev.yaml
image.png

| YAML
spring:
freemarker:
enabled: true
cache: false #关闭模板缓存,方便测试
settings:
template_update_delay: 0
suffix: .ftl #页面模板后缀名
charset: UTF-8
template-loader-path: classpath:/templates/ #页面模板位置(默认为 classpath:/templates/)
resources:
add-mappings: false #关闭项目中的静态资源映射(static、resources文件夹下的资源)
|
| — |

在内容管理接口工程添加freemarker-config-dev.yaml
image.png
添加模板,在resources下创建templates目录,添加test.ftl模板文件

| HTML

Hello World! Hello ${name}! | | --- |

编写controller方法,准备模型数据

| Java
package com.xuecheng.content.api;

import org.bouncycastle.math.raw.Mod;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.Map;

/**

  • @description freemarker测试
    */
    @Controller
    public class FreemarkerController {

    @GetMapping(“/testfreemarker”)
    public ModelAndView test(){
    ModelAndView modelAndView = new ModelAndView();
    //设置模型数据
    modelAndView.addObject(“name”,“小明”);
    //设置模板名称
    modelAndView.setViewName(“test”);
    return modelAndView;
    }
    } |
    | — |

启动内容管理接口工程,访问http://localhost:63040/content/testfreemarker
屏幕输出:Hello 小明!
freemarker提供很多指令用于解析各种类型的数据模型,参考地址:http://freemarker.foofun.cn/ref_directives.html

测试静态页面

部署网站门户

在课程预览界面上要加载css、js、图片等内容,这里部署nginx来访问这些静态资源,对于SpringBoot服务的动态资源由Nginx去代理请求,如下图:
image.png
1、在本机安装 Nginx ,nginx-1.23.1.zip并解压。
2、运行nginx-1.23.1目录下的nginx.exe。
默认端口为80,如果本机80端口被占用,则需要杀掉占用进程后再启动nginx。
如果无法杀掉80端口占用进程则需要修改nginx-1.23.1目录下conf/nginx.conf配置文件
image.png
将80端口修改为空闲端口。
启动nginx,访问http://localhost 出现下边的网页表示启动成功
image.png
下边开始部署前端工程:
1、获取xc-ui-pc-static-portal.zip 并解压。
2、修改本机hosts文件,加入127.0.0.1 www.51xuecheng.cn 51xuecheng.cn ucenter.51xuecheng.cn teacher.51xuecheng.cn file.51xuecheng.cn。
window10操作系统hosts文件在C:\Windows\System32\drivers\etc下
Centos7操作系统的hosts文件在/etc目录下。
在hosts文件加入如下配置

| Plain Text

127.0.0.1 www.51xuecheng.cn 51xuecheng.cn ucenter.51xuecheng.cn teacher.51xuecheng.cn file.51xuecheng.cn

3、在nginx-1.23.1目录中找到conf目录,配置目录下的nginx.conf文件。
配置内容如下,注意更改xc-ui-pc-static-portal目录的路径:

| Plain Text
server {
listen 80;
server_name www.51xuecheng.cn localhost;
#rewrite ^(.*) https://$server_name$1 permanent;
#charset koi8-r;
ssi on;
ssi_silent_errors on;
#access_log logs/host.access.log main;

    location / {
        alias   D:/itcast2022/xc_edu3.0/code_1/xc-ui-pc-static-portal/;
        index  index.html index.htm;
    }
    #静态资源
    location /static/img/ {  
            alias  D:/itcast2022/xc_edu3.0/code_1/xc-ui-pc-static-portal/img/;
    } 
    location /static/css/ {  
            alias   D:/itcast2022/xc_edu3.0/code_1/xc-ui-pc-static-portal/css/;
    } 
    location /static/js/ {  
            alias   D:/itcast2022/xc_edu3.0/code_1/xc-ui-pc-static-portal/js/;
    } 
    location /static/plugins/ {  
            alias   D:/itcast2022/xc_edu3.0/code_1/xc-ui-pc-static-portal/plugins/;
            add_header Access-Control-Allow-Origin http://ucenter.51xuecheng.cn;  
            add_header Access-Control-Allow-Credentials true;  
            add_header Access-Control-Allow-Methods GET;
    } 
    location /plugins/ {  
            alias   D:/itcast2022/xc_edu3.0/code_1/xc-ui-pc-static-portal/plugins/;
    } 

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \\.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \\.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\\.ht {
    #    deny  all;
    #}
} |
  • 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
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

| — |

启动nginx:
进入任务管理器,杀死nginx的两个进程
image.png
杀死后再次双击nginx.exe。
启动成功在任务管理器会出现nginx的进程。
日志文件在nginx安装目录下的logs目录:
image.png
启动成功访问http://www.51xuecheng.cn

课程详情页面

course_template.html是一个静态html页面,里边还没有添加freemarker标签,如果要预览该页面需要借助Nginx进行预览,因为页面需要加载一些css样式表、图片等内容。
course_template.html文件在xc-ui-pc-static-portal\course目录下
通过浏览器访问:http://www.51xuecheng.cn/course/course_template.html

文件服务器

在进行课程预览时需要展示课程的图片,在线插放课程视频,课程图片、视频这些都在MinIO文件系统存储,下边统一由Nginx代理,通过文件服务域名统一访问。如下图:
image.png
hosts文件配置127.0.0.1 www.51xuecheng.cn file.51xuecheng.cn如果已存在不要重复配置。
在nginx.conf中配置文件服务器的代理地址

| Java
#文件服务
upstream fileserver{
server 192.168.101.65:9000 weight=10;
}
server {
listen 80;
server_name file.51xuecheng.cn;
#charset koi8-r;
ssi on;
ssi_silent_errors on;
#access_log logs/host.access.log main;
location /video {
proxy_pass http://fileserver;
}

    location /mediafiles {
        proxy_pass   http://fileserver;
    }
  • 1
  • 2
  • 3
}

配置完毕,重新加载nginx配置文件。
通过cmd进入nginx.exe所在目录,运行如下命令

nginx.exe -s reload

通过http://file.51xuecheng.cn/mediafiles/图片文件地址 访问图片
在媒资数据库的文件表中找一个图片的地址进行测试。

视频播放页面

首先在nginx.conf中配置视频播放页面的地址
加载nginx配置文件
点击课程详情页面上的视频播放链接,打开视频播放页面
下边需要配置learning.html页面的视频播放路径来测试视频播放页面,找到learning.html页面中videoObject对象的定义处,配置视频的播放地址。
/注意:此页面会去请求后台接口获取课程计划,稍后处理;

c 课程审核

为什么课程审核通过才可以发布呢?
这样做为了防止课程信息有违规情况,课程信息不完善对网站用户体验也不好,课程审核不仅起到监督作用,也是帮助教学机构规范使用平台的手段。
如何控制课程审核通过才可以发布课程呢?
在课程基本表course_base表设置课程审核状态字段,包括:未提交、已提交(未审核)、审核通过、审核不通过。
image.png
说明如下:
1、一门课程新增后它的审核状为”未提交“,发布状态为”未发布“。
2、课程信息编辑完成,教学机构人员执行”提交审核“操作。此时课程的审核状态为”已提交“。
3、当课程状态为已提交时运营平台人员对课程进行审核。
4、运营平台人员审核课程,结果有两个:审核通过、审核不通过。
5、课程审核过后不管状态是通过还是不通过,教学机构可以再次修改课程并提交审核,此时课程状态为”已提交“。此时运营平台人员再次审核课程。
6、课程审核通过,教学机构人员可以发布课程,发布成功后课程的发布状态为”已发布“。
7、课程发布后通过”下架“操作可以更改课程发布状态为”下架“
8、课程下架后通过”上架“操作可以再次发布课程,上架后课程发布状态为“发布”。
1、课程提交审核后还允许修改课程吗?
如果不允许修改是不合理的,因为提交审核后可以继续做下一个阶段的课程内容,比如添加课程计划,上传课程视频等。
如果允许修改那么课程审核时看到的课程内容从哪里来?如果也从课程基本信息表、课程营销表、课程计划表查询那么存在什么问题呢?如下图:
image.png
运营人员审核课程和教学机构编辑课程操作的数据是同一份,此时会导致冲突。比如:运营人员正在审核时教学机构把数据修改了。
为了解决这个问题,专门设计课程预发布表
image.png
image.png
提交课程审核,将课程信息汇总后写入课程预发布表,课程预发布表记录了教学机构在某个时间点要发布的课程信息。
课程审核人员从预发布表查询信息进行审核。
课程审核的同时可以对课程进行修改,修改的内容不会写入课程预发布表。
课程审核通过执行课程发布,将课程预发布表的信息写入课程发布表。
2、提交审核课程后,也修改了课程信息,可以再次提交审核吗?
这个问题在上边分析课程审核状态时已经有了答案,如下图:
提交审核课程后,必须等到课程审核完成才可以再次提交课程。

仅仅实现教学机构提交审核功能,课程审核的结果通过手动修改数据库来实现。
提交审核将信息写入课程预发布表,课程预发布表结构如下
image.png
更新课程基本信息表的课程审核状态为:已经提交
课程审核后更新课程基本信息表的审核状态、课程预发布表的审核状态,并将审核结果写入课程审核记录。
审核记录表结构如下:
image.png
定义接口,写接口
Dao开发
1、查询课程基本信息、课程营销信息、课程计划信息等课程相关信息,整合为课程预发布信息。
2、向课程预发布表course_publish_pre插入一条记录,如果已经存在则更新,审核状态为:已提交。
3、更新课程基本表course_base课程审核状态为:已提交。
约束:
1、对已提交审核的课程不允许提交审核。
2、本机构只允许提交本机构的课程。
3、没有上传图片不允许提交审核。
4、没有添加课程计划不允许提交审核。
Service开发

// 实现类业务逻辑 提交审核
@Override
public void commitAudit(Long companyId, Long courseId) {

 //约束校验
 CourseBase courseBase = courseBaseMapper.selectById(courseId);
 //课程审核状态
 String auditStatus = courseBase.getAuditStatus();
 //当前审核状态为已提交不允许再次提交
 if("202003".equals(auditStatus)){
  XueChengPlusException.cast("当前为等待审核状态,审核完成可以再次提交。");
 }
 //本机构只允许提交本机构的课程
 if(!courseBase.getCompanyId().equals(companyId)){
  XueChengPlusException.cast("不允许提交其它机构的课程。");
 }

 //课程图片是否填写
 if(StringUtils.isEmpty(courseBase.getPic())){
  XueChengPlusException.cast("提交失败,请上传课程图片");
 }

 //添加课程预发布记录
 CoursePublishPre coursePublishPre = new CoursePublishPre();
 //课程基本信息加部分营销信息
 CourseBaseInfoDto courseBaseInfo = courseBaseInfoService.getCourseBaseInfo(courseId);
 BeanUtils.copyProperties(courseBaseInfo,coursePublishPre);
 //课程营销信息
 CourseMarket courseMarket = courseMarketMapper.selectById(courseId);
 //转为json
 String courseMarketJson = JSON.toJSONString(courseMarket);
 //将课程营销信息json数据放入课程预发布表
 coursePublishPre.setMarket(courseMarketJson);

 //查询课程计划信息
 List<TeachplanDto> teachplanTree = teachplanService.findTeachplanTree(courseId);
 if(teachplanTree.size()<=0){
  XueChengPlusException.cast("提交失败,还没有添加课程计划");
 }
 //转json
 String teachplanTreeString = JSON.toJSONString(teachplanTree);
 coursePublishPre.setTeachplan(teachplanTreeString);

 //设置预发布记录状态,已提交
 coursePublishPre.setStatus("202003");
 //教学机构id
 coursePublishPre.setCompanyId(companyId);
 //提交时间
 coursePublishPre.setCreateDate(LocalDateTime.now());
 CoursePublishPre coursePublishPreUpdate = coursePublishPreMapper.selectById(courseId);
 if(coursePublishPreUpdate == null){
  //添加课程预发布记录
  coursePublishPreMapper.insert(coursePublishPre);
 }else{
  coursePublishPreMapper.updateById(coursePublishPre);
 }

 //更新课程基本表的审核状态
 courseBase.setAuditStatus("202003");
 courseBaseMapper.updateById(courseBase);
}
  • 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
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

接口完善(控制层代码)

 @ResponseBody
@PostMapping ("/courseaudit/commit/{courseId}")
public void commitAudit(@PathVariable("courseId") Long courseId){
     Long companyId = 1232141425L;
     coursePublishService.commitAudit(companyId,courseId);

 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

d 课程发布

在网站上展示课程信息需要解决课程信息显示的性能问题,如果速度慢(排除网速)会影响用户的体验性。
如何去快速搜索课程?
打开课程详情页面仍然去查询数据库可行吗?
为了提高网站的速度需要将课程信息进行缓存,并且要将课程信息加入索引库方便搜索,下图显示了课程发布后课程信息的流转情况:
image.png
1、向内容管理数据库的课程发布表存储课程发布信息,更新课程基本信息表中发布状态为已发布。
2、向Redis存储课程缓存信息。
3、向Elasticsearch存储课程索引信息。
4、请求分布文件系统存储课程静态化页面(即html页面),实现快速浏览课程详情页面。
课程发布表的数据来源于课程预发布表,它们的结构基本一样,只是课程发布表中的状态是课程发布状态,image.png
redis中的课程缓存信息是将课程发布表中的数据转为json进行存储。
elasticsearch中的课程索引信息是根据搜索需要将课程名称、课程介绍等信息进行索引存储。
MinIO中存储了课程的静态化页面文件(html网页),查看课程详情是通过文件系统去浏览课程详情页面。

分布式事务技术方案⭐

一次课程发布操作需要向数据库、redis、elasticsearch、MinIO写四份数据,这里存在分布式事务问题。
什么是分布式事务?
首先理解什么是本地事务?
平常我们在程序中通过spring去控制事务是利用数据库本身的事务特性来实现的,因此叫数据库事务,由于应用主要靠关系数据库来控制事务,此数据库只属于该应用,所以基于本应用自己的关系型数据库的事务又被称为本地事务。
本地事务具有ACID四大特性,数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作 要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚。
理解了本地事务,什么是分布式事务?
现在的需求是课程发布操作后将数据写入数据库、redis、elasticsearch、MinIO四个地方,这四个地方已经不限制在一个数据库内,是由四个分散的服务去提供,与这四个服务去通信需要网络通信,而网络存在不可到达性,这种分布式系统环境下,通过与不同的服务进行网络通信去完成事务称之为分布式事务。
在分布式系统中分布式事务的场景很多:
例如用户注册送积分,银行转账,创建订单减库存,这些都是分布式事务。

//转账举例

//本地事务依赖数据库本身提供的事务特性来实现
begin transaction; 
//1.本地数据库操作:张三减少金额 
//2.本地数据库操作:李四增加金额 
commit transation; 

//分布式环境下
begin transaction; 
//1.本地数据库操作:张三减少金额 
//2.远程调用:让李四增加金额 

commit transation;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

假如当远程调用让李四增加金额成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了张三减少金额的操作,此时张三和李四的数据就不一致了。
因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在一个数据库中甚至不在一个应用系统里,实现转账事务需要通过远程调用,由于网络问题就会导致分布式事务问题。
产生分布式事务的场景
微服务架构下:微服务架构下:订单服务 远程调用 库存服务(库存服务更新库存DB),并更新订单DB
image.png
单服务多数据库:单服务多DB: 用户管理系统 更新用户DB和订单DB
image.png
多服务单数据库: 多服务单数据库: 订单S远程调 库存S 两个S都操作 订单库存DB
image.png

CAP理论

CAP是 Consistency、Availability、Partition tolerance三个词语的缩写,分别表示一致性、可用性、分区容忍性。

CAP理论里三点无法全部满足,由于是分布式系统就满足P ,因为服务间难免网络异常,不能因为局部网络异常导致整个系统不可用;
满足 P 那么AC只能取其一,
从结点1同步到结点2,如果 C ,必须等1的信息同步2完成后才可用(否则可能请求到2时查不到数据,违反一致性),信息同步过程中系统不可用,所以满足C的同时无法满足A
如果要满足A可用性,要时刻保证系统可用就不用等待信息同步完成,此时系统的一致性无法满足。
所以在分布式系统中进行分布式事务控制,要么保证CP、要么保证AP。

分布式事务控制方案

学习了CAP理论该如何控制分布式事务呢?
学习了CAP理论我们知道进行分布式事务控制要在C和A中作出取舍,保证一致性就不要保证可用性,保证可用性就不要保证一致,首先你确认是要CP还是AP,具体要根据应用场景进行判断。
CP的场景:满足C舍弃A,强调一致性。
跨行转账:一次转账请求要等待双方银行系统都完成整个事务才算完成,只要其中一个失败另一方执行回滚操作。
开户操作:在业务系统开户同时要在运营商开户,任何一方开户失败该用户都不可使用,所以要满足CP。
AP的场景:满足A舍弃C,强调可用性。
订单退款,今日退款成功,明日账户到账,只要用户可以接受在一定时间内到账即可。
注册送积分,注册成功积分在24分到账。
支付短信通信,支付成功发短信,短信发送可以有延迟,甚至没有发送成功。
在实际应用中符合AP的场景较多,其实虽然AP舍弃C一致性,实际上最终数据还是达到了一致,也就满足了最终一致性,所以业界定义了BASE理论。
什么是BASE理论?
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。
基本可用:当系统无法满足全部可用时保证核心服务可用即可,比如一个外卖系统,每到中午12点左右系统并发量很高,此时要保证下单流程涉及的服务可用,其它服务暂时不可用。
软状态:是指可以存在中间状态,比如:打印自己的社保统计情况,该操作不会立即出现结果,而是提示你打印中,请在XXX时间后查收。虽然出现了中间状态,但最终状态是正确的。
最终一致性:退款操作后没有及时到账,经过一定的时间后账户到账,舍弃强一致性,满足最终一致性。
分布式事务控制有哪些常用的技术方案?
实现CP就是要实现强一致性:

  • 使用Seata框架基于AT模式实现
  • 使用Seata框架基于TCC模式实现。

实现AP则要保证最终数据一致性:

  • 使用消息队列通知的方式去实现,通知失败自动重试,达到最大失败次数需要人工处理;
  • 使用任务调度的方案,启动任务调度将课程信息由数据库同步到elasticsearch、MinIO、redis中。

课程发布的事务控制方案

满足CP?
如果要满足CP就表示课程发布操作后向数据库、redis、elasticsearch、MinIO写四份数据,只要有一份写失败其它的全部回滚。
满足AP?
课程发布操作后,先更新数据库中的课程发布状态,更新后向redis、elasticsearch、MinIO写课程信息,只要在一定时间内最终向redis、elasticsearch、MinIO写数据成功即可。
选用任务调度的方案去实现分布式事务控制,课程发布满足AP即可
image.png
1、在内容管理服务的数据库中添加一个消息表,消息表和课程发布表在同一个数据库。
2、点击课程发布通过本地事务向课程发布表写入课程发布信息,同时向消息表写课程发布消息。通过数据库进行控制,只要课程发布表插入成功消息表也插入成功,消息表的数据就记录了某门课程发布的任务。
3、启动任务调度系统定时调度内容管理服务去定时扫描消息表的记录。
4、当扫描到课程发布的消息时即开始完成向redis、elasticsearch、MinIO同步数据的操作。
5、同步数据的任务完成后删除消息表记录。
课程发布操作的流程:
image.png
1、执行发布操作,内容管理服务存储课程发布表的同时向消息表添加一条“课程发布任务”。这里使用本地事务保证课程发布信息保存成功,同时消息表也保存成功。
2、任务调度服务定时调度内容管理服务扫描消息表,由于课程发布操作后向消息表插入一条课程发布任务,此时扫描到一条任务。
3、拿到任务开始执行任务,分别向redis、elasticsearch及文件系统存储数据。
4、任务完成后删除消息表记录。

写课程发布接口

根据课程发布的分布式事务控制方案,课程发布操作首先通过本地事务向课程发布表写入课程发布信息并向消息表插入一条消息,这里定义的课程发布接口要实现该功能。
DAO开发
课程发布操作对数据库操作如下:
1、向课程发布表course_publish插入一条记录,记录来源于课程预发布表,如果存在则更新,发布状态为:已发布。
2、更新course_base表的课程发布状态为:已发布
3、删除课程预发布表的对应记录。
4、向mq_message消息表插入一条消息,消息类型为:course_publish
约束:
1、课程审核通过方可发布。
2、本机构只允许发布本机构的课程。

1、在内容管理数据库创建mq_message消息表及消息历史消息表(历史表存储已经完成的消息)。
image.png消息表结构如下:
image.png
2、生成mq_message消息表、course_publish课程发布表的po和mapper接口
稍后会开发一个通用的消息处理组件
Service开发

Java
 @Transactional
 @Override
 public void publish(Long companyId, Long courseId) {

  //约束校验
  //查询课程预发布表
  CoursePublishPre coursePublishPre = coursePublishPreMapper.selectById(courseId);
  if(coursePublishPre == null){
     XueChengPlusException.cast("请先提交课程审核,审核通过才可以发布");
  }
  //本机构只允许提交本机构的课程
  if(!coursePublishPre.getCompanyId().equals(companyId)){
   XueChengPlusException.cast("不允许提交其它机构的课程。");
  }


  //课程审核状态
  String auditStatus = coursePublishPre.getStatus();
  //审核通过方可发布
  if(!"202004".equals(auditStatus)){
   XueChengPlusException.cast("操作失败,课程审核通过方可发布。");
  }

  //保存课程发布信息
  saveCoursePublish(courseId);

  //保存消息表
  saveCoursePublishMessage(courseId);

 //删除课程预发布表对应记录
  coursePublishPreMapper.deleteById(courseId);

 }

/**
 * @description 保存课程发布信息
 * @param courseId  课程id
 * @return void
 * @author Mr.M
 * @date 2022/9/20 16:32
*/
 private void saveCoursePublish(Long courseId){
   //整合课程发布信息
  //查询课程预发布表
  CoursePublishPre coursePublishPre = coursePublishPreMapper.selectById(courseId);
  if(coursePublishPre == null){
   XueChengPlusException.cast("课程预发布数据为空");
  }

  CoursePublish coursePublish = new CoursePublish();

  //拷贝到课程发布对象
  BeanUtils.copyProperties(coursePublishPre,coursePublish);
  coursePublish.setStatus("203002");
  CoursePublish coursePublishUpdate = coursePublishMapper.selectById(courseId);
  if(coursePublishUpdate == null){
   coursePublishMapper.insert(coursePublish);
  }else{
   coursePublishMapper.updateById(coursePublish);
  }
  //更新课程基本表的发布状态
  CourseBase courseBase = courseBaseMapper.selectById(courseId);
  courseBase.setStatus("203002");
  courseBaseMapper.updateById(courseBase);

 }

 /**
  * @description 保存消息表记录,稍后实现
  * @param courseId  课程id
  */
private void saveCoursePublishMessage(Long courseId){


}
  • 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
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76

接口完善(控制层代码)

Java
 @ApiOperation("课程发布")
 @ResponseBody
 @PostMapping ("/coursepublish/{courseId}")
public void coursepublish(@PathVariable("courseId") Long courseId){
     Long companyId = 1232141425L;
     coursePublishService.publish(companyId,courseId);

 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

消息处理SDK

消息模块技术方案
课程发布操作执行后需要扫描消息表的记录,有关消息表处理的有哪些?
image.png
上图中红色框内的都是与消息处理相关的操作:
1、新增消息表
2、扫描消息表。
3、更新消息表。
4、删除消息表。
使用消息表这种方式实现最终事务一致性的地方除了课程发布还有其它业务场景。
image.png
如果在每个地方都实现一套针对消息表定时扫描、处理的逻辑基本上都是重复的,软件的可复用性太低,成本太高。
如何解决这个问题?
针对这个问题可以想到将消息处理相关的逻辑做成一个通用的东西。
是做成通用的服务,还是做成通用的代码组件呢?

  • 通用的服务是完成一个通用的独立功能,并提供独立的网络接口,比如:项目中的文件系统服务,提供文件的分布式存储服务。
  • 代码组件也是完成一个通用的独立功能,通常会提供API的方式供外部系统使用,比如:fastjson、Apache commons工具包等。

如果将消息处理做成一个通用的服务,该服务需要连接多个数据库,因为它要扫描微服务数据库下的消息表,并且要提供与微服务通信的网络接口,单就针对当前需求而言开发成本有点高。
如果将消息处理做一个SDK工具包相比通用服务不仅可以解决将消息处理通用化需求,还能降低成本。
所以,本项目确定将对消息表相关的处理做成一个SDK组件供各微服务使用
image.png
下边对消息SDK的设计内容进行说明:
sdk需要提供执行任务的逻辑吗?
拿课程发布任务举例,执行课程发布任务是要向redis、索引库等同步数据,其它任务的执行逻辑是不同的,所以执行任务在sdk中不用实现任务逻辑,只需要提供一个抽象方法由具体的执行任务方去实现。
如何保证任务的幂等性?
在视频处理章节介绍的视频处理的幂等性方案,这里可以采用类似方案,任务执行完成后会从消息表删除,如果消息的状态是完成或不存在消息表中则不用执行。
如何保证任务不重复执行?
采用和视频处理章节一致方案,除了保证任务的幂等性外,任务调度采用分片广播,根据分片参数去获取任务,另外阻塞调度策略为丢弃任务。
注意:这里是信息同步类任务,即使任务重复执行也没有关系,不再使用抢占任务的方式保证任务不重复执行。
还有一个问题,根据消息表记录是否存在或消息表中的任务状态去保证任务的幂等性,如果一个任务有好几个小任务,比如:课程发布任务需要执行三个同步操作:存储课程到redis、存储课程到索引库,存储课程页面到文件系统。如果其中一个小任务已经完成也不应该去重复执行。这里该如何设计?

将小任务作为任务的不同的阶段,在消息表中设计阶段状态。
image.png
每完成一个阶段在相应的阶段状态字段打上完成标记,即使这个大任务没有完成再重新执行时,如果小阶段任务完成了也不会重复执行某个小阶段的任务。
综上所述,除了消息表的基本的增、删、改、查的接口外,消息SDK还具有如下接口功能:

Java
package com.xuecheng.messagesdk.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.xuecheng.messagesdk.model.po.MqMessage;

import java.util.List;

/**
 * <p>
 *  服务类
 * </p>
 */
public interface MqMessageService extends IService<MqMessage> {

    /**
     * @description 扫描消息表记录,采用与扫描视频处理表相同的思路
     * @param shardIndex 分片序号
     * @param shardTotal 分片总数
     * @param count 扫描记录数
     * @return java.util.List 消息记录
     */
    public List<MqMessage> getMessageList(int shardIndex, int shardTotal,  String messageType,int count);

    /**
     * @description 完成任务
     * @param id 消息id
     * @return int 更新成功:1
     */
    public int completed(long id);

    /**
     * @description 完成阶段任务
     * @param id 消息id
     * @return int 更新成功:1
     */
    public int completedStageOne(long id);
    public int completedStageTwo(long id);
    public int completedStageThree(long id);
    public int completedStageFour(long id);

    /**
     * @description 查询阶段状态
     * @param id
     * @return int
    */
    public int getStageOne(long id);
    public int getStageTwo(long id);
    public int getStageThree(long id);
    public int getStageFour(long id);

}
  • 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
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

消息SDK提供消息处理抽象类,此抽象类供使用方去继承使用

Java
package com.xuecheng.messagesdk.service;

import com.xuecheng.messagesdk.model.po.MqMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;
import java.util.concurrent.*;

/**
 * @description 消息处理抽象类
 */
@Slf4j
@Data
public abstract class MessageProcessAbstract {

    @Autowired
    MqMessageService mqMessageService;


    /**
     * @param mqMessage 执行任务内容
     * @return boolean true:处理成功,false处理失败
     * @description 任务处理
    public abstract boolean execute(MqMessage mqMessage);


    /**
     * @description 扫描消息表多线程执行任务
     * @param shardIndex 分片序号
     * @param shardTotal 分片总数
     * @param messageType  消息类型
     * @param count  一次取出任务总数
     * @param timeout 预估任务执行时间,到此时间如果任务还没有结束则强制结束 单位秒
     * @return void
    */
    public void process(int shardIndex, int shardTotal,  String messageType,int count,long timeout) {

        try {
            //扫描消息表获取任务清单
            List<MqMessage> messageList = mqMessageService.getMessageList(shardIndex, shardTotal,messageType, count);
            //任务个数
            int size = messageList.size();
            log.debug("取出待处理消息"+size+"条");
            if(size<=0){
                return ;
            }

            //创建线程池
            ExecutorService threadPool = Executors.newFixedThreadPool(size);
            //计数器
            CountDownLatch countDownLatch = new CountDownLatch(size);
            messageList.forEach(message -> {
                threadPool.execute(() -> {
                    log.debug("开始任务:{}",message);
                    //处理任务
                    try {
                        boolean result = execute(message);
                        if(result){
                            log.debug("任务执行成功:{})",message);
                            //更新任务状态,删除消息表记录,添加到历史表
                            int completed = mqMessageService.completed(message.getId());
                            if (completed>0){
                                log.debug("任务执行成功:{}",message);
                            }else{
                                log.debug("任务执行失败:{}",message);
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        log.debug("任务出现异常:{},任务:{}",e.getMessage(),message);
                    }
                    //计数
                    countDownLatch.countDown();
                    log.debug("结束任务:{}",message);

                });
            });

            //等待,给一个充裕的超时时间,防止无限等待,到达超时时间还没有处理完成则结束任务
            countDownLatch.await(timeout,TimeUnit.SECONDS);
            System.out.println("结束....");
        } catch (InterruptedException e) {
           e.printStackTrace();

        }

    }

}
  • 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
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91

集成消息SDK
1、在内容管理数据库创建消息表和消息历史表
2、拷贝资料中的xuecheng-plus-message-sdk到工程目录,如下图:
3、在内容管理service工程中添加sdk依赖

| XML

com.xuecheng
xuecheng-plus-message-sdk
0.0.1-SNAPSHOT

4、课程发布操作使用本地事务保存课程发布信息、添加消息表。
回到当初编写课程发布时的代码,如下

Java
@Transactional
@Override
public void publish(Long companyId, Long courseId) {

 //约束校验
 //查询课程预发布表
 CoursePublishPre coursePublishPre = coursePublishPreMapper.selectById(courseId);
 if(coursePublishPre == null){
    XueChengPlusException.cast("请先提交课程审核,审核通过才可以发布");
 }
 //本机构只允许提交本机构的课程
 if(!coursePublishPre.getCompanyId().equals(companyId)){
  XueChengPlusException.cast("不允许提交其它机构的课程。");
 }

 //课程审核状态
 String auditStatus = coursePublishPre.getStatus();
 //审核通过方可发布
 if(!"202004".equals(auditStatus)){
  XueChengPlusException.cast("操作失败,课程审核通过方可发布。");
 }
 //保存课程发布信息
 saveCoursePublish(courseId);

 //保存消息表
 saveCoursePublishMessage(courseId);

//删除课程预发布表对应记录
 coursePublishPreMapper.deleteById(courseId);

}
  • 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

我们要填充的saveCoursePublishMessage(courseId)方法,如下:

Java
 /**
  * @description 保存消息表记录
  * @param courseId  课程id
  * @return void
  */
private void saveCoursePublishMessage(Long courseId){
 MqMessage mqMessage = mqMessageService.addMessage("course_publish", String.valueOf(courseId), null, null);
 if(mqMessage==null){
  XueChengPlusException.cast(CommonError.UNKOWN_ERROR);
 }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

课程发布任务处理
在内容管理服务添加消息处理sdk的依赖即可使用它,实现sdk中的MessageProcessAbstract类,重写execte方法。
实现sdk中的MessageProcessAbstract类:

Java
package com.xuecheng.content.service.jobhandler;

import com.xuecheng.messagesdk.model.po.MqMessage;
import com.xuecheng.messagesdk.service.MessageProcessAbstract;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 *
 */
@Slf4j
@Component
public class CoursePublishTask extends MessageProcessAbstract {

    //课程发布任务处理
    @Override
    public boolean execute(MqMessage mqMessage) {
        //获取消息相关的业务信息
        String businessKey1 = mqMessage.getBusinessKey1();
        long courseId = Integer.parseInt(businessKey1);
        //课程静态化
        generateCourseHtml(mqMessage,courseId);
        //课程索引
        saveCourseIndex(mqMessage,courseId);
        //课程缓存
        saveCourseCache(mqMessage,courseId);
        return true;
    }


    //生成课程静态化页面并上传至文件系统
    public void generateCourseHtml(MqMessage mqMessage,long courseId){

        log.debug("开始进行课程静态化,课程id:{}",courseId);
        //消息id
        Long id = mqMessage.getId();
        //消息处理的service
        MqMessageService mqMessageService = this.getMqMessageService();
        //消息幂等性处理
        int stageOne = mqMessageService.getStageOne(id);
        if(stageOne >0){
            log.debug("课程静态化已处理直接返回,课程id:{}",courseId);
            return ;
        }
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //保存第一阶段状态
        mqMessageService.completedStageOne(id);

    }

    //将课程信息缓存至redis
    public void saveCourseCache(MqMessage mqMessage,long courseId){
        log.debug("将课程信息缓存至redis,课程id:{}",courseId);
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }


    }
    //保存课程索引信息
    public void saveCourseIndex(MqMessage mqMessage,long courseId){
        log.debug("保存课程索引信息,课程id:{}",courseId);
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

    }

}
  • 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
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82

开启任务调度
1、首先在内容管理service工程中添加xxl-job依赖
2、配置执行器
在nacos中在content-service-dev.yaml中配置
3、从媒资管理服务层工程中拷贝一个XxlJobConfig配置类到内容管理service工程中。

页面静态化

根据课程发布的操作流程,执行课程发布后要将课程详情信息页面静态化,生成html页面上传至文件系统。
什么是页面静态化?
课程预览功能通过模板引擎技术在页面模板中填充数据,生成html页面,这个过程是当客户端请求服务器时服务器才开始渲染生成html页面,最后响应给浏览器,服务端渲染的并发能力是有限的。
页面静态化则强调将生成html页面的过程提前,提前使用模板引擎技术生成html页面,当客户端请求时直接请求html页面,由于是静态页面可以使用nginx、apache等高性能的web服务器,并发性能高。
什么时候能用页面静态化技术?
当数据变化不频繁,一旦生成静态页面很长一段时间内很少变化,此时可以使用页面静态化。因为如果数据变化频繁,一旦改变就需要重新生成静态页面,导致维护静态页面的工作量很大。
根据课程发布的业务需求,虽然课程发布后仍可以修改课程信息,但需要经过课程审核,且修改频度不大,所以适合使用页面静态化。

上传文件测试

** 配置远程调用环境**
静态化生成文件后需要上传至分布式文件系统,根据微服务的职责划分,媒资管理服务负责维护文件系统中的文件,所以内容管理服务对页面静态化生成html文件需要调用媒资管理服务的上传文件接口。如下图:
image.png
微服务之间难免会存在远程调用,在Spring Cloud中可以使用Feign进行远程调用,
Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。

熔断降级处理

微服务中难免存在服务之间的远程调用,比如:内容管理服务远程调用媒资服务的上传文件接口,当微服务运行不正常会导致无法正常调用微服务,此时会出现异常,如果这种异常不去处理可能导致雪崩效应。
微服务的雪崩效应表现在服务与服务之间调用,当其中一个服务无法提供服务可能导致其它服务也死掉,比如:服务B调用服务A,由于A服务异常导致B服务响应缓慢,最后B、C等服务都不可用,像这样由一个服务所引起的一连串的多个服务无法提供服务即是微服务的雪崩效应,如下图:
image.png
如何解决由于微服务异常引起的雪崩效应呢?
可以采用熔断、降级的方法去解决。
熔断降级的相同点都是为了解决微服务系统崩溃的问题,但它们是两个不同技术手段,两者又存在联系。

  • 熔断:
    • 当下游服务异常而断开与上游服务的交互,它就相当于保险丝,下游服务异常触发了熔断,从而保证上游服务不受影响。
  • 降级:
    • 当下游服务异常触发熔断后,上游服务就不再去调用异常的微服务而是执行了降级处理逻辑,这个降级处理逻辑可以是本地一个单独的方法。
      :::info
      两者都是为了保护系统,熔断是当下游服务异常时一种保护系统的手段,降级是熔断后上游服务处理熔断的方法。
      :::
      使用Hystrix框架实现熔断、降级处理,在feign-dev.yaml中配置。
      1、开启Feign熔断保护
      2、设置熔断的超时时间,为了防止一次处理时间较长触发熔断这里还需要设置请求和连接的超时时间
      3、定义降级逻辑
      1)fallback
      定义一个fallback类MediaServiceClientFallback,此类实现了MediaServiceClient接口。
      第一种方法无法取出熔断所抛出的异常,第二种方法定义MediaServiceClientFallbackFactory 可以解决这个问题。
      2)fallbackFactory
      第二种方法在FeignClient中指定fallbackFactory
      降级处理逻辑:
      返回一个null对象,上游服务请求接口得到一个null说明执行了降级处理。

课程静态化开发

课程页面静态化和静态页面远程上传测试通过,下一步开发课程静态化功能,最终使用消息处理SDK去调度执行。
在课程发布的service编写这两部分内容,最后通过消息去调度执行。

e 课程搜索

搜索功能是一个系统的重要功能,是信息查询的方式。课程搜索是课程展示的渠道,用户通过课程搜索找到课程信息,进一步去查看课程的详细信息,进行选课、支付、学习。
本项目的课程搜索支持全文检索技术,什么是全文检索?
全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。
全文检索可以简单理解为通过索引搜索文章。
image.png
全文检索的速度非常快,早期应用在搜索引擎技术中,比如:百度、google等,现在通常一些大型网站的搜索功能都是采用全文检索技术。
image.png
课程搜索也要将课程信息建立索引,在课程发布时建立课程索引,索引建立好用户可通过搜索网页去查询课程信息。
image.png
所以,课程搜索模块包括两部分:课程索引、课程搜索。
课程索引是将课程信息建立索引。
课程搜索是通过前端网页,通过关键字等条件去搜索课程。

1、课程索引
在课程发布操作执行后通过消息处理方式创建课程索引
image.png
本项目使用elasticsearch作为索引及搜索服务。
2、课程搜索
课程索引创建完成,用户才可以通过前端搜索课程信息。
课程搜索可以从首页进入搜索页面。

搭建elasticsearch

在课前下发的虚拟中已经在docker容器中安装了elasticsearch和kibana。
kibana 是 ELK(Elasticsearch , Logstash, Kibana )之一,kibana 一款开源的数据分析和可视化平台,通过可视化界面访问elasticsearch的索引库,并可以生成一个数据报表。
开发中主要使用kibana通过api对elasticsearch进行索引和搜索操作,通过浏览器访问 http://192.168.101.65:5601/app/dev_tools#/console进入kibana的开发工具界面。
修改虚拟机中的启动脚本restart.sh添加

索引相当于MySQL中的表,Elasticsearch与MySQL之间概念的对应关系见下表:
image.png
要使用elasticsearch需要建立索引,Mapping相当于表结构,Mapping创建后其字段不能删除,如果要删除需要删除整个索引,下边介绍创建索引、查询索引、删除索引的方法:

搜索

根据搜索界面可知需求如下:
1、根据一级分类、二级分类搜索课程信息。
2、根据关键字搜索课程信息,搜索方式为全文检索,关键字需要匹配课程的名称、 课程内容。
3、根据难度等级搜索课程。
4、搜索结点分页显示。
技术点:
1、整体采用布尔查询。
2、根据关键字搜索,采用MultiMatchQuery,搜索name、description字段。
3、根据分类、课程等级搜索采用过滤器实现。
4、分页查询。
5、高亮显示。
为什么课程分类、课程等级等查询使用过滤器方式?
使用关键字查询需要计算相关度得分,根据课程分类、课程等级去查询不需要计算相关度得分,使用过滤器实现根据课程分类、课程等级查询的过程不会计算相关度得分,效率更高。

课程信息索引同步

技术方案

通过向索引中添加课程信息最终实现了课程的搜索,我们发现课程信息是先保存在关系数据库中,而后再写入索引,这个过程是将关系数据中的数据同步到elasticsearch索引中的过程,可以简单成为索引同步。
通常项目中使用elasticsearch需要完成索引同步,索引同步的方法很多:
1、针对实时性非常高的场景需要满足数据的及时同步,可以同步调用,或使用Canal去实现
1)同步调用即在向MySQL写数据后远程调用搜索服务的接口写入索引,此方法简单但是耦合代码太高。
2)可以使用一个中间的软件canal解决耦合性的问题,但存在学习与维护成本。
canal主要用途是基于 MySQL 数据库增量日志解析,并能提供增量数据订阅和消费,实现将MySQL的数据同步到消息队列、Elasticsearch、其它数据库等,应用场景十分丰富。
image.png
它的地址:
github地址:https://github.com/alibaba/canal
版本下载地址:https://github.com/alibaba/canal/releases
文档地址:https://github.com/alibaba/canal/wiki/Docker-QuickStart

Canal基于mysql的binlog技术实现数据同步,什么是binlog,它是一个文件,二进制格式,记录了对数据库更新的SQL语句,向数据库写数据的同时向binlog文件里记录对应的sql语句。当数据库服务器发生了故障就可以使用binlog文件对数据库进行恢复。
所以,使用canal是需要开启mysql的binlog写入功能,Canal工作原理如下:
image.png
1、canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump
协议
2、MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
3、canal 解析 binary log 对象(原始为 byte 流)
详细使用Canal进行索引同步的步骤参考:Canal实现索引同步.pdf

2、当索引同步的实时性要求不高时可用的技术比较多,比如:MQ、Logstash、任务调度等。
MQ:向mysql写数据的时候向mq写入消息,搜索服务监听MQ,收到消息后写入索引。使用MQ的优势是代码解耦,但是需要处理消息可靠性的问题有一定的技术成本,做到消息可靠性需要做到生产者投递成功、消息持久化以及消费者消费成功三个方面,另外还要做好消息幂等性问题。
Logstash: 开源实时日志分析平台 ELK包括Elasticsearch、Kibana、Logstash,Logstash负责收集、解析和转换日志信息,可以实现MySQL与Elasticsearch之间的
数据同步。也可以实现解耦合并且是官方推荐,但需要增加学习与维护成本。
任务调度:向mysql写数据的时候记录修改记录,开启一个定时任务根据修改记录将数据同步到Elasticsearch。

根据本项目的需求,课程发布后信息同步的实时性要求不高,从提交审核到发布成功一般两个工作日完成。综合比较以上技术方案本项目的索引同步技术使用任务调度的方法。
image.png
1、课程发布向消息表插入记录。
2、由任务调度程序通过消息处理SDK对消息记录进行处理。
3、向elasticsearch索引中保存课程信息。
如何向向elasticsearch索引中保存课程信息?
执行流程如下:
由内容管理服务远程调用搜索服务添加课程信息索引,搜索服务再请求elasticsearch向课程索引中添加文档。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号