单体应用和微服务
什么是单体应用架构?
一个归档包(例如war包)包含所有功能的应用程序,我们通常称为单体应用;架构单体应用的方法论,就是单体应用架构。
核心思想:一个归档包解决所有问题
一个war包包含了用户、选课、课程分类等模块,使用一个数据库
单体架构缺点:
- 部署慢、部署频率低
- 无法按需扩展 例如课程分类是IO密集型的业务 当需要扩展时 会需要整体迁移扩展
- 阻碍技术创新 用户模块想从struts改为springMvc比较困难
微服务定义
微服务架构风格是一种将一个单体应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间通信采用轻量级通信机制(通常HTTP资源API),这些服务围绕业务能力构建并且可通过全自动部署机制独立部署。这些服务公用一个最小型的集中式的管理,服务可用不同的语言开发,使用不同的数据存储技术。
微服务的特性
-
每个微服务可独立运行在自己的进程里
-
一系列独立运行的微服务共同构建起整个系统
-
每个微服务为独立的业务开发,只关注某个特定的功能
-
全自动机制(CI/CD) 微服务的基石 能够自动化的自动化
-
异构(不同语言 java php python 与数据存储 mongo redis mysql)
-
轻量的通信机制 通信协议应该是轻量的 webservice soap 这些协议就重量级 轻量型 还需要 能跨平台
微服务的核心思想:分而治之
微服务架构通览:
-
通过网关达到系统内部
-
每个微服务都有自己的web服务器(比如tomcat)
-
每个微服务之间通过轻量级通信机制进行通信
-
每个微服务都有自己的数据库
-
存在服务发现组件
微服务并不是黑科技,而是利用分而治之的思想将一个大型的服务拆分成若干个小型的应用。从微观来看,每个微服务也是一个单体应用
微服务适合场景
- 大型复杂应用
- 高并发、高负载应用
- 快速迭代
微服务拆分方法
- domain driven design 领域驱动设计 曲线比较高 《领域驱动设计》(l理论)《实现领域驱动设计》(理论+设计)《领域驱动设计》(精简版)
- 面向对象-》状态 byname 行为 by verb 比如ddd要流行一些
微服务拆分方法——个人心得总结
-
按照职责划分
订单服务只关注订单方面的
-
按照通用性划分
比如用户中心 很多的微服务都会用到用户中心 比如中台战略(多个微服务组成的能力中心)
-
微服务粒度
太细 -》 额外的网络开销 增加运维成本
太粗 -》 单个服务的复杂度依然过高
良好地满足业务需求
增量迭代–》保持相对的独立 一次迭代只涉及部分的微服务
持续进化–》更换语言 技术的更替(更换框架)
团队幸福感
项目简介与拆分
拆分-》建表-》写代码
用户微服务(通用性划分)
课程微服务(职责划分)
不建议在初期将项目分得更细 否则运维程度太高
技术选型
Spring Boot
Spring MVC
Spring Data JPA
Spring Cloud
创建用户微服务
构建一个maven项目
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.cloud</groupId>
<artifactId>ms-user</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ms-user</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 如果xxx是springboot出品 spring-boot-starter-xxx -->
<!-- 如果xxx是第三方出品 xxx-spring-boot-starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
编写配置文件
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/ms_user
hikari:
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: none # 让hibernate不去操作表结构
# 打印执行的sql
show-sql: true
创建实体类User
package com.cloud.msuser.domain.entity;
import java.math.BigDecimal;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Table(name = "user")
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // mysql的自增策略
private Integer id;
@Column
private String username;
@Column
private String password;
@Column
private BigDecimal money;
@Column
private String role;
@Column
private Date regTime;
setter/getter略
}
创建DAO
package com.cloud.msuser.repository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.cloud.msuser.domain.entity.User;
@Repository
public interface UserRepository extends CrudRepository<User, Integer> {
}
创建服务层
package com.cloud.msuser.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.cloud.msuser.domain.entity.User;
import com.cloud.msuser.repository.UserRepository;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User findById(Integer id) {
// 如果有就返回 否则抛出异常
return this.userRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("用户不存在"));
}
}
创建控制层
package com.cloud.msuser.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.cloud.msuser.domain.entity.User;
import com.cloud.msuser.service.UserService;
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public User findById(@PathVariable Integer id) {
return this.userService.findById(id);
}
}
创建启动类
package com.cloud.msuser;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MsUserApplication {
public static void main(String[] args) {
SpringApplication.run(MsUserApplication.class, args);
}
}
项目结构
启动异常
java.sql.SQLException: The server time zone value '�й���ʱ��' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support.
修改application.yml文件:
# 增加url的属性serverTimezone
url: jdbc:mysql://127.0.0.1:3306/ms_user?serverTimezone=UTC
另外如果url定义错误比如:jdbc:mysql//127.0.0.1:3306/ms_user?serverTimezone=UTC(msyql后面少一个:),启动会导致异常:
Description:
Failed to configure a DataSource: no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
启动成功访问网页
http://localhost:8081/users/1
{"id":1,"username":"itmuch","password":"1111","money":5,"role":"user","regTime":"2020-02-15T14:37:20.000+0000"}
创建课程微服务
创建一个maven项目
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.cloud</groupId>
<artifactId>ms-class</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ms-class</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 如果xxx是springboot出品 spring-boot-starter-xxx -->
<!-- 如果xxx是第三方出品 xxx-spring-boot-starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
编写配置文件
server:
port: 8010
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/ms_class?serverTimezone=UTC
hikari:
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: none # 让hibernate不去操作表结构
# 打印执行的sql
show-sql: true
创建实体类
package com.cloud.msclass.domain.entity;
import java.math.BigDecimal;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Table(name = "lesson")
@Entity
public class Lesson {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column
private String title;
@Column
private String cover;
@Column
private BigDecimal price;
@Column
private String description;
@Column
private Date createTime;
@Column
private String videoUrl;
}
package com.cloud.msclass.domain.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Table(name = "lesson_user")
@Entity
public class LessonUser {
@Column
@Id
private Integer lessonId;
@Column
private Integer userId;
public Integer getLessonId() {
return lessonId;
}
public void setLessonId(Integer lessonId) {
this.lessonId = lessonId;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
}
创建DTO
package com.cloud.msclass.domain.dto;
import java.math.BigDecimal;
import java.util.Date;
public class UserDTO {
private Integer id;
private String username;
// 实际项目中会隐藏password
private String password;
private BigDecimal money;
private String role;
private Date regTime;
}
创建DAO
package com.cloud.msclass.repository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.cloud.msclass.domain.entity.Lesson;
@Repository
public interface LessonRepository extends CrudRepository<Lesson, Integer> {
}
package com.cloud.msclass.repository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.cloud.msclass.domain.entity.LessonUser;
@Repository
public interface LessonUserRepository extends CrudRepository<LessonUser, Integer> {
LessonUser findByLessonId(Integer id);
}
创建服务层
package com.cloud.msclass.service;
import java.math.BigDecimal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.cloud.msclass.domain.dto.UserDTO;
import com.cloud.msclass.domain.entity.Lesson;
import com.cloud.msclass.domain.entity.LessonUser;
import com.cloud.msclass.repository.LessonRepository;
import com.cloud.msclass.repository.LessonUserRepository;
@Service
public class LessonService {
@Autowired
private LessonRepository lessonRepository;
@Autowired
private LessonUserRepository lessonUserRepository;
@Autowired
private RestTemplate restTemplate;
public Lesson buyById(Integer id) {
// 1. 根据id查询lesson
Lesson lesson = this.lessonRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("该课程不存在"));
// 2. 根据lesson.id查询user_lesson,那么直接返回lesson
LessonUser lessonUser = this.lessonUserRepository.findByLessonId(id);
if (lessonUser != null) {
return lesson;
}
// TODO 登录实现后需重构
Integer userId = 1;
// 3. 如果user_lesson==null && 用户的余额 > lesson.price 则购买成功
UserDTO userDTO = restTemplate.getForObject("http://localhost:8081/users/{userId}", UserDTO.class, userId);
BigDecimal money = userDTO.getMoney().subtract(lesson.getPrice());
if (money.doubleValue() < 0) {
throw new IllegalArgumentException("余额不足");
}
// TODO 购买逻辑 ... 1. 调用用户微服务的扣减金额接口 2.向lesson_user表插入数据
return lesson;
}
}
创建控制层
package com.cloud.msclass.controller;
import org.springframework.beans.factory.annotation.Autowired;
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;
import com.cloud.msclass.domain.entity.Lesson;
import com.cloud.msclass.service.LessonService;
@RestController
@RequestMapping("lesssons")
public class LessonController {
@Autowired
private LessonService lessonService;
/**
* http://localhost:8010/lesssons/buy/1
* 购买指定id的课程
* @param id
*/
@GetMapping("/buy/{id}")
public Lesson buyById(@PathVariable Integer id) {
return this.lessonService.buyById(id);
}
}
创建启动类
package com.cloud.msclass;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class MsClassApplication {
public static void main(String[] args) {
SpringApplication.run(MsClassApplication.class, args);
}
/**
* spring web提供的轻量级http client
* @return
*/
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
项目结构
启动项目
http://localhost:8010/lesssons/buy/1
{"id":1,"title":"SpringCloud视频教程","cover":"xxx","price":5,"description":"SpringCloud视频教程","createTime":"2020-02-15T15:50:35.000+0000","videoUrl":"https://ke.qq.com/classroom/index.html"}
此时数据库存在如下测试数据:
此时lesson_user表没有数据
在lesson_user表中添加数据:
本章总结
- 什么是微服务:大项目分而治之的思想
- 微服务架构通览:网关+微服务+服务发现组件
- 微服务适用场景:大型复杂应用、高并发高负载应用、快速迭代