- 示例工程
【免费下载链接】spring-data-examples
Spring Data Example Projects
本篇基于spring-data-examples仓库中的 couchbase/transactions 示例,完整讲解如何在 Spring Data Couchbase 中启用并使用 Couchbase 原生的 ACID 事务:从用 Docker 搭建本地 Couchbase 集群、配置TransactionsConfig,到通过@Transactional注解在命令式(Template/Repository)与响应式(Reactive)两种编程模型下完成“跨文档转账”操作,并验证异常抛出后的自动回滚行为。读完本文,你将掌握事务配置的关键参数、事务边界划分的写法,以及事务失败时异常包装(TransactionSystemUnambiguousException)的识别方式。
示例概述:一个“航司闸门转移”的事务 Demo
couchbase/transactions是 Spring Data Couchbase 官方示例集中专门演示**事务(Transactions)**能力的子项目(见 couchbase/pom.xml 中的<module>transactions</module>声明)。它的业务场景非常贴近数据库事务的经典案例——账户间转账:
- 初始化两条
AirlineGates文档,各持有 200 个gates; - 在事务内将 50 个 gates 从航司 A 转移到航司 B(成功提交);
- 再次在事务内转移 44 个 gates,但中途抛出异常(应整体回滚);
- 最后用响应式(Reactive)方式再转移 44 个 gates(成功提交)。
整个流程由应用启动后自动执行,并在每一步用Assert.isTrue校验文档中的gates数值是否符合预期,等价于一组“自带断言”的集成测试。子项目的模块坐标定义在 couchbase/transactions/pom.xml:
<artifactId>spring-data-couchbase-transactions</artifactId> <name>Spring Data Couchbase - Transaction example</name> <description>Transactions Demo project for Spring Data Couchbase</description>源码结构如下(位于couchbase/transactions/src/main/java/com/example/demo/):
| 文件 | 职责 |
|---|---|
| DemoApplication.java | Spring Boot 启动入口(@SpringBootApplication) |
| Config.java | Couchbase 连接与事务环境配置 |
| AirlineGates.java | @Document实体 |
| AirlineGatesRepository.java | Repository 接口 |
| AirlineGatesService.java | 事务边界所在的服务层 |
| CmdRunner.java | 启动后自动执行的演示驱动 |
环境搭建:用 Docker 运行 Couchbase
按照 couchbase/transactions/README.md 的 “Running the Sample” 章节,本地准备一个 Couchbase 服务需要三步:
第一步:启动容器
$> docker run -d --name db -p 8091-8097:8091-8097 -p 11210:11210 -p 11207:11207 -p 18091-18095:18091-18095 -p 18096:18096 -p 18097:18097 couchbase各端口段的含义:8091-8097为 Web 管理界面与节点间通信端口,11210是 XDCR 端口,11207是 KV/HTTP 直连端口,18091-18097是集群(Cluster)相关端口。由于示例连接串走的是127.0.0.1,这些端口映射是跑通示例的前提。
第二步:创建集群
浏览器访问http://localhost:8091/,按向导创建一个新集群。
第三步:创建 bucket
进入管理界面(http://localhost:8091/ui/index.html#/buckets),创建一个名为travel-sample的 bucket——这个名称必须与下文Config#getBucketName()的返回值一致。
另外要注意一点:示例 Config.java 中硬编码的用户名/密码为Administrator/password,因此本地集群应使用该凭据初始化(或按需修改配置类)。
连接与事务配置:Config 类逐行解析
事务示例的核心配置在 Config.java:
@Configuration @EnableCouchbaseRepositories({ "com.example.demo" }) @EnableTransactionManagement public class Config extends AbstractCouchbaseConfiguration { @Override public String getConnectionString() { return "127.0.0.1"; } @Override public String getUserName() { return "Administrator"; } @Override public String getPassword() { return "password"; } @Override public String getBucketName() { return "travel-sample"; } @Override public void configureEnvironment(ClusterEnvironment.Builder builder) { builder.transactionsConfig(TransactionsConfig.durabilityLevel(DurabilityLevel.NONE)); } }这里有两个要点:
- 事务管理的开启。类上的
@EnableTransactionManagement是 Spring Data Couchbase 事务支持的前提——它启用@Transactional注解驱动的代理机制,使服务类方法中的持久化操作在 Couchbase 事务上下文中执行。 TransactionsConfig与持久性级别。configureEnvironment钩子允许直接向 Couchbase Java SDK 的ClusterEnvironment.Builder传递客户端级配置。示例中通过TransactionsConfig.durabilityLevel(DurabilityLevel.NONE)将事务写入的持久性级别设为NONE,即不要求事务的写入复制到任何副本节点即视为成功。这一设置能换取更高的写入吞吐(适合演示场景),但对数据持久性的保证弱于默认的MAJORITY(多数副本持久化)级别;若从源码结构看生产环境的需求,应把该参数调整为DurabilityLevel.MAJORITY。
@EnableCouchbaseRepositories({ "com.example.demo" })则声明了在com.example.demo包下扫描 Repository 接口。
实体与 Repository:@Document、@Version 与 DynamicProxyable
实体类 AirlineGates.java 展示了事务场景下典型的文档映射:
@Document public class AirlineGates { @Id String id; @Version Long version; @QueryIndexed String name; String iata; Long gates; public AirlineGates(String id, String name, String iata, Long gates) { this.id = id; this.name = name; this.iata = iata; this.gates = gates; } // toString() 省略 }@Document将其注册为 Couchbase 文档实体;@Id指定文档 ID 来源字段;@Version映射 Couchbase 的文档版本号,支撑乐观并发控制;@QueryIndexed表示该字段会被纳入 N1QL 查询所需的二级索引。
Repository 接口见 AirlineGatesRepository.java:
public interface AirlineGatesRepository extends CouchbaseRepository<AirlineGates, String>, DynamicProxyable<AirlineGatesRepository> { }除了标准的CouchbaseRepository<AirlineGates, String>(提供findById、save等 CRUD 能力),它还继承了DynamicProxyable<AirlineGatesRepository>。从源码结构看,该标记接口用于让 Spring Data Couchbase 为 Repository 生成可动态增强的代理,使 Repository 方法也能纳入@Transactional的事务边界管理——这正是示例中transferGatesRepo方法能通过 Repository 完成事务操作的基础。
事务边界:服务层的三种调用方式
所有“转账”逻辑都集中在 AirlineGatesService.java。服务类同时注入了命令式CouchbaseTemplate、响应式ReactiveCouchbaseTemplate(通过template.reactive()派生)以及 Repository:
@Service public class AirlineGatesService { CouchbaseTemplate template; ReactiveCouchbaseTemplate reactiveTemplate; AirlineGatesRepository airlineGatesRepository; public AirlineGatesService(CouchbaseTemplate template, AirlineGatesRepository airlineGatesRepository) { this.template = template; this.reactiveTemplate = template.reactive(); this.airlineGatesRepository = airlineGatesRepository; } // ... }方式一:CouchbaseTemplate + @Transactional
transferGates 方法演示了最经典的事务用法:
@Transactional public void transferGates(String fromId, String toId, int gatesToTransfer, RuntimeException exceptionToThrow) { AirlineGates fromAirlineGates = template.findById(AirlineGates.class).one(fromId); AirlineGates toAirlineGates = template.findById(AirlineGates.class).one(toId); toAirlineGates.gates += gatesToTransfer; fromAirlineGates.gates -= gatesToTransfer; template.save(fromAirlineGates); if (exceptionToThrow != null) { throw exceptionToThrow; } template.save(toAirlineGates); }方法上的@Transactional使 Spring 的代理在执行方法体前开启 Couchbase 事务:方法内的findById与save都会参与同一事务;方法正常结束则提交,抛出未捕获的RuntimeException则回滚。注意exceptionToThrow参数是人为注入的“故障模拟”——先保存了fromAirlineGates、后保存toAirlineGates的中间状态,若中途抛异常,事务保证已执行的写操作不会部分生效。
方式二:Repository + @Transactional
transferGatesRepo 与上面的逻辑完全相同,只是把template.save/findById换成了airlineGatesRepository.save/findById。它验证了:事务边界不依赖于持久化访问方式——无论走 Template 还是 Repository,只要在@Transactional代理方法内执行,就参与同一 Couchbase 事务。
方式三:响应式(Reactive)+ @Transactional
transferGatesReactive 展示了响应式编程模型下的事务写法:
// The @Transactional annotation results in the method of the proxy for the service executing this in a transaction @Transactional public Mono<Void> transferGatesReactive(String fromId, String toId, int gatesToTransfer, RuntimeException exceptionToThrow) { return Mono.deferContextual(ctx -> { AirlineGates fromAirlineGates = template.findById(AirlineGates.class).one(fromId); AirlineGates toAirlineGates = template.findById(AirlineGates.class).one(toId); toAirlineGates.gates += gatesToTransfer; fromAirlineGates.gates -= gatesToTransfer; template.save(fromAirlineGates); if (exceptionToThrow != null) { throw exceptionToThrow; } return reactiveTemplate.save(toAirlineGates).then(); }); }源码注释点明了关键机制:@Transactional使代理执行该方法时就建立事务上下文,方法体返回的Mono<Void>内的持久化操作(包括命令式template.save与响应式reactiveTemplate.save)都在同一事务中进行。Mono.deferContextual用于延迟执行并在订阅时读取 Reactor 上下文——这与事务传播依赖当前线程/上下文绑定事务语义的需求相配合。
非事务方法的对照
服务类末尾还定义了save、findById、saveRepo、findByIdRepo四个不带@Transactional的便捷方法(见 AirlineGatesService.java),源码逐一注释 “This does not have the @Transactional annotation therefore is not executed in a transaction”。它们供CmdRunner在事务之外做数据初始化和结果读取,与事务内操作形成清晰对照:事务边界完全由注解声明,而非由持久化操作本身决定。
启动自动执行与回滚验证:CmdRunner 演示脚本
CmdRunner.java 是一个@Component且实现CommandLineRunner的类:应用启动完成后 Spring Boot 会自动调用其run方法(源码注释说明:“Components of the type CommandLineRunner are called right after the application start up”),无需手动触发。其执行流程(CmdRunner.java)分为四个阶段:
阶段 1:清理与初始化。先removeById删除 id 为"1"、"2"的旧文档(忽略异常),再通过非事务的service.save写入两条gates = 200的AirlineGates,并用非事务的service.findById回读打印初始状态。
阶段 2:成功的事务转账。调用transferGates("1", "2", 50, null),将 50 个 gates 从航司 1 转到航司 2。随后回读并用 Spring 的Assert.isTrue断言:
Assert.isTrue(found_b_1.getGates().equals(found_a_1.getGates() - gatesToTransfer), "should have transferred"); Assert.isTrue(found_b_2.getGates().equals(found_a_1.getGates() + gatesToTransfer), "should have transferred");阶段 3:失败回滚的事务转账。再次调用transferGates(..., 44, new SimulateErrorException())——中途抛出内部类SimulateErrorException(CmdRunner.java)。调用方捕获异常并做了如下甄别:
} catch (RuntimeException rte) { if (!(rte instanceof TransactionSystemUnambiguousException) && rte != null && rte.getCause() instanceof SimulateErrorException) { throw rte; } System.err.println(" got exception " + rte); }这里体现了 Spring Data Couchbase 事务异常处理的一个细节:业务异常抛出后,底层事务管理器会执行回滚并抛出org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException(一种不可恢复的事务系统异常)——捕获分支专门识别这一类型,说明“事务回滚引发的系统级异常”与“业务原始异常”都会被统一处理。回滚后回读两份文档,断言其gates与阶段 2 结束时完全一致(Assert.isTrue(found_c_1.getGates().equals(found_b_1.getGates()), "should be same as previous")),从而验证了中途失败时没有任何部分写入被持久化。
阶段 4:响应式转账。以.block()方式触发transferGatesReactive(..., 44, null),再回读断言 44 个 gates 转移成功,验证响应式路径下事务同样生效。
整个脚本通过System.err打印每一阶段的分隔线与文档状态,运行后即可在控制台完整观察到“提交—回滚—再提交”的全过程。
测试与构建运行
示例自带一个轻量集成测试 DemoApplicationTests.java:
@SpringBootTest(classes = DemoApplication.class) @EnabledOnCouchbaseAvailable class DemoApplicationTests { @Test void contextLoads() {} }@EnabledOnCouchbaseAvailable注解定义在示例仓库的公共工具模块 EnabledOnCouchbaseAvailable.java(example.springdata.couchbase.util包):它是一个 JUnit 5 条件注解,仅当本机localhost:8091上的 Couchbase 可达时才启用测试——没有本地服务时测试会被自动跳过,而不是失败。子项目 pom.xml 正是通过依赖spring-data-couchbase-example-utils获得该注解,并使用spring-boot-maven-plugin打包。
构建与运行方式(父模块 couchbase/pom.xml 已继承spring-boot-starter-data-couchbase,仓库根 pom.xml 基于 Spring Boot4.0.4、Java 25 编译):
# 在仓库根目录,仅构建 Couchbase 示例模块及其依赖 ./mvnw -pl couchbase/transactions -am package # 或进入子目录直接运行(需先完成 Docker 环境与 bucket 准备) cd couchbase/transactions mvn spring-boot:run运行后观察控制台输出:应能看到 “initialized airlines”、两次transferGates的前后文档状态、失败一次的异常打印,以及最终断言全部通过(无AssertionError抛出)。
小结与参考
这个示例用极小的代码面覆盖了 Spring Data Couchbase 事务支持的全部关键知识点:
- 配置面:
@EnableTransactionManagement+AbstractCouchbaseConfiguration.configureEnvironment中通过TransactionsConfig.durabilityLevel(...)调节事务持久性级别(示例取DurabilityLevel.NONE,生产慎用); - 事务边界:
@Transactional加在服务层代理方法上,Template 与 Repository 两种访问方式均可参与事务; - 响应式支持:
@Transactional+ 返回Mono的方法体在代理执行时建立事务上下文; - 回滚语义:中途抛
RuntimeException时整体回滚,并以TransactionSystemUnambiguousException标识事务系统层面的不可恢复异常。
原文档 couchbase/transactions/README.md 中列出的参考章节(Maven 官方文档、Spring Boot Maven Plugin Reference Guide、OCI 镜像构建、Spring Data Couchbase 的 Spring Boot 特性说明)指向的均为 Spring 官方文档站,建议结合这些文档与本文所引用的源码文件进一步深入。
- 示例工程
【免费下载链接】spring-data-examples
Spring Data Example Projects
相关推荐
Spring Data Couchbase终极指南:从入门到精通文档数据库与事务处理
Spring Data Couchbase终极指南:从入门到精通文档数据库与事务处理 Spring Data Couchbase 是 Spring 生态系统中用
示例工程Orleans 银行账户 ACID 事务示例实战:从接口标注到跨 Grain 转账实现
Orleans 银行账户 ACID 事务示例实战:从接口标注到跨 Grain 转账实现 本篇文章基于 Orleans 官方仓库中的 samples/BankAc
后端微服务从零做一只桌面宠物:VPet 完整上手
从零做一只桌面宠物:VPet 完整上手 下班后,屏幕右下角那只小家伙自己在蹲着玩,鼠标凑到头上方它还会配合地让你摸,饿了会讨食。这就是桌面宠物该有的样子。VPe
桌面应用游戏开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考