SpringCloud学习笔记(五)

8.Seata分布式事务

8.1. Seata简介

  • Seata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。

8.2. Seata工作组件

  • XID:全局事务的唯一标识,在微服务调用链中传递,绑定到服务的事务的上下文。
  • TC:事务协调者,就是Seata,维护全局和分支事务的状态,驱动全局事务提交或回滚。(以Seata Server的形式独立部署)
  • TM:事务管理器,标注全局@GlobalTransactional启动入口动作的微服务模块,是事务的发起者。定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  • RM:资源管理器,是MySQL数据库本身,管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

8.3. Seata工作流程

1.TM向TC申请开启全局事务,创建成功生成全局唯一的XID。

2.XID在微服务调用链中传播。

3.RM向TC注册分支事务,将其纳入XID对应的全局事务的管辖。

4.TM向TC发起针对XID的全局提交或回滚决议。

5.TC调度XID下管辖的全部分支事务完成提交或回滚请求。

8.4. Seata安装

8.4.1.安装流程
sql 复制代码
CREATE DATABASE seata;
USE seata;
yaml 复制代码
#  Copyright 1999-2019 Seata.io Group.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

server:
  port: 7091

spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${log.home:${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: ""
      group: SEATA_GROUP
      username: nacos
      password: nacos
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace: ""
      cluster: default
      username: nacos
      password: nacos
  store:
    # support: file 、 db 、 redis 、 raft
    mode: db
    db:
      datasource: druid
      db-type: mysql 
      driver-class-name: com.mysql.dj.jdbc.Driver
      url: jdbc:mysql://localhost:3307/seata?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true
      user: root
      password: 123456
      minConn: 10
      maxConn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
      query-limit: 1000
      maxWait: 5000
  #  server:
  #    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
8.4.2.验证
  • 访问nacos,服务注册成功
  • 访问seata,账号密码初始化均为seata,登录成功

8.5. Seata使用(订单下单案例)

1.创建三个数据库

CREATE DATABASE seata_order;

CREATE DATABASE seata_storage;

CREATE DATABASE seata_account;

2.创建表

3.mybatis一键生成实体和标准mapper

4.创建三个微服务

  • 订单服务
  • 库存服务
  • 账户服务

5.新增库存和账户两个Feign接口

  • 库存接口
java 复制代码
@FeignClient(name = "seata-storage-service")
public interface StorageFeignApi {

    /**
     * 扣减库存
     */
    @PostMapping("/storage/decrease")
    ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
  • 账户接口
java 复制代码
@FeignClient(name = "seata-account-service")
public interface AccountFeignApi {

    /**
     * 扣减账户余额
     */
    @PostMapping("/account/decrease")
    ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}

6.订单微服务

  • 配置文件
yaml 复制代码
server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    nacos:
      server-addr: localhost:8848
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3307/seata_order?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: 123456

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: org.example.cloud.entities
  configuration:
    map-underscore-to-camel-case: true

seata:
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
      namespace: ""
      group: SEATA_GROUP
      application: seata-server
  tx-service-group: default_tx_group # 事务组名称,由他获得TC服务的集群名称
  service:
    vgroup-mapping:
      # 事务组名称与集群名称的映射关系,例如当前事务组为ProjectA,这个ProjectA的集群名称为default,如果当前集群down了,只需要修改集群名称即可启动备用集群进行事务管理
      default_tx_group: default 
  data-source-proxy-mode: AT

logging:
  level:
    io:
      seata: info
  • 业务代码
java 复制代码
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderMapper orderMapper;

    @Resource// 订单微服务调用库存微服务
    private StorageFeignApi storageFeignApi;

    @Resource// 订单微服务调用账户微服务
    private AccountFeignApi accountFeignApi;


    @Override
    public void create(Order order) {
        //xid全局事务id检查
        String xid = RootContext.getXID();
        //1. 新建订单
        log.info("----->开始新建订单: " + "\t" + "xid: " + xid);
        // 订单新建初始状态为0
        order.setStatus(0);
        int result = orderMapper.insertSelective(order);

        Order orderFromDB = null;

        if(result > 0) {
            // 从mysql查出记录
            orderFromDB = orderMapper.selectOne(order);
            log.info("----->新建订单成功: " + "\t" + "orderFromDB info: " + orderFromDB);
            System.out.println();
            log.info("----->调用storage: " + "\t");
            storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());
            log.info("----->调用storage完成: " + "\t");
            log.info("----->调用account: " + "\t");
            accountFeignApi.decrease(orderFromDB.getUserId(), order.getMoney());
            log.info("----->调用account完成: " + "\t");
            System.out.println();
            // 修改订单状态,从0改为1
            log.info("----->修改订单状态: " + "\t");
            orderFromDB.setStatus(1);
            Example whereCondition = new Example(Order.class);
            Example.Criteria criteria = whereCondition.createCriteria();
            criteria.andEqualTo("userId", order.getId());
            criteria.andEqualTo("status", 0);
            int updateResult = orderMapper.updateByExampleSelective(orderFromDB, whereCondition);
            log.info("----->修改订单状态完成: " + "\t" + "updateResult: " + updateResult);
            log.info("----->orderFromDB info: " + orderFromDB);
        }
        System.out.println();
        System.out.println("----->结束新建订单: " + "\t" + "xid: " + xid);

    }
}

7.库存微服务

  • mapper
java 复制代码
public interface StorageMapper extends Mapper<Storage> {
    void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
xml 复制代码
<update id="decrease">
    update t_storage
    set used = used + #{count},
    residue = residue - #{count}
    where product_id = #{productId}
</update>

8.账户微服务

  • mapper
java 复制代码
public interface AccountMapper extends Mapper<Account> {
    void decrease(@Param("userId") Long userId, @Param("money") Long money);
}
xml 复制代码
<update id="decrease">
    update t_account
    set used = used + #{money},
    residue = residue - #{money}
    where user_id = #{userId}
</update>

8.6. Seata测试(订单下单案例)

  • 未添加@GlobalTransactional注解

    • 账户微服务超时异常
      • 账户微服务和库存微服务均未回滚
      • 订单状态为0,未进行更新
    • 账户微服务业务代码异常
      • 账户微服务和库存微服务均未回滚
      • 订单状态为0,未进行更新
java 复制代码
// OrderServiceImpl.java
@GlobalTransactional(name = "zzyy-create-order", rollbackFor = Exception.class)
public void create(Order order) {
    //xid全局事务id检查
    String xid = RootContext.getXID();
    //1. 新建订单
    log.info("----->开始新建订单: " + "\t" + "xid: " + xid);
    // 订单新建初始状态为0
    order.setStatus(0);
    int result = orderMapper.insertSelective(order);

    Order orderFromDB = null;

    if(result > 0) {
        // 从mysql查出记录
        orderFromDB = orderMapper.selectOne(order);
        log.info("----->新建订单成功: " + "\t" + "orderFromDB info: " + orderFromDB);
        System.out.println();
        log.info("----->调用storage: " + "\t");
        storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());
        log.info("----->调用storage完成: " + "\t");
        log.info("----->调用account: " + "\t");
        accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney());
        log.info("----->调用account完成: " + "\t");
        System.out.println();
        // 修改订单状态,从0改为1
        log.info("----->修改订单状态: " + "\t");
        orderFromDB.setStatus(1);
        Example whereCondition = new Example(Order.class);
        Example.Criteria criteria = whereCondition.createCriteria();
        criteria.andEqualTo("id", orderFromDB.getId());
        criteria.andEqualTo("status", 0);
        int updateResult = orderMapper.updateByExampleSelective(orderFromDB, whereCondition);
        log.info("----->修改订单状态完成: " + "\t" + "updateResult: " + updateResult);
        log.info("----->orderFromDB info: " + orderFromDB);
    }
    System.out.println();
    System.out.println("----->结束新建订单: " + "\t" + "xid: " + xid);
}
  • 添加@GlobalTransactional注解
    • 账户微服务超时异常
      • 超时前,账户微服务和库存微服务均未回滚,逻辑正常更新,但订单状态为0,未进行更新,且undolog记录存在

      • seata后台

      • 超时后,账户微服务和库存微服务均回滚,订单记录消失,undolog记录被删除

      • seata后台

8.7.原理总结与面试题

8.7.1 AT模式如何做到对业务无侵入

1.两阶段提交协议的演变

  • 一阶段:业务数据和回滚日志记录在同一个事务中提交,释放数据库锁资源。

  • 二阶段:

    • 提交异步化,非常快速地完成。
    • 基于undo_log回滚日志,反向补偿。

2.一阶段加载

在一阶段,Seata会拦截业务SQL,

(1)解析SQL语义,找到业务SQL要更新的业务数据,在业务数据被更新前,将其保存成"before image",

(2)执行业务SQL更新业务数据,在业务数据更新之后,

(3)将其保存成"after image",最后生成行锁。

上述操作在一个数据库事务完成,保证原子性。

3.二阶段提交

  • 二阶段如果是顺利提交,因为业务SQL在一阶段已经提交,所以Seata只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
  • 二阶段如果是回滚的话,Seata需要回滚一阶段已经执行的业务SQL,回滚的方式就是用"before image"还原业务数据,还原之前要首先校验脏写,对比"数据库当前业务数据"和"after image",如果数据不一致,说明有脏写,需要转人工处理,如果数据一致,则用"before image"去覆盖"当前业务数据",完成数据回滚。最后清理掉一阶段保存的快照数据和行锁。
相关推荐
web1309332039818 分钟前
JAVA面试之容器
java·开发语言·面试
扫地僧00919 分钟前
第18章 不可变对象设计模式(Java高并发编程详解:多线程与系统设计)
java·python·设计模式
伊一大数据&人工智能学习日志1 小时前
深度学习01 神经网络
人工智能·深度学习·神经网络·学习·机器学习
洛嘚1 小时前
多数据源配置及使用,在同一个方法下切换数据源。
java·服务器·数据库
LNsupermali1 小时前
力扣.270. 最接近的二叉搜索树值(中序遍历思想)
java·算法·leetcode
声网1 小时前
a16z 最新 Voice AI 报告:语音将成为关键切入点,而非最终产品本身丨 Voice AI 学习笔记
人工智能·笔记·学习
gyeolhada2 小时前
2025蓝桥杯JAVA编程题练习Day3
java·数据结构·算法·蓝桥杯
钮钴禄·爱因斯晨2 小时前
赛博算命之 ”梅花易数“ 的 “JAVA“ 实现 ——从玄学到科学的探索
java·开发语言·python
Beekeeper&&P...2 小时前
BCrypt加密密码和md5加密哪个更好一点///jwt和rsa有什么区别//为什么spring中经常要用个r类
java·spring·r语言
吴声子夜歌3 小时前
Linux运维——文件内容查看编辑
java·linux·运维