好的,我将为你详细讲解分布式事务在 Java Web 项目中的实践,并提供一个完整的基于 Seata AT 模式 和 RocketMQ 事务消息 的代码示例。
方案一:使用 Seata AT 模式(无侵入方案)
环境准备
- 下载 Seata Server:
wget https://github.com/seata/seata/releases/download/v1.5.2/seata-server-1.5.2.zip
unzip seata-server-1.5.2.zip
- 修改配置文件 (
conf/application.yml
):
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
数据库准备
每个服务的数据库中都需要创建 undo_log 表:
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
代码实现
1. 订单服务 (Order Service)
pom.xml 依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2021.0.1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
application.yml:
spring:
datasource:
url: jdbc:mysql://localhost:3306/order_db
username: root
password: root
cloud:
nacos:
discovery:
server-addr: localhost:8848
seata:
tx-service-group: my_test_tx_group
service:
vgroup-mapping:
my_test_tx_group: default
OrderController.java:
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/create")
public String createOrder(@RequestBody OrderDTO orderDTO) {
orderService.createOrder(orderDTO);
return "订单创建成功";
}
}
OrderService.java:
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountFeignClient accountFeignClient;
@Autowired
private StorageFeignClient storageFeignClient;
@Override
@GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
public void createOrder(OrderDTO orderDTO) {
// 1. 创建订单
Order order = new Order();
order.setUserId(orderDTO.getUserId());
order.setProductId(orderDTO.getProductId());
order.setCount(orderDTO.getCount());
order.setMoney(orderDTO.getMoney());
order.setStatus(0); // 0-创建中
orderMapper.insert(order);
// 2. 扣减库存
storageFeignClient.decrease(orderDTO.getProductId(), orderDTO.getCount());
// 3. 扣减余额
accountFeignClient.decrease(orderDTO.getUserId(), orderDTO.getMoney());
// 4. 修改订单状态
order.setStatus(1); // 1-已完成
orderMapper.updateById(order);
}
}
2. 库存服务 (Storage Service)
StorageController.java:
@RestController
@RequestMapping("/storage")
public class StorageController {
@Autowired
private StorageService storageService;
@PostMapping("/decrease")
public String decrease(@RequestParam Long productId, @RequestParam Integer count) {
storageService.decrease(productId, count);
return "库存扣减成功";
}
}
StorageService.java:
@Service
public class StorageServiceImpl implements StorageService {
@Autowired
private StorageMapper storageMapper;
@Override
@Transactional
public void decrease(Long productId, Integer count) {
Storage storage = storageMapper.selectByProductId(productId);
if (storage.getResidue() < count) {
throw new RuntimeException("库存不足");
}
storage.setResidue(storage.getResidue() - count);
storage.setUsed(storage.getUsed() + count);
storageMapper.updateById(storage);
}
}
3. 账户服务 (Account Service)
AccountController.java:
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private AccountService accountService;
@PostMapping("/decrease")
public String decrease(@RequestParam Long userId, @RequestParam BigDecimal money) {
accountService.decrease(userId, money);
return "账户扣减成功";
}
}
AccountService.java:
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
@Transactional
public void decrease(Long userId, BigDecimal money) {
Account account = accountMapper.selectByUserId(userId);
if (account.getBalance().compareTo(money) < 0) {
throw new RuntimeException("余额不足");
}
account.setBalance(account.getBalance().subtract(money));
accountMapper.updateById(account);
}
}
方案二:使用 RocketMQ 事务消息(最终一致性方案)
环境准备
- 启动 RocketMQ:
nohup sh bin/mqnamesrv &
nohup sh bin/mqbroker -n localhost:9876 &
代码实现
订单服务 (Order Service)
OrderServiceWithMQ.java:
@Service
@Slf4j
public class OrderServiceWithMQ {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 创建订单并发送事务消息
*/
@Transactional
public void createOrderWithTransactionMessage(OrderDTO orderDTO) {
// 1. 创建订单(状态为待确认)
Order order = new Order();
order.setUserId(orderDTO.getUserId());
order.setProductId(orderDTO.getProductId());
order.setCount(orderDTO.getCount());
order.setMoney(orderDTO.getMoney());
order.setStatus(0); // 0-创建中
orderMapper.insert(order);
// 2. 发送半事务消息
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
"order-transaction-topic",
MessageBuilder.withPayload(orderDTO).setHeader("order_id", order.getId()).build(),
order
);
log.info("发送事务消息结果: {}", sendResult.getSendStatus());
}
/**
* 事务消息本地事务执行器
*/
@RocketMQTransactionListener
public class OrderTransactionListenerImpl implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
Order order = (Order) arg;
// 这里可以执行一些本地检查
// 如果本地事务执行成功,返回COMMIT
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
log.error("本地事务执行失败", e);
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// 检查本地事务状态
String orderId = msg.getHeaders().get("order_id", String.class);
Order order = orderMapper.selectById(orderId);
if (order != null && order.getStatus() == 1) {
return RocketMQLocalTransactionState.COMMIT;
} else {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
}
库存服务 (Storage Service)
StorageMQListener.java:
@Service
@RocketMQMessageListener(
topic = "order-transaction-topic",
consumerGroup = "storage-consumer-group"
)
@Slf4j
public class StorageMQListener implements RocketMQListener<OrderDTO> {
@Autowired
private StorageService storageService;
@Override
public void onMessage(OrderDTO orderDTO) {
try {
// 扣减库存(需要保证幂等性)
storageService.decrease(orderDTO.getProductId(), orderDTO.getCount());
log.info("库存扣减成功: {}", orderDTO);
} catch (Exception e) {
log.error("库存扣减失败: {}", orderDTO, e);
// 可以加入重试机制或者死信队列
throw new RuntimeException(e);
}
}
}
账户服务 (Account Service)
AccountMQListener.java:
@Service
@RocketMQMessageListener(
topic = "order-transaction-topic",
consumerGroup = "account-consumer-group"
)
@Slf4j
public class AccountMQListener implements RocketMQListener<OrderDTO> {
@Autowired
private AccountService accountService;
@Override
public void onMessage(OrderDTO orderDTO) {
try {
// 扣减余额(需要保证幂等性)
accountService.decrease(orderDTO.getUserId(), orderDTO.getMoney());
log.info("账户扣减成功: {}", orderDTO);
} catch (Exception e) {
log.error("账户扣减失败: {}", orderDTO, e);
throw new RuntimeException(e);
}
}
}
方案对比和选择建议
特性 | Seata AT 模式 | RocketMQ 事务消息 |
一致性 | 强一致性 | 最终一致性 |
性能 | 中等 | 高 |
侵入性 | 低 | 中等 |
复杂度 | 低 | 中等 |
适用场景 | 需要强一致性的业务 | 高并发、允许短暂不一致的业务 |
最佳实践建议
- 根据业务场景选择方案:
-
- 金融核心系统:选择 Seata AT 或 TCC 模式
- 电商业务:选择 RocketMQ 事务消息
- 混合使用:核心链路用 Seata,非核心用 MQ
- 保证幂等性:
@Service
public class StorageService {
@Autowired
private StorageMapper storageMapper;
@Transactional
public void decrease(Long productId, Integer count, String transactionId) {
// 检查是否已经处理过
if (isProcessed(transactionId)) {
return;
}
// 执行业务逻辑
Storage storage = storageMapper.selectByProductId(productId);
storage.setResidue(storage.getResidue() - count);
storageMapper.updateById(storage);
// 记录处理状态
recordProcessed(transactionId);
}
}
- 设置重试机制和监控:
rocketmq:
consumer:
delay-level-when-next-consume: 0
max-reconsume-times: 3
- 人工补偿机制:
-
- 对于始终失败的消息,进入死信队列
- 提供管理界面进行人工干预和处理
这种分布式事务实践方案已经在大量生产环境中得到验证,能够有效解决微服务架构下的数据一致性问题。