最新java面试题

hashmap的遍历方式

使用 Iterator 遍历 HashMap EntrySet

package com.java.tutorials.iterations;
 
 
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
 
 
/**
 * 在 Java 中遍历 HashMap 的5种最佳方法
 * @author Ramesh Fadatare
 *
 */
public class IterateHashMapExample {
    public static void main(String[] args) {
        // 1. 使用 Iterator 遍历 HashMap EntrySet
        Map < Integer, String > coursesMap = new HashMap < Integer, String > ();
        coursesMap.put(1, "C");
        coursesMap.put(2, "C++");
        coursesMap.put(3, "Java");
        coursesMap.put(4, "Spring Framework");
        coursesMap.put(5, "Hibernate ORM framework");
 
 
        Iterator < Entry < Integer, String >> iterator = coursesMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry < Integer, String > entry = iterator.next();
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
    }
}

使用 Iterator 遍历 HashMap KeySet

package com.java.tutorials.iterations;
 
 
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
 
 
/**
 * 在 Java 中遍历 HashMap 的5种最佳方法
 * @author Ramesh Fadatare
 *
 */
public class IterateHashMapExample {
    public static void main(String[] args) {
        Map < Integer, String > coursesMap = new HashMap < Integer, String > ();
        coursesMap.put(1, "C");
        coursesMap.put(2, "C++");
        coursesMap.put(3, "Java");
        coursesMap.put(4, "Spring Framework");
        coursesMap.put(5, "Hibernate ORM framework");
 
 
        // 2. 使用 Iterator 遍历 HashMap KeySet
        Iterator < Integer > iterator = coursesMap.keySet().iterator();
        while (iterator.hasNext()) {
            Integer key = iterator.next();
            System.out.println(key);
            System.out.println(coursesMap.get(key));
        }
    }
}

3. 使用 For-each 循环遍历 HashMap

package com.java.tutorials.iterations;
 
 
import java.util.HashMap;
import java.util.Map;
 
 
/**
 * 在 Java 中遍历 HashMap 的5种最佳方法
 * @author Ramesh Fadatare
 *
 */
public class IterateHashMapExample {
    public static void main(String[] args) {
        Map < Integer, String > coursesMap = new HashMap < Integer, String > ();
        coursesMap.put(1, "C");
        coursesMap.put(2, "C++");
        coursesMap.put(3, "Java");
        coursesMap.put(4, "Spring Framework");
        coursesMap.put(5, "Hibernate ORM framework");
 
 
        // 3. 使用 For-each 循环遍历 HashMap
        for (Map.Entry < Integer, String > entry: coursesMap.entrySet()) {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
    }
}

4. 使用 Lambda 表达式遍历 HashMap

package com.java.tutorials.iterations;
 
 
import java.util.HashMap;
import java.util.Map;
 
 
/**
 * 在 Java 中遍历 HashMap 的5种最佳方法
 * @author Ramesh Fadatare
 *
 */
public class IterateHashMapExample {
    public static void main(String[] args) {
        Map < Integer, String > coursesMap = new HashMap < Integer, String > ();
        coursesMap.put(1, "C");
        coursesMap.put(2, "C++");
        coursesMap.put(3, "Java");
        coursesMap.put(4, "Spring Framework");
        coursesMap.put(5, "Hibernate ORM framework");
 
 
        // 4. 使用 Lambda 表达式遍历 HashMap
        coursesMap.forEach((key, value) -> {
            System.out.println(key);
            System.out.println(value);
        });
    }
}

使用 Stream API 遍历 HashMap

package com.java.tutorials.iterations;
 
 
import java.util.HashMap;
import java.util.Map;
 
 
/**
 * 在 Java 中遍历 HashMap 的5种最佳方法
 * @author Ramesh Fadatare
 *
 */
public class IterateHashMapExample {
    public static void main(String[] args) {
        Map < Integer, String > coursesMap = new HashMap < Integer, String > ();
        coursesMap.put(1, "C");
        coursesMap.put(2, "C++");
        coursesMap.put(3, "Java");
        coursesMap.put(4, "Spring Framework");
        coursesMap.put(5, "Hibernate ORM framework");
 
 
        // 5. 使用 Stream API 遍历 HashMap
        coursesMap.entrySet().stream().forEach((entry) - > {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        });
    }
}

 

springcloud分布式事务

1、说说你对分布式事务的了解

分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,

特别是在微服务架构中,几乎可以说是无法避免。

首先要搞清楚:ACID、CAP、BASE理论。

ACID

指数据库事务正确执行的四个基本要素:

  1. 原子性(Atomicity)

  2. 一致性(Consistency)

  3. 隔离性(Isolation)

  4. 持久性(Durability)

CAP

CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性

(Availability)、分区容忍性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同

时实现两点,不可能三者兼顾。

  • 一致性:在分布式系统中的所有数据备份,在同一时刻是否同样的值。

  • 可用性:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。

  • 分区容忍性:以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数

  • 据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。

BASE理论

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到

强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。

Basically Available(基本可用)

Soft state(软状态)

Eventually consistent(最终一致性)

2、你知道哪些分布式事务解决方案

我目前知道的有五种:

  1. 两阶段提交(2PC)

  2. 三阶段提交(3PC)

  3. 补偿事务(TCC=Try-Confifirm-Cancel)

  4. 本地消息队列表(MQ)

  5. Sagas事务模型(最终一致性)

     

3、什么是二阶段提交?

两阶段提交2PC是分布式事务中最强大的事务类型之一,两段提交就是分两个阶段提交:

第一阶段询问各个事务数据源是否准备好。

第二阶段才真正将数据提交给事务数据源。

为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被称为参与者

(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。

问题

1) 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。

2) 可靠性问题:如果协调者存在单点故障问题,或出现故障,提供者将一直处于锁定状态。

3) 数据一致性问题:在阶段 2 中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一

致。

优点:尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证

强一致)。

缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。

4、什么是三阶段提交?

三阶段提交是在二阶段提交上的改进版本,3PC最关键要解决的就是协调者和参与者同时挂掉的问

题,所以3PC把2PC的准备阶段再次一分为二,这样三阶段提交。

优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。

避免了协调者单点问题。阶段 3 中协调者出现问题时,参与者会继续提交事务。

缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,

此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造

成数据不一致。

线程怎么使用的

进程

一个正在执行中的程序就是一个进程,系统会为这个进程发配独立的【内存资源】。进程是程序的一次执行过程,它有自己独立的生命周期,它会在启动程序时产生,运行程序时存在,关闭程序时消亡。

线程:

是由进程创建的,是进程的一个实体,是具体干活的人,一个进程可能有多个线程。线程不独立分配内存,而是共享进程的内存资源,线程可以共享cpu的计算资源。

现在,进程更强调【内存资源的分配】,而线程更强调【计算资源的分配】。因为有了线程的概念,一个进程的线程就不能修改另一个线程的数据,隔离性更好,安全性更好。

但是这里有几个问题:

我们的进程可以直接创建、调度线程吗?QQ运行了一会说我累了,不想执行了,微信你来吧!这显然是不合理的。

QQ执行了一会,不执行了,那等其他线程执行完成之后,又轮上QQ了,QQ还能记得刚才运行到哪里了吗?

针对第一个问题,任何一个用户的线程是不允许调度其他的线程的,所有的线程调用都由一个大管家统一调度,这个大管家就是系统内核。

第二个问题,下一个执行时想要知道上一次的执行结果,就必须在上一次执行之后,将运行时的数据进行保存,那么整个过程就出来了。

其中,用户线程执行的过程我们称之为【用户态】,内核调度的状态称之为【内核态】,每一个线程运行时产生的数据我们称之为【上下文】,线程的每次切换都需要进行用户态到内核态的来回切换,同时伴随着上下文的切换,是一个比较消耗资源的操作,所以一个计算机当中不是线程越多越好,线程如果太多也是有可能拖垮整个系统的。

1、线程生命周期五种状态:

创建、就绪、运行、阻塞、死亡

首先通过继承thread类或者实现runnable接口来创建一个线程,当调用线程的start方法,线程进入就绪状态,如果这时处理器有资源运行此线程,线程就进入运行状态,如果线程内部调用了sleep就会放弃cpu资源,返回到阻塞状态,线程等待某个通知,sleep时间到了之后,如果其他线程发送通知,那么当前线程就从阻塞状态变为就绪状态恢复执行。另如果调用了yield()方法也会从运行状态变为就绪状态。一般来说线程执行完毕或者调用stop方法线程结束生命周期。

阻塞:

wait(等待)会让线程进入阻塞状态,会失去所占用的资源;

还有其他方式可以让一个线程进入阻塞状态(三个例子):

1.4.1 synchronized : 锁,同时调用同一个资源时,会等待正在执行的任务完成后在执行另一个任务。

1.4.2 sleep(睡眠):会让线程进入以毫秒为单位的休眠时间

1.4.3 join():会让主线程等待子线程执行完毕后再执行

2.应用场景

当我们需要在同一时间执行两个或多个方法的时候,就可以使用线程。

比如:

当我们的切面中需要记录日志,如果方法的运行和记录日志同时执行的话,可能会影响项目整体的运行效率,那么 这时就可以在开启一个线程,同时进行登录和日志记录。

再比如:

登录时如果需要添加账号,如果查询账号花费大量的时间,会影响用户的体验,那么就可以开启一个线程专门去查 询和添加数据,而主线程继续进行页面的跳转,大大的优化项目效率。

看项目需求灵活使用

3.线程的创建(常用两种方法)

①.继承Thread类

public class TestThread extends Thread{
    @Override
    public void run() {
        System.out.println("Thread运行");
    }
}
主进程中调用:
​
TestThread testThread = new TestThread();
testThread.start();

②.实现Runnable接口,实现run方法

public class TestRunnable implements Runnable{
•
    @Override
    public void run() {
       System.out.println("Runnable运行");
    }
}
需要注意:由于Runnable没有start()方法(Runnable全部源码)
​
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
而开启一个线程就必须要使用start,所以就需要使用Thread:
​
//可以这么些
//实例化一个Runnable方法
TestRunnable testRunnable = new TestRunnable();
//new一个Thread传参为Runnable
new Thread(testRunnable).start();
•
//或者这么写
new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Runnable运行");
            }
        }).start();
•
//这还有流式的写法(可以了解一下)
new Thread(() -> System.out.println("Runnable运行")).start();

两种方式的最大区别在于:

由于Java不支持多继承,而继承了Thread就不能继承其他的,所以一般都使用实现Runnable的方式去创建线程

还有一点

一个线程被start一次以后,就不可以再次start了,否则会报错哟

③.实现Callable接口,实现call(寇)方法和Runnable不同在于这个接口有返回值,并且依赖于线程池。

4.同步锁(synchronized)

被关键字修饰的资源同一时间不可以被其他资源访问,必须等待其他资源任务执行完成后再进行访问

synchronized 有三种方式来加锁,分别是

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

  2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

(可以看下StringBuffer和StringBuilder的源码

StringBuffer的底层几乎都是加了synchronized同步锁,所以他的线程是安全的;

而StringBuilder则相反没有,所以他的线程是不安全的;

但这样也导致了StringBuffer的运行效率较StringBuilder而言较低)

synchronized举例说明:

//一个上厕所的方法;只有一个厕所
    public static String 厕所(String name){
        //上厕所5分钟
        for (int i=0;i<5;i++){
            //这里是为了方便观察才进行睡眠
            if (i==3){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(name+"正在厕所");
        }
        return null;
    }
main方法
​
public static void main(String[] args) {
        //在这里开了四个线程,去厕所
        new Thread(new Runnable() {
            @Override
            public void run() {
                厕所("我");
            }
        }).start();
    
        new Thread(new Runnable() {
            @Override
            public void run() {
                厕所("彭于晏");
            }
        }).start();
    
        new Thread(new Runnable() {
            @Override
            public void run() {
                厕所("吴彦祖");
            }
        }).start();
    
        new Thread(new Runnable() {
            @Override
            public void run() {
                厕所("胡歌");
            }
        }).start();
    }

程序运行结果:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiA5p2v5Y-v5LmQ5bCxb2s=,size_8,color_FFFFFF,t_70,g_se,x_16

由输出结果可见,四个线程在一起调用同一个方法,方法只有一个,到最后一起执行

四个人一起上厕所,厕所只有一个,四个人都往里钻,造成混乱

那么如果在代码中加入同步锁(synchronized)

//一个上厕所的方法;只有一个厕所
    public static synchronized String 厕所(String name){
        //上厕所5分钟
        for (int i=0;i<5;i++){
            //这里是为了方便观察才进行睡眠
            if (i==3){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(name+"正在厕所");
        }
        return null;
    }

现在的运行结果:

4f3a0eed47ad41c19fa9006e01723380.png

什么算是线程安全:

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

简单来说如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

死锁产生条件

互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。

请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。

不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后才释放资源。

循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。

解决死锁

破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。

破坏请求与保持条件 :⼀次性申请所有的资源。

破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。

synchronized与Lock 区别

synchronized是 java中的关键字,相当于public一样,在jvm的层面,而lock是java的一个类。 synchronized无法判断是否获取锁的状态,lock可以判断是否获取到锁。 Lock 是显式锁 (手动开启和关闭锁 ,别忘关闭锁),synchronized 是隐式锁。 ReentrantLock可重入锁是一种递归无阻塞的同步锁机制,简单意思就是说可重入锁就是当前持有该锁的线程能够多次获取该锁,无需等待。 Lock 只有代码块锁,synchronized 有代码块锁和方法。 synchronized 的锁是可重入、不可中断、非公平的,而Lock 是可重入、可判断、公平以及非公平可以自己设置。 synchronized 适合代码少量的同步问题,Lock 适合大量代码的同步问题。

多线程应用场景

1.线程间有数据共享,并且数据是需要修改的(不同任务间需要大量共享数据或频繁通信时)。

2.提供非均质的服务(任务需要有优先级处理)事件响应有优先级。针对频繁阻塞(休眠或者 I/O 操作)的线程需要设置较 高优先级,而偏重计算(需要较多CPU 时间或者偏运算)的线程则设置较低的 优先级,确保处理器不会被独占。

3.与人有IO交互的应用,可以提供良好的用户体验(键盘鼠标的输入,立刻响应)

响应用户输入的是一个线程,后台程序处理是另外的线程;

线程池概念

线程池可以理解为一个容器,容器里边放着很多的线程,用的时候可以直接从线程池中拿过来一个直接用,用完之后再归还到线程池中。这时候就不需要去创建并维护线程了。 组成部分: 线程池管理器(ThreadPoolManager):用于创建并管理线程池 工作线程(WorkThread): 线程池中线程 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行。 任务队列:用于存放没有处理的任务。提供一种缓冲机制。

①.为什么需要线程池?

创建和销毁线程都需要消耗一定的时间和系统资源,为了节约时间,我们使用线程池来存放线程,线程在使用完毕之后,不会直接销毁,而是存放在线程池,这样,下一次使用的时候就不用创建了。

②.怎么使用线程池?

Executors提供了四个方法来创建线程池: a.newCachedThreadPool(卡湿t嘶ruai的噗):创建一个可缓存线程池,可以灵活回收线程,请求多的时候线程数量会自动变动。 b.newFixedThreadPool(new菲克斯特嘶ruai的噗):创建一个定长线程池 c.newSingleThreadPoolExecutor(森狗嘶ruai的噗一个载ki特):创建一个单线程的线程池,能够保证任务按照指定顺序(FIFO,LIFO,优先级)执行 d.newScheduledThreadPool(筛酒d嘶ruai的噗):创建一个定长线程池,支持定时任务。

合理利用线程池能够带来三个好处。

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池七大核心参数

corePoolSize(靠普塞子):核心线程数 maximumPoolSize(麦克斯麻木普塞子):最大线程数 keepAliveTime(k婆额赖喔太木):空闲线程存活时间 Unit(u泥特):时间单位 workQueue(沃克q):工作队列 threadFactory(嘶ruai的fai克特瑞):线程工厂 handler:拒绝策略

redis的使用场景

A、String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 用于常规key-value缓存应用, 常规计数:微博数,粉丝数等。

B、Hash是一个String类型的field和value的映射表,hash特别适合用于存储对象。存储部分变更的数据,多半用来保存hash结构,如用户信息、购物车等。

C、List就是链表,使用Lists结构,我们可以轻松地实现最新消息排序等功能。List的另一个应用就是消息队列,可以利用List的PUSH操作,将任务存在List中,然后工作线程再用POP操作将任务取出进行执行。Redis还提供了操作List中某一段的API,可以直接查询、删除List中某一段的元素。多半用于需要保证存放顺序的系统。 比如:将Redis用作日志收集器,实际上还是一个队列,多个端点将日志信息写入Redis,然后一个worker统一将所有日志写到磁盘。取最新N个数据的操作,如微博最新信息。

D、Set就是一个集合,集合的概念就是一堆不重复值的组合。利用Redis提供的set数据结构,可以存储一些集合性的数据。set中的元素是没有顺序的,并且不能重复,所以多半用来去重。如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。

E、Sorted set和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列,比如一个存储全班同学成绩的sorted set,其集合value可以是同学的学号,而score就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。可以用sorted set来做带权重的队列,比如普通消息的sc。多半用来取top N

缓存

作为Key-Value形态的内存数据库,Redis 最先会被想到的应用场景便是作为数据缓存。而使用 Redis 缓存数据非常简单,只需要通过string类型将序列化后的对象存起来即可,不过也有一些需要注意的地方:

  • 必须保证不同对象的 key 不会重复,并且使 key 尽量短,一般使用类名(表名)加主键拼接而成。

  • 选择一个优秀的序列化方式也很重要,目的是提高序列化的效率和减少内存占用。

  • 缓存内容与数据库的一致性,这里一般有两种做法:

    1. 只在数据库查询后将对象放入缓存,如果对象发生了修改或删除操作,直接清除对应缓存(或设为过期)。

    2. 在数据库新增和查询后将对象放入缓存,修改后更新缓存,删除后清除对应缓存(或设为过期)。

消息队列

Redis 中list的数据结构实现是双向链表,所以可以非常便捷的应用于消息队列(生产者 / 消费者模型)。消息的生产者只需要通过lpush将消息放入 list,消费者便可以通过rpop取出该消息,并且可以保证消息的有序性。如果需要实现带有优先级的消息队列也可以选择sorted set。而pub/sub功能也可以用作发布者 / 订阅者模型的消息。无论使用何种方式,由于 Redis 拥有持久化功能,也不需要担心由于服务器故障导致消息丢失的情况。

时间轴(Timeline)

list作为双向链表,不光可以作为队列使用。如果将它用作栈便可以成为一个公用的时间轴。当用户发完微博后,都通过lpush将它存放在一个 key 为LATEST_WEIBOlist中,之后便可以通过lrange取出当前最新的微博。

排行榜

使用sorted set和一个计算热度的算法便可以轻松打造一个热度排行榜,zrevrangebyscore可以得到以分数倒序排列的序列,zrank可以得到一个成员在该排行榜的位置(是分数正序排列时的位置,如果要获取倒序排列时的位置需要用zcard-zrank)。

计数器

计数功能应该是最适合 Redis 的使用场景之一了,因为它高频率读写的特征可以完全发挥 Redis 作为内存数据库的高效。在 Redis 的数据结构中,stringhashsorted set都提供了incr方法用于原子性的自增操作,下面举例说明一下它们各自的使用场景:

  • 如果应用需要显示每天的注册用户数,便可以使用string作为计数器,设定一个名为REGISTERED_COUNT_TODAY的 key,并在初始化时给它设置一个到凌晨 0 点的过期时间,每当用户注册成功后便使用incr命令使该 key 增长 1,同时当每天凌晨 0 点后,这个计数器都会因为 key 过期使值清零。

  • 每条微博都有点赞数、评论数、转发数和浏览数四条属性,这时用hash进行计数会更好,将该计数器的 key 设为weibo:weibo_idhash的 field 为like_numbercomment_numberforward_numberview_number,在对应操作后通过hincrby使hash 中的 field 自增。

  • 如果应用有一个发帖排行榜的功能,便选择sorted set吧,将集合的 key 设为POST_RANK。当用户发帖后,使用zincrby将该用户 id 的 score 增长 1。sorted set会重新进行排序,用户所在排行榜的位置也就会得到实时的更新。

好友关系

这个场景最开始是是一篇介绍微博 Redis 应用的 PPT 中看到的,其中提到微博的 Redis 主要是用在在计数和好友关系两方面上,当时对好友关系方面的用法不太了解,后来看到《Redis 设计与实现》中介绍到作者最开始去使用 Redis 便是希望能通过set解决传统数据库无法快速计算集合中交集这个功能。后来联想到微博当前的业务场景,确实能够以这种方式实现,所以姑且猜测一下:

对于一个用户 A,将它的关注和粉丝的用户 id 都存放在两个 set 中:

  • A:follow:存放 A 所有关注的用户 id

  • A:follower:存放 A 所有粉丝的用户 id

    那么通过sinter命令便可以根据A:followA:follower的交集得到与 A 互相关注的用户。当 A 进入另一个用户 B 的主页后,A:followB:follow的交集便是 A 和 B 的共同专注,A:followB:follower的交集便是 A 关注的人也关注了 B。

分布式锁

在 Redis 2.6.12 版本开始,stringset命令增加了三个参数:

  • EX:设置键的过期时间(单位为秒)

  • PX:设置键的过期时间(单位为毫秒)

  • NX | XX:当设置为NX时,仅当 key 存在时才进行操作,设置为XX时,仅当 key 不存在才会进行操作

    由于这个操作是原子性的,可以简单地以此实现一个分布式的锁,例如:

set key "lock" EX 1 XX

如果这个操作返回false,说明 key 的添加不成功,也就是当前有人在占用这把锁。而如果返回true,则说明得了锁,便可以继续进行操作,并且在操作后通过del命令释放掉锁。并且即使程序因为某些原因并没有释放锁,由于设置了过期时间,该锁也会在 1 秒后自动释放,不会影响到其他程序的运行。

倒排索引

倒排索引是构造搜索功能的最常见方式,在 Redis 中也可以通过set进行建立倒排索引,这里以简单的拼音 + 前缀搜索城市功能举例:

假设一个城市北京,通过拼音词库将北京转为beijing,再通过前缀分词将这两个词分为若干个前缀索引,有:北京bbebeijinbeijing。将这些索引分别作为set的 key(例如:index:北)并存储北京的 id,倒排索引便建立好了。接下来只需要在搜索时通过关键词取出对应的set并得到其中的 id 即可。

java反射

1、定义:

反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,

都能够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所

有信息。

这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。

2、哪里会用到反射机制?

jdbc就是典型的反射

Class.forName('com.mysql.jdbc.Driver.class');//加载MySQL的驱动类

这就是反射。如hibernate,struts等框架使用反射实现的。

3、反射的实现方式:

第一步:获取Class对象,有4中方法: 1)Class.forName(“类的路径”); 2)类名.class 3)对象

名.getClass() 4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象

4、实现Java反射的类:

1)Class:表示正在运行的Java应用程序中的类和接口 注意:

所有获取对象的信息都需要Class类

来实现。 2)Field:提供有关类和接口的属性信息,以及对它的动态访问权限。 3)Constructor:

提供关于类的单个构造方法的信息以及它的访问权限 4)Method:提供类或接口中某个方法的信息

5、反射机制的优缺点:

优点: 1)能够运行时动态获取类的实例,提高灵活性; 2)与动态编译结合

缺点: 1)使用反射

性能较低,需要解析字节码,将内存中的对象进行解析。 解决方案: 1、通过setAccessible(true)

关闭JDK的安全检查来提升反射速度; 2、多次创建一个类的实例时,有缓存会快很多 3、

ReflflectASM工具类,通过字节码生成的方式加快反射速度 2)相对不安全,破坏了封装性(因为通

过反射可以获得私有方法和属性)

sql优化

· 查询SQL尽量不要使用select *,而是select具体字段。

只取需要的字段,节省资源、减少网络开销。 select * 进行查询时,很可能就不会使用到覆盖索引了,就会造成回表查询。

· 如果知道查询结果只有一条或者只要最大/最小一条记录,建议用limit 1

加上limit 1后,只要找到了对应的一条记录,就不会继续向下扫描了,效率将会大大提高。 当然,如果name是唯一索引的话,是不必要加上limit 1了,因为limit的存在主要就是为了防止全表扫描,从而提高性能,如果一个语句本身可以预知不用全表扫描,有没有limit ,性能的差别并不大。

· 应尽量避免在where子句中使用or来连接条件

使用or可能会使索引失效,从而全表扫描,降低查询的效率和速度。

· 优化limit分页

我们日常做分页需求时,一般会用 limit 实现,但是当偏移量特别大的时候,查询效率就变得低下。 理由: 当偏移量最大的时候,查询效率就会越低,因为Mysql并非是跳过偏移量直接去取后面的数据,而是先把偏移量+要取的条数,然后再把前面偏移量这一段的数据抛弃掉再返回的。 如果使用优化方案一,返回上次最大查询记录(偏移量),这样可以跳过偏移量,效率提升不少。方案二使用order by+索引,也是可以提高查询效率的。方案三的话,建议跟业务讨论,有没有必要查这么后的分页啦。因为绝大多数用户都不会往后翻太多页。

· 优化你的like语句

把%放前面,并不走索引,把% 放关键字后面,还是会走索引的

· 使用where条件限定要查询的数据,避免返回多余的行

需要什么数据,就去查什么数据,避免返回不必要的数据,节省开销。

· 尽量避免在索引列上使用mysql的内置函数

索引列上使用mysql的内置函数,索引失效

· 应尽量避免在where子句中对字段进行表达式操作,这将导致系统放弃使用索引而进行全表扫

· Inner join 、left join、right join,优先使用Inner join,如果是left join,左边表结果尽量小

都满足SQL需求的前提下,推荐优先使用Inner join(内连接),如果要使用left join,左边表数据结果尽量小,如果有条件的尽量放到左边处理。如果inner join是等值连接,或许返回的行数比较少,所以性能相对会好一点。同理,使用了左连接,左边表数据结果尽量小,条件尽量放到左边处理,意味着返回的行数可能比较少。

· 应尽量避免在where子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。

· 使用联合索引时,注意索引列的顺序,一般遵循最左匹配原则。

当我们创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。 联合索引不满足最左原则,索引一般会失效,但是这个还跟Mysql优化器有关的。

· 对查询进行优化,应考虑在where及order by涉及的列上建立索引,尽量避免全表扫描。

· 慎用distinct关键字

distinct 关键字一般用来过滤重复记录,以返回不重复的记录。在查询一个字段或者很少字段的情况下使用时,给查询带来优化效果。但是在字段很多的时候使用,却会大大降低查询效率。 理由: 带distinct的语句cpu时间和占用时间都高于不带distinct的语句。因为当查询很多字段时,如果使用distinct,数据库引擎就会对数据进行比较,过滤掉重复数据,然而这个比较、过滤的过程会占用系统资源,cpu时间。

· 删除冗余和重复索引

重复的索引需要维护,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能的。

· where子句中考虑使用默认值代替null。

· 不要有超过5个以上的表连接

连表越多,编译的时间和开销也就越大。 把连接表拆开成较小的几个执行,可读性更高。 如果一定需要连接很多表才能得到数据,那么意味着糟糕的设计了。

· 尽量用union all替换 union

如果检索结果中不会有重复的记录,推荐union all 替换 union。

理由: 如果使用union,不管检索结果有没有重复,都会尝试进行合并,然后在输出最终结果前进行排序。如果已知检索结果没有重复记录,使用union all 代替union,这样会提高效率。

· 索引不宜太多,一般5个以内。

理由: 索引并不是越多越好,索引虽然提高了查询的效率,但是也降低了插入和更新的效率。 insert或update时有可能会重建索引,所以建索引需要慎重考虑,视具体情况来定。 一个表的索引数最好不要超过5个,若太多需要考虑一些索引是否没有存在的必要

· 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型

理由: 相对于数字型字段,字符型会降低查询和连接的性能,并会增加存储开销。

· 当在SQL语句中连接多个表时,请使用表的别名,并把别名前缀于每一列上,这样语义更加清晰。

· 尽可能使用varchar/nvarchar 代替 char/nchar。

理由: 因为首先变长字段存储空间小,可以节省存储空间。 其次对于查询来说,在一个相对较小的字段内搜索,效率更高。

· 如果字段类型是字符串,where时一定用引号括起来,否则索引失效

为什么第一条语句未加单引号就不走索引了呢?这是因为不加单引号时,是字符串跟数字的比较,它们类型不匹配,MySQL会做隐式的类型转换,把它们转换为浮点数再做比较。

· 使用explain 分析你SQL的计划

日常开发写SQL的时候,尽量养成一个习惯吧。用explain分析一下你写的SQL,尤其是走不走索引这一块。

 

分库分表的策略

一、背景: 系统刚开始的时候,数据库都是单库单表结构。随着业务量的增加进行第一次数据库升级,根据业务垂直拆分数据库,这样多变成多个业务数据库,每个数据库里面还是单表结构。接下来,继续随着业务量的继续增加,单表已经很难承受数据量,就要进行分表,这个时候就是,多个业务库,每个业务库下对需要分表的表进行分表。再接下来,随着应用的增加,数据库IO,磁盘等等都抗不住了,就要把分表的表分到多个库,这样就形成了如下的结构。

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L292ZWp1cg==,size_16,color_FFFFFF,t_70

二、分库分表的依据--分库分表字段的选择 分库分表首先要确定根据哪个字段、或者哪几个字段进行路由,一般的原则是按使用频率最高维度的字段去分库分表,尽量保证高使用维度下只查询单表。 常用的字段有主键ID,用户ID,时间,商户ID,产品ID,业务类型等等

三、分库分表策略 主要原理:分区、取模、数据路由表

1. 按照时间区间

1)基本原理: 一定区间内时间产生的数据放到一张表里面,多个时间区间的表放到一个库里面

2)简单例子: 单库多表结构,按月分表可以这样,user_201601,user_201602,...,user_201612这种结构。按年分表可以这样,user_2016,user_2017,...这种。

3)多库多表算法: 比如按天分表,每天一张表,当单库超过100张表的时候,进行分库到下一张表。那么假如第一张报表在库BD0,表名是user_20160201。从DB0.user_20160201,..到DB0.user_20160511就100张表了,接下来就要进行分库了,进入20160512,就是DB1.user_20160512,这个算法就是上线的时候定一个上线日期,具体算法如下

库ID = (当前日期 - 上线日期)/ 100 表ID = user_yyyyMMdd 注:好处是可以直接根据时间经过简单计算定位到哪个库和哪个表

还有一种算法: 库ID = (当前日期 - 上线日期)/ 100 表ID = (当前日期 - 上线日期) % 100 表名如下: DB0.user_0001, user_0002,....,user_01000。 注:表名和库名都要经过计算,比较麻烦

4)按月分表,每个月一张表;这种情况,一般就不用分库了,一年12张表说明量也不会特别大,如果量特别大,或者是热点数据,可以一年分一个库,具体算法和上面差不多。

5)按季度分表,基本不用分库。 6)按年分表,肯定不用分库了,没有必要了。

2. 按照主键ID区间 对于自增的主键ID,可以按照ID区间进行分表,以1000万数据量为分界线,对线性的ID进行切割分表,每涨到1000万数据,分到下一张表,超过一定数目的表,进行分库。

库ID = 主键ID / 1000万 / 100 表ID = 主键ID / 1000万 % 100 如:DB0.user_0000,...,DB0.user_0099, DB1.user_0000,...,DB1.user_0099

3. 按照指定字段hash后再取模 如果要取模的字段不是整数型,要先hash后,再通过取模算法,算出在哪个库和那个表。具体算法,参照下面的按用户ID取模。 4. 按照用户ID取模 这里把按照用户ID取模单独拎出来,因为就使用而言,是使用场景最多的情况,很多时候都是用户相关数据量最大,需要分库分表,查询维度更多也是按照用户来查询,所以对用户取模,让同一个用户的数据落到一张表里面,再好不过了。

案例:假设用户ID是整数型的。库数量要分4库,每个库表数量8表,一共32张表。

原理讲解: 一共要分4库,8表,共32张表,也就是1到32的用户ID要平均分配到每张表应该有一条数据,这样就有两种分法。 1) 1到8是第一个库,9到16第二个库,17到24第三个库,25到32是第四个库,每个库里面表的编号都是0到3,这个原则是一个库里面一个一个分,分完再下一个库一个一个分,保证不重复,不漏掉。 2)1,5,9这样每隔4个一个库,2开头隔4个一个库,这个原则是一个库分一个,在分下一个库,一圈走完,再在第一库没分到的表继续分,也保证了不重复,不漏掉原则。

库ID = userId % 库数量4 表ID = userId / 库数量4 % 表数量8

或者

库ID = userId / 表数量4 % 库数量4 表ID = userId % 表数量8

算法图示如下:

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L292ZWp1cg==,size_16,color_FFFFFF,t_70

5. 数据路由表 如果分库分表的算法很复杂,可以通过路由表+程序算法,来存储和计算分库分表规则,不过一般不建议,分库分表搞得太复杂,不便于维护和查询问题

四、各个方案对比 分区算法 优点:线性扩容,平滑扩容,不需要数据迁移 缺点:存在热点数据,非时间维度查询多的情况,聚合复杂 建议:冷数据,用户维度查询少,且数据量大的情况用分区算法

取模算法 优点:同一个热点的数据可以做到一个表里面,查询方便 缺点:扩容不是很方便,需要数据迁移 建议:用户维度查询多,热点维度查询多的情况,建议使用

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值