摘要

工欲善其事,必先利其器。作为JAVA的开发者,如果说会使用jdk是工作的基础,那么了解jdk的实现原理,甚至自己写一些框架或包就是工作的进阶。尤记得听一名老工程师跟我说过,等你把jdk都研究透了,你就是一名合格的高级工程师。Map集合在JAVA开发中很重要,就常用程度来讲,其在现代业务开发的代码里出现的频率位居前列。今天这篇文章主旨就是探索Map及其常见的实现类。
本文着重从jdk8代码出发来分析Map,与之响应的会讨论部分数据结构和算法方面的知识。我希望读者你已经有了很好的java基础,能够了解常用的设计模式并对面向对象编程思想拥有深刻的理解。本文会忽略部分过于基础的内容和不在本文讨论范围内的部分API。
首先,文章会简要的介绍Map接口,接着去分析jdk中几个最常见的Map接口的实现类。文章会花些笔墨在HashMap这个最常见的Map类上,其次会去分析TreeMap等类。最后文章还会对比HashMap来研究适合在并发场景下使用的ConcurrentHashMap,来分析它究竟是如何优化才能保证HashMap的线程安全性的。

方法

基本接口

Map是一种键-值对应的数据结构,java.util.Map 接口定义了Map常用的api。接下来我以代码的形式简要的对Map接口进行介绍。

package java.util;

import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.io.Serializable;

/**
 * 返回map中键值对集合的数目
 */
int size();

/**
 * 判断map中是否有键值对信息,如果该Map为空则返回 true 。
 */
boolean isEmpty();

/**
 判断map中是否有指定的key元素,如果有该元素,则返回 true 。
 */
boolean containsKey(Object key);

/**
 如果map中有一个或多个key所映射的值为value,则返回 true 。
 */
boolean containsValue(Object value);

/**
 根据指定的key在map中获取与之对应的value并返回。
 */
V get(Object key);

/**
 将key/value对放入到map中,如果map中已经存在了键为key的元素,则将原来的值替换为新value。
 */
V put(K key, V value);

/**
 从map中移除键为key的元素并返回
 */
V remove(Object key);


/**
将目标集合中的所有元素全部放到该map中
 */
void putAll(Map<? extends K, ? extends V> m);

/**
 清空集合中的所有元素
 */
void clear();


/**
 返回map中所有key的集合
 该方法所返回的集合并非创建一个全新集合,如果对该集合进行删除操作会直接影响到原来map中的元素
 */
Set<K> keySet();

/**
 返回map中所有value的集合
 该方法所返回的集合也与map有对应关系,对map本身所做的删除操作会进而影响到此集合
 */
Collection<V> values();

/**
 返回map中的元素集合
 该元素集合与map还存在映射关系
 */
Set<Map.Entry<K, V>> entrySet();

/**
 * map元素接口
 将map元素封装成键-值对的格式
 */
interface Entry<K,V> {
  ...//Entry中的内容暂不介绍
}

boolean equals(Object o);

int hashCode();

// Defaultable methods

/**
 返回指定的key所对应的value或默认值
 * @since 1.8
 */
default V getOrDefault(Object key, V defaultValue) {
    V v;
    return (((v = get(key)) != null) || containsKey(key))
        ? v
        : defaultValue;
}

/**
 对map集合进行遍历操作
 * @since 1.8
 */
default void forEach(BiConsumer<? super K, ? super V> action) {
    Objects.requireNonNull(action);
    for (Map.Entry<K, V> entry : entrySet()) {
        K k;
        V v;
        try {
            k = entry.getKey();
            v = entry.getValue();
        } catch(IllegalStateException ise) {
            // this usually means the entry is no longer in the map.
            throw new ConcurrentModificationException(ise);
        }
        action.accept(k, v);
    }
}

/**
 将符合条件的map中的元素进行替换
 * @since 1.8
 */
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
    Objects.requireNonNull(function);
    for (Map.Entry<K, V> entry : entrySet()) {
        K k;
        V v;
        try {
            k = entry.getKey();
            v = entry.getValue();
        } catch(IllegalStateException ise) {
            // this usually means the entry is no longer in the map.
            throw new ConcurrentModificationException(ise);
        }

        // ise thrown from function is not a cme.
        v = function.apply(k, v);

        try {
            entry.setValue(v);
        } catch(IllegalStateException ise) {
            // this usually means the entry is no longer in the map.
            throw new ConcurrentModificationException(ise);
        }
    }
}

/**
 如果map中没有指定的key,或指定的key对应的value值为null,则将key-value中的value放入到map中,并返回新value。如果map中已经存在该key,且value不是null,则返回原值。
 * @since 1.8
 */
default V putIfAbsent(K key, V value) {
    V v = get(key);
    if (v == null) {
        v = put(key, value);
    }

    return v;
}

/**
以同时指定键值对的方式来从map中移除元素,返回操作成功状态
 * @since 1.8
 */
default boolean remove(Object key, Object value) {
    Object curValue = get(key);
    if (!Objects.equals(curValue, value) ||
        (curValue == null && !containsKey(key))) {
        return false;
    }
    remove(key);
    return true;
}

/**
 如果原元素存在,则将原元素的值替换为新值
 * @since 1.8
 */
default boolean replace(K key, V oldValue, V newValue) {
    Object curValue = get(key);
    if (!Objects.equals(curValue, oldValue) ||
        (curValue == null && !containsKey(key))) {
        return false;
    }
    put(key, newValue);
    return true;
}

/**
 当前的key在map中有对应的映射时,使用新元素替换原有元素
 * @since 1.8
 */
default V replace(K key, V value) {
    V curValue;
    if (((curValue = get(key)) != null) || containsKey(key)) {
        curValue = put(key, value);
    }
    return curValue;
}

/**
 如果指定的key在map中的映射为空(或者map中不包含此key),则尝试使用给定的算法去计算一个值作为key的映射,并将这个元素存放进map中。
 * @since 1.8
 */
default V computeIfAbsent(K key,
        Function<? super K, ? extends V> mappingFunction) {
    Objects.requireNonNull(mappingFunction);
    V v;
    if ((v = get(key)) == null) {
        V newValue;
        if ((newValue = mappingFunction.apply(key)) != null) {
            put(key, newValue);
            return newValue;
        }
    }

    return v;
}

/**
 如果map中已有指定的key和其映射,则尝试使用给定的函数重新计算一个value替换原值
 * @since 1.8
 */
default V computeIfPresent(K key,
        BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    V oldValue;
    if ((oldValue = get(key)) != null) {
        V newValue = remappingFunction.apply(key, oldValue);
        if (newValue != null) {
            put(key, newValue);
            return newValue;
        } else {
            remove(key);
            return null;
        }
    } else {
        return null;
    }
}

/**
 使用给定的函数计算一个在map中与key对应的新值用来替换掉原来与key映射的value
 * @since 1.8
 */
default V compute(K key,
        BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    V oldValue = get(key);

    V newValue = remappingFunction.apply(key, oldValue);
    if (newValue == null) {
        // delete mapping
        if (oldValue != null || containsKey(key)) {
            // something to remove
            remove(key);
            return null;
        } else {
            // nothing to do. Leave things as they were.
            return null;
        }
    } else {
        // add or replace old mapping
        put(key, newValue);
        return newValue;
    }
}

/**
 如果在map中与key所对应的value为null,则使用新value去替换原来的值,否则就使用remappingFunction在原值和新值之间计算出一个值用来做key的映射。
 * @since 1.8
 */
default V merge(K key, V value,
        BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    Objects.requireNonNull(value);
    V oldValue = get(key);
    V newValue = (oldValue == null) ? value :
               remappingFunction.apply(oldValue, value);
    if(newValue == null) {
        remove(key);
    } else {
        put(key, newValue);
    }
    return newValue;
}
}

/*

*/

我们看到,jdk8对map类进行了大量的改动,增加了很多带有默认方法的api,其中包括如foreach等实用的api。

Map骨架

java.util.AbstractMap 类是一个抽象类,其对map接口提供了最简单的模版方法实现,如求map的entrySet(),hashCode()等方法。我们常用的类HashMap就是继承自此类,不过由于此类实现的时间较早(jdk1.2),当前大部分继承自此类的的类激斗都对此类的方法进行了更符合性能的优化重写。

哈希表 (HashTable)

java.util.HashTable是数据结构中的哈希表的java实现,其内部存储key-value形式的键值对元素,任何不是null的对象都可以被当做key或value。为了能够正常的检索、存储对象,所有的key都应该有可用的hashCode()方法。哈希表的性能主要由初始化大小和负载因数两个重要的要素所影响,HashTable基于大量数据进行测验与分析,综合考虑存储时的空间兼用与检索时的时间消耗情况比,将负载因数的初始值设置为 0.75f ,如无必要情况,无需修改此值。对于初始化大小这个参数,其默认值为11,我们可以根据实际业务任意进行定制。HashTable是线程安全的,但是在实际开发使用中在没有线程安全的场景下java官方推荐我们直接使用HashTable,在高并发的场景下,我们更应该优先考虑使用java.util.concurrent.ConcurrentHashMap类。

public class Hashtable<K,V>
  extends Dictionary<K,V>
  implements Map<K,V>, Cloneable, java.io.Serializable {

  /** 哈希表数据实际存放在Entry数组中 */
  private transient Entry<?,?>[] table;

  /** 哈希表中entry的数量 */
  private transient int count;

  /* 重新进行hash计算的阈值 */
  private int threshold;

  /** 哈希表的负载因数 */
  private float loadFactor;

  /**
   哈希表已修改数据结构的次数
   增加或减少Entry节点的数目或对哈希表进行扩容都会触发该值的变化,modCount主要用来在高并发场景下进行快速失败使用。
   */
  private transient int modCount = 0;

  /** 序列化版本id */
  private static final long serialVersionUID = 1421746759512286392L;


}
/**
while(i.hasBext()){
  Entry e = i.next();
  if(value == e.getValue || value.equals(e.getValue))
    return true;
  }
  return false;
  ****/

哈希表的基本属性中有一个 threshold 属性,这个属性代表着重新分配哈希空间的阈值,当哈希表中存放的数据超出此值所标识的大小,哈希表就会进行自动扩容并重新计算 threshold 值,其计算公式为 threshold = (int)(capacity * loadFactor)

首先我们分析一下用来检查是否包含某元素的contains()方法:

public synchronized boolean contains(Object value) {
      if (value == null) {
          throw new NullPointerException();
      }

      Entry<?,?> tab[] = table;
      for (int i = tab.length ; i-- > 0 ;) {
          for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
              if (e.value.equals(value)) {
                  return true;
              }
          }
      }
      return false;
  }
  public synchronized V get(Object key) {
      Entry<?,?> tab[] = table;
      int hash = key.hashCode();
      int index = (hash & 0x7FFFFFFF) % tab.length;
      for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
          if ((e.hash == hash) && e.key.equals(key)) {
              return (V)e.value;
          }
      }
      return null;
  }

  /** 为哈希表添加节点 ****/

  private void addEntry(int hash, K key, V value, int index) {
      modCount++;

      Entry<?,?> tab[] = table;
      if (count >= threshold) {
          // Rehash the table if the threshold is exceeded
          rehash();

          tab = table;
          hash = key.hashCode();
          index = (hash & 0x7FFFFFFF) % tab.length;
      }

      // Creates the new entry.
      @SuppressWarnings("unchecked")
      Entry<K,V> e = (Entry<K,V>) tab[index];
      tab[index] = new Entry<>(hash, key, value, e);
      count++;
  }

  /** 向哈希表中存放某值 */
  public synchronized V put(K key, V value) {
      // Make sure the value is not null
      if (value == null) {
          throw new NullPointerException();
      }

      // 确认哈希表中不存在该key
      Entry<?,?> tab[] = table;
      int hash = key.hashCode();
      int index = (hash & 0x7FFFFFFF) % tab.length;
      @SuppressWarnings("unchecked")
      Entry<K,V> entry = (Entry<K,V>)tab[index];
      for(; entry != null ; entry = entry.next) {
          if ((entry.hash == hash) && entry.key.equals(key)) {
              V old = entry.value;
              entry.value = value;
              return old;
          }
      }

      addEntry(hash, key, value, index);
      return null;
  }
  /** 向哈希表中添加节点 ****/
  public synchronized void clear() {
      Entry<?,?> tab[] = table;
      modCount++;
      for (int index = tab.length; --index >= 0; )
          tab[index] = null;
      count = 0;
  }

  public Set<K> keySet() {
      if (keySet == null)
          keySet = Collections.synchronizedSet(new KeySet(), this);
      return keySet;
  }

HashTable的contains方法只能遍历查找,其时间复杂度是O(n),在最坏的情况下我们可能需要遍历整个哈希表中的所有Entry。HashTable中可能发生线程安全问题的方法都使用 synchronized 关键字添加互斥锁,如此以来,虽然保证了其线程安全性,但对方法实现互斥会让整个对象都受影响,这也间接的给出了java不推荐在高并发的场景下使用HashTable的解释。其get()方法首先对key进行哈希运算然后通过取模等操作计算出key所在的索引,然后进行遍历操作,其时间复杂度为 O(logn)。这种算法被称之为哈希查找,拥有极高的效率,比较推崇。向哈希表中存放数据时,首先根据key的hash计算索引,然后在索引的table上找到其Entry链表,遍历该链表中是否已存在该key,如果已存在则将value替换,否则就在table上index索引的Entry链表后追加该元素。这种算法原理符合人类的思维形式,比较利于理解,可计算机CPU并不擅长求余计算,进行哈希查找时有一定的计算资源浪费。至于clear()方法就是遍历清除table上的每一个节点,将哈希表的大小置0。等待垃圾回收时直接就将哈希表中的内容全部回收,释放内存。哈希表的keySet()等方法也都是线程安全的,其实现原理就是通过Collections.synchronizedSet()方法将原有的keySet结合哈希表本身作为互斥锁转化成线程安全的集合。keySet的迭代操作使用哈希表内部进行专有优化的迭代器进行迭代,在保证线程安全的前提下,尽量加快迭代速度。

HashMap 经典容器

HashMap是我们在开发中最为钟爱的Map类,使用时简单方便,且性能极高。其内部实现由自己定制化的高效hash算法,jdk8更是对HashMap做出了很多的优化。在使用时HashMap允许null作为其键或值,增加了API的灵活性,但是HashMap并不是线程安全的,请注意不要在并发使用此类。HashMap的内部基本存储方式是大致可以看成是哈希表,当HashMap中的某个哈希桶内部的链表存储的元素达到一定数目时,随着链表的不断拓展,查找元素的算法的时间复杂度将上升到O(n)。为了能够保证HashMap的效率,当链表的程度大于阈值(默认为8)时,这个哈希桶就会被转化成效率更高红黑树。

HashMap的属性代码:

评论和共享

总览

所有的弱点中,最大的弱点就是害怕暴露弱点。

注重实效的程序员对他自己的职业生涯负责,并且不害怕承认无知或错误。

负责

责任是你主动承担的东西。面对责任除了尽你所能之外,还需要分析风险是否已经超出了你的控制。对于不可能或风险太大的事,有权不为之负责。当需要承担责任时,你必须依照自己的道德标准和判断来做出决定。

控制

假如在团队开发中,出现了一个无法控制的事务,且没有完美的解决,该事件久了能引发更多不可预知且不可控的事件。不可控的事件的熵总会趋向于最大化,这就是即便有着详细的计划,技术高超的开发者,但是软件还会开发失败的原因。项目成员的心理对项目有很大的作用,如果这种不可控变多了,最终软件可能因此腐烂。

标准

人们总会以最低标准来要求自己,特别是在有参照物的时候,人会更倾向于拿自己和最低的那个参照物相比较。在软件开发中,如果有一点很低劣的代码,或者管理者的一个错误决策,都有可能引起一场雪崩反应。人们总会容易产生这样的想法:“相中中有那么烂的代码,我只要写那样的代码就可以了,没必要太严格的要求自己。”,至于说项目在这之前有多好,代码在这之前结构有多清晰,注释有多明确,谁会去关注呢?

成就

首先,你有对整个系统的设计,整体构思。但是你不需要把整个系统全部拿出来,把自己的要求和期望全部详细的讲出来。在现今的企业管理架构中,同时请求太多资源或者对团队成员提太多任务,可能会遇到拖延和茫然。并且开会研讨,讨论审批,会让事情变得复杂化。每个人都会护卫他们自己的资源,不要让他们觉得你是哪个掠夺者。
可以先设计一部分比较合理的东西,等开发完成之后,就拿出来给大家看,首先先享受部分成功的喜悦,然后引导式的提出更多的功能需求或更多的优化点,最后等其它项目成员来确定并承担下这些任务。就这样,逐渐的把它们引导向整个项目的图景。
对于人们来说,参与正在发生成功的事会更容易让他们接受。能够让他们瞥见未来,你就能把它们聚集在周围。

大局

大多数软件腐烂都是从一点点的微不足道的小事开始的,大多数的项目拖延都是一天一天发生的。系统一个特征一个特性的偏离其规范,一个又一个的补丁被打到某段代码上,知道最初的代码一点也没留下,并且项目的设计也已经变得杂乱无章。常常是小事的积累破坏了士气和团队。
留心大图景,要观察项目的全局设计,要观察周围的环境和发生的事,而不只是做你自己在做的事情。

质量

我们无法精确的去控制软件的质量。作为研发者我们必须在保证代码的整洁高效的前提下,去满足软件用户的需求。
不管是公共库还是应用软件,我们的作品始终是要给用户使用的。质量需求很重要,尽量给用户直接参与权衡质量需求的空间。你应该将系统的范围和质量作为系统需求的一部分,规定下来。
无视用户的需求,一味的给程序增加新特性,或是一次又一次的润饰代码,等等行为都不是有职业素养的做法。
我们决不能向用户或其它领导许诺一个不可能兑换的时间标度,到最后为了赶上项目的最后期限而削减基本的工程内容。

收手

如果不懂止步,所有的辛苦劳作都会被破坏。
不要因为过度修饰和过于求精而毁坏完好的程序。让你的代码凭自身的质量运行一段时间,他也许不完美,你不用因此而担心它,没有完美的代码。

知识

知识上的投资总能得到更好的回报,你的知识和经验是你最重要的职业财富。但是这些财富是时效资产,随着新技术,语言和环境的出现,你的知识会变得过时。随着你的知识价值降低,对你的公司和客户来说你的价值也在降低。

知识资产

软件工程师所知道的关于计算技术和他们所工作的领域的全部事实、以及他们所有的经验都可以被视作他们的知识资产(Knowledge Portfolios)。管理知识资产与管理金融资产非常相似。

  1. 严肃的投资者会将定期投资作为一种习惯。
  2. 多元化是长期成功的关键。
  3. 聪明的投资者在保守的投资和高风险、高回报的投资之间平衡他们的财产。
  4. 投资者设法低买高卖,以获取最大回报。
  5. 应周期性的重新评估和平衡你的财产。
    要想在职业生涯中获得成功,你必须用同样的指导方针管理你的知识资产。最总要的是你需要定期为你的知识资产投资。

学习方法

这里给出一些学习技术的方法,以供参考。

  • 每年至少学习一种新语言。
    不同的语言以不同的方式解决相同的问题。通过学习若干种不同的方法,可以帮助你拓宽思维,并避免墨守成规。
  • 每季度阅读一本技术书籍。
  • 也要阅读一些非技术书籍。
    记住计算机是由人来使用的,你所做的软件也是为了满足人的需求。不要忘记等式这边的人,这很重要。
  • 上课。
  • 参加本地用户组织。
  • 实验不同的环境。
  • 跟上潮流。
  • 上网。
    是否在某个项目中使用这些技术,或者是否把它们放入你的简历,这些都不重要。重要的是学习的过程将拓宽你的思维,使你向着新的可能性和新的做事方式拓展。

批判的思考

批判的思考你听到的和读到的,你要警惕你所得到的知识的准确性,保持自己的大脑不要被错误的言论或知识所误导。

交流

最重要的问题不是你有什么,还要看你怎样去包装它。就算你拥有最好的主意、最漂亮的代码、或是最注重实效的想法,如果没有有效的交流,最终也会毫无结果。

主旨

在交流之前,首先规划好你要交流的东西,写出一个大纲。以能够表达出自己想要说的内容为目标,不断的提炼他。如果有时间的话,应该先去准备好几种把你要说的内容讲清楚的策略。

听众

只有当你是在传达信息时,你才是交流。为此,你需要了解你的听众的需要、兴趣和能力。我们不想看到这种情况:一个研发人员发表长篇独白,讲解各种神秘的技术优点,把所有的听众都弄得目瞪口呆。在我17年8月去宁波某银行演示系统的时候就犯过这种错误,后来回顾总结的时候意识到这种行为不是交流,而是空谈。下面引入一首离合诗,或许能够帮助你进行交流:

What do you want them to learn? 你想让他们学到什么?
What their interest in what you have got say? 他们对你讲的什么感兴趣?
How sophisticated are they? 他们有多丰富的经验?
How much detail do they want? 他们想要多少细节?
Whom do you want to own the information? 你想让谁拥有这些信息?
How can you motivate them to listen to you? 你如何促使他们听你说话?

时机

说话的时机很重要,不要因为分不清事情的轻重缓急,弄错了说话的时机导致无法达到本来的目的。

风格

根据不同的听众调整你的交流风格,有些人喜欢正式的“事实”简报,有些人喜欢在进入正题之前首先高谈阔论一番。如果需要正式的文档的话,在注重事实的基础上,尽量让你的文档美观一些。如果可能的话,在文档制作早期就让你的用户参与到文档的制作当中。获取他们的反馈,汲取他们的智慧。对于风格而言,请谨记:你怎么说和你说什么同样重要。

做倾听者

如果你想让别人听你讲话,首先你要学会听别人讲话。在进行交流的过程中,要学会及时的回复他人。除非你生活在真空中,你才不需要交流,否则请好好锻炼一下交流的技巧。交流的越有效,你的影响力就越大。

引用

本文是对软件工程师为人哲学的一点总结,主要参考了以下资料:

  1. 《马维达. 程序员修炼之道[J]. 程序员, 2004(4):121-124.》
非原创声明
>本文并非我的原创文章,而是我读书笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技术能够改变世界 。

评论和共享

JVM虚拟机-垃圾收集器

发布在 jvm

摘要

垃圾收集器(GC,Garbage Collector):自动化的内存管理工具,垃圾收集器主要做以下几种工作:

  • 在新生代中给新对象分配内存,并将老对象放到老年代中去管理。(Allocating objects to a young generation and promoting aged objects into an old generation.)
  • Java HotSpot VM在Java堆内存占用率超过默认阈值时触发标记阶段,标记阶段将通过并行标记的方式在老年代中寻找存活的对象。(Finding live objects in the old generation through a concurrent (parallel) marking phase. The Java HotSpot VM triggers the marking phase when the total Java heap occupancy exceeds the default threshold.)
  • 通过并行复制的算法压缩存活的对象从而恢复可用内存。(Recovering free memory by compacting live objects through parallel copying.)

    如何选择垃圾收集器才能使应用的性能达到最优?HotSpot团队都难以对这个问题给出完美的答案。一般而言,暂停应用程序进行垃圾回收的频率越低且每次垃圾回收的时间越短,应用程序的性能就会越高。但是拥有许多大数据对象的多线程高事务率的大型应用的垃圾回收很难同时满足以上两点。

    阿姆达定理(系统中对某一部件采用更快执行方式所能获得的系统性能改进程度,取决于这种执行方式被使用的频率,或所占总执行时间的比例。)说明,给定问题的并行加速度由问题的顺序执行部分限制其性能。其意为者部分工作并不能完美的并行化,只能串行的执行。在JAVA平台也受此定理的影响,JAVA SE 1.4之前并不支持并行垃圾回收,故垃圾收集器在多处理器下对性能的影响随程序的并行化而增长。

引言

小至简单的 public static void main 程序,大至拥有数百台服务器集群的大型WEB服务,全世界目前有几十亿部设备正在使用JAVA。为了支持这种多样化的部署,Java HotSpot虚拟机(Java HotSpot VM)提供了多个垃圾收集器,每个垃圾回收器被设计为满足不同的需求。JAVA应用程序可以根据自身运行的类型(调试版或生产版)与载体物理机的配置情况,自动的去选择合适的垃圾收集器。但当需求上需要我们写出更加高性能的应用程序时,JAVA的默认配置就很难再满足我们的需求。此时便需要作为开发或运维人员的你通过设置一些JVM参数,明确的指定垃圾收集器等方式,来达到性能需求上所指明的条件。

在阅读本文之前,我希望你已经读过我的上篇文章JVM虚拟机-垃圾回收算法,对常见的垃圾回收算法及其在HotSpot虚拟机下的实现有了解,如果说垃圾回收算法是理论基础,那么本文所介绍的垃圾收集器就是JVM垃圾收集的具体实现。另外希望你在工作或研究的过程中感受过JAVA默认的垃圾回收机制给你带来过性能上的压力,因为只有亲身的经历过,才能感受到了解垃圾收集器的机制的重要性。

本文会以JAVA8版本的HotSpot虚拟机为基础环境逐个的介绍每个垃圾收集器的实现原理并分析其优缺点,简略的探讨一下什么样的应用在什么样的环境下应该选用哪种垃圾收集器。本文的目标是能够帮助读者对应用程序进行JVM调优,做出高性能的应用。

本文着重对CMS和G1两款垃圾收集器进行介绍分析。

基础

HotSpot虚拟机提供了三种大类的垃圾收集器,每种垃圾收集器都有不同的特征和性能表现。简单描述如下:

  • 串行类垃圾收集器( serial collector):
    串行类垃圾收集器是最受争议的垃圾收集器,串行垃圾收集器使用单个线程完成所有的垃圾收集工作。其工作时不需要与其它的线程进行交互,所以垃圾回收的效率是很高的。串行类垃圾收集器尤其适合单CPU的硬件平台上使用,且在使用数据集小于100MB的多CPU硬件平台上也能取得很好的效果。HotSpot在Client模式下默认使用串行垃圾收集器,在其它环境下也可以通过使用 -XX:+UseSerialGC 参数来明确指定使用串行垃圾收集器。

  • 并行垃圾收集器(Parallel Collector):
    并行垃圾收集器又被称为吞吐量优先垃圾收集器(Throughput Collector),通常我们所指的吞吐量优先垃圾收集器指的都是适用于新生代的 Parallel Scavenge 垃圾收集器。并行垃圾收集器能够显著的减少多CPU环境下中型或大型应用垃圾收集所消耗的时间,HotSpot Server版本默认使用的使用的垃圾收集器就是并行垃圾收集器。通过启动虚拟机的时候指定参数 -XX:+UseParallelGC 可以手动指定年轻代使用吞吐量优先收集器。与年轻代相呼应的还有老年代并行垃圾收集器(Oracle 称其为 Parallel compaction 意为并行压缩),当使用参数 -XX:+UseParallelGC 时,JVM老年代的默认开启 Parallel compaction ,同时也可以通过参数 -XX:-UseParallelOldGC 来手动开启。

    • 并发垃圾收集器(Concurrent Collector):
      从字面上理解,并发垃圾收集器的意思是让用户线程和垃圾回收线程并发运行。其在大中型应用系统中并发垃圾收集器工作的过程中仅仅有很短的 ‘Stop the world’ ,且相对于并行垃圾收集器能够大幅度的减小系统响应时间(可能以牺牲系统吞吐量为代价)。JAVA8版本的HotSpot虚拟机下目前有两款并发收集器,CMS垃圾收集器(通过参数 XX:+UseConcMarkSweepGC 开启)和G1(通过参数 -XX:+UseG1GC 开启)。

探索

引用

本文是对JVM垃圾收集器的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java® Virtual Machine Specification · Java SE 8 Edition》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》
  3. 《Java Platform,Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》

    非原创声明

    本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技术能够改变世界 。

评论和共享

JVM虚拟机-垃圾回收算法

发布在 java, jvm

引言

C和C++ 程序员能够直接操作内存,凭自己的需要来决定何时去申请多大内存,何时去释放这块内存。他们甚至可以使用指针,来确定的去操作某块内存地址。作为后来者的JAVA远远不如他们强大,我认识的JAVA程序员可能有半数都从未去关注过内存。在使用JAVA进行业务开发时,我们不需要去关注对象到底存储在内存的哪个位置,也不需要关注这个对象到底占用了多大的内存空间,更不需要特意的为某个对象去释放内存空间。需要我们关注的,只有如何完成产品需求,在规定时间内交付合格的产品给客户。如果你对编程的看法跟上面所述的类似,或者你很认同“完成比完美更重要”这条工程师信条,那么这篇文章就不太适合你来阅读了。

内存控制给编程界造成了一个具有魔性的圈,圈内的人想出去,圈外的人想进来。就像C程序员早就受够了内存的申请与释放的折磨,受够了各种内存错误。而很多像你一样的JAVA程序员,立志要写出高性能应用的人,一直在用心地思索怎么才能写出性能更好的多线程应用,当这个追求达到一定层次的时候,你甚至已经开始在乎JAVA垃圾回收所消耗的时间了。由于JAVA自身的限制,我们没办法进入到内存控制的圈内,但我们可以通过设置JVM参数等方式,选择适当的垃圾收集器或设置垃圾回收线程对JAVA的垃圾回收机制进行优化。

概述

本文的重点是介绍JVM虚拟机下常用的垃圾回收算法的理论知识,并不介绍具体算法实现代码。在阅读本文之前,希望读者已经掌握了JVM的内存划分设计、对象引用的可达性分析算法与JAVA的四种对象引用级别(Strong Reference/Soft Reference/Weak Reference/Phantom Reference)等相关知识。在本文之后,我还会再去整理一些关于垃圾收集器等方面的知识。文中所涉及的代码或理论都已在JDK8中进行验证。

垃圾回收算法

标记-清除算法(Mark-Sweep)

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记-清除算法的工作过程如图1所示。

图1 标记-清除算法示意图

  • 优点: 算法简单清晰,其它垃圾收集算法都是据此算法的思想并对其不足改进而得到的。
  • 缺点: 1.效率低,标记和清除需要遍历内存,效率极低。2.回收内存之后产生大量的内存碎片,当内存碎片过多时,应用需要给大对象分配内存时无法分配连续内存。

复制算法(Copying)

将内存按容量划分为完全等大小的两块,每次只使用其中一块内存。当这一块内存用完后,将还活着的对象全部复制到另一块上面,然后把这一块上已使用的内存空间全部清理掉。其示意图如图二所示。


图2 复制算法示意图

  • 优点: 实现简单,运行高效。每次都是对整个半区进行内存回收,内存分配时无需考虑内存碎片等复杂情况。
  • 缺点: 浪费内存空间。在最坏的情况下,这种垃圾回收算法可能浪费了一半的内存空间。

    在JAVA应用中,90%之上的对象的生存时间都极短,所以JVM把内存分为一块较大的Eden空间和两块较小的Survivor空间,每次只使用Eden和其中一块Survior空间。当需要进行垃圾回收时,将Eden和Survivor中还处于可达状态的对象一次性的复制到另一块Survivor空间中,最后清空Eden和刚刚使用的Survivor空间。当Survivor空间不够用时,还会将长时间存活的对象转存的老年代中。在HotSpot虚拟机中,默认的Eden和Survivor空间的比例是8:1,这样可以做到只浪费10%的内存。

标记-整理算法(Mark-Compact)

标记-整理算法的标记过程与标记-清除算法的标记过程类似,但在标记完成后并不是直接对可回收的对象进行清理,而是让所有正在存活的对象都向前端移动,然后直接清理掉边界以外的内存。其示意图见图3.


图3 标记-整理算法示意图

  • 优点: 节省内存空间,提升内存运用率,且不会产生内存碎片。
  • 缺点: 性能低。

分代收集算法(Generational Collection)

根据对象的存活时间把内存分为新生代和老年代,根据个代对象的存活特点,每个代采用不同的垃圾回收算法。

分代收集就是根据不同代的特性,使用最合适的垃圾回收算法进行垃圾回收。如新生代中,每次垃圾收集都会有大量的对象死去,只有极小部分对象存活,所以更适合复制算法。老年代中对象存活率高,且没有额外的内存空间为它进行分配担保,所以更适合用标记-清除或标记-整理算法。

HotSpot算法实现

枚举根节点(GC Roots)

在垃圾回收时,我们要想办法找出哪些对象是存活的,一般会选取一些被称为GC Root的对象,从这些对象开始枚举。在进行GC Root枚举时要求所有对象停下来,也就是JVM所称的“Stop the world”。所有的算法实现都会将虚拟机停下来的,否则分析结果的准确性将无法保证。

由于HotSpot采用准确式GC,该技术主要功能就是让虚拟机可以准确的知道内存中某个位置的数据类型是什么,比如某个内存位置到底是一个整型的变量,还是对某个对象的 reference。这样在进行 GC Roots 枚举时,只需要枚举 reference 类型的即可。在能够准确地确定 Java 堆和方法区等 reference 准确位置之后,HotSpot 就能极大地缩短 GC Roots 枚举时间,所以当执行系统停顿下来之后,虚拟机不需要遍历所有的根节点和上下文去确定GC Roots,而是存在着一个OopMap的数据结构来达到这个目的。

在类加载完成的时候,虚拟机就会把什么类的什么偏移上是什么类型的数据计算出来。在JIT编译的时候也会在特定位置记下在寄存器和栈中哪些位置是引用,GC在扫描时就可直接得到信息。

安全点(Safepoint)

Safepoint:会导致 OopMap 内容变化的指令非常多,如果为每一条指令都生成对应的 OopMap,那么将需要大量的额外空间,这样对导致 GC 成本很高,所以 HotSpot 只在 “特定位置” 记录这些信息,这些位置被称为 安全点(Safepoint)。并非程序在任意时刻都可以停顿下来进行 GC,而只有程序到达 安全点(Safepoint) 以后才可以停顿下来进行 GC;所以安全点既不能太少,以至于 GC 过程等待程序到达安全点的时间过长,也不能太多,以至于 GC 过程带来的成本过高。

由于在 GC 过程中必须保证程序已执行,那么也就是说 必须等待所有线程都到达安全点上方可进行 GC。一般来说有两种解决方案可以选择:

  • 抢先式中断:不需要线程的执行代码去主动配合,当发生 GC 时,先强制中断所有线程,然后如果发现某些线程未处于安全点,那么将其唤醒,直至其到达安全点再次将其中断;这样一直等待所有线程都在安全点后开始 GC。现在几乎没有虚拟机使用这种方式。

  • 主动式中断:不强制中断线程,只是简单地设置一个中断标记,各个线程在执行时轮询这个标记,一旦发现标记被改变(出现中断标记)时,那么将运行到安全点后自己中断挂起;目前所有商用虚拟机全部采用主动式中断。

安全区(Safe Region)

安全点机制仅仅是保证了程序执行时不需要太长时间就可以进入一个安全点进行 GC 动作,但是当特殊情况时,比如线程休眠、线程阻塞等状态的情况下,显然 JVM 不可能一直等待被阻塞或休眠的线程正常唤醒执行,此时就引入了安全区的概念。

安全区(Safe Region):安全区域是指在一段代码区域内,对象引用关系等不会发生变化,在此区域内任意位置开始 GC 都是安全的。线程运行到Safe Region中的代码时,首先标记自己进入了安全区,然后在这段区域内,如果线程发生了阻塞、休眠等操作,JVM 发起 GC 时将忽略这些处于安全区的线程。当线程再次被唤醒时,首先他会检查是否完成了 GC Roots枚举(或这个GC过程),然后选择是否继续执行,否则将继续等待 GC 的完成。

引用

本文是对JVM垃圾收集算法的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java® Virtual Machine Specification · Java SE 8 Edition》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》
  3. 《Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》

    非原创声明

    本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到 weiwei02@vip.qq.com
我的github: https://github.com/weiwei02/
我相信技术能够改变世界。

评论和共享

引言

网站的伸缩性是指不需要改变服务器的硬件设计,仅仅靠改变应用服务器的部署数量,就可以扩大或缩小服务器的处理能力。一般来说,网站的伸缩性设计可分为两类,一类是根据功能进行物理分离实现伸缩,一类是单一功能通过集群实现伸缩。前者是不同服务器部署不同的服务,提供不同的功能。后者是集群中多台服务器部署相同的服务,提供相同的功能。

方法

依据功能实现伸缩

从网站发展早期,通过增加服务器提高网站处理能力时,新增服务器总是从现有服务器中分离出部分功能和服务。
通过物理实现服务器伸缩.jpg
图1 通过物理实现服务器伸缩

图1所描述的伸缩性就是每次伸缩操作都会有更多的服务器加入网站,新增的服务器用于处理特定的服务。依据功能进行进行架构伸缩的具体实现方案有两种,纵向分离和横向分离。

  • 纵向分离
    纵向分离是指服务根据业务流程上的不同部分分开进行部署,实现系统上的伸缩性,其分离方式如图2所示。
    纵向分离部署实现系统可伸缩性.jpg
    图2 纵向分离部署实现系统可伸缩性

  • 横向分离
    横向分离又称为业务分割,其实现方式是将不同的业务模块分离部署,实现可伸缩性,如图3所示。根据系统的性能要求可以将横向分离的力度做到非常小。
    横向分离.jpg
    图3 横向分离部署实现系统可伸缩性

通过集群实现伸缩

将不同功能分离部署可以实现一定程度的伸缩性,但是随着网站访问量的逐步增加,即使分离到最小力度的独立部署,单一的服务器也不能满足业务规模的需求。此时可以使用服务器的集群,将相同的服务部署到多台服务器上构成一个集群整体对外提供服务。具体来说,集群伸缩性又可分为应用服务器集群伸缩性和数据服务器集群伸缩性。而数据服务集群也可分为缓存数据服务器集群和存储数据服务器集群。

应用服务器集群的伸缩性设计

应用服务器应该被设计成无状态的,即应用服务器不存储请求上下文信息,如果将部署有相同应用的服务器组成一个集群,用户的每一个请求都可以发送到任意一台服务器上去处理,任何一台服务器处理的结果都是相同的。这样只要能将用户请求按照某种规则分发到集群的不同服务器上,就可以构成一个应用服务器集群,每个用户的请求都可能落在不同的服务器上。
如果HTTP请求分发装置可以感知或者可以配置集群服务器的数量,可以及时发现集群中新上线或下线的服务器,并能向新上线的服务器发送请求,停止向已下线的服务器分发请求,那么就实现了应用服务器的伸缩性。在这里,这个HTTP请求分发装置被称为负载均衡服务器。 使用负载均衡服务器实现应用服务器可伸缩架构如图4所示。
负载均衡.jpg
图4 负载均衡实现系统可伸缩性

负载均衡是大型网站必不可少的技术手段,不但可以实现网站的可伸缩性,同时还可以改善网络的可用性。负载均衡的具体实现也多种多样,可以使用专有硬件也可以使用软件来实现。

HTTP重定向负载均衡

利用HTTP重定向协议实现负载均衡,如图5所示。
请求重定向.jpg
图5 HTTP请求重定向负载均衡原理

HTTP重定向服务器时一台普通的WEB服务器,其唯一的功能就是根据用户的HTTP请求计算一台真实的WEB服务器地址,并将该WEB服务地址写入HTTP重定向响应中(响应状态码为302)返回给用户浏览器。这种负载均衡的优点是实现比较简单。缺点是浏览器需要两次请求服务器才能进行一次访问,性能较差。其次重定向服务器自身的处理能力有可能成为瓶颈,整个集群的伸缩性规模有限。再者使用HTTP302响应状态码重定向,有可能使搜索引擎判断为SEO作弊,降低搜索引擎排名。

DNS域名解析负载均衡

这是利用DNS进行域名解析请求时进行负载均衡处理的一种方案。每次域名解析请求都会根据负载均衡算法可能计算出一个不同的IP地址,以后该用户对网站的请求都发送到这个地址上。如图6所示。
DNS域名解析.jpg
图6 DNS域名解析负载均衡原理

在DNS服务器中配置多个A纪录,如: www.mysite.com IN A 10.0.0.1、www.mysite.com IN A 10.0.0.2、www.mysite.com IN A 10.0.0.3
优点:将负载均衡的工作交给DNS,省掉了网站的管理维护负载均衡服务器的麻烦,同时许多DNS还提供了了基于地理位置的域名解析,即会将域名解析成距离用户地理位置最近的一个服务器地址,这样可加快用户的访问速度,改善性能。
缺点:目前的DNS是多级DNS,每级DNS都可能对网站的DNS信息进行缓存,当下线某台服务器后,即使修改了DNS,也需要很长时间才能生效。其次,DNS负载均衡的控制权在域名运营商那里,网站无法做更多的改善和更强大的管理。
在实际使用中,大型网站总是部分使用DNS域名解析,利用域名解析作为第一级负载均衡手段。

反向代理负载均衡

反向代理服务器在网络部署上位于WEB服务器之前,其拥有两个网卡,分别拥有内部和外部两套IP地址。在使用中它可以将外部网卡的请求转发到内部网卡连接的网络中的WEB服务器。WEB服务器的响应信息也需要反向代理服务器转发给用户。由于反向代理负载均衡工作在HTTP协议层面,因此也叫作应用层负载均衡。其逻辑架构图如图7所示。
反向代理.jpg
图7 反向代理负载均衡原理

优点:和反向代理服务器部署在一起,部署简单。
缺点:反向代理服务器是所有请求和响应的中转站,其性能可能会成为瓶颈。

IP负载均衡

IP负载均衡是在网络层修改目标IP地址来实现负载均衡,其网络架构图如图8所示。
IP负载均衡.jpg
图8 IP负载均衡原理

IP负载均衡在处理真是物理WEB服务器的响应数据包时比较复杂。WEB服务器的响应包需要先返回到负载均衡服务器,再由负载均衡服务器转发给用户。响应的转发操作主要有两种实现方式:

  1. 源地址转换(SNAT):负载均衡服务器在修改目的地址时同时修改源地址,将数据包源地址修改为自身的地址,这样WEB服务器响应会再次回到自身。
  2. 负载均衡服务器同时作为真实服务器的网关设备,这样所有的响应数据都需要先来到负载均衡服务器上,再进行转发。

    优点:在内核进程完成数据分发,较反向代理负载均衡(在应用中分发数据)有更好的处理性能。
    缺点:所有的请求和回应都需要经过负载均衡服务器,集群的最大响应和吞吐量不得不受制于负载均衡服务器网卡的最大带宽。对于下载或视频服务等需要大量数据传输的手段而言,难以满足要求。

数据链路负载均衡

数据链路负载均衡是在通信协议的数据链路层修改mac地址,来达到负载均衡的目的。负载均衡数据分发过程中不修改IP地址,只修改mac地址,通过配置真是物理服务器集群所有机器虚拟IP和负载均衡服务器IP地址一致,从而达到不修改数据包源地址和目的地址就可以进行数据分发的目的。由于实际处理请求的真是物理服务器IP和数据请求目的IP一致,不需要通过负载均衡服务器进行地址转换,可将响应数据包直接返回给用户浏览器,避免负载均衡服务器网卡带宽成为瓶颈。这种数据三角传输模式又成为直接路由方式(DR)。数据链路负载均衡网络架构如图9所示。
数据链路.jpg
图7 数据链路负载均衡原理

使用三角传输模式的数据链路层负载均衡是目前大型网站使用最广泛的一种负载均衡手段。在linux平台上最好的链路层负载均衡的开源产品是LVS(Linux Virtual Server)。

负载均衡算法

负载均衡服务器的实现可分为两个部分:

  1. 根据负载均衡算法和WEB服务器列表计算得到集群中一台WEB服务器的地址。
  2. 将请求发送到该地址所对应的WEB服务器上。

    具体的负载均衡算法通常有以下几种:

轮询(Round Robin, RR)

所有请求依次呗分发到每台应用服务器上,即每台服务器要处理的请求数目都相同,适合所有服务器硬件都相同的场景。

加权轮询(Weight Round Robin, WRR)

根据应用服务器硬件性能的情况,在轮询的基础上,按照权重将请求分发到每个服务器,性能较高的服务器处理更多的请求。

随机(Random)

请求被随机分配到各个应用服务器。随机也可以使用加权。

最少连接(Least Connections)

纪录每个服务器正在处理的连接数,将新到的请求分发到最少连接的服务器上。最少连接最符合负载均衡的定义,同样最少连接算法也能够使用加权。

源地址散列(Source Hashing)

根据请求来源的IP地址进行hash计算,得到应用服务器,这样来自同一个IP地址的请求总在同一个服务器上处理,请求的上下文信息可以存储在这台服务器上,在一个会话周期内重复使用,从而实现会话黏滞。

引用

本文是对可伸缩架构的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《大型网站技术架构·核心原理与案例分析》 李智慧 2013 电子工业出版社

关于

我的github: https://github.com/weiwei02/
我相信技术能够改变世界。

评论和共享

高可用的数据

对于许多应用而言,数据是宝贵的,必须的资产。数据是整个应用的历史,是记录也有可能是配置信息,如果丢失了数据,那么对于某些应用来说结果可能就是毁灭性的,整个应用都有可能因此无法运行。
不同于高可用的应用或服务的设计方式,由于数据存储服务器上存储的数据不同,当某台服务器宕机之后,数据访问请求不能任意的切换到集群中其它数据服务器上。

CAP

CAP理论是分布式系统中对数据的管理而形成一套理论知识,CAP是设计分布式系统所必须考虑的架构问题。对于CAP本身可以解释如下:

  • Consistency(一致性): 数据一致更新,所有数据变动都是同步的
  • Availability(可用性): 好的响应性能
  • Partition tolerance(分区耐受性): 可靠性

    上面的解释可能显得太过抽象,举例来说在高可用的网站架构中,对于数据基础提出了以下的要求:

  • 分区耐受性
    保证数据可持久存储,在各种情况下都不会出现数据丢失的问题。为了实现数据的持久性,不但需要在写入的时候保证数据能够持久存储,还需要能够将数据备份一个或多个副本,存放在不同的物理设备上,防止某个存储设备发生故障时,数据不会丢失。

  • 数据一致性
    在数据有多份副本的情况下,如果网络、服务器、软件出现了故障,会导致部分副本写入失败。这就造成了多个副本之间的数据不一致,数据内容冲突。
  • 数据可用性
    多个副本分别存储于不同的物理设备的情况下,如果某个设备损坏,就需要从另一个数据存储设备上访问数据。如果这个过程不能很快完成,或者在完成的过程中需要停止终端用户访问数据,那么在切换存储设备的这段时间内,数据就是不可访问的。

    CAP原理认为,一个提供数据服务的存储系统无法同时完美的满足一致性(Consistency)、数据可用性(Availability)、分区耐受性(Partition Tolerance)这三个条件。对于三者的关系见图1.

    图1 CAP原理关系图

    在实际的大型网络应用中,数据的规模会快速扩张,因此数据架构的伸缩性(分区耐受性)必不可少。当规模变大之后,机器的数量也会增大,这时网络和服务器故障会更频繁出现,想要保证应用可用,就必须保证分布式处理系统的高可用性。所以在大型网站中,通常会选择强化分布式存储系统的可用性(A)和伸缩性(P),在某种程度上放弃一致性(C)。一般来说,数据不一致的情况通常出现在高并发写操作或者集群状态不稳(故障恢复,集群扩容…)的情况下,应用系统需要对分布式数据处理系统的数据不一致性有一定的了解并进行某种意义上的补偿工作,以避免应用出现数据不正确。

    CAP原理对于可伸缩的分布式系统设计具有重要的意义,在系统设计开发过程中,不恰当的迎合各种需求,企图打造一个完美的产品,可能会使设计进入两难之地,难以为继。
    具体来说,数据一致性又可分为以下几点:

  • 数据强一致
    各个副本中的数据总是强一致的。这种设计正确性很高,但是会在一定程度上损耗性能。
  • 数据用户一致
    应用访问数据时通过一定的纠错和校验机制,把多个数据可能不一致的副本的数据综合计算返回一个一致且确定的数据给用户。大型互联网架构一般采用这种设计,性能较好,并且数据不会出现错误。
  • 数据最终一致
    物理存储的数据不一致,用户访问得到的数据也可能不一致,但经过一段时间的自我修正(通常很短时间),数据会达到最终一致。该设计性能最高,但可能有数据错误。

    因为很难去同时满足CAP,大型网站通常会综合成本、技术、业务场景等条件,结合应用服务和其它的数据监控与纠错功能,使存储系统达到用户一致,保证用户最终访问数据的正确性。

    20170804 18:10:00 编辑到多种数据一致性

引用

本文是对class文件的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《大型网站技术架构·核心原理与案例分析》 李智慧 2013 电子工业出版社

评论和共享

  • 第 1 页 共 1 页
Copyrights © 2017 weiwei02. All Rights Reserved. github空间地址: https://weiwei02.github.io/ 国内空间地址: https://weiwei02.cording.me/
作者的图片

weiwei02

技术,改变世界


软件工程师


北京,海淀
国外主站 国内主站