下面我会先 全面拆解离线→同步的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. 完整流程(离线下单→联网同步)
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)
CREATE TABLE IF NOT EXISTS device_order (
order_id TEXT PRIMARY KEY,
device_id TEXT NOT NULL,
user_id TEXT NOT NULL,
product_id TEXT NOT NULL,
quantity INTEGER NOT NULL,
amount DECIMAL(10,2) NOT NULL,
create_time TEXT NOT NULL,
sync_status TEXT NOT NULL DEFAULT 'PENDING',
sync_retry_count INTEGER NOT NULL DEFAULT 0,
sync_fail_reason TEXT,
last_sync_time TEXT,
last_sync_order_id TEXT,
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生成+时间校准)
public class DeviceOrderIdGenerator {
private final String deviceId;
private final Snowflake snowflake;
public DeviceOrderIdGenerator(String deviceId) {
this.deviceId = deviceId;
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;
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();
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 {
return InetAddress.getByName("api.cloud-service.com").isReachable(3000);
} catch (IOException e) {
return false;
}
}
}
3. 设备端离线下单核心逻辑
/**
* 设备端离线订单服务(核心:本地事务+库存预占)
*/
public class DeviceOfflineOrderService {
private final Connection sqliteConn
private final DeviceOrderIdGenerator orderIdGenerator
private final DeviceTimeCalibrator timeCalibrator
private static final String DEVICE_ID = "DEV001"
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. 设备端联网同步核心逻辑(批量+断点续传+重试)
/**
* 设备端订单同步服务(核心:批量同步+断点续传+指数退避重试)
*/
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
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
private List<FailedOrder> failedOrders
private String lastSuccessOrderId
}
@Data
public class FailedOrder {
private String orderId
private String reason
}
第二部分:云端核心代码(接收同步+校验+处理)
1. 云端依赖配置(pom.xml)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<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)
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:
host: localhost
port: 6379
database: 0
timeout: 3000ms
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
publisher-confirm-type: correlated
publisher-returns: true
listener:
simple:
acknowledge-mode: manual
concurrency: 1
max-concurrency: 1
prefetch: 1
server:
port: 8080
servlet:
context-path: /
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.cloud.order.entity
3. 云端核心配置(RabbitMQ+Redis)
@Configuration
public class RabbitMQConfig {
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)
.build();
}
@Bean
public Queue orderSyncDlq() {
return QueueBuilder.durable(ORDER_SYNC_DLQ).build();
}
}
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
4. 云端同步接收接口(批量+去重)
@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<>();
for (DeviceOrderDTO order : orders) {
if (idempotentService.isOrderSynced(order.getOrderId())) {
successOrderIds.add(order.getOrderId());
continue;
}
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()));
}
}
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);
}
}
@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;
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. 云端订单处理消费者(有序+冲突处理)
@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);
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;
}
if (idempotentService.isOrderSynced(orderId)) {
channel.basicAck(deliveryTag, false);
return;
}
orderService.processOfflineOrder(order);
idempotentService.markOrderSynced(orderId);
channel.basicAck(deliveryTag, false);
System.out.println("订单[" + orderId + "]同步处理成功");
} catch (BusinessException e) {
System.err.println("订单[" + orderId + "]同步失败:" + e.getMessage());
channel.basicReject(deliveryTag, false);
} catch (Exception e) {
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) {
ProductStockPO stock = stockMapper.selectByProductId(dto.getProductId());
if (stock == null) {
throw new BusinessException("商品不存在");
}
if (stock.getStatus() != 1) {
throw new BusinessException("商品已下架");
}
if (stock.getAvailableStock() < dto.getQuantity()) {
throw new BusinessException("云端库存不足,当前可用库存:" + stock.getAvailableStock());
}
int rows = stockMapper.deductStock(
dto.getProductId(), dto.getQuantity(), stock.getVersion()
);
if (rows == 0) {
throw new BusinessException("库存扣减失败,可能已被其他订单占用");
}
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(库存扣减+订单创建)
<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>
<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云端系统,兼顾了可靠性、一致性和效率。