首页 文章 精选 留言 我的

精选列表

搜索[智能解析],共10000篇文章
优秀的个人博客,低调大师

死磕 java同步系列之volatile解析

问题 (1)volatile是如何保证可见性的? (2)volatile是如何禁止重排序的? (3)volatile的实现原理? (4)volatile的缺陷? 简介 volatile可以说是Java虚拟机提供的最轻量级的同步机制了,但是它并不容易被正确地理解,以至于很多人不习惯使用它,遇到多线程问题一律使用synchronized或其它锁来解决。 了解volatile的语义对理解多线程的特性具有很重要的意义,所以彤哥专门写了一篇文章来解释volatile的语义到底是什么。 语义一:可见性 前面介绍Java内存模型的时候,我们说过可见性是指当一个线程修改了共享变量的值,其它线程能立即感知到这种变化。 关于Java内存模型的讲解请参考【死磕 java同步系列之JMM(Java Memory Model)】。 而普通变量无法做到立即感知这一点,变量的值在线程之间的传递均需要通过主内存来完成,比如,线程A修改了一个普通变量的值,然后向主内存回写,另外一条线程B只有在线程A的回写完成之后再从主内存中读取变量的值,才能够读取到新变量的值,也就是新变量才能对线程B可见。 在这期间可能会出现不一致的情况,比如: (1)线程A并不是修改完成后立即回写; (线路A修改了变量x的值为5,但是还没有回写,线程B从主内存读取到的还旧值0) (2)线程B还在用着自己工作内存中的值,而并不是立即从主内存读取值; (线程A回写了变量x的值为5到主内存中,但是线程B还没有读取主内存的值,依旧在使用旧值0在进行运算) 基于以上两种情况,所以,普通变量都无法做到立即感知这一点。 但是,volatile变量可以做到立即感知这一点,也就是volatile可以保证可见性。 java内存模型规定,volatile变量的每次修改都必须立即回写到主内存中,volatile变量的每次使用都必须从主内存刷新最新的值。 volatile的可见性可以通过下面的示例体现: public class VolatileTest { // public static int finished = 0; public static volatile int finished = 0; private static void checkFinished() { while (finished == 0) { // do nothing } System.out.println("finished"); } private static void finish() { finished = 1; } public static void main(String[] args) throws InterruptedException { // 起一个线程检测是否结束 new Thread(() -> checkFinished()).start(); Thread.sleep(100); // 主线程将finished标志置为1 finish(); System.out.println("main finished"); } } 在上面的代码中,针对finished变量,使用volatile修饰时这个程序可以正常结束,不使用volatile修饰时这个程序永远不会结束。 因为不使用volatile修饰时,checkFinished()所在的线程每次都是读取的它自己工作内存中的变量的值,这个值一直为0,所以一直都不会跳出while循环。 使用volatile修饰时,checkFinished()所在的线程每次都是从主内存中加载最新的值,当finished被主线程修改为1的时候,它会立即感知到,进而会跳出while循环。 语义二:禁止重排序 前面介绍Java内存模型的时候,我们说过Java中的有序性可以概括为一句话:如果在本线程中观察,所有的操作都是有序的;如果在另一个线程中观察,所有的操作都是无序的。 前半句是指线程内表现为串行的语义,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。 关于Java内存模型的讲解请参考【死磕 java同步系列之JMM(Java Memory Model)】。 普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获得正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致,因为一个线程的方法执行过程中无法感知到这点,这就是“线程内表现为串行的语义”。 比如,下面的代码: // 两个操作在一个线程 int i = 0; int j = 1; 上面两句话没有依赖关系,JVM在执行的时候为了充分利用CPU的处理能力,可能会先执行int j = 1;这句,也就是重排序了,但是在线程内是无法感知的。 看似没有什么影响,但是如果是在多线程环境下呢? 我们再看一个例子: public class VolatileTest3 { private static Config config = null; private static volatile boolean initialized = false; public static void main(String[] args) { // 线程1负责初始化配置信息 new Thread(() -> { config = new Config(); config.name = "config"; initialized = true; }).start(); // 线程2检测到配置初始化完成后使用配置信息 new Thread(() -> { while (!initialized) { LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100)); } // do sth with config String name = config.name; }).start(); } } class Config { String name; } 这个例子很简单,线程1负责初始化配置,线程2检测到配置初始化完毕,使用配置来干一些事。 在这个例子中,如果initialized不使用volatile来修饰,可能就会出现重排序,比如在初始化配置之前把initialized的值设置为了true,这样线程2读取到这个值为true了,就去使用配置了,这时候可能就会出现错误。 (此处这个例子只是用于说明重排序,实际运行时很难出现。) 通过这个例子,彤哥相信大家对“如果在本线程内观察,所有操作都是有序的;在另一个线程观察,所有操作都是无序的”有了更深刻的理解。 所以,重排序是站在另一个线程的视角的,因为在本线程中,是无法感知到重排序的影响的。 而volatile变量是禁止重排序的,它能保证程序实际运行是按代码顺序执行的。 实现:内存屏障 上面讲了volatile可以保证可见性和禁止重排序,那么它是怎么实现的呢? 答案就是,内存屏障。 内存屏障有两个作用: (1)阻止屏障两侧的指令重排序; (2)强制把写缓冲区/高速缓存中的数据回写到主内存,让缓存中相应的数据失效; 关于“内存屏障”的知识点,各路大神的观点也不完全一致,所以这里彤哥也就不展开讲述了,感兴趣的可以看看下面的文章: (注意,公众号不允许外发链接,所以只能辛苦复制链接到浏览器中阅读了,而且还可能需要科学上网) (1) Doug Lea的《The JSR-133 Cookbook for Compiler Writers》 http://g.oswego.edu/dl/jmm/cookbook.html Doug Lea 就是java并发包的作者,大牛! (2)Martin Thompson的《Memory Barriers/Fences》 https://mechanical-sympathy.blogspot.com/2011/07/memory-barriersfences.html Martin Thompson 专注于把性能提升到极致,专注于从硬件层面思考问题,比如如何避免伪共享等,大牛! 它的博客地址就是上面这个地址,里面有很多底层的知识,有兴趣的可以去看看。 (3)Dennis Byrne的《Memory Barriers and JVM Concurrency》 https://www.infoq.com/articles/memory_barriers_jvm_concurrency 这是InfoQ英文站上面的一篇文章,我觉得写的挺好的,基本上综合了上面的两种观点,并从汇编层面分析了内存屏障的实现。 目前国内市面上的关于内存屏障的讲解基本不会超过这三篇文章,包括相关书籍中的介绍。 我们还是来看一个例子来理解内存屏障的影响: public class VolatileTest4 { // a不使用volatile修饰 public static long a = 0; // 消除缓存行的影响 public static long p1, p2, p3, p4, p5, p6, p7; // b使用volatile修饰 public static volatile long b = 0; // 消除缓存行的影响 public static long q1, q2, q3, q4, q5, q6, q7; // c不使用volatile修饰 public static long c = 0; public static void main(String[] args) throws InterruptedException { new Thread(()->{ while (a == 0) { long x = b; } System.out.println("a=" + a); }).start(); new Thread(()->{ while (c == 0) { long x = b; } System.out.println("c=" + c); }).start(); Thread.sleep(100); a = 1; b = 1; c = 1; } } 这段代码中,a和c不使用volatile修饰,b使用volatile修饰,而且我们在a/b、b/c之间各加入7个long字段消除伪共享的影响。 关于伪共享的相关知识,可以查看彤哥之前写的文章【杂谈 什么是伪共享(false sharing)?】。 在a和c的两个线程的while循环中我们获取一下b,你猜怎样?如果把long x = b;这行去掉呢?运行试试吧。 彤哥这里直接说结论了:volatile变量的影响范围不仅仅只包含它自己,它会对其上下的变量值的读写都有影响。 缺陷 上面我们介绍了volatile关键字的两大语义,那么,volatile关键字是不是就是万能的了呢? 当然不是,忘了我们内存模型那章说的一致性包括的三大特性了么? 一致性主要包含三大特性:原子性、可见性、有序性。 volatile关键字可以保证可见性和有序性,那么volatile能保证原子性么? 请看下面的例子: public class VolatileTest5 { public static volatile int counter = 0; public static void increment() { counter++; } public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(100); IntStream.range(0, 100).forEach(i-> new Thread(()-> { IntStream.range(0, 1000).forEach(j->increment()); countDownLatch.countDown(); }).start()); countDownLatch.await(); System.out.println(counter); } } 这段代码中,我们起了100个线程分别对counter自增1000次,一共应该是增加了100000,但是实际运行结果却永远不会达到100000。 让我们来看看increment()方法的字节码(IDEA下载相关插件可以查看): 0 getstatic #2 <com/coolcoding/code/synchronize/VolatileTest5.counter> 3 iconst_1 4 iadd 5 putstatic #2 <com/coolcoding/code/synchronize/VolatileTest5.counter> 8 return 可以看到counter++被分解成了四条指令: (1)getstatic,获取counter当前的值并入栈 (2)iconst_1,入栈int类型的值1 (3)iadd,将栈顶的两个值相加 (4)putstatic,将相加的结果写回到counter中 由于counter是volatile修饰的,所以getstatic会从主内存刷新最新的值,putstatic也会把修改的值立即同步到主内存。 但是中间的两步iconst_1和iadd在执行的过程中,可能counter的值已经被修改了,这时并没有重新读取主内存中的最新值,所以volatile在counter++这个场景中并不能保证其原子性。 volatile关键字只能保证可见性和有序性,不能保证原子性,要解决原子性的问题,还是只能通过加锁或使用原子类的方式解决。 进而,我们得出volatile关键字使用的场景: (1)运算的结果并不依赖于变量的当前值,或者能够确保只有单一的线程修改变量的值; (2)变量不需要与其他状态变量共同参与不变约束。 说白了,就是volatile本身不保证原子性,那就要增加其它的约束条件来使其所在的场景本身就是原子的。 比如: private volatile int a = 0; // 线程A a = 1; // 线程B if (a == 1) { // do sth } a = 1;这个赋值操作本身就是原子的,所以可以使用volatile来修饰。 总结 (1)volatile关键字可以保证可见性; (2)volatile关键字可以保证有序性; (3)volatile关键字不可以保证原子性; (4)volatile关键字的底层主要是通过内存屏障来实现的; (5)volatile关键字的使用场景必须是场景本身就是原子的; 彩蛋 关于“内存屏障”的三篇文章,考虑到有的同学无法科学上网,彤哥专门把这三篇下载下来整理了一下。 关注我的公众号“彤哥读源码”,后台回复“volatile”,下载这三篇资料。 欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

优秀的个人博客,低调大师

死磕 java魔法类之Unsafe解析

问题 (1)Unsafe是什么? (2)Unsafe只有CAS的功能吗? (3)Unsafe为什么是不安全的? (4)怎么使用Unsafe? 简介 本章是java并发包专题的第一章,但是第一篇写的却不是java并发包中类,而是java中的魔法类sun.misc.Unsafe。 Unsafe为我们提供了访问底层的机制,这种机制仅供java核心类库使用,而不应该被普通用户使用。 但是,为了更好地了解java的生态体系,我们应该去学习它,去了解它,不求深入到底层的C/C++代码,但求能了解它的基本功能。 获取Unsafe的实例 查看Unsafe的源码我们会发现它提供了一个getUnsafe()的静态方法。 @CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } } 但是,如果直接调用这个方法会抛出一个SecurityException异常,这是因为Unsafe仅供java内部类使用,外部类不应该使用它。 那么,我们就没有方法了吗? 当然不是,我们有反射啊!查看源码,我们发现它有一个属性叫theUnsafe,我们直接通过反射拿到它即可。 public class UnsafeTest { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); Unsafe unsafe = (Unsafe) f.get(null); } } 使用Unsafe实例化一个类 假如我们有一个简单的类如下: class User { int age; public User() { this.age = 10; } } 如果我们通过构造方法实例化这个类,age属性将会返回10。 User user1 = new User(); // 打印10 System.out.println(user1.age); 如果我们调用Unsafe来实例化呢? User user2 = (User) unsafe.allocateInstance(User.class); // 打印0 System.out.println(user2.age); age将返回0,因为Unsafe.allocateInstance()只会给对象分配内存,并不会调用构造方法,所以这里只会返回int类型的默认值0。 修改私有字段的值 使用Unsafe的putXXX()方法,我们可以修改任意私有字段的值。 public class UnsafeTest { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); Unsafe unsafe = (Unsafe) f.get(null); User user = new User(); Field age = user.getClass().getDeclaredField("age"); unsafe.putInt(user, unsafe.objectFieldOffset(age), 20); // 打印20 System.out.println(user.getAge()); } } class User { private int age; public User() { this.age = 10; } public int getAge() { return age; } } 一旦我们通过反射调用得到字段age,我们就可以使用Unsafe将其值更改为任何其他int值。(当然,这里也可以通过反射直接修改) 抛出checked异常 我们知道如果代码抛出了checked异常,要不就使用try...catch捕获它,要不就在方法签名上定义这个异常,但是,通过Unsafe我们可以抛出一个checked异常,同时却不用捕获或在方法签名上定义它。 // 使用正常方式抛出IOException需要定义在方法签名上往外抛 public static void readFile() throws IOException { throw new IOException(); } // 使用Unsafe抛出异常不需要定义在方法签名上往外抛 public static void readFileUnsafe() { unsafe.throwException(new IOException()); } 使用堆外内存 如果进程在运行过程中JVM上的内存不足了,会导致频繁的进行GC。理想情况下,我们可以考虑使用堆外内存,这是一块不受JVM管理的内存。 使用Unsafe的allocateMemory()我们可以直接在堆外分配内存,这可能非常有用,但我们要记住,这个内存不受JVM管理,因此我们要调用freeMemory()方法手动释放它。 假设我们要在堆外创建一个巨大的int数组,我们可以使用allocateMemory()方法来实现: class OffHeapArray { // 一个int等于4个字节 private static final int INT = 4; private long size; private long address; private static Unsafe unsafe; static { try { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); unsafe = (Unsafe) f.get(null); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } // 构造方法,分配内存 public OffHeapArray(long size) { this.size = size; // 参数字节数 address = unsafe.allocateMemory(size * INT); } // 获取指定索引处的元素 public int get(long i) { return unsafe.getInt(address + i * INT); } // 设置指定索引处的元素 public void set(long i, int value) { unsafe.putInt(address + i * INT, value); } // 元素个数 public long size() { return size; } // 释放堆外内存 public void freeMemory() { unsafe.freeMemory(address); } } 在构造方法中调用allocateMemory()分配内存,在使用完成后调用freeMemory()释放内存。 使用方式如下: OffHeapArray offHeapArray = new OffHeapArray(4); offHeapArray.set(0, 1); offHeapArray.set(1, 2); offHeapArray.set(2, 3); offHeapArray.set(3, 4); offHeapArray.set(2, 5); // 在索引2的位置重复放入元素 int sum = 0; for (int i = 0; i < offHeapArray.size(); i++) { sum += offHeapArray.get(i); } // 打印12 System.out.println(sum); offHeapArray.freeMemory(); 最后,一定要记得调用freeMemory()将内存释放回操作系统。 CompareAndSwap操作 JUC下面大量使用了CAS操作,它们的底层是调用的Unsafe的CompareAndSwapXXX()方法。这种方式广泛运用于无锁算法,与java中标准的悲观锁机制相比,它可以利用CAS处理器指令提供极大的加速。 比如,我们可以基于Unsafe的compareAndSwapInt()方法构建线程安全的计数器。 class Counter { private volatile int count = 0; private static long offset; private static Unsafe unsafe; static { try { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); unsafe = (Unsafe) f.get(null); offset = unsafe.objectFieldOffset(Counter.class.getDeclaredField("count")); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } public void increment() { int before = count; // 失败了就重试直到成功为止 while (!unsafe.compareAndSwapInt(this, offset, before, before + 1)) { before = count; } } public int getCount() { return count; } } 我们定义了一个volatile的字段count,以便对它的修改所有线程都可见,并在类加载的时候获取count在类中的偏移地址。 在increment()方法中,我们通过调用Unsafe的compareAndSwapInt()方法来尝试更新之前获取到的count的值,如果它没有被其它线程更新过,则更新成功,否则不断重试直到成功为止。 我们可以通过使用多个线程来测试我们的代码: Counter counter = new Counter(); ExecutorService threadPool = Executors.newFixedThreadPool(100); // 起100个线程,每个线程自增10000次 IntStream.range(0, 100) .forEach(i->threadPool.submit(()->IntStream.range(0, 10000) .forEach(j->counter.increment()))); threadPool.shutdown(); Thread.sleep(2000); // 打印1000000 System.out.println(counter.getCount()); park/unpark JVM在上下文切换的时候使用了Unsafe中的两个非常牛逼的方法park()和unpark()。 当一个线程正在等待某个操作时,JVM调用Unsafe的park()方法来阻塞此线程。 当阻塞中的线程需要再次运行时,JVM调用Unsafe的unpark()方法来唤醒此线程。 我们之前在分析java中的集合时看到了大量的LockSupport.park()/unpark(),它们底层都是调用的Unsafe的这两个方法。 总结 使用Unsafe几乎可以操作一切: (1)实例化一个类; (2)修改私有字段的值; (3)抛出checked异常; (4)使用堆外内存; (5)CAS操作; (6)阻塞/唤醒线程; 彩蛋 论实例化一个类的方式? (1)通过构造方法实例化一个类; (2)通过Class实例化一个类; (3)通过反射实例化一个类; (4)通过克隆实例化一个类; (5)通过反序列化实例化一个类; (6)通过Unsafe实例化一个类; public class InstantialTest { private static Unsafe unsafe; static { try { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); unsafe = (Unsafe) f.get(null); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } public static void main(String[] args) throws Exception { // 1. 构造方法 User user1 = new User(); // 2. Class,里面实际也是反射 User user2 = User.class.newInstance(); // 3. 反射 User user3 = User.class.getConstructor().newInstance(); // 4. 克隆 User user4 = (User) user1.clone(); // 5. 反序列化 User user5 = unserialize(user1); // 6. Unsafe User user6 = (User) unsafe.allocateInstance(User.class); System.out.println(user1.age); System.out.println(user2.age); System.out.println(user3.age); System.out.println(user4.age); System.out.println(user5.age); System.out.println(user6.age); } private static User unserialize(User user1) throws Exception { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://object.txt")); oos.writeObject(user1); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://object.txt")); // 反序列化 User user5 = (User) ois.readObject(); ois.close(); return user5; } static class User implements Cloneable, Serializable { private int age; public User() { this.age = 10; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } } } 欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

优秀的个人博客,低调大师

同步返回通知与异步通知区别解析

一,什么是同步通知 同步通知是指在请求参数中传入return_url参数,支付成功后跳转到return_url地址后携带的返回参数,如下可以在同步跳转的地址栏看到同步返通知: return_url参数 后面?拼接的就是同步返回参数。 注:目前在支付接口中只有手机网站支付的接口和电脑网站支付的接口可以传入return_url参数来接收到同步通知 其他的支付接口 如APP支付和当面付的接口都是不支持的。 二、什么是异步返通知 异步通知是指在请求参数中传入notify_url参数,在用户支付成功后,支付宝服务器会按照这个异步地址使用post方式给notify_url来发送交易信息,如下: gmt_create=2018-06-19[/url] 10:45:21&charset=UTF-8&seller_email=$$$&

优秀的个人博客,低调大师

Zookeeper详细参数解析zoo.cfg文件

Zookeeper分布式协调服务,在分布式架构中非常重要。比如我们搭建Kafka集群,或者Hadoop大数据集群。就需要使用到Zookeeper。Zookeeper本意是 动物管理员,动物饲养员。 ZooKeeper是一种分布式协调服务,提供集中式协调服务,用于维护配置信息,命名,提供分布式同步和选举服务。 zookeeper的配置文件包含许多可以配置的参数,这里在使用zookeeper的时候必须要明白每个参数的含义。zookeeper的配置文件在conf文件下的zoo.cfg。主要配置参数都在例子文件中可以看到,但是集群配置参数没有提供,特殊配置需要查看官方文档,每个版本不同可能存在差别。 #每个tick的毫秒数,每个确认消息持续2秒,等待应答。 tickTime= 2000 #初始同步阶段,可以发送的确认的最大的同步消息的数量 i

优秀的个人博客,低调大师

Tensorflow源码解析4 -- 图的节点 - Operation

1 概述 上文讲述了TensorFlow的核心对象,计算图Graph。Graph包含两大成员,节点和边。节点即为计算算子Operation,边则为计算数据Tensor。由起始节点Source出发,按照Graph的拓扑顺序,依次执行节点的计算,即可完成整图的计算,最后结束于终止节点Sink,并输出计算结果。 本文会对节点Operation进行详细讲解。 2 前端节点数据结构 在Python前端中,Operation表示Graph的节点,Tensor表示Graph的边。Operation包含OpDef和NodeDef两个主要成员变量。其中OpDef描述了op的静态属性信息,例如op入参列表,出参列表等。而NodeDef则描述op的动态属性信息,例如op运行的设备信息,用户给op设置的name等。 先来看Operation的数据结构,只列出重要代

优秀的个人博客,低调大师

Tensorflow源码解析3 -- TensorFlow核心对象 - Graph

1 Graph概述 计算图Graph是TensorFlow的核心对象,TensorFlow的运行流程基本都是围绕它进行的。包括图的构建、传递、剪枝、按worker分裂、按设备二次分裂、执行、注销等。因此理解计算图Graph对掌握TensorFlow运行尤为关键。 2 默认Graph 默认图替换 之前讲解Session的时候就说过,一个Session只能run一个Graph,但一个Graph可以运行在多个Session中。常见情况是,session会运行全局唯一的隐式的默认的Graph,operation也是注册到这个Graph中。 也可以显示创建Graph,并调用as_default()使他替换默认Graph。在该上下文管理器中创建的op都会注册到这个graph中。退出上下文管理器后,则恢复原来的默认graph。一般情况下,我们不用显式创建

优秀的个人博客,低调大师

Tensorflow源码解析5 -- 图的边 - Tensor

1 概述 前文两篇文章分别讲解了TensorFlow核心对象Graph,和Graph的节点Operation。Graph另外一大成员,即为其边Tensor。边用来表示计算的数据,它经过上游节点计算后得到,然后传递给下游节点进行运算。本文讲解Graph的边Tensor,以及TensorFlow中的变量。 2 前端边Tensor数据结构 Tensor作为Graph的边,使得节点Operation之间建立了连接。上游源节点Operation经过计算得到数据Tensor,然后传递给下游目标节点,是一个典型的生产者-消费者关系。下面来看Tensor的数据结构 @tf_export("Tensor") class Tensor(_TensorLike): def __init__(self, op, value_index, dtype):

优秀的个人博客,低调大师

《一起学 Sentinel》 原理-全解析

逅弈 转载请注明原创出处,谢谢! 系列文章 《一起学 Sentinel》 原理-调用链 《一起学 Sentinel》 原理-滑动窗口 《一起学 Sentinel》 原理-实体类 《一起学 Sentinel》 实战-限流篇 《一起学 Sentinel》 实战-控制台篇 《一起学 Sentinel》 实战-规则持久化 《一起学 Sentinel》 实战-集群限流篇 Sentinel 系列教程,现已上传到 github 和 gitee 中: GitHub:https://github.com/all4you/sentinel-tutorial Gitee:https://gitee.com/all_4_you/sentinel-tutorial Sentinel 是阿里中间件团队开源的,面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。 大家可能会问:Sentinel 和之前常用的熔断降级库 Netflix Hystrix 有什么异同呢?Sentinel官网有一个对比的文章,这里摘抄一个总结的表格,具体的对比可以点此 链接 查看。 对比内容 Sentinel Hystrix 隔离策略 信号量隔离 线程池隔离/信号量隔离 熔断降级策略 基于响应时间或失败比率 基于失败比率 实时指标实现 滑动窗口 滑动窗口(基于 RxJava) 规则配置 支持多种数据源 支持多种数据源 扩展性 多个扩展点 插件的形式 基于注解的支持 支持 支持 限流 基于 QPS,支持基于调用关系的限流 不支持 流量整形 支持慢启动、匀速器模式 不支持 系统负载保护 支持 不支持 控制台 开箱即用,可配置规则、查看秒级监控、机器发现等 不完善 常见框架的适配 Servlet、Spring Cloud、Dubbo、gRPC 等 Servlet、Spring Cloud Netflix 从对比的表格可以看到,Sentinel比Hystrix在功能性上还要强大一些,本文让我们一起来了解下Sentinel的源码,揭开Sentinel的神秘面纱。 项目结构 将Sentinel的源码fork到自己的github库中,接着把源码clone到本地,然后开始源码阅读之旅吧。 首先我们看一下Sentinel项目的整个结构: sentinel-core 核心模块,限流、降级、系统保护等都在这里实现 sentinel-dashboard 控制台模块,可以对连接上的sentinel客户端实现可视化的管理 sentinel-transport 传输模块,提供了基本的监控服务端和客户端的API接口,以及一些基于不同库的实现 sentinel-extension 扩展模块,主要对DataSource进行了部分扩展实现 sentinel-adapter 适配器模块,主要实现了对一些常见框架的适配 sentinel-demo 样例模块,可参考怎么使用sentinel进行限流、降级等 sentinel-benchmark 基准测试模块,对核心代码的精确性提供基准测试 运行样例 基本上每个框架都会带有样例模块,有的叫example,有的叫demo,sentinel也不例外。 那我们从sentinel的demo中找一个例子运行下看看大致的情况吧,上面说过了sentinel主要的核心功能是做限流、降级和系统保护,那我们就从“限流”开始看sentinel的实现原理吧。 可以看到sentinel-demo模块中有很多不同的样例,我们找到basic模块下的flow包,这个包下面就是对应的限流的样例,但是限流也有很多种类型的限流,我们就找根据qps限流的类看吧,其他的限流方式原理上都大差不差。 public class FlowQpsDemo { private static final String KEY = "abc"; private static AtomicInteger pass = new AtomicInteger(); private static AtomicInteger block = new AtomicInteger(); private static AtomicInteger total = new AtomicInteger(); private static volatile boolean stop = false; private static final int threadCount = 32; private static int seconds = 30; public static void main(String[] args) throws Exception { initFlowQpsRule(); tick(); // first make the system run on a very low condition simulateTraffic(); System.out.println("===== begin to do flow control"); System.out.println("only 20 requests per second can pass"); } private static void initFlowQpsRule() { List<FlowRule> rules = new ArrayList<FlowRule>(); FlowRule rule1 = new FlowRule(); rule1.setResource(KEY); // set limit qps to 20 rule1.setCount(20); // 设置限流类型:根据qps rule1.setGrade(RuleConstant.FLOW_GRADE_QPS); rule1.setLimitApp("default"); rules.add(rule1); // 加载限流的规则 FlowRuleManager.loadRules(rules); } private static void simulateTraffic() { for (int i = 0; i < threadCount; i++) { Thread t = new Thread(new RunTask()); t.setName("simulate-traffic-Task"); t.start(); } } private static void tick() { Thread timer = new Thread(new TimerTask()); timer.setName("sentinel-timer-task"); timer.start(); } static class TimerTask implements Runnable { @Override public void run() { long start = System.currentTimeMillis(); System.out.println("begin to statistic!!!"); long oldTotal = 0; long oldPass = 0; long oldBlock = 0; while (!stop) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } long globalTotal = total.get(); long oneSecondTotal = globalTotal - oldTotal; oldTotal = globalTotal; long globalPass = pass.get(); long oneSecondPass = globalPass - oldPass; oldPass = globalPass; long globalBlock = block.get(); long oneSecondBlock = globalBlock - oldBlock; oldBlock = globalBlock; System.out.println(seconds + " send qps is: " + oneSecondTotal); System.out.println(TimeUtil.currentTimeMillis() + ", total:" + oneSecondTotal + ", pass:" + oneSecondPass + ", block:" + oneSecondBlock); if (seconds-- <= 0) { stop = true; } } long cost = System.currentTimeMillis() - start; System.out.println("time cost: " + cost + " ms"); System.out.println("total:" + total.get() + ", pass:" + pass.get() + ", block:" + block.get()); System.exit(0); } } static class RunTask implements Runnable { @Override public void run() { while (!stop) { Entry entry = null; try { entry = SphU.entry(KEY); // token acquired, means pass pass.addAndGet(1); } catch (BlockException e1) { block.incrementAndGet(); } catch (Exception e2) { // biz exception } finally { total.incrementAndGet(); if (entry != null) { entry.exit(); } } Random random2 = new Random(); try { TimeUnit.MILLISECONDS.sleep(random2.nextInt(50)); } catch (InterruptedException e) { // ignore } } } } } 执行上面的代码后,打印出如下的结果: 可以看到,上面的结果中,pass的数量和我们的预期并不相同,我们预期的是每秒允许pass的请求数是20个,但是目前有很多pass的请求数是超过20个的。 原因是,我们这里测试的代码使用了多线程,注意看 threadCount 的值,一共有32个线程来模拟,而在RunTask的run方法中执行资源保护时,即在 SphU.entry 的内部是没有加锁的,所以就会导致在高并发下,pass的数量会高于20。 可以用下面这个模型来描述下,有一个TimeTicker线程在做统计,每1秒钟做一次。有N个RunTask线程在模拟请求,被访问的business code被资源key保护着,根据规则,每秒只允许20个请求通过。 由于pass、block、total等计数器是全局共享的,而多个RunTask线程在执行SphU.entry申请获取entry时,内部没有锁保护,所以会存在pass的个数超过设定的阈值。 那为了证明在单线程下限流的正确性与可靠性,那我们的模型就应该变成了这样: 那接下来我把 threadCount 的值改为1,只有一个线程来执行这个方法,看下具体的限流结果,执行上面的代码后打印的结果如下: 可以看到pass数基本上维持在20,但是第一次统计的pass值还是超过了20。这又是什么原因导致的呢? 其实仔细看下Demo中的代码可以发现,模拟请求是用的一个线程,统计结果是用的另外一个线程,统计线程每1秒钟统计一次结果,这两个线程之间是有时间上的误差的。从TimeTicker线程打印出来的时间戳可以看出来,虽然每隔一秒进行统计,但是当前打印时的时间和上一次的时间还是有误差的,不完全是1000ms的间隔。 要真正验证每秒限制20个请求,保证数据的精准性,需要做基准测试,这个不是本篇文章的重点,有兴趣的同学可以去了解下jmh,sentinel中的基准测试也是通过jmh做的。 深入原理 通过一个简单的示例程序,我们了解了sentinel可以对请求进行限流,除了限流外,还有降级和系统保护等功能。那现在我们就拨开云雾,深入源码内部去一窥sentinel的实现原理吧。 首先从入口开始:SphU.entry() 。这个方法会去申请一个entry,如果能够申请成功,则说明没有被限流,否则会抛出BlockException,表面已经被限流了。 从 SphU.entry() 方法往下执行会进入到 Sph.entry() ,Sph的默认实现类是 CtSph ,在CtSph中最终会执行到 entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException 这个方法。 我们来看一下这个方法的具体实现: public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException { Context context = ContextUtil.getContext(); if (context instanceof NullContext) { // Init the entry only. No rule checking will occur. return new CtEntry(resourceWrapper, null, context); } if (context == null) { context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType()); } // Global switch is close, no rule checking will do. if (!Constants.ON) { return new CtEntry(resourceWrapper, null, context); } // 获取该资源对应的SlotChain ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper); /* * Means processor cache size exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE}, so no * rule checking will be done. */ if (chain == null) { return new CtEntry(resourceWrapper, null, context); } Entry e = new CtEntry(resourceWrapper, chain, context); try { // 执行Slot的entry方法 chain.entry(context, resourceWrapper, null, count, args); } catch (BlockException e1) { e.exit(count, args); // 抛出BlockExecption throw e1; } catch (Throwable e1) { RecordLog.info("Sentinel unexpected exception", e1); } return e; } 这个方法可以分为以下几个部分: 1.对参数和全局配置项做检测,如果不符合要求就直接返回了一个CtEntry对象,不会再进行后面的限流检测,否则进入下面的检测流程。 2.根据包装过的资源对象获取对应的SlotChain 3.执行SlotChain的entry方法 3.1.如果SlotChain的entry方法抛出了BlockException,则将该异常继续向上抛出 3.2.如果SlotChain的entry方法正常执行了,则最后会将该entry对象返回 4.如果上层方法捕获了BlockException,则说明请求被限流了,否则请求能正常执行 其中比较重要的是第2、3两个步骤,我们来分解一下这两个步骤。 创建SlotChain 首先看一下lookProcessChain的方法实现: private ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) { ProcessorSlotChain chain = chainMap.get(resourceWrapper); if (chain == null) { synchronized (LOCK) { chain = chainMap.get(resourceWrapper); if (chain == null) { // Entry size limit. if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) { return null; } // 具体构造chain的方法 chain = Env.slotsChainbuilder.build(); Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(chainMap.size() + 1); newMap.putAll(chainMap); newMap.put(resourceWrapper, chain); chainMap = newMap; } } } return chain; } 该方法使用了一个HashMap做了缓存,key是资源对象。这里加了锁,并且做了 double check 。具体构造chain的方法是通过: Env.slotsChainbuilder.build() 这句代码创建的。那就进入这个方法看看吧。 public ProcessorSlotChain build() { ProcessorSlotChain chain = new DefaultProcessorSlotChain(); chain.addLast(new NodeSelectorSlot()); chain.addLast(new ClusterBuilderSlot()); chain.addLast(new LogSlot()); chain.addLast(new StatisticSlot()); chain.addLast(new SystemSlot()); chain.addLast(new AuthoritySlot()); chain.addLast(new FlowSlot()); chain.addLast(new DegradeSlot()); return chain; } Chain是链条的意思,从build的方法可看出,ProcessorSlotChain是一个链表,里面添加了很多个Slot。具体的实现需要到DefaultProcessorSlotChain中去看。 public class DefaultProcessorSlotChain extends ProcessorSlotChain { AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() { @Override public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args) throws Throwable { super.fireEntry(context, resourceWrapper, t, count, args); } @Override public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { super.fireExit(context, resourceWrapper, count, args); } }; AbstractLinkedProcessorSlot<?> end = first; @Override public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) { protocolProcessor.setNext(first.getNext()); first.setNext(protocolProcessor); if (end == first) { end = protocolProcessor; } } @Override public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) { end.setNext(protocolProcessor); end = protocolProcessor; } } DefaultProcessorSlotChain中有两个AbstractLinkedProcessorSlot类型的变量:first和end,这就是链表的头结点和尾节点。 创建DefaultProcessorSlotChain对象时,首先创建了首节点,然后把首节点赋值给了尾节点,可以用下图表示: 将第一个节点添加到链表中后,整个链表的结构变成了如下图这样: 将所有的节点都加入到链表中后,整个链表的结构变成了如下图所示: 这样就将所有的Slot对象添加到了链表中去了,每一个Slot都是继承自AbstractLinkedProcessorSlot。而AbstractLinkedProcessorSlot是一种责任链的设计,每个对象中都有一个next属性,指向的是另一个AbstractLinkedProcessorSlot对象。其实责任链模式在很多框架中都有,比如Netty中是通过pipeline来实现的。 知道了SlotChain是如何创建的了,那接下来就要看下是如何执行Slot的entry方法的了。 执行SlotChain的entry方法 lookProcessChain方法获得的ProcessorSlotChain的实例是DefaultProcessorSlotChain,那么执行chain.entry方法,就会执行DefaultProcessorSlotChain的entry方法,而DefaultProcessorSlotChain的entry方法是这样的: @Override public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args) throws Throwable { first.transformEntry(context, resourceWrapper, t, count, args); } 也就是说,DefaultProcessorSlotChain的entry实际是执行的first属性的transformEntry方法。 而transformEntry方法会执行当前节点的entry方法,在DefaultProcessorSlotChain中first节点重写了entry方法,具体如下: @Override public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args) throws Throwable { super.fireEntry(context, resourceWrapper, t, count, args); } first节点的entry方法,实际又是执行的super的fireEntry方法,那继续把目光转移到fireEntry方法,具体如下: @Override public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) throws Throwable { if (next != null) { next.transformEntry(context, resourceWrapper, obj, count, args); } } 从这里可以看到,从fireEntry方法中就开始传递执行entry了,这里会执行当前节点的下一个节点transformEntry方法,上面已经分析过了,transformEntry方法会触发当前节点的entry,也就是说fireEntry方法实际是触发了下一个节点的entry方法。具体的流程如下图所示: 从图中可以看出,从最初的调用Chain的entry()方法,转变成了调用SlotChain中Slot的entry()方法。从上面的分析可以知道,SlotChain中的第一个Slot节点是NodeSelectorSlot。 执行Slot的entry方法 现在可以把目光转移到SlotChain中的第一个节点NodeSelectorSlot的entry方法中去了,具体的代码如下: @Override public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) throws Throwable { DefaultNode node = map.get(context.getName()); if (node == null) { synchronized (this) { node = map.get(context.getName()); if (node == null) { node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null); HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size()); cacheMap.putAll(map); cacheMap.put(context.getName(), node); map = cacheMap; } // Build invocation tree ((DefaultNode)context.getLastNode()).addChild(node); } } context.setCurNode(node); // 由此触发下一个节点的entry方法 fireEntry(context, resourceWrapper, node, count, args); } 从代码中可以看到,NodeSelectorSlot节点做了一些自己的业务逻辑处理,具体的大家可以深入源码继续追踪,这里大概的介绍下每种Slot的功能职责: NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级; ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据; StatistcSlot 则用于记录,统计不同纬度的 runtime 信息; FlowSlot 则用于根据预设的限流规则,以及前面 slot 统计的状态,来进行限流; AuthorizationSlot 则根据黑白名单,来做黑白名单控制; DegradeSlot 则通过统计信息,以及预设的规则,来做熔断降级; SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量; 执行完业务逻辑处理后,调用了fireEntry()方法,由此触发了下一个节点的entry方法。此时我们就知道了sentinel的责任链就是这样传递的:每个Slot节点执行完自己的业务后,会调用fireEntry来触发下一个节点的entry方法。 所以可以将上面的图完整了,具体如下: 至此就通过SlotChain完成了对每个节点的entry()方法的调用,每个节点会根据创建的规则,进行自己的逻辑处理,当统计的结果达到设置的阈值时,就会触发限流、降级等事件,具体是抛出BlockException异常。 总结 sentinel主要是基于7种不同的Slot形成了一个链表,每个Slot都各司其职,自己做完分内的事之后,会把请求传递给下一个Slot,直到在某一个Slot中命中规则后抛出BlockException而终止。 前三个Slot负责做统计,后面的Slot负责根据统计的结果结合配置的规则进行具体的控制,是Block该请求还是放行。 控制的类型也有很多可选项:根据qps、线程数、冷启动等等。 然后基于这个核心的方法,衍生出了很多其他的功能: 1、dashboard控制台,可以可视化的对每个连接过来的sentinel客户端 (通过发送heartbeat消息)进行控制,dashboard和客户端之间通过http协议进行通讯。 2、规则的持久化,通过实现DataSource接口,可以通过不同的方式对配置的规则进行持久化,默认规则是在内存中的 3、对主流的框架进行适配,包括servlet,dubbo,rRpc等 Dashboard控制台 sentinel-dashboard是一个单独的应用,通过spring-boot进行启动,主要提供一个轻量级的控制台,它提供机器发现、单机资源实时监控、集群资源汇总,以及规则管理的功能。 我们只需要对应用进行简单的配置,就可以使用这些功能。 1 启动控制台 1.1 下载代码并编译控制台 下载 控制台 工程 使用以下命令将代码打包成一个 fat jar: mvn clean package 1.2 启动 使用如下命令启动编译后的控制台: $ java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -jar target/sentinel-dashboard.jar 上述命令中我们指定了一个JVM参数,-Dserver.port=8080 用于指定 Spring Boot 启动端口为 8080。 2 客户端接入控制台 控制台启动后,客户端需要按照以下步骤接入到控制台。 2.1 引入客户端jar包 通过 pom.xml 引入 jar 包: <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-transport-simple-http</artifactId> <version>x.y.z</version> </dependency> 2.2 配置启动参数 启动时加入 JVM 参数 -Dcsp.sentinel.dashboard.server=consoleIp:port 指定控制台地址和端口。若启动多个应用,则需要通过 -Dcsp.sentinel.api.port=xxxx 指定客户端监控 API 的端口(默认是 8719)。 除了修改 JVM 参数,也可以通过配置文件取得同样的效果。更详细的信息可以参考 启动配置项。 2.3 触发客户端初始化 确保客户端有访问量,Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包。 sentinel-dashboard是一个独立的web应用,可以接受客户端的连接,然后与客户端之间进行通讯,他们之间使用http协议进行通讯。他们之间的关系如下图所示: dashboard dashboard启动后会等待客户端的连接,具体的做法是在 MachineRegistryController 中有一个 receiveHeartBeat 的方法,客户端发送心跳消息,就是通过http请求这个方法。 dashboard接收到客户端的心跳消息后,会把客户端的传递过来的ip、port等信息封装成一个 MachineInfo 对象,然后将该对象通过 MachineDiscovery 接口的 addMachine 方法添加到一个ConcurrentHashMap中保存起来。 这里会有问题,因为客户端的信息是保存在dashboard的内存中的,所以当dashboard应用重启后,之前已经发送过来的客户端信息都会丢失掉。 client client在启动时,会通过CommandCenterInitFunc选择一个,并且只选择一个CommandCenter进行启动。 启动之前会通过spi的方式扫描获取到所有的CommandHandler的实现类,然后将所有的CommandHandler注册到一个HashMap中去,待后期使用。 PS:考虑一下,为什么CommandHandler不需要做持久化,而是直接保存在内存中。 注册完CommandHandler之后,紧接着就启动CommandCenter了,目前CommandCenter有两个实现类: SimpleHttpCommandCenter 通过ServerSocket启动一个服务端,接受socket连接 NettyHttpCommandCenter 通过Netty启动一个服务端,接受channel连接 CommandCenter启动后,就等待dashboard发送消息过来了,当接收到消息后,会把消息通过具体的CommandHandler进行处理,然后将处理的结果返回给dashboard。 这里需要注意的是,dashboard给client发送消息是通过异步的httpClient进行发送的,在HttpHelper类中。 但是诡异的是,既然通过异步发送了,又通过一个CountDownLatch来等待消息的返回,然后获取结果,那这样不就失去了异步的意义的吗?具体的代码如下: private String httpGetContent(String url) { final HttpGet httpGet = new HttpGet(url); final CountDownLatch latch = new CountDownLatch(1); final AtomicReference<String> reference = new AtomicReference<>(); httpclient.execute(httpGet, new FutureCallback<HttpResponse>() { @Override public void completed(final HttpResponse response) { try { reference.set(getBody(response)); } catch (Exception e) { logger.info("httpGetContent " + url + " error:", e); } finally { latch.countDown(); } } @Override public void failed(final Exception ex) { latch.countDown(); logger.info("httpGetContent " + url + " failed:", ex); } @Override public void cancelled() { latch.countDown(); } }); try { latch.await(5, TimeUnit.SECONDS); } catch (Exception e) { logger.info("wait http client error:", e); } return reference.get(); } 主流框架的适配 sentinel也对一些主流的框架进行了适配,使得在使用主流框架时,也可以享受到sentinel的保护。目前已经支持的适配器包括以下这些: Web Servlet Dubbo Spring Boot / Spring Cloud gRPC Apache RocketMQ 其实做适配就是通过那些主流框架的扩展点,然后在扩展点上加入sentinel限流降级的代码即可。拿Servlet的适配代码看一下,具体的代码是: public class CommonFilter implements Filter { @Override public void init(FilterConfig filterConfig) { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest sRequest = (HttpServletRequest)request; Entry entry = null; try { // 根据请求生成的资源 String target = FilterUtil.filterTarget(sRequest); target = WebCallbackManager.getUrlCleaner().clean(target); // “申请”该资源 ContextUtil.enter(target); entry = SphU.entry(target, EntryType.IN); // 如果能成功“申请”到资源,则说明未被限流 // 则将请求放行 chain.doFilter(request, response); } catch (BlockException e) { // 否则如果捕获了BlockException异常,说明请求被限流了 // 则将请求重定向到一个默认的页面 HttpServletResponse sResponse = (HttpServletResponse)response; WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse); } catch (IOException e2) { // 省略部分代码 } finally { if (entry != null) { entry.exit(); } ContextUtil.exit(); } } @Override public void destroy() { } } 通过Servlet的Filter进行扩展,实现一个Filter,然后在doFilter方法中对请求进行限流控制,如果请求被限流则将请求重定向到一个默认页面,否则将请求放行给下一个Filter。 规则持久化,动态化 Sentinel 的理念是开发者只需要关注资源的定义,当资源定义成功,可以动态增加各种流控降级规则。 Sentinel 提供两种方式修改规则: 通过 API 直接修改 (loadRules) 通过DataSource适配不同数据源修改 通过 API 修改比较直观,可以通过以下三个 API 修改不同的规则: FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控规则 DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降级规则 SystemRuleManager.loadRules(List<SystemRule> rules); // 修改系统规则 DataSource 扩展 上述 loadRules() 方法只接受内存态的规则对象,但应用重启后内存中的规则就会丢失,更多的时候规则最好能够存储在文件、数据库或者配置中心中。 DataSource 接口给我们提供了对接任意配置源的能力。相比直接通过 API 修改规则,实现 DataSource 接口是更加可靠的做法。 官方推荐通过控制台设置规则后将规则推送到统一的规则中心,用户只需要实现 DataSource 接口,来监听规则中心的规则变化,以实时获取变更的规则。 DataSource 拓展常见的实现方式有: 拉模式:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 SQL、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更; 推模式:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。 至此,sentinel的基本情况都已经分析了,更加详细的内容,可以继续阅读源码来研究。

优秀的个人博客,低调大师

深入解析:从源码窥探MySQL优化器

上图展示了优化器的大致执行过程,可以简单描述为: 1 根据语法树及统计统计,构建初始表访问数组(init_plan_arrays) 2 根据表访问数组,计算每个表的最佳访问路径(find_best_ref),同时保存当前最优执行计划(COST最小) 3 如果找到更优的执行计划则更新最优执行计划,否则优化结束。 从上述流程可以看出,执行计划的生成是一个“动态规划/贪心算法”的过程,动态规划公式可以表示为:Min(Cost(Tn+1)) = Min(Cost(T1))+Min(Cost(T2))+...Min(Cost(Tn-1))+Min(Cost(Tn)),其中Cost(Tn)表示访问表T1 T2一直到Tn的代价。如果优化器没有任何先验知识,则需要进行 A(n,n) 次循环,是一个全排列过程,很显然优化器是有先验知识的,如表大小,外连接,子查询等都会使得表的访问是部分有序的,可以理解为一个“被裁减”的动态规划,动态规则的核心函数为:Join::Best_extention_limited_search,在源码中有如下代码结构: bool Optimize_table_order::best_extension_by_limited_search( table_map remaining_tables, uint idx, uint current_search_depth) { for (JOIN_TAB **pos= join->best_ref + idx; *pos; pos++) { ...... best_access_path(s, remaining_tables, idx, false, idx ? (position-1)->prefix_rowcount : 1.0, position); ...... if (best_extension_by_limited_search(remaining_tables_after, idx + 1, current_search_depth - 1)) ...... backout_nj_state(remaining_tables, s); ...... } } 以上代码是在一个for循环中递归搜索,这是一个典型的全排列的算法。 02优化器参数 MySQL的优化器对于Oracle来说还显得比较幼稚。Oracle有着各种丰富的统计信息,比如系统统计信息,表统计信息,索引统计信息等,而MySQL则需要更多的常量,其中MySQL5.7提供了表mysql.server_cost和表mysql.engine_cost,可以供用户配置,使得用户能够调整优化器模型,下面就几个常见而又非常重要的参数进行介绍: 1 #define ROW_EVALUATE_COST 0.2f 计算符合条件的行的代价,行数越多,代价越大 2 #define IO_BLOCK_READ_COST 1.0f 从磁盘读取一个Page的代价 3 #define MEMORY_BLOCK_READ_COST 1.0f 从内存读取一个Page的代价,对于Innodb来说,表示从一个Buffer Pool读取一个Page的代价,因此读取内存页和磁盘页的默认代价是一样的 4 #define COND_FILTER_EQUALITY 0.1f 等值过滤条件默认值为0.1,例如name = ‘lily’, 表大小为100,则返回10行数据 5 #define COND_FILTER_INEQUALITY 0.3333f 非等值过滤条件的默认值是0.3333,例如col1>col2 6 #define COND_FILTER_BETWEEN 0.1111f Between过滤的默认值是0.1111f,例如:col1 between a and b ...... 这样的常量很多,涉及到过滤条件、JOIN缓存、临时表等等各种代价,理解这些常量后,看到执行计划的Cost后,你会有种豁然开朗的感觉! 03 优化器选项 在MySQL中,执行select @@optimizer_trace, 得到如下参数: index_merge=on,index_merge_union=off,index_merge_sort_union=off, index_merge_intersection=on, engine_condition_pushdown=on, index_condition_pushdown=on, mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,subquery_materialization_cost_based=on, use_index_extensions=on, condition_fanout_filter=on 04 Optimize Trace是如何生成的? 在流程图中的函数中,存在大量如下代码: Opt_trace_object trace_ls(trace, "searching_loose_scan_index"); 因此,在优化器运行过程中,优化器的执行路径也被保存在Opt_trace_object中,进而保存在information_schema.optimizer_trace中,方便用户查询和跟踪。 05 优化器的典型使用场景 5.1 全表扫描 select * from sakila.actor; 表actor统计信息如下:执行计划: { "query_block": { "select_id": 1, "cost_info": { "query_cost": "41.00" }, "table": { "table_name": "actor", "access_type": "ALL", "rows_examined_per_scan": 200, "rows_produced_per_join": 200, "filtered": "100.00", "cost_info": { "read_cost": "1.00", "eval_cost": "40.00", "prefix_cost": "41.00", "data_read_per_join": "56K" }, "used_columns": [ "actor_id", "first_name", "last_name", "last_update", "id" ] } } } IO_COST = CLUSTER_INDEX_SIZE * PAGE_READ_TIME = 1 * 1 =1; EVAL_COST = TABLE_ROWS*EVALUATE_COST = 200 * 0.2 =40; PREFIX_COST = IO_COST + EVAL_COST; 注意以上过程忽略了内存页和磁盘页的访问代价差异。 5.2 表连接时使用全表扫描 SELECT * FROM sakila.actor a, sakila.film_actor b WHERE a.actor_id = b.actor_id { "query_block": { "select_id": 1, "cost_info": { "query_cost": "1338.07" }, "nested_loop": [ { "table": { "table_name": "a", "access_type": "ALL", "possible_keys": [ "PRIMARY" ], "rows_examined_per_scan": 200, "rows_produced_per_join": 200, "filtered": "100.00", "cost_info": { "read_cost": "1.00", "eval_cost": "40.00", "prefix_cost": "41.00", "data_read_per_join": "54K" }, "used_columns": [ "actor_id", "first_name", "last_name", "last_update" ] } }, { "table": { "table_name": "b", "access_type": "ref", "possible_keys": [ "PRIMARY" ], "key": "PRIMARY", "used_key_parts": [ "actor_id" ], "key_length": "2", "ref": [ "sakila.a.actor_id" ], "rows_examined_per_scan": 27, "rows_produced_per_join": 5461, "filtered": "100.00", "cost_info": { "read_cost": "204.67", "eval_cost": "1092.40", "prefix_cost": "1338.07", "data_read_per_join": "85K" }, "used_columns": [ "actor_id", "film_id", "last_update" ] } } ] } } 第一张表actor的全表扫代价为41,可以参考示例1。 第二个表就是NET LOOP 代价: read_cost(204.67) =prefix_rowcount (1 + keys_per_value/table_rowscluster_index_size = 200 (1+27/1386312)*1 注意:27 相当于对于每个actor_id,film_actor的索引估计,对于每个actor_id,平均有27条记录=5462/200 Table_rows是如何计算的呢? Film_actor表的实际记录数是5462,一共12个page,11个叶子页,总大小为1116K(默认页大小)=180224Byte, 最小记录长度为26(通过计算字段长度可得),13863 = 180224/262, 2是安全因子,做最差的代价估计。 表连接返回行数=2005462/200,因此行估算代价为54620.2=1902.4 5.3 IN查询 表film_actor中索引idx_id(film_id)统计信息如下: EXPLAIN SELECT * FROM ACTOR WHERE actor_id IN (SELECT film_id FROM film_actor) { "query_block": { "select_id": 1, "cost_info": { "query_cost": "460.79" }, "nested_loop": [ { "table": { "table_name": "ACTOR", "access_type": "ALL", "possible_keys": [ "PRIMARY" ], "rows_examined_per_scan": 200, "rows_produced_per_join": 200, "filtered": "100.00", "cost_info": { "read_cost": "1.00", "eval_cost": "40.00", "prefix_cost": "41.00", "data_read_per_join": "56K" }, "used_columns": [ "actor_id", "first_name", "last_name", "last_update", "id" ] } }, { "table": { "table_name": "film_actor", "access_type": "ref", "possible_keys": [ "idx_id" ], "key": "idx_id", "used_key_parts": [ "film_id" ], "key_length": "2", "ref": [ "sakila.ACTOR.actor_id" ], "rows_examined_per_scan": 5, "rows_produced_per_join": 200, "filtered": "100.00", "using_index": true, "first_match": "ACTOR", "cost_info": { "read_cost": "200.66", "eval_cost": "40.00", "prefix_cost": "460.79", "data_read_per_join": "3K" }, "used_columns": [ "film_id" ], "attached_condition": "(`sakila`.`actor`.`actor_id` = `sakila`.`film_actor`.`film_id`)" } } ] } } id select_type table partitions type possible_keys key key_len ref rows filtered Extra ------ ----------- ---------- ---------- ------ ------------- ------ ------- --------------------- ------ -------- --------------------------------------------- 1 SIMPLE ACTOR (NULL) ALL PRIMARY (NULL) (NULL) (NULL) 200 100.00 (NULL) 1 SIMPLE film_actor (NULL) ref idx_id idx_id 2 sakila.ACTOR.actor_id 5 100.00 Using where; Using index; FirstMatch(ACTOR) 从执行计划中可以看出,MySQL采用FirstMatch方式。在MySQL中,半链接优化方式为:Materialization Strategy,LooseScan,FirstMatch,DuplicateWeedout,默认情况下四种优化方式都是存在的,选取方式基于最小COST。现在我们以FirstMatch为例,讲解优化器的执行流程。 SQL如下: select * from Country where Country.code IN (select City.Country from City where City.Population > 1*1000*1000) and Country.continent='Europe' 从上图可以看出,FirstMatch是通过判断记录是否已经在结果集中存在来减少查询和匹配流程。 表actor的访问代价可以参考示例1. 表film_actor表的访问代价200.66是如何计算的呢? 访问表film_actor中索引字段film_id,MySQL会走覆盖索引扫,即IDEX_ONLY_SCAN,一次索引访问的代价是如何计算的呢? 参考函数double handler::index_only_read_time(uint keynr, double records) 索引块大小为16K,并且MySQL假设块都是半满的,则一个块能够存放的索引记录数为: 16K/2/(索引长度+主键长度(注:二级索引存储的是主键的引用))=16K/2/(2+4)+1=1366, 其中主键为(actor_id,film_id),两个字段都是smallint,占用4个字节,而索引idx_id(film_id)是2个字节,因此每次访问索引的代价为:(5.47+1366-1)/1366 = 1.0032, 访问film_actor表一共需要200次,总访问代价为:200*1.0032=200.66 总代价460.79 = 表actor的访问代价+表film_actor访问代价+行估算代价= 41+200.66+20015.47102,其中两个1分别表示过滤因子,由于两个表均没有过滤条件因此过滤因子都是1。 原文发布时间为:2018-12-16本文作者: 汤爱中本文来自云栖社区合作伙伴“ 数据和云”,了解相关信息可以关注“OraNews”微信公众号

优秀的个人博客,低调大师

Android: 在native中访问assets全解析

在CMake中添加相关NDK LIB的 依赖 因为我们接下来用到的一些函数实现在NDK库libandroid.so中,因此我们直接在CMakeList.txt中添加对其依赖即可: target_link_libraries( # Specifies the target library. native-lib #lib to link android # other libs ) 如果没有添加此依赖,显然会提示undefined reference错误,比如: error: undefined reference to 'AAssetManager_fromJava' error: undefined reference to 'AAssetManager_open' error: undefined reference to 'AAsset_getLength' error: undefined reference to 'AAsset_getBuffer' error: undefined reference to 'AAsset_close' error: undefined reference to 'AAssetManager_open' error: undefined reference to 'AAsset_getLength' error: undefined reference to 'AAsset_openFileDescriptor' error: undefined reference to 'AAsset_close' error: undefined reference to 'AAssetManager_openDir' error: undefined reference to 'AAssetDir_getNextFileName' error: undefined reference to 'AAssetManager_open' 获得AssetManager 在Java中,我们可以通过Context.getAssets()轻松获得AssetManager。在NDK中,提供了AAssetManager_fromJava来获得Native中对应的AAssetManager。顾名思义,fromJava,肯定是要从Java层获取了,也即意味着要通过JNI来获得。代码如下: /***code in Java, such as MainActivity.java***/ //decale the jni func public native void setNativeAssetManager(AssetManager assetManager); //call it, such as during Activity.onCreate() setNativeAssetManager(getAssets()); /***end of java***/ /***code in native c++***/ extern "C" JNIEXPORT void JNICALL Java_willhua_androidstudioopencl_MainActivity_setNativeAssetManager( JNIEnv *env, jobject instance, jobject assetManager) { AAssetManager *nativeasset = AAssetManager_fromJava(env, assetManager); //the use of nativeasset } 下面所有的代码都是在Java_willhua_androidstudioopencl_MainActivity_setNativeAssetManager内实现。 访问assets下的文件 我们知道,assets文件夹下面是可以有子文件夹的,因为,下面我以读取图片为例,介绍各种情况的访问方法。例子中用到OpenCV的相关方法,在此不介绍,自行了解。 测试用assets文件夹目录: 已知完整路径的访问 如果我们已经知道assets下某个文件的完整路径,比如"sz.jpg","dir/cs.jpg",那么我们可以直接把这个路径传给AAssetManager_open来获得AAsset. //open file AAsset *assetFile = AAssetManager_open(nativeasset, "sz.jpg", AASSET_MODE_BUFFER); //this will also be ok //AAsset *assetFile = AAssetManager_open(nativeasset, "dir/cs.jpg", AASSET_MODE_BUFFER); //get file length size_t fileLength = AAsset_getLength(assetFile); char *dataBuffer2 = (char *) malloc(fileLength); //read file data AAsset_read(assetFile, dataBuffer2, fileLength); //the data has been copied to dataBuffer2, so , close it AAsset_close(assetFile); //decode the file data to cv::Mat std::vector<char> vec2(dataBuffer2, dataBuffer2 + fileLength); cv::Mat mat2 = cv::imdecode(vec2, CV_LOAD_IMAGE_COLOR); LOGD("asset file %d x %d %d", mat2.cols, mat2.rows, mat2.channels()); //free malloc free(dataBuffer2); 获取文件下的名字并访问之 如果我们只知道文件夹的名字,但并不知道文件夹下面有哪些具体文件,比如我们只知道有个dir文件夹,但不知道下面的具体情况。那么我们可以使用AAssetDir_getNextFileName来获取文件夹的文件名。但是有个问题,这个方法只能获得文件夹下的文件名,而无法获得子文件夹,有哪位知道的请告知。注意:AAssetDir_getNextFileName只返回文件名,而不是该文件的完整路径,比如只会返回cs.jpg,而不是dir/cs.jpg,所以如果直接把AAssetDir_getNextFileName的返回结果传给AAssetManager_open会读取不到正确的文件,返回NULL. AAssetDir *assetDir = AAssetManager_openDir(nativeasset, "dir"); const char *filename = AAssetDir_getNextFileName(assetDir); while (filename != NULL){ char fullname[1024]; sprintf(fullname, "dir/%s", filename); //get the full path AAsset *file = AAssetManager_open(nativeasset, fullname, AASSET_MODE_BUFFER); if(file == NULL){ LOGD("FILE NULL %s", filename); break; } size_t fileLength = AAsset_getLength(file); LOGD("filename next:%s, size:%d", filename, fileLength); char *buffer = (char*)malloc(fileLength); AAsset_read(file, buffer, fileLength); AAsset_close(file); //do something with the buffer free(buffer); filename = AAssetDir_getNextFileName(assetDir); } AAssetDir_close(assetDir); //remember to close it 使用AAsset_getBuffer读整个文件内容 在上面的case中,我们拿到AAsset之后都是malloc内存,然后把文件信息读出来的形式,其实这种方式适合不一次性读取整个文件内容的情况,按照官网的说法就是: Attempt to read 'count' bytes of data from the current offset. 也就是AAsset_read应该配合AAsset_seek使用更美味。 对于一次性读取整个文件的内容,更好的方式是使用AAsset_getBuffer AAsset *assetFile = AAssetManager_open(nativeasset, "sz.jpg", AASSET_MODE_BUFFER); size_t fileLength = AAsset_getLength(assetFile); const char *dataBuffer2 =(const char *) AAsset_getBuffer(assetFile); std::vector<char> vec2(dataBuffer2, dataBuffer2 + fileLength); cv::Mat mat2 = cv::imdecode(vec2, CV_LOAD_IMAGE_COLOR); LOGD("asset file lenght:%d mat: %d x %d %d", fileLength, mat2.cols, mat2.rows, mat2.channels()); AAsset_close(assetFile); 以FileDescriptor的方式来读取 我们可以使用AAsset_openFileDescriptor来获取FileDescriptor,然后再进行其他操作。需要注意的是,AAsset_openFileDescriptor返回当前fd的起始seek位置start以及文件长度length。在读取内容之前记得要先seek到start,否则会发现文件内容不对。见代码中的lseek. AAsset *assetFile = AAssetManager_open(nativeasset, "sz.jpg", AASSET_MODE_BUFFER); size_t fileLength = AAsset_getLength(assetFile); LOGD("before fd fileLength:%d",fileLength); off_t start = 0, length = 0; int fd = AAsset_openFileDescriptor(assetFile, &start, &length); LOGD("fd:%d start:%d length:%d", fd, start, length); lseek(fd, start, SEEK_CUR); //NOTICE char *dataBuffer = (char*)malloc(fileLength); memset(dataBuffer, 0, fileLength); read(fd, dataBuffer, fileLength); close(fd); //close fd LOGD("read_ %d %d %d", dataBuffer[0], dataBuffer[1], dataBuffer[2]); std::vector<char> vec2(dataBuffer, dataBuffer + fileLength); cv::Mat mat2 = cv::imdecode(vec2, CV_LOAD_IMAGE_COLOR); LOGD("use fd mat:%d x %d %d", mat2.cols, mat2.rows, mat2.channels()); AAsset_close(assetFile); 获得fd之后,也可以通过他来获得一个FILE:FILE * file = fdopen(fd, "rb");但是一定要记得fclose(file)。总的来说不如read方便。 open mode AAssetManager_open需要传入一个mode参数,各参数的含义如下,按需使用。 AASSET_MODE_UNKNOWN: Not known how the data is to be accessed AASSET_MODE_RANDOM: Read chunks, and seek forward and backward AASSET_MODE_STREAMING: Read sequentially, with an occasional forward seek AASSET_MODE_BUFFER: Attempt to load contents into memory,for fast small reads 细节提示 AAsset是只读的,比如上面获得FILE之后,不能用来写。 AAsset provides access to a read-only asset. 记得AAsset_close 记得AAssetDir_close 关于压缩文件 Android APK中有些文件是会进行压缩的,而有些文件则因为本身就是已经压缩过的,不再进行压缩,具体有: /* these formats are already compressed, or don't compress well */ static const char* kNoCompressExt[] = { ".jpg", ".jpeg", ".png", ".gif", ".wav", ".mp2", ".mp3", ".ogg", ".aac", ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet", ".rtttl", ".imy", ".xmf", ".mp4", ".m4a", ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2", ".amr", ".awb", ".wma", ".wmv" }; 那么对于在APK中会被压缩的文件,比如txt文件,就不能使用AAsset_openFileDescriptor来读了,否则,会返回-1这样的无效fd。对于会被压缩的文件,那么就只能使用AAsset_read或者AAsset_getBuffer来访问了。 现在加Android开发群;701740775,可免费领取一份最新Android高级架构技术体系大纲和视频资料,以及五年积累整理的所有面试资源笔记。加群请备注csdn领取xx资料

优秀的个人博客,低调大师

dubbo源码解析-集群容错架构设计

前期铺垫 官网介绍图.png 这张是官网的对于集群容错的架构设计图,即使你有一定的使用经验,第一眼看到这个图可能还是有些懵逼.因为这个图是从设计的角度画出来的,而不是使用的角度.但是即使这个图你看不懂也不影响你对本文的阅读,但是你必须要记住三个关键词,因为这三个关键词接下来会贯穿全文,他们就是Directory,Router,LoadBalance 再接下来给大家一张"地图","地图"上我已经标记了序号,再下面的源码分析中,我也会实时提醒我们所在的位置,以至于不会迷失方向. 执行时序图.png 环境准备 既然是集群,那么首先要启动两个Provider,我这里是一个虚拟机,一个本地的方式,因为环境准备不是本文重点,因此略过.本文所用到的源码是2.5.4版本,可以在guihub上找到. 正式发车 这次示例选用的源码用dubbo-demo的dubbo-demo-consumer,如果对dubbo原理有些简单的了解就知道,他给接口注入的不是接口的实现类,而是一个代理类,如下图 接着自然是到了代理类的invoke方法里,从图中我们也可以看出,他用的是jdk的动态代理 下面要开始紧盯着地图了,他现在就要开始执行地图中的序号1,此时我们抵达MockClusterInvoker这个类 执行invoke就要开始进入到集群,也就是Cluster,现在第一个关键词Directory已经浮出水面了 现在到了AbstractDirectory,也就是序号3 这个methodInvokerMap也比较重要,后面的文章会讲一下这个,但是我们这部分代码就可以从出,他是要从methodInvokerMap中取出invokers如图所示 将invokers返回后(序号5),下面来到了第二个关键词,Router,开始进入路由,现在我们到了序号6,此时到了MockInvokersSelector类,不要看类名和Router没有关系,其实他是Router接口的实现类,从官网的介绍图中我们也可以看到Router分为Script和Condition两种,翻译过来也就是脚本路由和条件路由这个后面再详细介绍,本篇主要介绍整体架构 源码的命名是很规范的,从getNormalInvokers就可以得知,他是要拿到能正常执行的invokers,并将其返回.也就是序号7 这个时候我们再次回到了AbstractClusterInvoker这个类,我们先不急着往下走,先适时做个总结.因为三个关键词,现在都已经出现了两个,那这个时候要回忆一下上面这些步骤,做一个总结.上面出现的这两个关键词,其实无非就是做两件事 在Directory中找出本次集群中的全部invokers 在Router中,将上一步的全部invokers挑选出能正常执行的invokers 对应到"地图",也就是序号5和序号7.(再次提醒,一定要紧跟地图的序号,不然很容易迷失方向) 从上面步骤我们也知道,已经挑选出能正常执行的invokers了,但是假如2个做集群,但是这两个都是正常的,我到底要执行哪一个呢?带着这个问题,我们继续往下看 根据官网的描述(深度解剖dubbo源码:http://www.roncoo.com/course/view/90c28309c72d472ab2b58e3802faeba6) 注:在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。 所以这个时候是到了FailoverClusterInvoker类,但是如果你配置的是Failfast Cluster(快速失败),Failsafe Cluster(失败安全),Failback Cluster(失败自动恢复),Forking Cluster(并行调用多个服务器,只要一个成功即返回),Broadcast Cluster(广播调用所有提供者,逐个调用,任意一台报错则报错)他也会到达相应的类 下面就要开始第三个关键词浮出水面,也就是LoadBalance(负载均衡),此时的位置是序号11,仔细留心源码的注释,其实这里可以出一个面试题,比如 dubbo的负载均衡策略是怎么样的? 为什么这可以作为一道面试题,因为他可以区分三个层次的人. 如果是没有使用过,或者一直停留在使用层次的人,肯定不会留级到这个负载均衡策略. 根据官网介绍在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为 random 随机调用。如果能回答出,缺省为随机调用的,说明还是有一定的使用经验,留意到官网的介绍 如果能回答出,缺省为随机调用,但是如果集群的数量为2,那么将退化成轮询.如果能回答到这个,那这个人就是有一定追求,不仅留心文档介绍,而且是看过源码,注意细节的人 根据前面我们知道,现在已经有两个备选的invokers,但是究竟哪一个能执行,这个需要LoadBalance来决定.这里涉及到了一定的算法,后面我也会有一篇文章加以介绍.剧透一下,这个在2.5.4的版本中,这个算法还是存在一些小的bug,此时我们的位置是序号13 到达终点站.我们回忆总结一下,文初提到的三个关键词,在这个集群容错的整体架构过程中,dubbo究竟做了什么.其实也就是三件事: 1、在Directory中找出本次集群中的全部invokers 2、在Router中,将上一步的全部invokers挑选出能正常执行的invokers 3、在LoadBalance中,将上一步的能正常的执行invokers中,根据配置的负载均衡策略,挑选出需要执行的invoker 作者:肥朝 链接:http://www.jianshu.com/p/8e007012367e

优秀的个人博客,低调大师

深入解析vue.js数据绑定操作

废话不多说,直接进入正题!!! 数据绑定 响应式的数据绑定系统。建立绑定之后,DOM将和数据保持同步,无须手动维护DOM。使代码能够更加简洁易懂、提升效率。 数据绑定语法 1.文本插值 {{ }}Mustache标签 <span>Hello {{ name }}</span> data:{ name: 'vue' } == > Hello vue 单次插值 首次赋值后再更改vm实例属性值不会引起DOM的变化 <span v-once="name">{{ name }}</span> 2.HTML属性 Mustache标签{{ }} <div v-bind:id="'id-'+id"></div> 简写: <div :id="'id-'+id"></div> 3.绑定表达式 放在Mustache标签内的文本内容。除了直接输出属性值之外,一段绑定表达式可以由一个简单的JavaScript表达式和可选的一个或多个过滤器构成(不支持正则表达式,若需要复杂的转换,则使用过滤器或者计算属性来进行处理)。 {{ index + 1}} {{ index == 0 ? 'a' : 'b' }} {{name.split('').join('|') }} {{ var a = 1 }} //无效 4.过滤器 vue.js允许在表达式后添加可选的过滤器,以管道符"|"指示。 {{ name | uppercase }} // Vue.js将name的值传入给uppercase这个内置的过滤器中(本质是一个函数),返回字符串的大写值。 {{ name | filterA | filterB }} //多个过滤器链式使用 {{ name | filterA arg1 arg2 }} //传入多个参数 filterA将name的值做为第一个参数,arg1,arg2做为第二、第三个参数传入过滤器函数中。 最终函数的返回值即为输出结果。arg1,arg2可以使用表达式,也可以加上单引号,直接传入字符串。 例如: {{ name.split('') | limitBy 3 1 }} // ->u,e 过滤器limitBy可以接受两个参数,第一个参数是设置显示个数,第二个参数为可选,指从开始元素的数组下标。 vue.js内置的10个过滤器(Vue.js2.0中去除) capitalize:字符串首字符转化为大写。 uppercase:字符串转化为大写。 lowercase:字符串转化为小写。 currency:参数为{String}[货币符号],{Number}[小数位],将数字转化成货币符号,并且会自动添加数字分节号。 {{ amount | currency '¥' 2 }} //若amount值为1000,则输出为¥1,000.00 pluralize:参数为{String}single,[double,triple],字符串复数化。 <p v-for="c in count">{{ c | pluralize 'item' }} {{ c | pliralize 'st' 'nd' 'rd' 'th' }} </p> 输出结果: 1item 1st 2items 2nd 3items 3rd 4items 4th json:参数为{Number}[indent]空格缩进数,将json对象数据输出成符合json格式的字符串。 debounce:传入值必须是函数,参数可选,为{Number}[wait],即延时时长。作用是当调用函数n毫秒后,才会执行该动作。 <input v-on:keyup="onKeyup | debounce 500"> //input元素上监听了keyup事件,并且延迟500ms触发 limitBy:传入值必须是数组,参数为{Number}limit,{Number}[offset],limit为显示个数,offset为开始显示数组下标。 <div v-for="item in items | limitBy 10"></div> //items为数组,且只显示数组中的前十个元素 filterBy:传入值必须是数组,参数为{String | Function}targetStringOrFunction,即需要匹配的字符串或函数;"in"可选分隔符。{String}[...searchKeys],为检索的属性区域。 <p v-for="name in names | filterBy '1.0'">{{ name }}</p> //检索names数组中值包含1.0的元素 <p v-for="item in items | filterBy '1.0' in 'name'">{{ item | json }}</p> //检索items中元素属性name值为1.0的元素输出。检索区域也可以为数组,即in [name,version],在多个属性中进行检索。 输出结果 vue1.0 {"name":"vue1.0","version":"1.0"} 使用自定义的过滤函数,函数可以在选项methods中定义 <p v-for="item in items | filterBy customFilter" methods:{ cuestomFilter:function(item){ if(item.name) return true; //检索所有元素中包含name属性的元素 }/欢迎加入全栈开发交流圈一起吹水聊天学习交流:864305860 } orderBy:传入值必须是数组,参数为{String|Array|Function}sortKeys,即指定排序的策略。 单个键名: <p v-for="item in items | orderBy 'name' -1">{{ item.name}}</p> //items数组中以键名name进行降序排列 多个键名: <p v-for="item in items | orderBy [name,version]">{{item.name}}</p> //使用items里的两个键名进行排序 自定义排序函数: <p v-for="item in items | orderBy customOrder">{{item.name}}</p> methods:{ customOrder: function(a,b){ return parseFloat(a.version) > parseFloat(b.version) //对比item中version的值的大小进行排序 }/欢迎加入全栈开发交流圈一起吹水聊天学习交流:864305860 } 5.指令 指令的值限定为绑定表达式,即当表达式的值发生改变时,会有些特殊行为作用到绑定的DOM上。 参数:src为参数 <img v-bind:src="avatar" /> <==> <img src="{{avatar}}" /> 修饰符:以半角句号.开始的特殊后缀,用于表示指令应该以特殊方式绑定。 计算属性 避免在模板中加入过重的业务逻辑,保证模版的结构清晰和可维护性。 1.基础例子 var vm = new Vue({ el: '#app', data: { firstName:'Gavin', lastName:'CLY' },/欢迎加入全栈开发交流圈一起吹水聊天学习交流:864305860 computed: { fullName:function(){ //this指向vm实例 return this.firstName + ' ' + this.lastName; }/欢迎加入全栈开发交流圈一起吹水聊天学习交流:864305860 } }) <p>{{ firstName }}</p> //Gavin <p>{{ lastName }}</p> //CLY <p>{{ fullName }}</p> //Gavin CLY 注:此时对vm.firstName和vm.lastName进行修改,始终会影响vm.fullName。 2.Setter 更新属性时带来便利 var vm = new Vue({ el:'#el', data:{ cents:100 }, computed:{ price:{ set:function(newValue) { this.cents = newValue * 100; }, get:function(){ return (this.cents / 100).toFixed(2); }/欢迎加入全栈开发交流圈一起吹水聊天学习交流:864305860 } } }) 表单控件 v-model:对表单元素进行双向数据绑定,在修改表单元素值时,实例vm中对应的属性值也同时更新,反之亦然。 var vm = new Vue({ el:'#app', data: { message: '', gender: '', cheched: '', multiChecked: '', a: 'checked', b: 'checked' }/欢迎加入全栈开发交流圈一起吹水聊天学习交流:864305860 }) 1. Text 输入框示例,用户输入的内容和vm.message直接绑定: <input type="text" v-model="message" /> <span>Your input is : {{ message }} </span> 2. Radio 单选框示例: <label><input type="radio" value="male" v-model="gender">男</lable> <label><input type="radio" value="famale" v-model="gender">女</lable> <p>{{ gender }}</p> //显示的是value值 3.Checkbox 单个勾选框,v-model即为布尔值,此时Input的value并不影响v-model的值。 <input type="checkbox" v-model="checked" /> <span>checked: {{ checked }} </span> //显示的是true/false 多个勾选框,v-model使用相同的属性名称,且属性为数组。 <label><input type="checkbox" value="1" v-model="multiChecked">1</label> <label><input type="checkbox" value="1" v-model="multiChecked">2</label> <label><input type="checkbox" value="1" v-model="multiChecked">3</label> <p>MultiChecked:{{ multiChecked.join{'|'} }}</p> //multiChecked:1|2 4.Select 单选 <select v-model="selected"> <option selected>A</option> <option>B</option> <option>C</option> </select>/欢迎加入全栈开发交流圈一起吹水聊天学习交流:864305860 <span>Selected: {{ selected }}</span> 多选 <select v-model="multiSelected" multiple> <option selected>A</option> <option>B</option> <option>C</option> </select>/欢迎加入全栈开发交流圈一起吹水聊天学习交流:864305860 <span>MultiSelected: {{ multiSelected.join('|') }}</span> 5.绑定value 通过v-bind实现,表单控件的值绑定到Vue市里的动态属性上。 Checkbox <input type="checkbox" v-model="checked" v-bind:true-value="a" v-bind:false-value=" 选中: vm.checked == vm.a //=> true 未选中: vm.checked == vm.b //=>true Radio <input type="radio" v-model="checked" v-bind:value="a" > 选中: vm.checked == vm.a //=> true 3.Select Options <select v-model="selected"> <!-- 对象字面量 --> <option v-bind:value="{ number:123}">123</option> </select>//欢迎加入全栈开发交流圈一起吹水聊天学习交流:864305860 选中: typeof vm.selected //=> object vm.selected.number //=> 123 6.参数特性 .lazy:默认情况下,v-model在input事件中同步输入框与数据,加lazy属性后会在change事件中同步。 <!-- 在 "change" 而不是 "input" 事件中更新 --> <input v-model.lazy="msg" > .number:自动将用户输入转为Number类型,如果原值转换结果为NaN,则返回原值。 <input v-model.number="age" type="number"> .trim:如果要自动过滤用户输入的首尾空格,可以添加 trim 修饰符到 v-model 上过滤输入 <input v-model.trim="msg"> Class与Style绑定 1.Class绑定 对象语法:v-bind:class接受参数是一个对象,而且可以与普通的class属性共存。 <div class="tab" v-bind:class="{'active':active,'unactive':!active}"></div> vm实例中需要包含: data:{ active:true }//欢迎加入全栈开发交流圈一起学习交流:864305860 渲染结果为: <div class="tab active"></div> 数组语法:v-bind:class也接受数组作为参数。 <div v-bind:class="[classA,classB]"></div> vm实例中需要包括: data:{ classA:"class-a", classB:"class-b" }//欢迎加入全栈开发交流圈一起学习交流:864305860 渲染结果为: <div class="class-a class-b"></div> 使用三元表达式切换数组中的class <div v-bind:class="[classA,isB?classB:""]"></div> 若 vm.isB = false 则渲染结果为 <div class="class-a"></div> 2.内联样式绑定(style属性绑定) 对象语法:直接绑定符合样式格式的对象。 <div v-bind:style="alertStyle"></div> vm实例中包含: data:{ alertStyle:{ color: 'red', fontSize: '2px' }//欢迎加入全栈开发交流圈一起学习交流:864305860 } <div v-bind:style="{fontSize:alertStyle.fontSize,color:'red'}"></div> 数组语法:v-bind:style允许将多个样式对象绑定到同一元素上。 <div v-bind:style="[ styleObjectA,styleObjectB]" .></div> 3.自动添加前缀 在使用transform这类属性时,v-bind:style会根据需要自动添加厂商前缀。:style在运行时进行前缀探测,如果浏览器版本本省就不支持不加前缀的css属性,那就不会添加。 结语 感谢您的观看,如有不足之处,欢迎批评指正。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册