sprngcoud alibaba seata1.4集群使用(个人笔记)
1.下载
官网seata.io
2.建库建表
create database seata;
-- -------------------------------- The script used when storeMode is 'db' -------------------------------- -- the table to store GlobalSession data CREATE TABLE IF NOT EXISTS
global_table(
xidVARCHAR(128) NOT NULL,
transaction_idBIGINT,
statusTINYINT NOT NULL,
application_idVARCHAR(32),
transaction_service_groupVARCHAR(32),
transaction_nameVARCHAR(128),
timeoutINT,
begin_timeBIGINT,
application_dataVARCHAR(2000),
gmt_createDATETIME,
gmt_modifiedDATETIME, PRIMARY KEY (
xid), KEY
idx_gmt_modified_status(
gmt_modified,
status), KEY
idx_transaction_id(
transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
– the table to store BranchSession data
CREATE TABLE IF NOT EXISTS branch_table
(
branch_id
BIGINT NOT NULL,
xid
VARCHAR(128) NOT NULL,
transaction_id
BIGINT,
resource_group_id
VARCHAR(32),
resource_id
VARCHAR(256),
branch_type
VARCHAR(8),
status
TINYINT,
client_id
VARCHAR(64),
application_data
VARCHAR(2000),
gmt_create
DATETIME(6),
gmt_modified
DATETIME(6),
PRIMARY KEY (branch_id
),
KEY idx_xid
(xid
)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
– the table to store lock data
CREATE TABLE IF NOT EXISTS lock_table
(
row_key
VARCHAR(128) NOT NULL,
xid
VARCHAR(96),
transaction_id
BIGINT,
branch_id
BIGINT NOT NULL,
resource_id
VARCHAR(256),
table_name
VARCHAR(32),
pk
VARCHAR(36),
gmt_create
DATETIME,
gmt_modified
DATETIME,
PRIMARY KEY (row_key
),
KEY idx_branch_id
(branch_id
)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;`
3.修改配置文件
conf目录下file.conf文件
以上所填的数据库地址就是,刚才建表建库的那个地址。(上面的那些表都是在全局事务运行期间所需要的,但是事务一结束就会自动删除掉里面的数据)
4.修改registry.conf文件
5.一些电脑没有配置的好的话是无法启动的
用cmd命令窗口启动可以看到错误信息。
错误可能1
他可能会提示一个找不到 jvm.dll文件 你可以在他给的那个目录下的某一个文件夹下去找这个文件(直接在那个目录下进行搜索)
错误可能2
内存不足
可以直接修改seata-server.bat这个文件中的内容
此时便可完美启动。(前提必须启动nacos服务器)
启动成功后可在nacos中看到
6.版本控制
1.父项目pom
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<spring.boot.version>2.2.4.RELEASE</spring.boot.version>
<spring.cloud.version>Hoxton.SR1</spring.cloud.version>
<spring.cloud.alibaba.version>2.2.0.RELEASE</spring.cloud.alibaba.version>
</properties>
<!--
引入 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者 BOM 文件,进行依赖版本的管理,防止不兼容。
在 https://dwz.cn/mcLIfNKt 文章中,Spring Cloud Alibaba 开发团队推荐了三者的依赖关系
-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.子项目pom
<dependencies>
<!-- 实现对 Spring MVC 的自动化配置 -->
<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>
<!-- 实现对数据库连接池的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency> <!-- 本示例,我们使用 MySQL -->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- 实现对 MyBatis 的自动化配置 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- 引入 Spring Cloud Alibaba Seata 相关依赖,使用 Seata 实现分布式事务,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
</dependency>
<dependency> <!-- 主要想使用 seata 1.1.0 版本 -->
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
<!-- 引入 Spring Cloud Alibaba Nacos Discovery 相关依赖,将 Nacos 作为注册中心,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入 Spring Cloud OpenFeign 相关依赖,使用 OpenFeign 提供声明式调用,并实现对其的自动配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
</dependencies>
7.账户服务
1.mapper
@Mapper
public interface AccountMapper {
public void payment(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
2.service
public interface AccountService {
public CommonResult payment(Long userId, BigDecimal money);
}
3.serviceimpl
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Resource
private AccountMapper accountMapper;
@Override
public CommonResult payment(Long userId, BigDecimal money) {
log.info("开始支付");
int a = 1/0;
accountMapper.payment(userId, money);
log.info("支付成功");
return new CommonResult(200,"支付成功");
}
}
4.controller
@RestController
public class AccountController {
@Autowired
private AccountService accountService;
@PostMapping("/account/payment")
public CommonResult payment(@RequestParam("userId")Long userId, @RequestParam("money") BigDecimal money){
return accountService.payment(userId,money);
}
}
5.domain
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 总额度
*/
private BigDecimal total;
/**
* 已用额度
*/
private BigDecimal used;
/**
* 剩余额度
*/
private BigDecimal residue;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}
6.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.ljc.springcloud.mapper.AccountMapper">
<resultMap id="BaseResultMap" type="com.ljc.springcloud.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="payment">
UPDATE t_account
SET
residue = residue - #{money},used = used + #{money}
WHERE
user_id = #{userId};
</update>
</mapper>
7.主启动
@SpringBootApplication//开启自动配置
@EnableFeignClients//表示是feign客户端(本次demo可以不要,因为它没有远程调用)
@EnableDiscoveryClient//注册到服务注册中心
@MapperScan("com.ljc.springcloud.mapper")//mybatis扫描mapper包
public class AccountMain2003 {
public static void main(String[] args) {
SpringApplication.run(AccountMain2003.class,args);
}
}
8.yaml
server:
port: 2003
spring:
application:
name: seata-account-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.83.130:3306/seata_account?useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password: root
seata:
application-id: ${spring.application.name} # Seata 应用编号,默认为 ${spring.application.name}
tx-service-group: ${spring.application.name}-group # Seata 事务组编号,用于 TC 集群名
# Seata 服务配置项,对应 ServiceProperties 类
service:
# 虚拟组和分组的映射
vgroup-mapping:
seata-account-service-group: default
# Seata 注册中心配置项,对应 RegistryProperties 类
registry:
type: nacos # 注册中心类型,默认为 file
nacos:
cluster: default # 使用的 Seata 分组
namespace: # Nacos 命名空间
serverAddr: localhost # Nacos 服务地址
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
8.库存服务
1.mapper
@Mapper
public interface StorageMapper {
public void update(@Param("productId") Long productId, @Param("count") Integer count);
}
2.service
public interface StorageService {
public void update(Long productId,Integer count);
}
3.serviceImpl
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Resource
private StorageDao storageDao;
@Override
public void update(Long productId, Integer count) {
log.info("StorageServiceImpl 减库存");
storageDao.update(productId,count);
log.info("StorageServiceImpl 减库存成功");
}
}
4.controller
@RestController
public class StorageController {
@Autowired
private StorageService storageService;
@PostMapping("/storage/update")
public CommonResult update(@RequestParam("productId") Long productId, @RequestParam("count") Integer count){
System.out.println(productId+" "+count);
storageService.update(productId,count);
return new CommonResult(200,"出库成功!!!");
}
}
5.domain
@Data
public class Storage {
000
private Long id;
// 产品id
private Long productId;
//总库存
private Integer total;
//已用库存
private Integer used;
//剩余库存
private Integer residue;
}
6.yaml与以上相同(只是端口和服务名不同)
7.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.ljc.springcloud.dao.StorageDao">
<resultMap id="storageMap" type="com.ljc.springcloud.domain.Storage">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="productId" column="productId" jdbcType="BIGINT"/>
<result property="total" column="total" jdbcType="INTEGER"/>
<result property="used" column="used" jdbcType="BIGINT"/>
<result property="residue" column="residue" jdbcType="BIGINT"/>
</resultMap>
<update id="update">
update t_storage set used = used + #{count},residue = residue - #{count} where product_id = #{productId};
</update>
</mapper>
8.主启动
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan({"com.ljc.springcloud.mapper"})
public class StorageMain2002 {
public static void main(String[] args) {
SpringApplication.run(StorageMain2002.class,args);
}
}
9.订单服务
1.mappr
@Mapper
public interface OrderMapper {
/**
* 订单业务
* @param order
*/
public void createOrder(Order order);
/**
* 修改订单状态
* @param userId
* @param status
*/
public void update(@Param("userId") Long userId, @Param("status") Integer status);
}
2.service
public interface OrderService {
/**
* 创建订单
* @param order
*/
public void createOrder(Order order);
}
3.serviceImpl
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private AccountService accountService;
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Override
public void createOrder(Order order) {
log.info("订单创建...........");
orderDao.createOrder(order);
log.info("订单创建成功.......");
log.info("减库存.............");
storageService.update(order.getProductId(),order.getCount());
log.info("减库存成功.........");
log.info("账户扣款...........");
accountService.payment(order.getUserId(),order.getMoney());
log.info("账户扣款成功.......");
log.info("修改订单状态.......");
orderDao.update(order.getUserId(),0);
log.info("修改订单状态成功...");
}
}
4.feign
@FeignClient("seata-account-service")//想要调用的微服务名
public interface AccountService {
/**
* 用户支付
* @param userId
* @param money
*/
@PostMapping("/account/payment")
public CommonResult payment(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
@FeignClient("seata-storage-service")
public interface StorageService {
/**
* 订单状态修改
* @param productId
* @param count
*/
@PostMapping("/storage/update")
public CommonResult update(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
5.controller
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/order/create")
//全局事务(TM向TC端发起事务请求返回xid并依次在需要调用的微服务之间传递(RM))
@GlobalTransactional(name="tx-order-fsp",rollbackFor = Exception.class)
public CommonResult createOrder(Order order){
orderService.createOrder(order);
return new CommonResult(200,"订单创建成功!!!!!");
}
}
在微服务调用链路中,的最前面一个微服务(或者需要最先操作数据库的那个微服务上注上GlobalTransactional此时这个地方相当于TM向TC发起事务请求)
6.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.ljc.springcloud.dao.OrderDao">
<resultMap id="OrderMap" type="com.ljc.springcloud.domain.Order">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="userId" column="userId" jdbcType="BIGINT"/>
<result property="productId" column="productId" jdbcType="BIGINT"/>
<result property="count" column="count" jdbcType="INTEGER"/>
<result property="money" column="money" jdbcType="DECIMAL"/>
<result property="status" column="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="createOrder">
insert into t_order(id,user_id,product_id,count,money,status)
values(null,#{userId},#{productId},#{count},#{money},0);
</insert>
<update id="update">
update t_order set status = 1 where user_id = #{userId} and status = #{status};
</update>
</mapper>
7.主启动
@SpringBootApplication//取消数据库的自动装配使用自己的seata代理
@EnableFeignClients
@EnableDiscoveryClient
@MapperScan({"com.ljc.springcloud.dao"})
public class OrderMain2001 {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(OrderMain2001.class, args);
}
}
8.yml
与以上相同只是端口和服务名不同
10.测试
原数据库数据
正常测试
测试结果
异常启动
手动在account服务中添加一个by/zero异常
重启account服务
同样的请求在访问一次
结果
控制台信息
数据库
全局事务成功!!!!!
11原理
术语
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
seata有许多模式 AT、TCC、SAGA 和 XA 事务模式
默认是AT模式
AT:提供无侵入自动补偿的事务模式,目前已支持 MySQL、 Oracle 、PostgreSQL和 TiDB的AT模式,H2 开发中
TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。 XID,会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。 RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。 TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。 TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。
脏写概念:在高并发情况下可能一个元组被多次修改导致,数据库实际数据和后置数据不同。
在执行sql之前会有一个前置镜像在执行之后会有一个后置镜像然后加上一个行锁
当执行成功时会删除前置镜像和后置镜像以及行锁完成提交。
但当执行失败那么会把数据库当前数据与后置镜像想对比若一致则说明没有被脏写回滚,否则只能人工处理。
前置镜像和后置镜像类比一下
--前
select money from account where uid = 1; --结果 1000
--需要执行的sql
update account set money = money - 100 where uid = 1;
--后
select money from account where uid = 1 ; --结果 900
当发生异常时,当数据库中是900且 后置也是900时说明没有被脏写,那么回滚。将数据库数据改为前置数据1000
学习自:尚硅谷周阳老师springcloud2020,以及seata.io官方文档以及官方博客。