SpringBoot 整合 Seata

SpringBoot 整合 Seata附代码

spring-boot.version :2.3.0.RELEASE
nacos-client : 1.3.1
seata版本 : 1.3.0

1. 适用场景

  1. 一个SpringBoot的单体项目整合多个关系型数据库。多数据源。
    在这里插入图片描述

2. Seata 地址

http://seata.io/zh-cn/docs/overview/what-is-seata.html

3.案例说明

模拟一个用户下订单场景。

  1. 创建三个数据库:用户库、商品库、订单库。
  2. SpringBoot 项目配置三个数据库。
  3. 订单controller–>订单service(调用商品service、用户service),各自service在调用各自的dao层

4.项目结构展示

在这里插入图片描述

5. 代码实现

1.pom.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zlw</groupId>
    <artifactId>seata-distributed-transaction</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>seata-distributed-transaction</name>
    <description>springboot项目测试seata分布式事务</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.0.RELEASE</spring-boot.version>
        <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- mybatis-spring-boot-starter -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <!-- seata-spring-boot-starter -->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>

        <!-- dynamic-datasource-spring-boot-starter动态数据源 -->
        <!-- 支持多数据源,,一个项目多个库的场景。 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>

        <!-- nacos-client springboot单体项目
        需要单独加一下 nacos的依赖
         不需要向nacos 进行注册的
         -->
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-client</artifactId>
            <version>1.3.1</version>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
        
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>
  1. yml 配置文件
# springboot 整合单体 TC server 配置。
server:
  port: 18080

seata:
  config:
    type: file
  application-id: springboot-seata
  #  enable-auto-data-source-proxy: false
  registry:
    type: file
  #    nacos:
  #      application: seata-server
  #      cluster: default
  #      group: SEATA_GROUP
  #      server-addr: 127.0.0.1:8801,127.0.0.1:8802,127.0.0.1:8803 #192.168.172.128:8848
  #      namespace: le_zi_jie
  #    type: nacos
  #  service:
  #    vgroup-mapping:
  #      springboot-seata-group: default
  service:
    grouplist:
      default: 127.0.0.1:8091
    vgroup-mapping:
      springboot-seata-group: default
  # seata 事务组编号 用于TC集群名
  tx-service-group: springboot-seata-group

spring:
  application:
    name: seata-distributed-transaction
  datasource:
    dynamic:
      datasource:
        # 设置 账号数据源配置
        account-ds:
          driver-class-name: com.mysql.cj.jdbc.Driver
          password: 123456
          url: jdbc:mysql://127.0.0.1:3306/accountdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
          username: root
          # 设置 订单数据源配置
        order-ds:
          driver-class-name: com.mysql.cj.jdbc.Driver
          password: 123456
          url: jdbc:mysql://127.0.0.1:3306/orderdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
          username: root
          # 设置商品 数据源配置
        product-ds:
          driver-class-name: com.mysql.cj.jdbc.Driver
          password: 123456
          url: jdbc:mysql://127.0.0.1:3306/productdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
          username: root
      # 设置默认数据源或者数据源组 默认值即为master
      primary: order-ds   # 默认指定一个数据源
      # 开启对 seata的支持
      seata: true


3.mapper 文件
3.1 product mapper

package com.zlw.seata.mapper;

import com.zlw.seata.mode.Product;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@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);
}
<?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="com.zlw.seata.mapper.ProductMapper">
    <resultMap id="BaseResultMap" type="com.zlw.seata.mode.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="com.zlw.seata.mode.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="com.zlw.seata.mode.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="com.zlw.seata.mode.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="com.zlw.seata.mode.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>

3.2 OrdersMapper

package com.zlw.seata.mapper;

import com.zlw.seata.mode.Orders;
import org.apache.ibatis.annotations.Mapper;

@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);
}
<?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="com.zlw.seata.mapper.OrdersMapper">
  <resultMap id="BaseResultMap" type="com.zlw.seata.mode.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="com.zlw.seata.mode.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="com.zlw.seata.mode.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="com.zlw.seata.mode.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="com.zlw.seata.mode.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>

3.3 AccountMapper

package com.zlw.seata.mapper;

import com.zlw.seata.mode.Account;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

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="com.zlw.seata.mapper.AccountMapper">

    <resultMap id="BaseResultMap" type="com.zlw.seata.mode.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="com.zlw.seata.mode.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="com.zlw.seata.mode.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="com.zlw.seata.mode.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="com.zlw.seata.mode.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>

4.service
4.1 ProductService

package com.zlw.seata.service;


import com.zlw.seata.mode.Product;

public interface ProductService {

    /**
     * 减库存
     *
     * @param productId 商品 ID
     * @param amount    扣减数量
     * @throws Exception 扣减失败时抛出异常
     */
    Product reduceStock(Integer productId, Integer amount) throws Exception;

}
package com.zlw.seata.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.zlw.seata.mapper.ProductMapper;
import com.zlw.seata.mode.Product;
import com.zlw.seata.service.ProductService;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
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;
    }
}

4.2 OrderService

package com.zlw.seata.service;

public interface OrderService {

    /**
     * 下订单
     *
     * @param userId 用户id
     * @param productId 产品id
     * @return 订单id
     * @throws Exception 创建订单失败,抛出异常
     */
    Integer createOrder(Integer userId, Integer productId) throws Exception;

}
package com.zlw.seata.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.zlw.seata.mapper.OrdersMapper;
import com.zlw.seata.mode.Orders;
import com.zlw.seata.mode.Product;
import com.zlw.seata.service.AccountService;
import com.zlw.seata.service.OrderService;
import com.zlw.seata.service.ProductService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
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());

        // 返回订单编号
        return order.getId();
    }
}

4.3 AccountService

package com.zlw.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 com.zlw.seata.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.zlw.seata.mapper.AccountMapper;
import com.zlw.seata.mode.Account;
import com.zlw.seata.service.AccountService;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
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);
    }
}

5 controller 调用

package com.zlw.seata.contorller;

import com.zlw.seata.service.OrderService;
import lombok.extern.slf4j.Slf4j;
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);
    }
}

6.表数据

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;

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;

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;

account 表:
INSERT INTO `account` (`id`, `user_id`, `balance`, `update_time`) VALUES ('1', '1', '200', '2021-01-15 00:02:17');
product 表:
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');



每个库都需要有一个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;

7.seata的配置
在这里插入图片描述
单体SpringBoot使用默认的文件进行seata TCServer的日志记录(集群的时候使用MySQL),此处也可以使用MySQL。


## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "file"

  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }
}

6. 调用测试

6.1 启动seata,seata默认端口是8091
在这里插入图片描述
win 使用cmd 运行seata-server.bat 文件

6.2 启动项目
在这里插入图片描述
可以看到8091 说明项目连接seata TCServer正常,项目启动正常。

浏览器发送请求:

http://localhost:18080/order?userId=1&productId=1

请求之前数据:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
正常请求:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
模拟异常:
二阶段回滚
在这里插入图片描述
在这里插入图片描述

需要自己 配置seata TC服务器。(代码中支持TC单机、TC集群)
https://gitee.com/zhaoxiaoluo/seata-distributed-transaction.git

  • 10
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值