阿米尔·汗《摔跤吧,爸爸》

f9dcd100baa1cd1139763732b312c8fcc2ce2ddb

一直很喜欢阿米尔·汗主演的电影,他的电影总是给给予人一些力量,并引发深入思考,看完这个电影,流了好几次泪,有下面几个体会:

1,当过去一直自己带来荣誉或好成绩的技能(或者叫思想更为贴切),当有人让你放弃它,或者告诉你那是错的,一定要持有一种怀疑的态度,若这个人毫无成就,那就更不要相信了。

2,要在任何一个领域取得成功,必定是要话费比其他人更多的练习,适当舍弃一些东西才会收获的更多。

3,即使准备的很好了,在第一次尝试中也不可能一次获胜,获胜了那算是运气,每一次都是一次吸取经验的机会,失败了还想不断尝试就一定会获胜。

4,每个人在自己行动中都会是一个“迷茫者”,因为看不到自己,所以希望有人能在旁给予一些指点;在关键的时刻以旁观者看看自己,便会对自己身处的环境有更好的认识,从而做出正确的选择。

谈注意力

 C5E52D0E-82BB-4021-B8CB-CD119E5702DA

我一直认为注意力是决定人成败的关键因素,也是最重要的因素,你把注意力放在哪,哪一点就会更突出

在谈注意力之前我先先说说健身

一直以来我还是一个比较喜欢锻炼的,由于一直是坐着办公,所以有时间一般就会发时间去锻炼下身体。最初是是想练就更强壮的肌肉,买过哑铃,等各种健身器材,也办过健身卡,请教练训练,教练指导的基本方式就是,今天炼胸,明天炼背,后天练手臂… 总之每一次要让练的地方受伤。之后又喜欢上了跑步,因为跑步相对于去练肌肉时间显的比较自由,不管是早上,还是晚上,也不至于损伤肌肉,关节

我想喜欢或健身的,应该知道这两种健身方式的不同,前者是爆发力练习,后者则是耐力练习。

先说说爆力练习吧,那是在教练指导练习时,我有时总找不到感觉,比如今天教练是指导练胸的,我推了几组哑铃后,教练就问,感觉胸部怎么样,我说没感觉啊!这时他就会对我说:你没练到位,你肯定是其它地方借力了,你要练胸,比如是上胸部,就应该把注意力放在那里,用它来发力。果然按照他的建议,的确有效果。这就是我所理解的集中注意力带来的效果,当把自己所有力量放在胸部上时,它就被激活了,迫使它要发动所有的力量去工作,当然前几次是会受伤的,因为之前没经历过,只能举几次,并因此产生了疼痛,但如果是下次再使用同样重量来练习,我们就可以举的更多了

相反跑步,跑步是一件很枯燥的事情,一般跑步我都会带上耳机,听听音乐。

跑步,没有人会对你说,你要把注意力要放在身体的哪个部位上,今年我参加了一次马拉松,回想起来,我全程根本没有去想我正在跑步,我的两个腿要怎么运动,手要怎么摆,我一直想的是能让我提高热情,能再坚持一会的事,比如:旁边有人给我鼓掌加油,我可不能停下脚步啊;看下路牌,还有1公里就到15公里了,1公里多简单啊,在路程还很远时,我会想,前面有一个目标,我要先跑到那个目标点,不会把注意力放在剩余的路程上,时刻让自己能保持热情持续去跑!

两种运动,通过转移你的注意力就能锻炼爆发力或耐力。

结合我们的工作,生活,当我决心去学习一门新技术时,这显然是跑步这项运动,而且是长跑,最初学习是枯燥的,所以开始划分一个小的目标点,再转移注意力到这个小目标上,这是就会实现起来容易的多。在实现目标中,每一个小难题,或遇到的障碍就相当于健身中的小创伤,通过集中注意力,思考和反思,下次如果再遇到就可以比先前做的更好了。

再说注意力的一个好处,他可以让大脑更省力,如果把人脑比作一台电脑,我们都知道,当电脑只执行一个进程时,速度是很快的,如果频繁切换任务,这中间是很消耗CPU资源的,大脑也是,在大脑正处于高速运转时,突然被迫停止,如果再要重新启动起来,是要花费时间成本恢复到先前的状态的。所以你看到一个人再深入思考时,尽量少去打扰别人,因为你浪费的不是这一点点时间,还有别人恢复到之前状态的时间。

说了这么多注意力带来的好处,那如何能训练提高自己的注意力,我觉得最好的办法是把自己要当前重要的时间和计划写下来,反复的去看,当有些其它事情进入我们的视线,引起我们关注时,想一想,我目前要做的事情是什么,我的计划实现了多少,这样一些无关紧要的事情,就会主动去放弃,把注意力转移到要做的重要事情上。

引用古人的一句话:精诚所至,金石为开,当一件事情,高度集中的去做,即时像金石顽固的障碍都可以攻克!

HashMap原理解析

HashMap,在程序中我们经常要用到的集合,它的实现是通过数组+单向链表来实现的

先从HashMap的put方法讲起,基本思路是:

1,会通过数据的key做hash算法,得到数组bucket的下标值,

2,然后再把<key,value>以链表的形式(只有一个节点)插入到bucket[i]中,如果两个key算出来的下标值i一样,那么新的元素就会添加到链表之后

查看源代码:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

当key为null时,默认放入到数组的第0个位置,不为null,获取key的hashcode,进行右移16位并与hashcode做异或运算,得到下标值

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //tab数组为空,则创建一个
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根据容量大小和hash值计算(&)下标值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //若节点存在,则替换
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //tab存放为树,jdk8默认为8个节点
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //tab存放为链表,添加元素
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //如果map数量大于负载因子*最大容量,则扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

获取元素,基本思路:如果key为null,直接命中value,如果不为空,通过key获取数组下标值,如果第一个节点的key值相同,则直接返回,若为树,则遍历树结构,若为链表,则遍历链表对比key

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //先判断首节点
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //多个节点
            if ((e = first.next) != null) {
                //为树
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //为链表
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

数据扩容,resize(),基本思路是:将新的容量扩充到原来的2倍,并将旧的数组copy到新的数组中,注意原来链表或树的节点,位置可能会发生变化,要么是原来的位置,要么是原来位置*2,下面一幅图可以很好的描述resize元素的变化情况

d7acbad8-d941-11e4-9493-2c5e69d084c0

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            //超过限制的最大值,则不再扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //计算新的容量,是原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            //将旧的bucket复制到新的bucket中
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //若节点为双数,则index位置不变
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //若节点为单数,则index位置为原位置+oldCap
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

总结:

对于HashMap的原理可以理解如下:

目前有n个篮子,我要把贴有标签(标签代表key)的苹果放入篮子中,首先确定放入哪一个篮子中,所以通过苹果的标签来分类(hash算法),某一类的都放入在一个篮子中

为何要这样做,想想,如果将所有苹果都放入一个篮子中,那么我要取出某个标签的苹果,我就要在篮子中一个一个的找,如果苹果很多,这样效率是很低的,所以我是先通过苹果的标签定位到某个分类的篮子,再去里面找,这样的效率就提高很多了

参考文章:

1,http://yikun.github.io/2015/04/01/Java-HashMap%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/

2,http://blog.csdn.net/vking_wang/article/details/14166593

将项目发布到Maven仓库

我的开源包eweb已经发布到maven中心库了,使用maven配置就可以引入:

    <dependency>
        <groupId>com.github.chuanzh</groupId>
        <artifactId>eweb</artifactId>
        <version>1.0.1</version>
    </dependency>

下面讲讲如何发布到中心库,查了网上的步骤,都说的比较复制,但其实也很简单,这里分下面几个步骤:

一,Sonatype账号注册&创建一个Issue

二,使用GPG生成秘钥

三,修改项目的maven配置文件

四,上传构件到OSS&发布

看懂了之后其实很好理解,其实就类似于申请接入腾讯,微博的APP应用,

第一步,你得成为一个开发者吧,也就是要注册一个开发者账号,接下来就是要创建一个应用,等待腾讯、微博服务商审核

第二步,审核通过后,腾讯、微博会给你一对秘钥,但maven是要自己生成的,也就是用GPG

第三步,在你的项目中配置密钥和其他配置。

第四部,就是上传你的项目到腾讯、微博的服务平台,然后再等待审核,审核通过,那你就可以在他们的平台使用APP了

下面就从这4个方面说明下。

一,Sonatype账号注册&创建一个Issue

注册地址: Sign up for JIRA

记住你的用户名和密码,之后要用到

创建一个Issue:https://issues.sonatype.org/secure/CreateIssue.jspa?issuetype=21&pid=10134

v2-404b13199782405587859b6e73c02854_b

填写项目的基本信息:简单描述,详细描述,项目地址

重点说明下:Group Id为你项目的groupid,一般为公司或个人域名,如果你没有,就不要随便写了(他们在审核时会要求你提供一些证明的),写github的项目地址就可以了

好了,现在你就可以静静的等待maven审核了,一般为1~2天

二,使用GPG生成秘钥

首先需要下载GPG软件,Mac下载地址:GPG Suite,Windows用户下载地址:Secure email and file encryption with GnuPG for Windows,我的是Mac,所以具体说说Mac下的生成方式,Window方式请参考这篇博客

Mac下生成很简单,如下几个步骤:

1,新建一个密钥

v2-4fe9a772f939f0a8ef4fd2bfd103f97d_b

2,点击右键,将公钥发送至公钥服务器

v2-30789dafea5f96661c8e22b9c58ac165_b

到此密钥就生成了,记住你的口令,后面要用到

三,修改项目的maven配置文件

1,修改maven的setting配置

<settings>

    ...

    <servers>
        <server>
            <id>oss</id>
            <username>用户名</username>
            <password>密码</password>
        </server>
    </servers>

    ...

</settings>

用户名和密码就是你注册Sonatype的账号

2,配置项目pom.xml文件

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.github.chuanzh</groupId>
  <artifactId>eweb</artifactId>
  <version>1.0.1</version>
  <packaging>jar</packaging>

  <name>eweb</name>
  <description>轻量web开发框架</description>
  <url>https://github.com/chuanzh/eweb</url>

  <licenses>
    <license>
      <name>The Apache Software License, Version 2.0</name>
      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
    </license>
  </licenses>

  <developers>
    <developer>
      <name>chuan.zhang</name>
      <email>zhangchuan0305@gmail.com</email>
    </developer>
  </developers>

  <scm>
    <connection>scm:git@github.com:chuanzh/eweb.git</connection>
    <developerConnection>scm:git@github.com:chuanzh/eweb.git</developerConnection>
    <url>git@github.com:chuanzh/eweb.git</url>
  </scm>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>utf-8</project.reporting.outputEncoding>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.5.1</version>
        <configuration>
          <source>1.7</source>
          <target>1.7</target>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <profiles>
    <profile>
      <id>release</id>
      <distributionManagement>
        <snapshotRepository>
          <id>oss</id>
          <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
        </snapshotRepository>
        <repository>
          <id>oss</id>
          <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
        </repository>
      </distributionManagement>
      <build>
        <plugins>
          <!-- Source -->
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-source-plugin</artifactId>
            <version>3.0.1</version>
            <executions>
              <execution>
                <phase>package</phase>
                <goals>
                  <goal>jar-no-fork</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
          <!-- Javadoc -->
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-javadoc-plugin</artifactId>
            <version>2.10.4</version>
            <executions>
              <execution>
                <phase>package</phase>
                <goals>
                  <goal>jar</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
          <!-- Gpg Signature -->
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-gpg-plugin</artifactId>
            <version>1.6</version>
            <executions>
              <execution>
                <id>sign-artifacts</id>
                <phase>verify</phase>
                <goals>
                  <goal>sign</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>

</project>

pom.xml必须配置的有:name,description,licenses,developers,scm。

snapshotRepository和repository在Issure初审通过后会给你,另外id要和setting.xml中保持一致

6EC7FDB0-E87F-4A66-ABCD-D7DC14EC17D1

四,上传构件到OSS&发布

1,使用下面命令,会提示要求你输入GPG的密钥,就是申请时候填写的,此过程比较慢,需要耐心等待

mvn clean deploy -P release

2,在OSS中发布构件

使用sonatype账号登陆https://oss.sonatype.org ,通过模糊查找,选择Staging Repositories,如下图,该构件的状态此时应该为open,勾选它,点击close按钮,sonatype会先做检验,通过后,就可以点击Release了,如果审核不通过,尝试删掉后重新上传

49767CB8-C939-456A-B314-1DA45F606BFB

A521FDDB-E66E-4116-B604-DF098982EA20

3,通知sonatype已经发布构建

也就是在Issure中回复,再次等待,也是需要审核1~2天,若审核通过,会回复你如下的信息

C86DFA4C-BC11-4A82-8CEB-FD3F3D9BF75C

等待10分钟,你就可以在maven库中搜索到你的jar包了

3CB6AF70-C0F5-4F63-8F3C-14D22E8C3CA8

 

若下次修改了本地的代码,只需要重新执行第四步的命令即可

Mysql插入表情字符问题处理

最近在处理微信数据时,出现如下的错误:

ERROR[12-02 11:11:57]cn.hl.basic.datapersist.DbBasicService.execSql(DbBasicService.java:632): java.sql.SQLException: Incorrect string value: '\xF0\x9F\x8D\xAC",...' for column 'user_info' at row 1

错误是在获取微信用户信息后,插入数据库时发生的,基本可以判断是微信用户名中有特殊字符,所以导致数据插入不了

在网上也找资源半天,说是编码问题,需要调整编码为utf8mb4,但看了数据库这个字段的编码,却就是utf8mb4的,又看了些文章,网上有说要改MySQL服务端的编码,也就是修改配置,然而服务器的配置不是那么好动的,现在运行的项目有很多。

是否是我连接的时候编码就指定错了,或是可以通过连接URL来指定呢?这样想着,查看了MySQL的连接URL,目前的如下:

jdbc:mysql://ipandport/dbname?useOldAliasMetadataBehavior=true&autoReconnect=true&failOverReadOnly=false&characterEncoding=utf8

不是说使用utf8mb4吗?这里用的utf8,会影响吗?是这个问题吗?带着问题,又查看了网上的资料,

明白了一个问题,这些乱码是微信中的表情符,这些表情符是占4个字节的,而utf8却三个字节的,如果在连接URL中指定characterEncoding=utf8的话,那么他写入数据时就按照3个字节写入了,那肯定写不进去的,有不指定写入字节,按照本身的字符的字节来写吗?

有!!!就是useUnicode参数,设置为useUnicode=true,那么在写入数据时,会根据数据本身的字节来写,好吧,明白了问题的原因,果断试下,果然不出所料,实践证明了预想的结论。

说明:useUnicode的作用:

当设置useUnicode=true时,数据在存入数据库时会根据数据库字段的编码进行转换后,再存储。所以数据库的编码设置为utf8mb4,那么存储肯定没啥问题了。

正确的配置如下:

jdbc:mysql://ipandport/dbname?useOldAliasMetadataBehavior=true&autoReconnect=true&failOverReadOnly=false&userUnicode=true

以下为再网上查询的一些资料,放在这里,以供参考:

http://ourmysql.com/archives/1402

http://www.jianshu.com/p/20740071d854

网上还说了另一种方案,就是在知道会有这样字符的字段时,统一对其进行转码,比如转成base64的存入到数据库,取出时再统一转码,但是对于之前数据库中已经存在历史大量的数据,操作是很麻烦的,还要去刷数据,所以在连接指定字符是最简单可行的方法。

另外说下,对于有这样的字符,插入到数据库中的时候会变成?,不用担心,取出来后数据还是可以还原的。

7e5771da-804a-49c1-948d-42c83ad8a9aa

9a84e520-7b2b-468b-85e3-24e14962146a

LTS算法-判断物体上升还是下降

LTS算法又称最长公共子序列,即找出一组数中最长的子序列,比如我们可以用于判断一个物体是在上升还是下降

如何判断物体是在上升还是下降,单纯用末尾数字-首位数字,可能结果并不准确,因为物体可能是一个抖动形的移动趋势。

比如一个物体的高度移动数据为:{ 2000, 3125, 3325, 2000, 3325, 4025, 4575, 5625, 6900, 7925, 7950, 7925, 7700, 7275, 7050, 6925, 6500, 6350, 5400, 4825, 3150, 2250 }

使用LTS算法正序求得:{2000 2250 3150 4025 4575 4825 6350 6925 7950 },

逆序求得:{2000 3125 3325 5400 5625 6500 6900 7050 7275 7700 7925 7950}

我们从结果中分析逆序LTS更长,表明物体是在下降的趋势。

 

关于join查询使用遇到的问题

今天发现服务器上的一个sql执行非常慢,两张关联表查询

有一张关注表user_focus,和一张关注备注表user_focus_note

sql如下:

select t1.FLYID,t1.FLYKEY,t1.ISPUSH,t1.NOTIFYSTATE,t2.description,t2.remind_times from USER_FOCUS t1 left join USER_FOCUS_note t2 on t1.ID=t2.user_focus_id where (t1.USERID=’1378416183′ or t1.PHONEID=’18820063′) and t1.ORDERTYPE=’0′ and t1.NOTIFYSTATE=’0′ and t1.UPDATETIME >= ’2016-09-24 00:00:00′ UNION select t3.FLYID,t3.FLYKEY,t3.ISPUSH,t3.NOTIFYSTATE,t4.description,t4.remind_times from USER_FOCUS t3 left join USER_FOCUS_note t4 on t3.ID=t4.user_focus_id where (t3.USERID=’1378416183′ or t3.PHONEID=’18820063′) and t3.ORDERTYPE=’0′ and t3.NOTIFYSTATE in (1,2) and t3.UPDATETIME >= ’2016-10-24 09:55:19′;

进一步分析,发现下面sql执行很慢,基本上再20s以上,但返回结果只有1000多条

select t1.FLYID,t1.FLYKEY,t1.ISPUSH,t1.NOTIFYSTATE,t2.description,t2.remind_times from USER_FOCUS t1 left join USER_FOCUS_NOTE t2 on t1.ID=t2.user_focus_id where (t1.USERID=’1378416183′ or t1.PHONEID=’18820063′) and t1.ORDERTYPE=’0′ and t1.NOTIFYSTATE=’0′ and t1.UPDATETIME >= ’2016-09-24 00:00:00′

其中user_focus有1600万条数据,PHONEID,USERID,UPDATETIME 都建了索引,

去掉关联查询后,速度一下子就提升上来了,基本100ms就执行完了,所以确定是使用left join的问题,查看了下user_focus_note表,发现有18w条数据,数据量不大啊,怎么回事?

仔细查看了sql,这里有用到t1.ID=t2.user_focus_id,USER_FOCUS_NOTE 表user_focus_id建立了一个索引,重新执行sql,速度果然很快。USER_FOCUS 表虽然只有1000多条数据满足,但是查询USER_FOCUS_NOTE表时都需要扫描全表,这样就有180000*1000条扫描,所以查询肯定很慢

总结:类似两个大表,或一个大表、一个小表做关联查询时,一定要建立好索引。

 

JAVA内存模型

Java内存模式是理解多线程执行的重要依据。

什么是内存模型,为什么需要它?

先看一张图:

8A628D2C-5E67-4A6B-9A2E-66699729E2D4

比如给一个变量aVariable赋值:aVariable=3,当然单线程下是没什么问题的,但如果是多线程呢?会不会有影响?答案是肯定的,多线程下数据的读写都会发生竞争。

所以用一句话总结JAVA内存模型是:

通过加锁来控制多线程下共享变量的读写顺序操作

如何保证?JMM定义了一套Happens-Before规则,包括:

程序顺序规则:如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行

监视器锁规则:同一监视器的解锁发生在加锁之前。

volatile变量规则:对volatile变量的写入操作必须在对该变量的读操作之前执行。

线程启动规则:Thread.start的调用必须在线程其他操作之前执行

线程结束规则:线程的任何操作必须在检测到其他线程结束之前执行,或从Thread.join中成功返回,或调用Thread.isAlive时返回false

中断规则:调用interrupt时,必须在中断线程检测到interrupt之前执行(就是先调用了interrupt才会抛出InterruptedException,感觉是废话)

终结器规则:对象的构造函数必须在启动对象的终结器之前执行完成

传递性:如果操作A在操作B之前执行,且操作B在C之前执行,那么A必须在C之前执行

如果程序中不满足以上的情况,那么多线程执行下就会发生重排序,什么是重排序?举个例子,如下:

public class PossibleRecordering {

	static int x=0,y=0;
	static int a=0,b=0;

	public static void main(String[] args) throws InterruptedException {
		Thread one = new Thread(new Runnable() {
			@Override
			public void run() {
				a = 1;
				x = b;
			}
		});

		Thread other = new Thread(new Runnable() {
			@Override
			public void run() {
				b = 1;
				y = a;
			}
		});

		one.start();other.start();
		one.join(); other.join();
		System.out.println("x="+x+",y="+y);
	}

}

上面的例子x和y是多少呢?不确定,可能是0,也可能是1,线程中a=1和x=b两者没有任何依耐性,所以JMM会对这两个操作执行重排,先执行哪个还真不一定。如果想要x=b在a=1之后执行,那么将x用volatile修饰

当使用volatile修饰后,JMM确保

1)当第二个操作是volatile写时,不管第一个是什么,都不能发生重排

2)当第一个操作是volatile读时,不管第二个操作是什么,都不能发生重排

3)当第一个操作为volatile写,第二个是volatile读时,都不能发生重排

为了好记就是:一读二写不能发生重排

final关键字

在构造函数中初始化的使用final修饰的关键字对象,优先于其他线程的读操作

举个例子:

public class SafeStates {

	private final Map<String, String> states;
	private int i=0;

	static SafeStates obj;

	public SafeStates () {
		states = new HashMap<String, String>();
		states.put("alaska", "AK");
		states.put("alabama", "AL");
                i=1;
	}

	public void addStates() {
		states.put("add", "AD");
	}

	public static void writer() {
		obj = new SafeStates();
	}

	public static void reader() {
		SafeStates o = obj;
		System.out.println(o.states);
		System.out.println(o.i);
	}

}

一个线程先调用了writer方法,然后另外一个线程调用reader方法,JMM可确保使用final修饰的变量,的赋值操作在reader方法之前,即reader方法读取到的states对象是{“alaska”, “AK”,alabama”, “AL”},但i值就不可保证了,可能读取的值为0

另外如果在非构造方法中,比如调用了addState方法,其他线程在读取的时候也不能确保读取到states值的最新数据

 

一个简单的爬虫

在做数据爬虫的时候,我们会面临以下几个问题:

1, 页面数据获取(有时需要传入cookie)

2, IP被封,请求失败

3, 请求失败如何重试

针对这几个问题,我写了一个小的爬虫例子。

1, 请求数据:

以某官网数据为例:

根据查询条件输入数据,进行查询,打开chrome的控制台,找出返回数据的请求,可以看到head中需要传入cookie,请求参数为一个json格式字符串

HMW2BI{GB`]5G_IL}PW{{T6

1

 

下面我们使用JAVA中httpClient包发送请求:

以下在head中添加相关信息,请求参数封装为一个StringEntity传入,模拟http请求即可获取请求数据

private String catchTicket(String depCode,String arrCode,String flightDate) throws Exception {
    String param = "json格式参数";
    String cookie = "cookie数据";
    
    DefaultHttpClient httpclient = new DefaultHttpClient();
    HttpResponse response = null;
    String responseString = null;
    HttpPost httpost = null;
    IProxy iproxy = null;
    try {
        httpclient = new DefaultHttpClient();
        HttpParams params = httpclient.getParams();
        HttpConnectionParams.setConnectionTimeout(params, 50*1000);
        HttpConnectionParams.setSoTimeout(params, 120*1000);
        
        /* 设置代理 */
        iproxy = HttpProxy.getProxy();
        HttpHost httphost = new HttpHost(iproxy.getIp(), iproxy.getPort());    
        httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, httphost); 
        
        httpost = new HttpPost(POST_URL);
        httpost.addHeader("Accept", "application/json, text/javascript, */*; q=0.01");
        httpost.addHeader("Accept-Language", "zh-CN,zh;q=0.8");
        httpost.addHeader("Connection", "keep-alive");
        httpost.addHeader("Content-Type","application/json; charset=UTF-8");
        httpost.addHeader("Cookie", cookie);
        httpost.addHeader("Host", "www.united.com");
        httpost.addHeader("UserAgent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36");
        httpost.addHeader("X-Requested-With", "XMLHttpRequest");
        //httpost.addHeader("Accept-Encoding", "gzip, deflate");
        
        StringEntity parEntity = new StringEntity(param);
        parEntity.setContentType("application/json; charset=utf-8");
        httpost.setEntity(parEntity);
        
        response = httpclient.execute(httpost); // 执行
        responseString = EntityUtils.toString(response.getEntity(),"UTF-8");
        if (response.getStatusLine().getStatusCode() != 200 && response.getStatusLine().getStatusCode() != 404) {
            logger.info("response code error({}) throw exception",response.getStatusLine().getStatusCode());
            throw new Exception();
        }
    } catch (Exception e) {
        e.printStackTrace();
        HttpProxy.removeProxy(iproxy);
        throw e;
    } finally {
        httpost.abort();
        httpost = null;
        httpclient.getConnectionManager().shutdown();
        httpclient = null;
        
    }

    // 休眠1秒
    TimeUnit.SECONDS.sleep(1);
    return responseString;
}

可以看到以上代码使用了代理,每次请求都会从HttpProxy获取一个代理,当此请求出现错误,标识代理异常,将此代理IP从HttpProxy中移除

代理IP我是在kuaidaili网站上抓取的,HttpProxy类如下:

/**
 * 代理类
 * chuan.zhang
 */
public class HttpProxy {
    
    private static final Logger logger = LoggerFactory.getLogger(HttpProxy.class);
    private final static String POST_URL = "http://www.kuaidaili.com/proxylist/1/";
    private static List<IProxy> iproxys = new ArrayList<IProxy>();
    
    public static void main(String[] args) throws Exception {
        System.out.println(HttpProxy.getProxy());
    }
    
    /**
     * 随机生成一个代理
     * @return
     * @throws Exception
     */
    public static IProxy getProxy() throws Exception {
        if (iproxys.size() == 0) {
            initProxys();
            logger.info("init proxy over");
        }
        
        Random rand = new Random();
        int num = rand.nextInt(iproxys.size()-1);
        return iproxys.get(num);
    }
    
    public static void removeProxy(IProxy iproxy) {
        if (iproxy != null) {
            iproxys.remove(iproxy);
            logger.info("send request error remove the iproxy: "+iproxy.getIp()+":"+iproxy.getPort());
        }
    }

    /**
     * 初始化代理
     * 从http://www.kuaidaili.com/获取最新代理
     */
    private static List<IProxy> initProxys() throws Exception {
        DefaultHttpClient httpclient = new DefaultHttpClient();
        HttpResponse response = null;
        String responseString = null;
        HttpGet httget = null;
        
        try {
            httpclient = new DefaultHttpClient();
            HttpParams params = httpclient.getParams();
            HttpConnectionParams.setConnectionTimeout(params, 50*1000);
            HttpConnectionParams.setSoTimeout(params, 120*1000);
            
            httget = new HttpGet(POST_URL);
            httget.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
            httget.addHeader("Accept-Language", "zh-CN,zh;q=0.8");
            httget.addHeader("Connection", "keep-alive");
            httget.addHeader("cookie", "channelid=0; sid=1470121255086558; _gat=1; _ga=GA1.2.2135905250.1469704395; Hm_lvt_7ed65b1cc4b810e9fd37959c9bb51b31=1469704395,1469781681,1470121266; Hm_lpvt_7ed65b1cc4b810e9fd37959c9bb51b31=1470121847");
            httget.addHeader("Content-Type","application/json; charset=UTF-8");
            httget.addHeader("Host", "www.kuaidaili.com");
            httget.addHeader("Referer", "http://www.kuaidaili.com/proxylist/2/");
            httget.addHeader("UserAgent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36");
            
            response = httpclient.execute(httget); // 执行
            responseString = EntityUtils.toString(response.getEntity(),"UTF-8");
            
            Pattern p = Pattern.compile("<td data-title=\"IP\">([\\s\\S]*?)</td>[\\s\\S]*?<td data-title=\"PORT\">([\\s\\S]*?)</td>",
                    Pattern.DOTALL);
            Matcher m = p.matcher(responseString);
            IProxy iproxy = new IProxy();
            while (m.find()) {
                iproxy.setIp(m.group(1));
                iproxy.setPort(Integer.parseInt(m.group(2)));
                iproxys.add(iproxy);
            }
            
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("init proxy error");
            throw e;
        } finally {
            httget.abort();
            httget = null;
            httpclient.getConnectionManager().shutdown();
            httpclient = null;
        }
        
        return iproxys;
    }
    
}

当代理池中没有IP,则从代理网站中抓取一批代理,我这里只抓取了10个,当然事前抓取更多IP保存下来

最后说说访问失败重试机制:

public void execute() {
    while(true) {
        try {
            Map<String, Object> routeMap = this.dao.getRoute();
            logger.info("start catch {}", routeMap);
            String depCode = routeMap.get("dep_code").toString();
            String arrCode = routeMap.get("arr_code").toString();
            String flightDate = routeMap.get("catch_date").toString();
            JSONObject json = null;
            try {
                String result = this.catchTicket(depCode,arrCode,flightDate);
                json = JSONObject.parseObject(result);
            } catch (Exception e) {
                logger.info("catch error result: "+routeMap);
                this.retryCatch(routeMap,5);
                continue;
            }
        
            this.parseDataToDb(json);
        } catch (Exception e) {
            e.printStackTrace();
	    try {
                TimeUnit.MINUTES.sleep(30);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        }
    }
    
}

当请求数据出现异常,则会调用this.retryCatch(routeMap,5);方法,前面的一个参数为请求数据,后面的一个参数为重试次数,

如下:retryCatach方法实质是开启了一个线程,防止堵塞原来的逻辑,此线程会重新调用请求方法获取数据,如果请求成功,则停止请求,若请求失败,则会请求指定重试次数后停止,并在错误信息记录到数据库

/**
 * 重新抓取
 * @param routeMap
 * @param count
 */
private void retryCatch(final Map<String, Object> routeMap,final int count) {
    new Thread(new Runnable() {
        
        @Override
        public void run() {
            String depCode = routeMap.get("dep_code").toString();
            String arrCode = routeMap.get("arr_code").toString();
            String flightDate = routeMap.get("catch_date").toString();
            JSONObject json = null;
            for (int i=0;i<count;i++) {
                logger.info("retry catch ("+i+") {}", routeMap);
                try {
                    String result = catchTicket(depCode,arrCode,flightDate);
                    json = JSONObject.parseObject(result);
                } catch (Exception e) {
                    logger.info("retry catch ("+i+") error result: "+routeMap);
                    if (i == count-1) {
                        dao.updateRoute(routeMap.get("id").toString(), flightDate);
                    }
                    continue;
                }
                break;
            }
            
            parseDataToDb(json);
        }
    }).start();
}

以上一个基本的数据爬虫基本完成。

CountDownLatch使用原理

先看一个例子,来说明下CountDownLatch运用场景

public class SpiderDemo {
	private static Logger logger = LoggerFactory.getLogger(SpiderDemo.class);
	private static ExecutorService spiderPool = Executors.newFixedThreadPool(5);

	public static void main(String[] args) {

		CountDownLatch countDown = new CountDownLatch(5);
		for (int i=0;i<5;i++) {
			spiderPool.execute(new SpiderThread(countDown, i));
		}

		try {
			countDown.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		logger.info("spider end");
		spiderPool.shutdown();
	}

	static class SpiderThread implements Runnable{
		private CountDownLatch countDown;
		private int index;

		public SpiderThread(CountDownLatch countDown, int index) {
			this.countDown = countDown;
			this.index = index;
		}

		@Override
		public void run() {
			// TODO Auto-generated method stub
			logger.info("spider"+index+" data begin.. ");
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			logger.info("spider"+index+" data complete.. ");

			countDown.countDown();
		}

	}

}

我们来看看上面的例子,这里创建了一个CountDownLatch对象,传入一个数值,使用5个线程去执行SpiderThread,结束后使用countDown()方法将计数器递减,最后使用await()方法堵塞,直到所有的线程执行完成。

所以CountDownLatch的使用场景是可以异步去执行多个任务,主线程可以等待所有任务执行完成后,再继续执行。

下面从源码方面分析下CountDownLatch的实现原理

首先查看构造方法CountDownLatch(int count )

    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

	Sync(int count) {
            setState(count);
        }

当count小于0的时候,直接抛出异常,大于0则创建了一个Sync对象。Sync构造函数则是直接设置了一个变量值state来保存count数组

再看看countDown()方法,很显然它的目的是将count数组-1

    public void countDown() {
        sync.releaseShared(1);
    }

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

它里面是直接调用了releaseShared(1)方法,而releaseShared又调用了tryReleaseShared方法,如果尝试执行成功,则再调用doReleaseShared()方法

	protected boolean tryReleaseShared(int releases) {
        // Decrement count; signal when transition to zero
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            int nextc = c-1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }

tryReleaseShared是干什么的呢?从上面的例子看它的作用就是将count值-1,如果当前count值就是0了,那就啥都不干,直接返回false了,否则使用CAS算法尝试将count值-1,最后判断当前的count值是否为0,若为0则返回true,返回true就需要执行doReleaseShared方法了

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

doReleaseShared又干了些啥?doReleaseShared的作用就是唤醒主线程继续执行,这里调用了unparkSuccessor方法

最后来看下await()方法

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

可以看到最终会调用doAcquireSharedInterruptibly(若中途线程有被打断,则抛出异常了)

首先会添加一个Node节点(Node.SHARED看源码实质就是new Node()),这个节点就是保存所有调用await()方法的线程(因为可能会有多个线程调用了await()方法),所以这里每调用一次就保存下来一个节点,最后一个一个的释放,如果这里判断count不为0,则会使用park方法将线程挂起。