1. 程式人生 > 其它 >「整合系列」分散式事務seata

「整合系列」分散式事務seata

本篇文章主要內容是使用Seata解決分散式事務問題。

場景說明

訂單服務order-service需要對外提供建立訂單的介面,建立訂單的業務邏輯如下:

先呼叫本地的orderService儲存訂單操作,然後通過feign呼叫遠端的accout-service進行賬戶餘額扣減,最後再通過feign呼叫遠端的product-service進行庫存扣減操作。

關鍵的邏輯程式碼如下:

order-service

(1)OrderController對外提供建立訂單的介面

@PostMapping("/order/create")
public Result<String> create(@RequestBody OrderDTO orderDTO){
	log.info("create orderDTO:{}",orderDTO);
	orderDTO.setOrderNo(UUID.randomUUID().toString());
	orderDTO.setAmount(orderDTO.getPrice().multiply(new BigDecimal(orderDTO.getCount())));
	orderService.createOrder(orderDTO);
	return Result.success("create order success");
}

OrderDTO如下:

@Data
public class OrderDTO extends Order{
    private BigDecimal price;
}

(2)OrderServiceImpl負責處理建立訂單的業務邏輯

public interface OrderService extends IService<Order> {

    void createOrder(OrderDTO orderDTO);
}

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

    @Autowired
    private ProductFeign productFeign;

    @Autowired
    private AccountFeign accountFeign;

    @Transactional(rollbackFor = RuntimeException.class)
    @Override
    public void createOrder(OrderDTO orderDTO) {
        Order order = new Order();
        BeanUtils.copyProperties(orderDTO, order);
        //本地儲存Order
        this.saveOrUpdate(order);
        //庫存扣減
        productFeign.deduct(orderDTO.getProductCode(), order.getCount());
        //賬戶餘額扣減
        accountFeign.reduce(orderDTO.getAccountCode(), orderDTO.getAmount());
    }
}

本地先儲存,然後呼叫兩個遠端服務進行扣減操作。

order-feign

這裡我們用到了ProductFeign和AccountFeign的兩個方法。

(1)在ProductFegin中新增方法

@PostMapping("/product/deduct")
Result deduct(@RequestParam("productCode") String productCode, @RequestParam("deductCount") Integer deductCount);

(2)在AccountFeign中新增方法

@PostMapping("/account/reduce")
Result<String> reduce(@RequestParam("accountCode")String accountCode, @RequestParam("amount")BigDecimal amount);

product-service

(1)在ProductController中提供一個供ProductFeign呼叫的方法

@PostMapping("/product/deduct")
public Result deduct(String productCode, Integer deductCount){
	log.info("deduct product,productCode is {}", productCode);
	productService.deduct(productCode, deductCount);
	return Result.success("操作成功");
}

(2)ProductServiceImpl負責庫存扣減的邏輯

public interface ProductService extends IService<Product> {

    void deduct(String productCode, Integer deductCount);
}

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {

    @Resource
    private ProductMapper productMapper;

    @Transactional(rollbackFor = RuntimeException.class)
    @Override
    public void deduct(String productCode, Integer deductCount) {
        Product product = productMapper.selectByCode(productCode);
        if(null == product){
            throw new RuntimeException("can't deduct product,product is null");
        }
        int surplus = product.getCount() - deductCount;
        if(surplus < 0){
            throw new RuntimeException("can't deduct product,product's count is less than deduct count");
        }
        product.setCount(surplus);
        productMapper.updateById(product);
    }
}

做些簡單的校驗,當產品庫存不足時不允許扣減操作。

(3)ProductMapper如下:

@Mapper
public interface ProductMapper extends BaseMapper<Product> {

    @Select("select * from product where product_code = #{productCode}")
    Product selectByCode(String productCode);
}

account-service

(1)在AccountController中提供一個供AccountFeign呼叫的方法

@PostMapping("/account/reduce")
public Result reduce(String accountCode, BigDecimal amount){
	log.info("reduce account,accountCode is {}", accountCode);
	accountService.reduceAccount(accountCode, amount);
	return Result.success("reduceAccount成功");
}

(2)AccountServiceImpl負責賬戶扣減的邏輯

public interface AccountService extends IService<Account> {

    void reduceAccount(String accountCode, BigDecimal amount);
}

@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {

    @Resource
    private AccountMapper accountMapper;

    @Transactional(rollbackFor = RuntimeException.class)
    @Override
    public void reduceAccount(String accountCode, BigDecimal amount) {
        Account account = accountMapper.selectByCode(accountCode);
        if(null == account){
            throw new RuntimeException("can't reduce amount,account is null");
        }
        BigDecimal subAmount = account.getAmount().subtract(amount);
        if(subAmount.compareTo(BigDecimal.ZERO) < 0){
            throw new RuntimeException("can't reduce amount,account'amount is less than reduce amount");
        }
        account.setAmount(subAmount);
        accountMapper.updateById(account);
    }
}

做些簡單的校驗,當賬戶餘額不足的時候不允許扣減操作。

(3)AccountMapper如下:

@Mapper
public interface AccountMapper extends BaseMapper<Account> {

    @Select("select * from account where account_code = #{accountCode}")
    Account selectByCode(String accountCode);
}

介面測試

入參:

{
	"accountCode":"javadaily",
	"productCode":"P004",
	"count":10,
	"price":0.5
}

使用postman呼叫:http://localhost:8040/order/create,結果如下:

檢視account、product、order表,資料都有變化了。

order-service、product-service、account-service分屬不同的服務,當其中一個服務丟擲異常無法提交時就會導致分散式事務,如當使用feign呼叫account-service執行扣減賬戶餘額時,account-service校驗賬戶餘額不足丟擲異常,但是order-service的儲存操作不會回滾;或者是前兩步執行成功但是product-service校驗不通過前面的操作也不會回滾,這就導致了資料不一致,也就是分散式事務問題!

Seata解決方案