jeecgboot集成seata实战

1. 环境描述
JeecgBoot 3.0
seata版本 : 1.3.0
2.数据库搭建
先创建3个数据库,加上jeecg-boot自有的数据库,一共4个数据库
jeecgboot集成seata实战
文章图片

首先在四个数据库中引入undo_log表

CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=137 DEFAULT CHARSET=utf8;

在jeecg-account中,创建表并插入数据
CREATE TABLE `account` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) DEFAULT NULL, `balance` int(11) DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; INSERT INTO `account` (`id`, `user_id`, `balance`, `update_time`) VALUES ('1', '1', '200', '2021-01-15 00:02:17');

在jeecg-order库中,创建表
CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) DEFAULT NULL, `product_id` int(11) DEFAULT NULL, `pay_amount` int(11) DEFAULT NULL, `add_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=50 DEFAULT CHARSET=utf8mb4;

在jeecg-product中,创建表并插入数据
CREATE TABLE `product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `price` int(11) DEFAULT NULL, `stock` int(11) DEFAULT NULL, `add_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; INSERT INTO `product` (`id`, `name`, `price`, `stock`, `add_time`, `update_time`) VALUES ('1', '电池', '10', '67', '2021-01-15 00:00:32', '2021-01-15 00:00:35');

3. 坐标引入
io.seata seata-spring-boot-starter 1.3.0

4. yml配置文件
seata: config: type: file application-id: springboot-seata #enable-auto-data-source-proxy: false registry: type: file service: grouplist: default: 127.0.0.1:8091 vgroup-mapping: springboot-seata-group: default # seata 事务组编号 用于TC集群名 tx-service-group: springboot-seata-group spring: datasource: dynamic: datasource: master: url: jdbc:mysql://127.0.0.1:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver # 设置 账号数据源配置 account-ds: driver-class-name: com.mysql.cj.jdbc.Driver password: root url: jdbc:mysql://127.0.0.1:3306/jeecg-account?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false username: root # 设置 订单数据源配置 order-ds: driver-class-name: com.mysql.cj.jdbc.Driver password: root url: jdbc:mysql://127.0.0.1:3306/jeecg-order?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false username: root # 设置商品 数据源配置 product-ds: driver-class-name: com.mysql.cj.jdbc.Driver password: root url: jdbc:mysql://127.0.0.1:3306/jeecg-product?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false username: root# 设置默认数据源或者数据源组 默认值即为master primary: master# 默认指定一个数据源 # 开启对 seata的支持 seata: true

5. Seata启动
采用jeecg-boot单体模式测试,使用默认的文件进行seata配置,不需要做额外的配置,直接启动seata-server.bat即可。
6. 代码编写
项目结构
jeecgboot集成seata实战
文章图片

其中三个实体类对应如下
package org.jeecg.modules.seata.entity; import lombok.Data; import java.math.BigDecimal; import java.util.Date; @Data public class Orders { private Integer id; private Integer userId; private Integer productId; private BigDecimal payAmount; private Date addTime; private Date updateTime; }

package org.jeecg.modules.seata.entity; import lombok.Data; import java.math.BigDecimal; import java.util.Date; @Data public class Product { private Integer id; private String name; private BigDecimal price; private Integer stock; private Date addTime; private Date updateTime; }

package org.jeecg.modules.seata.entity; import lombok.Data; import java.math.BigDecimal; import java.util.Date; @Data public class Account { private Integer id; private Integer userId; private BigDecimal balance; private Date updateTime; }

Mapper对应代码如下
package org.jeecg.modules.seata.mapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.jeecg.modules.seata.entity.Product; @Mapper public interface ProductMapper { int deleteByPrimaryKey(Integer id); int insert(Product record); int insertSelective(Product record); Product selectByPrimaryKey(Integer id); int updateByPrimaryKeySelective(Product record); int updateByPrimaryKey(Product record); int reduceStock(@Param("productId") Integer productId, @Param("amount") Integer amount); }

package org.jeecg.modules.seata.mapper; import org.apache.ibatis.annotations.Mapper; import org.jeecg.modules.seata.entity.Orders; @Mapper public interface OrdersMapper { int deleteByPrimaryKey(Integer id); int insert(Orders record); int insertSelective(Orders record); Orders selectByPrimaryKey(Integer id); int updateByPrimaryKeySelective(Orders record); int updateByPrimaryKey(Orders record); }

package org.jeecg.modules.seata.mapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.jeecg.modules.seata.entity.Account; import java.math.BigDecimal; @Mapper public interface AccountMapper { int deleteByPrimaryKey(Integer id); int insert(Account record); int insertSelective(Account record); Account selectByPrimaryKey(Integer id); Account selectAccountByUserId(Integer userId); int updateByPrimaryKeySelective(Account record); int updateByPrimaryKey(Account record); int reduceBalance(@Param("userId") Integer userId, @Param("money") BigDecimal money); }

id, name, price, stock, add_time, update_timeselect from product where id = #{id,jdbcType=INTEGER} delete from product where id = #{id,jdbcType=INTEGER} insert into product (id, name, price, stock, add_time, update_time ) values (#{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR}, #{price,jdbcType=DECIMAL}, #{stock,jdbcType=INTEGER}, #{addTime,jdbcType=TIMESTAMP}, #{updateTime,jdbcType=TIMESTAMP} ) insert into product id, name, price, stock, add_time, update_time, #{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR}, #{price,jdbcType=DECIMAL}, #{stock,jdbcType=INTEGER}, #{addTime,jdbcType=TIMESTAMP}, #{updateTime,jdbcType=TIMESTAMP}, update product name = #{name,jdbcType=VARCHAR}, price = #{price,jdbcType=DECIMAL}, stock = #{stock,jdbcType=INTEGER}, add_time = #{addTime,jdbcType=TIMESTAMP}, update_time = #{updateTime,jdbcType=TIMESTAMP}, where id = #{id,jdbcType=INTEGER} update product set name = #{name,jdbcType=VARCHAR}, price = #{price,jdbcType=DECIMAL}, stock = #{stock,jdbcType=INTEGER}, add_time = #{addTime,jdbcType=TIMESTAMP}, update_time = #{updateTime,jdbcType=TIMESTAMP} where id = #{id,jdbcType=INTEGER} update product SET stock = stock - #{amount, jdbcType=INTEGER} WHERE id = #{productId, jdbcType=INTEGER} AND stock >= #{amount, jdbcType=INTEGER}

id, user_id, product_id, pay_amount, add_time, update_timeselect from orders where id = #{id,jdbcType=INTEGER} delete from orders where id = #{id,jdbcType=INTEGER} insert into orders (id, user_id, product_id, pay_amount, add_time, update_time ) values (#{id,jdbcType=INTEGER}, #{userId,jdbcType=INTEGER}, #{productId,jdbcType=INTEGER}, #{payAmount,jdbcType=DECIMAL}, #{addTime,jdbcType=TIMESTAMP}, #{updateTime,jdbcType=TIMESTAMP} ) insert into orders id, user_id, product_id, pay_amount, add_time, update_time, #{id,jdbcType=INTEGER}, #{userId,jdbcType=INTEGER}, #{productId,jdbcType=INTEGER}, #{payAmount,jdbcType=DECIMAL}, #{addTime,jdbcType=TIMESTAMP}, #{updateTime,jdbcType=TIMESTAMP}, update orders user_id = #{userId,jdbcType=INTEGER}, product_id = #{productId,jdbcType=INTEGER}, pay_amount = #{payAmount,jdbcType=DECIMAL}, add_time = #{addTime,jdbcType=TIMESTAMP}, update_time = #{updateTime,jdbcType=TIMESTAMP}, where id = #{id,jdbcType=INTEGER} update orders set user_id = #{userId,jdbcType=INTEGER}, product_id = #{productId,jdbcType=INTEGER}, pay_amount = #{payAmount,jdbcType=DECIMAL}, add_time = #{addTime,jdbcType=TIMESTAMP}, update_time = #{updateTime,jdbcType=TIMESTAMP} where id = #{id,jdbcType=INTEGER}

id, user_id, balance, update_time select from account where user_id = #{userId, jdbcType=INTEGER} select from account where id = #{id,jdbcType=INTEGER} delete from account where id = #{id,jdbcType=INTEGER} insert into account (id, user_id, balance, update_time) values (#{id,jdbcType=INTEGER}, #{userId,jdbcType=INTEGER}, #{balance,jdbcType=DOUBLE}, #{updateTime,jdbcType=TIMESTAMP}) insert into account id, user_id, balance, update_time, #{id,jdbcType=INTEGER}, #{userId,jdbcType=INTEGER}, #{balance,jdbcType=DOUBLE}, #{updateTime,jdbcType=TIMESTAMP}, update account user_id = #{userId,jdbcType=INTEGER}, balance = #{balance,jdbcType=DOUBLE}, update_time = #{updateTime,jdbcType=TIMESTAMP}, where id = #{id,jdbcType=INTEGER} update account set user_id = #{userId,jdbcType=INTEGER}, balance = #{balance,jdbcType=DOUBLE}, update_time = #{updateTime,jdbcType=TIMESTAMP} where id = #{id,jdbcType=INTEGER} update account SET balance = balance - #{money} WHERE user_id = #{userId, jdbcType=INTEGER} AND balance >= ${money}

Service对应的代码如下
package org.jeecg.modules.seata.service; import org.jeecg.modules.seata.entity.Product; public interface ProductService { /** * 减库存 * * @param productId 商品 ID * @param amount扣减数量 * @throws Exception 扣减失败时抛出异常 */ Product reduceStock(Integer productId, Integer amount) throws Exception; }

package org.jeecg.modules.seata.service; public interface OrderService { /** * 下订单 * * @param userId 用户id * @param productId 产品id * @return 订单id * @throws Exception 创建订单失败,抛出异常 */ Integer createOrder(Integer userId, Integer productId) throws Exception; }

package org.jeecg.modules.seata.service; import java.math.BigDecimal; public interface AccountService { /** * 减余额 * * @param userId 用户id * @param money扣减金额 * @throws Exception 失败时抛出异常 */ void reduceBalance(Integer userId, BigDecimal money) throws Exception; }

package org.jeecg.modules.seata.service.impl; import com.baomidou.dynamic.datasource.annotation.DS; import io.seata.core.context.RootContext; import lombok.extern.slf4j.Slf4j; import org.jeecg.modules.seata.entity.Product; import org.jeecg.modules.seata.mapper.ProductMapper; import org.jeecg.modules.seata.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Slf4j @Service public class ProductServiceImpl implements ProductService { @Autowired private ProductMapper productMapper; @Override @DS(value = "https://www.it610.com/article/product-ds") public Product reduceStock(Integer productId, Integer amount) throws Exception { log.info("当前 XID: {}", RootContext.getXID()); // 检查库存 Product product = productMapper.selectByPrimaryKey(productId); if (product.getStock() < amount) { throw new Exception("库存不足"); } // 扣减库存 int updateCount = productMapper.reduceStock(productId, amount); // 扣除成功 if (updateCount == 0) { throw new Exception("库存不足"); } // 扣除成功 log.info("扣除 {} 库存成功", productId); return product; } }

package org.jeecg.modules.seata.service.impl; import com.baomidou.dynamic.datasource.annotation.DS; import io.seata.core.context.RootContext; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.jeecg.modules.seata.entity.Orders; import org.jeecg.modules.seata.entity.Product; import org.jeecg.modules.seata.mapper.OrdersMapper; import org.jeecg.modules.seata.service.AccountService; import org.jeecg.modules.seata.service.OrderService; import org.jeecg.modules.seata.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.math.BigDecimal; @Slf4j @Service public class OrderServiceImpl implements OrderService { @Autowired private OrdersMapper ordersMapper; @Autowired private AccountService accountService; @Autowired private ProductService productService; @Override @DS(value = "https://www.it610.com/article/order-ds") @GlobalTransactional //seata全局事务注解 public Integer createOrder(Integer userId, Integer productId) throws Exception { Integer amount = 1; // 购买数量暂时设置为 1 log.info("当前 XID: {}", RootContext.getXID()); // 减库存 - 远程服务 Product product = productService.reduceStock(productId, amount); // 减余额 - 远程服务 accountService.reduceBalance(userId, product.getPrice()); // 下订单 - 本地下订单 Orders order = new Orders(); order.setUserId(userId); order.setProductId(productId); order.setPayAmount(product.getPrice().multiply(new BigDecimal(amount))); ordersMapper.insertSelective(order); log.info("下订单: {}", order.getId()); //int a = 1/0; // 返回订单编号 return order.getId(); } }

package org.jeecg.modules.seata.service.impl; import com.baomidou.dynamic.datasource.annotation.DS; import io.seata.core.context.RootContext; import lombok.extern.slf4j.Slf4j; import org.jeecg.modules.seata.entity.Account; import org.jeecg.modules.seata.mapper.AccountMapper; import org.jeecg.modules.seata.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.math.BigDecimal; @Slf4j @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountMapper accountMapper; @Override @DS(value = "https://www.it610.com/article/account-ds") public void reduceBalance(Integer userId, BigDecimal money) throws Exception { log.info("当前 XID: {}", RootContext.getXID()); // 检查余额 Account account = accountMapper.selectAccountByUserId(userId); if (account.getBalance().doubleValue() < money.doubleValue()) { throw new Exception("余额不足"); } // 扣除余额 int updateCount = accountMapper.reduceBalance(userId, money); // 扣除成功 if (updateCount == 0) { throw new Exception("余额不足"); } log.info("扣除用户 {} 余额成功", userId); } }

controller对应的代码如下
package org.jeecg.modules.seata.controller; import lombok.extern.slf4j.Slf4j; import org.jeecg.modules.seata.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Slf4j //lombok @RestController public class OrderController { @Autowired private OrderService orderService; @RequestMapping("/order") public Integer createOrder(@RequestParam("userId") Integer userId, @RequestParam("productId") Integer productId) throws Exception { log.info("请求下单, 用户:{}, 商品:{}", userId, productId); return orderService.createOrder(userId, productId); } }

7. 测试结果
在浏览器请求
http://localhost:8080/jeecg-b...
jeecgboot集成seata实战
文章图片

正常提交,数据库数据都是正常的。
http://localhost:8080/jeecg-b...
jeecgboot集成seata实战
文章图片

【jeecgboot集成seata实战】更新异常,数据回滚。

    推荐阅读