Spring Cloud微服務學習筆記之服務和數據的拆分與代碼實現

一、服務拆分

服務拆分的起點和終點 微服務架構不是憑空設計出來的,而是進化出來的,在不斷地演變與改進。一般來講,企業應用微服務大都是由已有的項目架構去轉型爲微服務,所以服務拆分的起點是既有架構的形態,終點是高可用高併發的微服務架構。

1.1 拆分的原則與方案

  • 單一職責,松耦合,高內聚

  • 關注點分離(按職責、按通用性、按粒度級別)

  • 微服務與團隊結構

在這裏插入圖片描述

康威定律: 任何組織在設計一套系統時,所交付的設計方案在結構上都應與該組織的溝通結構保持一致。

1.2 不適合服務拆分的場景

  • 系統中包含很多強事務的場景

微服務是分佈式架構,涉及到的事務是分佈式事務遵循CAP原則,而C(一致性)A(可用性)P(容錯性)三者無法同時滿足,必要做出一定的犧牲。

  • 業務相對穩定,迭代週期長

  • 訪問壓力不大,可用性要求不高

二、商品服務的代碼實現

2.1 商品服務API與SQL介紹

1. 商品服務API如下:

GET  /product/list
請求參數:無
返回參數:
{
	"code":0,
	"msg":"成功",
	"data":[
		{
			"name":"熱榜",
			"type":1,
			"foods":[
				{
					"id":"123456",
					"name":"皮蛋粥",
					"price":1.2,
					"description":"好喫的皮蛋粥",
					"icon":"http://xxx.com",
				}
			]
		},
		{
			"name":"好喫的",
			"type":2,
			"foods":[
				{
					"id":"123457",
					"name":"慕斯蛋糕",
					"price":10.9,
					"description":"美味口",
					"icon":"http://xxx.com",
				}
			]
		}
	]
}

2. 商品服務分類表與商品表的SQL介紹

-- 分類表
CREATE TABLE `product_category`  (
  `category_id` int(11) NOT NULL AUTO_INCREMENT,
  `category_name` varchar(64) NOT NULL,
  `category_type` int(11) NOT NULL,
  `create_time` timestamp NOT NULL ,
  `update_time` timestamp NOT NULL ,
  PRIMARY KEY (`category_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci;

-- 商品表
CREATE TABLE `product_info`  (
  `product_id` varchar(32)  NOT NULL,
  `product_name` varchar(64)  NOT NULL,
  `product_price` decimal(8, 2) NOT NULL,
  `product_stock` int(11) NOT NULL,
  `product_description` varchar(64)  DEFAULT NULL,
  `product_icon` varchar(512)  DEFAULT NULL,
  `product_status` tinyint(3) DEFAULT 0,
  `category_type` int(11) NOT NULL,
  `create_time` timestamp NOT NULL,
  `update_time` timestamp NOT NULL,
  PRIMARY KEY (`product_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci;

2.2 代碼實現

1. 添加pom依賴

<!-- 持久層框架使用Spring Data JPA -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

 <!-- 數據庫使用MySQL -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

 <!-- 使用lombok插件簡化代碼開發 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>		

2. Entity層代碼

Entity層命名很多,常見有dataobject,entity,domain。不一定與數據庫字段完全對應

ProductCategory 實體類

@Data     //lombok插件註解 省去Getter And Setter方法
@Entity   //lombok插件註解 與數據庫表做對應
public class ProductCategory {

    @Id   //lombok插件註解 表示主鍵
    @GeneratedValue    //lombok插件註解 表示自增
    private Integer categoryId;

    /** 類目名字. */
    private String categoryName;

    /** 類目編號. */
    private Integer categoryType;

    private Date createTime;

    private Date updateTime;
}

ProductInfo 實體類

@Data
@Entity
public class ProductInfo {

    @Id
    private String productId;
    /** 名字. */
    private String productName;
    /** 單價. */
    private BigDecimal productPrice;
    /** 庫存. */
    private Integer productStock;
    /** 描述. */
    private String productDescription;
    /** 小圖. */
    private String productIcon;
    /** 狀態, 0正常1下架. */
    private Integer productStatus;
    /** 類目編號. */
    private Integer categoryType;

    private Date createTime;

    private Date updateTime;
}

3. Dao層代碼

Dao層命名很多,常見有dao,Repository,mapper.

ProductCategory 持久層

//接口無須實現,繼承JpaRepository接口即可
public interface ProductCategoryRepository extends JpaRepository<ProductCategory, Integer> {
	//方法命名符合JPA框架的格式,也無需實現
    List<ProductCategory> findByCategoryTypeIn(List<Integer> categoryTypeList);
}

ProductInfo 持久層

public interface ProductInfoRepository extends JpaRepository<ProductInfo, String>{
    List<ProductInfo> findByProductStatus(Integer productStatus);
}

4. 枚舉類代碼

對Service層和Controller層中要用到的碼值單獨封裝枚舉類

@Getter   //lombok插件註解 省去Getter方法
public enum ProductStatusEnum {     //商品上下架狀態
    UP(0, "在架"),
    DOWN(1, "下架"),
    ;
    private Integer code;
    private String message;

    ProductStatusEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

5. Service層代碼

ProductCategory 業務邏輯層

@Service
public class CategoryServiceImpl implements CategoryService {
    @Autowired
    private ProductCategoryRepository productCategoryRepository;

    @Override
    public List<ProductCategory> findByCategoryTypeIn(List<Integer> categoryTypeList) {
        return productCategoryRepository.findByCategoryTypeIn(categoryTypeList);
    }
}

ProductInfo 業務邏輯層

@Service
public class ProductServiceImpl implements ProductService {
    @Autowired
    private ProductInfoRepository productInfoRepository;

    @Override
    public List<ProductInfo> findUpAll() {
        return productInfoRepository.findByProductStatus(ProductStatusEnum.UP.getCode());
    }
}

6. Vo類(出參整合類)代碼

對在API中的返回參數按照展示要求進行類的封裝,按Json層級分爲三個Vo類

@Data
public class ResultVO<T> {
    // 返回碼
    private Integer code;
    // 返回信息
    private String msg;
    // 具體內容
    private T data;
}
@Data
public class ProductVO {
    @JsonProperty("name")  //Json串綁定
    private String categoryName;
    @JsonProperty("type")
    private Integer categoryType;
    @JsonProperty("foods")
    List<ProductInfoVO> productInfoVOList;
}
@Data
public class ProductInfoVO {
    @JsonProperty("id")
    private String productId;
    @JsonProperty("name")
    private String productName;
    @JsonProperty("price")
    private BigDecimal productPrice;
    @JsonProperty("description")
    private String productDescription;
    @JsonProperty("icon")
    private String productIcon;
}

7. Controller層代碼

@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private ProductService productService;
    @Autowired
    private CategoryService categoryService;

    @GetMapping("/list")
    public ResultVO<ProductVO> list() {
        //1. 查詢所有在架的商品
        List<ProductInfo> productInfoList = productService.findUpAll();

        //2. 獲取類目type列表
        //Java8新特性lambda表達式,從List<ProductInfo>類型的productInfoList中提取出List<Integer>類型的CategoryType
        List<Integer> categoryTypeList = productInfoList.stream()
                .map(ProductInfo::getCategoryType)
                .collect(Collectors.toList());

        //3. 從數據庫查詢類目
        List<ProductCategory> categoryList = categoryService.findByCategoryTypeIn(categoryTypeList);

        //4. 構造數據
        List<ProductVO> productVOList = new ArrayList<>();
        for (ProductCategory productCategory: categoryList) {
            ProductVO productVO = new ProductVO();
            productVO.setCategoryName(productCategory.getCategoryName());
            productVO.setCategoryType(productCategory.getCategoryType());

            List<ProductInfoVO> productInfoVOList = new ArrayList<>();
            for (ProductInfo productInfo: productInfoList) {
                if (productInfo.getCategoryType().equals(productCategory.getCategoryType())) {
                    ProductInfoVO productInfoVO = new ProductInfoVO();
                    //完成各屬性的拷貝,從productInfo複製到productInfoVO,省去繁瑣的setter與Getter
                    BeanUtils.copyProperties(productInfo, productInfoVO);
                    productInfoVOList.add(productInfoVO);
                }
            }
            productVO.setProductInfoVOList(productInfoVOList);
            productVOList.add(productVO);
        }

        return ResultVOUtil.success(productVOList);
    }
}

8. Utils類代碼

工具類對通用代碼進行封裝,對成功或錯誤情況的返回進行封裝

public class ResultVOUtil {
    public static ResultVO success(Object object) {
        ResultVO resultVO = new ResultVO();
        resultVO.setData(object);
        resultVO.setCode(0);
        resultVO.setMsg("成功");
        return resultVO;
    }
}

三、訂單服務的代碼實現

3.1 訂單服務API與SQL介紹

1. 訂單服務API如下:

創建訂單 POST  /order/create
請求參數
"name":"張三"
"phone":"18868822111"
"address":"xxxx"
"openid":"ev3euwhd7sjw9diwkg9"  //用戶的微信openid
"items":[{
	"productId":"1423113435324"
	"productQuantity":2  //購買數量
}]
返回參數
{
	"code":0,
	"msg":"成功",
	"data":{
		"orderid":"147283992738221"
	}
}

2. 訂單服務分類表與商品表的SQL介紹

-- 訂單
CREATE TABLE `order_master`  (
  `order_id` varchar(32)  NOT NULL,
  `buyer_name` varchar(32)  NOT NULL,
  `buyer_phone` varchar(32) NOT NULL,
  `buyer_address` varchar(128)  DEFAULT NULL,
  `buyer_openid` varchar(64)  DEFAULT NULL,
  `order_amount` decimal(8, 2) NOT NULL,
  `order_status` tinyint(3) NOT NULL DEFAULT 0,
  `create_time` timestamp NOT NULL,
  `update_time` timestamp NOT NULL,
  PRIMARY KEY (`order_id`) 
);

-- 訂單商品
CREATE TABLE `order_detail`  (
  `detail_id` varchar(32)  NOT NULL,
  `order_id` varchar(32)  NOT NULL,
  `product_id` varchar(32)  NOT NULL,
  `product_name` varchar(64)  NOT NULL,
  `product_price` decimal(8, 2) NOT NULL,
  `product_quantity` int(11) NOT NULL,
  `product_icon` varchar(512)  DEFAULT NULL,
  `create_time` timestamp NOT NULL,
  `update_time` timestamp NOT NULL,
  PRIMARY KEY (`detail_id`),
  foreign key(`order_id`) REFERENCES order_master(`order_id`)
);

3.2 代碼實現

1. 添加pom依賴

2. Entity層代碼

OrderMaster 實體類

@Data
@Entity
public class OrderMaster {
    /** 訂單id. */
    @Id
    private String orderId;
    /** 買家名字. */
    private String buyerName;
    /** 買家手機號. */
    private String buyerPhone;
    /** 買家地址. */
    private String buyerAddress;
    /** 買家微信Openid. */
    private String buyerOpenid;
    /** 訂單總金額. */
    private BigDecimal orderAmount;
    /** 訂單狀態, 默認爲0新下單. */
    private Integer orderStatus;
    /** 支付狀態, 默認爲0未支付. */
    private Integer payStatus;
    /** 創建時間. */
    private Date createTime;
    /** 更新時間. */
    private Date updateTime;
}

OrderDetail 實體類

@Data
@Entity
public class OrderDetail {
    @Id
    private String detailId;
    /** 訂單id. */
    private String orderId;
    /** 商品id. */
    private String productId;
    /** 商品名稱. */
    private String productName;
    /** 商品單價. */
    private BigDecimal productPrice;
    /** 商品數量. */
    private Integer productQuantity;
    /** 商品小圖. */
    private String productIcon;
}

3. Dao層代碼

OrderMaster 持久層

public interface OrderDetailRepository extends JpaRepository<OrderDetail, String> {
}

OrderDetail 持久層

public interface OrderDetailRepository extends JpaRepository<OrderDetail, String> {
}

4. 枚舉類代碼

@Getter
public enum OrderStatusEnum {  //訂單狀態
    NEW(0, "新訂單"),
    FINISHED(1, "完結"),
    CANCEL(2, "取消"),
    ;
    private Integer code;
    private String message;
    OrderStatusEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}
@Getter
public enum PayStatusEnum {  //支付狀態
    WAIT(0, "等待支付"),
    SUCCESS(1, "支付成功"),
    ;
    private Integer code;
    private String message;
    PayStatusEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}
@Getter
public enum ResultEnum {   //返回給用戶入參的錯誤狀態
    PARAM_ERROR(1, "參數錯誤"),
    CART_EMPTY(2, "購物車爲空")
    ;
	......同上

5. DTO類(入參整合類)代碼

@Data
public class OrderDTO {
    /** 訂單id. */
    private String orderId;
    /** 買家名字. */
    private String buyerName;
    /** 買家手機號. */
    private String buyerPhone;
    /** 買家地址. */
    private String buyerAddress;
    /** 買家微信Openid. */
    private String buyerOpenid;
    /** 訂單總金額. */
    private BigDecimal orderAmount;
    /** 訂單狀態, 默認爲0新下單. */
    private Integer orderStatus;
    /** 支付狀態, 默認爲0未支付. */
    private Integer payStatus;

    private List<OrderDetail> orderDetailList;
}

6. Utils類代碼

public class KeyUtil {
    public static synchronized String genUniqueKey() { //生成唯一的主鍵(自定義UUID)
        Random random = new Random();
        Integer number = random.nextInt(900000) + 100000;
        return System.currentTimeMillis() + String.valueOf(number); //格式: 時間+隨機數
    }
}

7. Service層代碼

OrderService 業務邏輯層

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderDetailRepository orderDetailRepository;
    @Autowired
    private OrderMasterRepository orderMasterRepository;
    @Override
    public OrderDTO create(OrderDTO orderDTO) {
       //TODO 查詢商品信息(調用商品服務)
       //TODO 計算總價
       //TODO 扣庫存(調用商品服務)
        //訂單入庫
        OrderMaster orderMaster = new OrderMaster();
        String orderId = KeyUtil.genUniqueKey();
        orderDTO.setOrderId(orderId);
        BeanUtils.copyProperties(orderDTO, orderMaster);
        orderMaster.setOrderAmount(new BigDecimal(5));
        orderMaster.setOrderStatus(OrderStatusEnum.NEW.getCode());
        orderMaster.setPayStatus(PayStatusEnum.WAIT.getCode());
        orderMasterRepository.save(orderMaster);
        return orderDTO;
    }
}

8. Vo類(出參整合類)代碼

@Data
public class ResultVO<T> {
    private Integer code;
    private String msg;
    private T data;
}

9. Form類代碼

通常爲了代碼的可讀性,和區分實體類的功能的原則下,我們會建立一個與表單對應的實體對象,況且大多數的情況下,我們的表單對應的字段少於實體類對應的字段
SpringBoot提供了強大的表單驗證功能實現。即校驗用戶提交的數據的合理性的

@Data
public class OrderForm {
    // 買家姓名
    @NotEmpty(message = "姓名必填")
    private String name;
    // 買家手機號
    @NotEmpty(message = "手機號必填")
    private String phone;
    // 買家地址
    @NotEmpty(message = "地址必填")
    private String address;
    // 買家微信openid
    @NotEmpty(message = "openid必填")
    private String openid;
    //購物車
    @NotEmpty(message = "購物車不能爲空")
    private String items;
}

10. Converter類代碼

爲了邏輯功能的實現和代碼的可讀性,我們專門創建一個類,來進行form 對象和 entity 對象的轉換

@Slf4j
public class OrderForm2OrderDTOConverter {

    public static OrderDTO convert(OrderForm orderForm) {
        Gson gson = new Gson();
        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setBuyerName(orderForm.getName());
        orderDTO.setBuyerPhone(orderForm.getPhone());
        orderDTO.setBuyerAddress(orderForm.getAddress());
        orderDTO.setBuyerOpenid(orderForm.getOpenid());

		//字符串String轉爲Json,再轉換爲List<OrderDetail>
        List<OrderDetail> orderDetailList = new ArrayList<>();
        try {
            orderDetailList = gson.fromJson(orderForm.getItems(),
                    new TypeToken<List<OrderDetail>>() {
                    }.getType());
        } catch (Exception e) {
            log.error("【json轉換】錯誤, string={}", orderForm.getItems());
            throw new OrderException(ResultEnum.PARAM_ERROR);
        }
        orderDTO.setOrderDetailList(orderDetailList);

        return orderDTO;
    }
}

11. Exception類代碼

爲了用戶體驗和代碼的可讀性,我們一般創建自己的Exception類,來區分來展示更豐富的異常信息

public class OrderException extends RuntimeException {
    private Integer code;
    public OrderException(Integer code, String message) {
        super(message);
        this.code = code;
    }
    public OrderException(ResultEnum resultEnum) {
        super(resultEnum.getMessage());
        this.code = resultEnum.getCode();
    }
}

12. Controller層代碼

@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {
    @Autowired
    private OrderService orderService;
    
    @PostMapping("/create")
    public ResultVO<Map<String, String>> create(@Valid OrderForm orderForm,
                           BindingResult bindingResult) {
        if (bindingResult.hasErrors()){
        	//將表單驗證的錯誤信息,打印在日誌中,並封裝在Exception後拋出
            log.error("【創建訂單】參數不正確, orderForm={}", orderForm);
            throw new OrderException(ResultEnum.PARAM_ERROR.getCode(),
                    bindingResult.getFieldError().getDefaultMessage());
        }
        // orderForm -> orderDTO
        OrderDTO orderDTO = OrderForm2OrderDTOConverter.convert(orderForm);
        if (CollectionUtils.isEmpty(orderDTO.getOrderDetailList())) {
            log.error("【創建訂單】購物車信息爲空");
            throw new OrderException(ResultEnum.CART_EMPTY);
        }

        OrderDTO result = orderService.create(orderDTO);

        Map<String, String> map = new HashMap<>();
        map.put("orderId", result.getOrderId());
        return ResultVOUtil.success(map);
    }
}

四、數據拆分

4.1 數據拆分原則

  1. 每個微服務都有單獨的數據存儲

避免在本服務的程序裏爲了省事直接調用其他服務的數據庫;服務之間要有隔離。

  1. 依據服務特點選擇不同結構的數據庫類型

對於展示數據類型的前置服務,事務要求不高,可以選用NoSQL的MongoDB數據庫;
對於專門做搜索類型的服務,可以考慮ElasticSearch存儲數據庫;
對於事務要求高的服務,可以選用支持強事務的關係型數據庫如MySQL。

  1. 難點在確定邊界

1.針對邊界設計API
例如支付服務對用戶服務的數據側重用戶狀態是否被鎖定等,積分服務對用戶服務側重用戶的註冊時間操作時間等,用戶服務如何針對不同服務設計API接口;
2.針對邊界權衡數據冗餘
例如上述訂單服務將一部分商品服務的數據冗餘在本地,並以某種機制與商品服務信息保持一致;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章