本文主要记录一下自己对于这个问题的想法和思路,由于时间匆忙有很多不完善的地方欢迎指正。
一般情况下,这样的场景线程的任务肯定全部没有了,程序重启的时候这些任务也没有办法重做了。因此我们需要借助其他的方式来重做这些丢失的任务。大体的思路是日志+定时器的方式。
处理的大体流程:
1) 提交线程池之前,记录数据库日志,设置标志位1(初始化状态)
2) 进入线程执行的时候,设置该日志的标志位2(运行中的状态)
3) 业务逻辑处理和删除日志信息,这两个步骤必须在一个事务中,必须同时成功。业务逻辑的处理需要设计成幂等操作,极端情况下任务可能被重复处理。
4) 定时器,每隔一分钟运行一次,首先查询日志记录中状态等于2并且当前时间大于超时时间(过期时间=设置状态为2的时间+两分钟,我们认为当前任务在两分钟内没有被处理完的时候,遇到了未知的情况,他本身不能正常处理,需要重新提交任务,正常情况下一个任务不大可能需要执行两分钟),将这些任务的状态设置为1。最好查询所有的状态=1并且当前时间大于超时时间的任务重新提交到线程池。
代码片
下面展示一些 内联代码片
。
package com.panda.open.api.gateway.controller;
import com.alibaba.fastjson.JSON;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.panda.open.api.gateway.pojo.TaskDO;
import com.panda.open.api.gateway.pojo.User;
import com.panda.open.api.gateway.service.TaskService;
import com.panda.open.api.gateway.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.*;
/**
* @author: renwei
* @Date:Created on 10:562018/11/12
*/
@RestController
public class UserController {
/**
* 自定义线程池
*/
public static ThreadFactory factory = new ThreadFactoryBuilder().setNameFormat("user-pool-%d").build();
public static Executor poolExecutor = new ThreadPoolExecutor(1, 1, 2,
TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(2), factory, new ThreadPoolExecutor.AbortPolicy());
@Autowired
private UserService userService;
@Autowired
private TaskService taskService;
@RequestMapping(value = "/test", method = RequestMethod.GET)
public void process() {
TaskDO taskDO = null;
try {
User user = new User();
//日志记录,任务提交到线程池
taskDO = new TaskDO();
String json = JSON.toJSONString(user);
taskDO.setJson(json);
//任务初始化状态
taskDO.setStatus(1);
int count = taskService.saveTask(taskDO);
if (count == 0) {
throw new RuntimeException("保存日志信息异常");
}
final Long taskId = taskDO.getId();
poolExecutor.execute(new Runnable() {
@Override
public void run() {
processUserTask(taskId, taskService, userService);
}
});
} catch (RejectedExecutionException e) {
//线程池无法处理新的任务,删除数据库的任务日志信息
System.out.println(e.getMessage());
}
}
public static void processUserTask(Long taskId, TaskService taskService, UserService userService) {
//记录日志信息吗,任务状态设置为2,处理中
TaskDO task = new TaskDO();
task.setId(taskId);
task.setStatus(2);
//设置时间为当前的时间的两分钟后,便于SQL优化
task.setExpiredTime(System.currentTimeMillis() + 2 * 60 * 1000);
taskService.updateTaskStatus(task);
Long userId = 2L;
//处理业务和
userService.updateStatus(userId, taskId);
}
}
UserService.class接口
package com.panda.open.api.gateway.service;
/**
* @author: renwei
* @Date:Created on 10:572018/11/12
*/
public interface UserService {
/**
* 更改用户的状态,并且删除task日志信息
* @param userId
* @param taskId
*/
void updateStatus(Long userId, Long taskId);
}
UserServiceImpl.class接口
package com.panda.open.api.gateway.service.impl;
import com.panda.open.api.gateway.mapper.TaskMapper;
import com.panda.open.api.gateway.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
/**
* @author: renwei
* @Date:Created on 10:572018/11/12
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private TaskMapper taskMapper;
@Transactional(propagation = Propagation.REQUIRED, readOnly = false, rollbackFor = Exception.class)
@Override
public void updateStatus(Long userId, Long taskId) {
try {
System.out.println("修改用户信息,幂等操作:userId=" + userId);
TimeUnit.MILLISECONDS.sleep(500);
taskMapper.deleteByPK(taskId);
} catch (InterruptedException e) {
throw new RuntimeException("位置异常,任务需要重新提交");
}
}
}
TaskService.class
package com.panda.open.api.gateway.service;
import com.panda.open.api.gateway.pojo.TaskDO;
import java.util.List;
/**
* @author: renwei
* @Date:Created on 16:132018/11/12
*/
public interface TaskService {
int saveTask(TaskDO taskDO);
int updateTaskStatus(TaskDO taskDO);
void deleteByPK(long id);
/**
* 获取失败的任务,status=2&System.currentTimeMillis()>expiredTime
*
* @param currentTime
* @return
*/
List<TaskDO> getFailedTask(Long currentTime,Integer status);
/**
* 设置任务为1的初始化状态
* @param list
*/
void batchUpdateForInit(List<Long> list);
}
TaskServiceImpl.class
package com.panda.open.api.gateway.service.impl;
import com.panda.open.api.gateway.mapper.TaskMapper;
import com.panda.open.api.gateway.pojo.TaskDO;
import com.panda.open.api.gateway.service.TaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* @author: renwei
* @Date:Created on 16:142018/11/12
*/
@Service
public class TaskServiceImpl implements TaskService {
@Autowired
private TaskMapper taskMapper;
/**
* 保存提交的任务日志信息
*
* @param taskDO
* @return
*/
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = false, rollbackFor = Exception.class)
public int saveTask(TaskDO taskDO) {
return taskMapper.insert(taskDO);
}
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = false, rollbackFor = Exception.class)
public int updateTaskStatus(TaskDO taskDO) {
return taskMapper.updateTaskStatus(taskDO);
}
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = false, rollbackFor = Exception.class)
public void deleteByPK(long taskId) {
taskMapper.deleteByPK(taskId);
}
@Override
public List<TaskDO> getFailedTask(Long currentTime,Integer status) {
return taskMapper.listFailedTask(currentTime,status);
}
@Override
public void batchUpdateForInit(List<Long> list) {
taskMapper.batchUpdateForInit(list);
}
}
pojo类
package com.panda.open.api.gateway.pojo;
import java.io.Serializable;
/**
* @author: renwei
* @Date:Created on 10:492018/11/12
*/
public class TaskDO implements Serializable {
/**
* 任务ID
*/
private Long id;
/**
* json的字符串
*/
private String json;
/**
* 任务状态,1:任务初始化提交线程池,2:线程池收到任务,正在处理中。
* 业务处理成功的时候删除任务,(业务处理和删除任务必须同时成功,要么多失败)
*/
private Integer status;
/**
* 设置任务状态2的时候设置超时的时间
*/
private Long expiredTime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getJson() {
return json;
}
public void setJson(String json) {
this.json = json;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getExpiredTime() {
return expiredTime;
}
public void setExpiredTime(Long expiredTime) {
this.expiredTime = expiredTime;
}
}
package com.panda.open.api.gateway.pojo;
import java.io.Serializable;
/**
* @author: renwei
* @Date:Created on 10:482018/11/12
*/
public class User implements Serializable {
private Integer id;
private String name;
private Integer status;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
SpringUtils工具类
package com.panda.open.api.gateway.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author: renwei
* @Date:Created on 22:232018/11/12
*/
@Component
public class SpringUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtils.applicationContext = applicationContext;
}
public static <T> T getBean(String beanName) {
if (applicationContext.containsBean(beanName)) {
return (T) applicationContext.getBean(beanName);
} else {
return null;
}
}
public static <T> Map<String, T> getBeansOfType(Class<T> baseType) {
return applicationContext.getBeansOfType(baseType);
}
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
}
ScheduledService.class定时器任务类
package com.panda.open.api.gateway.config;
import com.fasterxml.jackson.databind.annotation.JsonAppend;
import com.panda.open.api.gateway.controller.UserController;
import com.panda.open.api.gateway.pojo.TaskDO;
import com.panda.open.api.gateway.service.TaskService;
import com.panda.open.api.gateway.service.UserService;
import com.panda.open.api.gateway.util.SpringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
/**
* @author: renwei
* @Date:Created on 22:272018/11/12
*/
@Component
public class ScheduledService {
@Autowired
private UserService userService;
@Scheduled(cron = "0 0/1 * * * *")
public void scheduled() {
//定时器任务,每隔一分钟执行异常,如果当前时间和任务提交的时间超过2分钟任务还是2的状态,则任务失败需要重新提交
TaskService taskService = SpringUtils.getBean(TaskService.class);
List<TaskDO> failedTasks = taskService.getFailedTask(System.currentTimeMillis(), 2);
if (!CollectionUtils.isEmpty(failedTasks)) {
//设置这些task的status=1
List<Long> ids = new ArrayList<>();
for (TaskDO taskDO : failedTasks) {
ids.add(taskDO.getId());
}
taskService.batchUpdateForInit(ids);
}
List<TaskDO> initTasks = taskService.getFailedTask(System.currentTimeMillis(), 1);
if (CollectionUtils.isEmpty(initTasks)) {
return;
}
//提交任务
for (TaskDO taskDO : initTasks) {
final Long taskId = taskDO.getId();
UserController.poolExecutor.execute(new Runnable() {
@Override
public void run() {
UserController.processUserTask(taskId, taskService, userService);
}
});
}
}
}
TaskMapper.class
package com.panda.open.api.gateway.mapper;
import com.panda.open.api.gateway.pojo.TaskDO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @author: renwei
* @Date:Created on 10:532018/11/12
*/
public interface TaskMapper {
/**
* 查询task日志信息
*
* @param taskDO
* @return
*/
int insert(TaskDO taskDO);
/**
* 修改task日志信息
*
* @param taskDO
* @return
*/
int updateTaskStatus(TaskDO taskDO);
/**
* 根据主键删除task日志信息
*
* @param taskId
*/
void deleteByPK(Long taskId);
/**
* @param currentTime
* @return
*/
List<TaskDO> listFailedTask(@Param("currentTime") Long currentTime, @Param("status") Integer status);
/**
* @param list
*/
void batchUpdateForInit(List<Long> list);
}
TaskMapper .xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.panda.open.api.gateway.mapper.TaskMapper">
<resultMap id="BaseResultMap" type="com.panda.open.api.gateway.pojo.TaskDO">
<id column="id" property="id"/>
<result column="data" property="json"/>
</resultMap>
<sql id="Base_Column_List">
id, data
</sql>
<insert id="insert" parameterType="com.panda.open.api.gateway.pojo.TaskDO" useGeneratedKeys="true" keyProperty="id">
INSERT INTO rw_test(data,status)VALUES(#{json},#{status});
</insert>
<update id="updateTaskStatus" parameterType="com.panda.open.api.gateway.pojo.TaskDO">
UPDATE rw_test SET status=#{status},time=#{expiredTime} WHERE id=#{id} AND status=1
</update>
<delete id="deleteByPK" parameterType="java.lang.Long">
DELETE FROM rw_test WHERE id=#{id}
</delete>
<!--获取任务-->
<select id="listFailedTask" resultMap="BaseResultMap" resultType="java.lang.Long">
SELECT id,data FROM rw_test WHERE status=#{status} AND <![CDATA[ time < #{currentTime}
]]>
</select>
<!--设置任务为1的状态-->
<update id="batchUpdateForInit" parameterType="java.util.List">
UPDATE rw_test SET status=1 WHERE status=2 AND id IN
<foreach collection="list" item="item" index="index" separator="," open="(" close=")">
#{item}
</foreach>
</update>
</mapper>
解释:这里的位置设置的超时时间System.currentTimeMillis() + 2 * 60 * 1000,一开始写入的是System.currentTimeMillis()
我们分析两种不同的写法下如何判断如何是否超时
原始的写法查询超时任务的SQL:SELECT * FROM rw_test WHERE status
=2 AND 20-time>2。
我们看一下这个SQL的查询效率EXPLAIN SELECT * FROM rw_test WHERE status
=2 AND 20-time>2
Explain中type:这是重要的列,显示连接使用了何种类型。从最好到最差的连接类型为const、eq_reg、ref、range、index和ALL。type显示的是访问类型,是较为重要的一个指标,结果值从好到坏依次是:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL。一般来说,得保证查询至少达到range级别,最好能达到ref。
从explain的结果来看type=ALL是效率最差的。
优化后的SQL语句:SELECT * FROM rw_test WHERE status
=2 AND 20>time,这里的time=设置任务状态=2的时间+两分钟。我们explain一下看看有没有效果。
优化后的type=range,还是有效果的。
根据自己的一些想法写了一个简单的demo程序,不合理之处,请各位大侠批评指正。