Joe*_*ish 31 sql-server etl sql-clr hashing sql-server-2016
作为 ETL 过程的一部分,我们将暂存中的行与报告数据库进行比较,以确定自上次加载数据以来是否有任何列实际发生了更改。
比较基于表的唯一键和所有其他列的某种散列。我们目前使用HASHBYTES该SHA2_256算法,并发现如果许多并发工作线程都在调用HASHBYTES.
在 96 核服务器上进行测试时,以每秒哈希数衡量的吞吐量不会增加超过 16 个并发线程。我通过将并发MAXDOP 8查询的数量从 1更改为12 来进行测试。测试MAXDOP 1显示了相同的可扩展性瓶颈。
作为一种解决方法,我想尝试 SQL CLR 解决方案。这是我试图说明要求的尝试:
NVARCHAR或VARBINARY字符串的输入(所有相关列都连接在一起)CHECKSUM对我们不起作用,因为冲突太多。对于 Application Reasons™,假设我无法保存报告表的哈希值。这是一个不支持触发器或计算列的 CCI(还有其他我不想讨论的问题)。
HASHBYTES使用 SQL CLR 函数进行模拟的可扩展方式是什么?我的目标可以表示为在大型服务器上每秒获得尽可能多的哈希值,因此性能也很重要。我对 CLR 很糟糕,所以我不知道如何做到这一点。如果它激励任何人回答,我计划尽快为这个问题添加赏金。下面是一个示例查询,它非常粗略地说明了用例:
DROP TABLE IF EXISTS #CHANGED_IDS;
SELECT stg.ID INTO #CHANGED_IDS
FROM (
SELECT ID,
CAST( HASHBYTES ('SHA2_256',
CAST(FK1 AS NVARCHAR(19)) +
CAST(FK2 AS NVARCHAR(19)) +
CAST(FK3 AS NVARCHAR(19)) +
CAST(FK4 AS NVARCHAR(19)) +
CAST(FK5 AS NVARCHAR(19)) +
CAST(FK6 AS NVARCHAR(19)) +
CAST(FK7 AS NVARCHAR(19)) +
CAST(FK8 AS NVARCHAR(19)) +
CAST(FK9 AS NVARCHAR(19)) +
CAST(FK10 AS NVARCHAR(19)) +
CAST(FK11 AS NVARCHAR(19)) +
CAST(FK12 AS NVARCHAR(19)) +
CAST(FK13 AS NVARCHAR(19)) +
CAST(FK14 AS NVARCHAR(19)) +
CAST(FK15 AS NVARCHAR(19)) +
CAST(STR1 AS NVARCHAR(500)) +
CAST(STR2 AS NVARCHAR(500)) +
CAST(STR3 AS NVARCHAR(500)) +
CAST(STR4 AS NVARCHAR(500)) +
CAST(STR5 AS NVARCHAR(500)) +
CAST(COMP1 AS NVARCHAR(1)) +
CAST(COMP2 AS NVARCHAR(1)) +
CAST(COMP3 AS NVARCHAR(1)) +
CAST(COMP4 AS NVARCHAR(1)) +
CAST(COMP5 AS NVARCHAR(1)))
AS BINARY(32)) HASH1
FROM HB_TBL WITH (TABLOCK)
) stg
INNER JOIN (
SELECT ID,
CAST(HASHBYTES ('SHA2_256',
CAST(FK1 AS NVARCHAR(19)) +
CAST(FK2 AS NVARCHAR(19)) +
CAST(FK3 AS NVARCHAR(19)) +
CAST(FK4 AS NVARCHAR(19)) +
CAST(FK5 AS NVARCHAR(19)) +
CAST(FK6 AS NVARCHAR(19)) +
CAST(FK7 AS NVARCHAR(19)) +
CAST(FK8 AS NVARCHAR(19)) +
CAST(FK9 AS NVARCHAR(19)) +
CAST(FK10 AS NVARCHAR(19)) +
CAST(FK11 AS NVARCHAR(19)) +
CAST(FK12 AS NVARCHAR(19)) +
CAST(FK13 AS NVARCHAR(19)) +
CAST(FK14 AS NVARCHAR(19)) +
CAST(FK15 AS NVARCHAR(19)) +
CAST(STR1 AS NVARCHAR(500)) +
CAST(STR2 AS NVARCHAR(500)) +
CAST(STR3 AS NVARCHAR(500)) +
CAST(STR4 AS NVARCHAR(500)) +
CAST(STR5 AS NVARCHAR(500)) +
CAST(COMP1 AS NVARCHAR(1)) +
CAST(COMP2 AS NVARCHAR(1)) +
CAST(COMP3 AS NVARCHAR(1)) +
CAST(COMP4 AS NVARCHAR(1)) +
CAST(COMP5 AS NVARCHAR(1)) )
AS BINARY(32)) HASH1
FROM HB_TBL_2 WITH (TABLOCK)
) rpt ON rpt.ID = stg.ID
WHERE rpt.HASH1 <> stg.HASH1
OPTION (MAXDOP 8);
Run Code Online (Sandbox Code Playgroud)
为了稍微简化一下,我可能会使用以下内容进行基准测试。我将HASHBYTES在星期一发布结果:
CREATE TABLE dbo.HASH_ME (
ID BIGINT NOT NULL,
FK1 BIGINT NOT NULL,
FK2 BIGINT NOT NULL,
FK3 BIGINT NOT NULL,
FK4 BIGINT NOT NULL,
FK5 BIGINT NOT NULL,
FK6 BIGINT NOT NULL,
FK7 BIGINT NOT NULL,
FK8 BIGINT NOT NULL,
FK9 BIGINT NOT NULL,
FK10 BIGINT NOT NULL,
FK11 BIGINT NOT NULL,
FK12 BIGINT NOT NULL,
FK13 BIGINT NOT NULL,
FK14 BIGINT NOT NULL,
FK15 BIGINT NOT NULL,
STR1 NVARCHAR(500) NOT NULL,
STR2 NVARCHAR(500) NOT NULL,
STR3 NVARCHAR(500) NOT NULL,
STR4 NVARCHAR(500) NOT NULL,
STR5 NVARCHAR(2000) NOT NULL,
COMP1 TINYINT NOT NULL,
COMP2 TINYINT NOT NULL,
COMP3 TINYINT NOT NULL,
COMP4 TINYINT NOT NULL,
COMP5 TINYINT NOT NULL
);
INSERT INTO dbo.HASH_ME WITH (TABLOCK)
SELECT RN,
RN % 1000000, RN % 1000000, RN % 1000000, RN % 1000000, RN % 1000000,
RN % 1000000, RN % 1000000, RN % 1000000, RN % 1000000, RN % 1000000,
RN % 1000000, RN % 1000000, RN % 1000000, RN % 1000000, RN % 1000000,
REPLICATE(CHAR(65 + RN % 10 ), 30)
,REPLICATE(CHAR(65 + RN % 10 ), 30)
,REPLICATE(CHAR(65 + RN % 10 ), 30)
,REPLICATE(CHAR(65 + RN % 10 ), 30)
,REPLICATE(CHAR(65 + RN % 10 ), 1000),
0,1,0,1,0
FROM (
SELECT TOP (100000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
) q
OPTION (MAXDOP 1);
SELECT MAX(HASHBYTES('SHA2_256',
CAST(N'' AS NVARCHAR(MAX)) + N'|' +
CAST(FK1 AS NVARCHAR(19)) + N'|' +
CAST(FK2 AS NVARCHAR(19)) + N'|' +
CAST(FK3 AS NVARCHAR(19)) + N'|' +
CAST(FK4 AS NVARCHAR(19)) + N'|' +
CAST(FK5 AS NVARCHAR(19)) + N'|' +
CAST(FK6 AS NVARCHAR(19)) + N'|' +
CAST(FK7 AS NVARCHAR(19)) + N'|' +
CAST(FK8 AS NVARCHAR(19)) + N'|' +
CAST(FK9 AS NVARCHAR(19)) + N'|' +
CAST(FK10 AS NVARCHAR(19)) + N'|' +
CAST(FK11 AS NVARCHAR(19)) + N'|' +
CAST(FK12 AS NVARCHAR(19)) + N'|' +
CAST(FK13 AS NVARCHAR(19)) + N'|' +
CAST(FK14 AS NVARCHAR(19)) + N'|' +
CAST(FK15 AS NVARCHAR(19)) + N'|' +
CAST(STR1 AS NVARCHAR(500)) + N'|' +
CAST(STR2 AS NVARCHAR(500)) + N'|' +
CAST(STR3 AS NVARCHAR(500)) + N'|' +
CAST(STR4 AS NVARCHAR(500)) + N'|' +
CAST(STR5 AS NVARCHAR(2000)) + N'|' +
CAST(COMP1 AS NVARCHAR(1)) + N'|' +
CAST(COMP2 AS NVARCHAR(1)) + N'|' +
CAST(COMP3 AS NVARCHAR(1)) + N'|' +
CAST(COMP4 AS NVARCHAR(1)) + N'|' +
CAST(COMP5 AS NVARCHAR(1)) )
)
FROM dbo.HASH_ME
OPTION (MAXDOP 1);
Run Code Online (Sandbox Code Playgroud)
Pau*_*ite 21
由于您只是在寻找更改,因此您不需要加密哈希函数。
您可以从Brandon Dahler的开源Data.HashFunction 库中选择一种更快的非加密散列,该库根据宽松和 OSI 批准的MIT 许可获得许可。SpookyHash是一个受欢迎的选择。
using Microsoft.SqlServer.Server;
using System.Data.HashFunction.SpookyHash;
using System.Data.SqlTypes;
public partial class UserDefinedFunctions
{
[SqlFunction
(
DataAccess = DataAccessKind.None,
SystemDataAccess = SystemDataAccessKind.None,
IsDeterministic = true,
IsPrecise = true
)
]
public static byte[] SpookyHash
(
[SqlFacet (MaxSize = 8000)]
SqlBinary Input
)
{
ISpookyHashV2 sh = SpookyHashV2Factory.Instance.Create();
return sh.ComputeHash(Input.Value).Hash;
}
[SqlFunction
(
DataAccess = DataAccessKind.None,
IsDeterministic = true,
IsPrecise = true,
SystemDataAccess = SystemDataAccessKind.None
)
]
public static byte[] SpookyHashLOB
(
[SqlFacet (MaxSize = -1)]
SqlBinary Input
)
{
ISpookyHashV2 sh = SpookyHashV2Factory.Instance.Create();
return sh.ComputeHash(Input.Value).Hash;
}
}
Run Code Online (Sandbox Code Playgroud)
源提供了两个函数,一个用于 8000 字节或更少的输入,一个 LOB 版本。非 LOB 版本应该明显更快。
COMPRESS如果事实证明这对性能来说是值得的,那么您也许可以将 LOB 二进制文件包装起来以使其低于 8000 字节的限制。或者,您可以将 LOB 分解为低于 8000 字节的段,或者简单地HASHBYTES为 LOB 情况保留使用(因为更长的输入可以更好地扩展)。
您显然可以自己获取包并编译所有内容,但我构建了下面的程序集以简化快速测试:
https://gist.github.com/SQLKiwi/365b265b476bf86754457fc9514b2300
CREATE FUNCTION dbo.SpookyHash
(
@Input varbinary(8000)
)
RETURNS binary(16)
WITH
RETURNS NULL ON NULL INPUT,
EXECUTE AS OWNER
AS EXTERNAL NAME Spooky.UserDefinedFunctions.SpookyHash;
GO
CREATE FUNCTION dbo.SpookyHashLOB
(
@Input varbinary(max)
)
RETURNS binary(16)
WITH
RETURNS NULL ON NULL INPUT,
EXECUTE AS OWNER
AS EXTERNAL NAME Spooky.UserDefinedFunctions.SpookyHashLOB;
GO
Run Code Online (Sandbox Code Playgroud)
给出问题中的示例数据的示例用法:
SELECT
HT1.ID
FROM dbo.HB_TBL AS HT1
JOIN dbo.HB_TBL_2 AS HT2
ON HT2.ID = HT1.ID
AND dbo.SpookyHash
(
CONVERT(binary(8), HT2.FK1) + 0x7C +
CONVERT(binary(8), HT2.FK2) + 0x7C +
CONVERT(binary(8), HT2.FK3) + 0x7C +
CONVERT(binary(8), HT2.FK4) + 0x7C +
CONVERT(binary(8), HT2.FK5) + 0x7C +
CONVERT(binary(8), HT2.FK6) + 0x7C +
CONVERT(binary(8), HT2.FK7) + 0x7C +
CONVERT(binary(8), HT2.FK8) + 0x7C +
CONVERT(binary(8), HT2.FK9) + 0x7C +
CONVERT(binary(8), HT2.FK10) + 0x7C +
CONVERT(binary(8), HT2.FK11) + 0x7C +
CONVERT(binary(8), HT2.FK12) + 0x7C +
CONVERT(binary(8), HT2.FK13) + 0x7C +
CONVERT(binary(8), HT2.FK14) + 0x7C +
CONVERT(binary(8), HT2.FK15) + 0x7C +
CONVERT(varbinary(1000), HT2.STR1) + 0x7C +
CONVERT(varbinary(1000), HT2.STR2) + 0x7C +
CONVERT(varbinary(1000), HT2.STR3) + 0x7C +
CONVERT(varbinary(1000), HT2.STR4) + 0x7C +
CONVERT(varbinary(1000), HT2.STR5) + 0x7C +
CONVERT(binary(1), HT2.COMP1) + 0x7C +
CONVERT(binary(1), HT2.COMP2) + 0x7C +
CONVERT(binary(1), HT2.COMP3) + 0x7C +
CONVERT(binary(1), HT2.COMP4) + 0x7C +
CONVERT(binary(1), HT2.COMP5)
)
<> dbo.SpookyHash
(
CONVERT(binary(8), HT1.FK1) + 0x7C +
CONVERT(binary(8), HT1.FK2) + 0x7C +
CONVERT(binary(8), HT1.FK3) + 0x7C +
CONVERT(binary(8), HT1.FK4) + 0x7C +
CONVERT(binary(8), HT1.FK5) + 0x7C +
CONVERT(binary(8), HT1.FK6) + 0x7C +
CONVERT(binary(8), HT1.FK7) + 0x7C +
CONVERT(binary(8), HT1.FK8) + 0x7C +
CONVERT(binary(8), HT1.FK9) + 0x7C +
CONVERT(binary(8), HT1.FK10) + 0x7C +
CONVERT(binary(8), HT1.FK11) + 0x7C +
CONVERT(binary(8), HT1.FK12) + 0x7C +
CONVERT(binary(8), HT1.FK13) + 0x7C +
CONVERT(binary(8), HT1.FK14) + 0x7C +
CONVERT(binary(8), HT1.FK15) + 0x7C +
CONVERT(varbinary(1000), HT1.STR1) + 0x7C +
CONVERT(varbinary(1000), HT1.STR2) + 0x7C +
CONVERT(varbinary(1000), HT1.STR3) + 0x7C +
CONVERT(varbinary(1000), HT1.STR4) + 0x7C +
CONVERT(varbinary(1000), HT1.STR5) + 0x7C +
CONVERT(binary(1), HT1.COMP1) + 0x7C +
CONVERT(binary(1), HT1.COMP2) + 0x7C +
CONVERT(binary(1), HT1.COMP3) + 0x7C +
CONVERT(binary(1), HT1.COMP4) + 0x7C +
CONVERT(binary(1), HT1.COMP5)
);
Run Code Online (Sandbox Code Playgroud)
使用 LOB 版本时,第一个参数应强制转换或转换为varbinary(max).
该Data.HashFunction库使用了一些被认为CLR语言功能UNSAFE由SQL Server。可以编写一个与SAFE状态兼容的基本 Spooky Hash 。我根据Jon Hanna 的SpookilySharp写的一个例子如下:
https://gist.github.com/SQLKiwi/7a5bb26b0bee56f6d28a1d26669ce8f2
Sol*_*zky 17
我不确定 SQLCLR 的并行性是否会更好/明显更好。然而,它真的很容易测试,因为在SQL# SQLCLR 库(我编写的)的免费版本中有一个名为Util_HashBinary的哈希函数。支持的算法有:MD5、SHA1、SHA256、SHA384 和 SHA512。
它需要一个VARBINARY(MAX)值作为输入,因此您可以连接每个字段的字符串版本(正如您当前所做的那样)然后转换为VARBINARY(MAX),或者您可以直接转到VARBINARY每一列并连接转换后的值(这可能会更快,因为您不是在处理字符串或从字符串到VARBINARY)的额外转换。下面是显示这两个选项的示例。它还显示了该HASHBYTES函数,因此您可以看到它与SQL#.Util_HashBinary之间的值相同。
请注意,连接VARBINARY值时的散列结果与连接NVARCHAR值时的散列结果不匹配。这是因为INT值“1”的二进制形式是 0x00000001,而“1”值的 UTF-16LE(即NVARCHAR)形式INT(二进制形式,因为这是散列函数将对其进行操作)是 0x3100。
SELECT so.[object_id],
SQL#.Util_HashBinary(N'SHA256',
CONVERT(VARBINARY(MAX),
CONCAT(so.[name], so.[schema_id], so.[create_date])
)
) AS [SQLCLR-ConcatStrings],
HASHBYTES(N'SHA2_256',
CONVERT(VARBINARY(MAX),
CONCAT(so.[name], so.[schema_id], so.[create_date])
)
) AS [BuiltIn-ConcatStrings]
FROM sys.objects so;
SELECT so.[object_id],
SQL#.Util_HashBinary(N'SHA256',
CONVERT(VARBINARY(500), so.[name]) +
CONVERT(VARBINARY(500), so.[schema_id]) +
CONVERT(VARBINARY(500), so.[create_date])
) AS [SQLCLR-ConcatVarBinaries],
HASHBYTES(N'SHA2_256',
CONVERT(VARBINARY(500), so.[name]) +
CONVERT(VARBINARY(500), so.[schema_id]) +
CONVERT(VARBINARY(500), so.[create_date])
) AS [BuiltIn-ConcatVarBinaries]
FROM sys.objects so;
Run Code Online (Sandbox Code Playgroud)
您可以使用以下方法测试与非 LOB Spooky 更相似的内容:
CREATE FUNCTION [SQL#].[Util_HashBinary8k]
(@Algorithm [nvarchar](50), @BaseData [varbinary](8000))
RETURNS [varbinary](8000)
WITH EXECUTE AS CALLER, RETURNS NULL ON NULL INPUT
AS EXTERNAL NAME [SQL#].[UTILITY].[HashBinary];
Run Code Online (Sandbox Code Playgroud)
注意:Util_HashBinary使用内置于 .NET 的托管 SHA256 算法,不应使用“bcrypt”库。
除了问题的这一方面之外,还有一些其他想法可能有助于此过程:
你提到了几点:
我们将 staging 中的行与报告数据库进行比较,以确定自上次加载数据以来是否有任何列实际发生了更改。
和:
我无法保存报告表的哈希值。这是一个不支持触发器或计算列的 CCI
和:
表可以在 ETL 过程之外更新
听起来这个报表中的数据在一段时间内是稳定的,只是被这个ETL过程修改了。
如果没有其他东西修改这个表,那么我们真的不需要触发器或索引视图(我最初认为你可能需要)。
由于您无法修改报告表的架构,是否至少可以创建一个相关表来包含预先计算的散列(以及计算时的 UTC 时间)?这将允许您有一个预先计算的值来与下一次进行比较,只留下需要计算散列的传入值。这会将呼叫数量减少到一个HASHBYTES或SQL#.Util_HashBinary一半。您只需在导入过程中加入这个哈希表即可。
您还将创建一个单独的存储过程,它只是刷新此表的哈希值。它只是更新已更改为当前行的任何相关行的哈希值,并更新这些修改行的时间戳。此过程可以/应该在更新此表的任何其他进程结束时执行。它也可以安排在此 ETL 开始前 30 到 60 分钟运行(取决于执行所需的时间以及这些其他进程中的任何一个可能运行的时间)。如果您怀疑可能存在不同步的行,它甚至可以手动执行。
随后注意到:
有超过500张桌子
如此多的表确实让每个表都有一个额外的表来包含当前的哈希值变得更加困难,但这并非不可能,因为它可以编写脚本,因为它是标准模式。脚本只需要考虑源表名称和源表 PK 列的发现。
尽管如此,无论哪种哈希算法最终被证明是最具可扩展性的,我仍然强烈建议至少找到几个表(也许有一些比其余 500 个表大得多)并设置一个相关的表来捕获当前哈希值,因此可以在 ETL 过程之前知道“当前”值。即使是最快的函数也无法超越永远不必首先调用它;-)。
VARBINARY而不是NVARCHAR)无论SQLCLR的VS内置的HASHBYTES,我还是会建议直接转化为VARBINARY作为应该会更快。连接字符串并不是非常有效。并且,除了首先将非字符串值转换为字符串之外,这还需要额外的工作(我假设工作量因基本类型而异:DATETIME需要超过BIGINT),而转换为VARBINARY只是为您提供基础值(大多数情况下)。
而且,事实上,测试其他测试使用的相同数据集,并使用HASHBYTES(N'SHA2_256',...),显示在一分钟内计算的总哈希值增加了 23.415%。而这种增加只是为了使用VARBINARY而不是NVARCHAR! ?(详情请看社区维基答案)
进一步的测试表明,影响性能(超过这一执行量)的一个方面是输入参数:数量和类型。
当前在我的 SQL# 库中的Util_HashBinary SQLCLR 函数有两个输入参数:一个VARBINARY(要散列的值)和一个NVARCHAR(要使用的算法)。这是由于我镜像了HASHBYTES函数的签名。但是,我发现如果我删除NVARCHAR参数并创建一个只执行 SHA256 的函数,那么性能会得到很好的提升。我认为即使将NVARCHAR参数切换到INT也会有所帮助,但我也认为即使没有额外的INT参数也至少会稍微快一点。
此外,SqlBytes.Value可能比SqlBinary.Value.
我为此测试创建了两个新函数:Util_HashSHA256Binary和Util_HashSHA256Binary8k。这些将包含在 SQL# 的下一个版本中(尚未设置日期)。
我还发现测试方法可以稍微改进,所以我更新了下面社区 wiki 答案中的测试工具以包括:
CHECKSUM记录了超过 9k 的冲突,即 9%(yikes)。HASHBYTES+ SQLCLR 在一起?)根据瓶颈所在的位置,使用内置HASHBYTES和 SQLCLR UDF 的组合来执行相同的散列甚至可能会有所帮助。如果内置函数的约束与 SQLCLR 操作不同/分开,那么这种方法可能比HASHBYTES单独或 SQLCLR 单独完成更多并发。这绝对值得测试。
David Browne 的回答中建议的哈希算法对象的缓存当然看起来很有趣,所以我尝试了它并发现了以下两个有趣的点:
无论出于何种原因,它似乎并没有提供太多的性能改进(如果有的话)。我可能做错了一些事情,但这是我尝试过的:
static readonly ConcurrentDictionary<int, SHA256Managed> hashers =
new ConcurrentDictionary<int, SHA256Managed>();
[return: SqlFacet(MaxSize = 100)]
[SqlFunction(IsDeterministic = true)]
public static SqlBinary FastHash([SqlFacet(MaxSize = 1000)] SqlBytes Input)
{
SHA256Managed sh = hashers.GetOrAdd(Thread.CurrentThread.ManagedThreadId,
i => new SHA256Managed());
return sh.ComputeHash(Input.Value);
}
Run Code Online (Sandbox Code Playgroud)对于ManagedThreadId特定查询中的所有 SQLCLR 引用,该值似乎都相同。我测试了对同一个函数的多个引用,以及对不同函数的引用,所有 3 个函数都被赋予了不同的输入值,并返回了不同的(但预期的)返回值。对于这两个测试函数,输出是一个字符串,其中包括ManagedThreadId以及哈希结果的字符串表示。ManagedThreadId对于查询中的所有 UDF 引用以及所有行,该值都相同。但是,相同输入字符串的哈希结果相同,不同输入字符串的哈希结果不同。
虽然我在测试中没有看到任何错误的结果,但这不会增加竞争条件的机会吗?如果在特定查询中调用的所有 SQLCLR 对象的字典键都相同,那么它们将共享为该键存储的相同值或对象,对吗?关键是,即使认为它似乎在这里工作(在某种程度上,似乎没有太多的性能提升,但在功能上没有任何问题),这并没有让我相信这种方法将在其他情况下工作。
Joe*_*ish 12
这不是一个传统的答案,但我认为发布迄今为止提到的一些技术的基准会有所帮助。我正在使用 SQL Server 2017 CU9 的 96 核服务器上进行测试。
许多可伸缩性问题是由并发线程争用某些全局状态引起的。例如,考虑经典的 PFS 页面争用。如果太多工作线程需要修改内存中的同一页面,就会发生这种情况。随着代码变得更高效,它可以更快地请求锁存器。这增加了争用。简单来说,高效的代码更容易导致可伸缩性问题,因为全局状态的竞争更加严重。慢速代码不太可能导致可伸缩性问题,因为全局状态的访问频率不高。
HASHBYTES可伸缩性部分基于输入字符串的长度。我的理论是为什么会发生这种情况HASHBYTES是调用函数时需要访问某些全局状态。易于观察的全局状态是在某些版本的 SQL Server 上每次调用都需要分配内存页。更难观察的是存在某种操作系统争用。因此,如果HASHBYTES代码调用频率较低,那么争用就会减少。降低HASHBYTES调用率的一种方法是增加每次调用所需的散列工作量。散列工作部分基于输入字符串的长度。为了重现我在应用程序中看到的可扩展性问题,我需要更改演示数据。合理的最坏情况是一张包含 21BIGINT列。表的定义包含在底部的代码中。为了减少 Local Factors™,我使用了MAXDOP 1在相对较小的表上运行的并发查询。我的快速基准代码在底部。
注意函数返回不同的哈希长度。MD5和SpookyHash都是 128 位散列,SHA256是 256 位散列。
NVARCHAR与VARBINARY转换和串联)为了查看转换为和连接VARBINARY是否真的比 更有效/性能NVARCHAR,从同一模板创建NVARCHAR了RUN_HASHBYTES_SHA2_256存储过程的一个版本(请参阅下面基准代码部分中的“步骤 5” )。唯一的区别是:
_NVCBINARY(8)因为该CAST功能已更改为NVARCHAR(15)0x7C 被改为 N'|'导致:
CAST(FK1 AS NVARCHAR(15)) + N'|' +
Run Code Online (Sandbox Code Playgroud)
代替:
CAST(FK1 AS BINARY(8)) + 0x7C +
Run Code Online (Sandbox Code Playgroud)
下表包含 1 分钟内执行的哈希数。这些测试是在与下面提到的其他测试所用的服务器不同的服务器上执行的。
????????????????????????????????????????????
? Datatype ? Test # ? Total Hashes ?
????????????????????????????????????????????
? NVARCHAR ? 1 ? 10200000 ?
? NVARCHAR ? 2 ? 10300000 ?
? NVARCHAR ? AVERAGE ? * 10250000 * ?
? -------------- ? -------- ? ------------ ?
? VARBINARY ? 1 ? 12500000 ?
? VARBINARY ? 2 ? 12800000 ?
? VARBINARY ? AVERAGE ? * 12650000 * ?
????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)
只看平均值,我们可以计算切换到 的好处VARBINARY:
SELECT (12650000 - 10250000) AS [IncreaseAmount],
ROUND(((126500000 - 10250000) / 10250000) * 100.0, 3) AS [IncreasePercentage]
Run Code Online (Sandbox Code Playgroud)
那返回:
IncreaseAmount: 2400000.0
IncreasePercentage: 23.415
Run Code Online (Sandbox Code Playgroud)
下表包含 1 分钟内执行的哈希数。例如,使用CHECKSUM84 个并发查询导致在时间用完之前执行了超过 20 亿个散列。
?????????????????????????????????????????????????????????????
? Function ? 12 threads ? 48 threads ? 84 threads ?
?????????????????????????????????????????????????????????????
? CHECKSUM ? 281250000 ? 1122440000 ? 2040100000 ?
? HASHBYTES MD5 ? 75940000 ? 106190000 ? 112750000 ?
? HASHBYTES SHA2_256 ? 80210000 ? 117080000 ? 124790000 ?
? CLR Spooky ? 131250000 ? 505700000 ? 786150000 ?
? CLR SpookyLOB ? 17420000 ? 27160000 ? 31380000 ?
? SQL# MD5 ? 17080000 ? 26450000 ? 29080000 ?
? SQL# SHA2_256 ? 18370000 ? 28860000 ? 32590000 ?
? SQL# MD5 8k ? 24440000 ? 30560000 ? 32550000 ?
? SQL# SHA2_256 8k ? 87240000 ? 159310000 ? 155760000 ?
?????????????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)
如果您希望看到以每线程每秒的工作量来衡量的相同数字:
?????????????????????????????????????????????????????????????????????????????????????????????????????????????
? Function ? 12 threads per core-second ? 48 threads per core-second ? 84 threads per core-second ?
?????????????????????????????????????????????????????????????????????????????????????????????????????????????
? CHECKSUM ? 390625 ? 389736 ? 404782 ?
? HASHBYTES MD5 ? 105472 ? 36872 ? 22371 ?
? HASHBYTES SHA2_256 ? 111403 ? 40653 ? 24760 ?
? CLR Spooky ? 182292 ? 175590 ? 155982 ?
? CLR SpookyLOB ? 24194 ? 9431 ? 6226 ?
? SQL# MD5 ? 23722 ? 9184 ? 5770 ?
? SQL# SHA2_256 ? 25514 ? 10021 ? 6466 ?
? SQL# MD5 8k ? 33944 ? 10611 ? 6458 ?
? SQL# SHA2_256 8k ? 121167 ? 55316 ? 30905 ?
?????????????????????????????????????????????????????????????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)
关于所有方法的一些快速想法:
CHECKSUM:正如预期的那样非常好的可扩展性HASHBYTES:可扩展性问题包括每次调用分配一个内存和在操作系统中花费大量 CPUSpooky: 令人惊讶的良好可扩展性Spooky LOB: 自旋锁SOS_SELIST_SIZED_SLOCK旋转失控。我怀疑这是通过 CLR 函数传递 LOB 的普遍问题,但我不确定Util_HashBinary: 看起来它被同一个自旋锁击中了。到目前为止,我还没有研究过这个问题,因为我可能对此无能为力:Util_HashBinary 8k: 非常令人惊讶的结果,不知道这里发生了什么在较小的服务器上测试的最终结果:
?????????????????????????????????????????????????????????????????????????????
? Hash Algorithm ? Hashes over 11 threads ? Hashes over 44 threads ?
?????????????????????????????????????????????????????????????????????????????
? HASHBYTES SHA2_256 ? 85220000 ? 167050000 ?
? SpookyHash ? 101200000 ? 239530000 ?
? Util_HashSHA256Binary8k ? 90590000 ? 217170000 ?
? SpookyHashLOB ? 23490000 ? 38370000 ?
? Util_HashSHA256Binary ? 23430000 ? 36590000 ?
?????????????????????????????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)
设置 1:表格和数据
DROP TABLE IF EXISTS dbo.HASH_SMALL;
CREATE TABLE dbo.HASH_SMALL (
ID BIGINT NOT NULL,
FK1 BIGINT NOT NULL,
FK2 BIGINT NOT NULL,
FK3 BIGINT NOT NULL,
FK4 BIGINT NOT NULL,
FK5 BIGINT NOT NULL,
FK6 BIGINT NOT NULL,
FK7 BIGINT NOT NULL,
FK8 BIGINT NOT NULL,
FK9 BIGINT NOT NULL,
FK10 BIGINT NOT NULL,
FK11 BIGINT NOT NULL,
FK12 BIGINT NOT NULL,
FK13 BIGINT NOT NULL,
FK14 BIGINT NOT NULL,
FK15 BIGINT NOT NULL,
FK16 BIGINT NOT NULL,
FK17 BIGINT NOT NULL,
FK18 BIGINT NOT NULL,
FK19 BIGINT NOT NULL,
FK20 BIGINT NOT NULL
);
INSERT INTO dbo.HASH_SMALL WITH (TABLOCK)
SELECT RN,
4000000 - RN, 4000000 - RN
,200000000 - RN, 200000000 - RN
, RN % 500000 , RN % 500000 , RN % 500000
, RN % 500000 , RN % 500000 , RN % 500000
, 100000 - RN % 100000, RN % 100000
, 100000 - RN % 100000, RN % 100000
, 100000 - RN % 100000, RN % 100000
, 100000 - RN % 100000, RN % 100000
, 100000 - RN % 100000, RN % 100000
FROM (
SELECT TOP (10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
) q
OPTION (MAXDOP 1);
DROP TABLE IF EXISTS dbo.LOG_HASHES;
CREATE TABLE dbo.LOG_HASHES (
LOG_TIME DATETIME,
HASH_ALGORITHM INT,
SESSION_ID INT,
NUM_HASHES BIGINT
);
Run Code Online (Sandbox Code Playgroud)
设置 2:主执行过程
GO
CREATE OR ALTER PROCEDURE dbo.RUN_HASHES_FOR_ONE_MINUTE (@HashAlgorithm INT)
AS
BEGIN
DECLARE @target_end_time DATETIME = DATEADD(MINUTE, 1, GETDATE()),
@query_execution_count INT = 0;
SET NOCOUNT ON;
DECLARE @ProcName NVARCHAR(261); -- schema_name + proc_name + '[].[]'
DECLARE @RowCount INT;
SELECT @RowCount = SUM(prtn.[row_count])
FROM sys.dm_db_partition_stats prtn
WHERE prtn.[object_id] = OBJECT_ID(N'dbo.HASH_SMALL')
AND prtn.[index_id] < 2;
-- Load assembly if not loaded to prevent load time from skewing results
DECLARE @OptionalInitSQL NVARCHAR(MAX);
SET @OptionalInitSQL = CASE @HashAlgorithm
WHEN 1 THEN N'SELECT @Dummy = dbo.SpookyHash(0x1234);'
WHEN 2 THEN N'' -- HASHBYTES
WHEN 3 THEN N'' -- HASHBYTES
WHEN 4 THEN N'' -- CHECKSUM
WHEN 5 THEN N'SELECT @Dummy = dbo.SpookyHashLOB(0x1234);'
WHEN 6 THEN N'SELECT @Dummy = SQL#.Util_HashBinary(N''MD5'', 0x1234);'
WHEN 7 THEN N'SELECT @Dummy = SQL#.Util_HashBinary(N''SHA256'', 0x1234);'
WHEN 8 THEN N'SELECT @Dummy = SQL#.Util_HashBinary8k(N''MD5'', 0x1234);'
WHEN 9 THEN N'SELECT @Dummy = SQL#.Util_HashBinary8k(N''SHA256'', 0x1234);'
/* -- BETA / non-public code
WHEN 10 THEN N'SELECT @Dummy = SQL#.Util_HashSHA256Binary8k(0x1234);'
WHEN 11 THEN N'SELECT @Dummy = SQL#.Util_HashSHA256Binary(0x1234);'
*/
END;
IF (RTRIM(@OptionalInitSQL) <> N'')
BEGIN
SET @OptionalInitSQL = N'
SET NOCOUNT ON;
DECLARE @Dummy VARBINARY(100);
' + @OptionalInitSQL;
RAISERROR(N'** Executing optional initialization code:', 10, 1) WITH NOWAIT;
RAISERROR(@OptionalInitSQL, 10, 1) WITH NOWAIT;
EXEC (@OptionalInitSQL);
RAISERROR(N'-------------------------------------------', 10, 1) WITH NOWAIT;
END;
SET @ProcName = CASE @HashAlgorithm
WHEN 1 THEN N'dbo.RUN_SpookyHash'
WHEN 2 THEN N'dbo.RUN_HASHBYTES_MD5'
WHEN 3 THEN N'dbo.RUN_HASHBYTES_SHA2_256'
WHEN 4 THEN N'dbo.RUN_CHECKSUM'
WHEN 5 THEN N'dbo.RUN_SpookyHashLOB'
WHEN 6 THEN N'dbo.RUN_SR_MD5'
WHEN 7 THEN N'dbo.RUN_SR_SHA256'
WHEN 8 THEN N'dbo.RUN_SR_MD5_8k'
WHEN 9 THEN N'dbo.RUN_SR_SHA256_8k'
/* -- BETA / non-public code
WHEN 10 THEN N'dbo.RUN_SR_SHA256_new'
WHEN 11 THEN N'dbo.RUN_SR_SHA256LOB_new'
*/
WHEN 13 THEN N'dbo.RUN_HASHBYTES_SHA2_256_NVC'
END;
RAISERROR(N'** Executing proc: %s', 10, 1, @ProcName) WITH NOWAIT;
WHILE GETDATE() < @target_end_time
BEGIN
EXEC @ProcName;
SET @query_execution_count = @query_execution_count + 1;
END;
INSERT INTO dbo.LOG_HASHES
VALUES (GETDATE(), @HashAlgorithm, @@SPID, @RowCount * @query_execution_count);
END;
GO
Run Code Online (Sandbox Code Playgroud)
设置 3:碰撞检测程序
GO
CREATE OR ALTER PROCEDURE dbo.VERIFY_NO_COLLISIONS (@HashAlgorithm INT)
AS
SET NOCOUNT ON;
DECLARE @RowCount INT;
SELECT @RowCount = SUM(prtn.[row_count])
FROM sys.dm_db_partition_stats prtn
WHERE prtn.[object_id] = OBJECT_ID(N'dbo.HASH_SMALL')
AND prtn.[index_id] < 2;
DECLARE @CollisionTestRows INT;
DECLARE @CollisionTestSQL NVARCHAR(MAX);
SET @CollisionTestSQL = N'
SELECT @RowsOut = COUNT(DISTINCT '
+ CASE @HashAlgorithm
WHEN 1 THEN N'dbo.SpookyHash('
WHEN 2 THEN N'HASHBYTES(''MD5'','
WHEN 3 THEN N'HASHBYTES(''SHA2_256'','
WHEN 4 THEN N'CHECKSUM('
WHEN 5 THEN N'dbo.SpookyHashLOB('
WHEN 6 THEN N'SQL#.Util_HashBinary(N''MD5'','
WHEN 7 THEN N'SQL#.Util_HashBinary(N''SHA256'','
WHEN 8 THEN N'SQL#.[Util_HashBinary8k](N''MD5'','
WHEN 9 THEN N'SQL#.[Util_HashBinary8k](N''SHA256'','
--/* -- BETA / non-public code
WHEN 10 THEN N'SQL#.[Util_HashSHA256Binary8k]('
WHEN 11 THEN N'SQL#.[Util_HashSHA256Binary]('
--*/
END
+ N'
CAST(FK1 AS BINARY(8)) + 0x7C +
CAST(FK2 AS BINARY(8)) + 0x7C +
CAST(FK3 AS BINARY(8)) + 0x7C +
CAST(FK4 AS BINARY(8)) + 0x7C +
CAST(FK5 AS BINARY(8)) + 0x7C +
CAST(FK6 AS BINARY(8)) + 0x7C +
CAST(FK7 AS BINARY(8)) + 0x7C +
CAST(FK8 AS BINARY(8)) + 0x7C +
CAST(FK9 AS BINARY(8)) + 0x7C +
CAST(FK10 AS BINARY(8)) + 0x7C +
CAST(FK11 AS BINARY(8)) + 0x7C +
CAST(FK12 AS BINARY(8)) + 0x7C +
CAST(FK13 AS BINARY(8)) + 0x7C +
CAST(FK14 AS BINARY(8)) + 0x7C +
CAST(FK15 AS BINARY(8)) + 0x7C +
CAST(FK16 AS BINARY(8)) + 0x7C +
CAST(FK17 AS BINARY(8)) + 0x7C +
CAST(FK18 AS BINARY(8)) + 0x7C +
CAST(FK19 AS BINARY(8)) + 0x7C +
CAST(FK20 AS BINARY(8)) ))
FROM dbo.HASH_SMALL;';
PRINT @CollisionTestSQL;
EXEC sp_executesql
@CollisionTestSQL,
N'@RowsOut INT OUTPUT',
@RowsOut = @CollisionTestRows OUTPUT;
IF (@CollisionTestRows <> @RowCount)
BEGIN
RAISERROR('Collisions for algorithm: %d!!! %d unique rows out of %d.',
16, 1, @HashAlgorithm, @CollisionTestRows, @RowCount);
END;
GO
Run Code Online (Sandbox Code Playgroud)
设置 4:清理(删除所有测试过程)
DECLARE @SQL NVARCHAR(MAX) = N'';
SELECT @SQL += N'DROP PROCEDURE [dbo].' + QUOTENAME(sp.[name])
+ N';' + NCHAR(13) + NCHAR(10)
FROM sys.objects sp
WHERE sp.[name] LIKE N'RUN[_]%'
AND sp.[type_desc] = N'SQL_STORED_PROCEDURE'
AND sp.[name] <> N'RUN_HASHES_FOR_ONE_MINUTE'
PRINT @SQL;
EXEC (@SQL);
Run Code Online (Sandbox Code Playgroud)
设置 5:生成测试过程
SET NOCOUNT ON;
DECLARE @TestProcsToCreate TABLE
(
ProcName sysname NOT NULL,
CodeToExec NVARCHAR(261) NOT NULL
);
DECLARE @ProcName sysname,
@CodeToExec NVARCHAR(261);
INSERT INTO @TestProcsToCreate VALUES
(N'SpookyHash', N'dbo.SpookyHash('),
(N'HASHBYTES_MD5', N'HASHBYTES(''MD5'','),
(N'HASHBYTES_SHA2_256', N'HASHBYTES(''SHA2_256'','),
(N'CHECKSUM', N'CHECKSUM('),
(N'SpookyHashLOB', N'dbo.SpookyHashLOB('),
(N'SR_MD5', N'SQL#.Util_HashBinary(N''MD5'','),
(N'SR_SHA256', N'SQL#.Util_HashBinary(N''SHA256'','),
(N'SR_MD5_8k', N'SQL#.[Util_HashBinary8k](N''MD5'','),
(N'SR_SHA256_8k', N'SQL#.[Util_HashBinary8k](N''SHA256'',')
--/* -- BETA / non-public code
, (N'SR_SHA256_new', N'SQL#.[Util_HashSHA256Binary8k]('),
(N'SR_SHA256LOB_new', N'SQL#.[Util_HashSHA256Binary](');
--*/
DECLARE @ProcTemplate NVARCHAR(MAX),
@ProcToCreate NVARCHAR(MAX);
SET @ProcTemplate = N'
CREATE OR ALTER PROCEDURE dbo.RUN_{{ProcName}}
AS
BEGIN
DECLARE @dummy INT;
SET NOCOUNT ON;
SELECT @dummy = COUNT({{CodeToExec}}
CAST(FK1 AS BINARY(8)) + 0x7C +
CAST(FK2 AS BINARY(8)) + 0x7C +
CAST(FK3 AS BINARY(8)) + 0x7C +
CAST(FK4 AS BINARY(8)) + 0x7C +
CAST(FK5 AS BINARY(8)) + 0x7C +
CAST(FK6 AS BINARY(8)) + 0x7C +
CAST(FK7 AS BINARY(8)) + 0x7C +
CAST(FK8 AS BINARY(8)) + 0x7C +
CAST(FK9 AS BINARY(8)) + 0x7C +
CAST(FK10 AS BINARY(8)) + 0x7C +
CAST(FK11 AS BINARY(8)) + 0x7C +
CAST(FK12 AS BINARY(8)) + 0x7C +
CAST(FK13 AS BINARY(8)) + 0x7C +
CAST(FK14 AS BINARY(8)) + 0x7C +
CAST(FK15 AS BINARY(8)) + 0x7C +
CAST(FK16 AS BINARY(8)) + 0x7C +
CAST(FK17 AS BINARY(8)) + 0x7C +
CAST(FK18 AS BINARY(8)) + 0x7C +
CAST(FK19 AS BINARY(8)) + 0x7C +
CAST(FK20 AS BINARY(8))
)
)
FROM dbo.HASH_SMALL
OPTION (MAXDOP 1);
END;
';
DECLARE CreateProcsCurs CURSOR READ_ONLY FORWARD_ONLY LOCAL FAST_FORWARD
FOR SELECT [ProcName], [CodeToExec]
FROM @TestProcsToCreate;
OPEN [CreateProcsCurs];
FETCH NEXT
FROM [CreateProcsCurs]
INTO @ProcName, @CodeToExec;
WHILE (@@FETCH_STATUS = 0)
BEGIN
-- First: create VARBINARY version
SET @ProcToCreate = REPLACE(REPLACE(@ProcTemplate,
N'{{ProcName}}',
@ProcName),
N'{{CodeToExec}}',
@CodeToExec);
EXEC (@ProcToCreate);
-- Second: create NVARCHAR version (optional: built-ins only)
IF (CHARINDEX(N'.', @CodeToExec) = 0)
BEGIN
SET @ProcToCreate = REPLACE(REPLACE(REPLACE(@ProcToCreate,
N'dbo.RUN_' + @ProcName,
N'dbo.RUN_' + @ProcName + N'_NVC'),
N'BINARY(8)',
N'NVARCHAR(15)'),
N'0x7C',
N'N''|''');
EXEC (@ProcToCreate);
END;
FETCH NEXT
FROM [CreateProcsCurs]
INTO @ProcName, @CodeToExec;
END;
CLOSE [CreateProcsCurs];
DEALLOCATE [CreateProcsCurs];
Run Code Online (Sandbox Code Playgroud)
测试 1:检查碰撞
EXEC dbo.VERIFY_NO_COLLISIONS 1;
EXEC dbo.VERIFY_NO_COLLISIONS 2;
EXEC dbo.VERIFY_NO_COLLISIONS 3;
EXEC dbo.VERIFY_NO_COLLISIONS 4;
EXEC dbo.VERIFY_NO_COLLISIONS 5;
EXEC dbo.VERIFY_NO_COLLISIONS 6;
EXEC dbo.VERIFY_NO_COLLISIONS 7;
EXEC dbo.VERIFY_NO_COLLISIONS 8;
EXEC dbo.VERIFY_NO_COLLISIONS 9;
EXEC dbo.VERIFY_NO_COLLISIONS 10;
EXEC dbo.VERIFY_NO_COLLISIONS 11;
Run Code Online (Sandbox Code Playgroud)
测试 2:运行性能测试
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 1;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 2;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 3; -- HASHBYTES('SHA2_256'
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 4;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 5;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 6;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 7;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 8;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 9;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 10;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 11;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 13; -- NVC version of #3
SELECT *
FROM dbo.LOG_HASHES
ORDER BY [LOG_TIME] DESC;
Run Code Online (Sandbox Code Playgroud)
虽然专注于单一 SQLCLR UDF 的性能测试,但早期讨论的两个问题并未纳入测试,但理想情况下应该进行调查以确定哪种方法满足所有要求。
在后来被删除的评论中,保罗·怀特提到:
HASHBYTES用 CLR 标量函数替换的一个缺点- CLR 函数似乎不能使用批处理模式,而HASHBYTES可以。在性能方面,这可能很重要。
所以这是需要考虑的事情,显然需要测试。如果 SQLCLR 选项没有提供比内置的任何好处HASHBYTES,那么这增加了Solomon 将现有哈希(至少对于最大的表)捕获到相关表中的建议的权重。
您可能可以通过池化和缓存函数调用中创建的任何对象来提高性能,并且可能提高所有 .NET 方法的可伸缩性。EG 上面 Paul White 的代码:
static readonly ConcurrentDictionary<int,ISpookyHashV2> hashers = new ConcurrentDictonary<ISpookyHashV2>()
public static byte[] SpookyHash([SqlFacet (MaxSize = 8000)] SqlBinary Input)
{
ISpookyHashV2 sh = hashers.GetOrAdd(Thread.CurrentThread.ManagedThreadId, i => SpookyHashV2Factory.Instance.Create());
return sh.ComputeHash(Input.Value).Hash;
}
Run Code Online (Sandbox Code Playgroud)
SQL CLR 不鼓励并试图阻止使用静态/共享变量,但如果将共享变量标记为只读,它将允许您使用共享变量。当然,这毫无意义,因为您可以只分配某个可变类型的单个实例,例如ConcurrentDictionary.