
1. 从“自增”说起为什么它既是便利也是陷阱在数据库设计里给表的主键或者某些关键字段设置一个自动递增的数值几乎是很多开发者下意识的操作。在MySQL里我们习惯用AUTO_INCREMENT在Oracle里我们用序列SEQUENCE配合触发器在PostgreSQL里有SERIAL类型。这个功能太常见了以至于我们很少去深究它背后的实现细节和可能带来的问题。直到你开始接触国产数据库比如达梦数据库DM你会发现这个看似简单的“自增列”在不同的数据库产品里实现方式、行为表现乃至背后的设计哲学都可能存在微妙的差异。达梦数据库作为一款成熟的关系型数据库自然提供了自增列的功能。但如果你只是简单地把MySQL那套AUTO_INCREMENT的思维直接搬过来很可能会在后续的开发、迁移或者性能优化中踩到一些意想不到的坑。比如自增值的缓存机制是怎样的在并发插入的高压场景下会不会出现“跳号”或者重复自增列作为主键在分布式或分库分表场景下又该如何处理这些问题都需要我们深入到DM的实现机制里去寻找答案。我经历过一次从Oracle到DM的迁移项目其中一个核心痛点就是序列和自增列的转换。原系统大量使用了Oracle序列迁移到DM时是选择用DM的自增列特性还是继续沿用序列模拟这不仅仅是语法转换的问题更涉及到事务一致性、性能表现和未来扩展性的权衡。通过那次项目我深刻体会到理解一个数据库特性的“所以然”远比记住它的“如何使用”要重要得多。这篇笔记我就结合自己的实践和源码层面的探索基于公开文档和测试来拆解一下达梦数据库自增列的实现逻辑以及在实际使用中你必须留意的那些“坑”。2. 达梦自增列的两种面孔表内定义与序列达梦数据库实现自动递增功能主要提供了两种方式这两种方式看似都能达到“自动生成数字”的目的但底层机制和适用场景却有显著区别。理解这种区别是你正确使用该功能的第一步。2.1 表内自增列声明式的便利这是最接近MySQLAUTO_INCREMENT或 SQL ServerIDENTITY的使用方式。你在创建表或修改表时直接在列定义上使用IDENTITY关键字。-- 创建表时定义自增列 CREATE TABLE employee ( id INT IDENTITY(1, 1), -- 种子为1增量为1 name VARCHAR(50), PRIMARY KEY (id) ); -- 修改表增加自增列注意DM不允许直接向已有列添加IDENTITY属性通常需要新增列 ALTER TABLE employee ADD COLUMN serial_id INT IDENTITY(100, 1);这种方式最大的特点是声明式和强绑定。自增属性是表结构的一部分与特定的列紧密耦合。其管理如当前值的查看、重置也需要通过特定的表级操作或系统视图来完成例如使用IDENTITY_CURRENT(‘模式名.表名’)函数获取指定表自增列的当前值。对于简单的单表主键场景这种方式非常直观和方便DDL语句清晰意图明确。2.2 独立序列SEQUENCE灵活共享的利器另一种方式是使用独立的序列对象这继承了Oracle、PostgreSQL等数据库的传统。你需要先创建一个序列然后在插入数据时通过NEXTVAL函数来获取下一个值。-- 创建一个序列 CREATE SEQUENCE seq_employee_id START WITH 1 INCREMENT BY 1 CACHE 20; -- 在插入语句中使用序列 INSERT INTO employee (id, name) VALUES (seq_employee_id.NEXTVAL, ‘张三’);序列的核心优势在于灵活性与共享性。一个序列可以被多个表、多个列共享这在某些业务逻辑需要全局唯一递增ID时非常有用。序列也是一个独立的数据库对象拥有自己的属性如起始值START WITH、增量INCREMENT BY、缓存大小CACHE、是否循环CYCLE等管理起来更为独立和精细。注意在达梦中虽然IDENTITY列在内部很可能也是通过序列机制实现的这一点我们可以从一些系统行为推断但它在语法和语义层面对用户是隐藏的。你不能直接像一个普通序列那样去操作一个表的IDENTITY列。这种封装带来了便利但也失去了一些灵活性。那么在项目里到底该选哪种我的经验是如果你的自增ID严格服务于单表主键且没有跨表共享的需求优先使用表内IDENTITY列代码更简洁。如果你需要更复杂的控制比如自定义缓存、在多个地方生成序号、或者需要兼容Oracle语法那么独立序列是你的不二之选。特别是在异构数据库迁移或需要高度可控的ID生成策略时独立序列提供了更大的操作空间。3. 自增机制的“心脏”缓存与事务性探秘当我们谈论自增时最核心的关切往往是它在高并发下能保证唯一且连续吗要回答这个问题就必须揭开达梦自增无论是IDENTITY还是SEQUENCE的缓存机制和事务面纱。这是最容易产生误解和性能问题的地方。3.1 缓存CACHE机制性能与连续的权衡达梦的序列以及背后的IDENTITY列实现支持CACHE选项。CACHE 20意味着数据库会在内存中预先分配20个连续的序列值。当应用请求NEXTVAL时数据库直接从内存缓存中返回一个值速度极快。只有当这20个值用完后数据库才会更新磁盘上的序列元数据并获取下一个缓存块。这带来了巨大的性能提升特别是在高并发插入场景下避免了每次获取序列值都要更新磁盘数据字典的瓶颈。这也是为什么生产环境序列通常都建议设置一个合理的CACHE值比如100或更大。但缓存也带来了“副作用”不连续和“浪费”。假设一个序列设置了CACHE 100。会话A获取了值1到100缓存于内存。如果此时数据库重启或者会话A的缓存因某种原因被丢弃那么这1-100的值就“丢失”了。下次序列从磁盘加载时会从101开始缓存导致1-100成为“空洞”。这就是你有时会看到自增ID出现大幅度跳号的原因。这不是Bug而是为了性能做出的设计权衡。对于IDENTITY列达梦数据库同样有内部的缓存机制。虽然你不能像序列那样显式设置CACHE大小但数据库内部会进行优化管理。这意味着IDENTITY列在高并发下也可能出现跳号。3.2 事务性与唯一性保证这是一个关键问题我开启一个事务插入一条记录获取了一个自增ID然后我回滚了这个事务这个被“消耗”掉的ID会重新被使用吗在达梦数据库中答案是否定的无论是IDENTITY列还是SEQUENCE一旦一个值被生成即NEXTVAL被调用即使生成它的事务最终回滚这个值也永久性地被“消耗”了不会回滚。这一点和MySQL的AUTO_INCREMENT行为是一致的但和某些数据库如早期版本的SQL Server取决于隔离级别可能不同。这种设计确保了ID的唯一性和单调递增性尽管可能不连续避免了因事务回滚导致的ID冲突风险。从实现角度看序列值的分配是在调用NEXTVAL时即刻发生的是一个独立于用户事务的原子操作其提交不依赖于后续插入事务的提交或回滚。理解这一点对于业务逻辑很重要。你不能假设ID是严格连续无间断的也不能依赖“回滚后ID会回收”这种假设来设计逻辑。如果你的业务对ID的连续性有严格要求例如需要作为严格连续的流水号那么自增列可能不是最佳选择你需要考虑使用额外的逻辑如事务表锁或应用层队列来生成序号但这会牺牲性能。4. 实战中的“坑”与最佳实践了解了原理我们来看看在实际开发和运维中有哪些常见的坑以及如何规避。4.1 并发插入下的主键冲突幻觉这是一个经典场景你设计了一个表主键是IDENTITY列。在代码中你先执行INSERT然后立刻通过SELECT IDENTITY或IDENTITY_CURRENT()之类的函数获取刚插入的ID用于后续操作。在单线程测试下一切正常一旦上线面对并发请求偶尔就会报“主键冲突”错误。为什么问题往往不出在自增机制本身而在于你获取插入ID的方式和时机。IDENTITY或SCOPE_IDENTITY()如果DM支持类似功能这类函数返回的是当前会话最后生成的IDENTITY值。在高度并发的环境下如果两个插入操作在极短时间内发生A插入后在它执行获取ID的语句之前B插入操作可能已经完成并生成了下一个ID。这时A再去获取拿到的可能就是B的ID从而导致后续逻辑错乱甚至引发真正的冲突。解决方案最可靠的方式在INSERT语句中直接使用RETURNING子句如果DM支持或输出参数在同一个数据库交互中返回生成的ID。这是原子性的。如果DM不支持RETURNING确保“插入”和“获取ID”这两个操作在同一个数据库事务中并且中间不能有其他会生成自增ID的操作。但这在高并发时很难保证。对于序列则没有这个问题因为你是在INSERT语句中显式调用seq.NEXTVAL这个值在调用时就已经确定并绑定到当前SQL语句中了。4.2 数据迁移与ID重置的烦恼当你需要从生产环境导出一部分数据到测试环境或者进行数据归档时如果表中有IDENTITY列可能会遇到麻烦。直接导入数据IDENTITY列的现有值会被插入但表的自增计数器并不会自动更新到最大值之后。这可能导致后续新插入的数据其ID与已导入的数据发生冲突。处理方案达梦提供了SET IDENTITY_INSERT语句类似于SQL Server允许你显式地向IDENTITY列插入指定的值。-- 允许对指定表的IDENTITY列进行插入 SET IDENTITY_INSERT employee ON; -- 执行你的INSERT操作可以包含id列的值 INSERT INTO employee (id, name) VALUES (999, ‘迁移数据’); -- 操作完成后关闭 SET IDENTITY_INSERT employee OFF;在导入完数据后至关重要的一步是重置表的自增种子使其大于当前表中已有的最大值。达梦没有直接的ALTER TABLE ... AUTO_INCREMENT xxx语法像MySQL那样。你需要通过重建表或使用特定的管理命令如DBCC CHECKIDENT的类似功能具体命令需查阅对应版本手册来实现。一个常见的方法是先找出当前最大值然后使用ALTER TABLE ... MODIFY COLUMN ... IDENTITY (new_start, 1)来修改列的IDENTITY属性起始值。注意修改列属性可能是一个重量级操作在大表上需要谨慎并选择业务低峰期进行。4.3 自增列作为主键的局限性思考虽然自增列作为主键非常普遍但它并非银弹尤其在分布式架构下可预测性单调递增的ID容易被爬虫遍历数据。分布式瓶颈在分库分表场景下单纯的自增列无法保证全局唯一。你需要引入分布式ID生成方案如雪花算法、UUID等或者使用每个分片独立的ID区间。写入热点由于InnoDB或达梦的类似存储引擎聚集索引的特性自增主键会导致所有插入都发生在索引的末尾可能造成最后一个页的写入热点。在高并发写入场景下这可能成为瓶颈。此时可以考虑使用非自增的、离散度更高的主键如UUID或组合键来打散写入。因此在设计表结构时不要盲目使用自增主键。思考一下这个表的数据量和并发度如何未来是否需要分片ID是否需要全局唯一业务上是否介意ID不连续回答这些问题后再决定是否使用自增列以及是使用表内IDENTITY还是独立SEQUENCE。5. 性能调优让自增飞起来对于写入密集型的应用自增ID的生成速度也可能成为性能关键点。以下是一些针对达梦数据库的调优思路。5.1 序列缓存CACHE大小的艺术对于独立序列CACHE大小是首要的调优参数。设置得太小比如默认的1每次调用NEXTVAL都可能需要访问磁盘性能极差。设置得太大虽然性能好但一旦数据库重启丢失的号段就多不连续性更明显。如何设置这需要权衡。对于绝大多数OLTP应用我建议设置为一个适中的值比如100到1000之间。这个值应该远大于每秒的序列请求量这样可以确保在正常运行时序列缓存不会在1秒内耗尽从而避免频繁的磁盘同步。你可以通过监控序列的CACHE命中率或等待事件来辅助判断。如果业务完全不能接受跳号那么只能使用NOCACHE选项但必须承受相应的性能损失。5.2 监控与诊断当自增变慢时如果发现插入性能下降怀疑与自增列有关可以从以下方面排查序列争用检查是否有多个会话在频繁请求同一个序列的NEXTVAL。虽然缓存能缓解但如果CACHE设置过小仍可能在高并发下出现enq: SQ - contention类似的等待事件在Oracle中常见达梦可能有类似机制。通过数据库的动态性能视图如V$SESSION_WAIT或V$SYSTEM_EVENT查看相关等待。IDENTITY列的管理开销对于IDENTITY列虽然对用户透明但数据库内部需要维护其状态。在极高频的插入场景下这也可能成为瓶颈。对比测试使用IDENTITY和使用独立SEQUENCE的性能有时会有意外发现。日志写入序列值的更新当缓存耗尽时需要写重做日志Redo Log。确保日志文件所在磁盘的IO性能不是瓶颈。5.3 一个替代方案应用层批量ID生成在极端性能要求的场景下你可以跳出数据库自增的范畴考虑在应用层实现ID生成。例如应用启动时从数据库获取一个ID区间比如1-10000然后在内存中分配。用完后再申请下一个区间。这相当于把CACHE做到了应用层性能最好且减少了数据库的交互和争用。但实现复杂度高需要处理应用重启、区间浪费、全局唯一性在分布式环境下等问题。常用的分布式ID生成器如Snowflake也是这种思想的体现。6. 从达梦看数据库设计特性的共性与个性通过对达梦自增列的深入剖析我们可以管中窥豹看到数据库设计中的一个普遍道理没有绝对好的特性只有适合场景的用法。达梦的IDENTITY和SEQUENCE本质上提供了不同层次的抽象和灵活性。IDENTITY是开箱即用的便利隐藏了复杂性适合大多数常规场景SEQUENCE则暴露了更多的控制权适合需要精细调控或复杂逻辑的场景。这种设计在很多现代数据库中都能看到影子比如PostgreSQL的SERIAL类型背后也是序列。作为开发者或DBA我们的任务不是记住某个数据库的某个语法而是理解这些特性背后的机制缓存、事务性、唯一性保证以及它们带来的权衡性能 vs 连续性、便利性 vs 灵活性。这样无论面对的是达梦、Oracle、MySQL还是PostgreSQL你都能快速抓住核心做出合理的设计选择并有效地排查相关问题。最后关于自增列我个人的一个强烈建议是在项目初期设计数据表时即使你决定使用自增主键也最好额外增加一个具有业务意义的唯一约束如业务编号、用户邮箱等。自增ID最好只作为技术性的、无意义的代理键Surrogate Key。这样当未来某一天你需要做数据迁移、分库分表或者发现自增机制成为瓶颈时你会有更多的回旋余地和切换方案而不至于让业务逻辑与自增ID深度耦合动弹不得。这或许是从“如何实现”上升到“如何设计”的一个更重要的思考。