标签 批量删除 下的文章

背景

项目中有日志自动记录功能,但随着使用增加,会产生大量日志记录。于是,打算利用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);
    }
}