微服务架构中的数据库架构管理

微服务架构中,经常面临数据库资源的限制问题。例如,数据库托管服务的价格计划可能只包含有限数量的数据库。为了节省成本,不能为每个微服务创建一个单独的数据库。那么,如何有效地解决这个问题呢?本文将介绍一种解决方案,即使用单一数据库和自定义架构来避免数据冲突。

高级方法

本文以SQL Server作为数据库示例。解决方案其实很简单:所有微服务将使用同一个数据库。但是,如何确保不会有冲突呢?答案是使用架构(schemas)。每个微服务将在特定的数据库架构中创建数据库对象(表、视图、存储过程等),这个架构在所有微服务中是唯一的。为了避免访问其他微服务的数据,将为每个微服务创建单独的登录和用户,并仅授予他们对一个架构的权限。

例如,对于处理订单的微服务,可以这样操作:

CREATE LOGIN [orders_login] WITH PASSWORD = 'p@ssw0rd'; EXECUTE ( 'CREATE SCHEMA [orders]' ); CREATE USER [orders_user] FOR LOGIN [orders_login] WITH DEFAULT_SCHEMA=[orders]; GRANT CREATE TABLE to [orders_user]; GRANT ALTER, DELETE, SELECT, UPDATE, INSERT, REFERENCES ON SCHEMA :: [orders] to [orders_user];

现在可以创建数据库对象了。

FluentMigrator的使用

将使用FluentMigrator NuGet包来修改数据库结构。首先配置它:

var serviceProvider = new ServiceCollection() .AddFluentMigratorCore() .ConfigureRunner(builder => { builder .AddSqlServer2016() .WithGlobalConnectionString(connectionString) .ScanIn(typeof(Database).Assembly).For.Migrations(); }) .BuildServiceProvider();

这里,使用SQL Server2016或更高版本。connectionString变量包含数据库的连接字符串。Database可以是包含迁移的程序集中的任何类型。但是,什么是迁移呢?

迁移是描述想要对数据库进行的更改的方式。每个迁移是一个简单的类,继承自Migration:

[Migration(1)] public class FirstMigration : Migration { public const string TableName = "orders"; public override void Up() { Create.Table(TableName) .WithColumn("id").AsInt32().PrimaryKey().Identity() .WithColumn("code").AsString(100).NotNullable(); } public override void Down() { Delete.Table(TableName); } }

在Up和Down方法中,描述了应用和回滚迁移时想要执行的操作。Migration属性包含一个数字,指定了迁移应用的顺序。

现在,非常简单地将迁移应用到数据库:

var runner = serviceProvider.GetRequiredService(); runner.MigrateUp();

就是这样。现在,所有的迁移都应该已经应用到数据库了。FluentMigrator还会创建一个VersionInfo表,包含所有当前应用的迁移的信息。借助这个表,FluentMigrator将知道下次应该额外应用哪些迁移到数据库。

问题与解决方案

不幸的是,对于用例,事情并不那么简单。有两个问题。首先,VersionInfo表默认在dbo架构中创建。但这对来说是不可接受的。每个微服务必须在其自己的架构中有自己的VersionInfo表。第二个问题是,考虑以下迁移代码:

Create.Table("orders")

这段代码在dbo架构中创建了orders表。当然,可以显式指定架构:

Create.Table("orders").InSchema("orders")

但更倾向于避免这样做。有人会忘记写这个架构,可能会出错。希望为整个微服务替换默认架构。

设置VersionInfo表的自定义架构非常简单:

var serviceProvider = new ServiceCollection() .AddSingleton(new DefaultConventionSet("orders", null)) .AddFluentMigratorCore() .ConfigureRunner(builder => { builder .AddSqlServer2016() .WithGlobalConnectionString(connectionString) .ScanIn(typeof(Database).Assembly).For.Migrations(); }) .BuildServiceProvider();

这里,为IConventionSet接口注册了DefaultConventionSet类的新实例,指定了相应的架构。现在VersionInfo表将在orders架构中创建。

不幸的是,理解如何替换其他数据库对象的默认架构并不容易。这花了一些时间。让从AddSqlServer2016方法的代码开始。它注册了SqlServer2008Quoter类的实例。这个类继承了SqlServer2005Quoter类的QuoteSchemaName方法。在这里,可以看到默认架构来自哪里。

将用自己的Quoter类替换这个Quoter类:

sealed class Quoter : SqlServer2008Quoter { private readonly string _defaultSchemaName; public Quoter(string defaultSchemaName) { if (string.IsNullOrWhiteSpace(defaultSchemaName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(defaultSchemaName)); _defaultSchemaName = defaultSchemaName; } public override string QuoteSchemaName(string schemaName) { if (string.IsNullOrEmpty(schemaName)) return $"[{_defaultSchemaName}]"; return base.QuoteSchemaName(schemaName); } }

如所见,实现几乎与SqlServer2005Quoter类相同,但使用的是自定义架构,而不是dbo。现在只需要注册这个类:

var serviceProvider = new ServiceCollection() .AddSingleton(new DefaultConventionSet("orders", null)) .AddFluentMigratorCore() .ConfigureRunner(builder => { builder .AddSqlServer2016() .WithGlobalConnectionString(connectionString) .ScanIn(typeof(Database).Assembly).For.Migrations(); builder.Services.RemoveAll() .AddSingleton(new Quoter("orders")); }) .BuildServiceProvider();
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485