分布式事务解决方案:Seata TCC 模式

请添加图片描述

介绍

开源的TCC框架有很多,比如,hmily,EasyTransaction,ByteTCC,TCC-Transaction等。其实我刚开始是用hmily学习tcc的,后续我也看了一下hmily的源码,但是,hmily对各种异常流程的处理没有seata优雅。所以本篇就用seata tcc模式写一个转账demo,seata-tcc-tm项目向seata-tcc-rm项目转账

seata-tcc-tm

application.yaml

server:
  port: 30002

spring:
  application:
    name: seata-tcc-tm
  datasource:
    driver-class-name: 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

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: myhost:18091
    disable-global-transaction: false
  config:
    type: file
    file:
      name: file.conf
  registry:
    type: file
    file:
      name: file.conf
@EnableFeignClients
@MapperScan("com.javashitang.dao")
@SpringBootApplication
public class SeataTccTmAccount {

    public static void main(String[] args) {
        SpringApplication.run(SeataTccTmAccount.class, args);
    }
}
@RestController
@RequestMapping("account")
public class AccountController {

    @Resource
    private TmTccService tmTccService;
    @Resource
    private RmAccountClient rmAccountClient;


    @GlobalTransactional
    @RequestMapping("transfer")
    public String transfer(@RequestParam("fromUserId") String fromUserId,
                           @RequestParam("toUserId") String toUserId,
                           @RequestParam("money") Integer money) {
        boolean ret = tmTccService.prepare(null, fromUserId, toUserId, money);
        if (!ret) {
            throw new RuntimeException("预扣款失败");
        }
        String rmRet = rmAccountClient.transfer(fromUserId, toUserId, money);
        if ("fail".equals(rmRet)) {
            throw new RuntimeException("预收款失败");
        }
        return "success";
    }
}
@FeignClient(value = "seata-tcc-rm", url = "http://127.0.0.1:30001")
public interface RmAccountClient {

    @RequestMapping("account/transfer")
    String transfer(@RequestParam("fromUserId") String fromUserId,
                    @RequestParam("toUserId") String toUserId,
                    @RequestParam("money") Integer money);

}
@LocalTCC
public interface TmTccService {

    @TwoPhaseBusinessAction(name = "TmTccService", commitMethod = "commit", rollbackMethod = "cancel")
    boolean prepare(BusinessActionContext context,
                    @BusinessActionContextParameter(paramName = "fromUserId") String fromUserId,
                    @BusinessActionContextParameter(paramName = "toUserId") String toUserId,
                    @BusinessActionContextParameter(paramName = "money") Integer money);

    /**
     * 确认方法,可以重命名,但要和commitMethod保持一致
     */
    boolean commit(BusinessActionContext context);

    /**
     * 取消方法,可以重命名,但要和rollbackMethod保持一致
     */
    boolean cancel(BusinessActionContext context);
}

注意:@TwoPhaseBusinessAction的name属性在分布式应用中必须全局唯一,因为name属性在TCC模式中是资源管理器的唯一标识resourceId,而在at和xa模式中resourceId都为数据库连接url

我们可以用@BusinessActionContextParameter修饰入参让参数加到BusinessActionContext中,这样在后续的commit方法或者cancel方法中就能从BusinessActionContext中获取到这些参数

@Slf4j
@Service
public class TmTccServiceImpl implements TmTccService {

    @Resource
    private AccountInfoMapper accountInfoMapper;

    @Override
    public boolean prepare(BusinessActionContext context, String fromUserId, String toUserId, Integer money) {
        log.info("prepare");
        int result = accountInfoMapper.updateMoney(fromUserId, money * -1);
        return result == 1;
    }

    @Override
    public boolean commit(BusinessActionContext context) {
        log.info("commit");
        return true;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        log.info("cancel");
        String fromUserId = String.valueOf(context.getActionContext("fromUserId"));
        Integer money = (Integer) context.getActionContext("money");
        accountInfoMapper.updateMoney(fromUserId, money);
        return true;
    }
}
public interface AccountInfoMapper {

    @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);
}

seata-tcc-rm

application.yaml

server:
  port: 30001

spring:
  application:
    name: seata-tcc-rm
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url : jdbc:mysql://myhost:3306/db_account_1?useUnicode=true&characterEncoding=utf8
    username: test
    password: test
    type: com.alibaba.druid.pool.DruidDataSource

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: myhost:18091
    disable-global-transaction: false
  config:
    type: file
    file:
      name: file.conf
  registry:
    type: file
    file:
      name: file.conf
@MapperScan("com.javashitang.dao")
@SpringBootApplication
public class SeataTccRmAccount {

    public static void main(String[] args) {
        SpringApplication.run(SeataTccRmAccount.class, args);
    }
}
@RestController
@RequestMapping("account")
public class AccountController {

    @Resource
    private RmTccService rmTccService;

    @RequestMapping("transfer")
    public String transfer(@RequestParam("fromUserId") String fromUserId,
                           @RequestParam("toUserId") String toUserId,
                           @RequestParam("money") Integer money) {
        boolean flag = rmTccService.prepare(null, fromUserId, toUserId, money);
        return flag ? "success" : "fail";
    }
}
@LocalTCC
public interface RmTccService {

    @TwoPhaseBusinessAction(name = "RmTccService", commitMethod = "commit", rollbackMethod = "cancel")
    boolean prepare(BusinessActionContext context,
                    @BusinessActionContextParameter(paramName = "fromUserId") String fromUserId,
                    @BusinessActionContextParameter(paramName = "toUserId") String toUserId,
                    @BusinessActionContextParameter(paramName = "money") Integer money);

    /**
     * 确认方法,可以重命名,但要和commitMethod保持一致
     */
    boolean commit(BusinessActionContext context);

    /**
     * 取消方法,可以重命名,但要和rollbackMethod保持一致
     */
    boolean cancel(BusinessActionContext context);
}
@Slf4j
@Service
public class RmTccServiceImpl implements RmTccService {

    @Resource
    private AccountInfoMapper accountInfoMapper;

    @Override
    public boolean prepare(BusinessActionContext context, String fromUserId, String toUserId, Integer money) {
        log.info("prepare");
        int result = accountInfoMapper.selectByUserId(toUserId);
        return result == 1;
    }

    @Override
    public boolean commit(BusinessActionContext context) {
        log.info("commit");
        String toUserId = String.valueOf(context.getActionContext("toUserId"));
        Integer money = (Integer) context.getActionContext("money");
        accountInfoMapper.updateMoney(toUserId, money);
        return true;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        log.info("cancel");
        return true;
    }
}
public interface AccountInfoMapper {

    @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);

    @Select("select count(*) from account_info where user_id = #{userId}")
    int selectByUserId(@Param("userId") String userId);
}

参考博客

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Java识堂

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

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

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

打赏作者

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

抵扣说明:

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

余额充值