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&rewriteBatchedStatements=true&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&rewriteBatchedStatements=true&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&rewriteBatchedStatements=true&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&rewriteBatchedStatements=true&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"/>
以上完整代码可以下载:提取代码
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!