【Spring Cloud Alibaba】Seata 分布式事务

【Spring Cloud Alibaba】Seata 分布式事务,第1张

【Spring Cloud Alibaba】Seata 分布式事务

文章目录
  • 【Spring Cloud Alibaba】Seata 分布式事务
    • 1、Spring Cloud Alibabaseata
    • 2、服务公共内容
      • (1)相关依赖
      • (2)application.yml
      • (3)file.conf、registry.conf
      • (4)AjaxResult
      • (5)代码生成
      • (6)创建模块数据库
    • 3、搭建账户服务
    • 4、搭建订单服务
    • 5、搭建库存服务
    • 6、测试下单业务
      • (1)检查服务启动结果
      • (2)正常情况下单
      • (3)出错情况下单
      • (4)增加 Seata 事物
    • 7、常见报错
      • (1)endpoint format should like ip:port
  • 微信公众号

【Spring Cloud Alibaba】Seata 分布式事务 1、Spring Cloud Alibabaseata

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双 11,对各 BU 业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备

Seata 的官网,https://seata.io/zh-cn/
Spring Cloud 快速集成 Seata,https://github.com/seata/seata-samples/blob/master/doc/quick-integration-with-spring-cloud.md

本篇文章使用的是 Seata 的 AT 模式
业务需求:下订单 -> 减库存 -> 扣余额 -> 改订单状态

源码中的模块名称变更了,之前大意多了一个 boot,这里修改了一下

变更前变更后spring-cloud-alibaba-boot-seata-accountspring-cloud-alibaba-seata-accountspring-cloud-alibaba-boot-seata-orderspring-cloud-alibaba-seata-orderspring-cloud-alibaba-boot-seata-storagespring-cloud-alibaba-seata-storage 2、服务公共内容

公共内容需要在每个模块中都添加,除了 application.yml 有一点区别,其他所有配置相同

(1)相关依赖

三个模块的依赖相同

        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-actuator
        

        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
        

        
            org.springframework.cloud
            spring-cloud-starter-openfeign
            ${spring-cloud-starter-openfeign.version}
        

        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-seata
        

        
            com.baomidou
            mybatis-plus-boot-starter
            ${mybatis-plus-boot-starter.version}
        

        
            mysql
            mysql-connector-java
        

        
            org.projectlombok
            lombok
            ${lombok.version}
        

        
            com.spring4all
            swagger-spring-boot-starter
            ${swagger-spring-boot-starter.version}
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
            
                
                    org.junit.vintage
                    junit-vintage-engine
                
            
        
(2)application.yml

三个服务需要分别修改端口,数据库名字,其他的 application.yml 配置文件都一样

模块端口端点数据库spring-cloud-alibaba-boot-seata-account80079007alibaba-seata-accountspring-cloud-alibaba-boot-seata-order80089008alibaba-seata-orderspring-cloud-alibaba-boot-seata-storage80099009alibaba-seata-storage
# 应用配置
server:
  port: 8007

# 端点监控
management:
  endpoint:
    health:
      show-details: always
  endpoints:
    jmx:
      exposure:
        include: '*'
    web:
      exposure:
        include: '*'
  server:
    port: 9007

spring:
  # 应用名称
  application:
    name: spring-cloud-alibaba-boot-seata-account
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/alibaba-seata-account?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: 123456
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    serialization:
      write-dates-as-timestamps: false
  # 微服务配置
  cloud:
    # Nacos配置
    nacos:
      discovery:
        namespace: sandbox-configuration
        password: nacos
        server-addr: localhost:8848
        username: nacos
    alibaba:
      # Seata配置
      seata:
        tx-service-group: tellsea_tx_group

# MybatisPlus配置
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    auto-mapping-behavior: full
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:mapper/**/*Mapper.xml

(3)file.conf、registry.conf

在你下载的 seata 的 bin,目录中复制到项目的 resource 目录下

并在 file.conf 文件中增加,与 store 同级,因为默认配置文件中没有

service {
  #vgroup->rgroup
  vgroupMapping.tellsea_tx_group = "default"
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}
(4)AjaxResult

三个服务的 AjaxResult 相同

package cn.tellsea.entity;


import org.springframework.http.HttpStatus;
import org.springframework.util.ObjectUtils;

import java.util.HashMap;


public class AjaxResult extends HashMap {
    
    public static final String CODE_TAG = "code";
    
    public static final String MSG_TAG = "msg";
    
    public static final String DATA_TAG = "data";
    private static final long serialVersionUID = 1L;

    
    public AjaxResult() {
    }

    
    public AjaxResult(int code, String msg) {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
    }

    
    public AjaxResult(int code, String msg, T data) {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
        if (!ObjectUtils.isEmpty(data)) {
            super.put(DATA_TAG, data);
        }
    }

    
    public static AjaxResult success() {
        return AjaxResult.success(" *** 作成功");
    }

    
    public static  AjaxResult success(T data) {
        return AjaxResult.success(" *** 作成功", data);
    }

    
    public static AjaxResult success(String msg) {
        return AjaxResult.success(msg, null);
    }

    
    public static  AjaxResult success(String msg, T data) {
        return new AjaxResult(HttpStatus.OK.value(), msg, data);
    }

    
    public static AjaxResult error() {
        return AjaxResult.error(" *** 作失败");
    }

    
    public static AjaxResult error(String msg) {
        return AjaxResult.error(msg, null);
    }

    
    public static  AjaxResult error(String msg, T data) {
        return new AjaxResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, data);
    }

    
    public static AjaxResult error(int code, String msg) {
        return new AjaxResult(code, msg, null);
    }

    public Integer getCode() {
        return (Integer) super.get(CODE_TAG);
    }

    public String getMsg() {
        return (String) super.get(MSG_TAG);
    }

    public T getData() {
        return (T) super.get(DATA_TAG);
    }
}

(5)代码生成

使用代码生成器,分别生成 controller、service、serviceImpl、mapper、mapper.xml 文件,不会使用的看上一篇文章

【Spring Cloud Alibaba】Mybatis Plus 代码生成器:https://mp.weixin.qq.com/s/9OZRbIqhLEOhH3QKEJWwPg

有不懂的地方,可以微信公众号留言,项目源码在:https://gitee.com/tellsea/spring-cloud-alibaba-learn

(6)创建模块数据库

3、搭建账户服务

创建 spring-cloud-alibaba-boot-seata-account 模块,先把第二节的账户服务都 *** 作完成

控制层

package cn.tellsea.controller;


import cn.tellsea.entity.AjaxResult;
import cn.tellsea.service.IBizAccountService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;


@RestController
@RequestMapping("/bizAccount")
public class BizAccountController {

    @Autowired
    private IBizAccountService accountService;

    @ApiOperation("扣减账户")
    @PostMapping("/account/decrease")
    AjaxResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) {
        return accountService.decrease(userId, money);
    }
}

接口层

package cn.tellsea.service;

import cn.tellsea.entity.AjaxResult;
import cn.tellsea.entity.BizAccount;
import com.baomidou.mybatisplus.extension.service.IService;

import java.math.BigDecimal;


public interface IBizAccountService extends IService {

    
    AjaxResult decrease(Long userId, BigDecimal money);
}

接口实现层

package cn.tellsea.service.impl;

import cn.tellsea.entity.AjaxResult;
import cn.tellsea.entity.BizAccount;
import cn.tellsea.mapper.BizAccountMapper;
import cn.tellsea.service.IBizAccountService;
import cn.tellsea.utils.BigDecimalUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;


@Slf4j
@Service
public class BizAccountServiceImpl extends ServiceImpl implements IBizAccountService {

    @Override
    public AjaxResult decrease(Long userId, BigDecimal money) {
        log.info("------->扣减余额开始");
        //模拟超时异常,全局事务回滚
        //暂停几秒钟线程
        //try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        BizAccount account = baseMapper.selectById(userId);
        account.setResidue(BigDecimalUtils.subtract(account.getResidue(), money));
        account.setUsed(BigDecimalUtils.add(account.getUsed(), money));
        baseMapper.updateById(account);
        log.info("------->扣减余额结束");
        return AjaxResult.success("扣减余额成功");
    }
}

账户模块需要增加一个 BigDecimalUtils 工具类

package cn.tellsea.utils;

import java.math.BigDecimal;
import java.math.RoundingMode;


public class BigDecimalUtils {

    
    private static int POINTS = 2;

    
    private static RoundingMode MODE = RoundingMode.CEILING;


    
    public static BigDecimal add(BigDecimal a, BigDecimal b) {
        if (a == null) {
            a = BigDecimal.valueOf(0.00);
        }
        if (b == null) {
            b = BigDecimal.valueOf(0.00);
        }
        return computer(1, a, b, POINTS, MODE);
    }

    public static BigDecimal add(BigDecimal a, BigDecimal b, int points, RoundingMode mode) {
        if (a == null) {
            a = BigDecimal.valueOf(0.00);
        }
        if (b == null) {
            b = BigDecimal.valueOf(0.00);
        }
        return computer(1, a, b, points, mode);
    }

    
    public static BigDecimal subtract(BigDecimal a, BigDecimal b) {
        if (a == null) {
            a = BigDecimal.valueOf(0.00);
        }
        if (b == null) {
            b = BigDecimal.valueOf(0.00);
        }
        return computer(2, a, b, POINTS, MODE);
    }

    public static BigDecimal subtract(BigDecimal a, BigDecimal b, int points, RoundingMode mode) {
        if (a == null) {
            a = BigDecimal.valueOf(0.00);
        }
        if (b == null) {
            b = BigDecimal.valueOf(0.00);
        }
        return computer(2, a, b, points, mode);
    }


    
    public static BigDecimal multiply(BigDecimal a, BigDecimal b) {
        return computer(3, a, b, POINTS, MODE);
    }

    
    public static BigDecimal divide(BigDecimal a, BigDecimal b) {
        if (b.compareTo(BigDecimal.ZERO) == 0) {
            return BigDecimal.valueOf(0.00);
        }
        return computer(4, a, b, POINTS, MODE);
    }

    public static BigDecimal divide(BigDecimal a, BigDecimal b, int points, RoundingMode mode) {
        return computer(4, a, b, points, mode);
    }

    
    public static BigDecimal computer(int type, BigDecimal a, BigDecimal b, int points, RoundingMode mode) {
        BigDecimal rs;
        switch (type) {
            case 1:
                rs = a.add(b).setScale(points, mode);
                break;
            case 2:
                rs = a.subtract(b).setScale(points, mode);
                break;
            case 3:
                rs = a.multiply(b).setScale(points, mode);
                break;
            default:
                rs = a.divide(b, points, mode);
                break;
        }
        return rs;
    }


    public BigDecimal multiply(BigDecimal a, BigDecimal b, int points, RoundingMode mode) {
        return computer(3, a, b, points, mode);
    }

}

启动类增加注解

@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("cn.tellsea.mapper")
4、搭建订单服务

创建 spring-cloud-alibaba-boot-seata-order 模块,先把第二节的账户服务都 *** 作完成
控制层

package cn.tellsea.controller;

import cn.tellsea.entity.AjaxResult;
import cn.tellsea.entity.BizOrder;
import cn.tellsea.service.IBizOrderService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/bizOrder")
public class BizOrderController {

    @Autowired
    private IBizOrderService orderService;

    @ApiOperation("创建订单")
    @GetMapping("/createOrder")
    public AjaxResult createOrder(BizOrder entity) {
        return orderService.createOrder(entity);
    }
}

接口层

package cn.tellsea.service;

import cn.tellsea.entity.AjaxResult;
import cn.tellsea.entity.BizOrder;
import com.baomidou.mybatisplus.extension.service.IService;


public interface IBizOrderService extends IService {

    
    AjaxResult createOrder(BizOrder entity);
}

接口实现层

package cn.tellsea.service.impl;

import cn.tellsea.entity.AjaxResult;
import cn.tellsea.entity.BizOrder;
import cn.tellsea.feignclient.FeignBizAccountService;
import cn.tellsea.feignclient.FeignBizStorageService;
import cn.tellsea.mapper.BizOrderMapper;
import cn.tellsea.service.IBizOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Slf4j
@Service
public class BizOrderServiceImpl extends ServiceImpl implements IBizOrderService {

    @Autowired
    private FeignBizAccountService accountService;
    @Autowired
    private FeignBizStorageService storageService;

    @Override
    public AjaxResult createOrder(BizOrder entity) {
        log.info("------>开始新建订单");
        baseMapper.insert(entity);
        log.info("------>订单微服务开始调用库存,扣减count");
        storageService.decrease(entity.getProductId(), entity.getCount());
        log.info("------>订单微服务开始调用库存,扣减end");
        log.info("------>订单微服务开始调用账户,扣减money");
        accountService.decrease(entity.getUserId(), entity.getMoney());
        log.info("------>订单微服务开始调用账户,扣减end");
        log.info("------>开始修改订单状态");
        entity.setStatus(2);
        baseMapper.updateById(entity);
        log.info("------>修改订单状态完毕");
        log.info("------>下单完毕");
        return AjaxResult.success("下单成功");
    }
}

启动类增加注解

@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("cn.tellsea.mapper")

增加账户服务远程调用接口

package cn.tellsea.feignclient;

import cn.tellsea.entity.AjaxResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;


@FeignClient("spring-cloud-alibaba-boot-seata-account")
public interface FeignBizAccountService {

    
    @PostMapping("/bizAccount/account/decrease")
    AjaxResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

增加库存服务远程调用接口

package cn.tellsea.feignclient;

import cn.tellsea.entity.AjaxResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;


@FeignClient(value = "spring-cloud-alibaba-boot-seata-storage")
public interface FeignBizStorageService {

    
    @PostMapping("/bizStorage/storage/decrease")
    AjaxResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

5、搭建库存服务

创建 spring-cloud-alibaba-boot-seata-storage 模块,先把第二节的账户服务都 *** 作完成
控制层

package cn.tellsea.controller;


import cn.tellsea.entity.AjaxResult;
import cn.tellsea.service.IBizStorageService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/bizStorage")
public class BizStorageController {

    @Autowired
    private IBizStorageService storageService;

    @ApiOperation("扣减库存")
    @RequestMapping("/storage/decrease")
    public AjaxResult decrease(Long productId, Integer count) {
        return storageService.decrease(productId, count);
    }
}

接口层

package cn.tellsea.service;

import cn.tellsea.entity.AjaxResult;
import cn.tellsea.entity.BizStorage;
import com.baomidou.mybatisplus.extension.service.IService;


public interface IBizStorageService extends IService {

    
    AjaxResult decrease(Long productId, Integer count);
}

接口实现层

package cn.tellsea.service.impl;

import cn.tellsea.entity.AjaxResult;
import cn.tellsea.entity.BizStorage;
import cn.tellsea.mapper.BizStorageMapper;
import cn.tellsea.service.IBizStorageService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;


@Slf4j
@Service
public class BizStorageServiceImpl extends ServiceImpl implements IBizStorageService {

    @Override
    public AjaxResult decrease(Long productId, Integer count) {
        log.info("------->中扣减库存开始");
        BizStorage storage = baseMapper.selectById(productId);
        storage.setUsed(storage.getUsed() + count);
        storage.setResidue(storage.getResidue() - count);
        baseMapper.updateById(storage);
        log.info("------->中扣减库存结束");
        return AjaxResult.success("扣减库存成功");
    }
}

启动类增加注解

@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("cn.tellsea.mapper")
6、测试下单业务

业务需求:下订单 -> 减库存 -> 扣余额 -> 改订单状态

(1)检查服务启动结果

启动三个 服务

检查 Nacos 的服务注册

(2)正常情况下单

数据库请求之前的数据
账户

订单

库存

访问线面连接下单

http://localhost:8008/bizOrder/createOrder?userId=1&productId=1&count=10&money=100

返回结果下单成功

账户:扣减

订单:创建完成

库存:扣减

(3)出错情况下单

在 spring-cloud-alibaba-boot-seata-account 模块的扣减账户余额中增加线程休眠

    @Override
    public AjaxResult decrease(Long userId, BigDecimal money) {
        log.info("------->扣减余额开始");
        //模拟超时异常,全局事务回滚
        //暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        BizAccount account = baseMapper.selectById(userId);
        account.setResidue(BigDecimalUtils.subtract(account.getResidue(), money));
        account.setUsed(BigDecimalUtils.add(account.getUsed(), money));
        baseMapper.updateById(account);
        log.info("------->扣减余额结束");
        return AjaxResult.success("扣减余额成功");
    }

重新访问下单连接

http://localhost:8008/bizOrder/createOrder?userId=1&productId=1&count=10&money=100

报错,因为 OpenFeign 默认调用时限为 1 秒

账户:余额不变

订单:已创建,但未完成

检查商品库存,丢失了!!!

(4)增加 Seata 事物

在创建订单的接口上增加全局事物注解

@GlobalTransactional(name = "tellsea_tx_group", rollbackFor = Exception.class)

重启服务

再次调用

http://localhost:8008/bizOrder/createOrder?userId=1&productId=1&count=10&money=100

同样的报错

账户:未变动

订单:未变动

库存:未变动

到此,Spring Cloud Alibabaseata 分布式事物,控制成功

7、常见报错 (1)endpoint format should like ip:port

启动服务时,发现报错

2021-03-02 16:22:09.693 ERROR 8384 --- [           main] i.s.c.r.netty.NettyClientChannelManager  : Failed to get available servers: endpoint format should like ip:port

java.lang.IllegalArgumentException: endpoint format should like ip:port
	at io.seata.discovery.registry.FileRegistryServiceImpl.lookup(FileRegistryServiceImpl.java:95) ~[seata-all-1.3.0.jar:1.3.0]
	at io.seata.core.rpc.netty.NettyClientChannelManager.getAvailServerList(NettyClientChannelManager.java:217) ~[seata-all-1.3.0.jar:1.3.0]
	at io.seata.core.rpc.netty.NettyClientChannelManager.reconnect(NettyClientChannelManager.java:162) ~[seata-all-1.3.0.jar:1.3.0]
	at io.seata.core.rpc.netty.RmNettyRemotingClient.registerResource(RmNettyRemotingClient.java:181) [seata-all-1.3.0.jar:1.3.0]
	at io.seata.rm.AbstractResourceManager.registerResource(AbstractResourceManager.java:121) [seata-all-1.3.0.jar:1.3.0]
	at io.seata.rm.datasource.DataSourceManager.registerResource(DataSourceManager.java:146) [seata-all-1.3.0.jar:1.3.0]
	at io.seata.rm.DefaultResourceManager.registerResource(DefaultResourceManager.java:114) [seata-all-1.3.0.jar:1.3.0]

本文使用的 seata 依赖是 spring cloud alibabaseata,所以配置信息应该在 alibaba 下

  • tellsea_tx_group:自定义的组名称,与 vgroupMapping. 的点之后的名称对应
  • seata 版本的不同,vgroupMapping 可能是 vgroup-mapping,这个直接看源码就知道了
错误版本
spring:
  cloud:
	# Seata配置
	seata:
	  tx-service-group: tellsea_tx_group

正确版本
spring:
  cloud:
    alibaba:
      # Seata配置
      seata:
        tx-service-group: tellsea_tx_group
微信公众号

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5693421.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存