JeecgBoot 3.0
seata版本 : 1.3.0
先创建3个数据库,加上jeecg-boot自有的数据库,一共4个数据库
首先在四个数据库中引入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');
<!-- seata-spring-boot-starter --> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency>
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
采用jeecg-boot单体模式测试,使用默认的文件进行seata配置,不需要做额外的配置,直接启动seata-server.bat即可。
项目结构
其中三个实体类对应如下
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); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.jeecg.modules.seata.mapper.ProductMapper"> <resultMap id="BaseResultMap" type="org.jeecg.modules.seata.entity.Product"> <id column="id" jdbcType="INTEGER" property="id"/> <result column="name" jdbcType="VARCHAR" property="name"/> <result column="price" jdbcType="DECIMAL" property="price"/> <result column="stock" jdbcType="INTEGER" property="stock"/> <result column="add_time" jdbcType="TIMESTAMP" property="addTime"/> <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/> </resultMap> <sql id="Base_Column_List"> id, name, price, stock, add_time, update_time </sql> <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap"> select <include refid="Base_Column_List"/> from product where id = #{id,jdbcType=INTEGER} </select> <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer"> delete from product where id = #{id,jdbcType=INTEGER} </delete> <insert id="insert" parameterType="org.jeecg.modules.seata.entity.Product"> 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> <insert id="insertSelective" parameterType="org.jeecg.modules.seata.entity.Product"> insert into product <trim prefix="(" suffix=")" suffixOverrides=","> <if test="id != null"> id, </if> <if test="name != null"> name, </if> <if test="price != null"> price, </if> <if test="stock != null"> stock, </if> <if test="addTime != null"> add_time, </if> <if test="updateTime != null"> update_time, </if> </trim> <trim prefix="values (" suffix=")" suffixOverrides=","> <if test="id != null"> #{id,jdbcType=INTEGER}, </if> <if test="name != null"> #{name,jdbcType=VARCHAR}, </if> <if test="price != null"> #{price,jdbcType=DECIMAL}, </if> <if test="stock != null"> #{stock,jdbcType=INTEGER}, </if> <if test="addTime != null"> #{addTime,jdbcType=TIMESTAMP}, </if> <if test="updateTime != null"> #{updateTime,jdbcType=TIMESTAMP}, </if> </trim> </insert> <update id="updateByPrimaryKeySelective" parameterType="org.jeecg.modules.seata.entity.Product"> update product <set> <if test="name != null"> name = #{name,jdbcType=VARCHAR}, </if> <if test="price != null"> price = #{price,jdbcType=DECIMAL}, </if> <if test="stock != null"> stock = #{stock,jdbcType=INTEGER}, </if> <if test="addTime != null"> add_time = #{addTime,jdbcType=TIMESTAMP}, </if> <if test="updateTime != null"> update_time = #{updateTime,jdbcType=TIMESTAMP}, </if> </set> where id = #{id,jdbcType=INTEGER} </update> <update id="updateByPrimaryKey" parameterType="org.jeecg.modules.seata.entity.Product"> 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> <!--减库存--> <update id="reduceStock"> update product SET stock = stock - #{amount, jdbcType=INTEGER} WHERE id = #{productId, jdbcType=INTEGER} AND stock >= #{amount, jdbcType=INTEGER} </update> </mapper>
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.jeecg.modules.seata.mapper.OrdersMapper"> <resultMap id="BaseResultMap" type="org.jeecg.modules.seata.entity.Orders"> <id column="id" jdbcType="INTEGER" property="id" /> <result column="user_id" jdbcType="INTEGER" property="userId" /> <result column="product_id" jdbcType="INTEGER" property="productId" /> <result column="pay_amount" jdbcType="DECIMAL" property="payAmount" /> <result column="add_time" jdbcType="TIMESTAMP" property="addTime" /> <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" /> </resultMap> <sql id="Base_Column_List"> id, user_id, product_id, pay_amount, add_time, update_time </sql> <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from orders where id = #{id,jdbcType=INTEGER} </select> <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer"> delete from orders where id = #{id,jdbcType=INTEGER} </delete> <insert id="insert" parameterType="org.jeecg.modules.seata.entity.Orders"> 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> <insert id="insertSelective" parameterType="org.jeecg.modules.seata.entity.Orders"> insert into orders <trim prefix="(" suffix=")" suffixOverrides=","> <if test="id != null"> id, </if> <if test="userId != null"> user_id, </if> <if test="productId != null"> product_id, </if> <if test="payAmount != null"> pay_amount, </if> <if test="addTime != null"> add_time, </if> <if test="updateTime != null"> update_time, </if> </trim> <trim prefix="values (" suffix=")" suffixOverrides=","> <if test="id != null"> #{id,jdbcType=INTEGER}, </if> <if test="userId != null"> #{userId,jdbcType=INTEGER}, </if> <if test="productId != null"> #{productId,jdbcType=INTEGER}, </if> <if test="payAmount != null"> #{payAmount,jdbcType=DECIMAL}, </if> <if test="addTime != null"> #{addTime,jdbcType=TIMESTAMP}, </if> <if test="updateTime != null"> #{updateTime,jdbcType=TIMESTAMP}, </if> </trim> </insert> <update id="updateByPrimaryKeySelective" parameterType="org.jeecg.modules.seata.entity.Orders"> update orders <set> <if test="userId != null"> user_id = #{userId,jdbcType=INTEGER}, </if> <if test="productId != null"> product_id = #{productId,jdbcType=INTEGER}, </if> <if test="payAmount != null"> pay_amount = #{payAmount,jdbcType=DECIMAL}, </if> <if test="addTime != null"> add_time = #{addTime,jdbcType=TIMESTAMP}, </if> <if test="updateTime != null"> update_time = #{updateTime,jdbcType=TIMESTAMP}, </if> </set> where id = #{id,jdbcType=INTEGER} </update> <update id="updateByPrimaryKey" parameterType="org.jeecg.modules.seata.entity.Orders"> 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} </update> </mapper>
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.jeecg.modules.seata.mapper.AccountMapper"> <resultMap id="BaseResultMap" type="org.jeecg.modules.seata.entity.Account"> <id column="id" jdbcType="INTEGER" property="id"/> <result column="user_id" jdbcType="INTEGER" property="userId"/> <result column="balance" jdbcType="DECIMAL" property="balance"/> <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/> </resultMap> <sql id="Base_Column_List"> id, user_id, balance, update_time </sql> <!--根据userId--> <select id="selectAccountByUserId" parameterType="java.lang.Integer" resultMap="BaseResultMap"> select <include refid="Base_Column_List"/> from account where user_id = #{userId, jdbcType=INTEGER} </select> <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap"> select <include refid="Base_Column_List"/> from account where id = #{id,jdbcType=INTEGER} </select> <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer"> delete from account where id = #{id,jdbcType=INTEGER} </delete> <insert id="insert" parameterType="org.jeecg.modules.seata.entity.Account"> insert into account (id, user_id, balance, update_time) values (#{id,jdbcType=INTEGER}, #{userId,jdbcType=INTEGER}, #{balance,jdbcType=DOUBLE}, #{updateTime,jdbcType=TIMESTAMP}) </insert> <insert id="insertSelective" parameterType="org.jeecg.modules.seata.entity.Account"> insert into account <trim prefix="(" suffix=")" suffixOverrides=","> <if test="id != null"> id, </if> <if test="userId != null"> user_id, </if> <if test="balance != null"> balance, </if> <if test="updateTime != null"> update_time, </if> </trim> <trim prefix="values (" suffix=")" suffixOverrides=","> <if test="id != null"> #{id,jdbcType=INTEGER}, </if> <if test="userId != null"> #{userId,jdbcType=INTEGER}, </if> <if test="balance != null"> #{balance,jdbcType=DOUBLE}, </if> <if test="updateTime != null"> #{updateTime,jdbcType=TIMESTAMP}, </if> </trim> </insert> <update id="updateByPrimaryKeySelective" parameterType="org.jeecg.modules.seata.entity.Account"> update account <set> <if test="userId != null"> user_id = #{userId,jdbcType=INTEGER}, </if> <if test="balance != null"> balance = #{balance,jdbcType=DOUBLE}, </if> <if test="updateTime != null"> update_time = #{updateTime,jdbcType=TIMESTAMP}, </if> </set> where id = #{id,jdbcType=INTEGER} </update> <update id="updateByPrimaryKey" parameterType="org.jeecg.modules.seata.entity.Account"> update account set user_id = #{userId,jdbcType=INTEGER}, balance = #{balance,jdbcType=DOUBLE}, update_time = #{updateTime,jdbcType=TIMESTAMP} where id = #{id,jdbcType=INTEGER} </update> <!--减余额--> <update id="reduceBalance"> update account SET balance = balance - #{money} WHERE user_id = #{userId, jdbcType=INTEGER} AND balance >= ${money} </update> </mapper>
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 = "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 = "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 = "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); } }
在浏览器请求
http://localhost:8080/jeecg-boot/order?userId=1&productId=1
正常提交,数据库数据都是正常的。
http://localhost:8080/jeecg-boot/order?userId=2&productId=1
更新异常,数据回滚。