251 lines
9.3 KiB
C#
251 lines
9.3 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获取待处理操作,定时批量同步到数据库
|
||
/// 触发条件:每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);
|
||
}
|
||
}
|
||
}
|
||
|