464 lines
19 KiB
C#
464 lines
19 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|
||
|