为什么我们不用数据库生成 ID?
共 1930字,需浏览 4分钟
·
2021-01-17 14:25
先介绍一下背景 🔭
团队正在一个为 SQL Server 构建数据目录项目的历程中,我们优化系统以实现解耦。这对我们来说非常重要,从根本上来说,我归结为两个核心原则,希望每个软件专业人员都能认同:
我们不希望系统复杂度随功能的增加而线性增长,这样会大大拖慢我们在业务发展速度以及对于价值的信心。
我们希望能够优先从客户需求、访问性能、查询模式、业务变化等方面考虑,能够适应不断发展的需求和需要。换句话说,我们希望能够将系统内的任何组件换成更合适的组件,以满足当前而不是过去的需求。
下面是 protoactor-go 开源项目里的一句话 “software should be composed, not built”,与我要在本文陈述的观点非常相似:
对持久化有何影响?🤔
考虑到前面总体原则,我们不想把自己的状态持久化耦合到一个特定的数据库引擎上。从实际情况来看,就是说不将持久化的具体关注点传递到领域层。之所以要实现这一点,因为我们今天对规则的认知可能会让我们依赖某种具体数据库技术,比如 SQL Server,但并不能确保它能满足未来的能力需求。
有了这个具体的要求,持久化就需要出现在域事件而不是存储系统中,这也导致不同的存储需求。
幸运的是,有一些广为人知的、经过实战检验的模式可以解决这个问题,如结合 CQRS 领域驱动设计中的聚合设计等。因此这里的假设是,实现理想状态对我们来说是低成本的。
代码生成 ID 还是数据库生成 ID 🔬
许多数据存储系统(如 SQL Server)都具备为每条记录(如行、文档等)生成唯一标识符的方法。当将新记录插入表中时,使用自动增量键可以生成唯一编号。当我们要插入一条新记录时,数据库引擎就自动完成自增主键,并且可以确保该键对于表是唯一的。
但是如果依赖数据库来为我们业务域生成唯一标识符,这将让业务层与数据存储系统耦合在一起。如果我们想切换一个不支持生成自动增量主键的数据存储系统,那就不能简单更换。而且,每个数据存储系统生成标识符的方式都不一样,这可能会导致我们最终使用不同的主键类型。除此之外,这些类型的生成方式可能不适合分布式系统。例如,当我们在 SQL Server 数据库中拥有一个生成主键的表时,我们没有一个简单的方法在分布式环境中水平扩展这个表。
通过让业务域的消费者(即通过命令和查询与域通信的传输层)负责唯一键的标识符生成,就可以克服这些问题。它减少对环境的依赖性,又反过来让我们不用依赖数据库来生成 Id。这种方法还有一个好处:它可以支持分布式。例如,我们可以将一个表分区到两个物理 SQL Server上,并分担查询的请求。如果我们有一个自动增量的字段,这在 SQL Server 上就不行了。
我们决定做什么?🚀
基于这些事实,我们决定让业务域层来生成域集合的标识符。我们在域层内将这些标识符表示为 64 位无符号整数。域消费者可以根据自己的上下文自由决定应该用什么表示方式(例如ASP.NET Core MVC 可以将标识符序列化为字符串,以便于其客户端消费资源对象等)。
为什么要 64 位整型,而不是 UUID?
最后,您可能想知道为什么使用64位整数。此处的主要目的是使我们能够甚至跨聚合根生成唯一的标识符。考虑到几乎每个平台都有对应的 API,UUID 是非常方便的调用方式(例如 .NET 中 Guid.NewGuid() 等)。UUID 最大的问题是存储和索引方面的成本与开销,虽然对我们来说不是大问题。不过,在分布式系统中,已经有一些成熟的方法来生成唯一标识符作为更高效的主键类型,比如使用 64 位整数。Twitter Snowflake (1) 算法就是其中之一,这让我们选择了 64 位整数而不是 UUID。我们使用的是 Rob Janssen 的开源 IdGen (2) 库,它是一个适用于 .NET 的 Twitter Snowflake 类型的 ID 生成器。
(1) https://developer.twitter.com/en/docs/basics/twitter-ids.html
(2) https://github.com/RobThree/IdGen
关注公众号【Java技术江湖】后回复“PDF”即可领取200+页的《Java工程师面试指南》
强烈推荐,几乎涵盖所有Java工程师必知必会的知识点,不管是复习还是面试,都很实用。