服务和数据的拆分与代码实现

  • 一、服务拆分
  • 1.1 拆分的原则与方案
  • 1.2 不适合服务拆分的场景
  • 二、商品服务的代码实现
  • 2.1 商品服务API与SQL介绍
  • 1. 商品服务API如下:
  • 2. 商品服务分类表与商品表的SQL介绍
  • 2.2 代码实现
  • 1. 添加pom依赖
  • 2. Entity层代码
  • 3. Dao层代码
  • 4. 枚举类代码
  • 5. Service层代码
  • 6. Vo类(出参整合类)代码
  • 7. Controller层代码
  • 8. Utils类代码
  • 三、订单服务的代码实现
  • 3.1 订单服务API与SQL介绍
  • 1. 订单服务API如下:
  • 2. 订单服务分类表与商品表的SQL介绍
  • 3.2 代码实现
  • 1. 添加pom依赖
  • 2. Entity层代码
  • 3. Dao层代码
  • 4. 枚举类代码
  • 5. DTO类(入参整合类)代码
  • 6. Utils类代码
  • 7. Service层代码
  • 8. Vo类(出参整合类)代码
  • 9. Form类代码
  • 10. Converter类代码
  • 11. Exception类代码
  • 12. Controller层代码
  • 四、数据拆分
  • 4.1 数据拆分原则


一、服务拆分

服务拆分的起点和终点 微服务架构不是凭空设计出来的,而是进化出来的,在不断地演变与改进。一般来讲,企业应用微服务大都是由已有的项目架构去转型为微服务,所以服务拆分的起点是既有架构的形态,终点是高可用高并发的微服务架构。

1.1 拆分的原则与方案

  • 单一职责,松耦合,高内聚
  • 关注点分离(按职责、按通用性、按粒度级别)
  • 微服务与团队结构

springboot 拆分controller为微服务 springcloud拆分服务依据_List

康威定律: 任何组织在设计一套系统时,所交付的设计方案在结构上都应与该组织的沟通结构保持一致。

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.针对边界权衡数据冗余
例如上述订单服务将一部分商品服务的数据冗余在本地,并以某种机制与商品服务信息保持一致;