Seata AT模式案例讲解(Spring)

运行环境:

  • Spring 5.0.2.RELEASE
  • dubbo 2.7.15
  • mysql 8.0.23
  • jdk 1.8
  • Zookeeper 3.4.8

项目结构:

父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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.harvey</groupId>
    <artifactId>seata-dubbo-spring-zk</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>seata-dubbo-client</module>
        <module>seata-dubbo-storage</module>
        <module>seata-dubbo-order</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.version>5.0.2.RELEASE</spring.version>
        <jackson.version>2.11.4</jackson.version>
        <dubbo.version>2.7.15</dubbo.version>
        <zookeeper.version>3.4.8</zookeeper.version>
        <curator.version>2.12.0</curator.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-framework-bom</artifactId>
                <version>${spring.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-bom</artifactId>
                <version>${dubbo.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-dependencies-zookeeper</artifactId>
                <version>${dubbo.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <excludes>
                    <exclude>**/.svn/*</exclude>
                </excludes>
            </resource>
            <resource>
                <directory>src/main/java</directory>
                <excludes>
                    <exclude>**/.svn/*</exclude>
                </excludes>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>2.5</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                        <encoding>UTF-8</encoding>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>

seata-dubbo-client

该模块主要定义暴露的一些库存接口,供其他服务调用(即服务提供者)。

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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>seata-dubbo-spring-zk</artifactId>
        <groupId>com.harvey</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>seata-dubbo-client</artifactId>
    <!--这里要加上个版本,否则其他应用引用不到-->
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

</project>

StorageDto.java

public class StorageDto implements Serializable {

    /**
     * 库存id
     */
    private Long storageId;

    /**
     * 订单ID
     */
    private Long orderId;
    /**
     * 商品编码
     */
    private String productCode;
    /**
     * 当前订单的购买数量
     */
    private Integer quantity;

    /**
     * 商品名称
     */
    private String productName;

    /**
     * 商品单价
     */
    private BigDecimal productUnit;

    /**
     * 版本号
     */
    private Long version;


    public Long getStorageId() {
        return storageId;
    }

    public void setStorageId(Long storageId) {
        this.storageId = storageId;
    }

    public Long getOrderId() {
        return orderId;
    }

    public void setOrderId(Long orderId) {
        this.orderId = orderId;
    }

    public String getProductCode() {
        return productCode;
    }

    public void setProductCode(String productCode) {
        this.productCode = productCode;
    }

    public Integer getQuantity() {
        return quantity;
    }

    public void setQuantity(Integer quantity) {
        this.quantity = quantity;
    }

    public String getProductName() {
        return productName;
    }

    public void setProductName(String productName) {
        this.productName = productName;
    }

    public BigDecimal getProductUnit() {
        return productUnit;
    }

    public void setProductUnit(BigDecimal productUnit) {
        this.productUnit = productUnit;
    }

    public Long getVersion() {
        return version;
    }

    public void setVersion(Long version) {
        this.version = version;
    }

}

StorageReduceDubboService.java

public interface StorageReduceDubboService {

    /**
     * 扣减库存
     * @param storageDto
     * @return
     */
    boolean reduceProductStorage(StorageDto storageDto);

    /**
     * 回退库存
     * @param storageDto
     * @return
     */
    boolean rollProductStorage(StorageDto storageDto);

    /**
     * 查询商品信息
     * @param productCode
     * @return
     */
    StorageDto getProduct(String productCode);

}

seata-dubbo-storage

该模块是seata-dubbo-client定义的接口的具体实现。

1、设计库表

1)创建tz_storage库

2)创建tc_storage表

CREATE TABLE `tc_storage` (
  `storage_id` bigint unsigned NOT NULL,
  `product_code` varchar(255) COLLATE utf8mb4_bin NOT NULL,
  `quantity` int NOT NULL DEFAULT '0',
  `version` bigint unsigned NOT NULL DEFAULT '0',
  `product_name` varchar(255) COLLATE utf8mb4_bin NOT NULL,
  `product_unit` decimal(10,2) NOT NULL,
  PRIMARY KEY (`storage_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

3)插入数据

INSERT INTO `tz_storage`.`tc_storage` (`storage_id`, `product_code`, `quantity`, `version`, `product_name`, `product_unit`) 
VALUES ('10001', '100010001', '100', '0', '华为Mate 40', '4599.00');
INSERT INTO `tz_storage`.`tc_storage` (`storage_id`, `product_code`, `quantity`, `version`, `product_name`, `product_unit`) 
VALUES ('10002', '100020002', '100', '0', '小米6', '1499.00');
INSERT INTO `tz_storage`.`tc_storage` (`storage_id`, `product_code`, `quantity`, `version`, `product_name`, `product_unit`)
VALUES ('10003', '100030003', '100', '0', 'HUAWEI nova 7', '2999.00');
INSERT INTO `tz_storage`.`tc_storage` (`storage_id`, `product_code`, `quantity`, `version`, `product_name`, `product_unit`) 
VALUES ('10004', '100040004', '100', '0', '苹果12', '7999.00');
INSERT INTO `tz_storage`.`tc_storage` (`storage_id`, `product_code`, `quantity`, `version`, `product_name`, `product_unit`) 
VALUES ('10005', '100050005', '100', '0', 'OPPO Find X', '3999.00');

2、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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>seata-dubbo-spring-zk</artifactId>
        <groupId>com.harvey</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>seata-dubbo-storage</artifactId>

    <dependencies>

        <dependency>
            <groupId>com.harvey</groupId>
            <artifactId>seata-dubbo-client</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!--dubbo依赖-->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-context</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-registry-zookeeper</artifactId>
        </dependency>

        <!-- zookeeper客户端:curator-framework。我们使用的zookeeper服务器版本一般与引入的版本一致-->
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>${zookeeper.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>${curator.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>${curator.version}</version>
        </dependency>
        <dependency>
            <groupId>com.101tec</groupId>
            <artifactId>zkclient</artifactId>
            <version>0.11</version>
        </dependency>

        <!-- Spring 基础jar导入 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-expression</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>

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

        <!--dbcp数据库连接池-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.8.0</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>

</project>

3、配置web.xml

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>seata demo provider</display-name>

    <!--Spring 配置-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:applicationContext.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>

4、配置applicationContext.xml

包括包扫描、数据源、本地事务

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:beans="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">


    <!--扫描包,排除注解是@Controller和@RestController的-->
    <context:component-scan base-package="com.harvey">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:exclude-filter type="annotation" expression="org.springframework.web.bind.annotation.RestController"/>
    </context:component-scan>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/tz_storage?useUnicode=true&amp;rewriteBatchedStatements=true&amp;serverTimezone=Asia/Shanghai"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <property name="initialSize" value="20"/>
        <property name="maxIdle" value="5"/>
    </bean>


    <!--JDBC操作模板类-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--本地事务配置-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <!--当前服务的应用名称-->
    <dubbo:application name="dubbo-spring-storage"/>

    <!--注册中心-->
    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <!--通信规则(通信协议|通信端口)-->
    <dubbo:protocol name="dubbo" port="-1"/>

</beans>

5、添加Dubbo暴露接口的包扫描配置

@Configuration
@DubboComponentScan(basePackages = {"com.harvey"})
public class DubboConfig {
}

6、StorageReduceService.java

public interface StorageReduceService {

    boolean reduceProductStorage(StorageDto storageDto);

    boolean rollProductStorage(StorageDto storageDto);

    StorageDto getProduct(String productCode);

}

StorageReduceServiceImpl.java

@Service
public class StorageReduceServiceImpl implements StorageReduceService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public boolean reduceProductStorage(StorageDto storageDto) {
        String sql = "update tc_storage set quantity = quantity - ?, version = version+1 where product_code = ? and version = ? and quantity > 0";
        return jdbcTemplate.update(sql, storageDto.getQuantity(), storageDto.getProductCode(), storageDto.getVersion()) > 0;
    }

    @Override
    public boolean rollProductStorage(StorageDto storageDto) {
        String sql = "update tc_storage set quantity = quantity + ?, version=version+1 where product_code = ? and version = ?";
        return jdbcTemplate.update(sql, storageDto.getQuantity(), storageDto.getProductCode(), storageDto.getVersion()) > 0;
    }

    @Override
    public StorageDto getProduct(String productCode) {
        String sql = "select * from tc_storage where product_code = ?";
        return jdbcTemplate.queryForObject(sql, new RowMapper<StorageDto>() {
            @Override
            public StorageDto mapRow(ResultSet resultSet, int i) throws SQLException {
                StorageDto storageDto = new StorageDto();
                storageDto.setProductCode(resultSet.getString("product_code"));
                storageDto.setProductName(resultSet.getString("product_name"));
                storageDto.setQuantity(resultSet.getInt("quantity"));
                storageDto.setVersion(resultSet.getLong("version"));
                storageDto.setStorageId(resultSet.getLong("storage_id"));
                storageDto.setProductUnit(resultSet.getBigDecimal("product_unit"));
                return storageDto;
            }
        }, productCode);
    }
}

7、Dubbo暴露接口

StorageReduceDubboService接口的具体实现

@DubboService
public class StorageReduceDubboServiceImpl implements StorageReduceDubboService {


    @Autowired
    private StorageReduceService storageReduceService;

    /**
     * 扣减库存
     *
     * @param storageDto
     * @return
     */
    @Override
    public boolean reduceProductStorage(StorageDto storageDto) {
        return storageReduceService.reduceProductStorage(storageDto);
    }

    /**
     * 回退库存
     *
     * @param storageDto
     * @return
     */
    @Override
    public boolean rollProductStorage(StorageDto storageDto) {
        return storageReduceService.rollProductStorage(storageDto);
    }

    /**
     * 查询商品信息
     *
     * @param productCode
     * @return
     */
    @Override
    public StorageDto getProduct(String productCode) {
        return storageReduceService.getProduct(productCode);
    }
}

7、启动类

/**
 * 启动类,如果仅仅只是暴露服务,没必要用容器启动
 */
public class StorageStarter {

    public static void main(String[] args) throws InterruptedException {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"applicationContext.xml"});
        context.start();
        System.out.println("provider service start ......");
        new CountDownLatch(1).await();
    }
}

注:使用jar方式启动项目,必须在pom.xml配置如下内容:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>1.2.1</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <transformers>
                    <!-- 不覆盖同名文件,而是追加合并同名文件 -->
                    <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                        <resource>META-INF/spring.handlers</resource>
                    </transformer>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                        <resource>META-INF/spring.schemas</resource>
                    </transformer>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                        <resource>META-INF/spring.tooling</resource>
                    </transformer>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.harvey.storage.StorageStarter</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

seata-dubbo-order

服务消费者,调用服务提供者完成业务。

1、设计库表

1)创建tz_order数据库

2)创建tc_order表

CREATE TABLE `tc_order` (
  `order_id` bigint unsigned NOT NULL,
  `order_num` varchar(100) COLLATE utf8mb4_bin NOT NULL,
  `product_code` varchar(50) COLLATE utf8mb4_bin NOT NULL,
  `product_unit` decimal(10,2) NOT NULL,
  `quantity` int NOT NULL DEFAULT '0',
  `user_id` bigint unsigned NOT NULL,
  PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

2、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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>seata-dubbo-spring-zk</artifactId>
        <groupId>com.harvey</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>seata-dubbo-order</artifactId>

    <packaging>war</packaging>

    <dependencies>
        <dependency>
            <groupId>com.harvey</groupId>
            <artifactId>seata-dubbo-client</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!--dubbo依赖-->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-context</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-registry-zookeeper</artifactId>
        </dependency>

        <!-- zookeeper客户端:curator-framework。我们使用的zookeeper服务器版本一般与引入的版本一致-->
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>${zookeeper.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>${curator.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>${curator.version}</version>
        </dependency>
        <dependency>
            <groupId>com.101tec</groupId>
            <artifactId>zkclient</artifactId>
            <version>0.11</version>
        </dependency>

        <!-- Spring 基础jar导入 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-expression</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>

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

        <!--dbcp数据库连接池-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.8.0</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>

        <!--start > Test Dependencies -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!--end > Test Dependencies-->

        <!--Jackson-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>${jackson.version}</version>
        </dependency>

    </dependencies>

</project>

3、工具类

OrderCoderUtil.java

/**
 * @Desc:   * 订单编码码生成器,生成32位数字编码,
 * @生成规则 1位单号类型+17位时间戳+14位(用户id加密&随机数)
 */
public final class OrderCoderUtil {


    /** 订单类别头 */
    private static final String ORDER_CODE = "1";
    /** 退货类别头 */
    private static final String RETURN_ORDER = "2";
    /** 退款类别头 */
    private static final String REFUND_ORDER = "3";
    /** 未付款重新支付别头 */
    private static final String AGAIN_ORDER = "4";
    /** 随即编码 */
    private static final int[] r = new int[]{7, 9, 6, 2, 8 , 1, 3, 0, 5, 4};
    /** 用户id和随机数总长度 */
    private static final int maxLength = 14;


    /**
     * 生成订单单号编码
     * @param userId
     */
    public static String getOrderCode(Long userId){
        return ORDER_CODE + getCode(userId);
    }


    /**
     * 生成退货单号编码
     * @param userId
     */
    public static String getReturnCode(Long userId){
        return RETURN_ORDER + getCode(userId);
    }


    /**
     * 生成退款单号编码
     * @param userId
     */
    public static String getRefundCode(Long userId){
        return REFUND_ORDER + getCode(userId);
    }

    /**
     * 未付款重新支付
     * @param userId
     */
    public static String getAgainCode(Long userId){
        return AGAIN_ORDER + getCode(userId);
    }



    /**
     * 更具id进行加密+加随机数组成固定长度编码
     */
    private static String toCode(Long id) {
        String idStr = id.toString();
        StringBuilder idsbs = new StringBuilder();
        for (int i = idStr.length() - 1 ; i >= 0; i--) {
            idsbs.append(r[idStr.charAt(i)-'0']);
        }
        return idsbs.append(getRandom(maxLength - idStr.length())).toString();
    }

    /**
     * 生成时间戳
     */
    private static String getDateTime(){
        DateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        return sdf.format(new Date());
    }

    /**
     * 生成固定长度随机码
     * @param n    长度
     */
    private static long getRandom(long n) {
        long min = 1,max = 9;
        for (int i = 1; i < n; i++) {
            min *= 10;
            max *= 10;
        }
        long rangeLong = (((long) (new Random().nextDouble() * (max - min)))) + min ;
        return rangeLong;
    }

    /**
     * 生成不带类别标头的编码
     * @param userId
     */
    private static synchronized String getCode(Long userId){
        userId = userId == null ? 10000 : userId;
        return getDateTime() + toCode(userId);
    }
}

IdUtil.java

public final class IdUtil {

    private static final String DATE_PATTERN = "yyyyMMdd";

    private IdUtil() {
    }

    /**
     * 生成18位数字
     * 说明:循环10万左右会有重复ID
     *
     * @return
     */
    public static Long generate18Number() {
        //随机生成一位整数
        int random = (int) (Math.random() * 9 + 1);
        String prefix = String.valueOf(random);
        //格式化日期,生成8位数字
        String middle = LocalDate.now().format(DateTimeFormatter.ofPattern(DATE_PATTERN));
        //生成uuid的hashCode值
        int hashCode = UUID.randomUUID().toString().hashCode();
        //可能为负数
        if (hashCode < 0) {
            hashCode = -hashCode;
        }
        String code = String.valueOf(hashCode);
        if (code.length() > 9) {
            hashCode = Integer.parseInt(code.substring(1));
        }
        String value = prefix + middle + String.format("%09d", hashCode);
        return Long.valueOf(value);
    }
}

4、配置web.xml

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>seata demo provider</display-name>

    <!--Spring 配置-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:applicationContext.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!--Spring MVC 配置-->
    <servlet>
        <servlet-name>SpringMVC</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath*:springMVC.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>SpringMVC</servlet-name>
        <url-pattern>*.action</url-pattern>
    </servlet-mapping>


</web-app>

5、配置applicationContext.xml和springMVC.xml

包括包扫描、数据源、本地事务、dubbo配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:beans="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">


    <!--扫描包,排除注解是@Controller和@RestController的-->
    <context:component-scan base-package="com.harvey">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:exclude-filter type="annotation" expression="org.springframework.web.bind.annotation.RestController"/>
    </context:component-scan>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/tz_order?useUnicode=true&amp;rewriteBatchedStatements=true&amp;serverTimezone=Asia/Shanghai"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <property name="initialSize" value="20"/>
        <property name="maxIdle" value="5"/>
    </bean>


    <!--JDBC操作模板类-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--本地事务配置-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <!--当前服务的应用名称-->
    <dubbo:application name="dubbo-spring-order"/>

    <!--注册中心-->
    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <!--通信规则(通信协议|通信端口)-->
    <dubbo:protocol name="dubbo" port="-1"/>

</beans>

springMVC.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:beans="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--开启注解-->
    <mvc:annotation-driven/>

    <!--扫描包含@Controller和@RestController-->
    <context:component-scan base-package="com.harvey">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.RestController"/>
    </context:component-scan>


    <!-- 配置消息转换器,完成请求和注解POJO的映射  -->
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="messageConverters">
            <list>
                <ref bean="mappingJacksonHttpMessageConverter"/>
            </list>
        </property>
    </bean>

    <bean id="mappingJacksonHttpMessageConverter"
          class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
        <property name="supportedMediaTypes">
            <list>
                <value>application/json;charset=UTF-8</value>
            </list>
        </property>
    </bean>

</beans>

6、添加Dubbo注解的包扫描配置

@Configuration
@DubboComponentScan(basePackages = {"com.harvey"})
public class DubboConfig {

}

7、业务操作

OrderDao.java

public interface OrderDao {

    int saveOrder(OrderBO orderBO);

    List<OrderBO> findOrders(Long userId);
}

OrderDaoImpl.java

@Repository
public class OrderDaoImpl implements OrderDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public int saveOrder(OrderBO orderBO) {
        String saveSql = "insert into tc_order(order_id, order_num, product_code, product_unit, quantity, user_id) " +
                "values(?, ?, ?, ?, ?, ?)";
        return jdbcTemplate.update(saveSql, orderBO.getOrderId(), orderBO.getOrderNum(),
                orderBO.getProductCode(), orderBO.getProductUnit(), orderBO.getQuantity(), orderBO.getUserId());
    }

    @Override
    public List<OrderBO> findOrders(Long userId) {
        String querySql = "select * from tc_order where user_id = ?";
        List<Map<String, Object>> mapList = jdbcTemplate.queryForList(querySql, userId);
        List<OrderBO> orderBOList = new ArrayList();
        for(Map<String, Object> item : mapList){
            OrderBO orderBO = new OrderBO();
            orderBO.setOrderId(Long.parseLong(item.get("order_id").toString()));
            orderBO.setOrderNum(item.get("order_num").toString());
            orderBO.setUserId(Long.parseLong(item.get("user_id").toString()));
            orderBO.setProductCode(item.get("product_code").toString());
            orderBO.setProductUnit(new BigDecimal(item.get("product_unit").toString()));
            orderBO.setQuantity(Integer.parseInt(item.get("quantity").toString()));
            orderBO.setTotalPrice(new BigDecimal(item.get("product_unit").toString()).multiply(new BigDecimal(orderBO.getQuantity())));
            orderBOList.add(orderBO);
        }
        return orderBOList;
    }
}

实体类OrderBO.java

/**
 * 订单数据
 */
public class OrderBO implements Serializable {

    /**
     * 订单ID
     */
    private Long orderId;
    /**
     * 订单编号
     */
    private String orderNum;
    /**
     * 订单总价
     */
    private BigDecimal totalPrice;
    /**
     * 商品编码
     */
    private String productCode;
    /**
     * 商品单价
     */
    private BigDecimal productUnit;
    /**
     * 商品数量
     */
    private Integer quantity;

    /**
     * 用户id
     */
    private Long userId;

    public Long getOrderId() {
        return orderId;
    }

    public void setOrderId(Long orderId) {
        this.orderId = orderId;
    }

    public String getOrderNum() {
        return orderNum;
    }

    public void setOrderNum(String orderNum) {
        this.orderNum = orderNum;
    }

    public BigDecimal getTotalPrice() {
        return totalPrice;
    }

    public void setTotalPrice(BigDecimal totalPrice) {
        this.totalPrice = totalPrice;
    }

    public String getProductCode() {
        return productCode;
    }

    public void setProductCode(String productCode) {
        this.productCode = productCode;
    }

    public BigDecimal getProductUnit() {
        return productUnit;
    }

    public void setProductUnit(BigDecimal productUnit) {
        this.productUnit = productUnit;
    }

    public Integer getQuantity() {
        return quantity;
    }

    public void setQuantity(Integer quantity) {
        this.quantity = quantity;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    
}

OrderService.java

public interface OrderService {

    /**
     * 创建订单
     * @param userId
     * @return
     */
    String createOrder(Long userId, String productCode, Integer quantity);

    /**
     * 查询用户订单
     * @param userId
     * @return
     */
    List<OrderBO> listOrders(Long userId);

}

OrderServiceImpl.java

包括创建订单、调用Dubbo接口扣减库存。

@Service
public class OrderServiceImpl implements OrderService {


    @Autowired
    private OrderDao orderDao;

    @DubboReference
    private StorageReduceDubboService storageReduceDubboService;


    /**
     * 创建订单
     *
     * @param userId
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public String createOrder(Long userId, String productCode, Integer quantity) {
        OrderBO orderBO = new OrderBO();
        orderBO.setUserId(userId);
        StorageDto productDto = storageReduceDubboService.getProduct(productCode);
        orderBO.setProductCode(productDto.getProductCode());
        orderBO.setProductUnit(productDto.getProductUnit());
        orderBO.setQuantity(quantity);
        Long orderId = IdUtil.generate18Number();
        orderBO.setOrderId(orderId);
        String orderNumber = OrderCoderUtil.getOrderCode(userId);
        orderBO.setOrderNum(orderNumber);
        //保存订单
        orderDao.saveOrder(orderBO);
        //扣减库存
        StorageDto storageDto = new StorageDto();
        storageDto.setOrderId(orderId);
        storageDto.setStorageId(productDto.getStorageId());
        storageDto.setQuantity(quantity);
        storageDto.setVersion(productDto.getVersion());
        storageDto.setProductCode(productDto.getProductCode());
        storageReduceDubboService.reduceProductStorage(storageDto);
        return orderNumber;
    }

    /**
     * 查询用户订单
     *
     * @param userId
     * @return
     */
    @Override
    public List<OrderBO> listOrders(Long userId) {
        return orderDao.findOrders(userId);
    }
}

8、controller

@RestController
public class OrderController{

    @Autowired
    private OrderService orderService;

    //order是上下文
    //POST http://localhost:8888/order/createOrder.action?userId=10020201&productCode=100010001&quantity=1
    @RequestMapping("/createOrder")
    public Map<String, Object> createOrder(Long userId, String productCode, Integer quantity) {
        System.out.println("orderService:" + AopUtils.isAopProxy(orderService));
        String orderNum = orderService.createOrder(userId, productCode, quantity);
        Map<String, Object> resultMap = new HashMap();
        resultMap.put("msg", "创建订单成功");
        resultMap.put("orderNum", orderNum);
        return resultMap;
    }
}

9、运行测试

1)先启动Zookeeper(Dubbo注册中心要用到)

2)启动seata-dubbo-storage和seata-dubbo-order

3)使用postman调用controller接口验证

tc_order创建了一条订单记录:

tc_storage库存扣减了

引入Seata分布式事务

Seata 是一款阿里开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。

Seata 中有三大模块,分别是 TM、RM 和 TC。 其中 TM 和 RM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。

Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式。

  • AT:Auto Transaction,基于支持本地ACID事务的关系型数据库,对业务无侵入;
  • MT:Manual Transaction,不依赖于底层数据资源的事务支持,需自定义prepare/commit/rollback操作,对业务有侵入;
  • XA:基于数据库的XA实现,目前最新版seata已实现该模式。
  • TCC:TCC模式,对业务有侵入。

Seata Server配置

registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "zk"

  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "zk"

  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
    #nodePath = "/seata/seata.properties"
  }
}

启动Zookeeper(本人使用的是Zookeeper3.4.8),导入Seata Server的配置:

config.txt:

transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.storage-service-group=default
service.vgroupMapping.order-servive-group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=db
store.publicKey=
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.sentinel.masterName=
store.redis.sentinel.sentinelHosts=
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

一般我们修改的只有这些:

service.vgroupMapping.storage-service-group=default
service.vgroupMapping.order-servive-group=default
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai
store.db.user=root
store.db.password=root

1)将config.txt拷贝到项目的resources目录下,重命名zk-config.properties。然后初始化配置脚本

注意:将配置项都挂在/seata节点下,最终效果:

import io.seata.config.zk.DefaultZkSerializer;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.serialize.ZkSerializer;
import org.apache.zookeeper.CreateMode;
import org.springframework.util.ResourceUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.Set;

public class ZkDataInit {

    private static volatile ZkClient zkClient;

    public static void main(String[] args) {

        if (zkClient == null) {
            ZkSerializer zkSerializer = new DefaultZkSerializer();
            zkClient = new ZkClient("127.0.0.1:2181", 6000, 2000, zkSerializer);
        }
        if (!zkClient.exists("/seata")) {
            zkClient.createPersistent("/seata", true);
        }
        //获取key对应的value值
        Properties properties = new Properties();
        // 使用ClassLoader加载properties配置文件生成对应的输入流
        // 使用properties对象加载输入流
        try {
            File file = ResourceUtils.getFile("classpath:zk-config.properties");
            InputStream in = new FileInputStream(file);
            properties.load(in);
            Set<Object> keys = properties.keySet();//返回属性key的集合
            for (Object key : keys) {
                boolean b = putConfig(key.toString(), properties.get(key).toString());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * @param dataId
     * @param content
     * @return
     */
    public static boolean putConfig(final String dataId, final String content) {
        Boolean flag = false;
        String path = "/seata/" + dataId;
        if (!zkClient.exists(path)) {
            zkClient.create(path, content, CreateMode.PERSISTENT);
            flag = true;
        } else {
            zkClient.writeData(path, content);
            flag = true;
        }
        return flag;
    }
}

这样Seata Server就配置好了。

2、项目改造

(1)创建undo_log表(AT模式必须)

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

(2)seata-dubbo-order和seata-dubbo-storage引入依赖

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>1.4.1</version>
</dependency>

注:本人引入1.4.2不知为何事务没有回滚,所以还是用回1.4.1。

(3)将Seata Server的registry.conf放到每个项目(这里指seata-dubbo-order和seata-dubbo-storage)的resources目录下

(4)seata-dubbo-storage

① applicationContext.xml配置

主要是添加数据源代理、全局事务扫描器

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:beans="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">


    <!--扫描包,排除注解是@Controller和@RestController的-->
    <context:component-scan base-package="com.harvey">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:exclude-filter type="annotation" expression="org.springframework.web.bind.annotation.RestController"/>
    </context:component-scan>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/tz_storage?useUnicode=true&amp;rewriteBatchedStatements=true&amp;serverTimezone=Asia/Shanghai"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <property name="initialSize" value="20"/>
        <property name="maxIdle" value="5"/>
    </bean>

    <!--数据源代理-->
    <bean id="storageDataSourceProxy" class="io.seata.rm.datasource.DataSourceProxy">
        <constructor-arg ref="dataSource"/>
    </bean>

    <!--JDBC操作模板类-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="storageDataSourceProxy"/>
    </bean>

    <!--本地事务配置-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <!--当前服务的应用名称-->
    <dubbo:application name="dubbo-spring-storage"/>

    <!--注册中心-->
    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <!--通信规则(通信协议|通信端口)-->
    <dubbo:protocol name="dubbo" port="-1"/>

    <!--seata事务扫描器-->
    <bean class="io.seata.spring.annotation.GlobalTransactionScanner">
        <constructor-arg value="dubbo-spring-storage"/>
        <constructor-arg value="storage-service-group"/>
    </bean>

</beans>

(5)seata-dubbo-order

① applicationContext.xml配置

主要是添加数据源代理、全局事务扫描器

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:beans="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">


    <!--扫描包,排除注解是@Controller和@RestController的-->
    <context:component-scan base-package="com.harvey">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:exclude-filter type="annotation" expression="org.springframework.web.bind.annotation.RestController"/>
    </context:component-scan>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/tz_order?useUnicode=true&amp;rewriteBatchedStatements=true&amp;serverTimezone=Asia/Shanghai"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <property name="initialSize" value="20"/>
        <property name="maxIdle" value="5"/>
    </bean>

    <!--数据源代理-->
    <bean id="orderDataSourceProxy" class="io.seata.rm.datasource.DataSourceProxy">
        <constructor-arg ref="dataSource"/>
    </bean>

    <!--JDBC操作模板类-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="orderDataSourceProxy"/>
    </bean>

    <!--本地事务配置-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <!--当前服务的应用名称-->
    <dubbo:application name="dubbo-spring-order"/>

    <!--注册中心-->
    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <!--通信规则(通信协议|通信端口)-->
    <dubbo:protocol name="dubbo" port="-1"/>

    <!--seata事务扫描器-->
    <bean class="io.seata.spring.annotation.GlobalTransactionScanner">
        <constructor-arg value="dubbo-spring-order"/>
        <!--这里一直以为是service,没想到自己写错了, 是servive,总之跟seata server上的配置一致即可-->
        <constructor-arg value="order-servive-group"/>
    </bean>

</beans>

② 在我们需要的地方添加分布式事务注解,并设法抛出异常

@GlobalTransactional(rollbackFor = Exception.class) //全局事务注解
@Override
public String createOrder(Long userId, String productCode, Integer quantity) {
    System.out.println("XID:" + RootContext.getXID());
    OrderBO orderBO = new OrderBO();
    orderBO.setUserId(userId);
    StorageDto productDto = storageReduceDubboService.getProduct(productCode);
    orderBO.setProductCode(productDto.getProductCode());
    orderBO.setProductUnit(productDto.getProductUnit());
    orderBO.setQuantity(quantity);
    Long orderId = IdUtil.generate18Number();
    orderBO.setOrderId(orderId);
    String orderNumber = OrderCoderUtil.getOrderCode(userId);
    orderBO.setOrderNum(orderNumber);
    //保存订单
    orderDao.saveOrder(orderBO);
    //扣减库存
    StorageDto storageDto = new StorageDto();
    storageDto.setOrderId(orderId);
    storageDto.setStorageId(productDto.getStorageId());
    storageDto.setQuantity(quantity);
    storageDto.setVersion(productDto.getVersion());
    storageDto.setProductCode(productDto.getProductCode());
    storageReduceDubboService.reduceProductStorage(storageDto);
    if(true){
        throw new RuntimeException("throw business mock");
    }
    return orderNumber;
}

6、运行测试

1)运行Zookeeper

2)运行Seata Server

3)启动seata-dubbo-storage和seata-dubbo-order

4)使用postman调用controller请求验证,预期结果:tc_order不应该保存订单,tc_storage不应该扣减库存。

实际结果确是:tc_order保存了订单,tc_storage扣减了库存。

怎么不会回滚呢?原因是:在controller中注入的orderService并不是一个被GlobalTransactionalInterceptor增强的代理类,要想全局分布式事务生效,必须调用GlobalTransactionalInterceptor的invoke方法。

阴差阳错,当我们做如下调整时,发现分布式事务生效了:

① 去除OrderServiceImpl的@Service注解

//添加该注解后,使用@Autowired注入,然而获取到的不是代理对象,导致全局事务无法生效,改为xml配置
// @Service
public class OrderServiceImpl implements OrderService {}

② 在applicationContext.xml中配置

<!-- bean配置 -->
<bean id="orderService" class="com.harvey.order.service.impl.OrderServiceImpl"/>

以上完整代码可以下载:提取代码

 

posted @ 2022-04-01 12:51  残城碎梦  阅读(224)  评论(0编辑  收藏  举报