using FreeSql; using LiveForum.Code.Redis.Contract; using LiveForum.Model; using LiveForum.Code.Base; 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获取待处理操作,定时批量同步到数据库 /// 触发条件:每1分钟执行一次 /// public class LikeBatchSyncService : BackgroundService { private readonly IRedisService _redisService; private readonly IFreeSql _fsql; private readonly ILogger _logger; private const string LIKE_PENDING_OPERATIONS_KEY = "like:pending:operations"; private static readonly TimeSpan SYNC_INTERVAL = TimeSpan.FromSeconds(30); // 缩短间隔 private const int MAX_BATCH_SIZE = 100; // 限制批次大小 public LikeBatchSyncService( IRedisService redisService, IFreeSql fsql, ILogger logger) { _redisService = redisService; _fsql = fsql; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("[LikeBatchSync] 点赞批量同步服务已启动"); while (!stoppingToken.IsCancellationRequested) { try { // 等待同步间隔 await Task.Delay(SYNC_INTERVAL, stoppingToken); _logger.LogDebug("[LikeBatchSync] 开始定时批量同步"); await ProcessBatchSyncAsync(stoppingToken); } catch (OperationCanceledException) { // 正常停止 break; } catch (Exception ex) { _logger.LogError(ex, "[LikeBatchSync] 批量同步过程中发生异常"); // 发生异常后等待30秒再继续 await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } } _logger.LogInformation("[LikeBatchSync] 点赞批量同步服务已停止"); } /// /// 执行批量同步 /// private async Task ProcessBatchSyncAsync(CancellationToken stoppingToken) { try { // 1. 限量获取待处理操作,避免一次性处理过多数据 var (toInsert, toDelete, processedFields) = await GetLimitedOperationsAsync(); if (toInsert.Count == 0 && toDelete.Count == 0) { _logger.LogDebug("[LikeBatchSync] 没有待处理的点赞操作"); return; } _logger.LogInformation("[LikeBatchSync] 本次处理:插入 {InsertCount},删除 {DeleteCount}", toInsert.Count, toDelete.Count); // 2. 批量写入数据库 await ProcessDatabaseOperationsAsync(toInsert, toDelete, stoppingToken); // 3. 清除已处理的待处理记录 if (processedFields.Count > 0) { foreach (var field in processedFields) { await _redisService.HashDeleteAsync(LIKE_PENDING_OPERATIONS_KEY, field); } _logger.LogInformation("[LikeBatchSync] 已清除 {Count} 个已处理的操作记录", processedFields.Count); } _logger.LogInformation("[LikeBatchSync] 批量同步完成。插入: {InsertCount}, 删除: {DeleteCount}", toInsert.Count, toDelete.Count); } catch (Exception ex) { _logger.LogError(ex, "[LikeBatchSync] 批量同步处理失败"); throw; } } /// /// 限量获取Redis中的操作,避免一次性处理过多 /// private async Task<(List<(long UserId, int TargetType, long TargetId)> toInsert, List<(long UserId, int TargetType, long TargetId)> toDelete, List processedFields)> GetLimitedOperationsAsync() { var operationsByKey = new Dictionary(); var processedCount = 0; // 获取所有操作,但限制处理数量 var allOperations = await _redisService.HashGetAllAsync(LIKE_PENDING_OPERATIONS_KEY); if (allOperations == null || allOperations.Length == 0) { return (new List<(long, int, long)>(), new List<(long, int, long)>(), new List()); } foreach (var entry in allOperations) { if (processedCount >= MAX_BATCH_SIZE) break; var field = entry.Name.ToString(); // {userId}:{targetType}:{targetId} var value = entry.Value.ToString(); try { var operation = JsonSerializer.Deserialize(value, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (operation != null) { // 如果已存在,比较时间戳,保留最新的 if (operationsByKey.TryGetValue(field, out var existing)) { if (operation.OccurredAt > existing.OccurredAt) { operationsByKey[field] = (operation.Action, operation.OccurredAt); } } else { operationsByKey[field] = (operation.Action, operation.OccurredAt); } processedCount++; } } catch (Exception ex) { _logger.LogWarning(ex, "[LikeBatchSync] 解析操作失败。Field: {Field}, Value: {Value}", field, value); } } // 准备批量插入和删除 var toInsert = new List<(long UserId, int TargetType, long TargetId)>(); var toDelete = new List<(long UserId, int TargetType, long TargetId)>(); var processedFields = new List(); foreach (var kvp in operationsByKey) { var field = kvp.Key; // {userId}:{targetType}:{targetId} var parts = field.Split(':'); if (parts.Length != 3) { _logger.LogWarning("[LikeBatchSync] 无效的操作字段格式: {Field}", field); continue; } if (!long.TryParse(parts[0], out var userId) || !int.TryParse(parts[1], out var targetType) || !long.TryParse(parts[2], out var targetId)) { _logger.LogWarning("[LikeBatchSync] 解析操作字段失败: {Field}", field); continue; } processedFields.Add(field); if (kvp.Value.Action == "INSERT") { toInsert.Add((userId, targetType, targetId)); } else if (kvp.Value.Action == "DELETE") { toDelete.Add((userId, targetType, targetId)); } } return (toInsert, toDelete, processedFields); } /// /// 处理数据库操作 /// private async Task ProcessDatabaseOperationsAsync( List<(long UserId, int TargetType, long TargetId)> toInsert, List<(long UserId, int TargetType, long TargetId)> toDelete, CancellationToken stoppingToken) { const int maxRetries = 3; var retryCount = 0; while (retryCount < maxRetries) { try { // 使用FreeSql的事务 using (var uow = _fsql.CreateUnitOfWork()) { try { var likesRepo = uow.GetRepository(); // 1. 批量插入(先查询已存在的,避免重复插入) if (toInsert.Count > 0) { var userIds = toInsert.Select(x => x.UserId).Distinct().ToList(); var targetTypes = toInsert.Select(x => x.TargetType).Distinct().ToList(); var targetIds = toInsert.Select(x => x.TargetId).Distinct().ToList(); var existing = await likesRepo.Select .Where(x => userIds.Contains(x.UserId) && targetTypes.Contains(x.TargetType) && targetIds.Contains(x.TargetId)) .ToListAsync(); var existingKeys = existing.Select(x => (x.UserId, x.TargetType, x.TargetId)).ToHashSet(); var insertList = toInsert .Where(x => !existingKeys.Contains((x.UserId, x.TargetType, x.TargetId))) .Select(x => new T_Likes { UserId = x.UserId, TargetType = x.TargetType, TargetId = x.TargetId, CreatedAt = DateTime.Now }) .ToList(); if (insertList.Count > 0) { await likesRepo.InsertAsync(insertList); _logger.LogInformation("[LikeBatchSync] 批量插入 {Count} 条点赞记录", insertList.Count); } } // 2. 批量删除(使用SQL批量删除) if (toDelete.Count > 0) { // 按顺序排序,避免死锁 var sortedDeletes = toDelete.OrderBy(x => x.UserId).ThenBy(x => x.TargetType).ThenBy(x => x.TargetId).ToList(); // 构建批量删除SQL var deleteConditions = sortedDeletes.Select((item, index) => $"(UserId = @UserId{index} AND TargetType = @TargetType{index} AND TargetId = @TargetId{index})") .ToList(); var parameters = new Dictionary(); for (int i = 0; i < sortedDeletes.Count; i++) { parameters[$"@UserId{i}"] = sortedDeletes[i].UserId; parameters[$"@TargetType{i}"] = sortedDeletes[i].TargetType; parameters[$"@TargetId{i}"] = sortedDeletes[i].TargetId; } var sql = $@" DELETE FROM T_Likes WITH (UPDLOCK, READPAST) WHERE {string.Join(" OR ", deleteConditions)}"; var deletedRows = await _fsql.Ado.ExecuteNonQueryAsync(sql, parameters); _logger.LogInformation("[LikeBatchSync] 批量删除 {Count} 条点赞记录", deletedRows); } // 3. 提交事务 uow.Commit(); } catch (Exception ex) { uow.Rollback(); _logger.LogError(ex, "[LikeBatchSync] 数据库操作失败,已回滚"); throw; } } return; // 成功则退出重试循环 } catch (Exception ex) when (IsDeadlockException(ex) && retryCount < maxRetries - 1) { retryCount++; var delay = TimeSpan.FromMilliseconds(100 * Math.Pow(2, retryCount)); _logger.LogWarning(ex, "[LikeBatchSync] 检测到死锁,第 {Retry} 次重试,延迟 {Delay}ms", retryCount, delay.TotalMilliseconds); await Task.Delay(delay, stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "[LikeBatchSync] 处理数据库操作时发生异常"); throw; } } } private bool IsDeadlockException(Exception ex) { return ex.Message.Contains("deadlock") || ex.Message.Contains("deadlocked") || ex.Message.Contains("was chosen as the deadlock victim"); } /// /// 操作信息(用于反序列化) /// private class OperationInfo { public string Action { get; set; } public DateTime OccurredAt { get; set; } public long? ReceiverId { get; set; } } public override Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("[LikeBatchSync] 正在停止点赞批量同步服务..."); return base.StopAsync(cancellationToken); } } }