当业务需要对数据库进行频繁提交时,对数据库会产生压力。比如一个视频播放网站需要每隔几秒就记录用户观看的进度,就要反复向数据库update播放进度,会导致效率低下。
一个优化方案是使用java的DelayQueue,DelayQueue是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即队头对象的延迟到期时间最长。
如果一段时间内内有多个任务需要按照顺序被执行,可以将任务添加到队列中,然后按照任务的延迟时间排队执行任务。
使用场景:1. 淘宝订单业务:下单之后如果三十分钟之内没有付款就自动取消订单。
2. 饿了吗订餐通知:下单成功后60s之后给用户发送短信通知。
3.缓存。缓存中的对象,超过了空闲时间,需要从缓存中移出。
....
DelayQueue优化视频网站记录用户播放进度的思路:每次需要记录用户的播放进度时,现将当前进度的数据记录到redis而不是直接放入数据库,保证redis永远保存最新的播放进度数据,同时将当前的进度放入队列中,并设定等待时间为20毫秒。当到达等待时间后,将队列中的进度拿出并对比redis记录的最新播放进度,如果两个记录一致,有一段时间没有向redis提交新记录了,此时可向数据库提交更新,否则说明当前用户仍然在观看视频,无需去数据库更新记录。
由此,避免了对数据库反复提交和更新的操作,优化了效率。
Java中delayQueue的用法:
首先定义一个Delayed类型的延迟任务类,要能保持任务数据。
测试一下延迟队列的基本使用
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.util.concurrent.DelayQueue;
@Slf4j
class DelayTaskTest {
@Test
void testDelayQueue() throws InterruptedException {
// 1.初始化延迟队列
DelayQueue<DelayTask<String>> queue = new DelayQueue<>();
// 2.向队列中添加延迟执行的任务
log.info("开始初始化延迟任务。。。。");
queue.add(new DelayTask<>("延迟任务3", Duration.ofSeconds(3)));
queue.add(new DelayTask<>("延迟任务1", Duration.ofSeconds(1)));
queue.add(new DelayTask<>("延迟任务2", Duration.ofSeconds(2)));
// 3.尝试执行任务
while (true) {
DelayTask<String> task = queue.take();
log.info("开始执行延迟任务:{}", task.getData());
}
}
}
复制
上面提到的视频网站记录播放进度的案例:
当视频播放每间隔一段时间时,前端调用以下代码块中addLearningRecordTask方法,向队列和redis中写入播放进度数据;然后会持续运行的handleDelayTask会取出redis和延迟队列中的播放进度对比是否一致来确定是否将新的播放进度更新到数据库。
@Slf4j
@Component
@RequiredArgsConstructor
public class LearningRecordDelayTaskHandler {
private final StringRedisTemplate redisTemplate;
private final LearningRecordMapper recordMapper;
private final ILearningLessonService lessonService;
private final DelayQueue<DelayTask<RecordTaskData>> queue = new DelayQueue<>();
private final static String RECORD_KEY_TEMPLATE = "learning:record:{}";
private static volatile boolean begin = true;
@PostConstruct
public void init(){
CompletableFuture.runAsync(this::handleDelayTask);
}
@PreDestroy
public void destroy(){
begin = false;
log.debug("延迟任务停止执行!");
}
public void handleDelayTask(){
while (begin) {
try {
// 1.获取到期的延迟任务
DelayTask<RecordTaskData> task = queue.take();
RecordTaskData data = task.getData();
// 2.查询Redis缓存
LearningRecord record = readRecordCache(data.getLessonId(), data.getSectionId());
if (record == null) {
continue;
}
// 3.比较数据,moment值
if(!Objects.equals(data.getMoment(), record.getMoment())) {
// 不一致,说明用户还在持续提交播放进度,放弃旧数据
continue;
}
// 4.一致,持久化播放进度数据到数据库
// 4.1.更新学习记录的moment
record.setFinished(null);
recordMapper.updateById(record);
// 4.2.更新课表最近学习信息
LearningLesson lesson = new LearningLesson();
lesson.setId(data.getLessonId());
lesson.setLatestSectionId(data.getSectionId());
lesson.setLatestLearnTime(LocalDateTime.now());
lessonService.updateById(lesson);
} catch (Exception e) {
log.error("处理延迟任务发生异常", e);
}
}
}
public void addLearningRecordTask(LearningRecord record){
// 1.添加数据到Redis缓存
writeRecordCache(record);
// 2.提交延迟任务到延迟队列 DelayQueue
queue.add(new DelayTask<>(new RecordTaskData(record), Duration.ofSeconds(20)));
}
public void writeRecordCache(LearningRecord record) {
log.debug("更新学习记录的缓存数据");
try {
// 1.数据转换
String json = JsonUtils.toJsonStr(new RecordCacheData(record));
// 2.写入Redis
String key = StringUtils.format(RECORD_KEY_TEMPLATE, record.getLessonId());
redisTemplate.opsForHash().put(key, record.getSectionId().toString(), json);
// 3.添加缓存过期时间
redisTemplate.expire(key, Duration.ofMinutes(1));
} catch (Exception e) {
log.error("更新学习记录缓存异常", e);
}
}
public LearningRecord readRecordCache(Long lessonId, Long sectionId){
try {
// 1.读取Redis数据
String key = StringUtils.format(RECORD_KEY_TEMPLATE, lessonId);
Object cacheData = redisTemplate.opsForHash().get(key, sectionId.toString());
if (cacheData == null) {
return null;
}
// 2.数据检查和转换
return JsonUtils.toBean(cacheData.toString(), LearningRecord.class);
} catch (Exception e) {
log.error("缓存读取异常", e);
return null;
}
}
public void cleanRecordCache(Long lessonId, Long sectionId){
// 删除数据
String key = StringUtils.format(RECORD_KEY_TEMPLATE, lessonId);
redisTemplate.opsForHash().delete(key, sectionId.toString());
}
@Data
@NoArgsConstructor
private static class RecordCacheData{
private Long id;
private Integer moment;
private Boolean finished;
public RecordCacheData(LearningRecord record) {
this.id = record.getId();
this.moment = record.getMoment();
this.finished = record.getFinished();
}
}
@Data
@NoArgsConstructor
private static class RecordTaskData{
private Long lessonId;
private Long sectionId;
private Integer moment;
public RecordTaskData(LearningRecord record) {
this.lessonId = record.getLessonId();
this.sectionId = record.getSectionId();
this.moment = record.getMoment();
}
}
}