2022-05-13 23:41:35
今天在本地实现了部分业务的事务添加、异步线程池的添加以及mybatis二级缓存的添加。由于稳定性未知,仍然需要在本地测试一段时间,具体测试多久呢,可能也不会有多久,两天?或者和博客按tag检索功能实装一起?又或许等actuator监控完成了一起?又或者明天就上?
这次本地版本,对专辑删除、博客添加、删除相片分别添加了如下的一行注解添加事务:
@Transactional(rollbackFor = Exception.class)
我们知道springboot默认的提交策略是自动提交的,也就是一句一提交,这样如果第二句出现了异常,那么第一句由于已经提交无法自动rollback,造成错误的记录产生。比如图片的删除,如果删除了记录后,结果图集数量没有更新那么就会出现错误的数量,直到下次的插入或者删除才有可能正确统计。所以对以上这些模块添加了事务。
对于友链页评论的读取添加了如下的事务注解:
@Transactional(readOnly = true)
评论页除了查询当前分页的评论列表还要查询评论总数用于前端进行分页规划,如果评论总数和当前页不匹配,那么可能出现用户读取下一页时,读到了上一页的最后一条数据等情况发生,所以添加readonly,让读到的内容是在同一时间线上,从而达到数据的一致性。
之前使用线程都是 new Thread(...).start() 这种方式虽然确实能够达到目的,但是创建线程和删除线程零散的话,开销会比较大,所以,给改成了线程池的方式来进行管理和操作。
这里我们使用spring提供的ThreadPoolTaskExecutor来进行线程池的创建:
package top.dreamcenter.dreamcenter.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@EnableAsync
@Configuration
public class AsyncPoolConfig {
@Bean
public Executor asyncThreadPoolTaskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4); // 核心线程池大小
executor.setMaxPoolSize(8); // 最大线程池大小
executor.setQueueCapacity(4); // 队列大小
executor.setKeepAliveSeconds(60); // 存活时间
executor.setRejectedExecutionHandler( // 拒绝策略(采用调用者执行策略)
new ThreadPoolExecutor.CallerRunsPolicy()
);
executor.setWaitForTasksToCompleteOnShutdown(true); // 关闭时等待任务执行结束
return executor;
}
}
解释:这里使用的都是一些线程池最主要的参数,核心大小表示线程池稳定的大小。如果超过了这个大小,那么新的线程会到阻塞队列中等待,如果阻塞队列满了,那么会新建线程,直到数量达到最大线程池大小。倘若后面还有新的线程要来申请,那么会执行拒绝策略,我这里是将其置于调用者执行,默认策略会直接爆出异常,我当然不想那么直接。期间如果超过核心数的空闲线程超过了存活时间,那么它会自动销毁。如果执行线程遇到关闭类指令这里采用的是等待线程执行结束退出。
那么springboot如何创建线程呢?很简单,只需要一个 @EnableAsync 注解(我写在了线程池配置上),以及一个@Async注解于对应的方法上即可。注意!该方法将被代理,所以请一定要和实际业务的类分开来写,否则业务中调用该方法等于调用自己没有被代理的方法,自然无法达成异步。所以一定要新建一个类写该异步方法,然后业务自动注入该类后(代理类),调用对应方法。
将被代理的含异步方法的类核心结构:
package top.dreamcenter.dreamcenter.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import top.dreamcenter.dreamcenter.entity.ServerProperties;
import top.dreamcenter.dreamcenter.mapper.FriendReviewDao;
import top.dreamcenter.dreamcenter.service.AsyncService;
import...
@Service
public class AsyncServiceImpl implements AsyncService {
private final FriendReviewDao friendReviewDao;
...
@Autowired
public AsyncServiceImpl(FriendReviewDao friendReviewDao, ...) {
...
}
@Async("asyncThreadPoolTaskExecutor")
@Override
public void sendMail(FriendReview friendReview){
...
}
}
调用该异步方法的业务类核心结构:
package top.dreamcenter.dreamcenter.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
...
import java.util.List;
@Service
public class FriendReviewServiceImpl implements FriendReviewService {
private final FriendReviewDao friendReviewDao;
private final AsyncService asyncService;
public FriendReviewServiceImpl(FriendReviewDao friendReviewDao, AsyncService asyncService) {
this.friendReviewDao = friendReviewDao;
this.asyncService = asyncService;
}
@Override
public RetResult<Integer> insertReview(FriendReview friendReview) {
// mail module
asyncService.sendMail(friendReview);
// insert data to database
Integer count = friendReviewDao.insertReview(friendReview);
if(count == 1) {
return RetResult.success(count);
}
return RetResult.fail("失败");
}
@Transactional(readOnly = true)
@Override
public RetResult<ObjectNode> selectBySheep(Integer page, Integer size) {
...
}
}
这样就真正的实现了异步线程池的创建与使用。
我们已经知道博客页前端已经做了前端的缓存,之后除了刷新页面更新外,都只会直接从store中取缓存的数据。但是这是前端的缓存策略,后端却并没有启动缓存策略。一开始后端我打算将博客首页数据读入redis,直到下次更新前都可以直接从redis读取该首页数据,从而减少了查库的时间。但是突然想到了一二级缓存这东西,便稍微回顾了一下。
回顾之后发现,这大大的理论架构最终实践在springboot中却非常的简单。
简单说下一二级缓存的区别:
一级缓存:sqlsession会话级别的,多对单次请求多次查询同一数据开启的缓存策略,springboot默认开启的,且不需要配置。
二级缓存:mapper级别,跟着namespace走的,多次请求也只走一次数据库。springboot默认开启,但是如果要运用,需要<cache/>标签加在对应mapper上才能生效。
所以对于springboot,我们要做的很简单,只要给对应mapper的xml加上一个<cache/>标签即可开启二级缓存,一级缓存除了对实时性需求奇高,否则是不需要特意去关闭的。虽然说配置非常简单,但是还是有着非常多的坑在里面的。
其一:所有该mapper使用的entity都需要序列化!包括子查询和连接查询里用到的查询封装实体,全部都需要序列化。顺带一提,java的Integer等包装类的父级Number或者其它都是有序列化的。序列化只需要实现Serializable接口即可。
其二:连接查询时,另外一张表也必须在当前域下,否则就会出现不同步的情况!所以关联的那张表的xml需要配置<cache-ref namespace=""/>,namespace是当前的这张表的域空间。这样才能完成更新的同步(如何一个刷新都会清空缓存)。
基于这些,最终,把博客页和动态页加了二级缓存策略。其它的因为本身数据量就不大而且可能更新比较频繁,所以就没有加二级缓存了。
以上就是今天完成任务的总结。悄悄说一声>人脸分析也做好了哦,具体的实现细节等以后再说吧,嘿嘿。