SpringCloud Alibaba使用(一) - Nacos服务注册、配置中心、集群和持久化
SpringCloud Alibaba使用(二) - sentinel 流控规则、降级规则、热点key限流、系统规则、@SentinelResource
SpringCloud Alibaba使用(三) - sentinel fallback整合ribbon+openFeign 服务熔断功能 规则持久化
SpringCloud Alibaba使用(四) - seata分布式事务
1.Seata简介
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务
一个典型的分布式事务过程:分布式事务处理过程的-ID+三组件模型
Transaction ID XID :全局唯一的事务ID
Transaction Coordinator(TC) :事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;
Transaction Manager(TM) :控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
Resource Manager(RM) :控制分支事务,负责分支注册,状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚;
过程:
Spring 本地@Transactional
全局@GlobalTransactional
2.Seata-Server安装
2.1 file.conf文件修改
主要修改:自定义事务组名称+事务日志存储模式为db+数据库连接信息
2.2 mysql新建库seata
seata\conf目录里面db_store.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`)
);
2.3修改seata\conf目录下的registry.conf配置文件
配置nacos
2.4 启动
先启动nacos 在启动bin/seata-server.bat
3.对seata的@GlobalTransactional事务进行测试
3.1新建数据库
#订单库-----------------------------------------------------------------
CREATE DATABASE seata_order;
CREATE TABLE t_order(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`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 '金额',
`status` INT(1) DEFAULT NULL COMMENT '订单状态:0:创建中; 1:已完结'
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
SELECT * FROM t_order;
#库存库-----------------------------------------------------------------
CREATE DATABASE seata_storage;
CREATE TABLE t_storage(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`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 '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_storage.t_storage(`id`,`product_id`,`total`,`used`,`residue`)
VALUES('1','1','100','0','100');
#余额库-----------------------------------------------------------------
CREATE DATABASE seata_account;
CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY 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 '剩余可用额度'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_account.t_account(`id`,`user_id`,`total`,`used`,`residue`) VALUES('1','1','1000','0','1000')
SELECT * FROM t_account;
3.1.3 三库分别建对应的回滚日志表
-- 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;
最终效果
3.2相同模块
3.2.1pom
<dependencies>
<dependency>
<groupId>com.psf.spirngcloud.Alibaba</groupId>
<artifactId>alibaba-psf-basic</artifactId>
<version>${project.version}</version>
</dependency>
<!--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>1.0.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<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>
<!--mysql-druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3.2.2 DataSourceProxyConfig
其中最为重要的是将Seata所提供的数据源代理(DataSourceProxy)实例化配置放到了这个模块中,数据库代理相关配置代
package com.psf.springcloud.Alibaba.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
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();
}
}
3.2.3 MyBatisConfig
package com.psf.springcloud.Alibaba.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan({"com.psf.springcloud.Alibaba.dao"})
public class MyBatisConfig {
}
3.2.4 file.conf 和 registry.confq
复制seata/conf/file.conf文件及 registry.conf
file.conf
service {
#transaction service group mapping
vgroup_mapping.seata_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#disable seata
disableGlobalTransaction = false
}
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
}
## database store property
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://127.0.0.1:3306/seata"
user = "root"
password = "root"
}
}
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost:8848"
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"
}
}
3.2 新建订单seata-order-service8001
服务配置
server:
port: 8001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
#自定义事务组名称需要与seata-server中的对应
tx-service-group: seata_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order
username: root
password: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
业务
package com.psf.springcloud.Alibaba.service.impl;
import com.psf.springcloud.Alibaba.dao.OrderDao;
import com.psf.springcloud.Alibaba.entity.Order;
import com.psf.springcloud.Alibaba.service.AccountService;
import com.psf.springcloud.Alibaba.service.OrderService;
import com.psf.springcloud.Alibaba.service.StorageService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
//下订单->减库存->扣余额->改(订单)状态
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private AccountService accountService;
@Resource
private StorageService storageService;
@Override
//添加全局回滚
@GlobalTransactional(name = "fallback",rollbackFor = Exception.class)
public void Create(Order order) {
//1.新建订单
log.info("新建订单");
orderDao.insertOrder(order);
log.info("减库存");
storageService.decrease(order.getProductId(),order.getCount());
log.info("扣余额");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("改(订单)状态");
orderDao.updateOrder(order.getUserId(),0);
}
}
account接口
package com.psf.springcloud.Alibaba.service;
import com.psf.springcloud.Alibaba.entity.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
/**
* 余额account
*/
@FeignClient(value = "seata-account-service")
public interface AccountService {
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId")Long userId,@RequestParam("money") BigDecimal money);
}
库存storage接口
package com.psf.springcloud.Alibaba.service;
import com.psf.springcloud.Alibaba.entity.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 库存storage
*/
@FeignClient(value = "seata-storage-service")
public interface StorageService {
//减少库存
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId")Long productId,@RequestParam("count")Integer count);
}
controller
package com.psf.springcloud.Alibaba.controller;
import com.psf.springcloud.Alibaba.entity.CommonResult;
import com.psf.springcloud.Alibaba.entity.Order;
import com.psf.springcloud.Alibaba.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order){
orderService.Create(order);
return new CommonResult(200,"成功",order);
}
}
dao
package com.psf.springcloud.Alibaba.dao;
import com.psf.springcloud.Alibaba.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* @ClassName: OrderDao
* @Auther: Administrator
* @Date: 2020/6/30 16:36
* @return version 1.0
*/
@Mapper
public interface OrderDao {
/**
* 新增订单
* @param order
*/
void insertOrder(Order order);
/**
* 修改订单状态
* @param userId
* @param status
*/
void updateOrder(@Param("userId")Long userId,@Param("status")Integer status);
}
mapper
<?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.psf.springcloud.Alibaba.dao.OrderDao">
<resultMap id="baseResultMap" type="com.psf.springcloud.Alibaba.entity.Order">
<id column="id" property="id" jdbcType="BIGINT"></id>
<result column="user_id" property="userId" jdbcType="BIGINT"></result>
<result column="product_id" property="productId" jdbcType="BIGINT"></result>
<result column="count" property="count" jdbcType="INTEGER"></result>
<result column="money" property="money" jdbcType="DECIMAL"></result>
<result column="status" property="status" jdbcType="INTEGER"></result>
</resultMap>
<insert id="insertOrder" >
insert into t_order(id,user_id,product_id,count,money,status)
values (null,#{userId},#{productId},#{count},#{money},0)
</insert>
<update id="updateOrder">
update t_order set status=1 where user_id=#{userId} and status=#{status}
</update>
</mapper>
3.3 新建账户seata-account-service8002
服务配置
server:
port: 8002
spring:
application:
name: seata-account-service
cloud:
alibaba:
seata:
#自定义事务组名称需要与seata-server中的对应
tx-service-group: seata_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account
username: root
password: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
controller
package com.psf.springcloud.Alibaba.controller;
import com.psf.springcloud.Alibaba.entity.CommonResult;
import com.psf.springcloud.Alibaba.service.AccountService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.math.BigDecimal;
@RestController
@Slf4j
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,"余额扣减成功");
}
}
package com.psf.springcloud.Alibaba.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
@Mapper
public interface AccountDao {
void update(@Param("userId") Long userId,@Param("money") BigDecimal money);
}
<?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.psf.springcloud.Alibaba.dao.AccountDao">
<resultMap id="accountMap" type="com.psf.springcloud.Alibaba.entity.Account">
<id column="id" property="id" jdbcType="BIGINT"></id>
<result column="user_id" property="userId" jdbcType="BIGINT"></result>
<result column="total" property="total" jdbcType="DECIMAL"></result>
<result column="used" property="used" jdbcType="DECIMAL"></result>
<result column="residue" property="residue" jdbcType="DECIMAL"></result>
</resultMap>
<update id="update">
update t_account set used=used+#{money},residue=residue-#{money} where user_id=#{userId}
</update>
</mapper>
3.4 新建库存seata-storage-service8003
服务配置
server:
port: 8003
spring:
application:
name: seata-storage-service
cloud:
alibaba:
seata:
#自定义事务组名称需要与seata-server中的对应
tx-service-group: seata_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage
username: root
password: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
controller
package com.psf.springcloud.Alibaba.controller;
import com.psf.springcloud.Alibaba.entity.CommonResult;
import com.psf.springcloud.Alibaba.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class StorageController {
@Resource
private StorageService storageService;
@RequestMapping("/storage/decrease")
public CommonResult decrease(@RequestParam("productId")Long productId, @RequestParam("count")Integer count){
storageService.decrease(productId,count);
return new CommonResult(200,"修改库存");
}
}
package com.psf.springcloud.Alibaba.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface StorageDao {
void update(@Param("productId") Long productId,@Param("count") Integer count);
}
<?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.psf.springcloud.Alibaba.dao.StorageDao">
<resultMap id="storageMap" type="com.psf.springcloud.Alibaba.entity.Storage">
<id column="id" property="id" jdbcType="BIGINT"></id>
<result column="product_id" property="productId" jdbcType="BIGINT"></result>
<result column="total" property="total" jdbcType="INTEGER"></result>
<result column="used" property="used" jdbcType="INTEGER"></result>
<result column="residue" property="residue" jdbcType="INTEGER"></result>
</resultMap>
<update id="update">
update t_storage set used=used+#{count},residue=residue-#{count} where product_id=#{productId}
</update>
</mapper>
3.5添加全局回滚@GlobalTransactional
order
@GlobalTransactional(name = "fallback",rollbackFor = Exception.class)
public void Create(Order order) {
}
3.6启动测试
先启动 nacos,seata
再运行 8001–8002–8003
http://localhost:8001/order/create?userId=1&count=10&money=100&productId=1
不添加@GlobalTransactional时,运行异常时,当库存和账户余额扣减后,订单状态并没有设置为已经完成,没有从零改为1,而且由于feign的重试机制,账户余额还有可能被多次扣减。
添加@GlobalTransactional时:下单后数据库数据并没有任何改变,记录都添加不进来。