# 改进方案 v2.0 - 主从库同步问题解决 **日期**: 2026-05-13 **更新**: 解决主从库数据同步延迟问题 --- ## 🔍 问题分析 ### 您指出的问题 > "数据库采用了主从库,所以对于先保存在update可能会有问题" **具体问题**: ``` 时间线: T0: 主库 - 保存PDF缓存 ✅ ↓ 主从同步延迟(通常100-500ms) T1: 从库 - 数据尚未到达 ❌ 如果此时有其他请求查询: - 从库查询 → 缓存未命中 → 重新下载PDF ❌ - 浪费网络带宽 ``` **您的建议**: > "我建议是可以同步运行" **含义**:条码识别和PDF缓存可以同时进行,而不是等待主从同步。 --- ## ✅ 改进方案 v2.0 ### 新流程:同步并行处理 **原方案** ❌: ``` 下载PDF ↓ 验证 ✅ ↓ 保存缓存 → 【等待主从同步】← 这是问题 ↓ (T+100ms) 异步条码识别 ``` **改进方案 v2.0** ✅: ``` 下载PDF ↓ 验证 ✅ ↓ 【同步进行】 ├─ 线程A:保存PDF缓存到数据库 │ └─ 主库:NeutralWaybillNumber, PdfBytes, OriginalUrl, Status=1 │ └─ 线程B:条码识别(不依赖缓存写入结果) ├─ ConvertPdfFirstPageToBitmap() ├─ ExtractBarcodeFromPdfAsync() └─ 识别完成 → 更新主库 └─ UPDATE: BarcodeNumber, BarcodeType, BarcodeConfidence 【关键】无论哪个线程先完成,都不影响用户响应 ``` --- ## 🔧 具体实施调整 ### 修改点1:分离关键字段 vs 可选字段 **第一步保存**(立即执行,核心数据): ```csharp await SaveCacheAsync( waybillNumber: waybillNumber, pdfBytes: pdfBytes, pageCount: pageCount, fileSize: pdfBytes.Length, originalUrl: request.Label, // ✅ 新增:保存原始URL finalMileTrackingNumber: finalMileTrackingNumber, customerId: customerId, status: CacheStatus.Success ); ``` **同时执行**(后台异步,不等待结果): ```csharp // 【立即返回给用户】 return Ok(new { success = true, pdfBytes = pdfBytes, cached = true }); // 【后台异步,不阻塞用户】 _ = Task.Run(async () => { try { var (barcode, type, confidence) = await ExtractBarcodeFromPdfAsync(pdfBytes); if (!string.IsNullOrEmpty(barcode)) { // 异步更新主库(非阻塞) await _repository.UpdateBarcodeInfoAsync( waybillNumber: waybillNumber, barcodeNumber: barcode, barcodeType: type, confidence: confidence ); } } catch (Exception ex) { _logger.LogWarning(ex, "条码识别失败: {waybillNumber}", waybillNumber); } }); ``` --- ### 修改点2:数据库字段调整 #### 第一阶段保存(核心字段) ```sql INSERT INTO label_pdf_cache ( NeutralWaybillNumber, -- 主键/唯一索引 PdfBytes, -- 二进制内容 PageCount, -- 页数 FileSize, -- 大小 OriginalUrl, -- ✅ 新增:原始URL FinalMileTrackingNumber, -- 尾程号 CustomerId, -- 客户ID Status, -- 1=成功 CreatedTime, -- 创建时间 UpdatedTime -- 更新时间 ) VALUES (...) ``` #### 第二阶段异步更新(可选字段) ```sql UPDATE label_pdf_cache SET BarcodeNumber = 'xxx', BarcodeType = 2, BarcodeConfidence = 92, BarcodeExtractTime = GETUTCDATE(), UpdatedTime = GETUTCDATE() WHERE NeutralWaybillNumber = 'xxx' ``` **关键**: - ✅ 第一阶段:必须同步完成(用户等待) - ✅ 第二阶段:完全异步(用户不等待) - ✅ 两者互不依赖,可以并行 --- ### 修改点3:数据库表结构 ```csharp [SugarTable("label_pdf_cache")] [Index(IndexName = "IX_NeutralWaybillNumber", Columns = "NeutralWaybillNumber", IsUnique = true)] public class LabelPdfCache { // 核心字段(第一阶段) public long Id { get; set; } public string NeutralWaybillNumber { get; set; } public byte[]? PdfBytes { get; set; } public int? PageCount { get; set; } public int? FileSize { get; set; } public byte Status { get; set; } // 关联字段(第一阶段) public string? OriginalUrl { get; set; } // ✅ 新增 public string? FinalMileTrackingNumber { get; set; } public string? CustomerId { get; set; } // 重试信息(第一阶段) public int RetryCount { get; set; } public DateTime? LastRetryTime { get; set; } public string? ErrorMessage { get; set; } // 时间戳(第一阶段) public DateTime CreatedTime { get; set; } public DateTime UpdatedTime { get; set; } // 条码字段(第二阶段,异步更新) public string? BarcodeNumber { get; set; } public byte BarcodeType { get; set; } public int? BarcodeConfidence { get; set; } public DateTime? BarcodeExtractTime { get; set; } } ``` --- ## 📊 处理流程时间线对比 ### 原方案(有主从同步问题) ``` T0ms: 下载PDF T10ms: 验证 T20ms: 保存到主库 T100ms: 【等待主从同步】 ⏳ 浪费时间 T120ms: 异步条码识别开始 T800ms: 条码识别完成 T800ms: 【返回给用户】 ``` ### 改进方案 v2.0(并行处理) ``` T0ms: 下载PDF T10ms: 验证 T20ms: 同时发起: ├─ 保存主库(线程A) └─ 条码识别(线程B)✅ 并行 T50ms: 保存完成 ✅ T50ms: 【立即返回给用户】✅ 不等条码 T100ms: 【主从同步完成】 T500ms: 条码识别完成 T500ms: 异步更新主库(非阻塞) ``` **效果**: - 用户等待时间:从800ms → **50ms**(加速16倍)✅ - 主从库优先保证PDF数据一致性 - 条码识别失败不影响用户 - 充分利用并行处理能力 --- ## 🔄 核心改进点总结 | 方面 | 原方案 | 改进v2.0 | 效果 | |------|-------|---------|------| | **流程** | 串行(等条码) | 并行(分离) | ⬆️ 16倍快 | | **缓存时机** | 等条码识别 | 立即保存 | ⬆️ 更快 | | **主从同步** | 阻塞用户 | 后台进行 | ⬆️ 无感 | | **条码失败** | 缓存也失败 | 缓存保留 | ⬆️ 可靠 | | **URL保存** | 不保存 | 保存 ✅ | ✅ 新增 | --- ## 💻 代码实施示例 ### LabelPdfCacheService.cs - 修改方式 ```csharp public async Task<(bool success, string message)> CacheAndRecognizeAsync( string waybillNumber, byte[] pdfBytes, string originalUrl, string? finalMileTrackingNumber = null, string? customerId = null) { // 第一步:验证 if (!ValidatePdf(pdfBytes)) { return (false, "PDF验证失败"); } // 第二步:同步保存核心数据 ✅ try { await SaveCacheAsync( waybillNumber: waybillNumber, pdfBytes: pdfBytes, pageCount: 1, fileSize: pdfBytes.Length, originalUrl: originalUrl, finalMileTrackingNumber: finalMileTrackingNumber, customerId: customerId, status: CacheStatus.Success ); } catch (Exception ex) { _logger.LogError(ex, "缓存保存失败"); return (false, "缓存保存失败"); } // 【立即返回成功给用户】✅ // 不等待条码识别 // 第三步:异步识别条码(后台,不阻塞)🔄 _ = Task.Run(async () => { try { var bitmap = ConvertPdfFirstPageToBitmap(pdfBytes); if (bitmap != null) { var (barcodeNumber, barcodeType, confidence) = await RecognizeBarcodeAsync(bitmap, BarcodeFormats, "barcode_recognition"); if (!string.IsNullOrEmpty(barcodeNumber)) { // 异步更新:不阻塞用户,失败不影响缓存 await _repository.UpdateBarcodeInfoAsync( waybillNumber: waybillNumber, barcodeNumber: barcodeNumber, barcodeType: barcodeType, confidence: confidence ); } } } catch (Exception ex) { // 日志记录,但不抛异常 _logger.LogWarning(ex, "条码识别失败(已缓存PDF): {waybillNumber}", waybillNumber); } }); return (true, "缓存成功"); } ``` ### LabelController.cs - 下载端点调用 ```csharp [HttpGet("waybill/{waybillNumber}/download")] public async Task DownloadLabel( string waybillNumber, CancellationToken cancellationToken) { // 1. 检查缓存 var cachedPdf = await _labelPdfCacheService.GetValidCacheAsync(waybillNumber); if (cachedPdf != null) { return File(cachedPdf, "application/pdf"); // ✅ 100ms返回 } // 2. 下载PDF var labelBytes = await DownloadPdfAsync(waybillNumber, cancellationToken); // 3. 【新增】分离缓存和条码识别 // 立即缓存,条码识别放到后台 _ = _labelPdfCacheService.CacheAndRecognizeAsync( waybillNumber: waybillNumber, pdfBytes: labelBytes, originalUrl: originalUrl, finalMileTrackingNumber: finalMileTrackingNumber, customerId: customerId // ↑ 这个方法会立即保存缓存,然后异步识别条码 ); // 4. 【立即返回给用户】✅ 不等条码识别 return File(labelBytes, "application/pdf"); } ``` --- ## 📋 数据库迁移脚本 ```sql -- 添加原始URL字段 ALTER TABLE label_pdf_cache ADD OriginalUrl NVARCHAR(500) NULL; -- 如果Status字段不存在,添加 IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'label_pdf_cache' AND COLUMN_NAME = 'Status') BEGIN ALTER TABLE label_pdf_cache ADD Status TINYINT DEFAULT 1; END -- 创建索引以加快查询 CREATE NONCLUSTERED INDEX IX_Status_UpdatedTime ON label_pdf_cache (Status, UpdatedTime DESC); -- 如果条码字段不存在,添加 IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'label_pdf_cache' AND COLUMN_NAME = 'BarcodeNumber') BEGIN ALTER TABLE label_pdf_cache ADD BarcodeNumber NVARCHAR(100) NULL, BarcodeType TINYINT DEFAULT 0, BarcodeConfidence INT NULL, BarcodeExtractTime DATETIME2 NULL; END ``` --- ## ✅ 改进v2.0的优势 1. **无主从同步阻塞** ✅ - 用户不等待从库同步 - 缓存数据快速到位 2. **条码识别不阻塞缓存** ✅ - 即使识别失败,PDF已缓存 - 业务连续性保证 3. **并行处理效率高** ✅ - 保存缓存 + 条码识别同时进行 - 充分利用多线程 4. **保存原始URL** ✅ - 便于问题追踪和重新下载 - 完整的审计信息 5. **实施复杂度低** ✅ - 只需调整异步Task的时机 - 现有代码影响最小 --- ## 🎯 最终确认 **改进方案 v2.0 包含以下内容**: - [x] GhostScript.NET PDF渲染 - [x] 同步保存核心数据(立即返回) - [x] 异步识别条码(并行处理) - [x] 添加原始URL字段 - [x] 解决主从库同步问题 - [x] 提升用户响应速度 **是否同意此版本开始实施?**