分类 DotNet 下的文章

背景

项目中有日志自动记录功能,但随着使用增加,会产生大量日志记录。于是,打算利用Hangfire中的RecurringJob定时执行日志清理工作,设定为每天凌晨执行任务。

然后因日志量过于巨大,每天可产生大约近百万条日志记录。起初,使用EF进行删除,虽然知道效率贼低,但因EF没用批量操作功能,而且想着EF删除应该是多条删除同时提交,加上反正晚上没啥访问,慢慢删吧。然而,悲催的事情发生了,第二天发现服务器挂了,因数据库无法访问导致网站崩溃。几经波折,发现是日志删除的后台任务执行时,时间过长卡死了。

解决过程

既然EF没有批量删除功能,那就执行sql语句吧。删除条件是保留最近一个月的,之前的全部删除掉,需要使用时间进行检索删除,可能我数据库性能也不行,直接执行sql语句也奇慢无比。为了保证后台任务不再出现上次的直接死掉的严重问题,我决定通过每次删除1000条,多次执行sql语句方式进行删除。

遇到问题

我的处理方式是这样的:
通过IDbContextProvider获取dbContext。之后调用dbContext.Database.ExecuteSqlCommand(sql)执行删除语句。

var deleteunit = 1000;
var deadline = DateTime.Now.AddDays(-30);
bool isContinue = true;
while (isContinue)
{
    var first = _logRepository.GetAll().OrderBy(o => o.Id).FirstOrDefault();
    if (first == null || first.ExecutionTime > deadline)
    {
        isContinue = false;
        break;
    }

    var endId = first.Id + deleteunit;
    string sql = $"DELETE FROM logs where Id < {endId}" ;
    var rows = ExecuteSql(sql);
} 

ExecuteSql只有一行代码,执行sql语句

private int ExecuteSql(string sql)
{
    return _dbContextProvider.GetDbContext().Database.ExecuteSqlCommand(sql);
}

事情并没有按照我预想的方向发展。网站依然崩溃了,后台任务依然卡死了。sql语句并没有按照我预想的那样,一条一条的执行,而是当所有循环结束是,一起提交执行的,这样的话,执行一条语句和执行多条语句并没有什么区别了,甚至更慢了。

分析原因

因删除数据量过大,EF没有批量操作功能,最初的方案中,逐条删除时,在执行了所有的删除命令后,最后统一进行了提交操作,这时mysql才去进行实际的删除操作,而删除过程太过漫长,直接死掉。然而通过_dbContextProvider.GetDbContext()获取的DbContext与系统中仓储操作使用的是同一个DbContext,并没有新建DbContext。这就导致了网站其他功能直接挂掉了。

然而,第二次更改,直接执行sql语句,也是同理,abp默认支持UOW,循环执行sql语句,说白了还是一样,最后统一提交执行的。所以并没太大的改进,当然执行sql语句总比逐条删除好些。我曾经试过使用CurrentUnitOfWork.SaveChanges();试图关闭UOW,不过好像没起作用,我也没再细究。

解决方案

还是同样的方式执行sql语句,不过每次执行完后进行一次事务提交。也就是每次执行完后,销毁dbcontext,下次需要的是再次创建。不过_dbContextProvider.GetDbContext()获取的是系统内单例实现的,一旦销毁,其他仓储服务也无法使用了。所以我们可以新建DbContext,通过using方式使用。

本项目使用的是abp core2.2版本,这里的DbContext构造函数需要DbContextOptions类型参数。

public MyDbContext(DbContextOptions<MyDbContext> options)
    : base(options)
{
}

之前.net framework版本abp好像可以直接new DbContext()使用。当然,多一个参数而已,并不会阻碍我们的脚步。可以通过如下方式继续。

重写ExecuteSql方法。

private int ExecuteSql(string sql)
{
    using (var dbContext = new MyDbContextFactory().CreateDbContext(null))
    {
        return dbContext.Database.ExecuteSqlCommand(sql);
    }
    //return _dbContextProvider.GetDbContext().Database.ExecuteSqlCommand(sql);

}

Factory主要用来创建DbContext。

public class MyDbContextFactory : IDesignTimeDbContextFactory<MyDbContext>
{
    public MyDbContextFactory CreateDbContext(string[] args)
    {
        var builder = new DbContextOptionsBuilder<MyDbContext>();
        var configuration = AppConfigurations.Get(
            WebContentDirectoryFinder.CalculateContentRootFolder(), addUserSecrets: true
        );
        (new MySqlDbContextConfigurer()).ConfigureByConnectionString(
            builder,
            configuration.GetConnectionString(MyCoreConsts.ConnectionStringName)
        );

        return new MyDbContext(builder.Options);
    }
}

背景

net core2 web项目中,使用IdentityServer4时,在ConfigureServices中添加Identity配置,如下:

services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());

编译时,发现如下错误提示:

错误 CS1061 '“IIdentityServerBuilder”未包含“AddTemporarySigningCredential”的定义,并且找不到可接受第一个“IIdentityServerBuilder”类型参数的可访问扩展方法“AddTemporarySigningCredential”(是否缺少 using 指令或程序集引用?)

这是因为,AddTemporarySigningCredential是在net core1.0中使用的,在net core2.0中,使用AddDeveloperSigningCredential 代替。.

services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());

1、开发环境

  • 开发工具:Vs2019
  • 运行时:dotnet core 3.1
  • 数据库:mariadb或MySql
  • abp版本:2.0.0

2、项目创建

先按照abp官方CLI,

dotnet tool install -g Volo.Abp.Cli

如果之前安装过,更新可以使用:

dotnet tool update -g Volo.Abp.Cli

使用abp官方CLI工具创建项目,

abp new Acme.BookStore

当前版本abp,还支持创建项目时指定mongodb数据库,命令如下:

abp new Acme.BookStore -d mongodb

image-20200119130612630.png

3、更改数据库配置

等待项目构建完成,使用Vs打开项目,进行Nuget还原。abp CLI构建的项目默认使用了SqlServer数据库。本文将介绍在abp vNext 2.0.0版本下构建的项目,如何切换为使用MySql数据库。请注意版本,因abp vNext前期更新变动较大,所以不同版本之间的修改可能不尽相同。

1、修改数据库连接字符串

Acme.BookStore.Web项目中,打开其中“appsetting.json”文件,修改其中连接字符串为MySql格式,例子如下:

"ConnectionStrings": {
  "Default": "server=localhost;port=3306;database=testdb;User ID=root;Password=123456;"
},
2、管理“Acme.BookStore.EntityFrameworkCore”项目中Nuget程序包,卸载其中的“Volo.Abp.EntityFrameworkCore.SqlServer”包,并浏览安装“Volo.Abp.EntityFrameworkCore.MySQL”程序包。如下图

image-20200119131222738.png

3、将错误提示中的“UseSqlServer”代码替换为“UseMysql"。

有两部分需要修改:

Acme.BookStore.EntityFrameworkCore”项目中的BookStoreEntityFrameworkCoreModule.cs文件中options.UseSQLServer()
修改为:options.UseMySQL()
将依赖项目“typeof(AbpEntityFrameworkCoreSqlServerModule)”修改为“typeof(AbpEntityFrameworkCoreMySQLModule)”。

并删除无效的using引用。

image-20200119131451161.png

Acme.BookStore.EntityFrameworkCore.DbMigrations”项目中的BookStoreMigrationsDbContextFactory.cs文件中

var builder = new DbContextOptionsBuilder<EyinzhangMigrationsDbContext>()
                .UseSqlServer(configuration.GetConnectionString("Default"));

修改为:

var builder = new DbContextOptionsBuilder<EyinzhangMigrationsDbContext>()
                .UseMySql(configuration.GetConnectionString("Default"));

image-20200119131531113.png

注意这两处修改的红色部分的MySql的大小写是不一样的。

4、修改Web项目中错误

删除"Acme.BookStore.EntityFrameworkCore.DbMigrations”项目下的Migrator文件夹,重新生成解决方案。如果,出现如下图错误提示,

严重性 代码 说明 项目 文件 行 禁止显示状态
错误 CS8652 功能“可为 null 的引用类型”当前为预览版且不受支持。要使用预览版功能,请使用“预览”语言版本。 Acme.BookStore.Web F:Acme.BookStoresrcAcme.BookStore.WebobjDebugnetcoreapp3.1RazorPagesIndex.cshtml.g.cs 134 活动的

image-20200119132148185.png

此为使用了预览版功能造成的,可在“Acme.BookStore.Web”项目中,打开Pages文件夹下的Index.chtml文件,将如图所示部分注释掉,后期再根据个人情况修改即可。

image-20200119132812448.png

5、重新生成迁移

至此,Mysql配置的修改基本完成,项目也没有错误提示了,将Acme.BookStore.Web项目设置为启动项目,在程序包管理控制台,将默认项目设置为“Acme.BookStore.EntityFrameworkCore.DbMigrations”,输入add-migration命令重新生成迁移。

PM> add-migration
位于命令管道位置 1 的 cmdlet Add-Migration
请为以下参数提供值:
Name: init
Build started...
Build succeeded.
To undo this action, use Remove-Migration.
PM>

image-20200119133258066.png

如上,可以正常生成迁移文件。但是在执行update-database时,会提示如下错误:

Failed executing DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE IdentityServerApiSecrets (

`Type` varchar(250) CHARACTER SET utf8mb4 NOT NULL,
`Value` longtext CHARACTER SET utf8mb4 NOT NULL,
`ApiResourceId` char(36) NOT NULL,
`Description` varchar(2000) CHARACTER SET utf8mb4 NULL,
`Expiration` datetime(6) NULL,
CONSTRAINT `PK_IdentityServerApiSecrets` PRIMARY KEY (`ApiResourceId`, `Type`, `Value`),
CONSTRAINT `FK_IdentityServerApiSecrets_IdentityServerApiResources_ApiResou~` FOREIGN KEY (`ApiResourceId`) REFERENCES `IdentityServerApiResources` (`Id`) ON DELETE CASCADE

);

.....

BLOB/TEXT column 'Value' used in key specification without a key length

6、修改无法创建表IdentityServerApiSecurity的错误

针对如上问题,可使用下面的步骤解决:

参考:https://github.com/abpframework/abp/issues/2053

MysqlDemo.EntityFrameworkCore项目,“EntityFrameworkCore”文件夹下,添加IdentityServerModelCreatingExtensions类。代码如下:

    public static class IdentityServerModelCreatingExtensions
    {
        public static void ConfigureIdentityServerForMySQL(this ModelBuilder builder)
        {
            // Solve the problem of MySQL migration
            // https://github.com/abpframework/abp/issues/1920
    
            builder.Entity<ApiSecret>(b =>
            {
                // After trying, you can also set it to 400
                b.Property(x => x.Value).HasMaxLength(300);
            });
    
            builder.Entity<ClientPostLogoutRedirectUri>(b =>
            {
                b.Property(x => x.PostLogoutRedirectUri).HasMaxLength(300); // or 400 ?
            });
    
            builder.Entity<ClientRedirectUri>(b =>
            {
                b.Property(x => x.RedirectUri).HasMaxLength(300); // or 400 ?
            });
    
            builder.Entity<ClientSecret>(b =>
            {
                b.Property(x => x.Value).HasMaxLength(300); // or 400 ?
            });
    
        }
    }

BookStoreDbContext类中OnModelCreating方法末尾,添加如下代码,并添加对应using引用:

            builder.ConfigureIdentityServer(options =>
            {
                options.DatabaseProvider = EfCoreDatabaseProvider.MySql;
            });
            builder.ConfigureIdentityServerForMySQL();

image-20200119133832269.png

BookStoreDbContextModelCreatingExtensions文件中ConfigureBookStore方法下添加如下代码:

            builder.ConfigureIdentityServer(options =>
            {
                options.DatabaseProvider = EfCoreDatabaseProvider.MySql;
            });

image-20200119134113258.png

全部修改完成后,删除Migration文件夹,重新执行add-migration生成迁移,并执行update-database命令,同步数据库。成功!

image-20200119142517171.png