ConcurrentHashMap 和 Hashtable 的区别

底层数据结构

ConcurrentHashMap

[JDK1.7]
ConcurrentHashMap 底层采用 分段的数组+链表 实现,
[JDK1.8]
ConcurrentHashMap 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。

Hashtable

Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,
数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;

实现线程安全的方式(重要)

ConcurrentHashMap

[JDK1.7]
ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,
就不会存在锁竞争,提高并发访问率。
[JDK1.8]
摒弃了Segment的概念,而是直接用 Node 数组 + 链表 + 红黑树 的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。
(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,
虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;

Hashtable(同一把锁)

使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,
可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,
竞争会越来越激烈效率越低。HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,
每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。
这就是ConcurrentHashMap所采用的”分段锁”思想。

HashTable:

HashTable全表锁

JDK1.7的ConcurrentHashMap:

JDK1.7的ConcurrentHashMap

JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):

JDK1.8的ConcurrentHashMap

  

NodeJs apiDoc

apiDoc

全局安装,方便用命令创建文档

1
npm install apidoc -g

配置

在你的项目根目录下新建apidoc.json文件,该文件描述了项目对外提供接口的概要信息如名称、版本、描述、文档打开时浏览器显示标题和接口缺省访问地址。
apidoc.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "xxx Api",
"version": "1.0.0",
"description": "xxx Api Documentation",
"title": "xxx",
"url" : "http://域名/api/v1",
"sampleUrl": "http://域名/api/v1",

"template": {
"withCompare": true,
"withGenerator": true,
"forceLanguage":"en"
},
//顺序、若有需要可配置(books, student,xxx) Resources Name
"order": ["xxx","xxx"]
}

Sample

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* Get Access Token
* @api {POST} /token GetAccessToken
* @apiDescription Generate a token that can be passed with each API request.
* @apiName GetAccessToken
* @apiPermission API User
* @apiParam (Parameters) {String} username username
* @apiParam (Parameters) {String} password password
* @apiParamExample {json} Sample (Request body formats: text/plain, application/x-www-form-urlencoded, text/json, application/json) :
* {
* "username": "your accont",
* "password": "your password"
* }
* @apiSampleRequest off
* @apiSuccessExample {json} Response (Response body formats: application/json, text/json):
* Success Response :
* Status: 200 OK
* {
* "status": "success",
* "message": "Authentication successful.",
* "data": {
* "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6Ikpxxxxx.xxxxx2VybmFtZSI6InNsIiwiaWF0IjoxNTQ1MTkyMzY4LCJleHAiOjE1NDUxOTI4NDh9.xxxxxxgS1Yw8rNRD31p97A7fdWvmkrXxo3llMJowX7U"
* }
* }
* Or
* Bad Response:
* Status: 400
* {
* "status": "failure",
* "message": "Authentication failed. Please provide a correct username or password."
* }
* @apiGroup Token
* @apiVersion 1.0.0
*/

生成

文档初始化或发生改变时,在应用程序根目录执行相应命令
以此项目为例:

apidoc -i api/v1 -o public/apiDoc

1
2
3
4
-i input
-o output

指定文件夹路径

具体注释写法参照官网解释:
http://apidocjs.com/#run

public/apiDoc内文件不用上传到代码管理中
可直接执行npm run doc命令自动生成

【05期】Redis常见问题理解

Redis常见问题

1. 什么是缓存血崩?怎么解决 ?

通常,我们会使用缓存用于缓冲对 DB 的冲击,如果缓存宕机,所有请求将直接打在 DB,造成 DB 宕机——从而导致整个系统宕机。

解决方法:

2 种策略(同时使用):

  • 对缓存做高可用,防止缓存宕机

  • 使用断路器,如果缓存宕机,为了防止系统全部宕机,限制部分流量进入DB,保证部分可用,其余的请求返回断路器的默认值。

2. 什么是缓存穿透?怎么解决?

解释 1:缓存查询一个没有的 key,同时数据库也没有,如果黑客大量的使用这种方式,那么就会导致 DB 宕机。

解决方案:我们可以使用一个默认值来防止,例如,当访问一个不存在的 key,然后再去访问数据库,还是没有,那么就在缓存里放一个占位符比如说NULL等等,下次来的时候,检查这个占位符,如果发生时占位符,就不去数据库查询了,防止 DB 宕机。

解释 2:大量请求查询一个刚刚失效的 key,导致 DB 压力倍增,可能导致宕机,但实际上,查询的都是相同的数据。

解决方案:可以在这些请求代码加上双重检查锁。但是那个阶段的请求会变慢。不过总比 DB 宕机好。

3. 什么是缓存并发竞争?怎么解决?

解释:多个客户端写一个 key,如果顺序错了,数据就不对了。但是顺序我们无法控制。

解决方案:使用分布式锁,例如 zk,同时加入数据的时间戳。同一时刻,只有抢到锁的客户端才能写入,同时,写入时,比较当前数据的时间戳和缓存中数据的时间戳。

4.什么是缓存和数据库双写不一致?怎么解决?

解释:连续写数据库和缓存,但是操作期间,出现并发了,数据不一致了。

通常,更新缓存和数据库有以下几种顺序:

  • 先更新数据库,再更新缓存。

  • 先删缓存,再更新数据库。

  • 先更新数据库,再删除缓存。

三种方式的优劣来看一下:

先更新数据库,再更新缓存。

这么做的问题是:当有 2 个请求同时更新数据,那么如果不使用分布式锁,将无法控制最后缓存的值到底是多少。也就是并发写的时候有问题。

先删缓存,再更新数据库

这么做的问题:如果在删除缓存后,有客户端读数据,将可能读到旧数据,并有可能设置到缓存中,导致缓存中的数据一直是老数据。

有 2 种解决方案:

  • 使用“双删”,即删更删,最后一步的删除作为异步操作,就是防止有客户端读取的时候设置了旧值。

  • 使用队列,当这个key不存在时,将其放入队列,串行执行,必须等到更新数据库完毕才能读取数据。

总的来讲,比较麻烦。

先更新数据库,再删除缓存

这个实际是常用的方案,但是有很多人不知道,这里介绍一下,这个叫 Cache Aside Pattern,老外发明的。

如果在更新数据库之前,缓存刚好失效了,读客户端有可能读到旧值,然后在写客户端删除缓存结束后再次设置了旧值,非常巧合的情况。

有 2 个前提条件:缓存在写之前的时候失效,同时,在写客户度删除操作结束后,放置旧数据 —— 也就是读比写慢。甚至有的写操作还会锁表。

所以,这个很难出现,但是如果出现了怎么办?使用双删!!!记录更新期间有没有客户端读数据库,如果有,在更新完数据库之后,执行延迟删除。

还有一种可能,如果执行更新数据库,准备执行删除缓存时,服务挂了,执行删除失败怎么办???

这就坑了!!!不过可以通过订阅数据库的 binlog 来删除。

旁路缓存Cache Aside Pattern方案:

对于读请求

  • 先读cache,再读db
  • 如果,cache hit,则直接返回数据
  • 如果,cache miss,则访问db,并将数据set回缓存

    对于写请求

  • 淘汰缓存,而不是更新缓存

  • 先操作数据库,再淘汰缓存

Cache Aside Pattern为什么建议淘汰缓存,而不是更新缓存?

如果更新缓存,在并发写时,可能出现数据不一致。如果采用set缓存,在两个并发写发生时,由于无法保证时序,此时不管先操作缓存还是先操作数据库,都可能出现:

-(1)请求1先操作数据库,请求2后操作数据库

-(2)请求2先set了缓存,请求1后set了缓存

导致,数据库与缓存之间的数据不一致。

所以,Cache Aside Pattern建议,delete缓存,而不是set缓存。

Cache Aside Pattern为什么建议先操作数据库,再操作缓存?

在并发读写发生时,由于无法保证时序,可能出现:

(1)写请求淘汰了缓存

(2)写请求操作了数据库(主从同步没有完成)

(3)读请求读了缓存(cache miss)

(4)读请求读了从库(读了一个旧数据)

(5)读请求set回缓存(set了一个旧数据)

(6)数据库主从同步完成

导致,数据库与缓存的数据不一致。

所以,Cache Aside Pattern建议,先操作数据库,再操作缓存。

Cache Aside Pattern方案存在什么问题?

答:如果先操作数据库,再淘汰缓存,在原子性被破坏时:

(1)修改数据库成功了

(2)淘汰缓存失败了

导致,数据库与缓存的数据不一致。

【04期】Object类相关方法

Object类包含哪些方法

Java语言是一种单继承结构语言,Java中所有的类都有一个共同的祖先。这个祖先就是Object类。
如果一个类没有用extends明确指出继承于某个类,那么它默认继承Object类。
Object的方法我们在平时基本都会用到,但如果没有准备被忽然这么一问,还是有点懵圈的。
Object类是Java中所有类的基类。位于java.lang包中,一共有13个方法。

1.Object()

这个没什么可说的,Object类的构造方法。(非重点)

2.registerNatives()

为了使JVM发现本机功能,他们被一定的方式命名。例如,对于java.lang.Object.registerNatives,对应的C函数命名为Java_java_lang_Object_registerNatives。

通过使用registerNatives(或者更确切地说,JNI函数RegisterNatives),可以命名任何你想要你的C函数。(非重点)

3.clone()

clone()函数的用途是用来另存一个当前存在的对象。只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。(注意:回答这里时可能会引出设计模式的提问)

4.getClass()

final方法,用于获得运行时的类型。该方法返回的是此Object对象的类对象/运行时类对象Class。效果与Object.class相同。(注意:回答这里时可能会引出类加载,反射等知识点的提问)

5.equals()

equals用来比较两个对象的内容是否相等。默认情况下(继承自Object类),equals和==是一样的,除非被覆写(override)了。(注意:这里可能引出更常问的“equals与==的区别”及hashmap实现原理的提问)

6.hashCode()

该方法用来返回其所在对象的物理地址(哈希码值),常会和equals方法同时重写,确保相等的两个对象拥有相等的hashCode。用于散列存储结构中确定数据的存储位置,hasCode代表的对象地址就是对象在hash表中的位置,物理地址说的是放在内存中的地址,为了查找的便捷性(同样,可能引出hashmap实现原理的提问)

7.toString()

toString()方法返回该对象的字符串表示,这个方法没什么可说的。

8.wait()

导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。(引出线程通信及“wait和sleep的区别”的提问)

9.wait(long timeout)

导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。(引出线程通信及“wait和sleep的区别”的提问)

10.wait(long timeout, int nanos)

导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。(引出线程通信及“wait和sleep的区别”的提问)

11.notify()

唤醒在此对象监视器上等待的单个线程。(引出线程通信的提问)

12. notifyAll()

唤醒在此对象监视器上等待的所有线程。(引出线程通信的提问)

13.finalize()

当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。(非重点,但小心引出垃圾回收的提问)

引申常见问题

1
2
3
4
5
6
7
8
9
10
11
equals() 与 == 的区别是什么?

hashCode() 和 equals() 之间有什么联系?

wait()方法与sleep()方法的区别

为什么重写了equals就必须重写hashCode

HashMap的实现原理

谈谈类加载机制

【03期】如何决定使用 HashMap 还是 TreeMap?

如何决定使用 HashMap 还是 TreeMap?

问:如何决定使用 HashMap 还是 TreeMap?

介绍
TreeMap<K,V>的Key值是要求实现java.lang.Comparable,所以迭代的时候TreeMap默认是按照Key值升序排序的;TreeMap的实现是基于红黑树结构。适用于按自然顺序或自定义顺序遍历键(key)。

HashMap<K,V>的Key值实现散列hashCode(),分布是散列的、均匀的,不支持排序;数据结构主要是桶(数组),链表或红黑树。适用于在Map中插入、删除和定位元素。

结论
如果你需要得到一个有序的结果时就应该使用TreeMap(因为HashMap中元素的排列顺序是不固定的)。除此之外,由于HashMap有更好的性能,所以大多不需要排序的时候我们会使用HashMap。

拓展

1、HashMap 和 TreeMap 的实现

HashMap:基于哈希表实现。使用HashMap要求添加的键类明确定义了hashCode()和equals()[可以重写hashCode()和equals()],为了优化HashMap空间的使用,您可以调优==初始容量==和==负载因子==。

HashMap(): 构建一个空的哈希映像

HashMap(Map m): 构建一个哈希映像,并且添加映像m的所有映射

HashMap(int initialCapacity): 构建一个拥有特定容量的空的哈希映像

HashMap(int initialCapacity, float loadFactor): 构建一个拥有特定容量和加载因子的空的哈希映像

TreeMap:基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。

TreeMap():构建一个空的映像树

TreeMap(Map m): 构建一个映像树,并且添加映像m中所有元素

TreeMap(Comparator c): 构建一个映像树,并且使用特定的比较器对关键字进行排序

TreeMap(SortedMap s): 构建一个映像树,添加映像树s中所有映射,并且使用与有序映像s相同的比较器排序

2、HashMap 和 TreeMap 都是非线程安全

HashMap继承AbstractMap抽象类,TreeMap继承自SortedMap接口。

AbstractMap抽象类:覆盖了equals()和hashCode()方法以确保两个相等映射返回相同的哈希码。如果两个映射大小相等、包含同样的键且每个键在这两个映射中对应的值都相同,则这两个映射相等。映射的哈希码是映射元素哈希码的总和,其中每个元素是Map.Entry接口的一个实现。因此,不论映射内部顺序如何,两个相等映射会报告相同的哈希码。

SortedMap接口:它用来保持键的有序顺序。SortedMap接口为映像的视图(子集),包括两个端点提供了访问方法。除了排序是作用于映射的键以外,处理SortedMap和处理SortedSet一样。添加到SortedMap实现类的元素必须实现Comparable接口,否则您必须给它的构造函数提供一个Comparator接口的实现。TreeMap类是它的唯一一个实现。

3、TreeMap中默认是按照升序进行排序的,如何让他降序

通过自定义的比较器来实现

定义一个比较器类,实现Comparator接口,重写compare方法,有两个参数,这两个参数通过调用compareTo进行比较,而compareTo默认规则是:

  • 如果参数字符串等于此字符串,则返回 0 值;

  • 如果此字符串小于字符串参数,则返回一个小于 0 的值;

  • 如果此字符串大于字符串参数,则返回一个大于 0 的值。

自定义比较器时,在返回时多添加了个负号,就将比较的结果以相反的形式返回,代码如下:

1
2
3
4
5
6
7
8
9
static class MyComparator implements Comparator{
@Override
public int compare(Object o1, Object o2) {
// TODO Auto-generated method stub
String param1 = (String)o1;
String param2 = (String)o2;
return -param1.compareTo(param2);
}
}

之后,通过MyComparator类初始化一个比较器实例,将其作为参数传进TreeMap的构造方法中:

1
2
3
MyComparator comparator = new MyComparator();

Map<String,String> map = new TreeMap<String,String>(comparator);

使用自定义比较器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class MapTest {

public static void main(String[] args) {
//初始化自定义比较器
MyComparator comparator = new MyComparator();
//初始化一个map集合
Map<String,String> map = new TreeMap<String,String>(comparator);
//存入数据
map.put("a", "a");
map.put("b", "b");
map.put("f", "f");
map.put("d", "d");
map.put("c", "c");
map.put("g", "g");
//遍历输出
Iterator iterator = map.keySet().iterator();
while(iterator.hasNext()){
String key = (String)iterator.next();
System.out.println(map.get(key));
}
}

static class MyComparator implements Comparator{

@Override
public int compare(Object o1, Object o2) {
// TODO Auto-generated method stub
String param1 = (String)o1;
String param2 = (String)o2;
return -param1.compareTo(param2);
}

}

}

【02期】你能说说Spring框架中Bean的生命周期吗?

1、实例化一个Bean--也就是我们常说的new;

2、按照Spring上下文对实例化的Bean进行配置--也就是IOC注入;

3、如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String)方法,此处传递的就是Spring配置文件中Bean的id值

4、如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(setBeanFactory(BeanFactory)传递的是Spring工厂自身(可以用这个方式来获取其它Bean,只需在Spring配置文件中配置一个普通的Bean就可以);

5、如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文(同样这个方式也可以实现步骤4的内容,但比4更好,因为ApplicationContext是BeanFactory的子接口,有更多的实现方法);

6、如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用那个的方法,也可以被应用于内存或缓存技术;

7、如果Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法。

8、如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法、;

注:以上工作完成以后就可以应用这个Bean了,那这个Bean是一个Singleton的,所以一般情况下我们调用同一个id的Bean会是在内容地址相同的实例,当然在Spring配置文件中也可以配置非Singleton,这里我们不做赘述。

9、当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用那个其实现的destroy()方法;

10、最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。

【01期】Spring,SpringMVC,SpringBoot,SpringCloud有什么区别和联系?

简单介绍

Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。Spring使你能够编写更干净、更可管理、并且更易于测试的代码。

Spring MVC是Spring的一个模块,一个web框架。通过DispatcherServlet, ModelAndView 和 View Resolver,开发web应用变得很容易。主要针对的是网站应用程序或者服务开发——URL路由、Session、模板引擎、静态Web资源等等。

Spring配置复杂,繁琐,所以推出了Spring boot,约定优于配置,简化了spring的配置流程。

Spring Cloud构建于Spring Boot之上,是一个关注全局的服务治理框架。

Spring VS SpringMVC:

Spring是一个一站式的轻量级的java开发框架,核心是控制反转(IOC)和面向切面(AOP),针对于开发的WEB层(springMvc)、业务层(Ioc)、持久层(jdbcTemplate)等都提供了多种配置解决方案;

SpringMVC是Spring基础之上的一个MVC框架,主要处理web开发的路径映射和视图渲染,属于Spring框架中WEB层开发的一部分;

SpringMVC VS SpringBoot:

SpringMVC属于一个企业WEB开发的MVC框架,涵盖面包括前端视图开发、文件配置、后台接口逻辑开发等,XML、config等配置相对比较繁琐复杂;

SpringBoot框架相对于SpringMVC框架来说,更专注于开发微服务后台接口,不开发前端视图;

SpringBoot和SpringCloud:

SpringBoot使用了约定大于配置的理念,集成了快速开发的Spring多个插件,同时自动过滤不需要配置的多余的插件,简化了项目的开发配置流程,一定程度上取消xml配置,是一套快速配置开发的脚手架,能快速开发单个微服务;

SpringCloud大部分的功能插件都是基于SpringBoot去实现的,SpringCloud关注于全局的微服务整合和管理,将多个SpringBoot单体微服务进行整合以及管理;SpringCloud依赖于SpringBoot开发,而SpringBoot可以独立开发;

总结下来:

Spring是核心,提供了基础功能;

Spring MVC 是基于Spring的一个 MVC 框架 ;

Spring Boot 是为简化Spring配置的快速开发整合包;

Spring Cloud是构建在Spring Boot之上的服务治理框架。

SpringCloud进阶之Ribbon和Feign(负载均衡)

Spring Cloud 进阶之Ribbon和Feign(负载均衡)

Ribbon 负载均衡

Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端,负载均衡的工具;

Ribbon 核心组件IRule

根据特定算法,从服务列表中选取一个要访问的服务:

  • RoundRobinRule:轮询
  • RandomRule:随机
  • AvailabilityFilteringRule: 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,以及并发的连接数量
    超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问;
  • WeightedResponseTimeRule: 根据平均响应时间计算所有服务的权重,响应时间越快,服务权重越大,被选中的机率越高;
    刚启动时,如果统计信息不足,则使用RoundRobinRule策略,等统计信息足够时,会切换到WeightedResponseTimeRule
  • RetryRule: 先按照RoundRobinRule的策略获取服务,如果获取服务失败,则在指定时间内会进行重试,获取可用的服务;
  • BestAvailableRule: 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务;
  • ZoneAvoidanceRule: 默认规则,复合判断server所在区域的性能和server的可用性选择服务器;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// ConfigBean 添加新注解 @LoadBalanced, 用于加入 Ribbon 配置
@Configuration
public class ConfigBean {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}

@Configuration
public class ConfigBean {

@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}

@Bean
public IRule myRule() {
return new RoundRobinRule(); // 显式的指定使用轮询算法
}
}


// 修改主启动类
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name="MICROSERVICECLOUD-DEPT", configuration=MySelfRule.class) // 自定义Ribbon配置类
public class DeptConsumer80_App {

public static void main(String[] args) {

SpringApplication.run(DeptConsumer80_App.class, args);
}

}


// com.noodles.myrule
// 自定义Robbin规则类
@Configuration
public class MySelfRule{
@Bean
public IRule myRule(){
return new RandomRule(); //自定义均衡策略
}
}

Feign 负载均衡

Feign 是一个声明式WebService客户端:
使用方法: 定义一个接口,然后在上面添加注解;

  • 首先通过 @EnableFeignClients 注解开启 FeignClient 的功能。只有这个注解存在,才会在程序启动时开启 @FeignClient 注解的包扫描。
  • 根据Feign的规则实现接口,并在接口上面加上 @FeignClient 注解。
  • 程序启动后,会进行包扫描,扫描所有的@FeignClient 的注解的类,并将这些信息注入 IOC容器中。
  • 当接口的方法被调用时,通过JDK的代理来生成具体的 RequestTemplate 模板对象。
  • 根据 RequestTemplate 再生成 Http 请求的 Request 对象。
  • Request 对象交给 Client 去处理,其中 Client 的网络请求框架可以是 HTTPURLConnection、HttpClient 和 OkHttp。
  • 最后Client被封装到LoadBalanceClient类,这个类结合类 Ribbon 做到了负载均衡。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 新建DeptClientService接口,并新增注解@FeignClient,来指定调用哪个服务
@FeignClient(value="MICROSERVICECLOUD-DEPT")
public interface DeptClientService {

@RequestMapping(value="/dept/get/{id}", method= RequestMethod.GET)
public Dept get(@PathVariable("id") long id);

@RequestMapping(value="/dept/list", method= RequestMethod.GET)
public List<Dept> list();

@RequestMapping(value="/dept/add", method= RequestMethod.POST)
public boolean add(Dept dept);
}

// microservice-consumer-dept-feign 工程修改Controller
@RestController
public class DeptController_Consumer {

//用于服务调用
@Autowired
private DeptClientService service;

@RequestMapping(value="/consumer/dept/get/{id}")
public Dept get(@PathVariable("id") Long id) {
return this.service.get(id);
}

@RequestMapping(value="/consumer/dept/list")
public List<Dept> list(){
return this.service.list();
}

@RequestMapping(value="/consumer/dept/add")
public Object add(Dept dept) {
return this.service.add(dept);
}
}

RabbitMQ

RabbitMQ

简介

RabbitMQ 是采用 Erlang 语言实现 AMQP(Advanced Message Queuing Protocol,高级消息队列协议)的消息中间件,它最初起源于金融系统,用于在分布式系统中存储转发消息。

  • 可靠性: RabbitMQ使用一些机制来保证消息的可靠性,如持久化、传输确认及发布确认等。
  • 灵活的路由: 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。
  • 扩展性: 多个RabbitMQ节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。
  • 高可用性: 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队列仍然可用。
  • 支持多种协议: RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP、MQTT 等多种消息中间件协议。
  • 多语言客户端: RabbitMQ几乎支持所有常用语言,比如 Java、Python、Ruby、PHP、C#、JavaScript等。
  • 易用的管理界面: RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。在安装 RabbitMQ 的时候会介绍到,安装好 RabbitMQ 就自带管理界面。
  • 插件机制: RabbitMQ 提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。感觉这个有点类似 Dubbo 的 SPI机制。

核心概念

消息投递

RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。可以把消息传递的过程想象成:当你将一个包裹送到邮局,邮局会暂存并最终将邮件通过邮递员送到收件人的手上,RabbitMQ就好比由邮局、邮箱和邮递员组成的一个系统。从计算机术语层面来说,RabbitMQ 模型更像是一种交换机模型。

image

发布订阅 (生产消费)

  • Producer(生产者) :生产消息的一方(邮件投递者)
  • Consumer(消费者) :消费消息的一方(邮件收件人)

消息一般由 2 部分组成:消息头(或者说是标签 Label)和 消息体。消息体也可以称为payLoad,消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。生产者把消息交由 RabbitMQ 后,RabbitMQ 会根据消息头把消息发送给感兴趣的 Consumer(消费者)。

交换器 Exchange

RabbitMQ 的 Exchange(交换器)有4种类型(++常用++),不同的类型对应着不同的路由策略:

  • direct(默认)
  • fanout
  • topic
  • headers

不同类型的Exchange转发消息的策略有所区别。
在 RabbitMQ 中,消息并不是直接被投递到 Queue(消息队列) 中的,中间还必须经过 Exchange(交换器) 这一层,Exchange(交换器) 会把我们的消息分配到对应的 Queue(消息队列) 中
image

路由

生产者将消息发给交换器的时候,一般会指定一个==RoutingKey(路由键)==,用来指定这个消息的路由规则,而这个 ++RoutingKey++ 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。

RabbitMQ 中通过 Binding(绑定) 将 Exchange(交换器) 与 Queue(消息队列) 关联起来,在绑定的时候一般会指定一个 BindingKey(绑定建) ,这样 RabbitMQ 就知道如何正确将消息路由到队列了,如下图所示。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和 Queue 的绑定可以是多对多的关系。

Binding(绑定) 示意图:

image

生产者将消息发送给交换器时,需要一个RoutingKey,当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的 BindingKey。BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比如fanout类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。

生产者 ===》RouteKey ===> Exchange ===> BindKey ==(mutiple allowed)=> Queue

RabbitMq工作流程

对于 RabbitMQ 来说,一个 RabbitMQ Broker 可以简单地看作一个 RabbitMQ 服务节点,或者RabbitMQ服务实例。大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。

下图展示了生产者将消息存入 RabbitMQ Broker,以及消费者从Broker中消费数据的整个流程。

image

交换机类型

RabbitMQ 常用的 Exchange Type 有 fanout、direct、topic、headers 这四种(AMQP规范里还提到两种 Exchange Type,分别为 system 与 自定义,这里不予以描述)。

① fanout

fanout 类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中,不需要做任何判断操作,所以 fanout 类型是所有的交换机类型里面速度最快的。fanout 类型常用来广播消息。

② direct

direct 类型的Exchange路由规则也很简单,它会把消息路由到那些 Bindingkey 与 RoutingKey 完全匹配的 Queue 中。

image

③ topic

前面讲到direct类型的交换器路由规则是完全匹配 BindingKey 和 RoutingKey ,但是这种严格的匹配方式在很多情况下不能满足实际业务的需求。topic类型的交换器在匹配规则上进行了扩展,它与 direct 类型的交换器相似,也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,但这里的匹配规则有些不同,它约定:

RoutingKey 为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),如 “com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”;
BindingKey 和 RoutingKey 一样也是点号“.”分隔的字符串;
BindingKey 中可以存在两种特殊字符串“ ”和“ # ”,用于做模糊匹配,其中“ ”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。

image

④ headers(不推荐)

headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。在绑定队列和交换器时制定一组键值对,当发送消息到交换器时,RabbitMQ会获取到该消息的 headers(也是一个键值对的形式)’对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。

Eureka服务注册与发现

Eureka服务注册与发现

概念原理

过去,每个应用都是一个CPU,一个主机上的单一系统。然而今天,随着大数据和云计算时代的到来,任何独立的程序都可以运行在多个计算机上。并且随着业务的发展,访问用户量的增加,开发人员或小组的增加,
系统会被拆分成多个功能模块。拆分后每个功能模块可以作为一个独立的子系统提供其职责范围内的功能。而多个子系统中,由于职责不同并且会存在相互调用,同时可能每个子系统还需要多个实例部署在多台
服务器或者镜像中,导致了子系统间的相互调用形成了一个错综复杂的网状结构。

从单体演变成分布式架构。随着系统结构、架构的演变,系统功能的增加,用户量的增加,开发人员的增加等各种增加情况下,需要有一个比较好扩展的系统架构来快速、尽量减少代码改动的前提下以支持系统功能
的开发,用户量增加导致的硬件资源横向扩容,以及开发人员增加时的协同工作效率。在此基础上需要解决系统的稳定性、容错性、高并发的支持性等。以及随着系统功能的增加如何有效的管理系统,排查、
定位系统问题。同时当参与项目的人(包含测试、运维、业务等人员)越来越多时,如何能更高效的彼此之间协同办公的效率等等。所以微服务架构需要考虑的不仅仅是软件架构本身,需要从参与到整个项目实施
过程中的各个环节,可能的问题以及人员协同的整体情况去考虑。让整个项目做到可用(满足功能以及硬件资源的横向扩容)、可行(满足整个系统运行中的各个点的监控、排错等)、可持续(满足系统功能的可持续集成、
以及系统运行的可持续性)以及高效(系统运行的高效、人员协同工作的高效、功能迭代的高效等)。

微服务通俗讲解

Spring Cloud提供了微服务解决的一整套方案,而Eureka是其重要组件,所以先要了解什么是“微服务”。

在大型系统架构中,会拆分多个子系统。这些系统往往都有这几个功能:提供接口,调用接口,以及该子系统自身的业务功能。这样的一个子系统就称为一个“微服务”。(可以理解为一个子系统的代码所实现的功能)

实例:
每个服务都会部署到多个机器(或镜像)中,这些多个部署的应用就是实例。(可以理解为一套子系统代码被部署到了多个机器上)

Eureka的管理

基于以上概念,使用Eureka管理时会具备几个特性:

  • 服务需要有一个统一的名称(或服务ID)并且是唯一标识,以便于接口调用时各个接口的区分。并且需要将其注册到Eureka Server中,其他服务调用该接口时,也是根据这个唯一标识来获取。
  • 服务下有多个实例,每个实例也有一个自己的唯一实例ID。因为它们各自有自己的基础信息如:不同的IP。所以它们的信息也需要注册到Eureka Server中,其他服务调用它们的服务接口时,
    可以查看到多个该服务的实例信息,根据负载策略提供某个实例的调用信息后,调用者根据信息直接调用该实例。

eureka如何管理服务调用

  • 在Eureka Client启动的时候,将自身的服务的信息发送到Eureka Server。然后进行2调用当前服务器节点中的其他服务信息,保存到Eureka Client中。当服务间相互调用其它服务时,在Eureka Client中
    获取服务信息(如服务地址,端口等)后,进行第3步,根据信息直接调用服务。(注:服务的调用通过http(s)调用)

  • 当某个服务仅需要调用其他服务,自身不提供服务调用时。在Eureka Client启动后会拉取Eureka Server的其他服务信息,需要调用时,在Eureka Client的本地缓存中获取信息,调用服务。

  • Eureka Client通过向Eureka Serve发送心跳(默认每30秒)来续约服务的。 如果客户端持续不能续约,那么,它将在大约90秒内从服务器注册表中删除。 注册信息和续订被复制到集群中的Eureka Serve所有节点。 以此来确保当前服务还“活着”,可以被调用。

  • 来自任何区域的Eureka Client都可以查找注册表信息(每30秒发生一次),以此来确保调用到的服务是“活的”。并且当某个服务被更新或者新加进来,也可以调用到新的服务。

Eureka Server:

  • 提供服务注册:各个微服务启动时,会通过Eureka Client向Eureka Server进行注册自己的信息(例如服务信息和网络信息),Eureka Server会存储该服务的信息。

  • 提供服务信息提供:服务消费者在调用服务时,本地Eureka Client没有的情况下,会到Eureka Server拉取信息。

  • 提供服务管理:通过Eureka Client的Cancel、心跳监控、renew等方式来维护该服务提供的信息以确保该服务可用以及服务的更新。

  • 信息同步:每个Eureka Server同时也是Eureka Client,多个Eureka Server之间通过P2P复制的方式完成服务注册表的同步。同步时,被同步信息不会同步出去。也就是说有3个Eureka Server,Server1有新的服务信息时,同步到Server2后,Server2和Server3同步时,Server2不会把从Server1那里同步到的信息同步给Server3,只能由Server1自己同步给Server3。

  • 每个可用区有一个Eureka集群,并且每个可用区至少有一个eureka服务器来处理区内故障。为了实现高可用,一般一个可用区中由三个Eureka Server组成。

Eureka Client:

  • Eureka Client是一个Java客户端,用于简化与Eureka Server的交互。并且管理当前微服务,同时为当前的微服务提供服务提供者信息。

  • Eureka Client会拉取、更新和缓存Eureka Server中的信息。即使所有的Eureka Server节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者。

  • Eureka Client在微服务启动后,会周期性地向Eureka Server发送心跳(默认周期为30秒)以续约自己的信息。如果Eureka Server在一定时间内没有接收到某个微服务节点的心跳,Eureka Server将会注销该微服务节点(默认90秒)。

  • Eureka Client包含服务提供者Applicaton Service和服务消费者Application Client

  • Applicaton Service:服务提供者,提供服务给别个调用。

  • Application Client:服务消费者,调用别个提供的服务。

  • 往往大多数服务本身既是服务提供者,也是服务消费者。

动作

Register:服务注册

当Eureka客户端向Eureka Server注册时,它提供自身的元数据,比如IP地址、端口,运行状况指示符URL,主页等。

Renew:服务续约

Eureka Client会每隔30秒发送一次心跳来续约。 通过续约来告知Eureka Server该Eureka客户仍然存在,没有出现问题。 正常情况下,如果Eureka Server在90秒没有收到Eureka客户的续约,它会将实例从其注册表中删除。 建议不要更改续约间隔。

Fetch Registries:获取注册列表信息

Eureka客户端从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与Eureka客户端的缓存信息不同, Eureka客户端自动处理。如果由于某种原因导致注册列表信息不能及时匹配,Eureka客户端则会重新获取整个注册表信息。 Eureka服务器缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka客户端和Eureka 服务器可以使用JSON / XML格式进行通讯。在默认的情况下Eureka客户端使用压缩JSON格式来获取注册列表的信息。

Cancel:服务下线

Eureka客户端在程序关闭时向Eureka服务器发送取消请求。 发送请求后,该客户端实例信息将从服务器的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:

DiscoveryManager.getInstance().shutdownComponent();

Eviction:服务剔除

在默认的情况下,当Eureka客户端连续90秒没有向Eureka服务器发送服务续约,即心跳,Eureka服务器会将该服务实例从服务注册列表删除,即服务剔除。