leniu-java-task

SKILL.md

leniu-tengyun-core 定时任务规范

项目特征

特征 说明
包名前缀 net.xnzn.core.*
JDK 版本 21
任务框架 XXL-Job
任务注解 @XxlJob
租户上下文 TenantContextHolder
租户加载器 TenantLoader
跨租户工具 Executors
Redis工具 RedisUtil
异常类 LeException
国际化 I18n

基础使用

定时任务模板

import com.xxl.job.core.handler.annotation.XxlJob;
import net.xnzn.framework.data.tenant.TenantContextHolder;
import net.xnzn.core.common.loader.TenantLoader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class DataSyncTask {

    @Autowired
    @Lazy
    private DataSyncService dataSyncService;

    /**
     * 数据同步任务
     */
    @XxlJob(value = "dataSyncJob")
    public void syncData() {
        log.info("【定时任务】数据同步任务开始执行");

        try {
            int count = dataSyncService.sync();
            log.info("【定时任务】数据同步任务执行完成,同步数量:{}", count);
        } catch (Exception e) {
            log.error("【定时任务】数据同步任务执行失败", e);
            throw e;  // 重新抛出,XXL-Job会记录失败
        }
    }
}

商户遍历模式

模式一: 直接使用 TenantLoader(最常用)

@Component
@Slf4j
public class AccSubTimeTask {

    @Autowired
    @Lazy
    private TenantLoader tenantLoader;

    @Autowired
    @Lazy
    private AccSubTimeService accSubTimeService;

    /**
     * 补贴定时发放任务
     */
    @XxlJob("accSubTimeSendHandle")
    public void accSubTimeSendHandle() {
        log.info("[补贴定时规则]定时任务,开始时间:{}", LocalDateTime.now());

        // 遍历所有商户
        tenantLoader.listTenant().forEach(merchantId -> {
            try {
                // 设置租户上下文
                TenantContextHolder.setTenantId(merchantId);

                // 并发控制
                this.checkTask(merchantId);

                // 执行业务逻辑
                accSubTimeService.accSubRuleTask();
            } catch (Exception e) {
                log.error("[补贴定时规则]定时任务异常,商家={}", merchantId, e);
            }
        });

        log.info("[补贴定时规则]定时任务,结束时间:{}", LocalDateTime.now());
    }

    /**
     * 并发控制检查
     */
    protected void checkTask(Long tenantId) {
        String key = "yst:task:name:" + tenantId;
        boolean ifFirst = RedisUtil.setNx(key, LeConstants.COMMON_YES, 6);
        if (!ifFirst) {
            throw new LeException(I18n.getMessage("acc_operate_exits_task"));
        }
    }
}

模式二: 使用 Executors 工具类

import net.xnzn.framework.data.executor.Executors;

@Component
@Slf4j
public class CouponTask {

    @Autowired
    @Lazy
    private CouponService couponService;

    /**
     * 餐券状态自动处理
     */
    @XxlJob("couponStateAutoHandler")
    public void couponStateAutoHandler() {
        log.info("[餐券状态]定时任务开始执行");

        // 使用工具类遍历商户
        Executors.doInAllTenant(tenantId -> {
            try {
                TenantContextHolder.setTenantId(tenantId);
                couponService.autoHandleState();
            } catch (Exception e) {
                log.error("[餐券状态]定时任务异常,商家={}", tenantId, e);
            }
        });

        log.info("[餐券状态]定时任务执行完成");
    }
}

模式三: 商户遍历 + 分布式锁

import org.redisson.api.RLock;

@Component
@Slf4j
public class PayTask {

    @Autowired
    @Lazy
    private TenantLoader tenantLoader;

    private static final String LOCK_KEY_PAYING = "pay:paying:lock";

    /**
     * 处理支付中的交易记录
     */
    @XxlJob("payingTradeRecordHandler")
    public void payingTradeRecordHandler() {
        // 获取分布式锁
        RLock lock = RedisUtil.getLock(LOCK_KEY_PAYING);
        if (!lock.tryLock()) {
            log.warn("[支付定时任务]上一个任务进行中");
            return;
        }

        try {
            log.info("[支付定时任务]处理支付中的交易开始执行");

            for (Long tenantId : tenantLoader.listTenant()) {
                try {
                    TenantContextHolder.setTenantId(tenantId);
                    // 处理交易记录
                    tradeRecordService.handlePayingRecords();
                } catch (Exception e) {
                    log.error("[支付定时任务]商户处理异常,tenantId={}", tenantId, e);
                }
            }
        } finally {
            if (lock.isHeldByCurrentThread() && lock.isLocked()) {
                lock.unlock();
            }
        }
    }
}

并发控制

方式一: Redis setNx 简单锁(商户级)

/**
 * 商户级别的并发控制
 */
protected void checkTask(Long tenantId) {
    String key = "yst:task:name:" + tenantId;
    boolean ifFirst = RedisUtil.setNx(key, "1", 6);
    if (!ifFirst) {
        throw new LeException("任务正在执行");
    }
}

方式二: Redisson 分布式锁(全局级)

@XxlJob("globalTaskJob")
public void globalTask() {
    RLock lock = RedisUtil.getLock("task:global:lock");
    if (!lock.tryLock()) {
        log.warn("[全局任务]上一个任务进行中,跳过");
        return;
    }

    try {
        doTask();
    } finally {
        if (lock.isHeldByCurrentThread() && lock.isLocked()) {
            lock.unlock();
        }
    }
}

任务参数

使用任务参数

import com.xxl.job.core.context.XxlJobHelper;

@XxlJob("dataCleanJob")
public void cleanData() {
    // 获取任务参数
    String param = XxlJobHelper.getJobParam();
    log.info("【定时任务】数据清理任务开始执行,参数:{}", param);

    try {
        // 解析参数
        DataCleanParam cleanParam = JacksonUtil.readValue(param, DataCleanParam.class);
        int count = dataService.clean(cleanParam);
        log.info("【定时任务】数据清理任务执行完成,清理数量:{}", count);
    } catch (Exception e) {
        log.error("【定时任务】数据清理任务执行失败", e);
        throw e;
    }
}

任务分片

@XxlJob("userDataSyncJob")
public void syncUserData() {
    // 获取分片参数
    int shardIndex = XxlJobHelper.getShardIndex();  // 当前分片索引
    int shardTotal = XxlJobHelper.getShardTotal();  // 总分片数

    log.info("【定时任务】用户数据同步,分片:{}/{}", shardIndex, shardTotal);

    // 查询当前分片的数据
    List<User> users = userService.listBySharding(shardIndex, shardTotal);
    // 处理数据...
}

最佳实践

1. 依赖注入使用 @Autowired @Lazy

@Component
@Slf4j
public class XxxTask {

    @Autowired
    @Lazy
    private XxxService xxxService;

    @Autowired
    @Lazy
    private TenantLoader tenantLoader;
}

2. 商户遍历必须设置租户上下文

tenantLoader.listTenant().forEach(merchantId -> {
    try {
        // ⚠️ 必须设置租户上下文
        TenantContextHolder.setTenantId(merchantId);
        doSomething();
    } catch (Exception e) {
        log.error("[任务]处理异常,商家={}", merchantId, e);
    }
});

3. 单个商户失败不影响其他商户

// ✅ 推荐
tenantLoader.listTenant().forEach(merchantId -> {
    try {
        TenantContextHolder.setTenantId(merchantId);
        doBusiness();
    } catch (Exception e) {
        log.error("[任务]商户处理异常", e);  // 捕获异常,继续执行
    }
});

// ❌ 避免
for (Long merchantId : tenantLoader.listTenant()) {
    TenantContextHolder.setTenantId(merchantId);
    doBusiness();  // 异常会导致后续商户无法执行
}

4. 任务日志完整

@XxlJob("dataSyncJob")
public void syncData() {
    log.info("[任务名称]定时任务开始执行");
    try {
        int count = doSync();
        log.info("[任务名称]定时任务执行完成,数量:{}", count);
    } catch (Exception e) {
        log.error("[任务名称]定时任务执行失败", e);
        throw e;
    }
}

注意事项

  • scheduled-jobs 技能不同,本技能侧重 XXL-Job 使用规范
  • 商户遍历任务必须设置 TenantContextHolder.setTenantId()
  • 注意控制任务并发,避免重复执行
Weekly Installs
2
GitHub Stars
8
First Seen
7 days ago
Installed on
amp2
cline2
opencode2
cursor2
kimi-cli2
codex2