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