服务和数据的拆分与代码实现
- 一、服务拆分
- 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 拆分的原则与方案
- 单一职责,松耦合,高内聚
- 关注点分离(按职责、按通用性、按粒度级别)
- 微服务与团队结构
康威定律: 任何组织在设计一套系统时,所交付的设计方案在结构上都应与该组织的沟通结构保持一致。
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.针对边界权衡数据冗余
例如上述订单服务将一部分商品服务的数据冗余在本地,并以某种机制与商品服务信息保持一致;