Java56项目网
我们一直用心在做

ConcurrentHashMap的实现原理与使用

admin阅读(9)

​一、 为什么要使用ConcurrentHashMap

在并发编程中使用HashMap可能导致程序死循环。而使用线程安全的HashTable效率又非常低下,基于以上两个原因,便有了ConcurrentHashMap的登场机会。

(1)线程不安全的HashMap

在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。例如,执行以下代码会引起死循环。

final HashMap<String, String> map = new HashMap<String, String>(2);
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            map.put(UUID.randomUUID().toString(), "");
                        }
                    }, "ftf" + i).start();
                }
            }
        }, "ftf");
        t.start();
        t.join();

HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry

(2)效率低下的HashTable

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

(3)ConcurrentHashMap的锁分段技术可有效提升并发访问率

HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

二、ConcurrentHashMap的结构

通过ConcurrentHashMap的类图来分析ConcurrentHashMap的结构,如2-1所示。ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,

必须首先获得与它对应的Segment锁,如图2-2所示。

           图2-1 ConcurrentHashMap的类图

                图2-2 ConcurrentHashMap的结构图

三、ConcurrentHashMap的初始化

ConcurrentHashMap初始化方法是通过initialCapacity、loadFactor和concurrencyLevel等几个参数来初始化segment数组、段偏移量segmentShift、段掩码segmentMask和每个segment里的HashEntry数组来实现的。

(一)初始化segments数组

让我们来看一下初始化segments数组的源代码。

 if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
            int sshift = 0;
            int ssize = 1;
            while (ssize < concurrencyLevel) {
                ++sshift;
                ssize <<= 1;
        }
            segmentShift = 32 - sshift;
            segmentMask = ssize - 1;
            this.segments = Segment.newArray(ssize);

由上面的代码可知,segments数组的长度ssize是通过concurrencyLevel计算得出的。为了能通过按位与的散列算法来定位segments数组的索引,必须保证segments数组的长度是2的N次方(power-of-two size),所以必须计算出一个大于或等于concurrencyLevel的最小的2的N次方值来作为segments数组的长度。假如concurrencyLevel等于14、15或16,ssize都会等于16,即容器里锁的个数也是16。

注意 concurrencyLevel的最大值是65535,这意味着segments数组的长度最大为65536,对应的二进制是16位。

(二)初始化segmentShift和segmentMask

这两个全局变量需要在定位segment时的散列算法里使用,sshift等于ssize从1向左移位的次数,在默认情况下concurrencyLevel等于16,1需要向左移位移动4次,所以sshift等于4。segmentShift用于定位参与散列运算的位数,segmentShift等于32减sshift,所以等于28,这里之所以用32是因为ConcurrentHashMap里的hash()方法输出的最大数是32位的,后面的测试中我们可以看到这点。segmentMask是散列运算的掩码,等于ssize减1,即15,掩码的二进制各个位的值都是1。因为ssize的最大长度是65536,所以segmentShift最大值是16,segmentMask最大值是

65535,对应的二进制是16位,每个位都是1。

(三)初始化segmentShift和segmentMask

输入参数initialCapacity是ConcurrentHashMap的初始化容量,loadfactor是每个segment的负载因子,在构造方法里需要通过这两个参数来初始化数组中的每个segment。

if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = 1;
        while (cap < c)
            cap <<= 1;
        for (int i = 0; i < this.segments.length; ++i)
            this.segments[i] = new Segment<K,V>(cap, loadFactor);

上面代码中的变量cap就是segment里HashEntry数组的长度,它等于initialCapacity除以ssize的倍数c,如果c大于1,就会取大于等于c的2的N次方值,所以cap不是1,就是2的N次方。segment的容量threshold=(int)cap*loadFactor,默认情况下initialCapacity等于16,loadfactor等于0.75,通过运算cap等于1,threshold等于零。6.1.4 定位Segment既然ConcurrentHashMap使用分段锁Segment来保护不同段的数据,那么在插入和获取元素的时候,必须先通过散列算法定位到Segment。可以看到ConcurrentHashMap会首先使用Wang/Jenkins hash的变种算法对元素的hashCode进行一次再散列。

  private static int hash(int h) {
            h += (h << 15) ^ 0xffffcd7d;
            h ^= (h >>> 10);
            h += (h << 3);
            h ^= (h >>> 6);
            h += (h << 2) + (h << 14);
            return h ^ (h >>> 16);
        }

之所以进行再散列,目的是减少散列冲突,使元素能够均匀地分布在不同的Segment上,从而提高容器的存取效率。假如散列的质量差到极点,那么所有的元素都在一个Segment中,不仅存取元素缓慢,分段锁也会失去意义。笔者做了一个测试,不通过再散列而直接执行散列计算。

System.out.println(Integer.parseInt("0001111", 2) & 15);
System.out.println(Integer.parseInt("0011111", 2) & 15);
System.out.println(Integer.parseInt("0111111", 2) & 15);
System.out.println(Integer.parseInt("1111111", 2) & 15);

计算后输出的散列值全是15,通过这个例子可以发现,如果不进行再散列,散列冲突会非常严重,因为只要低位一样,无论高位是什么数,其散列值总是一样。我们再把上面的二进制数据进行再散列后结果如下(为了方便阅读,不足32位的高位补了0,每隔4位用竖线分割下)。

0100|0111|0110|0111|1101|1010|0100|1110
1111|0111|0100|0011|0000|0001|1011|1000
0111|0111|0110|1001|0100|0110|0011|1110
1000|0011|0000|0000|1100|1000|0001|1010

可以发现,每一位的数据都散列开了,通过这种再散列能让数字的每一位都参加到散列运算当中,从而减少散列冲突。ConcurrentHashMap通过以下散列算法定位segment。

 final Segment<K,V> segmentFor(int hash) {
            return segments[(hash >>> segmentShift) & segmentMask];
        }

默认情况下segmentShift为28,segmentMask为15,再散列后的数最大是32位二进制数据,向右无符号移动28位,意思是让高4位参与到散列运算中,(hash>>>segmentShift)&segmentMask的运算结果分别是4、15、7和8,可以看到散列值没有发生冲突。

四、ConcurrentHashMap的操作

(一) get操作

Segment的get操作实现非常简单和高效。先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素,代码如下。

 public V get(Object key) {
            int hash = hash(key.hashCode());
            return segmentFor(hash).get(key, hash);
        }

Segement大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读不需要写

共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,是因为根据Java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。

transient volatile int count;
volatile V value;

 

在定位元素的代码里我们可以发现,定位HashEntry和定位Segment的散列算法虽然一样,都与数组的长度减去1再相“与”,但是相“与”的值不一样,定位Segment使用的是元素的hashcode通过再散列后得到的值的高位,而定位HashEntry直接使用的是再散列后的值。其目的是避免两次散列后的值一样,虽然元素在Segment里散列开了,但是却没有在HashEntry里散列开。

hash >>> segmentShift) & segmentMask // 定位Segment所使用的hash算法
int index = hash & (tab.length - 1); // 定位HashEntry所使用的hash算法

(二).put操作

由于put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后在Segment里进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放在HashEntry数组里。

(1)是否需要扩容

在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阈值,则对数组进行扩容。值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。

(2)如何扩容

在扩容的时候,首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。

3.size操作

如果要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和。Segment里的全局变量count是一个volatile变量,那么在多线程场景下,是不是直接把所有Segment的count相加就可以得到整个ConcurrentHashMap大小了呢?不是的,虽然相加时可以获取每个Segment的count的最新值,但是可能累加前使用的count发生了变化,那么统计结果就不准了。所以,最安全的做法是在统计size的时候把所有Segment的put、remove和clean方法全部锁住,但是这种做法显然非常低效。因为在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put、remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

基于Docker和Kubernetes的最佳架构实践

admin阅读(66)

【编者的话】是否有可能基于Docker和Kubernests搭建一个最酷的架构?这篇文章将带您踏上最佳架构之旅,并一路为你讲解遇到的各种问题,现在,Let’s go!

软件开发领域在Docker和Kubernetes时代是如何变化的? 是否有可能使用这些技术搭建一劳永逸的架构? 当所有东西都被“打包”进容器中时,是否有可能统一开发及集成的流程? 这些决策的需求是什么? 它们会带来什么限制? 它们会让开发人员更轻松,或者相反,反而增加不必要的复杂性吗?

1.jpg

现在是时候以文本和原始插图方式阐明这些以及其他问题了!

这篇文章将带您踏上从现实生活到开发流程再到架构最后回到现实生活的旅程,并一路为您解答在这些停靠站点上遇到的最重要问题。 我们将试图确定一些应该成为架构一部分的组件和原则,并演示一些示例,但不会进入其实现领域。

文章的结论可能会让你心烦意乱,或者无比开心,这一切都取决于你的经验、你对这三章故事的看法,甚至在阅读本文时你的心情。 在下面可以发表评论或提出问题,让我知道您的想法!

从现实生活到开发工作流

2.jpg

在大多数情况下,我所见过的或者很荣幸搭建的所有开发流程都只是为了一个简单的目标——缩短从概念产生到交付生产环境之间的时间间隔,同时保持一定程度的代码质量。

想法的好坏无关紧要。 因为糟糕的想法来也匆匆,去也匆匆——你只要尝试一下,就可以把它们丢进故纸堆里。 这里值得一提的是,从一个糟糕的想法回滚是可以落在自动化设施的肩膀上的,这可以自动化您的工作流程。

持续集成和交付看起来像是软件开发领域的救命稻草。 究竟还有什么比这更简单呢? 如果你有一个想法,你有代码,那么就去做吧! 如果不是轻微的问题,这将是完美无瑕的——集成和交付过程相对而言难以独立于公司特有的技术和业务流程之外。

然而,尽管任务看起来很复杂,但在生活中不时会出现一些优秀的想法,这些想法可以让我们(当然,我自己是确定的)更接近构建一个无瑕疵的并且几乎可以在任何场合使用的机制。 对我来说,离这样的机制最近的步骤是Docker和Kubernetes,他们的抽象层次和思想方法使我认为现在可以用几乎相同的方法解决80%的问题。

其余的20%的问题显然还在原地踏步,但正因如此你才可以将你发自内心的创意天赋聚焦在有趣的工作上,而不是处理重复的例行公事。 只要照料一次“架构框架”,就可以让您忘掉已经解决的80%问题。

这一切意味着什么?以及Docker是如何解决开发工作流程的问题的? 让我们看一个简单的过程,这对于大多数工作环境来说也足够了:

3.jpg

通过适当的方法,您可以自动化并整合上面序列图中的所有内容,并在未来几个月内将其抛之脑后。

设置开发环境

4.jpg

一个项目应该包含一个docker-compose.yml文件,这可以让你省去考虑在本地机器上运行应用程序/服务需要做些什么以及如何操作的问题。 一个简单的命令docker-compose up应该启动您的应用程序及其所有依赖项,使用fixtures填充数据库,上传容器内的本地代码,启用代码跟踪以便即时编译,并最终在期望的端口开始响应请求。 即使在设置新服务时,您也不必担心如何启动、在哪里提交更改或使用哪个框架。 所有这些都应该提前在标准说明中描述,并由针对不同设置的服务模板指定:前端、后端和worker

自动化测试

5.jpg

所有你想知道的关于“黑匣子”(至于为什么我把容器称之为如此的更多信息,将在文章中的稍后部分阐明)的情况是,它里面的一切都完好无损,是或否,1或0。您可以在容器内部执行有限数量的命令,而docker-compose.yml描述了它的所有依赖关系,您可以轻松自动化和整合这些测试,而不必过分关注实现细节。

比如,像这样

在这里,测试不仅意味着单元测试,还包括功能测试、集成测试、(代码样式)测试和副本、检查过时的依赖关系以及已使用软件包的许可证正常与否等等。 关键是所有这些都应该封装在Docker镜像中。

系统交付

6.jpg

无论在何时何地想安装您的项目都无所谓。 结果就像安装进程一样,应该始终如一。 至于您要安装的是整个生态系统的哪个部分或者您将从哪个Git仓库获得代码也没有区别。 这里最重要的组件是幂等性。 唯一应该指定的是控制安装过程的变量。

以下是我在解决这个问题时相当有效的算法:

  1. 从所有Dockerfiles收集镜像(例如像这样
  2. 使用元项目,通过Kube API将这些镜像交付给Kubernetes。 启动交付通常需要几个输入参数:
    • Kube API端点
    • 一个“机密”对象,因不同的环境而异(本地/测试/预发布/生产)
    • 要展示的系统名称以及针对这些系统的Docker镜像的标签(在上一步中获取)

作为一个涵盖所有系统和服务的元项目的例子(换句话说,是一个描述生态系统如何编排以及如何交付更新的项目),我更愿意使用Ansible playbooks,通过这个模块来与Kube API集成。 然而,复杂的自动化可以参考其他选项,我稍后将详细讨论自己的选择。 但是,您必须考虑中心化/统一的管理架构的方式。 这样一个方式可以让您方便、统一地管理所有服务/系统,并消除即将到来的执行类似功能的技术和系统丛林可能带来的任何复杂情况。

通常,需要如下的安装环境:

  • “测试”——用于对系统进行一些手动检查或调试
  • “预发布”——用于近乎实时的环境以及与外部系统的集成(通常位于DMZ而不是测试环境
  • “生产”——最终用户的实际环境

 

集成和交付的连续性

7.jpg

如果你有一个统一的方式来测试Docker镜像——或者“黑盒子”——你可以假设这些测试结果可以让你无缝地(并且问心无愧)将功能分支集成到你的Git仓库的上游或主分支中。

也许,这里唯一的交易断路器是集成和交付的顺序。如果没有发行版,那么如何通过一组并行的功能分支阻止一个系统上的“竞争条件”?

因此,只有在没有竞争的情况下才能开始这个过程,否则“竞争条件”会萦绕脑海:

  1. 尝试将功能分支更新到上游git rebase/ merge
  2. Dockerfiles构建镜像
  3. 测试所有构建的镜像
  4. 开始并等待,直到系统交付了构建自步骤2的镜像
  5. 如果上一步失败,则将生态系统回滚到之前的状态
  6. 上游合并功能分支并将其发送到存储库

在任何步骤中的任何失败都应终止交付过程,并将任务返回给开发人员以解决错误,无论是失败的测试还是合并冲突。

您可以使用此过程来操作多个存储库。只需一次为所有存储库执行每个步骤(步骤1用于代码库A和B,步骤2用于代码库A和B等),而不是对每个单独的存储库重复执行整个流程(步骤1-6用于代码库A ,步骤1-6用于代码库B,等等)。

此外,Kubernetes允许您分批次地推出更新以进行各种AB测试和风险分析。 Kubernetes是通过分离服务(接入点)和应用程序在内部实现的。您可以始终以所需的比例平衡组件的新旧版本,以促进问题的分析并为潜在的回滚提供途径。

系统回滚

架构框架的强制性要求之一是能够回滚任何部署。反过来,这又需要一些显式和隐式的细微差别。以下是其中最重要的一些事项:

  • 服务应该能够设置其环境以及回滚更改。例如,数据库迁移、RabbitMQ schema等等。
  • 如果无法回滚环境,则该服务应该是多态的,并支持旧版本和新版本的代码。例如:数据库迁移不应该中断旧版本的服务(通常是2或3个以前的版本)
  • 向后兼容任何服务更新。通常,这是API兼容性,消息格式等。

在Kubernetes集群中回滚状态相当简单(运行kubectl rollout undo deployment/some-deployment,Kubernetes将恢复先前的“快照”),但是为了让此功能生效,您的元项目应包含有关此快照的信息。但是更为复杂的交付回滚算法让人望而生畏,尽管它们有时是必需的。

以下是可以触发回滚机制的内容:

  • 发布后应用程序错误的高比例
  • 来自关键监控点的信号
  • 失败的冒烟测试
  • 手动模式——人为因素

 

确保信息安全和审计

没有一个工作流程可以奇迹般地“搭建”刀枪不入的安全性并保护您的生态系统免受外部和内部威胁,因此您需要确保您的架构框架是在每个级别和所有子系统里按照公司的标准和安全策略执行的。

我将在后面的关于监控和告警的章节讨论有关解决方案的所有三个级别,它们本身也是系统完整性的关键。

Kubernetes拥有一套良好的针对访问控制网络策略事件审计以及其他与信息安全相关的强大工具的内置机制,可用于构建一个良好的防护边界,以抵御和阻止攻击及数据泄露。

从开发流程到架构

应该认真考虑将开发流程与生态系统紧密集成的想法。将这种集成的需求添加到架构的传统需求集(弹性、可伸缩性、可用性、可靠性、抵御威胁等)中,可以大大提高架构框架的价值。 这是至关重要的一个方面,由此导致出现了一个名为“DevOps”(开发运维)的概念,这是实现基础设施全面自动化并优化的合理步骤。 但是,如果有一个设计良好的架构和可靠的子系统,DevOps任务可以被最小化。

微服务架构

没有必要详细讨论面向服务的架构——SOA的好处,包括为什么服务应该是“微”的。 我只会说,如果你决定使用Docker和Kubernetes,那么你很可能理解(并接受)单体应用架构是很困难甚至根子上就是错误的。 Docker旨在运行一个进程并持久化,Docker让我们聚焦于DDD框架(领域驱动开发)内进行思考。 在Docker中,打包后的代码被视为具有一些公开端口的黑盒子。

生态系统的关键组件和解决方案

根据我在设计具有更高可用性和可靠性的系统方面的经验,有几个组件对于微服务的运维是至关重要的,稍后我会列出并讨论这些组件,我将在Kubernetes环境中引用它们,也可以参考我的清单作为其它任何平台的检查单。

如果你(像我一样)会得出这样的结论,即将这些组件作为常规的Kubernetes服务来管理,那么我建议你在除“生产环境”之外的单独集群中运行它们。 比如“预发布”集群,因为它可以在生产环境不稳定并且你迫切需要其镜像、代码或监控工具的来源时节省你的时间。 可以说,这解决了鸡和鸡蛋的问题。

身份认证

8.jpg

像往常一样,它始于访问——服务器、虚拟机、应用程序、办公室邮件等。 如果您是或想成为主要的企业平台(IBM、Google、Microsoft)之一的客户,则访问问题将由供应商的某个服务处理。 但是,如果您想拥有自己的解决方案,难道只能由您并在您的预算之内进行管理?

此列表可帮助您确定适当的解决方案并估算设置和维护所需的工作量。 当然,您的选择必须符合公司的安全政策并经信息安全部门批准。

自动化服务配置

9.jpg

尽管Kubernetes在物理机器/云虚拟机(Docker、kubelet、kube proxy、etcd集群)上只需要少量组件,但对于新机器的添加和集群管理仍然需要自动化。 以下是一些简单的方法:

  • KOPS——此工具允许您在两个云供应商(AWS或GCE)之一上安装集群
  • Teraform——这可以让您管理任何环境的基础设施,并遵循IAC(基础架设施即代码)的思想
  • Ansible——用于任何类型的通用自动化工具

就个人而言,我更喜欢第三个选项(带有一个Kubernetes的集成模块),因为它允许我使用服务器和Kubernetes对象并执行任何类型的自动化。 但是,没有什么能阻止您使用Teraform及其Kubernetes模块。 KOPS在“裸机”方面效果不佳,但它仍然是与AWS/GCE一起使用的绝佳工具!

Git代码库和任务跟踪器

10.jpg

对于任何Docker容器,使其日志可访问的唯一方法是将它们写入正在容器中运行的根进程的STDOUT或STDERR,服务开发人员并不关心日志数据接下来的变化,而主要是它们应该在必要时可用,并且最好包含过去某个点的记录。满足这些期许的所有责任在于Kubernetes以及支持生态系统的工程师。

官方文档中,您可以找到关于处理日志的基本(和好的)策略的说明,这将有助于您选择用于聚合和存储大量文本数据的服务。

在针对日志系统的推荐服务中,同一文档提到fluentd用于收集数据(在集群的每个节点上作为代理启动时)以及用于存储和索引数据的Elasticsearch。即使你可能不赞同这个解决方案的效率,但鉴于它的可靠性和易用性,我认为这至少是一个好的开始。

Elasticsearch是一个资源密集型的解决方案,但它可以很好地扩展并有现成的Docker镜像,可以运行在单个节点以及所需大小的集群上。

跟踪系统

11.jpg

即使代码非常完美,然而还是会确实发生故障,接着你想在生产环境中非常仔细地研究它们,并试图了解“如果在我的本地机器上一切工作正常,那么在生产环境上究竟发生了什么错误?”。比如缓慢的数据库查询、不正确的缓存、较慢的磁盘或与外部资源的连接、生态系统中的交易,瓶颈以及规模不足的计算服务都是您不得不跟踪和估算在实际负载下代码执行时间的一些原因。

OpentracingZipkin足以应付大多数现代编程语言的这一任务,并且在封装代码之后不会增加额外的负担。当然,收集到的所有数据应该存储在适当的地方,并作为一个组件使用。

通过上述的开发标准和服务模板可以解决在封装代码以及通过服务、消息队列、数据库等转发“Trace ID”时出现的复杂情况。后者也考虑到了方法的一致性。

监控和告警

12.jpg

Prometheus已经成为现代监控系统中事实上的标准,更重要的是,它在Kubernetes上获得了开箱即用的支持。您可以参考官方Kubernetes文档来了解更多关于监控和警报的信息。

监控是必须安装在集群内的少数几个辅助系统之一,集群是一个受监控的实体。但是对于监控系统的监控(抱歉有些啰嗦)只能从外部进行(例如,从相同的“预发布”环境)。在这种情况下,交叉检查可作为一个针对任何分布式环境的便捷解决方案,这不会使高度统一的生态系统架构复杂化。

整个监控范围可以分为三个完全逻辑隔离的层级。以下是我认为的在每个层级最重要的跟踪点例子:

  • 物理层:网络资源及其可用性——磁盘(I/O,可用空间)——单个节点(CPU、RAM、LA)的基本资源
  • 集群层:——每个节点上主集群系统的可用性(kubelet、kubeAPI、DNS、etcd等)——可用资源数量及其均匀分布——允许的可用资源相对于服务消耗的实际资源的监控——pod的重新加载
  • 服务层:——任何类型的应用程序监控——从数据库内容到API调用频率——API网关上的HTTP错误数量——队列大小和worker的利用率——数据库的多个度量标准(复制延迟、事务的时间和数量、缓慢的请求等)——对非HTTP进程的错误分析——发送到日志系统请求的监控(可以将任何请求转换为度量标准)

至于在每个层级的告警通知,我想推荐使用了无数次的其中一个外部服务,可以发送通知电子邮件,短信或打电话给手机号码。我还会提到另一个系统——OpsGenie——它与Prometheus的alertmanaer是紧密集成的。

OpsGenie是一种弹性的告警工具,可帮助处理升级、全天候工作、通知渠道选择等等。在团队之间分发告警也很容易。例如,不同级别的监控应向不同的团队/部门发送通知:物理——Infra + Devops,集群——Devops,应用程序——每一个相关的团队。

API Gateway和单点登录

13.jpg

要处理诸如授权、认证、用户注册(外部用户——公司客户)和其他类型的访问控制等任务,您需要高度可靠的服务,以保持与API Gateway的弹性集成。使用与“身份服务”相同的解决方案没有什么坏处,但是您可能需要分离这两种资源以实现不同级别的可用性和可靠性。

内部服务的集成不应该很复杂,您的服务不应该担心用户和对方的授权和身份验证。相反,架构和生态系统应该有一个处理所有通信和HTTP流量的代理服务。

让我们考虑一下最适合与API Gateway集成的方式,即整个生态系统——令牌。此方法适用于所有三种访问方案:从UI、从服务到服务以及从外部系统。接着,接收令牌(基于登录名和密码)的任务由用户界面本身或服务开发人员完成。区分UI中使用的令牌的生命周期(较短的TTL)和其他情况(较长的和自定义的TTL)也是有意义的。

以下是API Gateway解决的一些问题:

  • 从外部和内部访问生态系统服务(服务不直接相互通信)
  • 与单点登录服务集成:令牌转换和附加HTTPS请求,头部包含所请求服务的用户标识数据(ID、角色和其他详细信息)——根据从单点登录服务接收到的角色启用/禁用对所请求服务的访问控制
  • 针对HTTP流量的单点监控
  • 复合不同服务的API文档(例如,复合Swagger的json/yml文件
  • 能够根据域和请求的URI管理整个生态系统的路由
  • 用于外部流量的单一接入点,以及与接入供应商的集成

 

事件总线和企业集成/服务总线

14.jpg

如果您的生态系统包含数百个可在一个宏域中工作的服务,则您将不得不处理服务通信的数千种可能方式。为了简化数据流,您应该具备在发生特定事件时将信息分发到大量收件人的能力,而不管事件的上下文如何。换句话说,您需要一个事件总线来发布基于标准协议的事件并订阅它们。

作为事件总线,您可以使用任何可以操作所谓Broker的系统:RabbitMQKafkaActiveMQ等。一般来说,数据的高可用性和一致性对于微服务是至关重要的,但是由于CAP定理,您仍然不得不牺牲某些东西来实现总线的正确分布和集群化。

自然,事件总线应该能够解决各种服务间通信问题,但随着服务数量从几百个增加到几千个甚至几万个,即使是最好的基于事件总线的架构也会望而却步,您将需要寻找另一种解决方案。一个很好的例子就是集成总线方法,它可以扩展上述“Dumb管——智能消费”策略的功能。

有几十个使用“企业集成/服务总线”方法的理由,其目的是减少面向服务架构的复杂性。以下是其中几个理由:

  • 聚合多个消息
  • 将一个事件拆分为几个事件
  • 对于事件的系统响应的同步/事务分析
  • 接口的协调,这对于与外部系统的集成特别重要
  • 事件路由的高级逻辑
  • 与相同服务的多重集成(从外部和内部)
  • 数据总线的不可扩展中心化

作为企业集成总线的一款开源软件,您可能需要考虑Apache ServiceMix,其中包含几个对于此类SOA的设计和开发至关重要的组件。

数据库和其他有状态的服务

15.jpg

和Kubernetes一样,Docker一次又一次地改变了所有用于需要数据持久化以及与磁盘紧密相关的服务的游戏规则。有人说服务应该在物理服务器或虚拟机上以旧的方式“生存”。我尊重这一观点,并且不会谈论它的优点和缺点,但我相当肯定这种说法的存在仅仅是因为在Docker环境中暂时缺乏管理有状态服务的知识、解决方案和经验。

我还应该提到数据库经常占据存储世界的中心位置,因此您选择的解决方案应该完全准备好在Kubernetes环境中工作。

根据我的经验以及市场情况,我可以区分以下几组有状态的服务以及每个服务最适合的Docker解决方案的示例:

  • 数据库管理系统——PostDock是在任何Docker环境中PostgreSQL简单可靠的解决方案
  • 队列/消息代理——RabbitMQ是构建消息队列系统和路由消息的经典软件。 RabbitMQ配置中的cluster_formation参数对于集群设置是必不可少的
  • 高速缓存服务——Redis被认为是最可靠和弹性的数据高速缓存解决方案之一
  • 全文搜索——我上面已经提到过的Elasticsearch技术栈,最初用于全文搜索,但同样擅长存储日志和任何具有大量文本数据的工作
  • 文件存储服务——用于任何类型的文件存储和交付(ftp,sftp等)的一般化服务组

 

依赖镜像

16.jpg

如果您尚未遇到您需要的软件包或依赖项已被删除或暂时不可用的情况,请不要认为这种情况永远不会发生。 为避免不必要的不可用性并为内部系统提供安全保护,请确保构建和交付服务都不需要Internet连接。 配置镜像和复制所有的依赖项到内部网络:Docker镜像、rpm包、源代码库、python/go/js/php模块。

这些以及其他任何类型的依赖关系都有自己的解决方案。 最常见的可以通过查询“private dependency mirror for …”来Google搜索。

从架构到真实生活

17.jpg

不管你喜不喜欢,你的整个架构命中注定迟早会难以为继。它总是会发生:技术过时很快(1 – 5年),方法和方法论——有点慢(5 – 10年),设计原则和基础——偶尔(10 – 20年),但终归是不可避免的。

考虑到技术的过时,需要总是试图让自己的生态系统处于技术创新的高峰期,计划并推出新的服务以满足开发人员、业务和最终用户的需求,向您的利益相关者推广新的实用程序,交付知识来推动您的团队和公司前进。

通过融入专业社区、阅读相关文献并与同事交流,保持自己处于生态链的顶端。注意项目中的新机会以及正确使用新趋势。试验并应用科学方法来分析研究结果,或依靠您信任和尊重的其他人的结论。

除非你是本领域的专家,否则很难为根本性的变化做好准备。我们所有人只会在我们的整个职业生涯中见证一些重大的技术变化,但并不是我们头脑中的知识数量使得我们成为专业人士并让我们攀登到顶峰的,而是我们思维的开放性以及接受蜕变的能力。

回到标题中的问题:“是否有可能搭建一个更好的架构?”。答案显而易见:不,不是“一劳永逸”,但一定要在某种程度上积极争取,在未来某个“很短的时间”,你一定会成功的!

设计模式在 Spring 框架中的良好应用

admin阅读(72)

在开始正文之前,请你先思考几个问题:

  • 你项目中有使用哪些 GOF 设计模式
  • 说一说 GOF 23 种设计模式的设计理念
  • 说说 Spring 框架中如何实现设计模式

假设我是面试官问起了你这些面试题,你该如何回答呢,请先思考一分钟。

好的,我们开始进入正题。设计模式实践里面提供了许多经久不衰的解决方案和最佳方案。这里,GOF 设计模式主要分为三大类:创建模式、结构模式和行为模式。创建模式对于创建对象实例非常有用。结构模式通过处理类或对象的组合来作用于企业级应用的设计结构,从而降低了应用的复杂性,提高了应用的可重用性和性能。行为模式的意图是一组对象之间的交互作用,以执行单个对象无法自己执行的任务。它描述了类或对象交互以及职责的分配。

那么,本文的核心话题是 Spring 如何通过使用大量设计模式和良好实践来构建应用程序。

工厂方法模式

Spring 框架使用工厂模式来实现 Spring 容器的 BeanFactory 和 ApplicationContext 接口。Spring 容器基于工厂模式为 Spring 应用程序创建 bean,并管理着每一个 bean 的生命周期。BeanFactory 和 ApplicationContext 是工厂接口,并且在 Spring 中存在有很多实现类。getBean() 方法是相对应的 bean 的工厂方法。

抽象工厂模式

在 Spring 框架中,FactoryBean 接口是基于抽象工厂模式设计的。Spring 提供了很多这个接口的实现,比如 ProxyFactoryBean、JndiFactoryBean、LocalSessionFactoryBean、LocalContainerEntityManagerFactoryBean 等。FactoryBean 帮助 Spring 构建它自己无法轻松构建的对象。通常这是用来构造具有许多依赖关系的复杂对象。它也可以根据配置构造高易变的逻辑。例如,在 Spring 框架中,LocalSessionFactoryBean 是 FactoryBean 的一个实现,它用于获取 Hibernate 配置的关联的 bean 的引用。这是一个数据源的特定配置,它在得到 SessionFactory 的对象之前被使用。对此,在一致的情况下可以用 LocalSessionFactoryBean 获取特定的数据源配置。读者可以将 FactoryBean 的 getObject() 方法的返回结果注入到任何其他属性中。

单例模式

Spring 框架提供了一个单例的 bean 来实现单例模式。它类似于单例模式,但它与 Java 中的单例模式不完全相同。

建造者模式

Spring 框架中有一些功能实现了建造者模式。以下是 Spring 框架中基于建造者模式的类:

  • EmbeddedDatabaseBuilder
  • AuthenticationManagerBuilder
  • UriComponentsBuilder
  • BeanDefinitionBuilder
  • MockMvcWebClientBuilder

适配器模式

Spring 框架使用适配器模式来实现很多功能。以下列出的一些在 Spring 框架中使用到适配器模式的类:

  • JpaVendorAdapter
  • HibernateJpaVendorAdapter
  • HandlerInterceptorAdapter
  • MessageListenerAdapter
  • SpringContextResourceAdapter
  • ClassPreProcessorAgentAdapter
  • RequestMappingHandlerAdapter
  • AnnotationMethodHandlerAdapter
  • WebMvcConfigurerAdapter

桥接模式

以下是 Spring 模块中基于桥接模式的实现:

  • ViewRendererServlet: 它是一个 servlet 桥接 ,主要是对 Portlet MVC 的支持
  • 桥接模式: Spring 日志处理使用到桥梁模式

装饰器模式

Spring 框架使用装饰器模式构建重要功能,如事务、缓存同步和与安全相关的任务。让我们看看一些 Spring 实现此模式的功能:

  • 织入通知到 Spring 应用程序中。它使用装饰者模式的 CGLib 代理,其通过在运行时生成目标类的子类来工作。
  • BeanDefinitionDecorator: 它通过使用自定义属性来增强 bean 的定义。
  • WebSocketHandlerDecorator: 它用来增强一个 WebSocketHandler 附加行为。

外观模式

在企业级应用中,如果使用到 Spring 框架,那么外观模式是常用于应用程序的业务服务层,它用于整合所有服务。读者也可以在 DAO 的持久层上应用这种模式。

代理模式

Spring 框架使用 Spring AOP 模块中的代理模式。在 Spring AOP 中,笔者可以创建对象的代理来实现横切关注点。在 Spring 中,其他模块也实现了代理模式,如 RMI、 Spring 的 HTTP 调用、Hessian 和 Burlap。

责任链模式

Spring Security 项目实现了责任链模式。Spring Security 允许通过使用安全过滤器链在应用程序中实现身份验证和授权功能。这是一个高度可配置的框架。由于使用了责任链设计模式,我们可以在过滤器链上添加自定义过滤器以自定义功能。

命令模式

Spring MVC 实现了命令模式。在企业级应用中使用到 Spring 框架,读者经常会看到通过使用命令对象来实现命令模式。

解释器模式

在 Spring 框架中,解释器模式在 Spring 表达式语言(SpEL)中使用。Spring 从 Spring 3.0 中增加了这个新功能,读者可以在企业级应用程序中通过 Spring 框架使用它。

迭代器模式

Spring 框架还通过 CompositeIterator 类扩展迭代器模式。该模式主要用于 Java 语言的集合框架中,用于按顺序迭代访问元素。

观察者模式

在 Spring 框架中,观察者模式用于实现 ApplicationContext 的事件处理功能。Spring 为我们提供了 ApplicationEvent 类和 ApplicationListener 接口来启用事件处理。Spring 应用程序中的任何 bean 实现 ApplicationListener 接口,都会接收到 ApplicationEvent 作为事件发布者推送的消息。在这里,事件发布者是主题(Subject) 和实现 ApplicationListener 的 bean 的观察者(Observer)。

Jvm垃圾回收器(算法篇)

admin阅读(48)

作者: 不二尘

来源:https://www.cnblogs.com/chenpt/p/9797126.html

在《Jvm垃圾回收器(基础篇)》中我们主要学习了判断对象是否存活还是死亡?两种基础的垃圾回收算法:引用计数法、可达性分析算法。以及Java引用的4种分类:强引用、软引用、弱引用、虚引用。和方法区的回收介绍。

那么接下来我们重点研究下虚拟机的几种常见的垃圾回收算法:标记-清除算法、复制算法、标记-整理算法、分代收集算法。

一:标记-清除算法

最基础的收集算法,总共分为‘ 标记 ’和‘ 清除 ’两个阶段

1.标记

标记出所有需要回收的对象

在《Jvm垃圾回收器(基础篇)》中说明了判断对象是否回收需要两次标记,现在我们再来回顾一下

一次标记:在经过可达性分析算法后,对象没有与GC Root相关的引用链,那么则被第一次标记。并且进行一次筛选:当对象有必要执行finalize()方法时,则把该对象放入F-Queue队列中。

二次标记:对F-Queue队列中的对象进行二次标记。在执行finalize()方法时,如果对象重新与GC Root引用链上的任意对象建立了关联,则把他移除出“ 即将回收 ”集合。否则就等着被回收吧!!!

对被第一次标记切被第二次标记的,就可以判定位可回收对象了。

2.清除

两次标记后,还在“ 即将回收 ”集合的对象进行回收。

执行过程如下:

 

 

 

 

优点:基础最基础的可达性算法,后续的收集算法都是基于这种思想实现的

缺点:标记和清除效率不高,产生大量不连续的内存碎片,导致创建大对象时找不到连续的空间,不得不提前触发另一次的垃圾回收。

二:复制算法

将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉。

复制算法执行过程如下:

 

 

 

 

优点:实现简单,效率高。解决了标记-清除算法导致的内存碎片问题。

缺点:代价太大,将内存缩小了一半。效率随对象的存活率升高而降低。

现在的商业虚拟机都采用这种算法(需要改良1:1的缺点)来回收新生代。

2.1 HotSpot虚拟机的改良算法

1.弱代理论

分代垃圾收集基于弱代理论。具体描述如下:

  • 大多说分配了内存的对象并不会存活太长时间,在处于年轻时代就会死掉。
  • 很少有对象会从老年代变成年轻代。

其中IBM研究表明:新生代中98%的对象都是”朝生夕死”; 所以并不需要按1:1比例来划分内存(解决了缺点1);

 2.Hotspot虚拟机新生代内存布局及算法

新生代内存分配一块较大的Eden空间和两块较小的Survivor空间

每次使用Eden和其中一块Survivor空间

回收时将Eden和Survivor空间中存活的对象一次性复制到另一块Survivor空间上

最后清理掉Eden和使用过的Survivor空间。

Hotspot虚拟机默认Eden和Survivor的大小比例是8:1。

分配担保

如果另一块Survivor空间没有足够内存来存放上一次新生代收集下来的存活对象,那么这些对象则直接通过担保机制进入老年代。

关于分配担保的内容,我会在讲述垃圾收集器时详细描述。

三:标记-整理算法

标记-整理算法是根据老年代的特点应运而生。

3.1 标记

标记过程和标记-清理算法一致(也是基于可达性分析算法)。

3.2 整理

和标记-清理不同的是,该算法不是针对可回收对象进行清理,而是根据存活对象进行整理。让存活对象都向一端移动,然后直接清理掉边界以外的内存。

标记-整理算法示意图

 

 

 

 

优点:不会像复制算法那样随着存活对象的升高而降低效率,不像标记-清除算法那样产生不连续的内存碎片

缺点:效率问题,除了像标记-清除算法的标记过程外,还多了一步整理过程,效率更低。

四:分代收集算法

当前商业虚拟机的垃圾收集都是采用“ 分代收集 ”算法。

根据对象存活周期的不同将内存划分为几块。一般把java堆分为新生代和老年代。JVM根据各个年代的特点采用不同的收集算法。

新生代中,每次进行垃圾回收都会发现大量对象死去,只有少量存活,因此比较适合复制算法。只需要付出少量存活对象的复制成本就可以完成收集。

老年代中,因为对象存活率较高,没有额外的空间进行分配担保,所以适合标记-清理、标记-整理算法来进行回收。

Jvm垃圾回收器(基础篇)

admin阅读(71)

作者: 不二尘

来源:https://www.cnblogs.com/chenpt/p/9797126.html

一:概述

在这篇文章中《Jvm运行时数据区》介绍了Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈,3个区域随着线程的生存而生存的。内存分配和回收都是确定的。随着线程的结束内存自然就被回收了,因此不需要考虑垃圾回收的问题。而Java堆和方法区则不一样,各线程共享,内存的分配和回收都是动态的。因此垃圾收集器所关注的都是这部分内存。

接下来我们就讨论Jvm是怎么回收这部分内存的。在进行回收前垃圾收集器第一件事情就是确定哪些对象还存活,哪些已经死去。下面介绍两种基础的回收算法。

1.1 引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它时计数器就+1,当引用失效时计数器就-1,。只要计数器等于0的对象就是不可能再被使用的。

此算法在大部分情况下都是一个不错的选择,也有一些著名的应用案例。但是Java虚拟机中是没有使用的。

优点:实现简单、判断效率高。

缺点:很难解决对象之间循环引用的问题。例如下面这个例子

Object a = new Object();
Object b = new Object();
a=b;
b=a;
a=b=null; //这样就导致gc无法回收他们。  

1.2 可达性分析算法

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有使用任何引用链时,则说明该对象是不可用的。

主流的商用程序语言(Java、C#等)在主流的实现中,都是通过可达性分析来判定对象是否存活的。

通过下图来清晰的感受gc root与对象展示的联系。所示灰色区域对象是存活的,Object5/6/7均是可回收的对象

在Java语言中,可作为GC Roots 的对象包括下面几种

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中静态变量引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈(即一般说的 Native 方法)中JNI引用的对象

优点:更加精确和严谨,可以分析出循环数据结构相互引用的情况;

缺点:实现比较复杂、需要分析大量数据,消耗大量时间、分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为”Stop The World”,是垃圾回收重点关注的问题)。

二:引用

在jdk1.2之后,Java对引用的概念进行了扩充,总体分为4类:强引用、软引用、弱引用、虚引用,这4中引用强度依次逐渐减弱。

  • 强引用:指在代码中普遍存在的,类似 Object obj = new Object(); 这类的引用,只有强引用还存在,GC就永远不会收集被引用的对象
  • 软引用:指一些还有用但并非必须的对象。直到内存空间不够时(抛出OutOfMemoryError之前),才会被垃圾回收。采用SoftReference类来实现软引用
  • 弱引用:用来描述非必须对象。当垃圾收集器工作时就会回收掉此类对象。采用WeakReference类来实现弱引用。
  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响, 唯一目的就是能在这个对象被回收时收到一个系统通知, 采用PhantomRenference类实现

 2.1 判断一个对象生存还是死亡

宣告一个对象死亡,至少要经历两次标记。

  1、第一次标记

如果对象进行可达性分析算法之后没发现与GC Roots相连的引用链,那它将会第一次标记并且进行一次筛选。

筛选条件:判断此对象是否有必要执行finalize()方法。

筛选结果:当对象没有覆盖finalize()方法、或者finalize()方法已经被JVM执行过,则判定为可回收对象。如果对象有必要执行finalize()方法,则被放入F-Queue队列中。稍后在JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发这个方法;

 

  2、第二次标记

GC对F-Queue队列中的对象进行二次标记。

如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱,那么只能被回收了。

  3、finalize() 方法

finalize()是Object类的一个方法、一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;

特别说明:并不提倡在程序中调用finalize()来进行自救。建议忘掉Java程序中该方法的存在。因为它执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用)。

三:回收方法区

永久代的垃圾收集主要分为两部分内容:废弃常量和无用的类

3.1 回收废弃常量

回收废弃常量与Java堆的回收类似。下面举个栗子说明

假如一个字符串“abc” 已经进入常量池中,但当前系统没有一个string对象是叫做abc的,也就是说,没有任何string对象的引用指向常量池中的abc常量,也没用其他地方引用这个字面量。如果这是发生内存回收,那么这个常量abc将会被清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

3.2 回收无用的类

需要同时满足下面3个条件的才能算是无用的类。

  1. 该类所有的实例都已经被回收,也就是Java堆中无任何改类的实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

虚拟机可以对同时满足这三个条件的类进行回收,但不是必须进行回收的。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制。

MySQL · 性能优化 · MySQL常见SQL错误用法

admin阅读(63)

作者:db匠

来源:https://yq.aliyun.com/articles/72501

1. LIMIT 语句

分页查询是最常用的场景之一,但也通常也是最容易出问题的地方。比如对于下面简单的语句,一般DBA想到的办法是在type, name, create_time字段上加组合索引。这样条件排序都能有效的利用到索引,性能迅速提升。

SELECT * 
FROM   operation 
WHERE  type = 'SQLStats' 
       AND name = 'SlowLog' 
ORDER  BY create_time 
LIMIT  1000, 10; 

好吧,可能90%以上的DBA解决该问题就到此为止。但当 LIMIT 子句变成 “LIMIT 1000000,10” 时,程序员仍然会抱怨:我只取10条记录为什么还是慢?

要知道数据库也并不知道第1000000条记录从什么地方开始,即使有索引也需要从头计算一次。出现这种性能问题,多数情形下是程序员偷懒了。在前端数据浏览翻页,或者大数据分批导出等场景下,是可以将上一页的最大值当成参数作为查询条件的。SQL重新设计如下:

SELECT   * 
FROM     operation 
WHERE    type = 'SQLStats' 
AND      name = 'SlowLog' 
AND      create_time > '2017-03-16 14:00:00' 
ORDER BY create_time limit 10;

在新设计下查询时间基本固定,不会随着数据量的增长而发生变化。

2. 隐式转换

SQL语句中查询变量和字段定义类型不匹配是另一个常见的错误。比如下面的语句:

mysql> explain extended SELECT * 
     > FROM   my_balance b 
     > WHERE  b.bpn = 14000000123 
     >       AND b.isverified IS NULL ;
mysql> show warnings;
| Warning | 1739 | Cannot use ref access on index 'bpn' due to type or collation conversion on field 'bpn'

其中字段bpn的定义为varchar(20),MySQL的策略是将字符串转换为数字之后再比较。函数作用于表字段,索引失效。

上述情况可能是应用程序框架自动填入的参数,而不是程序员的原意。现在应用框架很多很繁杂,使用方便的同时也小心它可能给自己挖坑。

3. 关联更新、删除

虽然MySQL5.6引入了物化特性,但需要特别注意它目前仅仅针对查询语句的优化。对于更新或删除需要手工重写成JOIN。

比如下面UPDATE语句,MySQL实际执行的是循环/嵌套子查询(DEPENDENT SUBQUERY),其执行时间可想而知。

UPDATE operation o 
SET    status = 'applying' 
WHERE  o.id IN (SELECT id 
                FROM   (SELECT o.id, 
                               o.status 
                        FROM   operation o 
                        WHERE  o.group = 123 
                               AND o.status NOT IN ( 'done' ) 
                        ORDER  BY o.parent, 
                                  o.id 
                        LIMIT  1) t); 

执行计划:

+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
| id | select_type        | table | type  | possible_keys | key     | key_len | ref   | rows | Extra                                               |
+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
| 1  | PRIMARY            | o     | index |               | PRIMARY | 8       |       | 24   | Using where; Using temporary                        |
| 2  | DEPENDENT SUBQUERY |       |       |               |         |         |       |      | Impossible WHERE noticed after reading const tables |
| 3  | DERIVED            | o     | ref   | idx_2,idx_5   | idx_5   | 8       | const | 1    | Using where; Using filesort                         |
+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+

重写为JOIN之后,子查询的选择模式从DEPENDENT SUBQUERY变成DERIVED,执行速度大大加快,从7秒降低到2毫秒。

UPDATE operation o 
       JOIN  (SELECT o.id, 
                            o.status 
                     FROM   operation o 
                     WHERE  o.group = 123 
                            AND o.status NOT IN ( 'done' ) 
                     ORDER  BY o.parent, 
                               o.id 
                     LIMIT  1) t
         ON o.id = t.id 
SET    status = 'applying' 

执行计划简化为:

+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
| id | select_type | table | type | possible_keys | key   | key_len | ref   | rows | Extra                                               |
+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
| 1  | PRIMARY     |       |      |               |       |         |       |      | Impossible WHERE noticed after reading const tables |
| 2  | DERIVED     | o     | ref  | idx_2,idx_5   | idx_5 | 8       | const | 1    | Using where; Using filesort                         |
+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+

4. 混合排序

MySQL不能利用索引进行混合排序。但在某些场景,还是有机会使用特殊方法提升性能的。

SELECT * 
FROM   my_order o 
       INNER JOIN my_appraise a ON a.orderid = o.id 
ORDER  BY a.is_reply ASC, 
          a.appraise_time DESC 
LIMIT  0, 20 

执行计划显示为全表扫描:

+----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+
| id | select_type | table | type   | possible_keys     | key     | key_len | ref      | rows    | Extra    
+----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+
|  1 | SIMPLE      | a     | ALL    | idx_orderid | NULL    | NULL    | NULL    | 1967647 | Using filesort |
|  1 | SIMPLE      | o     | eq_ref | PRIMARY     | PRIMARY | 122     | a.orderid |       1 | NULL           |
+----+-------------+-------+--------+---------+---------+---------+-----------------+---------+-+

由于is_reply只有0和1两种状态,我们按照下面的方法重写后,执行时间从1.58秒降低到2毫秒。

SELECT * 
FROM   ((SELECT *
         FROM   my_order o 
                INNER JOIN my_appraise a 
                        ON a.orderid = o.id 
                           AND is_reply = 0 
         ORDER  BY appraise_time DESC 
         LIMIT  0, 20) 
        UNION ALL 
        (SELECT *
         FROM   my_order o 
                INNER JOIN my_appraise a 
                        ON a.orderid = o.id 
                           AND is_reply = 1 
         ORDER  BY appraise_time DESC 
         LIMIT  0, 20)) t 
ORDER  BY  is_reply ASC, 
          appraisetime DESC 
LIMIT  20; 

5. EXISTS语句

MySQL对待EXISTS子句时,仍然采用嵌套子查询的执行方式。如下面的SQL语句:

SELECT *
FROM   my_neighbor n 
       LEFT JOIN my_neighbor_apply sra 
              ON n.id = sra.neighbor_id 
                 AND sra.user_id = 'xxx' 
WHERE  n.topic_status < 4 
       AND EXISTS(SELECT 1 
                  FROM   message_info m 
                  WHERE  n.id = m.neighbor_id 
                         AND m.inuser = 'xxx') 
       AND n.topic_type <> 5 

执行计划为:

+----+--------------------+-------+------+-----+------------------------------------------+---------+-------+---------+ -----+
| id | select_type        | table | type | possible_keys     | key   | key_len | ref   | rows    | Extra   |
+----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+
|  1 | PRIMARY            | n     | ALL  |  | NULL     | NULL    | NULL  | 1086041 | Using where                   |
|  1 | PRIMARY            | sra   | ref  |  | idx_user_id | 123     | const |       1 | Using where          |
|  2 | DEPENDENT SUBQUERY | m     | ref  |  | idx_message_info   | 122     | const |       1 | Using index condition; Using where |
+----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+

去掉exists更改为join,能够避免嵌套子查询,将执行时间从1.93秒降低为1毫秒。

SELECT *
FROM   my_neighbor n 
       INNER JOIN message_info m 
               ON n.id = m.neighbor_id 
                  AND m.inuser = 'xxx' 
       LEFT JOIN my_neighbor_apply sra 
              ON n.id = sra.neighbor_id 
                 AND sra.user_id = 'xxx' 
WHERE  n.topic_status < 4 
       AND n.topic_type <> 5 

新的执行计划:

+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
| id | select_type | table | type   | possible_keys     | key       | key_len | ref   | rows | Extra                 |
+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
|  1 | SIMPLE      | m     | ref    | | idx_message_info   | 122     | const    |    1 | Using index condition |
|  1 | SIMPLE      | n     | eq_ref | | PRIMARY   | 122     | ighbor_id |    1 | Using where      |
|  1 | SIMPLE      | sra   | ref    | | idx_user_id | 123     | const     |    1 | Using where           |
+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+

6. 条件下推

外部查询条件不能够下推到复杂的视图或子查询的情况有:

  1. 聚合子查询;
  2. 含有LIMIT的子查询;
  3. UNION 或UNION ALL子查询;
  4. 输出字段中的子查询;

如下面的语句,从执行计划可以看出其条件作用于聚合子查询之后:

SELECT * 
FROM   (SELECT target, 
               Count(*) 
        FROM   operation 
        GROUP  BY target) t 
WHERE  target = 'rm-xxxx' 
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
| id | select_type | table      | type  | possible_keys | key         | key_len | ref   | rows | Extra       |
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
|  1 | PRIMARY     | <derived2> | ref   | <auto_key0>   | <auto_key0> | 514     | const |    2 | Using where |
|  2 | DERIVED     | operation  | index | idx_4         | idx_4       | 519     | NULL  |   20 | Using index |
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+

确定从语义上查询条件可以直接下推后,重写如下:

SELECT target, 
       Count(*) 
FROM   operation 
WHERE  target = 'rm-xxxx' 
GROUP  BY target

执行计划变为:

+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
| 1 | SIMPLE | operation | ref | idx_4 | idx_4 | 514 | const | 1 | Using where; Using index |
+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+

关于MySQL外部条件不能下推的详细解释说明请参考以前文章:MySQL · 性能优化 · 条件下推到物化表

7. 提前缩小范围

先上初始SQL语句:

SELECT * 
FROM   my_order o 
       LEFT JOIN my_userinfo u 
              ON o.uid = u.uid
       LEFT JOIN my_productinfo p 
              ON o.pid = p.pid 
WHERE  ( o.display = 0 ) 
       AND ( o.ostaus = 1 ) 
ORDER  BY o.selltime DESC 
LIMIT  0, 15 

该SQL语句原意是:先做一系列的左连接,然后排序取前15条记录。从执行计划也可以看出,最后一步估算排序记录数为90万,时间消耗为12秒。

+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
| id | select_type | table | type   | possible_keys | key     | key_len | ref             | rows   | Extra                                              |
+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
|  1 | SIMPLE      | o     | ALL    | NULL          | NULL    | NULL    | NULL            | 909119 | Using where; Using temporary; Using filesort       |
|  1 | SIMPLE      | u     | eq_ref | PRIMARY       | PRIMARY | 4       | o.uid |      1 | NULL                                               |
|  1 | SIMPLE      | p     | ALL    | PRIMARY       | NULL    | NULL    | NULL            |      6 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+

由于最后WHERE条件以及排序均针对最左主表,因此可以先对my_order排序提前缩小数据量再做左连接。SQL重写后如下,执行时间缩小为1毫秒左右。

SELECT * 
FROM (
SELECT * 
FROM   my_order o 
WHERE  ( o.display = 0 ) 
       AND ( o.ostaus = 1 ) 
ORDER  BY o.selltime DESC 
LIMIT  0, 15
) o 
     LEFT JOIN my_userinfo u 
              ON o.uid = u.uid 
     LEFT JOIN my_productinfo p 
              ON o.pid = p.pid 
ORDER BY  o.selltime DESC
limit 0, 15

再检查执行计划:子查询物化后(select_type=DERIVED)参与JOIN。虽然估算行扫描仍然为90万,但是利用了索引以及LIMIT 子句后,实际执行时间变得很小。

+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
| id | select_type | table      | type   | possible_keys | key     | key_len | ref   | rows   | Extra                                              |
+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
|  1 | PRIMARY     | <derived2> | ALL    | NULL          | NULL    | NULL    | NULL  |     15 | Using temporary; Using filesort                    |
|  1 | PRIMARY     | u          | eq_ref | PRIMARY       | PRIMARY | 4       | o.uid |      1 | NULL                                               |
|  1 | PRIMARY     | p          | ALL    | PRIMARY       | NULL    | NULL    | NULL  |      6 | Using where; Using join buffer (Block Nested Loop) |
|  2 | DERIVED     | o          | index  | NULL          | idx_1   | 5       | NULL  | 909112 | Using where                                        |
+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+

8. 中间结果集下推

再来看下面这个已经初步优化过的例子(左连接中的主表优先作用查询条件):

SELECT    a.*, 
          c.allocated 
FROM      ( 
              SELECT   resourceid 
              FROM     my_distribute d 
                   WHERE    isdelete = 0 
                   AND      cusmanagercode = '1234567' 
                   ORDER BY salecode limit 20) a 
LEFT JOIN 
          ( 
              SELECT   resourcesid, sum(ifnull(allocation, 0) * 12345) allocated 
              FROM     my_resources 
                   GROUP BY resourcesid) c 
ON        a.resourceid = c.resourcesid

那么该语句还存在其它问题吗?不难看出子查询 c 是全表聚合查询,在表数量特别大的情况下会导致整个语句的性能下降。

其实对于子查询 c,左连接最后结果集只关心能和主表resourceid能匹配的数据。因此我们可以重写语句如下,执行时间从原来的2秒下降到2毫秒。

SELECT    a.*, 
          c.allocated 
FROM      ( 
                   SELECT   resourceid 
                   FROM     my_distribute d 
                   WHERE    isdelete = 0 
                   AND      cusmanagercode = '1234567' 
                   ORDER BY salecode limit 20) a 
LEFT JOIN 
          ( 
                   SELECT   resourcesid, sum(ifnull(allocation, 0) * 12345) allocated 
                   FROM     my_resources r, 
                            ( 
                                     SELECT   resourceid 
                                     FROM     my_distribute d 
                                     WHERE    isdelete = 0 
                                     AND      cusmanagercode = '1234567' 
                                     ORDER BY salecode limit 20) a 
                   WHERE    r.resourcesid = a.resourcesid 
                   GROUP BY resourcesid) c 
ON        a.resourceid = c.resourcesid

但是子查询 a 在我们的SQL语句中出现了多次。这种写法不仅存在额外的开销,还使得整个语句显的繁杂。使用WITH语句再次重写:

WITH a AS 
( 
         SELECT   resourceid 
         FROM     my_distribute d 
         WHERE    isdelete = 0 
         AND      cusmanagercode = '1234567' 
         ORDER BY salecode limit 20)
SELECT    a.*, 
          c.allocated 
FROM      a 
LEFT JOIN 
          ( 
                   SELECT   resourcesid, sum(ifnull(allocation, 0) * 12345) allocated 
                   FROM     my_resources r, 
                            a 
                   WHERE    r.resourcesid = a.resourcesid 
                   GROUP BY resourcesid) c 
ON        a.resourceid = c.resourcesid

总结

  1. 数据库编译器产生执行计划,决定着SQL的实际执行方式。但是编译器只是尽力服务,所有数据库的编译器都不是尽善尽美的。上述提到的多数场景,在其它数据库中也存在性能问题。了解数据库编译器的特性,才能避规其短处,写出高性能的SQL语句。
  2. 程序员在设计数据模型以及编写SQL语句时,要把算法的思想或意识带进来。
  3. 编写复杂SQL语句要养成使用WITH语句的习惯。简洁且思路清晰的SQL语句也能减小数据库的负担 ^^。

手动模拟JDK动态代理

admin阅读(79)

为哪些方法代理?

实现自己动态代理,首先需要关注的点就是,代理对象需要为哪些方法代理? 原生JDK的动态代理的实现是往上抽象出一层接口,让目标对象和代理对象都实现这个接口,怎么把接口的信息告诉jdk原生的动态代理呢? 如下代码所示,Proxy.newProxyInstance()方法的第二个参数将接口的信息传递了进去第一个参数的传递进去一个类加载器,在jdk的底层用它对比对象是否是同一个,标准就是相同对象的类加载器是同一个


ServiceInterface) Proxy.newProxyInstance(service.getClass().getClassLoader()
                , new Class[]{ServiceInterface.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("前置通知");
                method.invoke(finalService,args);
                System.out.println("后置通知");
                return proxy;
            }
        });

我们也效仿它的做法. 代码如下:


public class Test {
    public static void main(String[] args) {
        IndexDao indexDao = new IndexDao();
        Dao  dao =(Dao) ProxyUtil.newInstance(Dao.class,new MyInvocationHandlerImpl(indexDao));
        assert dao != null;
        System.out.println(dao.say("changwu"));
    }
}

拿到了接口的Class对象后,通过反射就得知了接口中有哪些方法描述对象Method,获取到的所有的方法,这些方法就是我们需要增强的方法

如何将增强的逻辑动态的传递进来呢?

JDK的做法是通过InvocationHandler的第三个参数完成,他是个接口,里面只有一个抽象方法如下: 可以看到它里面有三个入参,分别是 代理对象,被代理对象的方法,被代理对象的方法的参数


public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

当我们使用jdk的动态代理时,就是通过这个重写这个钩子函数,将逻辑动态的传递进去,并且可以选择在适当的地方让目标方法执行

InvocationHandler接口必须存在必要性1:

为什么不传递进去Method,而是传递进去InvocationHandler对象呢? 很显然,我们的初衷是借助ProxyUtil工具类完成对代理对象的拼串封装,然后让这个代理对象去执行method.invoke(), 然而事与愿违,传递进来的Method对象的确可以被ProxyUtil使用,调用method.invoke()但是我们的代理对象不能使用它,因为代理对象在这个ProxyUtil还以一堆等待拼接字符串, ProxyUtil的作用只能是往代理对象上叠加字符串,却不能直接传递给它一个对象,所以只能传递一个对象进来,然后通过反射获取到这个对象的实例,继而有可能实现method.invoke()

InvocationHandler接口必须存在必要性2:

通过这个接口的规范,我们可以直接得知回调方法的名字就是invoke()所以说,在拼接字符串完成对代理对象的拼接时,可以直接写死它

思路

我们需要通过上面的ProxyUtil.newInstance(Dao.class,new MyInvocationHandlerImpl(indexDao))方法完成如下几件事

  • 根据入参位置的信息,提取我们需要的信息,如包名,方法名,等等
  • 根据我们提取的信息通过字符串的拼接完成一个全新的java的拼接
    • 这个java类就是我们的代理对象
  • 拼接好的java类是一个String字符串,我们将它写入磁盘取名XXX.java
  • 通过ProxyUtil使用类加载器,将XXX.java读取JVM中,形成Class对象
  • 通过Class对象反射出我们需要的代理对象

ProxyUtil的实现如下:

public static Object newInstance(Class targetInf, MyInvocationHandler invocationHandler) {

    Method methods[] = targetInf.getDeclaredMethods();
    String line = "\n";
    String tab = "\t";
    String infName = targetInf.getSimpleName();
    String content = "";
    String packageContent = "package com.myproxy;" + line;
    //   导包,全部导入接口层面,换成具体的实现类就会报错
    //   
    String importContent = "import " + targetInf.getName() + ";" + line
                           + "import com.changwu.代理技术.模拟jdk实现动态代理.MyInvocationHandler;" + line
                           + "import java.lang.reflect.Method;" + line
                           + "import java.lang.Exception;" + line;

    String clazzFirstLineContent = "public class $Proxy implements " + infName +"{"+ line;
    String filedContent = tab + "private MyInvocationHandler handler;"+ line;
    String constructorContent = tab + "public $Proxy (MyInvocationHandler  handler){" + line
            + tab + tab + "this.handler =handler;"
            + line + tab + "}" + line;
    String methodContent = "";
    // 遍历它的全部方法,接口出现的全部方法进行增强
    for (Method method : methods) {
        String returnTypeName = method.getReturnType().getSimpleName();         method.getReturnType().getSimpleName());

        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();

        // 参数的.class
        String paramsClass = "";
        for (Class<?> parameterType : parameterTypes) {
            paramsClass+= parameterType.getName()+",";
        }

        String[] split = paramsClass.split(",");

        //方法参数的类型数组 Sting.class String.class
        String argsContent = "";
        String paramsContent = "";
        int flag = 0;
        for (Class arg : parameterTypes) {
            // 获取方法名
            String temp = arg.getSimpleName();
            argsContent += temp + " p" + flag + ",";
            paramsContent += "p" + flag + ",";
            flag++;
        }
        // 去掉方法参数中最后面多出来的,
        if (argsContent.length() > 0) {
            argsContent = argsContent.substring(0, argsContent.lastIndexOf(",") - 1);
            paramsContent = paramsContent.substring(0, paramsContent.lastIndexOf(",") - 1);
        }
        methodContent += tab + "public " + returnTypeName + " " + methodName + "(" + argsContent + ") {" + line
                + tab + tab+"Method method = null;"+line
                + tab + tab+"String [] args0 = null;"+line
                + tab + tab+"Class<?> [] args1= null;"+line

                // invoke入参是Method对象,而不是上面的字符串,所以的得通过反射创建出Method对象
                + tab + tab+"try{"+line
                // 反射得到参数的类型数组
                 + tab + tab + tab + "args0 = \""+paramsClass+"\".split(\",\");"+line
                 + tab + tab + tab + "args1 = new Class[args0.length];"+line
                 + tab + tab + tab + "for (int i=0;i<args0.length;i++) {"+line
                 + tab + tab + tab + "   args1[i]=Class.forName(args0[i]);"+line
                 + tab + tab + tab + "}"+line
                // 反射目标方法
                + tab + tab + tab + "method = Class.forName(\""+targetInf.getName()+"\").getDeclaredMethod(\""+methodName+"\",args1);"+line
                + tab + tab+"}catch (Exception e){"+line
                + tab + tab+ tab+"e.printStackTrace();"+line
                + tab + tab+"}"+line
                + tab + tab + "return ("+returnTypeName+") this.handler.invoke(method,\"暂时不知道的方法\");" + line; //
                 methodContent+= tab + "}"+line;
    }

    content = packageContent + importContent + clazzFirstLineContent + filedContent + constructorContent + methodContent + "}";

    File file = new File("d:\\com\\myproxy\\$Proxy.java");
    try {
        if (!file.exists()) {
            file.createNewFile();
        }

        FileWriter fw = new FileWriter(file);
        fw.write(content);
        fw.flush();
        fw.close();

        // 将生成的.java的文件编译成 .class文件
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
        Iterable units = fileMgr.getJavaFileObjects(file);
        JavaCompiler.CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
        t.call();
        fileMgr.close();

        // 使用类加载器将.class文件加载进jvm
        // 因为产生的.class不在我们的工程当中
        URL[] urls = new URL[]{new URL("file:D:\\\\")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class clazz = urlClassLoader.loadClass("com.myproxy.$Proxy");
        return clazz.getConstructor(MyInvocationHandler.class).newInstance(invocationHandler);
    } catch (Exception e) {
        e.printStackTrace();
    }
       return null;
}
}

运行的效果:

package com.myproxy;
import com.changwu.myproxy.pro.Dao;
import com.changwu.myproxy.pro.MyInvocationHandler;
import java.lang.reflect.Method;
import java.lang.Exception;
public class $Proxy implements Dao{
    private MyInvocationHandler handler;
    public $Proxy (MyInvocationHandler  handler){
        this.handler =handler;
    }
    public String say(String p) {
        Method method = null;
        String [] args0 = null;
        Class<?> [] args1= null;
        try{
            args0 = "java.lang.String,".split(",");
            args1 = new Class[args0.length];
            for (int i=0;i<args0.length;i++) {
               args1[i]=Class.forName(args0[i]);
            }
            method = Class.forName("com.changwu.myproxy.pro.Dao").getDeclaredMethod("say",args1);
        }catch (Exception e){
            e.printStackTrace();
        }
        return (String) this.handler.invoke(method,"暂时不知道的方法");
    }
}

解读

通过newInstance()用户获取到的代理对象就像上面的代理一样,这个过程是在java代码运行时生成的,但是直接看他的结果和静态代理差不错,这时用户再去调用代理对象的say(), 实际上就是在执行用户传递进去的InvocationHandeler里面的invoke方法, 但是亮点是我们把目标方法的描述对象Method同时给他传递进去了,让用户可以执行目标方法+增强的逻辑

当通过反射区执行Method对象的invoke()方法时,指定的哪个对象的当前方法呢? 这个参数其实是我们手动传递进去的代理对象代码如下


public class MyInvocationHandlerImpl implements MyInvocationHandler {
    private Object obj;
    public MyInvocationHandlerImpl(Object obj) {
        this.obj = obj;
    }
    @Override
    public Object invoke(Method method, Object[] args) {
        System.out.println("前置通知");
        try {
            method.invoke(obj,args);
        } catch (Exception e) {
            e.printStackTrace();
        }  
        System.out.println("后置通知");
        return null;
    }
}

作者:赐我白日梦

https://www.cnblogs.com/ZhuChangwu/p/11648911.html

 

关注微信公众号,非常感谢;

技术大牛如何找到靠谱的创业想法?

admin阅读(62)

对于不少有志创业的技术大牛,技术能力并非瓶颈,怎么找到靠谱的创业想法却让他们踩坑。

技术创业的门槛越来越高,商汤、旷世、寒武纪等AI独角兽聚集了众多技术人才,凭借着技术和人才的优势成为赛道领导者,这让很多技术大牛跃跃欲试。

懂商业的CEO+技术强的CTO仍是很好的创始团队组合,但很多成功技术创业公司的创始人不仅技术强,而且懂商业,甚至会在创业初期同时身兼CEO+CTO的职责,这是技术创业对创业者提出的高要求。

对于不少有志创业的技术大牛,技术能力并非瓶颈,怎么找到靠谱的创业想法却让他们踩坑:

第一个坑:拿着锤子找钉子

在最能发挥自己技术优势的领域创业是一个正常的逻辑,但这里隐藏的坑是,一些技术大牛太过于重视技术,以技术而非用户(客户)为中心去开发产品,最后满足的是一个没有人买单的伪需求。

例如一家技术创业公司,创始人背景很好,技术领先,凭借技术优势在初期融资顺利,但产品开发出来后落地困难,没有足够的收入,又因为商业模式没有被验证而后续融资困难,最后因为资金枯竭而创业失败。

第二个坑:只想做下一个谷歌、Facebook

创业要Dream Big,要找到体量巨大的蓝海市场,但这并不意味着每一个创业者都要创立下一个谷歌或Facebook。创业十分讲究Timing,这些巨头看似是从无到有开创了一个新市场,但当它们进入的时机,都是需求和技术成熟到恰到好处的时候。

进入市场太早,很容易成为别人的垫脚石。在一个过于前沿的赛道奔跑,成功的概率并不高,可能需求确实存在,但若其他的条件不成熟,也会失败。例如闪送的创始人于建红在2009年就做过类似菜鸟裹裹的项目,结果因为各方面的(主要是人员分工专业化和信息化)条件不成熟,导致项目失败。积累了这次失败的经验后,他在2013年抓准时机重新开始做闪送,但直到2017年和2018年闪送才真正进入大众视野。

明确了哪些坑应该避开,技术大牛们要用什么方法找到自己的创业想法?

方法一:聚焦

其实无论是否技术大牛,创业者都应该用聚焦的方式寻找属于自己的创业想法。我们假设有两种创业的领域,一个领域的用户量非常大,但用户的需求并不强烈;另一个领域用户量较少,但用户的需求非常迫切。几乎所有良好的创业想法都诞生在第二个领域。

第一个领域看起来拥有庞大的用户量,“市场天花板”很高,但需求不迫切代表用户可用可不用,可代替性高,用户和客户也就不会有强烈的付费意愿。第二个领域用户需求迫切,就更容易建立护城河,客户(用户)用了就离不开,付费意愿强,创业公司会拥有自我造血的能力。

当然,选择聚焦并不代表选择一个小的市场,产品在一个聚焦的市场把模式跑通后,还要能拥有在同类市场复制的能力。例如Facebook,最初在哈佛的校园跑通了模式,随后扩展到所有大学校园,乃至全世界。Uber,最初是在旧金山一个城市试点,现在其服务已经覆盖了全球的上百个国家。

方法二:从自身出发

从自身出发包括两个部分,第一是从自身的需求出发,第二是从自身的热爱出发。教育领域的创业公司Remind就是从自身需求出发的典型,该公司的联合创始人之一Brett Kopf上学期间被诊断患有注意力缺陷症,他的兄弟David Kopf为Brett建立了一个工具,能提醒他不要错过各类测试,这真的提升了Brett的学校表现。之后,兄弟俩决定把这个工具扩充成创业项目,解决学生、家长和老师之间的沟通效率问题。目前Remind在美国的月活用户超过2000万,覆盖美国50%以上的公立学校。

从自身热爱出发的典型是GoPro,它由Nick Woodman创立。Woodman热爱冲浪,在前两次的创业失败后,他与女友进行了一次冲浪之旅,在旅行的过程中,他萌生了一个想法:要是有一个可以安装手腕上的摄像头,能拍摄高质量的冲浪动作镜头就好了。最初他实现这个想法的方式是开发了一个定制的腕带,并与其他小型相机配合,而GoPro的第一个大订单是来自日本体育节目的100台相机。之后,运动相机这个市场逐渐成熟,更多的竞争者进来,但因为对需求的把握最精准,GoPro在很长时间内占据着市场的领导地位。

在有了几个初步创业想法后,应该通过以下的步骤筛除不靠谱的想法,并让那个相对靠谱的想法更加完善。

步骤一:建立列表

这个列表包括创业想法应该考虑的各类因素,包括:

 

客户(用户)是谁,他的需求是什么?

我们的解决方案是什么(概要)?

市场规模有多大?

为什么我们的方案比现有解决方案更好?(更快?更便宜?更可靠?)

商业模式。(谁付钱,单次付费金额,付费频次多少?)

商业化。(可至少分三个阶段:如果是To C的想法,阶段可分为100个用户,10000个用户,1000000个用户。如果是To B想法,阶段可分为1个客户,10个客户,100个客户。)

未来的增长。(如果成功解决了现有问题,还有什么相关的问题可以解决?)

 

步骤二:剔除小市场

首先应该剔除的就是市场规模不够大的想法。做以下几个动作效率会更高:

1.找出客户群的大小

可以参考人口数据,各类研究机构的报告,也可以做比较分析,研究相似产品的客群。

2.判断预期使用频率

使用频率非常重要,因为它将直接影响获客策略,客户生命周期价值,客户流失预测及定价模型等。一个明显的例子:一个用户每年使用几次的产品肯定不适合用月度订阅的形式收费。

3.判断“痛点”的疼痛程度

正如前文所说,如果瞄准的需求和“痛点”不够疼痛,那就不够聚焦,也就不容易建立起护城河。

4.判断客户的支付意愿

免费模式对于获客确实有帮助,但公司要活下去需要持续的收入和现金流。对于初创公司,“通过广告赚钱”并不是一个很好的模式,因为只有规模够大的平台级公司才能用广告模式获取足够的收入和利润,例如谷歌或百度。

步骤三:剔除薄弱的商业模式

很多新颖的创业想法之所以胎死腹中,就是因为无法找到合适的商业模式。靠谱的商业模式,建立在对客户(用户)足够了解的基础上,这不仅意味着大量的市场调查,用户的体验测试,在现在的时代也包括数据分析。

在确定商业模式时,固定成本、客户增长速度、客户获取成本、定价、客户生命周期价值等都需要考虑。

在筛除时,可以把商业模式建立保守、合理和乐观三个版本。在保守模式中,将获客成本设得偏高,客户生命周期价值舍得偏低,客户增速设得较慢,反之则以此类推。

如果一个想法的商业模式在乐观版本下可行,那并不“性感”,但若在保守版本下也能Work,则相对靠谱。

步骤四:验证技术可行性

技术大牛们在验证技术可行性方面经验丰富,本文对具体的过程不再赘述,只强调与创业相关的几个要点:

第一、找出技术可行性的过程同时也是验证需求是否强烈、是否“真实”的过程,以需求为中心构建产品,是在创业的任何阶段都应该坚持的原则。

第二、在创业初期就应该搭建良好的技术架构,当创业进入扩张期,大量的新增用户和新增数据会带来很大的冲击,而架构不够好时,在升级时也会浪费很多成本。

第三、应该预想好每一个产品阶段需要哪些类型和多少数量的相关技术人才,这关乎人才成本以及团队的建设规划。

最后,创业是一个长期的战役,它可能会花费创业者至少4-5年的时间,所以找到靠谱的创业想法不能靠拍脑袋,也不应该在半个月或一个月的时间里做决定。靠谱的创业想法对于创业成功与否起到的作用是决定性的,它值得创业者们花时间慎重对待。

关注微信公众号,非常感谢;

如何从程序员走向技术管理岗位?

admin阅读(75)

    
华丽转身是华而不实的假面具,我作为一名技术管理人员,建议大家不要轻易的转向管理岗位,坚持自己的技术才是根本。因为只有10%的技术专业人士具备相应的管理岗位所需要的特质,而更少的这样的人能够走到最后,管理岗位所做的工作不是技术人员所认为的非黑即白这样的事情,管理人员整个发展轨迹不仅仅需要自身能力、理念的培养,更需要伯乐的支持,而伯乐的支持是可遇不可求的,与其等待不如在自己可控制的领域一直前进,有机会自己也能够以更好的状态接受挑战。

        想接受挑战首先要认识自己,看自己是否适合做管理不妨考虑以下几点:对待问题是否公平公正,是否能够牺牲自我的利益顾全大局,是否有洞察力,是否能够了解组员的想法,是否能够做出正确的决策在没有条件许可的前提下……另外在该职责里面有两个重要的事情要做,一件事情是帮着老板干活,另一件事是替组员说话帮助他们争取相应的权益,所以夹心层的处理方式是处理好承担与托付,即责任的承担和信任的建立。技术管理者的技术不是最资深的,但应该具备专业的能力,并且是某个领域的技术和业务专家,因为一个人的能力是有限的,管理上有所建树便注定了技术道路上深度有限,但他有自己的技术见解并能够在需求把控、产品设计、架构设计中给予建设性意见并作出正确的决策,也只有这样才不会变成空中楼阁的大领导,拥有良好的技术基础是赢得你的团队信任的前提,也是自己能够建设一个可信任的团队的前提。作为管理者只有得到大家的认可,大家的信任,才能进而营造一个可信任的团队,使得各个部门之间互信,协调一致。而得到大家的信任作为管理者必须要有一定技术基础,也只有基于该基础才能做出正确的判断力,才能够确保团队整体方向的正确性。

一定走上管理岗位就很有成就感吗,我个人不敢苟同,这在于自己对自身的认知和个人经历的不同而不同,不能强求,但我建议每个技术人员能够经历项目经理的角色,尝试一下管理团队,看一下有哪些问题要解决,换一种角度思考你所参与的项目,培养自己的大局观,大局观重要性在于能够让个体将一个事物分析的更清晰,只有站得高才能看得远,大局观对个人而言很重要,拥有大局观会使个体在把握处理事情上更公正清晰,并更有利于作出正确的决策使得工作向良性方向发展。对于技术学习本身也需要融会贯通、举一反三,这也是一种意义上的大局观,所以思考角度决定着自己在其发展方向前进的远近,不要只是将自己局限于技术细节当中。

认识自己很重要,因为只有自己做了擅长的事情才能够事半功倍,游刃有余,改变一个人太难,与其效果甚微的改掉缺点不如发挥自己的优势。国外有一种创新的思维,认为缺点和优势其实是相辅相成的,你的缺点正好反映出了你拥有它对应的优点,换一个角度扬长避短,真的就事半功倍了。想一想对于组员又何尝不是呢,与其说服改变一个人不如知人善用,扬长避短。

时间管理上,处理问题上要有优先级,因为时间是有限的,资源也是有限的,管理者的决策影响着你的团队的方向,只有将更多地精力放在重要不紧急的事情上,才能产生更大的效能。做不重要的事情,再辛苦也是徒劳的。

如果说了这些你还是想走这条路不如先从思维习惯上转变一下角度,以下是管理培训中经常会提到的,我们也不妨再思考一下:

1.强将手下无弱兵

由专业技术人员转型为管理人员的管理者,常常因为自身的技术能力强,而永远以一个强者的姿态出现在下属面前,下属的一切问题都代为解决,认为强将手下无弱兵,这样就能够领导好团队。实际上这种做法却在无形中阻碍了下属的健康成长,使他们不能在实际工作中独当一面,因而管理学中有句名言:强将手下必弱兵。

2.不打无准备之仗

由于专业技术人员崇尚科学,所以在很多情况下,如果认为还没有准备充分,就很难做出科学、正确的决定;而作为管理人才,有时候就需要在准备尚未充足的情况下做出决定。

3.亲力亲为

专业转型的管理人员喜欢凡事亲力亲为,总是替下属解决本属于他们自己应该解决的问题,结果往往在这方面花费过多的精力,而没有更好地通过有效管理措施,提高整个团队的绩效。

4.追求技术完美

专业转型的管理人员往往过度追求技术上的完美,而牺牲了工作速度,甚至舍本逐末,忽略了对最终结果的认定。

关注微信公众号,非常感谢;

 

Java56项目网 更专业 更方便

联系我们