环境:springboot、mybatis-plus、redisson-starter、hutool
方案说明
提交视频进度,这个接口很高频率。是高频率的写。
这里主要方案如下,
1.提交的进度先到redis进行保存,并且同时发送一个15秒的延时任务。
2.到了15s后,取出这个任务数据,和redis中的数据做比对。
2.1.如果进度数据一样的话,说明用户停止播放了视频,可以写库了。
这就是正常的流程了。
效果

前提代码
sql
-- 创建视频进度表
DROP TABLE IF EXISTS `video_progress`;
CREATE TABLE IF NOT EXISTS `video_progress`
(
`id` BIGINT PRIMARY KEY,
`user_id` BIGINT comment '用户id(关联)',
`video_id` BIGINT comment '视频id(关联)',
`position` DOUBLE comment '当前进度',
`duration` DOUBLE comment '视频总时长',
`is_finish` tinyint(1) default 0 comment '是否完成',
`update_time` datetime null on update CURRENT_TIMESTAMP comment '更新时间'
) comment '视频进度表';
java
@TableName(value ="video_progress")
@Data
public class VideoProgress implements Serializable {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long userId;
private Long videoId;
private Double position;
private Double duration;
private Integer isFinish;
private Date updateTime;
private static final long serialVersionUID = 1L;
}
java
public interface VideoProgressMapper extends BaseMapper<VideoProgress> {
}
java
@Getter
@ToString
public class DelayedTask<T> implements Delayed, Serializable {
private static final long serialVersionUID = 3018458065076640934L;
private final T taskContent;
private final Long triggerTime;
/**
* @param seconds 秒
*/
public DelayedTask(T taskContent, Long seconds) {
this.taskContent = taskContent;
this.triggerTime = System.currentTimeMillis() + seconds * 1000;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(triggerTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return this.triggerTime.compareTo(((DelayedTask) o).triggerTime);
}
}
java
@Data
public class VideoSubmitRequest implements Serializable {
private static final long serialVersionUID = -102757973917094785L;
long videoId;
long userId;
double duration;
double position;
}
核心代码
java
@Slf4j
@SpringBootApplication
@RestController
@RequestMapping("/")
@RequiredArgsConstructor
@MapperScan
public class VideoProgressApplication {
private final StringRedisTemplate stringRedisTemplate;
private final String VIDEO_PROGRESS_KEY = "video:progress";
private final RedissonClient redissonClient;
@PostMapping("/submit")
public void submit(@RequestBody VideoSubmitRequest videoSubmitRequest) {
// 1.校验参数后,直接写入到redis中
var key = VIDEO_PROGRESS_KEY + ":" + videoSubmitRequest.userId;
var value = JSONUtil.toJsonStr(videoSubmitRequest);
var hashKey = String.valueOf(videoSubmitRequest.videoId);
stringRedisTemplate.opsForHash().put(key, hashKey, value);
// 这里要设置过期时间,但是总是报StackOverFlow,为啥啊。
// stringRedisTemplate.opsForHash().expire(key, Duration.ofSeconds(15), Collections.singletonList(hashKey));
// 2.发布一个延时任务15s
RBlockingDeque<DelayedTask<VideoSubmitRequest>> blockingDeque = redissonClient.getBlockingDeque("video-progress-delay-queue");
RDelayedQueue<DelayedTask<VideoSubmitRequest>> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
delayedQueue.offer(new DelayedTask<>(videoSubmitRequest, 15L),15, TimeUnit.SECONDS);
log.info("发送延时任务,{}",value);
}
public static void main(String[] args) {
SpringApplication.run(VideoProgressApplication.class, args);
}
}
java
@Component
@RequiredArgsConstructor
@Slf4j
public class DelayQueueHandler implements CommandLineRunner {
private final RedissonClient redissonClient;
private final StringRedisTemplate stringRedisTemplate;
private final VideoProgressMapper videoProgressMapper;
private RBlockingDeque<DelayedTask<VideoSubmitRequest>> blockingDeque;
private static volatile boolean RUNNING = true;
@Override
public void run(String... args) {
blockingDeque = redissonClient.getBlockingDeque("video-progress-delay-queue");
new Thread(() -> {
while (RUNNING) {
try {
// 1.延时任务结束,需要检查提交的数据和redis中的数据是否一致,如果不一致就可以写库了
var take = blockingDeque.take();
log.info("处理延时任务,{}",take);
var oldProgress = take.getTaskContent();
// 2.取出redis中的数据,做比对
var key = "video:progress:" + oldProgress.getUserId();
var hashKey = oldProgress.getVideoId();
var value = stringRedisTemplate.opsForHash().get(key, String.valueOf(hashKey));
if (value == null) {
continue;
}
// 2.1.从redis中取出最新的数据
var newProgress = JSONUtil.toBean((String) value, VideoSubmitRequest.class);
// 2.2.比对,这里不一致就直接结束
if (!NumberUtil.equals(oldProgress.getPosition(), newProgress.getPosition())) {
continue;
}
// 3.写库
var userId = newProgress.getUserId();
var videoId = newProgress.getVideoId();
var position = newProgress.getPosition();
var duration = newProgress.getDuration();
var videoProgress = videoProgressMapper.selectOne(Wrappers.lambdaQuery(VideoProgress.class)
.eq(VideoProgress::getUserId, userId)
.eq(VideoProgress::getVideoId, videoId));
if (videoProgress == null) {
videoProgress = new VideoProgress();
videoProgress.setUserId(userId);
videoProgress.setVideoId(videoId);
videoProgress.setDuration(duration);
}
videoProgress.setPosition(position);
if (position / duration > 0.8) {
videoProgress.setIsFinish(1);
}
videoProgressMapper.insertOrUpdate(videoProgress);
stringRedisTemplate.opsForHash().delete(key, String.valueOf(hashKey));
} catch (Exception e) {
log.error("延时队列出错:{}", e);
}
}
}).start();
}
@PreDestroy
public void destroy() {
RUNNING = false;
}
}