using FreeSql; using LiveForum.Code.Base; using LiveForum.Code.Redis.Contract; using LiveForum.Model; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace LiveForum.WebApi.BackgroundServices { /// /// 浏览批量同步后台服务 /// 从Redis Hash获取待处理操作,定时批量同步到数据库 /// 触发条件:每2分钟执行一次 /// public class ViewBatchSyncService : BackgroundService { private readonly IRedisService _redisService; private readonly IFreeSql _fsql; private readonly ILogger _logger; private const string VIEW_PENDING_OPERATIONS_KEY = "view:pending:operations"; private static readonly TimeSpan SYNC_INTERVAL = TimeSpan.FromSeconds(30); // 缩短间隔 private const int MAX_BATCH_SIZE = 100; // 限制批次大小 public ViewBatchSyncService( IRedisService redisService, IFreeSql fsql, ILogger logger) { _redisService = redisService; _fsql = fsql; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("[ViewBatchSync] 浏览批量同步服务已启动"); while (!stoppingToken.IsCancellationRequested) { try { // 等待同步间隔 await Task.Delay(SYNC_INTERVAL, stoppingToken); _logger.LogDebug("[ViewBatchSync] 开始定时批量同步"); await ProcessBatchSyncAsync(stoppingToken); } catch (OperationCanceledException) { // 正常停止 break; } catch (Exception ex) { _logger.LogError(ex, "[ViewBatchSync] 批量同步过程中发生异常"); // 发生异常后等待30秒再继续 await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } } _logger.LogInformation("[ViewBatchSync] 浏览批量同步服务已停止"); } /// /// 执行批量同步 /// private async Task ProcessBatchSyncAsync(CancellationToken stoppingToken) { try { // 1. 限量获取待处理操作,避免一次性处理过多数据 var (viewCountsByPostId, processedFields) = await GetLimitedOperationsAsync(); if (viewCountsByPostId.Count == 0) { _logger.LogDebug("[ViewBatchSync] 没有待处理的浏览操作"); return; } _logger.LogInformation("[ViewBatchSync] 本次处理 {Count} 个帖子的浏览更新", viewCountsByPostId.Count); // 2. 批量更新数据库 await ProcessDatabaseOperationsAsync(viewCountsByPostId, stoppingToken); // 3. 清除已处理的待处理记录 if (processedFields.Count > 0) { foreach (var field in processedFields) { await _redisService.HashDeleteAsync(VIEW_PENDING_OPERATIONS_KEY, field); } _logger.LogInformation("[ViewBatchSync] 已清除 {Count} 个已处理的操作记录", processedFields.Count); } _logger.LogInformation("[ViewBatchSync] 批量同步完成。更新了 {Count} 个帖子的浏览次数", viewCountsByPostId.Count); } catch (Exception ex) { _logger.LogError(ex, "[ViewBatchSync] 批量同步处理失败"); throw; } } /// /// 限量获取Redis中的操作,避免一次性处理过多 /// private async Task<(Dictionary viewCountsByPostId, List processedFields)> GetLimitedOperationsAsync() { var viewCountsByPostId = new Dictionary(); var processedFields = new List(); var processedCount = 0; // 获取所有操作,但限制处理数量 var allOperations = await _redisService.HashGetAllAsync(VIEW_PENDING_OPERATIONS_KEY); if (allOperations == null || allOperations.Length == 0) { return (viewCountsByPostId, processedFields); } foreach (var entry in allOperations) { if (processedCount >= MAX_BATCH_SIZE) break; var field = entry.Name.ToString(); // {userId}:{postId} var value = entry.Value.ToString(); try { var parts = field.Split(':'); if (parts.Length != 2) { _logger.LogWarning("[ViewBatchSync] 无效的操作字段格式: {Field}", field); continue; } if (!long.TryParse(parts[0], out var userId) || !long.TryParse(parts[1], out var postId)) { _logger.LogWarning("[ViewBatchSync] 解析操作字段失败: {Field}", field); continue; } processedFields.Add(field); // 按postId分组,统计每个帖子需要增加的浏览次数 if (viewCountsByPostId.ContainsKey(postId)) { viewCountsByPostId[postId]++; } else { viewCountsByPostId[postId] = 1; } processedCount++; } catch (Exception ex) { _logger.LogWarning(ex, "[ViewBatchSync] 解析操作失败。Field: {Field}, Value: {Value}", field, value); } } return (viewCountsByPostId, processedFields); } /// /// 处理数据库操作 /// private async Task ProcessDatabaseOperationsAsync( Dictionary viewCountsByPostId, CancellationToken stoppingToken) { const int maxRetries = 3; var retryCount = 0; while (retryCount < maxRetries) { try { // 使用单条SQL批量更新,避免逐条操作 var updateCases = viewCountsByPostId .OrderBy(kvp => kvp.Key) // 统一顺序,避免死锁 .Select(kvp => $"WHEN {kvp.Key} THEN ViewCount + {kvp.Value}") .ToList(); if (updateCases.Any()) { var postIds = string.Join(",", viewCountsByPostId.Keys.OrderBy(x => x)); var sql = $@" UPDATE T_Posts WITH (UPDLOCK, READPAST, ROWLOCK) SET ViewCount = CASE Id {string.Join(" ", updateCases)} ELSE ViewCount END WHERE Id IN ({postIds})"; var affectedRows = await _fsql.Ado.ExecuteNonQueryAsync(sql); _logger.LogInformation("[ViewBatchSync] 成功更新了 {Count} 个帖子的浏览次数,影响 {Rows} 行", viewCountsByPostId.Count, affectedRows); } return; // 成功则退出重试循环 } catch (Exception ex) when (IsDeadlockException(ex) && retryCount < maxRetries - 1) { retryCount++; var delay = TimeSpan.FromMilliseconds(100 * Math.Pow(2, retryCount)); _logger.LogWarning(ex, "[ViewBatchSync] 检测到死锁,第 {Retry} 次重试,延迟 {Delay}ms", retryCount, delay.TotalMilliseconds); await Task.Delay(delay, stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "[ViewBatchSync] 处理数据库操作时发生异常"); throw; } } } private bool IsDeadlockException(Exception ex) { return ex.Message.Contains("deadlock") || ex.Message.Contains("deadlocked") || ex.Message.Contains("was chosen as the deadlock victim"); } public override Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("[ViewBatchSync] 正在停止浏览批量同步服务..."); return base.StopAsync(cancellationToken); } } }