场景

目前一个项目中数据持久化采用EF Core + MySQL,使用CodeFirst模式开发,并且对数据进行了分库,按照目前颗粒度分完之后,大概有一两百个库,每个库的数据都是相互隔离的。

借鉴了Github上一个开源的仓库 arch/UnitOfWork 实现UnitOfWork,核心操作就是每个api请求的时候带上库名,在执行CRUD之前先将DbContext切换到目标数据库,我们在切换数据库的时候加了一些操作,如检查数据库是否已创建、检查连接是否可用、判断是否需要 表结构迁移

/// <summary>
/// 切换数据库 这要求数据库在同一台机器上 注意:这只适用于MySQL。
/// </summary>
/// <param name="database">目标数据库</param>
public void ChangeDatabase(string database)
{
// 检查连接
...... // 检查数据库是否创建
...... var connection = _context.Database.GetDbConnection();
if (connection.State.HasFlag(ConnectionState.Open))
{
connection.ChangeDatabase(database);
}
else
{
var connectionString = Regex.Replace(connection.ConnectionString.Replace(" ", ""), @"(?<=[Dd]atabase=)\w+(?=;)", database, RegexOptions.Singleline);
connection.ConnectionString = connectionString;
} // 判断是否需要执行表结构迁移
if(_context..Database.GetPendingMigrations().Any())
{
//自定义的迁移的一些逻辑
_migrateExecutor.Migrate(_context);
}
}

但是当多个操作同时对一个库进行Migrate的时候,就会出现问题,比如“新增一张表”的操作已经被第一个迁移执行过了,第二个执行的迁移并不知道已经执行过了Migrate,就会报错表已存在。

于是考虑在执行Migrate的时候,加入一个锁的机制,对当前数据库执行Migrate之前先获取锁,然后再来决定接下来的操作。由于边有的服务无法访问Redis,这里使用数据库来实现锁的机制,当然用Redis来实现更好,加入锁的机制只是一种解决问题的思路。

利用数据库实现迁移锁

1. 新增 MigrationLocks 表来实现迁移锁

  • 锁的操作不依赖DbContext实例
  • 在执行Migrate之前,尝试获取一个锁,在获取锁之前,如果表不存在则创建
    CREATE TABLE IF NOT EXISTS MigrationLocks (
    LockName VARCHAR(255) PRIMARY KEY,
    LockedAt DATETIME NOT NULL
    );
  • 成功往表中插入一条记录,视为获取锁成功,主键为需要迁移的库的名称
    INSERT INTO MigrationLocks (LockName, LockedAt) VALUES (@database, NOW());
  • 迁移完成后,删除这条记录,视为释放锁成功;
    DELETE FROM MigrationLocks WHERE LockName = @database;
  • 为防止 “死锁” 发生,每次尝试获取锁之前,会对锁的状态进行检查,释放超过5分钟的锁(正常来说,上一个迁移的执行时间不会超过5分钟)。
    SELECT COUNT(*) FROM MigrationLocks WHERE LockName = @database AND LockedAt > NOW() - INTERVAL 5 MINUTE;

2. 封装一下MigrateLock的实现

/// <summary>
/// 迁移锁
/// </summary>
public interface IMigrateLock
{
/// <summary>
/// 尝试获取锁
/// </summary>
/// <param name="connection"></param>
/// <returns></returns>
bool TryAcquireLock(IDbConnection connection); /// <summary>
/// 尝试获取锁
/// </summary>
/// <param name="connection"></param>
/// <returns></returns>
Task<bool> TryAcquireLockAsync(IDbConnection connection); /// <summary>
/// 释放锁
/// </summary>
void ReleaseLock(IDbConnection connection); /// <summary>
/// 释放锁
/// </summary>
/// <returns></returns>
Task ReleaseLockAsync(IDbConnection connection);
} /// <summary>
/// 迁移锁
/// </summary>
public class MigrateLock : IMigrateLock
{
private readonly ILogger<MigrateLock> _logger; public MigrateLock(ILogger<MigrateLock> logger)
{
_logger = logger;
} private const string CreateTableSql = @"
CREATE TABLE IF NOT EXISTS MigrationLocks (
LockName VARCHAR(255) PRIMARY KEY,
LockedAt DATETIME NOT NULL
);"; private const string CheckLockedSql = "SELECT COUNT(*) FROM MigrationLocks WHERE LockName = @database AND LockedAt > NOW() - INTERVAL 5 MINUTE;"; private const string AcquireLockSql = "INSERT INTO MigrationLocks (LockName, LockedAt) VALUES (@database, NOW());"; private const string ReleaseLockSql = "DELETE FROM MigrationLocks WHERE LockName = @database;"; /// <summary>
/// 尝试获取锁
/// </summary>
/// <param name="connection"></param>
/// <returns></returns>
public bool TryAcquireLock(IDbConnection connection)
{
try
{
CheckLocked(connection); var result = connection.Execute(AcquireLockSql, new { database = connection.Database });
if (result == 1)
{
_logger.LogInformation("Lock acquired: {LockName}", connection.Database); return true;
} _logger.LogWarning("Failed to acquire lock: {LockName}", connection.Database); return false;
}
catch (Exception ex)
{
if (ex.Message.StartsWith("Duplicate"))
{
_logger.LogWarning("Failed acquiring lock due to duplicate entry: {LockName}", connection.Database);
}
else
{
_logger.LogError(ex, "Error acquiring lock: {LockName}", connection.Database);
} return false;
}
} /// <summary>
/// 尝试获取锁
/// </summary>
/// <param name="connection"></param>
/// <returns></returns>
public async Task<bool> TryAcquireLockAsync(IDbConnection connection)
{
try
{
await CheckLockedAsync(connection); var result = await connection.ExecuteAsync(AcquireLockSql, new { database = connection.Database });
if (result == 1)
{
_logger.LogInformation("Lock acquired: {LockName}", connection.Database); return true;
} _logger.LogWarning("Failed to acquire lock: {LockName}", connection.Database); return false;
}
catch (Exception ex)
{
if (ex.Message.StartsWith("Duplicate"))
{
_logger.LogWarning("Failed acquiring lock due to duplicate entry: {LockName}", connection.Database);
}
else
{
_logger.LogError(ex, "Error acquiring lock: {LockName}", connection.Database);
} return false;
}
} /// <summary>
/// 释放锁
/// </summary>
public void ReleaseLock(IDbConnection connection)
{
try
{
connection.ExecuteAsync(ReleaseLockSql, new { database = connection.Database });
_logger.LogInformation("Lock released: {LockName}", connection.Database);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error releasing lock: {LockName}", connection.Database);
}
} /// <summary>
/// 释放锁
/// </summary>
public async Task ReleaseLockAsync(IDbConnection connection)
{
try
{
await connection.ExecuteAsync(ReleaseLockSql, new { database = connection.Database });
_logger.LogInformation("Lock released: {LockName}", connection.Database);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error releasing lock: {LockName}", connection.Database);
}
} /// <summary>
/// 检查锁
/// </summary>
private void CheckLocked(IDbConnection connection)
{
connection.Execute(CreateTableSql); var databaseParam = new
{
database = connection.Database
}; var lockExists = connection.QueryFirstOrDefault<int>(CheckLockedSql, databaseParam);
if (lockExists <= 0)
{
return;
} _logger.LogWarning("Lock exists and is older than 5 minutes. Releasing old lock.");
connection.Execute(ReleaseLockSql, databaseParam);
} /// <summary>
/// 检查锁
/// </summary>
private async Task CheckLockedAsync(IDbConnection connection)
{
await connection.ExecuteAsync(CreateTableSql); var databaseParam = new
{
database = connection.Database
}; var lockExists = await connection.QueryFirstOrDefaultAsync<int>(CheckLockedSql, databaseParam);
if (lockExists <= 0)
{
return;
} _logger.LogWarning("Lock exists and is older than 5 minutes. Releasing old lock.");
await connection.ExecuteAsync(ReleaseLockSql, databaseParam);
}
}

3. 封装一下MigrateExecutor的实现

/// <summary>
/// 数据库迁移执行器
/// </summary>
public interface IMigrateExcutor
{
/// <summary>
/// 执行迁移
/// </summary>
/// <param name="dbContext"></param>
void Migrate(DbContext dbContext); /// <summary>
/// 执行迁移
/// </summary>
/// <param name="dbContext"></param>
/// <returns></returns>
Task MigrateAsync(DbContext dbContext); /// <summary>
/// 并发场景执行迁移
/// </summary>
/// <param name="dbContext"></param>
/// <param name="wait">是否等待至正在进行中的迁移完成</param>
void ConcurrentMigrate(DbContext dbContext, bool wait = true); /// <summary>
/// 并发场景执行迁移
/// </summary>
/// <param name="dbContext"></param>
/// <param name="wait">是否等待至正在进行中的迁移完成</param>
/// <returns></returns>
Task ConcurrentMigrateAsync(DbContext dbContext, bool wait = true); /// <summary>
/// 并发场景执行迁移
/// </summary>
/// <param name="dbContext"></param>
/// <param name="connection"></param>
/// <param name="wait">是否等待至正在进行中的迁移完成</param>
void ConcurrentMigrate(DbContext dbContext, IDbConnection connection, bool wait = true); /// <summary>
/// 并发场景执行迁移
/// </summary>
/// <param name="dbContext"></param>
/// <param name="connection"></param>
/// <param name="wait">是否等待至正在进行中的迁移完成</param>
Task ConcurrentMigrateAsync(DbContext dbContext, IDbConnection connection, bool wait = true);
} /// <summary>
/// 数据库迁移执行器
/// </summary>
public class MigrateExcutor : IMigrateExcutor
{
private readonly IMigrateLock _migrateLock;
private readonly ILogger<MigrateExcutor> _logger; public MigrateExcutor(
IMigrateLock migrateLock,
ILogger<MigrateExcutor> logger)
{
_migrateLock = migrateLock;
_logger = logger;
} /// <summary>
/// 执行迁移
/// </summary>
/// <param name="dbContext"></param>
/// <returns></returns>
public void Migrate(DbContext dbContext)
{
try
{
if (dbContext.Database.GetPendingMigrations().Any())
{
dbContext.Database.Migrate();
}
}
catch (Exception e)
{
_logger.LogError(e, "Migration failed"); HandleError(dbContext, e);
}
} /// <summary>
/// 执行迁移
/// </summary>
/// <param name="dbContext"></param>
/// <returns></returns>
public async Task MigrateAsync(DbContext dbContext)
{
try
{
if ((await dbContext.Database.GetPendingMigrationsAsync()).Any())
{
await dbContext.Database.MigrateAsync();
}
}
catch (Exception e)
{
_logger.LogError(e, "Migration failed"); await HandleErrorAsync(dbContext, e);
}
} /// <summary>
/// 并发场景执行迁移
/// </summary>
/// <param name="dbContext"></param>
/// <param name="wait">是否等待至正在进行中的迁移完成</param>
/// <returns></returns>
public void ConcurrentMigrate(DbContext dbContext, bool wait = true)
{
if (!dbContext.Database.GetPendingMigrations().Any())
{
return;
} using var connection = MySqlConnectionHelper.CreateConnection(dbContext.Database.GetDbConnection().Database); ConcurrentMigrate(dbContext, connection, wait);
} /// <summary>
/// 并发场景执行迁移
/// </summary>
/// <param name="dbContext"></param>
/// <param name="wait">是否等待至正在进行中的迁移完成</param>
/// <returns></returns>
public async Task ConcurrentMigrateAsync(DbContext dbContext, bool wait = true)
{
if ((await dbContext.Database.GetPendingMigrationsAsync()).Any())
{
return;
} await using var connection = await MySqlConnectionHelper.CreateConnectionAsync(dbContext.Database.GetDbConnection().Database); await ConcurrentMigrateAsync(dbContext, connection, wait);
} /// <summary>
/// 并发场景执行迁移(供数据同步相关服务使用,”迁移锁“ 使用传入的 <see cref="IDbConnection"/> 对象来完成)
/// </summary>
/// <param name="dbContext"></param>
/// <param name="connection"></param>
/// <param name="wait">是否等待至正在进行中的迁移完成</param>
public void ConcurrentMigrate(DbContext dbContext, IDbConnection connection, bool wait = true)
{
if (!dbContext.Database.GetPendingMigrations().Any())
{
return;
} while (true)
{
if (_migrateLock.TryAcquireLock(connection))
{
try
{
Migrate(dbContext); break;
}
finally
{
_migrateLock.ReleaseLock(connection);
}
} if (wait)
{
_logger.LogWarning("Migration is locked, wait for 2 seconds");
Thread.Sleep(20000); continue;
} _logger.LogInformation("Migration is locked, skip");
}
} /// <summary>
/// 并发场景执行迁移(供数据同步相关服务使用,”迁移锁“ 使用传入的 <see cref="IDbConnection"/> 对象来完成)
/// </summary>
/// <param name="dbContext"></param>
/// <param name="connection"></param>
/// <param name="wait">是否等待至正在进行中的迁移完成</param>
public async Task ConcurrentMigrateAsync(DbContext dbContext, IDbConnection connection, bool wait = true)
{
if ((await dbContext.Database.GetPendingMigrationsAsync()).Any())
{
return;
} while (true)
{
if (await _migrateLock.TryAcquireLockAsync(connection))
{
try
{
await MigrateAsync(dbContext);
break;
}
finally
{
await _migrateLock.ReleaseLockAsync(connection);
}
} if (wait)
{
_logger.LogWarning("Migration is locked, wait for 2 seconds");
Thread.Sleep(20000); continue;
} _logger.LogInformation("Migration is locked, skip"); break;
}
} private void HandleError(DbContext dbContext, Exception e)
{
var needChangeList = dbContext.Database.GetPendingMigrations().ToList();
var allChangeList = dbContext.Database.GetMigrations().ToList();
var hasChangeList = dbContext.Database.GetAppliedMigrations().ToList(); if (needChangeList.Count + hasChangeList.Count > allChangeList.Count)
{
int errIndex = allChangeList.Count - needChangeList.Count; if (hasChangeList.Count - 1 == errIndex && hasChangeList[errIndex] != needChangeList[0])
{
int index = needChangeList[0].IndexOf("_", StringComparison.Ordinal);
string errSuffix = needChangeList[0].Substring(index, needChangeList[0].Length - index);
if (hasChangeList[errIndex].EndsWith(errSuffix))
{
dbContext.Database.ExecuteSqlRaw($"Update __EFMigrationsHistory set MigrationId = '{needChangeList[0]}' where MigrationId = '{hasChangeList[errIndex]}'");
dbContext.Database.Migrate();
}
else
{
throw e;
}
}
else
{
throw e;
}
}
else
{
throw e;
} _logger.LogInformation("Migration failed, but success on second try.");
} private async Task HandleErrorAsync(DbContext dbContext, Exception e)
{
var needChangeList = (await dbContext.Database.GetPendingMigrationsAsync()).ToList();
var allChangeList = dbContext.Database.GetMigrations().ToList();
var hasChangeList = (await dbContext.Database.GetAppliedMigrationsAsync()).ToList(); if (needChangeList.Count + hasChangeList.Count > allChangeList.Count)
{
int errIndex = allChangeList.Count - needChangeList.Count; if (hasChangeList.Count - 1 == errIndex && hasChangeList[errIndex] != needChangeList[0])
{
int index = needChangeList[0].IndexOf("_", StringComparison.Ordinal);
string errSuffix = needChangeList[0].Substring(index, needChangeList[0].Length - index);
if (hasChangeList[errIndex].EndsWith(errSuffix))
{
await dbContext.Database.ExecuteSqlRawAsync($"Update __EFMigrationsHistory set MigrationId = '{needChangeList[0]}' where MigrationId = '{hasChangeList[errIndex]}'");
await dbContext.Database.MigrateAsync();
}
else
{
throw e;
}
}
else
{
throw e;
}
}
else
{
throw e;
} _logger.LogInformation("Migration failed, but success on second try.");
}
}

EntityFramework Core并发迁移解决方案的更多相关文章

  1. EntityFramework Core并发深挖详解,一纸长文,你准备好看完了吗?

    前言 之前有关EF并发探讨过几次,但是呢,博主感觉还是有问题,为什么会觉得有问题,其实就是理解不够透彻罢了,于是在项目中都是用的存储过程或者SQL语句来实现,利用放假时间好好补补EF Core并发的问 ...

  2. EntityFramework Core并发导致显示插入主键问题

    前言 之前讨论过EntityFramework Core中并发问题,按照官网所给并发冲突解决方案以为没有什么问题,但是在做单元测试时发现too young,too siimple,下面我们一起来看看. ...

  3. EntityFramework Core并发导致显式插入主键问题

    前言 之前讨论过EntityFramework Core中并发问题,按照官网所给并发冲突解决方案以为没有什么问题,但是在做单元测试时发现too young,too simple,下面我们一起来看看. ...

  4. EntityFramework Core解决并发详解

    前言 对过年已经无感,不过还是有很多闲暇时间来学学东西,这一点是极好的,好了,本节我们来讲讲EntityFramewoek Core中的并发问题. 话题(EntityFramework Core并发) ...

  5. EntityFramework Core高并发深挖详解,一纸长文,你准备好了吗?

    前言 之前有关EF并发探讨过几次,但是呢,博主感觉还是有问题,为什么会觉得有问题,其实就是理解不够透彻罢了,于是在项目中都是用的存储过程或者SQL语句来实现,利用放假时间好好补补EF Core并发的问 ...

  6. EntityFramework Core迁移时出现数据库已存在对象问题解决方案

    前言 刚开始接触EF Core时本着探索的精神去搞,搞着搞着发现出问题了,后来就一直没解决,觉得很是不爽,借着周末好好看看这块内容. EntityFramework Core迁移出现对象在数据库中已存 ...

  7. Cookies 初识 Dotnetspider EF 6.x、EF Core实现dynamic动态查询和EF Core注入多个上下文实例池你知道有什么问题? EntityFramework Core 运行dotnet ef命令迁移背后本质是什么?(EF Core迁移原理)

    Cookies   1.创建HttpCookies Cookie=new HttpCookies("CookieName");2.添加内容Cookie.Values.Add(&qu ...

  8. EntityFramework Core 运行dotnet ef命令迁移背后本质是什么?(EF Core迁移原理)

    前言 终于踏出第一步探索EF Core原理和本质,过程虽然比较漫长且枯燥乏味还得反复论证,其中滋味自知,EF Core的强大想必不用我再过多废话,有时候我们是否思考过背后到底做了些什么,到底怎么实现的 ...

  9. EntityFramework Core 迁移忽略主外键关系

    前言 本文来源于一位公众号童鞋私信我的问题,在我若加思索后给出了其中一种方案,在此之前我也思考过这个问题,借此机会我稍微看了下,目前能够想到的也只是本文所述方案. 为何要忽略主外键关系 我们不仅疑惑为 ...

  10. EntityFramework Core 3多次Include导致查询性能低之解决方案

    前言 上述我们简单讲解了几个小问题,这节我们再来看看如标题EF Core中多次Include导致出现性能的问题,废话少说,直接开门见山. EntityFramework Core 3多次Include ...

随机推荐

  1. 题解:AT_abc359_e [ABC359E] Water Tank

    背景 中考结束了,但是暑假只有一天,这就是我现在能在机房里面写题解的原因-- 分析 这道题就是个单调栈. 题目上问你第一滴水流到每个位置的时间.我们考虑,答案其实就是比当前木板高且距离当前木板最近的那 ...

  2. [rCore学习笔记 06]运行Lib-OS

    QEMU运行第一章代码 切换分支 git checkout ch1 detail git checkout ch1 命令是用来切换到名为 ch1 的分支或者恢复工作目录中的文件到 ch1 提交的状态 ...

  3. Midnight Commander (MC)

    Midnight Commander GNU Midnight Commander 是一个可视化文件管理器,根据 GNU 通用公共许可证获得许可,因此有资格成为自由软件.它是一个功能丰富的全屏文本模式 ...

  4. Pandas库学习笔记(1)

    参考:菜鸟教程 pandas库使用了NumPy的大多数功能.建议您先阅读有关NumPy的教程,然后再继续本教程. Pandas 适用于处理以下类型的数据: 与 SQL 或 Excel 表类似的,含异构 ...

  5. P6680 [CCO2019] Marshmallow Molecules 题解

    P6680 题意 一个 \(n\) 点 \(m\) 边的图,图无重边,无自环. 满足这样一条性质:如果三边互不相等,则三边可以构成三角形. 思路 思路简单,用集合的思想来做. 引用一下 K0stlin ...

  6. 测试工程师-年终总结PPT

    2022年年终总结-xxx 一.首页 2022年年终总结暨2023年工作计划 汇报人:测试组-xxx 日期: 2023.1.13 二.目录 1.年度工作概述 2.工作亮点展示 3.持续精进点 4.明年 ...

  7. web3产品介绍:mask将Web3的隐私和优势引入像Facebook和Twitter这样的社交媒体平台

    介绍: Mask Network是一个开源的浏览器扩展,将Web3的隐私和优势引入像Facebook和Twitter这样的社交媒体平台.它是一个功能强大的工具,允许用户在社交媒体上享受区块链的隐私保护 ...

  8. 【MySQL】全库调整表大小写语句

    统一修改字段成小写+下划线的命名规则: V1上线后,重新看SQL调整的较可行的写法: # = = = = = = = = = = = = = = = 统一更改全库所有字段大小写脚本SQL(会删除字段原 ...

  9. 【Spring-Security】Re06 自定义Access & 注解权限分配

    一.基于ACCESS方法处理的实现: 我们之前使用的任何放行规则的方法,本质上还是调用access方法执行的 这也意味之我们可以直接使用access方法去方向,只需要注入不同的字符串即可 自定义Acc ...

  10. 使用 Alba 对 AspnetCore项目进行测试

    前言 在AspnetCore生态系统中,我们测试项目一般使用Microsoft.AspNetCore.TestHost的TestServer 到.NET6后提供的Microsoft.AspNetCor ...