本篇选择是seata 1.3.0版本,项目添加依赖也是这个版本。Release v1.3.0 · seata/seata · GitHub
首先配置seata服务端: 下载seata-server和source code。 解压ource code,找到script文件里config-center文件,修改config.txt:
...
service.vgroup-mapping.my_test_tx_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_server?useUnicode=true&serverTimezone=UTC
store.db.user=root
store.db.password=123456
...
注意把vgroupMapping改成vgroup-mapping这是seata版本的问题。 新建nacos命名空间seata 运行写入配置中心:
sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t seata_namespace_id -u nacos -w nacos
打开script->server->db将里面mysql.sql写入seata_server数据库
将seata-server解压找到conf,按照官网配置file.conf和registry.conf,模式db,类型nacos, 命名空间同为seata_namespace_id。
启动seata-server服务。
然后配置seata-client服务,首先说明按照官网配置,不像其他技术栈那么简单明了,笔者尝试多种方法有不少坑要踩,另外还结合了官方给出的案例:GitHub - seata/seata-samples: seata-samples
找到里面的springcloud-nacos-seata文件,我们可以拿它的stock和order模块测试,在需要用到seata服务的不同库里我们都需要引入undo_log这个表,建表语句在官网和给出案例的readme.md里都能容易找到。 遗憾是按照其案例配置依然会报错,这大概和最新依赖配置不能匹配,所以按照idea工具的配置方式,配置了spring web/nacos/seata依赖。启动会报错,这是因为spring-cloud-starter-alibaba-seata这个包seata版本不是1.3.0或是缺少了seata-all这个包的依赖,最终引入依赖为:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>mysql-config</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
在父pom里处理好了所以版本问题:
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version>
</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>
</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>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.seata/seata-all -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.3.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
</dependencies>
</dependencyManagement>
引入的mysql-config模块配置:
@Configuration
public class config {
/**
* @param sqlSessionFactory SqlSessionFactory
* @return SqlSessionTemplate
*/
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* 从配置文件获取属性构造datasource,注意前缀,这里用的是druid,根据自己情况配置,
* 原生datasource前缀取"spring.datasource"
*
* @return
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSource druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
/**
* 构造datasource代理对象,替换原来的datasource
*
* @param druidDataSource
* @return
*/
@Primary
@Bean("dataSource")
public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSourceProxy);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// bean.setConfigLocation(resolver.getResource("classpath:mybatis-config.xml"));
bean.setMapperLocations(resolver.getResources("classpath*:mybatis/**/*-mapper.xml"));
SqlSessionFactory factory = null;
try {
factory = bean.getObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
return factory;
}
/**
* MP 自带分页插件
*
* @return
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor page = new PaginationInterceptor();
page.setDialectType("mysql");
return page;
}
}
同时在启动类上把DataSourceAutoConfiguration默认配置排除:
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(scanBasePackages = {"com.example"}, exclude = DataSourceAutoConfiguration.class)
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
application.yml:
server:
port: 8000
spring:
application:
name: order-service
cloud:
nacos:
discovery:
username: nacos
password: nacos
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
alibaba:
seata:
# seata 服务分组,要与服务端nacos-config.txt中service.vgroup_mapping的后缀对应
tx-service-group: my_test_tx_group
datasource:
druid:
url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
driverClassName: com.mysql.cj.jdbc.Driver
username: root
password: 123456
logging:
level:
io:
seata: debug
seata:
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group : "SEATA_GROUP"
namespace: "seata_namespace_id"
username: "nacos"
password: "nacos"
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: "SEATA_GROUP"
namespace: "seata_namespace_id"
username: "nacos"
password: "nacos"
application-id: ${spring.application.name}
StockService.java:
@Service
public class StockService {
@Resource
private StockDAO stockDAO;
/**
* 减库存
*
* @param commodityCode
* @param count
*/
@Transactional(rollbackFor = Exception.class)
public void deduct(String commodityCode, int count) {
QueryWrapper<Stock> wrapper = new QueryWrapper<>();
wrapper.setEntity(new Stock().setCommodityCode(commodityCode));
Stock stock = stockDAO.selectOne(wrapper);
stock.setCount(stock.getCount() - count);
stockDAO.updateById(stock);
if (commodityCode.equals("product-2")) {
throw new RuntimeException("异常:模拟业务异常:stock branch exception");
}
}
}
StockFeignClient.java:
@FeignClient(name = "stock-service")
public interface StockFeignClient {
@GetMapping("stock/deduct")
Boolean deduct(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count);
}
OrderService.java:
@Service
public class OrderService {
@Resource
private StockFeignClient stockFeignClient;
@Resource
private OrderDAO orderDAO;
//启用seata全局事务
@GlobalTransactional
@Transactional(rollbackFor = Exception.class)
public void placeOrder(String userId, String commodityCode, Integer count) {
BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
Order order = new Order().setUserId(userId).setCommodityCode(commodityCode).setCount(count).setMoney(
orderMoney);
orderDAO.insert(order);
stockFeignClient.deduct(commodityCode, count);
}
}
OrderController.java:
@RestController
@RequestMapping("order")
public class OrderController {
@Resource
private OrderService orderService;
/**
* 下单:插入订单表、扣减库存
*
* @return
*/
@RequestMapping("/placeOrder/commit")
public Boolean placeOrderCommit() {
orderService.placeOrder("1", "product-1", 1);
return true;
}
/**
* 下单:插入订单表、扣减库存,模拟回滚
*
* @return
*/
@RequestMapping("/placeOrder/rollback")
public Boolean placeOrderRollback() {
// product-2 扣库存时模拟了一个业务异常,
orderService.placeOrder("1", "product-2", 1);
return true;
}
@RequestMapping("/placeOrder")
public Boolean placeOrder(String userId, String commodityCode, Integer count) {
orderService.placeOrder(userId, commodityCode, count);
return true;
}
}
测试: 1.分布式事务成功,模拟正常下单、扣库存:
localhost:8000/order/placeOrder/commit
2.分布式事务失败,模拟下单成功、扣库存失败,最终同时回滚:
localhost:8000/order/placeOrder/rollback