seata用来解决分布式事务数据不一致问题
官网下载地址:https://seata.io/zh-cn/blog/download.html
新建数据库seata,然后在解压的seata文件找到script->server->db->mysql.sql,执行这个sql脚本。
config.txt
文件在seata/script/config-center
目录下,需要修改的地方如下:
进入seata目录,找到nacos-config.sh,路径为:script->config-center->nacos->nacos-config.sh
进入cmd窗口,执行以下命令:
sh nacos-config.sh -h 192.168.80.1 -p 8848 -g SEATA_GROUP -t ff6107f0-631b-499a-b2ba-ad436a958c28 -u nacos -w nacos
参数详解:
-h nacos服务IP
-p nacos服务端口
-u nacos登录名
-w nacos登录密码
-g nacos 配置的分组名称,默认设置SEATA_GROUP
-t 上一步配置的命名空间ID
执行成功后到nacos控制台配置列表查看多了许多配置。
进入seata/conf目录下,有两个配置文件,把application.yml 随意修改一个名字,然后把 application.example.yml修改成application.yml 作为主要配置文件。
修改application.yml文件,这里使用的nacos作为注册中心,所以需要修改的地方有:
seata.security
下的所有配置信息复制到现在的application.yml
下。在MySQL5.7之前的版本,安全性较低,存在任何用户都可以连接上数据库,所以官方在5.7版本加大了对隐私的保护。并且采用了默认 useSSL = true值防止对数据库的随意修改,到了8.0版本,仍然保留了SSL,并且默认值为 true。这里要可以对数据库进行修改,所以在数据库配置url后追加&useSSL=false
;需要检查yml配置文件和Nacos上配置列表中。store.db.url
。
最终配置文件如下:
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: seata
password: seata
seata:
config:
# support: nacos 、 consul 、 apollo 、 zk 、 etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ff6107f0-631b-499a-b2ba-ad436a958c28
group: SEATA_GROUP
username: nacos
password: nacos
#context-path:
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key:
#secret-key:
data-id: seataServer.properties
registry:
# support: nacos 、 eureka 、 redis 、 zk 、 consul 、 etcd3 、 sofa
type: nacos
preferred-networks: 30.240.*
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: ff6107f0-631b-499a-b2ba-ad436a958c28
cluster: default
username: nacos
password: nacos
#context-path:
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key:
#secret-key:
server:
service-port: 8091 #If not configured, the default is '${server.port} + 1000'
max-commit-retry-timeout: -1
max-rollback-retry-timeout: -1
rollback-retry-timeout-unlock-enable: false
enable-check-auth: true
enable-parallel-request-handle: true
retry-dead-threshold: 130000
xaer-nota-retry-timeout: 60000
enableParallelRequestHandle: true
recovery:
committing-retry-period: 1000
async-committing-retry-period: 1000
rollbacking-retry-period: 1000
timeout-retry-period: 1000
undo:
log-save-days: 7
log-delete-period: 86400000
session:
branch-async-queue-size: 5000 #branch async remove queue size
enable-branch-async-remove: false #enable to asynchronous remove branchSession
store:
# support: file 、 db 、 redis
mode: db
session:
mode: db
lock:
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true&useSSL=false
username: root
password: 123456
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 1000
max-wait: 5000
metrics:
enabled: false
registry-type: compact
exporter-list: prometheus
exporter-prometheus-port: 9898
transport:
rpc-tc-request-timeout: 15000
enable-tc-server-batch-send-response: false
shutdown:
wait: 3
thread-factory:
boss-thread-prefix: NettyBoss
worker-thread-prefix: NettyServerNIOWorker
boss-thread-size: 1
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login
找到seata-server.bat,点击启动,路径为:seata->bin->seata-server.bat,成功后可以在nacos控制台服务列表中看到多了一个服务。
写订单(order)、账户(account)和库存(storage)三个微服务,他们之间事务联系如下:
下订单->减库存->扣余额->修改订单状态
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan({"com.atguigu.springcloud.alibaba.dao"})
public class MyBatisConfig {
}
import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
@Configuration
public class SeataConfig {
@Autowired
DataSourceProperties dataSourceProperties;
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties) {
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())) {
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
}
cloud2020
com.atguigu.springcloud
1.0-SNAPSHOT
4.0.0
seata-order-service2001
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
io.seata
seata-spring-boot-starter
1.7.0
io.seata
seata-all
1.7.0
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
2022.0.0.0-RC2
io.seata
seata-spring-boot-starter
io.seata
seata-all
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
mysql
mysql-connector-java
5.1.37
com.alibaba
druid-spring-boot-starter
1.1.10
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.0
org.springframework.boot
spring-boot-starter-test
test
org.projectlombok
lombok
true
server:
port: 2001
spring:
application:
name: order
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.gjt.mm.mysql.Driver
url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
cloud:
nacos:
discovery:
server-addr: localhost:8848
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
seata:
enabled: true
application-id: order
# 是否启用数据源bean的自动代理
enable-auto-data-source-proxy: false
service:
vgroup-mapping:
order-seata-service-group: default # 必须和服务器配置一样
disable-global-transaction: false
tx-service-group: order-seata-service-group # 必须和服务器配置一样
registry:
type: nacos
nacos:
# Nacos 服务地址
server-addr: 192.168.80.1:8848
group: SEATA_GROUP
namespace: ff6107f0-631b-499a-b2ba-ad436a958c28
application: seata-server # 必须和服务器配置一样
username: nacos
password: nacos
cluster: default
config:
type: nacos
nacos:
server-addr: 192.168.80.1:8848
group: SEATA_GROUP
namespace: ff6107f0-631b-499a-b2ba-ad436a958c28
username: nacos
password: nacos
client:
rm:
# 是否上报成功状态
report-success-enable: true
# 重试次数
report-retry-count: 5
因为三个微服务是3个不同的数据源(3个不同数据库)SpringBoot里面默认存在单数据源,所以只能启动其中一个服务,如果想同时启动多个服务,需要在启动配置类下添加(exclude={DataSourceAutoConfiguration.class}),禁止SpringBoot自动注入数据源,使用我们自己配置的数据源SeataConfig。
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableFeignClients
@EnableDiscoveryClient
public class SeataOrderService2001Application {
public static void main(String[] args) {
SpringApplication.run(SeataOrderService2001Application.class,args);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status; //订单状态:0:创建中;1:已完结
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message) {
this(code, message, null);
}
}
@Mapper
public interface OrderDao {
//新建订单
void create(Order order);
//修改订单状态,从0改为1
void update(@Param("userId") Long userId,@Param("status") Integer status);
}
insert into t_order(user_id, product_id, count, money, status)
value (#{userId},#{productId},#{count},#{money},0)
update t_order
set status = 1
where user_id = #{userId}
and status = #{status}
public interface OrderService {
void create(Order order);
}
这里使用了openfeign来做远程服务调用
@FeignClient(value = "account")
public interface AccountService {
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
@FeignClient(value ="storage")
public interface StorageService {
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId,@RequestParam("count") Integer count);
}
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private AccountService accountService;
@Resource
private StorageService storageService;
@Override
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order) {
log.info("----->开始新建订单");
//1 新建订单
orderDao.create(order);
//2 扣减库存
log.info("----->订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(), order.getCount());
log.info("----->订单微服务开始调用库存,做扣减end");
//3 扣减账户
log.info("----->订单微服务开始调用账户,做扣减Money");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----->订单微服务开始调用账户,做扣减end");
//4 修改订单状态,从零到1,1代表已经完成
log.info("----->修改订单状态开始");
orderDao.update(order.getUserId(), 0);
log.info("----->修改订单状态结束");
log.info("----->下订单结束了,O(∩_∩)O哈哈~");
}
}
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order) {
orderService.create(order);
return new CommonResult(200, "订单创建成功");
}
}
配置内容都是default
赋值订单微服务的
cloud2020
com.atguigu.springcloud
1.0-SNAPSHOT
4.0.0
seata-account-service2003
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
io.seata
seata-spring-boot-starter
1.7.0
io.seata
seata-all
1.7.0
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
2022.0.0.0-RC2
io.seata
seata-spring-boot-starter
io.seata
seata-all
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.0
mysql
mysql-connector-java
5.1.37
com.alibaba
druid-spring-boot-starter
1.1.10
org.projectlombok
lombok
true
server:
port: 2003
spring:
application:
name: account
cloud:
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account
username: root
password: 123456
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
seata:
enabled: true
application-id: account
# 是否启用数据源bean的自动代理
enable-auto-data-source-proxy: false
service:
vgroup-mapping:
account-seata-service-group: default # 必须和服务器配置一样
disable-global-transaction: false
tx-service-group: account-seata-service-group # 必须和服务器配置一样
registry:
type: nacos
nacos:
# Nacos 服务地址
server-addr: 192.168.80.1:8848
group: SEATA_GROUP
namespace: ff6107f0-631b-499a-b2ba-ad436a958c28
application: seata-server # 必须和服务器配置一样
username: nacos
password: nacos
cluster: default
config:
type: nacos
nacos:
server-addr: 192.168.80.1:8848
group: SEATA_GROUP
namespace: ff6107f0-631b-499a-b2ba-ad436a958c28
username: nacos
password: nacos
client:
rm:
# 是否上报成功状态
report-success-enable: true
# 重试次数
report-retry-count: 5
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataAccountService2003Application {
public static void main(String[] args) {
SpringApplication.run(SeataAccountService2003Application.class, args);
System.out.println("启动成功");
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 总额度
*/
private BigDecimal total;
/**
* 已用额度
*/
private BigDecimal used;
/**
* 剩余额度
*/
private BigDecimal residue;
}
赋值order微服务的
@Mapper
public interface AccountDao {
/**
* 扣减账户余额
* @param userId
* @param money
*/
void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
UPDATE t_account
SET residue = residue - #{money},
used = used + #{money}
WHERE user_id = #{userId};
public interface AccountService {
void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
@Service
public class AccountServiceImpl implements AccountService {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
@Resource
AccountDao accountDao;
/**
* 扣减账户余额
*/
@Override
public void decrease(Long userId, BigDecimal money) {
LOGGER.info("------->account-service中扣减账户余额开始");
accountDao.decrease(userId, money);
LOGGER.info("------->account-service中扣减账户余额结束");
}
}
@RestController
public class AccountController {
@Resource
private AccountService accountService;
/**
* 扣减账户余额
*/
@RequestMapping("/account/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) {
accountService.decrease(userId, money);
return new CommonResult(200, "扣减账户余额成功!");
}
}
赋值订单微服务的
cloud2020
com.atguigu.springcloud
1.0-SNAPSHOT
4.0.0
seata-order-service2002
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
io.seata
seata-spring-boot-starter
1.7.0
io.seata
seata-all
1.7.0
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
2022.0.0.0-RC2
io.seata
seata-spring-boot-starter
io.seata
seata-all
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.0
mysql
mysql-connector-java
5.1.37
com.alibaba
druid-spring-boot-starter
1.1.10
org.projectlombok
lombok
true
server:
port: 2002
spring:
application:
name: storage
cloud:
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage
username: root
password: 123456
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
seata:
enabled: true
application-id: storage
# 是否启用数据源bean的自动代理
enable-auto-data-source-proxy: false
service:
vgroup-mapping:
storage-seata-service-group: default
disable-global-transaction: false
tx-service-group: storage-seata-service-group # 必须和服务器配置一样
registry:
type: nacos
nacos:
# Nacos 服务地址
server-addr: 192.168.80.1:8848
group: SEATA_GROUP
namespace: ff6107f0-631b-499a-b2ba-ad436a958c28
application: seata-server # 必须和服务器配置一样
username: nacos
password: nacos
cluster: default
config:
type: nacos
nacos:
server-addr: 192.168.80.1:8848
group: SEATA_GROUP
namespace: ff6107f0-631b-499a-b2ba-ad436a958c28
username: nacos
password: nacos
client:
rm:
# 是否上报成功状态
report-success-enable: true
# 重试次数
report-retry-count: 5
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataStorageService2002Application {
public static void main(String[] args) {
SpringApplication.run(SeataStorageService2002Application.class, args);
System.out.println("启动成功");
}
}
@Data
public class Storage {
private Long id;
/**
* 产品id
*/
private Long productId;
/**
* 总库存
*/
private Integer total;
/**
* 已用库存
*/
private Integer used;
/**
* 剩余库存
*/
private Integer residue;
}
赋值订单微服务的
@Mapper
public interface StorageDao {
//扣减库存
void decrease(@Param("productId") Long productId,@Param("count") Integer count);
}
UPDATE
t_storage
SET used = used + #{count},
residue = residue - #{count}
WHERE product_id = #{productId}
public interface StorageService {
void decrease(Long productId,Integer count);
}
@Service
public class StorageServiceImpl implements StorageService {
private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);
@Resource
private StorageDao storageDao;
/**
* 扣减库存
*/
@Override
public void decrease(Long productId, Integer count) {
LOGGER.info("------->storage-service中扣减库存开始");
storageDao.decrease(productId, count);
LOGGER.info("------->storage-service中扣减库存结束");
}
}
@RestController
public class StorageController {
@Resource
private StorageService storageService;
@RequestMapping("/storage/decrease")
public CommonResult decrease(Long productId,Integer count){
storageService.decrease(productId, count);
return new CommonResult(200,"库存扣减成功");
}
}
localhost:2001/order/create?userId=1&count=10&money=100
数据库情况:
AccountServiceImpl添加超时,然后重启账户微服务。
@Override
public void decrease(Long userId, BigDecimal money) {
LOGGER.info("------->account-service中扣减账户余额开始");
//模拟超时异常
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountDao.decrease(userId, money);
LOGGER.info("------->account-service中扣减账户余额结束");
}
openFien默认1秒访问不了就抛出异常,AccountServiceImpl添加睡眠20秒,肯定会发生报错,这时数据不能保持一致性了,这里出现错误页面可以写降级方法。
数据库情况:
当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从零改为1;
而且由于feign的重试机制,账户余额还有可能被多次扣减。
在OrderServiceImpl类业务方法处添加@GlobalTransactional注解,重启订单微服务,name随便取名,只要保证唯一就可以,rollbackFor = Exception.class表示发生任何异常都进行回滚。
@Override
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order) {
log.info("----->开始新建订单");
//1 新建订单
orderDao.create(order);
//2 扣减库存
log.info("----->订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(), order.getCount());
log.info("----->订单微服务开始调用库存,做扣减end");
//3 扣减账户
log.info("----->订单微服务开始调用账户,做扣减Money");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----->订单微服务开始调用账户,做扣减end");
//4 修改订单状态,从零到1,1代表已经完成
log.info("----->修改订单状态开始");
orderDao.update(order.getUserId(), 0);
log.info("----->修改订单状态结束");
log.info("----->下订单结束了,O(∩_∩)O哈哈~");
}
此时订单没有添加成功,库存和账户都没有被扣减,说明发生了回滚,写操作没提交。前台该做降级做降级,该做异常处理再做异常处理。
参考:
从安装 Seata 开始的分布式事务之旅 springboot集成seata (niftyadmin.cn)https://www.niftyadmin.cn/n/4933186.html?action=onClick