SpringBoot与Eventuate Tram整合 - 实现转账最终一致性系统

Eventuate Tram 是一个用于构建微服务架构的开源框架,提供事件驱动的消息传递和最终一致性保证,帮助企业高效地管理和协调分布式系统中的复杂业务逻辑。

为什么选择Eventuate Tram?

解耦和服务独立性:转账系统通常涉及多个服务(如账户服务、转账服务等)。Eventuate Tram 提供了一种事件驱动的方式来解耦这些服务,使得每个服务可以独立开发、部署和扩展。

灵活性:随着业务的发展,新的服务可能会被引入或现有服务需要重构。Eventuate Tram 的事件驱动模型允许这种灵活的变化而不需要大规模的重构。

分布式事务管理:传统的两阶段提交(2PC)在高并发环境下性能较差且复杂度高。Eventuate Tram 通过事件溯源和补偿机制实现了最终一致性,确保即使在分布式环境中也能保持数据的一致性。

幂等性和重试机制:Eventuate Tram 支持幂等处理和自动重试,确保消息传递的可靠性,防止重复处理导致的数据不一致问题。

异步通信:Eventuate Tram 使用事件总线进行异步通信,提高了系统的吞吐量和响应速度。这对于实时性强的应用场景尤为重要。

事件存储:Eventuate Tram 提供了内置的事件存储机制,记录所有发生的业务事件。这不仅有助于审计和调试,还能在系统故障后快速恢复状态。

多种消息代理支持:Eventuate Tram 支持多种消息代理(如 RabbitMQ、Kafka 等),可以根据现有的基础设施进行选择和集成。

代码生成工具:Eventuate 提供了一些代码生成工具和模板,帮助开发者快速搭建项目结构,减少了样板代码的数量。

代码实操

创建account-service

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>account-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.4</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.eventuate.tram</groupId>
            <artifactId>eventuate-tram-spring-jdbc</artifactId>
            <version>0.26.0.RELEASE</version><!-- Eventuate Tram JDBC支持 -->
        </dependency>
        <dependency>
            <groupId>io.eventuate.tram</groupId>
            <artifactId>eventuate-tram-messaging-rabbitmq</artifactId>
            <version>0.26.0.RELEASE</version><!-- Eventuate Tram RabbitMQ消息代理支持 -->
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId><!-- JPA数据访问支持 -->
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.properties

server.port=8081 # 应用监听端口

# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/accounts?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# Hibernate自动建表策略
spring.jpa.hibernate.ddl-auto=update

# RabbitMQ配置
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

Account.java

package com.example.accountservice;

import io.eventuate.tram.events.publisher.DomainEventPublisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.Collections;

@Service
public class AccountService {

    @Autowired
    private AccountRepository accountRepository; // 账户仓库,用于数据库操作

    @Autowired
    private DomainEventPublisher domainEventPublisher; // 域事件发布器,用于发布事件

    /**
     * 减少账户余额
     * @param accountId 账户ID
     * @param amount 需要减少的金额
     */
    @Transactional
    public void debit(String accountId, double amount) {
        Account account = accountRepository.findById(accountId).orElseThrow(() -> new RuntimeException("Account not found")); // 根据账户ID查找账户,如果找不到则抛出异常
        if (account.getBalance() < amount) { // 检查账户余额是否足够
            thrownew RuntimeException("Insufficient balance"); // 如果余额不足,则抛出异常
        }
        account.setBalance(account.getBalance() - amount); // 减少账户余额
        accountRepository.save(account); // 保存账户信息到数据库
        domainEventPublisher.publish(Account.class, account.getId(), Collections.singletonList(new AccountDebitedEvent(amount))); // 发布账户被借记的事件
    }

    /**
     * 增加账户余额
     * @param accountId 账户ID
     * @param amount 需要增加的金额
     */
    @Transactional
    public void credit(String accountId, double amount) {
        Account account = accountRepository.findById(accountId).orElseThrow(() -> new RuntimeException("Account not found")); // 根据账户ID查找账户,如果找不到则抛出异常
        account.setBalance(account.getBalance() + amount); // 增加账户余额
        accountRepository.save(account); // 保存账户信息到数据库
        domainEventPublisher.publish(Account.class, account.getId(), Collections.singletonList(new AccountCreditedEvent(amount))); // 发布账户被贷记的事件
    }
}

AccountRepository.java

package com.example.accountservice;

import org.springframework.data.jpa.repository.JpaRepository;

/**
 * 账户仓库接口,继承自JpaRepository,用于对Account实体进行CRUD操作
 */
public interface AccountRepository extends JpaRepository<Account, String> {}

AccountServiceApplication.java

package com.example.accountservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Spring Boot应用启动类
 */
@SpringBootApplication
public class AccountServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(AccountServiceApplication.class, args); // 启动Spring Boot应用
    }
}

AccountController.java

package com.example.accountservice.controller;

import com.example.accountservice.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * 控制器类,处理HTTP请求
 */
@RestController
@RequestMapping("/accounts")
public class AccountController {

    @Autowired
    private AccountService accountService; // 注入AccountService

    /**
     * 处理借记账户的HTTP POST请求
     * @param accountId 账户ID
     * @param amount 借记金额
     */
    @PostMapping("/{accountId}/debit/{amount}")
    public void debit(@PathVariable String accountId, @PathVariable double amount) {
        accountService.debit(accountId, amount); // 调用AccountService的debit方法
    }

    /**
     * 处理贷记账户的HTTP POST请求
     * @param accountId 账户ID
     * @param amount 贷记金额
     */
    @PostMapping("/{accountId}/credit/{amount}")
    public void credit(@PathVariable String accountId, @PathVariable double amount) {
        accountService.credit(accountId, amount); // 调用AccountService的credit方法
    }
}

AccountDebitedEvent.java

package com.example.accountservice.event;

/**
 * 账户借记事件类
 */
public class AccountDebitedEvent {
    private double amount; // 借记金额

    public AccountDebitedEvent(double amount) {
        this.amount = amount; // 构造函数初始化借记金额
    }

    // 获取借记金额的方法
    public double getAmount() {
        return amount;
    }

    // 设置借记金额的方法
    public void setAmount(double amount) {
        this.amount = amount;
    }
}

AccountCreditedEvent.java

/**
 * 账户贷记事件类
 */
public class AccountCreditedEvent {
    private double amount; // 贷记金额

    public AccountCreditedEvent(double amount) {
        this.amount = amount; // 构造函数初始化贷记金额
    }

    // 获取贷记金额的方法
    public double getAmount() {
        return amount;
    }

    // 设置贷记金额的方法
    public void setAmount(double amount) {
        this.amount = amount;
    }
}

TransferEventHandler.java

package com.example.accountservice.handler;

import com.example.accountservice.event.TransferMadeEvent;
import com.example.accountservice.AccountService;
import io.eventuate.tram.events.subscriber.DomainEventEnvelope;
import io.eventuate.tram.events.subscriber.EventHandlerMethod;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 事件处理器类,处理转账完成事件
 */
@Component
public class TransferEventHandler {

    @Autowired
    private AccountService accountService; // 注入AccountService

    /**
     * 处理转账完成事件的方法
     * @param event 包含转账完成事件的对象
     */
    @EventHandlerMethod
    public void handle(DomainEventEnvelope<TransferMadeEvent> event) {
        TransferMadeEvent transferMadeEvent = event.getEvent(); // 获取转账完成事件对象
        accountService.credit(transferMadeEvent.getCreditAccountId(), transferMadeEvent.getAmount()); // 调用AccountService的credit方法,增加目标账户的余额
    }
}

创建transfer-service

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>transfer-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.4</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.eventuate.tram</groupId>
            <artifactId>eventuate-tram-spring-jdbc</artifactId>
            <version>0.26.0.RELEASE</version><!-- Eventuate Tram JDBC支持 -->
        </dependency>
        <dependency>
            <groupId>io.eventuate.tram</groupId>
            <artifactId>eventuate-tram-messaging-rabbitmq</artifactId>
            <version>0.26.0.RELEASE</version><!-- Eventuate Tram RabbitMQ消息代理支持 -->
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.properties

server.port=8082

# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/transfers?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# Hibernate自动建表策略
spring.jpa.hibernate.ddl-auto=update

# RabbitMQ配置
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

Transfer.java

package com.example.transferservice;

import io.eventuate.tram.events.publisher.DomainEventPublisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.Collections;

/**
 * 转账服务类
 */
@Service
public class TransferService {

    @Autowired
    private TransferRepository transferRepository; // 转账仓库,用于数据库操作

    @Autowired
    private DomainEventPublisher domainEventPublisher; // 域事件发布器,用于发布事件

    /**
     * 执行转账操作
     * @param cmd 包含转账命令的对象
     */
    @Transactional
    public void makeTransfer(MakeTransferCommand cmd) {
        Transfer transfer = new Transfer(cmd.getSourceAccountId(), cmd.getTargetAccountId(), cmd.getAmount()); // 创建转账记录
        transferRepository.save(transfer); // 保存转账记录到数据库
        domainEventPublisher.publish(Transfer.class, transfer.getId(),
                Collections.singletonList(new TransferMadeEvent(cmd.getSourceAccountId(), cmd.getTargetAccountId(), cmd.getAmount()))); // 发布转账完成事件
    }
}

TransferRepository.java

package com.example.transferservice;

import org.springframework.data.jpa.repository.JpaRepository;

/**
 * 转账仓库接口,继承自JpaRepository,用于对Transfer实体进行CRUD操作
 */
public interface TransferRepository extends JpaRepository<Transfer, String> {}

TransferServiceApplication.java

package com.example.transferservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Spring Boot应用启动类
 */
@SpringBootApplication
public class TransferServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(TransferServiceApplication.class, args); // 启动Spring Boot应用
    }
}

TransferController.java

package com.example.transferservice.controller;

import com.example.transferservice.MakeTransferCommand;
import com.example.transferservice.TransferService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 控制器类,处理HTTP请求
 */
@RestController
@RequestMapping("/transfers")
public class TransferController {

    @Autowired
    private TransferService transferService; // 注入TransferService

    /**
     * 处理转账的HTTP POST请求
     * @param cmd 包含转账命令的对象
     */
    @PostMapping
    public void makeTransfer(@RequestBody MakeTransferCommand cmd) {
        transferService.makeTransfer(cmd); // 调用TransferService的makeTransfer方法执行转账操作
    }
}

MakeTransferCommand.java

package com.example.transferservice.command;

/**
 * 转账命令类,包含转账所需的信息
 */
public class MakeTransferCommand {
    private String sourceAccountId; // 源账户ID
    private String targetAccountId; // 目标账户ID
    private double amount; // 转账金额

    public MakeTransferCommand(String sourceAccountId, String targetAccountId, double amount) {
        this.sourceAccountId = sourceAccountId; // 初始化源账户ID
        this.targetAccountId = targetAccountId; // 初始化目标账户ID
        this.amount = amount; // 初始化转账金额
    }

    // 获取源账户ID的方法
    public String getSourceAccountId() {
        return sourceAccountId;
    }

    // 设置源账户ID的方法
    public void setSourceAccountId(String sourceAccountId) {
        this.sourceAccountId = sourceAccountId;
    }

    // 获取目标账户ID的方法
    public String getTargetAccountId() {
        return targetAccountId;
    }

    // 设置目标账户ID的方法
    public void setTargetAccountId(String targetAccountId) {
        this.targetAccountId = targetAccountId;
    }

    // 获取转账金额的方法
    public double getAmount() {
        return amount;
    }

    // 设置转账金额的方法
    public void setAmount(double amount) {
        this.amount = amount;
    }
}

TransferMadeEvent.java

package com.example.transferservice.event;

/**
 * 转账完成事件类
 */
public class TransferMadeEvent {
    private String debitAccountId; // 借记账户ID
    private String creditAccountId; // 贷记账户ID
    private double amount; // 转账金额

    public TransferMadeEvent(String debitAccountId, String creditAccountId, double amount) {
        this.debitAccountId = debitAccountId; // 初始化借记账户ID
        this.creditAccountId = creditAccountId; // 初始化贷记账户ID
        this.amount = amount; // 初始化转账金额
    }

    // 获取借记账户ID的方法
    public String getDebitAccountId() {
        return debitAccountId;
    }

    // 设置借记账户ID的方法
    public void setDebitAccountId(String debitAccountId) {
        this.debitAccountId = debitAccountId;
    }

    // 获取贷记账户ID的方法
    public String getCreditAccountId() {
        return creditAccountId;
    }

    // 设置贷记账户ID的方法
    public void setCreditAccountId(String creditAccountId) {
        this.creditAccountId = creditAccountId;
    }

    // 获取转账金额的方法
    public double getAmount() {
        return amount;
    }

    // 设置转账金额的方法
    public void setAmount(double amount) {
        this.amount = amount;
    }
}

AccountEventHandler.java

package com.example.transferservice.handler;

import com.example.transferservice.event.AccountDebitedEvent;
import com.example.transferservice.TransferService;
import io.eventuate.tram.events.subscriber.DomainEventEnvelope;
import io.eventuate.tram.events.subscriber.EventHandlerMethod;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 事件处理器类,处理账户借记事件
 */
@Component
public class AccountEventHandler {

    @Autowired
    private TransferService transferService; // 注入TransferService

    /**
     * 处理账户借记事件的方法
     * @param event 包含账户借记事件的对象
     */
    @EventHandlerMethod
    public void handle(DomainEventEnvelope<AccountDebitedEvent> event) {
        AccountDebitedEvent accountDebitedEvent = event.getEvent(); // 获取账户借记事件对象
        // 这里可以添加额外的逻辑,当账户被借记时执行的操作
    }
}

测试

curl -X POST http://localhost:8082/transfers -H "Content-Type: application/json" -d '{"sourceAccountId": "account1", "targetAccountId": "account2", "amount": 150}'

测试结果
无返回内容,说明操作成功。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小马不敲代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值