heim分布式事务----day02

可靠消息:张三一定保证消息发出去了。

最终一致性:李四失败张三不能回滚,李四你无论如何要把钱加上。

重复消息要解决幂等性。

要解决的问题:

1.本地事务与消息发送的原子性问题

2.事务参与方接受消息的可靠性

3.消息重复消费问题

------------------------------------------------01-----------------------------------------------------------------

方案:本地消息表方案。

第一步:解决本地事务和消息的原子性,发送成功才删除积分日志。否则等待下一个周期重试。只要没发送就一直发送。

第二步:积分服务监听消息队列,成功消费给mq一个回应,mq有消息确认机制确认就不推送了,不回的话会一直发送。

------------------------------------------------------------------------------02------------------------------------------------------------------

RocketMQ事务消息方案。

逻辑请求:

1.张三是发送方,发送数据给mq

2.半消息,mq返回发送成功。

3.执行本地事务,发送commit,此时mq把自己的消息更改为可消费。

4.mq发给消费者李四账户加钱。

5.发送端扣款失败则发一个rollback的ack,此时回滚。

6.mq会定时回查事务是不是提交了,没提交可以提交的。

7.检查本地事务状态是不是有问题了,有问题就自己处理。

------------------------------------------------------------------------------03------------------------------------------------------------------

开发案例:

1.创建数据库导入脚本

2.

3.启动mq注意这个mq直接在我们的本地运行就可以。

win启动rockerMQ,在我的电脑路径:D:\softWork\rocketmq4.5.0

4.导入项目

------------------------------------------------------------------------------04------------------------------------------------------------------

bank1就是mq的发起方,bank2就是mq的消费方。

bank1:张三扣减金额,本地事务提交,发消息。

第一步:

手动创建事务:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class AccountChangeEvent implements Serializable {
    /**
     * 账号
     */
    private String accountNo;
    /**
     * 变动金额
     */
    private double amount;
    /**
     * 事务号
     */
    private String txNo;

}
@RestController
@Slf4j
public class AccountInfoController {
    @Autowired
    private AccountInfoService accountInfoService;

    @GetMapping(value = "/transfer")
    public String transfer(@RequestParam("accountNo")String accountNo, @RequestParam("amount") Double amount){
        //创建一个事务id,作为消息内容发到mq
        String tx_no = UUID.randomUUID().toString();
        AccountChangeEvent accountChangeEvent = new AccountChangeEvent(accountNo,amount,tx_no);
        //发送消息
        accountInfoService.sendUpdateAccountBalance(accountChangeEvent);
        return "转账成功";
    }
}
   //将accountChangeEvent转成json
        JSONObject jsonObject =new JSONObject();
        jsonObject.put("accountChange",accountChangeEvent);
        String jsonString = jsonObject.toJSONString();
        //生成message类型
        Message<String> message = MessageBuilder.withPayload(jsonString).build();
        //发送一条事务消息
        /**
         * String txProducerGroup 生产组
         * String destination topic,
         * Message<?> message, 消息内容
         * Object arg 参数
         */
        rocketMQTemplate.sendMessageInTransaction("producer_group_txmsg_bank1","topic_txmsg",message,null);

注意这里是有问题的,老师也说了

第二步:

 //更新账户,扣减金额
    @Override
    @Transactional
    public void doUpdateAccountBalance(AccountChangeEvent accountChangeEvent) {
        //幂等判断
        if(accountInfoDao.isExistTx(accountChangeEvent.getTxNo())>0){
            return ;
        }
        //扣减金额
        accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount() * -1);
        //添加事务日志 判断幂等
        accountInfoDao.addTx(accountChangeEvent.getTxNo());
        if(accountChangeEvent.getAmount() == 3){
            throw new RuntimeException("人为制造异常");
        }
    }

实现了发送消息和更新本地事务。

------------------------------------------------------------------------------05------------------------------------------------------------------

消息发送成功回调。

回查状态。

我们写一个接口的实现类。

执行本地事务:

 //事务消息发送后的回调方法,当消息发送给mq成功,此方法被回调
    @Override
    @Transactional
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {

        try {
            //解析message,转成AccountChangeEvent
            String messageString = new String((byte[]) message.getPayload());
            JSONObject jsonObject = JSONObject.parseObject(messageString);
            String accountChangeString = jsonObject.getString("accountChange");
            //将accountChange(json)转成AccountChangeEvent
            AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
            //执行本地事务,扣减金额
            accountInfoService.doUpdateAccountBalance(accountChangeEvent);
            //当返回RocketMQLocalTransactionState.COMMIT,自动向mq发送commit消息,mq将消息的状态改为可消费
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            e.printStackTrace();
            return RocketMQLocalTransactionState.ROLLBACK;
        }


    }

这里是rollback则mq就删掉消息了。

发送commit则队列的消息就是可以消费了。

-------------------------------------------------------------------------------------

什么时候事务回查?就是没有收到4的确认的时候。

事务的回查:张三是不是扣减金额了。

 //事务状态回查,查询是否扣减金额
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        //解析message,转成AccountChangeEvent
        String messageString = new String((byte[]) message.getPayload());
        JSONObject jsonObject = JSONObject.parseObject(messageString);
        String accountChangeString = jsonObject.getString("accountChange");
        //将accountChange(json)转成AccountChangeEvent
        AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
        //事务id
        String txNo = accountChangeEvent.getTxNo();
        int existTx = accountInfoDao.isExistTx(txNo);
        if(existTx>0){
            return RocketMQLocalTransactionState.COMMIT;
        }else{
            // 这个就是等网络好了 还要继续发送的
            return RocketMQLocalTransactionState.UNKNOWN;
        }
    }

注意:在controller要有事务号:

------------------------------------------------------------------------------06------------------------------------------------------------------

李四的微服务:接受消息,监听mq,增加金额。

 //更新账户,增加金额
    @Override
    @Transactional
    public void addAccountInfoBalance(AccountChangeEvent accountChangeEvent) {
        log.info("bank2更新本地账号,账号:{},金额:{}",accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount());
        if(accountInfoDao.isExistTx(accountChangeEvent.getTxNo())>0){

            return ;
        }
        //增加金额
        accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount());
        //添加事务记录,用于幂等
        accountInfoDao.addTx(accountChangeEvent.getTxNo());
        if(accountChangeEvent.getAmount() == 4){
            throw new RuntimeException("人为制造异常");
        }
    }

监听mq:

@Component
@Slf4j
@RocketMQMessageListener(consumerGroup = "consumer_group_txmsg_bank2",topic = "topic_txmsg")
public class TxmsgConsumer implements RocketMQListener<String> {

    @Autowired
    AccountInfoService accountInfoService;

    //接收消息
    @Override
    public void onMessage(String message) {
        log.info("开始消费消息:{}",message);
        //解析消息
        JSONObject jsonObject = JSONObject.parseObject(message);
        String accountChangeString = jsonObject.getString("accountChange");
        //转成AccountChangeEvent
        AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
        //设置账号为李四的
        accountChangeEvent.setAccountNo("2");
        //更新本地账户,增加金额
        accountInfoService.addAccountInfoBalance(accountChangeEvent);

    }
}

注意上面的代码和配置文件的消息组是不一样的。

rocketmq.producer.group = producer_bank2
rocketmq.name-server = 127.0.0.1:9876

------------------------------------------------------------------------------07------------------------------------------------------------------

测试:

李四接到消息必须一直消费,直到成功。

张三的异常,在执行本地事务时候制造异常:

李四的异常:出现异常就重复的消费。也是执行本地事务制造异常。

onMessage抛出异常就循环接收。循环消费。不能回滚,要自己处理,可以打印日志什么的。消费多了。

------------------------------------------------------------------------------08----------------------------------------------------------------------------

最大努力通知:

可靠消息:保证消息发送成功我就提交事务,发送方发出去不会回滚。

最大努力通知:可靠性由接收方去保证。

最大努力不能用于交易,用于交易后的通知。

------------------------------------------------------------------------------09------------------------------------------------------------------

解决方案:最大努力基于ACK机制。

------------------------------------------------------------------------------10------------------------------------------------------------------

我们实现方案1:

第一步:数据库:

还需要bank1。

第二步:启动rockermq。

第三步:-n 127.0.0.1:9876 autoCreateTopicEnable=true

第三步:导入工程

第四步:注意

生产组的码字要和程序里面的生产组的名字是不一样的。

第五步:写代码

@Service
@Slf4j
public class AccountPayServiceImpl implements AccountPayService {

    @Autowired
    AccountPayDao accountPayDao;

    @Autowired
    RocketMQTemplate rocketMQTemplate;

    //插入充值记录
    @Override
    public AccountPay insertAccountPay(AccountPay accountPay) {
        int success = accountPayDao.insertAccountPay(accountPay.getId(), accountPay.getAccountNo(), accountPay.getPayAmount(), "success");
        if(success>0){
            //发送通知,使用普通消息发送通知
            accountPay.setResult("success");
            rocketMQTemplate.convertAndSend("topic_notifymsg",accountPay);
            return accountPay;
        }
        return null;
    }

    //查询充值记录,接收通知方调用此方法来查询充值结果
    @Override
    public AccountPay getAccountPay(String txNo) {
        AccountPay accountPay = accountPayDao.findByIdTxNo(txNo);
        return accountPay;
    }
}

第六步:写controller生成事务的编号

第七步:账户系统,监听mq通知,主动查询充值系统,账户系统会重复接受通知,要做好幂等。

 @Autowired
    AccountInfoDao accountInfoDao;

    @Autowired
    PayClient payClient;

    //更新账户金额 监听mq有消息会调用这个方法
    @Override
    @Transactional
    public void updateAccountBalance(AccountChangeEvent accountChange) {
        //幂等校验
        if(accountInfoDao.isExistTx(accountChange.getTxNo())>0){
            return ;
        }
        int i = accountInfoDao.updateAccountBalance(accountChange.getAccountNo(), accountChange.getAmount());
        //插入事务记录,用于幂等控制
        accountInfoDao.addTx(accountChange.getTxNo());
    }

    //远程调用查询充值结果
    @Override
    public AccountPay queryPayResult(String tx_no) {

        //远程调用
        AccountPay payresult = payClient.payresult(tx_no);
        if("success".equals(payresult.getResult())){
            //更新账户金额
            AccountChangeEvent accountChangeEvent = new AccountChangeEvent();
            accountChangeEvent.setAccountNo(payresult.getAccountNo());//账号
            accountChangeEvent.setAmount(payresult.getPayAmount());//金额
            accountChangeEvent.setTxNo(payresult.getId());//充值事务号
            updateAccountBalance(accountChangeEvent);
        }
        return payresult;
    }

第八步:写mq的监听

@Component
@Slf4j
@RocketMQMessageListener(topic = "topic_notifymsg",consumerGroup = "consumer_group_notifymsg_bank1")
public class NotifyMsgListener implements RocketMQListener<AccountPay> {

    @Autowired
    AccountInfoService accountInfoService;

    //接收消息
    @Override
    public void onMessage(AccountPay accountPay) {
        log.info("接收到消息:{}", JSON.toJSONString(accountPay));
        if("success".equals(accountPay.getResult())){
            //更新账户金额
            AccountChangeEvent accountChangeEvent = new AccountChangeEvent();
            accountChangeEvent.setAccountNo(accountPay.getAccountNo());
            accountChangeEvent.setAmount(accountPay.getPayAmount());
            accountChangeEvent.setTxNo(accountPay.getId());
            accountInfoService.updateAccountBalance(accountChangeEvent);
        }
        log.info("处理消息完成:{}", JSON.toJSONString(accountPay));
    }
}

------------------------------------------------------------11------------------12---------------13---------------------------------------------------

测试:

1.屏蔽发送结果到mq,账户系统主动查询。

充值成功

张三主动查询更新自己:

2.发送消息,账户去监听

-----------------------------------------------------------------------14--------------------------------------------------------------

发布了308 篇原创文章 · 获赞 11 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_28764557/article/details/104417019