需求: 某系统在启动或重启时需将未执行任务加载至 Redis 缓存中, 周期或定时执行缓存中的任务. 当用户增加新任务时 需要把新的周期任务或定时任务添加进 Redis 缓存, 当用户删除任务需要同时删除 Redis 缓存中的数据
(最先的方案是 只用 java 内存 不用 Redis , 但是此种方案存在高延时的风险, 即当用户删除任务同时要清空内存中的任务数据时, 如果线程在执行大批量任务命令 用户此时删除任务 用户会一直等待直到线程执行完毕
解决方案 使用俩个缓存 Redis+java 内存 hashmap<String,model> 首先库中未执行任务先缓存到 Redis 中 然后 java 内存 hashmap<String,model > 读取 reids 中的数据 线程循环 java 内存 hashmap<String,model > 中的数据 即使用户删除某些任务更新的是 Redis 中的数据 不会影响 java 内存 hashmap<String,model > 中命令下发 同时给 Redis 加锁 (存在用户和 java 内存 hashmap<String,model > 并行操作 Redis 情况)
但是这样的方案有个弊端 本就珍贵的内存中 我们存了双份的数据 一份是 Redis 数据一份是 java 内存数据 , 所以只用一份内存来处理是最高效的, 用 ConcurrentHashMap 来处理并发问题
ConcurrentHashMap 的优势在于 不会锁住整个对象 它的锁的粒度是 key 这样在对象层次上不存在锁 不会发生线程阻塞
设计结构:
初始化 ->Redis->java 内存 -> 在 java 内存中处理所有待执行任务
1: 初始化加载任务至 Redis
- @PostConstruct
- public void init() {
- try {
- // 初始化或重启服务 缓存待执行任务到 Redis 里
- ArrayList<TaskModel> a=taskMapper.getTasks();
- for (int i=0;i<a.size();i++){
初始化存 Redis
- redisUtil.set("task"+a.get(i).getTaskId(),a.get(i));
- }
- System.err.println("待执行任务加载到缓存中"+a.size()+"条");
- } catch (Exception e) {
- // 错误输出
- e.printStackTrace();
- }
- }
- View Code
2: 定时任务将 Redis 中数据专程 map 存储在 Redis 中
3: 用户删除 Redis 数据
删除 Redis 缓存
- redisUtil.del("task"+taskId);
- View Code
4: 概要设计流程图
5: 优化成只用 java 内存来处理 去掉 Redis 层 用 ConcurrentHashMap 做内存保存数据
任务工具类
- public class TaskUtil {
- /**
- * 定时任务的 java 缓存 key: 任务 id value: 任务对象
- */
- public static ConcurrentHashMap<String, TaskModel> hashMap=new ConcurrentHashMap<>();
- public static ConcurrentHashMap<String, TaskModel> getHashMap() {
- return hashMap;
- }
- public static void setHashMap(ConcurrentHashMap<String, TaskModel> hashMap) {
- TaskUtil.hashMap = hashMap;
- }
- }
- View Code
初始化加载数据至 ConcurrentHashMap 中
- @PostConstruct
- public void init() {
- try {
- // 初始化或重启服务 缓存待执行任务到 Redis 里
- ArrayList<TaskModel> a=taskMapper.getTasks();
- for (int i=0;i<a.size();i++){
- // 初始化存 java 内存
- TaskUtil.getHashMap().put(a.get(i).getTaskId(),a.get(i));
- }
- System.err.println("待执行任务加载到缓存中"+a.size()+"条");
- } catch (Exception e) {
- // 错误输出
- e.printStackTrace();
- }
- }
- View Code
开一个线程在内存中处理任务数据
(之前开发用的是 java 的 Timer 类 是一个线程处理一个任务, 如果任务有上万了那就要开上万个线程 系统会崩溃 优化方案是只用一个线程就处理所有任务 用的是 springboot 的 @Scheduled 注解, 此注解默认开的是一个线程)
- @Scheduled(cron = "0 0/5 * * * ?")
- @RequestMapping("/taskM")
- public void taskCron() {
- try {
- // 线程开始时间
- Date now=new Date();
- //java 内存
- ConcurrentHashMap<String, TaskModel> taskMap=TaskUtil.getHashMap();
- // 循环 java 内存中的任务
- for (Map.Entry<String,TaskModel> listEntry: taskMap.entrySet()){
- // 任务类型 (0: 立即执行, 1: 周期执行, 2: 定时执行)
- String taskTimeType=listEntry.getValue().getTaskTimeType();
- // 任务开始执行时间
- String startTime=listEntry.getValue().getTaskTimer();
- long btTime= TaskUtil.betweenTime(DateUtil.time4.parse(startTime),now);
- // 周期任务
- if (taskTimeType.equals(TaskUtil.TASK_1)){
- long count=TaskUtil.getTask2Count(btTime,TaskUtil.PERIOD);
- if (count==0){
- // 执行命令
- this.doCmd(listEntry,startTime,now);
- }else if (count<0){//start-now<0
- // 补时到当前周期
- startTime=TaskUtil.backTime(startTime,now,listEntry.getValue().getTaskTimerPeriod());
- long btTimeBack= TaskUtil.betweenTime(DateUtil.time4.parse(startTime),now);
- long countBack=TaskUtil.getTask2Count(btTimeBack,TaskUtil.PERIOD);
- if (countBack==0){
- // 执行命令
- this.doCmd(listEntry,startTime,now);
- }else {
- // 将下一次执行时间更新至内存
- String takId=listEntry.getValue().getTaskId();// 任务 id
- // 补时到下一个周期
- startTime=TaskUtil.backTime2(startTime,now,listEntry.getValue().getTaskTimerPeriod());
- if (TaskUtil.getHashMap().containsKey(takId)){
- TaskUtil.getHashMap().get(takId).setTaskTimer(startTime);
- }
- }
- }
- }
- // 定时任务
- else if (taskTimeType.equals(TaskUtil.TASK_2)){
- long count=TaskUtil.getTask2Count(btTime,TaskUtil.PERIOD);
- // 倒计时器为 0 时 开始执行定时任务 错过执行时间周期则不执行 等待用户重新添加定时任务
- if (count==0){
- // 执行命令
- this.doCmd(listEntry,startTime,now);
- }
- }
- }
- }catch (Exception e){
- e.printStackTrace();
- }
- }
- View Code
来源: http://www.bubuko.com/infodetail-3344366.html