分布式事务解决方案:RocketMQ事务消息

请添加图片描述

用RocketMQ事务消息实现分布式事务

在这里插入图片描述
RocketMQ实现分布式事务的流程如下

  1. producer向mq server发送一个半消息
  2. mq server将消息持久化成功后,向发送方确认消息已经发送成功,此时消息并不会被consumer消费
  3. producer开始执行本地事务逻辑
  4. producer根据本地事务执行结果向mq server发送二次确认,mq收到commit状态,将消息标记为可投递,consumer会消费该消息。mq收到rollback则删除半消息,consumer将不会消费该消息,如果收到unknow状态,mq会对消息发起回查
  5. 在断网或者应用重启等特殊情况下,步骤4提交的2次确认有可能没有到达mq server,经过固定时间后mq会对该消息发起回查
  6. producer收到回查后,需要检查本地事务的执行状态
  7. producer根据本地事务的最终状态,再次提交二次确认,mq仍按照步骤4对半消息进行操作

还是以转账的demo演示一下,在db_account_1和db_account_2这2个库中,建立如下2张表

-- 账户表
CREATE TABLE `account_info`
(
    `id`      INT(11)      NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `user_id` VARCHAR(255) NOT NULL COMMENT '用户id',
    `balance` INT(11)      NOT NULL DEFAULT 0 COMMENT '用户余额',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;

-- 账户流水表
CREATE TABLE `account_flow`
(
    `flow_id` INT(11)      NOT NULL COMMENT '流水id',
    `user_id` VARCHAR(255) NOT NULL COMMENT '用户id',
    `money`   INT(11)      NOT NULL COMMENT '变动金额' DEFAULT 0,
    `status`  INT(11)      NOT NULL COMMENT '状态,0待支付,1已完成' DEFAULT 0,
    PRIMARY KEY (`flow_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;

发送端

application.yaml

server:
  port: 30002

spring:
  application:
    name: transaction-msg-producer
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url : jdbc:mysql://myhost:3306/db_account_2?useUnicode=true&characterEncoding=utf8
    username: test
    password: test
    type: com.alibaba.druid.pool.DruidDataSource

rocketmq:
  name-server: myhost:9876
  producer:
    group: tx_producer
    send-message-timeout: 6000

事务消息对象

@Data
public class AccountMsg {

    private Integer flowId;

    private String fromUserId;

    private String toUserId;

    private Integer money;
}

转账接口

@RestController
@RequestMapping("account")
public class AccountController {

    @Resource
    private AccountService accountService;

    @RequestMapping("transfer")
    private String transfer(@RequestBody AccountMsg accountMsg) {
        try {
            accountService.sendUpdateMsg(accountMsg);
        } catch (Exception e) {
            return "fail";
        }
        return "success";
    }
}
public interface AccountService {

    void sendUpdateMsg(AccountMsg accountMsg);

    void update(AccountMsg accountMsg);
}

转账的时候先发送一条事务消息

@Slf4j
@Service
public class AccountServiceImpl implements AccountService {

    @Resource
    private RocketMQTemplate rocketMQTemplate;
    @Resource
    private AccountMapper accountMapper;
    @Resource
    private AccountFlowMapper accountFlowMapper;

    /**
     * 需要根据流水号加幂等哈,我就不加了
     */
    @Override
    public void sendUpdateMsg(AccountMsg accountMsg) {
        log.info("sendUpdateMsg param flowId: {}", accountMsg.getFlowId());
        Message<AccountMsg> message = MessageBuilder.withPayload(accountMsg).build();
        rocketMQTemplate.sendMessageInTransaction("account_topic:account_tag", message, null);
    }

    /**
     * 更新账户
     * 增加流水
     */
    @Override
    @Transactional
    public void update(AccountMsg accountMsg) {
        log.info("update param flowId: {}", accountMsg.getFlowId());
        accountMapper.updateMoney(accountMsg.getFromUserId(), accountMsg.getMoney() * -1);
        accountFlowMapper.insertFlow(accountMsg.getFlowId(), accountMsg.getFromUserId(), accountMsg.getMoney(), 1);
    }
}

当事务消息发送成功后回掉RocketMQLocalTransactionListener#executeLocalTransaction方法,执行本地事务。

同时需要提供回查方法的实现,让rocketmq查询本地事务的执行状态,来决定是否投递消息

@Slf4j
@Component
@RocketMQTransactionListener
public class AccountListener implements RocketMQLocalTransactionListener {

    @Resource
    private AccountService accountService;
    @Resource
    private AccountFlowMapper accountFlowMapper;

    /**
     * 事务消息发送成功回调
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
        try {
            String messageStr = new String((byte[]) message.getPayload(), StandardCharsets.UTF_8);
            AccountMsg accountMsg = JSONObject.parseObject(messageStr, AccountMsg.class);
            accountService.update(accountMsg);
        } catch (Exception e) {
            log.error("executeLocalTransaction error", e);
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        return RocketMQLocalTransactionState.COMMIT;
    }

    /**
     * 事务状态回查
     * 有流水说明账户更新成功,否则更新失败
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        String messageStr = new String((byte[]) message.getPayload(), StandardCharsets.UTF_8);
        AccountMsg accountMsg = JSONObject.parseObject(messageStr, AccountMsg.class);
        int result = accountFlowMapper.selectByFlowId(accountMsg.getFlowId());
        if (result == 1) {
            return RocketMQLocalTransactionState.COMMIT;
        } else {
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }
}
public interface AccountMapper {

    @Update("update account_info set balance = balance + #{money} where balance + #{money} > 0 and user_id = #{userId}")
    int updateMoney(@Param("userId") String userId, @Param("money") Integer money);
}
public interface AccountFlowMapper {

    @Insert("insert account_flow values (#{flowId}, #{userId}, #{money}, #{status})")
    int insertFlow(@Param("flowId") Integer flowId, @Param("userId") String userId,
                   @Param("money") Integer money, @Param("status") Integer status);

    @Select("select count(*) from account_flow where flow_id = #{flowId}")
    int selectByFlowId(@Param("flowId") Integer flowId);
}

接收端

收到消息后,增加账户余额,同时增加一条流水记录

@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "tx_consumer", topic = "account_topic", selectorExpression = "account_tag")
public class AccountConsumer implements RocketMQListener<AccountMsg> {

    @Resource
    private AccountMapper accountMapper;
    @Resource
    private AccountFlowMapper accountFlowMapper;

    /**
     * 需要根据流水号加幂等,我就不加幂等代码了
     */
    @Override
    @Transactional
    public void onMessage(AccountMsg accountMsg) {
        log.info("onMessage param flowId: {}", accountMsg.getFlowId());
        accountMapper.updateMoney(accountMsg.getToUserId(), accountMsg.getMoney());
        accountFlowMapper.insertFlow(accountMsg.getFlowId(), accountMsg.getToUserId(), accountMsg.getMoney(), 1);
    }
}

来测试一下,第一次请求正常转账

http://localhost:30002/account/transfer
{
    "flowId": 100,
    "fromUserId": "1001",
    "toUserId": "1002",
    "money": 100
}

第二次请求,因为流水id已经存在(报主键冲突异常),导致发送端扣钱失败,接收端也没收到消息,2边的钱都没发生变化,测试完成

参考博客

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Java识堂

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

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

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

打赏作者

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

抵扣说明:

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

余额充值