上一节介绍seata的安装。本节主要讲述使用seata实现分布式事务。
创建库和表
创建一个业务需要的数据库,例如seata_order。接下来是是创建表。
为了演示业务,创建三种表分别是order(订单表)、storage(商品库存表)、user_account(用户账号信息表)。
-- seata_order.user_account definition
CREATE TABLE `user_account` (
`userid` bigint(20) NOT NULL AUTO_INCREMENT,
`account` bigint(20) DEFAULT NULL,
`name` varchar(64) DEFAULT NULL,
PRIMARY KEY (`userid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- seata_order.storage definition
CREATE TABLE `storage` (
`commoditycode` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(64) DEFAULT NULL,
`count` bigint(20) DEFAULT NULL,
`price` bigint(20) DEFAULT NULL,
PRIMARY KEY (`commoditycode`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- seata_order.`order` definition
CREATE TABLE `order` (
`orderid` bigint(20) NOT NULL AUTO_INCREMENT,
`commoditycode` bigint(20) DEFAULT NULL,
`userId` bigint(20) DEFAULT NULL,
`num` bigint(20) DEFAULT NULL,
`money` bigint(20) DEFAULT NULL,
`status` bigint(20) DEFAULT NULL,
PRIMARY KEY (`orderid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
storage(商品库存表)和user_account(用户账号信息表)造几条数据备用。自己可以根据自己的喜好自己造,一下是我创建的几条数据,仅供参考。
这里还需要一张seata相关的表,跟业务表放在同一个数据库下,表结构如下
-- 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;
表中datetime类型改成timestamp,有疑问,请查阅上一节
商品微服务Goods
pom核心内容
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--引入nacos client的依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
</exclusion>
<exclusion>
<groupId>org.hdrhistogram</groupId>
<artifactId>HdrHistogram</artifactId>
</exclusion>
<exclusion>
<groupId>org.checkerframework</groupId>
<artifactId>checker-qual</artifactId>
</exclusion>
<exclusion>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotations</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- mybatis集成封装-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.springboot.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--mysql驱动包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 阿里巴巴连接池Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.springboot.version}</version>
</dependency>
</dependencies>
application.yml相关配置
server:
port: 7002
spring:
profiles:
active: dev
application:
name: GOODS
cloud:
nacos:
discovery:
namespace: ec6004af-f122-4ed1-b6d6-5d77ea5d5c94
group: SEATA_GROUP
server-addr: 192.168.43.85:8845,192.168.43.229:8846,192.168.43.251:8847
alibaba:
seata:
tx-service-group: my_test_tx_group
mybatis:
type-aliases-package: com.juwusheng.goods.model
mapper-locations: classpath:mapper/*.xml
#spring cloud alibaba 2.1.4 之后支持yml中配置seata属性,可以用来替换registry.conf文件
seata:
# seata 服务分组,要与服务端nacos-config.txt中service.vgroup_mapping的后缀对应
tx-service-group: my_test_tx_group
registry:
# 指定nacos作为注册中心
type: nacos
nacos:
server-addr: 192.168.43.85:8845,192.168.43.229:8846,192.168.43.251:8847
application: seata-server
group: SEATA_GROUP
namespace: ec6004af-f122-4ed1-b6d6-5d77ea5d5c94
config:
# 指定nacos作为配置中心
type: nacos
nacos:
server-addr: 192.168.43.85:8845,192.168.43.229:8846,192.168.43.251:8847
namespace: ec6004af-f122-4ed1-b6d6-5d77ea5d5c94
group: SEATA_GROUP
service:
vgroup-mapping:
my_test_tx_group: default
disable-global-transaction: false
client:
rm:
report-success-enable: false
上述主要是配置关于seata客户端注册到nacos先关配置。这里还增加了一个application-dev.yml的配置,主要配置数据库,application-dev.yml放在和application.yml同一个目录下。
spring:
datasource:
username: 账号
password: 密码
url: jdbc:mysql://127.0.0.1:3306/seata_order?characterEncoding=utf8&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5
min-idle: 5
max-wait: 60000
max-active: 20
validation-query: SELECT 1
stat-view-servlet: #启动控制平台
login-username: druid
login-password: druid
enabled: true
filter:
stat:
slow-sql-millis: 1
log-slow-sql: true
log4j2:
enabled: true
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
filters: stat,wall,log4j2
这里使用阿里druid的连接池,数据账号密码以及连接请更改。
主函数SpringBootApplication注解中剔除DataSourceAutoConfiguration。如下
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
手动配置数据库启动类MybatisConfig
package com.juwusheng.goods.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.annotation.MapperScan;
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.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import javax.sql.DataSource;
@Configuration
@MapperScan("com.juwusheng.goods.dao")
public class MybatisConfig {
@Value("${mybatis.mapper-locations:classpath:mapper/*.xml}")
String mybatisMapperLocation;
/**
* 从配置文件获取属性构造datasource,注意前缀,这里用的是druid,根据自己情况配置,
* 原生datasource前缀取"spring.datasource"
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
// /**
// * 构造datasource代理对象,替换原来的datasource
// * @param druidDataSource
// @Primary
// @Bean("dataSource")
// public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
// return new DataSourceProxy(druidDataSource);
// }*/
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactoryBean(DataSource druidDataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
//设置代理数据源
factoryBean.setDataSource(new DataSourceProxy(druidDataSource));
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources(mybatisMapperLocation));
org.apache.ibatis.session.Configuration configuration=new org.apache.ibatis.session.Configuration();
//使用jdbc的getGeneratedKeys获取数据库自增主键值
configuration.setUseGeneratedKeys(true);
//使用列别名替换列名
configuration.setUseColumnLabel(true);
//自动使用驼峰命名属性映射字段,如userId ---> user_id
configuration.setMapUnderscoreToCamelCase(true);
//更改默认的事务
factoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
factoryBean.setConfiguration(configuration);
return factoryBean.getObject();
}
}
因涉及三个微服务,代码较多,忽略部分非核心代码,比如数据库表对应的java的model对象类。
新增一个Controller的类,增加请求代码。
@PostMapping("/goods/deductStorage")
public String deductStorage(@RequestParam(value="id",defaultValue = "0") String id,@RequestParam(value="count",defaultValue = "0") String count) throws Exception {
boolean isSuccess=goodsService.deductStorage(id,count);
Map<String,String> map= new HashMap<>(8);
map.put("code",isSuccess?"ok":"fail");
map.put("msg",isSuccess?"成功":"失败");
return jsonUtil.objectToJson(map);
}
goodsService的核心代码
public boolean deductStorage(String id, String count) throws Exception {
logger.info("事务XID:{}", RootContext.getXID());
Storage storage = storageMapper.selectByPrimaryKey(Long.parseLong(id));
if (storage.getCount()<Long.parseLong(count)){
throw new Exception("库存不足");
}
int rowCount = storageMapper.storageReduce(Long.parseLong(id),Long.parseLong(count));
return rowCount > 0 ? true: false;
}
StorageMapper.xml中减少商品库存核心的sql
<update id="storageReduce" parameterType="java.lang.Long">
update storage
set count = count - #{count,jdbcType=BIGINT}
where commoditycode = #{id,jdbcType=BIGINT}
</update>
由于篇幅过长,下一节说明剩余的两个微服务的代码。