微服务架构简介
微服务架构是一种软件架构模式/思想, 并不是某个具体的框架和技术。微服务架构提倡将大的系统构建成一系列按业务功能划分的, 可独立运行的, 内聚的自治微服务(子系统)。各个服务彼此之间职责明确, 分工协作。
在传统的单体式架构中, 所有的模块代码都构建在一个应用中, 所有的数据存放在一个数据库中。在这种模式下,随着需求的不断增加, 业务功能不断增加,应用系统将会变得越来越庞大和臃肿,可维护性逐渐降低,灵活性降低,代码发布频率越来越慢,新功能交付效率也变得越来越低下。
微服务架构可以帮助我们克服旧架构所面临的挑战,微服务强调的"小快灵",微服务将单体系统的功能模块拆分成独立的微服务。应用系统由若干个微服务构建而成的,每个微服务仅负责归属于自己业务领域的功能,每个微服务本身是内聚的,而服务与服务间通过松耦合的形式交互。微服务具有灵活部署、可扩展、技术异构等特点。
单体应用通常需要使用相同的技术栈, 必须基于相同的开发语言和框架, 要想引入新框架或新技术平台会非常困难。微服务架构因为各个服务之间相互独立,不同的微服务可以根据业务及团队的特点, 灵活地选择不同的技术栈进行开发。可以仅针对一个微服务进行新技术的改造,而不会影响系统中的其它微服务,有利于系统的演进。
单体应用对系统的任何修改都必须对整个系统进行重新的构建、测试。而微服务则可以基于微服务进行修改和重构,粒度更小,更能适应频繁的变更。
单体应用只能作为一个整体进行扩展和部署。而微服务之间相互独立,可以独立部署,部署更灵活,每个微服务都是灵活易变、可伸缩、可扩展、可组合的, 就像搭积木一样, 拼出不同的产品。微服务架构更适云化和容器化。
微服务需要对外提供服务给其他系统调用(交互),目前主流的技术架构包括: RPC和REST。
- RPC框架有: Apache Dubbo (阿里巴巴), gRPC (Google), Apache Thrift(Facebook), …
- REST框架有: Spring Boot/Spring Cloud, Jersey, …
需要注意的是REST和RPC也是一种技术思想, 而非一种规范。RPC和REST的一个主要区别是RPC传输的是二进制数据,而REST传输的通常是(JSON)字符串,在性能方面,通常RPC在性能上要优于REST,RPC在响应时间上更为出色。REST采用的通常是HTTP+JSON/XML, 相对的通用性更好,更容易被各个编程语言, 平台,甚至是工具(postman/curl/…)支持。通常来说内部的, 对性能敏感的服务,选择RPC更具有优势,而对外做开放平台开放的服务,通常采用REST会更好一些。
对于REST,目前主流的是使用JSON作为数据格式,因为JSON比XML 简洁得多,XML除了主要传输的数据之外, 有较多冗余用在定义格式上,JSON在网络带宽,序列化和解析的性能方面有明显的优势。
最后需要注意的是,并不是使用了RPC或者REST的应用系统就是微服务架构,如果只是在单体架构中,提供了RPC或者REST类型的服务,这还是一个单体的架构。还是那句话,微服务架构是种软件设计思想,而非某种具体的技术,RPC/REST只是适合于实现微服务架构,或者说某种具体框架是基于微服务思想,为构建微服务架构而创建出来的。
Restfull API简介
REST是(REpresentational State Transfer)的缩写,在2000年Roy Fielding的博士论文中被提出,REST也是一种设计思想。如果一个接口符合REST原则(约束条件),那我们就称这类接口为REST风格的接口,也就是RESTful API。REST并不绑定某种编程语言,你可以使用Java/Spring编写RESTful API,也可以使用PHP/Python,… anyway.
REST指的是资源在网络中以某种表现形式进行状态转移。首先, REST是面向资源的架构, (ROA - Resource-Oriented Architecture)。 REST把一切程序需要访问到的业务对象统一定义为资源, REST中的表述(Representational ),指的是资源的某种形式(如通过JSON/XML)的表述, 表述不一定是所有信息,可以只是关注的部分信息。在REST中,传输的都是资源的表述。
状态转移中的"状态"指的是资源的状态,资源状态存储在服务器端,客户端通过指定http(GET/POST/PUT/DELETE/…)请求方法, 对资源的状态进行增删查改(CURD)。通过调用,引起资源状态的改变,称为状态转移。
无状态(Stateless)是REST架构设计中一个重要的原则,无状态指的是服务器端不会为客户端请求创建会话(Session)信息,因此无状态的请求必须包含有能够让服务器理解请求的全部信息。
对于有状态的应用,服务端维护一个会话状态信息列表,客户端第一次请求,服务器会创建一个会话,并通过创意一个唯一的session-id来标识该会话,并通过http响应的Set-Cookie头部返回给客户端,后续客户端请求携带包含session-id信息的cookie头部,服务端解析cookie取出session-id,就可以通过session-id找到保存在对应的会话中的信息。例如, 当一个用户登录成功后,我们可以把用户名(username)放到session中保存,这样后续的请求,请求不需要附带户名信息,服务器也知道你是谁(与你有关的信息)。
对于无状态的Web应用,因为服务器上没有会话(上下文)信息,所以每次请求都需要带上用户名等信息,服务器端才能知道,当然出于安全考虑,不能你说你是谁就是谁,通常会引入OAuth机制,要求每次请求带上授权凭证(access-token),对身份进行核验。
为什么要强调无状态原则呢?因为会话信息(变量)是存放在服务器的内存中的,这就意味着有状态的请求只能由同一台服务器进行处理,而无状态请求则完全没有这个限制,请求能够被任何可用的服务器处理,无状态请求有较强的容错性和横向扩展性。
每一个资源通过唯一的URI(Uniform Resource Identifier)标识,一个URI定位一个资源。在URI的设计上我们通常需要遵循规范(guidelines),就像Java语言的编码规则: 类名要大写开头, 方法名变量名小写开头, … 虽然不是强制的, 但推荐这么做,遵循统一的规则便于开发这和使用者之间形成默契,更容易理解URI的用途。
规则一: URI的路径(Path)部分应该只有名词,不包含动词。URI的路径部分应该体现的是资源的层次关系。
//符合REST风格的URI
http://api.example.com/human-resources/employees // 所有员工
http://api.example.com/human-resources/employees/1 // 员工编号为1的员工
http://api.example.com/human-resources/employees?country=china®ion=shanghai //中国,上海地区,所有员工
// 不符合REST风格的URI, 因为包含了动词(query-employees和update), 不是资源的层次关系。
http://api.example.com/human-resources/query-employees
http://api.example.com/human-resources/update/employees/1
规则二: 应该通过不同的http请求方式(GET/POST/PUT/PATCH/DELETE)代表对资源的不同的操作, 而不是通过URI。
//符合REST风格的URI, 对资源的增删查改操作请求的都是同一个URI,只是使用不同的http请求的方法来区分不同的操作。
GET http://api.example.com/human-resources/employees/1 // 获取服务器中员工编号为1的员工(资源)
POST http://api.example.com/human-resources/employees // 在服务器中创建一个新的员工(资源)
PUT http://api.example.com/human-resources/employees/1 // 更新服务器中员工编号为1的员工(资源)
// 不符合REST风格的URI, 使用不同的URI来对同一个资源进行增删查改操作
GET http://api.example.com/human-resources/employees/query-employee?emp_id=1
GET http://api.example.com/human-resources/employees/new?emp_id=1
GET http://api.example.com/human-resources/update/employees/1
规则三: 使用请求参数(Request Parameters)传递与资源以及资源层次无关的信息。
//符合REST风格的URI
http://api.example.com/human-resources/employees?offset=0&limit=10
http://api.example.com/human-resources/employees?sortby=ename&order=asc
// 不符合REST风格的URI
http://api.example.com/human-resources/employees?emp_id=1
规则四:URI全部小写,不要使用大写,或者大小写组合。 可以使用横杆"-"来增加URI的可读性,。
//符合REST风格
http://api.exmple.com/my-learning-exmple
//不符合REST风格, 使用了大小写组合
http://api.example.com/MyLearningExample
http://api.example.com/my_learning_example // 也不推荐使用下划线
对于URI设计的规则,个人的感悟是,要理解URI的本质,而不是生搬硬套。我们先看看URI的格式的定义:
http: //127.0.0.1:8080 /human-resources/employees ?country=china®ion=shanghai
[scheme:][ //authority ] [ path ] [ ?query ] [#fragment]
URI的本质是唯一定位资源,可以理解为资源的名称空间,名称空间是通过层次结构(authority+path)来区分,这和java的包(package)机制类似,例如"org.springframework.boot.SpringApplication", 通过package将Spring的SpringApplication与其他同名的类区分开来。
例如我们设计一个人力资源系统,我们有employees资源, departments资源,...我们该如何设计资源URI?
1#. http://127.0.0.1:8080/employees?country=china®ion=shanghai -- 不好的URI
2#. http://127.0.0.1:8080/china/shanghai/employees -- 不好的URI
3#. http://127.0.0.1:8080/human-resources/employees?country=china®ion=shanghai --好的URI
1# URI: 这个URI缺少层次结构, 当系统庞大, 有多个资源时, 容易产生名称冲突,
而且1# URI没有层次结构也不利于我们一眼就看出这个employees资源是什么系统,干什么的;
2# URI: 这个URI看似有层次结构(中国, 上海地区, 的所有员工资源), 但这个层次结构设计不合理,
这样意味着(/china/guangzhou/employees: 中国, 广州地区, 的所有员工资源)也是一个不同资源,
实际上我们系统中所有员工(employees)是一个资源, china/shanghai/guangzhou/...只对employees资源的过滤(query);
3# URI: 是个合理的URI设计;体现在编码过程中:
//3# URI的控制器
@RestController
@RequestMapping("/human-resources/employees")
public class EmployeesController {
@RequestMapping(method=RequestMethod.GET)
public List<Employee> getEmployees(
@RequestParam(name = "country", required = false) String country,
@RequestParam(name = "region") Optional<String> region
) { ...}
}
// 如果使用2# URI, 需要为每个地区的员工编写控制器
// 从OOP角度, 这些员工的基本是相同的, 仅区域属性不同,重用角度看,分成不同的类不合理, 也不通用。
@RestController
@RequestMapping("/china/shanghai/employees") //Shanghai
public class ShanghaiEmployeesController {
@RequestMapping(method=RequestMethod.GET)
public List<Employee>getEmployees() {...}
}
@RestController
@RequestMapping("/china/guangzhou/employees") //Guangzhou
public class GuangzhouEmployeesController {
@RequestMapping(method=RequestMethod.GET)
public List<Employee> getEmployees() {...}
}
// other country and region ...
4#. http://127.0.0.1:8080/employees?employee_id=7369 -- 不好的URI
5#. http://127.0.0.1:8080/employees/7369 -- 好的URI
4# URI的语义是, 操作所有员工, 过滤出employee_id=7369的一个员工;
5# URL的语义是, 操作employee_id=7369的员工;
因为我们系统在编码上会设计一个Employee类, 代表员工, 所有员工我们会用一个列表表达List<Employee>,对于操作具体一个员工资源,
5# URL逻辑更自然合理。在实际编码中也是:
@RestController
@RequestMapping("/human-resources/employees")
public class EmployeesController {
@RequestMapping(method=RequestMethod.GET)
public List<Employee> getEmployees(
@RequestParam(name = "country", required = false) String country,
@RequestParam(name = "region") Optional<String> region
) {
//...
}
@RequestMapping(method = RequestMethod.POST)
public void addEmployee(@RequestBody Employee user) {
}
@RequestMapping(path = "/{empno}", method = RequestMethod.GET)
public Employee getEmployee(@PathVariable("empno") String empNo) {
//...
}
@RequestMapping(path = "/{empno}", method = RequestMethod.DELETE)
public void deleteEmployee(@PathVariable("empno") String empNo) {}
// ...
}
在理解原理和概念后,编码是很简单的,Spring Boot对开发Restful API提供了强大的支持,编写起来非常简单,下面我们看看简单的实现。
使用Spring Boot编写Restful API
配置数据源
开始前,我们先准备数据源, 数据库采用了H2的内存数据库,数据库案例用Oracle的scott案例数据库迁移过来。为了与之前的代码分开,我们把代码放在learning6这个包下面,因为我们要使用JDBC数据源,所以需要引入相关依赖:
...
<-- Spring Boot JDBC自动配置支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- H2数据库 JDBC驱动 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!-- 我们使用阿里的Druid连接池, spring-boot-starter-jdbc默认包含了HikariCP连接池, 使用HikariCP 可以不用额外引入-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
...
因为有jdbc-starter自动配置JDBC数据源, 我们只需要按照约定配置,就可以直接使用了。
## application.properties中配置如下内容:
spring.datasource.type = com.alibaba.druid.pool.DruidDataSource
spring.datasource.url = jdbc:h2:mem:hr;mode=MySQL;database_to_lower=true
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username = sa
spring.datasource.password = passw0rd
## Enable H2 Console
spring.h2.console.enabled=true
#注意, 不要有空格
spring.h2.console.path=/h2
spring.h2.console.settings.web-allow-others=true
#数据源初始化时会运行resources/db/schema.sql文件,对数据库的结构进行操作。
spring.sql.init.schema-locations=classpath:db/schema.sql
#数据源初始化时会运行resources/db/data.sql文件, 对数据库的数据操作。
spring.sql.init.data-locations=classpath:db/data.sql
我们在resources/db目录下创建数据库初始化脚本:
---> schema.sql
-- from oracle sample db: scott
CREATE TABLE EMP(
EMPNO NUMBER(4) NOT NULL,
ENAME VARCHAR2(10),
JOB VARCHAR(9),
MGR NUMBER(4),
HIREDATE DATE,
SAL NUMBER(7,2),
COMM NUMBER(7,2),
DEPTNO NUMBER(2)
);
CREATE TABLE DEPT(
DEPTNO NUMBER(2),
DNAME VARCHAR(14),
LOC VARCHAR(13)
);
---> data.sql
INSERT INTO EMP VALUES(7369, 'SMITH', 'CLERK', 7902, PARSEDATETIME('17-Dec-1980', 'dd-MMM-yyyy', 'en', 'GMT'), 800, NULL, 20);
INSERT INTO EMP VALUES(7499, 'ALLEN', 'SALESMAN', 7698,PARSEDATETIME('20-FEB-1981', 'dd-MMM-yyyy', 'en', 'GMT'), 1600, 300, 30);
INSERT INTO EMP VALUES(7521, 'WARD', 'SALESMAN', 7698,PARSEDATETIME('22-FEB-1981', 'dd-MMM-yyyy', 'en', 'GMT'), 1250, 500, 30);
INSERT INTO EMP VALUES(7566, 'JONES', 'MANAGER', 7839,PARSEDATETIME('2-APR-1981', 'dd-MMM-yyyy', 'en', 'GMT'), 2975, NULL, 20);
INSERT INTO EMP VALUES(7654, 'MARTIN', 'SALESMAN', 7698,PARSEDATETIME('28-SEP-1981', 'dd-MMM-yyyy', 'en', 'GMT'), 1250, 1400, 30);
INSERT INTO EMP VALUES(7698, 'BLAKE', 'MANAGER', 7839,PARSEDATETIME('1-MAY-1981', 'dd-MMM-yyyy', 'en', 'GMT'), 2850, NULL, 30);
INSERT INTO EMP VALUES(7782, 'CLARK', 'MANAGER', 7839,PARSEDATETIME('9-JUN-1981', 'dd-MMM-yyyy', 'en', 'GMT'), 2450, NULL, 10);
INSERT INTO EMP VALUES(7788, 'SCOTT', 'ANALYST', 7566,PARSEDATETIME('09-DEC-1982', 'dd-MMM-yyyy', 'en', 'GMT'), 3000, NULL, 20);
INSERT INTO EMP VALUES(7839, 'KING', 'PRESIDENT', NULL,PARSEDATETIME('17-NOV-1981', 'dd-MMM-yyyy', 'en', 'GMT'), 5000, NULL, 10);
INSERT INTO EMP VALUES(7844, 'TURNER', 'SALESMAN', 7698,PARSEDATETIME('8-SEP-1981', 'dd-MMM-yyyy', 'en', 'GMT'), 1500, 0, 30);
INSERT INTO EMP VALUES(7876, 'ADAMS', 'CLERK', 7788,PARSEDATETIME('12-JAN-1983', 'dd-MMM-yyyy', 'en', 'GMT'), 1100, NULL, 20);
INSERT INTO EMP VALUES(7900, 'JAMES', 'CLERK', 7698,PARSEDATETIME('3-DEC-1981', 'dd-MMM-yyyy', 'en', 'GMT'), 950, NULL, 30);
INSERT INTO EMP VALUES(7902, 'FORD', 'ANALYST', 7566,PARSEDATETIME('3-DEC-1981', 'dd-MMM-yyyy', 'en', 'GMT'), 3000, NULL, 20);
INSERT INTO EMP VALUES(7934, 'MILLER', 'CLERK', 7782,PARSEDATETIME('23-JAN-1982', 'dd-MMM-yyyy', 'en', 'GMT'), 1300, NULL, 10);
INSERT INTO DEPT VALUES (10, 'ACCOUNTING', 'NEW YORK');
INSERT INTO DEPT VALUES (20, 'RESEARCH', 'DALLAS');
INSERT INTO DEPT VALUES (30, 'SALES', 'CHICAGO');
INSERT INTO DEPT VALUES (40, 'OPERATIONS', 'BOSTON');
这样启动程序后我们可以通过:我们可以通过http://127.0.0.1:8080/h2访问H2 Console。
编写Spring控制器
package org.littlestar.learning6.controller;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import org.littlestar.learning6.entity.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
//import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController // => @Controller + @ResponseBody
@RequestMapping("/human-resources/employees")
public class EmployeesController {
@Autowired JdbcTemplate jdbcTemplate;
GET http://127.0.0.1:8080/human-resources/employees
@GetMapping // => @RequestMapping(method = RequestMethod.GET)
public List<Employee> getEmployees() {
final String sqlText = "select * from emp";
List<Employee> emps = jdbcTemplate.query(sqlText, getEmployeeMap());
return emps;
}
POST http://127.0.0.1:8080/human-resources/employees
@PostMapping // => @RequestMapping(method = RequestMethod.POST)
public void addEmployee(@RequestBody Employee emp) {
String sqlText = "insert into emp(empno,ename,job,mgr,hiredate,sal,comm,deptno) values (?,?,?,?,?,?,?,?)";
jdbcTemplate.update(sqlText, emp.getEmpno(), emp.getEname(), emp.getJob(), emp.getMgr(), emp.getHiredate(),
emp.getSal(), emp.getComm(), emp.getDeptno());
}
GET http://127.0.0.1:8080/human-resources/employees/7369
@GetMapping(path = "/{empno}") // => @RequestMapping(path = "/{empno}", method = RequestMethod.GET)
public Employee getEmployee(@PathVariable("empno") String empNo) {
final String sqlText = "select * from emp where empno=?";
List<Employee> emps = jdbcTemplate.query(sqlText, getEmployeeMap(), empNo);
return emps.isEmpty() ? null : emps.get(0);
}
PUT http://127.0.0.1:8080/human-resources/employees/8000
@PutMapping(path = "/{empno}") // => @RequestMapping(path = "/{empno}", method = RequestMethod.PUT)
public void updateEmployee(@PathVariable("empno") String empNo, @RequestBody Employee emp) {
String sqlText = "update emp set empno=? , ename=? , job=?, mgr=?, hiredate=?, sal=?, comm=?, deptno=? where empno=?";
jdbcTemplate.update(sqlText, emp.getEmpno(), emp.getEname(), emp.getJob(), emp.getMgr(), emp.getHiredate(),
emp.getSal(), emp.getComm(), emp.getDeptno(), empNo);
}
DELETE http://127.0.0.1:8080/human-resources/employees/8000
@DeleteMapping(path = "/{empno}") // => @RequestMapping(path = "/{empno}", method = RequestMethod.DELETE)
public void deleteEmployee(@PathVariable("empno") String empNo) {
String sqlText = "delete from emp where empno=?";
jdbcTemplate.update(sqlText, empNo);
}
private RowMapper<Employee> getEmployeeMap() {
RowMapper<Employee> rowMapper = new RowMapper<>() {
@Override
public Employee mapRow(ResultSet rs, int rowNum) throws SQLException {
Employee emp = new Employee(
rs.getInt("empno"),
rs.getString("ename"),
rs.getString("job"),
rs.getInt("mgr"),
rs.getDate("hiredate"),
rs.getDouble("sal"),
rs.getDouble("comm"),
rs.getInt("deptno")
);
return emp;
}
};
return rowMapper;
}
}
编写启动类
package org.littlestar.learning6;
import java.sql.SQLException;
import org.h2.tools.Server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class App6 {
public static void main(String[] args) {
SpringApplication.run(App6.class, args);
}
// 启动H2服务器, 以便通过Dbeaver这类数据库工具连接
// url = jdbc:h2:tcp://127.0.0.1:9092/mem:hr
// sa/passw0rd
@Bean(initMethod = "start", destroyMethod = "stop")
public Server h2Server() throws SQLException {
return Server.createTcpServer("-tcpPort", "9092", "-tcpAllowOthers");
}
}
测试Restful API
启动即可,测试Restful API通常可以Postman来进行:
- 查询7934员工:
- 创建一个员工(8000)
- 更新员工(8000)
- 删除员工(8001)