引入sdk,实现分布式事务
在引入消息sdk,并完成存储课程发布表的同时插入消息表这一步骤后,下面就应该调用sdk的接口扫描消息表(也就是执行查询操作),课程缓存,课程所有,上传课程静态页面,删除消息表(也就是执行删除操作)这几步。
查询和删除sdk中已经写好,我们只需要重写sdk中的execute方法,实现中间三步即可。而execute方法是在sdk的service包下的MessageProcessAbstract抽象类中。所以我们在content-service的service包下定义一个jobhandler包,里面放执行任务调度的类。而现在我们需要执行的任务调度就是课程发布:
@Slf4j
@Component
/**
* 课程发布任务类
*/
public class CoursePublishTask extends MessageProcessAbstract {
@Autowired
CoursePublishService coursePublishService;
@Autowired
SearchServiceClient searchServiceClient;
@Autowired
CoursePublishMapper coursePublishMapper;
//任务调度入口
@XxlJob("CoursePublishJobHandler")
public void coursePublishJobHandler() throws Exception{
// 分片参数
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
log.debug("shardIndex="+shardIndex+",shardTotal="+shardTotal);
//调用抽象类的方法执行任务
//参数:分片序号、分片总数、消息类型、一次最多取到的任务数量、一次任务调度执行的超时时间
process(shardIndex,shardTotal,"course_publish",30,60);
}
//课程发布任务处理
@Override
public boolean execute(MqMessage mqMessage) {
//获取消息相关的业务信息(拿到课程id)
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 == 1){
log.debug("课程静态化已处理直接返回,课程id:{}",courseId);
return ;
}
//生成静态化页面
File file = coursePublishService.generateCourseHtml(courseId);
//上传静态化页面
if(file!=null){
coursePublishService.uploadCourseHtml(courseId,file);
}
//保存第一阶段状态
mqMessageService.completedStageOne(id);
}
//将课程信息缓存至redis
public void saveCourseCache(MqMessage mqMessage,long courseId){
log.debug("将课程信息缓存至redis,课程id:{}",courseId);
//消息id
Long id = mqMessage.getId();
//消息处理的service
MqMessageService mqMessageService = this.getMqMessageService();
//消息幂等性处理
//取出阶段三的执行状态
int stageThree = mqMessageService.getStageThree(id);
if(stageThree >0){
log.debug("课程静态化已处理直接返回,课程id:{}",courseId);
return ;
}
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//保存第三阶段状态
mqMessageService.completedStageThree(id);
}
//保存课程索引信息
public void saveCourseIndex(MqMessage mqMessage, long courseId){
log.debug("保存课程索引信息,课程id:{}",courseId);
//消息id
Long id = mqMessage.getId();
//消息处理的service
MqMessageService mqMessageService = this.getMqMessageService();
//取出阶段二的执行状态
int stageTwo = mqMessageService.getStageTwo(id);
//消息幂等性处理
if(stageTwo >0){
log.debug("课程索引已处理直接返回,课程id:{}",courseId);
return ;
}
//查询课程信息,调用搜索服务添加索引接口
//取出课程发布信息
CoursePublish coursePublish = coursePublishMapper.selectById(courseId);
//拷贝至课程索引对象
CourseIndex courseIndex = new CourseIndex();
BeanUtils.copyProperties(coursePublish,courseIndex);
//远程调用搜索服务api添加课程信息到索引
Boolean add = searchServiceClient.add(courseIndex);
if(!add){
XueChengPlusException.cast("远程调用搜索服务添加索引失败");
}
//完成本阶段的任务
mqMessageService.completedStageTwo(id);
}
}
@XxlJob("CoursePublishJobHandler"):
这个注解标记了一个方法作为一个XXL-Job任务处理器,当任务调度器触发 CoursePublishJobHandler 任务时,将会执行 coursePublishJobHandler 方法。
coursePublishJobHandler 方法是任务调度的入口,它获取了分片参数,然后调用了 sdk提供的抽象方法process 执行任务。
/**
* @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);
}finally {
//计数
countDownLatch.countDown();
}
log.debug("结束任务:{}",message);
});
});
//等待,给一个充裕的超时时间,防止无限等待,到达超时时间还没有处理完成则结束任务
countDownLatch.await(timeout,TimeUnit.SECONDS);
System.out.println("结束....");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
执行逻辑:
1.首先,通过调用 mqMessageService.getMessageList() 方法从消息表中获取待处理的任务清单。
然后,创建一个固定大小的线程池 threadPool,线程池的大小为待处理任务的数量。
2.使用 CountDownLatch 来控制任务的执行,在每个任务执行完成后,调用 countDownLatch.countDown() 方法进行计数。
3.对于每个任务,通过 threadPool.execute() 提交到线程池中执行,执行的具体任务由 execute() 方法实现。
4.等待所有任务执行完成,调用 countDownLatch.await(timeout, TimeUnit.SECONDS) 方法,等待一段预估的超时时间,如果超过了这个时间仍有任务没有执行完成,则强制结束任务。
1.实现生成课程静态化页面并上传至文件系统
调用service层的CoursePublishService接口如下:
/**
* @description 课程静态化
* @param courseId 课程id
* @return File 静态化文件
*/
public File generateCourseHtml(Long courseId);
/**
* @description 上传课程静态化页面
* @param file 静态化文件
* @return void
*/
public void uploadCourseHtml(Long courseId,File file);
接口实现如下:
@Override
public File generateCourseHtml(Long courseId) {
//静态化文件
File htmlFile = null;
try {
//配置freemarker
Configuration configuration = new Configuration(Configuration.getVersion());
//加载模板
//选指定模板路径,classpath下templates下
//得到classpath路径
String classpath = this.getClass().getResource("/").getPath();
configuration.setDirectoryForTemplateLoading(new File(classpath + "/templates/"));
//设置字符编码
configuration.setDefaultEncoding("utf-8");
//指定模板文件名称
Template template = configuration.getTemplate("course_template.ftl");
//准备数据
CoursePreviewDto coursePreviewInfo = this.getCoursePreviewInfo(courseId);
Map<String, Object> map = new HashMap<>();
map.put("model", coursePreviewInfo);
//静态化
//参数1:模板,参数2:数据模型
String content = FreeMarkerTemplateUtils.processTemplateIntoString(template, map);
// System.out.println(content);
//将静态化内容输出到文件中
InputStream inputStream = IOUtils.toInputStream(content);
//创建静态化文件
htmlFile = File.createTempFile("course",".html");
log.debug("课程静态化,生成静态文件:{}",htmlFile.getAbsolutePath());
//输出流
FileOutputStream outputStream = new FileOutputStream(htmlFile);
IOUtils.copy(inputStream, outputStream);
} catch (Exception e) {
log.error("课程静态化异常:{}",e.toString());
XueChengPlusException.cast("课程静态化异常");
}
return htmlFile;
}
@Override
public void uploadCourseHtml(Long courseId, File file) {
MultipartFile multipartFile = MultipartSupportConfig.getMultipartFile(file);
String course = mediaServiceClient.uploadFile(multipartFile, "course/"+courseId+".html");
if(course==null){
XueChengPlusException.cast("上传静态文件异常");
}
}
这两个方法分别是 `generateCourseHtml` 和 `uploadCourseHtml`,用于生成课程静态化页面并上传至文件系统。
1. `generateCourseHtml(Long courseId)`: 这个方法用于生成课程静态化页面。它的实现逻辑包括:
- 配置Freemarker模板引擎,加载模板文件。
- 准备数据,调用 `getCoursePreviewInfo` 方法获取课程预览信息。
- 将模板和数据模型传入Freemarker引擎中进行静态化,得到静态化后的内容。
- 将静态化内容写入临时文件中,并返回该文件。
2. `uploadCourseHtml(Long courseId, File file)`: 这个方法用于上传课程静态化页面至文件系统。它的实现逻辑包括:
- 将文件转换为 `MultipartFile` 对象。
- 调用 `mediaServiceClient.uploadFile` 方法上传文件至文件系统,其中 `mediaServiceClient` 是一个远程服务的客户端,用于与文件系统交互。
- 如果上传成功,则返回文件路径,否则抛出异常。
在uploadCourseHtml方法中调用了config包下的MultipartSupportConfig类:
@Configuration public class MultipartSupportConfig { @Autowired private ObjectFactory<HttpMessageConverters> messageConverters; @Bean @Primary//注入相同类型的bean时优先使用 @Scope("prototype") public Encoder feignEncoder() { return new SpringFormEncoder(new SpringEncoder(messageConverters)); } //将file转为Multipart public static MultipartFile getMultipartFile(File file) { FileItem item = new DiskFileItemFactory().createItem("file", MediaType.MULTIPART_FORM_DATA_VALUE, true, file.getName()); try (FileInputStream inputStream = new FileInputStream(file); OutputStream outputStream = item.getOutputStream();) { IOUtils.copy(inputStream, outputStream); } catch (Exception e) { e.printStackTrace(); } return new CommonsMultipartFile(item); } }
这段代码是一个Spring配置类,其中定义了一个 `MultipartSupportConfig` 类,用于配置多部分数据支持(Multipart)。让我们逐一解释其中的内容:
@Configuration
这个注解表示这是一个配置类,在Spring应用程序上下文中会被加载和解析。
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
这行代码使用了`@Autowired`注解,将一个 `ObjectFactory<HttpMessageConverters>` 类型的实例注入到了 `messageConverters` 字段中。`ObjectFactory` 是Spring的一个工厂接口,用于创建对象的实例。
@Bean
@Primary
@Scope("prototype")
public Encoder feignEncoder() {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
这个方法定义了一个名为 `feignEncoder` 的Bean,用于配置Feign的编码器。`@Primary` 注解表示在注入相同类型的Bean时优先使用这个Bean,`@Scope("prototype")` 注解表示每次调用这个Bean时都会创建一个新的实例。这个Bean返回了一个 `SpringFormEncoder` 对象,它接受一个 `SpringEncoder` 对象作为参数,并且使用了 `messageConverters` 字段注入的对象。
public static MultipartFile getMultipartFile(File file) {...}
这个方法定义了一个静态方法,用于将一个 `File` 对象转换为 `MultipartFile` 对象。在方法中,它使用了 `DiskFileItemFactory` 和 `FileItem` 来创建一个临时文件项,然后将文件的内容复制到这个临时文件项中,并最终将其包装为一个 `CommonsMultipartFile` 对象返回。
总的来说,这段代码主要是用于配置Multipart支持,并且提供了一个静态方法来将 `File` 对象转换为 `MultipartFile` 对象,以便在应用程序中方便地处理文件上传。
在uploadCourseHtml方法中还调用了feignclient包下的MediaServiceClient类:
@FeignClient(value = "media-api",configuration = MultipartSupportConfig.class)
/**
* 媒资管理服务远程接口
*/
public interface MediaServiceClient {
@RequestMapping(value = "/media/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String uploadFile(@RequestPart("filedata") MultipartFile upload, @RequestParam(value = "objectName",required=false) String objectName);
}
这段代码定义了一个名为 `MediaServiceClient` 的Feign客户端接口,用于远程调用媒资管理服务的上传文件接口。
@FeignClient(value = "media-api",configuration = MultipartSupportConfig.class)
这个注解标记了一个Feign客户端接口,指定了要调用的远程服务的名称为 `media-api`,并且指定了 `MultipartSupportConfig` 类作为配置类。Feign客户端接口中的方法会被动态代理实现,并通过HTTP请求调用远程服务的相应接口。
@RequestMapping(value = "/media/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
这个注解标记了一个方法,指定了要调用的远程服务的路径为 `/media/upload/coursefile`,并且指定了请求的 `Content-Type` 为 `multipart/form-data`。这个方法用于上传文件到媒资管理服务。
String uploadFile(@RequestPart("filedata") MultipartFile upload, @RequestParam(value = "objectName",required=false) String objectName)
这个方法定义了一个远程调用的接口方法,用于上传文件到媒资管理服务。方法参数包括:
- `@RequestPart("filedata") MultipartFile upload`:用于接收上传的文件,通过 `filedata` 参数名来指定。
- `@RequestParam(value = "objectName",required=false) String objectName`:用于接收文件在文件系统中的对象名称,这是一个可选参数。
package org.springframework.web.multipart;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import org.springframework.core.io.InputStreamSource;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.FileCopyUtils;
/**
* A representation of an uploaded file received in a multipart request.
*
* <p>The file contents are either stored in memory or temporarily on disk.
* In either case, the user is responsible for copying file contents to a
* session-level or persistent store as and if desired. The temporary storage
* will be cleared at the end of request processing.
*
* @author Juergen Hoeller
* @author Trevor D. Cook
* @since 29.09.2003
* @see org.springframework.web.multipart.MultipartHttpServletRequest
* @see org.springframework.web.multipart.MultipartResolver
*/
public interface MultipartFile extends InputStreamSource {
/**
* Return the name of the parameter in the multipart form.
* @return the name of the parameter (never {@code null} or empty)
*/
String getName();
/**
* Return the original filename in the client's filesystem.
* <p>This may contain path information depending on the browser used,
* but it typically will not with any other than Opera.
* @return the original filename, or the empty String if no file has been chosen
* in the multipart form, or {@code null} if not defined or not available
* @see org.apache.commons.fileupload.FileItem#getName()
* @see org.springframework.web.multipart.commons.CommonsMultipartFile#setPreserveFilename
*/
@Nullable
String getOriginalFilename();
/**
* Return the content type of the file.
* @return the content type, or {@code null} if not defined
* (or no file has been chosen in the multipart form)
*/
@Nullable
String getContentType();
/**
* Return whether the uploaded file is empty, that is, either no file has
* been chosen in the multipart form or the chosen file has no content.
*/
boolean isEmpty();
/**
* Return the size of the file in bytes.
* @return the size of the file, or 0 if empty
*/
long getSize();
/**
* Return the contents of the file as an array of bytes.
* @return the contents of the file as bytes, or an empty byte array if empty
* @throws IOException in case of access errors (if the temporary store fails)
*/
byte[] getBytes() throws IOException;
/**
* Return an InputStream to read the contents of the file from.
* <p>The user is responsible for closing the returned stream.
* @return the contents of the file as stream, or an empty stream if empty
* @throws IOException in case of access errors (if the temporary store fails)
*/
@Override
InputStream getInputStream() throws IOException;
/**
* Return a Resource representation of this MultipartFile. This can be used
* as input to the {@code RestTemplate} or the {@code WebClient} to expose
* content length and the filename along with the InputStream.
* @return this MultipartFile adapted to the Resource contract
* @since 5.1
*/
default Resource getResource() {
return new MultipartFileResource(this);
}
/**
* Transfer the received file to the given destination file.
* <p>This may either move the file in the filesystem, copy the file in the
* filesystem, or save memory-held contents to the destination file. If the
* destination file already exists, it will be deleted first.
* <p>If the target file has been moved in the filesystem, this operation
* cannot be invoked again afterwards. Therefore, call this method just once
* in order to work with any storage mechanism.
* <p><b>NOTE:</b> Depending on the underlying provider, temporary storage
* may be container-dependent, including the base directory for relative
* destinations specified here (e.g. with Servlet 3.0 multipart handling).
* For absolute destinations, the target file may get renamed/moved from its
* temporary location or newly copied, even if a temporary copy already exists.
* @param dest the destination file (typically absolute)
* @throws IOException in case of reading or writing errors
* @throws IllegalStateException if the file has already been moved
* in the filesystem and is not available anymore for another transfer
* @see org.apache.commons.fileupload.FileItem#write(File)
* @see javax.servlet.http.Part#write(String)
*/
void transferTo(File dest) throws IOException, IllegalStateException;
/**
* Transfer the received file to the given destination file.
* <p>The default implementation simply copies the file input stream.
* @since 5.1
* @see #getInputStream()
* @see #transferTo(File)
*/
default void transferTo(Path dest) throws IOException, IllegalStateException {
FileCopyUtils.copy(getInputStream(), Files.newOutputStream(dest));
}
}
这是Spring框架中 `MultipartFile` 接口的源代码。这个接口代表了一个在多部分请求中接收到的上传文件。
让我们来看一下其中定义的主要方法:
- `String getName()`: 返回上传文件在多部分表单中的参数名称。
- `String getOriginalFilename()`: 返回客户端文件系统中上传文件的原始文件名。
- `String getContentType()`: 返回上传文件的内容类型。
- `boolean isEmpty()`: 返回上传文件是否为空。
- `long getSize()`: 返回上传文件的大小(字节数)。
- `byte[] getBytes() throws IOException`: 返回上传文件的内容字节数组。
- `InputStream getInputStream() throws IOException`: 返回一个输入流,用于读取上传文件的内容。
- `void transferTo(File dest) throws IOException, IllegalStateException`: 将上传文件传输到指定的目标文件中。
- `Resource getResource()`: 将上传文件转换为Spring的 `Resource` 对象,用于进一步处理文件内容。
`MultipartFile` 接口提供了一系列方法,用于获取上传文件的相关信息和内容,并且提供了将上传文件传输到目标文件的方法,以及获取文件内容的方法。这些方法使得在Spring应用程序中处理文件上传变得更加方便和灵活。
2.实现将课程信息缓存至redis
见后续
3.实现保存课程索引信息
见后续