.NET 5 设计 API (资源站)
阅读原文时间:2023年07月09日阅读:3

跟新于 2022-11日

数据抓取端

随着数据的增多,问题也越来越多

用redis 主要是为了 以后进行,多个数据库写入。

例如我搭建一个 别的数据库论坛,我直接拿数据去redis里面拿,就不用跨库查询然后,加载到内存里面,以此类推 (比如我现在存的hash 是一个月度数据,和每日新增的数据 和修改的数据,这样我就可以直接传递不同参数写入不同数据库只需要定义 pipline 方法即可,不需要再查一下 在循环!)

问题点:

1、每天日常抓取数据写入的时候,之前没有处理Href链接,导致要查询一次富文本内容,去提取Href,(查一个月度的数据,特别卡,花费要五十多秒)

2、限流问题,之前被别人Doss攻击了导致服务器停止了一个月(我的服务器是轻量云的 每月只有1T多一点)。

3、爬虫挂了没有通知,之前爬虫挂了一个多星期我没有发现,数据没跟新一周,直接亏了!

新增需求

1、(打算对接一个stmp 邮件服务,不过好像服务器端口不支持,暂定把,邮箱服务通知-Net)

2、现在写入task 任务是使用写入task表,完成一个任务就去修改task表中的 State 状态,少量数据还可以,数据多了,或者多开几个线程,就有问题了(直接数据库顶不住了,当然我这个数据库,装在服务器上,一大把服务都在上面 性能有限)

解决方案:

日常数据增量问题

解决

1、开始用了一个比较呆的方法,使用SQL INSERT INTO SELET 语句去 写入一个新表(只需要字段,标题、Href、Id),最近发现这样去执行,效率不是很理想,如果后续数据累加,速度上比较慢

最近突然想到一个方法,使用 redis

使用Hset 进行存每日Insert 写入的数据 和 Update 的数据,这样就不用每次去删表,

实际使用效果非常好,(之前我写好了一个RabbitMQ的生产和消费,打算用定时任务每日去巡检,新增的然后写入到MQ中,然后专门写个消费者,定时去pull消费,我发现太大材小用了,就好比拿着 大刀去削个苹果,=-=)

解决效果

可以看到Redis 写入成功 这里我扩展了 post的序列化 和反序 操作,注意(因为抓取的数据来自不同的网站,可能会存在重复的数据 Id,我这里处理的方法是我这边生成的一个Base64加密 把数据Id + 抓取的网站Url路由 生成一个 HashId) 但是最近发现有的网站会换域名,如果发现不及时,导致还是有很小的部分重复,(这个重复暂时没想到解决办法!)

缓存预加载任务

使用 BackgroundService + IMediator

首先我们要声明一个 返回IMedator 进行注入

点击查看代码

 public class TaskCacheService : BackgroundService
    {
        private readonly ILogger<TaskCacheService> _logger;
        private readonly IServiceProvider _provider;

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            await Task.Delay(1000, stoppingToken);
            RecurringJob.AddOrUpdate(() => RedisCache(), Cron.Daily(1));
            throw new System.NotImplementedException();
        }

        /// <summary>
        /// 写入redis 任务
        /// </summary>
        /// <returns></returns>
        public async Task RedisCache()
        {
            await GetMediator().Send(new PostRedisRequest { StartTime = DateTime.Now.AddDays(-1).Date });
        }

        private IMediator GetMediator()
        {
            return _provider.CreateScope().ServiceProvider.GetService<IMediator>();
        }
    } 

大概现在有 19W 条数据,

gitee地址 : https://gitee.com/Lovely_Rabbit/pan_web-api.git

(后面使用go的框架 重写了一版 )

使用的 框架(go_spider) ==> https://gitee.com/huhuzhu/go_spider

go_spider =(这个功能很多用不到我自己取消了 部分功能)

ORM使用的是(Confluence) ==> https://goframe.org/pages/viewpage.action?pageId=1114245

另外使用了一个 第三方的http 库 http.Client ==> https://gitee.com/guonaihong/gout

前台使用的vue --> vue(大部分别人帮我写的,比如组件封装之类的,接口什么的是我自己调的)

http://xh.bozaiq.cn/ 每天早晨6点会更新一批数据

API后端

后台使用 net5 + ef core (开始是想代码实现读写分离,发现数据太少了 看不到效果,后面在慢慢爬取一点)

部署在docker 中,(第一次尝试部署在docker中踩了很多坑)

使用的ef core 框架

因为加入了 一个访问日志 ==》 https://www.bozaiq.cn/d/100-logmiddleware 感觉第一次加载特别慢

本来想用redis 缓存前十页,用redis存储List的话还需要进行jsonConvert 序列化 和反序列化操作,

我就用的微软推荐的 Microsoft.Extensions.Caching.Memory ==>https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/memory?view=aspnetcore-6.0

使用redis缓存了文章详情页,因为是收集网上的基本上是不会改动这个文章页 默认缓存(标识字段,IsCache = true)

swagger 地址 http://xh.bozaiq.cn:8084/swagger/index.html (jwt登陆 =》 todo - 权限管理)

这里Controller 用的是 using MediatR 个包

中介者(Mediator)模式:用一个中介对象来封装一系列的对象交互,中介者使各个对象不需要显示的相互引用,从而使得耦合松散,而且可以独立的改变他们之间的交互

可以看到Controller 是非常明了的只需要定义请求和返回 (abpNET5)

具体的功能实现 自己定义一个 Handler文件夹

这里的PagedAsync() 这个是自己扩展的一个静态的方法 意思就是 分页并异步(因为框架比较模块设计 每个模块都可以根据需求自己扩展)

CancellationToken 这里 对长时间阻塞调用的异步取消令牌应用,在某些场景中,我们需要请求外部的第三方资源,但是由于网络等原因,可能会造成长时间的等待以致业务超时退出,这种情况可以使用 CancellationToken 来进行优化,但请求超过指定时长后退出,而不必针对每个 HttpClient 进行单独的超时设置

CancellationToken 一般.netCore 它和取消异步任务相关的 可以参考:https://www.cnblogs.com/fanfan-90/p/12660996.html

线上的total 我给屏蔽了,实际是每天会自动跟新一批数据 每天自动抓取大概几千条左右

加入了 限流中间件和显示PageSize 大小,防止爬虫

点击查看代码

``` C#
public class HomeHandler : IRequestHandler>
{
private readonly IRepository _postsRepository;
private readonly IRepository _disRepository;
private readonly IMapper _mapper;

        public HomeHandler(IRepository<Posts, int> postsRepository, IRepository<Discussions, int> disRepository, IMapper mapper)
        {
            _postsRepository = postsRepository;
            _disRepository = disRepository;
            _mapper = mapper;
        }

        public async Task<PagedResultDto<GetPostItem>> Handle(GetPostRequest request, CancellationToken cancellationToken)
        {
            // 查询条件
            var postquery = _postsRepository.GetQuery();
            var disquery = _disRepository.GetQuery();
            var queries = from post in postquery
                          join dis in disquery
                          on (int)post.DiscussionId equals dis.Id
                          select new GetPostItem
                          {
                              Id = post.Id,
                              Type = post.Type,
                              Content = post.Content,
                              Title = dis.Title,
                              CreateOn = post.CreateOn,
                              UpdateOn = post.UpdateOn
                          };
            var query = queries.WhereIf(!string.IsNullOrEmpty(request.Keyworks), a => a.Title.Contains(request.Keyworks) || a.Content.Contains(request.Keyworks));

            //查询数据库
            var items = await query.PagedAsync(request);
            var total = await query.CountAsync();
            var pageTotal = items.Count();
            return new PagedResultDto<GetPostItem>(items, total, pageTotal);
        }
    }

    /// <summary>
    /// 请求类型
    /// </summary>
    public class GetPostRequest : PagedRequest, IRequest<PagedResultDto<GetPostItem>>
    {
        public string Keyworks { get; set; }
    }

    /// <summary>
    /// 返回类型
    /// </summary>
    public class GetPostItem : EntityDto<int>
    {
        /// <summary>
        /// 标题
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        /// 类型
        /// </summary>
        public string Type { get; set; }

        /// <summary>
        /// 内容
        /// </summary>
        public string Content { get; set; }

        /// <summary>
        /// 状态:-1-删除 0-禁用 1-正常
        /// </summary>
        public EntityStatusEnums Status { get; set; }

        /// <summary>
        /// 创建时间
        /// </summary>
        public DateTime? CreateOn { get; set; }

        /// <summary>
        /// 更新时间
        /// </summary>
        public DateTime? UpdateOn { get; set; }
    }


</details>

#### EFCore --> sql控制台日志
```C#
      public static readonly ILoggerFactory MyLogFactory = LoggerFactory.Create(build =>
    {
          build.AddConsole(); // 用于控制台程序的输出
                            // build.AddDebug(); // 用于VS调试,输出窗口的输出
    });
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseLoggerFactory(MyLogFactory);
    }