-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 475 KB
/
content.json
1
{"pages":[{"title":"about","text":"#About Me 互联网行业程序员一名,主要从事搜索工程架构、微服务工程架构,主要使用Java语言。希望能够把自己的经验总结和研究成果记录下来,形成完善的知识体系。主要内容包括算法数据结构,Java,微服务体系,Spring Cloud,大数据开发等,当然也可能会有生活,艺术,旅行等等的文章。 也希望自己的随笔能够帮助到大家,赠人玫瑰,手留余香!如果文章中有不正确的,欢迎批评指出,共同进步。 联系方式邮箱:[email protected] 觉得我写的不错,就拿钱砸我。哈哈","link":"/about/index.html"}],"posts":[{"title":"单例模式","text":"单例模式单例模式分为了饿汉式和懒汉式,总体来说懒汉式要优于饿汉式,饿汉式不管是否其他线程调用了getInstance,都在类加载阶段创建了实例。而懒汉式则只有在调用的时候,才实例化对象,更加节省系统资源。 饿汉式: 12345678public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton(){} public static Singleton getInstance() { return INSTANCE; }} 懒汉式-双重检查 1234567891011121314151617181920/** * 懒汉式-双重检查 */public class SingletonLazy { private static SingletonLazy instance = null; private SingletonLazy() { } public static SingletonLazy getInstance() { if (instance == null) { synchronized (SingletonLazy.class) { if (instance == null) { instance = new SingletonLazy(); } } } return instance; }} 懒汉式-内部类 1234567891011121314151617181920212223/** * 懒汉式-内部类 */public class SingletonLazy1 { private SingletonLazy1() { } private static class InnerSingleton { private final static SingletonLazy1 instance = new SingletonLazy1(); } public static SingletonLazy1 getInstance() { return InnerSingleton.instance; } public static void helloworld() { System.out.println(\"hello lazy singleton.\"); }} 执行Main方法测试,从输出结果看,只有执行了SingletonLazy1.getInstance()方法,才开始加载内部类SingletonLazy1$InnerSingleton。 123456789101112131415161718public class Main { public static void main(String[] args) throws InterruptedException{ SingletonLazy1.helloworld(); Object lock = new Object(); synchronized (lock) { lock.wait(1000); } System.out.println(\"分割线---------------\"); System.out.println(SingletonLazy1.getInstance()); }}输出结果:[Loaded sun.nio.cs.US_ASCII$Decoder from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]分割线---------------[Loaded sun.misc.VMSupport from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar][Loaded com.wsy.learn.designmodel.SingletonLazy1$InnerSingleton from file:/Users/wangsiyuan1/workspace/springtest/target/classes/]com.wsy.learn.designmodel.SingletonLazy1@27716f4","link":"/2018/12/11/design/单例模式/"},{"title":"JVM内存区域","text":"JVM内存区域总体介绍其中VM Stack(虚拟机栈),Native Method Stack(本地方法栈),Program Counter Register(程序计数器),是线程私有的。Method Area(方法区),Heap(堆)是线程共享的。 注意:没有特殊说明,本文中所讲的都是基于HotSpot虚拟机。 ##程序计数器 程序计数器是线程私有的,是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型中(也仅是概念模型,各个虚拟机可能通过更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 多线程场景下,为了线程切换后能恢复到正确的位置,所以每个线程需要一个独立的程序计数器。 如果线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,这个计数器值则为空(Undefined)。次内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。 Java虚拟机栈Java虚拟机栈,也是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 局部变量表存放了编译器可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double)、对象引用(reference类型,它不等同于对象本身,可能是指向对象起始地址的引用指针,也可能是代表对象的句柄)。局部变量表所需的内存空间在编译期间完成分配,方法在帧中分配多大的空间,也是完全确定的。 这个区域存在两种异常情况: 当栈的深度超过虚拟机所允许的深度的时候,抛出StackOverflowError。 如果虚拟机栈允许动态扩展,如果无法申请到足够的内存,则会抛出OutOfMemoryError。 本地方法栈本地方法栈的功能和Java虚拟机栈很接近,它们的区别是Java虚拟机栈是为虚拟机执行Java代码(字节码)服务,本地方法栈则为虚拟机使用到的Native方法服务。本地方法栈,也存在StackOverflowError和OutOfMemoryError。 Java堆对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建。此内存区域的唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存。但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有对象都分配到堆上也变得不那么绝对了。 Java堆是垃圾收集器管理的主要区域,从内存回收角度来看,由于现在收集器基本都采用分代收集算法,Java堆中还分为了新生代和老年代。当堆无法继续扩展的时候,将会抛出OutOfMemoryError。 方法区方法区(Method Area)也是线程共享区域,它用于存储被虚拟机加载的类元信息、常量、静态变量、及时编译器编译后的代码等数据。JDK8之前,虚拟机采用永久代实现方法区,类元信息、常量、静态变量等都存在堆中,和年轻代、老年代是连续的。 12345-XX:PermSize方法区初始大小-XX:MaxPermSize方法区最大大小超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen Jdk8之后,官方废弃了永久代,在本地内存中开辟了一块空间,叫做元空间(MetaSpace),专门存储类的元数据信息。常量、静态变量存储到了堆上。 直接内存很多人忽略了直接内存,举一个例子,JDK NIO中,Buffer调用的Native方法直接分配是堆外内存,也就是直接占用系统内存。所以在调整JVM参数的时候,要给系统留一些Buffer。避免内存动态扩展时出现问题。","link":"/2018/08/11/java/JVM内存区域/"},{"title":"Java-Provider一种写法","text":"12345678910111213141516171819202122232425262728293031323334353637383940414243444546public class Test { public static void main(String[] args) { Map<String, NodeProvider<Node>> map = new HashMap<>(); map.put(\"ik\", TestInner::getNode); System.out.println(map.get(\"ik\").get(\"name\", 1).getName()); System.out.println(map.get(\"ik\").get(\"name\", 200).getAge()); }}interface NodeProvider<T> { T get(String name, int age);}class Node { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; }}class TestInner { public static Node getNode(String name, int age) { Node node = new Node(); node.setAge(age); node.setName(name); return node; }}","link":"/2019/07/10/java/Java-Provider的一种写法/"},{"title":"生产者消费者模式-Java","text":"生产者消费者模式生产者-消费者模式在服务端编程中,是一种很常见的设计模式,比如消息队列的实现,就是这种思想。本文就是用Java语言编写一个简单的生产者消费者例子,从而引出concurrent包下的阻塞队列和ReentrantLock一些玩法。 ##基础知识 首先复习下基础知识,在Java中concurrent包下并发队列分为阻塞队列和非阻塞队列,ConcurrentLinkedQueue是非阻塞队列,底层实现用了CAS。阻塞队列包括LinkedBlockingQueue,LinkedBlockingDeque,LinkedTransferQueue,ArrayBlockingQueue,阻塞队列底层是靠ReentrantLock实现。Condition包括await,signal,signalAll,Condition作为条件锁 我们知道Lock的本质是AQS,AQS自己维护的队列是当前等待资源的队列,AQS会在被释放后,依次唤醒队列中从前到后的所有节点,使他们对应的线程恢复执行,直到队列为空。 而Condition自己也维护了一个队列,该队列的作用是维护一个等待signal信号的队列。 但是,两个队列的作用不同的,事实上,每个线程也仅仅会同时存在以上两个队列中的一个,流程是这样的: 1、线程1调用reentrantLock.lock时,尝试获取锁。如果成功,则返回,从AQS的队列中移除线程;否则阻塞,保持在AQS的等待队列中。 2、线程1调用await方法被调用时,对应操作是被加入到Condition的等待队列中,等待signal信号;同时释放锁。 3、锁被释放后,会唤醒AQS队列中的头结点,所以线程2会获取到锁。 4、线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。注意,这个时候,线程1 并没有被唤醒,只是被加入AQS等待队列。 5、signal方法执行完毕,线程2调用unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,线程1被唤醒,线程1恢复执行。 所以,发送signal信号只是将Condition队列中的线程加到AQS的等待队列中。只有到发送signal信号的线程调用reentrantLock.unlock()释放锁后,这些线程才会被唤醒。可以看到,整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作。 signal就是唤醒Condition队列中的第一个非CANCELLED节点线程,而signalAll就是唤醒所有非CANCELLED节点线程,本质是将节点从Condition队列中取出来一个还是所有节点放到AQS的等待队列。尽管所有Node可能都被唤醒,但是要知道的是仍然只有一个线程能够拿到锁,其它没有拿到锁的线程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。 ##生产者-消费者代码 1234567891011121314/** * 用阻塞队列实现生产-消费者模式 */public class Productor { static LinkedBlockingQueue<Integer> blockQueue = new LinkedBlockingQueue<>(10); void provide() throws InterruptedException { for (int i = 0; i < 10; i++) { blockQueue.offer(i); System.out.println(\"生产:\" + i); Thread.sleep(100); } }} 12345678910/** * 基于阻塞队列消费者 */public class Consumer { void cusume() throws InterruptedException { while (!Thread.currentThread().isInterrupted()) { System.out.println(\"消费:\" + Productor.blockQueue.take()); } }} 123456789101112131415161718192021222324252627282930313233343536373839404142434445static void test0() { //创建生产者 Productor productor = new Productor(); //创建消费者 Consumer consumer = new Consumer(); Thread cusumerThread = new Thread(() -> { try { consumer.cusume(); } catch (Exception e) { } }); cusumerThread.start(); try { productor.provide(); cusumerThread.interrupt(); } catch (Exception e) { e.printStackTrace(); }}输出:生产:0消费:0生产:1消费:1生产:2消费:2生产:3消费:3生产:4消费:4生产:5消费:5生产:6消费:6生产:7消费:7消费:8生产:8生产:9消费:9 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152/*** 自己实现的简单的阻塞队列*/public class MyBlockQueue<E> { private LinkedList<E> queue = new LinkedList<>(); //第一种实现方式,采用了Object wait和notify的方式 private Object lock = new Object(); //第二种实现方法,采用了ReentrantLock获取Condition,通过condition await和signal方式实现 private ReentrantLock reentrantLock = new ReentrantLock(); Condition con = reentrantLock.newCondition(); void offer(E e) { queue.offer(e); synchronized (lock) { lock.notifyAll(); } } E take() throws InterruptedException { if (queue.size() == 0) { synchronized (lock) { lock.wait(); } } return queue.poll(); } void offer1(E e) throws InterruptedException { try { reentrantLock.lockInterruptibly(); queue.offer(e); con.signalAll(); } finally { reentrantLock.unlock(); } } E take1() throws InterruptedException { if (queue.size() == 0) { try { reentrantLock.lockInterruptibly(); con.await(); } finally { reentrantLock.unlock(); } } return queue.poll(); }}","link":"/2018/12/11/java/Java生产者消费者模式/"},{"title":"ThreadLocal原理分析","text":"ThreadLocal总体介绍 ThreadLocal类在并发编程中,每个线程实例,都有一份独立的副本,采用以空间换时间的方式,处理并发。 在Thread类中,ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocal.ThreadLocalMap就是线程变量的容器。 123456789101112131415161718192021 static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as \"stale entries\" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ...} 如上代码可以看到Entry继承了WeakReference,在构造函数中super(key),key就是指ThreadLocal引用本身this。跟WeakHashMap不同的是,ThreadLocal的构造函数,没有指定ReferenceQueue。ThreadLocal用一种不同的方式,来避免内存泄漏,在下面的章节中,会详细介绍ThreadLocal的方式。 ThreadLocal的API,就不在此阐述,很简单。 每个线程独立备份 为了达到这个目的,有两个方案。 ## 方案A ThreadLocal 维护一个 Map,键是 Thread,值是它在该 Thread 内的实例。增加线程与减少线程均需要写 Map,故需保证该 Map 线程安全。线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存泄漏。 结论:加锁势必会降低ThreadLocal的性能。猜测JDK为了性能考虑,没有采用此方案。 方案BMap 由 Thread 维护,从而使得每个 Thread 只访问自己的 Map,那就不存在多线程写的问题,也就不需要锁。该方案虽然没有锁的问题,但是由于每个线程访问某 ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLocal 变量与具体实例的映射,如果不删除这些引用(映射),则这些 ThreadLocal 不能被回收,可能会造成内存泄漏。 结论:JDK采用了B方案,也有对应的方案来解决内存泄漏问题。 #JDK如何解决内存泄漏问题","link":"/2018/08/11/java/ThreadLocal原理/"},{"title":"Java对象刨根问底","text":"如何创建一个对象Java创建一个对象,最简单的方式就是 1Object a = new Object(); 可以,大家有没有想过,在JVM中,创建一个对象的流程是如何呢?其实具体流程入下图所示。 ###分配内存 对象所需的内存大小,在类加载完成后便可以完全确认,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。那么如何划分,有两种方式: 指针碰撞(Bump The Pointer):假设Java堆中的内存是绝对规整的,所有用过的内存都放到一边,空闲的内存放到另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就是朝着空闲的那一边“挪动”与对象大小一样的距离。 空闲列表(Free List):如果Java堆中的内存不规整,已使用的内存和未使用的内存相互交错,就没办法简单进行指针碰撞了,需要维护一个列表,记录哪些内存块是可用的,在分配的时候,找一块足够大的空间分配给对象,并更新列表的记录。 因此,在使用Serial,ParNew等带Compact过程的收集器时,系统采用的分配方式是指针碰撞法,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用的是空闲列表法。 除了分配方法外,还需要考虑同步的问题。实际上虚拟机采用了CAS配上失败重试的方式保证更新的原子性,另一种是把内存分配的动作按照线程划分在不同的空间之中,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),可以通过-XX:+UseTLAB参数来设定。 对象内存布局在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 对象头HotSpot虚拟机对象头包括两部分内容: 第一部分 用于存储对象自身的运行数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部分长度在32bit和64bit虚拟机中分别为32bit和64bit,官方称它为“Mark Word”。 存储内容 标志位 状态 对象哈希码、对象分代年龄 01 未锁定 指向锁记录的指针 00 轻量级锁定 指向重量级锁的指针 10 膨胀(重量级锁定) 空,不需要记录信息 11 GC标记 偏向线程ID,偏向时间戳,对象分代年龄 01 可偏向 第二部分 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 对象访问定位有两种方式,句柄访问和直接指针访问。这两种访问方式各有优点,使用句柄的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收移动对象是很常见的事情)时只会改变句柄中的实例数据指针,而reference本身不需要更新。使用直接指针访问方式最大好处就是速度更快,它节省一次指针定位的时间开销,由于对象访问在Java中非常频繁,这类开销积少成多,将是一个比较可观的提升。HotSpot就是采用直接指针法。 通过句柄访问对象 通过直接指针访问对象","link":"/2018/12/11/java/java创建对象的流程/"},{"title":"WeakHashMap原理分析","text":"WeakHashMap总体介绍 WeakHashMap继承自AbstractMap,实现了Map接口,拥有了Map的基础功能。但是与常用的Map(比如HashMap)不同的是,WeakHashMap底层的存储单元Entry采用WeakReference,在GC的时候,会回收K,V。所以WeakHashMap天然比较适合用作缓存,即使K,V丢失,也不会对业务造成影响。 12345678910111213141516171819private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> { V value; final int hash; Entry<K,V> next; /** * Creates new entry. */ Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) { super(key, queue); this.value = value; this.hash = hash; this.next = next; } ...} 如上代码可以看到Entry继承了WeakReference,在构造函数中super(key, queue)传入了key和ReferenceQueue。 在调用get(),put(),size()等方法的时候,会执行一个私有方法expungeStaleEntries,如下所示。 123456789101112131415161718192021222324252627282930private void expungeStaleEntries() { //迭代弱引用队列中要被回收的对象 for (Object x; (x = queue.poll()) != null; ) { synchronized (queue) { @SuppressWarnings(\"unchecked\") Entry<K,V> e = (Entry<K,V>) x; int i = indexFor(e.hash, table.length); //操作链表 Entry<K,V> prev = table[i]; Entry<K,V> p = prev; while (p != null) { Entry<K,V> next = p.next; if (p == e) { if (prev == e) table[i] = next; else prev.next = next; // Must not null out e.next; // stale entries may be in use by a HashIterator //敲黑板,这行代码很重要,如果不执行e.value = null,map的value会泄漏。 e.value = null; // Help GC size--; break; } prev = p; p = next; } } }} #图解WeakHashMap原理 图1 如图1所示,key1,key2,key3虚引用指向Entry1,Entry2,Entry3,每个entry又分别指向v1,v2,v3。GC之后,key2虚引用被垃圾回收。通过执行expungeStaleEntries方法,使得Entry2变成游离态,Entry2也和v2脱离了引用关系。所以Entry2,v2都变成了可被垃圾回收的状态。 图2","link":"/2018/08/11/java/WeekHashMap原理分析/"},{"title":"java引用详解","text":"一般情况下,java开发中用的都是强引用。例如如下代码所示。 1Object ref = new Object(); 实际上在java中,还有一种不太常用的“弱引用”类型。弱引用中分为了软引用(SoftReference),弱引用(WeakReference),虚引用(PhantomReference)。所谓的强引用,其实就是FinalReference。 问题随之而来,为什么引用要分为强弱?弱引用又适用于何种场景?本文结合源码,带大家了解java引用的世界。 引用强弱之分如果大家对JVM内存回收(GC)有一定了解,一定知道内存回收的前提是对象引用“不可达‘’,才能在回收阶段真正的释放掉对象占用的内存。强引用下,如果对象的引用一直处于可达状态,这块内存是没有办法回收的。在内存资源充沛的情况下还好,但是如果内存资源吃紧,为了服务的可用性,一些“不必要”,或者说“不那么重要的”内存,是不是可以释放掉?这个时候,弱引用就派上了用场。各个引用类的对比,请参考下标 引用类型 回收方式 必须配合ReferenceQueue 说明 FinalReference 不回收 否 默认情况下使用强引用 SoftReference 内存不充足情况下,GC之后回收,如果使用了queue同WeakReference一样的处理 否 非必要对象,为了内存空间 WeakReference GC立刻回收,如果使用了queue,需手动把要回收的对象置为null 否 非必要对象,为了内存空间 PhantomReference(幽灵引用) GC立刻回收 是 get默认返回null,使用虚引用的目的是通过queue监听对象回收 弱引用典型应用WeakHashMap,ThreadLocal,JVM缓存实现。 源码分析下图是java.lang.ref包下的类图,四种引用全部继承自Reference。 JDK把引用分为了四个状态。 状态 判定条件 说明 源码注释 Active (queue==ReferenceQueue\\ \\ ReferenceQueue.NULL) && next == null 新创建的引用对象是这个状态。在GC检测到引用对象已经到达合适的Reachability(可达性)时,GC会根据引用对象在创建时是否指定ReferenceQueue参数进行状态转移,如果指定则转移到pending,否则直接转移到Inactive。 Active: Subject to special treatment by the garbage collector. Some time after the collector detects that the reachability of the referent has changed to the appropriate state, it changes the instance’s state to either Pending or Inactive, depending upon whether or not the instance was registered with a queue when it was created. In the former case it also adds the instance to the pending-Reference list. Newly-created instances are Active. Pending queue == RefrenceQueue && next == this (jvm设置) pending-Refence链表中引用都是这个状态,它们等着被内部线程ReferenceHandler处理入队(调用RefenceQueue.enqueue方法),没有注册的实例不会进入此状态。 An element of the pending-Reference list, waiting to be enqueued by the Reference-handler thread. Unregistered instances are never in this state. Enqueued queue == ReferenceQueue.ENQUEUED && next == 下一个要处理的Reference对象,或者链表中最后一个next == this 相应的对象已经为待回收并放到queue中,准备由外部线程来询问queue获取相应的数据。调用ReferenceQueue.enqued方法后的Reference对象处于这个状态,当Reference实例从队列中移除之后,它的状态改为Inactive,没有注册的实例不会进入该状态。 Enqueued: An element of the queue with which the instance was registered when it was created. When an instance is removed from its ReferenceQueue, it is made Inactive. Unregistered instances are never in this state. Inactive queue == ReferenceQueue.NULL && next == this 即此Reference对象已由外部queue中获取到,并且已经处理掉了。即意味着此对象是可以被回收的,并且对内部封装的对象也可以被回收掉了(具体的回收运行,取决于Clear动作是否被调用,可以理解为进入此状态的Reference对象是应该被回收掉的)一旦Reference对象变成Inactive,它的状态就不会再变化。 Nothing more to do. Once an instance becomes Inactive its state will never change again. 基于上表的讲解,画出了如下引用状态流程图。","link":"/2018/08/11/java/java引用/"},{"title":"java类加载之ClassLoader","text":"何为类加载大家知道Java是一门编译型语言,源代码文件是以.java结尾的。通过编译器编译之后,会形成一个字节码文件,也就是class文件。在一个应用中,不同的class文件中封装着不同的功能,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。 类加载器BootstrapClassLoader:称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件: 1234URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();for (int i = 0; i < urls.length; i++) { System.out.println(urls[i].toExternalForm());} 如下是BootstrapClassLoader加载的类: file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/resources.jarfile:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jarfile:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/sunrsasign.jarfile:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/jsse.jarfile:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/jce.jarfile:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/charsets.jarfile:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/jfr.jarfile:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/classes ExtClassLoader: 称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。 AppClassLoader: 称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。可以说应用程序自己定义的类,都是由AppClassLoader负责加载。 CustomClassLoader: 除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。 ClassLoader加载类原理ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。 ##为何需要双亲委派 因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。 JDK如何判定Class相同JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。 如何定义CustomClassLoader如下代码,展示了本地目录的类加载器。 123456789101112131415161718192021222324252627282930313233343536373839public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + \".class\"; }}","link":"/2018/08/11/java/java类加载之ClassLoader/"},{"title":"阿里巴巴LTR Qcon分享笔记","text":"LTR, Leaning To Rank,是一种基于机器学习的Rank方法。 SENP : Search Engine Result Page 搜索引擎结果页面 宏观上看,分为了三类,分别是 PointWise,PaireWise,ListWise。 parameter of the classifier should be tuned to optimize the NDCG score on the cross validation set.query full:SERPs returned in response to a query.query less:SERPs teturned in response to the user click on some product category. i.i.d :独立同分布independent and identically distributed Personalized E-Commerce Search 个性化电子商务搜索 Predict relevance scores and re-rank products returned by an e- commerce search engine on the search engine result page 对搜索引擎结果页面中的item,预测相关性得分和re-rank 使用的数据 Search, browsing, and transaction histories for all users and specifically the user interactingwith the search engine in the current session 所有用户的搜索,浏览,交易历史,特别是在当前搜索引擎session中用户与系统交互的行为 Product properties and meta-data 商品特征和元数据 Data Using 使用的方法 Matchine Learning (e.g. RankSVM, LambdaMart) Ranking Function(e.g. BM25, Cosine Similarity) Theory理论 (PAC) Generalization Stability Applications应用 Search 搜索 Recommender System 推荐系统 Question Answering 问答系统 Sentiment Analysis 情感分析,在电商领域,用户评论数据可以用情感分析模型,分析出用户对商品是否满意 Formulation LTR的Formulation Machine Learning Supervised learning with labeled data 使用标记数据进行监督学习,日志分析,点击量,点击停留时间等等 Ranking of objects by subject Feature based ranking function 基于特征的排序方法 Approach Traditional:BM25 (Probabilistic Model) 概率模型 New Query and associated products form Group (Train Data) query的结果集称作group Groups are i.i.d group之间是独立同分布 Features (query and product) in Group are not i.i.d group中的特征不是独立同分布 Model is a function of features 特征产生函数 Issues Data Labeling 打标数据(训练集) Relevance metric (Point) 跟CTR预估有点像,点了或者没点 Ordered pairs Ordered list 对排列组合取最大概率的排列 Feature Extraction 特征提取 (非常非常重要) Relevance (User/Query-Prod Feature) 用户的意图(历史行为或者当前的行为)和文档的属性有match Semantic (User/Query-Prod Feature) 语义相关性 LDA,现在流行的是 deepLearning CNN Importance (Prod Feature) doc本身的重要性,比如sku的各种重要属性,类似PageRank是网页本身重要的特种 Learning Method 学习方法 Model 模型选择,要结合业务,要最合适的,需要对数据的理解,业务的理解 Lose Function 损失函数,比如交叉熵。遇到不平滑或者不好求导的的情况,所以很多情况直接选择比较好求导的损失函数 Optimized Algorithms 优化算法,随机梯度下降 Evaluation Measure 评估测量 NDCG 当前排序结果和预期排序结果的比值 Machine Learning Classification 分类 Regression 回归 Ordinal Classification/Regression Ordinal Regression 序数回归 Pointwise Transfer ranking to regression 转变ranking为回归问题 Ignore group info 忽略group信息 Learning to Rank Pairwise Transfer ranking to binary classification 转换ranking为二分类问题 Listwise Straightforward represent learning 直接了当学习 Pointwise Model用的不多 McRank(2007) Ordinal Liner Regression (1992) PaireWise Model比较流行 RankSVM(2000) Pairwise classification IR SVM Cost-sensitive Pairwise 排序结果A B C, A比C好很重要,C比A好很差劲 Using modified hinge loss RankBoost (2003) GBDT RankNet (2005) 神经网络 LambdaMart (2008) 最流行,既可以paireWise也可以Listwise Listwise Model 目标是 :正确的那个排序结果的概率最大 Plackett-Luce Model ListMLE (2008) ListNet (2007) Parameterized Plackett-Luce Model AdaRank (2007) PermuRank (2008) SVM-Map (2007) Optimize 优化 Direct Optimization 直接优化 AdaRank SVM Map Approximation 近似 Soft Rank Lambda Rank Learning Framework 学习框架 Data Representation 数据表示 Expected Risk 期望风险 Empirical Risk 经验风险 Generalization Analysis 概括分析 Evaluation 总结 Pairwise approach and Listwise approach perform better than Pointwiseapproach Applications Search Re-Ranking 重排序 Recommender System Collaborating Filter 协同过滤 PERSONALIZED E-COMMERCE SEARCH 个性化电商搜索 Pertinence 如何评价搜索的效果好坏 Log Analysis 通过分析日志分析来评价 Conversion in E-commerce 通过电商转化率评价,但是转化率的评价维度较多。比如通常意义上购买>加购>收藏,但是在双11前夕,用户有提前加购,收藏,等待双11零点下单的习惯,这个场景下,加购或者收藏的价值不低于购买。 Data 数据来源 User info 用户信息:基本信息,行为日志等 List of the terms that forms the query 检索词的term集合 Displayed items and their domains 展示的数据信息,在电商中指商品列表 Items on which the user clicked 用户点击了哪些item Timing of all of these actions 用户行为的时间轴,比如分析留长等等,用户的行为都是用时间串联起来的 History Behaviors Day 28 to Day 30 近一个月历史的行为数据 Ensemble Model 混合模型 Boosting Bagging Stacking Trap 需要注意的陷阱 Position Bias 偏见 人对排在前面的有天然的好感,自然的从上往下读。当用户输入一个比较泛指的词时,说明他的意图不明确(自己都不知道想要什么),比如输入“裙子”,此刻搜索引擎给出的结果也只能是泛泛的,用户点击了某一个item,可能并不是他真正的意图,但至少在页面中,点击了的商品比没点的要好。 线上的所有的日志和效果,都是由上一个模型产生的,上一个模型会引导用户的行为,导致产生了噪声数据,所以采集的数据并不是一个shuffle的结果。如果有条件,可以采用分桶实验。 Clicks feedback When to do personalize Long Term 长期的偏好:比如一个用户每2个月买一次牙膏 Short Term 短期的偏好:基于当前会话session的 Past interaction timescales 历史交互时间轴数据 Search behaviors timescales 搜索行为时间轴数据 Learing from all repeated results Features 特征 Aggregate features 聚合特征 Query features 检索特征 User click habits 用户点击习惯 Number of times the user clicked on the item in the past Session features Session会话级别的特征 Non-Personalized Rank 非个性化Rank Read linearly Computed with infomation Inhibiting/Promoting features Query click entropy Methodology Classification will be used Parameter of the classifier should be tuned optimize the NDCG score on the cross validation set. Query Full SERPs returned in response to a query Query Less SERPs returned in response to the user click Ensemble Model 混合Model A very powerful technique to increase accuracy on a variety of ML tasks Boosting Bagging Ensemble Correlation Voting Weighing Averaging Rank averaging","link":"/2019/01/21/ml/阿里巴巴LTR Qcon分享笔记/"},{"title":"java类加载内部流程","text":"Java类加载机制的七个阶段而 JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。 ##加载 下面是对于加载过程最为官方的描述。 加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。 其实加载阶段用一句话来说就是:把代码数据加载到内存中。这个过程对于我们解答这道问题没有直接的关系,但这是类加载机制的一个过程,所以必须要提一下。 验证当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。这个校验过程大致可以分为下面几个类型: JVM规范校验。JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。例如:文件是否是以 0x cafe bene开头,主次版本号是否在当前虚拟机处理范围之内等。 代码逻辑校验。JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。一个方法要求返回 String 类型的结果,但是最后却没有返回结果。代码中引用了一个名为 Apple 的类,但是你实际上却没有定义 Apple 类。 当代码数据被加载到内存中后,虚拟机就会对代码数据进行校验,看看这份代码是不是真的按照JVM规范去写的。这个过程对于我们解答问题也没有直接的关系,但是了解类加载机制必须要知道有这个过程。 ##准备(重点) 当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。 内存分配的对象。Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。 例如下面的代码在准备阶段,只会为 factor 属性分配内存,而不会为 website 属性分配内存。 12public static int factor = 3;public String website = "www.cnblogs.com/chanshuyi"; 初始化的类型。在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。 例如下面的代码在准备阶段之后,sector 的值将是 0,而不是 3。 1public static int sector = 3; 但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。 1public static final int number = 3; 之所以 static final 会直接被复制,而 static 变量会被赋予零值。其实我们稍微思考一下就能想明白了。 两个语句的区别是一个有 final 关键字修饰,另外一个没有。而 final 关键字在 Java 中代表不可改变的意思,意思就是说 number 的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,因此被 final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。 解析当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。 其实这个阶段对于我们来说也是几乎透明的,了解一下就好。 ##初始化(重点) 到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化: 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。 看到上面几个条件你可能会晕了,但是不要紧,不需要背,知道一下就好,后面用到的时候回到找一下就可以了。 ##使用 当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。这个阶段也只是了解一下就可以。 ##卸载 当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。这个阶段也只是了解一下就可以。 总结从上面几个例子可以看出,分析一个类的执行顺序大概可以按照如下步骤: 确定类变量的初始值。在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。 初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。 初始化类构造器。JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。 初始化对象构造器。JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。 如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。 引用https://www.cnblogs.com/chanshuyi/p/the_java_class_load_mechamism.html","link":"/2018/12/11/java/java类加载内部流程/"},{"title":"ElasticSearch之updateByQuery使用","text":"#背景 有时需要通过某些查询条件,更新文档某一个字段,这样的诉求,用SQL表达就是update t_student set class=’优秀的小男生’ where age = 10 and score = 100 and sex = 1。ES有updateByQuery API 和 deleteByQuery API,这样一条DSL就可以搞定类似的需求。 例子批量更新id = 9627361的文档,admin_rank数值++,address更新成”我是_update_by_query”,name_alias_array字段,更新为一个集合。 12345678910111213141516POST hotel_20190513/hotel/_update_by_query{ \"script\": { \"inline\": \"ctx._source.admin_rank++;ctx._source['address']='我是_update_by_query';ctx._source['name_alias_array']=['哈哈','hei嘿']\", \"lang\": \"painless\" }, \"query\": { \"bool\": {\"must\": [ {\"term\": { \"id\": { \"value\": 9627361 } }} ]} }} 删除entity_type = hotel_name的所有文档。 12345678910POST entity_hotel_20190528/entity_hotel/_delete_by_query{ \"query\": { \"term\": { \"entity_type\": { \"value\": \"hotel_name\" } } }} golang例子 123456789101112131415updateByQuery := client.UpdateByQuery().Query(elastic.NewTermQuery(\"entity_id\", node.TagId)).Script(elastic.NewScriptInline(script)).Index(index).Type(indexType) out, err := updateByQuery.Do(context.TODO()) if err != nil { panic(err) } b, err := json.Marshal(out) if err != nil { panic(err) } got := string(b) updated := tool.GoJson(got).Get(\"updated\").ToString() if tool.IsEmpty(updated) { updated = \"0\" } println(\"执行成功.tagId=\" + tool.ToString(node.TagId) + \"更新\" + updated + \"个文档.\") 参考文档官方文档","link":"/2019/06/13/search/ElasticSearch-updateByQuery/"},{"title":"ElasticSearch Scroll","text":"ElasticSearch Scroll##深分页 比如pageIndex=10,pageSize=20,{“start”: 180, “size”: 20},相当于每个分片召回了200条数据,排序之后截断前20条返回。但是如果pageIndex=1000,每个分片需要召回20000条数据,排序,最后仍然是取前20条,扔掉19980条。深分页的实现方式代价是很大的。 Scroll为了解决以上深分页,在页码太大情况下性能的问题,ES提供了Scroll方式。 scroll 查询 可以用来对 Elasticsearch 有效地执行大批量的文档查询,而又不用付出深度分页那种代价。 游标查询允许我们 先做查询初始化,然后再批量地拉取结果。 这有点儿像传统数据库中的 cursor 。 游标查询会取某个时间点的快照数据。 查询初始化之后索引上的任何变化会被它忽略。 它通过保存旧的数据文件来实现这个特性,结果就像保留初始化时的索引 视图 一样。 深度分页的代价根源是结果集全局排序,如果去掉全局排序的特性的话查询结果的成本就会很低。 游标查询用字段 _doc 来排序。 这个指令让 Elasticsearch 仅仅从还有结果的分片返回下一批结果。 启用游标查询可以通过在查询的时候设置参数 scroll 的值为我们期望的游标查询的过期时间。 游标查询的过期时间会在每次做查询的时候刷新,所以这个时间只需要足够处理当前批的结果就可以了,而不是处理查询结果的所有文档的所需时间。 这个过期时间的参数很重要,因为保持这个游标查询窗口需要消耗资源,所以我们期望如果不再需要维护这种资源就该早点儿释放掉。 设置这个超时能够让 Elasticsearch 在稍后空闲的时候自动释放这部分资源。 例如:依据查询条件开启滚动,镜像有效期为5分钟。这个查询结果中包含scroll_id 1234567891011121314curl -XPOST 'http://localhost:9200/es_cluster/goods_index/goods/_search?scroll=5m' -d '{ \"size\": 20, \"query\": { \"bool\": { \"filter\": [ { \"exists\": { \"field\": \"skuUpTime\" } } ] } }}' 下次查询的时候,就可以直接用scroll_id来查询了 1234curl -XPOST 'http://localhost:9200/es_cluster/scroll' -d '{ \"scroll\": \"5m\", \"scroll_id\": \"DnF1ZXJ5VGhlbkZldGNoJgAAAAA8lsqeFjRwN19JcDBKVFd1cy04dkZucUZ4dHcAAAAAPlQH_BZIMUU1VFA2dVIxMk10M0xoWW1sSEtRAAAAADtYOdcWUVBlYXZCNmRTSEtXV2llZTE4dkEwZwAAAAA6dfbHFkdEMS1VY21CVHFhSFpFS1ZrVnBwSVEAAAAAPaL6vBZsZmRFeHRBeFFJaWlVOXNWV2xnZlB3AAAAADy6UDUWRU9NVVRadG5STnV4cThrb2VMdHhqUQAAAAA9V-f6FnNYSV83SHZVVHpDTHVLZWFiNmVwcVEAAAAAPMlOFhY5Zk5ESlBZdFI5bVVEclI4b2paTld3AAAAADn6zjwWZEJ5RU9VOThTUW1HUnB5UzlOT18xUQAAAAA9EBaGFmM4Ykh6SHkxU3JPYkFFRFZMWVRiQUEAAAAAPSKSJxZVUlRzY0luTFJTQzQ3WTZydUxoQVN3AAAAADyw6E4WS21DZHdWX21TRXlkdzVvYUpRcVBKUQAAAAA7sX3BFnh4UC1rMGhDU1FlZFFSdjVDVVc0d3cAAAAAPMIY9RZla0lLajhFQlMtYUVOZDE1U0tXekRnAAAAADtYOdgWUVBlYXZCNmRTSEtXV2llZTE4dkEwZwAAAAA5-s49FmRCeUVPVTk4U1FtR1JweVM5Tk9fMVEAAAAAPMlOFxY5Zk5ESlBZdFI5bVVEclI4b2paTld3AAAAADzjiUkWMll6VHRaUDRUV2VjNXA4LUxfMlFxdwAAAAA8cKq6FndTQ2JfOWlWUXlHNHdNMUZ3ejZVZUEAAAAAPHCquRZ3U0NiXzlpVlF5RzR3TTFGd3o2VWVBAAAAADzx948Wdlk0dzdJWnBRWHEyRWFsVEhsVUdiQQAAAAA9cxFHFnpwdy1EaXFXUmR5REM5ZW5CYTB0ZEEAAAAAPXMRSBZ6cHctRGlxV1JkeURDOWVuQmEwdGRBAAAAAD1DIxMWM0E3dW1PbkVUQzZMSUJXV21LaHNiZwAAAAA9QyMUFjNBN3VtT25FVEM2TElCV1dtS2hzYmcAAAAAPPeZ9hZyY0luUVRFb1RsNmF1YWhZaVEwWUFBAAAAADz3mfcWcmNJblFURW9UbDZhdWFoWWlRMFlBQQAAAAA6pq_PFjRDZjN6XzBSU25DRE5PWlA2RFRUR2cAAAAAOqavzhY0Q2Yzel8wUlNuQ0ROT1pQNkRUVEdnAAAAADzcYCcWQ2xwZk15U01RNk9oelRQR2w2bTRmdwAAAAA84x3vFk8wYjdmdV9aU2E2QzREc045bUx4UUEAAAAAPPTj5RZoYlctclkyblM3cXpDeEZTa05FNlNnAAAAADzrVQEWLU9xeUFOVHRUQTZtd2R2eG5MTF96UQAAAAA861UAFi1PcXlBTlR0VEE2bXdkdnhuTExfelEAAAAAO1D_vBZheEpkUFp4UFNXS19BWDdvMTNkS1BnAAAAADuxfcIWeHhQLWswaENTUWVkUVJ2NUNVVzR3dwAAAAA7ungNFlpqWVViMnZsVDU2LXg4VzBlTlZ0cWcAAAAAPaL6vRZsZmRFeHRBeFFJaWlVOXNWV2xnZlB3\"}'","link":"/2018/08/11/search/ElasticSearch深分页浅分页/"},{"title":"IK分词器深入学习(1)-分词理论","text":"背景介绍词是表达语义的最小单位,分词是搜索引擎的基石。 在搜索引擎中,倒排索引就是由一个个Term所构成,分词器通过对自然语言的理解,拆分出Term。分词的输入是一串连续的文字,对于英文”girl is beautiful.boy is handsome.”分词比较容易,可以根据空格和标点符号分割,那最终会分为girl/is/beautiful/boy/is/handsome(一般来说,is作为停用词),是不是很容易?但这种方式在中文中却不行,”女孩美丽,男孩帅气。”这句话应该分为”女孩/美丽/男孩/帅气”,不是简单的靠空格和符号就能实现的。 上例提出了中文分词的背景,那么总要有理论来解决中文分词的问题。经过几十年的研究,中文分词方法也取得了长足的发展,产生了众多的分词方法,主要分为基于字符串匹配的分词方法,基于统计的分词方法,基于理解的分词方法三大流派。 ##基于字符串匹配的分词方法 基于字符串匹配的分词算法也叫做机械分词算法,一般都需要事先建立足够大的分词词典,然后将待分词文本中的字符串与分词词典中的词进行匹配,如果匹配成功,该字符串当做一个词从待分词文本中切分出来,否则不切分。 下面介绍下主流的字符串匹配算法 最大正向匹配分词算法(Forward Maximum Matching,FMM) 这是字符串匹配分词算法中最基本的一种分词算法,统计结果表明,单纯使用最大正向匹配算法的错误率为1/169。优点是原理简单,切分速度快,易与实现。缺点是歧义处理不精确。 最大逆向匹配分词算法(Reverse Maximum Matching,RMM) 与FMM算法的基本思想相同,不同的是该方法从待切分的汉字串的末位开始处理,每次不匹配时去掉最前面的一个汉字,继续匹配。优点同FMM一样,而且最大逆向匹配算法对交集型歧义字段的处理精度比正向的要高,错误率在1/245左右,缺点是不能完全排除歧义现象。 双向最大匹配算法(Bi-directction Matching method,BMM) 双向匹配算法基本原理是同时进行FMM和RMM扫描,然后分析两种扫描的结果。如果两种扫描结果一致,则认为不存在歧义现象;如果不一致,则需要定位到歧义字段处理。优点是提高了分词的准确率,消除了部分歧义现象。缺点是算法执行要做双向扫描,时间复杂度会有所增加。 全切分算法 全切分算法是指切分出字符串中所有可能的词语,该算法可以大大提高切分的召回率,识别出所有可能的歧义现象。但缺点是算法复杂度比较高。 总结: 字符串匹配分词方法,逻辑简单清晰,易实现,分词性能较高。但是严重依赖字典,如果某些词在字典中未登陆,则会对分词的精度产生较大的影响。另外算法的效率很大层面体现在字典的数据结构上,如果用Hash表的方式,检索最快但是内存占用大,一般情况下会牺牲部分性能来降低词典的容量比如TRIE索引树词典、逐词二分词典、整词二分词典。 基于统计的分词方法下面介绍下统计分词的常用分词方法 N元文法模型(N-gram) 令C=C1C2…Cm.C 是待切分的汉字串,W=W1W2…Wn.W 是切分的结果。设P(WlC)是汉字串C切分为W的某种估计概率。Wa,Wb,⋯.Wk是C的所有可能的切分方案。那么,基于统计的切分模型就是这样的一种分词模型,它能够找到目的词串W ,使得W 满足: P(W|C)=MAX(P(Wa|C),P(Wb|C)…P(Wk|C)),即估计概率为最大之词串。我们称函数P(W|C)为评价函数。一般的基于统计的分词模型的评价函数,都是根据贝叶斯公式.同时结合系统本身的资源限制,经过一定的简化,近似得来的。 根据贝叶斯公式, 有:P(W|C)=P(W) P(C|W)/P(C),对于C的多种切分方案,P(C)是一常数,而P(C|W)是在给定词串的条件下出现字串C的概率,故P(C|W)=1。所以 ,我们用P(W)来代替P(W|C)。那么,如何估计P(W)呢?最直接的估计P(W)的方法利用词的n-gram,即: P(W)=P(W1) P(W2lW1) P(W3|W1W2)⋯P(Wk|W1,W2…Wk-1) 但是,由于当前的计算机技术和我们现有的语料资源所限,这种方法存在致命的缺陷: ①对于有6万词的词典而言,仅词和词的bigram就可能需要60000 x 60000=3600M的统计空间,这是当前的计算机硬件水平所难以接受的,更不要说更大的n-gram 了。 ②需要与上述空间相当的熟语料,否则就会产生训练语料不足所产生的数据稀疏问题。 ③由于不同领域的语料库的用词有所差异,针对某一个领域的语料库统计出来的n-gram,若用于其它领域,其效果难以预料,而目前通过语料库搭配来克服领域差民间的方法尚未成熟。 因此,利用词的n-gram 直接估计P(W)的方法,在目前是不可行的。基于上述的原因,大多数基于统计的分词模型都没有直接采用上述公式,而是采用各种各样的估计方法,从不同的角度,实现对P(W)的近似。 隐马尔可夫模型(Hidden Markov Model,HMM) ##","link":"/2019/07/19/search/IK分词器深入学习-第一章/"},{"title":"Lucence分词原理研究","text":"#研究背景 近期工作需要优化公司的分词器,需要改写IK源码,故而知其然知其所以然,Lucene分词相关的原理和实现研究透彻之后,可以研究ES IK分词器的源码了。以下源码的研究,是基于Lucene 6.6.0版本,Lucene源码中文档很规范,结合文档和源码学习效率很高。本文的所有内容,均来自于Lucene的文档和代码示例。 #类图 如类图所示,Lucene中和分词相关的最核心的几个类。Analyzer用于构建TokenStream(用于分析文本),提供了用于提取文本生成词项(term)的策略。为了定义哪些Analyzer就绪,子类必须实现createComponents方法,组件分词时,调用tokenStream方法。 TokenStream枚举了一系列tokens,它们来自documents.field或者query text。TokenStream是一个抽象类,具体的实现分为Tokenizer和TokenFilter。Tokennizer的输入是Reader,TokenFilter的输入是另一个tokenFilter。 TokenStream API工作流程1、初始化TokenStream时添加对应的属性2、客户端调用TokenStream.reset()3、客户端获取需要访问的属性的本地引用4、客户端调用incrementToken()直到返回false,每次调用就可以获取对应的token的属性5、客户端调用end()方法让TokenStream执行扫尾操作6、客户端使用完TokenStream后调用close()释放资源 123456789101112131415161718192021222324252627282930public static void main(String[] args) throws Exception { Analyzer analyzer = new StandardAnalyzer(); String str = \"中华人民\"; TokenStream stream = analyzer.tokenStream(\"content\", new StringReader(str)); CharTermAttribute attr = stream.addAttribute(CharTermAttribute.class); //1,3 PositionIncrementAttribute posIncr = stream.addAttribute(PositionIncrementAttribute.class); //1,3 OffsetAttribute offset = stream.addAttribute(OffsetAttribute.class); //1,3 TypeAttribute type = stream.addAttribute(TypeAttribute.class); //1,3 stream.reset(); //2 int position = 0; while (stream.incrementToken()) { //4 int increment = posIncr.getPositionIncrement(); if (increment > 0) { position = position + increment; System.out.print(position + \": \"); } System.out.println(attr.toString() + \",\" + offset.startOffset() + \"->\" + offset.endOffset() + \",\" + type.type()); } stream.end(); //5 stream.close(); //6 analyzer.close(); }执行结果:1: 中,0->1,<IDEOGRAPHIC>2: 华,1->2,<IDEOGRAPHIC>3: 人,2->3,<IDEOGRAPHIC>4: 民,3->4,<IDEOGRAPHIC>","link":"/2019/04/13/search/Lucene分词原理研究/"},{"title":"ElasticSearch集群的搭建","text":"#相关技术点 in-memory buffer是通过refresh操作写到文件缓冲区的,refresh的过程除了在文件缓冲区生成这个文件以外,还会让底层Lucene的index reader打开这个文件,让新生成的这个文件对搜索可见。 周五的时候跟同事讨论下了,不知道这样理解对不对–refresh类似于 outputstream flush(此时数据从 java heap 写到 memory buffer),之后(按照大神上面说的)index reader读取新文件,然后等待触发flush(这个则类似于 outputstream close,此时数据从 memory buffer 写到 disk),请大神赐教 集群配置集群包括3台物理机共12个节点,每台物理机部署1个Master节点,1个Client节点,2个Data节点。 1234567891011121314151617181920212223242526272829303132333435363738# ---------------------------------- Cluster -----------------------------------#集群名称cluster.name: # ------------------------------------ Node ------------------------------------#节点名称node.name: #是否是Master Nodenode.master: true#是否是Data Nodenode.data: false#是否是Ingest Nodenode.ingest: falsenode.attr.rack: first# ----------------------------------- Paths ------------------------------------# Path to log files:#可以指定es的数据存储目录,默认存储在es_home/data目录下#path.data: /path/to/data#可以指定es的日志存储目录,默认存储在es_home/logs目录下#path.logs: /path/to/logs# ----------------------------------- Memory -----------------------------------#锁定物理内存地址,防止elasticsearch内存被交换出去,也就是避免es使用swap交换分区bootstrap.memory_lock: true#CentOS下不支持SecComp,故而需要设置成falsebootstrap.system_call_filter: false# ---------------------------------- Network -----------------------------------network.host: 192.168.1.97http.port: 9200transport.tcp.port: 9300# --------------------------------- Discovery ----------------------------------#discovery.zen.ping.unicast.hosts: [\"192.168.1.97:9300\",\"192.168.1.98:9300\",\"192.168.1.113:9300\"]#理论上是 masternum/2+1,防止脑裂discovery.zen.minimum_master_nodes: 2# ---------------------------------- Various -----------------------------------xpack.security.enabled: false#支持跨域访问http.cors.enabled: truehttp.cors.allow-origin: \"*\" 参考文档Linux sandboxing机制 从内核文件系统看文件读写过程","link":"/2019/04/23/search/ElasticSearch集群的搭建/"},{"title":"elasticSearch系列笔记(5)-客户端负载均衡","text":"常用负载均衡策略ES客户端负载均衡策略集群嗅探","link":"/2018/07/11/search/elasticSearch-sniff/"},{"title":"elasticSearch系列笔记(1)-Hello ElasticSearch","text":"读了上面的小故事,是不是觉得对ElasticSearch的前世今生,兴趣十足呢?!我计划写一篇ElasticSearch的系列学习笔记,深入浅出,不仅方便大家学习,还能够让我更加深入的了解这门技术。学习一门新技术的第一课就是hello world.接下来的章节,实操ElasticSearch hello world。 ElasticSearch简介回忆时光 许多年前,一个刚结婚的名叫 Shay Banon 的失业开发者,跟着他的妻子去了伦敦,他的妻子在那里学习厨师(感觉国内的女生很少有喜欢做饭的,反而男生喜欢做饭)。 在寻找一个赚钱的工作的时候,为了给他的妻子做一个食谱搜索引擎,他开始使用 Lucene 的一个早期版本。 直接使用 Lucene 是很难的,因此 Shay 开始做一个抽象层,Java 开发者使用它可以很简单的给他们的程序添加搜索功能。 他发布了他的第一个开源项目 Compass。 后来 Shay 获得了一份工作,主要是高性能,分布式环境下的内存数据网格。这个对于高性能,实时,分布式搜索引擎的需求尤为突出, 他决定重写 Compass,把它变为一个独立的服务并取名 Elasticsearch。 第一个公开版本在2010年2月发布,从此以后,Elasticsearch 已经成为了 Github 上最活跃的项目之一,他拥有超过300名 contributors(目前736名 contributors )。 一家公司已经开始围绕 Elasticsearch 提供商业服务,并开发新的特性,但是,Elasticsearch 将永远开源并对所有人可用。 据说,Shay 的妻子还在等着她的食谱搜索引擎… 切入正题 读了上面的小故事,是不是觉得对ElasticSearch的前世今生,兴趣十足呢?!我计划写一篇ElasticSearch的系列学习笔记,深入浅出,不仅方便大家学习,还能够让我更加深入的了解这门技术。学习一门新技术的第一课就是hello world.接下来的章节,实操ElasticSearch hello world。 Mac ElasticSearch 安装1、Java环境的安装配置2、brew install elasticSearch(仅适用于Mac)3、brew info elasticSearch,会显示如下信息表示安装成功 elasticSearch: stable 6.2.4, HEAD Distributed search & analytics engine https://www.elastic.co/products/elasticsearch /usr/local/Cellar/elasticSearch/6.2.4 (112 files, 30.8MB) Built from source on 2018-05-29 at 11:10:53 From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/elasticsearch.rb ==> Requirements Required: java = 1.8 ✔ ==> Options --HEAD Install HEAD version ==> Caveats 4、brew services start elasticsearch 启动后localhost:9200,打印如下,说明ElasticSearch启动成功 { "name" : "yptOhLD", "cluster_name" : "elasticsearch_1", "cluster_uuid" : "_na_", "version" : { "number" : "6.2.4", "build_hash" : "ccec39f", "build_date" : "2018-04-12T20:37:28.497551Z", "build_snapshot" : false, "lucene_version" : "7.2.1", "minimum_wire_compatibility_version" : "5.6.0", "minimum_index_compatibility_version" : "5.0.0" }, "tagline" : "You Know, for Search" } 5、brew install kibana(仅适用于Mac)6、brew services start kibana 7、打开kibana localhost:5601 基于以上7个步骤,环境问题搞定,下面来真实的操作一下ElasticSearch。 ElasticSearch Hello World创建索引(create index)curl -XPUT "http://localhost:9200/goods_index" curl -XGET "http://localhost:9200/goods_index" { "goods_index": { #索引名称 "aliases": {}, "mappings": {}, "settings": { "index": { "creation_date": "1530627979046", "number_of_shards": "5", #5个分片 "number_of_replicas": "1", #一个备份 "uuid": "kTy_hHlQQRaVPygKHZpUYQ", "version": { "created": "6020499" }, "provided_name": "goods_index" } } } } 索引文档(index document)curl -XPUT "http://localhost:9200/goods_index/goods/12343333" -H 'Content-Type:application/json' -d' { "skuName": "华为手机", "url":"https://item.jd.com/6946605.html" }' 索引数据之后,再执行查看索引详情,会发现mappings属性多了一个名字叫goods的type,goods下面多了skuName和url两个properties。 { "goods_index": { "aliases": {}, "mappings": { "goods": { "properties": { "skuName": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "url": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } }, "settings": { "index": { "creation_date": "1530627979046", "number_of_shards": "5", "number_of_replicas": "1", "uuid": "kTy_hHlQQRaVPygKHZpUYQ", "version": { "created": "6020499" }, "provided_name": "goods_index" } } } } 查询文档(query document)curl -XGET "http://localhost:9200/goods_index/goods/_search" 执行上面的查询语句,得到如下的结果集。 { "took": 1, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": 1, "hits": [ { "_index": "goods_index", "_type": "goods", "_id": "12343333", "_score": 1, "_source": { "skuName": "华为手机", "url": "https://item.jd.com/6946605.html" } } ] } } 小节 ElasticSearch的Hello world,是不是比java,python难一些呢?不过还好,一个检索系统,就此拉开了帷幕。这就是ElasticSearch推崇的“开箱即用”的理念。但是要学好ElasticSearch可不是一件容易的事情,细心的同学,可能会发现有一些细节:1、什么是索引?2、什么是文档?3、索引和文档是什么样的关系?……请持续关注我的博客,我们逐渐从ElasticSearch小白,变成大牛。 参考文献elasticSearch权威指南2.xelasticSearch reference 6.2","link":"/2018/07/11/search/elasticSearch-初体验/"},{"title":"elasticSearch系列笔记(4)-term_vector","text":"term_vectorhttps://www.elastic.co/guide/en/elasticsearch/reference/2.0/term-vector.html#term-vector","link":"/2018/07/11/search/elasticSearch-term_vector/"},{"title":"lucene系列-第一章-lucene包结构及工作流程","text":"lucene包结构及工作流程Lucene的analysis模块主要负责词法分析及语言处理而形成Term。Lucene的index模块主要负责索引的创建,里面有IndexWriter。Lucene的store模块主要负责索引的读写。Lucene的QueryParser主要负责语法分析。Lucene的search模块主要负责对索引的搜索。Lucene的similarity模块主要负责对相关性打分的实现。 包名 功能 org.apache.lucene.analysis 语言分析器,主要用于的切词,支持中文主要是扩展此类 org.apache.lucene.document 索引存储时的文档结构管理,类似于关系型数据库的表结构 org.apache.lucene.index 索引管理,包括索引建立、删除等 org.apache.lucene.queryParser 查询分析器,实现查询关键词间的运算,如与、或、非等 org.apache.lucene.search 检索管理,根据查询条件,检索得到结果 org.apache.lucene.store 数据存储管理,主要包括一些底层的I/O操作 org.apache.lucene.util 一些公用类","link":"/2019/06/02/search/lucene系列之lucene包结构及工作流程/"},{"title":"Spring初始化源码分析","text":"spring配置文件,需要关注如下两个类。 12org.springframework.beans.factory.config.PropertiesFactoryBeanorg.springframework.context.support.PropertySourcesPlaceholderConfigurer PropertiesFactoryBean类图 PropertySourcesPlaceholderConfigurer 如下是配置方式 1234567891011121314151617181920212223<!--使用PropertySourcesPlaceholderConfigurer的配置方式--><context:property-placeholder location=\"classpath*:config/*.properties\"/><!--使用PropertiesFactoryBean的配置方式--><util:properties id=\"appConfig1\" location=\"classpath*:config/*.properties\"/><!--使用PropertySourcesPlaceholderConfigurer的配置方式--><bean id=\"spc\" class=\"org.springframework.context.support.PropertySourcesPlaceholderConfigurer\"> <property name=\"locations\"> <list> <value>classpath:config/data.properties</value> <value>classpath:config/data1.properties</value> </list> </property> <property name=\"ignoreResourceNotFound\" value=\"false\"/> </bean><!--使用PropertiesFactoryBean的配置方式--><bean id=\"appConfig\" class=\"org.springframework.beans.factory.config.PropertiesFactoryBean\"> <property name=\"locations\"> <list> <value>classpath:config/data.properties</value> <value>classpath:config/data1.properties</value> </list> </property></bean> 使用方式上,用@Value注解 PropertiesFactoryBean用#{}的方式 PropertySourcesPlaceholderConfigurer用${}的方式 12345678910@Value(value = \"${driveClass}\")private String driveClass;@Value(\"${url}\")private String url;@Value(\"#{appConfig['userName']}\")private String userName;@Value(\"#{appConfig['config1']}\")private String password;","link":"/2018/08/11/spring/Spring的配置文件管理/"},{"title":"elasticSearch系列笔记(4)-elasticSearch搜索类型","text":"ElasticSearch本地源码调试转自:https://donlianli.iteye.com/blog/2094305 es在查询时,可以指定搜索类型为QUERY_THEN_FETCH,QUERY_AND_FEATCH,DFS_QUERY_THEN_FEATCH和DFS_QUERY_AND_FEATCH。那么这4种搜索类型有什么区别? 分布式搜索背景介绍: ES天生就是为分布式而生,但分布式有分布式的缺点。比如要搜索某个单词,但是数据却分别在5个分片(Shard)上面,这5个分片可能在5台主机上面。因为全文搜索天生就要排序(按照匹配度进行排名),但数据却在5个分片上,如何得到最后正确的排序呢?ES是这样做的,大概分两步。 step1、ES客户端会将这个搜索词同时向5个分片发起搜索请求,这叫Scatter, step2、这5个分片基于本Shard独立完成搜索,然后将符合条件的结果全部返回,这一步叫Gather。 客户端将返回的结果进行重新排序和排名,最后返回给用户。也就是说,ES的一次搜索,是一次scatter/gather过程(这个跟mapreduce也很类似). 然而这其中有两个问题。 第一、数量问题。比如,用户需要搜索”双黄连”,要求返回最符合条件的前10条。但在5个分片中,可能都存储着双黄连相关的数据。所以ES会向这5个分片都发出查询请求,并且要求每个分片都返回符合条件的10条记录。当ES得到返回的结果后,进行整体排序,然后取最符合条件的前10条返给用户。这种情况,ES5个shard最多会收到10*5=50条记录,这样返回给用户的结果数量会多于用户请求的数量。 第二、排名问题。上面搜索,每个分片计算分值都是基于自己的分片数据进行计算的。计算分值使用的词频率和其他信息都是基于自己的分片进行的,而ES进行整体排名是基于每个分片计算后的分值进行排序的,这就可能会导致排名不准确的问题。如果我们想更精确的控制排序,应该先将计算排序和排名相关的信息(词频率等)从5个分片收集上来,进行统一计算,然后使用整体的词频率去每个分片进行查询。 这两个问题,估计ES也没有什么较好的解决方法,最终把选择的权利交给用户,方法就是在搜索的时候指定query type。 1、query and fetch 向索引的所有分片(shard)都发出查询请求,各分片返回的时候把元素文档(document)和计算后的排名信息一起返回。这种搜索方式是最快的。因为相比下面的几种搜索方式,这种查询方法只需要去shard查询一次。但是各个shard返回的结果的数量之和可能是用户要求的size的n倍。 2、query then fetch(默认的搜索方式) 如果你搜索时,没有指定搜索方式,就是使用的这种搜索方式。这种搜索方式,大概分两个步骤,第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。 3、DFS query and fetch 这种方式比第一种方式多了一个初始化散发(initial scatter)步骤,有这一步,据说可以更精确控制搜索打分和排名。 4、DFS query then fetch 比第2 种 方式多了一个 初始化散发(initial scatter)步骤。 DSF是什么缩写?初始化散发是一个什么样的过程? 从es的官方网站我们可以指定,初始化散发其实就是在进行真正的查询之前,先把各个分片的词频率和文档频率收集一下,然后进行词搜索的时候,各分片依据全局的词频率和文档频率进行搜索和排名。显然如果使用DFS_QUERY_THEN_FETCH这种查询方式,效率是最低的,因为一个搜索,可能要请求3次分片。但,使用DFS方法,搜索精度应该是最高的。 至于DFS是什么缩写,没有找到相关资料,这个D可能是Distributed,F可能是frequency的缩写,至于S可能是Scatter的缩写,整个单词可能是分布式词频率和文档频率散发的缩写。 总结一下,从性能考虑QUERY_AND_FETCH是最快的,DFS_QUERY_THEN_FETCH是最慢的。从搜索的准确度来说,DFS要比非DFS的准确度更高。","link":"/2018/07/11/search/elasticSearch-搜索类型(query type)/"},{"title":"Mac环境下搭建大数据开发环境","text":"Mac下搭建大数据开发环境环境比较浪费时间,结合自己搭建环境的经验,整理成文档。 安装JDK 和 SCALA所有的前置条件是安装JDK,我用的是JDK8.x java version “1.8.0_162” Java(TM) SE Runtime Environment (build 1.8.0_162-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode) Scala code runner version 2.12.4 – Copyright 2002-2017, LAMP/EPFL and Lightbend, Inc. Spark V2.3.0 Spark安装比较简单,也没什么坑。网上一大堆,随意搞。 vmvare虚拟机安装vmware fusion 虚拟下安装Hadoop & hive1、hadoop伪分布式部署,版本 2.7.1, sbin/start-all.sh , sbin/stop-all.sh。jps验证其服务是否启动完全 2、hive用mysql(我用的是mariaDB,比安装Mysql省事)做元数据库,版本2.1.1 hive。hive-env.sh,hive-site.xml hdfs dfs -ls /user/hive/warehouse hive表都存到这个目录下 4、关闭防火墙 永久关闭: chkconfig iptables off 及时生效service iptables stop 5、mac 下 虚拟机vmware Fution 6、ssh -p 22 [email protected] ssh链接虚拟机 core-site.xml 12345678910111213141516<configuration> <property> <name>hadoop.tmp.dir</name> <value>/soft/data/hadoop/tmp</value> <description>Abase for other temporary directories.</description> </property> <property> <name>fs.defaultFS</name> <value>hdfs://172.16.192.130:9000</value> </property> <property> <name>hadoop.native.lib</name> <value>false</value> <description>Should native hadoop libraries, if present, be used. </description> </property></configuration> hdfs-site.xml 12345678910<configuration> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.permissions</name> <value>false</value> </property></configuration> 启动方式: $HADOOP_HOME/sbin/start-all 3825 Jps 3089 DataNode 3252 SecondaryNameNode 2903 NameNode 3405 ResourceManager 3695 NodeManager 虚拟机下安装Mysql,当做Hive的元数据库service mysql status 查看mysql是否启动 service mysql start 启动mysql服务 mysql用的mariaDB,yum安装,搜索“centOS 6 yum安装 mariaDB”。mySQL 启动:service mysql start,service mysql status 查看HIVE元数据: mysql -uroot -p 访问mysql use hive; show tables; 虚拟机下安装Hbase1、 进入hbase官网,下载hbase1.2.1,因为本地的hadoop版本是2.7.1,要注意hbase和hadoop的兼容性。 2、 下载之后,tar -zxvf hbase-1.2.1-bin.tar.gz 3、 修改hbase conf目录下hbase-site.xml文件 123456789101112131415<configuration> <property> <name>hbase.rootdir</name> <value>hdfs://192.168.2.100:9000/hbase</value> 注意别忘了端口号,URL要和hadoop core-site.xml保持一致 </property> <property> <name>hbase.cluster.distributed</name> <value>true</value> </property> <property> <name>hbase.zookeeper.property.dataDir</name> <value>/soft/zkdata</value> </property>注意/soft/zkdata要有读写权限</configuration> 4、 修改conf 下的 hbase-env.sh export JAVA_HOME=/soft/jdk1.8.0_171 export HBASE_CLASSPATH=$HADOOP_HOME/conf export HBASE_MANAGES_ZK=true 这个值默认是true的,作用是让HBase启动的时候同时也启动zookeeper. 5、 启动hbase ./start-hbase.sh,执行JPS会看到如下进程。 [root@localhost bin]# jps 20979 DataNode 21315 ResourceManager 22180 HRegionServer 21605 NodeManager 21158 SecondaryNameNode 21961 HQuorumPeer 23116 Jps 22060 HMaster 20845 NameNode 6、 启动Hbase shell, bin/hbase shell,执行命令测试下Hbase shell能否正常工作。 hbase(main):001:0> list “test” TABLE test 1 row(s) in 0.3870 seconds => [“test”] 遇到的雷本地SPARK连HIVE,需要把虚拟机中 hive_home/conf/hive-site.xml 复制到spark_home/conf下一份,并且需要把mysql驱动放到$spark_home/jars中 需要把虚拟机中的MYSQL (hive元数据库)权限调成任何IP都能访问。GRANT ALL PRIVILEGES ON . TO ‘root‘@’%’ IDENTIFIED BY ‘123456’; 执行flush privileges;刷新权限。 如果遇到这个错误。 Hive Schema version 1.2.0 does not match metastore’s schema version 2.1.0 Metastore is not upgraded or corrupt。 需要把hive-site.xml设置成hive.metastore.schema.verification = false hive.exec.local.scratchdir,hive.querylog.location,hive.server2.logging.operation.log.location 都配置成 /tmp/root。之前放到apache-hive目录下,会报错。 ElasticSearch-hadoop 6.2.3 匹配es 6.x ;ElasticSearch-hadoop 5.2.3 匹配 es 5.x mac 改成静态IP https://blog.csdn.net/zhishengqianjun/article/details/77046796 本地调用Hbase API的时候,需要执行以下三个步骤,不然就会找不到regionServer反复重试,照成阻塞。 1、 配置Linux的hostname :vim /etc/sysconfig/network NETWORKING=yes HOSTNAME=master 2、 配置Linux的hosts,映射ip的hostname的关系 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 193.168.2.100 master 3、配置访问windows的hosts,192.168.2.100 master mac下安装ElasticSearch特别简单,自行google brew services restart elasticsearch brew services restart kibana elasticsearch –version 观察es版本 安装目录: /usr/local/Cellar/elasticsearch/6.2.4 brew info elasticsearch 可以查看elasticsearch的详情。很实用的命令。 Mac下vmware 静态IP虚拟机默认是动态分配IP,一旦IP变更,大数据的环境会有问题。静态IP是必须要解决的问题。 https://blog.csdn.net/zhishengqianjun/article/details/77046796 照着这个帖子,一步一步来。稳稳的。 Idea中执行spark sql 需要把spark_home/conf下的hive-site.xml复制到resource目录下。 另外,如果pom中没有mysql jar包的话,需要加入。 将hdfs 文件关联到hive上1、 首先在虚拟机hive上,创建好表结构 2、 登录线上的hive,找到hive对应的HDFS文件,copy一个part下来。 3、 传到虚拟机的hdfs上 4、执行ALTER TABLE xxx ADD PARTITION (dt=’2013-02-28’) LOCATION ‘/wsy’; (如果是lzo压缩过的文件,执行lzop -dv 001008_0.lzo 解压缩,不然hive读的时候会乱码) 5、 大功告成。","link":"/2018/08/11/tool/MAC下搭建大数据开发环境/"},{"title":"jenkins自动化部署实践","text":"jenkins自动化部署实践1、依赖技术 jenkins:Jenkins是一款开源 CI&CD 软件,用于自动化各种任务,包括构建、测试和部署软件。Jenkins 支持各种运行方式,可通过系统包、Docker 或者通过一个独立的 Java 程序。 tomcat:web应用服务器 Jdk8 nexus:Maven的远程仓库 Shell 2、实现流程1、在一台服务器部署jenkins和maven 2、部署nexus远程仓库 3、mvn clean deploy -P online 打包到maven私服 4、更新最新的shell脚本 5、调用启动脚本 参考文献jenkins官网nexus官网 nexus搭建maven私服","link":"/2019/07/01/tool/jenkins自动化部署实践/"},{"title":"Spring初始化源码分析","text":"本文主要介绍下Spring的初始化过程。 XML方式Spring应用入口 首先,在web容器启动之后,会加载org.springframework.web.servlet.DispatcherServlet,作为容器的一个Servlet实例,该Servlet也是Spring WEB应用的唯一一个Servlet实例。Servlet初始化,会调用org.springframework.web.servlet.HttpServletBean#init方法,初始化Servlet,见下面源码。 123456789101112131415161718192021222324252627282930/** * Map config parameters onto bean properties of this servlet, and * invoke subclass initialization. * @throws ServletException if bean properties are invalid (or required * properties are missing), or if subclass initialization fails. */@Overridepublic final void init() throws ServletException { if (logger.isDebugEnabled()) { logger.debug(\"Initializing servlet '\" + getServletName() + \"'\"); } // Set bean properties from init parameters. try { PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment())); initBeanWrapper(bw); bw.setPropertyValues(pvs, true); } catch (BeansException ex) { logger.error(\"Failed to set bean properties on servlet '\" + getServletName() + \"'\", ex); throw ex; } // Let subclasses do whatever initialization they like. initServletBean(); if (logger.isDebugEnabled()) { logger.debug(\"Servlet '\" + getServletName() + \"' configured successfully\"); }} 注意代码initServletBean,该方法被FrameworkServlet重写。如下代码。 1234567891011121314151617181920212223242526272829303132/**- Overridden method of {@link HttpServletBean}, invoked after any bean properties- have been set. Creates this servlet's WebApplicationContext. */ @Override protected final void initServletBean() throws ServletException { getServletContext().log(\"Initializing Spring FrameworkServlet '\" + getServletName() + \"'\"); if (this.logger.isInfoEnabled()) { this.logger.info(\"FrameworkServlet '\" + getServletName() + \"': initialization started\"); } long startTime = System.currentTimeMillis(); try { this.webApplicationContext = initWebApplicationContext(); initFrameworkServlet(); } catch (ServletException ex) { this.logger.error(\"Context initialization failed\", ex); throw ex; } catch (RuntimeException ex) { this.logger.error(\"Context initialization failed\", ex); throw ex; } if (this.logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; this.logger.info(\"FrameworkServlet '\" + getServletName() + \"': initialization completed in \" + elapsedTime + \" ms\"); } } initWebApplicationContext()方法,最终会调用createWebApplicationContext()方法,来初始化Spring的上下文。 123456789101112131415161718192021222324protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) { Class<?> contextClass = getContextClass(); if (this.logger.isDebugEnabled()) { this.logger.debug(\"Servlet with name '\" + getServletName() + \"' will try to create custom WebApplicationContext context of class '\" + contextClass.getName() + \"'\" + \", using parent context [\" + parent + \"]\"); } if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException( \"Fatal initialization error in servlet with name '\" + getServletName() + \"': custom WebApplicationContext class [\" + contextClass.getName() + \"] is not of type ConfigurableWebApplicationContext\"); } ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); wac.setEnvironment(getEnvironment()); wac.setParent(parent); wac.setConfigLocation(getContextConfigLocation()); configureAndRefreshWebApplicationContext(wac); return wac;} WEB容器的方式Spring应用入口 WEB容器的方式Spring Context初始化最后引出最重要的代码,org.springframework.context.support.AbstractApplicationContext#refresh 123456789101112131415161718192021222324252627282930313233343536373839public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. prepareRefresh(); // Tell the subclass to refresh the internal bean factory. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses. postProcessBeanFactory(beanFactory); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); // Initialize message source for this context. initMessageSource(); // Initialize event multicaster for this context. initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses. onRefresh(); // Check for listener beans and register them. registerListeners(); // Instantiate all remaining (non-lazy-init) singletons. finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event. finishRefresh(); } catch (BeansException ex) { logger.warn(\"Exception encountered during context initialization - cancelling refresh attempt\", ex); // Destroy already created singletons to avoid dangling resources. destroyBeans(); // Reset 'active' flag. cancelRefresh(ex); // Propagate exception to caller. throw ex; } }} 这个代码的作用,是初始化整个容器,创建Bean.","link":"/2018/08/11/spring/Spring初始化/"},{"title":"Charles搭建网络抓包环境","text":"Charles搭建网络抓包环境Charles 是在 Mac 下常用的网络封包截取工具,在做移动开发时,我们为了调试与服务器端的网络通讯协议,常常需要截取网络封包来分析。Charles 通过将自己设置成系统的网络访问代理服务器,使得所有的网络访问请求都通过它来完成,从而实现了网络封包的截取和分析。除了在做移动开发中调试端口外,Charles 也可以用于分析第三方应用的通讯协议。配合 Charles 的 SSL 功能,Charles 还可以分析 Https 协议。 Charles 是收费软件,可以免费试用 30 天。试用期过后,未付费的用户仍然可以继续使用,但是每次使用时间不能超过 30 分钟,并且启动时将会有 10 秒种的延时。因此,该付费方案对广大用户还是相当友好的,即使你长期不付费,也能使用完整的软件功能。只是当你需要长时间进行封包调试时,会因为 Charles 强制关闭而遇到影响。 Charles 主要的功能包括: 截取 Http 和 Https 网络封包。 支持重发网络请求,方便后端调试。 支持修改网络请求参数。 支持网络请求的截获并动态修改。 支持模拟慢速网络。 Charles 4 新增的主要功能包括: 支持 Http 2。 支持 IPv6。 IOS http抓包需要在设置->无线局域网->连接的WIFI下面,设置HTTP代理: IP为启动Charles的服务器IP,端口默认是8888,如果端口被重新指定,请在Charles -> Proxy -> Proxy Setting中查看。 以上设置好之后,手机的网络请求,便可以在Charles中体现出来了。 IOS https抓包除了http抓包的设置之外,还需要安装https证书。 Step1: Charles -> Help -> SSL Proxy -> Install Charles Root Certificate on a mobile device or remote browser Step2: step1执行之后,会出现一个弹窗,需要用apple手机safari访问chls.pro/ssl,安装证书。 Step3: iOS 10.3系统,需要在 设置→通用→关于本机→证书信任设置 里面启用完全信任Charles证书。 基于以上步骤,Charles就能够正常抓取https的包了。 #引用 charles从入门到精通 Charles抓包(IOS的Http/Https请求)","link":"/2019/04/09/tool/charles搭建网络抓包环境/"},{"title":"mac安装mysql记录","text":"1brew install mysql CREATE USER ‘test‘@’%’ IDENTIFIED BY ‘root@12’; grant all privileges on . to ‘test‘@’%’; Flush privileges; 下载Sequel Pro开始使用","link":"/2018/07/11/tool/mac安装mysql指南/"},{"title":"linux使用jmeter进行压力测试","text":"linux使用jmeter进行压力测试安装 export PATH=/usr/local/jmeter/bin/:$PATH 添加到/etc/profile末尾。 1`$ wget https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-3.3.tgz$ tar -xvf apache-jmeter-3.3.tgz$ mv apache-jmeter-3.3.tgz jmeter$ mv jmeter /usr/local`","link":"/2019/08/11/tool/linux使用jmeter进行压力测试/"},{"title":"Elastic APM实战","text":"了解不同服务拓扑的运行状况和性能,能够快速确定问题的根本原因以及提高推特的整体可靠性和效率是极其重要的。APM就是解决上述问题的利器。 前言本文从最根本的可观察性出发,引出实现这种思想的APM(Application Performance Management)框架,通过对APM的核心组件和数据模型的理解,可以加深对ElasticAPM的理解。最后通过实战演练ElasticAPM,来实现应用的性能追踪。 可观察性 “可观察性”不是供应商能够在系统之外单独交付的功能,而是您在构建系统时植根于其中的一个属性,就像易用性、高可用性和稳定性一样。设计和构建“可观察”系统的目标在于,确保当它在生产中运行时,负责操作它的人员能够检测到不良行为(例如,服务停机、错误、响应缓慢),并拥有可操作的信息以有效地确定根本原因(例如,详细的事件日志、细粒度的资源使用信息,以及应用程序跟踪)。这个目标看似平淡无奇,但组织在实现这一目标时会遇到诸多挑战,常见挑战包括:没有收集足够的信息;收集了太多信息,但没有提取出有指导意义的内容;这些信息分别存储在诸多不同的位置。 如上述引用内容可知,可观测性重点是解决运行中的系统在遇到问题时,较难定位问题的问题,尤其是在大规模分布式系统中,一个业务流程依赖上下游众多的系统,更是提高了定位问题的复杂度。而一个具备良好可观测性的系统,能够用很低的成本有效的定位关键问题所在。下图所示可观测性主要有三大支撑: 日志:程序运行产生的事件,可以详细解释其运行状态 指标:一组聚合数值,主要用于监控基础设施(机器、容器、网络等),但也有应用会用于监控业务层面,比如开源搜索系统Elasticsearch就有关于查询或写入量、耗时、拒绝率等应用层面的指标; 应用性能监控(Application Performance Monitor):注意,这里的M代表的是Monitor,是指深入到代码层面的追踪(或监控),包括程序内部执行过程、服务之间链路调用等情况,能轻易的找到程序“慢”的原因。APM最常见被用于对web服务器中一次请求处理过程的追踪,包括内部执行逻辑、外部服务的调用及它们相应的耗时。 上面说的那么多,通俗一点讲举一个例子,当你负责的系统业务逻辑复杂,依赖的第三方服务或者中间件较多的前提下。有一天,你收到了一连串性能报警,你老板问你为啥这个接口这么慢?如果可观察性做的比较差的系统,你就疯狂的打log日志,统计各个代码行之间的耗时情况,然后上线,在茫茫日志的海洋里分析到底哪一行比较慢,如果找到了恭喜你,你是幸运的,只需要去掉日志或者改变日志级别,再上一次线就可以了! 但是,很有可能是没有找到,那会不会当时发生GC?当时带宽占用比较多?会不会当时服务器负载高?等等可能性,你由于没有完备的记录,又复现不了,就会很难保证你负责的接口是不是又会爆发性能报警问题,你的老板就会怀疑你的能力!很难受对嘛? 如果这时,天赐你一套功能完备的APM系统,你通过报警中写明的traceId,在APM平台中搜索出对应的调用链,发现你依赖的接口耗时很高,或者通过你的Metrics发现那段时间GC很频繁,再通过log拿到具体的执行信息,Bingo!相信问题已经八九不离十了,10分钟以内回复老板:“我找到问题了,原因是balabala”,没错,问我为什么要有APM,因为一切尽在掌握。 APM简述通过上一章节的分析,我们知道了什么是可观察性。那一款好的APM产品就是要提高应用的可观察性的。目前市面上大多数APM产品是应用性能监控(Application Performance Monitoring),但是在维基百科中定义的是应用性能管理(Application Performance Management)。 APM 在维基百科上的定义是应用性能管理(Application Performance Management),而市面上大多数APM产品定义则是应用性能监控(Application Performance Monitoring)。《What Is Application Performance Monitoring and Why It Is Not Application Performance Management》 此文认为应用性能监控是应用性能管理一部分,前者能帮你找到问题,而后者能帮你分析并解决问题。但实际上大部分APM产品都包含了分析问题的部分,并且业界也没有对两个定义作出明确的区分,所以基本上我们可以将两者视为是相同的。 最终用户体验监控(End user experience monitoring)。通过监控用户的行为以期优化用户体验。比如:监控用户和web界面/客户端的交互,并记录交互事件的时间。 运行时应用程序架构(Runtime application architecture)。理解服务间的依赖关系、架构中应用程序交互的网络拓扑。 业务事务(Business transaction)。产生有意义的SLA报告,并从业务角度提供有关应用程序性能的趋势信息。 深入组件监控(Deep dive component monitoring)。通常需要安装agent并且主要针对中间层,包括web服务器、应用和消息服务器等。健壮的监控应该能显示代码执行的清晰路径,因为这一维度和上述第二个维度紧密相关,APM产品通常会将这两个维度合并作为一个功能。 分析或报告(Analytics/reporting)。将从应用程序中收集的一系列指标数据,标准化的展现成应用性能数据的通用视图。 Elastic APM简介具体可以参考官方文档,总结起来,就是ElasticAPM是在ELK的基础之上,做了应用性能监控组件,把本来就存在的Logging,Metrics,和APM引入的Tracing数据,整合起来,变成了一个一站式的应用可观察性平台。 组件Elastic APM由四个组件组成: APM agents:以应用程序库的形式提供,收集程序中的性能监控数据并上报给APM server。 APM Server:从APM agents接收数据、进行校验和处理后写入Elasticsearch特定的APM索引中。虽然agent也可以实现为:将数据收集处理后直接上报到ES,不这么做官方给出的理由:使agent保持轻量,防止某些安全风险以及提升Elastic组件的兼容性。 Elasticsearch:用于存储性能指标数据并提供聚合功能。 Kibana:可视化性能数据并帮助找到性能瓶颈。 数据模型Elastic APM agent从其检测(instrument)的应用程序中收集不同类型的数据,这些被称为事件,类型包括span,transaction,错误和指标四种。 Span 包含有关已执行的特定代码路径的信息。它们从活动的开始到结束进行度量,并且可以与其他span具有父/子关系。 事务(Transaction) 是一种特殊的Span(没有父span,只能从中派生出子span,可以理解为“树”这种数据结构的根节点),具有与之关联的其他属性。可以将事务视为服务中最高级别的工作,比如服务中的请求等。 错误:错误事件包含有关发生的原始异常或有关发生异常时创建的日志的信息。 指标:APM agent自动获取基本的主机级别指标,包括系统和进程级别的CPU和内存指标。除此之外还可获取特定于代理的指标,例如Java agent中的JVM指标和Go代理中的Go运行时指标。 Elastic APM实战环境安装安装包准备采用了最新版的ELK和Apm agent Java agent: elastic-apm-agent-1.12.0.jar Apm Server: apm-server-7.5.1-darwin-x86_64 ElasticSearch: elasticsearch-7.5.1 Kibana: kibana-7.5.1-darwin-x86_64 环境搭建环境启动顺序如下 启动ElasticSearch,默认端口9200 启动Kibana,默认端口5601 启动APM Server,./apm-server -e,默认端口是8200,由于apm Server使用Golang编写,本地需要安装Go环境。 启动应用,java -javaagent:/Tools/apm/elastic-apm-agent-1.12.0.jar -Delastic.apm.secret_token= -jar apm-0.0.1-SNAPSHOT.jar。如果使用Idea启动应用,可以如下配置 启动之后,如果发现如下日志,则代表织入了apm-java-agent。Java agent采用了Byte Buddy技术动态织入字节码,使得agent变得没有侵入性。 122020-01-08 15:09:32.603 [apm-server-healthcheck] INFO co.elastic.apm.agent.report.ApmServerHealthChecker - Elastic APM server is available: { \"build_date\": \"2019-12-16T20:57:12Z\", \"build_sha\": \"348d8d83c3c823b64fc0692be607b1a5a8fac775\", \"version\": \"7.5.1\"}2020-01-08 15:09:32.779 [main] INFO co.elastic.apm.agent.configuration.StartupInfo - Starting Elastic APM 1.12.0 as sample_wsy on Java 1.8.0_201 (Oracle Corporation) Mac OS X 10.14.6 Demo开发POM.xml,项目中需要引入apm-agent依赖,apm-agent默认会收集一些事件,比如HTTP请求和database queries,并且为了不侵入应用,采用了字节码织入技术来实现java-agent,使得apm-agent对业务系统变得透明,但是透明意味着不可控,apm还提供了public API,来手动可控的方式,来告诉agent采集这些信息。 具体的Java-agent有很多功能,本文不赘述,请参考官方文档 12345<dependency> <groupId>co.elastic.apm</groupId> <artifactId>apm-agent-api</artifactId> <version>1.12.0</version></dependency> Controller代码 123456789101112131415161718192021@GetMapping(\"/test/{name}\")public String test(@PathVariable String name) { String result = null; Transaction transaction = ElasticApm.currentTransaction(); System.out.println(transaction.getTraceId()); try { transaction.setName(\"WsyController#test\"); transaction.setType(\"CUSTOM\"); result = testService.test(name); } catch (Exception e) { transaction.captureException(e); } finally { //WARN co.elastic.apm.agent.impl.transaction.AbstractSpan - End has already been called: '' //注意,如果调用ElasticApm.currentTransaction();就不需要transaction.end();否则会报如上警告 transaction.end(); } return result;} Service代码 12345678910111213141516171819202122232425262728@Servicepublic class TestService { public String test(String name) { Span span = ElasticApm.currentTransaction().startSpan(); try { span.setName(\"test0-wsy\"); test1(name); } catch (Exception e) { span.captureException(e); } finally { span.end(); } return \"hello \" + name; } public String test1(String name) { Span span = ElasticApm.currentTransaction().startSpan(); try { span.setName(\"test1-wsy\"); } catch (Exception e) { span.captureException(e); } finally { span.end(); } return name; }} 请求Controller之后,刷新Kibana,发现已经拥有了链路追踪 提出问题1、本文在单机版的环境中,测试通过,但是在分布式环境中,请求会串联起很多应用,那服务跟踪能否实现?实现的原理是什么? 2、Elastic APM可以自动采集http请求,在PRC分布式环境中,Elastic APM能否正常工作?是否必须采用 public API来实现? 总结1、APM可以提升系统的可观察性 2、ElasticAPM可以借助ElasticSearch和Kibana一站式的解决链路追踪 参考文档Dapper,大规模分布式系统的跟踪系统 Dapper, a Large-Scale Distributed Systems Tracing Infrastructure 官方APM Java Agent Reference 1.x 官方APM Overview 全链路分布式跟踪系统与APM 使用Elastic APM做应用性能监控 借助Elastic Stack实现可观察性 分布式跟踪、开放式跟踪和ElasticAPM","link":"/2020/01/20/elk/Elastic-APM实战/"},{"title":"Spring Cloud Alibaba集成ElasticAPM实战","text":"在微服务大行其道的今天,Spring Cloud Alibaba作为优秀的微服务实现,却不能很容易的集成ElasticAPM。本文就将解决的思路和实现,呈现给大家,希望能帮助大家。 前言继上一篇ElasticAPM初体验我们知道了什么是可观察性,并领略了ElasticAPM的强大功能,但是仅仅是上篇文章中单机模式的使用时远远不够的。还记得上一篇最后提出的两个问题: 1、本文在单机版的环境中,测试通过,但是在分布式环境中,请求会串联起很多应用,那服务跟踪能否实现?实现的原理是什么? 2、Elastic APM可以自动采集http请求,在PRC分布式环境中,Elastic APM能否正常工作?是否必须采用 public API来实现? 重点是分布式和RPC,即在分布式情况下,ElasticAPM能否良好工作?在RPC环境下,ElasticAPM是不是也能正常工作呢? 先说答案:在默认情况下,ElasticAPM能够支持分布式的Http方式调用,但是不支持RPC协议。但是很多公司都采用RPC协议作为其内部系统的通信协议,比如我司就采用Spring Cloud Alibaba作为搜索服务的框架,框架内应用的通信是借助RPC框架Dubbo来实现的。所以问题就变成了如何把ElasticAPM集成进Spring Cloud Alibaba中。 架构讲解&问题分析首先,我先大概图示下Spring Cloud Alibaba和ElasticAPM的架构和工作流程。 如架构图所示,搜索系统分为了网关应用(Gateway),US应用,AS应用,BS应用,用户的请求会先到达网关,网关会把请求,以Http协议转发给US应用,US应用会采用Dubbo协议调用AS应用,AS应用采用Dubbo协议调用BS应用。 Request —http—>US—RPC—->AS—–RPC——>BS 每一个应用启动的时候都已经集成了Apm-agent(如果不知道怎么集成请参考ElasticAPM初体验),如果APM-agent默认支持Dubbo就完美了(但是并没有)。所以整个链路追踪,到了US之后,就没有上报之后应用的锚点数据。在查看ElasticAPM官方文档的时候,我注意到了Public API,文档中交代了这样一件事情: The public API of the Elastic APM Java agent lets you customize and manually create spans and transactions, as well as track errors. 没错,你可以自定义Span和Transaction,如果不懂什么是Span和Transaction请参考ElasticAPM初体验或直接读一遍官方文档。既然Agent默认不支持Dubbo,那么我们使用Public API来实现功能。 设计思想基于Spring Cloud Alibaba的架构,我们可以如下图方式实现。 首先用户的请求一定要经过微服务网关,在网关的过滤器中,首先埋入父级Transaction。 请求经过网关,会被网关转发到第一层应用中,注意这次转发是http请求,如果是用SpringMVC实现的话,需要在Controller处,上报子Transaction。 请求被第一层应用处理之后,下层的应用全部是Dubbo协议的。这时可以采用Dubbo的过滤器机制,对Concumer和Provider都进行拦截,通过这种方式做到不侵入业务代码。 最终,请求返回到微服务网关,调用transaction.end()上报根Transaction。 所有流程完毕。 核心实现讲解微服务网关微服务网关需要做这样几件事情: 开启根Transaction POST请求body中增加追踪ID,GET请求Parameter中增加追踪ID 在请求返回之后调用transaction.end()完成上报 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { HttpMethod httpMethod = exchange.getRequest().getMethod(); //第一步 开启一个Transaction Transaction transaction = ElasticApm.startTransaction(); transaction.setName(\"mainSearch\"); transaction.setType(Transaction.TYPE_REQUEST); //第二步 创建Span Span span = transaction.startSpan(\"gateway\", \"filter\", \"gateway action\"); span.setName(\"com.mfw.search.gateway.filter.PostBodyCacheFilter#filter\"); LOGGER.info(\"APM埋点成功transactionId:{}\", transaction.getId()); //第三步 判定Http请求是POST还是GET if (HttpMethod.POST.equals(httpMethod)) { ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders); MediaType mediaType = exchange.getRequest().getHeaders().getContentType(); //第四步 定义Http body的处理逻辑 Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> { //判定body类型 if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)) { //重要!获取到了body的数据,传给callback函数,做业务逻辑处理 Map<String, String> bodyMap = decodeBody(body); //设置最新的bodyMap进入exchange exchange.getAttributes().put(GatewayConstant.CACHE_POST_BODY, bodyMap); //重点!动态增加body的transaction标记,为下游应用Controller使用 span.injectTraceHeaders((name, value) -> { bodyMap.put(name, value); LOGGER.info(\"APM埋点 key:{}, transactionId:{}\", name, value); }); //不要忘记span.end()否则会丢失上报 span.end(); return Mono.just(encodeBody(bodyMap)); } else if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) { // origin body map Map<String, String> bodyMap = decodeJsonBody(body); exchange.getAttributes().put(GatewayConstant.CACHE_POST_BODY, bodyMap); //重点!动态增加body的transaction标记,为下游应用Controller使用 span.injectTraceHeaders((name, value) -> { bodyMap.put(name, value); LOGGER.info(\"APM埋点 key:{}, transactionId:{}\", name, value); }); span.end(); return Mono.just(encodeJsonBody(bodyMap)); } return Mono.empty(); }); BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class); HttpHeaders headers = new HttpHeaders(); headers.putAll(exchange.getRequest().getHeaders()); // the new content type will be computed by bodyInserter // and then set in the request decorator headers.remove(HttpHeaders.CONTENT_LENGTH); CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers); return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> { ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) { public HttpHeaders getHeaders() { long contentLength = headers.getContentLength(); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.putAll(super.getHeaders()); if (contentLength > 0) { httpHeaders.setContentLength(contentLength); } else { httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, \"chunked\"); } return httpHeaders; } public Flux<DataBuffer> getBody() { return outputMessage.getBody(); } }; //第五步 在请求返回之后,上报transaction return chain.filter(exchange.mutate().request(decorator).build()).then(Mono.fromRunnable(() -> transaction.end())); })); } else if (HttpMethod.GET.equals(httpMethod)) { span.injectTraceHeaders((name, value) -> { exchange.getRequest().getQueryParams().set(name, transaction.getId()); LOGGER.info(\"APM埋点 key:{}, transactionId:{}\", name, value); }); return chain.filter(exchange).then(Mono.fromRunnable(() -> { span.end(); transaction.end(); LOGGER.info(\"APM买点完成,transactionId:{}\", transaction.getId()); })); } else { //not support other Http Method exchange.getResponse().setStatusCode(HttpStatus.UNSUPPORTED_MEDIA_TYPE); return exchange.getResponse().setComplete(); } } ControllerController层的实现采用了SpringAOP方式实现,这样的好处是对业务代码不侵入,可扩展性高,对想要监控的方法直接配置上@TransactionWithRemoteParent()即可。 如下代码是通过@TransactionWithRemoteParent()实现对Controller方法的上报。 123456@PostMapping(value = \"/search\", consumes = \"application/json\", produces = \"application/json\")@TransactionWithRemoteParent()public String searchForm(@RequestBody String req) { String result = asService.helloAs(req); return result;} AOP实现 12345678910111213141516171819202122232425262728293031323334353637383940414243444546@Aspectpublic class ApmAspect { private static final Logger LOGGER = LoggerFactory.getLogger(ApmAspect.class); @PostConstruct private void init() { LOGGER.info(\"ApmAspect加载完毕\"); } @Pointcut(value = \"@annotation(transactionWithRemoteParent)\", argNames = \"transactionWithRemoteParent\") public void pointcut(TransactionWithRemoteParent transactionWithRemoteParent) { } @Around(value = \"pointcut(transactionWithRemoteParent)\", argNames = \"joinPoint,transactionWithRemoteParent\") public Object around(ProceedingJoinPoint joinPoint, TransactionWithRemoteParent transactionWithRemoteParent) throws Throwable { Transaction transaction = null; try { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); transaction = ElasticApm.startTransactionWithRemoteParent(key -> { String httpRequest = (String) joinPoint.getArgs()[0]; JSONObject json = JSON.parseObject(httpRequest); String traceId = json.getString(key); LOGGER.info(\"切面添加了子Transaction,key={},value={}\", key, traceId); RpcContext.getContext().setAttachment(key, traceId); return traceId; }); transaction.setName(StringUtils.isNotBlank(transactionWithRemoteParent.name()) ? transactionWithRemoteParent.name() : signature.getName()); transaction.setType(Transaction.TYPE_REQUEST); return joinPoint.proceed(); } catch (Throwable throwable) { if (transaction != null) { transaction.captureException(throwable); } throw throwable; } finally { if (transaction != null) { LOGGER.info(\"切面执行完毕,上报Transaction:{}\", transaction.getId()); transaction.end(); } } }} Dubbo过滤器如下代码是DubboConsumer过滤器,专门用于处理APM。DubboProvider的实现类似。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849@Activate(group = \"consumer\")public class DubboConsumerApmFilter implements Filter { private static final Logger LOGGER = LoggerFactory.getLogger(DubboConsumerApmFilter.class); @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { Transaction transaction = ElasticApm.startTransactionWithRemoteParent(key -> { String traceId = invocation.getAttachment(key); LOGGER.info(\"key={},value={}\", key, traceId); return traceId; }); try (final Scope scope = transaction.activate()) { String name = \"consumer:\" + invocation.getInvoker().getInterface().getName() + \"#\" + invocation.getMethodName(); transaction.setName(name); transaction.setType(Transaction.TYPE_REQUEST); Result result = invoker.invoke(invocation); return result; } catch (Exception e) { transaction.captureException(e); throw e; } finally { transaction.end(); } }}@Activate(group = \"provider\")public class DubboProviderApmFilter implements Filter { @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { // use startTransactionWithRemoteParent to create transaction with parent, which id from prc context Transaction transaction = ElasticApm.startTransactionWithRemoteParent(key -> invocation.getAttachment(key)); try (final Scope scope = transaction.activate()) { String name = \"provider:\" + invocation.getInvoker().getInterface().getName() + \"#\" + invocation.getMethodName(); transaction.setName(name); transaction.setType(Transaction.TYPE_REQUEST); return invoker.invoke(invocation); } catch (Exception e) { transaction.captureException(e); throw e; } finally { transaction.end(); } }} 效果 源代码https://github.com/siyuanWang/springCloudAlibabaAPMDemo 参考文档ElasticAPM集成Dubbo的讨论","link":"/2020/01/20/elk/Spring Cloud Alibaba集成ElasticAPM/"},{"title":"Java函数式编程总结","text":"在阅读SpringBoot的源码时,框架运用了大量的函数式编程。可以说,业务代码你可以不使用函数式编程,但是上升到框架层面,函数式编程是基础。 什么是函数式编程函数式编程(英语:functional programming)或称函数程序设计、泛函编程,是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ演算(lambda calculus)为该语言最重要的基础。而且,λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。 比起指令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。 以上内容摘抄自维基百科,建议学习函数式编程前,先试着理解它的定义。当然,如果你十分聪明能够通过定义掌握函数式编程的思想,可以忽略以下的内容。如果你不能通过定义掌握它的思想,建议大家先循序渐进,敲敲代码,实战以下,再回来读这个定义,就会柳暗花明。 请大家细细品味斜体加粗的部分。 函数式编程特点函数是“第一等公民”所谓“第一等公民”(first class),指的是函数与其他数据类型一样,出于平等地位,可以赋值给其他变量,也可以作为参数传入或者返回。 在Java中,这种操作太常见了,比如SpringBoot源码中。入参是一个BiConsumer的函数,出参是一个自定义的函数DocumentConsumer。 1234567891011121314151617181920private DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod, boolean checkForExisting) { return (profile, document) -> { if (checkForExisting) { for (MutablePropertySources merged : this.loaded.values()) { if (merged.contains(document.getPropertySource().getName())) { return; } } } MutablePropertySources merged = this.loaded.computeIfAbsent(profile, (k) -> new MutablePropertySources()); addMethod.accept(merged, document.getPropertySource()); }; }@FunctionalInterfaceprivate interface DocumentConsumer { void accept(Profile profile, Document document);} 只用“表达式”,不用“语句”表达式(expression)是一个单纯的运算过程,总是有返回值。语句(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。换句话说,每一步都是单纯的运算,而且都有返回值。先说结论,在一个应用中,不可能全篇都是函数式编程,因为函数式不适合有I/O的场景,尽量把函数式应用在单纯的计算中。如果不能理解可以参考下下面对语句和表达式的说明。 语句语句(statement)又称述句、陈述式、描述式、语句、陈述句等。在计算机科学的编程中,一个语句是指令式编程语言中最小的独立元素,表达程序要运行的一些动作。多数语句是以高级语言编写成一或多个语句的序列,用于命令计算机运行指定的一系列操作。单一个语句本身也具有内部结构(例如表达式)。 许多语言(例如说,C语言)将语句与定义句(definition)分隔的很明确,因为语句只会有运算符号以及一些宣告标识符号(identifier)的定义。我们也可以找出简单语句与复合语句之间的差异;后者会在一个段落中包含了许多语句。 表达式在讲表达式的概念以前,我们需要先知道什么是“副作用”。函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并降低程序的可读性。严格的函数式语言要求函数必须无副作用。例如修改全局变量(函数外的变量),修改参数或改变外部存储。 在大多数编程语言中,语句与表达式互相对比,两者不同之处在于,语句是为了运作它们的副作用而运行;表达式则一定会传回评估后的结果,而且通常不产生副作用。 在措辞中经常出现这样的区别:一个语句是被“运行”(execute),而一个表达式是被“评估”或对其“求值”(evaluate)。一些语言中具备了exec和eval函数:比如在Python中,exec应用于语句,而eval应用于表达式。 ##代码简洁,接近自然语言,易于理解 (1+2)*3-4 1add(1,2).multiply(3).subtract(4) ##易于并发编程 函数式编程不需要考虑”死锁”(deadlock),因为它不修改变量,所以根本不存在”锁”线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署”并发编程”(concurrency)。 但是仅仅靠函数式编程是离不开并发编程基础的,换句话说,你想利用多核,还是需要编写多线程代码,只是在Runnable接口的实现中,采用一段函数式代码,或者一个函数式模块。真正解决用串行思想异步多核编程,请参见Reactor。 ##更方便的代码管理 函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。 常用的函数式接口在Java中,内置了很多函数式接口,它们大多数在java.util.function包中。 最常用的Consumer,Function,Supplier,Predicate。除了这些常用的之外,还有BiConsumer,BiFunction,BiPredicate等等,可以看他们源码中的注释来查看区别。 Consumer接受一个参数,不返回参数,通俗讲它就是“消费者”。方法consumer的第一个入参是一个Map迭代器Consumer函数,第二个参数是要迭代的Map。Main方法中,通过Lambda表达式,构造了一个函数传递给consumer方法进行传递。 1234567891011121314151617181920public static void main(String[] args) { Map<String, Object> map = new HashMap<>(); map.put(\"a\", 1); map.put(\"b\", 2); map.put(\"c\", 3); map.put(\"d\", 4); map.put(\"e\", 5); consumer((x) -> x.forEach((key, value) -> System.out.println(\"key=\" + key + \",value=\" + value)), map);}private void consumer(Consumer<Map<String, Object>> iterator, Map<String, Object> map) { iterator.accept(map);}#输出key=a,value=1key=b,value=2key=c,value=3key=d,value=4key=e,value=5 FunctionFunction函数,接受一个参数T,返回值R。方法function第一个参数是一个类型转换器(Function函数),第二个参数是要转换的字符串。 1234567891011121314System.out.println(function((str) -> Integer.parseInt(str), \"1024\"));/** * 类型转换 * * @param convert 传入一个输入为字符串,输出为整型的函数 * @param str 要转换的字符串 * @return 整数 */private Integer function(Function<String, Integer> convert, String str) { return convert.apply(str);}#输出1024 SupplierSupplier函数,不需要输入,返回一个值,通俗讲它是一个“生产者“,而且是不需要“原料”的无私奉献者。 123456System.out.println(supplier(() -> 1));private Integer supplier(Supplier<Integer> supplier) { return supplier.get();}##输出1 PredicatePredicate函数,输入一个值,返回一个Boolean类型,通俗讲是一个预测函数,判定值是真是假。 12345678910111213System.out.println(predicate((x) -> x > 10, 11));/** * 判定输入数字是否大于10 * @param predicate 预测函数 * @param num 输入数字 * @return true or false */private boolean predicate(Predicate<Integer> predicate, Integer num) { return predicate.test(num);}##输出true 自定义函数式接口当然有很多情况Java自带的函数式接口不能满足我们的需求,我们也可以通过@FunctionInterface注解自定义函数式接口。 例如,上一个章节中讲解的四大常用函数,我们也可以自定义一套和他们功能一样的函数。 1234567891011121314151617181920212223@FunctionalInterfaceprivate interface MyConsumer { void iterator(Map<String, Object> map);}@FunctionalInterfaceprivate interface MyFunction { Integer parse(String str);}@FunctionalInterfaceprivate interface MySupplier { Integer get();}@FunctionalInterfaceprivate interface MyPredicate { boolean test(Integer num); default boolean negate(Integer num) { return !test(num); }} 总结函数式编程不是新生事物,在学术界已经存在很久。但是最近几年,在工业界开始兴起,它是一种编程范式和新的思想,和它平行的几个思想分别是面向过程编程,面向对象编程,反应式编程,各有千秋,各有各的应用场景,不要神化谁。 在Java中,从JDK8开始支持函数式编程,从实际工作中,即使不学习函数式编程,也基本不影响正常工作(除非团队硬性要求使用),而且我个人的经历,很多人比较排斥函数式编程,理由是“晦涩难懂,可维护性差,需要学习成本”等等。但是最近学习Spring Cloud和Spring5的源码,发现框架中大量使用函数式编程,可以说不接纳函数式编程的思想,就会被业界淘汰,而且最近刚刚兴起的反应式编程,也是以函数式编程为基础的。习惯面向过程编程的人,会说“面向对象语言真麻烦,这么多设计模式真讨厌,一个接口那么多实现类真烦”,函数式编程又何尝不是在各种质疑声中成为现代框架和基础组件中的“水电煤”。 参考1、wiki百科函数式编程) 2、wiki百科语句(程序设计)](https://zh.wikipedia.org/zh-cn/語句_(程式設計)) 3、Java函数式编程初始篇","link":"/2019/10/28/java/Java函数式编程总结/"},{"title":"Mysql高可用架构总结","text":"为了创建高可用数据库系统,传统的实现方式是创建一个或多个备用的数据库实例,MySQL5.7新引入了Group Replication,用于搭建更高事务一致性的高可用数据库集群系统。 本文摘抄自MySQL · 引擎特性 · Group Replication内核解析 背景为了创建高可用数据库系统,传统的实现方式是创建一个或多个备用的数据库实例,原有的数据库实例通常称为主库master,其它备用的数据库实例称为备库或从库slave。当master故障无法正常工作后,slave就会接替其工作,保证整个数据库系统不会对外中断服务。master与slaver的切换不管是主动的还是被动的都需要外部干预才能进行,这与数据库内核本身是按照单机来设计的理念悉悉相关,并且数据库系统本身也没有提供管理多个实例的能力,当slave数目不断增多时,这对数据库管理员来说就是一个巨大的负担。 MySQL的传统主从复制机制MySQL传统的高可用解决方案是通过binlog复制来搭建主从或一主多从的数据库集群。主从之间的复制模式支持异步模式(async replication)和半同步模式(semi-sync replication)。无论哪种模式下,都是主库master提供读写事务的能力,而slave只能提供只读事务的能力。在master上执行的更新事务通过binlog复制的方式传送给slave,slave收到后将事务先写入relay log,然后重放事务,即在slave上重新执行一次事务,从而达到主从机事务一致的效果。 上图是异步复制(Async replication)的示意图,在master将事务写入binlog后,将新写入的binlog事务日志传送给slave节点,但并不等待传送的结果,就会在存储引擎中提交事务。 上图是半同步复制(Semi-sync replication)的示意图,在master将事务写入binlog后,将新写入的binlog事务日志传送给slave节点,但需要等待slave返回传送的结果;slave收到binlog事务后,将其写入relay log中,然后向master返回传送成功ACK;master收到ACK后,再在存储引擎中提交事务。 MySQL基于两种复制模式都可以搭建高可用数据库集群,也能满足大部分高可用系统的要求,但在对事务一致性要求很高的系统中,还是存在一些不足,主要的不足就是主从之间的事务不能保证时刻完全一致。 基于异步复制的高可用方案存在主从不一致乃至丢失事务的风险,原因在于当master将事务写入binlog,然后复制给slave后并不等待slave回复即进行提交,若slave因网络延迟或其它问题尚未收到binlog日志,而此时master故障,应用切换到slave时,本来在master上已经提交的事务就会丢失,因其尚未传送到slave,从而导致主从之间事务不一致。 基于semi-sync复制的高可用方案也存在主备不一致的风险,原因在于当master将事务写入binlog,尚未传送给slave时master故障,此时应用切换到slave,虽然此时slave的事务与master故障前是一致的,但当主机恢复后,因最后的事务已经写入到binlog,所以在master上会恢复成已提交状态,从而导致主从之间的事务不一致。 Group Replication应运而生为了应对事务一致性要求很高的系统对高可用数据库系统的要求,并且增强高可用集群的自管理能力,避免节点故障后的failover需要人工干预或其它辅助工具干预,MySQL5.7新引入了Group Replication,用于搭建更高事务一致性的高可用数据库集群系统。基于Group Replication搭建的系统,不仅可以自动进行failover,而且同时保证系统中多个节点之间的事务一致性,避免因节点故障或网络问题而导致的节点间事务不一致。此外还提供了节点管理的能力,真正将整个集群做为一个整体对外提供服务。 Group Replication的实现原理Group Replication由至少3个或更多个节点共同组成一个数据库集群,事务的提交必须经过半数以上节点同意方可提交,在集群中每个节点上都维护一个数据库状态机,保证节点间事务的一致性。Group Replication基于分布式一致性算法Paxos实现,允许部分节点故障,只要保证半数以上节点存活,就不影响对外提供数据库服务,是一个真正可用的高可用数据库集群技术。 Group Replication支持两种模式,单主模式和多主模式。在同一个group内,不允许两种模式同时存在,并且若要切换到不同模式,必须修改配置后重新启动集群。 在单主模式下,只有一个节点可以对外提供读写事务的服务,而其它所有节点只能提供只读事务的服务,这也是官方推荐的Group Replication复制模式。单主模式的集群如下图所示: 在多主模式下,每个节点都可以对外提供读写事务的服务。但在多主模式下,多个节点间的事务可能有比较大的冲突,从而影响性能,并且对查询语句也有更多的限制,具体限制可参见使用手册。多主模式的集群如下图所示: MySQL Group Replication是建立在已有MySQL复制框架的基础之上,通过新增Group Replication Protocol协议及Paxos协议的实现,形成的整体高可用解决方案。与原有复制方式相比,主要增加了certify的概念,如下图所示: certify模块主要负责检查事务是否允许提交,是否与其它事务存在冲突,如两个事务可能修改同一行数据。在单机系统中,两个事务的冲突可以通过封锁来避免,但在多主模式下,不同节点间没有分布式锁,所以无法使用封锁来避免。为提高性能,Group Replication乐观地来对待不同事务间的冲突,乐观的认为多数事务在执行时是没有并发冲突的。事务分别在不同节点上执行,直到准备提交时才去判断事务之间是否存在冲突。下面以具体的例子来解释certify的工作原理: 在上图中由3个节点形成一个group,当在节点s1上发起一个更新事务UPDATE,此时数据库版本dbv=1,更新数据行之后,准备提交之前,将其修改的数据集(write set)及事务日志相关信息发送到group,Write set中包含更新行的主键和此事务执行时的快照(由gtid_executed组成)。组内的每个节点收到certification请求后,进入certification环节,每个节点的当前版本cv=1,与write set相关的版本dbv=1,因为dbv不小于cv,也就是说事务在这个write set上没有冲突,所以可以继续提交。 下面是一个事务冲突的例子,两个节点同时更新同一行数据。如下图所示, 在节点s1上发起一个更新事务T1,几乎同时,在节点s2上也发起一个更新事务T2,当T1在s1本地完成更新后,准备提交之前,将其writeset及更新时的版本dbv=1发送给group;同时T2在s2本地完成更新后,准备提交之前,将其writeset及更新时的版本dbv=1也发送给group。 此时需要注意的是,group组内的通讯是采用基于paxos协议的xcom来实现的,它的一个特性就是消息是有序传送,每个节点接收到的消息顺序都是相同的,并且至少保证半数以上节点收到才会认为消息发送成功。xcom的这些特性对于数据库状态机来说非常重要,是保证数据库状态机一致性的关键因素。 本例中我们假设先收到T1事务的certification请求,则发现当前版本cv=1,而数据更新时的版本dbv=1,所以没有冲突,T1事务可以提交,并将当前版本cv修改为2;之后马上又收到T2事务的certification请求,此时当前版本cv=2,而数据更新时的版本dbv=1,表示数据更新时更新的是一个旧版本,此事务与其它事务存在冲突,因此事务T2必须回滚。 核心组件XCOM的特性MySQL Group Replication是建立在基于Paxos的XCom之上的,正因为有了XCom基础设施,保证数据库状态机在节点间的事务一致性,才能在理论和实践中保证数据库系统在不同节点间的事务一致性。 Group Replication在通讯层曾经历过一次比较大的变动,早期通讯层采用是的Corosync,而后来才改为XCom。 主要原因在于corosync无法满足MySQL Group Replication的要求,如 1. MySQL支持各种平台,包括windows,而corosync不都支持; 2. corosync不支持SSL,而只支持对称加密方式,安全性达不到MySQL的要求; 3. corosync采用UDP,而在云端采用UDP进行组播或多播并不是一个好的解决方案。 此外MySQL Group Replication对于通讯基础设施还有一些更高的要求,最终选择自研xcom,包括以下特性: 闭环(closed group):只有组内成员才能给组成员发送消息,不接受组外成员的消息。 消息全局有序(total order):所有XCOM传递的消息是全局有序(在多主集群中或是偏序),这是构建MySQL 一致性状态机的基础。 消息的安全送达(Safe Delivery):发送的消息必须传送给所有非故障节点,必须在多数节点确认收到后方可通知上层应用。 视图同步(View Synchrony):在成员视图变化之前,每个节点都以相同的顺序传递消息,这保证在节点恢复时有一个同步点。实际上,组复制并不强制要求消息传递必须在同一个节点视图中。 总结MySQL Group Replication旨在打造一款事务强一致性金融级的高可用数据库集群产品,目前还存在一些功能限制和不足,但它是未来数据库发展的一个趋势,从传统的主从复制到构建数据库集群,MySQL也在不断的前进,随着产品的不断完善和发展,必将成为引领未来数据库系统发展的潮流。","link":"/2019/12/12/db/Mysql高可用架构总结/"},{"title":"Nginx/LVS/HAProxy负载均衡软件的优缺点详解(转)","text":"Nginx/LVS/HAProxy负载均衡软件的优缺点详解(转)原文链接:http://www.ha97.com/5646.html,由于工作中遇到负载均衡系统的实现,记录笔记。 PS:Nginx/LVS/HAProxy是目前使用最广泛的三种负载均衡软件,本人都在多个项目中实施过,参考了一些资料,结合自己的一些使用经验,总结一下。 一般对负载均衡的使用是随着网站规模的提升根据不同的阶段来使用不同的技术。具体的应用需求还得具体分析,如果是中小型的Web应用,比如日PV小于1000万,用Nginx就完全可以了;如果机器不少,可以用DNS轮询,LVS所耗费的机器还是比较多的;大型网站或重要的服务,且服务器比较多时,可以考虑用LVS。 一种是通过硬件来进行进行,常见的硬件有比较昂贵的F5和Array等商用的负载均衡器,它的优点就是有专业的维护团队来对这些服务进行维护、缺点就是花销太大,所以对于规模较小的网络服务来说暂时还没有需要使用;另外一种就是类似于Nginx/LVS/HAProxy的基于Linux的开源免费的负载均衡软件,这些都是通过软件级别来实现,所以费用非常低廉。 目前关于网站架构一般比较合理流行的架构方案:Web前端采用Nginx/HAProxy+Keepalived作负载均衡器;后端采用MySQL数据库一主多从和读写分离,采用LVS+Keepalived的架构。当然要根据项目具体需求制定方案。下面说说各自的特点和适用场合。 一、NginxNginx的优点是: 1、工作在网络的7层之上,可以针对http应用做一些分流的策略,比如针对域名、目录结构,它的正则规则比HAProxy更为强大和灵活,这也是它目前广泛流行的主要原因之一,Nginx单凭这点可利用的场合就远多于LVS了。2、Nginx对网络稳定性的依赖非常小,理论上能ping通就就能进行负载功能,这个也是它的优势之一;相反LVS对网络稳定性依赖比较大,这点本人深有体会;3、Nginx安装和配置比较简单,测试起来比较方便,它基本能把错误用日志打印出来。LVS的配置、测试就要花比较长的时间了,LVS对网络依赖比较大。3、可以承担高负载压力且稳定,在硬件不差的情况下一般能支撑几万次的并发量,负载度比LVS相对小些。4、Nginx可以通过端口检测到服务器内部的故障,比如根据服务器处理网页返回的状态码、超时等等,并且会把返回错误的请求重新提交到另一个节点,不过其中缺点就是不支持url来检测。比如用户正在上传一个文件,而处理该上传的节点刚好在上传过程中出现故障,Nginx会把上传切到另一台服务器重新处理,而LVS就直接断掉了,如果是上传一个很大的文件或者很重要的文件的话,用户可能会因此而不满。5、Nginx不仅仅是一款优秀的负载均衡器/反向代理软件,它同时也是功能强大的Web应用服务器。LNMP也是近几年非常流行的web架构,在高流量的环境中稳定性也很好。6、Nginx现在作为Web反向加速缓存越来越成熟了,速度比传统的Squid服务器更快,可以考虑用其作为反向代理加速器。7、Nginx可作为中层反向代理使用,这一层面Nginx基本上无对手,唯一可以对比Nginx的就只有lighttpd了,不过lighttpd目前还没有做到Nginx完全的功能,配置也不那么清晰易读,社区资料也远远没Nginx活跃。8、Nginx也可作为静态网页和图片服务器,这方面的性能也无对手。还有Nginx社区非常活跃,第三方模块也很多。 淘宝的前端使用的Tengine就是基于nginx做的二次开发定制版。 Nginx常规的HTTP请求和响应流程图: Nginx的缺点是:1、Nginx仅能支持http、https和Email协议,这样就在适用范围上面小些,这个是它的缺点。2、对后端服务器的健康检查,只支持通过端口来检测,不支持通过url来检测。不支持Session的直接保持,但能通过ip_hash来解决。 二、LVSLVS:使用Linux内核集群实现一个高性能、高可用的负载均衡服务器,它具有很好的可伸缩性(Scalability)、可靠性(Reliability)和可管理性(Manageability)。 LVS的优点是:1、抗负载能力强、是工作在网络4层之上仅作分发之用,没有流量的产生,这个特点也决定了它在负载均衡软件里的性能最强的,对内存和cpu资源消耗比较低。2、配置性比较低,这是一个缺点也是一个优点,因为没有可太多配置的东西,所以并不需要太多接触,大大减少了人为出错的几率。3、工作稳定,因为其本身抗负载能力很强,自身有完整的双机热备方案,如LVS+Keepalived,不过我们在项目实施中用得最多的还是LVS/DR+Keepalived。4、无流量,LVS只分发请求,而流量并不从它本身出去,这点保证了均衡器IO的性能不会收到大流量的影响。5、应用范围比较广,因为LVS工作在4层,所以它几乎可以对所有应用做负载均衡,包括http、数据库、在线聊天室等等。 LVS DR(Direct Routing)模式的网络流程图: LVS的缺点是:1、软件本身不支持正则表达式处理,不能做动静分离;而现在许多网站在这方面都有较强的需求,这个是Nginx/HAProxy+Keepalived的优势所在。2、如果是网站应用比较庞大的话,LVS/DR+Keepalived实施起来就比较复杂了,特别后面有Windows Server的机器的话,如果实施及配置还有维护过程就比较复杂了,相对而言,Nginx/HAProxy+Keepalived就简单多了。 三、HAProxyHAProxy的特点是:1、HAProxy也是支持虚拟主机的。2、HAProxy的优点能够补充Nginx的一些缺点,比如支持Session的保持,Cookie的引导;同时支持通过获取指定的url来检测后端服务器的状态。3、HAProxy跟LVS类似,本身就只是一款负载均衡软件;单纯从效率上来讲HAProxy会比Nginx有更出色的负载均衡速度,在并发处理上也是优于Nginx的。4、HAProxy支持TCP协议的负载均衡转发,可以对MySQL读进行负载均衡,对后端的MySQL节点进行检测和负载均衡,大家可以用LVS+Keepalived对MySQL主从做负载均衡。5、HAProxy负载均衡策略非常多,HAProxy的负载均衡算法现在具体有如下8种:① roundrobin,表示简单的轮询,这个不多说,这个是负载均衡基本都具备的;② static-rr,表示根据权重,建议关注;③ leastconn,表示最少连接者先处理,建议关注;④ source,表示根据请求源IP,这个跟Nginx的IP_hash机制类似,我们用其作为解决session问题的一种方法,建议关注;⑤ ri,表示根据请求的URI;⑥ rl_param,表示根据请求的URl参数’balance url_param’ requires an URL parameter name;⑦ hdr(name),表示根据HTTP请求头来锁定每一次HTTP请求;⑧ rdp-cookie(name),表示根据据cookie(name)来锁定并哈希每一次TCP请求。 四、总结Nginx和LVS对比的总结:1、Nginx工作在网络的7层,所以它可以针对http应用本身来做分流策略,比如针对域名、目录结构等,相比之下LVS并不具备这样的功能,所以Nginx单凭这点可利用的场合就远多于LVS了;但Nginx有用的这些功能使其可调整度要高于LVS,所以经常要去触碰触碰,触碰多了,人为出问题的几率也就会大。2、Nginx对网络稳定性的依赖较小,理论上只要ping得通,网页访问正常,Nginx就能连得通,这是Nginx的一大优势!Nginx同时还能区分内外网,如果是同时拥有内外网的节点,就相当于单机拥有了备份线路;LVS就比较依赖于网络环境,目前来看服务器在同一网段内并且LVS使用direct方式分流,效果较能得到保证。另外注意,LVS需要向托管商至少申请多一个ip来做Visual IP,貌似是不能用本身的IP来做VIP的。要做好LVS管理员,确实得跟进学习很多有关网络通信方面的知识,就不再是一个HTTP那么简单了。3、Nginx安装和配置比较简单,测试起来也很方便,因为它基本能把错误用日志打印出来。LVS的安装和配置、测试就要花比较长的时间了;LVS对网络依赖比较大,很多时候不能配置成功都是因为网络问题而不是配置问题,出了问题要解决也相应的会麻烦得多。4、Nginx也同样能承受很高负载且稳定,但负载度和稳定度差LVS还有几个等级:Nginx处理所有流量所以受限于机器IO和配置;本身的bug也还是难以避免的。5、Nginx可以检测到服务器内部的故障,比如根据服务器处理网页返回的状态码、超时等等,并且会把返回错误的请求重新提交到另一个节点。目前LVS中 ldirectd也能支持针对服务器内部的情况来监控,但LVS的原理使其不能重发请求。比如用户正在上传一个文件,而处理该上传的节点刚好在上传过程中出现故障,Nginx会把上传切到另一台服务器重新处理,而LVS就直接断掉了,如果是上传一个很大的文件或者很重要的文件的话,用户可能会因此而恼火。6、Nginx对请求的异步处理可以帮助节点服务器减轻负载,假如使用apache直接对外服务,那么出现很多的窄带链接时apache服务器将会占用大 量内存而不能释放,使用多一个Nginx做apache代理的话,这些窄带链接会被Nginx挡住,apache上就不会堆积过多的请求,这样就减少了相当多的资源占用。这点使用squid也有相同的作用,即使squid本身配置为不缓存,对apache还是有很大帮助的。7、Nginx能支持http、https和email(email的功能比较少用),LVS所支持的应用在这点上会比Nginx更多。在使用上,一般最前端所采取的策略应是LVS,也就是DNS的指向应为LVS均衡器,LVS的优点令它非常适合做这个任务。重要的ip地址,最好交由LVS托管,比如数据库的 ip、webservice服务器的ip等等,这些ip地址随着时间推移,使用面会越来越大,如果更换ip则故障会接踵而至。所以将这些重要ip交给 LVS托管是最为稳妥的,这样做的唯一缺点是需要的VIP数量会比较多。Nginx可作为LVS节点机器使用,一是可以利用Nginx的功能,二是可以利用Nginx的性能。当然这一层面也可以直接使用squid,squid的功能方面就比Nginx弱不少了,性能上也有所逊色于Nginx。Nginx也可作为中层代理使用,这一层面Nginx基本上无对手,唯一可以撼动Nginx的就只有lighttpd了,不过lighttpd目前还没有能做到 Nginx完全的功能,配置也不那么清晰易读。另外,中层代理的IP也是重要的,所以中层代理也拥有一个VIP和LVS是最完美的方案了。具体的应用还得具体分析,如果是比较小的网站(日PV小于1000万),用Nginx就完全可以了,如果机器也不少,可以用DNS轮询,LVS所耗费的机器还是比较多的;大型网站或者重要的服务,机器不发愁的时候,要多多考虑利用LVS。 现在对网络负载均衡的使用是随着网站规模的提升根据不同的阶段来使用不同的技术: 第一阶段:利用Nginx或HAProxy进行单点的负载均衡,这一阶段服务器规模刚脱离开单服务器、单数据库的模式,需要一定的负载均衡,但是仍然规模较小没有专业的维护团队来进行维护,也没有需要进行大规模的网站部署。这样利用Nginx或HAproxy就是第一选择,此时这些东西上手快, 配置容易,在七层之上利用HTTP协议就可以。这时是第一选择。 第二阶段:随着网络服务进一步扩大,这时单点的Nginx已经不能满足,这时使用LVS或者商用Array就是首要选择,Nginx此时就作为LVS或者Array的节点来使用,具体LVS或Array的是选择是根据公司规模和预算来选择,Array的应用交付功能非常强大,本人在某项目中使用过,性价比也远高于F5,商用首选!但是一般来说这阶段相关人才跟不上业务的提升,所以购买商业负载均衡已经成为了必经之路。 第三阶段:这时网络服务已经成为主流产品,此时随着公司知名度也进一步扩展,相关人才的能力以及数量也随之提升,这时无论从开发适合自身产品的定制,以及降低成本来讲开源的LVS,已经成为首选,这时LVS会成为主流。最终形成比较理想的基本架构为:Array/LVS — Nginx/Haproxy — Squid/Varnish — AppServer。","link":"/2019/08/19/micro/Nginx:LVS:HAProxy负载均衡软件的优缺点详解/"},{"title":"Java线程基础知识","text":"Java线程基础如下图所示,Java中线程可分为NEW,RUNABLE,RUNING,BLOCKED,WAITING,TIMED_WAITING,TERMINATED 共七个状态,一个状态是如何过渡到另一个状态图中标识的很清楚。 初始状态(NEW)实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。 就绪状态(RUNNABLE)就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。 调用线程的start()方法,此线程进入就绪状态。 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。 锁池里的线程拿到对象锁后,进入就绪状态。 运行中状态(RUNNING)线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。 阻塞状态(BLOCKED)阻塞状态是线程阻塞在进入synchronized关键字(当然也包括ReentrantLock)修饰的方法或代码块(获取锁)时的状态。 等待(WAITING)处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。 超时等待(TIMED_WAITING)处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。 终止状态(TERMINATED)当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。 Java线程常用API 其中常用的方法为start(),yield(),sleep(),interrupt(),interrupted(),isInterrupted(),isAlive(),join(), setDaemon(),setName(),setPriority(),其中stop方法和destroy方法,都是被废弃的方法在日常使用中不建议用。除了Thread类下的API,Object类下的wait(),notify(),notifyAll(),这三个方法也经常在多线程场景中出现。本文的目的,主要讲解的就是这些方法的使用和内部原理。 方法 方法说明 Exception Thread.start 开启执行线程,由虚拟机负责调用run方法。(Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread.) IllegalThreadStateException if the thread was already started. Thread.yield 让出CPU,但是仅仅对同线程级别(Yield is a heuristic attempt to improve relative progression between threads that would otherwise over-utilise a CPU) 无 Thread.sleep 使得正在执行的当前线程睡眠。敲黑板!但是不会让出任何锁的所有权( The thread does not lose ownership of any monitors.)这个特性很重要,也就是说在同步块中使用Thread.sleep要谨慎。 当线程中断会抛出InterruptedException异常,并同时清空中断标志位 Thread.interrupt 中断线程,但只是设置了中断标志位,此刻调用isInterrupted返回true。例子请参考下面的示例代码testInterrupt0方法。只会打印到0-9循环跳出 SecurityException if the current thread cannot modify this thread 请教这个异常什么时候会发生呢? Thread.isInterrupted 查看线程是否处于中断状态.true为中断。调用之后不清除中断标志位。 无 Thread.interrupted 查看线程是否处于中断状态.true为中断。调用之后清除中断标志位。心细的同学已经发现和isInterrupted的区别了吧。 无 Thread.isAlive 线程是否存活,A thread is alive if it has been started and has not yet died. 无 Thread.join 等待线程死亡之后再执行。(Waits for this thread to die) 当线程中断会抛出InterruptedException异常,并同时清空中断标志位 Thread.setDaemon 设置为守护线程。任何非守护线程还在运行,守护线程就不会终止,最典型的守护线程是垃圾回收器的回收线程。 IllegalThreadStateException 当线程状态是alive的时候不能调用setDaemon Thread.setName 设置线程的name Thread.setPriority 设置线程的优先级。MIN_PRIORITY为1,MAX_PRIORITY为10,NORM_PRIORITY为5。 Object.wait 如果不指定timeout,则一直阻塞直到其他线程调用notify或者notifyAll。敲黑板!调用wait之后当前线程不在持有对象锁。 Object.notify 随机唤醒同一个对象monitor下的某个线程 Object.notifyAll 唤醒同一个对象monitor下的所有线程。查看testNotify示例。 123456789101112131415static void testInterrupt0() Exception { int i = 0; while (!Thread.currentThread().isInterrupted()) { System.out.println(\"loop\" + i++); if(i == 10) { Thread.currentThread().interrupt(); } } //echo true System.out.println(Thread.currentThread().isInterrupted()); //echo true System.out.println(Thread.currentThread().interrupted()); //echo false System.out.println(Thread.currentThread().isInterrupted());} 1234567891011121314151617181920212223242526public class MyThread extends Thread { private Object lock; private String name; public MyThread(Object lock, String name) { this.lock = lock; this.name = name; } @Override public void run() { synchronized (lock) { try { System.out.println(name + \" get lock,interrupt =\" + Thread.currentThread().isInterrupted()); lock.wait(); //Thread.sleep(2000); } catch (InterruptedException e) { System.out.println(name + \" is interrupt. notify all interrupt =\" + Thread.currentThread().isInterrupted()); lock.notifyAll(); } System.out.println(name + \":notified...\"); } }} 1234567891011121314151617181920212223static void testNotify() throws Exception { MyThread t1 = new MyThread(lock, \"t1\"); MyThread t2 = new MyThread(lock, \"t2\"); Thread thread1 = new Thread(t1); Thread thread2 = new Thread(t2); thread1.start(); thread2.start(); Thread.sleep(1000); long startTime = System.currentTimeMillis(); synchronized (lock) { System.out.println(\"main get lock\"); lock.notifyAll(); } thread1.join(); thread2.join(); long endTime = System.currentTimeMillis(); System.out.println(\"notify lock.time =\" + (endTime - startTime));} testNotify执行结果: t1 get lock,interrupt =falset2 get lock,interrupt =falsemain get lockt2:notified…t1:notified…","link":"/2018/12/11/java/Java线程基础知识/"},{"title":"Java动态代理","text":"代理模式代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。 举一个例子:老师要批作业,可以找学习委员代收,学一委员整理好之后,再送到老师的办公室。这个场景中,学习委员就是一个代理。 ##为什么要用代理模式 中介隔离作用:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。 开闭原则,增加功能:代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。 静态代理静态代理,就是采用代理模式思想,通过硬编码定义一个代理类,来实现代理的功能。由于静态代理灵活性较弱,在实际应用中,大多数采用动态代理模式。 动态代理模式jdk动态代理Jdk动态代理是基于接口的。 Step1 定义一个interface Hello 123public interface Hello { void sayHello(String str);} Step2 定义HelloImpl 123456789101112public final class HelloImpl implements Hello { private int[] array; public HelloImpl() { this.array = new int[1024*10]; } @Override public void sayHello(String str) { System.out.println(\"hello \" + str); }} Step3 实现InvocationHandler的invoke方法 1234567891011121314151617181920212223242526public class LogInvocationHandler implements InvocationHandler { private Hello hello; public LogInvocationHandler(Hello hello) { this.hello = hello; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (\"sayHello\".equals(method.getName())) { System.out.println(\"You said: \" + Arrays.toString(args)); } return method.invoke(hello, args); } public static void main(String[] args) { Object proxy = Proxy.newProxyInstance(LogInvocationHandler.class.getClassLoader(), // 1. 类加载器 new Class<?>[]{Hello.class}, // 2. 代理需要实现的接口,可以有多个 new LogInvocationHandler(new HelloImpl()));// 3. 方法调用的实际处理者 Hello hello = (Hello) proxy; hello.sayHello(\"world\"); System.out.println(hello); }} 但是Jdk动态代理使用上,有局限性。不能所有的类,都要实现一个interface,这会大大增加代码的冗余。如果使用继承的话,就能够解决这个问题。 CGLIBcglib通过继承来实现动态代理,cglib的底层实现,是基于节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。如下是class文件的格式。 123456789101112131415161718192021222324252627282930313233ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }magic: 作为一个魔数,确定这个文件是否是一个能被虚拟机接受的class文件,值固定为0xCAFEBABE。minor_version,major_version:分别表示class文件的副,主版本号,不同版本的虚拟机实现支持的Class文件版本号不同。constant_pool_count:常量池计数器,constant_pool_count的值等于常量池表中的成员数加1。constant_pool:常量池,constant_pool是一种表结构,包含class文件结构及其子结构中引用的所有字符常量、类或接口名、字段名和其他常量。access_flags:access_flags是一种访问标志,表示这个类或者接口的访问权限及属性,包括有ACC_PUBLIC,ACC_FINAL,ACC_SUPER等等。this_class:类索引,指向常量池表中项的一个索引。super_class:父类索引,这个值必须为0或者是对常量池中项的一个有效索引值,如果为0,表示这个class只能是Object类,只有它是唯一没有父类的类。interfaces_count:接口计算器,表示当前类或者接口的直接父接口数量。interfaces[]:接口表,里面的每个成员的值必须是一个对常量池表中项的一个有效索引值。fields_count:字段计算器,表示当前class文件中fields表的成员个数,每个成员都是一个field_info。fields:字段表,每个成员都是一个完整的fields_info结构,表示当前类或接口中某个字段的完整描述,不包括父类或父接口的部分。methods_count:方法计数器,表示当前class文件methos表的成员个数。methods:方法表,每个成员都是一个完整的method_info结构,可以表示类或接口中定义的所有方法,包括实例方法,类方法,以及类或接口初始化方法。attributes_count:属性表,其中是每一个attribute_info,包含以下这些属性,InnerClasses,EnclosingMethod,Synthetic,Signature,Annonation等。 1234567891011121314/** * 动态代理不能是final类 * 如果是final类,会抛出异常:java.lang.IllegalArgumentException: Cannot subclass final class com.wsy.learn.jvm.autoproxy.cglib.HelloConcrete */public class HelloConcrete { /** * 注意,此处如果定义的是final方法,则不会进入intercept方法进行方法增强 * @param str * @return */ public final String sayHello(String str) { return \"HelloConcrete: \" + str; }} 12345678910111213141516public class MyMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println(\"You said: \" + Arrays.toString(args)); return proxy.invokeSuper(obj, args); } public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(HelloConcrete.class); enhancer.setCallback(new MyMethodInterceptor()); HelloConcrete hello = (HelloConcrete)enhancer.create(); System.out.println(hello.sayHello(\"I love you!\")); }} 动态代理最佳实践 - SpringAOP关于什么是SpringAOP,在此不继续阐述。 springAOP既采用了jdk proxy,也采用了cglib。spring会根据情况,来选择jdk动态代理还是cglib动态代理。 如果要代理的对象实现了接口,则默认采用jdk proxy。 如果要代理的对象实现了接口,也可以强制使用cglib。需要配置<aop:aspectj-autoproxy proxy-target-class=”true”/> 如果要代理的对象没有实现接口,则采用cglib动态代理。 但是注意,敲黑板!敲黑板!敲黑板! 采用cglib的前提下,final类不能进行动态代理会抛出异常java.lang.IllegalArgumentException: Cannot subclass final class xxx(final类不能被继承,所以你懂得)。 采用cglib的前提下,final方法也不能进行动态代理,不过不会抛出异常,而是直接跳过功能增强。 采用jdk proxy,final类和final方法都能正常工作。","link":"/2018/12/11/java/java动态代理/"},{"title":"ElasticSearch系列笔记(2)-ElasticSearch 理论基础","text":"本节主要介绍下ElasticSearch 理论基础。知其然知其所以然,如下的理论和算法,就是ElasticSearch的魂! MathJax.Hub.Config({ extensions: [\"tex2jax.js\"], jax: [\"input/TeX\", \"output/HTML-CSS\"], tex2jax: { inlineMath: [ [\"$\",\"$\"], [\"\\\\(\",\"\\\\)\"]], displayMath:[['$$','$$'], [\"\\\\[\",\"\\\\]\"]], processEscapes: true }, \"HTML-CSS\": {availableFonts: [\"TeX\"]} }); 搜索引擎结果的好坏与否,体现在业界所称的在相关性(Relevance)上。相关性的定义包括狭义和广义两方面,狭义的解释是:检索结果和用户查询的相关程度。而从广义的层面,相关性可以理解为为用户查询的综合满意度。直观的来看,从用户进入搜索框的那一刻起,到需求获得满足为止,这之间经历的过程越顺畅,越便捷,搜索相关性就越好。 为了达到这个目的,各个搜索引擎都在不断的优化搜索质量,即提高搜索相关性。本文主要阐述了ElasticSearch中经典的算法模型。 倒排索引(Inverted index)概念 倒排索引(Inverted index),也常被称为反向索引、置入档案或反向档案,是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。它是文档检索系统中最常用的数据结构。通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。倒排索引主要由两个部分组成:“单词词典”和“倒排文件”。 举个例子 有四个文档(document) doc1:中国美丽 doc2:中国黄河美丽 doc3:中国长江美丽 doc4:中国北京首都 经过分词,倒排之后,生成的索引(index)文件如表格所示: 词 文档集合 数量 中国1,2,3,44 美丽1,2,33 黄河21 长江31 首都41 这种数据结构下,用户输入一个query词“美丽”,就会很快定位到文档1,2,3。 TF/IDF算法(term frequency–inverse document frequency)理论依据 tf-idf算法是创建在这样一个假设之上的:对区别文档最有意义的词语应该是那些在文档中出现频率高,而在整个文档集合的其他文档中出现频率少的词语,所以如果特征空间坐标系取 tf 词频作为测度,就可以体现同类文本的特点。另外考虑到单词区别不同类别的能力,tf-idf法认为一个单词出现的文本频数越小,它区别不同类别文本的能力就越大。因此引入了逆文本频度idf的概念,以tf和idf的乘积作为特征空间坐标系的取值测度,并用它完成对权值tf的调整,调整权值的目的在于突出重要单词,抑制次要单词。但是在本质上idf是一种试图抑制噪声的加权,并且单纯地认为文本频率小的单词就越重要,文本频率大的单词就越无用,显然这并不是完全正确的。idf的简单结构并不能有效地反映单词的重要程度和特征词的分布情况,使其无法很好地完成对权值调整的功能,所以tf-idf法的精度并不是很高。此外,在tf-idf算法中并没有体现出单词的位置信息。 虽然TF/IDF存在的问题不少,但是它简单又高效。能够召回高质量的搜索结果。同时Elasticsearch 还有其他模型,如 Okapi-BM25。 数学表达 词频(term frequency,tf):在一份给定的文件里,词频(term frequency,tf)指的是某一个给定的词语在该文件中出现的频率。这个数字是对词数(term count)的归一化,以防止它偏向长的文件。(同一个词语在长文件里可能会比短文件有更高的词数,而不管该词语重要与否。)对于在某一特定文件里的词语 $ti$ 来说,它的重要性可表示为:$$tf_i,_j=\\frac{n_i,_j}{\\sum_{k}n_k,_j}$$ 以上式子中$n_i,_j$是该词在文件$d_j$中的出现次数,而分母则是在文件$d_j$中所有字词的出现次数之和。 逆向文档频率(inverse document frequency,idf):是一个词语普遍重要性的度量。某一特定词语的idf,可以由总文件数目除以包含该词语之文件的数目,再将得到的商取以10为底的对数得到:$$idf_i=lg\\frac{|D|}{|1+j:t_i \\in d_j|}$$ 其中 $|D|$:语料库中的文件总数 $|1 + j:t_i \\in d_j|$:语料库中的文件总数 包含词语 $t_i$ 的文件数目(即 $n_i,_j\\neq 0$的文件数目)如果词语不在数据中,就导致分母为零,因此一般情况下使用$|1+j:t_i \\in d_j|$ 然后 $$tfidf_i,_j = tf_i,_j \\times idf_i$$ 某一特定文件内的高词语频率,以及该词语在整个文件集合中的低文件频率,可以产生出高权重的tf-idf。 举个栗子 假如一篇文件的总词语数是100个,而词语“母牛”出现了3次,那么“母牛”一词在该文件中的词频就是3/100=0.03。而计算文件频率(IDF)的方法是以文件集的文件总数,除以出现“母牛”一词的文件数。所以,如果“母牛”一词在1,000份文件出现过,而文件总数是10,000,000份的话,其逆向文件频率就是lg(10,000,000/1,000)=4。最后的tf-idf的分数为0.03*4=0.12。 布尔模型(Boolean Model) 布尔模型是基于集合论和布尔代数的一种简单检索模型,是早期搜索引擎所使用的检索模型。它的特点是查找那些对于某个查询词返回为“真”的文档。在该模型中,一个查询词就是一个布尔表达式,包括关键词以及逻辑运算符:与(AND),或(OR),非(NOT)。 布尔模型的优点是简单且迅速,非常适合召回数据。再对召回的数据作相关性排序之后,返回最终的结果。 举个栗子 基于倒排索引中的4个文档,使用布尔模型,查询包含“中国和首都”的文档,则相当于 中国 AND 首都,只会检索出doc4文档。如果查询包含“中国和(长江或黄河)”,相当于 中国 AND (长江 OR 黄河),会查询出doc2,和doc3。 向量空间模型(Vector Space Model,VSM) 向量空间模型是一个把文本文件表示为标识符(比如索引)向量的代数模型。它应用于信息过滤、信息检索、索引以及相关排序。 定义 文档和查询都用向量来表示。 $$d_j=(w_1,_j,w_2,_j,…,w_t,_j)$$$$q=(w_1,_q,w_2,_q,…,w_t,_q)$$ 每一维都对应于一个个别的词组。如果某个词组出现在了文档中,那它在向量中的值就非零。已经发展出了不少的方法来计算这些值,这些值叫做(词组)权重。其中一种最为知名的方式是tf-idf权重。词组的定义按不同应用而定。典型的词组就是一个单一的词、关键词、或者较长的短语。如果将词语选为词组,那么向量的维数就是词汇表中的词语个数(出现在语料库中的不同词语的个数)。通过向量运算,可以对各文档和各查询作比较。 应用 据文档相似度理论的假设,如要在一次关键词查询中计算各文档间的相关排序,只需比较每个文档向量和原先查询向量(跟文档向量的类型是相同的)之间的角度偏差。 实际上,计算向量之间夹角的余弦比直接计算夹角本身要简单。 $$cos\\theta = \\frac{d_2.q}{\\lVert d_2\\lVert .\\lVert q \\lVert}$$ 其中 $d_2.q$是文档向量(即右图中的d2)和查询向量(图中的q)的点乘。 $\\lVert d_2 \\lVert$是向量d2的模,而$\\lVert q\\lVert$是向量q的模。向量的模通过下面的公式来计算:$$\\lVert v \\lVert = \\sqrt{\\sum_{k=1}^{n}v_i^2}$$ 由于这个模型所考虑的所有向量都是每个元素严格非负的,因此如果余弦值为零,则表示查询向量和文档向量是正交的,即不符合(换句话说,就是检索项在文档中没有找到)。如果要了解详细的信息可以查看余弦相似性这条目。 Okapi BM25 能与 TF/IDF 和向量空间模型媲美的就是 Okapi BM25 ,它被认为是 当今最先进的 排序函数。 BM25 源自 概率相关模型(probabilistic relevance model) ,而不是向量空间模型,但这个算法也和 Lucene 的实用评分函数有很多共通之处。 BM25 is a bag-of-words retrieval function that ranks a set of documents based on the query terms appearing in each document, regardless of the inter-relationship between the query terms within a document (e.g., their relative proximity). It is not a single function, but actually a whole family of scoring functions, with slightly different components and parameters. One of the most prominent instantiations of the function is as follows. Given a query $Q$, containing keywords $ q_1,…,q_n$, the BM25 score of a document $D$ is:$$score(D,Q) = \\sum_{i=1}^{n}IDF(q_i).\\frac{f(q_i,D).(k_1+1)}{f(q_i,D).(1-b+b.\\frac{\\lvert D \\lvert}{avgd1})}$$where $f(q_i,D)$ is $q_i$’s term frequency in the document $D$, $\\lvert D\\lvert$ is the length of the document $D$ in words, and avgdl is the average document length in the text collection from which documents are drawn. $k_1$ and $b$ are free parameters, usually chosen, in absence of an advanced optimization, as $k_1 \\in[1.2,2.0]$ and $b=0.75$.$IDF(q_i)$is the IDF (inverse document frequency) weight of the query term $q_i$. It is usually computed as:$$IDF(q_i)=log\\frac{N-n(q_i)+0.5}{n(q_i)+0.5}$$where $N$ is the total number of documents in the collection, and $n(q_i)$ is the number of documents containing $q_i$. There are several interpretations for IDF and slight variations on its formula. In the original BM25 derivation, the IDF component is derived from the Binary Independence Model. Please note that the above formula for IDF shows potentially major drawbacks when using it for terms appearing in more than half of the corpus documents. These terms’ IDF is negative, so for any two almost-identical documents, one which contains the term and one which does not contain it, the latter will possibly get a larger score. This means that terms appearing in more than half of the corpus will provide negative contributions to the final document score. This is often an undesirable behavior, so many real-world applications would deal with this IDF formula in a different way: Each summand can be given a floor of 0, to trim out common terms;The IDF function can be given a floor of a constant $\\epsilon$ , to avoid common terms being ignored at all;The IDF function can be replaced with a similarly shaped one which is non-negative, or strictly positive to avoid terms being ignored at all. 不像 TF/IDF ,BM25 有一个比较好的特性就是它提供了两个可调参数: k1 这个参数控制着词频结果在词频饱和度中的上升速度。默认值为 1.2 。值越小饱和度变化越快,值越大饱和度变化越慢。 b 这个参数控制着字段长归一值所起的作用, 0.0 会禁用归一化, 1.0 会启用完全归一化。默认值为 0.75 。在实践中,调试 BM25 是另外一回事, k1 和 b 的默认值适用于绝大多数文档集合,但最优值还是会因为文档集不同而有所区别,为了找到文档集合的最优值,就必须对参数进行反复修改验证。 总结 本章对ES的理论基础,有一个全面的概括,包括了布尔模型和向量空间模型。这两种模型是lucene的核心理论基础。同时,也介绍了用于构建向量空间权值的两种算法,简单实用的TF/IDF算法和更先进Okapi BM25。大家可以先摸清这些模型的概念,然后在不断的推敲模型本质,温故而知新。 下一章展望 lucene实用评分函数讲解,让我们了解到lucene是如何改进固有的算法模型,来实现一套更优秀的算法评分模型的。 参考文献信息检索中“相关性”概念的研究 搜索引擎的检索模型-查询与文档的相关度计算 怎样量化评价搜索引擎的结果质量 lucene检索得分模型 Lucene 打分算法 apache-lucene实用评分算法官方文档 Lucene 6.0 默认算分公式 TF-IDF 详解(官方文档翻译)","link":"/2018/07/11/search/ElasticSearch-相关性实现/"},{"title":"elasticSearch系列笔记(3)-elastic本地源码调试","text":"ElasticSearch本地源码调试转自:https://www.felayman.com/articles/2017/11/10/1510291087246.html Elasticsearch版本为 5.5.1 1. 源码下载 git clone https://github.com/elastic/elasticsearch 2. 分支切换 git checkout v5.5.1 3 运行gradle idea,gradle build -x test如果运行gradle idea会报如下错误:请修改elasticsearch源文件中的BuildPlugin.groory文件中的findJavaHome()方法的第一行代码,源码为 String javaHome = System.getenv(‘JAVA_HOME’),修改为String javaHome = “你的JAVA_HOME地址,输入echo $JAVA_HOME($)” 1234What went wrong:A problem occurred evaluating project ':benchmarks'.> Failed to apply plugin [id 'elasticsearch.build']> JAVA_HOME must be set to build Elasticsearch 这里我使用的是idea工具来调试源码,因此先不要用idea导入该项目,而是进入到下载的elasticsearch目录下,执行gradle idea,等待漫长的依赖下载,等待下载完成后 执行 gradle build -x test 对源码进行编译,等待编译完成. 4. 注释掉jar hell相关代码 全局搜索JarHell.checkJarHell,以及checkJarHell,注释掉相应的代码,否则运行会报错 5. 配置Edit Configuation mainClass为 org.elasticsearch.bootstrap.Elasticsearch VM options为 Des.path.home=/Users/admin/elk/elasticsearch-5.5.0 -Dlog4j2.disable.jmx=true 6. 运行org.elasticsearch.bootstrap.Elasticsearch中的main方法123456789101112131415161718192021222324252627282930 [2017-07-11T22:00:08,396][INFO ][o.e.n.Node ] [] initializing ...[2017-07-11T22:00:08,487][INFO ][o.e.e.NodeEnvironment ] [iobPZcg] using [1] data paths, mounts [[/ (/dev/disk1)]], net usable_space [133.3gb], net total_space [232.6gb], spins? [unknown], types [hfs][2017-07-11T22:00:08,487][INFO ][o.e.e.NodeEnvironment ] [iobPZcg] heap size [3.5gb], compressed ordinary object pointers [true][2017-07-11T22:00:08,503][INFO ][o.e.n.Node ] node name [iobPZcg] derived from node ID [iobPZcgEQBKodmURFuo_Gw]; set [node.name] to override[2017-07-11T22:00:08,503][INFO ][o.e.n.Node ] version[5.5.0-SNAPSHOT], pid[65630], build[Unknown/Unknown], OS[Mac OS X/10.12/x86_64], JVM[Oracle Corporation/Java HotSpot(TM) 64-Bit Server VM/1.8.0_101/25.101-b13][2017-07-11T22:00:08,503][INFO ][o.e.n.Node ] JVM arguments [-Des.path.home=/Users/admin/elk/elasticsearch-5.5.0, -Dlog4j2.disable.jmx=true, -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=49964:/Applications/IntelliJ IDEA.app/Contents/bin, -Dfile.encoding=UTF-8][2017-07-11T22:00:08,504][WARN ][o.e.n.Node ] version [5.5.0-SNAPSHOT] is a pre-release version of Elasticsearch and is not suitable for production[2017-07-11T22:00:09,201][INFO ][o.e.p.PluginsService ] [iobPZcg] loaded module [aggs-matrix-stats][2017-07-11T22:00:09,201][INFO ][o.e.p.PluginsService ] [iobPZcg] loaded module [ingest-common][2017-07-11T22:00:09,201][INFO ][o.e.p.PluginsService ] [iobPZcg] loaded module [lang-expression][2017-07-11T22:00:09,201][INFO ][o.e.p.PluginsService ] [iobPZcg] loaded module [lang-groovy][2017-07-11T22:00:09,201][INFO ][o.e.p.PluginsService ] [iobPZcg] loaded module [lang-mustache][2017-07-11T22:00:09,201][INFO ][o.e.p.PluginsService ] [iobPZcg] loaded module [lang-painless][2017-07-11T22:00:09,201][INFO ][o.e.p.PluginsService ] [iobPZcg] loaded module [parent-join][2017-07-11T22:00:09,202][INFO ][o.e.p.PluginsService ] [iobPZcg] loaded module [percolator][2017-07-11T22:00:09,202][INFO ][o.e.p.PluginsService ] [iobPZcg] loaded module [reindex][2017-07-11T22:00:09,202][INFO ][o.e.p.PluginsService ] [iobPZcg] loaded module [transport-netty3][2017-07-11T22:00:09,202][INFO ][o.e.p.PluginsService ] [iobPZcg] loaded module [transport-netty4][2017-07-11T22:00:09,202][INFO ][o.e.p.PluginsService ] [iobPZcg] no plugins loaded[2017-07-11T22:00:11,386][INFO ][o.e.d.DiscoveryModule ] [iobPZcg] using discovery type [zen][2017-07-11T22:00:12,068][INFO ][o.e.n.Node ] initialized[2017-07-11T22:00:12,068][INFO ][o.e.n.Node ] [iobPZcg] starting ...[2017-07-11T22:00:12,120][INFO ][i.n.u.i.PlatformDependent] Your platform does not provide complete low-level API for accessing direct buffers reliably. Unless explicitly requested, heap buffer will always be preferred to avoid potential system instability.[2017-07-11T22:00:12,291][INFO ][o.e.t.TransportService ] [iobPZcg] publish_address {127.0.0.1:9300}, bound_addresses {[fe80::1]:9300}, {[::1]:9300}, {127.0.0.1:9300}[2017-07-11T22:00:12,308][WARN ][o.e.b.BootstrapChecks ] [iobPZcg] initial heap size [268435456] not equal to maximum heap size [4294967296]; this can cause resize pauses and prevents mlockall from locking the entire heap[2017-07-11T22:00:15,383][INFO ][o.e.c.s.ClusterService ] [iobPZcg] new_master {iobPZcg}{iobPZcgEQBKodmURFuo_Gw}{J8yIAMTeS3uOKeW35gG0kw}{127.0.0.1}{127.0.0.1:9300}, reason: zen-disco-elected-as-master ([0] nodes joined)[2017-07-11T22:00:15,408][INFO ][o.e.h.n.Netty4HttpServerTransport] [iobPZcg] publish_address {127.0.0.1:9200}, bound_addresses {[fe80::1]:9200}, {[::1]:9200}, {127.0.0.1:9200}[2017-07-11T22:00:15,409][INFO ][o.e.n.Node ] [iobPZcg] started[2017-07-11T22:00:15,639][INFO ][o.e.g.GatewayService ] [iobPZcg] recovered [2] indices into cluster_state[2017-07-11T22:00:15,850][INFO ][o.e.c.r.a.AllocationService] [iobPZcg] Cluster health status changed from [RED] to [YELLOW] (reason: [shards started [[twitter][3]] ...]). 可能遇到的异常 ERROR: the system property [es.path.conf] must be set 原因是没有指定es.path.conf,设置-Des.path.conf=你调试源码版本对应的conf目录 Exception in thread “main” java.lang.IllegalStateException: path.home is not configured 原因是因为没有为elasticsearch配置path.home参数,可以在Edit Configuation中设置虚拟机参数:-Des.path.home=你下载的对应的elasticsearch的安装目录,这么做的原因是elasticsearch在启动中会加载一些默认配置以及插件,我们直接加载elasticsearch安装目录下的配置和插件即可,后面会在源码中体现 2017-06-23 14:00:44,760 main ERROR Could not register mbeans java.security.AccessControlException: access denied (“javax.management.MBeanTrustPermission” “register”) 原因是因为elasticsearch在启动过程中使用到了jmx,我们这里禁止使用即可,配置也是在Edit Configuation中设置虚拟机参数 -Dlog4j2.disable.jmx=true org.elasticsearch.bootstrap.StartupException: org.elasticsearch.bootstrap.BootstrapException: java.lang.IllegalStateException: jar hell!或Classname: org.elasticsearch.search.aggregations.matrix.MatrixAggregationPlugin due to jar hell 原因是因为elasticsearch中大量存在一个类或一个资源文件存在多个jar中,我们注释掉相应代码即可,主要是PluginsService中374行的JarHell.checkJarHell(union)以及Bootstrap中220行的JarHell.checkJarHell(),最简单的方式就是将JarHell.checkJarHell()中的方法体注释掉 org.elasticsearch.bootstrap.StartupException: java.lang.IllegalArgumentException: plugin [aggs-matrix-stats] is incompatible with version [7.0.0-alpha1]; was designed for version [5.6.1] 原因是一般情况下我们调试的源码非某个发布版本,有些配置项并未发布,我们的配置与当前代码的版本匹配不上,这个时候我们需要将调试的源码设置成某个发布版本,一般来说,Elasticsearch每发布一个稳定版本,都会有一个对应的tag,我们进入到ES源码目录下执行git tag, 我这里调试的版本为v5.6.1,所以执行git checkout v5.6.1,切换到v5.6.1tag. 另一种源码调试方式:远程调试如果上面第五个报错之后解决不了无法继续进行,可以选择这种方式: 在 Elasticsearch 源码目录下打开 CMD,输入下面的命令启动一个 debug 实例 1gradle run --debug-jvm 如果启动失败可能需要先执行 gradle clean 再 gradle run --debug-jvm 或者 先退出 IDEA 在 IDEA 中打开 Edit Configurations,添加 remote 配置 host 和 port 点击 debug,浏览器访问 http://localhost:9200/,即可看到ES返回的信息 随机调试一下, 打开 elasticsearch/server/src/main/org/elasticsearch/rest/action/cat 下的 RestHealthAction 类,在第 54 行出设置一个断点,然后浏览器访问 http://localhost:9200/_cat/health,可以看到断点已经捕获到该请求了 运行成功,可以开始设置断点进行其他调试 源码本地编译工程包在es源码根目录,执行 gradle assemble 命令,编译好之后工程ZIP包位于下图位置。 其他可能遇到的问题1. 错误信息如下 1JAVA8_HOME required to run tasks gradle 配置环境变量 JAVA8_HOME,值为 JDK8 的安装目录 2. 错误信息如下 1234567[2018-08-22T13:07:23,197][INFO ][o.e.t.TransportService ] [EFQliuV] publish_address {10.100.99.118:9300}, bound_addresses {[::]:9300}[2018-08-22T13:07:23,211][INFO ][o.e.b.BootstrapChecks ] [EFQliuV] bound or publishing to a non-loopback address, enforcing bootstrap checksERROR: [1] bootstrap checks failed[1]: initial heap size [268435456] not equal to maximum heap size [4273995776]; this can cause resize pauses and prevents mlockall from locking the entire heap[2018-08-22T13:07:23,219][INFO ][o.e.n.Node ] [EFQliuV] stopping ...2018-08-22 13:07:23,269 Thread-2 ERROR No log4j2 configuration file found. Using default configuration: logging only errors to the console. Set system property 'log4j2.debug' to show Log4j2 internal initialization logging.Disconnected from the target VM, address: '127.0.0.1:5272', transport: 'socket' 在 Edit Configurations 的 VM options 加入下面配置 -Xms2g-Xmx2g 参考文献elasticSearch权威指南2.xelasticSearch reference 6.2 http://laijianfeng.org/2018/08/%E6%95%99%E4%BD%A0%E7%BC%96%E8%AF%91%E8%B0%83%E8%AF%95Elasticsearch-6-3-2%E6%BA%90%E7%A0%81/","link":"/2018/07/11/search/elasticSearch-本地源码调试/"},{"title":"分布式系统分片方法研究","text":"数据分片的基本算法介绍,以及数据分片在Redis中是如何运用的。 前言本文结合了带着问题学习分布式系统之数据分片和翻译了 Partitioning: how to split data among multiple Redis instances.,通过对Redis数据分片的学习,举一反三其他的分布式数据分片的实现。 分片的作用可能很多人搞不清楚数据分片和数据冗余的区别,下图便很形象的说明了什么是数据分片,什么是数据冗余。 其中,数据集A、B属于数据分片,原始数据被拆分成两个正交子集分布在两个节点上。而数据集C属于数据冗余,同一份完整的数据在两个节点都有存储。当然,在实际的分布式系统中,数据分片和数据冗余一般都是共存的。 何为数据分片(segment,fragment, shard, partition),就是按照一定的规则,将数据集划分成相互独立、正交的数据子集,然后将数据子集分布到不同的节点上。注意,这里提到,数据分片需要按照一定的规则,不同的分布式应用有不同的规则,但都遵循同样的原则:按照最主要、最频繁使用的访问方式来分片。 分片在分布式系统中,主要为了解决两大问题: 1、用多台服务器的内存和磁盘来存储更多的数据。没有分片你的数据需求将受到单机资源的限制。 2、它允许将计算能力扩展到多核和多台计算机,并将网络带宽扩展到多台计算机和网络适配器。 分片基础算法对于数据分片的任何算法或思想,都需要思考如下几个问题: 具体如何划分原始数据集 当原问题的规模变大的时候,能否通过增加节点来动态适应? 当某个节点故障的时候,能否将该节点上的任务均衡的分摊到其他节点? 对于可修改的数据(比如数据库数据),如果其节点数据变大,能否以及如何将部分数据迁移到其他负载较小的节点,及达到动态均衡的效果? 元数据的管理(即数据与节点对应关系)规模?元数据更新的频率及复杂度?元数据的一致性保证? 为了后面分析不同的数据分片方式,假设有三个物理节点,编号为N0, N1, N2;有以下几条记录: R0: {id: 95, name: ‘aa’, tag:’older’} R1: {id: 302, name: ‘bb’,} R2: {id: 759, name: ‘aa’,} R3: {id: 607, name: ‘dd’, age: 18} R4: {id: 904, name: ‘ff’,} R5: {id: 246, name: ‘gg’,} R6: {id: 148, name: ‘ff’,} R7: {id: 533, name: ‘kk’,} ##Range Partitioning 可以指定N0节点负责id在区间[0,333],N1节点负责id在区间[334,666],N3节点负责id在区间[667, 1000]。如下示意图所示,数据分布到三个数据节点上。 这种方式的优点是非常的简单,首先选择一个特征键,然后对特征键划分区间,数据只放到满足区间的节点上。但是这种方式的缺点也很多: 如果数据的特征键在某一个区间内特别密集,在其他的区间内又特别的稀疏,会导致数据倾斜,使得数据多的节点成为瓶颈,其他数据少的节点又不能发挥其作用。 扩容缩容平衡数据(rebalance)成本高,有可能需要移动全部分片的数据。 ##Hash Partitioning Hash分区跟Range Partitioning其实比较相似,只不过把区间,换成了哈希函数取余,hash(key)%mod,mod是节点的数量,取余的值就代表数据要放入到哪个节点中。 Hash分区的优点也是比较简单,缺点同Range Partitioning一致,只不过如果成倍增加节点数量的话,这样概率上来讲至多有50%的数据迁移。 Consistent Hashing一致性hash是将数据按照特征值映射到一个首尾相接的hash环上,同时也将节点(按照IP地址或者机器名hash)映射到这个环上。对于数据,从数据在环上的位置开始,顺时针找到的第一个节点即为数据的存储节点。这里仍然以上述的数据为例,假设id的范围为[0, 1000],N0, N1, N2在环上的位置分别是100, 400, 800,那么hash环示意图与数据的分布如下: 可以看到相比于上述的hash方式,一致性hash方式需要维护的元数据额外包含了节点在环上的位置,但这个数据量也是非常小的。 一致性hash在增加或者删除节点的时候,受到影响的数据是比较有限的,比如这里增加一个节点N3,其在环上的位置为600,因此,原来N2负责的范围段(400, 800]现在由N3(400, 600] N2(600, 800]负责,因此只需要将记录R7(id:533) 从N2,迁移到N3: 不难发现一致性hash方式在增删的时候只会影响到hash环上相邻的节点,不会发生大规模的数据迁移。 但是,一致性hash方式在增加节点的时候,只能分摊一个已存在节点的压力;同样,在其中一个节点挂掉的时候,该节点的压力也会被全部转移到下一个节点。我们希望的是“一方有难,八方支援”,因此需要在增删节点的时候,已存在的所有节点都能参与响应,达到新的均衡状态。 因此,在实际工程中,一般会引入虚拟节点(virtual node)的概念。即不是将物理节点映射在hash换上,而是将虚拟节点映射到hash环上。虚拟节点的数目远大于物理节点,因此一个物理节点需要负责多个虚拟节点的真实存储。操作数据的时候,先通过hash环找到对应的虚拟节点,再通过虚拟节点与物理节点的映射关系找到对应的物理节点。 引入虚拟节点后的一致性hash需要维护的元数据也会增加:第一,虚拟节点在hash环上的问题,且虚拟节点的数目又比较多;第二,虚拟节点与物理节点的映射关系。但带来的好处是明显的,当一个物理节点失效时,hash环上多个虚拟节点失效,对应的压力也就会发散到多个其余的虚拟节点,事实上也就是多个其余的物理节点。在增加物理节点的时候同样如此。 工程中,Dynamo、Cassandra都使用了一致性hash算法,且在比较高的版本中都使用了虚拟节点的概念。在这些系统中,需要考虑综合考虑数据分布方式和数据副本,当引入数据副本之后,一致性hash方式也需要做相应的调整, 可以参加cassandra的相关文档。 Hash Slot虚拟槽就是在物理机的基础上,有虚拟出了远远大于物理机数量的虚拟槽位,数据是保存在虚拟槽位中的。如下图所示。 虚拟槽的好处是当增加节点或者删除节点的时候,数据的压力能分布在多个虚拟槽中,而多个虚拟槽又分布到多个物理机中,这样会把压力均匀的分散到其他物理节点上。 Range base简单来说,就是按照关键值划分成不同的区间,每个物理节点负责一个或者多个区间。其实这种方式跟一致性hash有点像,可以理解为物理节点在hash环上的位置是动态变化的。 还是以上面的数据举例,三个节点的数据区间分别是N0(0, 200], N1(200, 500], N2(500, 1000]。那么数据分布如下: 注意,区间的大小不是固定的,每个数据区间的数据量与区间的大小也是没有关系的。比如说,一部分数据非常集中,那么区间大小应该是比较小的,即以数据量的大小为片段标准。在实际工程中,一个节点往往负责多个区间,每个区间成为一个块(chunk、block),每个块有一个阈值,当达到这个阈值之后就会分裂成两个块。这样做的目的在于当有节点加入的时候,可以快速达到均衡的目的。 不知道读者有没有发现,如果一个节点负责的数据只有一个区间,range based与没有虚拟节点概念的一致性hash很类似;如果一个节点负责多个区间,range based与有虚拟节点概念的一致性hash很类似。 range based的元数据管理相对复杂一些,需要记录每个节点的数据区间范围,特别单个节点对于多个区间的情况。而且,在数据可修改的情况下,如果块进行分裂,那么元数据中的区间信息也需要同步修改。 range based这种数据分片方式应用非常广泛,比如MongoDB, PostgreSQL, HDFS。 ##总结 对以上提到的分片方式进行简单总结,主要是针对提出的几个问题: 映射难度 元数据 节点增删 数据动态均衡 Range partition 简单 非常简单,几乎不用修改 需要迁移的数据比较多 不支持 hash方式 简单 非常简单,几乎不用修改 需要迁移的数据比较多 不支持 consistent hash without virtual node 简单 比较简单,取决于节点规模,几乎不用修改 增删节点的时候只影响hash环上相邻节点,但不能使所有节点都参与数据迁移过程 不支持 consistent hash with virtual node 中等 稍微复杂一些,主要取决于虚拟节点规模,很少修改 需要迁移的数据比较少,且所有节点都能贡献部分数据 若支持(修改虚拟节点与物理节点映射关系) range based 较为复杂 取决于每个块的大小,一般来说规模较大;且修改频率较高 需要迁移的数据比较少,且所有节点都能贡献部分数据 支持,且比较容易 上面的数据动态均衡,值得是上述问题的第4点,即如果某节点数据量变大,能否以及如何将部分数据迁移到其他负载较小的节点 不同的分片实现客户端分区(Client side partitioning)客户端直接选择出正确的节点(node)读或者写。Redis Cluster是客户端分区的一种实现。 代理分区(Proxy assisted partitioning)客户端发送请求到Proxy服务,由代理服务端负责选择正确的节点并实现数据存储Server端的通信协议,代理把请求发送到数据存储Server端并返回数据给客户端(Client)。twemproxy实现了代理分区这种方式。 ##Query routing分区 客户端发送请求到随机的节点B(node)上,接受到请求的实例负责确定正确的存储节点A,并把请求转发到A节点上,A节点处理完毕之后,返回结果到B,B返回结果到客户端。 分片的不利条件1、事务处理异常的困难 2、多个Key(multiple keys)同时操作讲很难支持。例如Redis Pipline功能,在Cluster模式默认是不支持的,当然如果能够知道每个key属于的具体节点,是可以实现多pipline同时更新的。 3、分区的粒度是基于key的选定,如果这个key下是大数据结构,比如Set,List,Hash表,那么就很难利用分区的优势,数据将产生倾斜。 4、数据处理更加复杂(is more complex)。数据分布到多个节点上,数据的维护工作将变得困难,并且数据恢复(recover)和备份(backup)的算法变得更加复杂,且空间复杂度和时间复杂度都会提升。比如,在Redis集群中,将持有更多的RDB/AOF文件,当备份数据时,需要聚合多个实例的文件。 5、扩容和缩容变得很复杂(can be complex)。例如Redis Cluster支持大多数的增加或减少节点的数据平衡(rebalancing)问题,并对用户透明,但是客户端分区或者代理分区不支持动态透明的数据平衡,可以用Pre-sharding来讨巧的实现数据平衡。 数据存储还是缓存尽管无论将Redis用作数据存储还是用作缓存,Redis中的分区在概念上都是相同的,但将其用作数据存储时存在很大的限制。 当Redis用作数据存储时,给定的Key必须始终映射到相同的Redis实例。 当Redis用作缓存时,如果给定的节点不可用,则使用不同的节点并不是什么大问题,因为我们希望提高系统的可用性(缓存穿透时系统来响应请求即可)。 如果给定的key首选节点不可用,则一致性哈希实现通常可以切换到其他节点。同样,如果添加新节点,则部分key将开始存储在新节点上。 这里主要的概念如下: 如果Redis用作缓存扩容(scaling up)和缩容(scaling down)使用一致性hash是容易实现的 如果将Redis用作存储,则使用固定的“key-node”映射(a fixed keys-to-nodes map ),因此节点数必须固定并且不能变化。 否则,需要一个能够在添加或删除节点时在节点之间重新平衡key的系统,并且目前只有Redis Cluster能够做到这一点-Redis Cluster于2015年4月1日全面可用并投入生产。 Redis分片实现方案总结##Redis集群(Redis Cluster) Redis集群是最推荐的方式来实现数据分片和高可用。它已经在2015年就具备生产环境的要求。可以阅读官方文档来了解更多信息。一旦Redis Cluster可用,并且如果兼容Redis Cluster的客户端可用于您的语言,则Redis Cluster将成为Redis分区的事实上的标准。 Redis Cluster分片的实现介于query routing 和 client side partitioning.Redis Cluster客户端是直连集群中的节点的(没有通过任何的其他代理,这点比Twemproxy要优),Redis Cluster在客户端的帮助下实现了混合形式的查询路由(请求不会从Redis实例直接转发到另一个实例,而是会将客户端重定向到正确的节点)。这种实现方式,使得Redis Cluster的情况下的性能不亚于单实例Redis。 TwemproxyTwemproxy是Twitter开源的一款Redis主从代理中间件。在Redis Cluster出现之前,很多人和组织使用这款中间件。它跟Redis Cluster相比,不能满足在线的扩缩容(scaling up & down),并且每次请求都要经过一层代理,对性能也是有一定损耗的。 客户端一致性Hash作为Twemproxy的替代方案,可以在客户端实现一致性hash路由算法,实现客户端路由,还可以使用其他的类似于一致性hash的其他算法。已经有很多Redis clients支持一致性hash的客户端路由,比如 Redis-rb 和 Predis.请参考所有的客户端路由来找到适合你的编程语言的选项。 引用带着问题学习分布式系统之数据分片 Partitioning: how to split data among multiple Redis instances. Redis Cluster集群知识学习总结","link":"/2019/12/13/distributed/分布式系统分片方法研究/"},{"title":"Jedis集群模式经典实现","text":"Jedis是Redis的Java客户端,本代码是Jedis应用的一个范例。 Redis常用模式Redis分了了主从模式和集群模式。 主从模式主从模式即使用一个Redis实例作为主机(Master),其余的实例作为备份机(Slave),Master支持写入和读取等各种操作,Slave支持读操作和与Master同步数据。主从模式的核心思想是读写分离,数据冗余存储和HA,Master节点出现问题,可以通过Redis Sentinel做到主从切换。 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务: 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。 集群模式Redis主从模式虽然很强大,但是其单Master的架构,当遇到单机内存、并发、流量等瓶颈时便束手无策,Redis集群的出现就是为了解决主从模式所遇到的问题。在Redis Cluster面世之前,业界为了解决Redis这个问题,也出现了一些优秀的Redis集群解决方案,比如Twemproxy和Codis,如果大家感兴趣,可以去学习,本文不再比较各自的优劣。 集群模式数据分布数据分布理论摘抄自参考文档3,该文作者已经有了很好的总结。 分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,每个节点负责整体数据的一个子集。 数据分布通常有哈希分区和顺序分区两种方式,对比如下: 分区方式 特点 相关产品 哈希分区 离散程度好,数据分布与业务无关,无法顺序访问 Redis Cluster,Cassandra,Dynamo 顺序分区 离散程度易倾斜,数据分布与业务相关,可以顺序访问 BigTable,HBase,Hypertable 由于Redis Cluster采用哈希分区规则,这里重点讨论哈希分区。常见的哈希分区规则有几种: 节点取余分区:使用特定的数据,如 Redis的键或用户ID,再根据节点数量N使用公式:hash(key)% N计算出 哈希值,用来决定数据 映射 到哪一个节点上。这种方式简单实用,常用语数据库分库分表,一般采用预分区的方式,提前按预估的数据量规划好分区数。缺点也很明显,当节点数量发生变化时,比如发生扩容或缩容时,数据节点的映射关系需要重新计算,会导致数据的重新迁移。 一致性哈希分区:一致性哈希可以很好的解决稳定性问题,可以将所有的存储节点排列在首尾相接的Hash环上,每个key在计算Hash后顺时针找到临接的存储节点存放。当有节点加入或退出时,仅影响该节点在hash环上顺时针相邻的后续节点。加入和删除节点,只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响,但是还是会造成哈希环中部分数据无法命中。当使用少量节点时,节点变化将大范围影响哈希环中的数据映射,不适合少量数据节点的分布式方案。普通的一致性哈希分区在增减节点时,需要增加一倍或减去一半节点,才能保证数据和负载的均衡。 虚拟槽分区:虚拟槽分区巧妙的使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围一般远远大于节点数,比如Redis Cluster的槽范围是0~16383。槽是集群内数据管理和迁移的基本单位。采用大范围槽的主要目的是为了方便数据拆分和集群扩展。每个节点会负责一定数量的槽。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽数量,都不会造成集群不可用的状态。 Redis虚拟槽分区的特点: 解耦数据和节点之间的关系,简化了节点扩容和收缩的难度 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。 支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景。 Redis集群的功能限制Redis 集群相对 单机 在功能上存在一些限制,需要 开发人员 提前了解,在使用时做好规避。 key 批量操作 支持有限。 类似 mset、mget 操作,目前只支持对具有相同 slot 值的 key 执行 批量操作。对于 映射为不同 slot 值的 key 由于执行 mget、mget 等操作可能存在于多个节点上,因此不被支持。 key 事务操作 支持有限。 只支持 多 key 在 同一节点上 的 事务操作,当多个 key 分布在 不同 的节点上时 无法 使用事务功能。 key 作为 数据分区 的最小粒度 不能将一个 大的键值 对象如 hash、list 等映射到 不同的节点。 不支持 多数据库空间 单机 下的 Redis 可以支持 16 个数据库(db0 ~ db15),集群模式 下只能使用 一个 数据库空间,即 db0。 复制结构 只支持一层 从节点 只能复制 主节点,不支持 嵌套树状复制 结构。 Jedis经典实现123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290import org.apache.ibatis.reflection.MetaObject;import org.apache.ibatis.reflection.SystemMetaObject;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import redis.clients.jedis.*;import redis.clients.util.JedisClusterCRC16;import java.util.*;public class RedisCacheDelegate extends AbstractCache implements CacheManager, Cache { private static Logger logger = LoggerFactory.getLogger(RedisCacheDelegate.class); /** * 集群节点 */ private String clusterNodes; /** * 重试次数 */ private int maxAttempts; /** * 超时时间,单位是秒 */ private int timeout; private JedisCluster jedisCluster; private JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); private static RedisCacheDelegate redisCacheDelegate = null; private JedisClusterInfoCache cache; private final static String WARM_KEY = \"warm_key\"; private final static String WARM_VALUE = \"value\"; public static RedisCacheDelegate getInstant(CacheProperties cacheProperties) { if (redisCacheDelegate == null) { synchronized (RedisCacheDelegate.class) { if (redisCacheDelegate == null) { redisCacheDelegate = new RedisCacheDelegate(cacheProperties.getNodes(), cacheProperties.getTimeout(), cacheProperties.getMaxAttempts()); } } } return redisCacheDelegate; } private RedisCacheDelegate(String clusterNodes, int timeout, int maxAttempts) { this.clusterNodes = clusterNodes; this.timeout = timeout; this.maxAttempts = maxAttempts; init(); } private JedisPoolConfig getJedisPoolConfig() { //连接最长等待时间,默认是-1 jedisPoolConfig.setMaxWaitMillis(200); //连接池最大数量 jedisPoolConfig.setMaxTotal(50); //最小闲置个数 闲置超过最小闲置个数但不超过最大闲置个数,则逐步清理闲置直到最小闲置个数 jedisPoolConfig.setMinIdle(10); //最大闲置个数 闲置超过最大闲置个数则直接杀死超过部分 jedisPoolConfig.setMaxIdle(30); //连接耗尽等待,等待最长{MaxWaitMillis}毫秒 jedisPoolConfig.setBlockWhenExhausted(true); //是否开启jmx监控 jedisPoolConfig.setJmxEnabled(true); //是否开启空闲资源监测 jedisPoolConfig.setTestWhileIdle(true); //空闲资源的检测周期(单位为毫秒) jedisPoolConfig.setMinEvictableIdleTimeMillis(60000); //资源池中资源最小空闲时间(单位为毫秒),达到此值后空闲资源将被移除 jedisPoolConfig.setTimeBetweenEvictionRunsMillis(30000); //做空闲资源检测时,每次的采样数,如果设置为-1,就是对所有连接做空闲监测 jedisPoolConfig.setNumTestsPerEvictionRun(-1); return jedisPoolConfig; } @Override public void init() { String[] serverArray = clusterNodes.split(\",\"); Set<HostAndPort> nodes = new HashSet<>(); for (String ipPort : serverArray) { String[] ipPortPair = ipPort.split(\":\"); nodes.add(new HostAndPort(ipPortPair[0].trim(), Integer.valueOf(ipPortPair[1].trim()))); } jedisCluster = new JedisCluster(nodes, timeout * 1000, maxAttempts, getJedisPoolConfig()); MetaObject metaObject = SystemMetaObject.forObject(jedisCluster); cache = (JedisClusterInfoCache) metaObject.getValue(\"connectionHandler.cache\"); warm(); } /** * warm the jedis pool */ @Override public void warm() { set(WARM_KEY, WARM_VALUE, 60); for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) { ttl(WARM_KEY); } } @Override public void set(String key, String value) { jedisCluster.set(key, value); } @Override public void set(String key, String value, int expiredTime) { jedisCluster.setex(key, expiredTime, value); } @Override public void mSet(Map<String, String> data) { if (data != null && data.size() > 0) { data.forEach((key, value) -> jedisCluster.set(key, value)); } } @Override public void mSetPipLine(Map<String, String> data) { setPipLine(data, 0); } private void setPipLine(Map<String, String> data, int expiredTime) { if (data.size() < 1) { return; } //保存地址+端口和命令的映射 Map<JedisPool, Map<String, String>> jedisPoolMap = new HashMap<>(); JedisPool currentJedisPool = null; for (String key : data.keySet()) { //计算哈希槽 int crc = JedisClusterCRC16.getSlot(key); //通过哈希槽获取节点的连接 currentJedisPool = cache.getSlotPool(crc); if (jedisPoolMap.containsKey(currentJedisPool)) { jedisPoolMap.get(currentJedisPool).put(key, data.get(key)); } else { Map<String, String> inner = new HashMap<>(); inner.put(key, data.get(key)); jedisPoolMap.put(currentJedisPool, inner); } } //保存结果 Map<String, String> map = null; //执行 for (Map.Entry<JedisPool, Map<String, String>> entry : jedisPoolMap.entrySet()) { try { currentJedisPool = entry.getKey(); map = entry.getValue(); Jedis jedis = currentJedisPool.getResource(); //获取pipeline Pipeline currentPipeline = jedis.pipelined(); // NX是不存在时才set, XX是存在时才set, EX是秒,PX是毫秒 if (expiredTime > 0) { map.forEach((k, v) -> currentPipeline.setex(k, expiredTime, v)); } else { map.forEach((k, v) -> currentPipeline.set(k, v)); } //从pipeline中获取结果 currentPipeline.sync(); currentPipeline.close(); jedis.close(); } catch (Exception e) { logger.error(\"setPipline error.\", e); } } } @Override public void mSet(Map<String, String> data, int expiredTime) { if (data != null && data.size() > 0) { data.forEach((key, value) -> jedisCluster.setex(key, expiredTime, value)); } } @Override public void mSetPipLine(Map<String, String> data, int expiredTime) { setPipLine(data, expiredTime); } @Override public String get(String key) { return jedisCluster.get(key); } @Override public List<String> mGet(List<String> keys) { if (keys.size() < 1) { return null; } List<String> result = new ArrayList<>(keys.size()); for (String key : keys) { result.add(jedisCluster.get(key)); } return result; } @Override public List<String> mGetPipLine(List<String> key) { return getPipLine(key); } @Override public long ttl(String key) { return jedisCluster.ttl(key); } private List<String> getPipLine(List<String> keys) { if (keys.size() < 1) { return null; } List<String> result = new ArrayList<>(keys.size()); Map<String, String> resultMap = new HashMap<>(keys.size()); if (keys.size() == 1) { result.add(jedisCluster.get(keys.get(0))); return result; } //保存地址+端口和命令的映射 Map<JedisPool, List<String>> jedisPoolMap = new HashMap<>(); List<String> keyList = null; JedisPool currentJedisPool = null; Pipeline currentPipeline = null; for (String key : keys) { //cuteculate hash int crc = JedisClusterCRC16.getSlot(key); //通过哈希槽获取节点的连接 currentJedisPool = cache.getSlotPool(crc); if (jedisPoolMap.containsKey(currentJedisPool)) { jedisPoolMap.get(currentJedisPool).add(key); } else { keyList = new ArrayList<>(); keyList.add(key); jedisPoolMap.put(currentJedisPool, keyList); } } //保存结果 List<Object> res; //执行 for (Map.Entry<JedisPool, List<String>> entry : jedisPoolMap.entrySet()) { try { currentJedisPool = entry.getKey(); keyList = entry.getValue(); //获取pipeline Jedis jedis = currentJedisPool.getResource(); currentPipeline = jedis.pipelined(); for (String key : keyList) { currentPipeline.get(key); } //从pipeline中获取结果 res = currentPipeline.syncAndReturnAll(); currentPipeline.close(); jedis.close(); for (int i = 0; i < keyList.size(); i++) { resultMap.put(keyList.get(i), res.get(i) == null ? null : res.get(i).toString()); } } catch (Exception e) { logger.error(\"getPipLine error.\", e); } } //sort for (String key : keys) { result.add(resultMap.containsKey(key) ? resultMap.get(key) : null); } return result; } @Override public void destroy() { try { jedisCluster.close(); } catch (Exception e) { } }} 参考文档1、玩转Redis集群之Cluster 2、Redis哨兵模式实现主从切换 3、深入剖析Redis系列(三) - Redis集群模式搭建与原理详解","link":"/2019/12/07/java/Jedis集群模式经典实现/"},{"title":"Hbase系列1-Hbase适用场景","text":"#HBase 使用场景 HBase 采用 LSM 架构,适合写多读少场景 ##HBase 适用场景 支持海量数据,需要 TB/PB 级别的在线服务(亿行以上) 拥有良好扩展性,数据量增长速度快,对水平扩展能力有需求 高性能读写,简单 kv 读写,响应延迟低 写入很频繁,吞吐量大 满足强一致性要求 批量读取数据需求 schema 灵活多变 无跨行跨表事务要求 ##HBase 的限制 只有主索引(Rowkey),不支持表联接、聚合、order by 等高级查询 ##案例分析 A. 统一日志案例 场景:写多读少,写入量巨大(日写入>37 亿),读取随机且访问较少。设计:将 Rowkey 用 UUID 类似方式生成随机字段,转换成 Byte 数组,经过优化,截取为 4 个 Byte。既随机,又很短,能满足 40 年存储。通过 HBase 组针对性的预分区,将数据区域划分到所有节点上,写入均衡到每个节点,因此压力平均。读取的时候,因为随机,设置了 BLOOMFILTER,所以随机读性能提高。因为读取少,所以对于读取的优化就没有写那么重要了。 B. 一次导入,多次读取 场景:晚上导入大量数据,白天提供用户访问,每次查询是带有客户 ID 的随机数据,数据量可以估算,每条数据<100B。设计:晚上导入大量数据,如果只有一台服务器提供服务,TPS 上不去,且容易导致节点宕机,因此需要写入是均衡分布的。另外一方面,用户访问的时间和方式是比较随机的,数据又是随机的,因此将 Rowkey随机分布有助于大量请求比较均衡在多个节点上。因为用户的访问更重要,因此将客户 ID 和相关业务类型作为 Rowkey。如果写入数据是对用户顺序处理的,就会出现压力集中在某个节点上,导致宕机。于是将导入 worker 设计为先在本地存储,然后分 10 个线程,将用户 Hash 成 10 组。每个线程读取一个用户的 300条数据,批量写入 HBase,然后写下一个用户的 300 条数据,继续直到第 10 个用户,再返回写本组第一个用户,直到所有数据写完。最终集群的表现比较平稳,且 TPS 很高。 C. 监控数据(京东云容器监控cap,京东大脑,jdh监控,bdp监控数据查询,ump,mjdos) D. 海量存储(商家罗盘,供应商罗盘,Storm实时应用,搜索推荐)","link":"/2019/01/21/bigdata/hbase/Hbase系列1-Hbase适用场景/"},{"title":"跟我学Springboot-按需加载Bean","text":"在启动SpringBoot应用时,有这样一种需求,不需要加载全部的Bean,按照业务不同,按需加载。 为什么要按需加载按需加载的概念很简单,按照系统的要求加载不同的模块。在SpringBoot应用中,即加载指定的Bean到Spring容器中。 按需加载有两个好处,首先容器中不需要加载额外的类,造成不必要的资源浪费。其次代码可以按package区分,既高内聚又方便管理。 按需加载的实现Conditional1234567891011121314151617181920212223242526272829303132/** Condition类是一个函数式接口,作为判定一个组件是否应该注入到Spring容器中。 在Bean definition前就执行检查,如果不符合校验规则就不进行注册。*/@FunctionalInterfacepublic interface Condition { boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);}public class TravelLoadCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { System.out.println(\"business=\" + context.getEnvironment().getProperty(\"search.business\")); //获取application.yml配置文件中的search.business属性,并匹配是否等于travel return context.getEnvironment().getProperty(\"search.business\").equals(\"travel\"); }}@Component//如果TravelLoadCondition返回true,则注入TravelBasicSearch。@Conditional(TravelLoadCondition.class)public class TravelBasicSearch { public TravelBasicSearch() { System.out.println(this.getClass().getSimpleName() + \" loaded....\"); } @PostConstruct private void init() { System.out.println(this.getClass().getSimpleName() + \" inited....\"); }} ComponentScanComponentScan是用在Configuration上的指令,等同于XML中的<context:component-scan>。它可以自动扫描@Component注解或者被它标记的注解,@Controller,@Service,@Repository都是被@Component标记的注解。 1234567<context:component-scan base-package=\"com.yibai.spring.annotation\" use-default-filters=\"false\"> <context:include-filter type=\"custom\" expression=\"com.yibai.spring.annotation.filter.ColorBeanLoadFilter\" /> <context:exclude-filter type=\"annotation\" expression=\"org.springframework.stereotype.Component\" /></context:component-scan> 属性 含义 value 指定要扫描的package includeFilters 指定只包含的组件,FilterType:指定过滤规则,支持的过滤规则有。ANNOTATION:按照注解规则,过滤被指定注解标记的类;ASSIGNABLE_TYPE:按照给定的类型; ASPECTJ:按照ASPECTJ表达式;REGEX:按照正则表达式 CUSTOM:自定义规则; excludeFilters 指定需要排除的组件。规则同上 useDefaultFilters True or False。指定是否需要使用Spring默认的扫描规则:被@Component, @Repository, @Service, @Controller或者已经声明过@Component自定义注解标记的组件; 1234567891011121. 扫描指定类文件@ComponentScan(basePackageClasses = Person.class)2. 扫描指定包,使用默认扫描规则,即被@Component, @Repository, @Service, @Controller或者已经声明过@Component自定义注解标记的组件;@ComponentScan(value = \"com.yibai\")3. 扫描指定包,加载被@Component注解标记的组件和默认规则的扫描(因为useDefaultFilters默认为true)@ComponentScan(value = \"com.yibai\", includeFilters = { @Filter(type = FilterType.ANNOTATION, value = Component.class) })4. 扫描指定包,只加载Person类型的组件@ComponentScan(value = \"com.yibai\", includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = Person.class) }, useDefaultFilters = false)5. 扫描指定包,过滤掉被@Component标记的组件@ComponentScan(value = \"com.yibai\", excludeFilters = { @Filter(type = FilterType.ANNOTATION, value = Component.class) })6. 扫描指定包,自定义过滤规则@ComponentScan(value = \"com.yibai\", includeFilters = { @Filter(type = FilterType.CUSTOM, value = ColorBeanLoadFilter.class) }, useDefaultFilters = true) 自定义扫描过滤规则 1234567891011121314151617181920212223242526public class ColorBeanLoadFilter implements TypeFilter { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { // 当前被扫描类的注解信息 @SuppressWarnings(\"unused\") AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata(); // 当前被扫描类信息 ClassMetadata classMetadata = metadataReader.getClassMetadata(); // 当前被扫描类资源信息 @SuppressWarnings(\"unused\") Resource resource = metadataReader.getResource(); try { String className = classMetadata.getClassName(); Class<?> forName = Class.forName(className); if (Color.class.isAssignableFrom(forName)) { // 如果是Color的子类,就加载到IOC容器 return true; } } catch (ClassNotFoundException e) { } return false; }} 参考1、Spring组件注册注解之@ComponentScan,@ComponentScans","link":"/2019/10/18/spring/springboot/跟我学Springboot-按需加载Bean/"},{"title":"SpringCloud-第二章-配置文件","text":"SpringCloud-第二章-配置文件1.配置文件在SpringBoot构建完之后,会在resource目录下创建一个application.properties文件,这是一个空文件。SpringBoot应用默认就会读取application.properties文件,但我们可以修改为application.yml,个人建议使用yml,因为YMAL的风格会更加适合配置文件。两者的功能上是一致的,只是语法不同,本文不重点介绍语法。 ##2.实战 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105@Configuration@EnableConfigurationPropertiespublic class ConditionConfig { @Bean public GatewayProperties gatewayProperties() { return new GatewayProperties(); }}@Validated@ConfigurationProperties(prefix = \"spring.cloud.gateway\")public class GatewayProperties { @NotNull @Valid private List<RouteDefinition> routes = new ArrayList<>(); public void setRoutes(List<RouteDefinition> routes) { this.routes = routes; } @PostConstruct private void init() { System.out.println(JSON.toJSONString(this)); }}@Validatedpublic class PredicateDefinition { @NotNull private String name; private Map<String, String> args = new LinkedHashMap<>(); public PredicateDefinition(){} public PredicateDefinition(String text) { int eqIdx = text.indexOf('='); if (eqIdx <= 0) { throw new ValidationException(\"Unable to parse PredicateDefinition text '\" + text + \"'\" + \", must be of the form name=value\"); } setName(text.substring(0, eqIdx)); String[] args = tokenizeToStringArray(text.substring(eqIdx + 1), \",\"); for (int i = 0; i < args.length; i++) { this.args.put(NameUtils.generateName(i), args[i]); } } public void setName(String name) { this.name = name; } public void setArgs(Map<String, String> args) { this.args = args; }}public class RouteDefinition { @NotEmpty private String id; @NotEmpty @Valid private List<PredicateDefinition> predicates = new ArrayList<>(); @NotNull private URI uri; private int order = 0; public RouteDefinition(){} public RouteDefinition(String text) { int eqIdx = text.indexOf('='); if (eqIdx <= 0) { throw new ValidationException(\"Unable to parse RouteDefinition text '\" + text + \"'\" + \", must be of the form name=value\"); } setId(text.substring(0, eqIdx)); String[] args = tokenizeToStringArray(text.substring(eqIdx + 1), \",\"); setUri(URI.create(args[0])); for (int i = 1; i < args.length; i++) { this.predicates.add(new PredicateDefinition(args[i])); } } public void setId(String id) { this.id = id; } public void setPredicates(List<PredicateDefinition> predicates) { this.predicates = predicates; } public void setUri(URI uri) { this.uri = uri; } public void setOrder(int order) { this.order = order; }} 12345678910111213spring: cloud: gateway: routes: - id: yml-id uri: www.baidu.com order: -1 predicates: - name: predicatesName args: arg0: 1 arg1: 2 - Path=/echo 以上代码实现用yml文件,映射到类GatewayProperties中。最终的打印结果是 1{\"routes\":[{\"id\":\"yml-id\",\"order\":-1,\"predicates\":[{\"args\":{\"arg0\":\"1\",\"arg1\":\"2\"},\"name\":\"predicatesName\"}],\"uri\":\"www.baidu.com\"}]} 细心的同学可能会注意到@EnableConfigurationProperties、@ConfigurationProperties(prefix = “spring.cloud.gateway”)、@Validated 这三个很重要的注解。是的,如果想实现配置文件直接映射成Bean,@EnableConfigurationProperties和@ConfigurationProperties是必须要配置的,prefix=”spring.cloud.gateway”代表从spring.cloud.gateway开始采集配置。 @Validated 是校验配置的工具注解,除了代码中应用到的注解外,其它常见注解: @AssertFalse 校验false @AssertTrue 校验true @DecimalMax(value=,inclusive=) 小于等于value,inclusive=true,是小于等于 @DecimalMin(value=,inclusive=) 与上类似 @Max(value=) 小于等于value @Min(value=) 大于等于value @NotNull 检查Null @Past 检查日期 @Pattern(regex=,flag=) 正则 @Size(min=, max=) 字符串,集合,map限制大小 @Validate 对po实体类进行校验 1- Path=/echo 这行代码有些不好理解,其实就是RouteDefinition 有两个构造函数,这句配置的意思是调用了有参数的构造函数来定义RouteDefinition对象,使得构建网关的配置更加简洁。 ##3.总结 以上的内容,是通过阅读SpringCloudGateway源代码,发现实现方式很优雅,便总结下来。利用好SpringBoot的配置,可以写出很优雅的代码,提高维护性。","link":"/2019/09/15/spring/springcloud/Spring-cloud技术栈学习-第二章-配置文件映射/"},{"title":"SpringCloud-第一章 入门","text":"SpringCloud-第一章 入门单体应用架构 vs 微服务架构 在说SpringCloud之前,首先要陈述一下单体应用架构和微服务架构。 一个归档包,包含所有功能的应用程序叫单体应用。在系统初期或者系统本身的复杂度不高,采用单体应用架构是合理的。它比较容易部署、维护、测试,大多数功能的实现,采用函数本地调用,所以说从架构合理性和性能角度考量,都是一个合理的方案。 但随着系统的复杂度越来越高,引入越来越多的功能,技术团队扩张,越来越多的人加入到系统的研发过程中。慢慢的,单体应用变得越来越臃肿,可维护性、灵活性逐渐降低,如果没有良好的编程规范约束和工程规范,一定会留下不少的技术债务(俗称”埋雷”),以上这些问题(当然还有其他的问题)共同导致了维护成本越来越高。 以上这些问题,相信大家在工作中,一定会经常碰到。那么如何解决单体应用架构的问题呢?微服务应运而生,微服务架构风格是一种将一个单一应用程序开发为一组小型服务的方法,每个服务都运行在自己的进程中,服务间采用轻量级通信机制,一系列独立运行的微服务共同构建起整个系统。 微服务虽然有很多吸引人的地方,但它并不是免费的午餐,使用它是有代价的。 1、运维成本较高:更多的服务,意味着更多的运维投入。在单体架构中,只需要保证一个应用的正常运行。而在微服务中,需要保证几十甚至几百个服务正常运行与协作,这给运维带来了很大的挑战。 2、分布式固有的复杂性:使用微服务架构即是分布式系统。对于一个分布式系统,系统容错、网络延迟、分布式事务等都会带来巨大的挑战。 3、接口调整成本高:微服务之间通过接口通信,如果修改某一个微服务的API,可能所有使用了该接口的微服务都需要做调整。 4、重复劳动:很多服务可能都会使用到相同的功能,而这个功能并没有达到分解为一个微服务的程度,这个时候,可能各个服务都会开发这一功能,从而导致代码重复。 虽然微服务有如上的缺点,但是瑕不掩瑜,为了更好的应用微服务,可以选择SpringCloud作为开发框架。 SpringCloud 上一章讲述了微服务的产生背景和微服务的优势和劣势,本章讲述应用SpringCloud生态,如何合理的搭建一套健壮,好维护的微服务的系统。 SpringBoot SpringCloud是基于SpringBoot构建的,因此它延续了SpringBoot的契约模式以及开发方式。SpringBoot的出现,解决了Java应用配置繁杂的缺点,整合了最优秀的中间件,使得基于Java的应用开发和部署十分简单。 Eureka 服务中心 没有服务中心,那么服务的调用必须采用硬编码的模式,那么每次服务的扩容,缩容,新的服务上线,都需要所有的服务上线,这显然是不合理的。 Eureka是一款开源的服务发现组件,https://github.com/Netflix/eureka Eureka的功能: 1、Eureka集群 2、Eureka用户认证 3、Eureka自我保护模式 4、Eureka健康检查 Eureka最佳实践如下图所示,要关注如下的参数 Renews threshold: 这个指标的计算是应用的数量int(count 2 0.85),图中实例正好是三个eureka-server实例,故而结果为5。 Renews(last min): 这个指标的含义是一分钟内接收到的心跳数量。如果renews(last min) < Renews threshold,则会触发Eureka的自我保护机制。 Instances currently registered with Eureka: 展示此时注册中心管理的应用及实例数量 Ceneral Info:最重要的是registered-replicas,available-replicas, 正常情况下unavailable-replicas是不应该有值的,如果有值,说明有replica不能使用。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546server: port: 8760spring: profiles: active: peer0 application: name: eureka-servereureka: server: eviction-interval-timer-in-ms: 3000 #eureka server清理无效节点的时间间隔,默认60000毫秒,即60秒 responseCacheUpdateIntervalMs: 3000 #eureka server刷新readCacheMap的时间,注意,client读取的是readCacheMap,这个时间决定了多久会把readWriteCacheMap的缓存更新到readCacheMap上 enable-self-preservation: true #是否开启自我保护,默认为true client: registry-fetch-interval-seconds: 5 #表示eureka client间隔多久去拉取服务注册信息,默认为30秒 healthcheck: enabled: true #开启健康检查(依赖spring-boot-starter-actuator) serviceUrl: defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/ dashboard: path: /eurekaui #eureka ui的路径 instance: hostname: eureka0 lease-expiration-duration-in-seconds: 15 #该值至少应该大于leaseRenewalIntervalInSeconds,目前设置的是leaseRenewalIntervalInSeconds三倍,相当于三次心跳如果都没有则进行服务摘除 lease-renewal-interval-in-seconds: 5 #表示eureka client发送心跳给server端的频率 preferIpAddress: falseribbon: ServerListRefreshInterval: 1000##prometheusjmanagement: metrics: export: prometheus: enabled: true step: 1m descriptions: true endpoint: metrics: enabled: true prometheus: enabled: true health: show-details: always endpoints: web: exposure: include: \"*\" Ribbon 实现客户端负载均衡 Ribbon是Netflix发布的负载均衡器。Ribbon的github地址,https://github.com/Netflix/ribbon 在Spring Cloud中,当Ribbon与Eureka配合使用时,Ribbon可自动从Eureka Server获取服务提供者地址列表,并基于负载均衡算法,请求其中一个服务提供者实例。 Feign 实现声明式REST调用Feign是Netflix开发的声明式、模板化的HTTP客户端,其灵感来自Retrofit,JAXRS-2.0以及WebSocket。Feign可帮助我们更加便捷、优雅的调用HTTP API。https://github.com/OpenFeign/feign Hystrix 微服务容错处理Hystrix是Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性和容错性。 SpringCloudGateway 网关服务https://github.com/spring-cloud/spring-cloud-gateway Apollo 配置中心https://github.com/ctripcorp/apollo Sleuth和Zipkin实现微服务跟踪https://github.com/spring-cloud/spring-cloud-sleuth","link":"/2019/09/15/spring/springcloud/Spring-cloud技术栈学习/"},{"title":"Hbase系列4-rowkey设计技巧","text":"#HBase Rowkey 设计 ##一 引言 HBase Rowkey是唯一索引(Rowkey用来表示唯一一行记录),Rowkey设计的优劣直接影响读写性能。 HBase中的行是按照Rowkey的ASCII字典顺序进行全局排序的。 举例说明:假如有5个Rowkey:”012”, “0”, “123”, “234”, “3”,按ASCII字典排序后的结果为:”0”, “012”, “123”, “234”, “3”。(注:文末附常用ASCII码表) Rowkey排序时会先比对两个Rowkey的第一个字节,如果相同,然后会比对第二个字节,依次类推… 对比到第X个字节时,已经超出了其中一个Rowkey的长度,短的Rowkey排在前面。 由于HBase是通过Rowkey查询的,一般Rowkey上都会存一些比较关键的检索信息,建议提前考虑数据具体需要如何查询,根据查询方式进行数据存储格式的设计,要避免做全表扫描,因为效率特别低,且会损耗集群性能。 ##二 Rowkey设计原则 Rowkey设计应遵循以下原则: ###Rowkey唯一原则 Rowkey在设计上必须保证唯一性,Rowkey在HBase中只插入不更新(不做update操作)。HBase中所有操作都是“流式“操作,不会更改原来存储过的数据,只会进行append,并通过加上新的版本标识表示是最新数据(VERSION为1会覆盖,默认是1)。 ###Rowkey的散列原则 Rowkey设计应散列,均匀的分布在各个HBase节点上如果Rowkey按照时间戳的方式递增,不要将时间放在二进制码的前面,建议将rowkey的高位作为散列字段,由程序随机生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer,以实现负载均衡的几率。 Rowkey的第一部分如果是时间戳,会将造成所有新数据都在最后一个Region,造成访问热点。 热点:大量的client直接访问集群的一个或极少数个节点(访问可能是读、写或者其他操作)。大量访问会使热点Region所在的单个机器超出自身承受能力,引起性能下降(Full GC)甚至Region不可用,这也会影响同一个RegionServer上的其他Region,由于主机无法服务其他Region的请求。 通常有3种方式来解决热点问题: 1、Reverse反转 针对固定长度的Rowkey反转后存储,这样可以使Rowkey中经常改变的部分放在最前面,可以有效的随机Rowkey。反转Rowkey的例子通常以手机举例,可以将手机号反转后的字符串作为Rowkey,这样的就避免了以手机号那样比较固定开头(137x、15x等)导致热点问题。缺点是牺牲了Rowkey的有序性。 2、Salt加盐 Salting是将每一个Rowkey加一个前缀,前缀使用一些随机字符,使得数据分散在多个不同的Region,达到Region负载均衡的目标。 比如在一个有4个Region(注:以 [ ,a)、[a,b)、[b,c)、[c, )为Region起至)的HBase表中,加Salt前的Rowkey:abc001、abc002、abc003 我们分别加上a、b、c前缀,加Salt后Rowkey为:a-abc001、b-abc002、c-abc003 可以看到,加盐前的Rowkey默认会在第2个region中,加盐后的Rowkey数据会分布在3个region中,理论上处理后的吞吐量应是之前的3倍。由于前缀是随机的,读这些数据时需要耗费更多的时间,所以Salt增加了写操作的吞吐量,不过缺点是同时增加了读操作的开销。 3、Hash散列或者MOD 用Hash散列来替代随机Salt前缀的好处是能让一个给定的行有相同的前缀,这在分散了Region负载的同时,使读操作也能够推断。 确定性Hash(比如md5后取前4位做前缀)能让客户端重建完整的RowKey,可以使用Get操作直接Get想要的行。 例如将上述的原始Rowkey经过hash处理,此处我们采用md5散列算法取前4位做前缀,结果如下 9bf0-abc001 (abc001在md5后是9bf049097142c168c38a94c626eddf3d,取前4位是9bf0) 7006-abc002 95e6-abc003 若以前4个字符作为不同分区的起止,上面几个Rowkey数据会分布在3个region中。实际应用场景是当数据量越来越大的时候,这种设计会使得分区之间更加均衡。 如果Rowkey是数字类型的,也可以考虑MOD方法(其他类型可以使用hashcode MOD),示意图: ###Rowkey长度原则 Rowkey设计建议定长,长度在10~100个字节,越短越好。RowKey是一个二进制码流,可以是任意字符串,最大长度 64kb,实际应用中一般为10-100bytes,以 byte[] 形式保存,一般设计成定长。建议越短越好,不要超过16个字节,原因如下: (1)数据的持久化文件HFile中是按照KeyValue存储的,如果rowkey过长,比如超过100字节,1000w行数据,光RowKey就要占用100*1000w=10亿个字节,将近1G数据,这样会极大影响HFile的存储效率; (2)MemStore将缓存部分数据到内存,如果rowkey字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率。 (3)目前操作系统都是64位系统,内存8字节对齐,控制在16个字节,8字节的整数倍利用了操作系统的最佳特性。 其他的如列族名、列名等属性名也是越短越好。value永远和它的key一起传输的。当具体的值在系统间传输时,它的RowKey、列名、时间戳也会一起传输。如果你的RowKey和列名和值相比较很大,那么你将会遇到一些有趣的问题。HFile中的索引最终占据了HBase分配的大量内存。 ###Rowkey有序原则 充分利用Rowkey字典顺序排序特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。时间戳反转设计:一个常见的数据库处理问题是快速获取数据的最近版本,使用反转的时间戳作为Rowkey的一部分对这个问题十分有用,可以将Long.MAX_VALUE-timestamp追加到key的末尾,例如[key][reverse_timestamp]。 表中[key]的最新值可以通过scan [key]获得 [key]的第一条记录,因为HBase中rowkey是有序的,最新的[key]在任何更旧的[key]之前,所以第一条记录就是最新的。 这个技巧可以替代使用多版本数据,多版本数据会永久(很长时间)保存数据的所有版本。同时,这个技巧用一个scan操作就可以获得数据的所有版本。 Hbase的排序,是按照rowkey,columnKey(columnFamily + qualifier),timestamp 三维排序,且都是按照字典顺序。首先比较rowkey字典排序,同样rowkey的按columnKey字典排序,最后按timestamp最新排到最前。 ###HBase Rowkey设计实战 在实际的设计中我们可能更多的是结合多种设计方法来实现Rowkey的最优化设计。 列1:设计订单状态表 使用Rowkey: reverse(order_id) + (Long.MAX_VALUE – timestamp) 设计优点:一、通过reverse订单号避免Region热点,二、可以按时间倒排显示。 列2:使用HBase作为事件(事件指的的终端在APP中发生的行为,比如登录、下单等等统称事件(event))的临时存储(HBase存储最近10分钟的热数据) 设计event事件的Rowkey为:两位随机数Salt + EventId + Date + Kafka的Offset 设计优点:加盐的目的是为了增加查询的并发性,假如Salt的范围是0~n,那我们在查询的时候,可以将数据分为n个split同时做Scan操作。经过测试验证,增加并发度能够将整体的查询速度提升5~20倍以上。 随后的EventId和Date是用来做范围Scan使用的。在大部分的查询场景中,都指定了EventId,因此把EventId放在了第二个位置上,同时EventId的取值有几十个,通过Salt + EventId的方式可以保证不会形成热点。 把Date放在Rowkey的第三个位置上以实现按Date做Scan。这里可以考虑对Data进行反转(参考时间戳反转设计) 这样的Rowkey设计能够很好的支持如下查询场景: 1、只按照EventId查询 2、按照EventId和Date查询 非必要情况,一般不建议进行全表的Scan查询,全表Scan对性能的消耗很大。 ###HBase的表设计 HBase表设计通常可以是宽表(Wide Table)模式,即一行包括很多列。同样的信息也可以用高表(Tall Table)形式存储,通常高表的性能比宽表要高出50%以上。所以推荐使用高表来完成表设计。 表设计时考虑HBase数据库的一些特性: 1、在HBase表中是通过Rowkey的字典序来进行数据排序的 2、所有存储在HBase表中的数据都是二进制的字节 3、原子性只在行内保证,HBase不支持跨行事务 4、列族(Column Family)在表创建之前就要定义好 5、列族中的列标识(Column Qualifier)可以在表创建完以后动态插入数据时添加 ##总结 在Rowkey设计时,请先考虑业务场景,比如是读比写多、还是读比写少。HBase本身是为写优化的,即便是这样,也可能会出现热点问题,设计时尽量避免热点。如果读场景比较多,除了考虑以上Rowkey设计原则外,还可以考虑和其他缓存数据库如Redis等结合。 ##附录 常用ASCLL码","link":"/2019/01/21/bigdata/hbase/Hbase系列4-rowkey设计技巧/"},{"title":"Hbase系列5-javademo","text":"基于Hadoop2.7.1 + Hbase 1.2.1 + HbaseClientAPI 1.2.8 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173package com.jd.union.sparkimport java.io.IOExceptionimport java.security.MessageDigestimport java.text.SimpleDateFormatimport java.utilimport java.util._import org.apache.hadoop.hbase._import org.apache.hadoop.hbase.client.{ConnectionFactory, HBaseAdmin, _}import org.apache.hadoop.hbase.util.Bytesobject SkuDataToHBaseJob extends Serializable { val dateFormat = new SimpleDateFormat(\"yyyyMMdd\") var conf = HBaseConfiguration.create() conf.set(\"hbase.zookeeper.quorum\", \"192.168.2.100\") conf.set(\"hbase.zookeeper.property.clientPort\", \"2181\") val conn = ConnectionFactory.createConnection(conf) def main(args: Array[String]): Unit = { val skuId = \"129399424324\" val orKey = dateFormat.format(new Date()) + \"$\" + skuId //todo 取前四位 + skuId + date 当做rowkey,避免热点数据 val rowkey = md5(orKey) val data: String = \" {\\\"abParam\\\":\\\"nature\\\",\\\"abVersion\\\":\\\"comm\\\",\\\"adowner\\\":\\\"p_10026704\\\",\\\"bestCouponId\\\":\\\"92737319\\\",\\\"brandCode\\\":382820,\\\"brandName\\\":\\\"谷邦(GUBANG)\\\",\\\"cid1\\\":1620,\\\"cid1Name\\\":\\\"家居日用\\\",\\\"cid2\\\":13780,\\\"cid2Name\\\":\\\"收纳用品\\\",\\\"cid3\\\":13785,\\\"cid3Name\\\":\\\"收纳架/篮\\\",\\\"color\\\":\\\"2个装北欧粉\\\",\\\"comments\\\":11,\\\"couponId\\\":0,\\\"couponLink\\\":\\\"\\\",\\\"createTime\\\":\\\"2019-01-22T05:44:42.969Z\\\",\\\"deliveryType\\\":0,\\\"endTime\\\":\\\"2999-01-01 23:59:59\\\",\\\"goodComments\\\":11,\\\"goodCommentsShare\\\":100,\\\"hasCoupon\\\":1,\\\"imageUrl\\\":\\\"jfs/t1/10013/33/7229/24225/5c286dfaEacd0c6a2/f16290038d59eea6.jpg\\\",\\\"imgList\\\":\\\"jfs/t1/8948/19/11086/256803/5c286df9E594f189f/5a2af0eeda043878.jpg|jfs/t1/23576/23/3598/256769/5c286df9E1e32d799/62ced8312a144260.jpg|jfs/t1/10013/33/7229/24225/5c286dfaEacd0c6a2/f16290038d59eea6.jpg|jfs/t1/26566/34/4201/410351/5c2f5aa6Eb441c098/fc86afded2d6334d.png|jfs/t1/20665/7/3549/279989/5c286dfaE5a253816/1dfab92f62045e00.jpg|jfs/t1/20297/14/3513/296706/5c286df4Efecc5d55/971d9dad36ea6788.jpg\\\",\\\"inOrderComm30Days\\\":129.07,\\\"inOrderComm30DaysSku\\\":84.35,\\\"inOrderCount30Days\\\":58,\\\"inOrderCount30DaysSku\\\":36,\\\"isCare\\\":0,\\\"isHot\\\":1,\\\"isLock\\\":0,\\\"isNew\\\":0,\\\"isOversea\\\":0,\\\"isPinGou\\\":1,\\\"isSeckill\\\":0,\\\"isStuPrice\\\":0,\\\"itemTag\\\":0,\\\"lowestPrice\\\":9.90,\\\"majorSuppBrevityCode\\\":\\\"\\\",\\\"orientationFlag\\\":0,\\\"owner\\\":\\\"p\\\",\\\"pcCommission\\\":5.97,\\\"pcCommissionShare\\\":30.0,\\\"pcPrice\\\":19.90,\\\"pid\\\":40520301670,\\\"pingouActiveId\\\":15461534722120,\\\"pingouEnd\\\":\\\"2037-08-16 09:30:48\\\",\\\"pingouPrice\\\":9.90,\\\"pingouStart\\\":\\\"2018-12-30 15:05:02\\\",\\\"pingouTmCount\\\":2,\\\"planId\\\":1507475989,\\\"qualityScore\\\":0.0631,\\\"qualityScoreNew\\\":0.4147,\\\"rfId\\\":0,\\\"ruleType\\\":7,\\\"seckillOriPrice\\\":0.0,\\\"seckillPrice\\\":0.0,\\\"shelvesTm\\\":\\\"2018-12-30 15:04:32.0\\\",\\\"shopId\\\":863648,\\\"size\\\":\\\"\\\",\\\"skuId\\\":40520301670,\\\"skuName\\\":\\\"谷邦-两个装卫生间牙刷架壁挂式免打孔浴室置物吸盘梳子筒座牙膏杯吸壁收纳盒 2个装北欧粉\\\",\\\"startTime\\\":\\\"2019-01-11 00:00:00\\\",\\\"stuPrice\\\":0.0,\\\"themeFlag\\\":2,\\\"unionCoupon\\\":[{\\\"batchId\\\":92737319,\\\"beginTime\\\":\\\"2019-01-10 14:22:00\\\",\\\"couponKind\\\":3,\\\"couponType\\\":1,\\\"createTime\\\":\\\"2019-01-18T09:50:26.589Z\\\",\\\"createUser\\\":\\\"lisen43\\\",\\\"discount\\\":8.0,\\\"endTime\\\":\\\"2019-01-31 14:23:00\\\",\\\"expireType\\\":5,\\\"key\\\":\\\"3172a45b3f8242d2ba92141a7c26b66d\\\",\\\"link\\\":\\\"http://coupon.m.jd.com/coupons/show.action?key=3172a45b3f8242d2ba92141a7c26b66d&roleId=17184533&to=mall.jd.com/index-863648.html\\\",\\\"ownerType\\\":\\\"92,95,444,498\\\",\\\"platformType\\\":0,\\\"quota\\\":17.0,\\\"remainCnt\\\":19965,\\\"source\\\":3,\\\"updateTime\\\":\\\"2019-01-18T09:50:26.589Z\\\",\\\"useEndTime\\\":\\\"2019-01-31 23:59:59\\\",\\\"useStartTime\\\":\\\"2019-01-10 00:00:00\\\",\\\"venderId\\\":10026704}],\\\"updateTime\\\":\\\"2019-01-22T15:01:31.194Z\\\",\\\"venderName\\\":\\\"谷邦家居拼购店\\\",\\\"vid\\\":10026704,\\\"wareId\\\":12567523910,\\\"wlCommission\\\":5.97,\\\"wlCommissionShare\\\":30.0,\\\"wlPrice\\\":19.90}\" val tableName = \"union_search_sku\" try { addData(rowkey, tableName, \"cf\", \"value\", data) get(rowkey, tableName) println(\"scan----------------\") scan(tableName) //createTable(\"admin_create\") //del(tableName, \"20190909$2\") println(\"创建表成功\") } catch { case e: Exception => { e.printStackTrace() null } } finally { } } def md5(source: String): String = { val sb = new StringBuffer(32) try { val md = MessageDigest.getInstance(\"MD5\") val bytes = md.digest(source.getBytes(\"utf-8\")) for (i <- 0 to bytes.length) { sb.append(Integer.toHexString((bytes.apply(i) & 0xFF) | 0x100).toLowerCase().substring(1, 3)) } } catch { case e: Exception => null } sb.toString() } @throws[IOException] def addData(rowKey: String, tableName: String, cf: String, column: String, value: String): Unit = { val put = new Put(Bytes.toBytes(rowKey)) println(\"开始创建table\") val table = conn.getTable(TableName.valueOf(tableName)) println(\"创建结束\") // 获取表 put.addColumn(Bytes.toBytes(cf), Bytes.toBytes(column), Bytes.toBytes(value)) table.put(put) System.out.println(\"add data Success!\") } @throws[IOException] def get(rowKey: String, tableName: String): Unit = { val get = new Get(Bytes.toBytes(rowKey)) val table = conn.getTable(TableName.valueOf(tableName)) val result: Result = table.get(get) val it = result.listCells().iterator() while (it.hasNext) { val cell: Cell = it.next() cell.getValueArray println(\"value:\" + Bytes.toString(cell.getValueArray, cell.getValueOffset, cell.getValueLength)) println(\"column familly:\" + Bytes.toString(cell.getValueArray, cell.getFamilyOffset, cell.getFamilyLength)) println(\"Qualifier:\" + Bytes.toString(cell.getValueArray, cell.getQualifierOffset, cell.getQualifierLength)) } } def scan(tableName: String): Unit = { val scan = new Scan() var rs: ResultScanner = null val table = conn.getTable(TableName.valueOf(tableName)) try { rs = table.getScanner(scan) val it = rs.iterator() while (it.hasNext) { val cellIt = it.next().listCells().iterator() while (cellIt.hasNext) { val kv = cellIt.next() println(\"value:\" + Bytes.toString(kv.getValueArray, kv.getValueOffset, kv.getValueLength)) println(\"column familly:\" + Bytes.toString(kv.getValueArray, kv.getFamilyOffset, kv.getFamilyLength)) println(\"Qualifier:\" + Bytes.toString(kv.getValueArray, kv.getQualifierOffset, kv.getQualifierLength)) System.out.println(\"timestamp:\" + kv.getTimestamp) System.out.println(\"-------------------------------------------\") } } } finally rs.close() } def createTable(tableName: String): Unit = { val admin = conn.getAdmin.asInstanceOf[HBaseAdmin] val tn = TableName.valueOf(tableName) val descriptor: HTableDescriptor = new HTableDescriptor(tn) val cf = new HColumnDescriptor(\"cf\") descriptor.addFamily(cf) admin.createTable(descriptor) admin.close() } def del(tableName: String, rowkey: String): Unit = { val table = conn.getTable(TableName.valueOf(tableName)) val del = new Delete(Bytes.toBytes(rowkey)) table.delete(del) table.close() println(\"删除成功\") } /** * 根据时间戳,删除数据 * @param tableName * @param minTime * @param maxTime */ def deleteTimeRange(tableName: String, minTime: Long, maxTime: Long) { var table = conn.getTable(TableName.valueOf(tableName)) try { val scan = new Scan() scan.setTimeRange(minTime, maxTime) val rs = table.getScanner(scan) val list = getDeleteList(rs) if (list.size() > 0) { table.delete(list) } } catch { case e: Exception => e.printStackTrace() } finally { table.close() } } def getDeleteList(rs: ResultScanner): util.List[Delete] = { val list = new util.ArrayList[Delete]() try { for (r: Result <- rs) { val d = new Delete(r.getRow()) list.add(d) } } finally { rs.close() } list }}","link":"/2018/08/11/bigdata/hbase/Hbase系列5-javademo/"},{"title":"跟我学springboot第一讲-应用启动流程总体分析","text":"SpringBoot的出现,极大简化了基于Spring的Java应用的开发,赋予了Spring应用及其丰富的组件。但是越是简单的东西蕴含的风险越大,真正的用好Springboot,需要对它的原理有一定的了解,本文就带着大家认识下它的启动流程。 前言new SpringApplicationBuilder(SearchBsBootstrap.class).run(args)一行代码,就可以帮你启动一个Springboot应用。 SpingApplication构造函数初始化SpringBoot初始化入口类是SpringApplication,如下代码已经把核心方法列出,并画出了SpingApplication初始化的时序图。 12345678910111213141516171819202122232425262728293031323334353637383940414243public class SpringApplication { //类加载器,一般情况下不需要指定 private ResourceLoader resourceLoader; //主资源 private Set<Class<?>> primarySources; //SpringBoot应用的类型 private WebApplicationType webApplicationType; //加载的ApplicationContextInitializer实例 private List<ApplicationContextInitializer<?>> initializers; //加载的ApplicationListener实例 private List<ApplicationListener<?>> listeners; //执行类,即带有main方法的类 private Class<?> mainApplicationClass; public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { //第一步,设置类加载器 this.resourceLoader = resourceLoader; Assert.notNull(primarySources, \"PrimarySources must not be null\"); //第二步,设置主资源类,即@Configuration类 this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); //第三步,推断SpringBoot应用类型 this.webApplicationType = WebApplicationType.deduceFromClasspath(); //第四步,初始化所有ApplicationContextInitializer实例 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); //第五步,初始化所有ApplicationListener实例 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); //第六步,推断主执行类 this.mainApplicationClass = deduceMainApplicationClass(); } private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) { //第一步,获取ClassLoader ClassLoader classLoader = getClassLoader(); //第二步,传入要加载的工厂类型和类加载器,返回要实例化的类 Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader)); //第三步实例化类 List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); //对实例进行排序,Order越小优先级越高 AnnotationAwareOrderComparator.sort(instances); return instances; }} SpringApplication实例化时序图所示。 判定SpringBoot应用类型SpringBoot共定义了三种类型,NONE,SERVLET,REACTIVE。 NONE: 不是web应用也不能运行在嵌入式web服务器中。不包含javax.servlet.Servlet或者不包含org.springframework.web.context.ConfigurableWebApplicationContext,就是NONE应用,注意两者缺一不可 SERVLET: 是一个基于servlet的web应用,也可以运行在嵌入式web服务器中。 REACTIVE: 是一个反应式web应用,也可以运行在嵌入式反应式web服务器中。包含org.springframework.web.reactive.DispatcherHandler,不包含org.springframework.web.servlet.DispatcherServlet和org.glassfish.jersey.servlet.ServletContainer,则是REACTIVE应用。 123456789101112static WebApplicationType deduceFromClasspath() { if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) { return WebApplicationType.REACTIVE; } for (String className : SERVLET_INDICATOR_CLASSES) { if (!ClassUtils.isPresent(className, null)) { return WebApplicationType.NONE; } } return WebApplicationType.SERVLET; } 加载ApplicationContextInitializer和ApplicationListener SpringFactoriesLoader.loadFactoryNames方法负责通过spring.factories,获取ApplicationContextInitializer和ApplicationListener的类的集合。如下是SringBoot下的spring.factories的示例。经过转化,配置文件变成了Properties,key和value都是一一对应配置文件的。举个例子key=org.springframework.boot.env.PropertySourceLoader,value={org.springframework.boot.env.PropertiesPropertySourceLoader,org.springframework.boot.env.YamlPropertySourceLoader} Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader))最终返回ApplicationContextInitializer的子类。 类列表返回之后,调用createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names)对子类进行实例化。最终被放入到SpringApplication类的成员变量中private List<ApplicationContextInitializer<?>> initializers。 12345678910111213141516171819# PropertySource Loadersorg.springframework.boot.env.PropertySourceLoader=\\org.springframework.boot.env.PropertiesPropertySourceLoader,\\org.springframework.boot.env.YamlPropertySourceLoader# Run Listenersorg.springframework.boot.SpringApplicationRunListener=\\org.springframework.boot.context.event.EventPublishingRunListener# Error Reportersorg.springframework.boot.SpringBootExceptionReporter=\\org.springframework.boot.diagnostics.FailureAnalyzers# Application Context Initializersorg.springframework.context.ApplicationContextInitializer=\\org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\\org.springframework.boot.context.ContextIdApplicationContextInitializer,\\org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\\org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer SpingApplication#run()图中是SpringApplication Run的总体示意图。具体的启动流程可以参见此图。此图创意来自SpringBoot 应用程序启动过程探秘。 Spring应用在启动过程中,存在七个阶段,对应SpringApplicationRunListener接口中的7个方法,每一个阶段完成以后,他们就负责发送通知,具体实现类EventPublishingRunListener。 ApplicationStartingEvent:starting函数触发 ApplicationEnvironmentPreparedEvent:environmentPrepared方法触发,携带ConfigurableEnvironment对象 ApplicationContextInitializedEvent:contextPrepared方法触发,携带ConfigurableApplicationContext对象 ApplicationPreparedEvent:contextLoaded方法触发,携带ConfigurableApplicationContext对象 ApplicationStartedEvent:started 方法触发,携带ConfigurableApplicationContext对象 ApplicationReadyEvent:runing方法触发,携带ConfigurableApplicationContext对象 ApplicationFailedEvent:failed方法触发,携带ConfigurableApplicationContext对象 SpringApplicationRunListener的唯一实现类EventPublishingRunListener,内部使用了事件多播器SimpleApplicationEventMulticaster来管理多个事件监听器(Listener),使得每一个事件都可以被相关的Listener监听到,具体的Listener是SpringApplication的成员变量private List<ApplicationListener<?>> listeners,它是在SpringApplication构造函数初始化的时候设置的。 123456789101112131415public interface SpringApplicationRunListener { void starting(); void environmentPrepared(ConfigurableEnvironment environment); void contextPrepared(ConfigurableApplicationContext context); void contextLoaded(ConfigurableApplicationContext context); void started(ConfigurableApplicationContext context); void running(ConfigurableApplicationContext context); void failed(ConfigurableApplicationContext context, Throwable exception);}public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered { private final SpringApplication application; private final String[] args; private final SimpleApplicationEventMulticaster initialMulticaster;} 参考文献1、SpringBoot 应用程序启动过程探秘 2、Spring5源码解析-Spring框架中的事件和监听器 3、基于SpringBoot的Environment源码理解实现分散配置","link":"/2019/11/26/spring/springboot/跟我学Springboot第一讲-应用启动流程总体分析/"},{"title":"Hbase系列2-Hbase架构深度解析","text":"原文:http://www.blogjava.net/DLevin/archive/2015/08/22/426877.html HBase架构组成HBase采用Master/Slave架构搭建集群,它隶属于Hadoop生态系统,由一下类型节点组成:HMaster节点、HRegionServer节点、ZooKeeper集群,而在底层,它将数据存储于HDFS中,因而涉及到HDFS的NameNode、DataNode等,总体结构如下: 其中HMaster节点用于: 管理HRegionServer,实现其负载均衡。 管理和分配HRegion,比如在HRegion split时分配新的HRegion;在HRegionServer退出时迁移其内的HRegion到其他HRegionServer上。 实现DDL操作(Data Definition Language,namespace和table的增删改,column familiy的增删改等)。 管理namespace和table的元数据(实际存储在HDFS上)。 权限控制(ACL)。 HRegionServer节点用于: 存放和管理本地HRegion。 读写HDFS,管理Table中的数据。 Client直接通过HRegionServer读写数据(从HMaster中获取元数据,找到RowKey所在的HRegion/HRegionServer后)。 ZooKeeper集群是协调系统用于: 存放整个 HBase集群的元数据以及集群的状态信息。 实现HMaster主从节点的failover。 HBase Client通过RPC方式和HMaster、HRegionServer通信;一个HRegionServer可以存放1000个HRegion;底层Table数据存储于HDFS中,而HRegion所处理的数据尽量和数据所在的DataNode在一起,实现数据的本地化;数据本地化并不是总能实现,比如在HRegion移动(如因Split)时,需要等下一次Compact才能继续回到本地化。 这个架构图比较清晰的表达了HMaster和NameNode都支持多个热备份,使用ZooKeeper来做协调;ZooKeeper并不是云般神秘,它一般由三台机器组成一个集群,内部使用PAXOS算法支持三台Server中的一台宕机,也有使用五台机器的,此时则可以支持同时两台宕机,既少于半数的宕机,然而随着机器的增加,它的性能也会下降;RegionServer和DataNode一般会放在相同的Server上实现数据的本地化。 HRegionHBase使用RowKey将表水平切割成多个HRegion,从HMaster的角度,每个HRegion都纪录了它的StartKey和EndKey(第一个HRegion的StartKey为空,最后一个HRegion的EndKey为空),由于RowKey是排序的,因而Client可以通过HMaster快速的定位每个RowKey在哪个HRegion中。HRegion由HMaster分配到相应的HRegionServer中,然后由HRegionServer负责HRegion的启动和管理,和Client的通信,负责数据的读(使用HDFS)。每个HRegionServer可以同时管理1000个左右的HRegion(这个数字怎么来的?没有从代码中看到限制,难道是出于经验?超过1000个会引起性能问题? 来回答这个问题:感觉这个1000的数字是从BigTable的论文中来的(5 Implementation节):Each tablet server manages a set of tablets(typically we have somewhere between ten to a thousand tablets per tablet server))。 HMasterHMaster没有单点故障问题,可以启动多个HMaster,通过ZooKeeper的Master Election机制保证同时只有一个HMaster出于Active状态,其他的HMaster则处于热备份状态。一般情况下会启动两个HMaster,非Active的HMaster会定期的和Active HMaster通信以获取其最新状态,从而保证它是实时更新的,因而如果启动了多个HMaster反而增加了Active HMaster的负担。前文已经介绍过了HMaster的主要用于HRegion的分配和管理,DDL(Data Definition Language,既Table的新建、删除、修改等)的实现等,既它主要有两方面的职责: 协调HRegionServer 启动时HRegion的分配,以及负载均衡和修复时HRegion的重新分配。 监控集群中所有HRegionServer的状态(通过Heartbeat和监听ZooKeeper中的状态)。 Admin职能 创建、删除、修改Table的定义。 ZooKeeper:协调者ZooKeeper为HBase集群提供协调服务,它管理着HMaster和HRegionServer的状态(available/alive等),并且会在它们宕机时通知给HMaster,从而HMaster可以实现HMaster之间的failover,或对宕机的HRegionServer中的HRegion集合的修复(将它们分配给其他的HRegionServer)。ZooKeeper集群本身使用一致性协议(PAXOS协议)保证每个节点状态的一致性。 How The Components Work TogetherZooKeeper协调集群所有节点的共享信息,在HMaster和HRegionServer连接到ZooKeeper后创建Ephemeral节点,并使用Heartbeat机制维持这个节点的存活状态,如果某个Ephemeral节点实效,则HMaster会收到通知,并做相应的处理。 另外,HMaster通过监听ZooKeeper中的Ephemeral节点(默认:/hbase/rs/*)来监控HRegionServer的加入和宕机。在第一个HMaster连接到ZooKeeper时会创建Ephemeral节点(默认:/hbasae/master)来表示Active的HMaster,其后加进来的HMaster则监听该Ephemeral节点,如果当前Active的HMaster宕机,则该节点消失,因而其他HMaster得到通知,而将自身转换成Active的HMaster,在变为Active的HMaster之前,它会创建在/hbase/back-masters/下创建自己的Ephemeral节点。 HBase的第一次读写在HBase 0.96以前,HBase有两个特殊的Table:-ROOT-和.META.(如BigTable中的设计),其中-ROOT- Table的位置存储在ZooKeeper,它存储了.META. Table的RegionInfo信息,并且它只能存在一个HRegion,而.META. Table则存储了用户Table的RegionInfo信息,它可以被切分成多个HRegion,因而对第一次访问用户Table时,首先从ZooKeeper中读取-ROOT- Table所在HRegionServer;然后从该HRegionServer中根据请求的TableName,RowKey读取.META. Table所在HRegionServer;最后从该HRegionServer中读取.META. Table的内容而获取此次请求需要访问的HRegion所在的位置,然后访问该HRegionSever获取请求的数据,这需要三次请求才能找到用户Table所在的位置,然后第四次请求开始获取真正的数据。当然为了提升性能,客户端会缓存-ROOT- Table位置以及-ROOT-/.META. Table的内容。如下图所示: 可是即使客户端有缓存,在初始阶段需要三次请求才能直到用户Table真正所在的位置也是性能低下的,而且真的有必要支持那么多的HRegion吗?或许对Google这样的公司来说是需要的,但是对一般的集群来说好像并没有这个必要。在BigTable的论文中说,每行METADATA存储1KB左右数据,中等大小的Tablet(HRegion)在128MB左右,3层位置的Schema设计可以支持2^34个Tablet(HRegion)。即使去掉-ROOT- Table,也还可以支持2^17(131072)个HRegion, 如果每个HRegion还是128MB,那就是16TB,这个貌似不够大,但是现在的HRegion的最大大小都会设置的比较大,比如我们设置了2GB,此时支持的大小则变成了4PB,对一般的集群来说已经够了,因而在HBase 0.96以后去掉了-ROOT- Table,只剩下这个特殊的目录表叫做Meta Table(hbase:meta),它存储了集群中所有用户HRegion的位置信息,而ZooKeeper的节点中(/hbase/meta-region-server)存储的则直接是这个Meta Table的位置,并且这个Meta Table如以前的-ROOT- Table一样是不可split的。这样,客户端在第一次访问用户Table的流程就变成了: 从ZooKeeper(/hbase/meta-region-server)中获取hbase:meta的位置(HRegionServer的位置),缓存该位置信息。 从HRegionServer中查询用户Table对应请求的RowKey所在的HRegionServer,缓存该位置信息。 从查询到HRegionServer中读取Row。 从这个过程中,我们发现客户会缓存这些位置信息,然而第二步它只是缓存当前RowKey对应的HRegion的位置,因而如果下一个要查的RowKey不在同一个HRegion中,则需要继续查询hbase:meta所在的HRegion,然而随着时间的推移,客户端缓存的位置信息越来越多,以至于不需要再次查找hbase:meta Table的信息,除非某个HRegion因为宕机或Split被移动,此时需要重新查询并且更新缓存。 hbase:meta表hbase:meta表存储了所有用户HRegion的位置信息,它的RowKey:tableName,regionStartKey,regionId,replicaId等,它只有info列族,这个列族包含三个列,他们分别是:info:regioninfo列是RegionInfo的proto格式:regionId,tableName,startKey,endKey,offline,split,replicaId;info:server格式:HRegionServer对应的server:port;info:serverstartcode格式是HRegionServer的启动时间戳。 HRegionServer详解HRegionServer一般和DataNode在同一台机器上运行,实现数据的本地性。HRegionServer包含多个HRegion,由WAL(HLog)、BlockCache、MemStore、HFile组成。 WAL即Write Ahead Log,在早期版本中称为HLog,它是HDFS上的一个文件,如其名字所表示的,所有写操作都会先保证将数据写入这个Log文件后,才会真正更新MemStore,最后写入HFile中。采用这种模式,可以保证HRegionServer宕机后,我们依然可以从该Log文件中读取数据,Replay所有的操作,而不至于数据丢失。这个Log文件会定期Roll出新的文件而删除旧的文件(那些已持久化到HFile中的Log可以删除)。WAL文件存储在/hbase/WALs/${HRegionServer_Name}的目录中(在0.94之前,存储在/hbase/.logs/目录中),一般一个HRegionServer只有一个WAL实例,也就是说一个HRegionServer的所有WAL写都是串行的(就像log4j的日志写也是串行的),这当然会引起性能问题,因而在HBase 1.0之后,通过HBASE-5699实现了多个WAL并行写(MultiWAL),该实现采用HDFS的多个管道写,以单个HRegion为单位。关于WAL可以参考Wikipedia的Write-Ahead Logging。顺便吐槽一句,英文版的维基百科竟然能毫无压力的正常访问了,这是某个GFW的疏忽还是以后的常态? BlockCache是一个读缓存,即“引用局部性”原理(也应用于CPU,分空间局部性和时间局部性,空间局部性是指CPU在某一时刻需要某个数据,那么有很大的概率在一下时刻它需要的数据在其附近;时间局部性是指某个数据在被访问过一次后,它有很大的概率在不久的将来会被再次的访问),将数据预读取到内存中,以提升读的性能。HBase中提供两种BlockCache的实现:默认on-heap LruBlockCache和BucketCache(通常是off-heap)。通常BucketCache的性能要差于LruBlockCache,然而由于GC的影响,LruBlockCache的延迟会变的不稳定,而BucketCache由于是自己管理BlockCache,而不需要GC,因而它的延迟通常比较稳定,这也是有些时候需要选用BucketCache的原因。这篇文章BlockCache101对on-heap和off-heap的BlockCache做了详细的比较。 HRegion是一个Table中的一个Region在一个HRegionServer中的表达。一个Table可以有一个或多个Region,他们可以在一个相同的HRegionServer上,也可以分布在不同的HRegionServer上,一个HRegionServer可以有多个HRegion,他们分别属于不同的Table。HRegion由多个Store(HStore)构成,每个HStore对应了一个Table在这个HRegion中的一个Column Family,即每个Column Family就是一个集中的存储单元,因而最好将具有相近IO特性的Column存储在一个Column Family,以实现高效读取(数据局部性原理,可以提高缓存的命中率)。HStore是HBase中存储的核心,它实现了读写HDFS功能,一个HStore由一个MemStore 和0个或多个StoreFile组成。 MemStore是一个写缓存(In Memory Sorted Buffer),所有数据的写在完成WAL日志写后,会 写入MemStore中,由MemStore根据一定的算法将数据Flush到地层HDFS文件中(HFile),通常每个HRegion中的每个 Column Family有一个自己的MemStore。 HFile(StoreFile) 用于存储HBase的数据(Cell/KeyValue)。在HFile中的数据是按RowKey、Column Family、Column排序,对相同的Cell(即这三个值都一样),则按timestamp倒序排列。 虽然上面这张图展现的是最新的HRegionServer的架构(但是并不是那么的精确),但是我一直比较喜欢看以下这张图,即使它展现的应该是0.94以前的架构。 HRegionServer中数据写流程图解当客户端发起一个Put请求时,首先它从hbase:meta表中查出该Put数据最终需要去的HRegionServer。然后客户端将Put请求发送给相应的HRegionServer,在HRegionServer中它首先会将该Put操作写入WAL日志文件中(Flush到磁盘中)。 写完WAL日志文件后,HRegionServer根据Put中的TableName和RowKey找到对应的HRegion,并根据Column Family找到对应的HStore,并将Put写入到该HStore的MemStore中。此时写成功,并返回通知客户端。 MemStore FlushMemStore是一个In Memory Sorted Buffer,在每个HStore中都有一个MemStore,即它是一个HRegion的一个Column Family对应一个实例。它的排列顺序以RowKey、Column Family、Column的顺序以及Timestamp的倒序,如下所示: 每一次Put/Delete请求都是先写入到MemStore中,当MemStore满后会Flush成一个新的StoreFile(底层实现是HFile),即一个HStore(Column Family)可以有0个或多个StoreFile(HFile)。有以下三种情况可以触发MemStore的Flush动作, 需要注意的是MemStore的最小Flush单元是HRegion而不是单个MemStore。据说这是Column Family有个数限制的其中一个原因,估计是因为太多的Column Family一起Flush会引起性能问题?具体原因有待考证。 当一个HRegion中的所有MemStore的大小总和超过了hbase.hregion.memstore.flush.size的大小,默认128MB。此时当前的HRegion中所有的MemStore会Flush到HDFS中。 当全局MemStore的大小超过了hbase.regionserver.global.memstore.upperLimit的大小,默认40%的内存使用量。此时当前HRegionServer中所有HRegion中的MemStore都会Flush到HDFS中,Flush顺序是MemStore大小的倒序(一个HRegion中所有MemStore总和作为该HRegion的MemStore的大小还是选取最大的MemStore作为参考?有待考证),直到总体的MemStore使用量低于hbase.regionserver.global.memstore.lowerLimit,默认38%的内存使用量。 当前HRegionServer中WAL的大小超过了hbase.regionserver.hlog.blocksize hbase.regionserver.max.logs的数量,当前HRegionServer中所有HRegion中的MemStore都会Flush到HDFS中,Flush使用时间顺序,最早的MemStore先Flush直到WAL的数量少于hbase.regionserver.hlog.blocksize hbase.regionserver.max.logs。这里说这两个相乘的默认大小是2GB,查代码,hbase.regionserver.max.logs默认值是32,而hbase.regionserver.hlog.blocksize是HDFS的默认blocksize,32MB。但不管怎么样,因为这个大小超过限制引起的Flush不是一件好事,可能引起长时间的延迟,因而这篇文章给的建议:“Hint: keep hbase.regionserver.hlog.blocksize hbase.regionserver.maxlogs just a bit above hbase.regionserver.global.memstore.lowerLimit HBASE_HEAPSIZE.”。并且需要注意,这里给的描述是有错的(虽然它是官方的文档)。 在MemStore Flush过程中,还会在尾部追加一些meta数据,其中就包括Flush时最大的WAL sequence值,以告诉HBase这个StoreFile写入的最新数据的序列,那么在Recover时就直到从哪里开始。在HRegion启动时,这个sequence会被读取,并取最大的作为下一次更新时的起始sequence。 HFile格式HBase的数据以KeyValue(Cell)的形式顺序的存储在HFile中,在MemStore的Flush过程中生成HFile,由于MemStore中存储的Cell遵循相同的排列顺序,因而Flush过程是顺序写,我们直到磁盘的顺序写性能很高,因为不需要不停的移动磁盘指针。 HFile参考BigTable的SSTable和Hadoop的TFile实现,从HBase开始到现在,HFile经历了三个版本,其中V2在0.92引入,V3在0.98引入。首先我们来看一下V1的格式: V1的HFile由多个Data Block、Meta Block、FileInfo、Data Index、Meta Index、Trailer组成,其中Data Block是HBase的最小存储单元,在前文中提到的BlockCache就是基于Data Block的缓存的。一个Data Block由一个魔数和一系列的KeyValue(Cell)组成,魔数是一个随机的数字,用于表示这是一个Data Block类型,以快速监测这个Data Block的格式,防止数据的破坏。Data Block的大小可以在创建Column Family时设置(HColumnDescriptor.setBlockSize()),默认值是64KB,大号的Block有利于顺序Scan,小号Block利于随机查询,因而需要权衡。Meta块是可选的,FileInfo是固定长度的块,它纪录了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY, COMPARATOR, MAX_SEQ_ID_KEY等。Data Index和Meta Index纪录了每个Data块和Meta块的其实点、未压缩时大小、Key(起始RowKey?)等。Trailer纪录了FileInfo、Data Index、Meta Index块的起始位置,Data Index和Meta Index索引的数量等。其中FileInfo和Trailer是固定长度的。 HFile里面的每个KeyValue对就是一个简单的byte数组。但是这个byte数组里面包含了很多项,并且有固定的结构。我们来看看里面的具体结构: 开始是两个固定长度的数值,分别表示Key的长度和Value的长度。紧接着是Key,开始是固定长度的数值,表示RowKey的长度,紧接着是 RowKey,然后是固定长度的数值,表示Family的长度,然后是Family,接着是Qualifier,然后是两个固定长度的数值,表示Time Stamp和Key Type(Put/Delete)。Value部分没有这么复杂的结构,就是纯粹的二进制数据了。随着HFile版本迁移,KeyValue(Cell)的格式并未发生太多变化,只是在V3版本,尾部添加了一个可选的Tag数组。 HFileV1版本的在实际使用过程中发现它占用内存多,并且Bloom File和Block Index会变的很大,而引起启动时间变长。其中每个HFile的Bloom Filter可以增长到100MB,这在查询时会引起性能问题,因为每次查询时需要加载并查询Bloom Filter,100MB的Bloom Filer会引起很大的延迟;另一个,Block Index在一个HRegionServer可能会增长到总共6GB,HRegionServer在启动时需要先加载所有这些Block Index,因而增加了启动时间。为了解决这些问题,在0.92版本中引入HFileV2版本: 在这个版本中,Block Index和Bloom Filter添加到了Data Block中间,而这种设计同时也减少了写的内存使用量;另外,为了提升启动速度,在这个版本中还引入了延迟读的功能,即在HFile真正被使用时才对其进行解析。 FileV3版本基本和V2版本相比,并没有太大的改变,它在KeyValue(Cell)层面上添加了Tag数组的支持;并在FileInfo结构中添加了和Tag相关的两个字段。关于具体HFile格式演化介绍,可以参考这里。 对HFileV2格式具体分析,它是一个多层的类B+树索引,采用这种设计,可以实现查找不需要读取整个文件: Data Block中的Cell都是升序排列,每个block都有它自己的Leaf-Index,每个Block的最后一个Key被放入Intermediate-Index中,Root-Index指向Intermediate-Index。在HFile的末尾还有Bloom Filter用于快速定位那么没有在某个Data Block中的Row;TimeRange信息用于给那些使用时间查询的参考。在HFile打开时,这些索引信息都被加载并保存在内存中,以增加以后的读取性能。 参考:https://www.mapr.com/blog/in-depth-look-hbase-architecture#.VdNSN6Yp3qx http://jimbojw.com/wiki/index.php?title=Understanding_Hbase_and_BigTable http://hbase.apache.org/book.html http://www.searchtb.com/2011/01/understanding-hbase.html http://research.google.com/archive/bigtable-osdi06.pdf","link":"/2019/01/21/bigdata/hbase/Hbase系列2-Hbase架构深度解析/"},{"title":"SpringCloudGateway源码解析(1)-揭开SpringCloudGateway神秘面纱","text":" Spring Cloud Gateway是Spring官方自己推出的网关组件,基于Spring5,Spring Boot 2.0 和 Project Reactor等技术开发的网关,它旨在为微服务架构提供一种简单有效的API路由管理方式,作为Spring Cloud全家桶替代Zuul的产品。想一探它的神秘面容嘛?学习源码会收获哪些?想知道的话,就跟我走吧 前言 最近工作中,使用到了Spring Cloud Gateway,由于它相关的文档较少,工作中又需要用到一些高级特性,决定对它的源码进行学习,版本为v2.1.3.RELEASE。通过对源码的学习,不仅对Spring Cloud Gateway的原理,有了更深的了解,还对Spencer Gibb大师的编码深深吸引,本系列博文不仅带大家了解Spring Cloud Gateway的工作原理,还提炼出它所用到的技术、采用的设计模式、独具匠心的代码。本人水平有限,如有错误之处,请大家多多指出,感谢! 揭开SpringCloudGateway神秘面纱Spring Cloud Gateway是Spring官方自己推出的网关组件,基于Spring5,Spring Boot 2.0 和 Project Reactor等技术开发的网关,它旨在为微服务架构提供一种简单有效的API路由管理方式。至于Spring Cloud Gateway与 Zuul的比较,孰优孰劣的文章,网上有比较好的介绍,本文不再陈述。 基本概念 Route(路由):路由是网关最基本的信息载体。它由id,目标URI,谓语列表,过滤器列表组成。如果断言为真,则路由匹配。 Predicate(断言):断言是Java8中的Predicate。输入类型为ServerWebExchange。断言的职责是用于匹配HTTP请求,包括Header、Parameter等。Spring Cloud Gateway提供了很多断言规则,可以通过配置文件定义规则也可以通过fluent API定义。 Filter(过滤器):过滤器负责富化请求(Request)或者响应(Response),分为全局过滤器(GlobalFilter)和普通过滤器(GatewayFilter),Spring Cloud Gateway提供了很多内置的过滤器,也可以自定义过滤器。配置文件和fluent API均可定义。 工作流程 客户端向 Spring Cloud Gateway 发出请求。如果 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。 源码准备克隆源码到本地:git clone https://github.com/spring-cloud/spring-cloud-gateway.git 切换到v2.1.3.RELEASE: git checkout tag_name ,验证是否切换成功,git branch,显示如下已切换成功。 12* (HEAD detached at v2.1.3.RELEASE) master 把代码导入到IDEA IDE。看到Spring Cloud Gateway的工程结构,spring-cloud-gateway-core就是接下来主要学习的网关核心模块,上一个章节讲到的所有内容,都在Core模块中,Core模块中最核心的包,就是 config包:Gateway所有的配置,Bean的创建,是Gateway服务启动的必备条件。 handler包:包含FilteringWebHandler类,RoutePredicateHandlerMapping类,和所有的内置断言类。这个包的功能是处理客户端请求并匹配出Route对象。 filter包:包含了Gateway所有的过滤器实现。在Handler匹配到路由之后,ServerWebExchange对象就会流转到Filter集合中,最后经过层层过滤富化的Request对象会抵达Target URI。 route包:定义了RouteDefinition、Route、RouteLocator,对Gateway的资源进行了描述。 在本地启动SpringCloudGateway也十分容易,在spring-cloud-gateway-sample包中,官方提供了org.springframework.cloud.gateway.sample.GatewaySampleApplication类,可以直接本地启动SpringCloudGateway应用,到此所有环节都已准备完毕。 Spring Cloud Gateway源码学习收益 Gateway的源码大多数是由Spencer Gibb 编写,他是Spring 技术布道师,Spring Cloud核心项目的联合创始人。Gateway的采用了先进的技术,运用的大量的设计模式,优雅的代码风格,阅读即是一种享受,抓住其中一点都可以反复推敲,渐入佳境。 源码中采用了如下的设计模式: 建造者模式(Builder Pattern) 工厂模式(Factory Pattern) 责任链模式(Chain of Responsibility Pattern) 观察者模式(Observer Pattern) 服务定位器模式(Service Locator Pattern) Gateway依赖的技术 SpringBoot Spring5.0 WebFlux Reactor Java8 #参考文档 Spring Cloud Gateway官方文档 Spring Cloud Gateway源码 服务网关 Spring Cloud Gateway入门 Spring Cloud Gateway源码解析 Reactive programming 一种技术 各自表述 Java 8函数式接口functional interface的秘密 Reactor官方文档 使用 Reactor 进行反应式编程 微服务网关Zuul迁移到Spring Cloud Gateway","link":"/2019/10/17/spring/springcloud/gateway/SpringCloudGateway源码解析(1)-认识SpringCloudGateway/"},{"title":"SpringCloudGateway源码解析(2)-反应式编程","text":" 反应式编程,作为一种新的思想,以函数式编程为基础,受到越来越多的开发人员欢迎,Spring5作为行业的标准,也全面拥抱了Reactor框架。 前言 为了应对 高并发环境下 的服务端编程,微软提出了一个实现 异步编程 的方案 - Reactive Programming,中文名称 反应式编程。Java也汲取了反应式编程的思想,Spring Reactor应势而生。Spring5.0以WebFlux为代表全面拥抱了Reactor的编程模式。Spring Cloud Gateway底层是webFlux,而webFlux就是基于Spring Reactor构建的新一代WEB框架。 想读懂Spring Cloud Gateway,先学Reactor博主在读Spring Cloud Gateway源码之前,对Reactor技术不了解,在看Gateway源码时候不知道所谓了Flux和Mono是个啥概念。比如如下这段代码 12345678910111213public Mono<Void> filter(ServerWebExchange exchange) { return Mono.defer(() -> { if (this.index < filters.size()) { GatewayFilter filter = filters.get(this.index); DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this, this.index + 1); return filter.filter(exchange, chain); } else { return Mono.empty(); // complete } }); } 所以想读懂Gateway的源码,Reactor技术也是要掌握的,至少能了解Reactor的思想,知道它API的含义。接下来会带着大家对Reactor入个门,为后续的学习Gateway源码打好基础。 Flux原理 Flux表示的是包含0到N个元素的异步序列。下图中,operator上面共有6个元素,时间轴从左到右,分边进入到operator容器中进行处理,处理之后的结果流入到result flux中,有些情况下某些元素在处理过程中会出现错误。 Mono原理 Mono表示的是包含0-1个元素的异步序列,是一个特殊的Flux。原理和Flux一致,只是Mono限制了元素的数量。 反应式编程的所有API都在Flux和Mono中体现。 Reactor示例代码Flux 可以根据fromIterable,just,fromArray 三种API创建异步序列。 12345678910111213141516171819202122232425private static List<String> strs = Arrays.asList("the","quick","brown","fox","jumped","over","the","lazy","dog");public static void test0() { Flux.fromIterable(strs).subscribe(System.out::println); Flux.just(strs).subscribe(System.out::println); String[] strArray = {"a", "b", "c", "d"}; Flux.fromArray(strArray).subscribe(System.out::println);}结果:thequickbrownfoxjumpedoverthelazydog[the, quick, brown, fox, jumped, over, the, lazy, dog]abcd 创建一个Flux和一个Mono,flatMap函数按空字符串把happy字符串拆分成h,a,p,p,y这些子字符串,concatWith融合了s字符串,然后调用distinct和sort函数,完成了去重和排序,最终通过zipWith拼接数字. 12345678910111213public static void test3() { List<String> list = Arrays.asList(\"happy\"); Mono<String> missting = Mono.just(\"s\"); Flux.fromIterable(list).flatMap(word -> Flux.fromArray(word.split(\"\"))).concatWith(missting) .distinct().sort().zipWith(Flux.range(1, Integer.MAX_VALUE), (string, count) -> String.format(\"%d. %s\", count, string)).subscribe(System.out::println); }结果: 1. a 2. h 3. p 4. y 5. s 总结反应式编程范式对于习惯了传统编程范式的开发人员来说,既是一个需要进行思维方式转变的挑战,也是一个充满了更多可能的机会。Reactor 作为一个基于反应式流规范的新的 Java 库,可以作为反应式应用的基础。但是Reactor还不算成熟,以后的发展还需拭目以待,至少Spring已经全面拥抱。 #参考文档 聊聊Spring Reactor反应式编程 Reactive programming 一种技术 各自表述 Java 8函数式接口functional interface的秘密 Reactor官方文档 使用 Reactor 进行反应式编程","link":"/2019/10/18/spring/springcloud/gateway/SpringCloudGateway源码解析(2)-反应式编程/"},{"title":"SpringBoot可以放心使用Embedded Tomcat","text":" 使用过SpringBoot的同学,都知道java -jar application.jar就可以启动一个tomcat应用,十分的简单。之所这么方便,是因为SpringBoot帮我们整合了Embedded Tomcat,那么Native Tomcat和Embedded的相比,有什么差别呢?是不是Embedded Tomcat比Native版的差太多? 前言 使用过SpringBoot的同学,都知道java -jar application.jar就可以启动一个tomcat应用,十分的简单。之所这么方便,是因为SpringBoot帮我们整合了Embedded Tomcat,那么Native Tomcat和Embedded的相比,有什么差别呢?是不是Embedded Tomcat比Native版的差太多? 话说Embedded TomcatConnector 要讲Embedded和Native版的区别,先要了解tomcat的Connector: BIO 即同步阻塞式I/O,每一个请求,都会创建一个线程来处理,在高并发的场景下不适合。 NIO 即同步非阻塞I/O,在Java中,采用的多路复用I/O(epoll),适合高并发场景。 NIO2 即异步非阻塞I/O,在NIO的基础上,进一步提高I/O的性能。 APR/native APR全称是Apache Portable Runtime,是一个高度可移植的库,APR有许多用途,包括访问高级IO功能(例如sendfile,epoll和OpenSSL),操作系统级别的功能(生成随机数,系统状态等)以及本机进程处理(共享内存,NT管道和Unix套接字)。这些功能使Tomcat成为通用的Web服务器,可以更好地与其他本机Web技术集成,并且总体上使Java作为完整的Web服务器平台而不是仅以后端为中心的技术,更加可行。可以认为,APR是NIO的操作系统级别的实现,理论上比NIO要快一些。 ##区别&Embedded利好 从Tomcat7.0开始,Embedded版本便与Native版的一同发布,不过官方文档上没有介绍Embedded Tomcat的特性,理论上源码是一套,只是适配层略有区别。而已知的最大区别,便是Embedded不支持APR,换句话说,如果你不使用APR,那么Embedded和Native版是没什么区别的!这个很重要,如果没有区别,谁还会在服务器上安装tomcat,还需要打WAR包扔到tomcat目录下等等繁琐的操作。在SpringBoot中,只需要java -jar app.jar,就能启动一个高性能应用,岂不美哉。 而且在网上搜到一封tomcat开发团队的一封邮件,就是关于在Tomcat10中废弃APR的讨论)。我们拭目以待吧。总之放心用内置版tomcat吧 ","link":"/2019/10/30/spring/springboot/tomcat/SpringBoot可以放心使用Embedded Tomcat/"},{"title":"Hbase系列3-Hbase深入浅出","text":"转自原文:https://www.ibm.com/developerworks/cn/analytics/library/ba-cn-bigdata-hbase/index.html HBase 在大数据生态圈中的位置提到大数据的存储,大多数人首先联想到的是 Hadoop 和 Hadoop 中的 HDFS 模块。大家熟知的 Spark、以及 Hadoop 的 MapReduce,可以理解为一种计算框架。而 HDFS,我们可以认为是为计算框架服务的存储层。因此不管是 Spark 还是 MapReduce,都需要使用 HDFS 作为默认的持久化存储层。那么 HBase 又是什么,可以用在哪里,解决什么样的问题?简单地,我们可以认为 HBase 是一种类似于数据库的存储层,也就是说 HBase 适用于结构化的存储。并且 HBase 是一种列式的分布式数据库,是由当年的 Google 公布的 BigTable 的论文而生。不过这里也要注意 HBase 底层依旧依赖 HDFS 来作为其物理存储,这点类似于 Hive。 可能有的读者会好奇 HBase 于 Hive 的区别,我们简单的梳理一下 Hive 和 HBase 的应用场景: Hive 适合用来对一段时间内的数据进行分析查询,例如,用来计算趋势或者网站的日志。Hive 不应该用来进行实时的查询(Hive 的设计目的,也不是支持实时的查询)。因为它需要很长时间才可以返回结果;HBase 则非常适合用来进行大数据的实时查询,例如 Facebook 用 HBase 进行消息和实时的分析。对于 Hive 和 HBase 的部署来说,也有一些区别,Hive 一般只要有 Hadoop 便可以工作。而 HBase 则还需要 Zookeeper 的帮助(Zookeeper,是一个用来进行分布式协调的服务,这些服务包括配置服务,维护元信息和命名空间服务)。再而,HBase 本身只提供了 Java 的 API 接口,并不直接支持 SQL 的语句查询,而 Hive 则可以直接使用 HQL(一种类 SQL 语言)。如果想要在 HBase 上使用 SQL,则需要联合使用 Apache Phonenix,或者联合使用 Hive 和 HBase。但是和上面提到的一样,如果集成使用 Hive 查询 HBase 的数据,则无法绕过 MapReduce,那么实时性还是有一定的损失。Phoenix 加 HBase 的组合则不经过 MapReduce 的框架,因此当使用 Phoneix 加 HBase 的组成,实时性上会优于 Hive 加 HBase 的组合,我们后续也会示例性介绍如何使用两者。最后我们再提下 Hive 和 HBase 所使用的存储层,默认情况下 Hive 和 HBase 的存储层都是 HDFS。但是 HBase 在一些特殊的情况下也可以直接使用本机的文件系统。例如 Ambari 中的 AMS 服务直接在本地文件系统上运行 HBase。 HBase 与传统关系数据库的区别首先让我们了解下什么是 ACID。ACID 是指数据库事务正确执行的四个基本要素的缩写,其包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)以及持久性(Durability)。对于一个支持事务(Transaction)的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction Processing)当中无法保证数据的正确性,交易过程极可能达不到交易方的要求。下面,我们就简单的介绍下这 4 个特性的含义。 原子性(Atomicity)是指一个事务要么全部执行,要么全部不执行。换句话说,一个事务不可能只执行了一半就停止了。比如一个事情分为两步完成才可以完成,那么这两步必须同时完成,要么一步也不执行,绝不会停留在某一个中间状态。如果事物执行过程中,发生错误,系统会将事物的状态回滚到最开始的状态。 一致性(Consistency)是指事务的运行并不改变数据库中数据的一致性。也就是说,无论并发事务有多少个,但是必须保证数据从一个一致性的状态转换到另一个一致性的状态。例如有 a、b 两个账户,分别都是 10。当 a 增加 5 时,b 也会随着改变,总值 20 是不会改变的。 隔离性(Isolation)是指两个以上的事务不会出现交错执行的状态。因为这样可能会导致数据不一致。如果有多个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。 持久性(Durability)指事务执行成功以后,该事务对数据库所作的更改便是持久的保存在数据库之中,不会无缘无故的回滚。 在具体的介绍 HBase 之前,我们先简单对比下 HBase 与传统关系数据库的(RDBMS,全称为 Relational Database Management System)区别。如表 1 所示。 表 1. HBase 与 RDBMS 的区别 HBase RDBMS 硬件架构 类似于 Hadoop 的分布式集群,硬件成本低廉 传统的多核系统,硬件成本昂贵 容错性 由软件架构实现,由于由多个节点组成,所以不担心一点或几点宕机 一般需要额外硬件设备实现 HA 机制 数据库大小 PB GB、TB 数据排布方式 稀疏的、分布的多维的 Map 以行和列组织 数据类型 Bytes 丰富的数据类型 事物支持 ACID 只支持单个 Row 级别 全面的 ACID 支持,对 Row 和表 查询语言 只支持 Java API (除非与其他框架一起使用,如 Phoenix、Hive) SQL 索引 只支持 Row-key,除非与其他技术一起应用,如 Phoenix、Hive 支持 吞吐量 百万查询/每秒 数千查询/每秒 理解了上面的表格之后,我们在看看数据是如何在 HBase 以及 RDBMS 中排布的。首先,数据在 RDBMS 的排布大致如表 2。 表 2. 数据在 RDBMS 中的排布示例 ID 姓 名 密码 时间戳 1 张 三 111 20160719 2 李 四 222 20160720 那么数据在 HBase 中的排布会是什么样子呢?如表 3 所示(这只是逻辑上的排布)。 表 3. 数据在 HBase 中的排布(逻辑上) Row-Key Value(CF、Qualifier、Version) 1 info{‘姓’: ‘张’,’名’:’三’} pwd{‘密码’: ‘111’} 2 Info{‘姓’: ‘李’,’名’:’四’} pwd{‘密码’: ‘222’} 从上面示例表中,我们可以看出,在 HBase 中首先会有 Column Family 的概念,简称为 CF。CF 一般用于将相关的列(Column)组合起来。在物理上 HBase 其实是按 CF 存储的,只是按照 Row-key 将相关 CF 中的列关联起来。物理上的数据排布大致可以如表 4 所示。 表 4. 数据在 HBase 中的排布 Row-Key CF:Column-Key 时间戳 Cell Value 1 info:fn 123456789 三 1 info:ln 123456789 张 2 info:fn 123456789 四 2 info:ln 123456789 李 我们已经提到 HBase 是按照 CF 来存储数据的。在表 3 中,我们看到了两个 CF,分别是 info 和 pwd。info 存储着姓名相关列的数据,而 pwd 则是密码相关的数据。上表便是 info 这个 CF 存储在 Hbase 中的数据排布。Pwd 的数据排布是类似的。上表中的 fn 和 ln 称之为 Column-key 或者 Qulifimer。在 Hbase 中,Row-key 加上 CF 加上 Qulifier 再加上一个时间戳才可以定位到一个单元格数据(Hbase 中每个单元格默认有 3 个时间戳的版本数据)。初学者,在一开始接触这些概念是很容易混淆。其实不管是 CF 还是 Qulifier 都是客户定义出来的。也就是说在 HBase 中创建表格时,就需要指定表格的 CF、Row-key 以及 Qulifier。我们会在后续的介绍中,尝试指定这些相关的概念,以便加深理解。这里我们先通过下图理解下 HBase 中,逻辑上的数据排布与物理上的数据排布之间的关系。 图 1. Hbase 中逻辑上数据的排布与物理上排布的关联 从上图我们看到 Row1 到 Row5 的数据分布在两个 CF 中,并且每个 CF 对应一个 HFile。并且逻辑上每一行中的一个单元格数据,对应于 HFile 中的一行,然后当用户按照 Row-key 查询数据的时候,HBase 会遍历两个 HFile,通过相同的 Row-Key 标识,将相关的单元格组织成行返回,这样便有了逻辑上的行数据。讲解到这,我们就大致了解 HBase 中的数据排布格式,以及与 RDBMS 的一些区别。 对于 RDBMS 来说,一般都是以 SQL 作为为主要的访问方式。而 HBase 是一种”NoSQL”数据库。”NoSQL”是一个通用词表示该数据库并 是 RDBMS 。现在的市面上有许多种 NoSQL 数据库,如 BerkeleyDB 是本地 NoSQL 数据库的例子, HBase 则为大型分布式 NoSql 数据库。从技术上来说,Hbase 更像是”数据存储”而非”数据库”(HBase 和 HDFS 都属于大数据的存储层)。因此,HBase 缺少很多 RDBMS 特性,如列类型,二级索引,触发器和高级查询语言等。然而, HBase 也具有许多其他特征同时支持线性化和模块化扩充。最明显的方式,我们可以通过增加 Region Server 的数量扩展 HBase。并且 HBase 可以放在普通的服务器中,例如将集群从 5 个扩充到 10 个 Region Server 时,存储空间和处理容量都可以同时翻倍。当然 RDBMS 也能很好的扩充,但仅对一个点,尤其是对一个单独数据库服务器而言,为了更好的性能,往往需要特殊的硬件和存储设备(往往价格也非常昂贵)。 HBase 相关的模块以及 HBase 表格的特性在这里,让我们了解下 HBase 都有哪些模块,以及大致的工作流程。前面我们提到过 HBase 也是构建于 HDFS 之上,这是正确的,但也不是完全正确。HBase 其实也支持直接在本地文件系统之上运行,不过这样的 HBase 只能运行在一台机器上,那么对于分布式大数据的环境是没有意义的(这也是所谓的 HBase 的单机模式)。一般只用于测试或者验证某一个 HBase 的功能,后面我们在详细的介绍 HBase 的几种运行模式。这里我们只要记得在分布式的生产环境中,HBase 需要运行在 HDFS 之上,以 HDFS 作为其基础的存储设施。HBase 上层提供了访问的数据的 Java API 层,供应用访问存储在 HBase 的数据。在 HBase 的集群中主要由 Master 和 Region Server 组成,以及 Zookeeper,具体模块如下图所示。 图 2. HBase 的相关模块 接下来,我们简单的一一介绍下 HBase 中相关模块的作用。 Master HBase Master 用于协调多个 Region Server,侦测各个 Region Server 之间的状态,并平衡 Region Server 之间的负载。HBase Master 还有一个职责就是负责分配 Region 给 Region Server。HBase 允许多个 Master 节点共存,但是这需要 Zookeeper 的帮助。不过当多个 Master 节点共存时,只有一个 Master 是提供服务的,其他的 Master 节点处于待命的状态。当正在工作的 Master 节点宕机时,其他的 Master 则会接管 HBase 的集群。 Region Server 对于一个 Region Server 而言,其包括了多个 Region。Region Server 的作用只是管理表格,以及实现读写操作。Client 直接连接 Region Server,并通信获取 HBase 中的数据。对于 Region 而言,则是真实存放 HBase 数据的地方,也就说 Region 是 HBase 可用性和分布式的基本单位。如果当一个表格很大,并由多个 CF 组成时,那么表的数据将存放在多个 Region 之间,并且在每个 Region 中会关联多个存储的单元(Store)。 Zookeeper 对于 HBase 而言,Zookeeper 的作用是至关重要的。首先 Zookeeper 是作为 HBase Master 的 HA 解决方案。也就是说,是 Zookeeper 保证了至少有一个 HBase Master 处于运行状态。并且 Zookeeper 负责 Region 和 Region Server 的注册。其实 Zookeeper 发展到目前为止,已经成为了分布式大数据框架中容错性的标准框架。不光是 HBase,几乎所有的分布式大数据相关的开源框架,都依赖于 Zookeeper 实现 HA。 一个完整分布式的 HBase 的工作原理示意图如下: 图 3. HBase 的工作原理 在上面的图中,我们需要注意几个我们之前没有提到的概念:Store、MemStore、StoreFile 以及 HFile。带着这几个新的概念,我们完整的梳理下整个 HBase 的工作流程。 首先我们需要知道 HBase 的集群是通过 Zookeeper 来进行机器之前的协调,也就是说 HBase Master 与 Region Server 之间的关系是依赖 Zookeeper 来维护。当一个 Client 需要访问 HBase 集群时,Client 需要先和 Zookeeper 来通信,然后才会找到对应的 Region Server。每一个 Region Server 管理着很多个 Region。对于 HBase 来说,Region 是 HBase 并行化的基本单元。因此,数据也都存储在 Region 中。这里我们需要特别注意,每一个 Region 都只存储一个 Column Family 的数据,并且是该 CF 中的一段(按 Row 的区间分成多个 Region)。Region 所能存储的数据大小是有上限的,当达到该上限时(Threshold),Region 会进行分裂,数据也会分裂到多个 Region 中,这样便可以提高数据的并行化,以及提高数据的容量。每个 Region 包含着多个 Store 对象。每个 Store 包含一个 MemStore,和一个或多个 HFile。MemStore 便是数据在内存中的实体,并且一般都是有序的。当数据向 Region 写入的时候,会先写入 MemStore。当 MemStore 中的数据需要向底层文件系统倾倒(Dump)时(例如 MemStore 中的数据体积到达 MemStore 配置的最大值),Store 便会创建 StoreFile,而 StoreFile 就是对 HFile 一层封装。所以 MemStore 中的数据会最终写入到 HFile 中,也就是磁盘 IO。由于 HBase 底层依靠 HDFS,因此 HFile 都存储在 HDFS 之中。这便是整个 HBase 工作的原理简述。 我们了解了 HBase 大致的工作原理,那么在 HBase 的工作过程中,如何保证数据的可靠性呢?带着这个问题,我们理解下 HLog 的作用。HBase 中的 HLog 机制是 WAL 的一种实现,而 WAL(一般翻译为预写日志)是事务机制中常见的一致性的实现方式。每个 Region Server 中都会有一个 HLog 的实例,Region Server 会将更新操作(如 Put,Delete)先记录到 WAL(也就是 HLog)中,然后将其写入到 Store 的 MemStore,最终 MemStore 会将数据写入到持久化的 HFile 中(MemStore 到达配置的内存阀值)。这样就保证了 HBase 的写的可靠性。如果没有 WAL,当 Region Server 宕掉的时候,MemStore 还没有写入到 HFile,或者 StoreFile 还没有保存,数据就会丢失。或许有的读者会担心 HFile 本身会不会丢失,这是由 HDFS 来保证的。在 HDFS 中的数据默认会有 3 份。因此这里并不考虑 HFile 本身的可靠性。 前面,我们很多次提到了 HFile,也就是 HBase 持久化的存储文件。也许有的读者还不能完全理解 HFile,这里我们便详细的看看 HFile 的结构,如下图。 图 4. HFile 的结构 从图中我们可以看到 HFile 由很多个数据块(Block)组成,并且有一个固定的结尾块。其中的数据块是由一个 Header 和多个 Key-Value 的键值对组成。在结尾的数据块中包含了数据相关的索引信息,系统也是通过结尾的索引信息找到 HFile 中的数据。HFile 中的数据块大小默认为 64KB。如果访问 HBase 数据库的场景多为有序的访问,那么建议将该值设置的大一些。如果场景多为随机访问,那么建议将该值设置的小一些。一般情况下,通过调整该值可以提高 HBase 的性能。 如果要用很短的一句话总结 HBase,我们可以认为 HBase 就是一个有序的多维 Map,其中每一个 Row-key 映射了许多数据,这些数据存储在 CF 中的 Column。我们可以用下图来表示这句话。 图 5. HBase 的数据映射关系 HBase 的使用建议之前我介绍了很多 HBase 与 RDBMS 的区别,以及一些优势的地方。那么什么时候最需要 HBase,或者说 HBase 是否可以替代原有的 RDBMS?对于这个问题,我们必须时刻谨记——HBase 并不适合所有问题,其设计目标并不是替代 RDBMS,而是对 RDBMS 的一个重要补充,尤其是对大数据的场景。当需要考量 HBase 作为一个备选项时,我们需要进行如下的调研工作。 首先,要确信有足够多数据,如果有上亿或上千亿行数据,HBase 才会是一个很好的备选。其次,需要确信业务上可以不依赖 RDBMS 的额外特性,例如,列数据类型, 二级索引,SQL 查询语言等。再而,需要确保有足够硬件。且不说 HBase,一般情况下当 HDFS 的集群小于 5 个数据节点时,也干不好什么事情 (HDFS 默认会将每一个 Block 数据备份 3 分),还要加上一个 NameNode。 以下我给了一些使用 HBase 时候对表格设计的一些建议,读者也可以理解背后的含义。不过我并不希望这些建议成为使用 HBase 的教条,毕竟也有不尽合理的地方。首先,一个 HBase 数据库是否高效,很大程度会和 Row-Key 的设计有关。因此,如何设计 Row-key 是使用 HBase 时,一个非常重要的话题。随着数据访问方式的不同,Row-Key 的设计也会有所不同。不过概括起来的宗旨只有一个,那就是尽可能选择一个 Row-Key,可以使你的数据均匀的分布在集群中。这也很容易理解,因为 HBase 是一个分布式环境,Client 会访问不同 Region Server 获取数据。如果数据排布均匀在不同的多个节点,那么在批量的 Client 便可以从不同的 Region Server 上获取数据,而不是瓶颈在某一个节点,性能自然会有所提升。对于具体的建议我们一般有几条: 当客户端需要频繁的写一张表,随机的 RowKey 会获得更好的性能。 当客户端需要频繁的读一张表,有序的 RowKey 则会获得更好的性能。 对于时间连续的数据(例如 log),有序的 RowKey 会很方便查询一段时间的数据(Scan 操作)。 上面我们谈及了对 Row-Key 的设计,接着我们需要想想是否 Column Family 也会在不同的场景需要不同的设计方案呢。答案是肯定的,不过 CF 跟 Row-key 比较的话,确实也简单一些,但这并不意味着 CF 的设计就是一个琐碎的话题。在 RDBMS(传统关系数据库)系统中,我们知道如果当用户的信息分散在不同的表中,便需要根据一个 Key 进行 Join 操作。而在 HBase 中,我们需要设计 CF 来聚合用户所有相关信息。简单来说,就是需要将数据按类别(或者一个特性)聚合在一个或多个 CF 中。这样,便可以根据 CF 获取这类信息。上面,我们讲解过一个 Region 对应于一个 CF。那么设想,如果在一个表中定义了多个 CF 时,就必然会有多个 Region。当 Client 查询数据时,就不得不查询多个 Region。这样性能自然会有所下降,尤其当 Region 夸机器的时候。因此在大多数的情况下,一个表格不会超过 2 到 3 个 CF,而且很多情况下都是 1 个 CF 就足够了。","link":"/2019/01/21/bigdata/hbase/Hbase系列3-Hbase深入浅出/"},{"title":"跟我学springboot第二讲-加载Environment源码解析&最佳实践","text":"SpringBoot中的配置文件是如何加载的?系统级变量SpringBoot是否获得的?如何做到按环境不同加载不同的配置文件?是不是对此感兴趣,那点进来一起学习吧! 在上一节SpringBoot应用启动流程分析中,我们了解了一个SpringBoot应用启动所有的环节,那么本章我们将重点展开Environment的最佳实践和实现原理。 什么是EnvironmentEnvironment表示当前应用程序运行环境的接口,为应用环境两个关键方面建模:profiles和properties,通过PropertyResolver接口来实现属性访问相关的方法。Environment是Spring对应用所处环境的抽象,一般情况下包含配置文件,系统变量等等。 SpringBoot启动流程中,构建环境只需要一行代码ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments),这行代码背后的秘密就是本文的主要内容。 SpringBoot Environment最佳实践在讲Environment初始化源码前掌握SpringBoot中Environment相关最佳实践,是学习的最佳方式。主要包括了配置文件路径变更,占位符,多环境配置,加载顺序,自动映射对象。 最佳实践中的内容,有些摘抄自Spring Boot 2.x基础教程:配置文件详解,该文章讲解的很详细,没有必要重复发明轮子了。 参数传递形式对于一个Java应用,参数有多种传递形式。可以是虚拟机参数(VM options),应用参数(Program arguments),操作系统变量(Environment variables)。 虚拟机参数 Standard Options(-D but not only) 标准参数,适用于所有的Java虚拟机,如果你需要传入自定义的参数,建议加上-D,当然你完全可以不加-D,但是可能会和JVM原生的 option冲突。比如你的参数叫-server=myApp,这样就和原生的-server参数冲突,在启动的时候会报错。 123Unrecognized option: -server=111Error: Could not create the Java Virtual Machine.Error: A fatal exception has occurred. Program will exit. 当你使用-Dserver=111时,应用则顺利启动,它会被SpringBoot的systemProperties采集到”server”->”111”。 Non-Standard Options(Prefixed with -X) -X不是通用的JVM参数,只适用于Java HotSpot虚拟机,例如:-Xmssize,-Xmxsize。 Advanced Runtime Options (prefixed with -XX) Java HotSpot虚拟机运行时参数 Advanced JIT Compiler Options (prefixed with -XX) Java HotSpot JIT 参数 Advanced Serviceability Options (prefixed with -XX) 收集系统信息和调试的参数 Advanced Garbage Collection Options (prefixed with -XX) Java HotSpot垃圾回收期参数 应用参数应用参数public static void main(String[] args){}即args,在SpringBoot中,是通过--前缀进行解析的。 12345678910111213141516171819202122232425262728293031323334class SimpleCommandLineArgsParser { /** * Parse the given {@code String} array based on the rules described {@linkplain * SimpleCommandLineArgsParser above}, returning a fully-populated * {@link CommandLineArgs} object. * @param args command line arguments, typically from a {@code main()} method */ public CommandLineArgs parse(String... args) { CommandLineArgs commandLineArgs = new CommandLineArgs(); for (String arg : args) { if (arg.startsWith(\"--\")) { String optionText = arg.substring(2, arg.length()); String optionName; String optionValue = null; if (optionText.contains(\"=\")) { optionName = optionText.substring(0, optionText.indexOf('=')); optionValue = optionText.substring(optionText.indexOf('=')+1, optionText.length()); } else { optionName = optionText; } if (optionName.isEmpty() || (optionValue != null && optionValue.isEmpty())) { throw new IllegalArgumentException(\"Invalid argument syntax: \" + arg); } commandLineArgs.addOptionArg(optionName, optionValue); } else { commandLineArgs.addNonOptionArg(arg); } } return commandLineArgs; }} 操作系统变量SpringBoot也会采集系统变量。Linux中通过export命令设定系统变量。 配置文件路径变更SpringBoot默认会自动检查classpath:/,classpath:/config/,file:/,file:/config/,这四个路径下的application.yml,application.yaml,application.properties,application.xml。 如果想更改配置文件路径或者文件名则需要增加系统环境变量。例如 java -jar xxx.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties,告诉SpringBoot加载指定的default.properties和override.properties。也可以指定目录,但是必须以\\结尾,例如java -jar xxx.jar --spring.config.location=classpath:/otherconfig/。 1234567891011121314151617181920212223242526272829/** * The \"config location\" property name.*/public static final String CONFIG_LOCATION_PROPERTY = \"spring.config.location\";/** * The \"config additional location\" property name.*/public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = \"spring.config.additional-location\";private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { getSearchLocations().forEach((location) -> { //看这里,一定要用/结尾 boolean isFolder = location.endsWith(\"/\"); Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES; names.forEach((name) -> load(location, name, profile, filterFactory, consumer)); });}//获取配置文件location的关键代码private Set<String> getSearchLocations() { if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) { return getSearchLocations(CONFIG_LOCATION_PROPERTY); } Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY); locations.addAll( asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS)); return locations;} 也可以追加自定义的配置文件和路径,通过指定spring.config.additional-location参数,例如java -jar xxx.jar --spring.config.additional-location=classpath:/default.properties,会读取默认配置的基础上再追加配置文件。也可以追加目录java -jar xxx.jar --spring.config.additional-location=classpath:/otherconfig/。 自定义参数我们除了可以在Spring Boot的配置文件中设置各个Starter模块中预定义的配置属性,也可以在配置文件中定义一些我们需要的自定义属性。比如在application.properties中添加: 12book.name=SpringCloudInActionbook.author=crowley 然后,在应用中我们可以通过@Value注解来加载这些自定义的参数,比如: 12345678910@Componentpublic class Book { @Value(\"${book.name}\") private String name; @Value(\"${book.author}\") private String author; // 省略getter和setter} @Value注解加载属性值的时候可以支持两种表达式来进行配置: 一种是我们上面介绍的PlaceHolder方式,格式为 ${…},大括号内为PlaceHolder 另外还可以使用SpEL表达式(Spring Expression Language), 格式为 #{…},大括号内为SpEL表达式 参数引用在application配置文件中的各个参数之间,也可以通过使用placeHolder的方式进行引用。 123book.name=SpringCloudbook.author=ZhaiYongchaobook.desc=${book.author} is writing《${book.name}》 在Spring应用程序的environment中读取属性的时候,每个属性的唯一名称符合如下规则: 通过.分离各个元素 最后一个.将前缀与属性名称分开 必须是字母(a-z)和数字(0-9) 必须是小写字母 用连字符-来分隔单词 唯一允许的其他字符是[和],用于List的索引 不能以数字开头 所以,如果我们要读取配置文件中spring.jpa.database-platform的配置,可以这样写:this.environment.containsProperty("spring.jpa.database-platform") 而下面的方式是无法获取到spring.jpa.database-platform配置内容的:this.environment.containsProperty("spring.jpa.databasePlatform") 注意:使用@Value获取配置内容的时候也需要这样的特点 使用随机数在一些特殊情况下,有些参数我们希望它每次加载的时候不是一个固定的值,比如:密钥、服务端口等。在Spring Boot的属性配置文件中,我们可以通过使用${random}配置来产生随机的int值、long值或者string字符串,这样我们就可以容易的通过配置来属性的随机生成,而不是在程序中通过编码来实现这些逻辑。 ${random}的配置方式主要有一下几种,读者可作为参考使用。 12345678910# 随机字符串com.didispace.blog.value=${random.value}# 随机intcom.didispace.blog.number=${random.int}# 随机longcom.didispace.blog.bignumber=${random.long}# 10以内的随机数com.didispace.blog.test1=${random.int(10)}# 10-20的随机数com.didispace.blog.test2=${random.int[10,20]} 该配置方式可以用于设置应用端口等场景,避免在本地调试时出现端口冲突的麻烦 命令行参数在命令行方式启动Spring Boot应用时,连续的两个减号–就是对application.properties中的属性值进行赋值的标识。所以,java -jar xxx.jar --server.port=8888命令,等价于我们在application.properties中添加属性server.port=8888。 通过命令行来修改属性值是Spring Boot非常重要的一个特性,通过此特性,理论上已经使得我们应用的属性在启动前是可变的,所以其中端口号也好、数据库连接也好,都是可以在应用启动时发生改变,而不同于以往的Spring应用通过Maven的Profile在编译器进行不同环境的构建。其最大的区别就是,Spring Boot的这种方式,可以让应用程序的打包内容,贯穿开发、测试以及线上部署,而Maven不同Profile的方案每个环境所构建的包,其内容本质上是不同的。但是,如果每个参数都需要通过命令行来指定,这显然也不是一个好的方案,所以下面我们看看如果在Spring Boot中实现多环境的配置。 多环境配置我们在开发任何应用的时候,通常同一套程序会被应用和安装到几个不同的环境,比如:开发、测试、生产等。其中每个环境的数据库地址、服务器端口等等配置都会不同,如果在为不同环境打包时都要频繁修改配置文件的话,那必将是个非常繁琐且容易发生错误的事。 对于多环境的配置,各种项目构建工具或是框架的基本思路是一致的,通过配置多份不同环境的配置文件,再通过打包命令指定需要打包的内容之后进行区分打包,Spring Boot也不例外,或者说更加简单。 在Spring Boot中多环境配置文件名需要满足application-{profile}.properties的格 式,其中{profile}对应你的环境标识,比如: application-dev.properties:开发环境 application-test.properties:测试环境 application-prod.properties:生产环境 至于哪个具体的配置文件会被加载,需要在application.properties文件中通过spring.profiles.active属性来设置,其值对应配置文件中的{profile}值。如:spring.profiles.active=test就会加载application-test.properties配置文件内容。 下面,以不同环境配置不同的服务端口为例,进行样例实验。 针对各环境新建不同的配置文件application-dev.properties、application-test.properties、application-prod.properties在这三个文件均都设置不同的server.port属性,如:dev环境设置为1111,test环境设置为2222,prod环境设置为3333 application.properties中设置spring.profiles.active=dev,就是说默认以dev环境设置 测试不同配置的加载 执行java -jar xxx.jar,可以观察到服务端口被设置为1111,也就是默认的开发环境(dev)执行java -jar xxx.jar –spring.profiles.active=test,可以观察到服务端口被设置为2222,也就是测试环境的配置(test)执行java -jar xxx.jar –spring.profiles.active=prod,可以观察到服务端口被设置为3333,也就是生产环境的配置(prod)按照上面的实验,可以如下总结多环境的配置思路: application.properties中配置通用内容,并设置spring.profiles.active=dev,以开发环境为默认配置application-{profile}.properties中配置各个环境不同的内容。也可以采用-Dspring.profiles.active=dev设置激活的Profile。通过命令行方式去激活不同环境的配置 加载顺序在上面的例子中,我们将Spring Boot应用需要的配置内容都放在了项目工程中,虽然我们已经能够通过spring.profiles.active或是通过Maven来实现多环境的支持。但是,当我们的团队逐渐壮大,分工越来越细致之后,往往我们不需要让开发人员知道测试或是生成环境的细节,而是希望由每个环境各自的负责人(QA或是运维)来集中维护这些信息。那么如果还是以这样的方式存储配置内容,对于不同环境配置的修改就不得不去获取工程内容来修改这些配置内容,当应用非常多的时候就变得非常不方便。同时,配置内容都对开发人员可见,本身这也是一种安全隐患。对此,现在出现了很多将配置内容外部化的框架和工具,比如Spring Cloud Config,Apollo,Nacos等等。 Spring Boot为了能够更合理的重写各属性的值,使用了下面这种较为特别的属性加载顺序: 1、命令行中传入的参数。 2、SPRING_APPLICATION_JSON中的属性。SPRING_APPLICATION_JSON是以JSON格式配置在系统环境变量中的内容。 3、java:comp/env中的JNDI属性。 4、Java的系统属性,可以通过System.getProperties()获得的内容。 5、操作系统的环境变量 6、通过random.*配置的随机属性 7、位于当前应用jar包之外,针对不同{profile}环境的配置文件内容,例如:application-{profile}.properties或是YAML定义的配置文件 8、位于当前应用jar包之内,针对不同{profile}环境的配置文件内容,例如:application-{profile}.properties或是YAML定义的配置文件 9、位于当前应用jar包之外的application.properties和YAML配置内容 10、位于当前应用jar包之内的application.properties和YAML配置内容 11、在@Configuration注解修改的类中,通过@PropertySource注解定义的属性 12、应用默认属性,使用SpringApplication.setDefaultProperties定义的内容 优先级按上面的顺序有高到低,数字越小优先级越高。 可以看到,其中第7项和第9项都是从应用jar包之外读取配置文件,所以,实现外部化配置的原理就是从此切入,为其指定外部配置文件的加载位置来取代jar包之内的配置内容。通过这样的实现,我们的工程在配置中就变的非常干净,我们只需要在本地放置开发需要的配置即可,而其他环境的配置就可以不用关心,由其对应环境的负责人去维护即可。 2.x新特性在Spring Boot 2.0中推出了Relaxed Binding 2.0,对原有的属性绑定功能做了非常多的改进以帮助我们更容易的在Spring应用中加载和读取配置信息。下面本文就来说说Spring Boot 2.0中对配置的改进。 配置文件绑定简单类型在Spring Boot 2.0中对配置属性加载的时候会除了像1.x版本时候那样移除特殊字符外,还会将配置均以全小写的方式进行匹配和加载。所以,下面的4种配置方式都是等价的: properties格式: 1234spring.jpa.databaseplatform=mysqlspring.jpa.database-platform=mysqlspring.jpa.databasePlatform=mysqlspring.JPA.database_platform=mysql yaml格式 123456spring: jpa: databaseplatform: mysql database-platform: mysql databasePlatform: mysql database_platform: mysql Tips:推荐使用全小写配合-分隔符的方式来配置,比如:spring.jpa.database-platform=mysql List类型在properties文件中使用[]来定位列表类型,比如: 12spring.my-example.url[0]=http://example.comspring.my-example.url[1]=http://spring.io 也支持使用逗号分割的配置方式,上面与下面的配置是等价的: 1spring.my-example.url=http://example.com,http://spring.io 而在yaml文件中使用可以使用如下配置: 12345spring: my-example: url: - http://example.com - http://spring.io 也支持逗号分割的方式: 123spring: my-example: url: http://example.com, http://spring.io 注意:在Spring Boot 2.0中对于List类型的配置必须是连续的,不然会抛出UnboundConfigurationPropertiesException异常,所以如下配置是不允许的: 12foo[0]=afoo[2]=b 在Spring Boot 1.x中上述配置是可以的,foo[1]由于没有配置,它的值会是null Map类型Map类型在properties和yaml中的标准配置方式如下: Properties格式: 12spring.my-example.foo=barspring.my-example.hello=world Yaml格式: 1234spring: my-example: foo: bar hello: world 注意:如果Map类型的key包含非字母数字和-的字符,需要用[]括起来,比如: 123spring: my-example: '[foo.baz]': bar 全新绑定API在Spring Boot 2.0中增加了新的绑定API来帮助我们更容易的获取配置信息。下面举个例子来帮助大家更容易的理解: 例1:简单类型 假设在propertes配置中有这样一个配置:com.didispace.foo=bar 12345@Data@ConfigurationProperties(prefix = \"com.didispace\")public class FooProperties { private String foo;} 接下来,通过最新的Binder就可以这样来拿配置信息了: 12345678910@SpringBootApplicationpublic class Application { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(Application.class, args); Binder binder = Binder.get(context.getEnvironment()); // 绑定简单配置 FooProperties foo = binder.bind(\"com.didispace\", Bindable.of(FooProperties.class)).get(); System.out.println(foo.getFoo()); }} 例2:List类型 如果配置内容是List类型呢?比如: 1234567com.didispace.post[0]=Why Spring Bootcom.didispace.post[1]=Why Spring Cloudcom.didispace.posts[0].title=Why Spring Bootcom.didispace.posts[0].content=It is perfect!com.didispace.posts[1].title=Why Spring Cloudcom.didispace.posts[1].content=It is perfect too! 要获取这些配置依然很简单,可以这样实现: 12345678ApplicationContext context = SpringApplication.run(Application.class, args);Binder binder = Binder.get(context.getEnvironment());// 绑定List配置List<String> post = binder.bind(\"com.didispace.post\", Bindable.listOf(String.class)).get();System.out.println(post);List<PostInfo> posts = binder.bind(\"com.didispace.posts\", Bindable.listOf(PostInfo.class)).get();System.out.println(posts); SpringBoot Environment加载源码解析通过最佳实践,我们对SpringBoot的Environment有了一个概要的了解,接下来我们要从源码角度,分析一下SpringBoot是如何实现这么人性化的配置功能的。 Spring应用启动的时候,会依赖系统的环境和应用的配置,所以Spring定义了Environment包装系统的环境和应用的配置。 12345678910111213private ConfigurableEnvironment getOrCreateEnvironment() { if (this.environment != null) { return this.environment; } switch (this.webApplicationType) { case SERVLET: return new StandardServletEnvironment(); case REACTIVE: return new StandardReactiveWebEnvironment(); default: return new StandardEnvironment(); }} 从上面的代码可以知道,SpringBoot会根据webApplicationType的不同,选择创建StandardServletEnvironment,StandardReactiveWebEnvironment,StandardEnvironment这三个对象中的某一个,它们的共同超类是AbstractEnvironment,下图中还整理了Environment依赖的其他核心类。AbstractEnvironment中最重要的成员变量是MutablePropertyResolver和PropertySourcePropertyResover,prepareEnvironment方法执行的过程,也是设置这两个成员变量的过程。 接下来,我们通过了解SpringBoot中Environment的启动流程来更加清晰的认识Environment。 1234567891011121314151617181920private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { //第一步,创建Environment,根据webApplicationType来判定具体创建哪个Environment实例 ConfigurableEnvironment environment = getOrCreateEnvironment(); //第二步,配置Environment,主要分为三步:1设置转换器(Conversion),2设置propertySources,3设置Profile configureEnvironment(environment, applicationArguments.getSourceArgs()); //第三步,装载configurationProperties,最终Environment持有的PropertySource如Enviroment加载debug图所示 ConfigurationPropertySources.attach(environment); //第四步,很关键。发送ApplicationEnvironmentPreparedEvent事件,最终该事件会被ConfigFileApplicationListener消费。具体消费之后如何绑定配置文件,下一小节会重点讲述。 listeners.environmentPrepared(environment); //第五步,绑定环境变量 bindToSpringApplication(environment); //如果是自定义Environment,则重新创建Environment,应用场景很少,不重点研究。 if (!this.isCustomEnvironment) { environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); } ConfigurationPropertySources.attach(environment); return environment;} 相信看过prepareEnvironment方法之后,还不能在脑海中形成一个具体的Environment的数据结构,Environment结构示意图可以很形象的描述各个类之间的关系。 加载之后的Environment对象内容如下。 ConfigFileApplicationListenerConfigFileApplicationListener实现了EnvironmentPostProcessor函数式接口,负责监听ApplicationEnvironmentPreparedEvent事件来加载application.properties或者application.yml。 1234567891011public class ConfigFileApplicationListener { @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ApplicationEnvironmentPreparedEvent) { onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event); } if (event instanceof ApplicationPreparedEvent) { onApplicationPreparedEvent(event); } }} LoaderLoader是ConfigFileApplicationListener的内部类,它通过监听ApplicationEnvironmentPreparedEvent事件,加载候选的property sources和配置激活的profile,相关的类图如下所示。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152private class Loader { private final ConfigurableEnvironment environment; private final PropertySourcesPlaceholdersResolver placeholdersResolver; private final ResourceLoader resourceLoader; private final List<PropertySourceLoader> propertySourceLoaders; //所有的profile private Deque<Profile> profiles; //处理过的Profile private List<Profile> processedProfiles; //是否激活了profile,如果激活就不再设置active profile private boolean activatedProfiles; //已经加载的属性,MutablePropertySources底层是CopyOnWriteArrayList private Map<Profile, MutablePropertySources> loaded; //缓存的属性 private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>(); Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { this.environment = environment; //把environment中的MutablePropertySources交给PropertySourcesPlaceholdersResolver,由PropertyPlaceholderHelper完成变量的替换 this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment); this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(); //获取org.springframework.boot.env.PropertiesPropertySourceLoaderh和org.springframework.boot.env.YamlPropertySourceLoader this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, getClass().getClassLoader()); } public void load() { this.profiles = new LinkedList<>(); this.processedProfiles = new LinkedList<>(); this.activatedProfiles = false; this.loaded = new LinkedHashMap<>(); //设置profile initializeProfiles(); //轮询profile while (!this.profiles.isEmpty()) { Profile profile = this.profiles.poll(); if (profile != null && !profile.isDefaultProfile()) { addProfileToEnvironment(profile.getName()); } //加载资源,参数分别是profile,DocumentFilter函数,DocumentConsumer函数 load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false)); //处理完一个profile就把该profile添加进processed容器中 this.processedProfiles.add(profile); } resetEnvironmentProfiles(this.processedProfiles); load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true)); addLoadedPropertySources(); } } 参考文档1、基于SpringBoot的Environment源码理解实现分散配置 2、Spring Boot 2.x基础教程:配置文件详解","link":"/2019/11/27/spring/springboot/跟我学springboot第二讲-加载Environment源码解析&最佳实践/"},{"title":"SpringCloudGateway源码解析(3)- 路由","text":" 在文章《SpringCloudGateway源码解析-揭开SpringCloudGateway神秘面纱》中,我们从宏观上了解了Spring Cloud Gateway的整体架构和思想,本篇文章就是要带着大家了解网关的一等公民”路由”的前世和今生。 前言 在文章《SpringCloudGateway源码解析-揭开SpringCloudGateway神秘面纱》中,我们从宏观上了解了Spring Cloud Gateway的整体架构和思想,本篇文章就是要带着大家了解网关的一等公民”路由”的前世和今生。 路由什么是路由?维基百科中定义。路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。 通过定义我们可以很清晰的认识到,路由中最核心的两点,即”源地址”和”目的地址”,更简单的说,网关最核心的功能就是把一个网络包安全快速的从源头搬运到目的地地址上。 在Spring Cloud Gateway中,路由是一个叫Route的对象定义的,作为Gateway的使用者,需要了解路由是如何创建?如何修改?如何删除?或者更高级的特性,如何在不重启网关的情况下,任意的修改路由的配置,并让它实时生效等等,而且网关左右微服务体系下的门户,”不重启”是很重要的特性。接下来我们就从源码角度,仔细的学习Spring Cloud Gateway是如何实现一个网关路由的。 ##Route && RouteDefinition 了解大数据技术的同学应该听说过”元数据”这个术语。元数据是描述数据的数据,而Gateway中的RouteDefinition,就是为了描述Route的。换句话说,配置文件—>RouteDefinition—>Route。那这个时候有的同学会发问了,为何不能直接配置文件——>Route,为何还要再加一个RouteDefinition?是因为Spring Cloud Gateway不仅仅支持Route通过配置文件的方式配置,还支持FluentAPI、Endpoint、注册中心等等其他的路由来源,根据”单一职责”设计,RouteDefinition使得Route和用户的行为解耦。聪明的小伙伴的可以发现,这种方式的可扩展性是非常强的,最左侧可以增加Redis,Mysql,Apollo等等组件,想想还有点小兴奋。 Route类 123456789101112public class Route implements Ordered { private final String id;//路由id private final URI uri;//target地址 private final int order;//路由优先级,值越小优先级越高 private final AsyncPredicate<ServerWebExchange> predicate;//路由断言 private final List<GatewayFilter> gatewayFilters;//路由过滤器栈} RouteDefinition类,包含了两个构造函数,还可以通过字符串的方式构建。 123456789101112131415161718192021222324252627282930313233343536@Validatedpublic class RouteDefinition { @NotEmpty private String id = UUID.randomUUID().toString();//路由元数据 @NotEmpty @Valid private List<PredicateDefinition> predicates = new ArrayList<>();//路由断言集合 @Valid private List<FilterDefinition> filters = new ArrayList<>();//路由过滤器集合 @NotNull private URI uri;//目标地址URI private int order = 0;//路由优先级 public RouteDefinition() { } public RouteDefinition(String text) { int eqIdx = text.indexOf('='); if (eqIdx <= 0) { throw new ValidationException(\"Unable to parse RouteDefinition text '\" + text + \"'\" + \", must be of the form name=value\"); } setId(text.substring(0, eqIdx)); String[] args = tokenizeToStringArray(text.substring(eqIdx + 1), \",\"); setUri(URI.create(args[0])); for (int i = 1; i < args.length; i++) { this.predicates.add(new PredicateDefinition(args[i])); } }} ##RouteDefinitionLocator RouteDefinitionLocator只有一个方法来获取RouteDefinition异步序列。 RouteDefinitionLocator继承关系图 123public interface RouteDefinitionLocator { Flux<RouteDefinition> getRouteDefinitions();} 它的子类的作用加载情况如下: CachingRouteDefinitionLocator不再使用。 PropertiesRouteDefinitionLocator在GatewayAutoConfiguration中加载。它持有GatewayProperties对象,GatewayProperties对象通过@ConfigurationProperties注解自动装配RouteDefinition,所以通过PropertiesRouteDefinitionLocator类可以直接获取application.yml中所有用户配置的路由信息。 1234567@ConfigurationProperties("spring.cloud.gateway")@Validatedpublic class GatewayProperties { @NotNull @Valid private List<RouteDefinition> routes = new ArrayList<>();} 12345678910111213spring: cloud: gateway: routes: - id: yml-id uri: www.baidu.com order: -1 predicates: - name: predicatesName args: arg0: 1 arg1: 2 - Path=/echo InMemoryRouteDefinitionRepository在GatewayAutoConfiguration中加载。InMemoryRouteDefinitionRepository提供了save()和delete()两个函数,默认通过gateway的AbstractGatewayControllerEndpoint调用,换句话说,Gateway提供了RestAPI来新增和删除网关的功能,但是这种方式比较鸡肋(JVM级的更新,没有提供持久化设定,如果Gateway集群有十台服务器,你需要操作十遍),生产环境下动态路由,只能借助其他的持久化中间件来实现动态路由,后续的章节,会专门讲如何使用Apollo(携程开源的高可用注册中心)来实现Gateway的动态路由。 12345678910111213141516171819202122232425262728293031public class InMemoryRouteDefinitionRepository implements RouteDefinitionRepository { private final Map<String, RouteDefinition> routes = synchronizedMap( new LinkedHashMap<String, RouteDefinition>()); @Override public Mono<Void> save(Mono<RouteDefinition> route) { return route.flatMap(r -> { routes.put(r.getId(), r); return Mono.empty(); }); } @Override public Mono<Void> delete(Mono<String> routeId) { return routeId.flatMap(id -> { if (routes.containsKey(id)) { routes.remove(id); return Mono.empty(); } return Mono.defer(() -> Mono.error( new NotFoundException(\"RouteDefinition not found: \" + routeId))); }); } @Override public Flux<RouteDefinition> getRouteDefinitions() { return Flux.fromIterable(routes.values()); }} CompositeRouteDefinitionLocator在GatewayAutoConfiguration中加载。CompositeRouteDefinitionLocator持有了所有的RouteDefinitionLocator。 1234567891011121314public class CompositeRouteDefinitionLocator implements RouteDefinitionLocator { private final Flux<RouteDefinitionLocator> delegates; public CompositeRouteDefinitionLocator(Flux<RouteDefinitionLocator> delegates) { this.delegates = delegates; } @Override public Flux<RouteDefinition> getRouteDefinitions() { return this.delegates.flatMap(RouteDefinitionLocator::getRouteDefinitions); }} DiscoveryClientRouteDefinitionLocator在GatewayDiscoveryClientAutoConfiguration中加载,目的是集成SpringCloud的注册中心(例如Eureka,Zookeeper,Nacos等),DiscoveryClientRouteDefinitionLocator依赖DiscoveryClient对象,所有的路由信息均是通过DiscoveryClient获得,然后转换成RouteDefinition对象。 ##RouteLocator RouteLocator只包含一个函数getRoutes()获取所有的Route对象。它有三个实现类,但是真正干活的实现类是RouteDefinitionRouteLocator, 123public interface RouteLocator { Flux<Route> getRoutes();} 123456789101112131415161718192021 @Overridepublic Flux<Route> getRoutes() { //获取RouteDefinition集合,调用convertToRoute转换成Route对象 return this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute) .map(route -> { if (logger.isDebugEnabled()) { logger.debug(\"RouteDefinition matched: \" + route.getId()); } return route; });}private Route convertToRoute(RouteDefinition routeDefinition) { //断言 AsyncPredicate<ServerWebExchange> predicate = combinePredicates(routeDefinition); //过滤器 List<GatewayFilter> gatewayFilters = getFilters(routeDefinition); //构建Route return Route.async(routeDefinition).asyncPredicate(predicate) .replaceFilters(gatewayFilters).build();} #设计模式 在Route包下,Spencer Gibb采用了模板模式和建造者模式。 模板模式在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。其实我个人理解模板模式就是面向接口编程。RouteLocator和RouteDefinitionLocator都采用了模板模式 建造者模式建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 一个 Builder 类会一步一步构造最终的对象。该 Builder 类是独立于其他对象的。 FluentAPI就是一个很经典的建造者模式的应用。 1builder.routes().route(r -> r.path(\"/anything/png\").uri(\"https://www.jd.com\")).build();","link":"/2019/10/20/spring/springcloud/gateway/SpringCloudGateway源码解析(3)-路由/"},{"title":"SpringCloudGateway源码解析(5)- 基于配置中心的动态路由","text":" 微服务网关,是一个微服务体系的门户,是多有流量的入口和出口。这样重要的地位就代表了网关需要很高的稳定性。而动态路由就是Spring Cloud Gateway高可用的一种解决方案。 前言 微服务网关,是一个微服务体系的门户,是多有流量的入口和出口。这样重要的地位就代表了网关需要很高的稳定性。通过前四章对Spring Cloud Gateway的学习,我们了解到框架提供了通过配置文件、FluentAPI和Restful接口三种定义路由的方式。前两种方式是通过配置文件或者硬编码的方式来实现,这种方式虽然简单易用,但是如果路由信息变更,则必须重新发布或者重启网关应用,这会对网关服务的稳定性带来挑战(任何一次发布,都有可能引入新的Bug,都会导致JIT的优化丢失,需要重新预热,导致系统抖动),通过Restful接口修改,虽然可以做到实时更新路由,但是缺少了持久化策略,网关一旦重启,则所有的修改都会丢失,而且也较难维护。基于以上分析,Spring Cloud Gateway如果想做到高可用,需要引入一套可维护的动态路由功能。结合配置中心实现动态路由,就是一个很好的解决方案。 配置中心是什么? 随着程序功能的日益复杂,程序的配置日益增多,各种功能的开关、参数的配置、服务器的地址。对程序配置的期望值也越来越高,配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制。在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对配置管理的需求。配置中心,就是为了解决如上的问题的。 常用的配置中心,如Spring Cloud Config、Apollo、Nacos等,比较了众多的配置中心组建,我选择Apollo作为配置中心,它的功能很强大。接下来对Apollo的基本功能做一个讲解。环境列表包括了DEV、QA、SIM、PRO四个环境,AppId是Apollo的一个重要配置,是项目的主键。右侧是application、gateway、auth、route四个命名空间(namespace)。根据AppId和nameSpace可以定位到一组配置。想进一步了解Apollo的同学,请参考官网。 客户端向 Spring Cloud Gateway 发出请求。如果 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。 动态配置理论基础 Spring Cloud Gateway中,有一个RefreshRoutesEvent类,顾名思义它是刷新路由的事件类。CachingRouteLocator实现了对RefreshRoutesEvent的监听,一旦RefreshRoutesEvent被publish,refresh函数就会被调用。refresh函数的作用是把cache清除,因为CacheFlux.lookup监听cache是否丢失,如果cache丢失则onCacheMissResume触发重新根据routeDefinition转化为Route。 12345public class RefreshRoutesEvent extends ApplicationEvent { public RefreshRoutesEvent(Object source) { super(source); }} 12345678910111213141516171819202122public class CachingRouteLocator implements RouteLocator, ApplicationListener<RefreshRoutesEvent> { private final Flux<Route> routes; public CachingRouteLocator(RouteLocator delegate) { this.delegate = delegate; routes = CacheFlux.lookup(cache, \"routes\", Route.class) .onCacheMissResume(() -> this.delegate.getRoutes() .sort(AnnotationAwareOrderComparator.INSTANCE)); } public Flux<Route> refresh() { this.cache.clear(); return this.routes; } @Override public void onApplicationEvent(RefreshRoutesEvent event) { refresh(); }} 动态配置实现 ApolloConfigChangePublisher通过@ApolloConfigChangeListener,实现Apollo route命名空间的监听,并实现了ApplicationEventPublisherAware接口。当Apollo配置文件发布之后,routeChange事件就会触发,json解析成对象之后,publisher出去。 123456789101112{ \"routeId\": \"travel-jd-route\", \"uri\": \"https://www.jd.com\", \"predicates\": [ { \"name\": \"Path\", \"args\": { \"_genkey_0\": \"/tojd\" } } ]} 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253import com.alibaba.fastjson.JSON;import com.ctrip.framework.apollo.Config;import com.ctrip.framework.apollo.enums.PropertyChangeType;import com.ctrip.framework.apollo.model.ConfigChange;import com.ctrip.framework.apollo.model.ConfigChangeEvent;import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.context.ApplicationEventPublisher;import org.springframework.context.ApplicationEventPublisherAware;import org.springframework.stereotype.Component;@Componentpublic class ApolloConfigChangePublisher implements ApplicationEventPublisherAware { /** * Apollo动态路由命名空间 */ @ApolloConfig(\"route\") private Config routeConfig; @ApolloConfigChangeListener(\"route\") private void routeChange(ConfigChangeEvent changeEvent) { for (String changeKey : changeEvent.changedKeys()) { ConfigChange cc = changeEvent.getChange(changeKey); PropertyChangeType ct = cc.getChangeType(); String value = cc.getNewValue(); String oldValue = cc.getOldValue(); if (ct.equals(PropertyChangeType.DELETED)) { publisher.publishEvent(new RouteChangeEvent(new Object(), GatewayConstant.Apollo.CHANGE_DELETE, JSON.toJavaObject(JSON.parseObject(oldValue), GatewayRoute.class).getRouteId())); } else if (ct.equals(PropertyChangeType.ADDED)) { publisher.publishEvent(new RouteChangeEvent( JSON.toJavaObject(JSON.parseObject(value), GatewayRoute.class), GatewayConstant.Apollo.CHANGE_ADD, null)); } else if (ct.equals(PropertyChangeType.MODIFIED)) { publisher.publishEvent(new RouteChangeEvent( JSON.toJavaObject(JSON.parseObject(value), GatewayRoute.class), GatewayConstant.Apollo.CHANGE_MODIFY, null)); } else { LOGGER.error(\"changeType错误.{}\", cc.toString()); } } } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; }} RouteChangeEventHandler通过实现ApplicationListener接口,监听了RouteChangeEvent,根据RouteChangeEvent的类型,来判定是新增、修改还是删除。 123456789101112131415161718192021222324252627282930313233343536373839404142434445@Componentpublic class RouteChangeEventHandler implements ApplicationListener<RouteChangeEvent> { private static final Logger LOGGER = LoggerFactory.getLogger(RouteChangeEventHandler.class); /** * 动态路由 */ @ApolloConfig(\"route\") private Config routeConfig; @Autowired private GatewayDynamicRouteService gatewayDynamicRouteService; /** * 初始化所有的路由 */ @PostConstruct private void initAllRoute() { String routeList = routeConfig.getProperty(GatewayConstant.Apollo.ROUTE_LIST, GatewayConstant.Apollo.EMPTY_VALUE); if (GatewayConstant.Apollo.EMPTY_VALUE.equals(routeList)) { throw new Error(\"重要!路由初始化错误,code:\" + ErrorCode.APOLLO_NOT_USE); } String[] routes = routeList.split(GatewayConstant.Apollo.ROUTE_LIST_SPLIT); for (String route : routes) { String dynamicRoute = routeConfig.getProperty(route, GatewayConstant.Apollo.EMPTY_VALUE); if (GatewayConstant.Apollo.EMPTY_VALUE.equals(dynamicRoute)) { throw new Error(\"重要!路由初始化错误,code:\" + ErrorCode.APOLLO_CONFIG_LACK); } gatewayDynamicRouteService.save(JSON.toJavaObject(JSON.parseObject(dynamicRoute), GatewayRoute.class)); } } @Override public void onApplicationEvent(RouteChangeEvent event) { LOGGER.info(\"动态路由变更已接收routeId:{},type:{}\", event.getRouteId(), event.getChangeType()); if(GatewayConstant.Apollo.CHANGE_ADD.equals(event.getChangeType())) { GatewayRoute route = (GatewayRoute) event.getSource(); gatewayDynamicRouteService.save(route); } else if (GatewayConstant.Apollo.CHANGE_DELETE.equals(event.getChangeType())) { gatewayDynamicRouteService.delete(event.getRouteId()); } else if(GatewayConstant.Apollo.CHANGE_MODIFY.equals(event.getChangeType())) { GatewayRoute route = (GatewayRoute) event.getSource(); gatewayDynamicRouteService.save(route); } }} GatewayDynamicRouteService也必须实现ApplicationEventPublisherAware,为了发送RefreshRoutesEvent。 1234567891011121314151617181920212223242526272829303132@Componentpublic class GatewayDynamicRouteService implements ApplicationEventPublisherAware { private static final Logger LOGGER = LoggerFactory.getLogger(GatewayDynamicRouteService.class); @Autowired private RouteDefinitionWriter routeDefinitionWriter; private ApplicationEventPublisher publisher; public void save(GatewayRoute route) { RouteDefinition definition = new RouteDefinition(); PredicateDefinition predicate = new PredicateDefinition(); definition.setId(route.getRouteId()); predicate.setName(route.getPredicates().get(0).getName()); predicate.setArgs(route.getPredicates().get(0).getArgs()); definition.setPredicates(Arrays.asList(predicate)); URI uri = UriComponentsBuilder.fromUriString(route.getUri()).build().toUri(); definition.setUri(uri); routeDefinitionWriter.save(Mono.just(definition)).subscribe(); this.publisher.publishEvent(new RefreshRoutesEvent(this)); } public void delete(String id) { routeDefinitionWriter.delete(Mono.just(id)).subscribe(); this.publisher.publishEvent(new RefreshRoutesEvent(this)); } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; }} 效果演示首先设置好京东的路由信息,访问localhost:8080/jd,可以看到会跳转网关。 删除京东的网关设定之后,再次访问localhost:8080/jd则404。","link":"/2019/10/23/spring/springcloud/gateway/SpringCloudGateway源码解析(5)-基于Apollo动态路由/"},{"title":"SpringCloudGateway源码解析(4)- 核心流程","text":" 在文章《SpringCloudGateway源码解析(3)- 路由的装配》中,我们了解了网关路由的相关实现,这一章节,主要讲解下SpringCloudGateway中handler包实现,其中最核心的两个类FilteringWebHandler和RoutePredicateHandlerMapping。 前言 在文章《SpringCloudGateway源码解析(3)- 路由的装配》中,我们了解了网关路由的相关实现,这一章节,主要讲解下SpringCloudGateway中handler包实现,其中最核心的两个类FilteringWebHandler和RoutePredicateHandlerMapping。 GatewayAutoConfiguration首先先看下FilteringWebHandler,RoutePredicateHandlerMapping 配置类注解含义如下 @Configuration表示是一个SpringBoot配置类 @ConditionalOnProperty 监听spring.cloud.gateway.enabled配置,缺少配置默认为true @EnableConfigurationProperties @ConfigurationProperties注解的前置条件,@ConfigurationProperties可以自动的将配置文件解析为Bean @AutoConfigureBefore 先加载WebFluxAutoConfiguration和HttpHandlerAutoConfiguration,再加载自身 @AutoConfigureAfter 先加载自身,再加载GatewayLoadBalancerClientAutoConfiguration和GatewayClassPathWarningAutoConfiguration @ConditionalOnClass 配置加载依赖DispatcherHandler,即依赖WebFlux组件 12345678910111213141516171819202122232425262728293031323334353637383940414243@Configuration@ConditionalOnProperty(name = \"spring.cloud.gateway.enabled\", matchIfMissing = true)@EnableConfigurationProperties@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class })@AutoConfigureAfter({ GatewayLoadBalancerClientAutoConfiguration.class, GatewayClassPathWarningAutoConfiguration.class })@ConditionalOnClass(DispatcherHandler.class)public class GatewayAutoConfiguration { @Bean public FilteringWebHandler filteringWebHandler(List<GlobalFilter> globalFilters) { return new FilteringWebHandler(globalFilters); } @Bean public RoutePredicateHandlerMapping routePredicateHandlerMapping( FilteringWebHandler webHandler, RouteLocator routeLocator,//CachingRouteLocator会优先注入 GlobalCorsProperties globalCorsProperties, Environment environment) { return new RoutePredicateHandlerMapping(webHandler, routeLocator, globalCorsProperties, environment); } @Bean public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties, List<GatewayFilterFactory> GatewayFilters, List<RoutePredicateFactory> predicates, RouteDefinitionLocator routeDefinitionLocator, @Qualifier(\"webFluxConversionService\") ConversionService conversionService) { return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates, GatewayFilters, properties, conversionService); } @Bean @Primary //重要!!CachingRouteLocator生命了@Primary注解,是优先选择注入的 public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) { return new CachingRouteLocator( new CompositeRouteLocator(Flux.fromIterable(routeLocators))); }} Gateway源码中,大量运用SpringBoot中autoconfigure的功能。 #WebFlux入口类DispatcherHandler 要说Spring Cloud Gateway的handler包,就不得不说DispatcherHandler类。DispatcherHandler实现了ApplicationContextAware接口和WebHandler,主要持有handlerMappings,handlerAdapters,resultHandlers这三个对象。在Spring初始化的时候,会调用setApplicationContext函数进行初始化,初始化的步骤就是利用工具函数BeanFactoryUtils#beansOfTypeIncludingAncestors取出其父类的子类,赋值给DispatcherHandler的三个成员变量。赋值之后,handlerMappings和handlerAdapters的集合中,包含如下类,其中RoutePredicateHandlerMapping和SimpleHandlerAdapter是我们的主角,其他的类不在我们的讨论范围内。 当在网络请求过来时,handle函数会被调用,第一步就是构建handlerMapping Flux,此异步队列的长度为6。再concatMap通过mapping获取handler,这时会走所有的HandlerMapping实现,在Gateway的实现中,对应RoutePredicateHandlerMapping#getHandlerInternal函数,匹配路由的返回Mono,不匹配的路由返回Mono.empty(),在网关服务中,核心的路由是通过网关的api配置的,所以核心路由都会RoutePredicateHandlerMapping中,不会在其他的HandlerMapping中出现,理论上只有RoutePredicateHandlerMapping能匹配上路由,如果匹配多个,只取第一个,而RoutePredicateHandlerMapping最终返回的是FilteringWebHandler的Mono。 特意验证了Flux这段代码,这端测试代码的逻辑和DispatcherHandler类似,最后返回的结果是3,大家可以参考下。 1234567List<Integer> list = Arrays.asList(1,2,3,4,5);Flux.fromIterable(list).concatMap(a -> { if(a == 1) return Mono.empty(); return Mono.just(a + 1);}).next().subscribe(System.out::println);最终输出结果:3 接下来就会执行另外一个核心的函数invokeHandler。invokeHandler中会轮询handlerAdapters中所有的Adapters,用到了适配器模式。由于上一个流返回FilteringWebHandler对象,所以FilteringWebHandler#handle会执行。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768public class DispatcherHandler implements WebHandler, ApplicationContextAware { @Nullable private List<HandlerMapping> handlerMappings; @Nullable private List<HandlerAdapter> handlerAdapters; @Nullable private List<HandlerResultHandler> resultHandlers; @Override public void setApplicationContext(ApplicationContext applicationContext) { initStrategies(applicationContext); } protected void initStrategies(ApplicationContext context) { Map<String, HandlerMapping> mappingBeans = //step1,获取 HandlerMapping的所有子类 BeanFactoryUtils.beansOfTypeIncludingAncestors( context, HandlerMapping.class, true, false); ArrayList<HandlerMapping> mappings = new ArrayList<>(mappingBeans.values()); AnnotationAwareOrderComparator.sort(mappings); this.handlerMappings = Collections.unmodifiableList(mappings); //step2,获取 HandlerAdapter的所有子类 Map<String, HandlerAdapter> adapterBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( context, HandlerAdapter.class, true, false); this.handlerAdapters = new ArrayList<>(adapterBeans.values()); AnnotationAwareOrderComparator.sort(this.handlerAdapters); //step3,获取 HandlerResultHandler的所有子类 Map<String, HandlerResultHandler> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( context, HandlerResultHandler.class, true, false); this.resultHandlers = new ArrayList<>(beans.values()); AnnotationAwareOrderComparator.sort(this.resultHandlers); } @Override public Mono<Void> handle(ServerWebExchange exchange) { if (this.handlerMappings == null) { return createNotFoundError(); } return Flux.fromIterable(this.handlerMappings) //通过mapping获取handler,对应Gateway中的RoutePredicateHandlerMapping#getHandlerInternal,匹配路由的返回Mono<Handler>,不匹配的路由返回Mono.empty() .concatMap(mapping -> mapping.getHandler(exchange)) //理论上只有一个handler能匹配上路由,如果匹配多个,只取第一个 .next() .switchIfEmpty(createNotFoundError()) //调用FilteringWebHandler#handle .flatMap(handler -> invokeHandler(exchange, handler)) .flatMap(result -> handleResult(exchange, result)); } private Mono<HandlerResult> invokeHandler(ServerWebExchange exchange, Object handler) { if (this.handlerAdapters != null) { for (HandlerAdapter handlerAdapter : this.handlerAdapters) { if (handlerAdapter.supports(handler)) { return handlerAdapter.handle(exchange, handler); } } } return Mono.error(new IllegalStateException(\"No HandlerAdapter: \" + handler)); } } RoutePredicateHandlerMapping 经过对DispatcherHandler源码的分析,相信大家已经摸清RoutePredicateHandlerMapping的定位了,他的作用就是断言路由,返回FilterWebHandler,其核心的方法是getHandlerInternal和lookupRoute。 12345678910111213141516171819202122232425262728@Overrideprotected Mono<?> getHandlerInternal(ServerWebExchange exchange) { // don't handle requests on management port if set and different than server port if (this.managementPortType == DIFFERENT && this.managementPort != null && exchange.getRequest().getURI().getPort() == this.managementPort) { return Mono.empty(); } exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName()); return lookupRoute(exchange) // .log(\"route-predicate-handler-mapping\", Level.FINER) //name this .flatMap((Function<Route, Mono<?>>) r -> { exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR); if (logger.isDebugEnabled()) { logger.debug( \"Mapping [\" + getExchangeDesc(exchange) + \"] to \" + r); } exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); return Mono.just(webHandler); }).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> { exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR); if (logger.isTraceEnabled()) { logger.trace(\"No RouteDefinition found for [\" + getExchangeDesc(exchange) + \"]\"); } })));} 通过this.routeLocator是CachingRouteLocator的实例,换句话说是优先从缓存中获取,如果缓存中获取不到,则从RouteDefinitionRouteLocator获取Route。获取所有的Route,从而获取Route对象的predicate属性。 123456789101112131415161718192021222324252627282930313233protected Mono<Route> lookupRoute(ServerWebExchange exchange) { return this.routeLocator.getRoutes() // individually filter routes so that filterWhen error delaying is not a // problem .concatMap(route -> Mono.just(route).filterWhen(r -> { // add the current route we are testing exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId()); return r.getPredicate().apply(exchange); }) // instead of immediately stopping main flux due to error, log and // swallow it .doOnError(e -> logger.error( \"Error applying predicate for route: \" + route.getId(), e)) .onErrorResume(e -> Mono.empty())) // .defaultIfEmpty() put a static Route not found // or .switchIfEmpty() // .switchIfEmpty(Mono.<Route>empty().log(\"noroute\")) .next() // TODO: error handling .map(route -> { if (logger.isDebugEnabled()) { logger.debug(\"Route matched: \" + route.getId()); } validateRoute(route, exchange); return route; }); /* * TODO: trace logging if (logger.isTraceEnabled()) { * logger.trace(\"RouteDefinition did not match: \" + routeDefinition.getId()); } */} FilteringWebHandler1234567891011121314151617@Overridepublic Mono<Void> handle(ServerWebExchange exchange) { //step1 获取断言匹配的路由对象 Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR); //step2 获取路由下的过滤器对象 List<GatewayFilter> gatewayFilters = route.getFilters(); //step3 合并全局过滤器和局部过滤器,并按照order排序 List<GatewayFilter> combined = new ArrayList<>(this.globalFilters); combined.addAll(gatewayFilters); AnnotationAwareOrderComparator.sort(combined); if (logger.isDebugEnabled()) { logger.debug(\"Sorted gatewayFilterFactories: \" + combined); } //step4 创建过滤器责任链 return new DefaultGatewayFilterChain(combined).filter(exchange);} 应用的设计模式工厂模式 Spring Cloud Gateway中,断言采用的是工厂模式。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。 RoutePredicateFactory是一个函数式接口,是负责生产泛型为ServerWebExchange的Predicate的工厂,更进一步说生产的是GatewayPredicate对象。 12345678910@FunctionalInterfacepublic interface RoutePredicateFactory<C> extends ShortcutConfigurable, Configurable<C> { ... Predicate<ServerWebExchange> apply(C config); ...}public interface GatewayPredicate extends Predicate<ServerWebExchange> { } 责任链模式 顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。 在Spring Cloud Gateway中,过滤就是采用了责任链模式。 123456789101112131415161718192021222324252627282930313233343536private static class DefaultGatewayFilterChain implements GatewayFilterChain { private final int index; private final List<GatewayFilter> filters; DefaultGatewayFilterChain(List<GatewayFilter> filters) { this.filters = filters; this.index = 0; } private DefaultGatewayFilterChain(DefaultGatewayFilterChain parent, int index) { this.filters = parent.getFilters(); this.index = index; } public List<GatewayFilter> getFilters() { return filters; } @Override public Mono<Void> filter(ServerWebExchange exchange) { return Mono.defer(() -> { if (this.index < filters.size()) { GatewayFilter filter = filters.get(this.index); DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this, this.index + 1); return filter.filter(exchange, chain); } else { return Mono.empty(); // complete } }); } }","link":"/2019/10/22/spring/springcloud/gateway/SpringCloudGateway源码解析(4)-核心流程/"},{"title":"SpringCloudGateway源码解析(6)- 常用功能实现","text":" 网关的鉴权,动态限流,注册中心自动路由功能,是一个网关的最基本功能,让我们一起来学习。 前言 前面几章主要是对Spring Cloud Gateway源码的了解,本章则讲述常用的网关功能的实现。 鉴权实现 鉴权通过对称加密,用户传入一个token。如下图展示的是POST方式的,GET方式比较简单,直接在Path后面拼装?token=dd9e46d0eb1aced7即可。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081public class AuthFilter implements GlobalFilter, Ordered { private DesUtils dest; @Autowired private AuthBean authBean; private static final String SPLIT_WORD = \",\"; @PostConstruct private void init() { try { LOGGER.info(\"AuthFilter初始化: token salt:{}\", authBean.getSalt()); dest = new DesUtils(authBean.getSalt()); LOGGER.info(\"token={}\", dest.encrypt(\"1001\")); } catch (Exception e) { throw new MfwSearchBusinessException(\"AuthFilter秘钥工具初始化失败:\", e.getMessage(), e); } } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { HttpMethod httpMethod = exchange.getRequest().getMethod(); LOGGER.debug(\"AuthFilter in.HTTP :{}\", httpMethod); if (HttpMethod.GET.equals(httpMethod)) { String token = exchange.getRequest().getQueryParams().getFirst(\"token\"); if (!checkToken(token)) { LOGGER.info(\"请求不通过鉴权,url:{}\", exchange.getRequest().getURI().toString()); //未通过验证 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } } else if (HttpMethod.POST.equals(httpMethod)) { Map<String, String> postBody = exchange.getAttribute(GatewayConstant.CACHE_POST_BODY); LOGGER.debug(\"http request body:{}\", JSON.toJSONString(postBody)); if (postBody == null) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } String token = postBody.get(\"token\"); if (!checkToken(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } } else { //not support other Http Method exchange.getResponse().setStatusCode(HttpStatus.UNSUPPORTED_MEDIA_TYPE); return exchange.getResponse().setComplete(); } //pass auth return chain.filter(exchange); } /** * 校验token是否合法 * token解密后是appId * * @param token * @return */ private boolean checkToken(String token) { if (StringUtils.isBlank(token)) { return false; } try { boolean flag = false; String tokenStr = dest.decrypt(token); String[] appIdArray = authBean.getAppId().split(SPLIT_WORD); for (String appId : appIdArray) { if (tokenStr.equals(appId)) { flag = true; break; } } return flag; } catch (Exception e) { //todo 解析失败需要报警 LOGGER.error(\"token解析失败,默认通过校验token:{}\", token, e); } return true; } ...} 动态限流功能 Spring Cloud Gateway中,限流功能是至关重要的,当有不正常的流量打过来,可以控制它的流速或者直接封杀调异常流量。限流熔断的中间件有很多,比如Hystrix、Sentinel,本文主要讲解Sentinel集成Apollo,实现动态的流量控制。首先需要加入依赖,Apollo对于Sentinel来说,是一个datasource,当然像Mysql,Redis等都可以当做Sentinel的数据源。 12345678<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency><dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-apollo</artifactId></dependency> 如下提供了一个包括总体限流、URL限流、用户限流的实现,需要在Apollo中配置限流规则。GATEWAY-LIMIT是总体限流的key,/search-bs/helloword是URL的限流规则,u_1001是用户限流规则。 [{"grade":1,"count":100,"resource":"GATEWAY-LIMIT"},{"grade":1,"count":5,"resource":"/search-bs/helloword"},{"grade":1,"count":20,"resource":"u_1001"}] 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263public class SentinelFilter implements GlobalFilter, Ordered { /** * 限流 * 限流是一个单key,value是FlowRule的集合 */ @PostConstruct private void init() { // It's better to provide a meaningful default value. String defaultFlowRules = \"[]\"; ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ApolloDataSource<>( GatewayConstant.Apollo.NAMESPACE_RULE, GatewayConstant.Sentinel.FLOW_RULE_KEY, defaultFlowRules, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() { })); FlowRuleManager.register2Property(flowRuleDataSource.getProperty()); } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { try (Entry entry = SphU.entry(GatewayConstant.FlowRule.GATEWAY_LIMIT)) { String uid = null; if (exchange.getRequest().getMethod().equals(HttpMethod.POST)) { Map<String, String> bodyMap = (Map<String, String>)exchange.getAttributes().get(GatewayConstant.CACHE_POST_BODY); uid = bodyMap.get(\"uid\"); } else if(exchange.getRequest().getMethod().equals(HttpMethod.GET)) { uid = exchange.getRequest().getQueryParams().getFirst(\"uid\"); } String url = exchange.getRequest().getPath().toString(); System.out.println(\"url=\" + url + \",uid=\" + uid); //校验用户限流 if (StringUtils.isNotBlank(uid)) { try (Entry userEntry = SphU.entry(\"u_\" + uid)) { } catch (BlockException ex) { LOGGER.error(\"触发了用户限流规则\"); exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS); return exchange.getResponse().setComplete(); } } //校验url限流 try (Entry urlEntry = SphU.entry(url)) { } catch (BlockException ex) { LOGGER.error(\"触发了url限流规则\"); exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS); return exchange.getResponse().setComplete(); } } catch (BlockException ex) { // 资源访问阻止,被限流或被降级 // 在此处进行相应的处理操作 LOGGER.error(\"触发了限流规则\"); exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS); return exchange.getResponse().setComplete(); } } @Override public int getOrder() { return GatewayConstant.FLOW_FILTER; }} 注册中心自动路由##实现原理 Spring Cloud Gateway可以与注册中心进行整合,比如常见的zookeeper,Eureka,Nacos等。本文就以nacos举例,其他的注册中心整合类似。 首先,需要引入Nacos的依赖,它通过SpringBoot的autoConfiguration特性,自动集成到工程中。 1234<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency> 最关键的代码是NacosWatch,它实现了ApplicationEventPublisherAware 和SmartLifecycle,拥有发布事件和监听Spring生命周期的能力。通过scheduleWithFixedDelay实现默认30s轮询的发送HeartbeatEvent事件对象。 1234567891011121314151617181920212223242526272829303132 @Bean @ConditionalOnMissingBean @ConditionalOnProperty(value = \"spring.cloud.nacos.discovery.watch.enabled\", matchIfMissing = true) public NacosWatch nacosWatch(NacosDiscoveryProperties nacosDiscoveryProperties) { return new NacosWatch(nacosDiscoveryProperties);}public class NacosWatch implements ApplicationEventPublisherAware, SmartLifecycle { @Override public void stop(Runnable callback) { this.stop(); callback.run(); } @Override public void start() { if (this.running.compareAndSet(false, true)) { this.watchFuture = this.taskScheduler.scheduleWithFixedDelay( this::nacosServicesWatch, this.properties.getWatchDelay()); } } @Override public void stop() { if (this.running.compareAndSet(true, false) && this.watchFuture != null) { this.watchFuture.cancel(true); } } public void nacosServicesWatch() { // nacos doesn't support watch now , publish an event every 30 seconds. this.publisher.publishEvent( new HeartbeatEvent(this, nacosWatchIndex.getAndIncrement())); }} HeartbeatEvent事件发出之后,会被Spring Cloud Gateway的RouteRefreshListener监听到,最终发送一个RefreshRoutesEvent,这个事件会触发所有的路由刷新。总结为,注册中心默认30s同步一次Gateway路由,可以通过spring.cloud.nacos.watch-delay属性来配置轮询的时间间隔。HeartbeatMonitor这个类象征性的判定是否Heartbeat更新,只要value不为null且不和现有值相等,都会执行reset()。 12345678910111213141516171819202122232425262728293031323334public class RouteRefreshListener implements ApplicationListener<ApplicationEvent> { private HeartbeatMonitor monitor = new HeartbeatMonitor(); @Override public void onApplicationEvent(ApplicationEvent event) { ... if (event instanceof HeartbeatEvent) { HeartbeatEvent e = (HeartbeatEvent) event; resetIfNeeded(e.getValue()); } } private void resetIfNeeded(Object value) { if (this.monitor.update(value)) { reset(); } } private void reset() { this.publisher.publishEvent(new RefreshRoutesEvent(this)); }}public class HeartbeatMonitor { private AtomicReference<Object> latestHeartbeat = new AtomicReference<>(); /** * @param value The latest heartbeat. * @return True if the state changed. */ public boolean update(Object value) { Object last = this.latestHeartbeat.get(); if (value != null && !value.equals(last)) { return this.latestHeartbeat.compareAndSet(last, value); } return false; }} 总结 总的来说,nacos这种实现方式,简单有效,但是也不太友好。因为路由是低频变化的,默认的30s刷新时间,会导致清空缓存,所有的路由都需要重新加载(非注册中心的路由也会重新加载)会对性能有一定的损耗,如果能判定nacos路由是否有变化,变化再更新就会最大限度降低缓存重建的问题。","link":"/2019/10/27/spring/springcloud/gateway/SpringCloudGateway源码解析(6)-常用功能实现/"},{"title":"Java并发-锁的应用与原理,看这一篇就够了","text":"通过对锁原理的分析,重点分析ReentrantLock和ReentrantReadWriteLock的源码,通过锁的实现更深入的理解AQS。 提出问题在本文的开篇,先提出问题,我们所有的研究,都为了解决这些问题的。 ReentrantLock的公平模式和非公平模式有什么区别?分别是如何的? 什么是可重入?ReentrantLock是如何实现可重入的? ReentrantLock与Syncronized有何不同? Condition的作用是什么?它的实现原理是什么?Condition和Object.wait,Object.notify有何异同? AQS是如何实现自旋的?AQS中的线程会一直保持自旋吗? ReentrantReadWriteLock与ReentrantLock的区别是什么? ReentrantReadWriteLock是如何实现读不阻塞的? AQS是如何实现独占锁和共享锁的? 锁基础知识在分析源码之前,先讲解下锁相关的基础。 自旋锁&互斥锁通常情况下解决多线程共享资源逻辑一致性问题有两种方式: 互斥锁:当发现资源被占用的时候,会阻塞自己直到资源解除占用,然后再次尝试获取。互斥锁会带来线程上下文切换,信号发送等开销,理论上性能比自旋锁要慢。 自旋锁:当发现资源被占用的时候,一直尝试获取锁,故而称为“自旋”。自旋锁会死循环检测锁标志位,期间是占用CPU的,并且在竞争激烈的时候,标志位会频繁的变更,会造成高频率的缓存同步。所以它适合锁保持时间比较短的情况。 对于这两种方式没有优劣之分,只有是否适合当前的场景。 但是如果竞争非常激烈的时候,使用自旋锁就会产生一些额外的问题: 每一个线程都在疯狂的自旋,会造成大量的CPU浪费; 因为自旋锁会依赖一个共享的锁标识,所以竞争激烈的时候,锁标识的同步也需要消耗大量的资源; 如果要用自旋锁实现公平锁(即先到先获取),此时就还需要额外的变量,也会比较麻烦; 解决这些问题其中的一种办法就是使用队列锁,简单来讲就是让这些线程排队获取,下面章节会介绍CLH锁,它是队列锁的一种优秀实现。 CLH锁无论是简单的非公平自旋锁还是公平的基于排队的自旋锁,由于执行线程均在同一个共享变量上自旋,申请和释放锁的时候必须对该共享变量进行修改,这将导致所有参与排队自旋锁操作的处理器的缓存变得无效。如果排队自旋锁竞争比较激烈的话,频繁的缓存同步操作会导致繁重的系统总线和内存的流量,从而大大降低了系统整体的性能。 所以,需要有一种办法能够让执行线程不再在同一个共享变量上自旋,避免过高频率的缓存同步操作。于是MCS和CLH锁应运而生,由于MCS和CLH很类似,Java中主要采用了CLH的思想,所以CMS在此就不再研究。 CLH锁的名称都来源于发明人的名字首字母: Craig、Landin、Hagersten三个人发明了CLH锁。其核心思想是:通过一定手段将所有线程对某一共享变量的轮询竞争转化为一个线程队列,且队列中的线程各自轮询自己的本地变量。 公平模式和非公平模式上文提到的自旋锁和互斥锁,是天然的非公平锁。在竞争激烈的时候,可能导致一些线程始终无法获取锁(争抢的时候必然是当前活跃线程获得锁的几率大),也就是饥饿现象。为了解决饥饿现象所以需要一种公平的方式,让每一个线程都有机会获取到锁。 公平锁(FairSync)的定义是:所有竞争资源的线程遵循排队原则,逐一获得锁,通俗一点来讲,就是“先来后到”。非公平锁(NonfairSync)允许“插队行为“。 在现实生活中,大家都对插队行为嗤之以鼻,但是大家完全遵循排队原则,效率(在计算机世界里,效率就是用最少的资源最大化利用CPU的计算能力)就真的高嘛?举一个简单的例子,有10个线程用公平排队的方式等待资源,但是由于线程A有大量的I/O操作,一直占用锁不释放,CPU空置,后面的线程为了遵循公平原则只能干瞪眼,此刻CPU资源不能最大化的利用。 那如果执行较快的线程B在到的时候,资源恰好是无竞争状态,那它可以直接“插队“进来优先被执行,但是由于线程B执行的速度很快,所以线程A也没有蒙受多大的‘’损失‘’‘。所以非公平锁可以尽量利用系统的计算资源,省去了线程的唤醒,用户态到内核态切换等等消耗资源的操作,从大局上提高系统的效率。 但是,所有的事物都要辩证的去看。举一个例子,如果线程A是一个很重要的线程,但是由于锁竞争很激烈,线程A始终拿不到锁,那么就会导致一些关键性功能受到影响,这就是锁饥饿。所以非公平锁在锁竞争很激烈的情况下,会存在锁饥饿现象。 公平和非公平各有利弊,但是大多数的锁的实现,默认实现的是非公平锁,如果没有特殊的场景,非公平锁就能满足你的大多数需求。 可重入先说场景来理解可重入性。如下代码会最终打印100。调用test的时候,主线程获得了锁,但是在主线程还未释放锁的时候,由于递归调用又再次获取了锁。一个线程可以多次获取同一把锁的行为,叫做锁的可重入。如果不可重入,如下代码就会造成死锁。 ReentrantLock 和 synchronized都是可重入锁。 12345678910111213141516public class TestClass { public static void main(String[] args) { AtomicInteger i = new AtomicInteger(0); test(i); System.out.println(i); } public static synchronized void test(AtomicInteger i) { if (i.get() < 100) { i.incrementAndGet(); test(i); } }}##结果 100 乐观锁和悲观锁乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而说一种人好于另外一种人。 Java中synchronized和ReentrantLock就是悲观锁思想实现的,数据库中的表锁和行锁,也都是悲观锁的实现。 Java中java.util.concurrent.atomic包下面的各种原子类,就是使用了乐观锁的一种实现方式CAS实现。 从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种。乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。 乐观锁的实现,一般会通过版本号机制或者CAS算法实现。 版本号机制一般在数据中,增加一个version字段,表示字段被修改的次数,经典的实现有数据库的乐观锁设计,ElasticSearch的_version机制等。用如下伪代码的形式,解释下版本号的思想。 123456789101112131415var user = select user.age,user.id,user.version from t_user where id = 1001;user.age++;int oldVersion = user.version;user.version++;int result = update t_user set user.age=#{user.age},version=#{user.version} where id = 1001 and version=#{oldVersion};if(result == 1) { return 200;} else { while(true){ //在执行一次如上逻辑 if(result == 1) { break; } }} CAS机制具体的CAS问题可以参考此文,作者写的也是比较详细,ABA的例子也来自他的博文。 CAS即compare and swap(比较与交换),是一种有名的无锁算法。CAS算法涉及到三个操作数 需要读写的内存值V 进行比较的值A 拟写入的新值B 当且仅当V的值等于A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是原子操作),通过自旋不断地重试。 但是CAS机制会存在ABA问题,举一个生活中取钱的例子。 在多线程场景下CAS会出现ABA问题,关于ABA问题这里简单科普下,例如有2个线程同时对同一个值(初始值为A)进行CAS操作,这三个线程如下 线程1,期望值为A,欲更新的值为B 线程2,期望值为A,欲更新的值为B 线程1抢先获得CPU时间片,而线程2因为其他原因阻塞了,线程1取值与期望的A值比较,发现相等然后将值更新为B,然后这个时候出现了线程3,期望值为B,欲更新的值为A,线程3取值与期望的值B比较,发现相等则将值更新为A,此时线程2从阻塞中恢复,并且获得了CPU时间片,这时候线程2取值与期望的值A比较,发现相等则将值更新为B,虽然线程2也完成了操作,但是线程2并不知道值已经经过了A->B->A的变化过程。 ABA问题带来的危害: 小明在提款机,提取了50元,因为提款机问题,有两个线程,同时把余额从100变为50 线程1(提款机):获取当前值100,期望更新为50, 线程2(提款机):获取当前值100,期望更新为50, 线程1成功执行,线程2某种原因block了,这时,某人给小明汇款50 线程3(默认):获取当前值50,期望更新为100, 这时候线程3成功执行,余额变为100, 线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50!!! 此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)这就是ABA问题带来的成功提交。 解决方法: 在变量前面加上版本号,每次变量更新的时候变量的版本号都+1,即A->B->A就变成了1A->2B->3A。 synchronizedsynchronized是Java中的内置锁,具体的实现在JVM中。synchronized可以给修饰代码块,修饰方法等,由于它的用法过于基础,不在本文中赘述。 synchronized的特点是使用简单,不需要手动指定锁的获取和锁的释放。被大家所诟病的性能问题,在JDK1.6之后不复存在。在JDK1.6中,引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。 关于重量级锁、轻量级锁、偏向锁,可以看此文,作者写的很用心。 总之在使用synchronized的时候,偏向锁、轻量级锁、重量级锁,分别对应锁只被一个线程持有,不同线程持交替持有,多线程竞争锁三种情况。当条件不满足时,锁会按偏向锁->轻量级锁->重量级锁的顺序升级。JVM的锁也是能降级的,不过条件十分苛刻。 针对问题:ReentrantLock与Syncronized有何不同?回答是: 在JDK1.6之前,由于还没有引入偏向锁和轻量级锁,JVM对synchronized的实现比较重,导致它的性能存在一定的问题,ReentrantLock的性能要完胜synchronized的。但是随着JVM对synchronized的不断优化,在jdk1.6之后,两者的差距越来越小,synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量,性能差距可以忽略不计。所以在一般情况下,性能不是两者最大的差距。真正的差距是在于两者的使用上,synchronized是很死板的,只能对一个代码块或者一个方法,提供非公平独占锁的实现。跟synchronized相比,ReentrantLock是非常灵活的,既可以公平锁也可以非公平锁,可以newCondition(),来实现锁的阻塞队列,但是灵活性带来的就是使用的门槛较高,使用不规范就很容易造成死锁!不要小看死锁,解决死锁的方式是进程重启!但是在使用synchronized时,是不用考虑死锁问题的。 ReentrantLock常用方法 void lock() 获取锁 当锁没有被其他线程持有时,lock方法立即返回,设置锁计数器为1。 当前线程已经持有该锁时,锁持有计数器继续增加,lock方法立即返回 锁被其他线程持有时当前线程出于线程调度的目的而被禁用,并出于休眠状态,直到获取锁为止,此时锁持有的计数器置为1。 void lockInterruptibly() 获取锁除非线程被中断 当锁没有被其他线程持有时,lock方法立即返回,设置锁计数器为1。 当前线程已经持有该锁时,锁持有计数器继续增加,lock方法立即返回 锁被其他线程持有时当前线程出于线程调度的目的而被禁用,并出于休眠状态,直到如下两件事情任意一件发生为止;当前线程获取到锁或者其他线程中断了当前线程,方法会抛出InterruptedException boolean tryLock()仅当其他线程在此刻没有持有该锁时,才会获取该锁,立即返回并返回true,设置标志位为1。如果当前线程已经持有该锁,立即返回,标志位递增并返回true。如果该锁被其他线程持有,将立即返回false。 boolean tryLock(long timeout, TimeUnit unit)具体逻辑跟tryLock相同但是多了一个等待的时间,如果在等待期间,线程被中断,还会抛出InterruptedException void unlock()试图释放锁,当前线程拥有该锁,则计数器减1,直到计数器为0,则释放锁。如果当前线程不持有该锁,则抛出IllegalMonitorStateException异常 Condition newCondition()返回锁的Condition实例。Condition实例的作用与Object.wait()、Object.notify()、Object.notifyAll()相同。如果lock没有被任何线程持有,调用Condition.awart()或者Condition.signal,会抛出IllegalMonitorStateException。如果线程被中断,则waiting将会终止,并抛出InterruptedException,线程的中断标识位将被清除。 int getHoldCount(),锁CLH队列的数量 boolean isHeldByCurrentThread()锁是否被当前线程持有 boolean isLocked()锁是否被线程持有 boolean isFair()是否是公平锁 Thread getOwner()返回当前持有锁的线程,如果锁没有被任何线程持有则返回null 公平模式&非公平模式实现原理如下代码只保留了关键性代码。Sync通过继承AbstractQueuedSynchronizer来获得基本的同步控制能力,NofairSync和FairSync通过实现AQS模板方法,来指定获取锁的资格算法。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172abstract static class Sync extends AbstractQueuedSynchronizer { ... //如果线程可以获取锁,则返回true,否则立即返回false final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //如果状态为零,则设置ownerThread,返回true if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //如果状态不为零,判断当前线程是不是等于ownerThread,如果是则递增state,并立即返回true else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error(\"Maximum lock count exceeded\"); setState(nextc); return true; } //其他状态均返回false return false; } ...}static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; final void lock() { //比较如果status=0则status=1,并设置ownerThread,通过这种方式使nonfairTryAcquire方法返回true if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }} static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //公平锁的实现中,比非公平锁多了一句hasQueuedPredecessors(),只有没有后继者并且status=0才可以有获取锁的资格,是非常之公平 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error(\"Maximum lock count exceeded\"); setState(nextc); return true; } return false; }} 如何实现可重入的?在上一章节公平锁&非公平锁中,可以看到current == getExclusiveOwnerThread() ,status会加上acquires值,来做递增并返回true,所以同一个线程进来之后,不会加入到CLH同步队列中。 Lock和unlock原理分析在公平模式&非公平模式的章节中,我们会发现无论是公平锁还是非公平锁,他们最终都会调用acquire方法进入到同步队列中。acquire方法包括如下流程 tryAcquire尝试获取锁,如果能获取锁,则直接返回。tryAcquire是一个模板方法,由实现AQS的类实现。在ReentrantLock中,分别有公平锁和非公平锁两种实现,具体实现可以参考对应章节。 如果发现不能获取到锁,则调用addWaiter方法,把currentThread包装成Node,并加入到同步队列中。这里有一个细节addWaiter(Node.EXCLUSIVE),ReentrantLock是独占锁的思想,所以需要指定模式为独占模式。 执行acquireQueued方法,采用自旋+阻塞的方式,控制同步队列。只有在waitStatus=SIGNAL状态下,才会阻塞线程,每次自旋都会检测当前节点node的前置节点,是否为head节点,如果是head节点并且能获取到锁,则跳出自旋。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();}private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node;}//独占模式的acquire方法final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; //自旋+park for (;;) { final Node p = node.predecessor(); //如果前置节点时head,注意此处还会再尝试一下能否获得到锁 if (p == head && tryAcquire(arg)) { //这直接把node设置为head setHead(node); p.next = null; // help GC failed = false; return interrupted; } //如果前置节点不是head或者tryAcquuire返回false,则需要park节点 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }}//pred为前置节点,node为当前节点//当获取锁失败时,检查和更新node.waitStatus,如果线程应该被阻塞,则返回true.//这个方法是自旋锁中最重要的控制手段private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //获取到前置节点的waitStatus int ws = pred.waitStatus; //如果前置节点的状态为SIGNAL,则返回true,意味着当前节点node应该被park if (ws == Node.SIGNAL) return true; //如果前置节点被CANCEL的,则移除 if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //原子的设置前置节点为SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false;}//阻塞线程并返回线程是否中断private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted();}protected final boolean tryRelease(int releases) { //重置标识位为0,这个很关键 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free;}//释放锁public final boolean release(int arg) { //判定currentThread是否等于ownerThread,如果不相等则抛出IllegalMonitorStateException //然后,判断status属性是否等于0,如果等于0则返回true,同时清空ownerThread。代表资源已经被释放掉 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) //unpark线程 unparkSuccessor(h); return true; } return false;} 图解ReentrantLock锁竞争原理可能看完源码还是一头雾水,我本人是看了很多次,才明白了AQS具体的执行流程。整理了一个流程图,希望大家仔细的按照图的流程梳理一遍整个流程,才能真正的理解AQS。 如图所示: 定义ReentrantLock实例,此刻head=null,tail=null,status=0 线程A调用lock.lock(),由于线程A是最先获取锁的,status=1,根据acquire方法的逻辑,不设置任何Node到同步队列,所以head和tail依旧为null。 线程B调用lock.lock(),由于此刻status=1,所以tryAcquire返回false。触发了addWariter逻辑,增加了Node和NodeB两个节点,head和tail指针也指向对应的链表头和链表尾。acquireQueued开始自旋操作,第一次自旋由于Node.waitStatus != SIGNAL,所以设置前置节点waitStatus=SIGNAL后,继续自旋,再次判定shouldParkAfterFailedAcquire方法,发现Node.waitStatus == SIGNAL,这时会阻塞ThreadB。 线程C调用lock.lock(),执行逻辑同上。最终会形成NodeNodeBNodeC这种的CLH同步队列。 线程A业务逻辑执行完毕,调用lock.unlock(),会最终调用release方法,把status=0,并唤醒head的后继节点NodeB,NodeB唤醒之后继续自旋,最终NodeB成为head节点,跳出自旋,执行ThreadB的逻辑。 线程B执行完毕,调用lock.lock(),最终会唤醒NodeC,最终全部线程执行完毕。 Condition原理分析通过如下代码给当前的Lock对象创建一个Condition对象。它的作用等同于Object.wait()和Object.notify(),条件队列是一个FIFO队列,可以通过signalAll解锁全部的线程,也可以通过signal单独解锁线程。 12ReentrantLock lock = new ReentrantLock();Condition condition = lock.newCondition(); 源码分析首先是await的逻辑: 判断中断则抛出InterruptedException 在FIFO条件队列(condition queue)增加节点,节点状态为CONDITION,在增加节点的同时清除掉状态为CANCELED的Node 把该节点从同步队列(sync queue)中去除 判定节点是否在同步队列中,只有不在同步队列,才阻塞线程 阻塞node 当node被signal之后,把node加入到同步队列中,准备获取锁 移除掉所有状态为CANCELED的节点 报告中断,如果在signal之前线程被中断则抛出中断异常,如果在signal之后线程被中断,则再次中断,有调用代码自行处理中断标识 然后是signal的逻辑: 移除掉firstWaiter,如果first.nextWaiter == null 则置空lastWaiter 转换node,从条件队列 -> 同步队列 node状态必须满足CONDITION unpark线程 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138//等待Conditonpublic final void await() throws InterruptedException { //第一步,判断中断则抛出InterruptedException if (Thread.interrupted()) throw new InterruptedException(); //第二步,在FIFO条件队列(condition queue)增加节点,节点状态为CONDITION,在增加节点的同时清除掉状态为CANCELED的Node Node node = addConditionWaiter(); //第三步,把该节点从同步队列(sync queue)中去除 int savedState = fullyRelease(node); int interruptMode = 0; //第四步,判定节点是否在同步队列中,只有不在同步队列,才阻塞线程 while (!isOnSyncQueue(node)) { //阻塞node LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } //第五步,当node被signal之后,把node加入到同步队列中,准备获取锁 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; //第六步,移除掉所有状态为CANCELED的节点 if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); //第七步,报告中断,如果在signal之前线程被中断则抛出中断异常,如果在signal之后线程被中断,则再次中断,有调用代码自行处理中断标识 if (interruptMode != 0) reportInterruptAfterWait(interruptMode);}//新的waiter节点加入条件队列private Node addConditionWaiter() { Node t = lastWaiter; // If lastWaiter is cancelled, clean out. if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node;}//轮询整个条件队列(condition queue),去掉节点状态不是CONDITION的节点private void unlinkCancelledWaiters() { Node t = firstWaiter; Node trail = null; while (t != null) { Node next = t.nextWaiter; //如果t的状态不是CONDITION,则把t节点从链表中摘除 if (t.waitStatus != Node.CONDITION) { t.nextWaiter = null; if (trail == null) firstWaiter = next; else trail.nextWaiter = next; if (next == null) lastWaiter = trail; } else trail = t; t = next; }}//从同步队列中移除nodefinal int fullyRelease(Node node) { boolean failed = true; try { int savedState = getState(); if (release(savedState)) { failed = false; return savedState; } else { throw new IllegalMonitorStateException(); } } finally { if (failed) node.waitStatus = Node.CANCELLED; }}//判定node是否在同步队列中,条件队列的next和prev都一定是null,条件队列是nextWaiter的单向链表final boolean isOnSyncQueue(Node node) { if (node.waitStatus == Node.CONDITION || node.prev == null) return false; //如果node.next != null,说明存在后继节点,说明它一定在同步队列中 if (node.next != null) // If has successor, it must be on queue return true; //从tail轮询同步队列,判定node是否在同步队列中 return findNodeFromTail(node);}//检测中断,返回THROW_IE如果中断发生在signalled之前,返回REINTERRUPT如果中断发生在中断之后,返回0表示没有发生中断private int checkInterruptWhileWaiting(Node node) { return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;}//wait结束后报告中断private void reportInterruptAfterWait(int interruptMode) throws InterruptedException { if (interruptMode == THROW_IE) throw new InterruptedException(); else if (interruptMode == REINTERRUPT) selfInterrupt();}//释放Conditionpublic final void signal() { //模板方法,由实现AQS的实现类实现,在ReentrantLock中,判定current线程是否等于ownerThread if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first);}private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null);}//转换node从 条件队列-> 同步队列final boolean transferForSignal(Node node) { if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; Node p = enq(node); int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) //解除节点阻塞 LockSupport.unpark(node.thread); return true;} Condition的中断处理在使用Condition的时候,一定要注意对中断的处理。Condition.await()跟Object.wait()相比,后者只支持中断抛出InterruptedException异常,而前者即抛出InterruptedException异常,也会再次重置中断标识位,Condition处理中断的逻辑是如果在signal之前线程被中断则抛出中断异常,如果在signal之后线程被中断,则再次中断,有调用代码自行处理中断标识。如下示例代码所示。 定义三个线程t1,t2,t3,在t2执行signal()之前,t3线程执行了t1.interrupt(),则t1线程中捕获中断异常。如果不执行t3线程并且在t2执行signal()之后再调用t1.interrupt(),则t1线程不会捕获中断异常,但是Thread.currentThread().isInterrupted() == true。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152/** 在退出wait之后再次中断 */private static final int REINTERRUPT = 1;/** 在退出wait之后抛出InterruptedException异常 */private static final int THROW_IE = -1;public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); Thread t1 = new Thread(() -> { try { lock.lock(); condition.await(); System.out.println(\"aaa=\" + Thread.currentThread().isInterrupted()); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }); Thread t2 = new Thread(() -> { try { Thread.sleep(2000); lock.lock(); condition.signal(); //t1.interrupt(); } catch (InterruptedException e) { e.printStackTrace(); } catch (IllegalMonitorStateException e) { e.printStackTrace(); } finally { lock.unlock(); } }); Thread t3 = new Thread(() -> { try { Thread.sleep(1000); t1.interrupt(); } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); t2.start(); t3.start();} 总结由于ReentrantLock是AQS的最基础实现,理解ReentrantLock十分重要。通过对ReentrantLock的源码分析,可以总结为如下结论: ReentrantLock有公平和非公平两种模式 ReentrantLock是可重入锁。 ReentrantLock是一种独占锁,核心逻辑由AQS框架来提供,简单的概括为自旋+同步队列来实现独占锁 ReentrantLock包括同步队列(sync queue)和条件队列(condition queue)两种队列 AQS的锁标识状态为status字段,同步队列节点为内部类Node,Node的状态通过waitStatus控制,共有SIGNAL,CANCELED,CONDITION,PROPAGATE四种状态,nextWaiter是条件队列的单向链表。 Condition是通过阻塞线程来实现,当条件队列全部执行完毕之后,节点才会被加入到同步队列。 AQS的自旋次数有限,当上一个节点的waitStatus=-1就会阻塞当前节点。且每次只唤醒head节点的下一个节点参与竞争锁,避免同时唤醒多个线程造成上下文切换过于频繁。 ReentrantLock一定要记得unlock,否则会发生死锁。 ReentrantReadWriteLock通过学习ReentrantLock,我们掌握了ReentrantLock和AQS的关系,知道了是如何通过AQS来实现一个独占锁。但是很多情况下,独占锁也是一种很重的操作,比如缓存这种读多写少的情况,多并发读取缓存值是很频繁的事情,如果采用独占锁,会大大降低缓存的吞吐。而读操作本身就不存在共享变量同步,如果资源能够同时被多个读线程访问而且能保证同步,则缓存的吞吐能力将大大提升。 那么ReentrantReadWriteLock的出现,就是要解决以上问题的利器。读写锁的定义为:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。 ReentrantReadWriteLock总览ReadLock 和 WriteLock 中的方法都是通过 Sync 这个类来实现的。Sync 是 AQS 的子类,然后再派生了公平模式和不公平模式。 从它们调用的 Sync 方法,我们可以看到: ReadLock 使用了共享模式,WriteLock 使用了独占模式。 同一个 AQS 实例怎么可以同时使用共享模式和独占模式??? AQS 的精髓在于内部的属性 state:对于独占模式来说,通常就是 0 代表可获取锁,1 代表锁被别人获取了,重入例外而共享模式下,每个线程都可以对 state 进行加减操作也就是说,独占模式和共享模式对于 state 的操作完全不一样,那读写锁 ReentrantReadWriteLock 中是怎么使用 state 的呢?答案是将 state 这个 32 位的 int 值分为高 16 位和低 16位,分别用于共享模式和独占模式。 1234567891011121314151617181920212223242526272829303132333435363738public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { /** 内部类 读锁 */ private final ReentrantReadWriteLock.ReadLock readerLock; /** 内部类 写锁 */ private final ReentrantReadWriteLock.WriteLock writerLock; /** AQS 读写锁实现类 */ final Sync sync; /** * 默认创建非公平锁 */ public ReentrantReadWriteLock() { this(false); } /** * 1、构建同步器:公平的&非公平的 * 2、构建读锁和死锁实例 */ public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; } public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; } abstract static class Sync extends AbstractQueuedSynchronizer {} static final class NonfairSync extends Sync{} static final class FairSync extends Sync{} public static class ReadLock implements Lock, java.io.Serializable{} public static class WriteLock implements Lock, java.io.Serializable{}} 源码分析读锁获取 在 AQS 中,如果 tryAcquireShared(arg) 方法返回值小于 0 代表没有获取到共享锁(读锁),大于 0 代表获取到。 AQS共享模式:tryAcquireShared 方法不仅仅在 acquireShared 的最开始被使用,这里是 try,也就可能会失败,如果失败的话,执行后面的 doAcquireShared,进入到阻塞队列,然后等待前驱节点唤醒。唤醒以后,还是会调用 tryAcquireShared 进行获取共享锁的。当然,唤醒以后再 try 是很容易获得锁的,因为其他节点都还出于阻塞状态,此刻参与竞争的,只有被唤醒的节点或者其他新的线程。 所以,你在看下面这段代码的时候,要想象到两种获取读锁的场景,一种是新来的,一种是排队排到它的。 123456789101112131415161718192021222324252627282930313233343536373839404142434445public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg);}//返回-1代表不能获取读锁//返回1代表可以获取读锁protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); //如果独占锁的数量不等于零,说明有写锁,而且当前线程不等于ownerThread,说明不是重入线程,所以返回-1 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; //获得共享锁数量 int r = sharedCount(c); //如果读锁不应该阻塞且CAS更新成功 //readerShouldBlock在公平模式和非公平模式有不同的实现 //在公平模式,只要同步队列中有节点在排队,则新的节点就乖乖的去排队 //在非公平模式,只有同步队列的head是写锁,才会去排队,否则不需要阻塞 //这段代码中对firstReader、firstReaderHoldCount、readHolds等设定,都是出于性能考量,可以直接返回1 if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { //首次获取锁 if (r == 0) { //出于性能考虑,可以 firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { //出于性能考虑 HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } return fullTryAcquireShared(current);} 回顾下这段代码,这个判定条件是重点if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARD_UNIT),共有三个判定条件: readerShouldBlock() 在FairSync中,如果同步队列中有其他元素在等待锁,你就需要乖乖的去排队。 1234567891011final boolean readerShouldBlock() { return hasQueuedPredecessors();}public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());} 在NonFairSync中,如果head的后继节点是写锁线程节点,则阻塞读锁的获取,先让写锁执行。如果head.next不是写锁,非公平模式下是允许竞争的。 1234567891011final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive();}final boolean apparentlyFirstQueuedIsExclusive() { Node h, s; return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null;} r < MAX_COUNT,static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;,MAX_COUNT一般情况下是不会达到的,最大值是65535。这个条件可以忽略不计。 compareAndSetState(c, c + SHARD_UNIT),CAS竞争失败,可以另一个读锁竞争,也可能是写锁操作竞争。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758/*** 这个方法是为了解决:* 1、CAS竞争失败,因为经过readerShouldBlock方法的验证,线程不需要阻塞,如果仅仅是因为CAS竞争失败而导致进 * 入同步队列,就太可惜了。* 2、处理重入**/final int fullTryAcquireShared(Thread current) { HoldCounter rh = null; //自旋 for (;;) { //获取状态 int c = getState(); //获取写锁数量,如果此刻有线程获取到了写锁,则宣告读线程进入队列排队 if (exclusiveCount(c) != 0) { if (getExclusiveOwnerThread() != current) return -1; //再次判定reader是否需要进入队列 } else if (readerShouldBlock()) { if (firstReader == current) { // assert firstReaderHoldCount > 0; } else { //为了确保读锁重入操作能成功,而不是被塞到同步队列中等待 if (rh == null) { rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) { rh = readHolds.get(); if (rh.count == 0) readHolds.remove(); } } if (rh.count == 0) return -1; } } //判定共享锁的数量,最大为65535 if (sharedCount(c) == MAX_COUNT) throw new Error(\"Maximum lock count exceeded\"); //再次参加CAS竞争,如果还没有竞争到则继续自旋 if (compareAndSetState(c, c + SHARED_UNIT)) { if (sharedCount(c) == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { if (rh == null) rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; cachedHoldCounter = rh; // cache for release } return 1; } }} 读锁释放读锁释放的过程还是比较简单的,主要就是将 hold count 减 1,如果减到 0 的话,还要将 ThreadLocal 中的 remove 掉。 然后是在 for 循环中将 state 的高 16 位减 1,如果发现读锁和写锁都释放光了,那么唤醒后继的获取写锁的线程。 123456789101112131415161718192021222324252627282930313233343536//共享模式releasepublic final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false;}protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); if (firstReader == current) { // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count <= 1) { readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } --rh.count; } for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) return nextc == 0; }} 写锁获取先说重点 1、写锁是独占锁 2、如果读锁被占用,写锁获取时是要进入阻塞队列中等待的 1234567891011121314151617181920212223242526272829303132333435363738protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); //获取status值和写锁数量 int c = getState(); int w = exclusiveCount(c); if (c != 0) { //如果存在读锁,则写锁进入队列阻塞 // (Note: if c != 0 and w == 0 then shared count != 0) if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error(\"Maximum lock count exceeded\"); // Reentrant acquire setState(c + acquires); return true; } //判定写锁是否应该阻塞,或者CAS静态失败,都会导致写锁阻塞 if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; //设置独占锁ownerThread setExclusiveOwnerThread(current); return true;}static final class NonfairSync extends Sync { //非公平模式,多个写锁之间就靠CAS去抢锁了,抢不到进入队列中排队 final boolean writerShouldBlock() { return false; }}static final class FairSync extends Sync { //公平模式如果队列存在节点则进入队列排队 final boolean writerShouldBlock() { return hasQueuedPredecessors(); }} 写锁释放写锁释放就很简单了,独占锁的释放是线程安全的,不需要CAS,就是把state - 1。 12345678910protected final boolean tryRelease(int releases) { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); int nextc = getState() - releases; boolean free = exclusiveCount(nextc) == 0; if (free) setExclusiveOwnerThread(null); setState(nextc); return free;} 锁降级Doug Lea 没有说写锁更高级,如果有线程持有读锁,那么写锁获取也需要等待。 不过从源码中也可以看出,确实会给写锁一些特殊照顾。如非公平模式下,为了提高吞吐量,write.lock的时候不需要管同步队列中是否存在等待节点,直接CAS去竞争。read.lock的时候,如果发现 head.next 是获取写锁的线程,就会建议阻塞读锁。 Doug Lea 将持有写锁的线程,去获取读锁的过程称为锁降级(Lock downgrading)。这样,此线程就既持有写锁又持有读锁。 但是,锁升级是不可以的。线程持有读锁的话,在没释放的情况下不能去获取写锁,因为会发生死锁。 注意:读写锁经常在这种地方造成死锁,一定要真正的理解这些思想,才能在使用的时候游刃有余 回去看下写锁获取的源码: 12345678910111213141516171819202122232425262728293031323334//锁不能升级的实现protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); if (c != 0) { // 看下这里返回 false 的情况: // c != 0 && w == 0: 写锁可用,但是有线程持有读锁(也可能是自己持有) // c != 0 && w !=0 && current != getExclusiveOwnerThread(): 其他线程持有写锁 // 也就是说,只要有读锁或写锁被占用,这次就不能获取到写锁 if (w == 0 || current != getExclusiveOwnerThread()) return false; ... } ...}//锁可以降级的实现protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); //重点!看这里,如果写锁数量不为0,代表有线程持有写锁。 //但是!如果current == ownerThread ,则跳出这个判定,有机会获取读锁,这就是锁降级。 //如果current != ownerThread,则读锁线程进入队列阻塞 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; int r = sharedCount(c); if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { ... } ...} 如果线程 a 先获取了读锁,然后获取写锁,那么线程 a 就到阻塞队列休眠了,自己把自己弄休眠了,而且可能之后就没人去唤醒它,造成死锁。 如果线程 a 先获取了写锁,然后获取读锁,那么线程 a 就不会被阻塞,继续有机会获取读锁。 死锁代码读写锁虽然适合读多写少的场景,提高了吞吐能力。但是其使用难度比ReentrantLock更高,必须要细心谨慎的操作锁,一旦出现死锁情况,只能靠重启进程来解决。设想一下刚重启完进程2分钟又进入死锁,线程池被占满又要重启的恐怖场景!所以,使用锁是一把双刃剑,要仔细检查,反复测试。 以下列出一些可能造成死锁的代码: Case1: 锁升级造成死锁。一个线程要获取读锁之后,想要获取写锁前一定要先unlock读锁。 123456789101112131415public static void main(String[] args) { try { System.out.println(\"获取读锁\"); lock.readLock().lock(); System.out.println(\"获取写锁\"); lock.writeLock().lock(); lock.writeLock().unlock(); System.out.println(\"释放写锁\"); lock.readLock().unlock(); } catch (Exception e) { e.printStackTrace(); } finally { }} Case2: lock两次, unlock一次,由于粗心大意。一定要多次检查lock和unlock是不是成对出现。 1234567891011121314151617181920212223242526public static void main(String[] args) { try { Random random = new Random(); lock.writeLock().lock(); lock.writeLock().lock(); System.out.println(\"获取写锁\"); lock.readLock().lock(); System.out.println(\"获取读锁\"); lock.readLock().unlock(); System.out.println(\"释放读锁\"); lock.writeLock().unlock(); new Thread(() -> { lock.readLock().lock(); System.out.println(\"另外线程获取读锁\"); lock.readLock().unlock(); }).start(); System.out.println(\"释放写锁\" + lock.writeLock().isHeldByCurrentThread()); } catch (Exception e) { e.printStackTrace(); } finally { }} 总结1、读写锁分为了读锁和写锁,可以多个线程共享读锁,但是只有一个线程能获取写锁。在获取写锁之后,读锁都会在同步队列中等待写锁释放。这样做最大化的提高了吞吐性能的同时,保证了数据一致性。 2、持有写锁的线程获取读锁的过程叫做锁降级,持有读锁的线程获取写锁的过程叫做锁升级。读写锁只能锁降级,锁升级会造成死锁。 3、读锁通过AQS共享模式实现,写锁通过AQS独占模式实现。AQS的status, 分为高 16 位和低 16位,分别用于共享模式和独占模式。 4、读锁和写锁最大是65535,超过这个值会抛出Error。当然一般是不会超过的。 AbstractQueuedSynchronizer相信大家在仔细研读可重入锁和读写锁的实现之后,对AQS已经不再陌生了。 AQS作为JUC中最基础的框架,光说它就可以写一篇论文了。本文不打算AQS长篇阔论,而是把上文中讲述的AQS知识再总结提炼一下。能看懂ReentrantLock和ReentrantReadWriteLock,就对AQS的精髓有一定掌握了。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970/*** * AQS是Node节点构成的同步队列 * +------+ prev +-----+ +-----+ * head | | <---- | | <---- | | tail * +------+ +-----+ +-----+ * */public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { /** 标识为共享模式 */ static final Node SHARED = new Node(); /** 表示为独占模式 */ static final Node EXCLUSIVE = null; private transient volatile Node head; private transient volatile Node tail; /** * 同步位,可重入,state标识重入次数。为零时代表资源可以被获取 * 读写锁时,把32位的int类型拆分出高16位和低16位,来区分读锁和写锁的状态 */ private volatile int state; static final class Node { /** 标识为共享模式,赋值给nextWaiter */ static final Node SHARED = new Node(); /** 标识为独占模式,赋值给nextWaiter */ static final Node EXCLUSIVE = null; /** waitStatus状态>0是取消状态,是不可逆的状态 */ static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; /** * Node状态字段,包括如下值: * SIGNAL: 该节点的后继节点是(或即将是)阻塞状态(通过LockSupport.park方法), * 当前节点释放锁或者被取消时,需要调用unpark函数来激活它的后继节点。 为了避免锁竞争,acquire方法必须明确指出一个信号。 * CANCELLED: 节点因为中断或者超时变更为取消状态. * canceled是最终态,不可逆变为其他状态. 被取消的节点不会继续阻塞. * CONDITION: 节点当前在条件队列中(condition queue).这个节点不会被当做同步队列节点 直到它从条件队列中释放,这时waitStatus的值被设置为0. * It will not be used as a sync queue node * until transferred, at which time the status * will be set to 0. * PROPAGATE: releaseShared应该传播到其他节点。 在doReleaseShared中对此进行了设置(仅适用于头节点), 以确保传播继续进行,即使此后进行了其他操作也是如此。 * 0: 除以上四种情况外 */ volatile int waitStatus; volatile Node prev; volatile Node next; /** * 等待锁的线程 */ volatile Thread thread; /** * condition的阻塞单向链表,阻塞队列只有在独占模式下才有(AQS还有共享模式) */ Node nextWaiter; }} 总结JUC的出现,大大降低了并发编程的难度,但是如果只是停留在会用的层面是不够的。希望本文能够给读者带来启发,也希望大家能够踊跃帮我挑出文章中不对的地方,大家一起进步。最后,写本篇的时候,也阅读了大量的其他博主写的文章,有些博主写的很好,我就直接摘抄了过来,方便自己日后review。感谢他们! 引用CLH,MCS队列锁简介 UMA架构与NUMA架构下的自旋锁(CLH锁与MCS锁) J.U.C同步框架 The java.util.concurrent Synchronizer Framewor论文 Doug Lea’s Home Page Thread.sleep、Object.wait、LockSupport.park 区别 MCS锁的原理和实现 面试必备之乐观锁与悲观锁 你真的了解ReentrantReadWriteLock嘛 Java读写锁ReentrantReadWriteLock源码分析","link":"/2019/12/12/java/Java并发-锁的应用与原理,看这一篇就够了/"},{"title":"Java并发-ThreadPoolExecutor深度解析","text":"ThreadPoolExecutor搞不懂?看这篇就够了 前言本文部分内容摘抄自throwable作者的博文,有些部分作者已经总结的很好,没必要再重复发明轮子,在这里感谢作者。本文的亮点在于: 对ThreadPoolExecutor的依赖关系,作了细致的分解以宏观层面了解Doug Lea的设计理念。 总结了ThreadPoolExecutor的核心流程,归纳了重要的结论。 ThreadPoolExecutor源码逐行解析。 例举了场景,加深理解线程池工作原理。 由于ThreadPoolExecutor的代码逻辑和设计理念比较复杂,建议大家学习的时候,多思考,顺着代码的思路,多动动笔画一画才能加深理解。 提出问题这些问题也是我当时的疑惑点,按本文的思路学习ThreadPoolExecutor,这些问题迎刃而解。 1、创建一个线程池需要哪些参数? 2、线程池中的状态都有哪些?是如何实现的? 3、线程池为什么需要工作队列?有哪几种工作队列? 4、AQS在线程池中扮演什么角色?它的作用是什么? 5、线程池在shutdown的时候,如何保证任务不丢失?如何保证任务正常执行完毕? 、线程池在shutdown之后还能提交任务进来嘛?线程池在shutdownNow之后还能提交任务进来吗? 9、ThreadPoolExecutor是如何控制工作线程能够重复利用的。 10、ThreadPoolExecutor的执行结果是如何到Futrue中的? 前置知识复习线程状态如下图所示,Java中线程可分为NEW,RUNABLE,RUNING,BLOCKED,WAITING,TIMED_WAITING,TERMINATED 共七个状态,一个状态是如何过渡到另一个状态图中标识的很清楚。 初始状态(NEW)实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。 就绪状态(RUNNABLE)就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。 调用线程的start()方法,此线程进入就绪状态。 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。 锁池里的线程拿到对象锁后,进入就绪状态。 运行中状态(RUNNING)线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。 阻塞状态(BLOCKED)阻塞状态是线程阻塞在进入synchronized关键字(当然也包括ReentrantLock)修饰的方法或代码块(获取锁)时的状态。 等待(WAITING)处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。 超时等待(TIMED_WAITING)处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。 终止状态(TERMINATED)当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。 线程池的作用1、减少了创建和销毁线程的次数,可以重复利用工作线程 2、规范化线程的使用,避免创建过多的线程导致耗尽系统资源 3、丰富的线程管理手段和结果控制 ThreadPoolExecutor的原理由ThreadPool原理图所示,ThreadPoolExecutor的核心组件包括: ThreadFactory:线程工厂,用于创建工作线程,默认的线程工厂是Executors.defaultThreadFactory() workQueue:阻塞工作队列,常用的有ArrayBlockingQueue,LinkedBlockingQueue 工作线程池:内部包含了cw(core worker)和aw(additional worker) RejectedExecutionHandler:拒绝执行处理器 类的设计 ThreadPoolExecutor的类图。 ExecutorExecutor该接口提供了一种将任务提交与每个任务将如何运行的机制,包括线程执行,调度等。Doug Lea将线程的执行抽象为任务(task),任务由各种实现Executor的执行器执行,具体执行的内容在执行器中回调。 12345678910111213public interface Executor { /** * 在不远的将来执行用户提供的command。 * command或许在一个新的线程中执行,或者在线程池中执行,或许在调用的线程中。 * 具体的执行方式在Executor的实现类实现 * * @param command runnable任务 * @throws RejectedExecutionException 如果command不能被Execute接受 * @throws NullPointerException comman是null */ void execute(Runnable command);} ExecutorServiceExecutorService实现了Executor接口,它提供了一系列方法管理终止(manage termination)和用于追踪一个或多个异步任务最终返回Future的方法。ExecutorService可以关闭(shutdown),关闭之后会拒绝新的任务。定义了两种关闭ExecutorService的方法。 shutdown()启动一个有序关闭,在该关闭中先执行已提交的任务,单不接受任何新的任务。如果执行器已经关闭,再次调用该方法也不会产生影响。该方法不负责等待已提交任务完成执行,awaitTermination来完成此项职责,在执行器发起shutdown request之后,它将阻塞直到所有任务已完成,或者触发了timeout,或者线程被中断(interrupted),无论这三种情况中的哪一种先触发,都会解除阻塞。 shutdownNow()尝试停止所有正在执行的任务,忽略正在等待的任务,并返回正在等待执行的任务的列表。该方法不负责等待已提交任务完成执行,awaitTermination来完成此项职责。除了尽最大努力尝试停止处理正在执行的任务之外,没有任何保证。比如,终止线程的方法是通过Thread.interupt()方法实现,但是如果任务中没有处理过中断,则shutdownNow()也无计可施,只能等待任务自然执行完毕。 终止(termination)时,执行器中没有任务在执行,没有任务在等待执行,更没有任务可以提交进执行器。终止后应关闭执行器,释放资源。 12345678910111213141516171819202122232425262728293031323334public interface ExecutorService extends Executor { void shutdown(); List<Runnable> shutdownNow(); //执行器是否关闭 boolean isShutdown(); //在调用shutdown之后,所有任务都已经完成,则返回true. //如果不提前调用shutdown或者shutdownNow,该方法不可能返回true boolean isTerminated(); boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; //提交一个Callable任务,有返回值并返回一个Future对象代表任务执行的结果。Future的get方法将等待任务执行完毕。 <T> Future<T> submit(Callable<T> task); //提交一个Runnable任务,并返回Futrue对象 <T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task); <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException; <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException; <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException; <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;} FutrueFutrue代表了异步执行的结果。提供了检测执行完成的方法,等待完成的方法,重试完成方法等控制结果的方法。 123456789101112131415161718192021222324252627282930313233343536373839public interface Future<V> { /** * 尝试取消执行这个任务.任务完成或者任务取消或者一些其他的原因会导致尝试失败。 * 如果任务还未开始执行,取消成功,那么这个任务将永远不会执行。 * 如果任务已经开始执行,取消成功,则mayInterruptIfRunning参数来决定是不是中断线程 */ boolean cancel(boolean mayInterruptIfRunning); /** * 如果任务在执行完成前被取消,则返回true */ boolean isCancelled(); /** * 如果任务正常执行完毕则返回true */ boolean isDone(); /** * 等待执行结果 * * @return the computed result * @throws CancellationException 如果任务被取消 * @throws ExecutionException 执行过程中出现异常 * @throws InterruptedException 如果任务被中断 */ V get() throws InterruptedException, ExecutionException; /** * 在有限时间内等待执行结果 * * @throws CancellationException 如果任务被取消 * @throws ExecutionException 执行过程中出现异常 * @throws InterruptedException 如果任务被中断 * @throws TimeoutException 如果任务超时 */ V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;} 一般情况下常用的实现是FutureTask,它是一个可取消的异步执行结果集,它实现了Futrue接口中定义的功能。 12345678910111213141516171819202122232425262728public class FutureTask<V> implements RunnableFuture<V> { /** * 任务的运行状态,初始状态为NEW. * * 可能的状态变换为: * NEW -> COMPLETING -> NORMAL * NEW -> COMPLETING -> EXCEPTIONAL * NEW -> CANCELLED * NEW -> INTERRUPTING -> INTERRUPTED */ private volatile int state; private static final int NEW = 0; private static final int COMPLETING = 1; private static final int NORMAL = 2; private static final int EXCEPTIONAL = 3; private static final int CANCELLED = 4; private static final int INTERRUPTING = 5; private static final int INTERRUPTED = 6; /** The underlying callable; nulled out after running */ private Callable<V> callable; /** 任务执行结果,get()方法获取的就是这个对象 */ private Object outcome; // non-volatile, protected by state reads/writes /** The thread running the callable; CASed during run() */ private volatile Thread runner; /** Treiber stack of waiting threads */ private volatile WaitNode waiters; } AbstractExecutorServiceAbstractExecutorService提供了ExecutorService的默认实现,实现了submit,invokeAny,invokeAll,通过FutureTask类对Runnable执行过程进行包装。 ThreadPoolExecutorThreadPoolExecutor是线程池实现类,它依赖了上面提到的所有类,也是本篇博文重点讲述的类。 总结通过对ThreadPoolExecutor依赖的类的了解,更能从宏观上理解作者的设计思想。Executor将并发执行的线程抽象成任务,交给任务执行器来执行,ExecutorService定义了任务执行器的关闭和提交任务。 线程池状态如下代码所示,线程池的状态分为了 RUNNING:接受新的任务和处理队列中的任务 SHUTDOWN:拒绝新的任务,但是处理队列中的任务 STOP:拒绝新的任务,不处理队列中的任务,并且中断在执行的任务 TIDYING:所有任务已经终止(terminated),workerCount=0,线程 TERMINATED:terminated已完成 线程池的状态按如下方式进行转换 RUNNING->SHUTDOWN 调用shutdown()方法 (RUNNING or SHUTDOWN) -> STOP,调用shutdownNow() SHUTDOWN -> TIDYING,当队列和线程池都为空 STOP->TIDYING,线程池为空 TIDYING -> TERMINATED,当terminated() hook method 执行完毕,所有线程都在awaitTermination()中等待线程池状态到达TERMINATED。 12345678910//接受新的任务和处理队列中的任务private static final int RUNNING = -1 << COUNT_BITS;//拒绝新的任务,但是处理队列中的任务private static final int SHUTDOWN = 0 << COUNT_BITS;//拒绝新的任务,不处理队列中的任务,并且中断在执行的任务private static final int STOP = 1 << COUNT_BITS;//所有任务已经终止(terminated),workerCount=0,线程private static final int TIDYING = 2 << COUNT_BITS;//terminated已完成private static final int TERMINATED = 3 << COUNT_BITS; 核心流程任务提交核心流程根据下图所示的任务提交核心流程,主要是ThreadPoolExecutor#execute()方法,核心流程总结为以下结论: 核心线程数小于corePoolSize,则需要创建新的工作线程来执行任务 核心线程数大于等于corePoolSize,需要将线程放入到阻塞任务队列中等待执行 队列满时需要创建非核心线程来执行任务,所有工作线程(核心线程+非核心线程)数量要小于等于maximumPoolSize 如果工作线程数量已经达到maximumPoolSize,则拒绝任务,执行拒绝策略 TIP:这里需要注意对核心线程和非核心线程的理解,它们不是工作线程的状态,核心线程提出的目的,是为了尽量维持工作线程的数量在corePoolSize。 创建工作线程核心流程根据下图对java.util.concurrent.ThreadPoolExecutor#addWorker的分析,可以得出如下结论: 只有工作线程被添加到线程池中并且工作线程start,才算添加成功,否则Worker对象只是一个临时对象(被GC掉) 状态为STOP,TIDYING,TERMINATED这三种时,是不会增加工作线程的 状态为SHUTDOWN时是一个重要的边界条件 任务执行核心流程如下图是任务执行的流程,可以总结如下: 提交任务时如果工作线程数量小于核心线程数量,则firstTask != null,一路顺利执行然后阻塞在队列的poll上。 提交任务时如果工作线程数量大于等于核心线程数量,则firstTask == null,需要从任务队列中poll一个任务执行,执行完毕之后继续阻塞在队列的poll上。 提交任务时如果工作线程数量大于等于核心线程数量并且任务队列已满,需要创建一个非核心线程来执行任务,则firstTask != null,执行完毕之后继续阻塞在队列的poll上,不过注意这个poll是允许超时的,最多等待时间为keepAliveTime。 工作线程在跳出循环之后,线程池会移除该线程对象,并且试图终止线程池(因为需要考量shutdown的情况) ThreadPoolExecutor提供了任务执行前和执行后的钩子方法,分别为beforeExecute和afterExecute。 工作线程通过实现AQS来保证线程安全(每次执行任务的时候都会lock和unlock) 线程池关闭核心流程如下图是shutdown方法的流程图,它是一种“温和“的关闭方式,执行完shutdown之后,会把线程状态置为SHUTDOWN,线程池并不会立即关闭,而是先中断能中断的工作线程,有些工作线程正在执行任务中,那么就先不中断,等待它们执行完毕之后中断工作线程。 如下图是shutdownNow的流程图,与shutdown不同的地方在于,它关闭线程池的方式比较“粗暴”,直接把线程池状态置为STOP。不管工作线程是否在执行,全部一一中断,也不等待队列中未执行的任务,把它们返回给客户线程由客户线程自行处置。 ThreadPoolExecutor源码分析源码分析主要从重要成员变量和核心方法两个层面来讲解。 关键属性12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879public class ThreadPoolExecutor extends AbstractExecutorService { //控制变量-存放状态和线程数 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); //线程池阻塞队列 private final BlockingQueue<Runnable> workQueue; //工作线程集合,存放(活跃的)工作线程,只有持有mainLock才能访问这个集合。 private final HashSet<Worker> workers = new HashSet<>(); //全局锁 private final ReentrantLock mainLock = new ReentrantLock(); //awaitTermination方法使用的等待条件变量 private final Condition termination = mainLock.newCondition(); //记录峰值线程数,只有持有全局锁才能访问 private int largestPoolSize; //记录完成任务数,只有持有全局锁才能访问 private long completedTaskCount; //线程工厂 private volatile ThreadFactory threadFactory; //拒绝执行处理器 private volatile RejectedExecutionHandler handler; //任务超时时间 private volatile long keepAliveTime; //是否允许coreSize超时,默认不允许 private volatile boolean allowCoreThreadTimeOut; //线程池容量 private volatile int maximumPoolSize; private final class Worker extends AbstractQueuedSynchronizer implements Runnable { //工作线程 final Thread thread; //初始要执行的任务,有可能是null Runnable firstTask; //每一个线程完成的任务数量 volatile long completedTasks; } /** * 创建线程池 * * @param corePoolSize 核心线程数量 * @param maximumPoolSize 最大线程容量 * @param keepAliveTime 超过CoreSize的工作线程保持的时间,超时即终止 * @param unit keepAliveTime的单位 * @param workQueue 阻塞工作队列 * @param threadFactory 线程工厂,默认Executors.defaultThreadFactory() * @param handler 拒绝执行处理回调 * @throws IllegalArgumentException if one of the following holds:<br> * {@code corePoolSize < 0}<br> * {@code keepAliveTime < 0}<br> * {@code maximumPoolSize <= 0}<br> * {@code maximumPoolSize < corePoolSize} * @throws NullPointerException if {@code workQueue} * or {@code threadFactory} or {@code handler} is null */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }} 状态控制在ThreadPoolExecutor中是通过位运算来处理状态的。位运算的基础是理解原码,反码,补码,用int ctl = 1来举例 原码: 0000 0000 0000 0000 0000 0000 0000 0001,十进制是1 反码:1111 1111 1111 1111 1111 1111 1111 1110,十进制是-2 补码:1111 1111 1111 1111 1111 1111 1111 1111, 十进制是-1 123456789101112131415161718192021222324252627//ctl的初始值是1110 0000 0000 0000 0000 0000 0000 0000,即状态为RUNNING,worker数量为0private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); //Java中Integer是32位,所以COUNT_BITS等于29private static final int COUNT_BITS = Integer.SIZE - 3; //0001 1111 1111 1111 1111 1111 1111 1111 private static final int CAPACITY = (1 << COUNT_BITS) - 1; //-1左移29位 1110 0000 0000 0000 0000 0000 0000 0000 高3位 111代表运行中private static final int RUNNING = -1 << COUNT_BITS; //0 左移 29位 高三位000代表SHUTDOWN private static final int SHUTDOWN = 0 << COUNT_BITS; //1 左移 29位 高三位001代表STOP private static final int STOP = 1 << COUNT_BITS; //2 左移 29位 高三位010代表TIDYING private static final int TIDYING = 2 << COUNT_BITS; //3 左移 29位 高三位011代表TERMINATED private static final int TERMINATED = 3 << COUNT_BITS; //获取线程池状态,CAPACITY取反是 1110 0000 0000 0000 0000 0000 0000 0000 //c & ~CAPACITY ,c代表ctl值,因为低29位都是0,所以与操作得出的值只与高三位相关 //这个算法证明:ctl高三位决定了线程池的状态private static int runStateOf(int c) { return c & ~CAPACITY; } //获取worder数量,低29位于ctl与操作 private static int workerCountOf(int c) { return c & CAPACITY; } private static int ctlOf(int rs, int wc) { return rs | wc; } 工作线程为0的前提下,小结下线程池的运行状态: 状态名称 位图 十进制值 描述 RUNNING 111-00000000000000000000000000000 -536870912 运行中状态,可以接收新的任务和执行任务队列中的任务 SHUTDOWN 000-00000000000000000000000000000 0 shutdown状态,不再接收新的任务,但是会执行任务队列中的任务 STOP 001-00000000000000000000000000000 536870912 停止状态,不再接收新的任务,也不会执行任务队列中的任务,中断所有执行中的任务 TIDYING 010-00000000000000000000000000000 1073741824 整理中状态,所有任务已经终结,工作线程数为0,过渡到此状态的工作线程会调用钩子方法terminated() TERMINATED 011-00000000000000000000000000000 1610612736 终结状态,钩子方法terminated()执行完毕 这里有一个比较特殊的技巧,由于运行状态值存放在高3位,所以可以直接通过十进制值(甚至可以忽略低29位,直接用ctl进行比较,或者使用ctl和线程池状态常量进行比较)来比较和判断线程池的状态: 工作线程数为0的前提下:RUNNING(-536870912) < SHUTDOWN(0) < STOP(536870912) < TIDYING(1073741824) < TERMINATED(1610612736) 1234567891011121314// ctl和状态常量比较,判断是否小于private static boolean runStateLessThan(int c, int s) { return c < s;}// ctl和状态常量比较,判断是否小于或等于private static boolean runStateAtLeast(int c, int s) { return c >= s;}// ctl和状态常量SHUTDOWN比较,判断是否处于RUNNING状态private static boolean isRunning(int c) { return c < SHUTDOWN;} 最后是线程池状态的跃迁图: 核心方法总体概括execute方法源码分析1234567891011121314151617181920212223242526272829303132333435363738394041424344/** * 异步执行给定的task,即可能用新的线程执行任务,也可以用线程池中已存在的线程执行任务 * 如果线程池已经shutdown,那么会拒绝执行任务并抛出异常,由RejectedExecutionHandler处理 * * @param command 要执行的任务 * @throws RejectedExecutionException at discretion of * {@code RejectedExecutionHandler}, if the task * cannot be accepted for execution * @throws NullPointerException if {@code command} is null */ public void execute(Runnable command) { if (command == null) throw new NullPointerException(); /* * Proceed in 3 steps: * * 1. 如果工作线程小于corePoolSize,ThreadPoolExecutor会把command当成firstTask, * 交给新创建的线程。addWorker函数会校验线程池状态和线程池数量. * * 2. 如果任务能够入队, 我们还是要进行double-check是否新加线程还是用线程池中的工作线程,因为线程可能在最后一次check的时候死掉. * * 3. 如果任务没有进入队列,就尝试创建一个新的线程。如果创建失败了,则线程池shutdown或者饱和,拒绝了这次请求 */ int c = ctl.get(); //如果工作线程数量小于corePoolSize则创建一个core worker if (workerCountOf(c) < corePoolSize) { //默认把command当做firstTask if (addWorker(command, true)) return; c = ctl.get(); } //说明创建新的核心线程失败,也就是当前工作线程数大于等于corePoolSize //判定线程池是否是RUNNING,工作队列添加command if (isRunning(c) && workQueue.offer(command)) { //再次确认状态,如果线程池状态不是RUNNING并且移除成功则拒绝掉command int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); } 这里简单的分析下流程: 如果工作线程数量小于核心线程数量workerCountOf(c) < corePoolSize,则直接创建核心线程执行任务 如果工作线程数量大于等于核心线程数量,判断线程池状态,并把任务放入到阻塞队列中等待调度。这里会进行二次检查线程池的状态,如果当前工作线程数量为0,则创建一个非核心线程并传入的任务对象为null。 如果添加任务队列失败,则说明阻塞队列已满,尝试创建非核心线程 如果创建非核心线程失败,则证明线程池已经饱和,调用拒绝策略执行任务 这里是一个疑惑点:为什么需要二次检查线程池的运行状态,当前工作线程数量为0,尝试创建一个非核心线程并且传入的任务对象为null?这个可以看API注释: 如果一个任务成功加入任务队列,我们依然需要二次检查是否需要添加一个工作线程(因为所有存活的工作线程有可能在最后一次检查之后已经终结)或者执行当前方法的时候线程池是否已经shutdown了。所以我们需要二次检查线程池的状态,必须时把任务从任务队列中移除或者在没有可用的工作线程的前提下新建一个工作线程。 addWorker方法源码分析123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081//检验线程池状态和线程池数量边界,通过校验则新增一个线程到线程池中//firstTask是新创建的worker第一次要执行的。//当线程数量少于corePoolSize线程或队列已满(这种情况下需要绕过队列),firstTask不为空private boolean addWorker(Runnable firstTask, boolean core) { retry: for (;;) { int c = ctl.get(); //获取线程池状态 int rs = runStateOf(c); //这个判断是线程池拒绝任务的逻辑,比较绕,包含如下两个逻辑 //1、线程池状态不再是RUNNING则拒绝创建worker //2、线程池状态如果是SHUTDOWN并且firstTask为Null且任务队列中有其他任务则拒绝新的任务 //其实这个判定的具体业务时:在线程池状态为STOP,TIDYING,TERMINATED状态下,不会再接受新的任务。如果状态是SHUTDOWN,firstTask为空并且workQueue不为空,则可以创建新的工作线程。当队列为空了,就不能继续提交任务了。 if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false; //自旋检测状态 for (;;) { //获取worker的数量 int wc = workerCountOf(c); //判定数量是否大于边界,大于的话则拒绝新建线程 if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; //CAS增加工作线程数量 if (compareAndIncrementWorkerCount(c)) break retry; //如果CAS失败,则需要再次读取ctl c = ctl.get(); // Re-read ctl if (runStateOf(c) != rs) continue retry; // else CAS failed due to workerCount change; retry inner loop } } boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { //创建工作线程 w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { //获取全局锁 final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // Recheck while holding lock. // Back out on ThreadFactory failure or if // shut down before lock acquired. //获取全局锁之后再次校验避免线程池shutdown int rs = runStateOf(ctl.get()); //线程池状态是RUNNING或者状态是SHUTDOWN但是core已满且队列未满 if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); //把线程添加进workers容器中 workers.add(w); int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { mainLock.unlock(); } //如果worker添加完毕,则需要启动线程 if (workerAdded) { t.start(); workerStarted = true; } } } finally { if (! workerStarted) addWorkerFailed(w); } return workerStarted; } 通过addWorker方法,得出一个很重要的结论 线程池状态 > SHUTDOWN时(即为STOP,TIDYING,TERMINATED这三种),线程池会拒绝所有的新提交任务。 线程池状态为SHUTDOWN的时候,并不会拒绝全部的任务,只会拒绝firstTask不为空,或者firstTask为空并且workQueue为空。其中firstTask是一个关键点,在工作线程数量小于coreSize或者队列满并且线程数量没有达到maxSize时,firstTask不为空。 所以基于以上分析,当调用threadPool.shutdown()之后,核心线程是提交不进来的,队列满之后的临时线程也提交不进来,只有队列不为空的情况下才能提交进来,直到队列中的任务被全部处理完毕,所有线程就都提交不进来了。 工作线程内部类Worker源码分析可以看到Worker实现了AQS,实现AQS的目的也是为了保证工作线程内部的线程安全和shutdown时判定工作线程是否在工作中。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162private final class Worker extends AbstractQueuedSynchronizer implements Runnable { final Thread thread; Runnable firstTask; volatile long completedTasks; /** * 默认构造函数 */ Worker(Runnable firstTask) { //设置state初始值为-1,阻止中断工作线程 setState(-1); this.firstTask = firstTask; //线程是通过线程工厂创建的. this.thread = getThreadFactory().newThread(this); } public void run() { //下文重点讲解 runWorker(this); } // Lock methods // // The value 0 represents the unlocked state. // The value 1 represents the locked state. protected boolean isHeldExclusively() { return getState() != 0; } protected boolean tryAcquire(int unused) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } protected boolean tryRelease(int unused) { setExclusiveOwnerThread(null); setState(0); return true; } public void lock() { acquire(1); } public boolean tryLock() { return tryAcquire(1); } public void unlock() { release(1); } public boolean isLocked() { return isHeldExclusively(); } void interruptIfStarted() { Thread t; if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { try { t.interrupt(); } catch (SecurityException ignore) { } } } } runWorker源码分析12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879//自旋的从队列中获取任务并执行final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // 允许中断线程 //突然停止标识位 boolean completedAbruptly = true; try { //task不为空或者从队列中获取task,如果队列为空则阻塞while while (task != null || (task = getTask()) != null) { // w.lock(); // If pool is stopping, ensure thread is interrupted; // 如果线程池正在停止(STOPING),请确保线程是中断的。 // 否则,要确保线程不是中断的 if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { //钩子方法,在执行任务前执行 beforeExecute(wt, task); Throwable thrown = null; try { //执行任务,此处调用应用实现代码 task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { //钩子方法,在任务执行之后执行 afterExecute(task, thrown); } } finally { task = null; //递增completedTasks,由于worker.lock,所以这个递增是安全的 w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { // processWorkerExit(w, completedAbruptly); }}private void processWorkerExit(Worker w, boolean completedAbruptly) { if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted decrementWorkerCount(); final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { completedTaskCount += w.completedTasks; workers.remove(w); } finally { mainLock.unlock(); } tryTerminate(); int c = ctl.get(); if (runStateLessThan(c, STOP)) { if (!completedAbruptly) { int min = allowCoreThreadTimeOut ? 0 : corePoolSize; if (min == 0 && ! workQueue.isEmpty()) min = 1; if (workerCountOf(c) >= min) return; // replacement not needed } addWorker(null, false); }} getTask方法源码分析1234567891011121314151617181920212223242526272829303132333435363738394041424344//getTask返回NULL代表工作线程将要终止private Runnable getTask() { //记录上次一poll时是否超时 boolean timedOut = false; // Did the last poll() time out? for (;;) { int c = ctl.get(); //获取线程池状态 int rs = runStateOf(c); //线程池状态为SHUTDOWN时,如果工作队列为空,则decrementWorkerCount,返回NULL //线程池状态大于SHUTDOWN时,decrementWorkerCount,返回NULL if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { //原子的减少线程池数量,ctl低29位 decrementWorkerCount(); return null; } // 获取工作线程数量 // 到这里说明线程池还处于RUNNING状态 int wc = workerCountOf(c); // 允许core线程超时或者线程数量大于coreSize boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { if (compareAndDecrementWorkerCount(c)) return null; continue; } try { Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); // 只有非null的时候才返回,null的情况下会进入下一轮循环 if (r != null) return r; timedOut = true; } catch (InterruptedException retry) { timedOut = false; } }} 这个方法中,有两处十分庞大的if逻辑,对于第一处if可能导致工作线程数减去1直接返回null的场景有: 线程池状态为SHUTDOWN,一般是调用了shutdown()方法,并且任务队列为空。 线程池状态为STOP。 对于第二处if,逻辑有点复杂,先拆解一下: 12345678910111213141516复制// 工作线程总数大于maximumPoolSize,说明了通过setMaximumPoolSize()方法减少了线程池容量boolean b1 = wc > maximumPoolSize;// 允许线程超时同时上一轮通过poll()方法从任务队列中拉取任务为nullboolean b2 = timed && timedOut;// 工作线程总数大于1boolean b3 = wc > 1;// 任务队列为空boolean b4 = workQueue.isEmpty();boolean r = (b1 || b2) && (b3 || b4);if (r) { if (compareAndDecrementWorkerCount(c)){ return null; }else{ continue; }} 这段逻辑大多数情况下是针对非核心线程。在execute()方法中,当线程池总数已经超过了corePoolSize并且还小于maximumPoolSize时,当任务队列已经满了的时候,会通过addWorker(task,false)添加非核心线程。而这里的逻辑恰好类似于addWorker(task,false)的反向操作,用于减少非核心线程,使得工作线程总数趋向于corePoolSize。如果对于非核心线程,上一轮循环获取任务对象为null,这一轮循环很容易满足timed && timedOut为true,这个时候getTask()返回null会导致Worker#runWorker()方法跳出死循环,之后执行processWorkerExit()方法处理后续工作,而该非核心线程对应的Worker则变成“游离对象”,等待被JVM回收。当allowCoreThreadTimeOut设置为true的时候,这里分析的非核心线程的生命周期终结逻辑同时会适用于核心线程。那么可以总结出keepAliveTime的意义: 当允许核心线程超时,也就是allowCoreThreadTimeOut设置为true的时候,此时keepAliveTime表示空闲的工作线程的存活周期。 默认情况下不允许核心线程超时,此时keepAliveTime表示空闲的非核心线程的存活周期。 在一些特定的场景下,配置合理的keepAliveTime能够更好地利用线程池的工作线程资源。 processWorkerExit方法源码分析processWorkerExit是在工作线程跳出工作队列死循环时,更新线程池数量,线程池容器。 12345678910111213141516171819202122232425262728293031323334private void processWorkerExit(Worker w, boolean completedAbruptly) { //突然结束,是因为线程抛出用户异常,直接使工作线程数减1 if (completedAbruptly) decrementWorkerCount(); final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { //全局锁锁定情况下,线程安全的方式更新完成的任务数量并移除Worker completedTaskCount += w.completedTasks; workers.remove(w); } finally { mainLock.unlock(); } //用于根据当前线程池的状态判断是否需要进行线程池terminate处理 tryTerminate(); int c = ctl.get(); //如果状态小于STOP,即RUNNING 或 SHUTDOWN if (runStateLessThan(c, STOP)) { //并且不是线程抛出用户异常 if (!completedAbruptly) { int min = allowCoreThreadTimeOut ? 0 : corePoolSize; //如果允许核心线程超时且队列不为空,则至少保证一个工作线程 if (min == 0 && ! workQueue.isEmpty()) min = 1; //工作线程大于等于最小值,直接返回不新增非核心线程 if (workerCountOf(c) >= min) return; // replacement not needed } //工作线程小于最小值,需要新增非核心线程 addWorker(null, false); }} tryTerminate方法源码分析每个工作线程在工作结束之后都会调用tryTerminate方法,总结为: 满足两种情况就会把线程池状态置为TERMINATED1、状态为SHUTDOWN切队列数量为0,工作线程数量为0,对应shutdown关闭的情况2、状态为STOP且工作线程数量为0,对应shutdownNow关闭的情况 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566//满足两种情况就会把线程池状态置为TERMINATED//1、状态为SHUTDOWN切队列数量为0,工作线程数量为0,对应shutdown关闭的情况//2、状态为STOP且工作线程数量为0,对应shutdownNow关闭的情况final void tryTerminate() { for (;;) { int c = ctl.get(); //1、线程池状态是RUNNING,不做任何操作直接返回 //2、线程池状态是TIDYING,不做任何操作直接返回 //3、线程池状态是SHUTDOWN,且队列不为空不做任何操作直接返回 if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) return; // 工作线程数不为0,则中断工作线程集合中的第一个空闲的工作线程 if (workerCountOf(c) != 0) { // Eligible to terminate interruptIdleWorkers(ONLY_ONE); return; } final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { //CAS设置状态为TIDYING状态 if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { try { //执行terminated方法 terminated(); } finally { //执行terminated方法完毕之后,设置线程池状态为TERMINATED ctl.set(ctlOf(TERMINATED, 0)); // 唤醒阻塞在termination条件的所有线程,这个变量的await()方法在awaitTermination()中调用 termination.signalAll(); } return; } } finally { mainLock.unlock(); } // else retry on failed CAS }}//中断空闲的工作线程,以便它们可以检查终止或配置更改//如果onlyOne未true,中断最多一个线程。private void interruptIdleWorkers(boolean onlyOne) { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { for (Worker w : workers) { Thread t = w.thread; // 这里判断线程不是中断状态并且尝试获取锁成功的时候才进行线程中断 if (!t.isInterrupted() && w.tryLock()) { try { t.interrupt(); } catch (SecurityException ignore) { } finally { w.unlock(); } } if (onlyOne) break; } } finally { mainLock.unlock(); }} 关闭线程池方法源码分析首先和关闭线程池相关的方法有三个shutdown(),shutdownNow(),awaitTermination(long timeout, TimeUnit unit) 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364//已提交的任务还会继续执行,但是新的任务将被拒绝,shutdown不会影响用户的正常使用 public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { //校验权限,安全策略 SecurityManager checkShutdownAccess(); //CAS更新状态为SHUTDOWN advanceRunState(SHUTDOWN); //中断所有空闲的工作线程 interruptIdleWorkers(); //钩子方法 onShutdown(); // hook for ScheduledThreadPoolExecutor } finally { mainLock.unlock(); } //尝试把线程池状态置为TERMINATED tryTerminate(); }public List<Runnable> shutdownNow() { List<Runnable> tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { //校验权限,安全策略 SecurityManager checkShutdownAccess(); //CAS更新状态为STOP advanceRunState(STOP); //中断所有空闲的工作线程 interruptWorkers(); //清空工作队列,并返回未执行的任务列表 tasks = drainQueue(); } finally { mainLock.unlock(); } //尝试把线程池状态置为TERMINATED tryTerminate(); //返回任务列表 return tasks;}public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { long nanos = unit.toNanos(timeout); final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { for (;;) { //如果发现状态已经变成TERMINATED,则返回true if (runStateAtLeast(ctl.get(), TERMINATED)) return true; if (nanos <= 0) return false; //阻塞直到超时,如果未超时,tryTerminate会signalAll,这时停止阻塞 nanos = termination.awaitNanos(nanos); } } finally { //释放全局锁 mainLock.unlock(); }} 总结下这三个终止线程池的方法,shutdown()和shutdownNow()是中断所有空闲工作线程,如果线程在执行Runnable#run(),那么这个工作线程是不会被中断的,而是等待下一轮执行getTask()方法的时候,通过线程池状态判断正常终结该工作线程。 awaitTermination很容易被忽略,因为对线程池状态不了解的人,是不会第一时间想到这个方法的作用的。它的好处是调用改方法的线程会阻塞直到线程状态更新为TERMINATED才返回,某些需要感知线程池终止的场景需要调用该方法来终止线程池。 reject方法源码分析拒绝方法的实现很简单了,就是一个回调函数,具体的拒绝策略需要应用者来实现。 123final void reject(Runnable command) { handler.rejectedExecution(command, this);} 场景举例通过对ThreadPoolExecutor的核心流程和源码的研究,相信大家对它的设计思想和实现原理有了一定的掌握,接下来需要结合实际场景分析下,本文整理了几个景点的场景,分析一下具体的流程,进一步加深理解。 场景1:线程池刚刚创建,第一次submit(Task),然后执行Futrue.get()等待执行结果。 此刻线程池状态是RUNNING,工作线程数量为零,其数量肯定小于corePoolSize,提交的任务会交给核心线程,且firstTask不为空。工作线程创建好之后,会立即start(),线程开始执行runWorker函数,runWorker中会判断线程的状态不是STOP,这时会执行任务对象的run()(一般情况下是Callable接口的run,Callable接口的run又调用了真正的用户执行逻辑)。在工作线程执行完毕之后,Callable的结果赋值给FutrueTask.outcome且FutrueTask.status == COMPLETING,Futrue.get()解除阻塞,结果返回。 场景2:线程池工作线程数量等于corePoolSize,队列未满,这时submit(Task),然后执行Futrue.get()等待执行结果。 此刻线程池状态是RUNNING,工作线程数量等于corePoolSize,提交的任务会加入到阻塞队列中。工作线程们“嗷嗷待哺”(阻塞在队列上),这时会有一个工作线程争抢到食物(Task),剩下的流程同场景1。 场景3:线程池工作线程等于corePoolSize,工作队列已满,这时submit(Task),然后执行Futrue.get()等待执行结果。 此刻线程池状态是RUNNING,由于队列已满,需要加入非核心线程来执行任务,非核心线程数量=maximumPoolSize - corePoolSize,加入新的工作线程且firstTask不为空。剩下的执行流程同场景1。 场景4:执行shutdown()操作。 在执行shutdown()之后,首先进行安全性检查,并将线程池状态设置为SHUTDOWN,中断未执行的线程(通过tryLock可知,在执行的线程是被锁定的),执行中的线程不中断。线程一旦中断,阻塞在队列take()方法或者poll()方法就会抛出InteruptException,getTask()方法内部的自旋重启,因为此刻线程池状态是SHUTDOWN,如果工作队列为空,就直接返回null;如果工作队列不为空,则继续take()出任务交给工作线程执行,知道队列为空,返回null,逐步使工作线程进入Terminated状态。在执行shutdown之后,所有的新提交任务,都会被拒绝,已提交的任务会等待执行完毕。最终线程池状态会变为TERMINATED,如果想获取到线程池终结的事件,需要调用awaitTermination`方法。 场景4:执行shutdownNow()操作。 在执行shutdownNow()之后,首先进行安全性检查,并将线程池状态设置为STOP,中断所有工作线程(不论工作线程是否在工作中),并移除工作队列中全部的任务,返回给用户自行处置。由于工作线程全部中断,阻塞在队列take()方法或者poll()方法就会抛出InteruptException,getTask()方法内部的自旋重启,因为此刻线程池状态是STOP,就直接返回null,跳出死循环,等待工作线程Terminated,最后返回所有的工作队列中未执行的任务。在执行shutdownNow之后,所有的新提交任务,都会被拒绝,未执行的任务会返回给用户自行处理,在执行中的任务虽然工作线程已经被中断,除非任务中处理了中断状态,否则不影响任务执行。 总结ThreadPoolExecutor作为Java并发编程中最常使用的工作类,研究其实现原理不仅能够熟练运用线程池写出高效的并发代码,避免采坑之外,还能够发掘作者再创作过程中的“点睛之笔”。 ThreadPoolExecutor中,大量运用了位计算来提高性能,线程池的状态和工作线程的数量,都交给一个32位的ctl变量控制,通过把线程抽象为Worker和全局锁mainLock,来保证工作线程执行任务的线程安全,巧妙的运用阻塞队列,来避免线程数量暴增,并通过中断来控制工作线程阻塞与否,线程的终止过程,即可“温和”也可“粗暴”,整个线程池设计精妙,应用者使用如丝般顺滑。 参考文档JUC线程池ThreadPoolExecutor源码分析","link":"/2020/01/09/java/Java并发-ThreadPoolExecutor/"}],"tags":[{"name":"reactor","slug":"reactor","link":"/tags/reactor/"},{"name":"reactive","slug":"reactive","link":"/tags/reactive/"},{"name":"Spring - Ioc - 依赖注入","slug":"Spring-Ioc-依赖注入","link":"/tags/Spring-Ioc-依赖注入/"},{"name":"Spring","slug":"Spring","link":"/tags/Spring/"},{"name":"thread","slug":"thread","link":"/tags/thread/"},{"name":"Lock","slug":"Lock","link":"/tags/Lock/"},{"name":"CountDownLatch","slug":"CountDownLatch","link":"/tags/CountDownLatch/"},{"name":"Spring - SpinrgBoot - Spring上下文","slug":"Spring-SpinrgBoot-Spring上下文","link":"/tags/Spring-SpinrgBoot-Spring上下文/"},{"name":"java","slug":"java","link":"/tags/java/"},{"name":"设计模式","slug":"设计模式","link":"/tags/设计模式/"},{"name":"JVM","slug":"JVM","link":"/tags/JVM/"},{"name":"LTR","slug":"LTR","link":"/tags/LTR/"},{"name":"ElasticSearch","slug":"ElasticSearch","link":"/tags/ElasticSearch/"},{"name":"分词","slug":"分词","link":"/tags/分词/"},{"name":"IK","slug":"IK","link":"/tags/IK/"},{"name":"搜索","slug":"搜索","link":"/tags/搜索/"},{"name":"lucene","slug":"lucene","link":"/tags/lucene/"},{"name":"Hbase","slug":"Hbase","link":"/tags/Hbase/"},{"name":"自动部署","slug":"自动部署","link":"/tags/自动部署/"},{"name":"jenkins","slug":"jenkins","link":"/tags/jenkins/"},{"name":"开发工具","slug":"开发工具","link":"/tags/开发工具/"},{"name":"jmeter","slug":"jmeter","link":"/tags/jmeter/"},{"name":"可观察性","slug":"可观察性","link":"/tags/可观察性/"},{"name":"APM","slug":"APM","link":"/tags/APM/"},{"name":"ElasticAPM","slug":"ElasticAPM","link":"/tags/ElasticAPM/"},{"name":"链路追踪","slug":"链路追踪","link":"/tags/链路追踪/"},{"name":"函数式编程","slug":"函数式编程","link":"/tags/函数式编程/"},{"name":"Lambda","slug":"Lambda","link":"/tags/Lambda/"},{"name":"Mysql","slug":"Mysql","link":"/tags/Mysql/"},{"name":"MGR","slug":"MGR","link":"/tags/MGR/"},{"name":"主从数据库架构","slug":"主从数据库架构","link":"/tags/主从数据库架构/"},{"name":"Nginx","slug":"Nginx","link":"/tags/Nginx/"},{"name":"LVS","slug":"LVS","link":"/tags/LVS/"},{"name":"HAProxy","slug":"HAProxy","link":"/tags/HAProxy/"},{"name":"负载均衡","slug":"负载均衡","link":"/tags/负载均衡/"},{"name":"线程","slug":"线程","link":"/tags/线程/"},{"name":"TF-IDF","slug":"TF-IDF","link":"/tags/TF-IDF/"},{"name":"布尔模型","slug":"布尔模型","link":"/tags/布尔模型/"},{"name":"向量空间模型","slug":"向量空间模型","link":"/tags/向量空间模型/"},{"name":"倒排索引","slug":"倒排索引","link":"/tags/倒排索引/"},{"name":"相关性","slug":"相关性","link":"/tags/相关性/"},{"name":"BM25","slug":"BM25","link":"/tags/BM25/"},{"name":"数据分片","slug":"数据分片","link":"/tags/数据分片/"},{"name":"一致性Hash","slug":"一致性Hash","link":"/tags/一致性Hash/"},{"name":"虚拟槽","slug":"虚拟槽","link":"/tags/虚拟槽/"},{"name":"range based","slug":"range-based","link":"/tags/range-based/"},{"name":"工程架构","slug":"工程架构","link":"/tags/工程架构/"},{"name":"spring","slug":"spring","link":"/tags/spring/"},{"name":"spring-cloud","slug":"spring-cloud","link":"/tags/spring-cloud/"},{"name":"spring-boot","slug":"spring-boot","link":"/tags/spring-boot/"},{"name":"微服务","slug":"微服务","link":"/tags/微服务/"},{"name":"Spring Boot","slug":"Spring-Boot","link":"/tags/Spring-Boot/"},{"name":"Spring Cloud Gateway","slug":"Spring-Cloud-Gateway","link":"/tags/Spring-Cloud-Gateway/"},{"name":"Spring Cloud","slug":"Spring-Cloud","link":"/tags/Spring-Cloud/"},{"name":"网关","slug":"网关","link":"/tags/网关/"},{"name":"反应式编程","slug":"反应式编程","link":"/tags/反应式编程/"},{"name":"webFlux","slug":"webFlux","link":"/tags/webFlux/"},{"name":"APR","slug":"APR","link":"/tags/APR/"},{"name":"Tomcat","slug":"Tomcat","link":"/tags/Tomcat/"},{"name":"Embedded Tomcat","slug":"Embedded-Tomcat","link":"/tags/Embedded-Tomcat/"},{"name":"建造者模式","slug":"建造者模式","link":"/tags/建造者模式/"},{"name":"配置中心","slug":"配置中心","link":"/tags/配置中心/"},{"name":"Apollo","slug":"Apollo","link":"/tags/Apollo/"},{"name":"SpringCloud","slug":"SpringCloud","link":"/tags/SpringCloud/"},{"name":"工厂模式","slug":"工厂模式","link":"/tags/工厂模式/"},{"name":"责任链模式","slug":"责任链模式","link":"/tags/责任链模式/"},{"name":"6entinel","slug":"6entinel","link":"/tags/6entinel/"},{"name":"ReentrantLock","slug":"ReentrantLock","link":"/tags/ReentrantLock/"},{"name":"ReentrantReadWriteLock","slug":"ReentrantReadWriteLock","link":"/tags/ReentrantReadWriteLock/"},{"name":"AQS","slug":"AQS","link":"/tags/AQS/"},{"name":"并发编程","slug":"并发编程","link":"/tags/并发编程/"},{"name":"锁","slug":"锁","link":"/tags/锁/"},{"name":"Java并发","slug":"Java并发","link":"/tags/Java并发/"},{"name":"线程池","slug":"线程池","link":"/tags/线程池/"},{"name":"JUC","slug":"JUC","link":"/tags/JUC/"},{"name":"ThreadPoolExecutor","slug":"ThreadPoolExecutor","link":"/tags/ThreadPoolExecutor/"}],"categories":[{"name":"reactor","slug":"reactor","link":"/categories/reactor/"},{"name":"SpringCloud","slug":"SpringCloud","link":"/categories/SpringCloud/"},{"name":"Java","slug":"Java","link":"/categories/Java/"},{"name":"机器学习","slug":"机器学习","link":"/categories/机器学习/"},{"name":"搜索","slug":"搜索","link":"/categories/搜索/"},{"name":"大数据技术","slug":"大数据技术","link":"/categories/大数据技术/"},{"name":"开发工具","slug":"开发工具","link":"/categories/开发工具/"},{"name":"ELK","slug":"ELK","link":"/categories/ELK/"},{"name":"软件架构","slug":"软件架构","link":"/categories/软件架构/"}]}