下面我会先 全面拆解离线→同步的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:离线下单后设备断电,订单不丢失
设备离线,用户下单生成订单A;
设备立即断电,重启后;
查看SQLite的device_order表和device_order_backup表,订单A存在;
联网后,设备成功同步订单A到云端。
测试场景2:同步时网络再次中断,断点续传
设备离线生成订单A、B、C、D(共4条);
联网后开始同步,同步完A、B后,网络中断;
网络恢复后,设备从last_sync_order_id=B开始,同步C、D,未重复同步A、B。
测试场景3:同步时云端库存不足,冲突处理
云端商品X库存=1;
设备A离线下单商品X(数量1),设备B离线下单商品X(数量1);
设备A先联网,同步成功(库存扣减为0);
设备B联网同步,云端返回"库存不足",设备B标记订单为FAILED,重试5次后标记为EXCEPTION,设备指示灯告警。
测试场景4:订单重复同步,去重生效
设备同步订单A成功,但未收到云端确认,重启后再次同步;
云端接收后,Redis去重校验发现订单A已同步,直接返回成功,未重复创建订单。
六、总结(核心思路回顾)
本地可靠是基础 :离线订单必须用嵌入式数据库(SQLite)做事务化存储+备份,杜绝设备故障导致丢失;
同步可靠是关键 :批量+断点续传+指数退避重试,应对网络波动,确保"不重不漏";
云端校验是保障 :同步时校验商品、库存、订单唯一性,拒绝非法订单,避免业务冲突;
冲突处理是核心 :用乐观锁、分布式锁、状态机解决库存/状态冲突,无需人工干预;
容错兜底是补充 :死信队列+同步日志+设备告警,确保极端情况可追溯、可恢复。
这套方案完全聚焦"离线下单→联网同步"的核心痛点,代码可直接落地到嵌入式设备和Spring Cloud云端系统,兼顾了可靠性、一致性和效率。