一.Seata-server搭建已完成前提下
详见 Seata-server搭建
二.Springcloud 项目集成Seata
项目整体测试业务逻辑是创建订单后(为了演示分布式事务,不做前置库存校验),再去扣减库存。库存不够的时候,创建的订单信息数据也会回退。
2.1 springboot,springcloud,springboot,seata项目版本的版本管理pom配置
<!-- 统一依赖版本管理 -->
<properties>
<spring.boot.version>2.6.11</spring.boot.version>
<spring-cloud.version>2021.0.4</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version>
<seata.version>1.5.2</seata.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
<exclusions>
<exclusion>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.2 Porduct项目seata核心依赖和配置及代码和undolog表初始化
基本的web,持久化,数据库,数据配置中心,注册中心依赖此处略。
核心pom.xml如下:
<!--seata 分布式事务组件-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
appliction.yml 核心配置如下:
# 端口
server:
port: 8050
servlet:
context-path: /productApi
spring:
application:
name: dolphin-jinyi-product
profiles:
active: dev
mvc:
pathmatch:
matching-strategy: ant_path_matcher
main:
allow-bean-definition-overriding: true
cloud:
nacos:
# 注册中心
discovery:
server-addr: localhost:8848
namespace: d6eccad6-681c-4133-b9ff-1abcd951297a
group: DOLPHIN_GROUP
# 配置中心
config:
server-addr: localhost:8848
file-extension: yml
group: DOLPHIN_GROUP
namespace: d6eccad6-681c-4133-b9ff-1abcd951297a
# 这里可以配置多个共享配置文件
shared-configs:
- data-id: mysql-common.yml
group: DEFAULT_GROUP
refresh: true
- data-id: redis-common.yml
group: DEFAULT_GROUP
refresh: true
logging:
level:
io:
seata: INFO
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
seata:
registry:
# 配置seata的注册中心, 告诉seata client 怎么去访问seata server(TC)
type: nacos
nacos:
server-addr: 127.0.0.1:8848 # seata server 所在的nacos服务地址
application: seata-server # seata server 的服务名seata-server ,如果没有修改可以不配
username: nacos
password: nacos
group: DOLPHIN_GROUP # seata server 所在的组,默认就是SEATA_GROUP,没有改也可以不配
namespace: d6eccad6-681c-4133-b9ff-1abcd951297a
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: DOLPHIN_GROUP # seata server 所在的组,默认就是SEATA_GROUP,没有改也可以不配
namespace: d6eccad6-681c-4133-b9ff-1abcd951297a
#指定Nacos上的DataId
data-id: seata-server.yml
tx-service-group: default_tx_group #这里每个服务都是对应不同的映射名,在配置中心可以看到 事务分组,必须和服务器配置一样
service:
vgroup-mapping:
default_tx_group: default
grouplist:
default: localhost:8091
java核心伪代码示例:
*/
@RestController
@RequestMapping("/product")
@Api(value = "商品Api", tags = {"商品Api"})
public class ProductController {
@Resource
private IProductService productService;
@PostMapping("updateProductStock")
@ApiOperation(value = "updateProductStock-更新商品库存", notes = "updateProductStock-更新商品库存")
public R updateProductStock(@RequestParam(value = "productId") Integer productId,@RequestParam(value = "num") Integer num) {
productService.updateProductStock(productId,num);
return R.ok();
}
}
public interface IProductService extends IService<Product> {
void updateProductStock(Integer productId, Integer num);
}
注:核心点在于根据产品id扣减库存的方法上需要分支事务 @Transactional(rollbackFor = Exception.class)注解。
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements IProductService {
@Override
@Transactional(rollbackFor = Exception.class)
public void updateProductStock(Integer productId, Integer num) {
System.out.println("事务id---------------------->" + RootContext.getXID());
LambdaQueryWrapper<Product> queryWrapper = new LambdaQueryWrapper<Product>()
.eq(Product::getProductId, productId)
.eq(Product::getStatus, 1).last("limit 1");
Product product = this.getOne(queryWrapper);
if (Objects.isNull(product)) {
//商品不存在时抛异常
throw new BizException(ResultCodeEnum.DATA_NOT_FOUND);
}
Integer stockNum = product.getStockNum() + num;
if (stockNum < 0) {
//库存不足时,抛异常
throw new BizException(ResultCodeEnum.PRODUCT_STOCK_NOT_ENOUGH);
}
product.setStockNum(stockNum);
this.updateById(product);
}
}
在product项目对应的数据库中初始化undo_log表
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(128) NOT NULL COMMENT 'global transaction id',
`context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime(6) NOT NULL COMMENT 'create datetime',
`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`),
KEY `ix_log_created` (`log_created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AT transaction mode undo table';
2.3 Order项目seata核心依赖和配置及代码和undolog表初始化
基本的web,持久化,数据库,数据配置中心,注册中心依赖此处略。
核心pom.xml如下:
<!--seata 分布式事务组件-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
appliction.yml 核心配置如下:
# 端口
server:
port: 8010
servlet:
context-path: /orderApi
spring:
application:
name: dolphin-jinyi-order
profiles:
active: dev
mvc:
pathmatch:
matching-strategy: ant_path_matcher
main:
allow-bean-definition-overriding: true
cloud:
nacos:
# 注册中心
discovery:
server-addr: localhost:8848
namespace: d6eccad6-681c-4133-b9ff-1abcd951297a
group: DOLPHIN_GROUP
# 配置中心
config:
server-addr: localhost:8848
file-extension: yml
group: DOLPHIN_GROUP
namespace: d6eccad6-681c-4133-b9ff-1abcd951297a
# 这里可以配置多个共享配置文件
shared-configs:
- data-id: redis-common.yml
group: DEFAULT_GROUP
refresh: true
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
seata:
registry:
# 配置seata的注册中心, 告诉seata client 怎么去访问seata server(TC)
type: nacos
nacos:
server-addr: 127.0.0.1:8848 # seata server 所在的nacos服务地址
application: seata-server # seata server 的服务名seata-server ,如果没有修改可以不配
username: nacos
password: nacos
group: DOLPHIN_GROUP # seata server 所在的组,默认就是SEATA_GROUP,没有改也可以不配
namespace: d6eccad6-681c-4133-b9ff-1abcd951297a
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: DOLPHIN_GROUP # seata server 所在的组,默认就是SEATA_GROUP,没有改也可以不配
namespace: d6eccad6-681c-4133-b9ff-1abcd951297a
#指定Nacos上的DataId
data-id: seata-server.yml
tx-service-group: default_tx_group #这里每个服务都是对应不同的映射名,在配置中心可以看到
service:
vgroup-mapping:
default_tx_group: default
grouplist:
default: localhost:8091
java核心伪代码示例:
@RestController
@RequestMapping("/order")
@Api(value = "订单Api", tags = {"订单Api"})
public class OrderController {
@Resource
private IOrderService orderService;
@ApiOperation(value = "createOrder-创建订单", notes = "createOrder-创建订单")
@PostMapping("/createOrder")
public R<Long> createOrder(@RequestBody @Validated OrderCreateDTO orderCreateDTO) {
Long orderNo = orderService.createOrder(orderCreateDTO);
return R.ok(orderNo);
}
}
public interface IOrderService extends IService<Order> {
Long createOrder(OrderCreateDTO orderCreateDTO);
}
注:核心注解 @GlobalTransactional
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {
@Resource
private ProductServiceFeignClient productServiceFeignClient;
public Long createOrderNo() {
return IdUtil.getSnowflakeNextId();
}
@Override
@GlobalTransactional
public Long createOrder(OrderCreateDTO orderCreateDTO) {
System.out.println("事务id---------------------->" + RootContext.getXID());
Order order = new Order();
BeanUtil.copyProperties(orderCreateDTO, order);
order.setOrderNo(this.createOrderNo());
//创建订单
this.insertOrder(order);
//通过feign调用商品服务扣减库存
productServiceFeignClient.updateProductStock(order.getProductId(), -1);
return order.getOrderNo();
}
@Transactional(rollbackFor = Exception.class)
public void insertOrder(Order order) {
System.out.println("事务id A---------------------->" + RootContext.getXID());
boolean save = this.save(order);
if (!save) {
throw new BizException(ResultCodeEnum.SYSTEM_EXECUTION_ERROR);
}
}
}
在order项目对应的数据库中初始化undo_log表
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(128) NOT NULL COMMENT 'global transaction id',
`context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime(6) NOT NULL COMMENT 'create datetime',
`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`),
KEY `ix_log_created` (`log_created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AT transaction mode undo table';
三.设计到的nacos中的seata-server.yml等配置
注意上面项目的yml配置中的namespace,groupId,data-id 保持和nacos一直
seata-server.yml
service:
vgroupMapping:
default_tx_group: default
四 测试
测试时,保证seata-server order product 服务都已经启动。
现在库存有一个,没有订单信息数据。
4.1 第一次下单
商品表库存被扣减,创建订单成功
现在库存为0.第二次下单
订单没有新增,库存也没有扣减。分布式事务测试ok