使用 SQL 服务器唯一标识符在 Java 中连续生成 GUID
Sequential GUID generation in Java with SQL Server uniqueidentifier
我要解决的问题是:
1 - 在我们的数据库中,我们拥有所有表(还有包含数百万条记录的表),其 PK id 列声明为 VARCHAR(36)。 它上面还有一个聚集索引当然,正如我在网上看到的那样,这对性能来说是一件很糟糕的事情,也因为数据库有很多读取、插入、更新和删除。
2 - 我们将 Hibernate 用于我们的 java Web 应用程序作为此数据库的 ORM
在网上大量阅读后,我开始将这些列的数据类型更改为 UNIQUEIDENTIFIER,并使用默认选项 newsequentialid() 作为此选项应该可以减轻我们索引的碎片化问题。
我注意到碎片问题仍然存在,重建后表变得严重碎片化(我们每晚都重建完整索引)。
然后我看到我们所有的 id 列的 Hibernate 映射都包含以下内容:
<id name="id" column="id" type="string">
<generator class="guid"/>
</id>
当我们的系统中发生插入时,日志显示插入是在调用 select newid()
之后完成的,因此由于此 returns 随机 guid,插入将被放置在索引,从而导致碎片(这完全破坏了我所做的列数据类型更改)。
所以在另一次在线搜索之后,我尝试自己在 Hibernate 中实现一个 guid 生成器,实现接口 IdentifierGenerator
并使用基于时间的生成器和 JUG (http://wiki.fasterxml.com/JugHome)。
生成(我认为是顺序的)id 的代码是这样的:
String uuid = null;
EthernetAddress nic = EthernetAddress.fromInterface();
TimeBasedGenerator uuidGenerator = Generators.timeBasedGenerator(nic);
uuid = uuidGenerator.generate().toString();
我相应地将映射更改为:
<id name="id" column="id" type="string">
<generator class="my_package.hibernate.CustomSequentialGuidGenerator">
</generator>
</id>
然后我尝试生成一些测试uuid来测试它们的顺序(以uniqueidentifier方式顺序,所以二进制),这是一个短列表(每个元素都在连续之前生成):
314a9a1b-6295-11e5-8d2c-2c27d7e1614f
3d867801-6295-11e5-ae09-2c27d7e1614f
4434ac7d-6295-11e5-9ed1-2c27d7e1614f
491462c4-6295-11e5-af81-2c27d7e1614f
5389ff4c-6295-11e5-84cf-2c27d7e1614f
57098959-6295-11e5-b203-2c27d7e1614f
5b62d144-6295-11e5-9883-2c27d7e1614f
这在我看来是按字母顺序排列的,而不是二进制顺序的。
以上测试执行了七次测试应用程序,不是循环。
我试图将这些值插入声明为唯一标识符的列中,并在此列上发出 select 之后,这是列表 sql 服务器输出:
5389FF4C-6295-11E5-84CF-2C27D7E1614F
314A9A1B-6295-11E5-8D2C-2C27D7E1614F
5B62D144-6295-11E5-9883-2C27D7E1614F
4434AC7D-6295-11E5-9ED1-2C27D7E1614F
3D867801-6295-11E5-AE09-2C27D7E1614F
491462C4-6295-11E5-AF81-2C27D7E1614F
57098959-6295-11E5-B203-2C27D7E1614F
所以我真的不明白我应该做什么以及我是否可以使用 JUG 作为顺序 guid 生成器来避免我的碎片问题。
这是另一个 JUG 测试,我尝试了 3 次运行,每次生成 10 个带循环的 guid:
运行 1
54bd156e-62a2-11e5-a1a7-2c27d7e1614f
54c3cc2f-62a2-11e5-a1a7-2c27d7e1614f
54caf820-62a2-11e5-a1a7-2c27d7e1614f
54d1aee1-62a2-11e5-a1a7-2c27d7e1614f
54d901e2-62a2-11e5-a1a7-2c27d7e1614f
54df9193-62a2-11e5-a1a7-2c27d7e1614f
54e64854-62a2-11e5-a1a7-2c27d7e1614f
54ecff15-62a2-11e5-a1a7-2c27d7e1614f
54f3b5d6-62a2-11e5-a1a7-2c27d7e1614f
54fa4587-62a2-11e5-a1a7-2c27d7e1614f
运行 2
87c66bcc-62a2-11e5-8e7c-2c27d7e1614f
87ccd46d-62a2-11e5-8e7c-2c27d7e1614f
87d3641e-62a2-11e5-8e7c-2c27d7e1614f
87d97e9f-62a2-11e5-8e7c-2c27d7e1614f
87e05c70-62a2-11e5-8e7c-2c27d7e1614f
87e6ec21-62a2-11e5-8e7c-2c27d7e1614f
87ed7bd2-62a2-11e5-8e7c-2c27d7e1614f
87f40b83-62a2-11e5-8e7c-2c27d7e1614f
87fac244-62a2-11e5-8e7c-2c27d7e1614f
880103d5-62a2-11e5-8e7c-2c27d7e1614f
运行 3
a4b690db-62a2-11e5-b667-2c27d7e1614f
a4bcd26c-62a2-11e5-b667-2c27d7e1614f
a4c2eced-62a2-11e5-b667-2c27d7e1614f
a4c92e7e-62a2-11e5-b667-2c27d7e1614f
a4cf48ff-62a2-11e5-b667-2c27d7e1614f
a4d5d8b0-62a2-11e5-b667-2c27d7e1614f
a4dc6861-62a2-11e5-b667-2c27d7e1614f
a4e34632-62a2-11e5-b667-2c27d7e1614f
a4e9d5e3-62a2-11e5-b667-2c27d7e1614f
a4f101d4-62a2-11e5-b667-2c27d7e1614f
运行 4
c2b872b2-62a2-11e5-b855-2c27d7e1614f
c2c17363-62a2-11e5-b855-2c27d7e1614f
c2c82a24-62a2-11e5-b855-2c27d7e1614f
c2ce92c5-62a2-11e5-b855-2c27d7e1614f
c2d57096-62a2-11e5-b855-2c27d7e1614f
c2dc2757-62a2-11e5-b855-2c27d7e1614f
c2e32c38-62a2-11e5-b855-2c27d7e1614f
c2e9bbe9-62a2-11e5-b855-2c27d7e1614f
c2f099ba-62a2-11e5-b855-2c27d7e1614f
c2f7507b-62a2-11e5-b855-2c27d7e1614f
运行 5
f0263d1b-62a2-11e5-8529-2c27d7e1614f
f02d1aec-62a2-11e5-8529-2c27d7e1614f
f033d1ad-62a2-11e5-8529-2c27d7e1614f
f03a615e-62a2-11e5-8529-2c27d7e1614f
f041181f-62a2-11e5-8529-2c27d7e1614f
f047a7d0-62a2-11e5-8529-2c27d7e1614f
f04dc251-62a2-11e5-8529-2c27d7e1614f
f05403e2-62a2-11e5-8529-2c27d7e1614f
f05a6c83-62a2-11e5-8529-2c27d7e1614f
f0608704-62a2-11e5-8529-2c27d7e1614f
运行 6(又从0开始)
00fd4ec3-62a3-11e5-8ab8-2c27d7e1614f
01042c94-62a3-11e5-8ab8-2c27d7e1614f
010b3175-62a3-11e5-8ab8-2c27d7e1614f
0111e836-62a3-11e5-8ab8-2c27d7e1614f
0118ed17-62a3-11e5-8ab8-2c27d7e1614f
011fcae8-62a3-11e5-8ab8-2c27d7e1614f
0126a8b9-62a3-11e5-8ab8-2c27d7e1614f
012d115a-62a3-11e5-8ab8-2c27d7e1614f
0133c81b-62a3-11e5-8ab8-2c27d7e1614f
013a30bc-62a3-11e5-8ab8-2c27d7e1614f
单个组按字母顺序(但不是二进制)排序并将不同的运行作为一个整体,它们不是按事件字母顺序排序的(叹息)。
我错过了什么?
*************************** 编辑 - 我的实施说明 **** **************
经过各种评论和回答后,我实施了以下策略:
我生成了自己的顺序(基于当前时间戳)guid,这是生成器 class:
package it.hibernate;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.commons.lang.RandomStringUtils;
import org.hibernate.HibernateException;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.id.IdentifierGenerator;
public class CustomSequentialGuidGenerator implements IdentifierGenerator{
@Override
public Serializable generate(SessionImplementor session, Object object)
throws HibernateException
{
String uuid = null;
try {
Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat();
String rand = RandomStringUtils.randomAlphanumeric(12);
sdf.applyPattern("yyyy");
String year = sdf.format(data);
sdf.applyPattern("MM");
String month = sdf.format(data);
sdf.applyPattern("dd");
String day = sdf.format(data);
sdf.applyPattern("HH");
String hour = sdf.format(data);
sdf.applyPattern("mm");
String mins = sdf.format(data);
sdf.applyPattern("ss");
String secs = sdf.format(data);
sdf.applyPattern("SSS");
String millis = sdf.format(data);
//G carachter is used to insert the rows after
uuid = "GG" + year + month + "-" + day + hour + "-" + mins + secs + "-" + "0" + millis + "-" + rand;
}
catch (Exception exception)
{
exception.printStackTrace();
}
return uuid;
}
}
您可以注意到所有行都以字符串 'GG'
开头,因为我必须确保在通过 select newid()
生成的所有旧行之后插入所有新行。之后是当前时间戳和 12 个随机字符,以避免在同一毫秒内插入多行时发生冲突。
经过 2000 次测试后,主键索引碎片从 17,92% 下降到 0,15%。
N.B。我重新引入的数据类型显然又是 varchar(36) 而不是 uniqueidentifier,因此行按字母顺序排序。
newsequentialid()
的默认选项当然不起作用,因为hibernate不使用默认值,它总是设置一个由它的生成器发出的值。
通过快速查看 JUG 库,它似乎没有提供任何顺序生成 GUID 的方法。我不知道您为什么认为通过 Generators.timeBasedGenerator()
获得的生成器的 generate()
方法会给您连续的 GUID。基于时间的生成器只是一个在生成 GUID 时将当前时间考虑 的生成器,但是在将其嵌入到GUID,因此它不保证生成的 GUID 会有任何顺序。
通常,术语 "GUID" 和 "sequential" 彼此不兼容。您可以拥有 GUID 键或顺序键,但在正常情况下,您不能同时拥有两者。
那么,您确定密钥必须是 GUID 吗?就个人而言,我发现 GUID 很难使用。
但是如果您必须 进行任何必要的修改以获得连续的 GUID,那么我的建议是编写您自己的函数生成 36 个字符的字符串,看起来像 GUID,但是是连续的。
顺序部分应该来自一个SEQUENCE
,它只是发出顺序整数。 (我相信 MS-SQL-Server 支持它们。)
您可以阅读有关如何正确构建 GUID 的 IETF's UUID specification,但您不必一字不差地遵循它。在大多数情况下,如果它只是 看起来像 一个 GUID,就足够了。
如果你能有一个单一的全球序列,那很好。如果您不能拥有一个单一的全局序列,那么您需要以某种方式识别您的序列,然后在生成您的 GUID 时考虑每个序列的标识符。 (这就是 IETF 文档中提到的 "node id"。)
我曾经有一个不合理的要求,即我要传输到某个 Web 服务的行必须由 GUID 标识,并且有太多的繁文缛节阻止我联系他们询问他们 "are you friggin' serious?" 所以我只是传输了如下的 GUID:
|--- random part -----| |-- key ---|
314a9a1b-6295-11e5-8d2c-000000000001
314a9a1b-6295-11e5-8d2c-000000000002
314a9a1b-6295-11e5-8d2c-000000000003
314a9a1b-6295-11e5-8d2c-000000000004
314a9a1b-6295-11e5-8d2c-000000000005
...
他们一句话也没说。
我要解决的问题是:
1 - 在我们的数据库中,我们拥有所有表(还有包含数百万条记录的表),其 PK id 列声明为 VARCHAR(36)。 它上面还有一个聚集索引当然,正如我在网上看到的那样,这对性能来说是一件很糟糕的事情,也因为数据库有很多读取、插入、更新和删除。
2 - 我们将 Hibernate 用于我们的 java Web 应用程序作为此数据库的 ORM
在网上大量阅读后,我开始将这些列的数据类型更改为 UNIQUEIDENTIFIER,并使用默认选项 newsequentialid() 作为此选项应该可以减轻我们索引的碎片化问题。
我注意到碎片问题仍然存在,重建后表变得严重碎片化(我们每晚都重建完整索引)。
然后我看到我们所有的 id 列的 Hibernate 映射都包含以下内容:
<id name="id" column="id" type="string">
<generator class="guid"/>
</id>
当我们的系统中发生插入时,日志显示插入是在调用 select newid()
之后完成的,因此由于此 returns 随机 guid,插入将被放置在索引,从而导致碎片(这完全破坏了我所做的列数据类型更改)。
所以在另一次在线搜索之后,我尝试自己在 Hibernate 中实现一个 guid 生成器,实现接口 IdentifierGenerator
并使用基于时间的生成器和 JUG (http://wiki.fasterxml.com/JugHome)。
生成(我认为是顺序的)id 的代码是这样的:
String uuid = null;
EthernetAddress nic = EthernetAddress.fromInterface();
TimeBasedGenerator uuidGenerator = Generators.timeBasedGenerator(nic);
uuid = uuidGenerator.generate().toString();
我相应地将映射更改为:
<id name="id" column="id" type="string">
<generator class="my_package.hibernate.CustomSequentialGuidGenerator">
</generator>
</id>
然后我尝试生成一些测试uuid来测试它们的顺序(以uniqueidentifier方式顺序,所以二进制),这是一个短列表(每个元素都在连续之前生成):
314a9a1b-6295-11e5-8d2c-2c27d7e1614f
3d867801-6295-11e5-ae09-2c27d7e1614f
4434ac7d-6295-11e5-9ed1-2c27d7e1614f
491462c4-6295-11e5-af81-2c27d7e1614f
5389ff4c-6295-11e5-84cf-2c27d7e1614f
57098959-6295-11e5-b203-2c27d7e1614f
5b62d144-6295-11e5-9883-2c27d7e1614f
这在我看来是按字母顺序排列的,而不是二进制顺序的。
以上测试执行了七次测试应用程序,不是循环。
我试图将这些值插入声明为唯一标识符的列中,并在此列上发出 select 之后,这是列表 sql 服务器输出:
5389FF4C-6295-11E5-84CF-2C27D7E1614F
314A9A1B-6295-11E5-8D2C-2C27D7E1614F
5B62D144-6295-11E5-9883-2C27D7E1614F
4434AC7D-6295-11E5-9ED1-2C27D7E1614F
3D867801-6295-11E5-AE09-2C27D7E1614F
491462C4-6295-11E5-AF81-2C27D7E1614F
57098959-6295-11E5-B203-2C27D7E1614F
所以我真的不明白我应该做什么以及我是否可以使用 JUG 作为顺序 guid 生成器来避免我的碎片问题。
这是另一个 JUG 测试,我尝试了 3 次运行,每次生成 10 个带循环的 guid:
运行 1
54bd156e-62a2-11e5-a1a7-2c27d7e1614f
54c3cc2f-62a2-11e5-a1a7-2c27d7e1614f
54caf820-62a2-11e5-a1a7-2c27d7e1614f
54d1aee1-62a2-11e5-a1a7-2c27d7e1614f
54d901e2-62a2-11e5-a1a7-2c27d7e1614f
54df9193-62a2-11e5-a1a7-2c27d7e1614f
54e64854-62a2-11e5-a1a7-2c27d7e1614f
54ecff15-62a2-11e5-a1a7-2c27d7e1614f
54f3b5d6-62a2-11e5-a1a7-2c27d7e1614f
54fa4587-62a2-11e5-a1a7-2c27d7e1614f
运行 2
87c66bcc-62a2-11e5-8e7c-2c27d7e1614f
87ccd46d-62a2-11e5-8e7c-2c27d7e1614f
87d3641e-62a2-11e5-8e7c-2c27d7e1614f
87d97e9f-62a2-11e5-8e7c-2c27d7e1614f
87e05c70-62a2-11e5-8e7c-2c27d7e1614f
87e6ec21-62a2-11e5-8e7c-2c27d7e1614f
87ed7bd2-62a2-11e5-8e7c-2c27d7e1614f
87f40b83-62a2-11e5-8e7c-2c27d7e1614f
87fac244-62a2-11e5-8e7c-2c27d7e1614f
880103d5-62a2-11e5-8e7c-2c27d7e1614f
运行 3
a4b690db-62a2-11e5-b667-2c27d7e1614f
a4bcd26c-62a2-11e5-b667-2c27d7e1614f
a4c2eced-62a2-11e5-b667-2c27d7e1614f
a4c92e7e-62a2-11e5-b667-2c27d7e1614f
a4cf48ff-62a2-11e5-b667-2c27d7e1614f
a4d5d8b0-62a2-11e5-b667-2c27d7e1614f
a4dc6861-62a2-11e5-b667-2c27d7e1614f
a4e34632-62a2-11e5-b667-2c27d7e1614f
a4e9d5e3-62a2-11e5-b667-2c27d7e1614f
a4f101d4-62a2-11e5-b667-2c27d7e1614f
运行 4
c2b872b2-62a2-11e5-b855-2c27d7e1614f
c2c17363-62a2-11e5-b855-2c27d7e1614f
c2c82a24-62a2-11e5-b855-2c27d7e1614f
c2ce92c5-62a2-11e5-b855-2c27d7e1614f
c2d57096-62a2-11e5-b855-2c27d7e1614f
c2dc2757-62a2-11e5-b855-2c27d7e1614f
c2e32c38-62a2-11e5-b855-2c27d7e1614f
c2e9bbe9-62a2-11e5-b855-2c27d7e1614f
c2f099ba-62a2-11e5-b855-2c27d7e1614f
c2f7507b-62a2-11e5-b855-2c27d7e1614f
运行 5
f0263d1b-62a2-11e5-8529-2c27d7e1614f
f02d1aec-62a2-11e5-8529-2c27d7e1614f
f033d1ad-62a2-11e5-8529-2c27d7e1614f
f03a615e-62a2-11e5-8529-2c27d7e1614f
f041181f-62a2-11e5-8529-2c27d7e1614f
f047a7d0-62a2-11e5-8529-2c27d7e1614f
f04dc251-62a2-11e5-8529-2c27d7e1614f
f05403e2-62a2-11e5-8529-2c27d7e1614f
f05a6c83-62a2-11e5-8529-2c27d7e1614f
f0608704-62a2-11e5-8529-2c27d7e1614f
运行 6(又从0开始)
00fd4ec3-62a3-11e5-8ab8-2c27d7e1614f
01042c94-62a3-11e5-8ab8-2c27d7e1614f
010b3175-62a3-11e5-8ab8-2c27d7e1614f
0111e836-62a3-11e5-8ab8-2c27d7e1614f
0118ed17-62a3-11e5-8ab8-2c27d7e1614f
011fcae8-62a3-11e5-8ab8-2c27d7e1614f
0126a8b9-62a3-11e5-8ab8-2c27d7e1614f
012d115a-62a3-11e5-8ab8-2c27d7e1614f
0133c81b-62a3-11e5-8ab8-2c27d7e1614f
013a30bc-62a3-11e5-8ab8-2c27d7e1614f
单个组按字母顺序(但不是二进制)排序并将不同的运行作为一个整体,它们不是按事件字母顺序排序的(叹息)。
我错过了什么?
*************************** 编辑 - 我的实施说明 **** **************
经过各种评论和回答后,我实施了以下策略:
我生成了自己的顺序(基于当前时间戳)guid,这是生成器 class:
package it.hibernate;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.commons.lang.RandomStringUtils;
import org.hibernate.HibernateException;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.id.IdentifierGenerator;
public class CustomSequentialGuidGenerator implements IdentifierGenerator{
@Override
public Serializable generate(SessionImplementor session, Object object)
throws HibernateException
{
String uuid = null;
try {
Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat();
String rand = RandomStringUtils.randomAlphanumeric(12);
sdf.applyPattern("yyyy");
String year = sdf.format(data);
sdf.applyPattern("MM");
String month = sdf.format(data);
sdf.applyPattern("dd");
String day = sdf.format(data);
sdf.applyPattern("HH");
String hour = sdf.format(data);
sdf.applyPattern("mm");
String mins = sdf.format(data);
sdf.applyPattern("ss");
String secs = sdf.format(data);
sdf.applyPattern("SSS");
String millis = sdf.format(data);
//G carachter is used to insert the rows after
uuid = "GG" + year + month + "-" + day + hour + "-" + mins + secs + "-" + "0" + millis + "-" + rand;
}
catch (Exception exception)
{
exception.printStackTrace();
}
return uuid;
}
}
您可以注意到所有行都以字符串 'GG'
开头,因为我必须确保在通过 select newid()
生成的所有旧行之后插入所有新行。之后是当前时间戳和 12 个随机字符,以避免在同一毫秒内插入多行时发生冲突。
经过 2000 次测试后,主键索引碎片从 17,92% 下降到 0,15%。
N.B。我重新引入的数据类型显然又是 varchar(36) 而不是 uniqueidentifier,因此行按字母顺序排序。
newsequentialid()
的默认选项当然不起作用,因为hibernate不使用默认值,它总是设置一个由它的生成器发出的值。
通过快速查看 JUG 库,它似乎没有提供任何顺序生成 GUID 的方法。我不知道您为什么认为通过 Generators.timeBasedGenerator()
获得的生成器的 generate()
方法会给您连续的 GUID。基于时间的生成器只是一个在生成 GUID 时将当前时间考虑 的生成器,但是在将其嵌入到GUID,因此它不保证生成的 GUID 会有任何顺序。
通常,术语 "GUID" 和 "sequential" 彼此不兼容。您可以拥有 GUID 键或顺序键,但在正常情况下,您不能同时拥有两者。
那么,您确定密钥必须是 GUID 吗?就个人而言,我发现 GUID 很难使用。
但是如果您必须 进行任何必要的修改以获得连续的 GUID,那么我的建议是编写您自己的函数生成 36 个字符的字符串,看起来像 GUID,但是是连续的。
顺序部分应该来自一个SEQUENCE
,它只是发出顺序整数。 (我相信 MS-SQL-Server 支持它们。)
您可以阅读有关如何正确构建 GUID 的 IETF's UUID specification,但您不必一字不差地遵循它。在大多数情况下,如果它只是 看起来像 一个 GUID,就足够了。
如果你能有一个单一的全球序列,那很好。如果您不能拥有一个单一的全局序列,那么您需要以某种方式识别您的序列,然后在生成您的 GUID 时考虑每个序列的标识符。 (这就是 IETF 文档中提到的 "node id"。)
我曾经有一个不合理的要求,即我要传输到某个 Web 服务的行必须由 GUID 标识,并且有太多的繁文缛节阻止我联系他们询问他们 "are you friggin' serious?" 所以我只是传输了如下的 GUID:
|--- random part -----| |-- key ---|
314a9a1b-6295-11e5-8d2c-000000000001
314a9a1b-6295-11e5-8d2c-000000000002
314a9a1b-6295-11e5-8d2c-000000000003
314a9a1b-6295-11e5-8d2c-000000000004
314a9a1b-6295-11e5-8d2c-000000000005
...
他们一句话也没说。