springCloud --- 高級篇(3)

本系列筆記涉及到的代碼在GitHub上冻璃,地址:https://github.com/zsllsz/cloud

本文涉及知識點:

  • 分布式事務解決方案之Alibaba seata;

一芙沥、分布式事務問題

打個比方,我們在淘寶下單買一件商品烤黍,可能涉及到了三個微服務魂爪,分別是訂單服務蝌数,庫存服務,支付服務贷祈。這三個服務連接的是三個不同的數(shù)據(jù)庫趋急,但是下單的這一個過程對外是表現(xiàn)出一個整體。比如下單成功势誊,然后扣庫存也成功了呜达,最后支付這一步失敗了,那么庫存系統(tǒng)和訂單系統(tǒng)都應該回滾這一次操作粟耻。同一個數(shù)據(jù)庫用事務就可以回滾了查近,不同數(shù)據(jù)庫,那就要用分布式事務了挤忙。即每個數(shù)據(jù)庫內(nèi)部的數(shù)據(jù)一致性由本地事務來保證霜威,全局數(shù)據(jù)一致性就由分布式事務來保證。


歡迎大家關注我的公眾號 javawebkf饭玲,目前正在慢慢地將簡書文章搬到公眾號侥祭,以后簡書和公眾號文章將同步更新叁执,且簡書上的付費文章在公眾號上將免費茄厘。


二、springCloud Alibaba Seata簡介

1谈宛、是什么次哈?
seata就是用來解決分布式事務的。官網(wǎng)地址:http://seata.io/zh-cn/

2吆录、seata相關術語:
分布式事務處理過程的1個id + 3個組件 模型:1個id就是指全局唯一的事務id(transaction id)窑滞;3個組件指的是:

  • Transaction Coordinator(TC):事務協(xié)調(diào)者,說白了就是你服務器上安裝的seata恢筝。維護全局和分支事務的狀態(tài)哀卫,驅(qū)動全局事務提交或回滾。

  • Transaction Manager(TM):事務管理者撬槽,說白了就是你加了@GlobalTransactional注解的那個方法此改。定義全局事務的范圍,負責開啟一個全局事務侄柔,并最終發(fā)起全局事務提交或者回滾的決議共啃。

  • Resource Manager(RM):資源管理器,說白了就是本次涉及到的數(shù)據(jù)庫暂题。管理分支事務的資源移剪,負責分支注冊、狀態(tài)匯報薪者,并接收事務協(xié)調(diào)器的指令纵苛,驅(qū)動分支(本地)事務的提交和回滾。

3、seata處理分布式事務的過程:

seata分布式事務處理過程
  • TM向TC申請開啟一個全局事務攻人,全局事務創(chuàng)建成功并生成一個全局唯一的XID幔虏;
  • XID在微服務調(diào)用鏈路的上下文中傳播;
  • RM向TC注冊分支事務贝椿,將其納入XID對用的全局事務的管轄想括;
  • TM向TC發(fā)起針對XID的全局提交或回滾決議;
  • TC調(diào)度XID下管轄的全部分支事務完成提交或回滾請求烙博。

簡單地說瑟蜈,整個過程就是用一個XID關聯(lián)起來的,比如下訂單的過程是一個整體過程渣窜,需要用分布式事務铺根,那么訂單系統(tǒng)、庫存系統(tǒng)和支付系統(tǒng)就會被同一個XID管著乔宿,表明它們是一個整體位迂。每個系統(tǒng)就是一個RM,每個系統(tǒng)自己的事務由本地事務完成详瑞,每個系統(tǒng)的操作提交還是回滾都會告訴TM掂林,TM再把結(jié)果告訴最終該提交還是回滾告訴TC去執(zhí)行。

3坝橡、怎么玩泻帮?

  • 去哪兒下:https://github.com/seata/seata/tags
  • 怎么用:本地加@Transactional注解,全局加@GlobalTrasactional注解就完事了(先有個映像计寇,編碼實戰(zhàn)部分再看具體用法)
  • 安裝:官網(wǎng)下載后锣杂,解壓
  • 在seata/config目錄下,有一個nacos-config.txt番宁,打開它元莫,關注文件中的
service.vgroup_mapping.my_test_tx_group=default

my_test_tx_group就是組名,等下在file.conf和項目的application.yml中都要用到的蝶押。

  • 修改conf目錄下的file.conf踱蠢,主要改的是自定義事務組名稱、事務日志存儲模式改為db播聪、數(shù)據(jù)庫連接信息朽基,如下:

這一段是修改事務組名稱,即修改了service塊的第一行离陶,注意第一行要跟nacos-config.txt中的那一行對應稼虎,說白了就是將nacos-config.txt中的那一行拷貝過來去掉service.,等于號后面的值用引號引起來就可以了招刨。還有就是default.grouplist后面的ip和端口霎俩,就是你seata啟動的ip和端口。

service {
  #vgroup->rgroup
  vgroup_mapping.my_test_tx_group = "default"
  #only support single node
  default.grouplist = "192.168.0.106: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"
}

store塊,存儲模式由file改為db打却。

store {
  ## store mode: file杉适、db
  mode = "db"
  ……
}

db塊中配置自己的數(shù)據(jù)庫連接信息。

db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    driver-class-name = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://192.168.0.106:3306/seata"
    user = "root"
    password = "zsl"
    min-conn = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
  • 新建數(shù)據(jù)庫seata柳击;
  • 在seata里新建表猿推,建表的sql在conf目錄下,名為db_store.sql捌肴,在seata庫執(zhí)行就好了蹬叭;
  • 修改conf目錄下的registry.conf,指明注冊中心為nacos状知,配置nacos的連接信息秽五,如下:
registry {
  # file 、nacos 饥悴、eureka坦喘、redis、zk西设、consul瓣铣、etcd3、sofa
  type = "nacos"

  nacos {
    serverAddr = "192.168.0.106:8848"
    namespace = ""
    cluster = "default"
  }
  • 啟動nacos济榨;
  • 初始化seata的nacos配置:進入seata/conf目錄坯沪,執(zhí)行:
sh nacos-config.sh 192.168.0.106

這個ip就是你nacos所在的服務器IP绿映。

  • 啟動seata-server擒滑,直接執(zhí)行seata/bin目錄執(zhí)行:
sh seata-server.sh -p 8091 -m db

-p是端口,-m是存儲模式叉弦,我們配置了db存儲丐一,所以這里用db。

最后日志打印出如下日志表明啟動成功:

load RegistryProvider[Nacos] extension by class[io.seata.discovery.registry.nacos.NacosRegistryProvider]

三淹冰、實戰(zhàn)之數(shù)據(jù)庫的準備

創(chuàng)建三個微服務库车,調(diào)用鏈路為 下訂單 ---> 扣庫存 ---> 減余額。
1樱拴、創(chuàng)建數(shù)據(jù)庫:

  • seata_order:存儲訂單信息的數(shù)據(jù)庫
  • seata_storage:存儲庫存信息的數(shù)據(jù)庫
  • seata_account:存儲賬戶信息的數(shù)據(jù)庫
    建庫的sql如下:
create database seata_order;
create database seata_storage;
create database seata_account;

2柠衍、建立業(yè)務數(shù)據(jù)表:

  • seata_order庫下新建t_order表:
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`  (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) DEFAULT NULL COMMENT '用戶id',
  `product_id` bigint(11) DEFAULT NULL COMMENT '產(chǎn)品id',
  `count` int(11) DEFAULT NULL COMMENT '數(shù)量',
  `money` decimal(11, 0) DEFAULT NULL COMMENT '金額',
  `status` int(1) DEFAULT NULL COMMENT '訂單狀態(tài):  0:創(chuàng)建中 1:已完結(jié)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '訂單表' ROW_FORMAT = Dynamic;
  • seata_storage庫下新建t_storage表:
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage`  (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `product_id` bigint(11) DEFAULT NULL COMMENT '產(chǎn)品id',
  `total` int(11) DEFAULT NULL COMMENT '總庫存',
  `used` int(11) DEFAULT NULL COMMENT '已用庫存',
  `residue` int(11) DEFAULT NULL COMMENT '剩余庫存',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '庫存' ROW_FORMAT = Dynamic;
INSERT INTO `t_storage` VALUES (1, 1, 100, 0, 100);
  • seata_account庫下新建t_account表:
CREATE TABLE `t_account`  (
  `id` bigint(11) NOT NULL 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 '剩余可用額度',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '賬戶表' ROW_FORMAT = Dynamic;
 
INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);

3、新建事務回滾日志表:
上面新建的三個數(shù)據(jù)庫都需要新建各自的回滾日志表晶乔。在三個業(yè)務數(shù)據(jù)庫中都執(zhí)行seata-server-0.9.0\seata\conf\目錄下的db_undo_log.sql即可珍坊。

四、實戰(zhàn)之業(yè)務代碼的編寫

業(yè)務需求:下訂單 ---> 減庫存 ---> 扣余額 ---> 改訂單狀態(tài)正罢。
1阵漏、新建訂單模塊seata-order-service2001:

  • pom.xml:主要是nacos、seata、openfeign和數(shù)據(jù)庫連接那一套履怯。
 <!-- nacos -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos -->
<!-- seata -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>0.9.0</version>
</dependency>
<!-- seata -->
<!--feign -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- druid-spring-boot-starter -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.22</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!--jdbc -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
  • application.yml(主要是要注意tx-service-group的值要與nacos-config.txt和file.conf中的對應):
server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        # 自定義事務組名稱需要與seata-server中的對應
        tx-service-group: my_test_tx_group
    nacos:
      discovery:
        server-addr: 192.168.0.106:8848
  datasource:
    # 當前數(shù)據(jù)源操作類型
    type: com.alibaba.druid.pool.DruidDataSource
    # mysql驅(qū)動類
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.0.106:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: zsl
feign:
  hystrix:
    enabled: false
logging:
  level:
    io:
      seata: info

mybatis:
  mapper-locations: classpath:mapper/*.xml
  • seata配置文件:將seata/conf下的file.conf和registry.conf拷貝到application.yml的同級目錄下回还。
  • CommonResult.java:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
    
    private Integer code;
    private String message;
    private T data;
}
  • Order.java:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    
    private Long id;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal money;
    private Integer status; // 0:創(chuàng)建中, 1:已完結(jié)

}
  • OrderDao.java:
@Mapper
public interface OrderDao {
    /** 創(chuàng)建訂單 */
    public void create(Order order);
    /** 修改訂單狀態(tài) */
    public void update(@Param("userId") Long userId, @Param("status") Integer status);
}
  • OrderMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhusl.springcloud.dao.OrderDao">
  
   <insert id="create" parameterType="Order" useGeneratedKeys="true" keyProperty="id">
      insert into t_order(user_id, product_id, count, money, status) 
      values(#{userId}, #{productId}, #{count}, #{money}, 0);
   </insert>
   
   <update id="update">
      update t_order set status = 1 where user_id = #{userId} and status = #{status};
   </update>
  
</mapper>
  • OrderService.java:
public interface OrderService {
    /** 創(chuàng)建訂單 */
    public void create(Order order);
}
  • OrderServiceImpl.java:
@Service
@Slf4j
public class OrderServiceImpl implements OrderService{
    
    @Autowired
    private OrderDao orderDao;
    @Autowired
    private StorageService storageService;
    @Autowired
    private AccountService accountService;

    @Override
    public void create(Order order) {
        log.info("================= 新建訂單start ==============");
        orderDao.create(order);
        log.info("================= 新建訂單end ==============");
        
        log.info("================= 訂單微服務調(diào)用庫存微服務扣減庫存start ==============");
        storageService.decrease(order.getProductId(), order.getCount());
        log.info("================= 訂單微服務調(diào)用庫存微服務扣減庫存end ==============");
        
        log.info("================= 訂單微服務調(diào)用賬戶微服務做扣減余額start ==============");
        accountService.decrease(order.getUserId(), order.getMoney());
        log.info("================= 訂單微服務調(diào)用賬戶微服務做扣減余額end ==============");
        
        log.info("================= 修改訂單狀態(tài)start ==============");
        orderDao.update(order.getUserId(), 0);
        log.info("================= 修改訂單狀態(tài)end ==============");
        
        log.info("================= 下單完成? ==============");
        
    }
}
  • StorageService.java:
@FeignClient(value = "seata-storage-service")
public interface StorageService {

    /** 扣減庫存 */
    @PostMapping("/storage/decrease")
    public CommonResult<?> decrease(@RequestParam("productId") Long id, @RequestParam("count") Integer count);
}
  • AccountService.java:
@FeignClient(value = "seata-account-service")
public interface AccountService {

    /** 扣余額 */
    @PostMapping("/account/decrease")
    public CommonResult<?> decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
  • OrderController.java:
@RestController
@RequestMapping("/order")
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    @GetMapping("/create")
    public CommonResult<?> create(Order order) {
        orderService.create(order);
        return new CommonResult<>(200, "訂單創(chuàng)建成功", null);
    }
}
  • DataSourceProxyConfig.java:
package com.zhusl.springcloud.config;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import com.alibaba.druid.pool.DruidDataSource;

import io.seata.rm.datasource.DataSourceProxy;


/**
 * 使用seata對數(shù)據(jù)源進行代理
 * @author zhu
 *
 */
@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    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 sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception{
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}
  • 主啟動類:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消數(shù)據(jù)源的自動創(chuàng)建叹洲,用自己配置的
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan({"com.zhusl.springcloud.dao"})
public class App {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(App.class, args);
    }
}

準備完成柠硕,現(xiàn)在依次啟動nacos、seata和2001這個項目运提,最后項目控制臺打印出如下日志并且在nacos中有seata和2001這兩個服務表明啟動成功仅叫。
啟動成功的日志:

2020-06-05 11:29:12.393  INFO 74764 --- [           main] com.zhusl.springcloud.App                : Started App in 7.465 seconds (JVM running for 7.931)
2020-06-05 11:29:12.676  INFO 74764 --- [imeoutChecker_1] i.s.c.r.netty.NettyClientChannelManager  : will connect to 192.168.2.43:8091
2020-06-05 11:29:12.676  INFO 74764 --- [imeoutChecker_1] i.s.core.rpc.netty.NettyPoolableFactory  : NettyPool create channel to transactionRole:TMROLE,address:192.168.2.43:8091,msg:< RegisterTMRequest{applicationId='seata-order-service', transactionServiceGroup='my_test_tx_group'} >
2020-06-05 11:29:12.751  INFO 74764 --- [imeoutChecker_1] i.s.core.rpc.netty.NettyPoolableFactory  : register success, cost 71 ms, version:0.9.0,role:TMROLE,channel:[id: 0x9fe21753, L:/192.168.2.36:65186 - R:/192.168.2.43:8091]

在配置啟動過程中遇到了很多問題,大家可以去官網(wǎng)尋找解決方案糙捺。遇事不要慌诫咱,官網(wǎng)來幫忙。
https://github.com/seata/seata-samples

2洪灯、新建名為seata-storage-service2002的庫存module:

  • pom.xml:和2001訂單module的一模一樣坎缭;
  • application.yml:端口改成2002,微服務名稱改成seata-storage-service签钩,連接的數(shù)據(jù)庫改成seata_storage掏呼,其他的都和2001的一樣;
  • 把file.conf和registry.conf復制粘貼到application.yml的同級目錄下铅檩;
  • Storage.java:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {

    private Long id;
    /** 產(chǎn)品id */
    private Long productId;
    /** 總庫存 */
    private Integer total;
    /** 已用庫存 */
    private Integer used;
    /** 剩余庫存 */
    private Integer residue;
}
  • StorageDao.java:
@Mapper
public interface StorageDao {
    /** 扣減庫存 */
    void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
  • StorageMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhusl.springcloud.dao.StorageDao">
   <update id="decrease">
      update t_storage set used = used + #{count}, residue = residue - #{count} where product_id = #{productId};
   </update>
</mapper>
  • StorageServiceImpl.java:
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
    @Autowired
    private StorageDao storageDao;
    @Override
    public void decrease(Long productId, Integer count) {
        log.info("============== storageService 扣減庫存 start =============");
        storageDao.decrease(productId, count);
        log.info("============== storageService 扣減庫存 end =============");
    }
}
  • StorageController.java:
@RestController
@RequestMapping("/storage")
public class StorageController {

    @Autowired
    private StorageService storageService;
    
    @PostMapping("/decrease")
    public CommonResult<?> decrease(Long productId, Integer count) {
        storageService.decrease(productId, count);
        return new CommonResult<>(200, "扣減庫存成功憎夷!", null);
    }
}
  • 最后數(shù)據(jù)源的配置和主啟動類和都和2001的一樣,復制粘貼即可昧旨。

3拾给、新建名為seata-account-service2003的賬戶module:

  • pom.xml:和2001的一模一樣;
  • application.yml:端口改為2003兔沃,服務名改成seata-account-service蒋得,連接的數(shù)據(jù)庫改成seata_account;
  • 復制粘貼file.conf和registry.conf到application.yml的同級目錄乒疏;
  • Account.java:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
    private Long id;
    /** 用戶id */
    private Long userId;
    /** 總額度 */
    private BigDecimal total;
    /** 已用額度 */
    private BigDecimal used;
    /** 剩余額度 */
    private BigDecimal residue;
}
  • AccountDao.java:
@Mapper
public interface AccountDao {
    /** 扣減余額 */
    void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
  • AccountMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhusl.springcloud.dao.AccountDao">
   <update id="decrease">
      update t_account set residue = residue - #{money},used = used + #{money} where user_id = #{userId};
   </update>
</mapper>
  • AccountServiceImpl.java:
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;
    @Override
    public void decrease(Long userId, BigDecimal money) {
        log.info("================ account-service 扣減余額 start ===============");
        accountDao.decrease(userId, money);
        log.info("================ account-service 扣減余額 end ===============");
    }
}
  • AccountController.java:
@RestController
@RequestMapping("/account")
public class AccountController {
    @Autowired
    private AccountService accountService;
    @PostMapping("/decrease")
    public CommonResult<?> decrease(Long userId, BigDecimal money) {
        accountService.decrease(userId, money);
        return new CommonResult<>(200, "扣減余額成功!", null);
    }
}
  • 最后別忘記主啟動類和數(shù)據(jù)源配置類额衙。

4、測試:
3個module建完怕吴,先測試一下能否成功運行起來窍侧,先啟動nacos,再啟動seata转绷,然后依次啟動3個module伟件。下面是3張表的初始情況:

數(shù)據(jù)庫初始狀態(tài)

現(xiàn)在模擬正常下單:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
訪問之后,可能出現(xiàn)兩種情況:

  • 返回成功信息暇咆,數(shù)據(jù)庫成功的創(chuàng)建了一條訂單锋爪,account和storage也成功的扣除了對應的數(shù)量丙曙。
  • openfeign報錯,read timeout其骄,成功創(chuàng)建了訂單亏镰,但是account沒有扣減。

如果出現(xiàn)第二種情況拯爽,那也充分說明了目前這三個操作沒有在一個事務里索抓。如果你想不報錯,不想讓openfeign超時毯炮,加上在application.yml中加上如下配置即可:

ribbon:
  ReadTimeout: 10000 #10秒應該就不會超時了
  ConnectTimeout: 10000

接下來我們在account的service里讓線程睡11秒鐘逼肯,雖然剛才openfeign設置了超時時間10秒,但是現(xiàn)在睡11秒桃煎,肯定還是會異常的篮幢。然后在OrderServiceImpl類上加上全局事務注解:

@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)

name隨意,不沖突就好为迈,rollbackFor表示什么情況下回滾三椿,這里的意思是報異常了就回滾。

配置好之后葫辐,重新啟動account和order這兩個微服務搜锰,最后再次訪問下訂單的鏈接。就會發(fā)現(xiàn)報超時異常了耿战,但是三個數(shù)據(jù)庫的三張表都沒有數(shù)據(jù)變化蛋叼,即使全部都回滾了,這就表明分布式事務起作用了剂陡。

五狈涮、關于seata的其他說明

seata官網(wǎng)上說它支持AT、TCC鹏倘、SAGA 和 XA 事務模式薯嗤,我們用的是默認的AT模式。

1纤泵、AT模式如何做到對業(yè)務無侵入的?

  • AT模式的前提:基于支持ACID事務的關系型數(shù)據(jù)庫镜粤,通過JDBC訪問數(shù)據(jù)庫的java應用捏题;
  • 整體機制:兩階段提交協(xié)議。
  • 一階段:seata會攔截業(yè)務sql肉渴,找到業(yè)務要更新的數(shù)據(jù)公荧,在被更新之前,將其保存為before image同规;執(zhí)行業(yè)務sql循狰,更新業(yè)務數(shù)據(jù)窟社;在業(yè)務數(shù)據(jù)更新之后,將其保存為after image绪钥,最后生成行鎖灿里。一階段的操作都在一個數(shù)據(jù)庫事務內(nèi)完成,保證了一階段操作的原子性程腹。這就類似spring的aop思想匣吊,前置通知和后置通知。
  • 二階段提交:如果順利寸潦,二階段就進行提交色鸳。因為一階段已經(jīng)執(zhí)行過業(yè)務sql了,所以這里只需要將一階段保存的before image见转、after image和行鎖刪除即可命雀。
  • 二階段回滾:如果出異常了需要回滾,通過一階段的回滾日志進行反向補償斩箫。首先會比較當前庫中數(shù)據(jù)和after image是否一致咏雌,如果一致,那么就將數(shù)據(jù)庫中的數(shù)據(jù)還原成before image校焦;如果不一致赊抖,說明數(shù)據(jù)出現(xiàn)過臟寫,需要人工處理寨典。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載氛雪,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末耸成,一起剝皮案震驚了整個濱河市报亩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌井氢,老刑警劉巖弦追,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異花竞,居然都是意外死亡劲件,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門约急,熙熙樓的掌柜王于貴愁眉苦臉地迎上來零远,“玉大人,你說我怎么就攤上這事厌蔽∏@保” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵奴饮,是天一觀的道長纬向。 經(jīng)常有香客問我择浊,道長,這世上最難降的妖魔是什么逾条? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任琢岩,我火速辦了婚禮,結(jié)果婚禮上膳帕,老公的妹妹穿的比我還像新娘粘捎。我一直安慰自己,他們只是感情好危彩,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布攒磨。 她就那樣靜靜地躺著,像睡著了一般汤徽。 火紅的嫁衣襯著肌膚如雪娩缰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天谒府,我揣著相機與錄音拼坎,去河邊找鬼。 笑死完疫,一個胖子當著我的面吹牛泰鸡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播壳鹤,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼盛龄,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了芳誓?” 一聲冷哼從身側(cè)響起余舶,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎锹淌,沒想到半個月后匿值,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡赂摆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年挟憔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片库正。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡曲楚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出褥符,到底是詐尸還是另有隱情,我是刑警寧澤抚垃,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布喷楣,位于F島的核電站趟大,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏铣焊。R本人自食惡果不足惜逊朽,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望曲伊。 院中可真熱鬧叽讳,春花似錦、人聲如沸坟募。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽懈糯。三九已至涤妒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赚哗,已是汗流浹背她紫。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留屿储,地道東北人贿讹。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像够掠,于是被迫代替她去往敵國和親民褂。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349