18、高性能分片数据库操作指南

高性能分片数据库操作指南

1. 分片读取操作

在了解了应用程序和库如何处理连接字符串后,接下来需要知道分片如何对多个数据库执行 SELECT 操作。在最简单的形式中,库会对之前定义的连接列表执行 SELECT 操作。

客户端应用程序调用 ExecuteShardQuery 方法,该方法会遍历 SqlConnection 对象列表。为避免客户端代码多次调用此方法时可能出现的冲突,会先复制每个连接对象(因为一个连接一次只能进行一次调用)。然后,对于每个连接,代码会调用 ExecuteSingleQuery 方法,该方法是分片库中用于调用数据库的方法。

下面是使用 Parallel.ForEach 并行调用 ExecuteSingleQuery 方法的代码:

Parallel.ForEach(connections,
    delegate(SqlConnection c)
    {
        DataTable dt = ExecuteSingleQuery(command, c, exceptions);
        lock (data)
            data.Merge(dt, true, MissingSchemaAction.Add);
    }
);

以下是简化版的示例应用程序代码:

SqlCommand cmd = new SqlCommand();
DataTable dataRes = new DataTable();

cmd.CommandText = this.textBox1.Text;
dataRes = cmd.ExecuteShardQuery();

dataGridView2.DataSource = dataRes;

执行上述代码时,SELECT 语句会按预期返回数据库对象名称和类型。不过,显示结果中会添加一个额外的列 __guidDB__ ,这是之前介绍的 GUID 列。该列在读取时作用不大,但在后续的更新和删除操作中会发挥作用。

添加 GUID 的逻辑如下:

// Add the connection GUID to this set of records
// This helps us identify which row came from which connection
DataColumn col = dt.Columns.Add(_GUID_, typeof(string));
col.DefaultValue = connection.ConnectionGuid();

// Get the data
da.Fill(dt);
2. 缓存机制

为减少对源数据库的往返次数,分片库提供了可选的缓存机制。该库使用的缓存技术具备基本功能,并且可以扩展以应对更复杂的场景。其目标是在需要时缓存每个数据库后端的整个 DataTable

缓存会根据每个参数、参数值、每个 SQL 语句以及数据库的 GUID 计算缓存键。当连接到 SQL 数据库时,缓存的效果会很明显。由于首次连接到 SQL 数据库实例可能需要长达 250 毫秒,而内存访问速度要快得多。随着分片数据库中记录数量和数据库数量的增加,缓存的重要性也会提高。

缓存还提供了生存时间(TTL)机制,可实现绝对过期或滑动过期方案。绝对过期会在未来特定时间自动重置缓存,而滑动过期会在缓存项在过期前被使用时移动过期时间。以下是实现缓存的代码:

CacheItemPolicy cip = new CacheItemPolicy();
if (UseSlidingWindow)
    cip.SlidingExpiration = defaultTTL;
else
    cip.AbsoluteExpiration = new DateTimeOffset(System.DateTime.Now.Add(defaultTTL));
MemoryCache.Default.Add(cacheKey, dt, cip);

可以通过多种方式增强缓存技术:
- 当缓存中的 DataTable 对象包含大量行时,可以对其进行压缩。虽然压缩算法可能会增加延迟,但整体性能提升可能值得这点延迟。
- 创建不同的缓存容器,以便更精细地控制每个容器存储的数据类型。这样可以为每个容器设置不同的参数,例如对某个容器进行压缩,而对另一个容器不进行压缩。
- 该库提供的缓存是本地的,如果需要更强大的缓存,可以考虑使用 Windows Server AppFabric,其缓存技术具备企业级能力。

3. 分片记录的更新和删除

在分片数据库中进行更新和删除操作时,可以针对单个数据库中的记录,也可以针对所有数据库。以下是一些决策指南:
- 更新或删除单个数据库中的记录 :当已知要使用的数据库 GUID 时,可以更新或删除数据库中的一个或多个记录。使用分片检索记录时,返回的所有记录都会提供数据库 GUID。
- 更新或删除跨数据库的记录 :当不知道记录所在的数据库,或者需要评估所有记录时,通常会更新或删除分片中跨数据库的记录。

更新单个记录的代码如下:

cmd.CommandText = "sproc_update_user";
cmd.CommandType = CommandType.StoredProcedure;

cmd.Parameters.Add(new SqlParameter("@id", SqlDbType.Int));
cmd.Parameters["@id"].Value = int.Parse(labelIDVal.Text);
cmd.Parameters.Add(new SqlParameter("@name", SqlDbType.NVarChar, 20));
cmd.Parameters["@name"].Value = textBoxUser.Text;

cmd.Parameters.Add(new SqlParameter(PYN.EnzoAzureLib.Shard._GUID_, labelGUID.Text)); 
ExecuteShardNonQuery (cmd);

删除记录的代码与更新记录类似:

cmd.CommandText = "sproc_delete_user";
cmd.CommandType = CommandType.StoredProcedure;

cmd.Parameters.Add(new SqlParameter("@id", SqlDbType.Int));
cmd.Parameters["@id"].Value = int.Parse(labelIDVal.Text);

cmd.Parameters.Add(new SqlParameter(PYN.EnzoAzureLib.Shard._GUID_, labelGUID.Text));
ExecuteShardNonQuery (cmd);

ExecuteShardNonQuery 方法的行为取决于其数据库 GUID 参数:
- 如果没有 GUID 参数,会对所有数据库执行查询。
- 如果有带值的数据库 GUID 参数,会对指定的数据库执行查询。
- 如果数据库 GUID 参数为 NULL,会使用轮询方式对分片中的下一个数据库执行查询。

在更新或删除记录时,客户端代码会清除缓存,以强制未来的 SELECT 语句从分片中的数据库获取记录。可以通过在缓存中执行相同的更新或删除操作来改进此逻辑。

更新或删除跨数据库记录的代码如下:

PYN.EnzoAzureLib.Shard.UseParallel = checkBoxParallel.Checked; 
cmd.CommandText = "UPDATE TestUsers2 SET LastUpdated = GETDATE()";
cmd.CommandType = CommandType.Text;
ExecuteShardNonQuery (cmd);
4. 向分片中添加记录

向分片数据库中添加记录有两种方式:
- 单个数据库 :首次加载分片时,可以选择将某些记录加载到特定数据库中。或者,如果某个数据库的硬件性能更优,可以向该数据库加载更多记录。
- 跨数据库 :通常在不指定数据库的情况下向分片中加载记录,分片库会使用轮询机制来加载记录。

向特定数据库添加记录的操作与更新或删除数据库中的记录类似,只需创建 SqlCommand 对象,设置 INSERT 语句,并添加表示要使用的数据库 GUID 的 SqlParameter

向跨数据库添加一个或多个记录需要不同的方法。分片库提供了两种插入方法:
- ExecuteShardNonQuery :如果 GUID 参数为 NULL,该方法会扩展 SqlCommand 对象,并使用轮询方式对分片中的下一个数据库执行语句。
- ExecuteParallelRoundRobinLoad :该方法扩展了 List<SqlCommand> ,提供了创建 SqlCommand 对象集合的机制。每个 SqlCommand 对象包含要执行的 INSERT 语句。该方法会添加 NULL 数据库 GUID,并调用 ExecuteShardNonQuery 以轮询方式执行所有语句。

以下是客户端调用 ExecuteParallelRoundRobinLoad 的代码:

List<SqlCommand> commands = new List<SqlCommand>();

foreach (string name in userName)
{
    if (name != null && name.Trim().Length > 0)
    {
        SqlCommand cmdToAdd = new SqlCommand();
        cmdToAdd.CommandText = "sproc_add_user";
        cmdToAdd.CommandType = CommandType.StoredProcedure;

        cmdToAdd.Parameters.Add(
            new SqlParameter("name", SqlDbType.NVarChar, 20));
        cmdToAdd.Parameters["name"].Value = name;

        commands.Add(cmdToAdd);
    }
}

// Make the call!
if (commands.Count > 0)
{
    commands.ExecuteParallelRoundRobinLoad();
    Shard.ResetCache();
}
5. 分片管理
5.1 异常管理

当前的分片库不处理回滚操作,但可能会抛出需要代码捕获的异常。库通过 TPL 提供的 AggregateException 类处理异常,该类可以容纳多个异常。因为库会并行执行数据库调用,所以可能会同时发生多个异常,需要将这些异常聚合并返回给客户端进行进一步处理。

ExecuteSingleNonQuery 方法会接收一个 ConcurrentQueue<Exception> 参数,用于存储异常。如果检测到异常,会将其添加到队列中;如果未提供队列,则会重新抛出异常。

private static long ExecuteSingleNonQuery(
    SqlCommand command,
    SqlConnection connectionToUse,
    System.Collections.Concurrent.ConcurrentQueue<Exception> exceptions
)
{
    try
    {
        // . . .
    }
    catch (Exception ex)
    {
        if (exceptions != null)
            exceptions.Enqueue(ex);
        else
            throw;
    }
}

ExecuteShardNonQuery 方法会调用 ExecuteSingleNonQuery 方法,并在并行执行数据库调用完成后检查异常队列是否为空。如果不为空,则抛出 AggregateException

var exceptions = new System.Collections.Concurrent.ConcurrentQueue<Exception>();

Parallel.ForEach(connections, delegate(SqlConnection c)
{
    long rowsAffected = ExecuteSingleNonQuery(command, c, exceptions);

    lock (alock)
        res += rowsAffected;
});

if (!exceptions.IsEmpty)
    throw new AggregateException(exceptions);
5.2 性能管理

分片库允许客户端应用程序水平扩展数据库的部分或全部内容,旨在提高性能、可扩展性或两者兼得。然而,分片并不一定能提高性能,在某些情况下,分片反而会损害性能和可扩展性,因为分片会带来额外的开销。

以下是标准 ADO.NET 调用和分片获取相同记录时的最佳和最坏情况对比:
| 情况 | 操作 |
| ---- | ---- |
| 最佳情况 | 所有记录分布在三个不同的数据库中,分片可以并发访问所有三个数据库,聚合结果集,并对数据进行过滤和/或排序。但需要处理连接数据库、获取数据、数据聚合、排序和过滤等操作,会消耗处理时间。 |
| 最坏情况 | 所有操作无法并行执行,需要串行执行。例如,TPL 检测到只有单个处理器可用时。 |
| 混合情况 | 部分调用可以并行进行,但不是全部。 |

不过,在某些场景下,分片可以提高性能和可扩展性。例如,一个 DOC 表包含两条记录,其中一个 varbinary Document 存储了几兆字节的 PDF 文件。当两条记录都在一个数据库中时,执行 SELECT * FROM DOCS 语句平均需要 2.5 秒;但将第二条记录移动到另一个 SQL 数据库实例后,平均执行时间降至约 1.8 秒。如果不返回 Document 字段,执行时间仅需 103 毫秒。这表明即使存在开销,使用分片仍可以带来性能提升。

综上所述,使用分片库需要进行适当的规划,以确保能够实现性能和可扩展性的提升。

高性能分片数据库操作指南(续)

6. 操作流程总结

为了更清晰地展示在分片数据库中进行各种操作的流程,我们将其总结如下:

6.1 读取操作流程
graph TD;
    A[客户端应用程序] --> B[调用 ExecuteShardQuery 方法];
    B --> C[遍历 SqlConnection 对象列表];
    C --> D[复制每个连接对象];
    D --> E[调用 ExecuteSingleQuery 方法];
    E --> F[执行 SELECT 操作并获取结果];
    F --> G[合并结果集到 DataTable];
    G --> H[显示记录];

具体步骤如下:
1. 客户端应用程序调用 ExecuteShardQuery 方法。
2. 该方法遍历 SqlConnection 对象列表,并复制每个连接对象以避免冲突。
3. 针对每个连接对象调用 ExecuteSingleQuery 方法执行 SELECT 操作。
4. 将每个连接的查询结果合并到 DataTable 中。
5. 最后将 DataTable 绑定到数据源进行显示。

6.2 更新和删除操作流程
graph TD;
    A[确定操作类型(更新或删除)] --> B[选择操作范围(单个数据库或跨数据库)];
    B -- 单个数据库 --> C[创建 SqlCommand 对象并设置参数,包含数据库 GUID];
    B -- 跨数据库 --> D[创建 SqlCommand 对象,不指定数据库 GUID];
    C --> E[调用 ExecuteShardNonQuery 方法];
    D --> E;
    E --> F[执行操作];
    F --> G[清除缓存];

详细步骤:
1. 首先确定是更新还是删除操作,以及操作范围是单个数据库还是跨数据库。
2. 如果是单个数据库操作,创建 SqlCommand 对象并添加数据库 GUID 参数;如果是跨数据库操作,不指定数据库 GUID。
3. 调用 ExecuteShardNonQuery 方法执行操作。
4. 操作完成后清除缓存,确保后续查询从数据库获取最新数据。

6.3 添加记录操作流程
graph TD;
    A[确定添加方式(单个数据库或跨数据库)] --> B -- 单个数据库 --> C[创建 SqlCommand 对象,设置 INSERT 语句和数据库 GUID];
    A --> B -- 跨数据库 --> D[创建 List<SqlCommand> 集合];
    D --> E[遍历要添加的数据,创建 SqlCommand 对象并添加到集合];
    C --> F[调用 ExecuteShardNonQuery 方法];
    E --> G[调用 ExecuteParallelRoundRobinLoad 方法];
    F --> H[执行插入操作];
    G --> H;
    H --> I[清除缓存];

步骤说明:
1. 确定添加记录的方式是单个数据库还是跨数据库。
2. 若为单个数据库,创建 SqlCommand 对象并设置 INSERT 语句和数据库 GUID,然后调用 ExecuteShardNonQuery 方法。
3. 若为跨数据库,创建 List<SqlCommand> 集合,遍历要添加的数据,为每个数据创建 SqlCommand 对象并添加到集合中,最后调用 ExecuteParallelRoundRobinLoad 方法。
4. 执行插入操作后清除缓存。

7. 常见问题及解决方法

在使用分片数据库进行操作时,可能会遇到一些常见问题,以下是这些问题及相应的解决方法:

问题 描述 解决方法
性能问题 分片操作可能会因为额外的开销导致性能下降,如循环连接数据库、数据聚合等操作消耗时间。 1. 合理规划分片策略,确保数据均匀分布在各个数据库中。
2. 使用缓存机制减少对数据库的频繁访问。
3. 评估硬件配置,确保有足够的 CPU 资源支持并行处理。
异常处理 并行执行数据库调用时可能会同时抛出多个异常,需要进行聚合处理。 1. 使用 AggregateException 类来聚合多个异常。
2. 在 ExecuteSingleNonQuery 方法中使用 ConcurrentQueue<Exception> 存储异常。
3. 在 ExecuteShardNonQuery 方法中检查异常队列,若不为空则抛出 AggregateException
缓存管理 缓存可能会过期或需要更新,以保证数据的一致性。 1. 使用缓存的 TTL 机制,根据业务需求设置绝对过期或滑动过期方案。
2. 在更新、删除或添加记录后,及时清除缓存或更新缓存中的数据。
数据一致性问题 跨数据库操作时可能会出现数据不一致的情况。 1. 确保在更新、删除或添加记录时,对涉及的所有数据库进行相应的操作。
2. 使用事务管理,保证操作的原子性。
8. 总结与建议

通过以上对分片数据库的读取、缓存、更新、删除、添加操作以及异常和性能管理的介绍,我们可以看到分片数据库在提高性能和可扩展性方面具有一定的潜力,但也面临着一些挑战。以下是一些总结和建议:

  • 合理使用缓存 :缓存机制可以显著减少对数据库的往返次数,提高性能。根据业务需求选择合适的缓存策略,如压缩缓存数据、创建不同的缓存容器等。同时,要注意缓存的更新和过期管理,确保数据的一致性。
  • 异常处理要完善 :由于分片数据库会并行执行数据库调用,可能会同时出现多个异常。使用 AggregateException 类和 ConcurrentQueue<Exception> 来聚合和管理异常,确保客户端能够及时处理这些异常。
  • 性能规划很重要 :分片并不总是能提高性能,在某些情况下可能会带来额外的开销。在使用分片库之前,要进行充分的性能评估和规划,确保数据均匀分布在各个数据库中,以实现性能和可扩展性的提升。
  • 操作范围要明确 :在进行更新、删除和添加记录操作时,要明确操作范围是单个数据库还是跨数据库,并根据不同的情况设置相应的参数。同时,在操作完成后及时清除缓存,保证后续查询的准确性。

总之,掌握分片数据库的操作技巧和管理方法,可以帮助我们更好地应对大规模数据处理和高并发访问的需求,提高应用程序的性能和可扩展性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值