Spring Cloud Alibaba入門實踐(三十八)-引入Seata元件

紳士jiejie發表於2020-11-11

在電商的業務場景中,下單和扣庫存的操作是發生在訂單服務和商品服務,所以下單的業務操作需要保證分散式事務,所以接下來我們引入Seata來完善我們的分散式事務問題。

先下載Seata,地址如下:

https://github.com/seata/seata/releases/v0.9.0/

修改配置檔案

下載後解壓,進入conf目錄,調整下面的配置檔案:

registry.conf

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-HwPpV9KC-1605107782991)(/Volumes/小可愛/Typora/blogpic/image-20201111155532070.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-WPFzvojA-1605107783002)(/Volumes/小可愛/Typora/blogpic/image-20201111155603954.png)]

nacos-config.txt

在這裡插入圖片描述

上述截圖中的配置項的規則是service.vgroup_mapping.$ {your-service-gruop}=default,其中的${your-service-gruop}就是我們自己在配置檔案中定義的服務組名稱。由於目前只需要訂單服務和商品服務做測試,所以只定義了以上兩個屬性,原來的那個做示例的vgroup就可以刪除了。

初始化seata在nacos的配置

先啟動nacos,然後進入seata安裝包的conf資料夾下,輸入nacos-config.sh 127.0.0.1命令,初始化seata的nacos配置。執行成功後開啟Nacos的控制檯,在配置列表中,可以看到初始化了很多Group為SEATA_GROUP 的配置,如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-gzrPCHG7-1605107783012)(/Volumes/小可愛/Typora/blogpic/image-20201111161733079.png)]

啟動seata服務

進入seata解壓包下的bin檔案,使用seata-server.bat -p 9000 -m file命令啟動seata服務,如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-bnh3sfTk-1605107783016)(/Volumes/小可愛/Typora/blogpic/image-20201111162400440.png)]

然後在nacos的服務列表中就會看到一個名為serverAddr的服務,如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-EkEKNnQf-1605107783019)(/Volumes/小可愛/Typora/blogpic/image-20201111162450467.png)]

初始化資料表

seata需要記錄事務日誌,方便之後做資料回滾或日誌刪除操作,可以把這個日誌想象成資料庫的undo日誌,所以需要建立一張對應的undo_log表。由於目前用來測試的資料庫只有一個,所以就在該資料庫下新建該表,建表語句如下:

CREATE TABLE `undo_log` (
`id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT ( 20 ) NOT NULL,
`xid` VARCHAR ( 100 ) NOT NULL,
`context` VARCHAR ( 128 ) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT ( 11 ) NOT NULL,
`log_created` DATETIME NOT NULL,
`log_modified` DATETIME NOT NULL,
`ext` VARCHAR ( 100 ) DEFAULT NULL,
PRIMARY KEY ( `id` ),
UNIQUE KEY `ux_undo_log` ( `xid`, `branch_id` ) 
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;

在需要進行分散式事務控制的每個微服務下新增如下幾項配置:

  • 新增seata依賴,這裡為了方便,把依賴統一新增在了mall-common模組中,具體專案的話,再根據具體情況來做選擇,依賴如下:
   <!--分散式事務seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
  • 新增DataSourceProxyConfig配置:

由於Seata是通過代理資料來源實現事務分支的,所以需要配置io.seata.rm.datasource.DataSourceProxy的Bean,且是@Primary預設的資料來源,否則事務不會回滾,無法實現分散式事務。DataSourceProxyConfig配置如下:

package com.example.mallproduct.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class DataSourceProxyConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }

    @Primary
    @Bean
    public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }
}

同時由於引入了DruidDataSource,所以要排除DataSourceAutoConfiguration,每個微服務的啟動類都需要排除DataSourceAutoConfiguration,示例如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-4fGNyaTd-1605107783023)(/Volumes/小可愛/Typora/blogpic/image-20201111172846320.png)]

在每個微服務的resources資料夾下新增Seata的配置檔案registry.conf(也可以直接複製Seata資料夾下修改後的registry.conf),內容如下:

registry {
    type = "nacos"
    nacos {
        serverAddr = "localhost"
        namespace = "public"
        cluster = "default"
}
}
config {
    type = "nacos"
    nacos {
        serverAddr = "localhost"
        namespace = "public"
        cluster = "default"
}
} 

修改每個微服務的bootstrap.properties配置檔案內容,參考如下:

#指定配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
#指定專案名
spring.application.name=mall-order
#指定名稱空間
spring.cloud.nacos.config.namespace=public
#指定配置檔案分組
spring.cloud.nacos.config.group=SEATA_GROUP
#對應之前registry.conf配置檔案裡新增的配置項內容
spring.cloud.alibaba.seata.tx-service-group=${spring.application.name}

接著修改下單程式碼邏輯,方便測試,看到效果:

首先在商品服務新增一個減庫存的方法,如下:

ProductController類:

package com.example.mallproduct.controller;

import com.example.mallcommon.domain.Product;
import com.example.mallproduct.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RefreshScope//在需要動態讀取配置的類上新增此註解就可以實現動態重新整理功能
public class ProductController {

    @Autowired
    private ProductService productService;


    //商品資訊查詢
    @GetMapping("/product/{pid}")
    public Product findById(@PathVariable("pid") Integer pid) {
        Product product = productService.findByPid(pid);
        return product;
    }

    //減少庫存
    @RequestMapping("/product/reduceInventory")
    public void reduceInventory(Integer pid, int num) {
        productService.reduceInventory(pid, num);
    }
}

ProductService類:

package com.example.mallproduct.service;


import com.example.mallcommon.domain.Product;


public interface ProductService {

    //根據pid查詢商品資訊
    Product findByPid(Integer pid);

    /**
     * 扣減商品庫存
     *
     * @param pid
     * @param num
     */
    void reduceInventory(Integer pid, int num);
}

ProductServiceImpl類:

package com.example.mallproduct.service.impl;

import com.example.mallcommon.domain.Product;
import com.example.mallproduct.dao.ProductDao;
import com.example.mallproduct.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductDao productDao;

    @Override
    public Product findByPid(Integer pid) {
        return productDao.selectById(pid);
    }

   

    @Override
    public void reduceInventory(Integer pid, int number) {
        Product product = productDao.selectById(pid);
        if (product.getStock() < number) {
            throw new RuntimeException("庫存不足");
        }
        int i = 1 / 0;
        product.setStock(product.getStock() - number);
        productDao.updateById(product);
    }
}

可以發現在減庫存的方法裡,我們模擬了一個異常情況。

接著修改訂單微服務中的ProductService類,新增上扣減庫存的遠端呼叫介面,如下:

ProductService類:

package com.example.mallorder.fegin;

import com.example.mallcommon.domain.Product;
import com.example.mallorder.fallback.ProductServiceFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "mall-product")
//宣告呼叫的提供者的name,這個name就是我們配置檔案裡設定的application.name
public interface ProductService {

    /**
     * 指定呼叫提供者的哪個方法,可以發現這路徑和註解以及引數完全就和ProductController中的一樣,對的,就是拷貝過來的
     * 可以簡單的理解成:@FeignClient+@GetMapping就能夠對應到一個完整的請求路徑,http://mall-product/product/{pid}
     *
     * @param pid
     * @return
     */
    @GetMapping("/product/{pid}")
    Product findById(@PathVariable("pid") Integer pid);

    /***
     * 扣減庫存
     * @param pid
     * @param num
     */
    @RequestMapping("/product/reduceInventory")
    void reduceInventory(@RequestParam("pid") Integer pid, @RequestParam("num")
            int num);
}

整合一下方法,把下單邏輯都放到service層,所以修改後OrderController類如下:

package com.example.mallorder.controller;

import com.example.mallcommon.domain.Order;
import com.example.mallcommon.domain.Product;
import com.example.mallorder.fegin.ProductService;
import com.example.mallorder.service.OrderService;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;


    //下單
    @RequestMapping("/order/prod/{pid}")
    public Order createOrder(@PathVariable("pid") Integer pid) {
        return orderService.createOrder(pid);
    }
}

OrderService類:

package com.example.mallorder.service;

import com.example.mallcommon.domain.Order;

public interface OrderService {

    //建立訂單
    Order createOrder(Integer pid);
}

OrderServiceImpl類:

package com.example.mallorder.service.impl;

import com.example.mallcommon.domain.Order;
import com.example.mallcommon.domain.Product;
import com.example.mallorder.dao.OrderDao;
import com.example.mallorder.fegin.ProductService;
import com.example.mallorder.service.OrderService;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderDao orderDao;
    @Autowired
    private ProductService productService;
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Override
    public Order createOrder(Integer pid) {
        //呼叫商品微服務,查詢商品資訊
        Product product = productService.findById(pid);
        if (product.getPid() == -1) {
            Order order = new Order();
            order.setPname("下單失敗");
            return order;
        }
        //下單(建立訂單)
        Order order = new Order();
        order.setUid(1);
        order.setUsername("測試使用者");
        order.setPid(pid);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(1);
        orderDao.insert(order);
        //3 扣庫存
        productService.reduceInventory(pid, order.getNumber());
        //下單成功之後,將訊息放到mq中
        rocketMQTemplate.convertAndSend("order-topic", order);
        return order;
    }
}

此時就啟動訂單服務和商品服務做測試,其餘服務就不啟動了,然後其他一些專案執行需要的中介軟體也得啟動,免得影響專案啟動。

此時的訂單表時沒有訂單的,而商品表中每個商品有5000庫存,如下:

商品表:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-o1HoDxUz-1605107783029)(/Volumes/小可愛/Typora/blogpic/image-20201111225753077.png)]

瀏覽器發起下單請求http://localhost:10020/order/prod/1,連續下單兩次,訂單表和商品表結果如下:

在這裡插入圖片描述

在這裡插入圖片描述

對比下可以發現,明明已經有兩個訂單下單成功了,但是庫存還是5000,沒有減少。是分散式事務沒生效?這到不是,之前所有的步驟都是seata的使用環境準備,確實繁瑣了些,不過搞定後,使用起來確實簡單的很,只需要使用@GlobalTransactional註解,我們就可以開啟全域性事務控制了,在下單的方法上加上@GlobalTransactional註解,如下:

在這裡插入圖片描述

重啟服務,再次發起多次下單請求,資料庫結果如下:

訂單表:

在這裡插入圖片描述

商品表:

在這裡插入圖片描述

可以發現此時哪怕點再多次,訂單表的資料和商品表的資料也是一致的,分散式事務控制成功。

其實可以打打斷點,看看之前用來記錄事務日誌的undo_log表的資料情況,訂單服務提交事務後,undo_log表資料如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-SmEqHdQv-1605107783049)(/Volumes/小可愛/Typora/blogpic/image-20201111231413372.png)]

業務結束後,資料就會被刪除了。

PS:如果啟動服務,控制檯一直打no available server to connect日誌,或者是請求發起,也是報類似的這種錯,那麼不要慌,大概率是seata環境配置有問題,這時候,重新理一遍邏輯,重新配置一遍seata的環境,再試試看。

相關文章