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