一、hmily框架

1、到github拉取最新的源码

idea:File - New - Project form Version Control

java 事务 分布式系统 java分布式事务框架_mysql

 2、切换release版本

此时拉下来的是master版本,我们就用master分支吧。在右下角可以选择分支,比如选择分支2.2.1版本

java 事务 分布式系统 java分布式事务框架_sql_02

3、pom.xml中添加hmily-demo

<modules>
        <module>hmily-common</module>
        <module>hmily-core</module>
        <module>hmily-annotation</module>
        <module>hmily-spring-boot-starter</module>
        <module>hmily-spi</module>
        <module>hmily-serializer</module>
        <module>hmily-repository</module>
        <module>hmily-config</module>
        <module>hmily-spring</module>
        <module>hmily-rpc</module>
        <module>hmily-tcc</module>
        <module>hmily-tac</module>
        <module>hmily-metrics</module>
        <module>hmily-bom</module>
        <module>hmily-all</module>
        <module>hmily-xa</module>
        <module>hmily-demo</module>
    </modules>

4、初始化sql
hmily-demo/sql/hmily-demo.sql
建立库:
hmily:框架自带的库,事务的管理数据库
hmily_account:账户
hmily_order:订单
hmily_stock:库存

5、测试项目路径
hmily-demo/hmily-demo-tcc/hmily-demo-tcc-springcloud
hmily-demo-tcc-springcloud-account
hmily-demo-tcc-springcloud-eureka
hmily-demo-tcc-springcloud-inventory
hmily-demo-tcc-springcloud-order

6、业务说明

java 事务 分布式系统 java分布式事务框架_java 事务 分布式系统_03

下订单的同时,减库存,扣余额。这三个数据库要么同时提交,要么同时回滚。

7、修改数据库配置
hmily-demo-tcc-springcloud-order
application.yml文件:

spring:
    main:
        allow-bean-definition-overriding: true
    datasource:
        driver-class-name:  com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/hmily_order?useUnicode=true&characterEncoding=utf8
        username: root
        password: 123456
    application:
      name: order-service

hmily.yml文件:

repository:
  database:
    driverClassName: com.mysql.jdbc.Driver
    url : jdbc:mysql://127.0.0.1:3306/hmily?useUnicode=true&characterEncoding=utf8
    username: root
    password: 123456
    maxActive: 20
    minIdle: 10
    connectionTimeout: 30000
    idleTimeout: 600000
    maxLifetime: 1800000

hmily-demo-tcc-springcloud-account、hmily-demo-tcc-springcloud-inventory做相同修改。

8、启动服务
先起eureka
再起order
再起inventory
再起account

9、体验
http://127.0.0.1:8090/swagger-ui.htmljava 事务 分布式系统 java分布式事务框架_sql_04

测试/order/orderPay

java 事务 分布式系统 java分布式事务框架_sql_05

10、查询数据库
账户:

mysql> select * from hmily_account.account;
+----+---------+---------+---------------+---------------------+---------------------+
| id | user_id | balance | freeze_amount | create_time         | update_time         |
+----+---------+---------+---------------+---------------------+---------------------+
|  1 | 10000   | 9999991 |             0 | 2017-09-18 14:54:22 | 2022-04-27 14:08:03 |
+----+---------+---------+---------------+---------------------+---------------------+
1 row in set (0.00 sec)

订单:

mysql> select * from hmily_order.order;
+----+---------------------+----------------------+--------+------------+--------------+-------+---------+
| id | create_time         | number               | status | product_id | total_amount | count | user_id |
+----+---------------------+----------------------+--------+------------+--------------+-------+---------+
|  1 | 2022-04-27 14:08:03 | -6725939227652632576 |      4 | 1          |            9 |     1 | 10000   |
+----+---------------------+----------------------+--------+------------+--------------+-------+---------+
1 row in set (0.00 sec)

库存:

mysql> select * from hmily_stock.inventory;
+----+------------+-----------------+----------------+
| id | product_id | total_inventory | lock_inventory |
+----+------------+-----------------+----------------+
|  1 | 1          |         9999999 |              0 |
+----+------------+-----------------+----------------+
1 row in set (0.00 sec)

11、hmily库

+-------------------------------+
| Tables_in_hmily               |
+-------------------------------+
| hmily_lock                    |
| hmily_participant_undo        |
| hmily_transaction_global      |
| hmily_transaction_participant |
+-------------------------------+
4 rows in set (0.00 sec)

二、分布式事务订单支付的原理

java 事务 分布式系统 java分布式事务框架_sql_06

1、try
第一步:
try:插入一条订单数据,状态为支付中

@Override
    @HmilyTCC(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus")
    public void makePayment(Order order) {
        updateOrderStatus(order, OrderStatusEnum.PAYING);
//        //检查数据
//        final BigDecimal accountInfo = accountClient.findByUserId(order.getUserId());
//        final Integer inventoryInfo = inventoryClient.findByProductId(order.getProductId());
//        if (accountInfo.compareTo(order.getTotalAmount()) < 0) {
//            throw new HmilyRuntimeException("余额不足!");
//        }
//        if (inventoryInfo < order.getCount()) {
//            throw new HmilyRuntimeException("库存不足!");
//        }
        accountClient.payment(buildAccountDTO(order));
        inventoryClient.decrease(buildInventoryDTO(order));
    }

第二步:rpc调用,减账户
try:账户余额 - 消耗,冻结余额 + 消耗

@Override
    @HmilyTCC(confirmMethod = "confirm", cancelMethod = "cancel")
    public boolean payment(final AccountDTO accountDTO) {
        LOGGER.info("============执行try付款接口===============");
        accountMapper.update(accountDTO);
        return Boolean.TRUE;
    }
@Update("update account set balance = balance - #{amount}," +
            " freeze_amount= freeze_amount + #{amount} ,update_time = now()" +
            " where user_id =#{userId}  and  balance >= #{amount}  ")
    int update(AccountDTO accountDTO);

第三步:rpc调用,减库存
try:总库存 - 支出,冻结库存 + 支出

/**
     * 扣减库存操作.
     * 这一个tcc接口
     *
     * @param inventoryDTO 库存DTO对象
     * @return true
     */
    @Override
    @HmilyTCC(confirmMethod = "confirmMethod", cancelMethod = "cancelMethod")
    public Boolean decrease(InventoryDTO inventoryDTO) {
        LOGGER.info("==========try扣减库存decrease===========");
        inventoryMapper.decrease(inventoryDTO);
        return true;
    }
@Update("update inventory set total_inventory = total_inventory - #{count}," +
            " lock_inventory= lock_inventory + #{count} " +
            " where product_id =#{productId} and total_inventory > 0  ")
    int decrease(InventoryDTO inventoryDTO);

2、事务管理器

事务管理器把所有的事务记录到hmily库。

事务管理器调用confirm、cancel。异步确认或异步取消。

java 事务 分布式系统 java分布式事务框架_java_07

3、confirm
订单:
confirm:订单状态改为支付成功

public void confirmOrderStatus(Order order) {
        updateOrderStatus(order, OrderStatusEnum.PAY_SUCCESS);
        LOGGER.info("=========进行订单confirm操作完成================");
    }

账户:
confirm:冻结余额 - 消耗

public boolean confirm(final AccountDTO accountDTO) {
        LOGGER.info("============执行confirm 付款接口===============");
        return accountMapper.confirm(accountDTO) > 0;
    }
@Update("update account set " +
            " freeze_amount= freeze_amount - #{amount}" +
            " where user_id =#{userId}  and freeze_amount >= #{amount} ")
    int confirm(AccountDTO accountDTO);

库存:
confirm:冻结库存 - 支出

public Boolean confirmMethod(InventoryDTO inventoryDTO) {
        LOGGER.info("==========confirmMethod库存确认方法===========");
        return inventoryMapper.confirm(inventoryDTO) > 0;
    }
@Update("update inventory set " +
            " lock_inventory = lock_inventory - #{count} " +
            " where product_id =#{productId} and lock_inventory > 0 ")
    int confirm(InventoryDTO inventoryDTO);

4、cancel
订单:
cancel:订单状态改为支付失败

public void cancelOrderStatus(Order order) {
        updateOrderStatus(order, OrderStatusEnum.PAY_FAIL);
        LOGGER.info("=========进行订单cancel操作完成================");
    }

账户:
cancel:账户余额 + 消耗,冻结余额 - 消耗

public boolean cancel(final AccountDTO accountDTO) {
        LOGGER.info("============执行cancel 付款接口===============");
        return accountMapper.cancel(accountDTO) > 0;
    }
@Update("update account set balance = balance + #{amount}," +
            " freeze_amount= freeze_amount -  #{amount} " +
            " where user_id =#{userId}  and freeze_amount >= #{amount}")
    int cancel(AccountDTO accountDTO);

库存:
cancel:总库存 + 支出,冻结库存 - 支出

public Boolean cancelMethod(InventoryDTO inventoryDTO) {
        LOGGER.info("==========cancelMethod库存取消方法===========");
        return inventoryMapper.cancel(inventoryDTO) > 0;
    }
@Update("update inventory set total_inventory = total_inventory + #{count}," +
            " lock_inventory= lock_inventory - #{count} " +
            " where product_id =#{productId}  and lock_inventory > 0 ")
    int cancel(InventoryDTO inventoryDTO);

5、小结

java 事务 分布式系统 java分布式事务框架_sql_08

6、hmily-admin
master分支最新代码,去除了这个模块

三、txx-transaction和例子

1、到github拉取源码
https://github.com/changmingxie/tcc-transaction.git

2、选择master分支

3、初始化数据库
tcc-transaction-tutorial-sample/src/dbscripts
create_db_tcc.sql:事务管理器初始化数据
create_db_red.sql:红包账户数据库
create_db_ord.sql:订单数据库
create_db_cap.sql:账户余额数据库

创建了4个库:
tcc
tcc_cap
tcc_ord
tcc_red

4、txx-transaction例子示例图

java 事务 分布式系统 java分布式事务框架_sql_09

5、改配置
tcc-transaction/tcc-transaction-tutorial-sample/pom.xml

<modules>
<!--        <module>tcc-transaction-dubbo-sample</module>-->
        <module>tcc-transaction-http-sample</module>
        <module>tcc-transaction-sample-domain</module>
        <module>tcc-transaction-multiple-tier-sample</module>
    </modules>

使用的模块:
tcc-transaction/tcc-transaction-tutorial-sample/tcc-transaction-http-sample/tcc-transaction-http-capital:账户余额
tcc-transaction/tcc-transaction-tutorial-sample/tcc-transaction-http-sample/tcc-transaction-http-order:订单
tcc-transaction/tcc-transaction-tutorial-sample/tcc-transaction-http-sample/tcc-transaction-http-redpacket:红包

6、改jdbc连接

没找到

经排查,例子里数据库链接用的是h2内存数据库,在tcc-transaction-sample-domain模块里

java 事务 分布式系统 java分布式事务框架_spring_10

7、部署tomcat

java 事务 分布式系统 java分布式事务框架_mysql_11

8、体验

地址:http://localhost:8080/o/

(1)第一步:点击商品列表链接

java 事务 分布式系统 java分布式事务框架_mysql_12

(2)第二步:点击商品

java 事务 分布式系统 java分布式事务框架_mysql_13

(3)第三步:输入红包金额

java 事务 分布式系统 java分布式事务框架_sql_14

(4)第四步:点击支付

java 事务 分布式系统 java分布式事务框架_sql_15

(5)日志信息

capital try record called. time seq:2022-05-16 15:17:22
red packet try record called. time seq:2022-05-16 15:17:23
order confirm make payment called. time seq:2022-05-16 15:17:24
capital confirm record called. time seq:2022-05-16 15:17:25
red packet confirm record called. time seq:2022-05-16 15:17:26

四、txx-transaction组合支付原理

1、入口

@RequestMapping(value = "/placeorder", method = RequestMethod.POST)
    public RedirectView placeOrder(@RequestParam String redPacketPayAmount,
                                   @RequestParam long shopId,
                                   @RequestParam long payerUserId,
                                   @RequestParam long productId) {


        PlaceOrderRequest request = buildRequest(redPacketPayAmount, shopId, payerUserId, productId);

        String merchantOrderNo = placeOrderService.placeOrder(request.getPayerUserId(), request.getShopId(),
                request.getProductQuantities(), request.getRedPacketPayAmount());

        return new RedirectView("payresult/" + merchantOrderNo);
    }

2、创建订单

public String placeOrder(long payerUserId, long shopId, List<Pair<Long, Integer>> productQuantities, BigDecimal redPacketPayAmount) {
        Shop shop = shopRepository.findById(shopId);

        Order order = orderService.createOrder(payerUserId, shop.getOwnerUserId(), productQuantities);
        order.needToPay(redPacketPayAmount,order.getTotalAmount().subtract(redPacketPayAmount));
        orderService.update(order);

        Boolean result = false;

        try {
            paymentService.makePayment(order.getMerchantOrderNo());

        } catch (ConfirmingException confirmingException) {
            //exception throws with the tcc transaction status is CONFIRMING,
            //when tcc transaction is confirming status,
            // the tcc transaction recovery will try to confirm the whole transaction to ensure eventually consistent.

            result = true;
        } catch (CancellingException cancellingException) {
            //exception throws with the tcc transaction status is CANCELLING,
            //when tcc transaction is under CANCELLING status,
            // the tcc transaction recovery will try to cancel the whole transaction to ensure eventually consistent.
        } catch (Throwable e) {
            //other exceptions throws at TRYING stage.
            //you can retry or cancel the operation.
            e.printStackTrace();
        }

        return order.getMerchantOrderNo();
    }

3、使用了tcc的注解

@Compensable(confirmMethod = "confirmMakePayment", cancelMethod = "cancelMakePayment", asyncConfirm = true)
    @Transactional
    public void makePayment(String orderNo) {

        System.out.println("order try make payment called.time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

        Order order = orderRepository.findByMerchantOrderNo(orderNo);

        String result = tradeOrderServiceProxy.record(buildCapitalTradeOrderDto(order));
        String result2 = tradeOrderServiceProxy.record(buildRedPacketTradeOrderDto(order));

//        String result = tradeOrderServiceProxy.record(null,buildCapitalTradeOrderDto(order));
//        String result2 = tradeOrderServiceProxy.record(null,buildRedPacketTradeOrderDto(order));
    }

4、红包接口,也是tcc,先冻结,再提交

@Override
    @Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord")
    @Transactional
    public String record(TransactionContext transactionContext, RedPacketTradeOrderDto tradeOrderDto) {

        try {
            Thread.sleep(1000l);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("red packet try record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

        TradeOrder foundTradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());

        //check if the trade order has need recorded.
        //if record, then this method call return success directly.
        if (foundTradeOrder == null) {

            TradeOrder tradeOrder = new TradeOrder(
                    tradeOrderDto.getSelfUserId(),
                    tradeOrderDto.getOppositeUserId(),
                    tradeOrderDto.getMerchantOrderNo(),
                    tradeOrderDto.getAmount()
            );

            try {
                tradeOrderRepository.insert(tradeOrder);

                RedPacketAccount transferFromAccount = redPacketAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());

                transferFromAccount.transferFrom(tradeOrderDto.getAmount());

                redPacketAccountRepository.save(transferFromAccount);
            } catch (DataIntegrityViolationException e) {

            }
        }

        return "success";
    }