You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

控制器如何同步写入数据库?博客浏览量统计并发处理疑问

解决博客文章浏览量并发更新的问题

咱们先聊聊你当前用RowVersion+重试循环的方案痛点:这种方式在单表场景下勉强能用,但多表并发更新时确实会变得非常繁琐,而且每次重试都要重新查询、加载实体,在高并发访问下还可能带来不必要的数据库开销。下面给你几个更优雅的解决方案,以及聊聊任务队列是否真的小题大做。

一、优化并发更新:跳过实体加载,直接用数据库原子操作

其实浏览量这种计数场景,完全不需要先把实体加载到内存再更新——直接让数据库执行原子增量操作就行,这样从根源上避免并发冲突,代码也更简洁。

比如用EF Core的ExecuteUpdateAsync(EF Core 7+支持),直接生成SQL的UPDATE语句:

public async Task<IActionResult> View(string postTitleSlug)
{
    var updatedRows = await _db.Posts
        .Where(p => p.TitleSlug.Equals(postTitleSlug))
        .ExecuteUpdateAsync(s => s.SetProperty(p => p.ViewCount, p => p.ViewCount + 1));

    // 如果updatedRows为0,说明没找到对应文章,可以返回404
    return updatedRows > 0 ? Ok() : NotFound();
}

这种方式的好处是:

  • 数据库层面直接执行原子更新,不存在并发冲突(SQL的UPDATE本身是原子性的)
  • 不需要加载实体、处理RowVersion、重试循环,代码极简
  • 性能更高,减少了数据库往返次数

如果你的EF Core版本低于7,也可以直接执行原生SQL:

var sql = "UPDATE Posts SET ViewCount = ViewCount + 1 WHERE TitleSlug = @slug";
await _db.Database.ExecuteSqlRawAsync(sql, new SqlParameter("@slug", postTitleSlug));

二、任务队列是否小题大做?看你的流量规模

你提到用RabbitMQ、Resque这类队列,对于简单博客来说,确实要看情况:

  • 如果你的博客是个人小站,日均访问量几百到几千,那用队列确实有点“杀鸡用牛刀”,反而增加了运维复杂度
  • 如果是日均几万+访问量,或者峰值流量很高,那队列是合理的选择——它能把同步的数据库操作变成异步,避免数据库被大量更新请求压垮

轻量替代方案:内存队列+后台批量更新

如果不想用重量级消息队列,可以试试内存队列+后台定时批量更新的方式,既达到异步解耦的目的,又不用额外部署中间件:

  1. 先定义一个内存队列(比如用ConcurrentQueue),用来接收浏览量增量请求:
public class ViewCountQueue
{
    private readonly ConcurrentQueue<string> _slugQueue = new();

    public void Enqueue(string postSlug)
    {
        _slugQueue.Enqueue(postSlug);
    }

    public bool TryDequeue(out string slug)
    {
        return _slugQueue.TryDequeue(out slug);
    }

    public int Count => _slugQueue.Count;
}
  1. 注册为单例服务:
builder.Services.AddSingleton<ViewCountQueue>();
  1. 在Controller里把请求加入队列:
private readonly ViewCountQueue _viewQueue;

public PostController(ViewCountQueue viewQueue, ...)
{
    _viewQueue = viewQueue;
}

public async Task<IActionResult> View(string postTitleSlug)
{
    // 先验证文章是否存在(可选,避免无效队列消息)
    var exists = await _db.Posts.AnyAsync(p => p.TitleSlug == postTitleSlug);
    if (!exists) return NotFound();

    _viewQueue.Enqueue(postTitleSlug);
    return Ok();
}
  1. 写一个IHostedService后台服务,定时批量处理队列中的请求:
public class ViewCountProcessor : BackgroundService
{
    private readonly ViewCountQueue _queue;
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<ViewCountProcessor> _logger;

    public ViewCountProcessor(ViewCountQueue queue, IServiceScopeFactory scopeFactory, ILogger<ViewCountProcessor> logger)
    {
        _queue = queue;
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("View count processor started");

        while (!stoppingToken.IsCancellationRequested)
        {
            if (_queue.Count > 0)
            {
                // 批量取出一批slug(比如一次处理100条)
                var slugs = new List<string>();
                while (slugs.Count < 100 && _queue.TryDequeue(out var slug))
                {
                    slugs.Add(slug);
                }

                // 分组统计每个slug的访问次数
                var slugGroups = slugs.GroupBy(s => s)
                                      .ToDictionary(g => g.Key, g => g.Count());

                // 批量更新数据库
                using var scope = _scopeFactory.CreateScope();
                var db = scope.ServiceProvider.GetRequiredService<YourDbContext>();

                foreach (var (slug, count) in slugGroups)
                {
                    await db.Posts
                        .Where(p => p.TitleSlug == slug)
                        .ExecuteUpdateAsync(s => s.SetProperty(p => p.ViewCount, p => p.ViewCount + count));
                }

                await db.SaveChangesAsync(stoppingToken);
                _logger.LogInformation("Processed {Count} view count updates", slugs.Count);
            }

            // 每隔5秒检查一次队列
            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }
}
  1. 注册后台服务:
builder.Services.AddHostedService<ViewCountProcessor>();

这种方式的优点是:

  • 完全异步,Controller响应速度极快
  • 批量更新减少数据库IO次数,性能更好
  • 不需要额外中间件,适合小到中型流量的博客

三、总结

  • 如果是小流量博客:优先用数据库原子更新的方式,代码最简单,性能也足够
  • 如果流量中等,想进一步优化响应速度:用内存队列+后台批量更新,轻量易维护
  • 如果是高流量博客:再考虑RabbitMQ这类专业消息队列,能更好地应对分布式场景和峰值流量

内容的提问来源于stack exchange,提问作者Trung Do

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:28:48