探索 PostgreSQL 18 的全新 UUIDv7 支持
Postgres 18 中引入的 UUIDv7 解决了将完全随机的 UUIDv4 用作主键时存在的性能缺陷。通过加入时间戳,UUIDv7 确保新生成标识符具有天然排序性,从而实现高效的顺序插入、提升缓存利用率并减少索引碎片。
是否该在数据库中使用 UUID 作为主键?您可能听说过它们会严重影响性能——传统 UUIDv4 确实常有此弊。
然而,UUIDv7 的引入解决了 UUIDv4 的大部分问题。让我们深入了解其特性,并探讨为何采用它是个明智之选。
什么是 UUIDv7?
UUIDv7 是相对较新的全局唯一标识符(UUID)类型。Postgres 18 版本引入该特性,旨在缓解使用传统 UUID(即 UUIDv4)作为数据库主键时的性能问题。
与完全随机的传统UUIDv4不同,UUIDv7在其128位结构中将时间戳作为最高位部分,从而支持基于创建时间的自然排序。
本文将阐述其多重优势,但与其仅阅读说明,您不妨通过以下命令亲自尝试。
创建螃蟹商店
avn service create -t pg --cloud do-nyc --plan pg:free-1-1gb 'crab-store' -c 'pg_version=18'
完成后创建两个表,区别仅在于一个使用UUIDv4,另一个使用UUIDv7。
CREATE TABLE crab_inventory_4
(id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
crab_type VARCHAR(75) NOT NULL,
acquisition TIMESTAMP DEFAULT NOW;
CREATE TABLE crab_inventory_7
(id UUID PRIMARY KEY DEFAULT uuidv7(),
crab_type VARCHAR(75) NOT NULL);
CREATE OR REPLACE FUNCTION insert_random_crabs_explain(table_name TEXT, num_rows INT)
RETURNS TABLE(query_plan TEXT) AS $$
BEGIN
RETURN QUERY EXECUTE format(
'EXPLAIN ANALYZE
INSERT INTO %I (crab_type)
SELECT CASE (random() * 9)::int
WHEN 0 THEN ''Dungeness Crab''
WHEN 1 THEN ''Blue Crab''
WHEN 2 THEN ''Hermit Crab''
WHEN 3 THEN ''Fiddler Crab''
WHEN 4 THEN ''Coconut Crab''
WHEN 5 THEN ''Snow Crab''
WHEN 6 THEN ''King Crab''
WHEN 7 THEN ''Stone Crab''
WHEN 8 THEN ''Spider Crab''
ELSE ''Horseshoe Crab''
END
FROM generate_series(1, %s)',
table_name, num_rows
);
END;
$$ LANGUAGE plpgsql;
性能优势探索
要使用创建的函数并亲自对比结果,可执行以下命令。首先分析使用UUIDv4在表中创建10000条记录的过程。
-- create 10000 entries with uuidv4
> EXPLAIN ANALYZE (SELECT * FROM insert_random_crabs('crab_inventory_4', 10000));
+-----------------------------------------------------------------------------------------------------------------------+
| QUERY PLAN |
|-----------------------------------------------------------------------------------------------------------------------|
| Function Scan on insert_random_crabs (cost=0.25..0.26 rows=1 width=4) (actual time=88.996..88.997 rows=1.00 loops=1) |
| Buffers: shared hit=40220 dirtied=397 written=103 |
| Planning Time: 0.033 ms |
| Execution Time: 89.084 ms |
+-----------------------------------------------------------------------------------------------------------------------+
> table crab_inventory_4 limit 3;
+--------------------------------------+-------------+----------------------------+
| id | crab_type | acquisition |
|--------------------------------------+-------------+----------------------------|
| bdf3b756-938b-454c-a9a6-252279baa310 | King Crab | 2025-10-08 15:23:27.002235 |
| 2fd0efc3-5e30-4116-883c-5d5836a6bf0e | Spider Crab | 2025-10-08 15:23:27.002235 |
| f7db0247-8741-4156-9e95-b37a23989bae | Spider Crab | 2025-10-08 15:23:27.002235 |
+--------------------------------------+-------------+----------------------------+
现在使用UUIDv7的表重复相同操作。
-- create 10000 entries with uuidv7
> EXPLAIN ANALYZE (SELECT * FROM insert_random_crabs('crab_inventory_7', 10000));
+-----------------------------------------------------------------------------------------------------------------------+
| QUERY PLAN |
|-----------------------------------------------------------------------------------------------------------------------|
| Function Scan on insert_random_crabs (cost=0.25..0.26 rows=1 width=4) (actual time=64.746..64.746 rows=1.00 loops=1) |
| Buffers: shared hit=20415 dirtied=112 written=112 |
| Planning Time: 0.035 ms |
| Execution Time: 64.834 ms |
+-----------------------------------------------------------------------------------------------------------------------+
EXPLAIN 4
Time: 0.094s
> table crab_inventory_7 limit 3;
+--------------------------------------+----------------+
| id | crab_type |
|--------------------------------------+----------------|
| 0199c9cc-31a7-7703-9703-91b7151a3698 | Dungeness Crab |
| 0199c9cc-31a8-7442-805c-e4b64c3766de | Stone Crab |
| 0199c9cc-31a8-74be-a3d4-1f2c18a91629 | Spider Crab |
+--------------------------------------+----------------+
由此可见,UUIDv4完全随机的结构会显著影响性能:它迫使B树索引进行随机插入,导致索引页分裂并降低缓存效率。
UUIDv7通过包含时间戳解决了此问题,使新生成UUID可按创建时间自然排序。这使得索引能像自动递增整数一样高效执行顺序插入操作。由此带来的性能优势显著:索引碎片减少、缓存局部性增强、缓存利用率提升。
UUIDv7的排序特性还简化了查询操作,因其天然具备时间排序属性,无需额外设置时间戳列进行排序。这种结构特别适用于需要高插入速率和高效查询的应用场景。
通过对UUIDv4表按acquisition字段排序,再对UUIDv7表按id字段排序,我们可清晰验证上述特性。
-- Sorting using a timestamp (UUIDv4 primary key)
> EXPLAIN ANALYZE SELECT * from crab_inventory_4
ORDER BY acquisition;
+-------------------------------------------------------------------------------------------------------------------------------+
| QUERY PLAN |
|-------------------------------------------------------------------------------------------------------------------------------|
| Sort (cost=6799.80..6949.80 rows=60000 width=35) (actual time=30.339..47.615 rows=60000.00 loops=1) |
| Sort Key: acquisition |
| Sort Method: external merge Disk: 2952kB |
| Buffers: shared hit=503, temp read=369 written=371 |
| -> Seq Scan on crab_inventory_4 (cost=0.00..1100.00 rows=60000 width=35) (actual time=0.016..4.946 rows=60000.00 loops=1) |
| Buffers: shared hit=500 |
| Planning: |
| Buffers: shared hit=25 |
| Planning Time: 0.397 ms |
| Execution Time: 52.249 ms |
+-------------------------------------------------------------------------------------------------------------------------------+
-- Sorting using the UUIDv7 primary key
> EXPLAIN ANALYZE SELECT * from crab_inventory_7
ORDER BY id;
+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| QUERY PLAN |
|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| Index Scan using crab_inventory_7_pkey on crab_inventory_7 (cost=0.29..1574.29 rows=60000 width=27) (actual time=0.012..14.191 rows=60000.00 loops=1) |
| Index Searches: 1 |
| Buffers: shared hit=673 |
| Planning: |
| Buffers: shared hit=17 |
| Planning Time: 0.173 ms |
| Execution Time: 16.597 ms |
+--------------------------------------------------------------------------------------------------------------------------------------------------------+
UUIDv7正广泛普及
除Postgres外,该版本已在多种编程语言和平台获得支持。例如本月发布的Python 3.14中,用户可直接通过标准库调用该功能。
# /// script
# requires-python = ">=3.14"
# ///
import uuid
print(uuid.uuid7())
# Gives us this id - 0199c489-c5b3-70aa-9ad0-37afd22ef141
为验证其结构与版本是否与Postgres一致,可通过内置函数uuid_extract_version执行以下命令进行核查。
-- Check the verion of the id we created in Python
SELECT uuid_extract_version ('0199c489-c5b3-70aa-9ad0-37afd22ef141');
uuid_extract_version
----------------------
7
-- Comparing that against a PG generated id
SELECT uuid_extract_version (uuidv7());
uuid_extract_version
----------------------
7
这表明UUID在不同平台上具有统一格式,意味着您可像使用典型UUIDv4那样在系统间通用。
使用UUIDv7可能引发的问题
当主键在对外应用或API中暴露给终端用户时,出于安全考虑通常不建议使用UUIDv7。核心问题在于UUIDv7将48位Unix时间戳作为最高位组成部分,这意味着标识符本身泄露了记录的创建时间。
此类泄露主要涉及隐私风险。攻击者可利用时间数据作为去匿名化或账户关联的元数据,从而可能暴露组织内部的活动模式或增长率。尽管UUIDv7仍包含随机数据,但依赖主键实现安全防护被视为存在缺陷的方法。专家建议仅将UUIDv7用于内部键值,并单独暴露真正随机的UUIDv4作为外部标识符。
如何从UUIDv4迁移至UUIDv7
从UUIDv4迁移至UUIDv7时,建议优先制定周密的迁移计划,因处理新格式可能需要修改应用程序。
由于UUIDv7采用时间戳排序(不同于随机生成的UUIDv4),需评估其对现有索引和查询的影响。因此建议针对具体工作负载进行全面性能测试。
需注意以下几点:UUIDv7依赖系统时钟,需通过NTP等机制实现时钟同步;且时间戳精度仅支持毫秒级。
最后,务必更新所有依赖特定UUID格式的外键及外部系统,确保迁移后功能无损。
总结
正如我们所见,Postgres 18 中引入的 UUIDv7 解决了将完全随机的 UUIDv4 用作主键时存在的性能缺陷。通过加入时间戳,UUIDv7 确保新生成标识符具有天然排序性,从而实现高效的顺序插入、提升缓存利用率并减少索引碎片。
但请谨记关键隐私注意事项:由于创建时间会泄露,UUIDv7最适合用作内部键,而非面向外部的应用程序。
Postgres 18还包含更多激动人心的新特性,您可立即通过Aiven探索Postgres 18的全部潜力。
> 当主键在面向外部的应用程序或API中暴露给最终用户时,出于安全考虑通常不建议使用UUIDv7。主要问题在于UUIDv7将48位Unix时间戳作为其最高位组成部分,这意味着标识符本身会泄露记录的创建时间…专家建议仅将UUIDv7用于内部键值,并单独暴露一个真正的随机UUIDv4作为外部标识符。
这基本抵消了UUIDv7的所有性能提升。因为任何来自用户的数据都需要查询UUIDv4,这意味着每条新记录都需要额外生成随机UUIDv4并插入到 第二个 B树索引中,从而重新制造了UUIDv7本应解决的性能问题。
换言之,UUIDv7仅适用于 永远 无需用户数据查询的行。或许在某些连接操作的数据中存在这种情况…但这似乎更像是例外而非常态,且无法预知内部ID何时可能需要转为外部ID。
这仅在记录创建时间泄露本身构成安全隐患时成立。
对我而言最关键的问题是:如何在20+工程师的环境中扩展v7?
使用v7时,我需要某种审计机制,在每个API契约中检查v7的使用情况及潜在信息泄露风险。
检测API契约中的V7 UUID可能需要强制使用特殊键名(uuidv7与uuid分别对应v4/v7),以便审计。
工程师们会多次犯错——尤其在初级/高级工程师混合团队中。
此外,API契约将显得不一致:部分资源采用v7标识,部分仍用v4。更严重的是,在特定资源上使用v4会泄露信息:那些由v4标识的资源必然包含敏感数据。
若坚持使用v4,所有API资源将采用统一标识符。必要时可单独在响应中暴露创建时间戳。由于字段明确标注其内容,审计工作将大幅简化。
归根结底都是人为工程问题。
某些高可靠性/高保障环境明确禁止使用UUIDv4,因为长期存在工程师无视警告使用弱熵源生成UUIDv4的情况——这类问题往往在生产环境引发故障后才被发现。显然部分工程师根本不理解“强熵源”的含义。
混合使用UUID类型本应可被检测,因为类型是UUID的组成部分。但许多公司系统中存在非标准UUID覆盖类型字段,与标准UUID混用。实际操作中,往往不得不将UUID视为不带语义的128位不透明整数。
> 若要在API契约中检测V7 UUID,可能需要强制使用特殊键名(uuidv7与uuid分别对应v4和v7)以便审计。
除非我遗漏了什么,否则应在接收时进行验证,若不匹配则拒绝。`uuid.replace(“-”, “”)[12]` 或 `uuid >> 76 & 0xf`。
无论难度如何,这终究是优先级问题。暂且不论潜在的安全隐患(我坚持认为对多数企业而言,这远不如人们想象的重要),关键在于你是否在意大规模环境下的性能表现。若表规模永远不会超过几百万行,则无需在意。但若涉及数亿级数据,性能就至关重要——尤其当这些ID用作主键时,若使用InnoDB引擎则影响加倍。
> 坚持使用v4方案,所有API资源将拥有统一标识符。必要时可单独在响应中暴露创建时间戳。由于字段明确标注其内容,审计工作将大幅简化
若业务规模可观且需关注数据库维护/吞吐量,祝你好运。不妨询问公司DBA团队的偏好。
若你读过前文评论,这已形成恶性循环
>>> 当主键暴露于面向外部的应用程序或API时,出于安全考虑通常不建议使用UUIDv7。
>> 这基本抵消了UUIDv7的所有性能优势。因为用户请求必须先查询UUIDv4,意味着每条新记录都需要额外生成随机UUIDv4并插入到第二个B树索引中,这恰恰重现了UUIDv7本应解决的性能问题。
> 除非记录创建时间泄露本身构成安全隐患,否则上述情况并不成立。
不成立,因为当API返回的资源包含表示创建/修改时间戳的属性时,“泄露创建时间”并非安全问题。
当暴露可预测标识符会引发安全风险时(例如暴露作为数据库主键的UUIDv7或serial[0]类型),攻击者便能比使用随机标识符时更快地合成匹配任意资源的标识符。
0 – https://www.postgresql.org/docs/current/datatype-numeric.htm…
只要实施完善的数据权限检查,可预测ID完全可行。UUIDv7的随机部分足够大,其可预测性远低于自动递增ID。
若安全机制依赖于攻击者无法知晓ID(即未实施数据权限检查),则该安全机制存在缺陷。
这种机制在邀请/无用户账户共享场景中很常见吧?
确实如此,但有人会反驳说128位的熵值本身就不够作为优质邀请令牌。
我只是困惑为何delifue将这种行业标准做法称为不良实践
讨论涉及两种情况:UUIDv7作为密钥确实不妥,但适用于其他多数标识符。若我能猜出你的用户ID,理论上不构成威胁——你的业务逻辑本应阻止我利用该信息。但若我猜中密码重置令牌,情况就不同了,因为仅凭该令牌我就能造成实际损害。
因为它就是?
不是吗?
完全正确。几天前我刚写过相关文章。
使用UUID v7作为主键(可能)构成人力资源违规行为。
https://mikenotthepope.com/primary-keys-using-uuid-v7-are-po…
究竟哪部分违反了这里的年龄歧视法?是k-排序UUID泄露信息的事实,还是有人利用它们歧视求职者?
若属后者(根据维基百科摘要推测应是如此),那么“k-排序UUID构成HR违规”的整个前提就是无稽之谈。
争论时间戳泄露此类信息的问题在于——任何事物都可能泄露这种模糊的时间信息。
– 在2010年后消失的网站上看到?抓到你了!
– 被Waybackmachine索引?抓到你了!
– 2022年前的记录使用<不同UUID方案>?抓到你了!
要彻底避免泄露实体的时间线索,唯一方法就是永远不以任何可关联的方式暴露其存在(就我目前的思考而言,这似乎完全违背了向用户界面展示该信息的初衷)。
具体场景是怎样的?
2025年我提交申请被拒。
二十年后我用原有用户资料再次申请同家公司,却因有人通过用户ID发现我年事已高而遭拒?
本质上是这样。这只是个特殊案例。我觉得挺有意思才写了简要说明,不值得深入分析。
所以你只是把被拒的“老家伙”身份拖延到他们与你互动时才暴露,而不是通过UUID识别?
在人力资源系统场景中,UUIDv7相较UUIDv4真的存在性能优势吗?你们到底要追踪多少求职者?
我实在不明白你们为何要考虑UUIDv7。
你可以按ID排序记录、按ID检索最近N条记录等等。这比使用时间戳更方便。
这种情况应该很少见吧?
过去大家使用顺序键时,我们总在泄露近似创建时间。顺序键反而更糟:它们泄露近似记录数,让人轻易观察到新键生成速率,一旦掌握这些信息就能推算出任意键的近似创建日期。
UUIDv4 消除了这三种泄露途径。UUIDv7 仍能消除其中两种。它不会泄露记录总数或生成速率,仅泄露创建时间。且仍无法推测相邻键值。对于你日常主动披露的信息而言,这属于相当有限的信息泄露。
我常看到顺序订单ID,它们每次递增1,因此通过自行下单就能估算每分钟的订单量。我亲眼观察到这种现象——当时我故意删除并创建新订单(因为系统不支持修改尚未确认的现有订单)。但作为普通用户,利用这些信息能造成什么危害?这是个正当疑问,我无意造成损害,只是真心不明白这有什么问题。
我认为这会对追踪ID造成影响,但对订单ID影响不大——除非系统允许查看非本人账户的订单,这本身就是根本性的安全漏洞。使用UUIDv4或随机字符串不过是掩盖安全隐患的障眼法。
这同样构成对竞争对手或潜在收购对象的工业间谍行为。
或是战时情报:<https://en.wikipedia.org/wiki/German_tank_problem>
但UUIDv7在创建时间泄露方面更糟糕。对于顺序ID,攻击者需要大量数据才能缩小创建时间范围,这大幅提高了攻击门槛,只有极端执着的攻击者才能推断出具体时间。
而UUIDv7在无需采样的情况下始终泄露创建时间。普通攻击者能轻松查到时间戳,从而获得进一步探测和关联账户的动机。
> 对于顺序ID,攻击者需要大量数据才能缩小创建时间范围。
当顺序整数ID被外部化时,攻击者无需创建时间即可实施预测性攻击。他们只需对已知标识符应用增量即可。
这似乎过于偏执,即便对安全研究人员而言也是如此。
某些实际应用未必涉及安全问题。例如存储医疗记录时,你不会将其作为患者就诊的公开标识,因为日期受HIPAA法规约束。
这种说法可能并不准确。
你不会公开发布患者就诊记录,唯一合法查看记录的人员是那些需要访问该就诊记录的人员,而他们很可能需要知道就诊时间。这种访问应通过身份验证、授权控制并进行审计。
通常还会对这类数据进行大量基于时间的查询:我今天有哪些就诊记录?本周有哪些?等等。若需处理时区偏移,可考虑添加DateTime字段,但此场景下v7版本可能优于v4。
但必须将该ID关联至患者身份信息才行吧?仅日期本身不构成HIPAA违规。否则每天看病的人都会触犯HIPAA,因为每个日期都存在。
记得破解密码的年代,当我们尝试攻破ElGamal加密时发现:某些用Delphi编写的代码(其随机数生成器基于日期时间且强度不足),若能推测出编译时间和密钥生成时间, 就能推算出大致时间范围。若将该时间范围作为随机数生成器的种子进行暴力破解,再据此生成随机ElGamal密钥,就能大幅缩小可能范围(例如只需暴力破解1000万个整数,而非数十亿甚至更多)。
很久以前某在线赌场就遭遇过类似攻击。据我所知,有人发现其伪随机数生成器的种子源自系统时钟,因此只需在近似时间戳前后暴力破解所有洗牌操作,再将结果与已知牌型(即自己收到的牌)比对,一旦匹配成功就能知晓所有玩家的底牌。
我一直觉得这种攻击方式很优雅(关键在于攻击者并未直接利用时间作为种子)。
难道不能直接给种子加盐实现真正随机吗?这方案在我看来设计得不够完善。
难点在于——他们用日期当盐。
我曾阻止某款飞机维护软件采用这种极其恶劣的SSL会话密钥生成方案。在实时操作系统上获取优质随机种子实在困难。我告诉你。
关键不在单条记录,而在关联记录。若能按时间序列排列所有数据,去匿名化就容易得多。
但若API对象包含常见的createdAt字段,通过标识符获取创建时间的能力就显得纯属理论了。
担忧不仅限于完整记录的访问。它延伸至任何标识符的意外暴露,尤其是通过短信或邮件等不安全旁路渠道传输的情况。
多数情况下这属于合规问题而非公开攻击途径,但仍需面对“是否最小化了隐私暴露面?”这类质询时给出否定答复,或至少附加保留条款。
正因如此,某些人对“禁止SELECT *”条款如此狂热。
能否举例说明:在医疗记录交互场景中,何种情况下合法持有ID却不关联日期/时间?
电子邮件本身不安全,但发送包含“预约信息”链接的邮件是可行的。若该链接指向
/appointments/sjdhfaskfhjaksdjf,则不会泄露数据;若指向/appointments/20251017lkafjdslfjalsdkjfa,则链接本身就包含个人健康信息(PHI)。关于创建日期是否属于PHI…我认为可以支持此观点,因为它与医疗信息相关联(即患者就诊时间,可能对应症状出现时间)。
电子邮件或许不安全,但面部识别和手机同样存在风险,医疗从业者却始终在使用这些技术。
打错字的传真……是传真,不是脸!
值得注意的是,这完全是荒谬的论点。目前我接触过的所有系统都会在邮件中明文发送日期/时间/地点/执业医师信息(或类似内容)。
唯一看似受保护的只有“预约原因”,而且并非所有系统都这样做。
患者初次就诊时都签署过授权文件!
你这条评论ID为45622189,界面明确显示发布于11小时前。假设ID是连续编号,结合这两点信息,我更倾向于认为这是HN系统泄露而非UUID泄露——毕竟这些内容本就属于公开信息范畴。
或许吧,但你的观点是什么?
这确实可能构成隐私问题。试想当我进行在线支付时,若某个ID能精确透露我的开户时间,这便成了推测我年龄的有效依据。
银行账号(假设讨论的是此类信息而非代币)本身已是高度敏感的法律受保护信息。
相比之下,年龄的近似值泄露风险微乎其微。
银行账号印在每张支票上。虽然如今多数人不再使用支票,但在线账单支付仍会偶尔寄送实体支票。这些账号从未真正属于敏感信息范畴。
银行安全体系并不依赖账户信息的私密性。本质上所有银行安全措施都可归结为银行拥有一个“魔法撤销按钮”——当发现交易存在问题时,他们就能撤销该交易。当然现在银行会在前端设置过滤机制以减少使用该按钮的必要性,但这只是额外的防护措施,旨在将撤销按钮的使用频率控制在可控范围内。
没错,若将不可预测的公共ID作为安全模型,根本算不上安全措施。
完全正确
这曾是历史隐患——过去人们常使用基于当前时间的确定性密码生成工具。
此前有篇文章提到过,通过向密码生成工具输入特定时间段内所有可能的时间戳,并尝试所有组合密码,成功找回了部分比特币访问权限。
使用UUIDv4作为主键存在意想不到的弊端,因为数据局部性在某些出人意料的地方会产生影响[1]。
采用UUIDv7主键似乎能减轻/消除这些问题。
若同时存在用于外部ID的UUIDv4索引列,我推测其调用频率不会高于主键索引,因此不会抵消UUIDv7带来的性能提升。
[1] https://www.cybertec-postgresql.com/en/unexpected-downsides-…
> 使用UUIDv4作为主键存在意想不到的弊端,因为数据局部性在某些出人意料的地方会产生影响。
确实如此,正如您提供的链接中所详述。因此我发现一种有效的方法是:同时设置内部主键
id(serial类型[0]列,绝不对外共享)以及 另一列external_id(带UUIDv4唯一约束),专门用于向跨进程协作方提供标识符。0 – https://www.postgresql.org/docs/current/datatype-numeric.htm…
> 我怀疑它不会像主键索引那样频繁被使用
这无关紧要,因为关键在于索引条目的创建成本,而非查询频率。查询成本本就相同。
我链接的页面展示的是创建后的使用场景,此时成本可能不同。
假设如下:
> 由于工作负载通常关注最近插入的行
这仅适用于非常特定类型的应用程序,并非普遍情况。
许多应用程序会从所有时间段抓取数据行,最新数据行并无特殊之处。最新数据行甚至可能是最不受欢迎的数据行,因为很少有操作会引用它们。
若对此存有顾虑,可将UUIDv7标识符通过ECB分组密码进行加密,初始向量设为0。采用128位UUID与128位AES分组,即可实现标识符在应用程序进出时的加密解密,操作简便且开销近乎为零。
当可实时计算映射关系时,无需将隐私保护ID放入数据库索引
严格来说这算改进,但效果有限。你无法更换密钥,因为下游用户已依赖旧密钥加密的ID,且一旦密钥泄露,加密的所有优势即刻丧失。虽可通过“密钥版本”标记ID来更新密钥,但该标记本身又构成某种信息泄露。
为何需要前向保密性?
我已将该表述从帖子中删除,因不确定其术语是否准确,但问题依然存在。若密钥泄露,所有用该密钥加密的ID都可被解密,最终回到原点。
那反而比存储64位bigint+128位UUIDv4更糟糕复杂。你的盐值(AES分组)已超出bigint容量。除非你指的是AES的固定值(这种做法是否存在),但那属于胡乱添加盐值——本质上是通过混淆实现的安全性。
呃…什么?直接用AES分组模式加密,固定密钥和初始向量就行。
输入128位,输出128位。加密强度足够高,客户端无法从中推断任何信息,后端又能保留序列ID的所有优势。
你还可以预留UUID中的几位作为版本号(采用循环遍历机制),实现未来兼容性。
我还是觉得调用像uuid.v4()这样的函数更简单,认知负担更小。
单调递增的UUID具有优势,它们与B树和关系型数据库配合更佳。
我的意思是内部使用UUIDv7,若担心日期泄露则对外暴露UUIDv4(两者共存于同一对象)。
UUIDv7在分布式系统中依然表现优异,且如你所述具有算法优势。
无需新增UUIDv4字段,只需对UUIDv7进行格式保留加密(FPE)即可。
这种转换的计算复杂度与为每个UUIDv7维护UUIDv4查找表相比如何?
数据库查询加额外索引的开销远高于硬件辅助解码。
即使UUIDv4被缓存,你仍需承担额外存储和索引成本。百万级系统尚可承受,但想象十亿、百亿级规模。
若未缓存呢?更糟的是,此时你将直接访问磁盘。
计算机并不缺乏CPU性能,尤其当你能部署CPU指令集时。天啊,你甚至不需要加密。何不采用简单的位移操作,加入一个简易查找标识符?虽属黑盒方案,泄露时效果不佳,但若实际位移模式泄露,你还有更严重的问题要应对。只需额外占用一两个字节来标识模式即可。
ID混淆很简单,无需完整加密。
硬件辅助方案在此是转移视线。如你所言,核心问题在于随机读取导致数据局部性差,这种数据库性能损耗的修复成本极高。
为何会计算复杂?加密功能已集成在芯片中,实际成本近乎为零。而查找表在绝大多数情况下会因内存局部性差而导致成本激增。
在规范化良好的架构中或许可行。使用uuidv4作为外部标识符,再建立映射表关联内部使用的标识。这样只需销毁暴露的uuid、更新映射表并生成新uuid,内部所有指针和外键都无需修改。
关键在于,映射表本身会产生索引开销——这恰恰是规范化试图规避的问题。规范化与否无关紧要。
不知关系型数据库领域是否对这类映射表有专有名称?
我们称之为查找表或映射表。
问题:为何不采用UUIDv7,同时加密分发给用户的ID?这样查询时只需快速解密,还能额外获得优势——例如能为不同用户分配不同ID
暴露创建时间真的有风险吗?我觉得对大多数应用来说这并不关键
我不会说这必然“有风险”,更准确地说,当你不想透露实体创建时间时,它会迫使你采取行动。假设你用这些ID标识网站用户,且它们出现在API查询/URL等场景中,那么用户何时创建账户就一目了然。诚然许多网站已公开此信息,但并非全部如此;若你不想暴露呢?若你认为用户资历不该被公开,以免影响其他用户对其的偏好行为呢?
这需要深思熟虑。像Facebook和Twitter这类系统虽采用隐含时间信息的ID,但它们标识的对象本身已具备公开的创建时间戳。
看到v7和V4时,人们自然会期待数字越大越好,最好在所有方面都更优。我没想到升级前竟需要如此周全的考量。那不如直接命名为UUID-b更贴切 😉
这在UUID中很常见。例如多数情况下你仍会选择普通UUID4而非UUID5。当然也可能需要UUID5,这取决于具体用例。
像UUID这样的规范已臻完善——充其量只是重新排列字节及其含义。
这些“专家”是谁?我身为数据库架构师且高度注重安全,认为这种假设对多数企业而言纯属荒谬。
若对应用确实重要,那就别暴露原始ID——用AEAD加密生成不透明ID再暴露即可。
我认为适用场景是数据库多主模式,或数据库不负责主键生成。
此时将uuidv7用作主键和外键,反而能提升性能。
我在某张表中使用此方案,要求按ID(主键)排序时需同步按创建时间排序(新记录ID值更大)。
该ID将向用户展示。若采用整数类型,会暴露表内记录数量。
各位,我的用法正确吗?
呃。
用户表可能不该/无需使用v7,因为用户年龄对查询模式的影响有限甚至无关紧要。例如我们的Steam和亚马逊账户都相当老旧,但我们可能仍在使用它们。
然而订单表更可能基于时间进行查询,因此在此使用v7非常合理。
我认为安全影响被夸大了,但通常情况下你可能允许用户查询他人信息,比如查看我的Steam个人资料或亚马逊愿望清单。你大概不需要查询其他用户的订单记录。
或者,若你正在构建企业风险解决方案,或许会认为不应让用户知晓风险存在多久,但多数方案仍会展示历史数据,并认为这是关键信息。
虽然存在判断失误的情况,但实际操作并不复杂到那种程度。
观点精辟。此外,支持多重ID机制会带来维护困扰。
我认为UUID的核心价值在于支持客户端生成ID,这使其天然面向用户。其重要应用场景在于:当网络问题导致插入失败时,能避免客户端意外重复创建记录,从而实现插入操作的幂等性。
例如:当用户点击表单按钮向数据库插入记录时,客户端可生成UUID,将其附加到JSON对象后发送至服务器插入; 若期间发生网络问题导致插入状态不明,程序可自动重试(或用户手动重试),只要使用相同的UUID就不会产生数据重复风险。
自动递增ID无法实现此功能,因其由数据库集中生成,用户无法预知ID值。若表单提交时发生网络故障,客户端无法自动判断记录是否成功插入;若强行重试,可能导致数据库中出现重复记录。除非依赖某种在数据库端具有唯一性约束的固定ID,否则无法实现操作的幂等性。
若担心泄露创建时间,能否伪造时间戳?可采用保留大部分性能优势的方式实现——例如以1970年为基准时间,间歇性累加基准时间,并为新记录随机分配月份和日期(或基于用户ID分配——这样用户记录在时间上保持一致,但与其他用户记录不同步)。
我确信存在折中方案,既能保留大部分性能提升,又能大幅降低去匿名化风险。
编辑:传输过程中加密值似乎才是更简单的解决方案
这种情况下,自动递增字段也可定期重置,并从十亿级开始计数。
它们比uuidv7更高效。为何还要使用UUID?或许我仍需要UUID,因为它们可在客户端生成,且能使错误的连接操作返回空结果集。
没错,这种情况直接用UUIDv4加另一个“ULID”就行
这毫无意义
文章写得很好,特别是这部分:
> 使用UUIDv7可能引发哪些问题
当主键暴露于面向外部的应用程序或API时,出于安全考虑通常不建议使用UUIDv7。核心问题在于UUIDv7将48位Unix时间戳作为最高位组成部分,这意味着标识符本身泄露了记录的创建时间。
> 此类泄露主要涉及隐私风险 攻击者可利用时间数据作为去匿名化或账户关联的元数据,可能暴露组织内部的活动模式或增长率。尽管UUIDv7仍包含随机数据,但依赖主键实现安全防护被视为存在缺陷的方法。专家建议仅将UUIDv7用于内部键值,并单独暴露真正随机的UUIDv4作为外部标识符。
> 专家建议
哪些专家?具体针对哪些场景?他们何时认为创建时间属于敏感信息?
或者直接批量生成并从列表中提取?
> 专家建议仅将UUIDv7用于内部键值,同时暴露独立生成的真正随机UUIDv4作为外部标识符。
那这样还有什么意义?我过去的做法是:内部主键使用自动递增的大整数,对外键则使用独立的随机UUID。我认为“专家”的建议相当愚蠢——既然仍使用独立内部键,采用UUIDV7几乎毫无优势(除少量可移植性改进外)。
虽然我不建议像使用UUIDv4那样将UUIDv7当作安全令牌,但将其作为对外暴露的对象键并无不妥——毕竟权限检查始终是必要的。
我曾问过类似问题,确实这似乎完全适用于分布式系统,且仅限部分场景。基础单数据库Postgres只需使用序列主键即可。
适用于无法使用自动递增的分布式数据库。
或因某些原因需在插入数据库前生成ID的场景。例如同时向多个服务插入数据时。
实际上许多分布式数据库都支持自动递增机制——通常会批量生成大段ID范围进行分配。
我们这家财富90强企业的“分布式数据库”至少横跨10种不同的数据库产品。
UUIDv4让我们得以规避这个问题。
这是糟糕的设计吗?大概是。但大型企业会这样做吗?肯定会。
你说的没错。这是放弃数据库管理员(DBA)和计算能力日益增强的必然结果——即使有人注意到性能下降是主键选择的问题,他们往往也能通过投入更多资金来“解决”。
真希望Postgres能支持按字段随机组件检索记录,80位随机值的碰撞概率有多高?我猜应该足够低。
当然可以创建这种索引。
没错,但关键在于若将其自动化并集成到Postgres中,用户就能无需深思直接使用。这将消除大多数大型系统采用此方案的阻力——在我看来这本是明智之举,而非因安全问题引发争议的方案。
更理想的设计是允许创建自定义显示的类型,同时在SQL中内部设置原生类型(这需要在C语言层面实现)。
> 增长率
恕我直言,实在看不出来这有什么用处。
UUIDv7仅在范围分区和隐私保护方面存在缺陷。
“天然可排序性”对Postgres及多数UUID使用者而言是优势,因为它避免了排序分布桶中最后一个桶在插入时持续扩容的问题。
期待看到类似HBase或S3路径的实现方案应用于UUIDv7场景。
> UUIDv7仅在范围分区和隐私保护方面存在缺陷。
若您担忧泄露的是UUID生成时间,那么其隐私性并不比其他UUID变体更差。
至于范围分区,当然可以选择基于UUIDv7的哈希值进行分区,代价是放弃更经济的权限/更快的索引。另一方面,这必然牺牲局部性——这是分区方案的普遍挑战。具体取决于系统的端到端设计,但我不会断言UUIDv7本身优劣或优于/劣于其他UUID方案。
它至少比v4差一点吧?毕竟v4完全没有时间戳。虽然可能存在使用非安全随机数生成位值的担忧,但说它与字面时间戳无法区分并不准确。
UUIDv4不会泄露生成时间。
为什么它对范围分区不利?反而不更好吗?使用UUIDv7时,你基本上可以按主键分区,从而实现“全局”唯一约束。
不明白为何会对范围分区不利?
我认为时间戳部分和UUID部分应该都建立了索引?
这样不更有利于分区吗?毕竟查询时只需匹配时间戳部分的分区
题外话,感谢本文让我知道Postgres的“table foo”是“select * from foo”的简写形式。虽然不会在代码中使用,但很乐意在交互式查询中用它。
我从未意识到Postgres在插入单调值时效率更高。这源于B+树的特性,合乎逻辑。但在分布式数据库领域,单调插入会引发热点分区和可扩展性问题,因此更倾向于均匀分布的ID。
换言之:“别在CRDB上尝试这种做法”。
这是B+树的特性,叠加了聚簇索引的特性:若使用UUIDv4作为主键,整行数据会被随机移动到不同位置,这在需要顺序检索时简直糟透了。若采用非聚集索引(例如为公共API使用UUIDv4标识以避免泄露v7信息),随机数据仍会加剧碎片化,但自动清空机制通常能及时处理。不过这会增加自动清空机制的额外工作负荷。
Gp提到Postgres没有聚簇索引,它采用的是表聚簇功能——这本质上是重写整个表的点操作,而非持久化属性。
啊,我忘了在PG中CLUSTER需要手动执行。本质上是同样的陷阱,但你需要自己装填瞄准,不像MySQL那样全自动——在MySQL中似乎无法拒绝主键聚簇(SQL Server情况类似,但可更改聚簇索引)。感谢澄清。
这有点吹毛求疵,但你混淆了MySQL和InnoDB的概念。(在MySQL模型中,选择不使用聚簇索引的存储引擎即可规避聚簇索引机制。)
实际操作中,上游MySQL选择非InnoDB存储引擎的情况极为罕见,但在Percona Server或MariaDB中或许稍常见些。
数据库中的抽象层漏洞正是每位开发者都应研读项目所用主流数据库目录的原因之一。但据我观察,几乎无人践行此道。
能否详细说明热分区机制?
文章对比了UUIDv7与v4,但未说明为何要选择它们而非我惯用的serial/bigserial。是我漏看了什么吗?
好问题。选择UUID而非序列号有几个理由:
– 序列号会泄露总记录数及新增记录速率的信息。用户/攻击者可能据此推测系统中记录数量(通过统计用户/客户/发票等数量)。这是个需具体分析的微妙问题。根据应用场景不同,可能无害也可能造成灾难性后果。
– 序列号必须由数据库生成。UUID可在任何位置生成(包括后端或前端应用),有时能简化逻辑。
– 由于UUID可跨平台生成,更易于实现分片。
UUID的明显缺点是生成速度略逊于序列键。UUIDv7虽提升了插入性能,但会泄露生成时间。
实践表明:序列键泄露的数据往往引发严重问题;而UUID(v4)几乎总能满足速度要求。若需迁移,将表转换为UUIDv7也相对简单。
不仅能推测客户等对象的总数,还能猜测具体个体。
这是世上最简单的黑客手段。你正在查看/customers/3836/bills?若将编号改为4000呢?他们是大型企业,我敢打赌这个记录存在。
他们是否在每个地方都设置了适当的安全检查?这很容易测试。
但如果你访问的是 /customers/{big-long-hex-string}/bill,你猜中另一个有效ID的可能性基本为零。
没错,这是通过模糊性实现的安全。但这种模糊性做得 非常出色。
此建议基于/customers/:id/bills为公开路径的假设。受保护的路由本就不应暴露账单等敏感信息,因此这更多是授权问题(谁能访问哪些资源),而非隐私问题。这意味着,若能访问customes/4000/bills,则问题根源在于应用逻辑而非ID类型本身。
在设计良好的应用中,仅通过访问受保护URL不应能推测记录是否存在。反驳观点:常规BIGINT或序列号主键性能优异,足以满足多数应用需求。
你描述的场景需要依赖人工技能来预防此类漏洞,但实践反复证明:人非圣贤,疏漏难免。
系统必须从_架构设计_层面就考虑安全性。
安全需多层防护:使用128位随机密钥可使UUID不可破解。但_同时_还应在记录层面实施授权控制,并在API端设置速率限制以防止暴力破解。
通常情况下本就不该暴露主键。
该建议主要源于bigint/serial类型的问题。若主键采用UUIDv4,暴露主键的风险则相对较低。
某些场景下可排除或匿名化主键,但其他场景仍需主键。一旦开始构建API供外部访问系统,UUIDv4便是最佳标识方案。
不过超大规模表存在性能问题。若表级超大(数十亿行量级),UUIDv7能在微小安全代价下提供性能优势。
个人而言,我几乎所有表都采用v4,因为真正达到关键规模的表数量极少。但具体情况具体分析。
关键在于连接操作次数而非表大小。若在序列主键表上暴露UUID4次要字段,便无需在安全与性能间权衡。
客户端可在插入前生成ID——这正是我采用此方案的主要场景。另一场景是分布式系统中后期需要合并数据时,可避免ID冲突。
让客户端为你生成ID似乎是个糟糕的主意?
此处的客户端指后端系统吧?这样就能批量创建关联数据行再整体插入,无需每次调用数据库分配序列ID。通常我采用后者方案,但确实能想象某些场景下会导致效率低下。
常规流程是使用INSERT … RETURNING id语法,这样既能获取刚插入记录的数据库生成ID,又不会造成性能损失。但这种方法不适用于循环依赖关系,且会限制批量处理规模。不过相较于使用128位主键与64位主键的性能差异,这些限制通常影响较小。
没错,我也是这么做的
这种方案相当优雅。当客户端生成ID时,可完全规避临时ID或外部ID的混乱局面,这对离线优先客户端尤为实用。
当然需确保服务器能接受该ID,但UUID的唯一性特性几乎能保证这一点。
客户端生成ID对分布式或离线优先系统必不可少。我们公司Vori为杂货店开发收银系统,该系统为所有生成数据分配UUIDv7标识符,这些数据最终会同步至后端。同步时长差异极大:网络畅通的门店可能不到1秒,离线门店则可能耗时数小时。
是否可能发生冲突?是的,但冲突概率极低,无需过度担忧(尽管我在系统设计时确实为此纠结过)。
此处“客户端”可能指后端应用服务器。因此可让数十至数百台后端服务器向同一张表插入数据,而无需单一权威机构协调ID分配。
那张表本质上仍是单一权威机构吧?不过步骤越少确实越高效。
除非使用分片或集群数据库系统,此时记录本身可能存储在不同服务器,键值生成也可能分散处理。
在这种情况下确实如此。根据使用模式,顺序存储仍有其适用场景,但写入密集型操作能从无需等待单服务器返回ID中获益。
为什么?
没错,我认为这是两大关键因素。
当你不可避免地需要向公众暴露ID时,UUID能防止序列号易受攻击的多种漏洞。理论上它们在某些场景下也能更快速/便捷——生成UUID无需依赖中央索引来协调创建过程。它们还能被视为全局唯一标识符,在特定场景下颇具实用价值。不过我认为没人会说它们的整体性能优于serial/bigserial,毕竟它们在索引中占用更多空间。
但这些仅是内部ID,公开ID应另设专属字段。在分布式系统中无需中央索引即可生成uuid7固然便利,但这毕竟是Postgres数据库。
虽然公共ID的索引使用uuid7比uuid4更快,但存在文章提及的类似信息泄露风险。
所谓“分布式系统”不必是复杂的专用架构。两个Postgres数据库之间的关联操作就可能属于此类需求,或者数据库与平面文本文件的关联也算。
我通常采用UUID4作为次要键进行关联,主键则使用序列号。曾尝试直接用UUID4作主键,结果在数据量不大时就因影响每一次连接操作而变慢。
人们对此想得太复杂了。使用对称加密算法(如Feistel密码)处理内部ID即可安全暴露。即使顺序ID也能呈现随机效果。
表面看似简单,但关键在于密钥轮换机制。
若需生成类似UUID的不透明ID(例如需兼容不同系统生成的无冲突ID),最佳方案是分离这两项需求。对外使用UUIDv4,内部采用bigint存储。这样既无需暴露生成时间,又能在主系统中通过全序关系管理数据属性。
现在需要在分片或集群数据库系统中协调这些序列ID。
这正是关键所在。这些ID仅在系统内部唯一,并非全局唯一。它属于底层实现细节属性,类似关系型数据库管理系统中的参照完整性机制。此时若需原子递增操作,可另行处理。
UUID可由堆栈中多个服务生成
bigserial必须由数据库生成
但若直接用毫秒作为bigserial呢?末尾再加硬件随机数避免冲突?等等
单单给这条评论点赞似乎远远不够。
没错,这会成为标识符,但它将具备 全局唯一性——覆盖所有设备的宇宙范围。得给这个方案起个名字
核心担忧在于:若数据库主键是序列号,除非额外屏蔽外部API访问,否则该ID可能暴露给用户。若授权检查存在漏洞,将导致枚举攻击泄露私密或半私密信息。而UUID几乎无法被猜中,能有效规避此风险。
但正如文章所述,uuid7仍可被猜出。前提是这些仅作为内部主键使用。
两者存在本质差异:序列号会泄露数据添加速率,
UUID7仅能让他人知晓创建时间,无法推测特定时间段内创建的记录数量(近似值)。它泄露的是单条记录信息,而非其他记录数据。
其可预测性远低于顺序ID,且随机部分数值范围相当庞大…生成服务器每毫秒可产生数十亿种可能值… 。实际操作中根本不可能“猜中”这些值。
仅凭80位熵值就能猜中?
>为何要选择这两种而非始终作为首选的序列ID/大序列ID?我是否遗漏了什么?
常见应对方案是恶意行为者通过顺序ID爬取。UUID通常不可猜测,可直接作为主标识符投入Mongo这类杂烩数据库或S3存储,无需担心权限问题或被精明的攻击者攻陷整个数据库。这属于典型的安全通过模糊化实现。
你们不做水平扩展吧?
多数人会吗?不是人人都是谷歌。
很多人拥有多台服务器,需要在服务器间生成一致的标识符。这并非“谷歌级规模”才有的需求。
你的评论(在我看来)强烈暗示了数据库的水平扩展。没错,这未必达到“谷歌级规模”,但会带来大量额外复杂性——我很乐意规避这些。不过谷歌员工处理每个面向公众的项目时,大概都会默认采用全面水平扩展的思路。
即使多台服务器访问单一数据库,我仍倾向于让数据库自行生成ID。
确实,太多建议毫无缘由地直接推荐使用uuid4或uuid7作为主键。除非是分片数据库场景,即便如此也需视具体情况而定。
说到谷歌,Spanner确实推荐uuid4,且明确排除了uuid7这类开头包含时间戳的uuid格式。
这是Postgres。虽然有Citus扩展,但它依然支持(甚至可能推荐?)序列主键。
我的客户在所有API调用中都返回created_at属性,因此UUIDv7对他们毫无影响。他们同样使用序列ID。仅有一家客户曾将UUIDv4用作主键。虽然我们未遇到性能问题,但整个生产系统仅由一个psql保险实例和一个Elixir应用服务器支撑。在这个规模下,几乎任何架构选择都算得上合理。
我能证实性能提升效果。今年初为新数据库规划时,我曾着手开发临时函数实现UUIDv7功能。待原生函数发布后,我们将直接迁移至新方案。
供参考:
CREATE FUNCTION uuidv7() RETURNS uuid AS $$ — 获取基础随机UUID并叠加时间戳 select encode( set_bit( set_bit( overlay(uuid_send(gen_random_uuid()) placing substring(int8send((extract(epoch from clock_timestamp())*1000):: bigint) from 3) from 1 for 6), 52, 1), — 设置版本位为 0111 53, 1), ‘hex’)::uuid; $$ LANGUAGE sql volatile;
我不同意此观点
> 尽管UUIDv7仍包含随机数据,但依赖主键实现安全被视为有缺陷的方法
正确做法是:1. 在服务器端生成ID而非客户端 2. 始终验证客户端发送的所有ID的数据访问权限
可预测ID仅在未验证客户端发送ID的访问权限时才存在安全隐患。此外,UUIDv7的可预测性远低于自动递增ID。
但我认同在公开ID中包含创建时间可能泄露分析信息。
我坚决反对因隐私风险而完全弃用该方案,即便在医疗场景中亦然。
虽然能构想出极端泄露场景,但这假设了信息本就不会被泄露。
“暴露账户创建时间”——多数API默认会在响应中返回此信息。
你见过仅包含UUID列表而无其他敏感元数据的情况吗?
而真正攻陷99%企业的元凶是什么?钓鱼攻击。
API响应应仅限认证用户访问。ID常出现在不安全的邮件超链接中,或通过各类网络跳转传输的URL中——这些链接可能被截获并作为元数据公开。
这里是否存在不可避免的权衡?排序优美的密钥(自动递增整数、UUIDv7)必然泄露信息;而更安全的密钥(UUIDv4)因局部性差可能引发性能问题。
是否存在能兼顾安全性的随机ID生成器?既保持序列化特性,又不泄露精确时间戳和全局排序信息?
确实如此。空间局部性优势会迅速衰减。例如采用旋转盐值的哈希化UUIDv7方案,既能保持短期局部性及其性能优势,又可规避长期局部性带来的弊端。
在边缘节点对ID进行对称加密。可选嵌入式HMAC校验。可选文本编码方案。对于单调递增的大序列值,我倾向于采用base58编码(AES_K1(id{8} || HMAC_K2(id{8})[0..7])),其哈希派生函数子密钥由scrypt加密的系统密码短语配合用途/表盐生成。该方案的热路径处理速度相当快。如同所有加密方案,它伴随着全新丛林般的陷阱、注意事项与权衡取舍,但确实有效。
权衡不可避免。一端是UUIDv4,另一端是具有卓越局部性的灰码,但其本质允许识别记录所属索引区间(即使不进行反转)。UUIDv7堪称理想的中间方案。
鉴于API响应中普遍会暴露created_at时间戳,我认为这种说法不应被解读为“通常不建议”。现实案例如Stripe ID,其特性(k-排序)与UUIDv7相似:https://brandur.org/nanoglyphs/026-ids#ulids
这些都不是问题——永远不要允许最终用户自行决定序列主键。
它泄露的信息量微乎其微——用户可能知道最旧和最新的记录,但两者之间存在无限大的差距。
它在各个方面都比SERIAL或BIGSERIAL更优越实用——若需要随机/外部ID,添加第二列即可。搞定。
为何不采用序列主键搭配uuid4辅助键?所有连接操作都将基于主键进行,效率更高。
> 若需随机/外部ID,添加第二列即可。搞定。
正如他人所言,若需通过其他ID进行查询,这将彻底背离性能优化的初衷。
我尚存疑虑的是:在客户端生成v7 UUID的安全性如何?
v4 UUID的优势之一在于:可在客户端直接构造新实体的主键,数据库可直接使用。诚然存在微小冲突风险,但概率极低,基本可忽略不计。
但v7的大部分UUID基于时间生成,因此我不确定在任何应用中是否仍可安全忽略冲突——尤其考虑到客户端时钟可能存在较大误差。
我是否想得太复杂了?
同一毫秒内会收到多少次客户端请求?
UUIDv7的结构如下:
– 48位:Unix毫秒级时间戳
– 12位:毫秒级时间戳小数部分(用于额外排序)
– 62位:随机数据(确保唯一性)
– 6位:版本与变体标识符
因此每毫秒内可生成>4,600,000,000,000,000,000个ID。
客户端时间不精确无关紧要,因为部分设备时间超前部分落后,但这不会增加ID冲突概率。
这是否考虑了生日悖论?
若客户端能生成uuid4,则可能重复使用已知uuid4
类似[1]方案或其变体可解决安全顾虑。详见[2]讨论
[1] https://github.com/stateless-me/uuidv47
[2] https://news.ycombinator.com/item?id=45275973
对我而言,UUID在令牌等有效负载中的冗长度令人困扰。希望存在类似Git的通用缩写方案。
克罗克福德base32[0]方案是最佳折中方案,个人认为。26个字符的长度合理,仅使用字母数字字符,避免了大小写敏感和字符混淆问题(如0与O)。
0: https://www.crockford.com/base32.html
这是128位数值。若采用62进制(26个大写字母+26个小写字母+10个数字)表示,仅需略多于20个字符。通过增加进制并使用其他8位ASCII字符可进一步压缩。
有人遇到uuidv4的性能问题吗?我曾处理过数十亿行数据的数据库,完全没遇到问题。很想听听其他工程师的实际使用经验
我在处理数十亿行数据的数据库时遇到过问题,当时主键使用UUID。主键索引以及其他表指向该表的外键索引都相当庞大,甚至导致索引本身无法全部加载到内存中。例如在customer_id和document_id(均为UUIDv4)上创建复合索引。由于数据库不支持UUID原生存储,这些字段以字符串形式存储——仅10亿行数据就占用约30GiB内存用于主键索引,复合索引更耗费60GiB。最终索引体积超出内存容量。若支持UUID原生存储或采用字节存储,内存消耗或许能减半,但终究会超出极限。
假设需要查询最近100条文档,由于UUIDv4的随机特性,仅检索索引就需要进行100多次随机磁盘寻道。若UUID是顺序或半顺序排列,则可将检索次数降至寥寥数次,且由于多数命中均为近期记录,更易被缓存。按时间大致排序还能辅助分区操作。若无分区机制,随着表增长,仍需遍历包含大量五年前条目的B树。而按年份或年份-月份分区后,只需检索小部分数据子集,这些数据通常可轻松装入内存。
你使用的是什么数据库?例如SQL Server默认按主键对磁盘数据进行聚簇。对于uuidv4这类随机(非顺序)主键,插入行时需随机调整聚簇顺序,导致IO负载增加并引发性能问题。
而Postgres则不会对主键进行聚簇索引…如果我没记错的话。
Postgres。它也是单实例架构,这让事情简单多了。不过知道SQL Server也存在这个问题挺好的。
Postgres并非免疫于uuid问题,只是不那么敏感。uuidv4仍与btree索引配合不佳,会导致索引膨胀并影响性能。
难道还要求用户按姓名首字母顺序注册?
新病毒式营销创意诞生:仅限B开头姓名注册!赶快行动,别让C开头姓名用户永远抢走你的机会!
那换种聚簇方式?数据库中uuidv7解决的问题,在多数场景根本不存在。
坦白说影响不大。随机键确实会降低插入速度,但若插入操作仅占数据库负载的1%,那基本可以忽略不计。
反之,若你的数据库主要用于日志记录导致插入操作占比高达99%,那确实需要考虑这个问题。
有道理。感谢指正
有趣的是,aiven在几年前丢失客户数据后居然还能存在至今。
关于URL安全编码方案,对uuidv7与ulid、nanoid等方案有何看法?
我更倾向TypeID:https://github.com/jetify-com/typeid
个人认为ULID是最佳平衡方案:更紧凑、支持双击选中、且不区分大小写,可在macOS文件系统中无冲突保存。
现在需要有人开发UUIDv7→ULID转换库,实现UUIDv7与ULID的1:1映射,同时保留所有时间戳精度和随机性位,这样就能利用数据库层面的UUIDv7支持来存储ULID了。
UUID本质是具有特定结构的128位数字。若需编码,直接采用base32即可,无需任何转换方案。
必须进行转换才能正确保留时间戳信息,确保ULID库读取base32格式时能还原相同的时间戳。
我的观点是:若只需实现“双击选中且不区分大小写”的功能,ULID既无关紧要也毫无必要。直接将UUID编码为base32即可,其本质仍是UUID。
这取决于你对“URL安全”的定义
uuidv7(-)和nanoid(__-)包含特殊字符,URL编码后仍保留原貌。
这些ID都不够简短到适合电话报读;但从字符可读性看,ulid更合理。
精彩读物——简洁高效;我完全掌握了要点。做得很好
顺序主键对于基于主键索引按记录创建时间进行可扩展稳定排序至关重要,其原理类似序列号(整型),但能规避猜测漏洞。针对此场景,采用类似UUID v9的方案更优:https://uuidv9.jhunt.dev