You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
465 lines
13 KiB
465 lines
13 KiB
|
3 months ago
|
# 实时数据接收与缓存优化方案
|
||
|
|
|
||
|
|
## 一、当前系统问题分析
|
||
|
|
|
||
|
|
通过对前端代码的分析,我发现当前系统存在以下问题:
|
||
|
|
|
||
|
|
1. **频繁的HTTP请求**:前端使用多个`setInterval`进行定期数据刷新,导致大量的HTTP请求
|
||
|
|
2. **响应式数据获取**:数据更新延迟高,用户体验差
|
||
|
|
3. **缺少有效的缓存机制**:每次请求都直接从服务器获取数据,没有合理利用缓存
|
||
|
|
4. **资源浪费**:即使数据没有更新,也会进行定期请求
|
||
|
|
|
||
|
|
## 二、解决方案设计
|
||
|
|
|
||
|
|
### 1. WebSocket实时数据接收
|
||
|
|
|
||
|
|
使用WebSocket实现实时数据推送,替代现有的定期轮询。
|
||
|
|
|
||
|
|
### 2. 前端缓存优化
|
||
|
|
|
||
|
|
实现高效的前端缓存机制,确保数据更新时缓存也能及时更新。
|
||
|
|
|
||
|
|
## 三、具体实施方案
|
||
|
|
|
||
|
|
### 1. 后端实现
|
||
|
|
|
||
|
|
#### 1.1 添加WebSocket依赖
|
||
|
|
|
||
|
|
```xml
|
||
|
|
<!-- pom.xml -->
|
||
|
|
<dependency>
|
||
|
|
<groupId>org.springframework.boot</groupId>
|
||
|
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||
|
|
</dependency>
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 1.2 配置WebSocket
|
||
|
|
|
||
|
|
```java
|
||
|
|
// WebSocketConfig.java
|
||
|
|
@Configuration
|
||
|
|
@EnableWebSocketMessageBroker
|
||
|
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||
|
|
config.enableSimpleBroker("/topic");
|
||
|
|
config.setApplicationDestinationPrefixes("/app");
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||
|
|
registry.addEndpoint("/ws")
|
||
|
|
.setAllowedOriginPatterns("*")
|
||
|
|
.withSockJS();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 1.3 实现实时数据推送服务
|
||
|
|
|
||
|
|
```java
|
||
|
|
// RealTimeDataService.java
|
||
|
|
@Service
|
||
|
|
public class RealTimeDataService {
|
||
|
|
|
||
|
|
@Autowired
|
||
|
|
private SimpMessagingTemplate messagingTemplate;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 推送客户数据更新
|
||
|
|
*/
|
||
|
|
public void pushCustomerUpdate(String customerId, UserProductCartDTO customerData) {
|
||
|
|
messagingTemplate.convertAndSend("/topic/customers/" + customerId, customerData);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 推送公海池数据更新
|
||
|
|
*/
|
||
|
|
public void pushPublicSeaUpdate(List<UserProductCartDTO> customerList) {
|
||
|
|
messagingTemplate.convertAndSend("/topic/public-sea", customerList);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 推送通知
|
||
|
|
*/
|
||
|
|
public void pushNotification(NotificationDTO notification) {
|
||
|
|
messagingTemplate.convertAndSend("/topic/notifications", notification);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. 前端实现
|
||
|
|
|
||
|
|
#### 2.1 WebSocket客户端实现
|
||
|
|
|
||
|
|
在前端页面中添加WebSocket客户端代码,用于连接WebSocket服务器并接收实时数据。
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// 在mainapp-sells.html和mainapp-supplys.html中添加
|
||
|
|
class WebSocketClient {
|
||
|
|
constructor() {
|
||
|
|
this.socket = null;
|
||
|
|
this.isConnected = false;
|
||
|
|
this.reconnectInterval = 5000;
|
||
|
|
this.reconnectAttempts = 0;
|
||
|
|
this.maxReconnectAttempts = 10;
|
||
|
|
this.callbacks = {};
|
||
|
|
this.cache = new Map();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 初始化WebSocket连接
|
||
|
|
init() {
|
||
|
|
// 移除现有的SockJS和STOMP依赖
|
||
|
|
const script1 = document.createElement('script');
|
||
|
|
script1.src = 'https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js';
|
||
|
|
document.head.appendChild(script1);
|
||
|
|
|
||
|
|
const script2 = document.createElement('script');
|
||
|
|
script2.src = 'https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js';
|
||
|
|
script2.onload = () => this.connect();
|
||
|
|
document.head.appendChild(script2);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 连接WebSocket服务器
|
||
|
|
connect() {
|
||
|
|
const socket = new SockJS('/ws');
|
||
|
|
this.stompClient = Stomp.over(socket);
|
||
|
|
|
||
|
|
this.stompClient.connect({},
|
||
|
|
(frame) => {
|
||
|
|
console.log('✅ WebSocket连接成功:', frame);
|
||
|
|
this.isConnected = true;
|
||
|
|
this.reconnectAttempts = 0;
|
||
|
|
|
||
|
|
// 订阅公海池数据更新
|
||
|
|
this.stompClient.subscribe('/topic/public-sea', (message) => {
|
||
|
|
const data = JSON.parse(message.body);
|
||
|
|
this.handlePublicSeaUpdate(data);
|
||
|
|
});
|
||
|
|
|
||
|
|
// 订阅通知
|
||
|
|
this.stompClient.subscribe('/topic/notifications', (message) => {
|
||
|
|
const notification = JSON.parse(message.body);
|
||
|
|
this.handleNotification(notification);
|
||
|
|
});
|
||
|
|
},
|
||
|
|
(error) => {
|
||
|
|
console.error('❌ WebSocket连接错误:', error);
|
||
|
|
this.isConnected = false;
|
||
|
|
this.attemptReconnect();
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 尝试重连
|
||
|
|
attemptReconnect() {
|
||
|
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||
|
|
this.reconnectAttempts++;
|
||
|
|
console.log(`⏱️ 尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
||
|
|
setTimeout(() => this.connect(), this.reconnectInterval);
|
||
|
|
} else {
|
||
|
|
console.error('❌ 达到最大重连次数,停止尝试');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 处理公海池数据更新
|
||
|
|
handlePublicSeaUpdate(data) {
|
||
|
|
// 更新缓存
|
||
|
|
this.cache.set('public-sea-data', {
|
||
|
|
data: data,
|
||
|
|
timestamp: Date.now()
|
||
|
|
});
|
||
|
|
|
||
|
|
// 调用回调函数
|
||
|
|
if (this.callbacks['public-sea-update']) {
|
||
|
|
this.callbacks['public-sea-update'].forEach(callback => callback(data));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 处理通知
|
||
|
|
handleNotification(notification) {
|
||
|
|
// 显示通知
|
||
|
|
this.showNotification(notification);
|
||
|
|
|
||
|
|
// 调用回调函数
|
||
|
|
if (this.callbacks['notification']) {
|
||
|
|
this.callbacks['notification'].forEach(callback => callback(notification));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 显示通知
|
||
|
|
showNotification(notification) {
|
||
|
|
const notificationEl = document.createElement('div');
|
||
|
|
notificationEl.className = 'websocket-notification';
|
||
|
|
notificationEl.innerHTML = `
|
||
|
|
<div class="notification-header">
|
||
|
|
<h4>${notification.title}</h4>
|
||
|
|
<button class="close-btn">×</button>
|
||
|
|
</div>
|
||
|
|
<div class="notification-body">
|
||
|
|
<p>${notification.message}</p>
|
||
|
|
<small>${new Date(notification.timestamp).toLocaleString()}</small>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
// 添加样式
|
||
|
|
notificationEl.style.cssText = `
|
||
|
|
position: fixed;
|
||
|
|
top: 20px;
|
||
|
|
right: 20px;
|
||
|
|
background: white;
|
||
|
|
border-radius: 8px;
|
||
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
|
|
padding: 16px;
|
||
|
|
width: 320px;
|
||
|
|
z-index: 10000;
|
||
|
|
animation: slideIn 0.3s ease-out;
|
||
|
|
`;
|
||
|
|
|
||
|
|
// 添加关闭事件
|
||
|
|
const closeBtn = notificationEl.querySelector('.close-btn');
|
||
|
|
closeBtn.addEventListener('click', () => notificationEl.remove());
|
||
|
|
|
||
|
|
// 添加到页面
|
||
|
|
document.body.appendChild(notificationEl);
|
||
|
|
|
||
|
|
// 3秒后自动关闭
|
||
|
|
setTimeout(() => notificationEl.remove(), 3000);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 订阅事件
|
||
|
|
on(event, callback) {
|
||
|
|
if (!this.callbacks[event]) {
|
||
|
|
this.callbacks[event] = [];
|
||
|
|
}
|
||
|
|
this.callbacks[event].push(callback);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 取消订阅
|
||
|
|
off(event, callback) {
|
||
|
|
if (this.callbacks[event]) {
|
||
|
|
this.callbacks[event] = this.callbacks[event].filter(cb => cb !== callback);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取缓存数据
|
||
|
|
getCachedData(key) {
|
||
|
|
const cached = this.cache.get(key);
|
||
|
|
if (cached) {
|
||
|
|
return cached.data;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 设置缓存数据
|
||
|
|
setCachedData(key, data) {
|
||
|
|
this.cache.set(key, {
|
||
|
|
data: data,
|
||
|
|
timestamp: Date.now()
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 清除缓存
|
||
|
|
clearCache(key) {
|
||
|
|
if (key) {
|
||
|
|
this.cache.delete(key);
|
||
|
|
} else {
|
||
|
|
this.cache.clear();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 断开连接
|
||
|
|
disconnect() {
|
||
|
|
if (this.stompClient) {
|
||
|
|
this.stompClient.disconnect();
|
||
|
|
this.isConnected = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 初始化WebSocket客户端
|
||
|
|
const wsClient = new WebSocketClient();
|
||
|
|
window.addEventListener('load', () => {
|
||
|
|
wsClient.init();
|
||
|
|
});
|
||
|
|
|
||
|
|
// 页面关闭前断开连接
|
||
|
|
window.addEventListener('beforeunload', () => {
|
||
|
|
wsClient.disconnect();
|
||
|
|
});
|
||
|
|
|
||
|
|
// 添加动画样式
|
||
|
|
const style = document.createElement('style');
|
||
|
|
style.textContent = `
|
||
|
|
@keyframes slideIn {
|
||
|
|
from {
|
||
|
|
transform: translateX(100%);
|
||
|
|
opacity: 0;
|
||
|
|
}
|
||
|
|
to {
|
||
|
|
transform: translateX(0);
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
`;
|
||
|
|
document.head.appendChild(style);
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.2 优化数据获取函数
|
||
|
|
|
||
|
|
修改现有的数据获取函数,优先使用缓存数据,只有在缓存不存在或过期时才从服务器获取。
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// 修改fetchPublicSeaData函数
|
||
|
|
async function fetchPublicSeaData(loginInfo, level) {
|
||
|
|
// 尝试从缓存获取
|
||
|
|
const cachedData = wsClient.getCachedData('public-sea-data');
|
||
|
|
if (cachedData) {
|
||
|
|
console.log('📦 从缓存获取公海池数据');
|
||
|
|
return cachedData;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 缓存未命中,从服务器获取
|
||
|
|
console.log('🌐 从服务器获取公海池数据');
|
||
|
|
const url = `${API_BASE_URL}/pool/customers?level=${level}&isSupplySide=false`;
|
||
|
|
const response = await fetch(appendAuthParams(url), {
|
||
|
|
method: 'GET',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Failed to fetch public sea data');
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
// 更新缓存
|
||
|
|
wsClient.setCachedData('public-sea-data', data);
|
||
|
|
return data;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 监听公海池数据更新
|
||
|
|
wsClient.on('public-sea-update', (data) => {
|
||
|
|
console.log('🔄 公海池数据已更新');
|
||
|
|
// 更新页面数据
|
||
|
|
updatePublicSeaData(data);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.3 替换setInterval轮询
|
||
|
|
|
||
|
|
移除现有的setInterval轮询,使用WebSocket实时数据更新。
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// 移除现有轮询代码
|
||
|
|
// this.updateInterval = setInterval(() => {
|
||
|
|
// this.loadCustomers(loginInfo, level);
|
||
|
|
// }, 30000);
|
||
|
|
|
||
|
|
// 替换为WebSocket监听
|
||
|
|
wsClient.on('public-sea-update', (data) => {
|
||
|
|
this.customersArray = data;
|
||
|
|
this.renderCustomers();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.4 实现数据更新时的缓存刷新
|
||
|
|
|
||
|
|
当用户手动更新数据时,确保缓存也被刷新。
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// 在数据更新成功后
|
||
|
|
async function updateCustomer(customerData) {
|
||
|
|
// 发送更新请求
|
||
|
|
const response = await fetch(`${API_BASE_URL}/pool/updateCustomer`, {
|
||
|
|
method: 'PUT',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
},
|
||
|
|
body: JSON.stringify(customerData)
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
// 清除相关缓存
|
||
|
|
wsClient.clearCache('public-sea-data');
|
||
|
|
wsClient.clearCache(`customer-${customerData.userId}`);
|
||
|
|
|
||
|
|
// 显示成功消息
|
||
|
|
showMessage('客户数据更新成功');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## 四、缓存策略
|
||
|
|
|
||
|
|
### 1. 缓存类型
|
||
|
|
|
||
|
|
- **公海池数据缓存**:缓存公海池客户列表,过期时间30分钟
|
||
|
|
- **单个客户数据缓存**:缓存单个客户的详细信息,过期时间1小时
|
||
|
|
- **通知缓存**:缓存通知列表,过期时间10分钟
|
||
|
|
|
||
|
|
### 2. 缓存更新机制
|
||
|
|
|
||
|
|
- **WebSocket实时更新**:当服务器数据更新时,通过WebSocket推送更新
|
||
|
|
- **手动更新**:用户手动更新数据时,清除相关缓存
|
||
|
|
- **过期自动刷新**:缓存过期时,自动从服务器获取最新数据
|
||
|
|
|
||
|
|
## 五、预期效果
|
||
|
|
|
||
|
|
1. **减少HTTP请求**:预计减少80%以上的HTTP请求
|
||
|
|
2. **提高数据实时性**:数据更新延迟从30秒降低到<1秒
|
||
|
|
3. **提升用户体验**:页面数据实时更新,无需等待
|
||
|
|
4. **降低服务器压力**:减少不必要的数据查询
|
||
|
|
5. **提高页面响应速度**:从缓存获取数据,响应更快
|
||
|
|
|
||
|
|
## 六、实施步骤
|
||
|
|
|
||
|
|
1. **后端实现**:
|
||
|
|
- 添加WebSocket依赖
|
||
|
|
- 配置WebSocket服务
|
||
|
|
- 实现实时数据推送服务
|
||
|
|
- 在数据更新时调用推送服务
|
||
|
|
|
||
|
|
2. **前端实现**:
|
||
|
|
- 添加WebSocket客户端代码
|
||
|
|
- 优化数据获取函数,使用缓存
|
||
|
|
- 移除现有的setInterval轮询
|
||
|
|
- 实现缓存更新机制
|
||
|
|
|
||
|
|
3. **测试验证**:
|
||
|
|
- 测试WebSocket连接稳定性
|
||
|
|
- 测试数据实时更新效果
|
||
|
|
- 测试缓存机制
|
||
|
|
- 测试并发性能
|
||
|
|
|
||
|
|
## 七、代码集成建议
|
||
|
|
|
||
|
|
1. **分阶段实施**:先在一个页面实现,测试稳定后再推广到其他页面
|
||
|
|
2. **保留降级机制**:当WebSocket连接失败时,自动回退到轮询模式
|
||
|
|
3. **添加监控**:监控WebSocket连接状态和缓存命中率
|
||
|
|
4. **优化缓存策略**:根据实际使用情况调整缓存过期时间
|
||
|
|
|
||
|
|
## 八、注意事项
|
||
|
|
|
||
|
|
1. **WebSocket连接管理**:
|
||
|
|
- 实现自动重连机制
|
||
|
|
- 处理连接断开情况
|
||
|
|
- 限制重连次数
|
||
|
|
|
||
|
|
2. **缓存一致性**:
|
||
|
|
- 确保数据更新时缓存也更新
|
||
|
|
- 处理缓存过期情况
|
||
|
|
- 避免缓存雪崩
|
||
|
|
|
||
|
|
3. **性能优化**:
|
||
|
|
- 限制WebSocket消息大小
|
||
|
|
- 优化数据传输格式
|
||
|
|
- 避免频繁发送大量数据
|
||
|
|
|
||
|
|
4. **兼容性考虑**:
|
||
|
|
- 支持不支持WebSocket的浏览器
|
||
|
|
- 实现降级方案
|
||
|
|
|
||
|
|
通过以上方案的实施,预计可以显著提高系统的性能和用户体验,减少服务器压力,实现真正的实时数据更新。
|