场景

目前一个项目中数据持久化采用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. let 和 const 是 JavaScript 中用于声明变量的关键字

    let 和 const 是 JavaScript 中用于声明变量的关键字. let 关键字用于声明可变(可重新赋值)的变量.通过使用 let 关键字声明的变量可以在其作用域内被重新赋值.例如: let ...

  2. [oeasy]python0108_谷腾堡活字_哥特字体_罗马帝国_希腊文化_文艺复兴

    谷腾堡活字 回忆上次内容 上次回顾了字型编码的进化过程 7-seg 七位数码管 显示数字   14-seg 十四位数码管 显示字母     ​   添加图片注释,不超过 140 字(可选)   米字管 ...

  3. oeasy教您玩转vim - 25 - 更多颜色

    ​ 更多颜色 回忆上节课内容 我们上次深入了配色方案 定义了自己的配色方案 oeasy 建立了自己的配色 oeasy 在状态栏应用了自己的配色 ​ 明确能用的颜色 先胡乱地尝试一下修改颜色代码 hi ...

  4. 02-springboot配置

    目录 1,前言 2,YAML介绍 3,获取yml配置文件内容 4,springboot的配置文件 5,springboot使用@Value实现映射 6,@PropertySource.@ImportR ...

  5. odoo 给form表单视图内联列表添加按钮

    实践环境 Odoo 14.0-20221212 (Community Edition) 代码实现 模块文件组织结构 说明:为了更好的表达本文主题,一些和主题无关的文件.代码已略去 odoo14\cus ...

  6. 云计算:基于Redis的文章投票系统(Python完整版)

    | Redis的安装不懂的可前往 https://www.zeker.top/posts/9d3a5b2a/ 网上搜到的代码很多,但大多都有点小毛病(方法不可用,逻辑错误等) 自己基于网上找到的代码进 ...

  7. 【Big Data】 DBeaver连接Phoenix

    前言 Phoenix是Hbase数据库的一个SQL化中间件 Hbase本身是一个NoSQL类型的列族库,Phoenix可以将其转换成SQL操作 Phoenix提供的客户端Jar包,可以让DBeaver ...

  8. 【Tool】常用软件地址(装机备用)

    浏览器: 360极速 https://browser.360.cn/ee/ 谷歌 https://www.google.cn/chrome/ 社交通讯 微信 https://weixin.qq.com ...

  9. .Net内存管理释放的两种方式

    在.Net中,资源回收主要是指内存管理和非托管资源的释放.分别提供了两种主要的方式进行处理: 垃圾回收(GC) 确认性资源释放(DRD) 官网相关文档的链接:https://learn.microso ...

  10. 强化学习baseline论文—— rainbow算法中给出实验结果的54个atari2600游戏名称列表

    alien amidar assault asterix asteroids atlantis bank_heist battle_zone beam_rider berzerk bowling bo ...