hystrix弹性客户端

This commit is contained in:
fxb 2018-12-17 21:45:04 +08:00
parent c8bbd1cfda
commit 132abf6edc
10 changed files with 686 additions and 366 deletions

View File

@ -1,315 +1,315 @@
[id]:2018-09-10 [id]:2018-09-10
[type]:javaee [type]:javaee
[tag]:java,spring,springboot,mybatis,读写分离 [tag]:java,spring,springboot,mybatis,读写分离
  近日工作任务较轻,有空学习学习技术,遂来研究如果实现读写分离。这里用博客记录下过程,一方面可备日后查看,同时也能分享给大家(网上的资料真的大都是抄来抄去,,还不带格式的,看的真心难受)。   近日工作任务较轻,有空学习学习技术,遂来研究如果实现读写分离。这里用博客记录下过程,一方面可备日后查看,同时也能分享给大家(网上的资料真的大都是抄来抄去,,还不带格式的,看的真心难受)。
[完整代码](https://github.com/FleyX/demo-project/tree/master/dxfl) [完整代码](https://github.com/FleyX/demo-project/tree/master/dxfl)
## 1、背景 ## 1、背景
  一个项目中数据库最基础同时也是最主流的是单机数据库,读写都在一个库中。当用户逐渐增多,单机数据库无法满足性能要求时,就会进行读写分离改造(适用于读多写少),写操作一个库,读操作多个库,通常会做一个数据库集群,开启主从备份,一主多从,以提高读取性能。当用户更多读写分离也无法满足时,就需要分布式数据库了(可能以后会学习怎么弄)。   一个项目中数据库最基础同时也是最主流的是单机数据库,读写都在一个库中。当用户逐渐增多,单机数据库无法满足性能要求时,就会进行读写分离改造(适用于读多写少),写操作一个库,读操作多个库,通常会做一个数据库集群,开启主从备份,一主多从,以提高读取性能。当用户更多读写分离也无法满足时,就需要分布式数据库了(可能以后会学习怎么弄)。
  正常情况下读写分离的实现首先要做一个一主多从的数据库集群同时还需要进行数据同步。这一篇记录如何用mysql搭建一个一主多次的配置下一篇记录代码层面如何实现读写分离。   正常情况下读写分离的实现首先要做一个一主多从的数据库集群同时还需要进行数据同步。这一篇记录如何用mysql搭建一个一主多次的配置下一篇记录代码层面如何实现读写分离。
## 2、搭建一主多从数据库集群 ## 2、搭建一主多从数据库集群
  主从备份需要多台虚拟机我是用wmware完整克隆多个实例注意直接克隆的虚拟机会导致每个数据库的uuid相同需要修改为不同的uuid。修改方法参考这个[点击跳转](https://blog.csdn.net/pratise/article/details/80413198)。   主从备份需要多台虚拟机我是用wmware完整克隆多个实例注意直接克隆的虚拟机会导致每个数据库的uuid相同需要修改为不同的uuid。修改方法参考这个[点击跳转](https://blog.csdn.net/pratise/article/details/80413198)。
- 主库配置 - 主库配置
主数据库master中新建一个用户用于从数据库slave读取主数据库二进制日志sql语句如下 主数据库master中新建一个用户用于从数据库slave读取主数据库二进制日志sql语句如下
```sql ```sql
mysql> CREATE USER 'repl'@'%' IDENTIFIED BY '123456';#创建用户 mysql> CREATE USER 'repl'@'%' IDENTIFIED BY '123456';#创建用户
mysql> GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';#分配权限 mysql> GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';#分配权限
mysql>flush privileges; #刷新权限 mysql>flush privileges; #刷新权限
``` ```
同时修改mysql配置文件开启二进制日志新增部分如下 同时修改mysql配置文件开启二进制日志新增部分如下
```sql ```sql
[mysqld] [mysqld]
server-id=1 server-id=1
log-bin=master-bin log-bin=master-bin
log-bin-index=master-bin.index log-bin-index=master-bin.index
``` ```
然后重启数据库,使用`show master status;`语句查看主库状态,如下所示: 然后重启数据库,使用`show master status;`语句查看主库状态,如下所示:
![主库状态](./picFolder/pic1.png) ![主库状态](./picFolder/pic1.png)
- 从库配置 - 从库配置
同样先新增几行配置: 同样先新增几行配置:
```sql ```sql
[mysqld] [mysqld]
server-id=2 server-id=2
relay-log-index=slave-relay-bin.index relay-log-index=slave-relay-bin.index
relay-log=slave-relay-bin relay-log=slave-relay-bin
``` ```
然后重启数据库,使用如下语句连接主库: 然后重启数据库,使用如下语句连接主库:
```sql ```sql
CHANGE MASTER TO CHANGE MASTER TO
MASTER_HOST='192.168.226.5', MASTER_HOST='192.168.226.5',
MASTER_USER='root', MASTER_USER='root',
MASTER_PASSWORD='123456', MASTER_PASSWORD='123456',
MASTER_LOG_FILE='master-bin.000003', MASTER_LOG_FILE='master-bin.000003',
MASTER_LOG_POS=154; MASTER_LOG_POS=154;
``` ```
接着运行`start slave;`开启备份,正常情况如下图所示Slave_IO_Running和Slave_SQL_Running都为yes。 接着运行`start slave;`开启备份,正常情况如下图所示Slave_IO_Running和Slave_SQL_Running都为yes。
![1536223020742](./picFolder/pic2.png) ![1536223020742](./picFolder/pic2.png)
可以用这个步骤开启多个从库。 可以用这个步骤开启多个从库。
  默认情况下备份是主库的全部操作都会备份到从库,实际可能需要忽略某些库,可以在主库中增加如下配置:   默认情况下备份是主库的全部操作都会备份到从库,实际可能需要忽略某些库,可以在主库中增加如下配置:
```sql ```sql
# 不同步哪些数据库 # 不同步哪些数据库
binlog-ignore-db = mysql binlog-ignore-db = mysql
binlog-ignore-db = test binlog-ignore-db = test
binlog-ignore-db = information_schema binlog-ignore-db = information_schema
# 只同步哪些数据库,除此之外,其他不同步 # 只同步哪些数据库,除此之外,其他不同步
binlog-do-db = game binlog-do-db = game
``` ```
## 3、代码层面进行读写分离 ## 3、代码层面进行读写分离
  代码环境是springboot+mybatis+druib连接池。想要读写分离就需要配置多个数据源在进行写操作是选择写的数据源读操作时选择读的数据源。其中有两个关键点   代码环境是springboot+mybatis+druib连接池。想要读写分离就需要配置多个数据源在进行写操作是选择写的数据源读操作时选择读的数据源。其中有两个关键点
- 如何切换数据源 - 如何切换数据源
- 如何根据不同的方法选择正确的数据源 - 如何根据不同的方法选择正确的数据源
### 1)、如何切换数据源 ### 1)、如何切换数据源
  通常用springboot时都是使用它的默认配置只需要在配置文件中定义好连接属性就行了但是现在我们需要自己来配置了spring是支持多数据源的多个datasource放在一个HashMap`TargetDataSource`中,通过`dertermineCurrentLookupKey`获取key来觉定要使用哪个数据源。因此我们的目标就很明确了建立多个datasource放到TargetDataSource中同时重写dertermineCurrentLookupKey方法来决定使用哪个key。   通常用springboot时都是使用它的默认配置只需要在配置文件中定义好连接属性就行了但是现在我们需要自己来配置了spring是支持多数据源的多个datasource放在一个HashMap`TargetDataSource`中,通过`dertermineCurrentLookupKey`获取key来觉定要使用哪个数据源。因此我们的目标就很明确了建立多个datasource放到TargetDataSource中同时重写dertermineCurrentLookupKey方法来决定使用哪个key。
### 2)、如何选择数据源 ### 2)、如何选择数据源
  事务一般是注解在Service层的因此在开始这个service方法调用时要确定数据源有什么通用方法能够在开始执行一个方法前做操作呢相信你已经想到了那就是**切面 **。怎么切有两种办法:   事务一般是注解在Service层的因此在开始这个service方法调用时要确定数据源有什么通用方法能够在开始执行一个方法前做操作呢相信你已经想到了那就是**切面 **。怎么切有两种办法:
- 注解式,定义一个只读注解,被该数据标注的方法使用读库 - 注解式,定义一个只读注解,被该数据标注的方法使用读库
- 方法名根据方法名写切点比如getXXX用读库setXXX用写库 - 方法名根据方法名写切点比如getXXX用读库setXXX用写库
### 3)、代码编写 ### 3)、代码编写
#### a、编写配置文件配置两个数据源信息 #### a、编写配置文件配置两个数据源信息
  只有必填信息,其他都有默认设置   只有必填信息,其他都有默认设置
```yml ```yml
mysql: mysql:
datasource: datasource:
#读库数目 #读库数目
num: 1 num: 1
type-aliases-package: com.example.dxfl.dao type-aliases-package: com.example.dxfl.dao
mapper-locations: classpath:/mapper/*.xml mapper-locations: classpath:/mapper/*.xml
config-location: classpath:/mybatis-config.xml config-location: classpath:/mybatis-config.xml
write: write:
url: jdbc:mysql://192.168.226.5:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true url: jdbc:mysql://192.168.226.5:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root username: root
password: 123456 password: 123456
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
read: read:
url: jdbc:mysql://192.168.226.6:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true url: jdbc:mysql://192.168.226.6:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root username: root
password: 123456 password: 123456
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
``` ```
#### b、编写DbContextHolder类 #### b、编写DbContextHolder类
  这个类用来设置数据库类别其中有一个ThreadLocal用来保存每个线程的是使用读库还是写库。代码如下   这个类用来设置数据库类别其中有一个ThreadLocal用来保存每个线程的是使用读库还是写库。代码如下
```java ```java
/** /**
* Description 这里切换读/写模式 * Description 这里切换读/写模式
* 原理是利用ThreadLocal保存当前线程是否处于读模式通过开始READ_ONLY注解在开始操作前设置模式为读模式 * 原理是利用ThreadLocal保存当前线程是否处于读模式通过开始READ_ONLY注解在开始操作前设置模式为读模式
* 操作结束后清除该数据,避免内存泄漏,同时也为了后续在该线程进行写操作时任然为读模式 * 操作结束后清除该数据,避免内存泄漏,同时也为了后续在该线程进行写操作时任然为读模式
* @author fxb * @author fxb
* @date 2018-08-31 * @date 2018-08-31
*/ */
public class DbContextHolder { public class DbContextHolder {
private static Logger log = LoggerFactory.getLogger(DbContextHolder.class); private static Logger log = LoggerFactory.getLogger(DbContextHolder.class);
public static final String WRITE = "write"; public static final String WRITE = "write";
public static final String READ = "read"; public static final String READ = "read";
private static ThreadLocal<String> contextHolder= new ThreadLocal<>(); private static ThreadLocal<String> contextHolder= new ThreadLocal<>();
public static void setDbType(String dbType) { public static void setDbType(String dbType) {
if (dbType == null) { if (dbType == null) {
log.error("dbType为空"); log.error("dbType为空");
throw new NullPointerException(); throw new NullPointerException();
} }
log.info("设置dbType为{}",dbType); log.info("设置dbType为{}",dbType);
contextHolder.set(dbType); contextHolder.set(dbType);
} }
public static String getDbType() { public static String getDbType() {
return contextHolder.get() == null ? WRITE : contextHolder.get(); return contextHolder.get() == null ? WRITE : contextHolder.get();
} }
public static void clearDbType() { public static void clearDbType() {
contextHolder.remove(); contextHolder.remove();
} }
} }
``` ```
#### c、重写determineCurrentLookupKey方法 #### c、重写determineCurrentLookupKey方法
&emsp;&emsp;spring在开始进行数据库操作时会通过这个方法来决定使用哪个数据库因此我们在这里调用上面DbContextHolder类的`getDbType()`方法获取当前操作类别,同时可进行读库的负载均衡,代码如下: &emsp;&emsp;spring在开始进行数据库操作时会通过这个方法来决定使用哪个数据库因此我们在这里调用上面DbContextHolder类的`getDbType()`方法获取当前操作类别,同时可进行读库的负载均衡,代码如下:
```java ```java
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource { public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {
@Value("${mysql.datasource.num}") @Value("${mysql.datasource.num}")
private int num; private int num;
private final Logger log = LoggerFactory.getLogger(this.getClass()); private final Logger log = LoggerFactory.getLogger(this.getClass());
@Override @Override
protected Object determineCurrentLookupKey() { protected Object determineCurrentLookupKey() {
String typeKey = DbContextHolder.getDbType(); String typeKey = DbContextHolder.getDbType();
if (typeKey == DbContextHolder.WRITE) { if (typeKey == DbContextHolder.WRITE) {
log.info("使用了写库"); log.info("使用了写库");
return typeKey; return typeKey;
} }
//使用随机数决定使用哪个读库 //使用随机数决定使用哪个读库
int sum = NumberUtil.getRandom(1, num); int sum = NumberUtil.getRandom(1, num);
log.info("使用了读库{}", sum); log.info("使用了读库{}", sum);
return DbContextHolder.READ + sum; return DbContextHolder.READ + sum;
} }
} }
``` ```
#### d、编写配置类 #### d、编写配置类
&emsp;&emsp;由于要进行读写分离不能再用springboot的默认配置我们需要手动来进行配置。首先生成数据源使用@ConfigurProperties自动生成数据源 &emsp;&emsp;由于要进行读写分离不能再用springboot的默认配置我们需要手动来进行配置。首先生成数据源使用@ConfigurProperties自动生成数据源
```java ```java
/** /**
* 写数据源 * 写数据源
* *
* @Primary 标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean 优先被考虑。 * @Primary 标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean 优先被考虑。
* 多数据源配置的时候注意,必须要有一个主数据源,用 @Primary 标志该 Bean * 多数据源配置的时候注意,必须要有一个主数据源,用 @Primary 标志该 Bean
*/ */
@Primary @Primary
@Bean @Bean
@ConfigurationProperties(prefix = "mysql.datasource.write") @ConfigurationProperties(prefix = "mysql.datasource.write")
public DataSource writeDataSource() { public DataSource writeDataSource() {
return new DruidDataSource(); return new DruidDataSource();
} }
``` ```
读数据源类似注意有多少个读库就要设置多少个读数据源Bean名为read+序号。 读数据源类似注意有多少个读库就要设置多少个读数据源Bean名为read+序号。
&emsp;&emsp;然后设置数据源使用的是我们之前写的MyAbstractRoutingDataSource类 &emsp;&emsp;然后设置数据源使用的是我们之前写的MyAbstractRoutingDataSource类
```java ```java
/** /**
* 设置数据源路由通过该类中的determineCurrentLookupKey决定使用哪个数据源 * 设置数据源路由通过该类中的determineCurrentLookupKey决定使用哪个数据源
*/ */
@Bean @Bean
public AbstractRoutingDataSource routingDataSource() { public AbstractRoutingDataSource routingDataSource() {
MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(); MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>(2); Map<Object, Object> targetDataSources = new HashMap<>(2);
targetDataSources.put(DbContextHolder.WRITE, writeDataSource()); targetDataSources.put(DbContextHolder.WRITE, writeDataSource());
targetDataSources.put(DbContextHolder.READ+"1", read1()); targetDataSources.put(DbContextHolder.READ+"1", read1());
proxy.setDefaultTargetDataSource(writeDataSource()); proxy.setDefaultTargetDataSource(writeDataSource());
proxy.setTargetDataSources(targetDataSources); proxy.setTargetDataSources(targetDataSources);
return proxy; return proxy;
} }
``` ```
&emsp;&emsp;接着需要设置sqlSessionFactory &emsp;&emsp;接着需要设置sqlSessionFactory
```java ```java
/** /**
* 多数据源需要自己设置sqlSessionFactory * 多数据源需要自己设置sqlSessionFactory
*/ */
@Bean @Bean
public SqlSessionFactory sqlSessionFactory() throws Exception { public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(routingDataSource()); bean.setDataSource(routingDataSource());
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// 实体类对应的位置 // 实体类对应的位置
bean.setTypeAliasesPackage(typeAliasesPackage); bean.setTypeAliasesPackage(typeAliasesPackage);
// mybatis的XML的配置 // mybatis的XML的配置
bean.setMapperLocations(resolver.getResources(mapperLocation)); bean.setMapperLocations(resolver.getResources(mapperLocation));
bean.setConfigLocation(resolver.getResource(configLocation)); bean.setConfigLocation(resolver.getResource(configLocation));
return bean.getObject(); return bean.getObject();
} }
``` ```
&emsp;&emsp;最后还得配置下事务,否则事务不生效 &emsp;&emsp;最后还得配置下事务,否则事务不生效
```java ```java
/** /**
* 设置事务,事务需要知道当前使用的是哪个数据源才能进行事务处理 * 设置事务,事务需要知道当前使用的是哪个数据源才能进行事务处理
*/ */
@Bean @Bean
public DataSourceTransactionManager dataSourceTransactionManager() { public DataSourceTransactionManager dataSourceTransactionManager() {
return new DataSourceTransactionManager(routingDataSource()); return new DataSourceTransactionManager(routingDataSource());
} }
``` ```
### 4)、选择数据源 ### 4)、选择数据源
&emsp;&emsp;多数据源配置好了,但是代码层面如何选择选择数据源呢?这里介绍两种办法: &emsp;&emsp;多数据源配置好了,但是代码层面如何选择选择数据源呢?这里介绍两种办法:
#### a、注解式 #### a、注解式
&emsp;&emsp;首先定义一个只读注解被这个注解方法使用读库其他使用写库如果项目是中途改造成读写分离可使用这个方法无需修改业务代码只要在只读的service方法上加一个注解即可。 &emsp;&emsp;首先定义一个只读注解被这个注解方法使用读库其他使用写库如果项目是中途改造成读写分离可使用这个方法无需修改业务代码只要在只读的service方法上加一个注解即可。
```java ```java
@Target({ElementType.METHOD,ElementType.TYPE}) @Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly { public @interface ReadOnly {
} }
``` ```
&emsp;&emsp;然后写一个切面来切换数据使用哪种数据源重写getOrder保证本切面优先级高于事务切面优先级在启动类加上`@EnableTransactionManagement(order = 10) `,为了代码如下: &emsp;&emsp;然后写一个切面来切换数据使用哪种数据源重写getOrder保证本切面优先级高于事务切面优先级在启动类加上`@EnableTransactionManagement(order = 10) `,为了代码如下:
```java ```java
@Aspect @Aspect
@Component @Component
public class ReadOnlyInterceptor implements Ordered { public class ReadOnlyInterceptor implements Ordered {
private static final Logger log= LoggerFactory.getLogger(ReadOnlyInterceptor.class); private static final Logger log= LoggerFactory.getLogger(ReadOnlyInterceptor.class);
@Around("@annotation(readOnly)") @Around("@annotation(readOnly)")
public Object setRead(ProceedingJoinPoint joinPoint,ReadOnly readOnly) throws Throwable{ public Object setRead(ProceedingJoinPoint joinPoint,ReadOnly readOnly) throws Throwable{
try{ try{
DbContextHolder.setDbType(DbContextHolder.READ); DbContextHolder.setDbType(DbContextHolder.READ);
return joinPoint.proceed(); return joinPoint.proceed();
}finally { }finally {
//清楚DbType一方面为了避免内存泄漏更重要的是避免对后续在本线程上执行的操作产生影响 //清楚DbType一方面为了避免内存泄漏更重要的是避免对后续在本线程上执行的操作产生影响
DbContextHolder.clearDbType(); DbContextHolder.clearDbType();
log.info("清除threadLocal"); log.info("清除threadLocal");
} }
} }
@Override @Override
public int getOrder() { public int getOrder() {
return 0; return 0;
} }
} }
``` ```
#### b、方法名式 #### b、方法名式
&emsp;&emsp;这种方法不许要注解,但是需要事务名称按一定规则编写,然后通过切面来设置数据库类别,比如`setXXX`设置为写、`getXXX`设置为读,代码我就不写了,应该都知道怎么写。 &emsp;&emsp;这种方法不许要注解,但是需要事务名称按一定规则编写,然后通过切面来设置数据库类别,比如`setXXX`设置为写、`getXXX`设置为读,代码我就不写了,应该都知道怎么写。
## 4、测试 ## 4、测试
&emsp;&emsp;编写好代码来试试结果如何,下面是运行截图: &emsp;&emsp;编写好代码来试试结果如何,下面是运行截图:
![1536312274474](./picFolder/pic3.png) ![1536312274474](./picFolder/pic3.png)
&emsp;&emsp;断断续续写了好几天终于是写完了如果有帮助到你欢迎star哦这里是完整代码地址[点击跳转](https://github.com/FleyX/demo-project/tree/master/dxfl) &emsp;&emsp;断断续续写了好几天终于是写完了如果有帮助到你欢迎star哦这里是完整代码地址[点击跳转](https://github.com/FleyX/demo-project/tree/master/dxfl)

View File

@ -0,0 +1,204 @@
---
id="2018-11-28-15-57-00"
title="springCloud之Netflix Hystrix弹性客户端"
headWord="在任何分布式架构中,都需要找到机器所在的物理地址,这个概念自分布式计算开始就已经存在,并且被正式称为服务发现,本篇是对服务发现的一个学习总结"
tags=["spring-boot", "spring-cloud","netflix-hystrix"]
category="java"
serie="springCloud实战"
---
# 一、为什么要有客户端弹性模式
&emsp;&emsp;所有的系统都会遇到故障,分布式系统单点故障概率更高。如何构建应用程序来应对故障,是每个软件开发人员工作的关键部分。但是通常在构建系统时,大多数工程师只考虑到基础设施或关键服务彻底发生故障,使用诸如集群关键服务器、服务间的负载均衡以及异地部署等技术。尽管这些方法考虑到组件系统的彻底故障,但他们之解决了构建弹性系统的一小部分问题。当服务崩溃时,很容易检测到该服务以及失效,因此应用程序可以饶过它。然而,当服务运行缓慢时,检测到这个服务性能越发低下并绕过它是非常困难的,因为以下几个原因:
- 服务的降级可以是以间歇性的故障开始,并形成不可逆转的势头————可能开始只是一小部分服务调用变慢,直到突然间应用程序容器耗尽了线程(所有线程都在等待调用完成)并彻底崩溃。
- 应用程序通常的设计是处理远程资源的彻底故障,而不是部分降级————通常,只要服务没有完全死掉,应用程序将继续调用这个服务,直到资源耗尽崩溃。
&emsp;&emsp;性能较差的远程服务会导致很大的潜在问题,它们不仅难以检测,还会触发连锁反应,从而影响整个应用程序生态系统。如果没有适当的保护措施,一个性能不佳的服务可以迅速拖垮整个应用程序。基于云、基于微服务的应用程序特别容易受到这些类型的终端影响,因为这些应用由大量细粒度的分布式服务组成,这些服务在完成用户的事务时涉及不同的基础设施。
# 二、什么是客户端弹性模式
&emsp;&emsp;客户端弹性模式是在远程服务发生错误或表现不佳时保护远程资源另一个微服务调用或者数据库查询免于崩溃。这些模式的目标是为了能让客户端“快速失败”不消耗诸如数据库连接、线程池之类的资源还可以避免远程服务的问题向客户端的消费者进行传播引发“雪崩”效应。spring cloud 主要使用的有四种客户端弹性模式:
- 客户端负载均衡client load balance模式
&emsp;&emsp;上一篇已经说过,这里不再赘述。
- 断路器(circuit breaker)模式
&emsp;&emsp;本模式模仿的是电路中的断路器。有了软件断路器,当远程服务被调用时,断路器将监视这个调用,如果调用时间太长,断路器将介入并中断调用。此外,如果对某个远程资源的调用失败次数达到某个阈值,将会采取快速失败策略,阻止将来调用失败的远程资源。
- 后备(fallback)模式
&emsp;&emsp;当远程调用失败时,将执行替代代码路径,并尝试通过其他方式来处理操作,而不是产生一个异常。也就是为远程操作提供一个应急措施,而不是简单的抛出异常。
- 舱壁(bulkhead)模式
&emsp;&emsp;舱壁模式是建立在造船的基础概念上。我们都知道一艘船会被划分为多个水密舱(舱壁),因而即使少数几个部位被击穿漏水,整艘船并不会被淹没。将这个概念带入到远程调用中,如果所有调用都使用的是同一个线程池来处理,那么很有可能一个缓慢的远程调用会拖垮整个应用程序。在舱壁模式中可以隔离每个远程资源,并分配各自的线程池,使之互不影响。
&emsp;&emsp;下图展示了这些模式是如何运用到微服务中的:
![客户端弹性模式架构](./picFolder/客户端弹性模式架构.png)
# 三、spring cloud 中使用
&emsp;&emsp;使用 Netflix 的 Hystrix 库来实现上述弹性模式。继续使用上一节的项目,给 licensingservice 服务实现弹性模式。
## 1、代码修改
### 1、依赖引入
&emsp;&emsp;首先修改 POM 文件,添加下面两个依赖:
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<!--本依赖不是必须的spring-cloud-starter-hystrix已经带了但是在Camden.SR5发行版本中使用了1.5.6,这个版本有一个不一致的地方在没有后备的情况下会抛出java.lang.reflect.UndeclaredThrowableException而不是com.netflix.hystrix.exception.HystrixRuntimeException,
在后续版本中修复了这个问题-->
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
<version>1.5.9</version>
</dependency>
```
&emsp;&emsp;然后在启动类上加入`@EnableCircuitBreaker`启用 Hystrix。
## 2、实现断路器
&emsp;&emsp;首先修改 organizationservice 项目中的 OrganizationController模拟延迟每隔两次让线程 sleep 2 秒
```java
@RestController
public class OrganizationController {
private static int count=1;
@GetMapping(value = "/organization/{orgId}")
public Object getOrganizationInfo(@PathVariable("orgId") String orgId) throws Exception{
if(count%2==0){
TimeUnit.SECONDS.sleep(2);
}
count++;
Map<String, String> data = new HashMap<>(2);
data.put("id", orgId);
data.put("name", orgId + "公司");
return data;
}
}
```
&emsp;&emsp;只需在方法上添加`@HystrixCommand`,即可实现超时短路。如果 Spring 扫描到该注解注释的类,它将动态生成一个代理,来包装这个方法,并通过专门用于处理远程调用的线程池来管理对该方法的所有调用。
&emsp;&emsp;修改 licensingservice 服务中的 OrganizationByRibbonService,OrganizationFeignClient给其中的方法加上`@HystrixCommand`的注解。然后再访问接口[localhost:10011/licensingByRibbon/11313](localhost:10011/licensingByRibbon/11313),[localhost:10011/licensingByFeign/11313](localhost:10011/licensingByFeign/11313)。多次访问可发现抛出错误`com.netflix.hystrix.exception.HystrixRuntimeException`,断路器生效,默认情况下操时时间为 1s。
```json
{
"timestamp": 1543823192424,
"status": 500,
"error": "Internal Server Error",
"exception": "com.netflix.hystrix.exception.HystrixRuntimeException",
"message": "OrganizationFeignClient#getOrganization(String) timed-out and no fallback available.",
"path": "/licensingByFeign/11313/"
}
```
&emsp;&emsp;可通过设置注解参数来修改操时时间。设置超时时间大于 2s 后便不会报操时错误。(不知道为什么在 Feign 中设置失败ribbon 中正常。)。一般都是将配置写在配置文件中。
```java
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "20000")
})
```
## 3、后备处理
&emsp;&emsp;由于远程资源的消费者和资源本身之间存在存在一个"中间人",因此开发人员能够拦截服务故障,并选择替代方案。在 Hystrix 中进行后备处理,非常容易实现。
1. 在 ribbon 中的实现
&emsp;&emsp;只需在`@HystrixCommand`注解中加入属性 fallbackMethod="methodName",那么在执行失败时,便会执行后备方法。注意防备方法必须和被保护方法在同一个类中,并且方法签名必须相同。修改 licensingservice 中 service 包下的 OrganizationByRibbonService 类,改为如下:
```java
@Component
public class OrganizationByRibbonService {
private RestTemplate restTemplate;
@Autowired
public OrganizationByRibbonService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
},fallbackMethod = "getOrganizationWithRibbonBackup")
public Organization getOrganizationWithRibbon(String id) throws Exception {
ResponseEntity<Organization> responseEntity = restTemplate.exchange("http://organizationservice/organization/{id}",
HttpMethod.GET, null, Organization.class, id);
return responseEntity.getBody();
}
public Organization getOrganizationWithRibbonBackup(String id)throws Exception{
Organization organization = new Organization();
organization.setId("0");
organization.setName("组织服务调用失败");
return organization;
}
}
```
&emsp;&emsp;启动应用,多次访问[localhost:10011/licensingByRibbon/11313/](localhost:10011/licensingByRibbon/11313/),可以发现调用失败时,会启用后备方法。
2. 在 feign 中实现
&emsp;&emsp;在 feign 中实现后备模式,需要编写一个 feign 接口的实现类,然后在 feign 接口中指定该类。以 licensingservice 为例。首先在 client 包中添加一个 OrganizationFeignClientImpl 类,代码如下:
```java
@Component
public class OrganizationFeignClientImpl implements OrganizationFeignClient{
@Override
public Organization getOrganization(String orgId) {
Organization organization=new Organization();
organization.setId("0");
organization.setName("后备模式返回的数据");
return organization;
}
}
```
然后修改OrganizationFeignClient接口的注解`@FeignClient("organizationservice")`改为`@FeignClient(name="organizationservice",fallback = OrganizationFeignClientImpl.class`
&emsp;&emsp;重启项目,多次访问[localhost:10011/licensingByFeign/11313/](localhost:10011/licensingByFeign/11313/),可发现后备服务起作用了。
&emsp;&emsp;在确认是否要启用后备服务时,要注意以下两点:
- 后备是一种在资源操时或失败时提供行动方案的机制。如果只是用后备来捕获操时异常然后只做日志记录那只需要try..catch即可捕获HystrixRuntimeException异常。
- 注意后备方法所执行的操作。如果在后备服务中调用另一个分布式服务,需要注意用@HystrixCommand方法注解包装后备方法
## 4、实现舱壁模式
&emsp;&emsp;在基于微服务的应用程序中通常需要调用多个微服务来完成特定的任务在不适用舱壁的模式下这些调用默认是使用同一批线程来执行调用的而这些线程是为了处理整个Java容器的请求而预留的。因此在存在大量请求的情况下一个服务出现性能问题会导致Java容器内的所有线程被占用同时阻塞新请求最终容器彻底崩溃。
&emsp;&emsp;Hystrix使用线程池来委派所有对远程服务的调用,默认情况下这个线程池有10个工作线程。但是这样很容易出现一个运行缓慢的服务占用全部的线程所有hystrix提供了一种一种易于使用的机制在不同的远程资源调用间创建舱壁将不同服务的调用隔离到不同的线程池中使之互不影响。
&emsp;&emsp;要实现隔离的线程池,只需要在`@HystrixCommand`上加入线程池的注解这里以ribbon为例Feign类似)。修改licensingservice中service包下的OrganizaitonByRibbonService类`getOrganizationWithRibbon`方法的注解改为如下:
```java
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
}, fallbackMethod = "getOrganizationWithRibbonBackup",
threadPoolKey = "licenseByOrgThreadPool",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "30"),
@HystrixProperty(name = "maxQueueSize", value = "10")
})
```
如果将`maxQueueSize`属性值设为-1将使用`SynchronousQueue`保存所有的传入请求同步队列会强制要求正在处理中的请求数量永远不能超过线程池的大小。设为大于1的值将使用`LinkedBlockingQueue`
&emsp;&emsp;**注意**示例代码中都是硬编码属性值到Hystrix注解中的。在实际应用环境中一般都是将配置项配置在Spring Cloud Config中的方便统一管理。
本次用到全部代码:[记得补全代码呀]()

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,50 +1,50 @@
--- ---
id="2018-11-20-10-38-05" id="2018-11-20-10-38-05"
title="linux下mongodb的配置与安装" title="linux下mongodb的配置与安装"
headWord="linux下docker的安装与配置" headWord="linux下docker的安装与配置"
tags=["docker", "linux","ubuntu"] tags=["docker", "linux","ubuntu"]
category="linux" category="linux"
serie="linux软件安装配置" serie="linux软件安装配置"
--- ---
# 一、安装 # 一、安装
&emsp;&emsp;有两种安装方法脚本安装和apt安装 &emsp;&emsp;有两种安装方法脚本安装和apt安装
## 1、脚本安装 ## 1、脚本安装
&emsp;&emsp;root下执行以下命令(非root下会要求输入密码 &emsp;&emsp;root下执行以下命令(非root下会要求输入密码
```bash ```bash
wget -qO- https://get.docker.com/ | sh wget -qO- https://get.docker.com/ | sh
``` ```
等待执行完毕后会有如下提示: 等待执行完毕后会有如下提示:
``` ```
If you would like to use Docker as a non-root user, you should now consider If you would like to use Docker as a non-root user, you should now consider
adding your user to the "docker" group with something like: adding your user to the "docker" group with something like:
sudo usermod -aG docker 用户名 sudo usermod -aG docker 用户名
Remember that you will have to log out and back in for this to take effect! Remember that you will have to log out and back in for this to take effect!
``` ```
就是如果要以非root用户直接运行docker时需要执行`sudo usermod -aG docker 非root用户名`,然后重新登陆即可。 就是如果要以非root用户直接运行docker时需要执行`sudo usermod -aG docker 非root用户名`,然后重新登陆即可。
## 2、apt安装 ## 2、apt安装
&emsp;&emsp;直接运行(root下) &emsp;&emsp;直接运行(root下)
```bash ```bash
apt-get update apt-get update
apt-get install docker.io apt-get install docker.io
``` ```
# 二、配置国内源 # 二、配置国内源
&emsp;&emsp;国外源太慢配置为国内源。修改或创建`/etc/docker/daemon.json`,内容如下: &emsp;&emsp;国外源太慢配置为国内源。修改或创建`/etc/docker/daemon.json`,内容如下:
```json ```json
{ {
"registry-mirrors": [ "registry-mirrors": [
"加速地址" "加速地址"
], ],
"insecure-registries": [] "insecure-registries": []
} }
``` ```
以下是国内加速地址: 以下是国内加速地址:
- 网易 [http://hub-mirror.c.163.com](http://hub-mirror.c.163.com) - 网易 [http://hub-mirror.c.163.com](http://hub-mirror.c.163.com)
- ustc [https://docker.mirrors.ustc.edu.cn](https://docker.mirrors.ustc.edu.cn) - ustc [https://docker.mirrors.ustc.edu.cn](https://docker.mirrors.ustc.edu.cn)

View File

@ -1 +1 @@
{"2018-11-22-15-57-00":0,"2018-11-20-10-38-05":0,"2018-11-19-15-57-00":0,"2018-10-22-10-38-05":4,"2018-10-21-10-38-05":0,"2018-10-20-10-38-05":20} {"2018-11-22-15-57-00":14,"2018-11-20-10-38-05":18,"2018-11-19-15-57-00":3,"2018-10-22-10-38-05":14,"2018-10-21-10-38-05":3,"2018-10-20-10-38-05":20}

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,116 @@
---
id="2018-12-17-13-58"
title="vscode开发调试typescript"
headWord="本篇用于记录如何在vscode下开发typescript以及端点调试ts代码"
tags=["vscode", "node","typescript","ts"]
category="node"
serie="node开发环境配置"
---
### 1、安装 typescript
&emsp;&emsp;npm 全局安装 typescript
```bash
npm i -g typescript
```
### 2、初始化 ts 配置
&emsp;&emsp;以 test 项目为例,在 test 目录下执行`tsc --init`,会在项目目录下产生一个`tsconfig.json`的配置文件,每项配置都有说明,如下图所示:
![tscofnig文件部分截图](./picFolder/tsconfig界面.png)
主要用到的配置项如下:
```javascript
{
"compilerOptions": {
"module": "commonjs", //指定生成哪个模块系统代码
"target": "ES2017", //目标代码类型
"noImplicitAny": false, //在表达式和声明上有隐含的'any'类型时报错。
"sourceMap": true, //用于debug,断点调试需打开
"rootDir":"./", //需要编译的目录
"outDir":"./dist", //编译后的js存放路径
"watch":true //在监视模式下运行编译器。会监视输出文件,在它们改变时重新编译。
"strict":true //严格模式
},
//需要监控的路径,非必要无需配置有rootDir就够了
"include":[
"./"
],
//需要排除的路径,非必要无需配置
//排除监控目录
"exclude":[
"node_module",
"temp"
]
}
```
### 3、编译
&emsp;&emsp;执行`tsc -w`,将 ts 编译位 js 并输出到 dist 目录下,`-w`监控文件变动,自动编译。
### 4、断点运行
&emsp;&emsp;首先添加 launch.json,如图所示:
![添加配置](./picFolder/添加配置.png)
选中 node 环境,然后将要调试的文件改为输出目录中对应的 js 文件,修改 program 属性:
```json
"program": "${workspaceFolder}\\dist\\index.js"
```
最后**F5**运行即可断点调试。
### 5、热更新
&emsp;&emsp;如何在修改文件保存后自动重启服务器,首先全局安装`nodemon`,然后在 test 根目录下编写 nodemon 配置文件--nodemon.json实际使用中不能带注释需删除
```json
{
"restartable": "rs", //重启的命令,默认是 rs
"ignore": [""], //忽略的文件后缀名或者文件夹,文件路径的书写用相对于 nodemon.json 所在位置的相对路径
"verbose": true, //表示输出详细启动与重启信息
"events": {
"start": "node ./dist/index.js", //子进程(即监控的应用)启动
"crash": "", //子进程崩溃,不会触发 exit
"exit": "", //子进程完全退出,不是非正常的崩溃
"restart": "" //子进程重启
},
"ext": "js json", //监控指定后缀名的文件,用空格间隔。默认监控的后缀文件:.js, .coffee, .litcoffee, .json。但是对于没有文件后缀的文件比如 www 文件,我暂时找不到怎么用 nodemon 去监控,就算在 watch 中包含了nodemon 也会忽略掉
"watch": ["./src/**"],
"env": {
"NODE_ENV": "env", // env 是开发环境; 运行环境 development; production 是生产环境
"PORT": "3000"
},
"legacy-watch": false //nodemon 使用 Chokidar 作为底层监控系统但是如果监控失效或者提示没有需要监控的文件时就需要使用轮询模式polling mode即设置 legacy-watch 为 true也可以在命令行中指定
}
```
然后使用`nodemon --inspect nodemon.json`命令启动(--inspect 是 vscode 断点调试关键),接着在.vscode 目录中将 launch.json 修改为下面的代码:
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Node: Nodemon",
"processId": "${command:PickProcess}",
"restart": true,
"protocol": "inspector"
}
]
}
```
最后调试页面启动调试,选择代码真实运行的进程,如下图所示:
![vscode符加进程](./picFolder/vscode附加进程.png)
&emsp;&emsp;大功告成。。