Seata AT模式案例讲解(SpringBoot)
运行环境:
- SpringBoot 2.6.2
- Spring 5.3.14
- Dubbo 2.7.15
- MySQL 8.0.23
项目结构:
设计库表:
-- 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');
-- 1、创建tz_order数据库
-- 2、创建tc_order表
CREATE TABLE `tc_order` (
`order_id` bigint unsigned NOT NULL,
`order_num` varchar(100) CHARACTER SET utf8mb4 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;
父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-springboot-nacos</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>dubbo-springboot-client</module>
<module>dubbo-springboot-storage</module>
<module>dubbo-springboot-order</module>
</modules>
<properties>
<mysql.version>8.0.23</mysql.version>
<dubbo.version>2.7.15</dubbo.version>
</properties>
<!--依赖管理必须加-->
<dependencyManagement>
<dependencies>
<!--SpringBoot 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>5.3.14</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<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>
</build>
</project>
dubbo-springboot-client
1、pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seata-dubbo-springboot-nacos</artifactId>
<groupId>com.harvey</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dubbo-springboot-client</artifactId>
<!--这里要加上个版本,否则其他应用引用不到-->
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
</project>
2、实体类StorageDto
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;
}
}
3、接口定义
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);
}
dubbo-springboot-storage
1、pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seata-dubbo-springboot-nacos</artifactId>
<groupId>com.harvey</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dubbo-springboot-storage</artifactId>
<dependencies>
<dependency>
<groupId>com.harvey</groupId>
<artifactId>dubbo-springboot-client</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web应用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--数据库/JDBC操作-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--dubbo支持-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-nacos</artifactId>
<version>${dubbo.version}</version>
</dependency>
<!--nacos支持-->
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>2.0.3</version>
</dependency>
</dependencies>
</project>
2、业务操作
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);
}
}
3、要暴露的接口的具体实现
@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);
}
}
4、启动类
@SpringBootApplication
public class StorageApp {
public static void main(String[] args) {
SpringApplication.run(StorageApp.class, args);
}
}
5、application.yml
server:
port: 8888
spring:
application:
name: storage-service
#数据源配置
datasource:
#通用配置
username: root
password: root
url: jdbc:mysql://localhost:3306/tz_storage?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.jdbc.Driver
#hikari数据源特性配置
hikari:
maximum-pool-size: 1000 #最大连接数,默认值10.
minimum-idle: 200 #最小空闲连接,默认值10.
connection-timeout: 60000 #连接超时时间(毫秒),默认值30秒.
#空闲连接超时时间,默认值600000(10分钟),只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放
#如果大于等于 max-lifetime 且 max-lifetime>0,则会被重置为0.
idle-timeout: 600000
max-lifetime: 3000000 #连接最大存活时间,默认值30分钟.设置应该比mysql设置的超时时间短
connection-test-query: select 1 #连接测试查询
dubbo:
application:
name: ${spring.application.name} #应用名
registry:
address: nacos://127.0.0.1:8848?namespace=9d239df8-ee1f-4da8-a218-57483efa90f4
protocol:
name: dubbo
port: -1 #dubbo服务暴露的端口
scan:
base-packages: com.harvey #扫描的包名
provider:
timeout: 5000
consumer:
timeout: 5000
dubbo-springboot-order
1、pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seata-dubbo-springboot-nacos</artifactId>
<groupId>com.harvey</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dubbo-springboot-order</artifactId>
<dependencies>
<dependency>
<groupId>com.harvey</groupId>
<artifactId>dubbo-springboot-client</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web应用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--数据库/JDBC操作-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--dubbo支持-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-nacos</artifactId>
<version>${dubbo.version}</version>
</dependency>
<!--nacos支持-->
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>2.0.3</version>
</dependency>
</dependencies>
</project>
2、工具类
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);
}
}
/**
* @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);
}
}
3、业务操作
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
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@DubboReference
private StorageReduceDubboService storageReduceService;
/**
* 创建订单
*
* @param userId
* @return
*/
@Override
public String createOrder(Long userId, String productCode, Integer quantity) {
OrderBO orderBO = new OrderBO();
orderBO.setUserId(userId);
StorageDto productDto = storageReduceService.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);
//扣减库存
StorageDto storageDto = new StorageDto();
storageDto.setOrderId(orderId);
storageDto.setStorageId(productDto.getStorageId());
storageDto.setQuantity(quantity);
storageDto.setVersion(productDto.getVersion());
storageDto.setProductCode(productDto.getProductCode());
storageReduceService.reduceProductStorage(storageDto);
//保存订单
orderDao.saveOrder(orderBO);
return orderNumber;
}
/**
* 查询用户订单
*
* @param userId
* @return
*/
@Override
public List<OrderBO> listOrders(Long userId) {
return orderDao.findOrders(userId);
}
}
4、controller
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
//POST http://localhost:9090/createOrder?userId=10020201&productCode=100010001&quantity=1
@RequestMapping("/createOrder")
public Map<String, Object> createOrder(Long userId, String productCode, Integer quantity) {
String orderNum = orderService.createOrder(userId, productCode, quantity);
Map<String, Object> resultMap = new HashMap();
resultMap.put("msg", "创建订单成功");
resultMap.put("orderNum", orderNum);
return resultMap;
}
}
5、启动类
@SpringBootApplication
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class, args);
}
}
6、application.yml
server:
port: 9090
spring:
application:
name: storage-service
#数据源配置
datasource:
#通用配置
username: root
password: root
url: jdbc:mysql://localhost:3306/tz_order?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.jdbc.Driver
#hikari数据源特性配置
hikari:
maximum-pool-size: 1000 #最大连接数,默认值10.
minimum-idle: 200 #最小空闲连接,默认值10.
connection-timeout: 60000 #连接超时时间(毫秒),默认值30秒.
#空闲连接超时时间,默认值600000(10分钟),只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放
#如果大于等于 max-lifetime 且 max-lifetime>0,则会被重置为0.
idle-timeout: 600000
max-lifetime: 3000000 #连接最大存活时间,默认值30分钟.设置应该比mysql设置的超时时间短
connection-test-query: select 1 #连接测试查询
dubbo:
application:
name: ${spring.application.name} #应用名
registry:
address: nacos://127.0.0.1:8848?namespace=9d239df8-ee1f-4da8-a218-57483efa90f4
protocol:
name: dubbo
port: -1 #dubbo服务暴露的端口
scan:
base-packages: com.harvey #扫描的包名
provider:
timeout: 5000
consumer:
timeout: 5000
运行测试Dubbo调用
1)运行注册中心Nacos
2)启动服务提供者dubbo-springboot-storage
3)启动服务消费者dubbo-springboot-order
在Nacos中我们可以看到已注册上的服务:
4)使用postman请求controller
以上我们会发现在创建订单的时候,会先调用接口扣除库存,其中涉及到的是两个数据库的写操作,如果扣减库存成功,最后保存订单失败,那岂不是就造成脏数据了。
引入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 = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = "dc3eb2bd-2b95-4cd8-aefc-799829fb0a8c"
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "dc3eb2bd-2b95-4cd8-aefc-799829fb0a8c"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}
config.txt
一般我们会使用nacos或者zk作为配置中心来配置,具体每项的含义可以参考官方文档。
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
以下是nacos作为配置中心的最终效果:
Seata AT模式
目前seata场景中使用AT模式较多。
AT模式的前提是基于支持本地 ACID 事务的关系型数据库和Java应用基于JDBC访问数据库。AT模式是二阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:commit异步化快速完成;rollback通过一阶段的回滚日志进行反向补偿。
AT模式下每个DataSource Resource(即参与分布式事务的数据库)都必须创建一个undo_log表用于反向补偿。
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=1 DEFAULT CHARSET=utf8mb3;
1、dubbo-springboot-storage改造
1)引入seata依赖
<!--seata支持, 这里使用的是1.4.1, 使用1.4.2的时候分布式事务没有回滚,不知为何???-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.1</version>
</dependency>
2)application.yml配置
seata:
enabled: true
enable-auto-data-source-proxy: true
application-id: ${spring.application.name}
# ① 注意点1
tx-service-group: storage-service-group
service:
disable-global-transaction: false
vgroup-mapping:
# ② 注意点1指定的
storage-service-group: default
registry:
type: nacos
nacos:
application: seata-server
serverAddr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: dc3eb2bd-2b95-4cd8-aefc-799829fb0a8c
cluster: default
username: nacos
password: nacos
config:
type: nacos
nacos:
serverAddr: 127.0.0.1:8848
namespace: dc3eb2bd-2b95-4cd8-aefc-799829fb0a8c
group: SEATA_GROUP
username: nacos
password: nacos
序号① 这里定义的tx-service-group属性值必须与seata server事务协调器保持一致
2、dubbo-springboot-order改造
1)引入seata依赖
<!--seata支持-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.1</version>
</dependency>
2)application.yml配置
seata:
enabled: true
enable-auto-data-source-proxy: true
application-id: ${spring.application.name}
# ① 注意点1
tx-service-group: order-servive-group
service:
disable-global-transaction: false
vgroup-mapping:
# ① 注意点1指定的
order-servive-group: default
registry:
type: nacos
nacos:
application: seata-server
serverAddr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: dc3eb2bd-2b95-4cd8-aefc-799829fb0a8c
cluster: default
username: nacos
password: nacos
config:
type: nacos
nacos:
serverAddr: 127.0.0.1:8848
namespace: dc3eb2bd-2b95-4cd8-aefc-799829fb0a8c
group: SEATA_GROUP
username: nacos
password: nacos
3)OrderServiceImpl.java
/**
* 创建订单
*
* @param userId
* @return
*/
@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 = storageReduceService.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);
//扣减库存
StorageDto storageDto = new StorageDto();
storageDto.setOrderId(orderId);
storageDto.setStorageId(productDto.getStorageId());
storageDto.setQuantity(quantity);
storageDto.setVersion(productDto.getVersion());
storageDto.setProductCode(productDto.getProductCode());
storageReduceService.reduceProductStorage(storageDto);
if(true){
throw new RuntimeException("business mock");
}
//保存订单
orderDao.saveOrder(orderBO);
return orderNumber;
}
3、运行测试
1)启动Seata Server
2)启动服务提供提供者和服务消费者,可以再Seata Server 控制台看到如下内容:
3)postman请求验证。
查看数据库,异常时,库存并不会扣减。
Seata Server控制台输出如下日志:
有分支事务id和全局事务id。
使用Zookeeper作为注册中心
1、dubbo-springboot-storage和dubbo-springboot-order模块都引入依赖
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
<version>${dubbo.version}</version>
</dependency>
<!--Zookeeper支持-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.11</version>
</dependency>
2、dubbo-springboot-storage的application.yml
server:
port: 8888
spring:
application:
name: storage-service
#数据源配置
datasource:
#通用配置
username: root
password: root
url: jdbc:mysql://localhost:3306/tz_storage?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.jdbc.Driver
#hikari数据源特性配置
hikari:
maximum-pool-size: 1000 #最大连接数,默认值10.
minimum-idle: 200 #最小空闲连接,默认值10.
connection-timeout: 60000 #连接超时时间(毫秒),默认值30秒.
#空闲连接超时时间,默认值600000(10分钟),只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放
#如果大于等于 max-lifetime 且 max-lifetime>0,则会被重置为0.
idle-timeout: 600000
max-lifetime: 3000000 #连接最大存活时间,默认值30分钟.设置应该比mysql设置的超时时间短
connection-test-query: select 1 #连接测试查询
dubbo:
application:
name: ${spring.application.name} #应用名
registry:
address: zookeeper://127.0.0.1:2181
# address: nacos://127.0.0.1:8848?namespace=9d239df8-ee1f-4da8-a218-57483efa90f4
protocol:
name: dubbo
port: -1 #dubbo服务暴露的端口
scan:
base-packages: com.harvey #扫描的包名
provider:
timeout: 5000
consumer:
timeout: 5000
seata:
enabled: true
enable-auto-data-source-proxy: true
application-id: ${spring.application.name}
# 注意点1
tx-service-group: storage-service-group
service:
disable-global-transaction: false
vgroup-mapping:
# 注意点1指定的
storage-service-group: default
registry:
type: zk
zk:
cluster: default
serverAddr: 127.0.0.1:2181
sessionTimeout: 6000
connectTimeout: 2000
config:
type: zk
zk:
serverAddr: 127.0.0.1:2181
sessionTimeout: 6000
connectTimeout: 2000
nodePath: /seata/seata.properties
3、dubbo-springboot-order的application.yml
server:
port: 9090
spring:
application:
name: storage-service
#数据源配置
datasource:
#通用配置
username: root
password: root
url: jdbc:mysql://localhost:3306/tz_order?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.jdbc.Driver
#hikari数据源特性配置
hikari:
maximum-pool-size: 1000 #最大连接数,默认值10.
minimum-idle: 200 #最小空闲连接,默认值10.
connection-timeout: 60000 #连接超时时间(毫秒),默认值30秒.
#空闲连接超时时间,默认值600000(10分钟),只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放
#如果大于等于 max-lifetime 且 max-lifetime>0,则会被重置为0.
idle-timeout: 600000
max-lifetime: 3000000 #连接最大存活时间,默认值30分钟.设置应该比mysql设置的超时时间短
connection-test-query: select 1 #连接测试查询
dubbo:
application:
name: ${spring.application.name} #应用名
registry:
address: zookeeper://127.0.0.1:2181
# address: nacos://127.0.0.1:8848?namespace=9d239df8-ee1f-4da8-a218-57483efa90f4
protocol:
name: dubbo
port: -1 #dubbo服务暴露的端口
scan:
base-packages: com.harvey #扫描的包名
provider:
timeout: 5000
consumer:
timeout: 5000
seata:
enabled: true
enable-auto-data-source-proxy: true
application-id: ${spring.application.name}
# ① 注意点1
tx-service-group: order-servive-group
service:
disable-global-transaction: false
vgroup-mapping:
# ① 注意点1指定的
order-servive-group: default
registry:
type: zk
zk:
cluster: default
serverAddr: 127.0.0.1:2181
sessionTimeout: 6000
connectTimeout: 2000
config:
type: zk
zk:
serverAddr: 127.0.0.1:2181
sessionTimeout: 6000
connectTimeout: 2000
nodePath: /seata/seata.properties
4、修改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"
}
}
5、启动Zookeeper(本人使用的是Zookeeper3.4.8),导入Seata Server的配置
注:本人尝试使用官网提供的zk-config.sh导入config.txt的配置没有成功,只能代码导入。
1)将config.txt拷贝到项目的resources目录下,重命名zk-config.properties。然后初始化配置脚本
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;
}
}
6、运行测试
1)启动Zookeeper
2)启动Seata Server:双击bin下的seata-server.bat即可
3)启动dubbo-springboot-storage和dubbo-springboot-order
4)使用postman调用controller请求测试
seata+dubbo+Springboot+zk的代码可以下载:提取代码
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!