在Java中使用SQL Server唯一标识符生成连续的GUID

5
我要解决的问题是:
1 - 在我们的数据库中,所有表(包括数百万条记录的表)都声明了一个PK id列,类型为VARCHAR(36)。它还有一个聚簇索引。根据我在网上读到的信息,这对性能来说是非常糟糕的,因为数据库有很多读取、插入、更新和删除操作。
2 - 我们在java web应用程序中使用Hibernate作为ORM连接到这个数据库。
经过大量在线阅读后,我开始将这些列的数据类型更改为UNIQUEIDENTIFIER,并使用默认选项newsequentialid(),因为这个选项应该减轻索引的碎片问题。
但我注意到,碎片问题仍然存在,表在重建后不久就变得严重碎片化了(我们每晚进行完整的索引重建)。
接着我发现,我们所有的id列的Hibernate映射都包含了这个内容:
<id name="id" column="id" type="string">
        <generator class="guid"/>
</id>

当我们的系统发生插入操作时,日志显示在调用 select newid() 后进行了插入,因此由于它返回一个随机 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来测试它们的顺序性(按照唯一标识符的方式,因此是二进制),这是一个简短的列表(每个元素都在后续之前生成):

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

这对我来说看起来是按字母顺序排列的,但不是二进制顺序。
上述测试执行了七次测试应用程序,而不是循环。
我尝试将这些值插入声明为唯一标识符的列中,然后在此列上发出选择后,这是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
单个分组按字母顺序排序(但不是二进制顺序),将不同的运行作为整体考虑时,它们甚至没有按字母顺序排序(叹气)。

我错过了什么吗?

************************* 编辑 - 我的实现说明 ******************

在各种评论和答案之后,我采用了以下策略:

我生成了自己的顺序(基于当前时间戳)guids,这是生成器类:

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%。
注:我重新引入的数据类型显然又是varchar(36),而不是uniqueidentifier,所以行按字母顺序排序。

1
很遗憾,似乎没有像Identity那样适用于GUID的Hibernate生成器。如果有的话,你可以在服务器端使用NEWSEQUENTIALID的默认约束条件,并且Hibernate将简单地读取它所提供的值...(顺便问一下,你需要使用GUID而不是简单的自增IDENTITY列有特殊原因吗?(复制?))或者你可以考虑添加一个IDENTITY列并按簇集索引GUID。 - Matt Gibson
谢谢Matt!您能否更好地解释一下这部分内容:“然后您可以在服务器端使用NEWSEQUENTIALID的默认约束,假设,并且Hibernate将简单地读回它提供的值”?我已经生成了我已更改为uniqueidentifier的列上已有的约束,这是SQL Server告诉我的:ALTER TABLE [dbo].[e1_tur_servizi] ADD CONSTRAINT [DF__Tmp_e1_tur_s__id__759D1B5E] DEFAULT (newsequentialid()) FOR [id] GO因此,看起来newsequentialid()约束已经存在,或者我漏掉了什么(可能性很大)? - frankieta
1
你当前在列上有一个我认为是正确的默认约束;但是,由于它是默认值,只有在插入语句不包括 id 列时才会使用。由于 Hibernate 生成 GUID 并在 INSERT 中提供它,因此您获得的是该值,而忽略了您的默认值。对于 IDENTITY 列,我认为 Hibernate 有一个特定的生成器,将不包括该列在 INSERT 语句中,而是检索服务器生成的值。 - Matt Gibson
谢谢Matt。是的,Hibernate有一个选项用于IDENTITY列,但我想它只适用于int或bigint列,而我的是varchar(36)。你完全正确,因为Hibernate已经提供了默认值,所以SQL Server会忽略我的默认值。 - frankieta
1个回答

6
newsequentialid() 的默认选项当然不起作用,因为 hibernate 不使用默认值,它总是设置由其生成器发出的值。
快速查看 JUG 库,似乎它没有提供任何按顺序生成 GUID 的方法。我不知道你为什么认为通过 Generators.timeBasedGenerator() 获取的生成器的 generate() 方法会给你顺序的 GUID。基于时间的生成器只是一个在生成 GUID 时考虑当前时间的生成器,但是在嵌入到 GUID 中时,它可以自由地混淆当前时间坐标,因此不能保证生成的 GUID 有任何顺序。
一般来说,“GUID”和“顺序”这两个术语是不兼容的。您可以选择具有 GUID 的键或具有顺序的键,但在正常情况下,您不能同时拥有两者。
所以,你确定键必须是 GUID 吗?就个人而言,我觉得 GUID 非常难处理。

但是如果你必须进行任何必要的黑客攻击以获得连续的GUID,则我的建议是编写自己的函数,生成看起来像GUID的36个字符字符串,但是是连续的。

连续部分应该来自于一个“序列”,它只是发出连续的整数。(我相信MS-SQL-Server支持它们。)

您可以阅读IETF的UUID规范,了解如何正确构造GUID,但您不必完全遵循它。在大多数情况下,如果它看起来像GUID,那就足够了。

如果您可以拥有单个全局序列,那就很好。如果您无法拥有单个全局序列,那么您需要以某种方式标识您的序列,然后在生成GUID时考虑每个序列的标识符。(这将是IETF文档中提到的“节点ID”.)

我曾经有一个不合理的要求,即我要传输到某个 Web 服务的行必须由 GUID 标识,并且有太多的繁文缛节阻止我联系他们询问“你们是认真的吗?”因此我只传输了像以下这样的 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
...

他们一言不发。


2
首先,您是否拥有服务器集群或其他多节点配置,以实际需要使用GUID?如果没有,那么事情很简单:使用普通整数标识键列,如果有人必须将其视为GUID,则添加一个计算列,该计算列将标识值呈现为GUID。如果您必须拥有GUID,并且没有序列,则需要使用具有单个整数标识列的表来模拟序列,但它将比序列慢得多。 - Mike Nakis
1
是的,性能很可能会受到影响。而且根据你描述的情况,即使你没有真正需要GUID,似乎你也必须坚持使用GUID。 - Mike Nakis
1
因此,选择生成自己的GUID。 - Mike Nakis
你可以这样做,但并不是必须的。通过使用 SEQUENCE 发行的数字(或由另一个表中的标识列实现的序列),可以保证顺序性,因为你的 MS-SQL-Server 版本不支持序列。 - Mike Nakis
1
@frankieta 当然,你可以在GUID上拥有一个非聚集的唯一索引,同时在标识列上拥有一个聚集索引。这应该仍然可以为您当前的查询提供良好的性能。 - Matt Gibson
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接