【文章内容输出来源:拉勾教育Java高薪训练营】
--- 所有脑图均本人制作,未经允许请勿滥用 ---
需要深刻把握各组件的职责和使用方式,多写多练
夫学须静也,才须学也,非学无以广才,非志无以成学
一、微服务架构
(结合前面的课程在此回顾)
- |【简单单体应用】
- |->
业务上涨-需要 集群部署、负载均衡、缓存服务器、文件服务器、数据库集群(读写分离) - |--> 【复杂单体应用】
- |--->
为了提高效率和解耦-主要是 将业务拆分,使之互不影响 - |----> 【垂直应用架构】
- |----->
接口协议不统一 + 服务监控困难-使用分布式RPC(Dubbo等) - |------> 【SOA-面向服务架构】
- |------->
服务抽取粒度较⼤ + 服务调用方 + 提供方耦合度较⾼-将应用拆分为微型服务 - |--------> 【微服务】
微服务关键词 & 优点
- 业务粒度微小
- 便于功能聚焦
- 每个微服务都可以被单独实施(Dev->Test->Deploy->Ops)便于敏捷开发
- 每个问服务之间 易重用 + 易组装
- 服务独立
- 不同的微服务可以使用不同的语言开发,松耦合
- 轻量级通信
- Restful
缺点
- 分布式管理难度加大、分布式跟踪链路复杂
相关概念
- 服务注册 服务提供者 将所提供服务的信息(服务器IP和端⼝、服务访问协议等)注册/登记到注册中心
- 服务发现 服务消费者 能够从注册中⼼获取到较为实时的服务列表,然后根究⼀定的策略选择⼀个服务访问
- 负载均衡 将请求压⼒分配到多个服务器,从而提高性能+可靠性
- 熔断(断路保护) 当下游服务因访问压力太大而
相应变慢/失败时,上游服务可暂时斩断下游的调用- 链路追踪 对⼀次请求涉及的很多个服务链路进行 日志记录、性能监控
- API网关 API请求调用 统⼀接⼊API网关层,由⽹关转发请求!
- 路由
- 安全防护(身份认证)
- 黑白名单(访问控制)
- 协议适配(通信协议适配 + 协议校验)
- 流量管控(限流)
- 长短链接支持
- 容错能力(负载均衡)
二、Spring Cloud综述
利用 Spring Boot 的开发便利性,巧妙地简化了分布式系统基础设施的开发
将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来
通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,
最终给开发者留出了⼀套简单易懂、易部署和易维护的分布式系统开发工具包
======> Spring Cluod 是一个 「规范」(一系列框架的有序集合)
Part 1 - 组件简述
- 第一代主要是 Netfix 进行封装
- 第二代主要是 Alibaba 进行封装
Nacos不仅可以作为注册中心,也可以作为配置中心
Part 2 - 体系结构
Part 3 - 对比 Spring Cloud 和Dubbo
- SCN(Spring Cloud)目前使用率较高,但是它基于HTTP,效率上没有Dubbo高
- Dubbo体系组件不全,无法提供
一站式解决方案——如 注册于发现依赖于Zookeeper等的实现 - => 目前Spring Cloud生态环境越来越好了
三、案例 ☆☆☆
Part 1 - 需求说明
使用拉勾提供的功能需求点:
招聘者(R) 与 应聘者(C)的 双向匹配:
1.给 R 开启一个定时任务,根据「用人标准」每日向 R 匹配推送一定数量的 C 到 R 的资源池中;
2.推送过程需要校验 ==> C 的简历状态(公开/隐藏),只推送公开简历;
A.【自动投递微服务】拥有 「自动投递」功能;
B.【简历微服务】拥有 「简历查询」功能;
在 A 调用 B 的时候,A-服务消费者 / B-服务提供者
Part 2 - 数据库准备
MySQL 8.0+
简历基本信息表 r_resume
【核心字段】
- userId 简历所属人
- isDefault 是否默认简历
- isOpenResume 简历开放状态
DROP TABLE IF EXISTS `r_resume`;
CREATE TABLE `r_resume` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`sex` varchar(10) DEFAULT NULL COMMENT '性别',
`birthday` varchar(30) DEFAULT NULL COMMENT '出生日期',
`work_year` varchar(100) DEFAULT NULL COMMENT '工作年限',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号码',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`status` varchar(80) DEFAULT NULL COMMENT '目前状态',
`resumeName` varchar(500) DEFAULT NULL COMMENT '简历名称',
`name` varchar(40) DEFAULT NULL,
`createTime` datetime DEFAULT NULL COMMENT '创建日期',
`headPic` varchar(100) DEFAULT NULL COMMENT '头像',
`isDel` int(2) DEFAULT NULL COMMENT '是否删除 默认值0-未删除 1-已删除',
`updateTime` datetime DEFAULT NULL COMMENT '简历更新时间',
`userId` int(11) DEFAULT NULL COMMENT '用户ID',
`isDefault` int(2) DEFAULT NULL COMMENT '是否为默认简历 0-默认 1-非默认',
`highestEducation` varchar(20) DEFAULT '' COMMENT '最高学历',
`deliverNearByConfirm` int(2) DEFAULT '0' COMMENT '投递附件简历确认 0-需要确认 1-不需要确认',
`refuseCount` int(11) NOT NULL DEFAULT '0' COMMENT '简历被拒绝次数',
`markCanInterviewCount` int(11) NOT NULL DEFAULT '0' COMMENT '被标记为可面试次数',
`haveNoticeInterCount` int(11) NOT NULL DEFAULT '0' COMMENT '已通知面试次数',
`oneWord` varchar(100) DEFAULT '' COMMENT '一句话介绍自己',
`liveCity` varchar(100) DEFAULT '' COMMENT '居住城市',
`resumeScore` int(3) DEFAULT NULL COMMENT '简历得分',
`userIdentity` int(1) DEFAULT '0' COMMENT '用户身份1-学生 2-工人',
`isOpenResume` int(1) DEFAULT '3' COMMENT '人才搜索-开放简历 0-关闭,1-打开,2-简历未达到投放标准被动关闭 3-从未设置过开放简历',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2195388 DEFAULT CHARSET=utf8;
具体数据参考笔记附录1
Part 3 - 工程环境准备 & 实现效果
基于 Spring Boot 构建工程环境,关系图如下:
转化为如下工程结构
「父模块」
- pom中引入所需包
<?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>
<groupId>com.archie</groupId>
<artifactId>resume-demo-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>service-common</module>
<module>service-resume</module>
<module>service-autodeliver</module>
</modules>
<!--父工程打包方式为pom-->
<packaging>pom</packaging>
<!--spring boot 父启动器依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<dependencies>
<!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--日志依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!--测试依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Actuator可以帮助你监控和管理Spring Boot应用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<!--编译插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
<!--打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
「子模块」
- common 模块的 pom 引入 JPA 坐标(简单项目免去代码的编写)
<!--Spring Data Jpa-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
- common 模块创建 POJO
public class Resume {
private Long id; // 主键
private String sex; // 性别
private String birthday; // 生日
private String work_year; // 工作年限
private String phone; // 手机号
private String email; // 邮箱
private String status; // 目前状态
private String resumeName; // 简历名称
private String name; // 姓名
private String createTime; // 创建时间
private String headPic; // 头像
private Integer isDel; //是否删除 默认值0-未删除 1-已删除
private String updateTime; // 简历更新时间
private Long userId; // 用户ID
private Integer isDefault; // 是否为默认简历 0-默认 1-非默认
private String highestEducation; // 最高学历
private Integer deliverNearByConfirm; // 投递附件简历确认 0-需要确认 1-不需要确认
private Integer refuseCount; // 简历被拒绝次数
private Integer markCanInterviewCount; //被标记为可面试次数
private Integer haveNoticeInterCount; //已通知面试次数
private String oneWord; // 一句话介绍自己
private String liveCity; // 居住城市
private Integer resumeScore; // 简历得分
private Integer userIdentity; // 用户身份1-学生 2-工人
private Integer isOpenResume; // 人才搜索-开放简历 0-关闭,1-打开,2-简历未达到投放标准被动关闭 3-从未设置过开放简历
// 省略 get/set
}
- resume 模块创建 Dao
public interface ResumeDao extends JpaRepository<Resume, Long> {
}
- resume 模块创建 Service
public interface ResumeService {
Resume findDefaultResumeByUserId(Long userId);
}
-----------------------------
@Service
public class ResumeServiceImpl implements ResumeService {
@Autowired
private ResumeDao resumeDao;
@Override
public Resume findDefaultResumeByUserId(Long userId) {
Resume resume = new Resume();
resume.setUserId(userId);
// 只查询 默认型 简历
resume.setIsDefault(1);
Example<? extends Resume> example = Example.of(resume);
return resumeDao.findOne(example).get();
}
}
- resume 模块创建 Controller
@RestController
@RequestMapping("/resume")
public class ResumeController {
@Autowired
private ResumeService resumeService;
@GetMapping("/openstate/{userId}")
public Integer findDefaultResumeState(@PathVariable Long userId) {
Resume resume = resumeService.findDefaultResumeByUserId(userId);
return resume.getIsOpenResume();
}
}
- resume 模块创建 Main 启动入口
@SpringBootApplication
@EntityScan("com.archie.dao")
public class ResumeApplication {
public static void main(String[] args) {
SpringApplication.run(ResumeApplication.class, args);
}
}
- resume 模块创建 yml 配置
server:
port: 8080
Spring:
application:
name: lagou-service-resume
# 数据库连接信息
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/springDB?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
database: MySQL
show-sql: true
hibernate:
naming:
#避免将驼峰命名转换为下划线命名
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
- autodeliver 模块 application.yml
server:
port: 8090
Spring:
application:
name: lagou-service-autodeliver
# 数据库连接信息
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/springdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
database: MySQL
show-sql: true
hibernate:
naming:
#避免将驼峰命名转换为下划线命名
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
- autodeliver 模块 Controller
@RestController
@RequestMapping("/autodeliver")
public class AutodeliverController {
@Autowired
private RestTemplate restTemplate;
/**
* http://localhost:8090/autodeliver/checkState/{userId}
* @param userId
* @return
*/
@GetMapping("/checkState/{userId}")
public Integer findResumeOpenState(@PathVariable Long userId) {
// 调用远程服务—> 简历微服务接口 RestTemplate ---same as--> JdbcTempate
Integer forObject = restTemplate.getForObject("http://localhost:8080/resume/openstate/" + userId, Integer.class);
return forObject;
}
}
- autodeliver 模块 Main 入口
@SpringBootApplication
public class AutodeliverApplication {
public static void main(String[] args) {
SpringApplication.run(AutodeliverApplication.class, args);
}
// 使用RestTemplate模板对象进行远程调用
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
- autodeliver 模块 远程调用 resume 服务
Part 4 - 问题分析
【分布式集群环境下,存在的问题】:
- 服务消费者中,我们把 url地址硬编码 到代码中,不方便后期维护
- 服务提供者只有⼀个服务,即便服务提供者形成集群,服务消费者还需要自己实现负载均衡
- 服务消费者中,不清楚服务提供者的状态
- 服务消费者调用服务提供者时候,如果出现故障能否及时发现不向用户抛出异常页面?
- RestTemplate 这种请求调用方式是否还有优化空间?能不能类似于Dubbo那样玩?
- 这么多的微服务统⼀认证如何实现?
- 配置文件每次都进行修改岂不是很繁琐...
接下来,我们就来逐个攻破!ㄟ( ▔, ▔ )ㄏ
四、初代 Spring Cloud 核心组件(I)
Part 1 - 服务中心简述
目前主流服务中心:
- Zookeeper
Apache Hadoop 的一个子项目,主要解决 分布式应用中的数据管理问题:(统一命名服务、状态同步服务、集群管理、分布式应用配置...)
Zookeeper 具有节点变更通知功能,可以及时通知到监听客户;
此外 Zookeeper可用性高,半数节点存活则整个集群可用;- Eureka
Netflix开源的子项目,由Pivatal集成到Spring Cloud中;是一个基于RestfulAPI的服务注册和发现 组件;- Consul
由HashiCorp基于Go语⾔开发的⽀持多数据中心分布式高可用的服务发布和注册服务软件
采用Raft算法保证服务的⼀致性,且⽀持健康检查- Nacos
Spring Cloud Alibaba的核心组件之一,约等于 注册中心+配置中心。
「Zookeeper」
具体参考之前的 拉勾教育学习-笔记分享の"解剖"Zookeeper #掘金文章#
「Eureka 简述」
- Eureka 两大组件
- ES-Eureka Server 提供服务发现功能
- EC-Eureka Client 一个Java客户端,简化了和Eureka Server交互
- 交互 微服务们启动时,EC 会向 ES 进行注册自身的信息,ES 存储它们。
- Eureka 交互流程
- us-east-1c、us-east-1d,us-east-1e代表不同的区, 也就是不同的机房
- 每⼀个Eureka Server都是⼀个集群
- Application Service 向 Eureka Server 注册服务
- Eureka Server 接收到注册事件 -> 在集群和分区中进行数据同步
- Application Service 从而具备 从 Eureka Server 调用服务的功能
- 微服务启动后,会周期性地向Eureka Server发送心跳(默认周期为30秒)以续约自己的信息
- Eureka Server 同时也是 Eureka Client,多个ES之间 通过复制的方式完成服务注册列表的同步
- Eureka Client 会缓存 Eureka Server 中的信息。
( 即使所有的Eureka Server节点都宕掉,服务消费者依然可以使⽤缓存中的信息找到服务提供者 )
Eureka通过
心跳检测、健康检查和客户端缓存等机制,提高系统的灵活性、可伸缩性和可用性
「Eureka 应用及高可用集群」☆☆
在之前demo基础上改动
- lagou-parent 的 pom 中引入 Spring Cloud 依赖
<!-- Spring Cloud 是一个综合项目,下面有很多子项目 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 当前工程 pom.xml 中引入依赖
<dependencies>
<!--Eureka server依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!--eureka server 需要引入Jaxb,开始-->
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.2.10-b140310.1920</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!--引入Jaxb,结束-->
</dependencies>
【注意】 jdk9+ 没有加载 jaxb 的 jar 包,需要再pom中手动引入,否则 EurekaServer 服务无法启动。
- application.yml
# Eureka 服务端口号
server:
port: 8761
spring:
application:
name: cloud-eureka-server # 应用名称,会在 Eureka 中作为服务名称
# Eureka 客户端配置 (与 Server 交互)
eureka:
instance:
hostname: localhost # 当前 Eureka Server 的主机名
client:
service-url: # 客户端与 EurekaServer 交互的地址,如果是集群,也需要写其它Server的地址 (当前交互地址就是自己-->8761)
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
register-with-eureka: false # 当前自己就是 Server,不需要注册自己
fetch-registry: false # 自己就是 Server,不需要从服务中心获取 其他服务
- SpringBoot启动类,使用
@EnableEurekaServer声明当前项目为 EurekaServer 服务
@SpringBootApplication
// 声明当前项目为Eureka服务
@EnableEurekaServer
public class LagouEurekaServerApp8761 {
public static void main(String[] args) {
SpringApplication.run(LagouEurekaServerApp8761.class,args);
}
}
- 启动 Main 入口,并访问 http://127.0.0.1:8761
--搭建 Eureka Server HA 高可用集群--
在生产环境中,我们会配置Eureka Server集群实现高可用
Eureka Server集群之中的节点通过点对点(P2P)通信的方式共享服务注册表
- 修改本机host属性
127.0.0.1 myEurekaA
127.0.0.1 myEurekaB
- 修改 lagou-cloud-eureka-server 工程中的两个 yml 配置文件
#eureka server服务端口
server:
port: 8761
spring:
application:
name: lagou-cloud-eureka-server # 应用名称,应用名称会在Eureka中作为服务名称
# eureka 客户端配置(和Server交互),Eureka Server 其实也是一个Client
eureka:
instance:
hostname: myEurekaA # 当前eureka实例的主机名
client:
service-url:
# 配置客户端所交互的Eureka Server的地址(Eureka Server集群中每一个Server其实相对于其它Server来说都是Client)
# 集群模式下,defaultZone应该指向其它Eureka Server,如果有更多其它Server实例,逗号拼接即可
defaultZone: http://myEurekaB:8762/eureka/
register-with-eureka: true # 集群模式下可以改成true
fetch-registry: true # 集群模式下可以改成true
dashboard:
enabled: true
#############
#eureka server服务端口
server:
port: 8762
spring:
application:
name: lagou-cloud-eureka-server # 应用名称,应用名称会在Eureka中作为服务名称
# eureka 客户端配置(和Server交互),Eureka Server 其实也是一个Client
eureka:
instance:
hostname: myEurekaB # 当前eureka实例的主机名
client:
service-url: # 配置客户端所交互的Eureka Server的地址
defaultZone: http://myEurekaA:8761/eureka/
register-with-eureka: true
fetch-registry: true
- 在 ⼀个实例中,把其他的实例作为了集群中的镜像节点
- register-with-eureka 和 fetch-registry 在单节点时设置为了 false, 因为只有⼀台 Eureka Server,并不需要自己注册自己,而现在有了集群,可以在集群的其他节点中注册本服务
- 分别启动两个 Main 入口
- 访问两个EurekaServer的管理台页面 http://myEurekaA:8761/ 和 http://myEurekaB:8762/ 会发现注册中心
LAGOU-CLOUD-EUREKA-SERVER已经有两个节点,并且 registered-replicas (相邻集群复制节点)中已经包含对方
--微服务提供者 —> 注册到Eureka Server集群--
- 注册简历微服务(8080、8081)
- 父工程中引入 spring-cloud-commons 依赖
<!-- spring cloud commons 依赖引入 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
- pom文件引入坐标,添加eureka client的相关坐标
<!-- Eureka Client 客户端依赖引入-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 配置 application.ylml
# 注册到 Eureka 服务中心
eureka:
client:
service-url:
# 注册到集群,用 [,] 分隔
defaultZone: http://myEurekaB:8762/eureka/,http://myEurekaB:8762/eureka/
instance:
# 服务实例中显示ip,而非主机名(为了兼容老版本)
prefer-ip-address: true
# 可以对实例名称自定义
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:@project.version@
- 启动类添加注解
// @EnableEurekaClient // 开启 Eureka Client (Eureka 独有)
@EnableDiscoveryClient // 开启注册中心客户端 (通用型注解,后期也可以使用到Nacos)---> 推荐
【注意】
从 Spring Cloud Edgware 版本开始,@EnableDiscoveryClient或@EnableEurekaClient可省略。只需加上相关依赖,并进⾏相应配置,即可将微服务注册到服务发现组件上 @EnableDiscoveryClient和@EnableEurekaClient⼆者的功能是⼀样的。但是如果选用的是eureka服务器,那么就推荐@EnableEurekaClient,如果是其他的注册中⼼,那么推荐使⽤@EnableDiscoveryClient,考虑到通用性,后期我们可以使⽤@EnableDiscoveryClient
- 启动 Main 入口
--微服务消费者 —> 注册到Eureka Server集群--
- pom 文件引入坐标,添加 eureka client 的相关坐标
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 配置 application.yml 文件
server:
port: 8090
Spring:
application:
name: lagou-service-autodeliver
# 数据库连接信息
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/springdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
database: MySQL
show-sql: true
hibernate:
naming:
#避免将驼峰命名转换为下划线命名
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
# 注册到 Eureka 服务中心
eureka:
client:
service-url:
# 注册到集群,用 [,] 分隔
defaultZone: http://myEurekaA:8761/eureka/,http://myEurekaB:8762/eureka/
instance:
prefer-ip-address: true # 服务实例中显示ip,而非主机名(为了兼容老版本)
# 可以对实例名称自定义
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:@project.version@
- 在启动类添加注解 @EnableDiscoveryClient,开启服务发现
@EnableDiscoveryClient
- 改造消费者的接口
/**
* Eureka 改造升级版
* http://localhost:8090/autodeliver/checkState2/{userId}
* @param userId
* @return
*/
@GetMapping("/checkState2/{userId}")
public String findResumeOpenState2(@PathVariable Long userId) {
// 获取 Eureka Server 中我们关注的那个 服务实例信息 以及 接口信息
/* 1. 从 Eureka Server 中获取 service-resume 的 实例信息 */
List<ServiceInstance> instances = discoveryClient.getInstances("lagou-service-resume");
/* 2. 如果有多个实例,选择一个使用(负载均衡的过程) */
ServiceInstance instance = instances.get(0);
/* 3. 从元数据信息获取 host port*/
String host = instance.getHost();
int port = instance.getPort();
String URL = "http://" + host + ":" + port + "/resume/openstate/" + userId;
// 调用远程服务—> 简历微服务接口 RestTemplate ---same as--> JdbcTempate
Integer forObject = restTemplate.getForObject( URL, Integer.class);
return forObject + "\t来自Eureka集群的服务获取";
}
- 结果
「Eureka 自定义元数据」
- Eureka 两种元数据:
- 标准元数据 主机名、IP地址、端口号等信息(服务之间调用)
- 自定义元数据 使用
eureka.instance.metadata-map配置,Key/Value 格式(远程客户端中访问)如:
instance:
prefer-ip-address: true
metadata-map:
node1: "hello"
node2: "world"
【Eureka的自我保护】:
- 默认情况下,如果 Eureka Server 在⼀定时间内(默认90秒)没有接收到某个微服务实例的心跳,Eureka Server将会移除该实例。但是当网络分区故障发生时,微服务与 Eureka Server 之间⽆法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,所以引入了自我保护机制
- 当处于⾃我保护模式时
- 不会剔除任何服务实例
- 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用
- 通过
eureka.server.enable-self-preservation配置可用关停自我保护,默认值是打开
「Eureka 核心源码」
待跟进
Part 2 - Ribbon 负载均衡
【通识】 负载均衡分类
- 服务端负载均衡:
请求到达它们时,根据指定算法将请求路由到目标服务器;- 客户端负载均衡:
客户端拥有一个地址Server地址列表,调用前通过指定算法选择一个服务器访问。
Ribbon 是 Netflix 发布的负载均衡器
通常配合 Eureka 使用 ==> 从 Eureka 中读取到服务信息,然后根据算法进行负载并调用服务
「Ribbon 应用」
不需要引入额外的Jar坐标,因为在服务消费者中我们引入过 eureka-client,它会引入Ribbon相关Jar
使用只需在 RestTemplate 上添加注解即可
@Bean
// Ribbon负载均衡
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
修改服务提供者api返回值,返回当前实例的端口号,便于观察负载情况
测试用例
结果
「Ribbon 负载均衡策略」
顶级接口为 com.netflix.loadbalancer.IRule
- RoundRobinRule:轮询
超过10次获取到的server都不可用,会返回⼀个空的server - RandomRule:随机
随机到的server为null或者不可用的话,会while不停的循环选取 - RetryRule:重试
⼀定时限内循环重试
默认继承 RoundRobinRule,也支持自定义注入。在每次选取之后,对选举的server进行判断,是否为 null,是否 alive,并且在500ms内会不停的选取判断 - BestAvailableRule:最小连接数
遍历serverList,选取出可用的且连接数最小的⼀个 server。如果选取到的 server 为null,那么会调用 RoundRobinRule 重新选取; - AvailabilityFilteringRule:可用过滤
扩展了轮询策略,会先通过默认的轮询选取⼀个 server,再去判断该 server 是否超时可用,当前连接数是否超限,都成功再返回。 - ZoneAvoidanceRule:区域权衡(默认)
扩展了轮询策略,继承了2个过滤器:
1. ZoneAvoidancePredicate
2. AvailabilityPredicate
除了过滤超时和链接数过多的 server,还会过滤掉不符合要求的 zone 区域里面的所有节点
修改策略方式:
# 针对的被调用方微服务名称,不加就是全局生效
lagou-service-resume:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载策略调整
「Ribbon 核心源码」
待跟进
Part 3 - Hystrix 熔断器
「雪崩效应」
- 扇入:代表着该微服务被调用的次数,扇入大 ==> 说明该模块 复用性好↑
- 扇出:该微服务调用其他微服务的个数,扇出大 ==> 说明 业务逻辑复杂↓
扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,这就是所谓的 “雪崩效应”
【解决方案】:
- 服务熔断
- 应对雪崩效应的⼀种微服务链路保护机制
当扇出链路的某个微服务不可用或者响应时间太长时,熔断该节点微服务的调用,进⾏服务的降级,快速返回错误的响应信息。当检测到该节点微服务调⽤响应正常后,恢复调用链路;
- (服务熔断重点在“断”,切断对下游服务的调用)
- (服务熔断和服务降级往往是⼀起使⽤的,Hystrix就是这样)
- 服务降级
- 是整体资源不够用时,先关闭非必要服务(被调用时,先返回一个预留的值:
兜底数据),链路恢复后再打开;- 服务限流
- 当有些服务不可以使用降级处理时,可以进行 “限流”:
- 限制总并发数(线程池)
- 限制瞬时并发数(nginx的瞬时并发连接数)
- 限制时间窗口内的平均速率
- 限制远程接口的调用速率
- 限制 MQ 的消费速率...
「Hystrix 简述」
Hystrix(豪猪----->刺)
由 Netflix 开源的⼀个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性
【特性】:
- 包裹请求 使用 HystrixCommand 包裹对依赖的调用逻辑
- 跳闸机制 当某服务的错误率超过⼀定的阈值时,Hystrix可以跳闸,停止请求该服务⼀段时间
- 资源隔离 Hystrix为每个依赖都维护了⼀个小型的线程池(舱壁模式)(或者信号量)。如果该线程池已满, 发往该依赖的请求就被立即拒绝,而不是排队等待,从而加速失败判定。
- 监控 可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时、以及被拒绝 的请求
- 回退机制 当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回 退逻辑由开发人员 自行提供,例如返回⼀个缺省值
- 自我修复 断路器打开⼀段时间后,会自动进入“ 半开 ”状态
「Hystrix 应用」
- 服务消费者工程(自动投递微服务)中引入 Hystrix 依赖坐标
<!--熔断器Hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
- 服务消费者工程(自动投递微服务)的启动类中添加熔断器开启注解
@EnableCircuitBreaker
// @EnableHytrix // 开启 Hystrix (Hystrix 独有)
@EnableCircuitBreaker // 开启断路器
目前 spring cloud 提供了非常好的针对 Eureka/Ribbon/Hystrix 的整个注解
@SpringBootApplication它 等价于@SpringBootApplication+@EnableDiscoveryClient+@EnableCircuitBreaker
- 对业务方法进行横切处理
- HystrixCommandProperties.java 中可以找到底层定义的所有属性
/**
* Hystrix 简易改造版
* http://localhost:8090/autodeliver/checkState4/{userId}
* @param userId
* @return
*/
@HystrixCommand(
// 熔断的属性
commandProperties = {
// 超过两秒未响应则熔断
@HystrixProperty(name="execution.isolation.thread.interruptOnTimeout", value="2000")
}
)
@GetMapping("/checkState4/{userId}")
public String findResumeOpenState4(@PathVariable Long userId) {
// 使用 Ribbon 不需要我们自己选择 服务实例 去访问
// 它会直接识别以下路径中的 lagou-service-resume 去找到对应的 host 和 port 添加进入
String URL = "http://lagou-service-resume/resume/openstate/" + userId;
// 调用远程服务—> 简历微服务接口 RestTemplate ---same as--> JdbcTempate
Integer forObject = restTemplate.getForObject( URL, Integer.class);
return forObject + "\t测试超时熔断";
}
/**
* Hystrix 服务降级改造版
* 很多非核心业务,不能直接抛出异常,需要返回一个兜底数据,使功能更友好
* http://localhost:8090/autodeliver/checkState5/{userId}
* @param userId
* @return
*/
@HystrixCommand(
// 熔断的属性
commandProperties = {
// 超过两秒未响应则熔断
@HystrixProperty(name="execution.isolation.thread.interruptOnTimeout", value="2000")
},
fallbackMethod = "myFallback"
)
@GetMapping("/checkState5/{userId}")
public String findResumeOpenState5(@PathVariable Long userId) {
// 使用 Ribbon 不需要我们自己选择 服务实例 去访问
// 它会直接识别以下路径中的 lagou-service-resume 去找到对应的 host 和 port 添加进入
String URL = "http://lagou-service-resume/resume/openstate/" + userId;
// 调用远程服务—> 简历微服务接口 RestTemplate ---same as--> JdbcTempate
Integer forObject = restTemplate.getForObject( URL, Integer.class);
return forObject + "\t测试超时熔断(含兜底数据)";
}
/**
* 回退方法,当超时2s时,返回兜底数据
* (形参/返回值 与 原方法 保持一致)
* @return
*/
public String myFallback(Long userId) {
return "非常抱歉!服务器响应异常,请再次刷新尝试";
}
- 实现效果
「Hystrix 舱壁模式(线程池隔离策略)」
如果不进行任何设置,所有熔断方法使用⼀个Hystrix线程池(10个线程),会导致问题!
这个问题并不是扇出链路微服务不可用导致的,而是我们的线程机制导致的:如果方法A的请求把10个线程都用了,方法2请求处理的时候压根都没法去访问B,因为没有线程可用,并不是B服务不可用。
为了避免问题服务请求过多导致正常服务无法访问,Hystrix 不是采用增加线程数,而是单独的为每⼀个控制方法创建⼀个线程池的方式,这种模式叫做 “舱壁模式",也是线程隔离的手段:
可自行使用 jps 命令 配合 jstack 命令查看机器线程状态
「Hystrix 工作流程与高级应用」
- Hystrix 跳闸 (时间窗内判定)
- Hystrix 自我修复(活动窗内判定)
Hystrix 流程定制及状态观察
我们可以基于 SpringBoot 的健康检查机制来观察跳闸状态
# springboot中暴露健康检查等断点接口
management:
endpoints:
web:
exposure:
include: "*"
# 暴露健康接口的细节
endpoint:
health:
show-details: always
同时定制化 Hystrix流程
@HystrixCommand(
// 熔断的属性
commandProperties = {
// 超过两秒未响应则熔断
@HystrixProperty(name="execution.isolation.thread.interruptOnTimeout", value="2000"),
/* hystrix高级配置,定制工作过程细节 */
/*
* 8秒钟内,请求次数达到2个,并且失败率在50%以上,就跳闸
* 跳闸后活动窗口设置为3s
*
* */
// 统计时间窗口定义
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds",value = "8000"),
// 统计时间窗口内的最小请求数
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "2"),
// 统计时间窗口内的错误数量百分比阈值
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "50"),
// 自我修复时的活动窗口长度
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "3000")
},
fallbackMethod = "myFallback"
)
@GetMapping("/checkState5/{userId}")
public String findResumeOpenState5(@PathVariable Long userId) {
// 使用 Ribbon 不需要我们自己选择 服务实例 去访问
// 它会直接识别以下路径中的 lagou-service-resume 去找到对应的 host 和 port 添加进入
String URL = "http://lagou-service-resume/resume/openstate/" + userId;
// 调用远程服务—> 简历微服务接口 RestTemplate ---same as--> JdbcTempate
Integer forObject = restTemplate.getForObject( URL, Integer.class);
return forObject + "\t测试超时熔断(含兜底数据)";
}
/**
* 回退方法,当超时2s时,返回兜底数据
* (形参/返回值 与 原方法 保持一致)
* @return
*/
public String myFallback(Long userId) {
return "非常抱歉!服务器响应异常,请再次刷新尝试";
}
「Hystrix Dashboard 断路监控仪表盘」
新建 Module cloud-hystrix-dashboard-9000
观察每个熔断器的细节
- 确保父工程之前 已引入 SpringBoot 的 actuator(健康监控)
<!-- Actuator可以帮助你监控和管理Spring Boot应用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 在 入口方法 加入
@EnableHystrixDashboard注解
@SpringBootApplication
@EnableHystrixDashboard // 使能 Hystrix 仪表盘
public class HystrixDashboardApplication9000 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication9000.class);
}
}
- 配置 新模块的 application.yml
server:
port: 9000
Spring:
application:
name: cloud-hystrix-dashboard
# 注册到 Eureka 服务中心
eureka:
client:
service-url:
# 注册到集群,用 [,] 分隔
defaultZone: http://myEurekaA:8761/eureka/,http://myEurekaB:8762/eureka/
instance:
prefer-ip-address: true # 服务实例中显示ip,而非主机名(为了兼容老版本)
# 可以对实例名称自定义
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
- 在消费者中 注册一个 Servlet
/**
* 在被监控的微服务中注册一个serlvet,后期我们就是通过访问这个servlet来获取该服务的Hystrix监控数据的
* 前提:被监控的微服务需要引入springboot的actuator功能
* @return
*/
@Bean
public ServletRegistrationBean getServlet(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/actuator/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
- 简单效果
- 友好展示仪表盘
http://localhost:9000/hystrix
「Hystrix Turbine 聚合监控」
新建 Module cloud-hystrix-turbine-9001
- 引入pom
<dependencies>
<!--hystrix turbine聚合监控-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>
<!--
引入eureka客户端的两个原因
1、微服务架构下的服务都尽量注册到服务中心去,便于统一管理
2、后续在当前turbine项目中我们需要配置turbine聚合的服务,比如,我们希望聚合
lagou-service-autodeliver这个服务的各个实例的hystrix数据流,那随后
我们就需要在application.yml文件中配置这个服务名,那么turbine获取服务下具体实例的数据流的
时候需要ip和端口等实例信息,那么怎么根据服务名称获取到这些信息呢?
当然可以从eureka服务注册中心获取
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
- application.yml
server:
port: 9001
Spring:
application:
name: cloud-hystrix-turbine
# 注册到 Eureka 服务中心
eureka:
client:
service-url:
# 注册到集群,用 [,] 分隔
defaultZone: http://myEurekaA:8761/eureka/,http://myEurekaB:8762/eureka/
instance:
prefer-ip-address: true # 服务实例中显示ip,而非主机名(为了兼容老版本)
# 可以对实例名称自定义
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
#turbine配置
turbine:
# appCofing配置需要聚合的服务名称,比如这里聚合自动投递微服务的hystrix监控数据
# 如果要聚合多个微服务的监控数据,那么可以使用英文逗号拼接,比如 a,b,c
appConfig: service-autodeliver
clusterNameExpression: "'default'" # 集群默认名称
- 入口创建
public static void main(String[] args) {
SpringApplication.run(HystrixTurbineApplication9001.class,args);
}
「Hystrix 核心源码」
待跟进
Part 4 - Feign 远程调用组件
「Feign 简述」
Feign 是 Netflix 开发的⼀个轻量级 RESTful 的 HTTP 服务客户端(用它来发起请求,远程调用的),是以Java接口注解的方式调用 Http请求,而不用像Java中通过封装 HTTP请求报文 的方式直接调用
Feign被广泛应用在 Spring Cloud 的解决方案中。
本质:封装了Http调用流程,更符合面向接⼝化的编程习惯,类似于Dubbo的服务调用
「Feign 应用」
- 引入 Feign依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 启动类使用注解
@EnableFeignClients添加支持
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // 开启Feign 客户端功能
public class AutodeliverApplication8091 {
public static void main(String[] args) {
SpringApplication.run(AutodeliverApplication8091.class, args);
}
}
注意:此时去掉Hystrix熔断的支持注解 @EnableCircuitBreaker 即可包括引入的依赖,因为Feign会自动引入
- 创建 Feign 接口
// 原来:http://service-resume/resume/openstate/ + userId;
// @FeignClient 表明当前类是一个Feign客户端,
// value(Alias->name) 指定该客户端要请求的服务名称(登记到注册中心上的服务提供者的服务名称)
@FeignClient(value = "lagou-service-resume",fallback = ResumeFallback.class,path = "/resume")
public interface ResumeServiceFeignClient {
// Feign要做的事情就是,拼装url发起请求
// 我们调用该方法就是调用本地接口方法,那么实际上做的是远程请求
// value必须设置,否则会抛出异常
@GetMapping("/openstate/{userId}")
public Integer findDefaultResumeState(@PathVariable("userId") Long userId);
}
【注意】:
- @FeignClient注解的name属性⽤于指定要调⽤的服务提供者名称,和服务提供者yml⽂件中
spring.application.name保持⼀致- 接⼝中的接⼝方法,就好比是远程服务提供者Controller中的Hander⽅法(只不过如同本地调⽤了),那么在进行参数绑定的时,可以使⽤
@PathVariable、@RequestParam、@RequestHeader等,这也是 OpenFeign 对 SpringMVC 注解的⽀持,但是需要注意value必须设置,否则会抛出异常
- 启动入口实现远程调用
「Feign 对 负载均衡和熔断的支持」
内部整合了Ribbon,直接在消费者中配置即可
#针对的被调用方微服务名称,不加就是全局生效
lagou-service-resume:
ribbon:
#请求连接超时时间
ConnectTimeout: 2000
#请求处理超时时间
##########################################Feign超时时长设置
ReadTimeout: 15000
#对所有操作都进行重试
OkToRetryOnAllOperations: true
####根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置),
####如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置),
####如果依然不行,返回失败信息。
MaxAutoRetries: 0 #对当前选中实例重试次数,不包括第一次调用
MaxAutoRetriesNextServer: 0 #切换实例的重试次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整
以上配置使得 如果某一个请求中断了,只要15s内恢复,都可以正常访问
# Feign 对 Hystrix 的支持
feign:
hystrix:
enabled: true
# 原生 Hystrix 的超时配置
hystrix:
command:
default:
execution:
isolation:
thread:
##########################################Hystrix的超时时长设置
timeoutInMilliseconds: 15000
降级回退逻辑需要定义一个类,实现FeignClient接口,实现接口中的方法
@Component // 别忘了这个注解,还应该被扫描到
public class ResumeFallback implements ResumeServiceFeignClient {
@Override
public Integer findDefaultResumeState(Long userId) {
return -9;
}
}
将该类表明到 @FeignClient 的 fallback 属性中
@FeignClient(value = "lagou-service-resume",fallback = ResumeFallback.class,path = "/resume")
public interface ResumeServiceFeignClient {
...
}
「Feign 对请求压缩和响应压缩的支持」
Feign 支持对请求和响应进行 GZIP压缩,以减少通信过程中的性能损耗
feign:
compression:
request:
enabled: true # 开启请求压缩
mime-types: text/html,application/xml,application/json # 设置压缩的数据类型,此处也是默认值
min-request-size: 2048 # 设置触发压缩的⼤小下限,此处也是默认值
response:
enabled: true # 开启响应压缩
「Feign 日志级别配置」
Feign是http请求客户端,类似于咱们的浏览器,它在请求和接收响应的时候,可以打印出比较详细的⼀些日志信息(响应头,状态码等等)
如果我们想看到Feign请求时的日志,我们可以进行配置,默认情况下Feign的日志没有开启。
- 开启Feign日志功能及级别
// Feign的日志级别(Feign请求过程信息)
// NONE:默认的,不显示任何⽇志----性能最好
// BASIC:仅记录请求⽅法、URL、响应状态码以及执⾏时间----⽣产问题追踪
// HEADERS:在BASIC级别的基础上,记录请求和响应的header
// FULL:记录请求和响应的header、body和元数据----适⽤于开发及测试环境定位问
题
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLevel() {
return Logger.Level.FULL;
}
}
- 配置log日志级别为debug
logging:
level:
# 以下配置 :Feign 日志只会对日志级别为debug的做出响应
com.lagou.edu.controller.service.ResumeServiceFeignClient: debug
「Feign 核心源码」
待跟进
【附录1】-简历数据
BEGIN;
INSERT INTO `r_resume` VALUES (2195320, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 13:40:14', 'images/myresume/default_headpic.png', 0, '2015-04-24 13:40:14', 1545132, 1, '本科', 0, 0, 0, 0, '', '广州', 15, 0, 3);
INSERT INTO `r_resume` VALUES (2195321, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 14:17:54', 'images/myresume/default_headpic.png', 0, '2015-04-24 14:20:35', 1545133, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195322, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 14:42:45', 'images/myresume/default_headpic.png', 0, '2015-04-24 14:43:34', 1545135, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195323, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 14:48:19', 'images/myresume/default_headpic.png', 0, '2015-04-24 14:50:34', 1545136, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195331, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 18:43:35', 'images/myresume/default_headpic.png', 0, '2015-04-24 18:44:08', 1545145, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195333, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 19:01:13', 'images/myresume/default_headpic.png', 0, '2015-04-24 19:01:14', 1545148, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195336, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-27 14:13:02', 'images/myresume/default_headpic.png', 0, '2015-04-27 14:13:02', 1545155, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195337, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-27 14:36:55', 'images/myresume/default_headpic.png', 0, '2015-04-27 14:36:55', 1545158, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195369, '女', '1990', '10年以上', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿', 'wps', '2015-05-15 18:08:19', 'images/myresume/default_headpic.png', 0, '2015-05-15 18:08:19', 1545346, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195374, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 17:53:37', 'images/myresume/default_headpic.png', 0, '2015-06-04 17:53:39', 1545523, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195375, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:11:06', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:11:07', 1545524, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195376, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:12:19', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:12:19', 1545525, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195377, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:13:28', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:13:28', 1545526, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195378, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:15:16', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:15:16', 1545527, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195379, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:23:06', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:23:06', 1545528, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195380, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:23:38', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:23:39', 1545529, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195381, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:27:33', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:27:33', 1545530, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195382, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:31:36', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:31:39', 1545531, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195383, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:36:48', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:36:48', 1545532, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195384, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 19:15:15', 'images/myresume/default_headpic.png', 0, '2015-06-04 19:15:16', 1545533, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195385, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 19:28:53', 'images/myresume/default_headpic.png', 0, '2015-06-04 19:28:53', 1545534, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195386, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 19:46:42', 'images/myresume/default_headpic.png', 0, '2015-06-04 19:46:45', 1545535, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195387, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 19:48:16', 'images/myresume/default_headpic.png', 0, '2015-06-04 19:48:16', 1545536, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;