java基础面经

面试题
1.final, finally, finalize 的区别
2.int和Interger的区别
3.重写与重载的区别
4.抽象类和接口有什么区别
5.说说反射的用途
6.说说自定义注解的场景及实现
7.session 分布式处理
8.JDBC 流程
9.List 和 Set 区别
10.Arraylist 与 LinkedList 区别
11.ArrayList 与 Vector 区别
12.HashMap 和 Hashtable 的区别
13.HashSet 和 HashMap 区别
14.HashMap 和 ConcurrentHashMap 的区别
15.HashMap 的工作原理
16.ConcurrentHashMap 的工作原理
17.sleep() 、join()、yield()有什么区别
18.CountDownLatch
19.CyclicBarrier原理
20.Semaphore原理
21.Exchanger 原理
22.ThreadLocal实现原理与使用场景
23.线程池原理
24.线程池的几种方式和优缺点
25.线程的生命周期
26.说说线程安全问题
27.volatile 实现原理
28.synchronized和Lock的区别
29.共享锁、排它锁、可重入锁
30.CAS乐观锁
31.ABA问题与解决方法
32.Mysql索引的注意事项
33.说说分库与分表设计
34.分库与分表带来的分布式困境与应对之策
35.说说 SQL 优化之道

面试答案—借鉴
final, finally, finalize 的区别

  1. 在java中,final可以用来修饰类,方法和变量(成员变量或局部变量)。

  2. finally作为异常处理的一部分,它只能用在try/catch语句中,并且附带一个语句块,表示这段语句最终一定会被执行(不管有没有抛出异常),经常被用在需要释放资源的情况下。

  3. finalize()是在java.lang.Object里定义的,也就是说每一个对象都有这么个方法。这个方法在gc启动,该对象被回收的时候被调用。其实gc可以回收大部分的对象(凡是new出来的对象,gc都能搞定,一般情况下我们又不会用new以外的方式去创建对象),所以一般是不需要程序员去实现finalize的。特殊情况下,需要程序员实现finalize,当对象被回收的时候释放一些资源,比如:一个socket链接,在对象初始化时创建,整个生命周期内有效,那么就需要实现finalize,关闭这个链接。

int和Interger的区别

  1. a == b 吗? 不相等。两个new出来的对象地址不一样。

  2. c == d 吗? 都是基本数据类型的值肯定相等。
    e == f 吗? g == h 吗?
    答案是:e == f; g != h。为什么会出现这种情况?因为ava在进行编译时 Integer g = 130会被编译成 Integer.valueOf(130) ,这个可以通过反编译class文件看到。而通过Integer源码可以得出,Integer.valueOf() 方法会在数值-128~127之间会对Integer进行缓存,不会再重新new一个,所以 e==f ;当数值二大于127或者小于-128的时候则会重新new一个,所以g != h 。

  3. c == e 吗, i == j 吗?
    答案都是相等的。因为封装类和基本数据类型进行比较的时候,java会自动拆箱,然后比较数值是否相等。

重写与重载的区别
答:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求,不能根据返回类型进行区分。

抽象类和接口有什么区别

  1. 抽象类要被子类继承,接口要被类实现;

  2. 接口只能做方法声明,抽象类中可以作方法声明,也可以做方法实现;

  3. 接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量;

  4. 接口是设计的结果,抽象类是重构的结果;

  5. 抽象类和接口都是用来抽象具体对象的,但是接口的抽象级别最高;

  6. 抽象类可以有具体的方法和属性,接口只能有抽象方法和不可变常量;

  7. 抽象类主要用来抽象类别,接口主要用来抽象功能;

说说反射的用途

  1. 在运行时判断任意一个对象所属的类

  2. 在运行时构造任意一个类的对象

  3. 在运行时判断任意一个类所具有的成员变量和方法(通过反射设置可以调用 private)

  4. 在运行时调用一个对象的方法

说说自定义注解的场景及实现
登陆、权限拦截、日志处理,以及各种 Java 框架,如 Spring,Hibernate,JUnit 提到注解就不能不说反射,Java 自定义注解是通过运行时靠反射获取注解。

session 分布式处理

  1. Session Replication 方式管理 (即session复制)

简介:将一台机器上的Session数据广播复制到集群中其余机器上

使用场景:机器较少,网络流量较小

优点:实现简单、配置较少、当网络中有机器Down掉时不影响用户访问

缺点:广播式复制到其余机器有一定廷时,带来一定网络开销

  1. Session Sticky 方式管理

简介:即粘性Session、当用户访问集群中某台机器后,强制指定后续所有请求均落到此机器上

使用场景:机器数适中、对稳定性要求不是非常苛刻

优点:实现简单、配置方便、没有额外网络开销

缺点:网络中有机器Down掉时、用户Session会丢失、容易造成单点故障

  1. 缓存集中式管理

简介:将Session存入分布式缓存集群中的某台机器上,当用户访问不同节点时先从缓存中拿Session信息使用场景:集群中机器数多、网络环境复杂

优点:可靠性好

缺点:实现复杂、稳定性依赖于缓存的稳定性、Session信息放入缓存时要有合理的策略写入

JDBC 流程

  1. 注冊驱动 (仅仅做一次)

  2. 建立连接(Connection)

  3. 创建运行SQL的语句(Statement)

  4. 运行语句

  5. 处理运行结果(ResultSet)

  6. 释放资源

List 和 Set 区别

  1. list方法可以允许重复的对象,而set方法不允许重复对象

  2. list可以插入多个null元素,而set只允许插入一个null元素

  3. list是一个有序的容器,保持了每个元素的插入顺序。即输出顺序就是输入顺序,而set方法是无序容器,无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序

Arraylist 与 LinkedList 区别

  1. ArrayList是依靠数组来存放对象的;

  2. LinkedList可以看做为一个双向链表,所有的操作都可以认为是一个双向链表的操作。并不是用普通的数组来存放数据的,而是使用结点来存放数据的,有一个指向链表头的结点first和一个指向链表尾的结点last;

  3. 不同于ArrayList只能在数组末尾添加数据,LinkList可以很方便在链表头或者链表尾插入数据,或者在指定结点前后插入数据;

  4. 对于数据频繁出入的情况下,并且要求操作要足够灵活,建议使用LinkedList;对于数组变动不大,主要是用来查询的情况下,可以使用ArrayList。

ArrayList 与 Vector 区别

  1. Vector是线程安全的,源码中有很多的synchronized可以看出,而ArrayList不是。导致Vector效率无法和ArrayList相比;

  2. ArrayList和Vector都采用线性连续存储空间,当存储空间不足的时候,ArrayList默认增加为原来的50%,Vector默认增加为原来的一倍;

  3. Vector可以设置capacityIncrement,而ArrayList不可以,从字面理解就是capacity容量,Increment增加,容量增长的参数。

HashMap 和 Hashtable 的区别

  1. Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口;

  2. Hashtable是线程安全的,而HashMap非线程安全;

  3. HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey。Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同;

  4. Hashtable中,key和value都不允许出现null值。HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null;

  5. Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 ;

  6. HashTable直接使用对象的hashCode。而HashMap重新计算hash值;

  7. HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

HashSet 和 HashMap 区别

HashMap

HashSet

HashMap实现了Map接口

HashSet实现了Set接口

HashMap储存键值对

HashSet仅仅存储对象

使用put()方法将元素放入map中

使用add()方法将元素放入set中

HashMap中使用键对象来计算hashcode值

HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false

HashMap比较快,因为是使用唯一的键来获取对象

HashSet较HashMap来说比较慢

HashMap 和 ConcurrentHashMap 的区别

  1. ConcurrentHashMap对整个桶数组进行了分段,而HashMap则有;

  2. ConcurrentHashMap在每一个分段上都用锁进行保护,从而让锁的粒度更精细一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。

HashMap 的工作原理

  1. 调用hashCode计算hash从而得到bucket位置;

  2. HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍);

  3. 如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。

ConcurrentHashMap 的工作原理

  1. ConcurrentHashMap是Java1.5中引用的一个线程安全的支持高并发的HashMap集合类。

  2. ConcurrentHashMap采用了非常精妙的"分段锁"策略(将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。),ConcurrentHashMap的主干是个Segment数组。Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的;

sleep() 、join()、yield()有什么区别

  1. sleep() 方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是 sleep() 方法不会释放“锁标志”,也就是说如果有 synchronized 同步块,其他线程仍然不能访问共享数据;

  2. wait() 方法需要和 notify() 及 notifyAll() 两个方法一起介绍,这三个方法用于协调多个线程对共享数据的存取,所以必须在 synchronized 语句块内使用,也就是说,调用 wait(),notify() 和 notifyAll() 的任务在调用这些方法前必须拥有对象的锁。注意,它们都是 Object 类的方法,而不是 Thread 类的方法。wait() 方法与 sleep() 方法的不同之处在于,wait() 方法会释放对象的“锁标志”。当调用某一对象的 wait() 方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了 notify() 方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的 notifyAll() 方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。除了使用 notify() 和 notifyAll() 方法,还可以使用带毫秒参数的 wait(long timeout) 方法,效果是在延迟 timeout 毫秒后,被暂停的线程将被恢复到锁标志等待池;

  3. yield() 方法和 sleep() 方法类似,也不会释放“锁标志”,区别在于,它没有参数,即 yield() 方法只是使当前线程重新回到可执行状态,所以执行 yield() 的线程有可能在进入到可执行状态后马上又被执行,另外 yield() 方法只能使同优先级或者高优先级的线程得到执行机会,这也和 sleep() 方法不同;

  4. join() 方法会使当前线程等待调用 join() 方法的线程结束后才能继续执行。

CountDownLatch

  1. 解释一下CountDownLatch概念

a) CountDownLatch是在jdk1.5引入的,在java.util.concurrent包下,允许一个或多个线程等待直到其他线程完成操作;

b) CountDownLatch通过计数器来实现,初始值为线程的数量,每执行完一个线程 计数器减1,直到变成0,调用await方法的线程才能继续执行。

  1. 给出一些CountDownLatch使用的例子

a) 核心服务检测

b) 模拟高并发

c) zook连接

  1. CountDownLatch 类中主要的方法

a) await 当前线程等待直到计数器为0

b) countDown 计数器减1

CyclicBarrier原理

  1. 线程达到屏障点时被阻塞,直到最后一个线程达到屏障点,屏障才会打开;

  2. 构造方法为CyclicBarrier(int parties, Runnable barrierAction),barrierAction非必需,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动barrier时执行给定的屏障操作,该操作由最后一个进入barrier的线程执行;

  3. 调用await()方法使得线程进入等待状态,直到最后一个线程调用await()方法时,才会启动barrier;

  4. 调用await(long timeout, TimeUnit unit)方法也可以使得线程进入等待状态,当等待超时后,该线程调用breakBarrier()方法终止CyclicBarrier,抛出TimeoutException 异常,而所有处于等待状态的线程将会抛出BrokenBarrierException 异常,其他线程调用await()方法时,也会抛出BrokenBarrierException异常,另外,这种情况下,最后一个线程也不会执行barrierAction。

Semaphore原理

  1. Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,保证合理的使用公共资源;

  2. 线程可以通过acquire()方法来获取信号量的许可,当信号量中没有可用的许可的时候,线程阻塞,直到有可用的许可为止。线程可以通过release()方法释放它持有的信号量的许可;

  3. "公平信号量"和"非公平信号量"的释放信号量的机制是一样的!不同的是它们获取信号量的机制:线程在尝试获取信号量许可时,对于公平信号量而言,如果当前线程不在CLH队列的头部,则排队等候;而对于非公平信号量而言,无论当前线程是不是在CLH队列的头部,它都会直接获取信号量。该差异具体的体现在,它们的tryAcquireShared()函数的实现不同。

Exchanger 原理

  1. Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。因此使用Exchanger的重点是成对的线程使用exchange()方法,当有一对线程达到了同步点,就会进行交换数据。因此该工具类的线程对象是成对的;

  2. Exchanger类提供了两个方法,String exchange(V x):用于交换,启动交换并等待另一个线程调用exchange;String exchange(V x,long timeout,TimeUnit unit):用于交换,启动交换并等待另一个线程调用exchange,并且设置最大等待时间,当等待时间超过timeout便停止等待。

ThreadLocal实现原理与使用场景

  1. 实现原理:
    每一个Thread对象维护一个ThreadLocalMap,它以ThreadLoal作为key值,Object作为value值。当调用ThreadLocal对象的set方法来设置值时,ThreadLocal先获取到当前Thread对象,然后获取到该Thread对象的ThreadLocalMap对象,通过ThreadLocalMap对象的set方法,以ThreadLocal对象作为key值,ThreadLocal对象set方法传入的Object对象作为value值,将数据加入到ThreadLocalMap对象中。

  2. 使用场景:

a) 每个线程需要有自己单独的实例

b) 实例需要在多个方法中共享,但不希望被多线程共享

线程池原理

  1. 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;

  2. 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;

  3. 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;

  4. 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

  5. 默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。

在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:

prestartCoreThread():初始化一个核心线程;

prestartAllCoreThreads():初始化所有核心线程

  1. workQueue的类型为BlockingQueue,通常可以取下面三种类型:

a) ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;

b) LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;

c) synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

  1. 还有任务到来就会采取任务拒绝策略,通常有以下四种策略:

a) ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常;

b) ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常;

c) ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程);

d) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果

  1. ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:

a) shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务

b) shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

  1. 一般需要根据任务的类型来配置线程池大小:

a) 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1

b) 如果是IO密集型任务,参考值可以设置为2*NCPU

线程池的几种方式和优缺点
Java通过Executors提供四种线程池,分别为:

  1. newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。Executors.newCachedThreadPool(); 缺点:大家一般不用是因为newCachedThreadPool 可以无线的新建线程,容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值

  2. newFixedThreadPool Executors.newFixedThreadPool(3);创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。可参考PreloadDataCache。其实newFixedThreadPool()在严格上说并不会复用线程,每运行一个Runnable都会通过ThreadFactory创建一个线程

  3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。Executors.newScheduledThreadPool(5);与Timer 对比:Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。

ScheduledThreadPoolExecutor的设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。

通过对比可以发现ScheduledExecutorService比Timer更安全,功能更强大,在以后的开发中尽可能使用ScheduledExecutorService(JDK1.5以后)替代Timer

  1. newSingleThreadExecutor Executors.newSingleThreadExecutor() 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。现行大多数GUI程序都是单线程的。Android中单线程可用于数据库操作,文件操作,应用批量安装,应用批量删除等不适合并发但可能IO阻塞性及影响UI线程响应的操作。

线程的生命周期

  1. 线程的生命周期一共分为五个部分分别是:新建,就绪,运行,阻塞以及死亡。由于cpu需要在多条线程中切换因此线程状态也会在多次运行和阻塞之间切换;

  2. 当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动);

  3. 当调用线程对象的start方法后,线程加入到就绪队列中,等待获取CPU资源,此时线程进入就绪状态;

  4. 线程获取到CPU资源,此时线程进入到运行状态;

  5. 由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。如调用sleep方法或者wait方法,可以使当前线程进度堵塞状态,对于sleep方法进入堵塞状态的线程,一段时间后,该线程会进入就绪状态,而对于调用wait方法进入堵塞状态的线程,需要通过调用notify或notifyAll方法使得线程进入就绪状态;

  6. 当线程执行完毕或被其它线程杀死,线程就进入死亡状态。

说说线程安全问题
简单来说,线程安全就是: 在多线程环境中,能永远保证程序的正确性。

分三种方式解决:

  1. 第一种,修改线程模型。即不在线程之间共享该状态变量。一般这个改动比较大,需要量力而行。

  2. 第二种,将对象变为不可变对象。有时候实现不了。

  3. 第三种,就比较通用了,在访问状态变量时使用同步。 synchronized和Lock都可以实现同步。简单点说,就是在你修改或访问可变状态时加锁,独占对象,让其他线程进不来。这也算是一种线程隔离的办法。(这种方式也有不少缺点,比如说死锁,性能问题等等)

volatile 实现原理
当修改一个变量的数据时,该数据并不会立即写到内存中,而是写到处理器的缓存行中,这样就导致了其他线程对该数据是不可见的。而当给变量增加了volatile修饰后,当修改变量的数据时,会向CPU发出一个lock指令,该lock指令会将当前处理器缓存行中的数据写到内存中,同时使得其他CPU中缓存了该内存地址的数据无效,这样其他线程在读取该变量的值时,就会直接从内存中去读取。

synchronized和Lock的区别

  1. synchronized自动释放锁,而Lock必须手动释放,并且代码中出现异常会导致unlock代码不执行,所以Lock一般在Finally中释放,而synchronized释放锁是由JVM自动执行的;

  2. Lock有共享锁的概念,所以可以设置读写锁提高效率,synchronized不能。(两者都可重入);

  3. Lock可以让线程在获取锁的过程中响应中断,而synchronized不会,线程会一直等待下去。lock.lockInterruptibly()方法会优先响应中断,而不是像lock一样优先去获取锁;

  4. Lock锁的是代码块,synchronized还能锁方法和类;

  5. Lock可以知道线程有没有拿到锁,而synchronized不能。

共享锁、排它锁、可重入锁

  1. 共享锁又称读锁、S锁,共享锁允许多个线程同时获取一个锁,如CountDownLatch就是一种共享锁;

  2. 排它锁又称写锁、X锁,也称为独占锁,一个锁在某一时刻只能被一个线程占用,如ReentrantLock就是一种排它锁(ReentrantReadWriteLock包含读写和写锁功能)

  3. 可重入锁指的是支持一个线程对资源的重复加锁。

CAS乐观锁

  1. CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换;

  2. CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B;

  3. 更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

ABA问题与解决方法

  1. 在CAS算法中,需要取出内存中某时刻的数据(由用户完成),在下一时刻比较并替换(由CPU完成,该操作是原子的)。这个时间差中,会导致数据的变化。

  2. 假设如下事件序列:

线程 1 从内存位置V中取出A。

线程 2 从位置V中取出A。

线程 2 进行了一些操作,将B写入位置V。

线程 2 将A再次写入位置V。

线程 1 进行CAS操作,发现位置V中仍然是A,操作成功。

  1. 尽管线程 1 的CAS操作成功,但不代表这个过程没有问题——对于线程 1 ,线程 2 的修改已经丢失。

  2. 解决方法:AtomicStampedReference

Mysql索引的注意事项

  1. 索引的优点:

a) 大大加快数据的查询速度

b) 使用分组和排序进行数据查询时,可以显著减少查询时分组和排序的时间

c) 创建唯一索引,能够保证数据库表中每一行数据的唯一性

d) 在实现数据的参考完整性方面,可以加速表和表之间的连接

  1. 索引的缺点

a) 创建索引和维护索引需要消耗时间,并且随着数据量的增加,时间也会增加

b) 索引需要占据磁盘空间

c) 对数据表中的数据进行增加,修改,删除时,索引也要动态的维护,降低了维护的速度

  1. 创建索引的原则

a) 更新频繁的列不应设置索引

b) 数据量小的表不要使用索引(毕竟总共2页的文档,还要目录吗?)

c) 重复数据多的字段不应设为索引(比如性别,只有男和女,一般来说:重复的数据超过百分之15就不该建索引)

d) 首先应该考虑对where 和 order by 涉及的列上建立索引

  1. 优化mysql查询语句

a) 不要在where条件语句 ‘=’ 的左边进行函数,运算符或表达式的计算,如 select name from tb_user where age/2=20,因为索引不会生效(引擎会放弃使用索引,进行全表扫描)

b) 不要使用 <>,!=,not in ,因为索引不会生效

c) 避免对字段进行null的判断,因为索引不会生效(可以用一个值代替null,如-999)

d) 使用like模糊查询时,like '%xx%'会导致索引不生效,like ‘xx%’ 索引能够被使用,所以避免使用第一种

e) 避免使用or,可以用union替代(要想使用or,又让索引生效,or条件中的每个列都必须加上索引)

f) 使用exist代替in(表中数据越多,exist的效率就比in要越大)

g) 数据类型隐形转换,索引不会生效:如 select name from user where phone=13155667788;(phone字段在数据库中为varchar类型,应改成 phone=‘13155667788’)

h) 联合索引必须要按照顺序才会生效:如创建的索引顺序为a,b,where a=“xx” and b=“xx” 生效,但 b=“xx” and a=“xx” 则不会生效,补充:a=“xx” 没有后面的,索引也会生效

i) 尽量避免使用游标(游标效率低)

j) 不要使用 select *

说说分库与分表设计

  1. 垂直分表

垂直分表在日常开发和设计中比较常见,通俗的说法叫做“大表拆小表”,拆分是基于关系型数据库中的“列”(字段)进行的。通常情况,某个表中的字段比较多,可以新建立一张“扩展表”,将不经常使用或者长度较大的字段拆分出去放到“扩展表”中。在字段很多的情况下,拆分开确实更便于开发和维护(笔者曾见过某个遗留系统中,一个大表中包含100多列的)。某种意义上也能避免“跨页”的问题(MySQL、MSSQL底层都是通过“数据页”来存储的,“跨页”问题可能会造成额外的性能开销,拆分字段的操作建议在数据库设计阶段就做好。如果是在发展过程中拆分,则需要改写以前的查询语句,会额外带来一定的成本和风险,建议谨慎。

  1. 垂直分库

垂直分库在“微服务”盛行的今天已经非常普及了。基本的思路就是按照业务模块来划分出不同的数据库,而不是像早期一样将所有的数据表都放到同一个数据库中。系统层面的“服务化”拆分操作,能够解决业务系统层面的耦合和性能瓶颈,有利于系统的扩展维护。而数据库层面的拆分,道理也是相通的。与服务的“治理”和“降级”机制类似,我们也能对不同业务类型的数据进行“分级”管理、维护、监控、扩展等。

众所周知,数据库往往最容易成为应用系统的瓶颈,而数据库本身属于“有状态”的,相对于Web和应用服务器来讲,是比较难实现“横向扩展”的。数据库的连接资源比较宝贵且单机处理能力也有限,在高并发场景下,垂直分库一定程度上能够突破IO、连接数及单机硬件资源的瓶颈,是大型分布式系统中优化数据库架构的重要手段。

  1. 水平分表

水平分表也称为横向分表,比较容易理解,就是将表中不同的数据行按照一定规律分布到不同的数据库表中(这些表保存在同一个数据库中),这样来降低单表数据量,优化查询性能。最常见的方式就是通过主键或者时间等字段进行Hash和取模后拆分。水平分表,能够降低单表的数据量,一定程度上可以缓解查询性能瓶颈。但本质上这些表还保存在同一个库中,所以库级别还是会有IO瓶颈。所以,一般不建议采用这种做法。

  1. 水平分库

水平分库分表与上面讲到的水平分表的思想相同,唯一不同的就是将这些拆分出来的表保存在不同的数据中。这也是很多大型互联网公司所选择的做法。某种意义上来讲,有些系统中使用的“冷热数据分离”(将一些使用较少的历史数据迁移到其他的数据库中。而在业务功能上,通常默认只提供热点数据的查询),也是类似的实践。在高并发和海量数据的场景下,分库分表能够有效缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源的瓶颈。当然,投入的硬件成本也会更高。同时,这也会带来一些复杂的技术问题和挑战(例如:跨分片的复杂查询,跨分片事务等)。

分库与分表带来的分布式困境与应对之策
随着用户数的不断增加,以及数据量的不断增加,通过分库与分表的方式提高查询性能的同时,带来了一系列分布式困境。

  1. 数据迁移与扩容问题

水平分表策略归纳总结为随机分表和连续分表两种情况。连续分表有可能存在数据热点的问题,有些表可能会被频繁地查询从而造成较大压力,热数据的表就成为了整个库的瓶颈,而有些表可能存的是历史数据,很少需要被查询到。连续分表的另外一个好处在于比较容易,不需要考虑迁移旧的数据,只需要添加分表就可以自动扩容。随机分表的数据相对比较均匀,不容易出现热点和并发访问的瓶颈。但是,分表扩展需要迁移旧的数据。
针对于水平分表的设计至关重要,需要评估中短期内业务的增长速度,对当前的数据量进行容量规划,综合成本因素,推算出大概需要多少分片。对于数据迁移的问题,一般做法是通过程序先读出数据,然后按照指定的分表策略再将数据写入到各个分表中。

  1. 表关联问题

在单库单表的情况下,联合查询是非常容易的。但是,随着分库与分表的演变,联合查询就遇到跨库关联和跨表关系问题。在设计之初就应该尽量避免联合查询,可以通过程序中进行拼装,或者通过反范式化设计进行规避。

  1. 分页与排序问题

一般情况下,列表分页时需要按照指定字段进行排序。在单库单表的情况下,分页和排序也是非常容易的。但是,随着分库与分表的演变,也会遇到跨库排序和跨表排序问题。为了最终结果的准确性,需要在不同的分表中将数据进行排序并返回,并将不同分表返回的结果集进行汇总和再次排序,最后再返回给用户。

  1. 分布式事务问题

随着分库与分表的演变,一定会遇到分布式事务问题,那么如何保证数据的一致性就成为一个必须面对的问题。目前,分布式事务并没有很好的解决方案,难以满足数据强一致性,一般情况下,使存储数据尽可能达到用户一致,保证系统经过一段较短的时间的自我恢复和修正,数据最终达到一致。

  1. 分布式全局唯一ID

在单库单表的情况下,直接使用数据库自增特性来生成主键ID,这样确实比较简单。在分库分表的环境中,数据分布在不同的分表上,不能再借助数据库自增长特性。需要使用全局唯一 ID,例如 UUID、GUID等。关于如何选择合适的全局唯一 ID,我会在后面的章节中进行介绍。

总结

分库与分表主要用于应对当前互联网常见的两个场景:海量数据和高并发。然而,分库与分表是一把双刃剑,虽然很好的应对海量数据和高并发对数据库的冲击和压力,但是却提高的系统的复杂度和维护成本。

因此建议:需要结合实际需求,不宜过度设计,在项目一开始不采用分库与分表设计,而是随着业务的增长,在无法继续优化的情况下,再考虑分库与分表提高系统的性能。

说说 SQL 优化之道

  1. 负向条件查询不能使用索引,可以优化为 in 查询:

select from order where status!=0 and status!=1

  1. 前导模糊查询不能使用索引,而非前导模糊查询则可以:

select from order where desc like ‘%XX’

  1. 数据区分度不大的字段不宜使用索引。原因:性别只有男,女,每次过滤掉的数据很少,不宜使用索引。经验上,能过滤80%数据时就可以使用索引:

select from user where sex=1

  1. 在属性上进行计算不能命中索引:

select from order where YEAR(date) < = ‘2017’

即使date上建立了索引,也会全表扫描,可优化为值计算:

select from order where date < = CURDATE()

  1. 如果业务大部分是单条查询,使用Hash索引性能更好,例如用户中心。原因:B-Tree 索引的时间复杂度是 O(log(n));Hash 索引的时间复杂度是 O(1):

select from user where uid=?

select from user where login_name=?

  1. 允许为 null 的列,查询有潜在大坑。单列索引不存 null 值,复合索引不存全为 null 的值,如果列允许为 null,可能会得到“不符合预期”的结果集:

select from user where name != ‘shenjian’

如果 name 允许为 null,索引不存储 null 值,结果集中不会包含这些记录。所以,请使用 not null 约束以及默认值。

  1. 使用 ENUM 而不是字符串。ENUM 保存的是 TINYINT,别在枚举中搞一些“中国”“北京”“技术部”这样的字符串,字符串空间又大,效率又低。

  2. 如果明确知道只有一条结果返回,limit 1 能够提高效率

select from user where login_name=?

可以优化为:

select from user where login_name=? limit 1

原因:你知道只有一条结果,但数据库并不知道,明确告诉它,让它主动停止游标移动

  1. 把计算放到业务层而不是数据库层,除了节省数据的 CPU,还有意想不到的查询缓存优化效果

select from order where date < = CURDATE()

  1. 强制类型转换会全表扫描

select from user where phone=13800001234

  1. 不要使用 select *,只返回需要的列,能够大大的节省数据传输量,与数据库的内存使用量
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值