Java内存访问重排序的研究

public class PossibleReordering {
    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() {
            public void run() {
                a = 1;
                x = b;
            }
        });
    
        Thread other = new Thread(new Runnable() {
            public void run() {
                b = 1;
                y = a;
            }
        });
        one.start();other.start();
        one.join();other.join();
        System.out.println(“(” + x + “,” + y + “)”);
    }
}

这段代码的执行结果也可能是(0,0),因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。

得到(0,0)结果的语句执行过程:

a=1x=b这两个语句的赋值操作的顺序被颠倒了,或者说,发生了指令“重排序(reording)

大多数现代微处理器都会采用将指令乱序执行的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避免获取下一条指令所需数据时造成的等待,通过乱序执行的技术,处理器可以大大提高执行效率。

as-if-serial语义

所有的动作都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。

int a = 1;
int b = 2;
int c = a + b;

将上面的代码编译成Java字节码或生成机器指令,可视为展开成了以下动作:

1. 对a赋值1
2. 对b赋值2
3. 取a的值
4. 取b的值
5. 将取到两个值相加后存入c

在上面5个动作中,动作1可能会和动作2、4重排序,动作2可能会和动作1、3重排序,动作3可能会和动作2、4重排序,动作4可能会和动作1、3重排序。但动作1动作3、5不能重排序。动作2动作4、5不能重排序。因为它们之间存在数据依赖关系,一旦重排,as-if-serial语义便无法保证

内存访问重排序与内存可见性

计算机系统中,为了尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能。

在这种模型下会存在一个现象,即缓存中的数据与主内存的数据并不是实时同步的,各CPU(或CPU核心)间缓存的数据并不是实时同步的。从程序的视角来看,就是同一个时间点,各个线程所看到的共享变量的值可能是不一致的。

内存访问重排序与Java内存模型

根据Java内存模型中的规定,可以总结出以下几条happens-before规则。happens-before的前后两个操作不会被重排序且后者对前者的内存可见。

  • 程序次序法则:线程中的每个动作Ahappens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都能出现在动作A之后。
  • 监视器锁法则:对一个监视器锁的解锁happens-before于每一个后续对同一监视器锁的加锁。
  • volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。
  • 线程启动法则:在一个线程里,对Thread.start()的调用会happens-before于每个启动线程的动作。
  • 线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join()调用中成功返回,或Thread.isAlive()返回false
  • 中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。
  • 终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
  • 传递性:如果A happens-beforeB,且B happens-beforeC,则A happens-beforeC

Reference

http://tech.meituan.com/java-memory-reordering.html

2016/3/25 posted in  Java

`MySQL`索引原理及慢查询优化

MySQL索引原理

索引目的

在于提高查询效率,可以类比字典,如果要查mysql这个单词,我们肯定需要定位到m字母,然后从下往上找到y字母,再找到剩下的sql。如果没有索引,那么你可能需要把所有单词看一遍才能找到你想要的。

索引原理

2016/3/25 posted in  数据库

`HashMap`底层原理

引言

HashMap基于哈希表的Map接口的实现。此实现提供所有可选的映射操作,并允许使用null值和null键。(除了不同步和允许使用null之外,HashMap类与Hashtable大致相同)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap

Map map = Collections.synchronizedMap(new HashMap());

一、数据结构与冲突

HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap中主要是通过keyhashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。解决hash冲突的方法有很多,HashMap底层是通过链表来解决hash冲突的

图中,左边部分代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。右边部分则显示数组内部结构,HashMap其实就是一个Entry数组,Entry对象中包含了键和值,其中next也是一个Entry对象,它就是用来处理hash冲突的,形成一个链表。

二、HashMap相关属性

transient Entry[] table;//存储元素的实体数组

transient int size;//存放元素的个数

int threshold; //临界值,当实际大小超过临界值时,会进行扩容,扩容大小为当前的2倍。threshold = 负载因子*容量

final float loadFactor; //负载因子

transient int modCount;//被修改的次数

其中比较重要的两个参数是容量(Capacity) 和 负载因子(Load factor)

Initial capacity: The capacity is the number of buckets in the hash table, The initial capacity is simply the capacity at the time the hash table is created.
Load factor: The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.

简单的说,Capacity就是bucket的大小,loadFactor就是bucket填满程度的最大比例。

loadFactor越大,填满的元素越多,好处是,空间利用率高了,但冲突的机会加大了,链表长度会越来越长,查找效率降低。

反之,loadFactor越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了,表中的数据将过于稀疏(很多空间还没用,就开始扩容了)

因此,必须在 “冲突的机会” 与 “空间利用率” 之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的 “时-空” 矛盾的平衡与折衷.

如果机器内存足够,并且想要提高查询速度的话可以将loadFactor设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将loadFactor设置大一点。不过一般取默认值0.75就好。

三、使用频率最高的两个方法putget

put函数大致的思路为:

1、对keyhashCode()hash,然后再计算index

2、如果没碰撞直接放到bucket里;

3、如果碰撞了,以链表的形式存在buckets后;

4、如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;

5、如果节点已经存在就替换old value(保证key的唯一性);

6、如果bucket满了(超过loadFactor * current capacity),就要resize

public V put(K key, V value) 
{
    // 对key的hashCode()做hash
    return putVal(hash(key), key, value, false, true);
}
 
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;

    // 计算index,并对null做处理
    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;

        // 该链为树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

        // 该链为链表
        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;
    // 超过load factor*current capacity,resize
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

get函数大致思路如下:

1、bucket里的第一个节点,直接命中;

2、如果有冲突,则通过key.equals(k)去查找对应的entry

3、若为树,则在树中通过key.equals(k)查找,O(logn)

4、若为链表,则在链表中通过key.equals(k)查找,O(n)

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) 
        {
            // 在树中get
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 在链表中get
            do 
            {
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

四、resize的实现

put时,如果发现目前的bucket占用程度已经超过了loadFactor所希望的比例,那么就会发生resize。在resize的过程,简单的说就是把bucket扩充为2倍,之后重新计算index,把节点再放到新的bucket中。resize的注释是这样描述的:

Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.

大致意思就是说,当超过限制的时候会resize,然而又因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置

怎么理解呢?例如我们从16扩展为32时,具体的变化如下所示:

因此元素在重新计算hash之后,因为n变为2倍,那么n-1mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成原索引+oldCap。可以看看下图为16扩充为32的resize示意图:

这个设计非常巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。

总结

通过hash的方法,通过putget存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过loadFactorresize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,HashMap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。

2016/3/25 posted in  Java

倒排索引及`tf-idf`算法简介

倒排索引简介

倒排索引源于实际应用中需要根据属性的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)。带有倒排索引的文件我们称为倒排索引文件,简称倒排文件(inverted file)。

倒排文件(倒排索引),索引对象是文档或者文档集合中的单词等,用来存储这些单词在一个文档或者一组文档中的存储位置,是对文档或者文档集合的一种最常用的索引机制。

搜索引擎的关键步骤就是建立倒排索引,倒排索引一般表示为一个关键词,然后是它的频度(出现的次数),位置(出现在哪一篇文章或网页中,及有关的日期,作者等信息),它相当于为互联网上几千亿页网页做了一个索引,好比一本书的目录、标签一般。读者想看哪一个主题相关的章节,直接根据目录即可找到相关的页面。不必再从书的第一页到最后一页,一页一页的查找。

Lucene倒排索引原理

Lucene是一个开放源代码的高性能的Java全文检索引擎工具包,不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎。目的是为软件开发人员提供一个简单易用的工具包,以方便在目标系统中实现全文检索的功能,或者以此为基础建立起完整的全文检索引擎。

Lucene使用的是倒排文件索引结构。该结构及相应的生成算法如下:   

设有两篇文章1和2:

文章1的内容为:

    Tom lives in Guangzhou, I live in Guangzhou too.

文章2的内容为:

    He once lived in Shanghai.

取得关键词

由于Lucene是基于关键词索引和查询的,首先我们要取得这两篇文章的关键词,通常我们需要如下处理措施:   

  • 我们现在有的是文章内容,即一个字符串,我们先要找出字符串中的所有单词,即分词。英文单词由于用空格分隔,比较好处理。中文单词间是连在一起的需要特殊的分词处理    

  • 去掉没有意义的大众词汇。文章中的in, once, too等词没有什么实际意义,中文中的等字通常也无具体含义,这些不代表概念的词可以过滤掉   

  • 用户通常希望查He时能把含heHE的文章也找出来,所以所有单词需要统一大小写。   

  • 单词的统一化。用户通常希望查live时能把含liveslived的文章也找出来,所以需要把liveslived还原成live   

  • 文章中的标点符号通常不表示某种概念,也可以过滤掉   

在Lucene中以上措施由Analyzer类完成。 经过上面处理后,

文章1的所有关键词为:

    [tom] [live] [guangzhou] [i] [live] [guangzhou]

文章2的所有关键词为:

    [he] [live] [shanghai]

建立倒排索引

有了关键词后,我们就可以建立倒排索引了。上面的对应关系是:文章号文章中所有关键词。倒排索引把这个关系倒过来,变成: 关键词拥有该关键词的所有文章号

文章1,2经过倒排后变成   

关键词            文章号   
guangzhou        1
he               2 
i                1 
live             1,2
shanghai         2
tom              1 

通常仅知道关键词在哪些文章中出现还不够,我们还需要知道关键词在文章中出现次数和出现的位置,通常有两种位置:

  • 字符位置,即记录该词是文章中第几个字符(优点是关键词亮显时定位快)

  • 关键词位置,即记录该词是文章中第几个关键词(优点是节约索引空间、词组(phase)查询快),lucene中记录的就是这种位置  

加上出现频率出现位置信息后,我们的索引结构变为:   

关键词               文章号[出现频率]            出现位置 
guangzhou           1[2]                      3, 6  
he                  2[1]                      1
i                   1[1]                      4
live                1[2]                      2, 5
                    2[1]                      2 
shanghai            2[1]                      3
tom                 1[1]                      1 

live这行为例我们说明一下该结构:live文章1中出现了2次,文章2中出现了一次,它的出现位置为2,5,2这表示什么呢?我们需要结合文章号和出现频率来分析,文章1中出现了2次,那么2,5就表示live文章1中出现的两个位置,文章2中出现了一次,剩下的2就表示live文章2中第2个关键字。   

以上就是Lucene索引结构中最核心的部分。我们注意到关键字是按字符顺序排列的(Lucene没有使用B树结构),因此Lucene可以用二元搜索算法快速定位关键词。

实现

实现时,Lucene将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)、位置文件(positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。   

Lucene中使用了field的概念,用于表达信息所在位置(如标题中,文章中,url中),在建索引中,该field信息也记录在词典文件中,每个关键词都有一个field信息(因为每个关键字一定属于一个或多个field)。

压缩算法

为了减小索引文件的大小,Lucene对索引还使用了压缩技术。

首先,对词典文件中的关键词进行了压缩,关键词压缩为<前缀长度,后缀>

    例如:当前词为“阿拉伯语”,上一个词为“阿拉伯”,那么“阿拉伯语”压缩为<3,语>。

其次大量用到的是对数字的压缩数字只保存与上一个值的差值(这样可以减小数字的长度,进而减少保存该数字需要的字节数)。例如当前文章号是16389(不压缩要用3个字节保存),上一文章号是16382,压缩后保存7(只用一个字节)。

应用原因

下面我们可以通过对该索引的查询来解释一下为什么要建立索引。   

假设要查询单词live,Lucene先对词典二元查找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果。词典通常非常小,因而,整个过程的时间是毫秒级的。   

而用普通的顺序匹配算法,不建索引,而是对所有文章的内容进行字符串匹配,这个过程将会相当缓慢,当文章数目很大时,时间往往是无法忍受的。

TF-IDF及其算法

TF-IDF(term frequency – inverted document frequency)是一种用于资讯检索与资讯探勘的常用加权技术。TF-IDF是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。TF-IDF加权的各种形式常被搜寻引擎应用,作为文件与用户查询之间相关程度的度量或评级。除了TF-IDF以外,因特网上的搜寻引擎还会使用基于连结分析的评级方法,以确定文件在搜寻结果中出现的顺序。

原理

在一份给定的文件里,词频 (term frequency, TF)指的是某一个给定的词语在该文件中出现的次数。这个数字通常会被归一化(分子一般小于分母区别于IDF),以防止它偏向长的文件。(同一个词语在长文件里可能会比短文件有更高的词频,而不管该词语重要与否。)

逆向文件频率 (inverse document frequency, IDF)是一个词语普遍重要性的度量。某一特定词语的IDF,可以由总文件数目除以包含该词语之文件的数目,再将得到的商取对数得到。

某一特定文件内的高词语频率,以及该词语在整个文件集合中的低文件频率,可以产生出高权重的TF-IDF。因此,TF-IDF倾向于过滤掉常见的词语,保留重要的词语。

TFIDF的主要思想是:如果某个词或短语在一篇文章中出现的频率TF高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。TFIDF实际上是:TF * IDF,TF词频(Term Frequency),IDF反文档频率(Inverse Document Frequency)。TF表示词条在文档d中出现的频率(另一说:TF词频(Term Frequency)指的是某一个给定的词语在该文件中出现的次数)。IDF的主要思想是:如果包含词条t的文档越少,也就是n越小,IDF越大,则说明词条t具有很好的类别区分能力。如果某一类文档C中包含词条t的文档数为m,而其它类包含t的文档总数为k,显然所有包含t的文档数n=m+k,当m大的时候,n也大,按照IDF公式得到的IDF的值会小,就说明该词条t类别区分能力不强。(另一说:IDF反文档频率(Inverse Document Frequency)是指果包含词条的文档越少,IDF越大,则说明词条具有很好的类别区分能力。)但是实际上,如果一个词条在一个类的文档中频繁出现,则说明该词条能够很好代表这个类的文本的特征,这样的词条应该给它们赋予较高的权重,并选来作为该类文本的特征词以区别与其它类文档。这就是IDF的不足之处.

在一份给定的文件里,词频(term frequency,TF)指的是某一个给定的词语在该文件中出现的频率。这个数字是对词数(term count)的归一化,以防止它偏向长的文件。(同一个词语在长文件里可能会比短文件有更高的词数,而不管该词语重要与否。)

对于在某一特定文件 dj 里的词语 ti 来说,它的重要性可表示为:

以上式子中
是该词在文件
中的出现次数,而分母则是在文件中所有字词的出现次数之和。

逆向文件频率(inverse document frequency,IDF)是一个词语普遍重要性的度量。某一特定词语的IDF,可以由总文件数目除以包含该词语之文件的数目,再将得到的商取对数得到:

其中

  • |D|:语料库中的文件总数
  • :包含词语 的文件数目(即 的文件数目)如果该词语不在语料库中,就会导致被除数为零,因此一般情况下使用

然后

某一特定文件内的高词语频率,以及该词语在整个文件集合中的低文件频率,可以产生出高权重的TF-IDF。因此,TF-IDF倾向于过滤掉常见的词语,保留重要的词语。

示例

示例一

假如一篇文件的总词语数是100个,而词语母牛出现了3次,那么母牛一词在该文件中的词频tf就是3/100=0.03。一个计算文件频率 (DF) 的方法是测定有多少份文件出现过母牛一词,然后除以文件集里包含的文件总数。所以,如果母牛一词在1,000份文件出现过,而文件总数是10,000,000份的话,其逆向文件频率就是 log(10,000,000 / 1,000)=4。最后的TF-IDF的分数为0.03 * 4=0.12。

示例二

根据关键字k1,k2,k3进行搜索结果的相关性就变成 TF1 x IDF1 + TF2 x IDF2 + TF3 x IDF3。比如document1的term总量为1000,k1,k2,k3在document1出现的次数是100,200,50。包含了 k1, k2, k3的docuement总量分别是 1000, 10000,5000。document set的总量为10000。 TF1 = 100/1000 = 0.1 TF2 = 200/1000 = 0.2 TF3 = 50/1000 = 0.05 IDF1 = log(10000/1000) = log(10) = 2.3 IDF2 = log(10000/100000) = log(1) = 0; IDF3 = log(10000/5000) = log(2) = 0.69 这样关键字k1,k2,k3与docuement1的相关性= 0.1*2.3 + 0.2*0 + 0.05*0.69 = 0.2645其中k1比k3的比重在document1要大,k2的比重是0.

示例三

在某个一共有一千词的网页中原子能应用分别出现了2次、35次和5次,那么它们的词频就分别是 0.002、0.035 和 0.005。 我们将这三个数相加,其和 0.042 就是相应网页和查询“原子能的应用” 相关性的一个简单的度量。概括地讲,如果一个查询包含关键词 w1,w2,...,wN, 它们在一篇特定网页中的词频分别是: TF1, TF2, ..., TFN。 (TF: term frequency)。 那么,这个查询和该网页的相关性就是:TF1 + TF2 + ... + TFN。

读者可能已经发现了又一个漏洞。在上面的例子中,词站了总词频的 80% 以上,而它对确定网页的主题几乎没有用。我们称这种词叫应删除词(Stopwords),也就是说在度量相关性是不应考虑它们的频率。在汉语中,应删除词还有“是”、“和”、“中”、“地”、“得”等等几十个。忽略这些应删除词后,上述网页的相似度就变成了0.007,其中“原子能”贡献了 0.002,“应用”贡献了 0.005。细心的读者可能还会发现另一个小的漏洞。在汉语中,“应用”是个很通用的词,而“原子能”是个很专业的词,后者在相关性排名中比前者重要。因此我们需要给汉语中的每一个词给一个权重,这个权重的设定必须满足下面两个条件:

  1. 一个词预测主题能力越强,权重就越大,反之,权重就越小。我们在网页中看到“原子能”这个词,或多或少地能了解网页的主题。我们看到“应用”一次,对主题基本上还是一无所知。因此,“原子能“的权重就应该比应用大。

  2. 应删除词的权重应该是零。

我们很容易发现,如果一个关键词只在很少的网页中出现,我们通过它就容易锁定搜索目标,它的权重也就应该大。反之如果一个词在大量网页中出现,我们看到它仍然不很清楚要找什么内容,因此它应该小。概括地讲,假定一个关键词 w 在 Dw 个网页中出现过,那么 Dw 越大,w的权重越小,反之亦然。在信息检索中,使用最多的权重是“逆文本频率指数” (Inverse document frequency 缩写为IDF),它的公式为log(D/Dw)其中D是全部网页数。比如,我们假定中文网页数是D=10亿,应删除词“的”在所有的网页中都出现,即Dw=10亿,那么它的IDF=log(10亿/10亿)= log (1) = 0。假如专用词原子能在两百万个网页中出现,即Dw=200万,则它的权重IDF=log(500) =6.2。又假定通用词应用,出现在五亿个网页中,它的权重IDF = log(2)则只有 0.7。也就只说,在网页中找到一个原子能的比配相当于找到九个应用的匹配。利用 IDF,上述相关性计算个公式就由词频的简单求和变成了加权求和,即 TF1*IDF1 + TF2*IDF2 +... + TFN*IDFN。在上面的例子中,该网页和“原子能的应用”的相关性为 0.0161,其中“原子能”贡献了 0.0126,而“应用”只贡献了0.0035。这个比例和我们的直觉比较一致了。

附录:ElasticSearch相关查询

文档内容
curl -XGET '172.168.5.110:9200/logstash-wap-2016.03.22/wap/AVOcKIIFPPR76qlEVfH2?pretty'

结果

{
"_index" : "logstash-wap-2016.03.22",
"_type" : "wap",
"_id" : "AVOcKIIFPPR76qlEVfH2",
"_version" : 1,
"found" : true,
"_source" : {
"message" : "2016-03-22 10:30:12 - [ INFO ] [appName: wap] 172.168.5.224 - [cn.hao24.mobile.aop.RequestAOP] Beginning method: cn.hao24.mobile.controller.goods.GoodsController.getGoodsExtendDescAPP\tRequest end. This request cost [26 ms] time. => [SID: MasmqDCe1iFiullGvJWHPe8VIfzIJjeZk] [CustId: ] [CustIp: ] [OsVersion: ] [PhoneModel: ] V1",
"@version" : "1",
"@timestamp" : "2016-03-22T02:30:13.287Z",
"host" : "template-CentOS6.5",
"path" : "/hao24/logs/admin-log.log",
"timestamp" : "2016-03-22 10:30:12",
"loglevel" : "INFO",
"appName" : "wap",
"serverip" : "172.168.5.224",
"class" : "cn.hao24.mobile.aop",
"method" : "RequestAOP",
"status" : "Beginning method: cn.hao24.mobile.controller.goods.GoodsController.getGoodsExtendDescAPP\tRequest end. This request cost [26 ms] time.",
"sid" : "MasmqDCe1iFiullGvJWHPe8VIfzIJjeZk"
}
}

查看Status字段分词
curl -XPOST '172.168.5.110:9200/logstash-wap-2016.03.22/_analyze?pretty' -d '
{
    "text": "Beginning method: cn.hao24.mobile.controller.category.CategoryController.listAjax  Request end. This request cost [268 ms] time."
}'
结果
{
  "tokens" : [ {
    "token" : "beginning",
    "start_offset" : 0,
    "end_offset" : 9,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "method",
    "start_offset" : 10,
    "end_offset" : 16,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "cn.hao24",
    "start_offset" : 18,
    "end_offset" : 26,
    "type" : "<ALPHANUM>",
    "position" : 2
  }, {
    "token" : "mobile.controller.category.categorycontroller.listajaxrequest",
    "start_offset" : 27,
    "end_offset" : 88,
    "type" : "<ALPHANUM>",
    "position" : 3
  }, {
    "token" : "end",
    "start_offset" : 89,
    "end_offset" : 92,
    "type" : "<ALPHANUM>",
    "position" : 4
  }, {
    "token" : "this",
    "start_offset" : 94,
    "end_offset" : 98,
    "type" : "<ALPHANUM>",
    "position" : 5
  }, {
    "token" : "request",
    "start_offset" : 99,
    "end_offset" : 106,
    "type" : "<ALPHANUM>",
    "position" : 6
  }, {
    "token" : "cost",
    "start_offset" : 107,
    "end_offset" : 111,
    "type" : "<ALPHANUM>",
    "position" : 7
  }, {
    "token" : "268",
    "start_offset" : 113,
    "end_offset" : 116,
    "type" : "<NUM>",
    "position" : 8
  }, {
    "token" : "ms",
    "start_offset" : 117,
    "end_offset" : 119,
    "type" : "<ALPHANUM>",
    "position" : 9
  }, {
    "token" : "time",
    "start_offset" : 121,
    "end_offset" : 125,
    "type" : "<ALPHANUM>",
    "position" : 10
  } ]
}
TF-IDF分值
curl -XGET '172.168.5.110:9200/logstash-wap-2016.03.22/wap/AVOcKIIFPPR76qlEVfH2/_explain?pretty' -d '{
      "query" : {
        "term" : { "status" : "request" }
      }
}'
结果

{
"_index" : "logstash-wap-2016.03.22",
"_type" : "wap",
"_id" : "AVOcKIIFPPR76qlEVfH2",
"matched" : true,
"explanation" : {
"value" : 2.5711107,
"description" : "sum of:",
"details" : [ {
"value" : 2.5711107,
"description" : "weight(status:request in 31) [PerFieldSimilarity], result of:",
"details" : [ {
"value" : 2.5711107,
"description" : "fieldWeight in 31, product of:",
"details" : [ {
"value" : 1.4142135,
"description" : "tf(freq=2.0), with freq of:",
"details" : [ {
"value" : 2.0,
"description" : "termFreq=2.0",
"details" : [ ]
} ]
}, {
"value" : 1.8180498,
"description" : "idf(docFreq=439561, maxDocs=996081)",
"details" : [ ]
}, {
"value" : 1.0,
"description" : "fieldNorm(doc=31)",
"details" : [ ]
} ]
} ]
}, {
"value" : 0.0,
"description" : "match on required clause, product of:",
"details" : [ {
"value" : 0.0,
"description" : "# clause",
"details" : [ ]
}, {
"value" : 0.55003995,
"description" : "_type:wap, product of:",
"details" : [ {
"value" : 1.0,
"description" : "boost",
"details" : [ ]
}, {
"value" : 0.55003995,
"description" : "queryNorm",
"details" : [ ]
} ]
} ]
} ]
}
}

2016/3/23 posted in  others

高性能MySQL - Schema与数据类型优化

选择优化的数据类型

字符串类型

VARCHAR

VARCHAR类型用于存储可变长字符串。它比定长类型更节省空间,因为它仅使用必要的空间。

VARCHAR需要使用1或2个额外字节记录字符串的长度:如果列的最大长度小于或等于255字节,则只使用1个字节表示;否则使用2个字节。

CHAR

CHAR类型是定长的:MySQL总是根据定义的字符串长度分配足够的空间。当存储CHAR值时,MySQL会删除所有的末尾空格。

CHAR适合存储很短的字符串,或者所有值都接近同一个长度。;对于经常变更的数据,CHAR也比VARCHAR更好,因为定长的CHAR类型不容易产生碎片。对于非常短的列,CHARVARCHAR在存储空间上也更有效率。

日期和时间类型

除了特殊情况,通常应该尽量使用TIMESTAMP,因为它比DATETIME空间效率更高。

特殊类型数据

人们经常使用VARCHAR(15)列来存储IP地址。然而,它们实际上是32位无符号整数,不是字符串。用小数点将地址分成四段的表示方法只是为了让人们阅读容易。所以应该用无符号整数存储IP地址。

范式和反范式

范式的优点和缺点

范式化通常能够带来的好处

  • 范式化的更新操作通常比反范式化要快
  • 当数据较好地范式化时,就只有很少或者没有重复数据,所以只需要修改更少的数据
  • 范式化的表通常更小,可以更好地放在内存里,所以执行操作会更快
  • 很少有多余的数据意味着检索列表数据时更少需要DISTINCT或者GROUP BY语句。

范式化设计的schema的缺点是通常需要关联。稍微复杂一些的查询语句在符合范式的schema上都可能需要至少一次关联,也许更多。

反范式的优点和缺点

反范式化的schema因为所有数据都在一张表中,可以很好地避免关联。

如果不需要关联表,则对大部分查询最差的情况——即使表没有使用索引——是全表扫描。当数据比内存大时,这可能比关联要快得多,因为这样避免了随机I/O。

混用范式化和反范式化

加快ALTER TABLE操作的速度

不是所有的ALTER TABLE操作都会引起表重建。例如,有两种方法可以改变或者删除一个列的默认值(一种方法很快,另一种则很慢)。

假如要修改电影的默认租赁期限,从三天改到五天。下面是很慢的方式:

ALTER TABLE sakila.film
MODIFY COLUMN rental_duration TINYINT(3) NOT NULL DEFAULT 5;

SHOW STATUS显示这个语句做了1000次读和1000次插入操作。换句话说,它拷贝了整张表到一张新表,甚至列的类型、大小和可否为NULL属性都没改变。

另外一种方法是通过ALTER COLUMN操作来改变列的默认值:

ALTER TABLE sakila.film
ALTER COLUMN rental_duration SET DEFAULT 5;

这个语句会直接修改.frm文件而不涉及表数据。所以,这个操作是非常快的。

只修改.frm文件

下面这些操作是有可能不需要重建表的:

  • 移除(不是增加)一个列的AUTO_INCREMENT属性
  • 增加、移除,或更改ENUMSET常量。如果移除的是已经有行数据用到其值的常量,查询将会返回一个空字符串值

基本的技术是为想要的表结构创建一个新的.frm文件,然后用它替换掉已经存在的那张表的.frm文件,像下面这样:

  • 创建一张有相同结构的空表,并进行所需要的修改
  • 执行FLUSH TABLES WITH READ LOCK。这将会关闭所有正在使用的表,并且禁止任何表被打开。
  • 交换.frm文件
  • 执行UNLOCK TABLES来释放第二步的读锁

快速创建MyISAM索引

为了高效地载入数据到MyISAM表中,有一个常用的技巧是先禁用、载入数据,然后重新启用索引:

ALTER TABLE test.load_data DISABLE KEYS;
-- load the data
ALTER TABLE test.load_data ENABLE KEYS;

这个技巧能够发挥作用,是因为构建索引的工作被延迟到数据完全载入以后,这个时候已经可以通过排序来构建索引了。这样做会快很多,并且使得索引树的碎片更少、更紧凑。

2016/3/21 posted in  数据库