ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Seata AT模式适配达梦DM8:分布式事务源码改造实战

Seata AT模式适配达梦DM8:分布式事务源码改造实战 前阵子做一个国产化改造项目数据库从原来的商业数据库切到达梦DM8应用层是Spring Cloud微服务订单服务和库存服务各连一套库。切换本身倒还好真正让人崩溃的是分布式事务——订单服务写成功了库存服务返回超时排查完发现库存其实扣了但订单那边已经回滚两边数据对不齐。后来我把Seata AT模式在DM8上完整跑通核心工作其实是源码适配达梦不在Seata官方支持的数据库列表里通过改源码让Seata的RM端资源管理器认识达梦的方言、元数据和undo_log读写。这篇文章把我从环境准备、源码修改到最终验证的完整过程整理出来适合正在用达梦做微服务、又必须解决分布式事务的团队。1. 订单扣库存炸了分布式事务故障现场与AT模式的解题思路1.1 从一次数据不一致说起场景一点都不复杂用户下单一件事涉及两个服务。order-service负责写订单表inventory-service负责扣库存。正常流程是先写订单再远程调用扣库存。问题出在远程调用失败的那一刻——订单已经提交到本地数据库库存没扣成功用户重新下单又扣了一次或者反过来说订单回滚了但库存已经扣了两边就是永远对不上。传统的解决思路里最“正统”的是XA协议。数据库原生支持两阶段提交业务通过TransactionManager开启全局事务所有参与者要么一起提交要么一起回滚。这套方案在单库时代没问题但微服务场景下用XA会遇到几个实际问题一是XA事务对连接持有时间长数据库的锁和连接池压力都很大高并发根本扛不住二是达梦DM8虽然支持XA相关接口但在一些中间件和连接池的组合下配置链路过长稍微一复杂就开始出幺蛾子三是XA对业务代码有侵入你得在代码里显式管理全局事务的生命周期分布式系统里一多就乱。后来我换成了Seata的AT模式至少业务侧终于能“无侵入”了。1.2 为什么XA两阶段锁在达梦上不划算XM的全局锁是把所有参与者的资源全部锁定直到全局提交或回滚这个锁的粒度是“数据库级别”的。对达梦这种国产数据库来说它兼容了Oracle和MySQL两套行为但XA事务的优先级和资源隔离并不像传统商业数据库那么“顺手”遇到大事务时性能衰减非常明显。更具体一点说微服务架构里订单服务和库存服务各连各的库XA要求两个数据库在同一个全局事务协调器下工作一旦某个分支事务网络超时全局事务可能长时间挂着数据库连接被白白占住。我在项目里试过并发一上来DBA那边就开始报警锁等待、连接池耗尽全来了。这时候Seata AT模式的“最终一致性”思路反而更实用业务操作正常执行通过undo_log记录回滚信息二阶段再异步做补偿锁的粒度小得多对数据库的压力也小得多。1.3 AT模式用一张undo_log表换来业务无侵入AT模式的核心机制可以拆成三句话。一阶段业务SQL正常执行但在执行前后Seata会为每一行受影响的数据生成before image和after image连同分支事务信息一起写入undo_log表然后和业务SQL放在同一个本地事务里提交。这个本地事务的提交起着“锁定资源”的作用相当于用业务数据库自身的行锁代替了全局锁。二阶段如果所有分支都成功了TC通知各分支删除undo_log这个操作不涉及业务数据动作很轻。如果任意一个分支失败TC通知各分支用undo_log里的镜像数据反向生成补偿SQL把数据改回原来的样子这个补偿SQL也是在一个本地事务里执行的同样要落undo_log。换到达梦的场景关键在于Seata官方源码里没有达梦的方言实现你让AT模式的RM端去操作达梦的undo_log它不知道该生成什么样的SQL、该用哪个元数据缓存类去读表结构、该用哪种方式管理全局锁。所以我们要做的事情就是把这些“方言”补上。2. 达梦DM8的三个“个性”适配前必须心里有数2.1 用户即模式连接达梦数据库时的权限与schema坑达梦DM8跟MySQL最大的区别是它没有MySQL那种“一个实例多个database”的清晰概念而是采用了“一个用户就是一个schema模式”的设计这点跟Oracle更像。你创建一个用户系统会同时创建一个同名的schema用户的表默认建在自己的schema下。连接的URL是jdbc:dm://127.0.0.1:5236端口固定5236登录用户默认是SYSDBA但生产环境肯定不能全用SYSDBA要按业务建两个用户比如订单库的用户叫ORDER_USER库存库的用户叫INVENTORY_USER。这个设计对Seata的适配影响很大。Seata的TableMetaCache在读取表结构时通过JDBC的DatabaseMetaData.getColumns()方法拿列信息MySQL里第一个参数传database名称达梦里你需要传schema名称也就是用户名。拿不到正确的模式名Seata就识别不了表结构后面的一切都无从谈起。所以接达梦的第一个动作就是确认业务表的归属用户以及Seata客户端连接数据库时使用的用户名两者必须匹配。2.2 默认大写和大小写敏感的标识符规则达梦另一个容易踩的坑是标识符的大小写。默认情况下不带双引号建的表名和列名达梦会统一转成大写存储如果你建表时给表名加了双引号并且用了小写那后续所有SQL里的表名、列名都必须带双引号保持一致否则就会报“无效的表名”或者“列不存在”。这个对从MySQL迁移过来的团队特别不友好。MySQL的SQL语句和表结构习惯用反引号、小写命名把这些脚本直接拿到达梦上执行表面上建表成功了实际上表名在数据字典里是带双引号的小写形式。Seata的元数据解析和SQL生成器默认按照MySQL的规则处理一查表结构就找不到目标表。我的建议是达梦上的业务表、undo_log表建表时全部统一用大写、不加引号。这样Seata和业务SQL生成器生成的大多数字符串能直接匹配上。2.3 兼容模式选MySQL还是Oracle直接影响改造量达梦在初始化实例的时候有一个COMPATIBLE_MODE参数决定了数据库对外表现更接近MySQL还是Oracle。这个参数不是随便选的它直接决定了Seata适配的改造成本。如果你选MySQL兼容模式那么达梦会接受AUTO_INCREMENT自增列、LIMIT分页、反引号等MySQL语法。Seata源码里对MySQL的支持是最完善的MySqlDialect、MySqlTableMetaCache、MySQLUndoLogManager这一套实现可以直接继承复用只需要针对达梦的特殊点做少量覆盖比如CLOB字段的读写、系统查询语句的替换。如果你选Oracle兼容模式Seata的Oracle实现比较依赖Oracle的序列、SYSDATE等特性达梦虽然兼容了一部分但细节上仍然有偏差改造成本明显更高。所以我强烈建议如果业务系统是全新开发、没有历史包袱达梦实例初始化的时候直接选MySQL兼容模式后面的源码修改会省很多事。如果是老系统从Oracle迁到达梦那已经是Oracle兼容模式了也能适配只是你要做好多改几个方法的心理准备。我们项目因为是从一个偏MySQL的架构迁移过来的所以走的就是MySQL兼容模式这条路。3. 环境与版本矩阵先搭好能跑通的基础设施3.1 版本搭配JDK、Spring Boot、Seata、达梦驱动怎么选我先给一个参考版本矩阵这些组合是我实测跑通的如果你用的版本差异太大后面编译源码、跑demo时可能多出不少额外错误。组件推荐版本说明JDK1.8Seata 1.6.x对JDK8的支持最稳达梦驱动也兼容Spring Boot2.7.x2.7是目前企业项目里比较主流的版本Seata1.6.1源码结构清晰SPI加载机制对二次开发友好达梦DM88.1.x及以上开启MySQL兼容模式达梦JDBC驱动DmJdbcDriver18.jar对应JDK8驱动类名dm.jdbc.driver.DmDriverNacos/注册中心2.x可选也可以用Seata内置的file注册方式为什么Seata选1.6.1而不是更新的版本因为我试过在1.8.x系列上做二次开发模块拆分变化比较大而且部分依赖对JDK版本要求更高适配达梦这种非官方数据库找一个结构稳定、社区资料多的版本更合适。1.6.1的rm-datasource模块里方言、undo_log、TableMetaCache这些抽象都做得比较干净通过EnhancedServiceLoader按dbType加载非常适合改源码加一个自定义类型。3.2 达梦初始化建用户、建业务表、建undo_log表我用disql或者达梦自带的管理工具执行以下SQL。先在数据库中创建两个业务用户CREATE USER ORDER_USER IDENTIFIED BY order_pass; CREATE USER INVENTORY_USER IDENTIFIED BY inventory_pass; GRANT DBA TO ORDER_USER; GRANT DBA TO INVENTORY_USER;测试环境直接给DBA权限省事生产环境按最小权限去给但至少要有建表、增删改查、创建索引的权限。然后在ORDER_USER下建订单表注意表名和列名都不带引号、统一大写CREATE TABLE ORDER_INFO ( ID BIGINT IDENTITY(1,1) PRIMARY KEY, ORDER_NO VARCHAR(32) NOT NULL, PRODUCT_ID VARCHAR(32) NOT NULL, QUANTITY INT DEFAULT 1, STATUS TINYINT DEFAULT 0 );IDENTITY(1,1)是达梦的自增语法等价于MySQL的AUTO_INCREMENT。MySQL兼容模式下写成AUTO_INCREMENT也认但用IDENTITY更符合达梦原生习惯。接着在INVENTORY_USER下建库存表CREATE TABLE INVENTORY ( ID BIGINT IDENTITY(1,1) PRIMARY KEY, PRODUCT_ID VARCHAR(32) NOT NULL, QUANTITY INT NOT NULL ); INSERT INTO INVENTORY (PRODUCT_ID, QUANTITY) VALUES (P001, 100);最后是关键的一张表undo_log。这张表是Seata AT模式的工作基础每个参与全局事务的业务库都必须建。DDL不能直接照抄MySQL版我把达梦可用的版本贴出来CREATE TABLE UNDO_LOG ( ID BIGINT IDENTITY(1,1) NOT NULL, BRANCH_ID BIGINT NOT NULL, XID VARCHAR(128) NOT NULL, CONTEXT VARCHAR(128) NOT NULL, ROLLBACK_INFO CLOB NOT NULL, LOG_STATUS INT NOT NULL, LOG_CREATED DATETIME(6) NOT NULL, LOG_MODIFIED DATETIME(6) NOT NULL, CONSTRAINT PK_UNDO_LOG PRIMARY KEY (ID) ); CREATE INDEX IDX_UNDO_LOG_XID_BRANCH ON UNDO_LOG (XID, BRANCH_ID);注意ROLLBACK_INFO字段MySQL官方DDL用的是BLOB到达梦上我建议用CLOB。原因后面避坑实录里细说这里先记住结论。3.3 Seata ServerTC部署先别急着改源码源码修改针对的是客户端RM那一侧但分布式事务的协调者TC得先跑起来。Seata Server 1.6.1下载解压之后先修改conf/application.yml最简配置如下server: port: 7091 seata: registry: type: file config: type: file store: mode: fileregistry.type: file表示客户端直接通过IP:端口连TC不走注册中心本地验证最方便。store.mode: file表示事务日志存本地文件也不依赖数据库。以file模式启动sh seata-server.sh -p 8091 -h 127.0.0.1-p是TC的服务端口默认8091-h是对外暴露的IP。如果是在服务器上部署-h要填服务器的内网IP而不是127.0.0.1否则客户端连不上。测到这一步整个基础环境就绪了可以开始动源码。4. 源码改造四个关键点让Seata真正认识DM84.1 改造点一DBType枚举与JDBC URL识别先下载Seata 1.6.1源码我用的是GitHub上官方仓库的1.6.1tag。第一次改动要加一个数据库类型。打开common/src/main/java/io/seata/common/model/DBType.java在枚举里加入DM8public enum DBType { MYSQL, ORACLE, ... DM8 }然后打开common/src/main/java/io/seata/common/util/JdbcUtils.java在getDbType(String jdbcUrl)方法里加入对达梦JDBC URL的识别if (jdbcUrl.startsWith(jdbc:dm:)) { return DBType.DM8; }这一步的意义在于客户端在启动时会根据JDBC URL判断数据源类型后续所有的方言加载、配置选择都以这个DBType为入口。加了枚举和判断之后Seata至少不再报“not support dbType”这类错误。这里有个编译上的连锁反应DBType加了新枚举项目里所有对DBType做switch的代码都会提示“未枚举分支”编译可能要报错。你需要全局搜索switch (dbType)和DBType.MYSQL出现的位置把DM8补进去大多数情况下直接复用MYSQL的分支即可。常见的位置包括rm-datasource模块下的SqlGeneratorFactory、UndoLogManagerFactory、TableMetaCacheFactory以及server模块下的存储工厂类。如果只改了common和rm-datasource一些依赖common的模块编译时也会影响到耐心一个个补上就行。4.2 改造点二DmDialect方言实现在Seata 1.6.1里rm-datasource模块有一个io.seata.rm.datasource.dialect包里面的MySqlDialect定义了MySQL这套数据库方言对应的各种组件实现。我们的思路是继承它覆盖需要差异化处理的组件。新建rm-datasource/src/main/java/io/seata/rm/datasource/dialect/DmDialect.javapackage io.seata.rm.datasource.dialect; import io.seata.common.loader.LoadLevel; import io.seata.rm.datasource.sql.struct.DmTableMetaCache; import io.seata.rm.datasource.undo.DmUndoLogManager; LoadLevel(name dm8) public class DmDialect extends MySqlDialect { Override public String getDefaultTableMetaCache() { return DmTableMetaCache.class.getName(); } Override public String getDefaultUndoLogManager() { return DmUndoLogManager.class.getName(); } }LoadLevel的name就是方言名称Seata的EnhancedServiceLoader在运行时按dbType加载这个类。SQL生成器、锁管理器这两个组件继续沿用MySQL的实现因为达梦在MySQL兼容模式下对这些SQL的兼容性足够。注意编译的时候Java文件里引用的DmTableMetaCache和DmUndoLogManager是我们后面要新建的两个类得先把类建好或者先写一个空壳再继续往下走。4.3 改造点三UndoLogManager的SQL与字段适配新建rm-datasource/src/main/java/io/seata/rm/datasource/undo/DmUndoLogManager.java参考同一目录下的MySQLUndoLogManager改。这个类管着undo_log表的增删查是AT模式二阶段回滚的直接执行者。核心方法改造如下。插入undo_log的方法注意时间函数替换LoadLevel(name dm8) public class DmUndoLogManager extends AbstractUndoLogManager { Override public String getDBType() { return DBType.DM8.name(); } Override protected void insertUndoLogWithNormal(Connection conn, UndoLogDO undoLog) throws SQLException { String sql INSERT INTO UNDO_LOG (BRANCH_ID, XID, CONTEXT, ROLLBACK_INFO, LOG_STATUS, LOG_CREATED, LOG_MODIFIED) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setLong(1, undoLog.getBranchId()); ps.setString(2, undoLog.getXid()); ps.setString(3, undoLog.getContext()); ps.setString(4, undoLog.getRollbackInfo()); ps.setInt(5, undoLog.getLogStatus()); ps.executeUpdate(); } } // 查询、删除等方法的实现参考MySQLUndoLogManagerSQL里把now()替换为CURRENT_TIMESTAMP }setString绑到CLOB列上实测达梦驱动是支持的不需要额外的类型转换。但查询的时候有坑MySQL版里读rollback_info用的是rs.getBytes(rollback_info)这在达梦上大概率拿不到东西或者直接抛异常。因为字段是CLOB类型要改成这样private String getRollbackInfo(ResultSet rs) throws SQLException { java.sql.Clob clob rs.getClob(ROLLBACK_INFO); if (clob null) { return null; } return clob.getSubString(1, (int) clob.length()); }这个改动是源码适配里最容易被忽略的地方。我第一次没改一阶段插入没问题二阶段回滚的时候一读CLOB就报“流已关闭”或者拿到null跟踪日志才发现是字段类型和读取方式不匹配。另外deleteUndoLogByLogCreated方法里MySQL版用了LIMIT做历史数据清理达梦MySQL兼容模式下同样支持LIMIT所以这条SQL可以保留原样。4.4 改造点四TableMetaCache元数据读取新建rm-datasource/src/main/java/io/seata/rm/datasource/sql/struct/DmTableMetaCache.java。这个类负责读取表结构Seata执行控制SQL、做镜像对比时都需要它。参考MySqlTableMetaCache大部分逻辑可以复用唯一要改的是getTableMeta里的元数据查询方式。MySQL版用connection.getMetaData().getColumns(catalog, null, tableName, %)这个catalog参数在达梦里传database名称或者不传都不会得到预期结果最稳的方式是通过达梦的系统视图查询。我这里给一个验证过的思路查询ALL_TAB_COLUMNS拿列信息查询ALL_CONSTRAINTS和ALL_CONS_COLUMNS拿主键信息再把这些结果映射成Seata的TableMeta对象。核心片段示意如下String schema connection.getSchema(); String sql SELECT COLUMN_NAME, DATA_TYPE, DATA_LENGTH, NULLABLE FROM ALL_TAB_COLUMNS WHERE OWNER ? AND TABLE_NAME ? ORDER BY COLUMN_ID; String pkSql SELECT CC.COLUMN_NAME FROM ALL_CONSTRAINTS C, ALL_CONS_COLUMNS CC WHERE C.OWNER ? AND C.TABLE_NAME ? AND C.CONSTRAINT_TYPE P AND C.CONSTRAINT_NAME CC.CONSTRAINT_NAME;拿到列名列表和主键列之后构造TableMeta的字段映射再设置自增列。注意达梦的表名、列名默认是大写返回结果也要按大写匹配如果你的业务表建表时用了带引号的小写这里就要额外处理大小写转换。4.5 编译打包与替换jar包的顺序改动完成后不用把整个Seata源码全量重新打包只需要编译common和rm-datasource两个模块然后找到对应的jar替换到客户端项目里。在Seata源码根目录执行mvn -pl common,rm-datasource -am clean install -DskipTests构建成功后到rm-datasource/target/目录下找seata-rm-datasource-1.6.1.jar到common/target/下找seata-common-1.6.1.jar。你的客户端应用如果用的是seata-all或者seata-spring-boot-starter这类聚合依赖可能还要同时检查seata-core、seata-tm这些包的编译产物是否变化。替换jar包有个容易犯的错误只替换了服务端的Seata客户端没替换结果客户端启动时用的还是官方原版代码新增的DBType.DM8根本没进去。Seata AT模式的RM端运行在业务应用进程里所以业务应用依赖的所有seata相关jar都必须是改造后重新编译的版本。最好的做法是先把改好的模块mvn install到本地Maven仓库然后客户端项目的pom.xml里直接引用本地仓库里的1.6.1版本版本号可以保持一样但要确保本地Maven仓库里的jar确实是改造过的。5. 客户端接入Spring Boot Seata Client跑通下单回滚5.1 依赖与配置数据源代理、注册中心、事务分组demo我做了两个服务order-service和inventory-service。各自连接达梦上不同的用户这样能真实模拟跨库的分布式事务。order-service的application.yml核心配置如下spring: datasource: url: jdbc:dm://127.0.0.1:5236?schemaORDER_USER username: ORDER_USER password: order_pass driver-class-name: dm.jdbc.driver.DmDriver seata: application-id: order-service tx-service-group: my_test_tx_group service: vgroup-mapping: my_test_tx_group: default grouplist: default: 127.0.0.1:8091 enable-auto-data-source-proxy: true这里有几个容易搞错的地方。一是jdbc:dm://127.0.0.1:5236后面可以跟?schemaORDER_USER来指定默认schema也可以不指定由驱动根据用户名推算。但建议显式写上避免连到SYSDBA的schema下。二是tx-service-group这个事务分组名客户端和Seata Server端的配置必须一致。vgroup-mapping: my_test_tx_group: default表示把事务分组映射到TC的集群名defaultgrouplist则告诉客户端TC的地址。如果后面排查发现客户端一直连不上TC优先检查这两个配置。inventory-service的配置类似数据源换成INVENTORY_USER。两个服务都要引入改造后编译的Seata依赖确保数据源代理生效。5.2 demo代码GlobalTransactional下的一次完整回滚order-service的Service层代码核心就一个注解Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private InventoryClient inventoryClient; GlobalTransactional(name create-order, rollbackFor Exception.class) public void createOrder(OrderDTO dto) { Order order new Order(); order.setOrderNo(dto.getOrderNo()); order.setProductId(dto.getProductId()); order.setQuantity(dto.getQuantity()); orderMapper.insert(order); inventoryClient.deduct(dto.getProductId(), dto.getQuantity()); } }inventory-service的deduct方法里我故意在写入库存表之后抛出异常模拟“库存已经扣减但下游处理失败”的场景public void deduct(String productId, int quantity) { inventoryMapper.deduct(productId, quantity); // 模拟业务失败 throw new RuntimeException(inventory deduct failed); }这里的关键在于GlobalTransactional开启的是全局事务inventory-service的本地事务即使自己提交了如果最终整个全局事务失败TC也会根据undo_log去回滚。实际跑下来订单表的INSERT和库存表的UPDATE都会被回滚库存数量恢复原值。5.3 验证日志里的分支提交与回滚痕迹跑完一次失败用例之后看两个服务的日志。正常一阶段日志会类似UndoLogManager insert undo log, xid... branchId... Branch register success, xid... branchId...随后inventory-service抛异常全局事务进入二阶段回滚order-service日志里出现Branch rollback success, xid... branchId...同时到达梦的INVENTORY库查UNDO_LOG表里面会留下对应的回滚记录。全局事务提交成功的场景下二阶段会执行delete undo_logUNDO_LOG里查不到记录全局事务失败的场景下undo_log记录会被标记状态并保留一段时间方便排查。6. 避坑实录从报错到跑通的完整排查链路6.1 UNDO_LOG表DDL直接抄MySQL版的后果这个坑几乎每个从MySQL迁到达梦的人都会踩。Seata官方文档里MySQL建表语句的rollback_info字段是BLOB很多同事直接把这套DDL拿到达梦上执行表面没问题但真正跑全局事务的时候插入undo_log会报错或者数据写入异常。原因是BLOB在达梦里存二进制数据而Seata代码里生成的rollback_info本质是JSON字符串通过JDBC的setString去写入BLOB字段部分版本的达梦驱动会告诉你“列类型不匹配”。换成CLOB之后整个链路就顺畅了。同样的问题也会出现在context字段上不过官方DDL里context本来就是VARCHAR不需要额外处理。6.2 大小写不一致导致列找不到第二个坑和达梦的大小写规则有关。我们的建表脚本一开始是从一个在MySQL上跑得飞起的项目直接复制过来的表名带了反引号执行到达梦之后数据字典里记录的表名是带引号的小写形式。Seata的DmTableMetaCache在查询ALL_TAB_COLUMNS时传入ORDER_INFO结果查不到任何列日志里报“Failed to get table meta”。这个问题的排查链路比较长先从TableMetaCache下手打印出它实际查询的schema和tableName再去数据库里查ALL_TAB_OBJECTS对比大小写最后才发现是建表时的引号问题。解决方案就是前面说的建表时全部用不带引号的大写标识符然后Seata侧查询时也统一传大写。如果你有很多历史表已经是带引号的小写那就得在DmTableMetaCache里加一个大小写转换逻辑把传入的表名统一按照数据字典里的实际值做一次匹配这个改动比较繁琐不如直接规范建表省事。6.3 全局锁等待超时undo_log的记录没被删除AT模式的全局锁实际上是用数据库自身的行锁去实现。一阶段本地事务提交前Seata会对受影响的行执行SELECT ... FOR UPDATE拿到这个行的锁之后业务SQL才放行。如果某个分支事务执行成功但TC迟迟没有收到二阶段提交指令或者分支事务的处理线程挂了那这些行锁就会一直占着后续所有操作同一行的全局事务都会卡在全局锁等待上日志里会出现“Global lock wait timeout”之类的关键字。排查这种问题先看是不是某个分支事务的本地事务没有正常提交。一个常见的原因是连接池配置里开启了自动提交导致业务SQL和undo_log插入不在同一个本地事务里一阶段提交时序乱了全局锁持有时间变长。另外如果全局事务失败后undo_log记录没有被清理也会出现越积越多的情况。正常情况下回滚完成后undo_log记录会保留一条状态为“已回滚”的数据如果连这条记录都没有说明二阶段的rollback SQL根本没执行成功。这时候要重点看回滚日志里是不是有针对CLOB读取的异常大概率就是4.3节说的rs.getBytes那个坑。6.4 自增主键关闭ID字段必须显式IDENTITY还有一个比较隐蔽的问题达梦对自增列的管理和MySQL不完全一样。MySQL里只要字段定义了AUTO_INCREMENT插入时不传该字段就能自动生成达梦的MySQL兼容模式下AUTO_INCREMENT大部分时候也可以用但有些版本或者某些特殊配置下如果不给ID字段传值插入会报“不能为NULL”或者“违反非空约束”。我在测试时遇到过这样的情况业务表建表时用了INT PRIMARY KEY没加IDENTITYSeata生成INSERT语句时没有包含ID字段达梦直接报错。后来把ID列的DDL改成BIGINT IDENTITY(1,1) PRIMARY KEY问题就消失了。如果你在建表时确实不想用自增那业务代码里必须手动给ID赋值否则Seata的insert语句不会帮你生成主键。6.5 关于TC端存储的国产化补充建议最后说一下TC端存储。前面的部署用的是file存储单机验证没问题但生产环境一般会改成DB存储或者Redis存储。DB存储的好处是TC重启后事务记录不丢支持集群。如果项目要求全链路不出现其他商业数据库想把TC的事务日志也放到达梦上这个思路可行但改造成本会明显上升。TC端DB存储用的是server模块里的DAO层里面有大量SQL是针对MySQL、Oracle这些数据库写的达梦兼容MySQL模式下可以尝试把store.db.db-type配置成mysql去走MySQL那套SQL部分版本可能跑得通但稳定性需要压测。更稳妥的方案是TC用Redis存储事务日志Redis是国产化项目里允许使用的中间件这样避免了再次改源码又能满足高可用。我当时为了控制风险最终生产环境就是用Redis存储业务库全部用达梦整体链路跑了一年多没有出过问题。从一个坑一个坑踩过来回头看Seata AT模式适配达梦DM8本质上不是多复杂的深水区核心就四件事让JDBC URL识别达梦、让方言加载器认识dm8、让undo_log的读写符合达梦字段特性、让元数据读取查询达梦的系统表。把这四件事做完分布式事务在达梦上就能跟MySQL一样顺畅运转。如果你也正在做类似的事情按着这篇文章的顺序来能少走不少弯路。
返回列表