Browse Source

标签打印定时任务优化

clodop
han\hanst 1 month ago
parent
commit
5ced8878b5
  1. 38
      src/main/java/com/gaotao/modules/base/dao/PrintTaskDao.java
  2. 36
      src/main/java/com/gaotao/modules/base/service/Impl/PrintTaskServiceImpl.java
  3. 2
      src/main/java/com/gaotao/modules/base/service/PrintTaskService.java
  4. 144
      src/main/java/com/gaotao/modules/base/task/PrintTaskScheduler.java

38
src/main/java/com/gaotao/modules/base/dao/PrintTaskDao.java

@ -3,6 +3,7 @@ package com.gaotao.modules.base.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gaotao.modules.base.entity.PrintTask;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@ -16,12 +17,17 @@ import java.util.List;
@Mapper
public interface PrintTaskDao extends BaseMapper<PrintTask> {
/** 仅限制打印队列语句的执行等待,不代表数据库网络通信的硬时限,单位为秒。 */
int QUEUE_SQL_TIMEOUT_SECONDS = 10;
int ARCHIVE_SQL_TIMEOUT_SECONDS = 60;
/**
* 获取待执行的打印任务(使用READPAST避免锁等待)
* 获取待执行的打印任务(使用READPAST跳过行锁,其他等待由语句超时限制)
*
* @param limit 获取数量
* @return 待执行任务列表
*/
@Options(timeout = QUEUE_SQL_TIMEOUT_SECONDS)
@Select("SELECT TOP ${limit} * FROM print_task WITH (READPAST, READCOMMITTEDLOCK) " +
"WHERE task_status = 'PENDING' " +
"ORDER BY created_date ASC")
@ -33,6 +39,7 @@ public interface PrintTaskDao extends BaseMapper<PrintTask> {
* @param limit 获取数量
* @return 站点列表
*/
@Options(timeout = QUEUE_SQL_TIMEOUT_SECONDS)
@Select("SELECT TOP ${limit} " +
"CASE WHEN site IS NULL OR site = '' THEN 'DEFAULT' ELSE site END AS site " +
"FROM print_task WITH (READPAST, READCOMMITTEDLOCK) " +
@ -48,6 +55,7 @@ public interface PrintTaskDao extends BaseMapper<PrintTask> {
* @param limit 获取数量
* @return 待执行任务列表
*/
@Options(timeout = QUEUE_SQL_TIMEOUT_SECONDS)
@Select("SELECT TOP ${limit} * FROM print_task WITH (READPAST, READCOMMITTEDLOCK) " +
"WHERE task_status = 'PENDING' " +
"AND ((#{site} = 'DEFAULT' AND (site IS NULL OR site = '')) OR site = #{site}) " +
@ -55,12 +63,32 @@ public interface PrintTaskDao extends BaseMapper<PrintTask> {
List<PrintTask> selectPendingTasksBySiteWithReadPast(@Param("site") String site,
@Param("limit") int limit);
/** Put tasks abandoned by a crashed server back into the print queue. */
@Update("UPDATE print_task " +
"SET task_status = 'PENDING', started_date = NULL, completed_date = NULL, error_message = NULL " +
/** 领取任务必须确认更新成功,数据库结果不明确时不得发送打印。 */
@Options(timeout = QUEUE_SQL_TIMEOUT_SECONDS)
@Update("UPDATE print_task SET task_status = 'PROCESSING', started_date = #{startedDate} " +
"WHERE id = #{taskId} AND task_status = 'PENDING'")
int markAsProcessing(@Param("taskId") Long taskId, @Param("startedDate") Date startedDate);
/** 只更新仍在执行中的任务,避免覆盖其他终态。 */
@Options(timeout = QUEUE_SQL_TIMEOUT_SECONDS)
@Update("UPDATE print_task SET task_status = 'SUCCESS', completed_date = #{completedDate}, error_message = NULL " +
"WHERE id = #{taskId} AND task_status = 'PROCESSING'")
int markAsSuccess(@Param("taskId") Long taskId, @Param("completedDate") Date completedDate);
@Options(timeout = QUEUE_SQL_TIMEOUT_SECONDS)
@Update("UPDATE print_task SET task_status = 'FAILED', error_message = #{errorMessage}, " +
"completed_date = #{completedDate}, retry_count = COALESCE(retry_count, 0) + 1 " +
"WHERE id = #{taskId} AND task_status = 'PROCESSING'")
int markAsFailed(@Param("taskId") Long taskId, @Param("errorMessage") String errorMessage,
@Param("completedDate") Date completedDate);
/** 遗留任务可能已经出纸,转为失败等待人工核对,不再自动加入打印队列。 */
@Options(timeout = QUEUE_SQL_TIMEOUT_SECONDS)
@Update("UPDATE print_task SET task_status = 'FAILED', completed_date = #{completedDate}, " +
"error_message = N'执行超时或结果未知,可能已出纸,重试前必须人工核对标签及服务日志' " +
"WHERE task_status = 'PROCESSING' " +
"AND (started_date IS NULL OR started_date < #{before})")
int resetStaleProcessingTasks(@Param("before") Date before);
int resetStaleProcessingTasks(@Param("before") Date before, @Param("completedDate") Date completedDate);
/**
* 将已完成的打印任务插入到历史表

36
src/main/java/com/gaotao/modules/base/service/Impl/PrintTaskServiceImpl.java

@ -65,8 +65,7 @@ public class PrintTaskServiceImpl extends ServiceImpl<PrintTaskDao, PrintTask> i
@Override
public List<PrintTask> getPendingTasks(int limit) {
// 使用原生SQL + READPAST提示,避免锁等待
// READPAST: 跳过被其他事务锁定的行,确保不阻塞INSERT操作
// READPAST只跳过行锁,页锁等等待另由打印队列语句超时限制。
return this.baseMapper.selectPendingTasksWithReadPast(limit);
}
@ -83,42 +82,27 @@ public class PrintTaskServiceImpl extends ServiceImpl<PrintTaskDao, PrintTask> i
}
@Override
@Transactional
@Transactional(timeout = PrintTaskDao.QUEUE_SQL_TIMEOUT_SECONDS)
public int resetStaleProcessingTasks(Date before) {
return this.baseMapper.resetStaleProcessingTasks(before);
return this.baseMapper.resetStaleProcessingTasks(before, new Date());
}
@Override
@Transactional
@Transactional(timeout = PrintTaskDao.QUEUE_SQL_TIMEOUT_SECONDS)
public boolean markAsProcessing(Long taskId) {
return this.lambdaUpdate()
.set(PrintTask::getTaskStatus, "PROCESSING")
.set(PrintTask::getStartedDate, new Date())
.eq(PrintTask::getId, taskId)
.eq(PrintTask::getTaskStatus, "PENDING") // 乐观锁:只更新PENDING状态的任务
.update();
return this.baseMapper.markAsProcessing(taskId, new Date()) == 1;
}
@Override
@Transactional
@Transactional(timeout = PrintTaskDao.QUEUE_SQL_TIMEOUT_SECONDS)
public boolean markAsSuccess(Long taskId) {
return this.lambdaUpdate()
.set(PrintTask::getTaskStatus, "SUCCESS")
.set(PrintTask::getCompletedDate, new Date())
.eq(PrintTask::getId, taskId)
.update();
return this.baseMapper.markAsSuccess(taskId, new Date()) == 1;
}
@Override
@Transactional
@Transactional(timeout = PrintTaskDao.QUEUE_SQL_TIMEOUT_SECONDS)
public boolean markAsFailed(Long taskId, String errorMessage) {
return this.lambdaUpdate()
.set(PrintTask::getTaskStatus, "FAILED")
.set(PrintTask::getErrorMessage, errorMessage)
.set(PrintTask::getCompletedDate, new Date())
.setSql("retry_count = retry_count + 1")
.eq(PrintTask::getId, taskId)
.update();
return this.baseMapper.markAsFailed(taskId, errorMessage, new Date()) == 1;
}
/**
@ -134,7 +118,7 @@ public class PrintTaskServiceImpl extends ServiceImpl<PrintTaskDao, PrintTask> i
* @return 归档的任务数量
*/
@Override
@Transactional
@Transactional(timeout = PrintTaskDao.ARCHIVE_SQL_TIMEOUT_SECONDS)
public int archivePrintTaskHistory() {
log.info("开始归档打印任务历史数据,归档天数: {} 天", ARCHIVE_DAYS);

2
src/main/java/com/gaotao/modules/base/service/PrintTaskService.java

@ -53,7 +53,7 @@ public interface PrintTaskService extends IService<PrintTask> {
*/
List<PrintTask> getPendingTasksBySite(String site, int limit);
/** Requeue PROCESSING tasks left behind by a stopped server. */
/** 将超时遗留的执行中任务转为失败,人工核对后才能重试,不自动重新入队。 */
int resetStaleProcessingTasks(Date before);
/**

144
src/main/java/com/gaotao/modules/base/task/PrintTaskScheduler.java

@ -16,11 +16,15 @@ import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@ -54,7 +58,7 @@ public class PrintTaskScheduler {
private static final Gson gson = new Gson();
/**
* 打印调度总开关(未配置时默认开启)
* 打印调度总开关(未配置时默认关闭)
*/
@Value("${dashboard.print.enabled:false}")
private boolean dashboardPushEnabled;
@ -84,16 +88,20 @@ public class PrintTaskScheduler {
private int printerConnectTimeoutMs;
/**
* 打印机读写超时(毫秒,未配置时默认10000)
* 打印机读取超时(不限制写入,毫秒,未配置时默认10000)
*/
@Value("${print.scheduler.printer-read-timeout-ms:10000}")
private int printerReadTimeoutMs;
/** Maximum age of a PROCESSING task before it is considered abandoned. */
/** 单次打印发送的总时限,包含全部份数及RFID等待时间。 */
@Value("${print.scheduler.printer-task-timeout-ms:60000}")
private int printerTaskTimeoutMs = 60000;
/** 执行中任务超过该时长后视为异常遗留任务。 */
@Value("${print.scheduler.stale-processing-timeout-minutes:30}")
private int staleProcessingTimeoutMinutes;
/** How often abandoned PROCESSING tasks are recovered. */
/** 异常遗留任务的恢复检查间隔。 */
@Value("${print.scheduler.recovery-interval-ms:60000}")
private long recoveryIntervalMs;
@ -101,6 +109,8 @@ public class PrintTaskScheduler {
private static final AtomicLong LAST_IDLE_LOG_AT = new AtomicLong(0);
private final Map<String, AtomicBoolean> siteProcessingFlags = new ConcurrentHashMap<>();
private ExecutorService siteTaskExecutor;
private ScheduledThreadPoolExecutor printerTimeoutExecutor;
private final Set<Socket> activePrinterSockets = ConcurrentHashMap.newKeySet();
@PostConstruct
public void initSiteExecutor() {
@ -122,6 +132,17 @@ public class PrintTaskScheduler {
recoveryIntervalMs = 60000;
}
if (printerTaskTimeoutMs <= 0) {
log.warn("打印任务超时配置无效,使用60000毫秒,原配置={}", printerTaskTimeoutMs);
printerTaskTimeoutMs = 60000;
}
printerTimeoutExecutor = new ScheduledThreadPoolExecutor(1, runnable -> {
Thread thread = new Thread(runnable, "print-socket-timeout");
thread.setDaemon(true);
return thread;
});
printerTimeoutExecutor.setRemoveOnCancelPolicy(true);
int actualThreadPoolSize = Math.max(1, siteThreadPoolSize);
if (actualThreadPoolSize != siteThreadPoolSize) {
log.warn("打印站点线程池配置无效,使用默认最小值1,原配置={}", siteThreadPoolSize);
@ -144,6 +165,10 @@ public class PrintTaskScheduler {
@PreDestroy
public void shutdownSiteExecutor() {
if (printerTimeoutExecutor != null) {
printerTimeoutExecutor.shutdownNow();
}
activePrinterSockets.forEach(this::closePrinterSocket);
if (siteTaskExecutor != null) {
siteTaskExecutor.shutdown();
log.info("打印任务站点线程池已关闭");
@ -204,11 +229,11 @@ public class PrintTaskScheduler {
}
} catch (Exception e) {
log.error("打印任务调度器执行异常: {}", e.getMessage(), e);
log.error("打印任务调度器执行异常,阶段=查询待打印站点: {}", e.getMessage(), e);
}
}
// Recover tasks left in PROCESSING after a server crash or forced restart.
// 遗留任务可能已经出纸,只转为失败待核对,不自动重打。
@Scheduled(fixedDelayString = "${print.scheduler.recovery-interval-ms:60000}",
initialDelayString = "${print.scheduler.recovery-initial-delay-ms:5000}",
scheduler = "printQueueTaskScheduler")
@ -222,11 +247,11 @@ public class PrintTaskScheduler {
Date before = new Date(System.currentTimeMillis() - timeoutMillis);
int recovered = printTaskService.resetStaleProcessingTasks(before);
if (recovered > 0) {
log.warn("Recovered {} stale PROCESSING print tasks older than {} minutes",
log.warn("已将 {} 个超过 {} 分钟的执行中打印任务转为失败,可能已出纸,重试前必须人工核对",
recovered, staleProcessingTimeoutMinutes);
}
} catch (Exception e) {
log.error("Failed to recover stale PROCESSING print tasks", e);
log.error("打印队列遗留任务处理失败,阶段=超时任务转失败", e);
}
}
@ -234,6 +259,7 @@ public class PrintTaskScheduler {
* 处理指定站点的打印任务(站内串行)
*/
private void processSiteTasks(String site, AtomicBoolean siteProcessingFlag) {
String stage = "查询站点待打印任务";
try {
List<PrintTask> pendingTasks = printTaskService.getPendingTasksBySite(site, siteBatchSize);
if (pendingTasks.isEmpty()) {
@ -242,11 +268,12 @@ public class PrintTaskScheduler {
log.info("【打印队列】site={} 发现 {} 个待执行任务", site, pendingTasks.size());
stage = "处理站点任务";
for (PrintTask task : pendingTasks) {
processTask(task);
}
} catch (Exception e) {
log.error("【打印队列】site任务执行异常 site={}, error={}", site, e.getMessage(), e);
log.error("【打印队列】退出本轮站点任务 site={}, 阶段={}, error={}", site, stage, e.getMessage(), e);
} finally {
siteProcessingFlag.set(false);
}
@ -259,6 +286,9 @@ public class PrintTaskScheduler {
Long taskId = task.getId();
String site = task.getSite();
String printerIp = task.getPrinterIp();
String stage = "领取任务";
boolean claimed = false;
boolean updatingStatus = false;
try {
// 1. 标记为执行中(乐观锁,防止并发重复执行)
@ -268,6 +298,8 @@ public class PrintTaskScheduler {
return;
}
claimed = true;
stage = "解析标签数据";
log.info("【打印队列】开始执行任务 taskId={}, site={}, 打印机={}, 份数={}",
taskId, site, printerIp, task.getCopies());
@ -278,6 +310,7 @@ public class PrintTaskScheduler {
}
// 3. 执行打印
stage = "发送打印数据";
boolean success = executePrint(
printerIp,
task.getZplCode(),
@ -286,18 +319,36 @@ public class PrintTaskScheduler {
labelData
);
// 4. 更新任务状态
// 状态回写异常时结果可能未知,不能再写失败覆盖已提交的成功结果。
updatingStatus = true;
stage = success ? "发送完成后回写成功" : "发送失败后回写失败";
if (success) {
printTaskService.markAsSuccess(taskId);
if (!printTaskService.markAsSuccess(taskId)) {
throw new IllegalStateException("成功状态未更新,可能已出纸,需人工核对");
}
log.info("【打印队列】✓ 任务完成 taskId={}, site={}, 打印机={}", taskId, site, printerIp);
} else {
printTaskService.markAsFailed(taskId, "打印失败,未返回成功标识");
if (!printTaskService.markAsFailed(taskId, "打印发送失败或超时,可能已部分出纸,重试前请核对标签及服务日志")) {
throw new IllegalStateException("失败状态未更新,需人工核对");
}
log.error("【打印队列】✗ 任务失败 taskId={}, site={}, 打印机={}", taskId, site, printerIp);
}
} catch (Exception e) {
log.error("【打印队列】✗ 任务执行异常 taskId={}, site={}, 错误: {}", taskId, site, e.getMessage(), e);
printTaskService.markAsFailed(taskId, "执行异常: " + e.getMessage());
log.error("【打印队列】任务异常 taskId={}, site={}, 阶段={}, 错误={}",
taskId, site, stage, e.getMessage(), e);
// 领取结果不确定时不打印、不改失败;回写结果不确定时停止本批次,由遗留任务检查兜底。
if (!claimed || updatingStatus) {
throw new IllegalStateException("数据库操作未确认,停止当前站点批次,taskId=" + taskId, e);
}
try {
if (!printTaskService.markAsFailed(taskId, "执行异常,重试前请核对: " + e.getMessage())) {
throw new IllegalStateException("异常任务的失败状态未更新");
}
} catch (Exception updateEx) {
log.error("【打印队列】异常后回写失败 taskId={}, site={}, 阶段=异常回写失败", taskId, site, updateEx);
throw new IllegalStateException("失败状态回写未确认,停止当前站点批次,taskId=" + taskId, updateEx);
}
}
}
@ -314,45 +365,60 @@ public class PrintTaskScheduler {
*/
private boolean executePrint(String printerIp, String zplCode, Integer copies,
String rfidFlag, Map<String, Object> labelData) {
Socket socket = null;
Socket socket = new Socket();
ScheduledFuture<?> timeoutFuture = null;
AtomicBoolean timedOut = new AtomicBoolean(false);
try {
// 1. 连接打印机
socket = new Socket();
activePrinterSockets.add(socket);
// 保持原站点线程串行发送,只用独立计时器关连接,不另开线程接管任务。
// 先注册超时再连接,避免写入阻塞;取消成功任务的计时器,防止队列累积。
timeoutFuture = printerTimeoutExecutor.schedule(() -> {
timedOut.set(true);
closePrinterSocket(socket);
log.error("打印发送超过{}毫秒,已关闭连接,可能部分出纸,重试前请核对: printerIp={}",
printerTaskTimeoutMs, printerIp);
}, printerTaskTimeoutMs, TimeUnit.MILLISECONDS);
if (zplCode == null || zplCode.isBlank()) {
throw new IllegalArgumentException("打印内容为空");
}
socket.connect(new InetSocketAddress(printerIp, 9100), printerConnectTimeoutMs);
socket.setSoTimeout(printerReadTimeoutMs);
OutputStream os = socket.getOutputStream();
int actualCopies = copies != null && copies > 0 ? copies : 1;
// 2. 直接发送ZPL代码(已在入队时组装好,包括RFID指令)
log.debug("开始发送ZPL到打印机 {}, 份数: {}, RFID: {}", printerIp, actualCopies, rfidFlag);
byte[] payload = zplCode.getBytes(StandardCharsets.UTF_8);
for (int i = 0; i < actualCopies; i++) {
os.write(zplCode.getBytes(StandardCharsets.UTF_8));
if (timedOut.get() || socket.isClosed() || Thread.currentThread().isInterrupted()) {
return false;
}
os.write(payload);
os.flush();
// RFID标签需要等待处理时间
if ("Y".equals(rfidFlag)) {
Thread.sleep(1000); // RFID标签等待1秒
log.debug("RFID标签打印完成 {} / {}", i + 1, actualCopies);
Thread.sleep(1000); // 保留原有RFID间隔,超时后最多等待本次间隔结束。
}
}
log.debug("✓ ZPL已发送到打印机 {}, 份数: {}", printerIp, actualCopies);
if (timedOut.get() || socket.isClosed()) return false;
log.debug("ZPL已发送到打印机 {}, 份数: {}(不代表实际出纸确认)", printerIp, actualCopies);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("打印发送被中断: printerIp={}", printerIp);
return false;
} catch (Exception e) {
log.error("发送ZPL到打印机失败: printerIp={}, error={}", printerIp, e.getMessage(), e);
log.error("发送ZPL失败: printerIp={}, timedOut={}, error={}", printerIp, timedOut.get(), e.getMessage(), e);
return false;
} finally {
if (socket != null) {
try {
socket.close();
} catch (Exception e) {
log.warn("关闭socket失败: {}", e.getMessage());
}
}
if (timeoutFuture != null) timeoutFuture.cancel(false);
closePrinterSocket(socket);
activePrinterSockets.remove(socket);
}
}
/** 关闭连接会解除阻塞的网络写入,让原工作线程正常退出并释放站点占用。 */
private void closePrinterSocket(Socket socket) {
try {
socket.close();
} catch (Exception e) {
log.warn("关闭打印机连接失败: {}", e.getMessage());
}
}

Loading…
Cancel
Save