live-forum/server/webapi/LiveForum/LiveForum.WebApi/BackgroundServices/FlowerBatchSyncService.cs
2026-03-24 11:27:37 +08:00

464 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
{
/// <summary>
/// 送花批量同步后台服务
/// 从Redis Hash获取待处理操作定时批量同步到数据库
/// 触发条件每1分钟执行一次
/// </summary>
public class FlowerBatchSyncService : BackgroundService
{
private readonly IRedisService _redisService;
private readonly IFreeSql _fsql;
private readonly ILogger<FlowerBatchSyncService> _logger;
private const string FLOWER_PENDING_OPERATIONS_KEY = "flower:pending:operations";
private static readonly TimeSpan SYNC_INTERVAL = TimeSpan.FromSeconds(30); // 缩短间隔
private const int MAX_BATCH_SIZE = 100; // 限制批次大小
public FlowerBatchSyncService(
IRedisService redisService,
IFreeSql fsql,
ILogger<FlowerBatchSyncService> logger)
{
_redisService = redisService;
_fsql = fsql;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("[FlowerBatchSync] 送花批量同步服务已启动");
while (!stoppingToken.IsCancellationRequested)
{
try
{
// 等待同步间隔
await Task.Delay(SYNC_INTERVAL, stoppingToken);
_logger.LogDebug("[FlowerBatchSync] 开始定时批量同步");
await ProcessBatchSyncAsync(stoppingToken);
}
catch (OperationCanceledException)
{
// 正常停止
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "[FlowerBatchSync] 批量同步过程中发生异常");
// 发生异常后等待30秒再继续
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
_logger.LogInformation("[FlowerBatchSync] 送花批量同步服务已停止");
}
/// <summary>
/// 执行批量同步
/// </summary>
private async Task ProcessBatchSyncAsync(CancellationToken stoppingToken)
{
try
{
// 1. 限量获取待处理操作,避免一次性处理过多数据
var (toInsert, processedFields) = await GetLimitedOperationsAsync();
if (toInsert.Count == 0)
{
_logger.LogDebug("[FlowerBatchSync] 没有待处理的送花操作");
return;
}
_logger.LogInformation("[FlowerBatchSync] 本次处理 {Count} 条送花记录", toInsert.Count);
// 2. 批量写入数据库
await ProcessDatabaseOperationsAsync(toInsert, stoppingToken);
// 3. 清除已处理的待处理记录
if (processedFields.Count > 0)
{
foreach (var field in processedFields)
{
await _redisService.HashDeleteAsync(FLOWER_PENDING_OPERATIONS_KEY, field);
}
_logger.LogInformation("[FlowerBatchSync] 已清除 {Count} 个已处理的操作记录", processedFields.Count);
}
_logger.LogInformation("[FlowerBatchSync] 批量同步完成。处理: {Count} 条送花记录", toInsert.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "[FlowerBatchSync] 批量同步处理失败");
throw;
}
}
/// <summary>
/// 限量获取Redis中的操作避免一次性处理过多
/// </summary>
private async Task<(List<(long UserId, string TargetType, long TargetId, string SendMonth, DateTime SendTime, long? ReceiverId)> toInsert,
List<string> processedFields)> GetLimitedOperationsAsync()
{
var operationsByKey = new Dictionary<string, OperationInfo>();
var processedCount = 0;
// 获取所有操作,但限制处理数量
var allOperations = await _redisService.HashGetAllAsync(FLOWER_PENDING_OPERATIONS_KEY);
if (allOperations == null || allOperations.Length == 0)
{
return (new List<(long, string, long, string, DateTime, long?)>(), new List<string>());
}
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<OperationInfo>(value, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (operation != null)
{
// 如果已存在,比较时间戳,保留最新的
if (operationsByKey.TryGetValue(field, out var existing))
{
if (operation.OccurredAt > existing.OccurredAt)
{
operationsByKey[field] = operation;
}
}
else
{
operationsByKey[field] = operation;
}
processedCount++;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[FlowerBatchSync] 解析操作失败。Field: {Field}, Value: {Value}",
field, value);
}
}
// 准备批量插入数据
var toInsert = new List<(long UserId, string TargetType, long TargetId, string SendMonth, DateTime SendTime, long? ReceiverId)>();
var processedFields = new List<string>();
foreach (var kvp in operationsByKey)
{
var field = kvp.Key; // {userId}:{targetType}:{targetId}
var parts = field.Split(':');
if (parts.Length != 3)
{
_logger.LogWarning("[FlowerBatchSync] 无效的操作字段格式: {Field}", field);
continue;
}
if (!long.TryParse(parts[0], out var userId) ||
!long.TryParse(parts[2], out var targetId))
{
_logger.LogWarning("[FlowerBatchSync] 解析操作字段失败: {Field}", field);
continue;
}
var targetType = parts[1];
var operation = kvp.Value;
processedFields.Add(field);
toInsert.Add((userId, targetType, targetId, operation.SendMonth, operation.OccurredAt, operation.ReceiverId));
}
return (toInsert, processedFields);
}
/// <summary>
/// 处理数据库操作
/// </summary>
private async Task ProcessDatabaseOperationsAsync(
List<(long UserId, string TargetType, long TargetId, string SendMonth, DateTime SendTime, long? ReceiverId)> toInsert,
CancellationToken stoppingToken)
{
const int maxRetries = 3;
var retryCount = 0;
while (retryCount < maxRetries)
{
try
{
await ProcessDatabaseOperationsInternalAsync(toInsert, stoppingToken);
return; // 成功则退出
}
catch (Exception ex) when (ex.Message.Contains("FOREIGN KEY constraint") && retryCount < maxRetries - 1)
{
retryCount++;
_logger.LogWarning("[FlowerBatchSync] 外键约束错误,第 {RetryCount} 次重试", retryCount);
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
catch (Exception ex) when (IsDeadlockException(ex) && retryCount < maxRetries - 1)
{
retryCount++;
var delay = TimeSpan.FromMilliseconds(100 * Math.Pow(2, retryCount));
_logger.LogWarning(ex, "[FlowerBatchSync] 检测到死锁,第 {Retry} 次重试,延迟 {Delay}ms",
retryCount, delay.TotalMilliseconds);
await Task.Delay(delay, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "[FlowerBatchSync] 处理数据库操作时发生异常");
throw;
}
}
}
/// <summary>
/// 内部数据库操作处理
/// </summary>
private async Task ProcessDatabaseOperationsInternalAsync(
List<(long UserId, string TargetType, long TargetId, string SendMonth, DateTime SendTime, long? ReceiverId)> toInsert,
CancellationToken stoppingToken)
{
try
{
// 使用FreeSql的事务
using (var uow = _fsql.CreateUnitOfWork())
{
try
{
var flowerRecordsInfoRepo = uow.GetRepository<T_FlowerRecordsInfo>();
var flowerRecordsRepo = uow.GetRepository<T_FlowerRecords>();
var streamersRepo = uow.GetRepository<T_Streamers>();
var postsRepo = uow.GetRepository<T_Posts>();
var usersRepo = uow.GetRepository<T_Users>();
// 验证用户ID有效性过滤无效数据
var userIds = toInsert.Select(x => x.UserId).Distinct().ToList();
var validUserIds = await usersRepo.Select
.Where(u => userIds.Contains(u.Id))
.ToListAsync(u => u.Id);
var validUserIdSet = new HashSet<long>(validUserIds);
var originalCount = toInsert.Count;
toInsert = toInsert.Where(x => validUserIdSet.Contains(x.UserId)).ToList();
if (originalCount != toInsert.Count)
{
_logger.LogWarning("[FlowerBatchSync] 过滤掉 {InvalidCount} 条无效用户ID的记录剩余 {ValidCount} 条",
originalCount - toInsert.Count, toInsert.Count);
}
if (toInsert.Count == 0)
{
_logger.LogWarning("[FlowerBatchSync] 所有记录都包含无效用户ID跳过本批次");
return;
}
// 按SendMonth分组便于处理月度汇总
var groupedByMonth = toInsert.GroupBy(x => x.SendMonth).ToList();
foreach (var monthGroup in groupedByMonth)
{
var sendMonth = monthGroup.Key;
var monthOperations = monthGroup.ToList();
// 1. 批量插入送花详细记录
var flowerRecordsInfo = monthOperations.Select(x => new T_FlowerRecordsInfo
{
UserId = x.UserId,
TargetId = x.TargetId,
TargetType = x.TargetType,
FlowerCount = 1,
SendMonth = sendMonth,
SendTime = x.SendTime
}).ToList();
if (flowerRecordsInfo.Count > 0)
{
await flowerRecordsInfoRepo.InsertAsync(flowerRecordsInfo);
_logger.LogInformation("[FlowerBatchSync] 批量插入 {Count} 条送花详细记录,月份: {Month}",
flowerRecordsInfo.Count, sendMonth);
}
// 2. 处理月度汇总记录(按 (userId, targetType, targetId, sendMonth) 分组)
var monthlyGroups = monthOperations
.GroupBy(x => new { x.UserId, x.TargetType, x.TargetId })
.ToList();
foreach (var monthlyGroup in monthlyGroups)
{
var userId = monthlyGroup.Key.UserId;
var targetType = monthlyGroup.Key.TargetType;
var targetId = monthlyGroup.Key.TargetId;
var count = monthlyGroup.Count();
// 查询是否已存在月度汇总记录
var monthlyRecord = await flowerRecordsRepo.Select
.Where(r => r.UserId == userId
&& r.TargetId == targetId
&& r.TargetType == targetType
&& r.SendMonth == sendMonth)
.FirstAsync();
if (monthlyRecord != null)
{
monthlyRecord.FlowerCount += count;
await flowerRecordsRepo.UpdateAsync(monthlyRecord);
}
else
{
var newMonthlyRecord = new T_FlowerRecords
{
UserId = userId,
TargetId = targetId,
TargetType = targetType,
FlowerCount = count,
SendMonth = sendMonth,
SendTime = monthlyGroup.First().SendTime
};
await flowerRecordsRepo.InsertAsync(newMonthlyRecord);
}
}
_logger.LogInformation("[FlowerBatchSync] 处理完成 {Count} 个月度汇总记录,月份: {Month}",
monthlyGroups.Count, sendMonth);
}
// 3. 使用SQL批量更新目标表的花数避免逐条更新
await UpdateTargetFlowerCountsAsync(toInsert);
// 4. 提交事务
uow.Commit();
}
catch (Exception ex)
{
uow.Rollback();
_logger.LogError(ex, "[FlowerBatchSync] 数据库操作失败,已回滚");
throw;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[FlowerBatchSync] 处理数据库操作时发生异常");
throw;
}
}
/// <summary>
/// 批量更新目标表的花数
/// </summary>
private async Task UpdateTargetFlowerCountsAsync(
List<(long UserId, string TargetType, long TargetId, string SendMonth, DateTime SendTime, long? ReceiverId)> toInsert)
{
// 3.1 更新帖子花数
var postOperations = toInsert.Where(x => x.TargetType == "Post").ToList();
if (postOperations.Any())
{
var postGroups = postOperations
.GroupBy(x => x.TargetId)
.OrderBy(g => g.Key) // 统一顺序避免死锁
.ToList();
var updateCases = postGroups.Select(g =>
$"WHEN {g.Key} THEN FlowerCount + {g.Count()}").ToList();
if (updateCases.Any())
{
var postIds = string.Join(",", postGroups.Select(g => g.Key));
var sql = $@"
UPDATE T_Posts WITH (UPDLOCK, READPAST, ROWLOCK)
SET FlowerCount = CASE Id
{string.Join(" ", updateCases)}
ELSE FlowerCount
END
WHERE Id IN ({postIds})";
await _fsql.Ado.ExecuteNonQueryAsync(sql);
_logger.LogInformation("[FlowerBatchSync] 更新了 {Count} 个帖子的花数", postGroups.Count);
}
}
// 3.2 更新主播花数
var streamerOperations = toInsert.Where(x => x.TargetType == "Streamer").ToList();
if (streamerOperations.Any())
{
var streamerGroups = streamerOperations
.GroupBy(x => x.TargetId)
.OrderBy(g => g.Key) // 统一顺序避免死锁
.ToList();
var updateCases = streamerGroups.Select(g =>
$"WHEN {g.Key} THEN FlowerCount + {g.Count()}").ToList();
if (updateCases.Any())
{
var streamerIds = string.Join(",", streamerGroups.Select(g => g.Key));
var sql = $@"
UPDATE T_Streamers WITH (UPDLOCK, READPAST, ROWLOCK)
SET FlowerCount = CASE Id
{string.Join(" ", updateCases)}
ELSE FlowerCount
END
WHERE Id IN ({streamerIds})";
await _fsql.Ado.ExecuteNonQueryAsync(sql);
_logger.LogInformation("[FlowerBatchSync] 更新了 {Count} 个主播的花数", streamerGroups.Count);
}
}
}
private bool IsDeadlockException(Exception ex)
{
return ex.Message.Contains("deadlock") ||
ex.Message.Contains("deadlocked") ||
ex.Message.Contains("was chosen as the deadlock victim");
}
/// <summary>
/// 操作信息(用于反序列化)
/// </summary>
private class OperationInfo
{
public string SendMonth { get; set; }
public DateTime OccurredAt { get; set; }
public long? ReceiverId { get; set; }
}
public override Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("[FlowerBatchSync] 正在停止送花批量同步服务...");
return base.StopAsync(cancellationToken);
}
}
}