SpringCloud全家桶:SpringCloud01 – 初识
SpringCloud全家桶:SpringCloud02 – 服务注册 Eureka Zookeeper Consul Nacos
SpringCloud全家桶:SpringCloud03 – 服务调用 Ribbon OpenFeign
SpringCloud全家桶:SpringCloud04 – 服务降级熔断 Hystrix Sentinel
SpringCloud全家桶:SpringCloud05 – 服务网关 Gateway
SpringCloud全家桶:SpringCloud06 – 服务配置 Config Nacos
SpringCloud全家桶:SpringCloud07 – 消息总线 Bus
SpringCloud全家桶:SpringCloud08 – 消息驱动 Stream
SpringCloud全家桶:SpringCloud09 – 分布式请求链路追踪 Sleuth
SpringCloud全家桶:SpringCloud10 – Alibaba Nacos
SpringCloud全家桶:SpringCloud11 – Alibaba Sentinel
SpringCloud全家桶:SpringCloud12 – Alibaba 分布式事务 Seata
分布式前:
单机单库没有这个问题
从1:1 -> 1:N -> N:N
分布式后:
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用
,分别使用三个独立的数据源
业务操作需要调用三个服务来完成,此时每个服务内部的数据一致性由本地事务
来保证,但是全局
的数据一致性问题就没法保证。
场景:
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
==简单来讲一句话:==一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题
Seata是一款开源的分布式事务解决
方案,致力于在微服务架构下提供高性能
和简单易用
的分布式事务服务。
核心
:分布式事务处理过程的-ID + 三组件模型
Transaction ID XID 全局唯一的事务ID
三组件概念:
过程:
一个完整的分布式事务大致分为以下几步:
官网下载 https://github.com/seata/seata/releases/tag/v1.0.0
直接解压。
备份file.conf 并修改 本人使用的还是my_test_tx_group=default
用自定义的后期出现了错误
创建seata数据库 并导入sql文件
https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql
seata成功的标志
新建数据库
create database seata_order
create database seata_storage
create database seata_account
新建数据表
-- 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
-- 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;
drop table t_storage
INSERT INTO seata_storage.t_storage(id,product_id,total,used,residue)VALUES('1','1','100','0','100');
SELECT *FROM t_storage;
-- 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 null COMMENT '剩余可用额度'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_account.t_account(id,user_id,total,used,residue)VALUES(1,1,10000,1000,0)
SELECT*FROM t_account;
三张表都添加
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;
建成功后 数据库展示
业务需求:
下订单 --> 减库存 --> 扣余额 --> 改(订单)状态
pom
<dependencies>
<dependency>
<groupId>com.jsu.springcloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>1.0.0version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.10version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-captchaartifactId>
<version>5.2.0version>
dependency>
dependencies>
yml
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
# 自定义事务组名称需要与seata-server中对应 就是配置文件中service修改的
seata:
tx-service-group: my_test_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
# 当前数据源操作类型
type: com.alibaba.druid.pool.DruidDataSource
# mysql驱动类
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xx.x.x.x:x/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT
username: root
password: 555
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapper-locations: classpath*:mapper/*.xml
type-aliases-package: com.jsu.seata.pojo
修改的内容和本地安装Seata内容一样
// pojo CommonResult在cloud-api-commons中写过了
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
//订单状态 0:创建中 1:已完结
private Integer status;
}
// dao层 使用mybatis 不要使用mybatis-plus
@Mapper
public interface OrderDao {
/**
* 创建订单
*/
void create(Order order);
/**
* 修改订单状态 ,从0改到1
*/
void update(@Param("userId") Long userId,@Param("status") Integer status);
}
<mapper namespace="com.jsu.seata.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.jsu.seata.pojo.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 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>
// service
public interface OrderService {
/**
* 创建order
*
* @param order
*/
public void createOrder(Order order);
}
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
// StorageService AccountService 使用openFeign远程调用其他项目中的方法
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
* 简单说:下订单->扣库存->减余额->改状态
*/
@Override
// name 创建全局事务的名字
@GlobalTransactional(name = "springcloud-tx-group",rollbackFor = Exception.class)
public void createOrder(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哈哈~");
}
}
// StorageService OpenFeign
@Component
@FeignClient(value = "seata-storage-service")
public interface StorageService {
/**
* 做减少库存的操作 post请求携带参数
*/
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
// AccountService OpenFeign
@Component
@FeignClient(value = "seata-account-service")
public interface AccountService {
/**
* 对账户进行金额做扣减操作
*/
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
// controller
@RestController
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
@RequestMapping(value = "/order/create",method = RequestMethod.GET)
public CommonResult createOrder(Order order){
System.out.println(order);
orderService.createOrder(order);
return new CommonResult(200,"订单创建成功");
}
}
config配置类
Seata必须有@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapper-locations}")
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 sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSourceProxy);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
bean.setTransactionFactory(new SpringManagedTransactionFactory());
return bean.getObject();
}
}
@Configuration
@MapperScan({"com.jsu.seata.dao"})
public class MyBatisConfig {
}
主启动类
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataOrderMain2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMain2001.class);
}
}
pojo
@Data
public class Storage {
private Long id;
// 产品id
private Long productId;
// 总库存
private Integer total;
// 已用库存
private Integer used;
// 剩余库存
private Integer residue;
}
业务逻辑
// dao
@Mapper
public interface StorageDao {
//扣减库存
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
<mapper namespace="com.jsu.seata.dao.StorageDao">
<resultMap id="BaseResultMap" type="com.jsu.seata.pojo.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
t_storage
SET
used = used + #{count},residue = residue - #{count}
WHERE
product_id = #{productId}
update>
mapper>
// service
public interface StorageService {
// 扣减库存
void decrease(Long productId, Integer count);
}
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Resource
private StorageDao storageDao;
// 扣减库存
@Override
public void decrease(Long productId, Integer count) {
log.info("------->storage-service中扣减库存开始");
storageDao.decrease(productId,count);
log.info("------->storage-service中扣减库存结束");
}
}
启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableFeignClients
@EnableDiscoveryClient
public class SeataStorageMain2002 {
public static void main(String[] args) {
SpringApplication.run(SeataStorageMain2002.class);
}
}
业务逻辑
// dao
@Mapper
public interface AccountDao {
//扣减账户余额
void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
<mapper namespace="com.jsu.seata.dao.AccountDao">
<resultMap id="BaseResultMap" type="com.jsu.seata.pojo.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 t_account
SET
residue = residue - #{money},used = used + #{money}
WHERE
user_id = #{userId};
update>
mapper>
// service
public interface AccountService {
// 扣减账户余额
void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Resource
AccountDao accountDao;
// 扣减账户余额
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("------->account-service中扣减账户余额开始");
accountDao.decrease(userId,money);
log.info("------->account-service中扣减账户余额结束");
}
}
@RestController
@Slf4j
public class AccountController {
@Autowired
private AccountService accountService;
//做减少库存的操作 post请求携带参数
@RequestMapping(value = "/account/decrease", method = RequestMethod.POST)
public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
accountService.decrease(userId,money);
return new CommonResult(200,"扣减账户余额成功!");
}
}
启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableFeignClients
@EnableDiscoveryClient
public class SeataAccountMain2003 {
public static void main(String[] args) {
SpringApplication.run(SeataAccountMain2003.class);
}
}
数据库显示
数据库显示:当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从0变成1,由于Feign的重试机制 还可能继续扣账户余额
AT、TCC、SAGA 和XA事务模式
前提:
整个机制:
两阶段提交协议的演变:
第一阶段:
在一阶段,Seata会拦截“业务SQL”
二阶段之提交
:
因为“业务SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉
,完成数据清理即可。
因为代码正常运行结束,删除前面保存的快照数据,直接更改数据库数据即可
二阶段之回滚:
二阶段如果是回滚的话,Seata就需要回滚一阶段已经执行的“业务SQL”,还原业务数据
。
回滚方式便是用“before image”还原业务数据
;但在还原前要首先要校验脏写
,对比“数据库当前业务数据”和“after image”
,如果两份数据完全一致
就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理
。
可以看到,他们的xid全局事务id是一样的,证明他们在一个事务下
before 和 after的原理就是
在更新数据之前,先解析这个更新sql,然后查询要更新的数据,进行保存
,
3. 其保存成“after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
[外链图片转存中…(img-wx52YwGT-1606816353208)]
二阶段之提交
:
因为“业务SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉
,完成数据清理即可。
因为代码正常运行结束,删除前面保存的快照数据,直接更改数据库数据即可
[外链图片转存中…(img-XqXjIvZz-1606816353209)]
二阶段之回滚:
二阶段如果是回滚的话,Seata就需要回滚一阶段已经执行的“业务SQL”,还原业务数据
。
回滚方式便是用“before image”还原业务数据
;但在还原前要首先要校验脏写
,对比“数据库当前业务数据”和“after image”
,如果两份数据完全一致
就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理
。
[外链图片转存中…(img-RnSkronb-1606816353210)]
断点:
[外链图片转存中…(img-chOZp7Nb-1606816353211)]
可以看到,他们的xid全局事务id是一样的,证明他们在一个事务下
[外链图片转存中…(img-HCUZ4oXT-1606816353212)]
before 和 after的原理就是
[外链图片转存中…(img-ZWHC2GFB-1606816353213)]
在更新数据之前,先解析这个更新sql,然后查询要更新的数据,进行保存