- 【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
- 微信公众号
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,这里修改了一下
公共内容需要在每个模块中都添加,除了 application.yml 有一点区别,其他所有配置相同
(1)相关依赖三个模块的依赖相同
(2)application.ymlorg.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-actuatorcom.alibaba.cloud spring-cloud-starter-alibaba-nacos-discoveryorg.springframework.cloud spring-cloud-starter-openfeign${spring-cloud-starter-openfeign.version} com.alibaba.cloud spring-cloud-starter-alibaba-seatacom.baomidou mybatis-plus-boot-starter${mybatis-plus-boot-starter.version} mysql mysql-connector-javaorg.projectlombok lombok${lombok.version} com.spring4all swagger-spring-boot-starter${swagger-spring-boot-starter.version} org.springframework.boot spring-boot-starter-testtest org.junit.vintage junit-vintage-engine
三个服务需要分别修改端口,数据库名字,其他的 application.yml 配置文件都一样
# 应用配置 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(5)代码生成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); } }
使用代码生成器,分别生成 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 ServiceImplimplements 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 ServiceImplimplements 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 ServiceImplimplements 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 的服务注册
数据库请求之前的数据
账户
订单
库存
访问线面连接下单
http://localhost:8008/bizOrder/createOrder?userId=1&productId=1&count=10&money=100
返回结果下单成功
账户:扣减
订单:创建完成
库存:扣减
在 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 秒
账户:余额不变
订单:已创建,但未完成
检查商品库存,丢失了!!!
在创建订单的接口上增加全局事物注解
@GlobalTransactional(name = "tellsea_tx_group", rollbackFor = Exception.class)
重启服务
再次调用
http://localhost:8008/bizOrder/createOrder?userId=1&productId=1&count=10&money=100
同样的报错
账户:未变动
订单:未变动
库存:未变动
到此,Spring Cloud Alibabaseata 分布式事物,控制成功
启动服务时,发现报错
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微信公众号
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)