一、服務拆分
服務拆分的起點和終點 微服務架構不是憑空設計出來的,而是進化出來的,在不斷地演變與改進。一般來講,企業應用微服務大都是由已有的項目架構去轉型爲微服務,所以服務拆分的起點是既有架構的形態,終點是高可用高併發的微服務架構。
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 數據拆分原則
- 每個微服務都有單獨的數據存儲
避免在本服務的程序裏爲了省事直接調用其他服務的數據庫;服務之間要有隔離。
- 依據服務特點選擇不同結構的數據庫類型
對於展示數據類型的前置服務,事務要求不高,可以選用NoSQL的MongoDB數據庫;
對於專門做搜索類型的服務,可以考慮ElasticSearch存儲數據庫;
對於事務要求高的服務,可以選用支持強事務的關係型數據庫如MySQL。
- 難點在確定邊界
1.針對邊界設計API
例如支付服務對用戶服務的數據側重用戶狀態是否被鎖定等,積分服務對用戶服務側重用戶的註冊時間操作時間等,用戶服務如何針對不同服務設計API接口;
2.針對邊界權衡數據冗餘
例如上述訂單服務將一部分商品服務的數據冗餘在本地,並以某種機制與商品服務信息保持一致;