Java面试题事务隔离级别JVM调优equals和hashCodesynchronized与LockMapSetListThreadLocal死锁多线程最佳实践扩容缓存消息队列应用拆分高可用

事务隔离级别

  • 脏读:如果我们开启了一个事务,那么我们希望的正确数据应该是commit之后的数据。事务A在commit之前对一些数据做了修改,却可以被事务B读取到,这就是脏读。
  • 不可重复读:在一个事务内,多次读同一数据,结果不一样。比如事务B对某数据做了修改,事务A 在事务B commit之前和事务B commit之后分别读取一次,两次的结果不一样。因为只有在事务B commit之后,事务A才读取的到。这种情况下就没有脏读问题,因为只有commit之后才能读取到。
  • 幻读 innodb的RR级别,使用GAP锁是解决了幻读的问题的? 幻读指的是多了一个或者少了一个,就像幻影一样,是和一个范围有关的,幻读的必要条件是两个,第一是有Insert/delete操作,第二个是你做了范围查询; 当我们将当前会话的隔离级别设置为repeatable read的时候,当前会话可以重复读,就是每次读取的结果集都相同,而不管其他事务有没有提交; 第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样,一般是新增或删除一条数据?(远远不是这么简单吧,我现在没太理解)

事务隔离级别主要有以下几种

事务隔离级别

  • read uncommitted(读未提交) 级别最低。所以脏读、不可重复度、幻读都有可能出现
  • read committed(读提交) 即不可重复度。可以避免脏读,但不能避免不可重复度和幻读。
  • repeatable read(可重复读) MySQL默认的隔离级别,可以避免脏读和不可重复读,但不能避免幻读。
  • serializable(串行化) 级别最高,但是效率最低。当我们将当前会话的隔离级别设置为serializable的时候,其他会话对该表的写操作将被挂起。这是隔离级别中最严格的,但是这样做势必对性能造成影响。

JVM调优

调优入门

调优策略

两个基本原则:

  • 将转移到老年代的对象数量降到最少。
  • 减少Full GC的执行时间。目标是Minor GC时间在100ms以内,Full GC时间在1s以内。

设定堆内存大小,这是最基本的:

  • -Xms:启动JVM时的堆内存空间
  • -Xmx:堆内存最大限制

设定新生代大小。新生代不宜太小,否则会有大量对象涌入老年代

  • -XX:NewRatio:新生代和老年代的占比
  • -XX:NewSize:新生代空间
  • -XX:SurvivorRatio:Eden空间和Survivor空间的占比
  • -XX:MaxTenuringThreshold:对象进入老年代的年龄阈值

设定垃圾回收器

  • 年轻代:-XX:+UseParNewGC。
  • 老年代:-XX:+UseConcMarkSweepGC。 CMS可以将STW时间降到最低,但是不对内存进行压缩,有可能出现“并行模式失败”。比如老年代空间还有300MB空间,但是一些10MB的对象无法被顺序的存储。这时候会触发压缩处理,但是CMS GC模式下的压缩处理时间要比Parallel GC长很多。 G1采用”标记-整理“算法,解决了内存碎片问题,建立了可预测的停顿时间类型,能让使用者指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。

jps

列出JVM进程

jstat

监视虚拟机运行状态信息,使用方式:

jstat -<option> <pid> [interval[s|ms]]

选项 作用

每隔1秒输出一次JVM运行信息:

jstat -gc 28389 1s
S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT   
52416.0 52416.0 4744.9  0.0   419456.0 28180.6  2621440.0   439372.6  131072.0 33564.8 160472 1760.603  61      2.731 1763.334

equals和hashCode

如果重写了equals方法,则一定要重写hashCode方法

  • 在程序执行期间,只要equals方法的比较操作用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法必须始终如一地返回同一个整数
  • 如果两个对象通过equals方法比较得到的结果是相等的,那么对这两个对象进行hashCode得到的值应该相同
  • 两个不同的对象,hashCode的结果可能是相同的,这就是哈希表中的冲突。为了保证哈希表的效率,哈希算法应尽可能的避免冲突

如果只重写了equals方法而没有重写hashCode方法的话,则会违反约定的第二条:相等的对象必须具有相等的散列码(hashCode)

synchronized与Lock

  1. 都是可重入锁。
  2. Lock可中断,Lock接口中的lockInterruptibly(),默认下是非公平的,但是可以设置成公平锁;synchronized不可以中断,非公平锁。
  3. Lock.lock()方法用于获取锁,如果这时候锁被其他人占用了,则处于等待状态。在finally语句块调用unlock方法释放锁。
  4. 过lockInterruptibly方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。
  5. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。
  6. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  7. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  8. 性能上来说,在资源竞争不激烈的情形下,Lock性能稍微比synchronized差点(编译程序通常会尽可能的进行优化synchronized)。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。

Map

List接口 和 Set接口 都继承了java.util.Collection接口,Map接口没有继承java.util.Collection接口; HashMap不保证数据有序,LinkedHashMap保证数据可以保持插入顺序,而如果我们希望Map可以保持key的大小顺序的时候,我们就需要利用TreeMap了

HashMap

HashMap 非线程安全,多线程情况下,扩容操作可能会导致死循环

数组:查找快,插入删除慢;链表:插入删除快,查找慢。HashMap可以看作是这两种数据结构的综合,底层实现基于数组 + 链表。 当我们put一个元素的时候,将key对应的hashCode做hash,得到一整数,然后再用这个整数对数组长度进行取模操作得到index,这样就能保证key能尽量均匀的分布在数组中,这里的取模操作为了提高效率,运用了一些技巧,使用了&运算 (n - 1) & hash,所以要求数组的长度是2的指数倍,要不然也没有办法用这种方式进行取模运算。这个过程可能会产生hash碰撞,即两个不一样的key有相同的hash值,这也意味着数组某个元素对应的hash值和当前要插入key的hash值相同。这时候需要判断,如果hash相同并且 key.equals 方法返回true,则覆盖;如果已存在元素是单个Node,转换成链表;如果存在的元素对应的是链表,转换成红黑树。反正如果hash相同并且 key.equals 方法返回true,则覆盖元素;否则,就是链表或者红黑树。

    /**
     * The default initial capacity - MUST be a power of two.
     * 初始容量 2^4
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     * 最大容量 2^30
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     * 负载因子 0.75。当map中的元素个数 大于capacity*load factor时就需要调整buckets的数目为当前的2倍
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /** ---------------------------------------- 下面三个和红黑树有关------------------------------------*/

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     * 链表转换红黑树的阈值
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     * 红黑树转换为链表的阈值
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     * 红黑树结构的最小容量
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     * 修改次数
     */
    transient int modCount;

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

Java中transient关键字的作用,简单地说,就是让某些被修饰的成员属性变量不被序列化 hashCode 相同,则 equals方法不一定会返回true;equals返回true,hashCode则一定相同

当产生hash碰撞的时候,需要借助key.equals()方法去链表或树中去查找对应的节点是否已经存在,存在就覆盖,不存在就插入到链表或者树中

put方法

有关于插入链表头部还是尾部:jdk1.8之前是插入头部的,在jdk1.8中是插入尾部,HashTable一直都是插头部

  1. 对key的hashCode()做hash,然后再计算index;
  2. 如果没碰撞直接放到bucket里;
  3. 如果碰撞了,以链表的形式存在buckets后;
  4. 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
  5. 如果节点已经存在就替换old value(保证key的唯一性)
  6. 如果bucket中的元素超过load factor*current capacity,就要resize。
get方法
  1. 如果没有冲突,即该下标对应的bucket中只有一个元素,则直接取该元素;
  2. 如果产生了冲突,则通过key.equals(k)去查找对应的entry:若为树,则在树中通过key.equals(k)查找,O(logn);若为链表,则在链表中通过key.equals(k)查找,O(n)
resize方法

当put时,如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。在resize的过程,简单的说就是把bucket扩充为2倍,之后重新计算index,把节点再放到新的bucket中。 java7 和 java8 的扩容机制不太一样,主要体现在计算元素在New Entry中的下标时的优化 相同点:初始化一个新的Entry数组为之前的2倍,将Old Entry里的数据拷贝到 New Entry中,并重新计算阈值(threshold = (int)(newCapacity * loadFactor))。 不同点:不同点在于数据拷贝的这个过程中,在java7中,是通过重新计算的方式确定每个元素在New Entry中的下标,重新计算,意味着小标可能完全变了,因为下标是通过取模计算出的,New Entry的长度是Old Entry的两倍,下标可能就变了。不过变不变无所谓,主要是这里重新计算了一次,效率低;在java8这部分内容做了优化,因为New Entry是通过2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。所以在java8扩充HashMap的时,不需要像java7那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+16”。

在Java8 中,如果一个桶中的元素个数超过 TREEIFY_THRESHOLD(默认是 8 ),就使用红黑树来替换链表,从而提高速度,这部分内容挺复杂。

LinkedHashMap

非线程安全,LinkedHashMap是Hash表和链表的实现,并且依靠着双向链表保证了迭代顺序是插入的顺序。 LinkedHashMap 是 HashMap 的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用 LinkedHashMap。 根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用 get 方法)的链表。默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。

TreeMap

非线程安全,HashMap不保证数据有序,LinkedHashMap保证数据可以保持插入顺序,TreeMap可以保持key的大小顺序,底层基于红黑树实现。适用于按自然顺序或自定义顺序遍历键(key)

public static void test() {
    TreeMap<String, String> tmp = new TreeMap<String, String>();
    tmp.put("b", "bbb");
    tmp.put("c", "ccc");
    tmp.put("d", "cdc");
    tmp.put("a", "aaa");

    Iterator<String> iterator_2 = tmp.keySet().iterator();
    while(iterator_2.hasNext()){
        String key = iterator_2.next();
        System.out.println("tmp.get(key) is :" + tmp.get(key));
    }
}

输出结果

tmp.get(key) is :aaa
tmp.get(key) is :bbb
tmp.get(key) is :ccc
tmp.get(key) is :cdc

HashTable

线程安全

  1. HashTable是线程安全的,在其内部,几乎每个public方法都加了synchronized关键字。
  2. 基于数组和链表,在计算数组下标的时候,是通过 % 取模,而不是通过 & 操作进行取模, 效率低。
  3. 添加一个元素的时候,先计算出数组下标,如果该下标有值,说明hash碰撞了,这时候要通过equals方法来判断元素是否存在,如果存在,就覆盖;否则,将新元素添加到链表的头。
  4. HashMap可以有null,hashTable不能有null。

Set

List接口 和 Set接口 都继承了java.util.Collection接口,Map接口没有继承java.util.Collection接口; 不能存重复的值,对于添加到Set中的元素,需要重写hashCode和equals方法。

HashSet

非线程安全,因为基于HashMap实现。没有排序,不重复,可以存null

  1. 不可重复的集合,实现安了Set接口,底层完全基于HashMap实现。
  2. 对于添加到HashSet中的元素,需要重写hashCode和equals方法。
  3. 在添加一个元素的时候,实际上将该元素作为HashMap中的key,而所有元素的值,其实是一个final类型的对象:private static final Object PRESENT = new Object();
  4. 通过Iterator遍历的时候,实际上是遍历HashMap的key,map.keySet().iterator()。也就是说,HashSet依赖于HashMap实现,仅仅是利用了HashMap的key,至于value,就是一个常量对象,没有什么意义。

LinkedHashSet

非线程安全,可以按插入顺序和访问顺序排序,不可重复,可以存null

LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有的方法操作上又与 HashSet 相同

TreeSet

非线程安全,TreeSet基于TreeMap实现,它的作用是提供有序的Set集合,底层基于红黑树。 实现Comparable接口,对象可以比较按照插入值大小排序。

List

List接口 和 Set接口 都继承了java.util.Collection接口,Map接口没有继承java.util.Collection接口;

ArrayList

非线程安全,在执行 add 和 remove 涉及到 i++ 和 i-- 操作。有序,可存null值,可存重复元素

  1. 线程不安全,初始化大小为10,初始化的时候也可以指定大小。
  2. 扩容的时候,默认是扩充1.5倍,如果还是不够,就扩充容量为实际的元素个数。扩容就是数组拷贝,System.arraycopy,效率很低,所以最好在初始化的时候指定大小。
  3. 增删改查中,增导致扩容,则会修改modCount;删一定会修改modCount; 改和查一定不会修改modCount。
  4. 扩容操作会导致数组复制,删除会导致数组复制操作,因此,增、删都相对低效。 而 改、查都是很高效的操作。
  5. 可以存储null
// 初始化大小
private static final int DEFAULT_CAPACITY = 10;

// list已有的元素个数
private int size;

// 表示list集合的修改次数
protected transient int modCount = 0;

ArrayList默认的构造方法其实是构造了一个空数组,当进行第一次add的时候,elementData将会变成默认的长度:10

add方法
  1. 确保数组已使用长度(size)加1之后足够存下 下一个数据
  2. 修改次数modCount 标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用grow方法扩容,grow方法会将当前数组的长度变为原来容量的1.5倍。
  3. 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
  4. 返回添加成功布尔值。
remove方法
  1. 检查删除元素的下标,越界就抛异常
  2. modCount++
  3. 如果是最后一个元素,直接将最后一个元素置为null
  4. 如果不是最后一个元素, 通过System.arraycopy将该元素后面的元素向前移动一位下标,然后将最后元素的下标置为null
public E remove(int index) {
    // 下标越界就抛异常
    rangeCheck(index);
    
    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;

    // 如果不是最后一个元素,通过System.arraycopy将该元素后面的元素向前移动一位下标
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}
arraycopy(Object src, int srcPos, Object dest, int destPos, int length)方法

src:源数组; srcPos:源数组要复制的起始位置; dest:目的数组; destPos:目的数组放置的起始位置; length:复制的长度; 注意:src 和 dest 必须是同类型或者可以进行转换类型的数组

LinkedList

非线程安全,双向链表,插入和删除操作比 ArrayList 更加高效,随机访问的效率要比 ArrayList 差

Vector

线程安全,与ArrayList相比

  1. Vector在API上都加了synchronized所以它是线程安全的;
  2. Vector扩容时,是翻倍size,而ArrayList是扩容50%。

Stack

线程安全,继承自Vector

ThreadLocal

ThreadLocal 在Thread类中,有这么一个属性

ThreadLocal.ThreadLocalMap threadLocals = null;

而ThreadLocalMap是ThreadLocal中的一个静态类,即每个线程的局部变量是存储在自己的threadLocals属性中。也就是说,每个线程都有自己的一个ThreadLocalMap对象。

可以看看,我们调用ThreadLocal的set方法时,发生了什么

public void set(T value) {
    Thread t = Thread.currentThread();
    // 从当前线程中获取ThreadLocalMap , 即Thread中的threadLocals属性
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        // 为当前线程创建一个ThreadLocalMap
        createMap(t, value);
}


// 从当前线程中获取ThreadLocalMap , 即Thread中的threadLocals属性
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 为当前线程创建一个ThreadLocalMap
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap构造函数
private static final int INITIAL_CAPACITY = 16;

private Entry[] table;

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化容量为16
    table = new Entry[INITIAL_CAPACITY];

    // 这其实是一个取模操作,根据当前ThreadLocal对象的HashCode做hash然后 & (INITIAL_CAPACITY - 1) 得到在table数组中的下标
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    
    // 用于计算扩容上限,即当size到达threashold时,需要resize整个Map,threshold的初始值为len * 2 / 3
    setThreshold(INITIAL_CAPACITY);
}

/**
    * The next size value at which to resize.
    */
private int threshold; // Default to 0

private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

有关于Entry,它继承了WeakReference,是一个弱引用,todo

在调用ThreadLoca的set方法时,从当前线程中获取threadLocals的值,如果当前线程的threadLocals为空,就创建一个ThreadLocalMap对象,并赋值给当前线程的threadLocals属性。

set方法
  1. 首先还是根据key计算出位置i,然后查找i位置上的Entry,
  2. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值。
  3. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry
  4. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。
  5. 最后调用cleanSomeSlots,这个函数就不细说了,你只要知道内部还是调用了上面提到的expungeStaleEntry函数清理key为null的Entry就行了,最后返回是否清理了Entry,接下来再判断size>thresgold,这里就是判断是否达到了rehash的条件,达到的话就会调用rehash函数。
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

HashMap采用拉链法处理哈希冲突,即在一个位置已经有元素了,就采用链表把冲突的元素链接在该元素后面,而ThreadLocal采用的是开放地址法,即有冲突后,把要插入的元素放在要插入的位置后面为null的地方

remove方法

它用于在map中移除一个不用的Entry。也是先计算出hash值,若是第一次没有命中,就循环直到null,在此过程中也会调用expungeStaleEntry清除空key节点

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}
总结
  1. 一个ThreadLocal只能保存一个变量,不管这个变量时什么类型,只能保存一个。在保存的时候,是以当前的ThreadLocal对象的hashCode做hash为key,我们设置的值为值保存到和当前线程管关联的ThreadLocalMap对象中。 也就是说,如果你要保存多个变量,就需要定义多个ThreadLocal对象。总结一下就是:一个线程中的所有的局部变量其实存储在该线程自己的同一个ThreadLocalMap属性中;
  2. 线程死亡时,线程局部变量会自动回收内存;
  3. 当线程拥有的局部变量超过了容量的2/3(没有扩大容量时是10个),会涉及到ThreadLocalMap中Entry的回收;

我们发现无论是set,get还是remove方法,过程中key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,GC时就会被回收。那么怎么会存在内存泄露呢?但是以上的思路是假设你调用get或者set方法了,很多时候我们都没有调用过。 所以正确的使用方式是:

  1. 使用者需要手动调用remove函数,删除不再使用的ThreadLocal。
  2. 还有尽量将ThreadLocal设置成private static的,这样ThreadLocal会尽量和线程本身一起消亡。

死锁

死锁产生的原因

  • 互斥条件
  • 请求和保持条件
  • 不剥夺条件
  • 环路等待条件

避免死锁

  • 加锁顺序
  • 加锁时限:比如说 Lock
  • 死锁检测 线程优先级

多线程最佳实践

  • 使用本地对象
  • 使用不可变类
  • 使用线程池
  • 宁可使用同步也不要使用wait和nitify
  • 使用BlockingQueue实现从生产消费模式
  • 使用并发集合而不是使用枷锁的同步集合
  • 使用同步快,不要使用同步方法
  • 避免使用静态变量

扩容

  • 垂直扩容:纵向扩容,提高系统部件能力
  • 水平扩容:横向扩容,增加系统成员来实现
  • 读操作扩展:使用缓存,redis、memcache、CDN
  • 写操作扩展:NoSQL、Hbase、

缓存

  • 命中率
  • 最大元素:最大空间
  • 清空策略:FIFO(先进先出)、LFU(最少使用)、LRU(最近最少使用)、过期时间、随机

本地缓存

编程实现(变量等)、Guava cache

分布式缓存

Memcache、redis

  • Memcache key:最大为256个字节 value:最大为1M
  • redis 性能极高:读11W/s、读8W/s。

缓存一致性

缓存一致性

缓存并发

缓存并发

缓存穿透

如果缓存中的值为空,穿透到数据库

缓存穿透

缓存雪崩

由于缓存原因,导致大量请求到达数据库,导致雪崩。

image.png

解决方案:限流、降级、断熔

消息队列

生成和消费的速度或稳定不一致。应用场景:系统解耦,比如:下单、短信、邮件等等。

  • 业务无关:只做消息分发

好处:业务解耦、最终一致性、广播、错峰与流控。

  • FIFO:先投递先到达
  • 容灾:系统的动态增减和消息的持久化
  • 性能:吞吐量上去了,系统内的通讯效率提升

应用拆分

应用拆分

  • 业务优先
  • 循环渐进
  • 兼顾技术:重构、分层
  • 可靠测试

高可用

分布式调度系统

elastic-job + zookeeper

主备切换

apach curator + zookeeper 分布式锁实现

监控报警机制

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏社区的朋友们

理解 B+ 树算法

最近有接触到 b+ 树,花了点时间,顺便总结梳理下方便后续翻阅;时间仓促,且文中多是个人的理解,仅供参考。

1K0
来自专栏尾尾部落

[剑指offer] 最小的K个数

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。

2862
来自专栏DOTNET

.Net多线程编程—Parallel LINQ、线程池

Parallel LINQ 1 System.Linq.ParallelEnumerable 重要方法概览: 1)public static ParallelQ...

2997
来自专栏程序生活

Leetcode-Easy 101. Symmetric Tree

101. Symmetric Tree 描述: 判断一个颗二叉树是否左右对称 ? 思路: 将二叉树的左右节点对放在的队列里,然后出队,判断节...

3457
来自专栏butterfly100

ConcurrentHashMap源码阅读

1. 前言 HashMap是非线程安全的,在多线程访问时没有同步机制,并发场景下put操作可能导致同一数组下的链表形成闭环,get时候出现死循环,导致CPU利用...

3097
来自专栏Android知识点总结

Java总结IO篇之File类和Properties类

打开颜色选择器 :读流I-->字符串分割-->字符串存入Map-->使用Map对象还原用户配置 修改配置时 :写流O-->创建Map对象-->字符...

1802
来自专栏JavaEdge

Netty源码阅读入门实战(八)-解码(更新 ing)

就像很多标准的架构模式都被各种专用框架所支持一样,常见的数据处理模式往往也是目标实现的很好的候选对象,它可以节省开发人员大量的时间和精力。 当然这也适应于本文...

1294
来自专栏java一日一条

Java 日期时间处理

java.util.Date对象表示一个精确到毫秒的瞬间; 但由于Date从JDK1.0起就开始存在了,历史悠久,而且功能强大(既包含日期,也包含时间),所以他...

3322
来自专栏何俊林

原来Android还可以这样通过反射,获取jar包属性及方法

1、写一个java类,生成Jar包 2、初始Jar的转换 3、创建一个android工程 4、复制Jar包 5、反射获取属性和方法 5、完整Demo 6、补充 ...

5625
来自专栏芋道源码1024

Spring Webflux —— 源码阅读之 handler 包

查找给定请求的handler,如果找不到特定的请求,则返回一个空的Mono。这个方法被getHandler(org.springframework.web.se...

3435

扫码关注云+社区

领取腾讯云代金券