[Spring Cloud Alibaba] Seata distributed transaction
1,Spring Cloud Alibaba Seata
Seata is an open source distributed transaction solution, which is committed to providing high-performance and easy-to-use distributed transaction services under the microservice architecture. Before the opening of Seata, the corresponding internal version of Seata has been playing the role of distributed consistency Middleware in Alibaba economy, helping the economy survive the double 11 over the years and providing strong support for BU businesses. After years of precipitation and accumulation, commercial products have been sold in Alibaba cloud and financial cloud. On January 2019, in order to create a more perfect technological ecology and inclusive technological achievements, Seata officially announced open source. In the future, Seata will help its technology more reliable and complete in the form of community construction
Seata's official website, https://seata.io/zh-cn/
Spring Cloud quickly integrates with Seata, https://github.com/seata/seata-samples/blob/master/doc/quick-integration-with-spring-cloud.md
This article uses Seata's AT mode
Business requirements: place order - > reduce inventory - > deduct balance - > change order status
2. Service public content
Public content needs to be added in each module, except application YML is a little different. All other configurations are the same
(1) Correlation dependency
The dependencies of the three modules are the same
<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>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>${spring-cloud-starter-openfeign.version}</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus-boot-starter.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <dependency> <groupId>com.spring4all</groupId> <artifactId>swagger-spring-boot-starter</artifactId> <version>${swagger-spring-boot-starter.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency>
(2)application.yml
The three services need to modify the port, database name and other applications The YML configuration files are the same
modular | port | Endpoint | database |
---|---|---|---|
spring-cloud-alibaba-boot-seata-account | 8007 | 9007 | alibaba-seata-account |
spring-cloud-alibaba-boot-seata-order | 8008 | 9008 | alibaba-seata-order |
spring-cloud-alibaba-boot-seata-storage | 8009 | 9009 | alibaba-seata-storage |
# Application configuration server: port: 8007 # Endpoint monitoring management: endpoint: health: show-details: always endpoints: jmx: exposure: include: '*' web: exposure: include: '*' server: port: 9007 spring: # apply name 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 # Microservice configuration cloud: # Nacos configuration nacos: discovery: namespace: sandbox-configuration password: nacos server-addr: localhost:8848 username: nacos alibaba: # Seata configuration seata: tx-service-group: tellsea_tx_group # MybatisPlus configuration 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
In the bin directory of seata you downloaded, copy it to the resource directory of the project
And in file It is added in the conf file, which is the same level as the store, because there is no in the default configuration file
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
The Ajax results of the three services are the same
package cn.tellsea.entity; import org.springframework.http.HttpStatus; import org.springframework.util.ObjectUtils; import java.util.HashMap; /** * Common return value * * @author Tellsea * @date 2021/12/31 */ public class AjaxResult<T> extends HashMap<String, Object> { /** * Status code */ public static final String CODE_TAG = "code"; /** * Return content */ public static final String MSG_TAG = "msg"; /** * data object */ public static final String DATA_TAG = "data"; private static final long serialVersionUID = 1L; /** * Initialize a newly created Ajax result object to represent an empty message. */ public AjaxResult() { } /** * Initialize a newly created Ajax result object * * @param code Status code * @param msg Return content */ public AjaxResult(int code, String msg) { super.put(CODE_TAG, code); super.put(MSG_TAG, msg); } /** * Initialize a newly created Ajax result object * * @param code Status code * @param msg Return content * @param data data object */ 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); } } /** * Return success message * * @return Success message */ public static AjaxResult<Void> success() { return AjaxResult.success("Operation succeeded"); } /** * Return success data * * @return Success message */ public static <T> AjaxResult<T> success(T data) { return AjaxResult.success("Operation succeeded", data); } /** * Return success message * * @param msg Return content * @return Success message */ public static AjaxResult<Void> success(String msg) { return AjaxResult.success(msg, null); } /** * Return success message * * @param msg Return content * @param data data object * @return Success message */ public static <T> AjaxResult<T> success(String msg, T data) { return new AjaxResult(HttpStatus.OK.value(), msg, data); } /** * Return error message * * @return */ public static AjaxResult<Void> error() { return AjaxResult.error("operation failed"); } /** * Return error message * * @param msg Return content * @return Warning message */ public static AjaxResult<Void> error(String msg) { return AjaxResult.error(msg, null); } /** * Return error message * * @param msg Return content * @param data data object * @return Warning message */ public static <T> AjaxResult<T> error(String msg, T data) { return new AjaxResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, data); } /** * Return error message * * @param code Status code * @param msg Return content * @return Warning message */ public static AjaxResult<Void> 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) Code generation
Use the code generator to generate controller, service, serviceImpl, mapper and mapper XML file, which will not be used. See the previous article
[Spring Cloud Alibaba] Mybatis Plus code generator: https://mp.weixin.qq.com/s/9OZRbIqhLEOhH3QKEJWwPg
If you do not know something, you can leave a message on WeChat official account. https://gitee.com/tellsea/spring-cloud-alibaba-learn
(6) Create module database
3. Set up account service
Create the spring cloud Alibaba boot Seata account module. First complete the account services in Section 2
Control layer
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; /** * <p> * Account table front end controller * </p> * * @author Tellsea * @since 2021-12-31 */ @RestController @RequestMapping("/bizAccount") public class BizAccountController { @Autowired private IBizAccountService accountService; @ApiOperation("Deduction account") @PostMapping("/account/decrease") AjaxResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) { return accountService.decrease(userId, money); } }
Interface layer
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; /** * <p> * Account table service * </p> * * @author Tellsea * @since 2021-12-31 */ public interface IBizAccountService extends IService<BizAccount> { /** * Deduction account * * @param userId * @param money * @return */ AjaxResult decrease(Long userId, BigDecimal money); }
Interface implementation layer
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; /** * <p> * Account table service implementation class * </p> * * @author Tellsea * @since 2021-12-31 */ @Slf4j @Service public class BizAccountServiceImpl extends ServiceImpl<BizAccountMapper, BizAccount> implements IBizAccountService { @Override public AjaxResult decrease(Long userId, BigDecimal money) { log.info("------->Start of deduction balance"); //Simulation timeout exception, global transaction rollback //Pause the thread for a few seconds //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("------->End of deduction balance"); return AjaxResult.success("Balance deducted successfully"); } }
The account module needs to add a BigDecimalUtils tool class
package cn.tellsea.utils; import java.math.BigDecimal; import java.math.RoundingMode; /** * Amount calculation tool * * @author Tellsea * @date 2021/12/31 */ public class BigDecimalUtils { /** * Save 2 decimal places by default */ private static int POINTS = 2; /** * Default carry mode */ private static RoundingMode MODE = RoundingMode.CEILING; /** * plus * * @param a * @param b * @return */ 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); } /** * reduce * * @param a * @param b * @return */ 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); } /** * ride * * @param a * @param b * @return */ public static BigDecimal multiply(BigDecimal a, BigDecimal b) { return computer(3, a, b, POINTS, MODE); } /** * except * * @param a * @param b * @return */ 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); } /** * computing method * * @param type 1-Add 2 - subtract 3 - multiply 4 - divide * @param a * @param b * @param points * @param mode * @return */ 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); } }
Add annotation to startup class
@EnableDiscoveryClient @EnableFeignClients @MapperScan("cn.tellsea.mapper")
4. Build order service
Create the spring cloud Alibaba boot Seata order module. First complete the account services in Section 2
Control layer
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; /** * <p> * Order form front end controller * </p> * * @author Tellsea * @since 2021-12-31 */ @RestController @RequestMapping("/bizOrder") public class BizOrderController { @Autowired private IBizOrderService orderService; @ApiOperation("Create order") @GetMapping("/createOrder") public AjaxResult createOrder(BizOrder entity) { return orderService.createOrder(entity); } }
Interface layer
package cn.tellsea.service; import cn.tellsea.entity.AjaxResult; import cn.tellsea.entity.BizOrder; import com.baomidou.mybatisplus.extension.service.IService; /** * <p> * Order form service class * </p> * * @author Tellsea * @since 2021-12-31 */ public interface IBizOrderService extends IService<BizOrder> { /** * Create order * * @param entity * @return */ AjaxResult createOrder(BizOrder entity); }
Interface implementation layer
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; /** * <p> * Order table service implementation class * </p> * * @author Tellsea * @since 2021-12-31 */ @Slf4j @Service public class BizOrderServiceImpl extends ServiceImpl<BizOrderMapper, BizOrder> implements IBizOrderService { @Autowired private FeignBizAccountService accountService; @Autowired private FeignBizStorageService storageService; @Override public AjaxResult createOrder(BizOrder entity) { log.info("------>Start new order"); baseMapper.insert(entity); log.info("------>The order micro service starts to call inventory and deduct count"); storageService.decrease(entity.getProductId(), entity.getCount()); log.info("------>The order micro service starts to call inventory and deduct end"); log.info("------>The order micro service starts to call the account and deduct money"); accountService.decrease(entity.getUserId(), entity.getMoney()); log.info("------>The order micro service starts to call the account and deduct end"); log.info("------>Start modifying order status"); entity.setStatus(2); baseMapper.updateById(entity); log.info("------>Finished modifying order status"); log.info("------>Order completed"); return AjaxResult.success("checkout success "); } }
Add annotation to startup class
@EnableDiscoveryClient @EnableFeignClients @MapperScan("cn.tellsea.mapper")
Add account service remote calling interface
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; /** * Account services * * @author Tellsea * @date 2021/12/31 */ @FeignClient("spring-cloud-alibaba-boot-seata-account") public interface FeignBizAccountService { /** * Deduction of account balance * * @param userId * @param money * @return */ @PostMapping("/bizAccount/account/decrease") AjaxResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); }
Add inventory service remote call interface
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; /** * Inventory service * * @author Tellsea * @date 2021/12/31 */ @FeignClient(value = "spring-cloud-alibaba-boot-seata-storage") public interface FeignBizStorageService { /** * Deduct inventory * * @param productId * @param count * @return */ @PostMapping("/bizStorage/storage/decrease") AjaxResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); }
5. Build inventory service
Create the spring cloud Alibaba boot Seata storage module. First complete the account services in Section 2
Control layer
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; /** * <p> * Inventory table front controller * </p> * * @author Tellsea * @since 2021-12-31 */ @RestController @RequestMapping("/bizStorage") public class BizStorageController { @Autowired private IBizStorageService storageService; @ApiOperation("Deduct inventory") @RequestMapping("/storage/decrease") public AjaxResult decrease(Long productId, Integer count) { return storageService.decrease(productId, count); } }
Interface layer
package cn.tellsea.service; import cn.tellsea.entity.AjaxResult; import cn.tellsea.entity.BizStorage; import com.baomidou.mybatisplus.extension.service.IService; /** * <p> * Inventory table service class * </p> * * @author Tellsea * @since 2021-12-31 */ public interface IBizStorageService extends IService<BizStorage> { /** * Deduct inventory * * @param productId * @param count * @return */ AjaxResult decrease(Long productId, Integer count); }
Interface implementation layer
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; /** * <p> * Inventory table service implementation class * </p> * * @author Tellsea * @since 2021-12-31 */ @Slf4j @Service public class BizStorageServiceImpl extends ServiceImpl<BizStorageMapper, BizStorage> implements IBizStorageService { @Override public AjaxResult decrease(Long productId, Integer count) { log.info("------->Start of inventory deduction"); BizStorage storage = baseMapper.selectById(productId); storage.setUsed(storage.getUsed() + count); storage.setResidue(storage.getResidue() - count); baseMapper.updateById(storage); log.info("------->End of inventory deduction"); return AjaxResult.success("Inventory deduction succeeded"); } }
Add annotation to startup class
@EnableDiscoveryClient @EnableFeignClients @MapperScan("cn.tellsea.mapper")
6. Test order business
Business requirements: place order - > reduce inventory - > deduct balance - > change order status
(1) Check service startup results
Start three services
Check the service registration of Nacos
visit
http://localhost:8008/bizOrder/createOrder?userId=1&productId=1&count=10&money=100
7. Common error reporting
(1)endpoint format should like ip:port
An error was found when starting the service
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]
The seata used in this article depends on spring cloud alibaba seata, so the configuration information should be under alibaba
- tellsea_tx_group: user defined group name, which is the same as vgroupMapping Corresponding to the name after the point
- Different from the seata version, vgroupMapping may be vgroup mapping, which can be seen directly from the source code
Wrong version spring: cloud: # Seata configuration seata: tx-service-group: tellsea_tx_group Correct version spring: cloud: alibaba: # Seata configuration seata: tx-service-group: tellsea_tx_group