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

251 lines
9.3 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获取待处理操作定时批量同步到数据库
/// 触发条件每2分钟执行一次
/// </summary>
public class ViewBatchSyncService : BackgroundService
{
private readonly IRedisService _redisService;
private readonly IFreeSql _fsql;
private readonly ILogger<ViewBatchSyncService> _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<ViewBatchSyncService> 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] 浏览批量同步服务已停止");
}
/// <summary>
/// 执行批量同步
/// </summary>
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;
}
}
/// <summary>
/// 限量获取Redis中的操作避免一次性处理过多
/// </summary>
private async Task<(Dictionary<long, int> viewCountsByPostId, List<string> processedFields)> GetLimitedOperationsAsync()
{
var viewCountsByPostId = new Dictionary<long, int>();
var processedFields = new List<string>();
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);
}
/// <summary>
/// 处理数据库操作
/// </summary>
private async Task ProcessDatabaseOperationsAsync(
Dictionary<long, int> 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);
}
}
}