控制器如何同步写入数据库?博客浏览量统计并发处理疑问
解决博客文章浏览量并发更新的问题
咱们先聊聊你当前用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这类队列,对于简单博客来说,确实要看情况:
- 如果你的博客是个人小站,日均访问量几百到几千,那用队列确实有点“杀鸡用牛刀”,反而增加了运维复杂度
- 如果是日均几万+访问量,或者峰值流量很高,那队列是合理的选择——它能把同步的数据库操作变成异步,避免数据库被大量更新请求压垮
轻量替代方案:内存队列+后台批量更新
如果不想用重量级消息队列,可以试试内存队列+后台定时批量更新的方式,既达到异步解耦的目的,又不用额外部署中间件:
- 先定义一个内存队列(比如用
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; }
- 注册为单例服务:
builder.Services.AddSingleton<ViewCountQueue>();
- 在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(); }
- 写一个
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); } } }
- 注册后台服务:
builder.Services.AddHostedService<ViewCountProcessor>();
这种方式的优点是:
- 完全异步,Controller响应速度极快
- 批量更新减少数据库IO次数,性能更好
- 不需要额外中间件,适合小到中型流量的博客
三、总结
- 如果是小流量博客:优先用数据库原子更新的方式,代码最简单,性能也足够
- 如果流量中等,想进一步优化响应速度:用内存队列+后台批量更新,轻量易维护
- 如果是高流量博客:再考虑RabbitMQ这类专业消息队列,能更好地应对分布式场景和峰值流量
内容的提问来源于stack exchange,提问作者Trung Do
相关产品推荐
相关产品推荐

