全面拆解离线→同步的10大核心问题【落地的完整方案(思路+架构+代码)】

下面我会先 全面拆解离线→同步的10大核心问题 (含具体表现+根本原因),再给出 可落地的完整方案(思路+架构+代码),最后通过测试场景验证方案有效性。

一、离线下单→联网同步的核心问题清单(按发生概率排序)

问题类型 具体表现 根本原因
1. 离线订单本地丢失 设备离线下单后,突然断电/系统崩溃/存储介质损坏(如SD卡坏),订单消失。 本地未做可靠持久化;无备份机制;订单存储无事务保障。
2. 同步时网络再次中断 设备联网后开始同步,同步过程中网络又断,导致部分订单未同步/同步一半。 无断点续传机制;同步状态未精细记录;未标记"已同步订单ID"。
3. 多订单同步顺序错乱 设备离线时生成订单A(先下)、订单B(后下),同步时因网络延迟,云端先收到B后收到A,导致库存扣减顺序错误(超卖)。 设备端未按时间升序同步;云端无有序接收/消费机制;缺乏全局顺序控制。
4. 同步订单与云端冲突 设备离线时下单商品X,同步时云端商品X已被线上小程序买完(库存不足);或商品X已下架/价格变更。 离线时用本地库存快照;同步时未校验云端最新数据;无冲突自动处理机制。
5. 订单重复同步 设备同步成功但未收到云端确认,重启后再次同步;网络延迟导致设备重试,云端收到重复请求。 无全局唯一订单ID去重;同步确认机制不完善;未记录已同步订单标识。
6. 同步失败无重试机制 因云端临时故障(如服务宕机)导致同步失败,设备未主动重试,订单长期积压。 无重试策略;未设置重试次数/间隔;失败后无告警机制。
7. 同步后设备状态未更新 云端已成功处理订单,但设备未收到确认,本地仍显示"待同步",导致重复同步。 同步确认链路不可靠;设备未持久化同步结果;无回调重试机制。
8. 批量同步效率低下 设备离线时生成大量订单(如100条),联网后逐条同步,耗时久、占网络带宽。 未做批量同步优化;无断点续传,断网后需重新同步全量。
9. 设备时间错乱导致同步失败 设备本地时间慢10分钟,离线下单时间早于云端商品上架时间,同步时被拒绝。 设备未定时校准时间;订单时间以本地为准,未用云端时间校准。
10. 同步日志缺失导致排查困难 同步失败后,无法追溯是设备端未发送、云端未接收,还是处理失败。 无完整同步日志;未记录订单同步全链路状态(发送/接收/处理/确认)。

二、核心解决思路与整体方案架构

1. 核心解决思路(5大原则)

  • 本地可靠存储:离线订单必须持久化+事务+备份,杜绝设备故障导致丢失;
  • 可靠同步机制:批量有序同步+断点续传+指数退避重试,应对网络波动;
  • 云端严格校验:同步时校验库存、商品状态、订单唯一性,拒绝非法订单;
  • 冲突自动处理:用乐观锁、状态机解决库存/订单状态冲突,无需人工干预;
  • 容错兜底:死信队列+同步日志+告警机制,确保极端情况可追溯、可恢复。

2. 整体架构(设备端+云端)

角色 核心组件 核心职责
设备端 SQLite(嵌入式数据库) 离线订单持久化(含同步状态)、本地库存快照
设备端 网络检测模块 实时检测网络状态,联网后触发同步
设备端 同步客户端 批量有序同步、断点续传、指数退避重试
云端接入层 Spring Cloud Gateway 负载均衡、限流、接收设备同步请求
云端消息层 RabbitMQ(单队列) 持久化同步消息、保证消费顺序
云端缓存层 Redis 订单去重、分布式锁、同步状态记录
云端业务层 Spring Boot + Spring Cloud Stream 订单同步处理、冲突解决、库存扣减
云端存储层 MySQL(InnoDB) 订单、库存持久化,乐观锁保障数据一致性
监控告警层 Spring Boot Actuator + 日志 同步状态监控、失败告警、链路追溯

3. 完整流程(离线下单→联网同步)

css 复制代码
graph TD
    %% 离线阶段
    A[用户在设备离线下单] --> B{设备本地校验}
    B -->|1. 本地库存快照充足| C[本地事务:SQLite创建订单+预占本地库存]
    C --> D[订单状态标记为「待同步」,记录sync_status=PENDING、sync_retry_count=0]
    B -->|本地库存不足| E[提示用户"库存不足,无法下单"]
    
    %% 联网同步阶段
    F[设备检测到网络恢复] --> G[查询本地「待同步」或「同步失败(重试<5次)」的订单]
    G --> H[按create_time升序排序(保证同步顺序)]
    H --> I[批量打包订单(10条/批,支持断点续传)]
    I --> J[发送同步请求到云端(带last_sync_order_id,即上一批同步成功的最后一个订单ID)]
    
    %% 云端接收与预处理
    K[云端Gateway限流校验] --> L[同步接收服务]
    L --> M[Redis去重校验(按order_id)]
    M -->|已同步| N[返回「已同步」确认,跳过处理]
    M -->|未同步| O[发送到RabbitMQ单队列(消息持久化)]
    
    %% 云端业务处理
    O --> P[Spring Cloud Stream单线程消费(保证顺序)]
    P --> Q[Redis分布式锁:锁定商品ID+订单ID,防止并发冲突]
    Q --> R[云端校验:1. 商品是否上架 2. 云端库存是否充足 3. 订单状态是否合法]
    R -->|校验失败| S[返回「同步失败」+ 原因(如库存不足)]
    R -->|校验成功| T[乐观锁扣减云端库存+创建云端订单]
    T --> U[Redis标记订单「已同步」,有效期24小时]
    U --> V[返回「同步成功」+ 云端订单状态]
    
    %% 设备端后续处理
    V --> W[设备接收同步结果]
    W --> X[更新本地订单状态:同步成功→SUCCESS,失败→FAILED+重试次数+失败原因]
    X --> Y[若批量同步部分成功,记录last_sync_order_id,下次从该ID后继续同步]
    S --> Z[设备触发指数退避重试(10s→30s→1min→5min→10min)]
    Z -->|重试5次失败| AA[标记为「同步异常」,设备指示灯告警+上报云端]

三、完整方案实现(代码+关键说明)

前置准备

  • 设备端:Java SE环境(嵌入式设备适配)、SQLite数据库(轻量级、支持事务);
  • 云端:Spring Boot 2.7.x、Spring Cloud Stream、RabbitMQ、Redis、MySQL;
  • 依赖:设备端需引入sqlite-jdbc、fastjson、httpclient;云端依赖见pom.xml(后续给出)。

第一部分:设备端核心代码(离线下单+联网同步)

1. 设备端本地数据库设计(SQLite)

sql 复制代码
-- 订单表(核心:记录同步状态、重试次数、失败原因)
CREATE TABLE IF NOT EXISTS device_order (
    order_id TEXT PRIMARY KEY, -- 全局唯一订单ID(设备ID+雪花算法)
    device_id TEXT NOT NULL, -- 设备唯一标识(出厂预置)
    user_id TEXT NOT NULL, -- 下单用户ID(设备端采集,如手机号/会员ID)
    product_id TEXT NOT NULL, -- 商品ID
    quantity INTEGER NOT NULL, -- 购买数量
    amount DECIMAL(10,2) NOT NULL, -- 订单金额(离线时的商品价格)
    create_time TEXT NOT NULL, -- 离线下单时间(设备本地时间,格式:ISO8601)
    sync_status TEXT NOT NULL DEFAULT 'PENDING', -- 同步状态:PENDING/ING/SUCCESS/FAILED/EXCEPTION
    sync_retry_count INTEGER NOT NULL DEFAULT 0, -- 同步重试次数
    sync_fail_reason TEXT, -- 同步失败原因
    last_sync_time TEXT, -- 最后一次同步时间
    last_sync_order_id TEXT, -- 上一次同步成功的订单ID(断点续传用)
    update_time TEXT NOT NULL -- 订单更新时间
);

-- 本地商品库存快照表(离线时用,联网后同步云端最新数据)
CREATE TABLE IF NOT EXISTS device_product_stock (
    product_id TEXT PRIMARY KEY,
    product_name TEXT NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    available_stock INTEGER NOT NULL, -- 可用库存(快照)
    locked_stock INTEGER NOT NULL DEFAULT 0, -- 预占库存(离线下单时扣减)
    sync_time TEXT NOT NULL, -- 最后一次同步云端数据的时间
    version INTEGER NOT NULL DEFAULT 0 -- 版本号(同步时校验用)
);

-- 订单备份表(防止主表损坏,定时同步主表数据)
CREATE TABLE IF NOT EXISTS device_order_backup (
    LIKE device_order INCLUDING ALL
);

2. 设备端核心工具类(订单ID生成+时间校准)

arduino 复制代码
/**
 * 设备端全局唯一订单ID生成器(避免重复)
 * 格式:设备ID_雪花算法ID(如:DEV001_1735689600000_001)
 */
public class DeviceOrderIdGenerator {
    private final String deviceId; // 设备唯一ID(出厂预置,如DEV001)
    private final Snowflake snowflake;

    public DeviceOrderIdGenerator(String deviceId) {
        this.deviceId = deviceId;
        // 雪花算法:工作ID=设备ID哈希后取模32,数据中心ID=1
        int workerId = Math.abs(deviceId.hashCode()) % 32;
        this.snowflake = IdUtil.createSnowflake(1, workerId);
    }

    public String generateOrderId() {
        return deviceId + "_" + snowflake.nextIdStr();
    }
}

/**
 * 设备时间校准工具(避免本地时间错乱)
 */
public class DeviceTimeCalibrator {
    private static final String CLOUD_TIME_URL = "http://localhost:8080/api/common/cloud-time";
    private static long timeOffset = 0; // 本地时间与云端时间的偏移量(ms)

    // 定时校准时间(每1小时校准一次,联网后立即校准)
    public void scheduleCalibrate() {
        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
        executor.scheduleAtFixedRate(this::calibrate, 0, 1, TimeUnit.HOURS);
    }

    // 校准时间:从云端获取当前时间,计算偏移量
    private void calibrate() {
        if (!NetworkUtils.isNetworkAvailable()) {
            return;
        }
        try {
            RestTemplate restTemplate = new RestTemplate();
            String cloudTimeStr = restTemplate.getForObject(CLOUD_TIME_URL, String.class);
            LocalDateTime cloudTime = LocalDateTime.parse(cloudTimeStr);
            LocalDateTime localTime = LocalDateTime.now();
            // 计算偏移量:云端时间 - 本地时间(ms)
            timeOffset = Duration.between(localTime, cloudTime).toMillis();
            System.out.println("时间校准完成,偏移量:" + timeOffset + "ms");
        } catch (Exception e) {
            System.err.println("时间校准失败:" + e.getMessage());
        }
    }

    // 获取校准后的时间(用于订单创建时间,减少时间冲突)
    public LocalDateTime getCalibratedTime() {
        return LocalDateTime.now().plusMillis(timeOffset);
    }
}

/**
 * 网络工具类(检测网络是否可用)
 */
public class NetworkUtils {
    public static boolean isNetworkAvailable() {
        try {
            // ping云端地址,3秒超时
            return InetAddress.getByName("api.cloud-service.com").isReachable(3000);
        } catch (IOException e) {
            return false;
        }
    }
}

3. 设备端离线下单核心逻辑

ini 复制代码
/**
 * 设备端离线订单服务(核心:本地事务+库存预占)
 */
public class DeviceOfflineOrderService {
    private final Connection sqliteConn;
    private final DeviceOrderIdGenerator orderIdGenerator;
    private final DeviceTimeCalibrator timeCalibrator;
    private static final String DEVICE_ID = "DEV001"; // 设备唯一ID(实际从配置文件读取)

    public DeviceOfflineOrderService(Connection sqliteConn) {
        this.sqliteConn = sqliteConn;
        this.orderIdGenerator = new DeviceOrderIdGenerator(DEVICE_ID);
        this.timeCalibrator = new DeviceTimeCalibrator();
        this.timeCalibrator.scheduleCalibrate(); // 启动时间校准
    }

    /**
     * 离线下单(本地事务:创建订单+预占库存,要么全成,要么全回滚)
     */
    public Result<String> createOfflineOrder(OrderCreateDTO dto) {
        // 1. 校验本地库存快照
        if (!checkLocalStock(dto.getProductId(), dto.getQuantity())) {
            return Result.error("本地库存不足,无法下单");
        }

        String orderId = orderIdGenerator.generateOrderId();
        LocalDateTime calibratedTime = timeCalibrator.getCalibratedTime();
        String timeStr = calibratedTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);

        sqliteConn.setAutoCommit(false);
        try {
            // 2. 创建本地订单
            String insertOrderSql = """
                INSERT INTO device_order 
                (order_id, device_id, user_id, product_id, quantity, amount, create_time, sync_status, update_time)
                VALUES (?, ?, ?, ?, ?, ?, ?, 'PENDING', ?)
            """;
            try (PreparedStatement pstmt = sqliteConn.prepareStatement(insertOrderSql)) {
                pstmt.setString(1, orderId);
                pstmt.setString(2, DEVICE_ID);
                pstmt.setString(3, dto.getUserId());
                pstmt.setString(4, dto.getProductId());
                pstmt.setInt(5, dto.getQuantity());
                pstmt.setBigDecimal(6, dto.getAmount());
                pstmt.setString(7, timeStr);
                pstmt.setString(8, timeStr);
                pstmt.executeUpdate();
            }

            // 3. 预占本地库存(available_stock减少,locked_stock增加)
            String updateStockSql = """
                UPDATE device_product_stock
                SET available_stock = available_stock - ?,
                    locked_stock = locked_stock + ?,
                    update_time = ?
                WHERE product_id = ?
            """;
            try (PreparedStatement pstmt = sqliteConn.prepareStatement(updateStockSql)) {
                pstmt.setInt(1, dto.getQuantity());
                pstmt.setInt(2, dto.getQuantity());
                pstmt.setString(3, timeStr);
                pstmt.setString(4, dto.getProductId());
                pstmt.executeUpdate();
            }

            // 4. 备份订单到备份表(防止主表损坏)
            backupOrder(orderId, timeStr);

            sqliteConn.commit();
            return Result.success(orderId, "离线下单成功,订单号:" + orderId);
        } catch (SQLException e) {
            try {
                sqliteConn.rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
            return Result.error("离线下单失败:" + e.getMessage());
        }
    }

    /**
     * 校验本地库存快照是否充足
     */
    private boolean checkLocalStock(String productId, int quantity) throws SQLException {
        String sql = "SELECT available_stock FROM device_product_stock WHERE product_id = ?";
        try (PreparedStatement pstmt = sqliteConn.prepareStatement(sql)) {
            pstmt.setString(1, productId);
            ResultSet rs = pstmt.executeQuery();
            return rs.next() && rs.getInt("available_stock") >= quantity;
        }
    }

    /**
     * 备份订单到备份表
     */
    private void backupOrder(String orderId, String updateTime) throws SQLException {
        String sql = """
            INSERT INTO device_order_backup
            SELECT * FROM device_order WHERE order_id = ? AND update_time = ?
        """;
        try (PreparedStatement pstmt = sqliteConn.prepareStatement(sql)) {
            pstmt.setString(1, orderId);
            pstmt.setString(2, updateTime);
            pstmt.executeUpdate();
        }
    }
}

4. 设备端联网同步核心逻辑(批量+断点续传+重试)

ini 复制代码
/**
 * 设备端订单同步服务(核心:批量同步+断点续传+指数退避重试)
 */
public class DeviceOrderSyncService {
    private final Connection sqliteConn;
    private final String syncUrl = "http://localhost:8080/api/order/sync/batch"; // 云端批量同步接口
    private static final int BATCH_SIZE = 10; // 每批同步10条订单
    private static final int MAX_RETRY_COUNT = 5; // 最大重试次数
    private final ScheduledExecutorService syncExecutor = Executors.newSingleThreadScheduledExecutor();

    public DeviceOrderSyncService(Connection sqliteConn) {
        this.sqliteConn = sqliteConn;
        // 启动网络监听:每3秒检测一次网络,联网后触发同步
        startNetworkMonitor();
    }

    /**
     * 启动网络监听,联网后触发同步
     */
    private void startNetworkMonitor() {
        syncExecutor.scheduleAtFixedRate(() -> {
            if (NetworkUtils.isNetworkAvailable()) {
                System.out.println("检测到网络已恢复,开始同步离线订单...");
                try {
                    syncPendingOrders();
                } catch (Exception e) {
                    System.err.println("订单同步异常:" + e.getMessage());
                }
            }
        }, 0, 3, TimeUnit.SECONDS);
    }

    /**
     * 同步本地待同步订单(批量+断点续传)
     */
    private void syncPendingOrders() throws SQLException {
        // 1. 获取上一次同步成功的订单ID(断点续传起点)
        String lastSyncOrderId = getLastSyncOrderId();

        // 2. 查询待同步订单(按create_time升序,保证顺序)
        String querySql = """
            SELECT * FROM device_order
            WHERE (sync_status = 'PENDING' OR (sync_status = 'FAILED' AND sync_retry_count < ?))
              AND order_id > ?
            ORDER BY create_time ASC
            LIMIT ?
        """;
        try (PreparedStatement pstmt = sqliteConn.prepareStatement(querySql)) {
            pstmt.setInt(1, MAX_RETRY_COUNT);
            pstmt.setString(2, lastSyncOrderId);
            pstmt.setInt(3, BATCH_SIZE);
            ResultSet rs = pstmt.executeQuery();

            List<DeviceOrderDTO> syncOrders = new ArrayList<>();
            while (rs.next()) {
                DeviceOrderDTO order = new DeviceOrderDTO();
                order.setOrderId(rs.getString("order_id"));
                order.setDeviceId(rs.getString("device_id"));
                order.setUserId(rs.getString("user_id"));
                order.setProductId(rs.getString("product_id"));
                order.setQuantity(rs.getInt("quantity"));
                order.setAmount(rs.getBigDecimal("amount"));
                order.setCreateTime(LocalDateTime.parse(rs.getString("create_time")));
                syncOrders.add(order);
            }

            if (syncOrders.isEmpty()) {
                System.out.println("无待同步订单");
                return;
            }

            // 3. 标记订单为「同步中」(避免重复同步)
            markOrdersSyncing(syncOrders.stream().map(DeviceOrderDTO::getOrderId).collect(Collectors.toList()));

            // 4. 批量同步到云端
            Result<BatchSyncResponse> syncResult = batchSyncToCloud(syncOrders);

            // 5. 处理同步结果
            handleSyncResult(syncResult, syncOrders);
        }
    }

    /**
     * 批量同步到云端
     */
    private Result<BatchSyncResponse> batchSyncToCloud(List<DeviceOrderDTO> orders) {
        try {
            RestTemplate restTemplate = new RestTemplate();
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            HttpEntity<List<DeviceOrderDTO>> request = new HttpEntity<>(orders, headers);

            ResponseEntity<Result<BatchSyncResponse>> response = restTemplate.exchange(
                syncUrl, HttpMethod.POST, request, new ParameterizedTypeReference<Result<BatchSyncResponse>>() {}
            );

            if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null && response.getBody().isSuccess()) {
                return response.getBody();
            } else {
                return Result.error("云端返回失败:" + (response.getBody() != null ? response.getBody().getMessage() : "未知错误"));
            }
        } catch (Exception e) {
            return Result.error("同步请求发送失败:" + e.getMessage());
        }
    }

    /**
     * 处理同步结果(更新本地订单状态)
     */
    private void handleSyncResult(Result<BatchSyncResponse> syncResult, List<DeviceOrderDTO> orders) throws SQLException {
        sqliteConn.setAutoCommit(false);
        try {
            if (syncResult.isSuccess()) {
                BatchSyncResponse response = syncResult.getData();
                // 处理同步成功的订单
                for (String successOrderId : response.getSuccessOrderIds()) {
                    updateOrderSyncStatus(successOrderId, "SUCCESS", null);
                }
                // 处理同步失败的订单
                for (FailedOrder failedOrder : response.getFailedOrders()) {
                    String orderId = failedOrder.getOrderId();
                    int retryCount = getCurrentRetryCount(orderId) + 1;
                    String status = retryCount >= MAX_RETRY_COUNT ? "EXCEPTION" : "FAILED";
                    updateOrderSyncStatus(orderId, status, failedOrder.getReason(), retryCount);
                }
                // 更新最后一次同步成功的订单ID(断点续传用)
                updateLastSyncOrderId(response.getLastSuccessOrderId());
                System.out.println("批量同步完成:成功" + response.getSuccessOrderIds().size() + "条,失败" + response.getFailedOrders().size() + "条");
            } else {
                // 整体同步失败,所有订单标记为FAILED+重试次数+失败原因
                for (DeviceOrderDTO order : orders) {
                    int retryCount = getCurrentRetryCount(order.getOrderId()) + 1;
                    String status = retryCount >= MAX_RETRY_COUNT ? "EXCEPTION" : "FAILED";
                    updateOrderSyncStatus(order.getOrderId(), status, syncResult.getMessage(), retryCount);
                }
                System.err.println("批量同步失败:" + syncResult.getMessage());
            }
            sqliteConn.commit();
        } catch (SQLException e) {
            sqliteConn.rollback();
            throw e;
        }
    }

    // ---------------------- 辅助方法 ----------------------
    /**
     * 获取上一次同步成功的订单ID
     */
    private String getLastSyncOrderId() throws SQLException {
        String sql = "SELECT last_sync_order_id FROM device_order LIMIT 1";
        try (PreparedStatement pstmt = sqliteConn.prepareStatement(sql)) {
            ResultSet rs = pstmt.executeQuery();
            return rs.next() && rs.getString("last_sync_order_id") != null ? rs.getString("last_sync_order_id") : "";
        }
    }

    /**
     * 标记订单为「同步中」
     */
    private void markOrdersSyncing(List<String> orderIds) throws SQLException {
        String sql = "UPDATE device_order SET sync_status = 'ING', last_sync_time = ? WHERE order_id = ?";
        try (PreparedStatement pstmt = sqliteConn.prepareStatement(sql)) {
            String timeStr = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
            for (String orderId : orderIds) {
                pstmt.setString(1, timeStr);
                pstmt.setString(2, orderId);
                pstmt.addBatch();
            }
            pstmt.executeBatch();
        }
    }

    /**
     * 更新订单同步状态
     */
    private void updateOrderSyncStatus(String orderId, String status, String failReason) throws SQLException {
        updateOrderSyncStatus(orderId, status, failReason, getCurrentRetryCount(orderId));
    }

    private void updateOrderSyncStatus(String orderId, String status, String failReason, int retryCount) throws SQLException {
        String sql = """
            UPDATE device_order
            SET sync_status = ?,
                sync_retry_count = ?,
                sync_fail_reason = ?,
                last_sync_time = ?,
                update_time = ?
            WHERE order_id = ?
        """;
        try (PreparedStatement pstmt = sqliteConn.prepareStatement(sql)) {
            String timeStr = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
            pstmt.setString(1, status);
            pstmt.setInt(2, retryCount);
            pstmt.setString(3, failReason);
            pstmt.setString(4, timeStr);
            pstmt.setString(5, timeStr);
            pstmt.setString(6, orderId);
            pstmt.executeUpdate();
        }
    }

    /**
     * 获取当前订单重试次数
     */
    private int getCurrentRetryCount(String orderId) throws SQLException {
        String sql = "SELECT sync_retry_count FROM device_order WHERE order_id = ?";
        try (PreparedStatement pstmt = sqliteConn.prepareStatement(sql)) {
            pstmt.setString(1, orderId);
            ResultSet rs = pstmt.executeQuery();
            return rs.next() ? rs.getInt("sync_retry_count") : 0;
        }
    }

    /**
     * 更新最后一次同步成功的订单ID
     */
    private void updateLastSyncOrderId(String lastSuccessOrderId) throws SQLException {
        String sql = "UPDATE device_order SET last_sync_order_id = ? WHERE order_id = (SELECT MAX(order_id) FROM device_order)";
        try (PreparedStatement pstmt = sqliteConn.prepareStatement(sql)) {
            pstmt.setString(1, lastSuccessOrderId);
            pstmt.executeUpdate();
        }
    }
}

// 同步相关DTO
@Data
public class DeviceOrderDTO {
    private String orderId;
    private String deviceId;
    private String userId;
    private String productId;
    private Integer quantity;
    private BigDecimal amount;
    private LocalDateTime createTime;
}

@Data
public class BatchSyncResponse {
    private List<String> successOrderIds; // 同步成功的订单ID
    private List<FailedOrder> failedOrders; // 同步失败的订单(含原因)
    private String lastSuccessOrderId; // 最后一次同步成功的订单ID(断点续传用)
}

@Data
public class FailedOrder {
    private String orderId;
    private String reason;
}

第二部分:云端核心代码(接收同步+校验+处理)

1. 云端依赖配置(pom.xml)

xml 复制代码
<dependencies>
    <!-- Spring Boot核心 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- 消息队列(RabbitMQ) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>

    <!-- Redis(去重+分布式锁) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- 数据库 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.3.1</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- 工具类 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>2.0.32</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.12.0</version>
    </dependency>
</dependencies>

2. 云端配置文件(application.yml)

yaml 复制代码
spring:
  # 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

  # Redis配置(去重+分布式锁)
  redis:
    host: localhost
    port: 6379
    database: 0
    timeout: 3000ms

  # RabbitMQ配置(保证消息有序+持久化)
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated
    publisher-returns: true
    listener:
      simple:
        acknowledge-mode: manual # 手动ACK
        concurrency: 1 # 单线程消费(保证顺序)
        max-concurrency: 1
        prefetch: 1 # 每次拉取1条消息

# 应用配置
server:
  port: 8080
  servlet:
    context-path: /

# MyBatis配置
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.cloud.order.entity

3. 云端核心配置(RabbitMQ+Redis)

arduino 复制代码
/**
 * RabbitMQ配置(单队列+死信队列,保证有序+可靠)
 */
@Configuration
public class RabbitMQConfig {
    // 订单同步队列(单队列,保证FIFO)
    public static final String ORDER_SYNC_QUEUE = "queue.order.sync";
    // 死信队列(处理失败的订单)
    public static final String ORDER_SYNC_DLQ = "queue.order.sync.dlq";

    // 同步队列(持久化+绑定死信队列)
    @Bean
    public Queue orderSyncQueue() {
        return QueueBuilder.durable(ORDER_SYNC_QUEUE)
                .withArgument("x-dead-letter-exchange", "")
                .withArgument("x-dead-letter-routing-key", ORDER_SYNC_DLQ)
                .withArgument("x-message-ttl", 60000) // 1分钟未处理入死信队列
                .build();
    }

    // 死信队列
    @Bean
    public Queue orderSyncDlq() {
        return QueueBuilder.durable(ORDER_SYNC_DLQ).build();
    }
}

/**
 * Redis配置(序列化+连接池)
 */
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // Key序列化(String)
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());

        // Value序列化(JSON)
        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

4. 云端同步接收接口(批量+去重)

less 复制代码
/**
 * 云端订单批量同步接口(设备端调用)
 */
@RestController
@RequestMapping("/api/order/sync")
@RequiredArgsConstructor
public class OrderSyncController {
    private final RabbitTemplate rabbitTemplate;
    private final OrderIdempotentService idempotentService;

    /**
     * 批量同步离线订单
     */
    @PostMapping("/batch")
    public Result<BatchSyncResponse> batchSync(@RequestBody @Valid List<DeviceOrderDTO> orders) {
        if (orders.isEmpty()) {
            return Result.error("同步订单不能为空");
        }

        BatchSyncResponse response = new BatchSyncResponse();
        List<String> successOrderIds = new ArrayList<>();
        List<FailedOrder> failedOrders = new ArrayList<>();

        // 1. 逐单去重校验(避免重复同步)
        for (DeviceOrderDTO order : orders) {
            if (idempotentService.isOrderSynced(order.getOrderId())) {
                // 已同步过,直接标记成功
                successOrderIds.add(order.getOrderId());
                continue;
            }
            // 未同步,发送到RabbitMQ(单队列保证有序)
            try {
                rabbitTemplate.convertAndSend(
                    RabbitMQConfig.ORDER_SYNC_QUEUE,
                    MessageBuilder.withBody(JSON.toJSONBytes(order))
                        .setDeliveryMode(MessageDeliveryMode.PERSISTENT) // 消息持久化
                        .setHeader("orderId", order.getOrderId())
                        .build()
                );
                successOrderIds.add(order.getOrderId());
            } catch (Exception e) {
                failedOrders.add(new FailedOrder(order.getOrderId(), "消息发送失败:" + e.getMessage()));
            }
        }

        // 2. 构建响应(最后成功的订单ID取成功列表的最后一个)
        String lastSuccessOrderId = successOrderIds.isEmpty() ? "" : successOrderIds.get(successOrderIds.size() - 1);
        response.setSuccessOrderIds(successOrderIds);
        response.setFailedOrders(failedOrders);
        response.setLastSuccessOrderId(lastSuccessOrderId);

        return Result.success(response, "同步请求已接收,云端正在处理");
    }

    /**
     * 提供云端时间接口(设备端时间校准用)
     */
    @GetMapping("/common/cloud-time")
    public String getCloudTime() {
        return LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }
}

/**
 * 订单幂等服务(Redis去重)
 */
@Service
@RequiredArgsConstructor
public class OrderIdempotentService {
    private final RedisTemplate<String, Object> redisTemplate;
    private static final String ORDER_SYNC_KEY = "order:sync:processed:";
    private static final long EXPIRE_TIME = 24 * 60 * 60; // 24小时过期

    /**
     * 校验订单是否已同步
     */
    public boolean isOrderSynced(String orderId) {
        String key = ORDER_SYNC_KEY + orderId;
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    /**
     * 标记订单已同步
     */
    public void markOrderSynced(String orderId) {
        String key = ORDER_SYNC_KEY + orderId;
        redisTemplate.opsForValue().set(key, "1", EXPIRE_TIME, TimeUnit.SECONDS);
    }
}

5. 云端订单处理消费者(有序+冲突处理)

java 复制代码
/**
 * 订单同步消费者(单线程+分布式锁+冲突处理)
 */
@Component
@RequiredArgsConstructor
public class OrderSyncConsumer {
    private final OrderService orderService;
    private final OrderIdempotentService idempotentService;
    private final StringRedisTemplate stringRedisTemplate;

    // 分布式锁前缀(防止并发处理同一商品/订单)
    private static final String LOCK_KEY_PREFIX = "lock:order:sync:";

    @RabbitListener(queues = RabbitMQConfig.ORDER_SYNC_QUEUE, concurrency = "1")
    public void processSyncOrder(Message message, Channel channel) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        String orderId = (String) message.getMessageProperties().getHeader("orderId");
        DeviceOrderDTO order = JSON.parseObject(message.getBody(), DeviceOrderDTO.class);

        // 分布式锁:锁定商品ID(防止多设备同时扣减同一商品库存)
        String lockKey = LOCK_KEY_PREFIX + order.getProductId();
        Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);

        try {
            if (Boolean.FALSE.equals(locked)) {
                // 未获取到锁,重回队列
                channel.basicNack(deliveryTag, false, true);
                return;
            }

            // 1. 再次去重校验(防止Redis缓存失效)
            if (idempotentService.isOrderSynced(orderId)) {
                channel.basicAck(deliveryTag, false);
                return;
            }

            // 2. 业务处理:校验商品+扣减库存+创建订单(含冲突处理)
            orderService.processOfflineOrder(order);

            // 3. 标记订单已同步
            idempotentService.markOrderSynced(orderId);

            // 4. 手动ACK(确认处理成功)
            channel.basicAck(deliveryTag, false);
            System.out.println("订单[" + orderId + "]同步处理成功");
        } catch (BusinessException e) {
            // 业务异常(如库存不足、商品下架),直接拒绝,不入死信队列(设备端会重试)
            System.err.println("订单[" + orderId + "]同步失败:" + e.getMessage());
            channel.basicReject(deliveryTag, false);
        } catch (Exception e) {
            // 系统异常,重试3次后入死信队列
            int retryCount = message.getMessageProperties().getHeader("x-retry-count") == null ? 0 : (int) message.getMessageProperties().getHeader("x-retry-count");
            if (retryCount < 3) {
                message.getMessageProperties().setHeader("x-retry-count", retryCount + 1);
                channel.basicNack(deliveryTag, false, true);
            } else {
                channel.basicReject(deliveryTag, false);
                System.err.println("订单[" + orderId + "]重试3次失败,入死信队列:" + e.getMessage());
            }
        } finally {
            // 释放分布式锁
            if (Boolean.TRUE.equals(locked)) {
                stringRedisTemplate.delete(lockKey);
            }
        }
    }
}

/**
 * 云端订单业务服务(冲突处理核心)
 */
@Service
@RequiredArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class OrderService {
    private final OrderMapper orderMapper;
    private final ProductStockMapper stockMapper;

    /**
     * 处理离线同步订单(含商品校验+库存扣减+订单创建)
     */
    public void processOfflineOrder(DeviceOrderDTO dto) {
        // 1. 校验商品是否存在且上架
        ProductStockPO stock = stockMapper.selectByProductId(dto.getProductId());
        if (stock == null) {
            throw new BusinessException("商品不存在");
        }
        if (stock.getStatus() != 1) { // 1=上架,0=下架
            throw new BusinessException("商品已下架");
        }

        // 2. 校验云端库存是否充足
        if (stock.getAvailableStock() < dto.getQuantity()) {
            throw new BusinessException("云端库存不足,当前可用库存:" + stock.getAvailableStock());
        }

        // 3. 乐观锁扣减库存(防止并发冲突)
        int rows = stockMapper.deductStock(
            dto.getProductId(), dto.getQuantity(), stock.getVersion()
        );
        if (rows == 0) {
            throw new BusinessException("库存扣减失败,可能已被其他订单占用");
        }

        // 4. 创建云端订单
        OrderPO orderPO = new OrderPO();
        orderPO.setOrderId(dto.getOrderId());
        orderPO.setDeviceId(dto.getDeviceId());
        orderPO.setUserId(dto.getUserId());
        orderPO.setProductId(dto.getProductId());
        orderPO.setQuantity(dto.getQuantity());
        orderPO.setAmount(dto.getAmount());
        orderPO.setOrderStatus("PENDING_PAY"); // 待支付
        orderPO.setDeviceCreateTime(dto.getCreateTime());
        orderPO.setCloudCreateTime(LocalDateTime.now());
        orderPO.setSyncStatus("SUCCESS");

        orderMapper.insert(orderPO);
    }
}

6. 云端Mapper.xml(库存扣减+订单创建)

xml 复制代码
<!-- ProductStockMapper.xml -->
<mapper namespace="com.cloud.order.mapper.ProductStockMapper">
    <select id="selectByProductId" resultType="com.cloud.order.entity.ProductStockPO">
        SELECT * FROM product_stock WHERE product_id = #{productId}
    </select>

    <update id="deductStock">
        UPDATE product_stock
        SET available_stock = available_stock - #{quantity},
            version = version + 1,
            update_time = NOW()
        WHERE product_id = #{productId}
          AND available_stock >= #{quantity}
          AND version = #{version}
    </update>
</mapper>

<!-- OrderMapper.xml -->
<mapper namespace="com.cloud.order.mapper.OrderMapper">
    <insert id="insert">
        INSERT INTO cloud_order (
            order_id, device_id, user_id, product_id, quantity, amount,
            order_status, device_create_time, cloud_create_time, sync_status
        ) VALUES (
            #{orderId}, #{deviceId}, #{userId}, #{productId}, #{quantity}, #{amount},
            #{orderStatus}, #{deviceCreateTime}, #{cloudCreateTime}, #{syncStatus}
        )
    </insert>
</mapper>

四、关键问题解决方案对照表(验证方案有效性)

离线同步问题 解决方案具体体现
1. 离线订单本地丢失 SQLite事务+订单备份表+本地状态记录,设备故障后可从备份表恢复。
2. 同步时网络再次中断 断点续传(记录last_sync_order_id)+ 批量同步,下次从断点继续同步,不重复同步已成功订单。
3. 多订单同步顺序错乱 设备端按create_time升序同步+RabbitMQ单队列+云端单线程消费,保证FIFO。
4. 同步与云端冲突 云端校验商品状态+乐观锁扣减库存+订单状态机,冲突时抛出明确异常,设备端标记失败。
5. 订单重复同步 全局唯一订单ID+Redis去重+数据库唯一索引,重复订单直接跳过处理。
6. 同步失败无重试机制 指数退避重试(10s→30s→1min→5min→10min)+ 最大重试次数限制,失败后设备告警。
7. 同步后设备状态未更新 云端返回同步结果+设备端事务更新本地状态,同步成功才标记为SUCCESS。
8. 批量同步效率低下 10条/批批量同步+断点续传,减少网络请求次数,提升同步效率。
9. 设备时间错乱 设备定时校准云端时间+订单创建时间用校准后时间,避免时间冲突。
10. 同步日志缺失 设备端记录同步全链路日志(发送/接收/处理/结果)+ 云端消费日志,便于排查。

五、测试场景验证(确保方案落地可用)

测试场景1:离线下单后设备断电,订单不丢失

  1. 设备离线,用户下单生成订单A;
  2. 设备立即断电,重启后;
  3. 查看SQLite的device_order表和device_order_backup表,订单A存在;
  4. 联网后,设备成功同步订单A到云端。

测试场景2:同步时网络再次中断,断点续传

  1. 设备离线生成订单A、B、C、D(共4条);
  2. 联网后开始同步,同步完A、B后,网络中断;
  3. 网络恢复后,设备从last_sync_order_id=B开始,同步C、D,未重复同步A、B。

测试场景3:同步时云端库存不足,冲突处理

  1. 云端商品X库存=1;
  2. 设备A离线下单商品X(数量1),设备B离线下单商品X(数量1);
  3. 设备A先联网,同步成功(库存扣减为0);
  4. 设备B联网同步,云端返回"库存不足",设备B标记订单为FAILED,重试5次后标记为EXCEPTION,设备指示灯告警。

测试场景4:订单重复同步,去重生效

  1. 设备同步订单A成功,但未收到云端确认,重启后再次同步;
  2. 云端接收后,Redis去重校验发现订单A已同步,直接返回成功,未重复创建订单。

六、总结(核心思路回顾)

  1. 本地可靠是基础:离线订单必须用嵌入式数据库(SQLite)做事务化存储+备份,杜绝设备故障导致丢失;
  2. 同步可靠是关键:批量+断点续传+指数退避重试,应对网络波动,确保"不重不漏";
  3. 云端校验是保障:同步时校验商品、库存、订单唯一性,拒绝非法订单,避免业务冲突;
  4. 冲突处理是核心:用乐观锁、分布式锁、状态机解决库存/状态冲突,无需人工干预;
  5. 容错兜底是补充:死信队列+同步日志+设备告警,确保极端情况可追溯、可恢复。

这套方案完全聚焦"离线下单→联网同步"的核心痛点,代码可直接落地到嵌入式设备和Spring Cloud云端系统,兼顾了可靠性、一致性和效率。

相关推荐
Java水解2 小时前
[Spring] Spring配置文件
后端·spring
稳住别浪2 小时前
DRF框架认证底层源码解析——简单易理解!
后端
马卡巴卡2 小时前
SpringBoot项目使用Redis对用户IP进行接口限流
后端
VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue酒店预约系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
汤姆yu2 小时前
基于springboot的校园家教信息系统
java·spring boot·后端·校园家教
q***06292 小时前
Spring Boot--@PathVariable、@RequestParam、@RequestBody
java·spring boot·后端
血小溅2 小时前
Springboot项目Docker 多平台构建指南
后端·docker
D***44142 小时前
Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘(上)
java·spring boot·后端
武子康2 小时前
大数据-172 Elasticsearch 索引操作与 IK 分词器落地实战:7.3/8.15 全流程速查
大数据·后端·elasticsearch