【架构实战】任务调度XXL-JOB:定时任务的正确姿势

【架构实战】任务调度XXL-JOB:定时任务的正确姿势

XXL-JOB架构、分片任务、失败重试、实战案例


一、从一个真实的故事说起

2024年某支付公司对账系统出现严重问题------连续3天的交易记录没有对账。

排查后发现,对账任务每天凌晨2点执行,但3天前的凌晨2点,数据库正在做慢查询优化,导致对账任务执行超时。更诡异的是,任务显示"执行成功",但实际没有任何对账记录生成。

"我们不是配置了超时告警吗?为什么没有收到告警?"

"告警是配置了,但任务执行器在超时后直接kill掉了线程,异常没有被正确捕获,告警逻辑根本没有执行。而且由于任务状态被标记为'成功',第二天、第三天的任务也不会重新执行。"

"那我们不是配置了失败重试吗?"

"失败重试只在任务抛出异常时触发,超时kill线程不会触发重试。"

这个故事告诉我们:定时任务不是简单的"到点执行",还需要考虑超时处理、失败重试、幂等性、监控告警等多个维度。


二、XXL-JOB架构解析

2.1 整体架构

XXL-JOB采用"调度中心+执行器"的架构:

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      调度中心(Admin)                       │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐              │
│  │ 任务管理  │  │ 调度管理  │  │ 日志管理  │              │
│  └───────────┘  └───────────┘  └───────────┘              │
│         │              │              │                     │
│         └──────────────┼──────────────┘                     │
│                        │                                    │
│              ┌─────────▼─────────┐                         │
│              │   调度触发器      │                         │
│              │  (Quartz集群)     │                         │
│              └─────────┬─────────┘                         │
└────────────────────────┼───────────────────────────────────┘
                         │ HTTP触发
         ┌───────────────┼───────────────┐
         │               │               │
    ┌────▼────┐     ┌────▼────┐     ┌────▼────┐
    │ 执行器1 │     │ 执行器2 │     │ 执行器3 │
    │         │     │         │     │         │
    │ JobHandler1    │ JobHandler2    │ JobHandler3
    │ JobHandler2    │ JobHandler3    │ JobHandler1
    └─────────┘     └─────────┘     └─────────┘

调度中心职责

  1. 任务管理:任务的CRUD操作
  2. 调度管理:基于Quartz的调度触发
  3. 日志管理:任务执行日志的查询和管理
  4. 执行器管理:执行器的注册和心跳检测

执行器职责

  1. 任务执行:接收调度请求,执行具体的任务逻辑
  2. 结果回调:将执行结果回调给调度中心
  3. 日志记录:记录任务执行的详细日志

2.2 核心流程

复制代码
1. 调度中心根据cron表达式触发任务
2. 调度中心选择一个执行器(路由策略)
3. 调度中心通过HTTP请求触发执行器
4. 执行器将任务放入任务队列
5. 执行器线程池执行任务
6. 执行器回调结果给调度中心
7. 调度中心记录执行日志

2.3 核心配置

调度中心配置

properties 复制代码
# application.properties
server.port=8080

# 数据库配置(存储任务信息和执行日志)
spring.datasource.url=jdbc:mysql://localhost:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root

# 调度中心集群配置
xxl.job.accessToken=your_token

执行器配置

properties 复制代码
# application.properties
server.port=8081

# 执行器配置
xxl.job.admin.addresses=http://localhost:8080/xxl-job-admin
xxl.job.executor.appname=xxl-job-executor-sample
xxl.job.executor.ip=
xxl.job.executor.port=9999
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
xxl.job.executor.logretentiondays=30
xxl.job.accessToken=your_token

三、任务类型与使用

3.1 BEAN模式(推荐)

BEAN模式是最常用的任务类型,任务逻辑写在Spring Bean中:

java 复制代码
@Component
public class SampleJob {
    
    @XxlJob("demoJob")
    public void demoJob() {
        XxlJobHelper.log("开始执行任务...");
        
        // 业务逻辑
        doBusiness();
        
        XxlJobHelper.log("任务执行完成");
    }
    
    private void doBusiness() {
        // 具体业务逻辑
    }
}

获取任务参数

java 复制代码
@XxlJob("paramJob")
public void paramJob() {
    // 获取任务参数
    String param = XxlJobHelper.getJobParam();
    
    // 解析参数
    JSONObject config = JSON.parseObject(param);
    String startDate = config.getString("startDate");
    String endDate = config.getString("endDate");
    
    XxlJobHelper.log("参数: startDate={}, endDate={}", startDate, endDate);
}

3.2 GLUE模式

GLUE模式允许在调度中心直接编辑任务代码:

java 复制代码
// GLUE(Java)模式
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.context.XxlJobHelper;

public class DemoGlueJobHandler extends IJobHandler {
    @Override
    public void execute() throws Exception {
        XxlJobHelper.log("GLUE模式任务执行...");
        
        // 业务逻辑
        // ...
    }
}

优点 :无需重启执行器,修改即时生效

缺点:代码在数据库中,版本管理困难,不推荐生产环境使用

3.3 分片任务

分片任务可以将一个大任务拆分为多个小任务,并行执行:

java 复制代码
@Component
public class ShardingJob {
    
    @XxlJob("shardingJob")
    public void shardingJob() {
        // 获取分片参数
        int shardIndex = XxlJobHelper.getShardIndex();  // 当前分片序号
        int shardTotal = XxlJobHelper.getShardTotal();  // 总分片数
        
        XxlJobHelper.log("分片参数: {}/{}", shardIndex, shardTotal);
        
        // 根据分片参数处理数据
        // 例如:处理订单表,按订单ID取模分配
        List<Order> orders = getOrdersBySharding(shardIndex, shardTotal);
        
        for (Order order : orders) {
            processOrder(order);
        }
        
        XxlJobHelper.log("处理完成,共{}条", orders.size());
    }
    
    private List<Order> getOrdersBySharding(int shardIndex, int shardTotal) {
        // SQL: SELECT * FROM orders WHERE id % #{shardTotal} = #{shardIndex}
        return orderMapper.selectBySharding(shardIndex, shardTotal);
    }
}

分片任务配置

在调度中心配置任务时,设置"路由策略"为"分片广播",并配置"分片数量"。


四、高级特性

4.1 失败重试

XXL-JOB支持任务失败后自动重试:

java 复制代码
@XxlJob("retryJob")
public void retryJob() {
    XxlJobHelper.log("执行任务,尝试次数: {}", XxlJobHelper.getRetryCount());
    
    try {
        // 业务逻辑
        doBusiness();
    } catch (Exception e) {
        XxlJobHelper.log("任务执行失败: {}", e.getMessage());
        
        // 抛出异常触发重试
        throw e;
    }
}

配置方式

在调度中心配置任务时,设置"失败重试次数"(如3次)。任务失败后,会自动重试,最多重试3次。

重试间隔

默认立即重试,可以通过自定义实现延迟重试:

java 复制代码
@XxlJob("delayRetryJob")
public void delayRetryJob() {
    int retryCount = XxlJobHelper.getRetryCount();
    
    if (retryCount > 0) {
        // 延迟重试
        try {
            Thread.sleep(retryCount * 1000L);  // 第1次重试延迟1秒,第2次延迟2秒...
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    // 业务逻辑
    doBusiness();
}

4.2 任务超时

XXL-JOB支持任务超时设置:

java 复制代码
@XxlJob("timeoutJob")
public void timeoutJob() {
    long startTime = System.currentTimeMillis();
    int timeoutSeconds = 60;  // 超时时间60秒
    
    XxlJobHelper.log("任务开始执行,超时时间: {}秒", timeoutSeconds);
    
    try {
        // 分批处理,每批检查超时
        int page = 1;
        int pageSize = 1000;
        
        while (true) {
            // 检查是否超时
            if (System.currentTimeMillis() - startTime > timeoutSeconds * 1000L) {
                XxlJobHelper.log("任务超时,停止执行");
                XxlJobHelper.handleFail("任务超时");
                return;
            }
            
            // 处理一批数据
            List<Data> dataList = getDataByPage(page, pageSize);
            if (dataList.isEmpty()) {
                break;
            }
            
            processDataList(dataList);
            page++;
        }
        
        XxlJobHelper.handleSuccess("任务执行成功");
    } catch (Exception e) {
        XxlJobHelper.log("任务执行异常: {}", e.getMessage());
        XxlJobHelper.handleFail(e.getMessage());
    }
}

配置方式

在调度中心配置任务时,设置"任务超时时间"(如60秒)。

4.3 任务依赖(工作流)

XXL-JOB支持任务依赖,实现工作流调度:

复制代码
任务A(数据抽取)
    ↓
任务B(数据清洗)
    ↓
任务C(数据统计)

配置方式

在调度中心配置任务C时,设置"上游任务"为任务B。任务B执行成功后,才会触发任务C执行。

子任务触发

java 复制代码
@XxlJob("parentJob")
public void parentJob() {
    XxlJobHelper.log("父任务执行...");
    
    // 执行业务逻辑
    doBusiness();
    
    // 触发子任务
    XxlJobHelper.handleSuccess("执行成功,触发子任务");
}

4.4 动态调度

XXL-JOB提供API接口,支持动态创建和触发任务:

java 复制代码
@RestController
public class JobController {
    
    @Autowired
    private XxlJobService xxlJobService;
    
    @PostMapping("/addJob")
    public Result addJob(@RequestBody JobInfo jobInfo) {
        // 动态添加任务
        return xxlJobService.add(jobInfo);
    }
    
    @PostMapping("/triggerJob")
    public Result triggerJob(@RequestParam int jobId) {
        // 手动触发任务
        return xxlJobService.trigger(jobId);
    }
    
    @PostMapping("/stopJob")
    public Result stopJob(@RequestParam int jobId) {
        // 停止任务
        return xxlJobService.stop(jobId);
    }
}

五、实战案例:电商订单对账系统

5.1 业务需求

某电商平台需要每天凌晨对账,检查订单系统和支付系统的数据一致性:

  1. 从订单系统获取前一天的所有订单
  2. 从支付系统获取前一天的所有支付记录
  3. 对比订单金额和支付金额,发现差异
  4. 生成对账报告,差异记录发送告警

5.2 架构设计

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      XXL-JOB调度中心                        │
│                                                             │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐  │
│  │ 对账任务      │  │ 告警任务      │  │ 报表任务      │  │
│  │ (分片广播)    │  │ (单机执行)    │  │ (单机执行)    │  │
│  └───────┬───────┘  └───────────────┘  └───────────────┘  │
└──────────┼──────────────────────────────────────────────────┘
           │
    ┌──────▼──────┐
    │ 对账执行器  │
    │             │
    │ 分片1: 处理订单1-10000
    │ 分片2: 处理订单10001-20000
    │ 分片3: 处理订单20001-30000
    └─────────────┘

5.3 代码实现

对账任务

java 复制代码
@Component
public class ReconciliationJob {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private PaymentService paymentService;
    
    @Autowired
    private ReconciliationService reconciliationService;
    
    @Autowired
    private AlertService alertService;
    
    @XxlJob("reconciliationJob")
    public void reconciliationJob() {
        // 获取分片参数
        int shardIndex = XxlJobHelper.getShardIndex();
        int shardTotal = XxlJobHelper.getShardTotal();
        
        // 获取对账日期(默认昨天)
        String param = XxlJobHelper.getJobParam();
        String date = StringUtils.isNotEmpty(param) ? param : getYesterday();
        
        XxlJobHelper.log("开始对账: 日期={}, 分片={}/{}", date, shardIndex, shardTotal);
        
        try {
            // 1. 获取订单数据(按分片)
            List<Order> orders = orderService.getOrdersByDateAndSharding(date, shardIndex, shardTotal);
            XxlJobHelper.log("获取订单{}条", orders.size());
            
            // 2. 获取支付数据(按分片)
            List<Payment> payments = paymentService.getPaymentsByDateAndSharding(date, shardIndex, shardTotal);
            XxlJobHelper.log("获取支付记录{}条", payments.size());
            
            // 3. 对账
            List<ReconciliationResult> results = reconciliationService.reconcile(orders, payments);
            XxlJobHelper.log("对账完成,发现差异{}条", results.size());
            
            // 4. 保存对账结果
            reconciliationService.saveResults(results);
            
            // 5. 发送告警(如果有差异)
            if (!results.isEmpty()) {
                alertService.sendReconciliationAlert(date, results);
            }
            
            XxlJobHelper.handleSuccess("对账完成");
        } catch (Exception e) {
            XxlJobHelper.log("对账异常: {}", e.getMessage(), e);
            XxlJobHelper.handleFail("对账失败: " + e.getMessage());
        }
    }
    
    private String getYesterday() {
        LocalDate yesterday = LocalDate.now().minusDays(1);
        return yesterday.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    }
}

对账服务

java 复制代码
@Service
public class ReconciliationService {
    
    public List<ReconciliationResult> reconcile(List<Order> orders, List<Payment> payments) {
        List<ReconciliationResult> results = new ArrayList<>();
        
        // 构建支付记录Map
        Map<String, Payment> paymentMap = payments.stream()
            .collect(Collectors.toMap(Payment::getOrderId, Function.identity()));
        
        // 对账
        for (Order order : orders) {
            Payment payment = paymentMap.get(order.getId());
            
            if (payment == null) {
                // 支付记录缺失
                results.add(new ReconciliationResult(
                    order.getId(),
                    "PAYMENT_MISSING",
                    order.getAmount(),
                    BigDecimal.ZERO,
                    "支付记录缺失"
                ));
                continue;
            }
            
            if (order.getAmount().compareTo(payment.getAmount()) != 0) {
                // 金额不一致
                results.add(new ReconciliationResult(
                    order.getId(),
                    "AMOUNT_MISMATCH",
                    order.getAmount(),
                    payment.getAmount(),
                    "订单金额与支付金额不一致"
                ));
            }
        }
        
        // 检查是否有订单缺失的支付记录
        Set<String> orderIds = orders.stream()
            .map(Order::getId)
            .collect(Collectors.toSet());
        
        for (Payment payment : payments) {
            if (!orderIds.contains(payment.getOrderId())) {
                results.add(new ReconciliationResult(
                    payment.getOrderId(),
                    "ORDER_MISSING",
                    BigDecimal.ZERO,
                    payment.getAmount(),
                    "订单记录缺失"
                ));
            }
        }
        
        return results;
    }
}

5.4 幂等性保证

对账任务需要保证幂等性,避免重复执行导致数据错误:

java 复制代码
@Service
public class ReconciliationService {
    
    public void saveResults(List<ReconciliationResult> results) {
        // 使用INSERT ON DUPLICATE KEY UPDATE保证幂等性
        for (ReconciliationResult result : results) {
            reconciliationMapper.insertOrUpdate(result);
        }
    }
}

// Mapper
@Mapper
public interface ReconciliationMapper {
    
    @Insert("""
        INSERT INTO reconciliation_result 
        (order_id, date, type, order_amount, payment_amount, message, create_time, update_time)
        VALUES 
        (#{orderId}, #{date}, #{type}, #{orderAmount}, #{paymentAmount}, #{message}, NOW(), NOW())
        ON DUPLICATE KEY UPDATE
        type = #{type},
        order_amount = #{orderAmount},
        payment_amount = #{paymentAmount},
        message = #{message},
        update_time = NOW()
    """)
    void insertOrUpdate(ReconciliationResult result);
}

六、踩坑实录

踩坑一:任务执行超时未正确处理

问题:任务执行超时后,线程被kill,异常未捕获,任务状态错误。

java 复制代码
@XxlJob("timeoutJob")
public void timeoutJob() {
    // 错误:没有检查超时
    while (true) {
        processData();
        // 如果执行时间超过超时时间,线程被kill,异常未捕获
    }
}

解决方案:主动检查超时

java 复制代码
@XxlJob("timeoutJob")
public void timeoutJob() {
    long startTime = System.currentTimeMillis();
    int timeoutSeconds = 60;
    
    while (true) {
        // 检查超时
        if (System.currentTimeMillis() - startTime > timeoutSeconds * 1000L) {
            XxlJobHelper.log("任务超时,停止执行");
            XxlJobHelper.handleFail("任务超时");
            return;
        }
        
        processData();
    }
}

踩坑二:分片任务数据倾斜

问题:分片任务数据分配不均匀,某些分片执行时间过长。

java 复制代码
// 错误:按ID取模分片,但ID分布不均匀
SELECT * FROM orders WHERE id % #{shardTotal} = #{shardIndex}

解决方案:按时间或其他均匀分布的字段分片

java 复制代码
// 方案一:按创建时间分片
SELECT * FROM orders 
WHERE DATE(create_time) = #{date} 
AND HOUR(create_time) % #{shardTotal} = #{shardIndex}

// 方案二:按用户ID分片(假设用户ID分布均匀)
SELECT * FROM orders 
WHERE user_id % #{shardTotal} = #{shardIndex}

踩坑三:任务失败重试导致重复执行

问题:任务失败重试时,已执行的部分逻辑重复执行。

java 复制代码
@XxlJob("retryJob")
public void retryJob() {
    // 错误:没有考虑重试的情况
    List<Order> orders = getOrders();
    for (Order order : orders) {
        processOrder(order);  // 重试时会重复处理已成功的订单
    }
}

解决方案:记录执行进度,重试时跳过已执行的部分

java 复制代码
@XxlJob("retryJob")
public void retryJob() {
    String jobId = XxlJobHelper.getJobId();
    
    // 获取已执行的订单ID
    Set<String> processedOrderIds = getProcessedOrderIds(jobId);
    
    List<Order> orders = getOrders();
    for (Order order : orders) {
        // 跳过已执行的订单
        if (processedOrderIds.contains(order.getId())) {
            continue;
        }
        
        processOrder(order);
        
        // 记录执行进度
        saveProgress(jobId, order.getId());
    }
}

踩坑四:调度中心单点故障

问题:调度中心单机部署,故障后所有任务无法执行。

解决方案:调度中心集群部署

properties 复制代码
# 调度中心集群配置(多个实例连接同一个数据库)
spring.datasource.url=jdbc:mysql://localhost:3306/xxl_job

# Quartz集群配置
spring.quartz.job-store-type=jdbc
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
spring.quartz.properties.org.quartz.jobStore.isClustered=true

踩坑五:执行器内存溢出

问题:任务处理大量数据,导致执行器内存溢出。

java 复制代码
@XxlJob("batchJob")
public void batchJob() {
    // 错误:一次性加载所有数据
    List<Order> orders = orderService.getAllOrders();  // 可能OOM
    processOrders(orders);
}

解决方案:分批处理

java 复制代码
@XxlJob("batchJob")
public void batchJob() {
    int page = 1;
    int pageSize = 1000;
    
    while (true) {
        // 分批加载数据
        List<Order> orders = orderService.getOrdersByPage(page, pageSize);
        if (orders.isEmpty()) {
            break;
        }
        
        processOrders(orders);
        page++;
    }
}

七、监控与运维

7.1 任务监控

XXL-JOB提供Web界面查看任务执行情况:

复制代码
调度中心 → 任务管理 → 执行日志

可以查看:

  • 任务执行时间
  • 执行结果(成功/失败)
  • 执行日志
  • 执行耗时

7.2 告警配置

XXL-JOB支持邮件告警,任务失败时自动发送邮件:

properties 复制代码
# 调度中心配置
xxl.job.mail.host=smtp.qq.com
xxl.job.mail.port=25
xxl.job.mail.username=your_email@qq.com
xxl.job.mail.password=your_password
xxl.job.mail.sendNick=XXL-JOB告警

也可以自定义告警逻辑:

java 复制代码
@Component
public class CustomAlertService {
    
    @Autowired
    private DingTalkService dingTalkService;
    
    public void sendAlert(String jobName, String message) {
        // 发送钉钉告警
        dingTalkService.send("任务告警: " + jobName + "\n" + message);
    }
}

7.3 性能优化

优化一:调整执行器线程池

properties 复制代码
# 执行器配置
xxl.job.executor.logretentiondays=30

# 线程池配置(在代码中配置)
@Bean
public XxlJobExecutor xxlJobExecutor() {
    XxlJobExecutor executor = new XxlJobExecutor();
    executor.setAdminAddresses(adminAddresses);
    executor.setAppname(appname);
    executor.setIp(ip);
    executor.setPort(port);
    executor.setLogPath(logPath);
    executor.setLogRetentionDays(logRetentionDays);
    
    // 设置线程池大小(默认10)
    executor.setExecutorService(
        Executors.newFixedThreadPool(50)
    );
    
    return executor;
}

优化二:批量处理

java 复制代码
@XxlJob("batchJob")
public void batchJob() {
    int batchSize = 1000;
    
    List<Order> orders = getOrders();
    
    // 分批处理
    Lists.partition(orders, batchSize).forEach(batch -> {
        processBatch(batch);
    });
}

优化三:异步处理

java 复制代码
@XxlJob("asyncJob")
public void asyncJob() {
    List<Order> orders = getOrders();
    
    // 异步并行处理
    CompletableFuture<?>[] futures = orders.stream()
        .map(order -> CompletableFuture.runAsync(() -> processOrder(order)))
        .toArray(CompletableFuture[]::new);
    
    CompletableFuture.allOf(futures).join();
}

八、总结

XXL-JOB作为分布式任务调度平台,其核心优势在于:

  1. 架构简单:调度中心+执行器,职责清晰
  2. 功能丰富:支持分片、重试、超时、依赖等高级特性
  3. 运维友好:Web界面管理,日志查询方便
  4. 扩展性强:支持动态调度、自定义路由策略

但同时,使用时需要注意:

  1. 幂等性:任务需要保证幂等性,避免重复执行导致数据错误
  2. 超时处理:主动检查超时,避免线程被kill后状态错误
  3. 分片均衡:合理设计分片策略,避免数据倾斜
  4. 监控告警:配置完善的监控告警,及时发现和处理问题

九、思考题

  1. 如果你的任务需要处理上亿条数据,如何设计分片策略?如何保证任务的可恢复性(执行到一半失败后,能从断点继续)?

  2. XXL-JOB的调度中心使用Quartz实现,Quartz是基于数据库锁实现的集群调度。如果数据库性能成为瓶颈,你会如何优化?是否可以考虑使用Redis实现分布式锁?

  3. 对于金融场景的对账任务,除了金额一致性,还需要检查哪些维度?如何设计对账规则和差异处理流程?


十、个人观点

在我参与过的多个项目中,定时任务最常见的误区是:忽视幂等性设计

很多开发同学觉得"定时任务每天执行一次,不会重复"。但实际上,任务失败重试、手动触发、调度异常等情况都可能导致重复执行。如果任务没有幂等性设计,重复执行可能导致数据错误、重复发送通知等严重问题。

我的建议是:所有定时任务都应该设计为幂等的。可以通过以下方式实现:

  1. 唯一标识:为每次执行生成唯一标识,已执行的直接跳过
  2. 状态检查:检查数据状态,已处理的直接跳过
  3. 去重表:记录已处理的数据ID,重复执行时过滤

另一个误区是:忽视任务监控。很多团队配置了任务,就以为万事大吉,直到业务出问题才发现任务早就失败了。建议从项目初期就建立完善的监控体系:

  1. 执行日志:记录每次执行的详细信息
  2. 告警通知:任务失败时及时告警
  3. 定期检查:定期检查任务执行情况,发现异常

最后,XXL-JOB虽然功能强大,但也有其局限性。对于复杂的任务依赖(如DAG依赖),XXL-JOB的工作流功能相对简单。如果业务场景复杂,可以考虑使用Airflow、DolphinScheduler等专业的工作流调度平台。


作者:架构实战系列 | 字数:约5200字

相关推荐
2603_9547083114 小时前
边缘计算在微电网架构中的应用:低时延控制的技术支撑
人工智能·物联网·架构·能源·边缘计算
@insist12314 小时前
系统架构设计师-需求工程与系统设计全体系指南
架构·系统架构·软考·系统架构设计师·软件水平考试
程序员老乔15 小时前
04-Spring-AI多模型架构
人工智能·spring·架构
颖火虫盟主15 小时前
Lua 协程:从 API 到底层原理再到 Skynet 架构的完整学习路径
学习·架构·lua
2601_9574188015 小时前
相机如何连接手机?通俗易懂的PTP/MTP连接原理解析
android·数码相机·架构
段一凡-华北理工大学15 小时前
工业领域的Hadoop架构学习~系列文章01:Hadoop与工业4.0深度融合
大数据·hadoop·学习·架构·知识图谱·高炉炼铁·工业智能体
向日的葵00615 小时前
Redis后端分布式与高并发架构演进
redis·分布式·架构
wb0430720115 小时前
架构是“长“出来的
adb·架构
“码”力全开15 小时前
容器化架构下的边缘计算:基于Docker与GB28181/RTSP多协议汇聚的AI视频管理平台架构解析与源码交付实践
人工智能·架构·边缘计算