百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

基于.NetCore开发 StarBlog 番外篇 (1) 全新的访问统计功能,异步队列,分库存储

cac55 2025-03-24 14:18 30 浏览 0 评论

前言

虽然现在工作重心以AI为主了,不过相比起各种大模型的宏大叙事,我还是更喜欢自己构思功能、写代码,享受解决问题和发布上线的过程。

之前 StarBlog 系列更新的时候我也有提到,随着功能更新,会在教程系列完结之后继续写番外,这不第一篇番外就来了。

这次是全新设计的访问统计功能。

访问统计

访问统计功能很早就已经实现了,在之前这篇 基于.NetCore开发博客项目 StarBlog - (11) 实现访问统计


旧实现存在的问题

之前是添加了一个中间件 VisitRecordMiddleware ,每个请求都写入到数据库里

这样会导致两个问题:

  1. 影响性能
  2. 导致数据库太大,不好备份


新的实现

我一直对之前这个实现不满意

这次索性重新设计了,一次性把以上提到的问题都解决了

我用 mermaid 画了个简单的图(第一次尝试在文章里插入 mermaid 画的图,不知道效果咋样)

https://mermaid.js.org/syntax/flowchart.html


---
title: 新的访问统计功能设计图
---
flowchart LR
Request(用户请求) --> Middleware(访问日志中间件)
Middleware(访问日志中间件) --> Queue[/日志队列/]
Worker[后台定时任务] --取出日志--- Queue[/日志队列/]
Worker[后台定时任务] --写入数据库--> DB[(访问日志独立数据库)]

新的实现用一个队列来暂存访问日志

并且添加了后台任务,定时从队列里取出访问日志来写入数据库

这样就不会影响访问速度

到这里这个新的功能基本就介绍完了

当然具体实现会有一些细节需要注意,接下来的代码部分会介绍


新的技术栈

这次我用了 EFCore 作为 ORM

原因和如何引入我在之前这篇文章有介绍了:Asp-Net-Core开发笔记:快速在已有项目中引入efcore

主要目的是使用 EFCore 能更方便实现分库

具体实现

接下来是具体的代码实现


队列

StarBlog.Web/Services 里添加 VisitRecordQueueService.cs 文件


public classVisitRecordQueueService {
privatereadonly ConcurrentQueue _logQueue = new ConcurrentQueue();
privatereadonly ILogger _logger;
privatereadonly IServiceScopeFactory _scopeFactory;

///
/// 批量大小
///

privateconstint BatchSize = 10;

public VisitRecordQueueService(ILogger logger, IServiceScopeFactory scopeFactory) {
_logger = logger;
_scopeFactory = scopeFactory;
}

// 将日志加入队列
public void EnqueueLog(VisitRecord log) {
_logQueue.Enqueue(log);
}

// 定期批量写入数据库的
public async Task WriteLogsToDatabaseAsync(CancellationToken cancellationToken) {
if (_logQueue.IsEmpty) {
// 暂时等待,避免高频次无意义的检查
await Task.Delay(1000, cancellationToken);
return;
}

var batch = new List();
// 从队列中取出一批日志
while (_logQueue.TryDequeue(outvar log) && batch.Count < BatchSize) {
batch.Add(log);
}

try {
usingvar scope = _scopeFactory.CreateScope();
var dbCtx = scope.ServiceProvider.GetRequiredService();
awaitusingvar transaction = await dbCtx.Database.BeginTransactionAsync(cancellationToken);
try {
dbCtx.VisitRecords.AddRange(batch);
await dbCtx.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
_logger.LogInformation("访问日志 Successfully wrote {BatchCount} logs to the database", batch.Count);
}
catch (Exception) {
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
catch (Exception ex) {
_logger.LogError(ex, "访问日志 Error writing logs to the database: {ExMessage}", ex.Message);
}
}
}

这里使用了:

  • ConcurrentQueue 这个线程安全的FIFO队列
  • 在批量写入数据库的时候用了事务,遇到报错自动回滚


中间件

修改 StarBlog.Web/Middlewares/VisitRecordMiddleware.cs


public classVisitRecordMiddleware {
privatereadonly RequestDelegate _next;

public VisitRecordMiddleware(RequestDelegate requestDelegate) {
_next = requestDelegate;
}

public Task Invoke(HttpContext context, VisitRecordQueueService logQueue) {
var request = context.Request;
var ip = context.GetRemoteIpAddress()?.ToString();
var item = new VisitRecord {
Ip = ip?.ToString(),
RequestPath = request.Path,
RequestQueryString = request.QueryString.Value,
RequestMethod = request.Method,
UserAgent = request.Headers.UserAgent,
Time = DateTime.Now
};
logQueue.EnqueueLog(item);

return _next(context);
}
}

没什么特别的,就是把之前数据库操作替换为添加到队列

注意依赖注入不能在中间件的构造方法里,IApplicationBuilder 注册中间件的时候依赖注入容器还没完全准备好


后台任务

在 StarBlog.Web/Services 里添加 VisitRecordWorker.cs 文件


public classVisitRecordWorker : BackgroundService {
privatereadonly ILogger _logger;
privatereadonly IServiceScopeFactory _scopeFactory;
privatereadonly VisitRecordQueueService _logQueue;
privatereadonly TimeSpan _executeInterval = TimeSpan.FromSeconds(30);

public VisitRecordWorker(ILogger logger, IServiceScopeFactory scopeFactory, VisitRecordQueueService logQueue) {
_logger = logger;
_scopeFactory = scopeFactory;
_logQueue = logQueue;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
await _logQueue.WriteLogsToDatabaseAsync(stoppingToken);
await Task.Delay(_executeInterval, stoppingToken);
_logger.LogDebug("后台任务 VisitRecordWorker ExecuteAsync");
}
}
}

要注意的是,BackgroundService 是 singleton 生命周期的,而数据库相关的是 scoped 生命周期,所以在使用前要先获取 scope ,而不是直接注入。

这里使用了 IServiceScopeFactory 而不是 IServiceProvider

在多线程环境里可以保证可以获取根容器的实例,这也是微软文档里推荐的做法。

分库与重构


引入EFCore

如上文所说,访问日志是比较大的,上线这个功能之后几个月的时间,就积累了几十万的数据,在数据库里占用也有100多M了,虽然这还远远达不到数据库的瓶颈

但是对于我们这个轻量级的项目来说,当我想要备份的时候,相比起几个MB的博客数据,这上百MB的访问日志就成了冗余数据,这部分几乎没有备份的意义

所以分库就是势在必得的

这次我使用了EFCore来单独操作这个新的数据库

具体如何引入和实现,之前那篇文章介绍得很详细了,本文不再重复。

Asp-Net-Core开发笔记:快速在已有项目中引入efcore


重构服务

因为使用了EFCore,涉及到的服务也需要调整一下,从FreeSQL换到EFCore

修改 StarBlog.Web/Services/VisitRecordService.cs


public classVisitRecordService {
privatereadonly ILogger _logger;
privatereadonly AppDbContext _dbContext;

public VisitRecordService(ILogger logger, AppDbContext dbContext) {
_logger = logger;
_dbContext = dbContext;
}

publicasync Task GetById(int id) {
var item = await _dbContext.VisitRecords.FirstOrDefaultAsync(e => e.Id == id);
return item;
}

publicasync Task<List> GetAll() {
returnawait _dbContext.VisitRecords.OrderByDescending(e => e.Time).ToListAsync();
}

publicasync Task<IPagedList> GetPagedList(VisitRecordQueryParameters param) {
var querySet = _dbContext.VisitRecords.AsQueryable();

// 搜索
if (!string.IsOrEmpty(param.Search)) {
querySet = querySet.Where(a => a.RequestPath.Contains(param.Search));
}

// 排序
if (!string.IsOrEmpty(param.SortBy)) {
var isDesc = param.SortBy.StartsWith("-");
var orderByProperty = param.SortBy.Trim('-');
if (isDesc) {
orderByProperty = $"{orderByProperty} desc";
}

querySet = querySet.OrderBy(orderByProperty);
}

IPagedList pagedList = new StaticPagedList(
await querySet.Page(param.Page, param.PageSize).ToListAsync(),
param.Page, param.PageSize,
Convert.ToInt32(await querySet.CountAsync())
);
return pagedList;
}

///
/// 总览数据
///

public async Task<object> Overview(
)
{
var querySet = _dbContext.VisitRecords
.Where(e => !e.RequestPath.StartsWith("/Api"));

returnnew {
TotalVisit = await querySet.CountAsync(),
TodayVisit = await querySet.Where(e => e.Time.Date == DateTime.Today).CountAsync(),
YesterdayVisit = await querySet
.Where(e => e.Time.Date == DateTime.Today.AddDays(-1).Date)
.CountAsync()
};
}

///
/// 趋势数据
///

/// 查看最近几天的数据,默认7天
public async Task<object> Trend(int days = 7) {
var startDate = DateTime.Today.AddDays(-days).Date;
returnawait _dbContext.VisitRecords
.Where(e => !e.RequestPath.StartsWith("/Api"))
.Where(e => e.Time.Date >= startDate)
.GroupBy(e => e.Time.Date)
.Select(g => new {
time = g.Key,
date = $"{g.Key.Month}-{g.Key.Day}",
count = g.Count()
})
.OrderBy(e => e.time)
.ToListAsync();
}

///
/// 统计数据
///

public async Task<object> Stats(DateTime date) {
returnnew {
Count = await _dbContext.VisitRecords
.Where(e => e.Time.Date == date)
.Where(e => !e.RequestPath.StartsWith("/Api"))
.CountAsync()
};
}
}

主要变动的就是 GetPagedList 和 Overview 接口

  • EFCore默认不支持按字段名称排序,为此我引入了 Microsoft.EntityFrameworkCore.DynamicLinq 库来实现
  • EFCore 似乎没有FreeSQL的Aggregate API,可以用原生SQL来替换,但我没有这么做,还是做了多次查询,其实影响不大

其他的属于语法的区别,简单修改即可。

小结

时隔好久再次为 StarBlog 开发新功能,C# 的开发体验还是那么丝滑

然而 "Packages with vulnerabilities have been detected" 的警告也在提醒我这个项目的SDK版本已经outdated了

所以接下来会找时间尽快升级

预告一波:下一个功能与备份有关

参考资料

  • https://learn.microsoft.com/zh-cn/dotnet/core/extensions/scoped-service
  • https://www.cnblogs.com/wucy/p/16566495.html


解锁AI驱动的生产力跃迁

程序设计实验室 专注前沿技术落地,每周解析代码级解决方案。

关注获取:

《DeepSeek极速上手手册》24页干货:零基础3天玩转智能编码

清华独家课程三部曲:

  • 《DeepSeek从入门到精通》104页精讲(附30+代码实例)
  • 《职场效能革命指南》35页实战:7大行业应用场景深度拆解
  • 《AI红利捕获手册》65页秘籍:普通人快速构建竞争壁垒的5种路径

与万千技术人共建智能开发新范式。


相关推荐

上网行为管理有什么用,行为审计软件解决方案?

上网行为管理有什么用,行为审计软件解决方案?我们生活在互联网的时代,网络是比较复杂的,有时候经常会出现一些网络违规现象,这时候就可以进行上网行为管理了,现在有很多的公司都会进行上网行为管理,因为这样可...

上网行为管理软件如何监控员工访问网址信息

上网行为管理软件可以通过域之盾软件来监控员工访问的网址信息:主要方式↓1、网页日志记录上网行为管理软件可以通过网页日志记录功能,完整地记录员工在工作中访问的所有网站,包括访问时间、访问页面、访问方式等...

企业如何确保上网行为管理顺利进行?

企业的稳定长久发展离不开对员工的上网行为管理,因为员工的上网行为关乎到工作效率、生产效率、以及重要的数据信息安全问题。一旦有不规范的上网行为,便容易造成企业终端数据泄密事件,对企业造成重大的财产损失以...

员工上网行为监控如何实现?六个妙招!帮你轻松管理员工上网!

员工上网行为不仅关乎个人工作效率,更直接影响到企业的信息安全和整体运营。不当的上网行为,如访问非法网站、泄露公司机密、长时间闲聊等,都可能给企业带来不可估量的损失。因此,监控员工上网行为,不仅是为了提...

终端管理系统规范企业上网行为管理

企业内部经常会遇到不同的上网行为管理问题,如职员在上班时间炒股、打游戏、上网聊天等不正当的上网行为,用U盘、硬盘等移动设备随意拷贝资料,终端资产难以管理等,降低办公效率的同时,也增加了企业内部泄密风...

企业如何进行上网行为管理?_如何管理企业网络

企业如何进行上网行为管理?为了保障网络安全,提高员工工作效率,企业有必要部署上网行为管理。上网行为管理可对企业内部员工的上网行为进行全方位有效管理,保护Web访问安全,降低互联网使用风险,避免机密信息...

演员赵露思的官方微博注销的原因是什么?

根据多方权威媒体报道及平台验证,演员赵露思的微博账号已于2025年8月19日正式注销。目前搜索该账号显示“因用户自行申请关闭,现已无法查看”。这一结果源于她8月13日在直播中宣布的注销决定,当时她直言...

分手传闻仅4月,关晓彤的一张海边亲吻照,撕碎了鹿晗最后的体面

“原来八年真能被四个月的‘海边吻照’一键清空。”热搜上那张模糊的侧脸一贴,评论瞬间爆炸:关晓彤亲的是李昀锐,鹿晗的生日祝福还停留在去年。八年,够让一部剧从开播到大结局,也够让一对顶流情侣把微博背景换成...

计算机基础知识(五)(3)_计算机基础知识100道

四、如何使用计算机4、软件的通用使用方法计算机发展到现在,正常使用时均采用窗口式界面。我们的介绍不涉及苹果机。操作系统从DOS(DOS阶段有两类,一类是微软的,称为MS—DOS;另一类称为PC—DOS...

笔记本的这些基础知识,你再不知道,就真的被社会淘汰了!

请您在阅读前点击上面的“关注”二字,后续会为您提供更多有价值的电脑知识,感谢每一位朋友的支持!这篇文章依然是给大家讲讲关于笔记本电脑的基础知识,新手小白可一定要收藏起来,以后想看的时候可以找出来就看,...

电脑基础知识,引起电脑故障的原因

引起电脑出现故障的原因非常多,概括来说主要包括以下几个方面的问题。操作不当:操作不当是指误删除文件或非法关机等不当操作。操作不当通常会造成电脑程序无法运行或电脑无法启动,修复此类故障,只要将删除或损坏...

笔记本电脑新手使用教程,笔记本电脑使用技巧

学电脑能够快速入门是每个新手梦寐以求的事情,但是不是每个人都能快速入门的。但是如果定制好合理计划,循序渐进,就会收到非常好的效果。今天小编来跟大家说说笔记本电脑新手教程的详细介绍-装机吧,大家一起...

电脑基础知识:(二)电脑主机_电脑主机组成图解

你好!我是麦秋~前言现代生活随着互联网通讯的快速发展,通讯技术日趋完美,应用软件日益普及,电脑的使用与维护则成为人们日常生活中密不可分的、重要的生活内容。电脑恐怕是中老年人的短板,然而又是急需解决的问...

电脑基础知识:(六)电脑应用程序_电脑的应用程序是什么意思

你好!我是麦秋~应用软件是专为解决一些具体问题的软件,是体现电脑用途的部分,种类繁多,如:办公软件、游戏软件、杀毒软件等;自媒体平台上的西瓜、头条、抖音、剪映等,对于上述软件主要是操作和使用上的问题,...

键盘操作方法大全_键盘操作教程

【键盘操作方法大全】键盘可不仅仅能帮我们打字哦,还有很多快捷的操作你都知道吗?除了Ctrl+C、Ctrl+V以外,再多学几种吧,让你用起电脑来十指如飞~别再慢慢用鼠标点了,用开始键+Tab键切换程序让...

取消回复欢迎 发表评论: