一、事务(回顾)
1.1 概念:
事务:保证多条sql 语句执行的一致性, 同一个事务中执行的sql 语句都是使用了同一个 sqlsession
sqlsession.setAutoCommit(false) // 关闭自动提交,开启事务
sqlSession.commit() 提交事务 /
1.2 四大特性
原子性 不可以分割,要么成功,都成功,有一个失败,其他全部失败
一致性 举例:同一表中转账,一个用户给另外一个用户专账,整体钱数不能多,也不能少
隔离性 多个事务(sqlSession)是否能够看到另外一个 事务未提交的数据
持久性 提交事务后,掉电不丢失
个人理解:
隔离性:一个sql session 能否读取到另外一个sqlsession 提交的
一致性:已转账为例
持久性:事务提交之后
1.3 隔离级别
一个事务 看到另外两个事务 是否提交数据的程度 ,即允许脏数据的程度
脏读数据程度:
脏读:一个事务 可以读取到另外一个事务未提交的数据
不可重复读:同一个事务中对同一行数据读取两次,两次读取的数据不一致
幻读:同一事务中,执行两次同一查询多条数据的sql,查询的结果数量不一致。
MySQL默认的隔离级别:可重复读,也可以解决幻读的问题。
1.3 传播行为
传播行为,就是在多个方法调用过程中,是否共享 同一个 sqlsession
传播行为(7种):
support :如果调用者有事务,就是使用调用者(外部)的事务,调用者没有开启,则不开启
required: :如果调用者有事务,则使用外部的事务,调用者没有开启,则自己开启
supportnew:不管调用者有没有事务,我都开启新的事务
二、分布式事务
分布式事务:就是在我们微服务的业务中,跨应用调用,保证数据的一致性
分布式业务中的基本知识
2.1 CAP
CAP理论一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance )
- 一致性:每时每刻从每一个节点读取的数据都是一致的
- 可用性:如果有部分节点挂了,不影响整个集群
- 分区容错性:数据可以有分区隔离,每一个分区都有备份
在分布式业务中不可能同时满足 三个特性CAP,只能满足其中两个
- Eureka:AP,保证了可用性,舍弃了一致性。
- Zookeeper:CP,每一个节点必须能够找到Master才能对外提供服务,舍弃了可用性。
2.2 Base理论
BA:基本可用,S:中间状态,E:最终一致性。
Base 是对cap 的妥协,暂时丢失一致性获取的可用性 和分区容错,从而达到最终的一致性
基于CAP理论演化而来的,是对CAP定理中一致性和可用性的一个权衡结果。
核心思想:我们无法做到强一致性,但是每一个应用都可以根据自身的业务特点,采用一些适当的方式来权衡,最终达到一致性。
S:允许系统之间存在一个中间状态,并不会影响正常的去使用整个系统,允许数据的同步存在延迟。
E:系统中所有的数据副本经过一定时间后,最终能够达到一致的状态,不需要保证系统数据强一致性。
三、分布式事务的解决方案【重点】
3.1 PC两段提交
两段提交分为两个阶段:
第一个阶段是准备阶段,参与者需要开启事务,执行SQL,保证数据库中已经存在相应的数据。参与者会向TransactionManager准备OK。
第二个阶段当TransactionManager收到了所有的参与者的通知之后,向所有的参与者发送Commit请求。
缺陷:
问题1:执行的性能是很低的。一般是传统事务的10倍以上。
问题2:TransactionManager是没有超时时间的。
问题3:TransactionManager存在单点故障的问题
3.2 3PC
三段提交在二段提交的基础上,引入了超时时间机制,并且在二段提交的基础上,又多了一个步骤,在提交事务之前,再询问一下,数据库的日志信息,是否已经完善。
优化:
1.引入超时机制
2.引入sql 日志,便于回滚
3.3 TCC机制
TCC(Try,Confirm,Cancel),和你的业务代码切合在一起。
Try:尝试去预执行具体业务代码。 下单订ing。。。
try成功了:Confirm:再次执行Confirm的代码。
try失败了:Cancel:再次执行Cancel的代码。
TCC 一般需要我们自己实现对应的 try Confirm cancel 接口
TCC 使用起来非常麻烦,但是性能非常好,适合高并发场合
Try:包含资源的预留,锁定 ,执行sql
Confirm:提交数据
Cancel:回滚 已执行的sql,释放锁定的资源
3.4 MQ分布式事务
RabbitMQ在发送消息时,confirm机制,可以保证消息发送到MQ服务中,消费者有手动ack机制,保证消费到MQ中的消息。
四、Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
4.1 AT 模式
前提
基于支持本地 ACID 事务的关系型数据库。
Java 应用,通过 JDBC 访问数据库。
整体机制
两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
4.2 TCC 模式
回顾总览中的描述:一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
一阶段 prepare 行为 资源的预留,锁定
二阶段 commit 或 rollback 行为
AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库:
一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
二阶段 commit 行为:调用 自定义 的 commit 逻辑。
二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
4.3 Saga 模式
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
长事务:就是执行该事务,占用时间很长,造成占用数据连接资源,造成mysql主从 数据不同不问题
适用场景:
业务流程长、业务流程多
参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
4.4 XA
XA模式 是 AT 模式的升级版。
4.5 Seata原理和设计
定义一个分布式事务
我们可以把一个分布式事务理解成一个包含了若干分支事务的全局事务,全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个满足ACID的本地事务。这是我们对分布式事务结构的基本认识,与 XA 是一致的。
协议分布式事务处理过程的三个组件
-
Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;
事务协调器: 他是通信员,秘书 负责跑腿传递消息
-
Transaction Manager TM: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
全局事务管理器: 就是大boss,决策者
-
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
资源管理器: 负责接收 TC 的命令, 并协调本地事务的提交和回滚
一个典型的分布式事务过程
TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
XID 在微服务调用链路的上下文中传播;
RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
TM 向 TC 发起针对 XID 的全局提交或回滚决议;
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
4.6 案例
4.6.1 创建seataspringboot父工程
1.创建seataspringboot父工程
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.qfedu</groupId>
<artifactId>seata-springboot</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>seata-order-service</module>
<module>seata-storage-service</module>
<module>seata-account-service</module>
</modules>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
<mysql-connector-java.version>5.1.37</mysql-connector-java.version>
<mybatis-spring-boot-starter.version>2.0.0</mybatis-spring-boot-starter.version>
<druid-spring-boot-starter.version>1.1.10</druid-spring-boot-starter.version>
<lombok.version>1.18.8</lombok.version>
<seata.version>0.9.0</seata.version>
</properties>
<dependencyManagement>
<dependencies>
<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>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
4.6.2 创建seata-account-service子工程
创建seata-account-service子工程
pom文件如下:
<?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-springboot</artifactId>
<groupId>com.qfedu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-account-service</artifactId>
<dependencies>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<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>
<version>${seata.version}</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid-spring-boot-starter.version}</version>
</dependency>
</dependencies>
</project>
在Config文件夹下创建DataSourceProxyConfig,MyBatisConfig配置类
在controller文件夹下创建AccountController类
在dao文件夹下创建AccountDao
在domain文件下创建Account、CommonResult
在service文件夹下创建AccountService,在文件夹创建impl子文件夹,然后创建AccountServiceImpl实现类
创建启动类SeataAccountServiceApplication
package com.qfedu.cloud.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean// DataSourceProxy代理类代理 druid数据源,因为RM 管理提交本地事务都是通过 DataSourceProxy
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
package com.qfedu.cloud.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan({"com.qfedu.cloud.dao"})
public class MyBatisConfig {
}
package com.qfedu.cloud.controller;
import com.qfedu.cloud.domain.CommonResult;
import com.qfedu.cloud.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private AccountService accountService;
/**
* 扣减账户余额
*/
@RequestMapping("/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
accountService.decrease(userId,money);
return new CommonResult("扣减账户余额成功!",200);
}
}
package com.qfedu.cloud.dao;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
@Repository
public interface AccountDao {
/**
* 扣减账户余额
*/
void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
package com.qfedu.cloud.domain;
import java.math.BigDecimal;
public class Account {
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 总额度
*/
private BigDecimal total;
/**
* 已用额度
*/
private BigDecimal used;
/**
* 剩余额度
*/
private BigDecimal residue;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public BigDecimal getTotal() {
return total;
}
public void setTotal(BigDecimal total) {
this.total = total;
}
public BigDecimal getUsed() {
return used;
}
public void setUsed(BigDecimal used) {
this.used = used;
}
public BigDecimal getResidue() {
return residue;
}
public void setResidue(BigDecimal residue) {
this.residue = residue;
}
}
package com.qfedu.cloud.domain;
/**
* Created by macro on 2019/8/29.
*/
public class CommonResult<T> {
private T data;
private String message;
private Integer code;
public CommonResult() {
}
public CommonResult(T data, String message, Integer code) {
this.data = data;
this.message = message;
this.code = code;
}
public CommonResult(String message, Integer code) {
this(null, message, code);
}
public CommonResult(T data) {
this(data, "操作成功", 200);
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
package com.qfedu.cloud.service;
import java.math.BigDecimal;
public interface AccountService {
/**
* 扣减账户余额
* @param userId 用户id
* @param money 金额
*/
void decrease(Long userId, BigDecimal money);
}
package com.qfedu.cloud.service.impl;
import com.qfedu.cloud.dao.AccountDao;
import com.qfedu.cloud.service.AccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* 账户业务实现类
* Created by macro on 2019/11/11.
*/
@Service
public class AccountServiceImpl implements AccountService {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
@Autowired
private AccountDao accountDao;
/**
* 扣减账户余额
*/
@Override
public void decrease(Long userId, BigDecimal money) {
LOGGER.info("------->account-service中扣减账户余额开始");
// 模拟超时异常,全局事务回滚
// try {
// Thread.sleep(30*1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// int a = 1/0;
accountDao.decrease(userId,money);
LOGGER.info("------->account-service中扣减账户余额结束");
}
}
package com.qfedu.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataAccountServiceApplication {
public static void main(String[] args) {
SpringApplication.run(SeataAccountServiceApplication.class, args);
}
}
在resources目录下创建mapper文件夹,mapper文件下创建AccountMapper.xml配置文件
在resources目录下创建application.yml文件,file.conf文件、registry.conf文件
AccountMapper.xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.qfedu.cloud.dao.AccountDao">
<resultMap id="BaseResultMap" type="com.qfedu.cloud.domain.Account">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="DECIMAL"/>
<result column="used" property="used" jdbcType="DECIMAL"/>
<result column="residue" property="residue" jdbcType="DECIMAL"/>
</resultMap>
<update id="decrease">
UPDATE account
SET residue = residue - #{money},
used = used + #{money}
WHERE user_id = #{userId};
</update>
</mapper>
application.yml文件
server:
port: 8182
spring:
application:
name: seata-account-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
alibaba:
seata:
tx-service-group: fsp_tx_group
datasource:
driver-class-name: com.mysql.jdbc.Driver
password: 123456
url: jdbc:mysql://localhost:3306/seat-account
username: root
mybatis:
mapperLocations: classpath:mapper/*.xml
file.conf文件
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#vgroup->rgroup
vgroup_mapping.fsp_tx_group = "default"
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
client {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
}
report.retry.count = 5
tm.commit.retry.count = 1
tm.rollback.retry.count = 1
}
## transaction log store
store {
## store mode: file、db
## mode = "file"
mode = "db" #修改此处将事务信息存储到数据库中
## file store
file {
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
max-branch-session-size = 16384
# globe session size , if exceeded throws exceptions
max-global-session-size = 512
# file buffer size , if exceeded allocate new buffer
file-write-buffer-cache-size = 16384
# when recover batch read size
session.reload.read_size = 100
# async, sync
flush-disk-mode = async
}
## database store
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://localhost:3306/seata" #修改数据库连接地址
user = "root" #修改数据库用户名
password = "123456" #修改数据库密码
min-conn = 1
max-conn = 3
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
}
lock {
## the lock store mode: local、remote
mode = "remote"
local {
## store locks in user's database
}
remote {
## store locks in the seata's server
}
}
recovery {
#schedule committing retry period in milliseconds
committing-retry-period = 1000
#schedule asyn committing retry period in milliseconds
asyn-committing-retry-period = 1000
#schedule rollbacking retry period in milliseconds
rollbacking-retry-period = 1000
#schedule timeout retry period in milliseconds
timeout-retry-period = 1000
}
transaction {
undo.data.validation = true
undo.log.serialization = "jackson"
undo.log.save.days = 7
#schedule delete expired undo_log in milliseconds
undo.log.delete.period = 86400000
undo.log.table = "undo_log"
}
## metrics settings
metrics {
enabled = false
registry-type = "compact"
# multi exporters use comma divided
exporter-list = "prometheus"
exporter-prometheus-port = 9898
}
support {
## spring
spring {
# auto proxy the DataSource bean
datasource.autoproxy = false
}
}
registry.conf文件
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
# type = "file"
type = "nacos" #改为nacos
nacos {
# serverAddr = "localhost"
serverAddr = "localhost:8848" #改为nacos的连接地址
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
4.6.3 创建seata-order-service子工程
创建seata-order-service子工程
pom文件如下:
<?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-springboot</artifactId>
<groupId>com.qfedu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-order-service</artifactId>
<dependencies>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<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>
<version>${seata.version}</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid-spring-boot-starter.version}</version>
</dependency>
</dependencies>
</project>
在Config文件夹下创建DataSourceProxyConfig,MyBatisConfig配置类
在controller文件夹下创建OrderController类
在dao文件夹下创建OrderDao
在domain文件下创建Order、CommonResult
在service文件夹下创建OrderService,AccountService,StorageService,在文件夹创建impl子文件夹,然后创建OrderServiceImpl实现类
创建启动类SeataOrderServiceApplication
DataSourceProxyConfig,MyBatisConfig配置类 和上一个工程的内容一样
OrderController
package com.qfedu.cloud.controller;
import com.qfedu.cloud.domain.CommonResult;
import com.qfedu.cloud.domain.Order;
import com.qfedu.cloud.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 创建订单
*/
@GetMapping("/create")
public CommonResult create(Order order) {
orderService.create(order);
return new CommonResult("订单创建成功!", 200);
}
}
package com.qfedu.cloud.dao;
import com.qfedu.cloud.domain.Order;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface OrderDao {
/**
* 创建订单
*/
void create(Order order);
/**
* 修改订单金额
*/
void update(@Param("userId") Long userId, @Param("status") Integer status);
}
package com.qfedu.cloud.domain;
import java.math.BigDecimal;
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
/**
* 订单状态:0:创建中;1:已完结
*/
private Integer status;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public BigDecimal getMoney() {
return money;
}
public void setMoney(BigDecimal money) {
this.money = money;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
package com.qfedu.cloud.domain;
/**
* Created by macro on 2019/8/29.
*/
public class CommonResult<T> {
private T data;
private String message;
private Integer code;
public CommonResult() {
}
public CommonResult(T data, String message, Integer code) {
this.data = data;
this.message = message;
this.code = code;
}
public CommonResult(String message, Integer code) {
this(null, message, code);
}
public CommonResult(T data) {
this(data, "操作成功", 200);
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
package com.qfedu.cloud.service;
import com.qfedu.cloud.domain.Order;
public interface OrderService {
/**
* 创建订单
*/
void create(Order order);
}
package com.qfedu.cloud.service;
import com.qfedu.cloud.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
@FeignClient(value = "seata-account-service")
public interface AccountService {
/**
* 扣减账户余额
*/
@RequestMapping("/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
package com.qfedu.cloud.service;
import com.qfedu.cloud.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "seata-storage-service")
public interface StorageService {
/**
* 扣减库存
*/
@GetMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
package com.qfedu.cloud.service.impl;
import com.qfedu.cloud.dao.OrderDao;
import com.qfedu.cloud.domain.Order;
import com.qfedu.cloud.service.AccountService;
import com.qfedu.cloud.service.OrderService;
import com.qfedu.cloud.service.StorageService;
import io.seata.spring.annotation.GlobalTransactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 订单业务实现类
* Created by macro on 2019/11/11.
*/
@Service
public class OrderServiceImpl implements OrderService {
private static final Logger LOGGER = LoggerFactory.getLogger(OrderServiceImpl.class);
@Autowired
private OrderDao orderDao;
@Autowired
private StorageService storageService;
@Autowired
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
*/
@Override
// @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order) {
LOGGER.info("------->下单开始");
//本应用创建订单
orderDao.create(order);
//远程调用库存服务扣减库存
LOGGER.info("------->order-service中扣减库存开始");
storageService.decrease(order.getProductId(),order.getCount());
LOGGER.info("------->order-service中扣减库存结束");
//远程调用账户服务扣减余额
LOGGER.info("------->order-service中扣减余额开始");
accountService.decrease(order.getUserId(),order.getMoney());
LOGGER.info("------->order-service中扣减余额结束");
//修改订单状态为已完成
LOGGER.info("------->order-service中修改订单状态开始");
orderDao.update(order.getUserId(),0);
LOGGER.info("------->order-service中修改订单状态结束");
LOGGER.info("------->下单结束");
}
}
package com.qfedu.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataOrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(SeataOrderServiceApplication.class, args);
}
}
在resources目录下创建mapper文件夹,mapper文件下创建OrderMapper.xml配置文件
在resources目录下创建application.yml文件,file.conf文件、registry.conf文件
OrderMapper.xml配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.qfedu.cloud.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.qfedu.cloud.domain.Order">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="count" property="count" jdbcType="INTEGER"/>
<result column="money" property="money" jdbcType="DECIMAL"/>
<result column="status" property="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="create">
INSERT INTO `order` (`id`, `user_id`, `product_id`, `count`, `money`, `status`)
VALUES (NULL, #{userId}, #{productId}, #{count}, #{money}, 0);
</insert>
<update id="update">
UPDATE `order`
SET status = 1
WHERE user_id = #{userId}
AND status = #{status};
</update>
</mapper>
application.yml文件
server:
port: 8180
spring:
application:
name: seata-order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
alibaba:
seata:
tx-service-group: fsp_tx_group
datasource:
driver-class-name: com.mysql.jdbc.Driver
password: 123456
url: jdbc:mysql://localhost:3306/seat-order
username: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
file.conf文件、registry.conf文件和上一个工程代码一样
4.6.4 创建seata-storage-service子工程
创建seata-storage-service子工程
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-springboot</artifactId>
<groupId>com.qfedu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-storage-service</artifactId>
<dependencies>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<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>
<version>${seata.version}</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid-spring-boot-starter.version}</version>
</dependency>
</dependencies>
</project>
在Config文件夹下创建DataSourceProxyConfig,MyBatisConfig配置类
在controller文件夹下创建StorageController类
在dao文件夹下创建StorageDao
在domain文件下创建Storage、CommonResult
在service文件夹下创建StorageService,在文件夹创建impl子文件夹,然后创建StorageServiceImpl实现类
创建启动类SeataStorageServiceApplication
DataSourceProxyConfig,MyBatisConfig配置类 和上一个工程的内容一样
StorageController类如下:
package com.qfedu.cloud.controller;
import com.qfedu.cloud.domain.CommonResult;
import com.qfedu.cloud.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/storage")
public class StorageController {
@Autowired
private StorageService storageService;
/**
* 扣减库存
*/
@RequestMapping("/decrease")
public CommonResult decrease(Long productId, Integer count) {
storageService.decrease(productId, count);
return new CommonResult("扣减库存成功!",200);
}
}
package com.qfedu.cloud.dao;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface StorageDao {
/**
* 扣减库存
*/
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
package com.qfedu.cloud.domain;
public class Storage {
private Long id;
/**
* 产品id
*/
private Long productId;
/**
* 总库存
*/
private Integer total;
/**
* 已用库存
*/
private Integer used;
/**
* 剩余库存
*/
private Integer residue;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public Integer getTotal() {
return total;
}
public void setTotal(Integer total) {
this.total = total;
}
public Integer getUsed() {
return used;
}
public void setUsed(Integer used) {
this.used = used;
}
public Integer getResidue() {
return residue;
}
public void setResidue(Integer residue) {
this.residue = residue;
}
}
package com.qfedu.cloud.domain;
/**
* Created by macro on 2019/8/29.
*/
public class CommonResult<T> {
private T data;
private String message;
private Integer code;
public CommonResult() {
}
public CommonResult(T data, String message, Integer code) {
this.data = data;
this.message = message;
this.code = code;
}
public CommonResult(String message, Integer code) {
this(null, message, code);
}
public CommonResult(T data) {
this(data, "操作成功", 200);
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
package com.qfedu.cloud.service;
public interface StorageService {
/**
* 扣减库存
*/
void decrease(Long productId, Integer count);
}
package com.qfedu.cloud.service.impl;
import com.qfedu.cloud.dao.StorageDao;
import com.qfedu.cloud.service.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class StorageServiceImpl implements StorageService {
private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);
@Autowired
private StorageDao storageDao;
/**
* 扣减库存
*/
@Override
public void decrease(Long productId, Integer count) {
LOGGER.info("------->storage-service中扣减库存开始");
storageDao.decrease(productId,count);
LOGGER.info("------->storage-service中扣减库存结束");
}
}
package com.qfedu.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataStorageServiceApplication {
public static void main(String[] args) {
SpringApplication.run(SeataStorageServiceApplication.class, args);
}
}
在resources目录下创建mapper文件夹,mapper文件下创建StorageMapper.xml配置文件
在resources目录下创建application.yml文件,file.conf文件、registry.conf文件
StorageMapper.xml配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.qfedu.cloud.dao.StorageDao">
<resultMap id="BaseResultMap" type="com.qfedu.cloud.domain.Storage">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="INTEGER"/>
<result column="used" property="used" jdbcType="INTEGER"/>
<result column="residue" property="residue" jdbcType="INTEGER"/>
</resultMap>
<update id="decrease">
UPDATE storage
SET used = used + #{count},
residue = residue - #{count}
WHERE product_id = #{productId}
</update>
</mapper>
application.yaml文件
server:
port: 8181
spring:
application:
name: seata-storage-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
alibaba:
seata:
tx-service-group: fsp_tx_group
datasource:
driver-class-name: com.mysql.jdbc.Driver
password: 123456
url: jdbc:mysql://localhost:3306/seat-storage
username: root
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
file.conf文件、registry.conf文件和上一个工程代码一样
4.6.5 创建数据库seat-account
然后导入创建表的sql语句
CREATE TABLE `account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`total` decimal(10,0) DEFAULT NULL COMMENT '总额度',
`used` decimal(10,0) DEFAULT NULL COMMENT '已用余额',
`residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用额度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seat-account`.`account` (`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
drop table `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=utf8;
4.6.6 创建数据库seat-order
然后导入创建表的sql语句
CREATE TABLE `order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
`count` int(11) DEFAULT NULL COMMENT '数量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
ALTER TABLE `order` ADD COLUMN `status` int(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结' AFTER `money` ;
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
drop table `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=utf8;
4.6.7 创建数据库seat-storage
然后导入创建表的sql语句
CREATE TABLE `storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
`total` int(11) DEFAULT NULL COMMENT '总库存',
`used` int(11) DEFAULT NULL COMMENT '已用库存',
`residue` int(11) DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seat-storage`.`storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
drop table `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=utf8;
4.6.8 创建数据库seata
然后导入创建表的sql语句
-- the table to store GlobalSession data
drop table if exists `global_table`;
create table `global_table` (
`xid` varchar(128) not null,
`transaction_id` bigint,
`status` tinyint not null,
`application_id` varchar(32),
`transaction_service_group` varchar(32),
`transaction_name` varchar(128),
`timeout` int,
`begin_time` bigint,
`application_data` varchar(2000),
`gmt_create` datetime,
`gmt_modified` datetime,
primary key (`xid`),
key `idx_gmt_modified_status` (`gmt_modified`, `status`),
key `idx_transaction_id` (`transaction_id`)
);
-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
`branch_id` bigint not null,
`xid` varchar(128) not null,
`transaction_id` bigint ,
`resource_group_id` varchar(32),
`resource_id` varchar(256) ,
`lock_key` varchar(128) ,
`branch_type` varchar(8) ,
`status` tinyint,
`client_id` varchar(64),
`application_data` varchar(2000),
`gmt_create` datetime,
`gmt_modified` datetime,
primary key (`branch_id`),
key `idx_xid` (`xid`)
);
-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
`row_key` varchar(128) not null,
`xid` varchar(96),
`transaction_id` long ,
`branch_id` long,
`resource_id` varchar(256) ,
`table_name` varchar(32) ,
`pk` varchar(36) ,
`gmt_create` datetime ,
`gmt_modified` datetime,
primary key(`row_key`)
);
等安装好Seata,再测试
4.7 安装Seata
1.下载seata seata-server-0.9.0.zip
2.解压
3.修改 seata的配置文件conf文件夹下的file.conf文件
下面是我的file.conf文件内容
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#vgroup->rgroup
vgroup_mapping.fsp_tx_group = "default"
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
client {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
}
report.retry.count = 5
tm.commit.retry.count = 1
tm.rollback.retry.count = 1
}
## transaction log store
store {
## store mode: file、db
## mode = "file"
mode = "db" #修改此处将事务信息存储到数据库中
## file store
file {
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
max-branch-session-size = 16384
# globe session size , if exceeded throws exceptions
max-global-session-size = 512
# file buffer size , if exceeded allocate new buffer
file-write-buffer-cache-size = 16384
# when recover batch read size
session.reload.read_size = 100
# async, sync
flush-disk-mode = async
}
## database store
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://localhost:3306/seata" #修改数据库连接地址
user = "root" #修改数据库用户名
password = "123456" #修改数据库密码
min-conn = 1
max-conn = 3
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
}
lock {
## the lock store mode: local、remote
mode = "remote"
local {
## store locks in user's database
}
remote {
## store locks in the seata's server
}
}
recovery {
#schedule committing retry period in milliseconds
committing-retry-period = 1000
#schedule asyn committing retry period in milliseconds
asyn-committing-retry-period = 1000
#schedule rollbacking retry period in milliseconds
rollbacking-retry-period = 1000
#schedule timeout retry period in milliseconds
timeout-retry-period = 1000
}
transaction {
undo.data.validation = true
undo.log.serialization = "jackson"
undo.log.save.days = 7
#schedule delete expired undo_log in milliseconds
undo.log.delete.period = 86400000
undo.log.table = "undo_log"
}
## metrics settings
metrics {
enabled = false
registry-type = "compact"
# multi exporters use comma divided
exporter-list = "prometheus"
exporter-prometheus-port = 9898
}
support {
## spring
spring {
# auto proxy the DataSource bean
datasource.autoproxy = false
}
}
4.配置 seata的nacos 修改conf文件夹下的registry.conf
下面是我的registry.conf文件内容
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
# type = "file"
type = "nacos" #改为nacos
nacos {
# serverAddr = "localhost"
serverAddr = "localhost:8848" #改为nacos的连接地址
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
启动nacos,找到Nacos安装目录,进入到bin目录下
启动Seata,找到Seata安装目录,进入到bin目录下
启动方式和Nacos一样
4.8 测试
测试:http://localhost:8180/order/create?userId=1&productId=1&count=1&money=100
正常测试:三个数据库中的表的数据一致
当在账户服务中发生异常时:可以保证数据的一致
将第一个工程(seata-account-service)的AccountServiceImpl类中的异常打开
将第二个工程(seata-order-service)的OrderServiceImpl的全局异常注解开启
测试:http://localhost:8180/order/create?userId=2&productId=2&count=10&money=200
发现数据没有变化,即事务开启了,并回滚了