基于SpringBoot實(shí)現(xiàn)離線應(yīng)用的4種實(shí)現(xiàn)方式
在當(dāng)今高度依賴網(wǎng)絡(luò)的環(huán)境中,離線應(yīng)用的價(jià)值日益凸顯。
無論是在網(wǎng)絡(luò)不穩(wěn)定的區(qū)域運(yùn)行的現(xiàn)場系統(tǒng),還是需要在斷網(wǎng)環(huán)境下使用的企業(yè)內(nèi)部應(yīng)用,具備離線工作能力已成為許多應(yīng)用的必備特性。
本文將介紹基于SpringBoot實(shí)現(xiàn)離線應(yīng)用的5種不同方式。
一、離線應(yīng)用的概念與挑戰(zhàn)
離線應(yīng)用(Offline Application)是指能夠在網(wǎng)絡(luò)連接不可用的情況下,仍然能夠正常運(yùn)行并提供核心功能的應(yīng)用程序。
這類應(yīng)用通常具備以下特點(diǎn):
1. 本地?cái)?shù)據(jù)存儲:能夠在本地存儲和讀取數(shù)據(jù)
2. 操作緩存:能夠緩存用戶操作,待網(wǎng)絡(luò)恢復(fù)后同步
3. 資源本地化:應(yīng)用資源(如靜態(tài)資源、配置等)可以在本地訪問
4. 狀態(tài)管理:維護(hù)應(yīng)用狀態(tài),處理在線/離線切換
實(shí)現(xiàn)離線應(yīng)用面臨的主要挑戰(zhàn)包括:數(shù)據(jù)存儲與同步、沖突解決、用戶體驗(yàn)設(shè)計(jì)以及安全性考慮。
二、嵌入式數(shù)據(jù)庫實(shí)現(xiàn)離線數(shù)據(jù)存儲
原理介紹
嵌入式數(shù)據(jù)庫直接集成在應(yīng)用程序中,無需外部數(shù)據(jù)庫服務(wù)器,非常適合離線應(yīng)用場景。
在SpringBoot中,可以輕松集成H2、SQLite、HSQLDB等嵌入式數(shù)據(jù)庫。
實(shí)現(xiàn)步驟
1. 添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>2. 配置文件
# 使用文件模式的H2數(shù)據(jù)庫,支持持久化 spring.datasource.url=jdbc:h2:file:./data/offlinedb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect # 自動創(chuàng)建表結(jié)構(gòu) spring.jpa.hibernate.ddl-auto=update # 啟用H2控制臺(開發(fā)環(huán)境) spring.h2.console.enabled=true spring.h2.console.path=/h2-console
3. 創(chuàng)建實(shí)體類
@Entity
@Table(name = "offline_data")
public class OfflineData {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
@Column(name = "is_synced")
private boolean synced;
@Column(name = "created_at")
private LocalDateTime createdAt;
// 構(gòu)造函數(shù)、getter和setter
}4. 創(chuàng)建Repository
@Repository
public interface OfflineDataRepository extends JpaRepository<OfflineData, Long> {
List<OfflineData> findBySyncedFalse();
}5. 創(chuàng)建Service
@Service
public class OfflineDataService {
private final OfflineDataRepository repository;
@Autowired
public OfflineDataService(OfflineDataRepository repository) {
this.repository = repository;
}
// 保存本地?cái)?shù)據(jù)
public OfflineData saveData(String content) {
OfflineData data = new OfflineData();
data.setContent(content);
data.setSynced(false);
data.setCreatedAt(LocalDateTime.now());
return repository.save(data);
}
// 獲取所有未同步的數(shù)據(jù)
public List<OfflineData> getUnsyncedData() {
return repository.findBySyncedFalse();
}
// 標(biāo)記數(shù)據(jù)為已同步
public void markAsSynced(Long id) {
repository.findById(id).ifPresent(data -> {
data.setSynced(true);
repository.save(data);
});
}
// 當(dāng)網(wǎng)絡(luò)恢復(fù)時(shí),同步數(shù)據(jù)到遠(yuǎn)程服務(wù)器
@Scheduled(fixedDelay = 60000) // 每分鐘檢查一次
public void syncDataToRemote() {
List<OfflineData> unsyncedData = getUnsyncedData();
if (!unsyncedData.isEmpty()) {
try {
// 嘗試連接遠(yuǎn)程服務(wù)器
if (isNetworkAvailable()) {
for (OfflineData data : unsyncedData) {
boolean syncSuccess = sendToRemoteServer(data);
if (syncSuccess) {
markAsSynced(data.getId());
}
}
}
} catch (Exception e) {
// 同步失敗,下次再試
log.error("Failed to sync data: " + e.getMessage());
}
}
}
private boolean isNetworkAvailable() {
// 實(shí)現(xiàn)網(wǎng)絡(luò)檢測邏輯
try {
InetAddress address = InetAddress.getByName("api.example.com");
return address.isReachable(3000); // 3秒超時(shí)
} catch (Exception e) {
return false;
}
}
private boolean sendToRemoteServer(OfflineData data) {
// 實(shí)現(xiàn)發(fā)送數(shù)據(jù)到遠(yuǎn)程服務(wù)器的邏輯
// 這里使用RestTemplate示例
try {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.postForEntity(
"https://api.example.com/data",
data,
String.class
);
return response.getStatusCode().isSuccessful();
} catch (Exception e) {
log.error("Failed to send data: " + e.getMessage());
return false;
}
}
}6. 創(chuàng)建Controller
@RestController
@RequestMapping("/api/data")
public class OfflineDataController {
private final OfflineDataService service;
@Autowired
public OfflineDataController(OfflineDataService service) {
this.service = service;
}
@PostMapping
public ResponseEntity<OfflineData> createData(@RequestBody String content) {
OfflineData savedData = service.saveData(content);
return ResponseEntity.ok(savedData);
}
@GetMapping("/unsynced")
public ResponseEntity<List<OfflineData>> getUnsyncedData() {
return ResponseEntity.ok(service.getUnsyncedData());
}
@PostMapping("/sync")
public ResponseEntity<String> triggerSync() {
service.syncDataToRemote();
return ResponseEntity.ok("Sync triggered");
}
}優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
- 完全本地化的數(shù)據(jù)存儲,無需網(wǎng)絡(luò)連接
- 支持完整的SQL功能,可以進(jìn)行復(fù)雜查詢
- 數(shù)據(jù)持久化到本地文件,應(yīng)用重啟不丟失
缺點(diǎn):
- 嵌入式數(shù)據(jù)庫性能和并發(fā)處理能力有限
- 占用本地存儲空間,需要注意容量管理
- 數(shù)據(jù)同步邏輯需要自行實(shí)現(xiàn)
- 復(fù)雜的沖突解決場景處理困難
適用場景
• 需要結(jié)構(gòu)化數(shù)據(jù)存儲的單機(jī)應(yīng)用
• 定期需要將數(shù)據(jù)同步到中心服務(wù)器的現(xiàn)場應(yīng)用
• 對數(shù)據(jù)查詢有SQL需求的離線系統(tǒng)
• 數(shù)據(jù)量適中的企業(yè)內(nèi)部工具
三、本地緩存與離線數(shù)據(jù)訪問策略
原理介紹
本方案利用Java內(nèi)存緩存框架(如Caffeine、Ehcache)結(jié)合本地持久化存儲,實(shí)現(xiàn)數(shù)據(jù)的本地緩存和離線訪問。
該方案特別適合讀多寫少的應(yīng)用場景。
實(shí)現(xiàn)步驟
1. 添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>2. 配置緩存
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public Caffeine<Object, Object> caffeineConfig() {
return Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.DAYS)
.initialCapacity(100)
.maximumSize(1000)
.recordStats();
}
@Bean
public CacheManager cacheManager(Caffeine<Object, Object> caffeine) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(caffeine);
return cacheManager;
}
@Bean
public CacheSerializer cacheSerializer() {
return new CacheSerializer();
}
}3. 創(chuàng)建緩存序列化器
@Component
public class CacheSerializer {
private final ObjectMapper objectMapper = new ObjectMapper();
private final File cacheDir = new File("./cache");
public CacheSerializer() {
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
}
public void serializeCache(String cacheName, Map<Object, Object> entries) {
try {
File cacheFile = new File(cacheDir, cacheName + ".json");
objectMapper.writeValue(cacheFile, entries);
} catch (IOException e) {
throw new RuntimeException("Failed to serialize cache: " + cacheName, e);
}
}
@SuppressWarnings("unchecked")
public Map<Object, Object> deserializeCache(String cacheName) {
File cacheFile = new File(cacheDir, cacheName + ".json");
if (!cacheFile.exists()) {
return new HashMap<>();
}
try {
return objectMapper.readValue(cacheFile, Map.class);
} catch (IOException e) {
throw new RuntimeException("Failed to deserialize cache: " + cacheName, e);
}
}
}4. 創(chuàng)建離線數(shù)據(jù)服務(wù)
@Service
@Slf4j
public class ProductService {
private final RestTemplate restTemplate;
private final CacheSerializer cacheSerializer;
private static final String CACHE_NAME = "products";
@Autowired
public ProductService(RestTemplate restTemplate, CacheSerializer cacheSerializer) {
this.restTemplate = restTemplate;
this.cacheSerializer = cacheSerializer;
// 初始化時(shí)加載持久化的緩存
loadCacheFromDisk();
}
@Cacheable(cacheNames = CACHE_NAME, key = "#id")
public Product getProductById(Long id) {
try {
// 嘗試從遠(yuǎn)程服務(wù)獲取
return restTemplate.getForObject("https://api.example.com/products/" + id, Product.class);
} catch (Exception e) {
// 網(wǎng)絡(luò)不可用時(shí),嘗試從持久化緩存獲取
Map<Object, Object> diskCache = cacheSerializer.deserializeCache(CACHE_NAME);
Product product = (Product) diskCache.get(id.toString());
if (product != null) {
return product;
}
throw new ProductNotFoundException("Product not found in cache: " + id);
}
}
@Cacheable(cacheNames = CACHE_NAME)
public List<Product> getAllProducts() {
try {
// 嘗試從遠(yuǎn)程服務(wù)獲取
Product[] products = restTemplate.getForObject("https://api.example.com/products", Product[].class);
return products != null ? Arrays.asList(products) : Collections.emptyList();
} catch (Exception e) {
// 網(wǎng)絡(luò)不可用時(shí),返回所有持久化緩存的產(chǎn)品
Map<Object, Object> diskCache = cacheSerializer.deserializeCache(CACHE_NAME);
return new ArrayList<>(diskCache.values());
}
}
@CachePut(cacheNames = CACHE_NAME, key = "#product.id")
public Product saveProduct(Product product) {
try {
// 嘗試保存到遠(yuǎn)程服務(wù)
return restTemplate.postForObject("https://api.example.com/products", product, Product.class);
} catch (Exception e) {
// 網(wǎng)絡(luò)不可用時(shí),只保存到本地緩存
product.setOfflineSaved(true);
// 同時(shí)更新持久化緩存
Map<Object, Object> diskCache = cacheSerializer.deserializeCache(CACHE_NAME);
diskCache.put(product.getId().toString(), product);
cacheSerializer.serializeCache(CACHE_NAME, diskCache);
return product;
}
}
@Scheduled(fixedDelay = 300000) // 每5分鐘
public void persistCacheToDisk() {
Cache cache = cacheManager.getCache(CACHE_NAME);
if (cache != null) {
Map<Object, Object> entries = new HashMap<>();
cache.getNativeCache().asMap().forEach(entries::put);
cacheSerializer.serializeCache(CACHE_NAME, entries);
}
}
@Scheduled(fixedDelay = 600000) // 每10分鐘
public void syncOfflineData() {
if (!isNetworkAvailable()) {
return;
}
Map<Object, Object> diskCache = cacheSerializer.deserializeCache(CACHE_NAME);
for (Object value : diskCache.values()) {
Product product = (Product) value;
if (product.isOfflineSaved()) {
try {
restTemplate.postForObject("https://api.example.com/products", product, Product.class);
product.setOfflineSaved(false);
} catch (Exception e) {
// 同步失敗,下次再試
log.error(e.getMessage(),e);
}
}
}
// 更新持久化緩存
cacheSerializer.serializeCache(CACHE_NAME, diskCache);
}
private void loadCacheFromDisk() {
Map<Object, Object> diskCache = cacheSerializer.deserializeCache(CACHE_NAME);
Cache cache = cacheManager.getCache(CACHE_NAME);
if (cache != null) {
diskCache.forEach((key, value) -> cache.put(key, value));
}
}
private boolean isNetworkAvailable() {
try {
return InetAddress.getByName("api.example.com").isReachable(3000);
} catch (Exception e) {
return false;
}
}
}5. 創(chuàng)建數(shù)據(jù)模型
@Data
public class Product implements Serializable {
private Long id;
private String name;
private String description;
private BigDecimal price;
private boolean offlineSaved;
}6. 創(chuàng)建Controller
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
try {
return ResponseEntity.ok(productService.getProductById(id));
} catch (ProductNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
return ResponseEntity.ok(productService.getAllProducts());
}
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
return ResponseEntity.ok(productService.saveProduct(product));
}
@GetMapping("/sync")
public ResponseEntity<String> triggerSync() {
productService.syncOfflineData();
return ResponseEntity.ok("Sync triggered");
}
}優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
- 內(nèi)存緩存訪問速度快,用戶體驗(yàn)好
- 結(jié)合本地持久化,支持應(yīng)用重啟后恢復(fù)緩存
- 適合讀多寫少的應(yīng)用場景
缺點(diǎn):
- 緩存同步和沖突解決邏輯復(fù)雜
- 大量數(shù)據(jù)緩存會占用較多內(nèi)存
- 不適合頻繁寫入的場景
- 緩存序列化和反序列化有性能開銷
適用場景
• 產(chǎn)品目錄、知識庫等讀多寫少的應(yīng)用
• 需要快速響應(yīng)的用戶界面
• 有限的數(shù)據(jù)集合且結(jié)構(gòu)相對固定
• 偶爾離線使用的Web應(yīng)用
四、離線優(yōu)先架構(gòu)與本地存儲引擎
原理介紹
離線優(yōu)先架構(gòu)(Offline-First)是一種設(shè)計(jì)理念,它將離線狀態(tài)視為應(yīng)用的默認(rèn)狀態(tài),而不是異常狀態(tài)。
在這種架構(gòu)中,數(shù)據(jù)首先存儲在本地,然后在條件允許時(shí)同步到服務(wù)器。
該方案使用嵌入式KV存儲(如LevelDB、RocksDB)作為本地存儲引擎。
實(shí)現(xiàn)步驟
1. 添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.iq80.leveldb</groupId>
<artifactId>leveldb</artifactId>
<version>0.12</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>2. 創(chuàng)建LevelDB存儲服務(wù)
@Component
public class LevelDBStore implements InitializingBean, DisposableBean {
private DB db;
private final ObjectMapper objectMapper = new ObjectMapper();
private final File dbDir = new File("./leveldb");
@Override
public void afterPropertiesSet() throws Exception {
Options options = new Options();
options.createIfMissing(true);
db = factory.open(dbDir, options);
}
@Override
public void destroy() throws Exception {
if (db != null) {
db.close();
}
}
public <T> void put(String key, T value) {
try {
byte[] serialized = objectMapper.writeValueAsBytes(value);
db.put(bytes(key), serialized);
} catch (Exception e) {
throw new RuntimeException("Failed to store data: " + key, e);
}
}
public <T> T get(String key, Class<T> type) {
try {
byte[] data = db.get(bytes(key));
if (data == null) {
return null;
}
return objectMapper.readValue(data, type);
} catch (Exception e) {
throw new RuntimeException("Failed to retrieve data: " + key, e);
}
}
public <T> List<T> getAll(String prefix, Class<T> type) {
List<T> result = new ArrayList<>();
try (DBIterator iterator = db.iterator()) {
byte[] prefixBytes = bytes(prefix);
for (iterator.seek(prefixBytes); iterator.hasNext(); iterator.next()) {
String key = asString(iterator.peekNext().getKey());
if (!key.startsWith(prefix)) {
break;
}
T value = objectMapper.readValue(iterator.peekNext().getValue(), type);
result.add(value);
}
} catch (Exception e) {
throw new RuntimeException("Failed to retrieve data with prefix: " + prefix, e);
}
return result;
}
public boolean delete(String key) {
try {
db.delete(bytes(key));
return true;
} catch (Exception e) {
return false;
}
}
private byte[] bytes(String s) {
return s.getBytes(StandardCharsets.UTF_8);
}
private String asString(byte[] bytes) {
return new String(bytes, StandardCharsets.UTF_8);
}
}3. 創(chuàng)建離線同步管理器
@Component
public class SyncManager {
private final LevelDBStore store;
private final RestTemplate restTemplate;
@Value("${sync.server.url}")
private String syncServerUrl;
@Autowired
public SyncManager(LevelDBStore store, RestTemplate restTemplate) {
this.store = store;
this.restTemplate = restTemplate;
}
// 保存并跟蹤離線操作
public <T> void saveOperation(String type, String id, T data) {
String key = "op:" + type + ":" + id;
OfflineOperation<T> operation = new OfflineOperation<>(
UUID.randomUUID().toString(),
type,
id,
data,
System.currentTimeMillis()
);
store.put(key, operation);
}
// 同步所有未同步的操作
@Scheduled(fixedDelay = 60000) // 每分鐘嘗試同步
public void syncOfflineOperations() {
if (!isNetworkAvailable()) {
return;
}
List<OfflineOperation<?>> operations = store.getAll("op:", OfflineOperation.class);
// 按時(shí)間戳排序,確保按操作順序同步
operations.sort(Comparator.comparing(OfflineOperation::getTimestamp));
for (OfflineOperation<?> operation : operations) {
boolean success = sendToServer(operation);
if (success) {
// 同步成功后刪除本地操作記錄
store.delete("op:" + operation.getType() + ":" + operation.getId());
} else {
// 同步失敗,下次再試
break;
}
}
}
private boolean sendToServer(OfflineOperation<?> operation) {
try {
HttpMethod method;
switch (operation.getType()) {
case "CREATE":
method = HttpMethod.POST;
break;
case "UPDATE":
method = HttpMethod.PUT;
break;
case "DELETE":
method = HttpMethod.DELETE;
break;
default:
return false;
}
// 構(gòu)建請求URL
String url = syncServerUrl + "/" + operation.getId();
if ("DELETE".equals(operation.getType())) {
// DELETE請求通常不需要請求體
ResponseEntity<Void> response = restTemplate.exchange(
url, method, null, Void.class
);
return response.getStatusCode().is2xxSuccessful();
} else {
// POST和PUT請求需要請求體
HttpEntity<Object> request = new HttpEntity<>(operation.getData());
ResponseEntity<Object> response = restTemplate.exchange(
url, method, request, Object.class
);
return response.getStatusCode().is2xxSuccessful();
}
} catch (Exception e) {
return false;
}
}
private boolean isNetworkAvailable() {
try {
URL url = new URL(syncServerUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(3000);
connection.connect();
return connection.getResponseCode() == 200;
} catch (Exception e) {
return false;
}
}
@Data
@AllArgsConstructor
private static class OfflineOperation<T> {
private String operationId;
private String type; // CREATE, UPDATE, DELETE
private String id;
private T data;
private long timestamp;
}
}4. 創(chuàng)建任務(wù)服務(wù)
@Service
public class TaskService {
private final LevelDBStore store;
private final SyncManager syncManager;
@Autowired
public TaskService(LevelDBStore store, SyncManager syncManager) {
this.store = store;
this.syncManager = syncManager;
}
public Task getTaskById(String id) {
return store.get("task:" + id, Task.class);
}
public List<Task> getAllTasks() {
return store.getAll("task:", Task.class);
}
public Task createTask(Task task) {
// 生成ID
if (task.getId() == null) {
task.setId(UUID.randomUUID().toString());
}
// 設(shè)置時(shí)間戳
task.setCreatedAt(System.currentTimeMillis());
task.setUpdatedAt(System.currentTimeMillis());
// 保存到本地存儲
store.put("task:" + task.getId(), task);
// 記錄離線操作,等待同步
syncManager.saveOperation("CREATE", task.getId(), task);
return task;
}
public Task updateTask(String id, Task task) {
Task existingTask = getTaskById(id);
if (existingTask == null) {
throw new RuntimeException("Task not found: " + id);
}
// 更新字段
task.setId(id);
task.setCreatedAt(existingTask.getCreatedAt());
task.setUpdatedAt(System.currentTimeMillis());
// 保存到本地存儲
store.put("task:" + id, task);
// 記錄離線操作,等待同步
syncManager.saveOperation("UPDATE", id, task);
return task;
}
public boolean deleteTask(String id) {
Task existingTask = getTaskById(id);
if (existingTask == null) {
return false;
}
// 從本地存儲刪除
boolean deleted = store.delete("task:" + id);
// 記錄離線操作,等待同步
if (deleted) {
syncManager.saveOperation("DELETE", id, null);
}
return deleted;
}
}5. 創(chuàng)建任務(wù)模型
@Data
public class Task {
private String id;
private String title;
private String description;
private boolean completed;
private long createdAt;
private long updatedAt;
}6. 創(chuàng)建Controller
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService taskService;
@Autowired
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@GetMapping("/{id}")
public ResponseEntity<Task> getTaskById(@PathVariable String id) {
Task task = taskService.getTaskById(id);
if (task == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(task);
}
@GetMapping
public ResponseEntity<List<Task>> getAllTasks() {
return ResponseEntity.ok(taskService.getAllTasks());
}
@PostMapping
public ResponseEntity<Task> createTask(@RequestBody Task task) {
return ResponseEntity.ok(taskService.createTask(task));
}
@PutMapping("/{id}")
public ResponseEntity<Task> updateTask(@PathVariable String id, @RequestBody Task task) {
try {
return ResponseEntity.ok(taskService.updateTask(id, task));
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTask(@PathVariable String id) {
boolean deleted = taskService.deleteTask(id);
if (deleted) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
@PostMapping("/sync")
public ResponseEntity<String> triggerSync() {
return ResponseEntity.ok("Sync triggered");
}
}7. 配置文件
# 同步服務(wù)器地址 sync.server.url=https://api.example.com/tasks
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
- 離線優(yōu)先設(shè)計(jì),保證應(yīng)用在任何網(wǎng)絡(luò)狀態(tài)下可用
- 高性能的本地存儲引擎,適合大量數(shù)據(jù)
- 支持完整的CRUD操作和離線同步
- 細(xì)粒度的操作跟蹤,便于解決沖突
缺點(diǎn):
- 實(shí)現(xiàn)復(fù)雜度較高
- 同步策略需要根據(jù)業(yè)務(wù)場景定制
- 不支持復(fù)雜的關(guān)系型查詢
適用場景
• 需要全面離線支持的企業(yè)應(yīng)用
• 現(xiàn)場操作類系統(tǒng),如倉庫管理、物流系統(tǒng)
• 數(shù)據(jù)量較大的離線應(yīng)用
• 需要嚴(yán)格保證離線和在線數(shù)據(jù)一致性的場景
五、嵌入式消息隊(duì)列與異步處理
原理介紹
該方案使用嵌入式消息隊(duì)列(如ActiveMQ Artemis嵌入模式)實(shí)現(xiàn)離線操作的異步處理和持久化。
操作被發(fā)送到本地隊(duì)列,在網(wǎng)絡(luò)恢復(fù)后批量處理。
實(shí)現(xiàn)步驟
1. 添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-artemis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-server</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-jms-server</artifactId>
</dependency>2. 配置嵌入式Artemis
@Configuration
@Slf4j
public class ArtemisConfig {
@Value("${artemis.embedded.data-directory:./artemis-data}")
private String dataDirectory;
@Value("${artemis.embedded.queues:offlineOperations}")
private String queues;
@Bean
public ActiveMQServer activeMQServer() throws Exception {
Configuration config = new ConfigurationImpl();
config.setPersistenceEnabled(true);
config.setJournalDirectory(dataDirectory + "/journal");
config.setBindingsDirectory(dataDirectory + "/bindings");
config.setLargeMessagesDirectory(dataDirectory + "/largemessages");
config.setPagingDirectory(dataDirectory + "/paging");
config.addAcceptorConfiguration("in-vm", "vm://0");
config.addAddressSetting("#",
new AddressSettings()
.setDeadLetterAddress(SimpleString.toSimpleString("DLQ"))
.setExpiryAddress(SimpleString.toSimpleString("ExpiryQueue")));
ActiveMQServer server = new ActiveMQServerImpl(config);
server.start();
// 創(chuàng)建隊(duì)列
Arrays.stream(queues.split(","))
.forEach(queue -> {
try {
server.createQueue(
SimpleString.toSimpleString(queue),
RoutingType.ANYCAST,
SimpleString.toSimpleString(queue),
null,
true,
false
);
} catch (Exception e) {
log.error(e.getMessage(),e);
}
});
return server;
}
@Bean
public ConnectionFactory connectionFactory() {
return new ActiveMQConnectionFactory("vm://0");
}
@Bean
public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {
JmsTemplate template = new JmsTemplate(connectionFactory);
template.setDeliveryPersistent(true);
return template;
}
}3. 創(chuàng)建離線操作消息服務(wù)
@Service
public class OfflineMessageService {
private final JmsTemplate jmsTemplate;
private final ObjectMapper objectMapper;
@Value("${artemis.queue.operations:offlineOperations}")
private String operationsQueue;
@Autowired
public OfflineMessageService(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
this.objectMapper = new ObjectMapper();
}
public void sendOperation(OfflineOperation operation) {
try {
String json = objectMapper.writeValueAsString(operation);
jmsTemplate.convertAndSend(operationsQueue, json);
} catch (Exception e) {
throw new RuntimeException("Failed to send operation to queue", e);
}
}
public OfflineOperation receiveOperation() {
try {
String json = (String) jmsTemplate.receiveAndConvert(operationsQueue);
if (json == null) {
return null;
}
return objectMapper.readValue(json, OfflineOperation.class);
} catch (Exception e) {
throw new RuntimeException("Failed to receive operation from queue", e);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class OfflineOperation {
private String type; // CREATE, UPDATE, DELETE
private String endpoint; // API endpoint
private String id; // resource id
private String payload; // JSON payload
private long timestamp;
}
}4. 創(chuàng)建離線操作處理服務(wù)
@Service
public class OrderService {
private final OfflineMessageService messageService;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
@Value("${api.base-url}")
private String apiBaseUrl;
@Autowired
public OrderService(OfflineMessageService messageService, RestTemplate restTemplate) {
this.messageService = messageService;
this.restTemplate = restTemplate;
}
// 創(chuàng)建訂單 - 直接進(jìn)入離線隊(duì)列
public void createOrder(Order order) {
try {
// 生成ID
if (order.getId() == null) {
order.setId(UUID.randomUUID().toString());
}
order.setCreatedAt(System.currentTimeMillis());
order.setStatus("PENDING");
String payload = objectMapper.writeValueAsString(order);
OfflineMessageService.OfflineOperation operation = new OfflineMessageService.OfflineOperation(
"CREATE",
"orders",
order.getId(),
payload,
System.currentTimeMillis()
);
messageService.sendOperation(operation);
} catch (Exception e) {
throw new RuntimeException("Failed to create order", e);
}
}
// 更新訂單狀態(tài) - 直接進(jìn)入離線隊(duì)列
public void updateOrderStatus(String orderId, String status) {
try {
Map<String, Object> update = new HashMap<>();
update.put("status", status);
update.put("updatedAt", System.currentTimeMillis());
String payload = objectMapper.writeValueAsString(update);
OfflineMessageService.OfflineOperation operation = new OfflineMessageService.OfflineOperation(
"UPDATE",
"orders",
orderId,
payload,
System.currentTimeMillis()
);
messageService.sendOperation(operation);
} catch (Exception e) {
throw new RuntimeException("Failed to update order status", e);
}
}
// 處理離線隊(duì)列中的操作 - 由定時(shí)任務(wù)觸發(fā)
@Scheduled(fixedDelay = 60000) // 每分鐘執(zhí)行一次
public void processOfflineOperations() {
if (!isNetworkAvailable()) {
return; // 網(wǎng)絡(luò)不可用,跳過處理
}
int processedCount = 0;
while (processedCount < 50) { // 一次處理50條,防止阻塞太久
OfflineMessageService.OfflineOperation operation = messageService.receiveOperation();
if (operation == null) {
break; // 隊(duì)列為空
}
boolean success = processOperation(operation);
if (!success) {
// 處理失敗,重新入隊(duì)(可以考慮添加重試次數(shù)限制)
messageService.sendOperation(operation);
break; // 暫停處理,等待下一次調(diào)度
}
processedCount++;
}
}
private boolean processOperation(OfflineMessageService.OfflineOperation operation) {
try {
String url = apiBaseUrl + "/" + operation.getEndpoint();
if (operation.getId() != null && !operation.getType().equals("CREATE")) {
url += "/" + operation.getId();
}
HttpMethod method;
switch (operation.getType()) {
case "CREATE":
method = HttpMethod.POST;
break;
case "UPDATE":
method = HttpMethod.PUT;
break;
case "DELETE":
method = HttpMethod.DELETE;
break;
default:
return false;
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = operation.getType().equals("DELETE") ?
new HttpEntity<>(headers) :
new HttpEntity<>(operation.getPayload(), headers);
ResponseEntity<String> response = restTemplate.exchange(url, method, request, String.class);
return response.getStatusCode().isSuccessful();
} catch (Exception e) {
log.error(e.getMessage(),e);
return false;
}
}
private boolean isNetworkAvailable() {
try {
URL url = new URL(apiBaseUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(3000);
connection.connect();
return connection.getResponseCode() == 200;
} catch (Exception e) {
return false;
}
}
}5. 創(chuàng)建訂單模型
@Data
public class Order {
private String id;
private String customerName;
private List<OrderItem> items;
private BigDecimal totalAmount;
private String status;
private long createdAt;
private Long updatedAt;
}
@Data
public class OrderItem {
private String productId;
private String productName;
private int quantity;
private BigDecimal price;
}6. 創(chuàng)建Controller
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
@Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<String> createOrder(@RequestBody Order order) {
orderService.createOrder(order);
return ResponseEntity.ok("Order submitted for processing");
}
@PutMapping("/{id}/status")
public ResponseEntity<String> updateOrderStatus(
@PathVariable String id,
@RequestParam String status) {
orderService.updateOrderStatus(id, status);
return ResponseEntity.ok("Status update submitted for processing");
}
@PostMapping("/process")
public ResponseEntity<String> triggerProcessing() {
orderService.processOfflineOperations();
return ResponseEntity.ok("Processing triggered");
}
}7. 配置文件
# API配置 api.base-url=https://api.example.com # Artemis配置 artemis.embedded.data-directory=./artemis-data artemis.embedded.queues=offlineOperations artemis.queue.operations=offlineOperations
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
- 強(qiáng)大的消息持久化能力,確保操作不丟失
- 異步處理模式,非阻塞用戶操作
- 支持大批量數(shù)據(jù)處理
- 內(nèi)置的消息重試和死信機(jī)制
缺點(diǎn):
- 資源消耗較大,尤其是內(nèi)存和磁盤
- 配置相對復(fù)雜
- 需要處理消息冪等性問題
- 不適合需要即時(shí)反饋的場景
適用場景
• 批量數(shù)據(jù)處理場景,如訂單處理系統(tǒng)
• 需要可靠消息處理的工作流應(yīng)用
• 高并發(fā)寫入場景
• 對操作順序有嚴(yán)格要求的業(yè)務(wù)場景
六、方案對比
| 方案 | 復(fù)雜度 | 數(shù)據(jù)容量 | 沖突處理 | 適用場景 | 開發(fā)維護(hù)成本 |
|---|---|---|---|---|---|
| 嵌入式數(shù)據(jù)庫 | 中 | 中 | 較復(fù)雜 | 單機(jī)應(yīng)用、結(jié)構(gòu)化數(shù)據(jù) | 中 |
| 本地緩存 | 低 | 小 | 簡單 | 讀多寫少、數(shù)據(jù)量小 | 低 |
| 離線優(yōu)先架構(gòu) | 高 | 大 | 完善 | 企業(yè)應(yīng)用、現(xiàn)場系統(tǒng) | 高 |
| 嵌入式消息隊(duì)列 | 高 | 大 | 中等 | 批量處理、異步操作 | 高 |
總結(jié)
在實(shí)際應(yīng)用中,可以根據(jù)項(xiàng)目特點(diǎn)選擇合適的方案,也可以結(jié)合多種方案的優(yōu)點(diǎn),定制最適合自己需求的離線解決方案。
無論選擇哪種方案,完善的數(shù)據(jù)同步策略和良好的用戶體驗(yàn)都是成功實(shí)現(xiàn)離線應(yīng)用的關(guān)鍵因素。
以上就是基于SpringBoot實(shí)現(xiàn)離線應(yīng)用的4種實(shí)現(xiàn)方式的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot離線應(yīng)用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring數(shù)據(jù)庫連接池實(shí)現(xiàn)原理深入刨析
開發(fā)web項(xiàng)目,我們肯定會和數(shù)據(jù)庫打交道,因此就會涉及到數(shù)據(jù)庫鏈接的問題。在以前我們開發(fā)傳統(tǒng)的SSM結(jié)構(gòu)的項(xiàng)目時(shí)進(jìn)行數(shù)據(jù)庫鏈接都是通過JDBC進(jìn)行數(shù)據(jù)鏈接,我們每和數(shù)據(jù)庫打一次交道都需要先獲取一次鏈接,操作完后再關(guān)閉鏈接,這樣子效率很低,因此就出現(xiàn)了連接池2022-11-11
解決因jdk版本引起的TypeNotPresentExceptionProxy異常
這篇文章介紹了解決因jdk版本引起的TypeNotPresentExceptionProxy異常的方法,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-12-12
Spring Security實(shí)現(xiàn)5次密碼錯誤觸發(fā)賬號自動鎖定功能
在現(xiàn)代互聯(lián)網(wǎng)應(yīng)用中,賬號安全是重中之重,然而,暴力 破解攻擊依然是最常見的安全威脅之一,攻擊者通過自動化腳本嘗試大量的用戶名和密碼組合,試圖找到漏洞進(jìn)入系統(tǒng),所以為了解決這一問題,賬號鎖定機(jī)制被廣泛應(yīng)用,本文介紹了Spring Security實(shí)現(xiàn)5次密碼錯誤觸發(fā)賬號鎖定功能2024-12-12
java實(shí)現(xiàn)TCP socket和UDP socket的實(shí)例
這篇文章主要介紹了本文主要介紹了java實(shí)現(xiàn)TCP socket和UDP socket的實(shí)例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02
SpringBoot實(shí)現(xiàn)定時(shí)任務(wù)和異步調(diào)用
這篇文章主要為大家詳細(xì)介紹了SpringBoot實(shí)現(xiàn)定時(shí)任務(wù)和異步調(diào)用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-04-04
Java 文件傳輸助手的實(shí)現(xiàn)(單機(jī)版)
這篇文章主要介紹了Java 文件傳輸助手的實(shí)現(xiàn)(單機(jī)版),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05

