Java面试

Java集合

HashMap

为什么重写equals还要重写hashCode方法

  1. Object 的 hashcode 方法是本地方法,也就是用 c 或 c++ 实现的,该方法直接返回对象的内存地址,让后再转换整数。

  2. Equals方法

    规定:

    1. 两个对象的Hashcode值相等,但是两个对象的内容值不一定相等;Hash冲突的问题

    2. 两个对象的值Equals比较相等的情况下,则两个对象的Hashcode值一定相等

== 比较两个对象的内存地址是否相同、equals默认的情况下比较两个对象的内存地址

  1. 【强制】关于 hashCode 和 equals 的处理,遵循如下规则:

    1. 只要覆写 equals,就必须覆写 hashCode。
    2. 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两个方法。
    3. 如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals。

    说明:String 已覆写 hashCode 和 equals 方法,所以我们可以愉快地使用 String 对象作为 key 来使用。

HashMap如何避免内存泄漏问题

自定义对象作为key的时候,重写equals和hashCode方法,保证对象的key不重复创建

HashMap与HashTable之间的区别

  1. HashMap实现不同步,线程不安全。 HashTable线程安全 HashMap中的key-value都是存储在Entry中的。

  2. 继承不同。

    Hashtable 继承 Dictionary 类,而 HashMap 继承 AbstractMap

    public class Hashtable extends Dictionary implements Map

    public class HashMap extends AbstractMap implements Map

  3. Hashtable中的方法是同步的,而HashMap中的方法在缺省情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就要自己增加同步处理了。

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

    当 get() 方法返回 null 值时,即可以表示 HashMap 中没有该键,也可以表示该键所对应的值为 null 。因此,在 HashMap 中不能由 get() 方法来判断

    HashMap 中是否存在某个键, 而应该用 containsKey() 方法来判断。

  5. 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值

时间复杂度对比:

  1. o(1) 只需要查询一次 ,hashMap key查询没有发生冲突的时候 数组的下标位置查询
  2. O(N) 需要查询N次, 在集合中根据 内容查找的时候
  3. O(logN) 平方查询,二叉树 红黑树 平衡二叉树

线程池

线程的生命周期

Java中的线程生命周期总共可以分为六种状态:初始化状态(NEW)、可运行/运行状态(RUNNABLE)、阻塞状态(BLOCKED)、无时限等待状态(WAITING)、有时限等待状态(TIMED_WAITING)、终止状态(TERMINATED)。

需要大家重点理解的是:虽然Java语言中线程的状态比较多,但是,其实在操作系统层面,Java线程中的阻塞状态(BLOCKED)、无时限等待状态(WAITING)、有时限等待状态(TIMED_WAITING)都是一种状态,即通用线程生命周期中的休眠状态。也就是说,只要Java中的线程处于这三种状态时,那么,这个线程就没有CPU的使用权。

线程的生命周期

谈谈什么是线程池

线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没有必要的开销。

为什么要使用线程池

因为频繁的开启线程或者停止线程,线程需要从新被cpu从就绪到运行状态调度,需要发生cpu的上下文切换,效率非常低。

线程池是复用机制,提前创建好一些固定的线程数一直在运行状态实现复用,从而可以减少就绪到运行状态的切换。

线程池有哪些作用

核心点:复用机制提前创建好固定的线程一直在运行状态实现复用限制线程创建数量

  1. 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  2. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  4. 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

线程池的创建方式

Executors..new CachedThreadPool():可缓存线程池

Executors..new FixedThreadPool():可定长度

Executors.new ScheduledThreadPool();可定时

Executors.new SingleThreadExecutor();单例

线程池底层是如何实现复用的

本质思想:创建一个线程,不会立马停止或者销毁而是一直实现复用。

  1. 提前创建固定大小的线程一直保持在正在运行状态;(可能会非常消耗cpu的资源)
  2. 当需要线程执行任务,将该任务提交缓存在并发队列中;如果缓存队列满了,则会执行拒绝策略;
  3. 正在运行的线程从并发队列中获取任务执行从而实现多线程复用问题;

ThreadPoolExecutor核心参数有哪些

  • corePoolSize:核心线程数量一直正在保持运行的线程
  • maximumPoolSize:最大线程数,线程池允许创建的最大线程数。
  • keepAliveTime:超出corePoolSize后创建的线程的存活时间。
  • unit:keepAliveTime的时间单位。
  • workQueue:任务队列,用于保存待执行的任务。
  • threadFactory:线程池内部创建线程所用的工厂。
  • handler:任务无法执行时的处理器。

线程池创建的线程会一直在运行状态吗?

不会
例如:配置核心线程数corePoolSize为2、最大线程数maximumPoolSize为5,我们可以通过配置超出corePoolSize核心线程数后创建的线程的存活时间例如为60s,在60s内没有核心线程一直没有任务执行,则会停止该线程。

线程池底层ThreadPoolExecutor底层实现原理

专业术语。

  1. 当线程数小于核心线程数时,创建线程。
  2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
  3. 当线程数大于等于核心线程数,且任务队列已满
    1. 若线程数小于最大线程数,创建线程
    2. 若线程数等于最大线程数,抛出异常,拒绝任务

线程池队列满了,任务会丢失吗

如果队列满了,且任务总数>最大线程数则当前线程走拒绝策略。

可以自定义异拒绝异常,将该任务缓存到Redis、本地文件、mysql中后期项目启动实现补偿。

  1. AbortPolicy丢弃任务,抛运行时异常
  2. CallerRunsPolicy执行任务
  3. DiscardPolicy忽视,什么都不会发生
  4. DiscardOldestPolicy从队列中踢出最先进入队列(最后一个执行)的任务
  5. 实现RejectedExecutionHandler接口,可自定义处理器

线程池拒绝策略类型有哪些呢

  1. AbortPolicy丢弃任务,抛运行时异常
  2. CallerRunsPolicy执行任务
  3. DiscardPolicy忽视,什么都不会发生
  4. DiscardOldestPolicy从队列中踢出最先进入队列(最后一个执行)的任务
  5. 实现RejectedExecutionHandler接口,可自定义处理器

为什么阿里巴巴不建议使用Executors

线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

  1. FixedThreadPoolSingleThreadPool

    允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

  2. CachedThreadPool

    允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

因为默认的Executors线程池底层是基于ThreadPoolExecutor构造函数封装的,采用无界队列存放缓存任务,会无限缓存任务容易发生内存溢出,会导致我们最大线程数会失效。

JUC相关

什么是悲观锁?什么是乐观锁

  • 悲观锁:

    • 站在mysql的角度分析:悲观锁就是比较悲观,当多个线程对同一行数据实现修改的时候,最后只有一个线程才能修改成功,只要谁能够对获取到行锁则其他线程时不能够对该数据做任何修改操作,且是阻塞状态。

    • 站在java锁层面,如果没有获取到锁,则会阻塞等待,后期唤醒的锁的成本就会非常高。

      从新被我们cpu从就绪调度为运行状态。

  • 乐观锁:

    • 乐观锁比较乐观,通过预值或者版本号比较,如果不一致性的情况则通过循环控制修改,当前线程不会被阻塞,是乐观,效率比较高,但是乐观锁比较消耗cpu的资源。

      注意:MySQLInnoDB引擎中存在行锁的概念

公平锁与非公平锁之间的区别

  • 公平锁:
    • 就是比较公平,根据请求锁的顺序排列,先来请求的就先获取锁,后来获取锁就最后获取到,采用队列存放类似于吃饭排队。
  • 非公平锁:
    • 不是根据根据请求的顺序排列,通过争抢的方式获取锁。非公平锁效率是公平锁效率要高,Synchronized是非公平锁

独享锁与共享锁之间的区别

独享锁和共享锁同样是一种概念。我们先介绍一下具体的概念,然后通过ReentrantLockReentrantReadWriteLock的源码来介绍独享锁和共享锁。

独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

CAS(自旋锁)的优缺点

CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

CAS算法涉及到三个操作数:

  • 需要读写的内存值 V。
  • 进行比较的值 A。
  • 要写入的新值 B。

当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。

  1. CAS是通过硬件指令,保证原子性
  2. Java是通过unsafe jni技术
    原子类:AtomicBoolean,AtomicInteger,AtomicLong等使用CAS实现
    • 优点:没有获取到锁的线程,会十直在用户态,不会阻塞,没有锁的线程余一直通过循环控制重试。
    • 缺点:通过死循环控制,消耗cpu资源比较高,需要控制循次数,避免cpu飙高间题

CAS虽然很高效,但是它也存在三大问题,这里也简单说一下:

  1. ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

    • JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
  2. 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。

  3. 只能保证一个共享变量的原子操作

    。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。

    • Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

什么是锁的可重入性

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLocksynchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

CAS锁如何解决ABA的问题

ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

  • JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。

如何利用cas手写一个java锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class AtomicTryLock {

private final AtomicBoolean atomicBoolean = new AtomicBoolean(false);
private static final ExecutorService executor = Executors.newCachedThreadPool();

private Thread lockCurrentThread;

public boolean tryLock() {
if (atomicBoolean.compareAndSet(false, true)) {
lockCurrentThread = Thread.currentThread();
return true;
}
return false;
}

public boolean unLock() {
if (lockCurrentThread != Thread.currentThread()) {
return false;
}
return atomicBoolean.compareAndSet(true, false);
}

public static void main(String[] args) {
AtomicTryLock lock = new AtomicTryLock();
IntStream.range(1, 10).forEach(i -> executor.execute(() -> {
try {
if (lock.tryLock()) {
System.out.println(Thread.currentThread().getName() + "\t获取锁成功~");
}else {
System.out.println(Thread.currentThread().getName() + "\t获取锁失败~");
}
}finally {
lock.unLock();
}
}));
}
}

ThreadLocal

谈谈你对Threadlocal理解?

ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将
变量与线程相绑定。Threadloca适用于在多线程的情况下,可以实现传递数据,实现线程隔离。

Threadlocal基本API

  1. New Threadlocal():–创建Threadlocal
  2. set设置当前线程绑定的局部变量
  3. get获取当前线程绑定的局部变量

Threadlocal与Synchronized区别

Synchronized与Threadlocal都可以实现多线程访问,保证线程安全的问题。

A.Synchronized采用当多个线程竞争到同一个资源的时候,最终只能够有一个线程访问,采用时间换空间的方式,保证线程安全问题

B.Threadlocal在每个线程中都自己独立的局部变量,空间换时间,相互之间都是隔离。

相比来说Threadlocal效率比Synchronized效率更高。

Threadlocal底层实现原理

  1. 在每个线程中都有自己独立的ThreadLocalMap对象,中Entry对象。

  2. 如果当前线程对应的的ThreadLocalMap对象为空的情况下,则创建该ThreadLocalMap对象,并且赋值键值对。

    Key为当前new ThreadLocal对象,value就是为object变量值。

为什么线程缓存的是ThreadlocalMapi对象

ThreadLocalMap可以存放n多个不同的ThreadLocal对象;每个ThreadLocal对象只能缓存一个变量值:

ThreadLocalMap<ThreadLocal对象,value > threadLocalMap

ThreadLocal.get();

threadLocalMap.get(ThreadLocal)–缓存变量值

谈谈强、软、弱、虚引用区别

  • 强引用(StrongReference)

    当内存不足时,VM开始进行GC(垃圾回收),对于强引用对象,就算是出现了OOM也不会对该对象进行回收,死都不会收。

  • 软引用(SoftReference)

    当系统内存充足的时候,不会被回收;当系统内存不足时,它会被回收,软引用通常用在对内存敏感的程序中,比如高速缓存就用到软引用,内存够用时就保留,不够时就回收。

  • 弱引用(WeakReference)

    弱引用需要用到java.lang.ref.WeakReference类来实现,它比软引用的生存周期更短。对于只有弱引用的对象来说,只要有垃圾回收,不管VM的内存空间够不够用,都会回收该对象占用的内存空间。

  • 虚引用(PhantomReference)

    虚引用需要java.lang.ref.PhantomReference类来实现。顾名思义,虚引用就是形同虚设。与其它几种引用不同,虚引用并不会决定对象的声明周期。

Threadlocal为何发内存泄漏问题

因为每个线程中都有自己独立的ThreadLocalMap对象,key为ThreadLocal,value是为变量值。

Key为ThreadLocal作为Entry对象的key,是弱引用,当ThreadLocal指向null的时候,Enty对象中的key变为null,该对象一直无法被垃圾收集机制回收,一直占用到了系统内存,有可能会发生内存泄漏的问题。

AQS

谈谈Lock锁底层实现原理

底层基于AQS+CAS+LockSupport锁实现

CAS+LockSupport+AQS手写Lock锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class Lock {

/**
* 锁的状态 0 === 表示没有线程获取到锁 1 === 表示已经有线程获取到锁
*/
private AtomicInteger lockState = new AtomicInteger(0);

/**
* 当前获取到锁的线程
*/
private Thread exclusiveOwnerThread;

private ConcurrentLinkedDeque<Thread> queue = new ConcurrentLinkedDeque<>();

/**
* 获取锁
*/
public void lock() {
acquire();
}

public boolean acquire() {
for (; ; ) {
System.out.println(Thread.currentThread().getName() + "====>CAS操作");
if (compareAndSet(0, 1)) {
// 获取锁成功
setExclusiveOwnerThread(Thread.currentThread());
System.err.println(Thread.currentThread().getName() + "线程获取锁成功");
return true;
} else {
// 获取锁失败
Thread thread = Thread.currentThread();
queue.add(thread);
// 阻塞
System.out.println(thread.getName() + "被阻塞");
LockSupport.park();
}
}
}

private void setExclusiveOwnerThread(Thread currentThread) {
exclusiveOwnerThread = currentThread;
}


private boolean compareAndSet(int expect, int update) {
return lockState.compareAndSet(expect, update);
}

/**
* 释放锁
*/
public void unlock() {
if (exclusiveOwnerThread == null) {
return;
}

if (exclusiveOwnerThread == Thread.currentThread()) {
if (compareAndSet(1, 0)) {
// 公平锁唤醒链表中阻塞的第一个线程
// Thread queueFirstThread = queue.getFirst();
// System.out.println(queueFirstThread.getName() + "被唤醒");
// LockSupport.unpark(queueFirstThread);

// 非公平锁唤醒
queue.forEach(LockSupport::unpark);
}
}
}

public static void main(String[] args) throws InterruptedException {
Lock lock = new Lock();
lock.lock();
IntStream.range(0, 3).forEach(i -> new Thread(() -> {
System.out.println("第" + i + "个线程:" + Thread.currentThread().getName() + "--->start");
lock.lock();
System.err.println("第" + i + "个线程:" + Thread.currentThread().getName() + "--->end");
}).start());

TimeUnit.SECONDS.sleep(1);
System.err.println(Thread.currentThread().getName() + "线程释放锁");
lock.unlock();
}
}

Semaphore信号量底层原理

  1. Semaphore用于限制可以访问某些资源(物理或逻辑的)的线程数目,他维护了一个许可证集合,有多少资源需要限制就维护多少许可证集合,假如这里有N个资源,那就对应于N个许可证,同一时刻也只能有N个线程访问。一个线程获取许可证就调用acquire方法,用完了释放资源就调用release方法。
  2. 可以简单理解为Semaphore信号量可以实现对接口限流,底层是基于aqs实现

Semaphore简单用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SemaphoreDemo {

public static void main(String[] args) {
Semaphore semaphore = new Semaphore(5);
IntStream.range(0, 10).forEach(i -> new Thread(() -> {
try {
// 获取票据 -1 aqs 锁的状态 -1
semaphore.acquire();
System.out.println(String.format("第%s线程:", i) + Thread.currentThread().getName());
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start());
}

}

Semaphore工作原理

  1. 可以设置Semaphore信号量的状态state值为5
  2. 当一个线程获取到锁的情况下,将state-1,锁释放成功之后state+1;

CountDownLatch底层原理

CountDownLatch源码分析

CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成。和join方法非常类似

CountDownLatch底层是基于AQS实现的

CountDownLatch countDownLatch=new CountDownLatch(2) AQS的state状态为2

调用countDownLatch.countDown();方法的时候状态-l当AQS状态state为0的情况下,则唤醒正在等待的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CountDownLatchDemo {

public static void main(String[] args) throws InterruptedException {
int threadNum = 4;
CountDownLatch latch = new CountDownLatch(threadNum);
IntStream.range(0, threadNum).forEach(i -> new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
latch.countDown();
System.out.println(Thread.currentThread().getName() + "线程执行完毕");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start());

System.out.println("等待其他线程执行完毕");
latch.await();
System.out.println(Thread.currentThread().getName() + "线程执行完毕");
}

}

MQ

你们项目中哪些地方有使用到MQ

  1. 使用MQ异步发送优惠券
  2. 使用MQ异步发送短信
  3. 使用MQ异步扣减库存

耗时的代码操作都可以交给MQ异步实现接口

为什么需要使用MQ

  1. 异步处理(多线程和MQ)
  2. 实现解耦
  3. 流量削峰(MQ可以实现抗高并发)

MQ的缺点

MQ的缺点

kafka、activemq、RabbitMQ、rocketmq都有什么优点和缺点

特性 ActiveMQ RabbitMQ RocketMQ Kafka
单机吞吐量 万级,吞吐量比RocketMQ和Kafka要低了一个数量级 万级,吞吐量比RocketMQ和Kafka要低了一个数量级 10万级,RocketMQ也是可以支撑高吞吐的一种MQ 10万级别,这是kafka最大的优点,就是吞吐量高。
一般配合大数据类的系统来进行实时数据计算、日志采集等场景
topic数量对吞吐量的影响 topic可以达到几百,几千个的级别,吞吐量会有较小幅度的下降
这是RocketMQ的一大优势,在同等机器下,可以支撑大量的topic
topic从几十个到几百个的时候,吞吐量会大幅度下降
所以在同等机器下kafka尽量保证topic数量不要过多。如果要支持大规模的topic,需要增加更多的机器资源
时效性 ms级 微秒级,这是RabbitMQ的一大特点,延迟是最低的 ms级 延迟在ms级以内
可用性 高,基于主从架构实现高可用性 高,基于主从架构实现高可用性 非常高,分布式架构 非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
消息可靠性 有较低的概率丢失数据 经过参数优化配置,可以做到0丢失 经过参数优化配置,消息可以做到0丢失数据
核心特点 MQ领域的功能极其完备 基于erlang开发,所以并发能力很强,性能极其好,延时很低 MQ功能较为完善,还是分布式的,扩展性好 功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准
优劣势总结 非常成熟,功能强大在业内大量的公司以及项目中都有应用
偶尔会有较低概率丢失消息,
而且现在社区以及国内应用都越来越少,官方社区现在对ActiveMQ5.x维护越来越少,
而且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用大规模吞吐的场景中使用
erlang语言开发,性能极其好,延迟很低;
而且开源提供的管理界面非常棒,用起来很好用,在国内一些互联网公司近几年用RabbitMQ也比较多一些
但是问题也是显而易见的RabbitMQ确实吞吐量会低一些,这是因为他做的实现机制比较重。而且erlang开发,国内有几个公司有实力做erlang源码级别的研究和定制?而且RabbitMQ集群动态扩展会很麻烦,不过这个我觉得还好。其实主要是erlang语言本身带来的问题。很难读源码,很难定制和拿控。
接口简单易用,而且毕竟在阿里大规模应用过,有阿里品牌保障
日处理消息上百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都是k的,还可以支挥大规模的topic数量,支持复杂MQ业务场景
而且一个很大的优势在于,阿里出品都是java系的,我们可以自己阅读源码,定制自己公司的MQ,可以拿控
社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准JMS规范走的有些系统要迁移需要修改大量代码
还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用RocketMQ挺好的
kafka的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展
同时kafka最好是支撑较少的topic数量即可,保证其超高吞吐量
而且kafka唯的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个
特性天然适合大数据实时计算以及日志收集

MQ的高可用

RabbitMQ的高可用性

RabbitMQ是比较有代表性的,因伪是基于主从做高可用性的,我们就以他为例子讲解第一种MQ的高可用性怎么实现。

RabbitMQ有三种模式:单机模式,普通集群模式,镜像集群模式

  1. 单机模式就是demo级别的,一般就是你本地启动了玩玩儿的,没人生产用单机模式

  2. 普通集群模式意思就是在多台机器上启动多个RabbitMQ实例,每个机器启动一个。但是你创建的queue,只会放在一个RabbitMQ实例上,但是每个实例都同步queue的元数据。完了你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从queue所在实例上拉取数据过来。这种方式确实很麻烦,也不怎么好,没做到所谓的分布式,就是个普通集群。因为这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个queue所在实例消费数据,前者有数据拉取的开销,后者导致单实例性能瓶颈。

    而且如果那个放queue的实例宕机了,会导致接下来其他实例就无法从那个实例拉取,如果你开启了消息持久化,让RabbitMQ落地存储消息的话,消息不一定会丢,得等这个实例恢复了,然后才可以继续从这个queue拉取数据。

    所以这个事儿就比较尴尬了,这就没有什么所谓的高可用性可言了,这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个queue的读写操作。

    RabbitMQ普通集群模式

  3. 镜像集群模式

    这种模式,才是所谓的RabbitMQ的高可用模式,跟普通集群模式不一样的是,你创建的queue,无论元数据还是queue里的消息都会存在于多个实例上,然后每次你写消息到queue的时候,都会自动把消息到多个实例的queue里进行消息同步。

    这样的话,好处在于,你任何一个机器宕机了,没事儿,别的机器都可以用。坏处在于,第一,这个性能开销也太大了吧,消息同步所有机器,导致网路带宽压力和消耗很重!第二,这么玩儿,就没有扩展性可言了,如果某个queue负载很重,你加机器,新增的机器也包含了这个queue的所有数据,并没有办法线性扩展你的queue

    那么怎么开启这个镜像集群模式呢?我这里简单说一下,避免面试人家问你你不知道,其实很简单RabbitMQ有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候可以要求数据同步到所有节点的,也可以要求就同步到指定数量的节点,然后你再次创建quue的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。

    RabbitMQ镜像集群模式

kafka的高可用性

kafka一个最基本的架构认识:多个broker组成,每个broker是一个节点;你创建一个topic,这个topic?可以划分为多个partition,每个partition可以存在于不同的broker.上,每个partition就放一部分数据。

这就是天然的分布式消息队列,就是说一个topc的数据,是分散放在多个机器上的,每个机器就放一部分数据。

实际上RabbitMQ之类的,并不是分布式消息队列,他就是传统的消息队列,只不过提供了一些集群、HA的机制而已,因为无论怎么玩儿,RabbitMQ一个queue的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个queue的完整数据。

kafka0.8以前,是没有HA机制的,就是任何一个broker宕机了,那个broker上的partition就废了,没法写也没法读,没有什么高可用性可言。

kafka 0.8以后,提供了HA机制,就是replica副本机制。每个partition的数据者都会同步到吉他机器上,形成自己的修个replica副本。然后所有replica会选举一个leader出来,那么生产和消费都跟这个leader打交道,然后其他replica就是follower。写的时候,leader会负责把数据同步到所有follower上去,读的时候就直接读leader上数据即可。只能读写leader?很简单,要是你可以随意读写每个follower,那么就要care数据一致性的问题,系统复杂度太高,很容易出问题。kafka会均匀的将一个partition的所有replica分布在不同的机器上,这样才可以提高容错性。

这么搞,就有所谓的高可用性了,因为如果某个broker宕机了,没事儿,那个broker.上面的partition在其他机器上都有副本的,如果这上面有某个partition的leader,那么此时会重新选举一个新的leader出来,大家续读写那个新的leader即可。

这就有所谓的高可用性了。写数据的时候,生产者就写leader,然后leader将数据落地写本地弦盘,接看其他follower自己主动从leader来pull数据,一旦所有follower同步好数据了,就会发送ack给leader,leader收到所有follower的ack之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)

消费的时候,只会从leader去读,但是只有一个消息已经被所有follower都同步成功返回ack的时候,这个消息才会被消费者读到。

Kafka高可用架构

如何保证消息不被重复消费啊(如何保证消息消费时的幂等性)?

给你举个例子吧。假设你有个系统,消费一条往数据库里插入一条,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下已经消费过了,直接扔了,不就保留了一条数据?

幂等性,我通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错。

  1. 比如你写个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update一下好吧
  2. 比如你是写Redis,那没问题了,反正每次都是set,天然幂等性
  3. 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的id,类似订单id之类的东西,然后你这里消费到了之后,先根据这个id去比如Redis里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个id写Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
  4. 数据库唯一约束

kafka消费端可能出现的重复消费问题件

如何保证消息的可靠性传输(如问处理消息丢失的问题)?

RabbitMQ可能存在数据丢失的问题

生产者:

生产者将数据发送到RabbitMQ的时候,可能数据就在半路给搞丢了,因为网络啥的问题,都有可能。

此时可以选择用RabbitMQ提供的事务功能,就是生产者发送数据之前开启RabbitMQ事务(channel.txSelect),然后发送消息,如果消息没有成功被RabbitMQ接收到,那么生产者会收到异常报错,此时就可以回滚事务(channel.txRollback),然后重试发送消息;如果收到了消息,那么可以提交事务(channel..txCommit)。但是问题是,RabbitMQ事务机制一搞,基本上吞吐量会下来,因为太耗性能。

所以一般来说,如果你要确保说写RabbitMQ的消息别丢,可以开启confirm模式,在生产者那里设置开启confirm模式之后,你每次写的消息都会分配一个唯一的id,然后如果写入了RabbitMQ中,RabbitMQ会给你回传一个ack消息,告诉你说这个消息ok了。如果RabbitMQ没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。

事务机制和confirm机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息RabbitMQ接收了之后会异步回调你一个接口通知你这个消息接收到了。

所以一般在生产者这块避免数据丢失,都是用confirm机制的。

RabbitMQ弄丢了数据

就是RabbitMQ自己弄丢了数据,这个你必须开启RabbitMQ的持久化,就是消息写入之后会持久化到磁盘,哪怕是RabbitMQ自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,RabbitMQ还没持久化,自己就挂了,可能导致少量数据会丢失的,但是这个概率较小。

设置持久化有两个步骤,第一个是创建queue的时候将其设置为持久化的,这样就可以保证RabbitMQ持久化queue的元数据,但是不会持久化queue里的数据;第二个是发送消息的时候将消息的deliveryMode设置为2,就是将消息设置为持久化的,此时RabbitMQ就会将消息持久化到滋盘上去。必须要同时设置这两个持久化才行。而且持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到兹盘之前,RabbitMQ挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。

哪怕是你给RabbitMQ开启了持久化机制,也有一种可能,就是这个消息写到了RabbitMQ中,但是还没来得及持久化到滋盘上,结果不巧,此时RabbitMQ挂了,就会导致内存里的一点点数据会丢失。

消费端弄丢了数据

RabbitMQ如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,RabbitMQ认为你都消费了,这数据就丢了。

这个时候得用RabbitMQ提供的ack机制,简单来说,就是你关闭RabbitMQ自动ack,可以通过一个api来调用就行,然后每次你自己代码里确保处理完的时候,再程序里ack一把。这样的话,如果你还没处理完,不就没有ack?那RabbitMQ就认为你还没处理完,这个时候RabbitMQ会把这个消费分配给别的consumer去处理,消息是不会丢的。

MQ如何保证消息的顺序性?

RabbitMQ消息顺序性

RabbitMQ: 拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点;或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理

MQ如何解决消息积压

有几百万消息持续积压几小时,说说怎么解决?

一般这个时候,只能操作临时紧急扩容了,具体操作步骤和思路如下:

  1. 先修复consumer的问题,确保其恢复消费速度,然后将现有consumer都停掉
  2. 新建一个topic,partition是原来的10倍,临时建立好原先10倍或者20倍的queue数量
  3. 然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue
  4. 接看临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据
  5. 这种做法相当于是临时将queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据
  6. 等快速消费完积压数据之后,得恢复原先部署架构,重新用原先的consumer机器来消费消息

如阿解决消息队列的延时以及过期失效问题?

假设你用的是RabbitMQ,RabbitMQ是可以设置过期时间的,就是TTL,如果消息在queue中积压超过一定的时间就会被RabbitMQ给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在mq里,而是大量的数据会直接搞丢。

这个情况下,就不是说要增加consumer消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户睡觉了。

这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入mq里面去,把白天丢的数据给他补回来。也只能是这样了。

消息队列满了以后该怎么处理?

如果走的方式是消息积压在mq里,那么如果你很长时间都没处理掉,此时导致mq都快写满了,咋动?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。

如何设计一个消息中间件

  1. 首先这个mq得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下kafka的设计理念,broker>topic>partition,每个partition放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给topic增加partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的香吐量了?
  2. 其次你得考虑一下这个mq的数据要不要落地磁盘吧?那肯定要了,落滋盘,才能保证别进程挂了数据就丢了。那落滋盘的时候怎么落啊?顺序写,这样就没有磁滋盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是kafka的思路。
  3. 其次你考虑一下你的q的可用性啊?这个事儿,具体参考我们之前可用性那个环节讲解的kafka的高可用保障机制。多副本>leader&follower>broker挂了重新选举leader即可对外服务。
  4. 能不能支持数据0丢失啊?可以的,参考我们之前说的那个kafka数据零丢失方案

其实一个mq肯定是很复杂的,面试官问你这个问题,其实是个开放题,他就是看看你有没有从架构角度整体构思和设计的思维以及能力。确实这个问题可以刷掉一大批人,因为大部分人平时不思考这些东西。

ElasticSearch

es的分布式架构原理能说一下么(es是如何实现分布式的啊)?

elasticsearch设计的理念就是分布式搜索引擎,底层其实还是基于lucene的。

核心思想就是在多台机器上启动多个es进程实例,组成了一个es集群。

接看你搞一个索引,这个索引可以拆分成多个shard,,每个shrd存储分数据。

接着就是这个shard的数据实际是有多个备份,就是说每个shard都有一个primary shard,负责写入数据,但是还有几个replica shardprimary shard写入数据之后,会将数据同步到其他几个replica shard上去。

通过这个replica的方案,每个shard的数据都有多个备份,如果某个机器宕机了,没关系啊,还有别的数据副本在别的机器上呢。高可用了吧。

es集群多个节点,会自动选举一个节点为master节点,这个master节点其实就是干一些管理的工作的,比如维护索引元数据拉,负责切换primary shard和replica shard身份拉,之类的。

要是master节点宕机了,那么会重新选举一个节点为master节点。

如果是非master节点宕机了,那么会由master节点,让那个宕机节点上的primary shard的身份转移到其他机器上的replica shard。急着你要是修复了那个宕机机器,重启了之后,master节点会控制将缺失的replica shard分配过去,同步后续修改的数据之类的,让集群恢复正常。

其实上述就是elasticsearch作为一个分布式搜索引擎最基本的一个架构设计

ElasticSearch分布式架构原理

MySQL索引优化

MyISAM与InnoDb之间区别

  1. InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

  2. InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;

  3. InnoDB 是聚集索引,MyISAM 是非聚集索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。

  4. InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;

  5. InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

InnoDB默认的主键索引,自己没有指定的话,则会默认使用mysql自带rowid为主键索引。

如何选择:

  1. 是否要支持事务,如果要请选择 InnoDB,如果不需要可以考虑 MyISAM;

  2. 如果表中绝大多数都只是读查询,可以考虑 MyISAM,如果既有读写也挺频繁,请使用InnoDB。

  3. 系统奔溃后,MyISAM恢复起来更困难,能否接受,不能接受就选 InnoDB;

  4. MySQL5.5版本开始Innodb已经成为Mysql的默认引擎(之前是MyISAM),说明其优势是有目共睹的。如果你不知道用什么存储引擎,那就用InnoDB,至少不会差。

为什么InnoDb引擎表必须有主键,并且推荐使用整型的自增方式?

1、如果设置了主键,那么InnoDB会选择主键作为聚集索引、如果没有显式定义主键,则InnoDB会选择第一个不包含有NULL值的唯一索引作为主键索引、如果也没有这样的唯一索引,则InnoDB会选择内置6字节长的ROWID作为隐含的聚集索引(ROWID随着行记录的写入而主键递增)。

2、如果表使用自增主键
那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,主键的顺序按照数据记录的插入顺序排列,自动有序。当一页写满,就会自动开辟一个新的页

3、如果使用非自增主键(如果身份证号或学号等)
由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置,此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。

EXPLAIN Type需要达到什么级别

依次从最优到最差分别为:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

  • **const, system**:

    mysql能对查询的某部分进行优化并将其转化成一个常量(可以看show warnings 的结果)。用于 primary key 或 unique key 的所有列与常数比较时,所以表最多有一个匹配行,读取1次,速度比较快。

  • eq_ref

    primary key 或 unique key 索引的所有部分被连接使用 ,最多只会返回一条符合条件的记录。这可能是在 const 之外最好的联接类型了,简单的 select 查询不会出现这种 type。

  • **ref**:

    相比 eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行。

  • **range**:

    范围扫描通常出现在 in(), between ,> ,<, >= 等操作中。使用一个索引来检索给定范围的行。

  • **index**:

    和ALL一样,不同就是mysql只需扫描索引树,这通常比ALL快一些。

  • **ALL**:

    即全表扫描,意味着mysql需要从头到尾去查找所需要的行。通常情况下这需要增加索引来进行优化了

MySQL索引为什么需要遵循遵循最佳左前缀法则

索引的数据结构是根据联合索引的顺序存储的

MySQL索引为什么需要避免回表查询

利用覆盖索引来进行查询操作,避免回表。

说明:如果一本书需要知道第 11 章是什么标题,会翻开第 11 章对应的那一页吗?目录浏览一下就好,这 个目录就是起到覆盖索引的作用。

正例:能够建立索引的种类分为主键索引、唯一索引、普通索引三种,而覆盖索引只是一种查询的一种效 果,用 explain 的结果,extra 列会出现:using index。

超过多少张表需要表禁止join

超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时, 保证被关联的字段需要有索引。

说明:即使双表 join 也要注意表索引、SQL 性能。

为什么阿里巴巴需要禁止存储过程

禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。

一张表达到多少级别需要分表分库

单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。

说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。

MySQL与Redis数据一致性

MySQL与Redis如何保证数据一致性

  1. 更新mysql数据,在手动清除Redis缓存,在重新查询最新的数据同步到Redis中
  2. 更新mysql数据,在采用mg异步的形式同步数据到Redis中;
  3. 基于订阅mysql binlog采用mq异步的形式将数据同步到Redis中;
  4. 延迟双删策略

MySQL与Redis同步数据是否存在延迟呢?

数据同步过程中,会存在短暂的延迟,这属于正常的现象。

  • 弱一致性:

    主从之间数据允许不一致性;

  • 强一致性:

    主从之间数据必须一致性;如果实现成本是非常高,会设计到一些锁的技术,

  • 最终一致性:

    短暂的数据延迟是允许的,但是最终数据是需要一致;

    在分布式中做数据同步需要经过网络传输的,网络传输数据需要一定的时间,所以数据短暂的延迟是允许的,但是最终数据一定达成一致。延迟是很难避免的,优化减少延迟的时间。公司中数据同步延迟优化在10-30毫秒。

Redis

为什么Redis是单线程的但是还可以支撑高并发?

Redis线程模型

  1. 文件事件处理器

    Redis基于reactor模式开发了网铬事件处理器,这个处理器叫做文件事件处理器,file event handler。这个文件事件处理器采用Io多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器来处理这个事件。

    如果被监听的socket准备好执行accept、read、write、close等操作的时候,跟操作对应的文件事件就会产生,这个时候文件事件处理器就会调用之前关联好的事件处理器来处理这个事件。

    文件事件处理器是单线程模式运行的,但是通过1O多路复用机制监听多个socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了Redis内部的线程模型的简单性。

    文件事件处理器的结构包含4个部分:多个scoket, IO多路复用程序,文件事件分派器,事件处理器(命令请求处理器、命令回复处理器、连接应答处理器,等等)。

    多个socket可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个socket,但是会将socket放入一个队列中排队,每次从队列中取出一个socket给事件分派器,事件分派器把socket给对应的事件处理器。

    然后一个socket的事件处理完之后,IO多路复用程序才会将队列中的下一个socket给事件分派器。文件事件分派器会根据每个socket当前产生的事件,来选择对应的事件处理器来处理

  2. 文件事件

    当socket变得可读时(比如客户端对Redis执行wite操作,或者close操作),或者有新的可以应答的sccket出现时(客户端对Redis执行connect操作),socket就会产生一个
    AE READABLE事件。

    当socket变得可写的时候(客户端对Redis执行read操作),socket会产生一个AE_WRITABLE事件。

    IO多路复用程序可以同时监听AE_REABLE和AE_WRITABLE两种事件,要是一个sockt同时产生了AE READABLE和AE WRITABLE两种事件,那么文件事件分派器优先处理AE REABLE事件,然后才是AE_WRITABLE事件。

  3. 文件事件处理器

    如果是客户端要连接Redis,那么会为socket关联应答处理器

    如果是客户端要写数据到Redis,那么会为socket关联命令请求处理器

    如果是客户端要从Redis读数据,那么会为socket关联命令回复处理器

  4. 客户端与Redis通信的一次流程

    在Redis启动初始化的时候,Redis会将连接应答处理器跟AE_READABLE事件关联起来,接着如果一个客户端跟Redis发起连接,此时会产生一个AE_READABLE事件,然后由连接应答处理器来处理跟客户端建立连接,创建客户端对应的socket,.同时将这个socket的AE_READABLE
    事件跟命令请求处理器关联起来。

    当客户端向Redis发起请求的时候(不管是读请求还是写请求,都一样),首先就会在socket产生一个AE_READABLE事件,然后由对应的命令请求处理器来处理。这个命令请求处理器就会从socket中读取请求相关数据,然后进行执行和处理。

    接着Redis这边准备好了给客户端的响应数据之后,就会将socket的AE_WRITABLE事件跟命令回复处理器关联起来,当客户端这边准备好读取响应数据时,就会在sockt上产生一个AE_WRITABLE事件,会由对应的命令回复处理器来处理,就是将准备好的响应数据写入socket,供客户端来读取。

    命令回复处理器写完之后,就会删除这个socket的AE WRITABLE事件和命令回复处理器的关联关系。

为啥Redis单线程棋型也能效率这么高?

  1. 纯内存操作
  2. 核心是基于非阻塞的多路复用机制
  3. 单线程反而避免了多线程的频敏上下文切换问题

Redis单线程模型

Redis者都有哪些数据类型?分别在那些场景下使用比较合适?

  • string:这是最基本的类型了,没啥可说的,就是普通的st和gt,做简单的kw缓存

  • hash:这个是类似map的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在Redis里,然后每次读写缓存的时候,可以就操作hash里的某个字段。

  • list:有序列表,这个是可以玩儿出很多花样的

    比如可以通过list存储一些列表型的数据结构,类似粉丝列表了、文章的评论列表了之类的东西

    比如可以通过Irange命令,就是从某个元素开始读取多少个元素,可以基于Iist实现分页查询,这个很棒的一个功能,基于Redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走

    比如可以搞个简单的消息队列,从list头怼进去,从list尾巴那里弄出来

  • set:无序集合,自动去重

    直接基于set将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于jvm内存里的HashSet进行去重,但是如果你的某个系统部曙在多台机器上呢?得基于Redis进行全局的set去重

    可以基于set玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?对吧把两个主播的粉丝都放在两个set中,对两个set做交集

  • sorted set:排序的set,去重但是可以排序,写进去的时候给一个分数,自动根据分数排序,这个可以玩儿很多的花样,最大的特点是有个分数可以自定义排序规则

    比如说你要是想根据时间对数据排序,那么可以写入进去的时候用某个时间作为分数,人家自动给你按照时间排序了

    排行榜:将每个用户以及其对应的什么分数写入进去,zadd board score username,

    1
    2
    3
    4
    zadd board 85 Jobs
    zadd board 72 Jerry
    zadd board 96 Walking
    zadd board 62 Tom

    接看zrevrange board 0 99,就可以获取排名前100的用户;zrank board username,可以看到用户在排行榜里的排名

Redis过期策略

如果假设你设置一个一批key只能存活1个小时,那么接下来1小时后,Redis是怎么对这批key进行删除的?

答案是:定期删除+惰性删除

所谓定期删除,指的是Redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就册除。假设Redis里放了10万个key,都设置了过期时间,你每隔几百毫秒,就检查10万个key,那Redis基本上就死了,cpu负载会很高的,消耗在你的检查过期key上了。注意,这里可不是每隔100ms就遍历所有的设置过期时间的key,那样
就是一场性能上的灾难。实际上Redis是每隔100ms随机抽取一些key来检查和删除的。

但是问题是,定期删除可能会导致很多过期ky到了时间并没有被册除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某key的时候,Redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会册除,不会给你返回任何东西。

并不是key到时间就被删除掉,而是你查询这个key的时候,Redis再懒情的检查一下

通过上述两种手段结合起来,保证过期的key一定会被干掉。

但是实际上这还是有问题的,如果定期册除漏掉了很多过期key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致Redis内存块耗尽了,咋整?

答案是:走内存淘汰机制。

如果Redis的内存占用过多的时候,此时会进行内存淘汰,有如下一些策略:

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的ky给干掉啊
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除

怎么保证Redis是高并发以及高可用的?

  1. Redis高并发跟整个系统的高并发之问的关系

    Redis,你要搞高并发的话,不可避免,要把底层的缓存搞得很好

    mysql,高并发,儆到了,那么也是通过一系列复杂的分岸分表,订单系统,事务要求的,QPS到几万,比较高了,要做一些电商的商品详情贝,真正的超高并发,QPS上十万,甚至是百万,一秒钟百万的请求量

    光是Redis,是不够的,但是reids是整个大型的缓存架构中,支撑高并发的架构里面,非常重要的一个环节

    首先,你的底层的级容中同件,领存系统。必须能等支裤的起我们说的部种高并发,其次。月经过良好的整体的饭存架构的设计(多级级容架构、热点级存),支排真正的上十万,至上百万的高

  2. Redis?不能支球高并发的瓶颈在哪里?

    单机

  3. 如果Redis要支撑超过10万+的并发,那应该怎么做?

    单机的Redis几乎不太可能说QPS超过10万+,除非一些特殊情况,比如你的机器性能特别好,配置特别高,物理机,能护做的特别好,而且你的整体的操作不是太复杂单机在几万

    读写分离,一般来说,对缓存,一班都是用来支撑读高并发的,写的请求是比较少的,可能写请求也就一秒钟几千,一两千,大量的请求都是读,一秒钟二十万次读

    架构做成注从架构,一主多从,主负责写,并目将数据同步复制到其他的slave节点,从节点负责读.所有的读请求全部走从节点。

    水平扩容,就是说,如果你的读QPS再增加,也很简单,继续增加Redis slave就可以了

Redis replication以及master持久化对主从架构的安全意义?

Redis replication->主从架构->读写分离->水平扩容支撑读高并发

1、图解Redis Replication基本原理

Redis Replication基本原理

2、Redis replication的核心机制

  1. Redis采用异步方式复制数据到slave节点,不过Redis2.8开始,slave node会周期性地确认自已每次复制的数据量
  2. 一个master node是可以配置多个slave node的
  3. slave node也可以连接其他的slave node
  4. slave node做复制的时候,是不会block master node的正常工作的
  5. slave node在做复制的时保,也不会block对自已的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服了
  6. slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量

3、master持久化对于主从架构的安全保障的意义

如果采用了主从架构,那么建议必须开启master nodel的持久化!

不建议用slave node作为master node的数据热备,因为那样的话,如果你关掉master的持久化,可能在master宕机重启的时候数据是空的,然后可能一经过复制,salve node数据也丢了

即使采用了后续讲解的高可用机制,slave node可以自动接管master node,但是也可能sentinal还没有检测到master failure, master node就自动重启了,还是可能导致上面的所有slave node数据清空故障

Redis主从复制原理、断点续传、无磁盘化复制、过期key处理

1、主从架构的核心原理

当启动一个slave node的时候,它会发送一个PSYNC命令给master node

如果这是slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据;否则如果是slave node第一次连接master node,那么会触发一次full resynchronization

开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。

slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。

2、主从复制的断点续传

从Redis2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份

master node会在内存中常见一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的,如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制

但是如果没有找到对应的offset,那么就会执行一次resynchronization

3、无磁盘化复制

master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了

repl-diskless-sync

repl-diskless-sync-delay,等待一定时长再开始复制,因为要等更多slave重新连接过来

4、过期key处理

slave不会过期key,只会等待master过期key,如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave.

Redis Replication的完整流运行程和原理

复制的完整流程

  1. slave node启动,仅仅保存master node的信息,包括master node的host和ip,但是复制流程没开始
  2. slave nodep内部有个定时任务,每秒检查是否有新的master node要连接和复制,如果发现,就跟master node建立socket网络连接
    slave node发送ping命令给master node
  3. 口令认证,如果masteri设置了requirepass,那么salve node必须发送masterauth的口令过去进行认证
  4. master node第一次执行全量复制,将所有数据发给slave node
  5. master node,后续持续将写命令,异步复制给slave node

复制的完整流程

数据同步相关的核心机制

  1. master和slave都会维护一个offset

    master会在自身不断系加offset,slave也会在自身不断系加offset

    slave每秒都会上报自己的offset给master,同时master.也会保存每个slave的offset

  2. backlog

    master node有一个backlog,默认是1MB大小

    master node给slave node,复制数据时,也会将数据在backlog中同步写一份

  3. master run id

    info server,可以看到master run id

    如果根据host+ip定位master node,是不靠谱的,如果master node,重启或者数据出现了变化,那么slave node应该根据不同的run id区分,run id不同就做全量复制

    如果需要不更改run id重启Redis,可以使用Redis-cli debug reload命令

    master run id的作用

  4. psync

    从节点使用psync.从master nodei进行复制,psync runid offset

    master node会根据自身的情况返回响应信息,可能是FULLRESYNC runid offset触发全量复制,可能是CONTINUE触发增量复制

全量复制

  1. master执行bgsave,在本地生成一份rdb快照文件
  2. master node将rdb快照文件发送给salve node,如果rdb复制时间超过60秒(repl-timeout),那么slave nodei就会认为复制失败,可以适当调节大这个参数
  3. 对于千兆网卡的机器,一般每秒传输100MB,6G文件,很可能超过60s
  4. master node在生成rdb时,会将所有新的写命令缓存在内存中,在salve nodef保存了rdb之后,再将新的写命令复制给salve node
  5. client-output-buffer-limit slave 256MB 64MB 60,如果在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败
  6. slave node接收到rdb之后,清空自己的旧数据,然后重新加载rdb到自己的内存中,同时基于旧的数据版本对外提供服务
  7. 如果slave node开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF

rdb生成、rdb通过网络拷贝、slave旧数据的消理、slave aof rewrite,很耗费时间

如果复制的数据量在4G~6G之间,那么很可能全量复制时间消耗到1分半到2分钟

增量复制

  1. 如果全量复制过程中,master-slave网络连接断掉,那么salve重新连接master时,会触发增量复制
  2. master直接从自己的backlog中获取部分丢失的数据,发送给slave node,默认backlog就是1MB
  3. msater就是根据slave发送的psync中的offset来从backlog中获取数据的

heartbeat

主从节点互相都会发送heartbeat信息

master默认每隔10秒发送一次heartbeat,salve node每隔1秒发送一个heartbeat

异步复制

master每次接收到写命令之后,现在内部写入数据,然后异步发送给slave node

Redis主从架构下如何才能做到99.99%的高可用性?

哨兵模式,主从切换

讲的学术,99.99%,公式,系统可用的时间/系统故障的时间,365天,在365天*99.99%的时间内,你的系统都是可以对外提供服务的那就是高可用性,99.99%系统可用的时间/总的时间=高可用性,然后会对各种时间的概念,说一大堆解释

指标 计算结果 备注说明
1个9: 90% (1-90%)* 365 =36.5天 系统在1年内服务中断的时间≤36.5d
2个9: 99% (1-99%)* 365 =3.65天 系统在1年内服务中断的时间≤3.65d
3个9: 99.9% (1-99.9%)* 365* 24 =8.76小时 系统在1年内服务中断的时间≤8.76H
4个9: 99.99% (1-99.99%)* 365* 24 =52.6分钟 系统在1年内服务中断的时间≤52.6min
5个9: 99.999% (1-99.999%)* 365* 24* 60 =5.26分钟 系统在1年内服务中断的时间≤5.26min
6个9: 99.9999% (1-99.9999%)* 365* 24* 60* 60 =32秒 系统在1年内服务中断的时间≤32s

Redis哨兵架构的相关基础知识的讲解

1、哨兵的介绍

sentinal,中文名是哨兵

哨兵是Redis集群架构中非常重要的一个组件,主要功能如下

  1. 集群监控,负责监控Redis master和slave进程是否正常工作
  2. 消息通知,如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
  3. 故障转移,如果master node挂掉了,会自动转移到slave node上
  4. 配置中心,如果故障转移发生了,通知client客户端新的master地址

哨兵本身也是分布式的,作为一个哨兵集群去运行,互相协同工作

  1. 故障转移时,判断一个master node,是宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题
  2. 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了

目前采用的是sentinal 2版本,sentinal2相对于sentinal 1来说,重写了很多代码,主要是让故障转移的机制和算法变得更加健壮和简单

2、哨兵的核心知识

  1. 哨兵至少需要3个实例,来保证自己的健壮性
  2. 哨兵+Redis主从的部署架构,是不会保证数据零丢失的,只能保证Redis集群的高可用性
  3. 对于哨兵+Redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练

3、为什么Redis哨兵集群只有2个节点无法正常工作?

哨兵集群必须部署2个以上节点

如果哨兵集群仅仅部署了个2个哨兵实例,quorum=

+—-+ +—-+

| M1 |—— | R2 |

| S1 | | S2 |

+—-+ +—-+

Configuration: quorum 1

master宕机,s1和s2中只要有1个哨兵认为naster:宕机就可以还行切换,同时s1和s2中会选举出一个哨兵来执行故障转移

同时这个时候,需要majority,也就是大多数哨兵都是运行的,2个哨兵的majority就是2,2个哨兵都运行着,就可以允许执行故障转移

但是如果整个M1和S1运行的机器宕机了,那么哨兵只有1个了,此时就没有majority来允许执行故障转移,虽然另外一台机器还有一个R1,但是故障转移不会执行

4、经典的3节点哨兵集群

​ +—-+

​ | M1 |

​ | S1 |

​ +—–+

​ |

+—-+ | +—-+

| M2 |—-+— | R3 |

| S2 | | S3 |

+—-+ +—-+

Configuration: quorum 2

如果M1所在机器宕机了,那么三个哨兵还剩下2个,S2和s3可以一致认为master宕机,然后选举出一个来执行故障转移

同时3个哨兵的majority是2,所以还剩下的2个哨兵运行着,就可以允许执行故障转移

Redis哨兵主备切换的数据丢失问题:异步复制、集群脑裂

1、两种数据丢失的情况

主备切换的过程,可能会导致数据丢失

  1. 异步复制导致的数据丢失

    因为master->slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失

  2. 脑裂导致的数据丢失

    脑裂,也就是说,某个master所在机器突然脱离了正常的网络,跟其他slavei机器不能连接,但是实际上master还运行着

    此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master

    这个时候,集群里就会有两个master,也就是所谓的脑裂

    此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向I旧master的数据可能也丢失了

    因此I旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据

2、解决异步复制和脑裂导致的数据丢失

min-slaves-to-write 1
min-slaves-max-log 10

要求至少有1个slave,数据复制和同步的延迟不能超过10秒

如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候,master就不会再接收任何请求了

上面两个配置可以减少异步复制和脑裂导致的数据丢失

  1. 减少异步复制的数据丢失

    有了min-slaves-max-log这个配置,就可以确保说,一旦slave复制数据和ack延时太长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机由于部分数据未同步到slave导致的数据丢失降低的可控范围内

  2. 减少脑裂的数据丢失

    如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave发送数据,而且slavei超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求

    这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失

    上面的配置就确保了,如果跟任何一个slave丢了连接,在10秒后发现没有slave给自己ack,那么就拒绝新的写请求

    因此在脑裂场景下,最多就丢失10秒的数据

Redis哨兵的多个核心底层原理的深入解析(包含slave选举算法)

1、sdown和odown转换机制

sdown和odown两种失败状态

sdown是主观宕机,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机

odown,是客观宕机,如果quorum数量的哨兵都觉得一个master:宕机了,那么就是客观宕机

sdown;达成的条件很简单,如果一个哨兵ping一个master.,超过了is-master-down-after.-milliseconds指定的毫秒数之后,就主观认为master宕机

sdown到odown转换的条件很简单,如果一个哨兵在指定时间内,收到了quorum指定数量的其他哨兵也认为那个master,是sdown了,那么就认为是odown了,客观认为master宕机

2、哨兵和slave:集群的自动发现机制

哨兵互相之间的发现,是通过redis的pub/sub系统实现的,每个哨兵都会往sentinel:hello这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在

每隔两秒钟,每个哨兵都会往自己监控的某个master+slaves对应的__sentinel___:hello channel里发送一个消息,内容是自己的host、ip和runid还有对这个master的监拉配置

每个哨兵也会去监听自已监控的每个master+slaves对应的__sentinel___:hello channel,然后去感知到同样在监听这个master+slaves的其他哨兵的存在

每个哨兵还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步

3、slave配置的自动纠正

哨兵会负责自动纠正slave的一些配置,比如slave?如果要成为潜在的masterf候选人,哨兵会确保slave在复制现有master的数据;

如果slave连接到了一个错误的master.上,比如故障转移之后,那么哨兵会确保它们连接到正确的master上

4、slave->master选举算法

如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来会考虑slave的一些信息

  1. 跟master断开连接的时长
  2. slave优先级
  3. 复制offset
  4. run id

如果一个slave跟master断开连接已经超过了down-after-milliseconds10倍,外加master宕机的时长,那么slave就被认为不适合选举为master

(down-after-milliseconds 10)+milliseconds_since_master_is_in _SDOWN_state

接下来会对slave进行排序

  1. 按照slave优先级进行排序,slave priority越低,优先级就越高

    1. 如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高
  2. 如果上面两个条件都相同,那么选择一个run id比较小的那个slave

5、quorum和majority

每次一个哨兵要做主备切换,首先需要quorum数量的哨兵认为odown,.然后选举出一个哨兵来做切换,这个哨兵还得得到majority哨兵的授权,才能正式执行切换

如果quorum<majority,比如5个哨兵,majority就是3,quorum设置为2,那么就3个哨兵授权就可以执行切换

但是如果quorum>=majority,那么必须quorum数量的哨兵都授权,比如5个哨兵,quorum,是5,那么必须5个哨兵都同意授权,才能执行切换

6.configuration epoch

执行切换的那个哨兵,会从要切换到的新master那里得到一个configuration epoch,.这就是一个version-号,每次切换的version号都必须是唯一的

如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch,作为新的version号

7、configuraiton传播

哨兵完成切换之后,会在自己本地更新生成最新的master配置,然后同步给其他的哨兵,就是通过之前说的pub/sub消息机制

这里之前的version-号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的masteri配置是跟着新的version号的

其他的哨兵都是根据版本号的大小来更新自己的master配置的

Redis的RDB和AOF两种持久化机制的工作原理

1、RDB和AOF两种持久化机制的介绍

RDB持久化机制,对redis中的数据执行周期性的持久化

AoF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集

如果我们想要redis仅仅作为纯内存的缓存来用,那么可以禁止DB和AOF所有的持久化机制

如果redis挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录中,然后重新启动redis,redis就会自动根据持久化数据文件中的数据,去恢复存年中的数据,继续对外提供服务

如果同时使用RDB和AOF两种持久化机制,那么在redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整

RDB与AOF介绍

AOF Rewrite原理

2、RDB持久化机制的优点

  1. DB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说Amazon的S3云服务上去,在国内内可以是阿里云的ODPS分布式存储上,以预定好的备份策略来定期备份redis中的数据

    RDB也可以做冷备,生成多个文件,每个文件都代表了某一个时刻的完整的数据快照

    AOF也可以做冷备,只有一个文件,但是你可以,每隔一定时间,去copy一份这个文件出来

    RDB做冷备,优势在哪儿呢?由redis去控制固定时长生成快照文件的事情,比较方便;AOF,还需要自己写一些脚本去做这个事情,各种定时

    RDB数据做冷备,在最坏的情况下,提供数据恢复的时候,速度比AOF快

  2. RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行DB持久化即可

    • RDB,每次写,都是直接写redis内存,只是在一定的时候,才会将数据写入磁盘中
    • AOF,每次都是要写文件的,虽然可以快速写入os cachet中,但是还是有一定的时间开销的,速度肯定比RDB略馒一些
  3. 相对于AoF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速

    • A0F,存放的指令日志,做数据恢复的时候,其实是要回放和执行所有的指令日志,来恢复出来内存中的所有数据的
    • RDB,就是一份数据文件,恢复的时候,直接加载到内存中即可

结合上述优点,RDB特别适合做冷备份,冷备

3、RDB持久化机制的缺点

  1. 如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据

  2. RDB每次在fork子进程来执行DB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒

4、AOF持久化机制的优点

  1. AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsyc操作,最多丢失1秒钟的数据
  2. AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复
  3. AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写:因为在rewritelog的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入,当新的merge后的日志文件ready的时候,再交换新老日志文件即可。
  4. AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复:比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据

5、AOF持久化机制的缺点

  1. 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
  2. AOF开启后,支持的写QPS会比RDB支持的写QPs低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的
  3. 以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来,所以说,类似AOF这种较为复杂的基于命令日志/merge,/回放的方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug,不过AoF就是为了避免rewritei过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多
  4. 唯一的比较大的缺点,其实就是做数据恢复的时候,会比较慢,还有做冷备,定期的备份,不太方便,可能要自己手写复杂的脚本去做,做冷备不太合适

6、RDB和AOF到底该如何选择

  1. 不要仅仅使用RDB,因为那样会导致你丢失很多数据

  2. 也不要仅仅使用AOF,因为那样有两个问题,第一,你通过AOF做冷备,没有DB做冷备,来的恢复速度更快;

    第二,RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug

  3. 综合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择;用DB来做不同程度的冷备,在AOF文件都丢失或损环不可用的时候,还可以使用RDB来进行快速的数据恢复

Redis缓存雪崩以及穿透问题

Redis解决缓存雪崩

缓存雪崩:redis因为故障,导致缓存信息不可用,所有请求都落在db上,导致系统崩溃。

事前:redis的高可用方案。哨兵或者集群方案。

事中:通过ehcache做一部分数据的本地缓存,请求先查本地缓存,查不到再去redis中查。然后使用hystrix做限流降级,避免db被大量请求打死。这样至少可以保证系统可以正常运行。

事后:redis做持久化,然后快速恢复数据。

缓存穿透:很多恶意请求,发送一些在缓存和db都查不到的数据,导致大量请求直接透过缓存直接打到db上导致系统故障。

解决:将请求去db中查的值,只要没查到的,返回为null,也给存到redis中,存一个UNKNOWN。这样下次这些值来就可以直接从redis中查到了

Cache Aside Pattern缓存+数据库读写模式的分析

最经典的缓存+数据库读写的模式,cache aside pattern

1、Cache Aside Pattern

  1. 读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应
  2. 更新的时候,先删除缓存,然后再更新数据库

2、为什么是删除缓存,而不是更新缓存呢?

原因很简单,很多时候,复杂点的缓存的场景,因为缓存有的时候,不简单是数据库中直接取出来的值

商品详情页的系统,修改库存,只是修改了某个表的某些字段,但是要真正把这个影响的最终的库存计算出来,可能还需要从其他表查询一些数据,然后进行一些复杂的运算,才能最终计算出现在最新的库存是多少,然后才能将库存更新到缓存中去

比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据,并进行运算,才能计算出缓存最新的值的,更新缓存的代价是很高的

是不是说,每次修改数据库的时候,都一定要将其对应的缓存去跟新一份?也许有的场景是这样的,但是对于比较复杂的缓存数据计算的场景,就不是这样了

如果你频繁修改一个缓存涉及的多个表,那么这个缓存会被频繁的更新,频繁的更新缓存

但是问题在于,这个缓存到底会不会被频繁访问到???

举个例子,一个缓存涉及的表的字段,在1分钟内就修改了20次,或者是100次,那么缓存跟新20次,100次;但是这个缓存在1分钟内就被读取了1次,有大量的冷数据

20法则,黄金法则,20%的数据,占用了88%的访问量

实际上,如果你只是别除缓存的话,那么1分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低

每次数据过来,就只是别除缓存,然后修改数据库,如果这个缓存,在1分钟内只是被访问了1次,那么只有那1次,缓存是要被重新计算的,用缓存才去算缓存

其实删除缓存,而不是更新缓存,就是一个Lazy计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算

分布式

分布式服务接口的幂等性如何设计(比如不能重复扣款)?

其实保证幂等性主要是三点:

  1. 对于每个请求必须有一个唯一的标识,举个例子:订单支付请求,肯定得包含订单id,一个订单id最多支付一次
  2. 每次处理完请求之后,必须有一个记录标识这个请求处理过了,比如说常见的方案是在mysq中记录个状态啥的,比如支付之前记录一条这个订单的支付流水,而且支付流水采
    用orderld作为唯一键。只有成功插入这个支付流水,才可以执行实际的支付扣款。
  3. 每次接收请求需要进行判断之前是否处理过的逻辑处理,比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如课重复发送这个请求,则此时先插入支付流水,orderld已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。
  4. 上面只是给大家举个例子,实际运作过程中,你要结合自己的业务来,比如说用redis来保存一个是否处理过的标识也可以,服务的不同实例可以一起操作redis。

分布式系统中的接口调用如何保证顺序性?

1、尽量避免引入顺序性

首先,一般来说,我个人给你的建议是,你们从业务逻辑上最好设计的这个系统不需要这种顺序性的保证,因为一旦引入顺序性保障,我们就需要引入一些的别的、复杂的技术(如分布式锁)来保证,这样会导致系统的复杂度上升,而且会导致系统性能下降,吞吐量降低,热点数据压力过大等问题。

2、一致性hash+内存队列

其次,如果不得不保证顺序性的话,下面给个我们用过的方案吧。

简单来说,首先你得用一致性hash负载均衡策略,将比如同一个订单id对应的请求都给分发到同一个机器上去。接着就是在那个机器上,因为可能还是多线程并发执行的,你就得将这个订单id对应的请求扔进一个内存队列里去,强制排队,这样来确保他们的顺序性。

如下图所示:

3、分布式锁

复杂点的,使用基于zookeeper的分布式锁来实现接口调用的强顺序性。

首先服务A发送的三个有序请求请求1、2、3,依次发送到消息对列,然后服务B的多个实例从消息对列消费。假如分别是三个实例拿到了1/2/3三个请求,那么当请求执行时需要小从zookeeper获取锁,才能执行。所以此时我们的服务A还要指明这三个请求的执行顺序,即seq=1/2/3,服务B才能知道执行顺序。

这时候三个请求都来获取锁,假如请求3先获取到锁,然后看Redis这个list是不是有比自己小的序号,有则释放锁。然后如果请求1拿到了锁,也去Redis判断是不是有比自己小的序号,一看没有,就执行请求1,然后从Redis的list里删掉这个序号。。。依次这样来获取锁->判断->删除redis里的序号。。。来保证接口的顺序性。

如下图所示:

分布式系统事务

1、两阶段提交方案/XA方案

也叫做两阶段提交事务方案,这个举个例子,比如说咱们公司里经常tb是吧(tb,team building,就是团建),然后一般会有个b主席(就是负责组织团建的那个人)。

第一个阶段,一般tb主席会提前一周问一下团队里的海个人,说,大家伙,下周六我们去滑雪+烧烤,去吗?这个时候b主席开始等待每个人的回答,如果所有人都说k,那么就可以决定一起去这次tb。如果这个阶段里,任何一个人回答说,我有事不去了,那么tb主席就会取消这次活动。

第二个阶段,那下周六大家就一起去滑雪+烧烤了

所以这个就是所谓的XS事务,两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都
回复ok,那么就正式提交事务,在各个数据库上执行操作;如果任何一个数据库回答不ok,那么就回滚事务。

这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。如果要玩儿,那么基
于spring+JTA就可以搞定,自己随便搜个demo看看就道了。

2、TCC方案

Tcc的全程是:Try、Confirm、Cancel

这个其实是用到了补偿的概念,分为了三个阶段:

  1. try阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留
  2. Confirm阶段:这个阶段说的是在各个服务中执行实际的操作
  3. Cancel阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作

给大家举个例子吧,比如说跨银行转账的时候,要涉及到两个银行的分布式事务,如果用TCC方案来实现,思路是这样的:

  1. Try阶段:先把两个银行账户中的资金给它冻结住就不让操作了
  2. Confirm阶段:执行实际的转规账操作,A银行账户的资金扣减,B银行账户的资金增加
  3. Cancel阶段:如果任何一个银行的操作执行失败,那么就需要回滚进行补偿,就是比如A银行账户如果已经扣减了,但是B银行账户资金增加失败了,那么就得把A银行账户资金给加回去

这种方案说实话几乎很少用人使用,因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常之恶心。

比较适合的场景:这个就是除非你是真的一致性要求太高,是你系统中核心之核心的场景,比如常见的就是资金类的场景,那你可以用TCC方案了,自己编写大量的业务逻辑,自己判断一个事务中的各个环节是否水,不k就执行补偿回滚代码。而且最好是你的各个业务执行的时间都比较短。

但是说实话,一般尽量别这么搞,自己手写回滚逻辑,或者是补偿逻辑,实在太恶心了,那个业务代码很难维护。

3、本地消息表

这个大概意思是这样的

  1. A系统在自己本地一个事务里操作同时,插入一条数据到消息表
  2. 接看A系统将这个消息发送到MQ中去
  3. B系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息
  4. B系统执行成功之后,就会更新自己本地消息表的状态以及A系统消息表的状态
  5. 如果B系统处理失败了,那么就不会更新消息表状态,那么此时A系统会定时扫描自己的消息表,如果有没处理的消息,会再次发送到MQ中去,让B再次处理
  6. 这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止

这个方案说实话最大的问题就在于严重依赖于数据库的消息表来管理事务啥的???这个会导致如果是高并发场景咋动呢?咋扩展呢?所以一般确实很少用

本地消息表

4、可靠消息最终一致性方案

这个的意思,就是干脆不要用本地的消息表了,直接基于MQ来实现事务。比如阿里的RocketMQ就支持消息事务。

大概的意思就是:

  1. A系统先发送一个prepared消息到MQ,如果这个prepared消息发送失败那么就直接取消操作别执行了
  2. 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉MQ发送确认消息,如果失败就告诉MQ回滚消息
  3. 如果发送了确认消息,那么此时B系统会接收到确认消息,然后执行本地的事务
  4. mq会自动定时轮询所有prepared消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认消息?那是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,别确认消息发送失败了。
  5. 这个方案里,要是系统B的事务失败了咋动?重试略,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如B系统本地回滚后,想动法通知系统A也,回滚;或者是发送报警由人工来手工回滚和补偿

这个还是比较合适的,目前国内互联网公司大都是这么玩儿的,要不你举用RocketMQ支持的,要不你就自己基于类似ActiveMQ?RabbitMQ?自己封装一套类似的逻辑出来,,总之思路就是这样子的

可靠消息最终一致性方案

5、最大努力通知方案

这个方案的大致意思就是:

  1. 系统A本地事务执行完之后,发送个消息到MQ
  2. 这里会有个专门消费MQ的最大努力通知服务,这个服务会消费MQ然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统B的接口
  3. 要是系统B执行成功就ok了;要是系统B执行失败了,那么最大努力通知服务就定时尝试重新调用系统B,反复N次,最后还是不行就放弃

如何设计一个高并发系统

  1. 系统拆分,将一个系统拆分为多个子系统,用dubbo来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以抗高并发么。
  2. 缓存,必须得用缓存。大部都分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟人家redis轻轻松松单机几万的并发啊。没问题的。所以你可以考虑考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发。
  3. MQ,必须得用MQ。可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删做,疯了。那高并发绝对搞挂你的系统,你要是用redis来承载写那肯定不行,人家是缓存,数据随时就被LU了,数据格式还无比简单,没有事务支持。所以该用mysql还得用mysql啊。那你咋办?用MQ吧,大量的写请求灌入MQ里,排队慢慢玩儿,后边系统消费后慢慢写,控制在mysql承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用MQ来异步写,提升并发性。MQ单机抗几万并发也是ok的,这个之前还特意说过。
  4. Elasticsearch,可以考虑用es。es是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来抗更高的并发。那么一些比较简单的查询、统计类的操作,可以考虑用es来承载,还有一些全文搜索类的操作,也可以考虑用es来承载。
  5. 分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来抗更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高sql跑的性能。
  6. 读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。读流量太多的时候,还可以加更多的从库。

如何设计一个高并发系统

如何分库分表

1、你们具体是如何对数据库如何进行垂直拆分或水平拆分的?

水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部喽数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。

垂直拆分的意思,就是把一个有很多字段的婊给拆分成多个表,或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的防问颜率很高的字段
放到一个表里去,然后将较多的防问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些

这个其实挺常见的,不一定我说,大家很多同学可能自己都做过,把一个大表拆开,订单表、订单支付表、订单商品表。

还有表层面的拆分,就是分表,将一个表变成个表,就是让每个表的数据量控制在一定范围内,保证SQL的性能。否贝侧单表数据量越大,SQL性能就越差。一般是200万行左右,不要太多,但是也得看具体你怎么操作,也可能是500万,或者是100万。你的SQL越复杂,就最好让单表行数越少。

你就得考虑一下,你的项目里该如何分库分表?一般来说,垂直拆分,你可以在表层面来做,对一些字段特别多的表欣一下拆分;水平拆分,你可以说是并发承栽不了,或者是数据量太
大,容量严载不了,你给拆了,按什么字段来拆,你自己想好;分表,你考虑一下,你如果那怕是拆到每个库里去,并发和容量都k了,但是每个库的表还是太大了,那么你就分表,将这个表分开,保证每个表的数据量并不是很大。

而目这儿还有两种分库分表的方式,一种是按照range来分,就是每个库一段连续的数据,这个一般是按比如时间违来的,但是这种一般校少用,因为很容易产生热点问题,大量的流理都打在最新的数据上了;或者是按照某个字段ash一下均问分散,这个较为常用。

range来分,好处在于说,后面扩容的时候,就很容易,因助你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的据。实际生产用range,要看场景,你的用户不足仅仅访问最新的数据,而足均匀的访问现在的数据以及历史的数据

hash分法,好处在于说,可以平均分配没给库的数据量和请求压力,坏处在于说扩容起来比较麻烦,会有一个数据迁移的这么一个过程。

2、如何把系统不停机迁移到分库分表的

双写迁移方案

这个是我们常用的一种迁移方案,比较靠谱一些,不用停机,不用看北京凌晨4点的风景

简单来说,就是在线上系统里面,之前所有写库的地方,增删改操作,都除了对老库增删改,都加上对新库的增删改,这就是所谓双写,同时写俩库,老库和新库。

然后系统部署之后,新库数据差太远,用之前说的导数工具,跑起来读老库数据写新库,写的时候要根据gmt_modified这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。

接着导完一轮之后,有可能数据还是存在不一致,那么就程序自动做一轮校验,比对新老库每个表的每条数据,接着如果有不一样的,就针对那些不一样的,从老库读数据再次写。反复循环,直到两个库每个表的数据都完全一致为止。

接着当数据完全一致了,就ok了,基于仅仅使用分库分表的最新代码,重新署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。所以现在基本玩儿数据迁移之类的,都是这么干了。

不停机双写分库分表方案

3、如何设计可以动态扩容缩容的分库分表方案

刚开始的时候,这个库可能就是逻辑库,建在一个数据库上的,就是一个mysql服务器可能建了n个库,比如16个库。后面如果要拆分,就是不断在库和mysql服务器之间做迁移就可以了。然后系统配合改一下配置即可。

比如说最多可以扩展到32个数据库服务器,每个数据库服务器是一个库。如果还是不够?最多可以扩展到1024个数据库服务器,每个数据库服务器上面一个库一个表。因为最多是1024个表么。

这么搞,是不用自己写代码做数据迁移的,都交给dba来搞好了,但是dba确实是需要做些库表迁移的工作,但是总比你自己写代码,抽数据导数据来的效率高得多了。

哪怕是要减少库的数量,也很简单,其实说白了就是按倍数缩容就可以了,然后修改一下路由规则。

对2^n取模

orderld 模 32=库

orderld / 32 模 32=表

  1. 设定好几台数据库服务器,每台服务器上几个库,每个库多少个表,推荐是32库*32表,对于大部分公司来说,可能几年都够了
  2. 路由的规则,orderld模32=库,orderld/32模32=表
  3. 扩容的时候,申请增加更多的数据库服务器,装好mysql,倍数扩容,4台服务器,扩到8台服务器,16台服务器
  4. 由dba负责将原先数据库服务器的库,迁移到新的数据库服务器上去,很多工具,库迁移,比较便捷
  5. 我们这边就是修改一下配置,调整迁移的库所在数据库服务器的地址
  6. 重新发布系统,上线,原先的路由规侧变都不用变,直接可以基于2倍的数据库服务器的资源,续进行线上系统的提供服务

4、分库分表之后全局id咋生成

1、数据库自增id

这个就是说你的系统里每次得到一个d,都是往一个库的一个表里插入一条没什么业务含义的数据,然后获取一个数据库自增的一个id。掌到这个id之后再往对应的分库分表里去写入

这个方案的好处就是方便简单,谁都会用;缺点就是单库生成自增,要是高并发的话,就会有瓶颈的;如果你硬是要改进一下,那么就专门开一个服务出来,这个服务每次就掌到当前id最大值,然后自己递增几个id,一次性返回一批id,然后再把当前最大id值修改成递增几个id之后的一个值;但是无论怎么说都是基于单个数据库。

适合的场景:你分库分表就俩原因,要不就是单库并发太高,要不就是单库数据量太大;除非是你并发不高,但是数据量太大导致的分库分表扩容,你可以用这个方案,因为可能海秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。

并发很低,几百/s,但是数据量大,几十亿的数据,所以需要靠分库分表来存放海量的数据

2、uuid

好处就是本地生成,不要基于数据库来了;不好之处就是,uuid太长了,作为主键性能太差了,不适合用于主键。

适合的场景:如果你是要随机生成个什么文件名了,编号之类的,你可以用uuid,但是作为主键是不能用uuid的。

3、获取系统当前时间

这个就是获取当前时间即可,但是问题是,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的。基本就不用考虑了。

适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间

4、snowflake算法

twitter开源的分布式id生成算法,就是把一个64位的Iong型的id,1个bit是不用的,用其中的41 bit作为毫秒数,用10 bit作为工作机器id,12 bit作为序列号

  • 1bit:不用,为啥呢?因为二进制里第一个bit为如果是1,那么都是负数,但是我们生成的d都是正数,所以第一个bit统一都是0
  • 41bit:表示的是时间戳,单位是毫秒。41bit可以表示的数字多达241-1,也就是可以标识2^41-1个毫秒值,换算成年就是表示69年的时间。
  • 10bit`:记录工作机器id,代表的是这个服务最多可以部曙在210台机器上哪,也就是1024台机器。但是10bit里5个bit代表机房id,5个bit代表机器id。意思就是最多代表2^5个机房(32个机房),每个机房里可以代表2^5个机器(32台机器)。
  • 12bit:这个是用来记录同一个毫秒内产生的不同1d,12bit可以代表的最大正整数是2^12-1=4096,也就是说可以用这个12bit代表的数字来区分同一个毫秒内的4096个不同的id

0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 11001 | 0000 00000000

比如我们来观察上面的那个,就是一个典型的二进制的64位的id,换算成10进制就是910499571847892992。

2018-01-0110:00:00 -> 做了一些计算,再换算成一个二进制,41bit来放 -> 00011001010001010111110100010010101110000

机房id,17 -> 换算成一个二进制 >10001

机器id,25 -> 换算成一个二进制 >11001

snowflake算法服务,会判断一下,当前这个请求是否是,机房17的机器25,在2175/11/7 12:12:14时间点发送过来的第一个请求,如果是第一个请求

假设,在2175/11/7 12:12:14时间里,机房17的机器25,发送了第二条消息,snowf1ake算法服务,会发现说机房17的机器25,在2175/11/712:12:14时间里,在这一毫秒,之前已经生成过一个id了,此时如果你同一个机房,同一个机器,在同一个毫秒内,再次要求生成一个id,此时我只能把加1

MySQL读写分离的原理?主从同步延时咋解决?

如何实现MySQL的读写分离?

其实很简单,就是基于主从复制架构,简单来说,就搞一个主库,挂多个从库,然后我们就单单只是写主库,然后主库会自动把数据给同步到从库上去。

MySQL主从复制原理的是啥?

主库将变更写binlog日志,然后从库连接到主库之后,从库有一个IO线程,将主库的binlog日志拷贝到自己本地,写入一个中继日志中。接着从库中有一个SQL线程会从中继日志读取binlog,然后执行binlog日志中的内容,也就是在自己本地再次执行一遍SQL,这样就可以保证自己跟主库的数据是一样的。

这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行SQL的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。

而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。

所以mysq实际上在这一块有两个机制,一个是半同步复制,用来解决主库数据丢失问题;一个是并行复制,用来解决主从同步延时问题。

这个所谓半同步复制,指的就是主库写入binlog日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的relay log之后,接着会返回一个ack给主库,主库接收到至少一个从库的ack之后才会认为写操作完成了。

这个所谓半同步复制,semi-sync半同步复制,指的就是主库写入binlog日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的relay log之后,接着会返回一个ack给主库,主库接收到至少一个从库的ack之后才会认为写操作完成了。

所谓并行复制,指的是从库开启多个线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

MySQL主从复制原理

MySQL主从同步延时问题

show status,Seconds_Behind_Master,你可以看到从库复制主库的数据落后了几ms

其实这块东西我们经常会碰到,就比如说用了ysq主从架构之后,可能会发现,刚写入库的数据结果没查到,结果就完蛋了。。

所以实际上你要考虑好应该在什么场景下来用这个mysql主从同步,建议是一般在读远远多于写,而且读的时候一般对数据时效性要求没那么高的时候,用mysql主从同步所以这个时候,我们可以考虑的一个事情就是,你可以用mysql的并行复制,但是问题是那是库级别的并行,所以有时候作用不是很大

所以这个时候。。通常来说,我们会对于那种写了之后立马就要保证可以查到的场景,采用强制读主库的方式,这样就可以保证你肯定的可以读到数据了吧。其实用一些数据库中间件是没问题的。

生产环境定位

线上日志分析的步骤

  • 通过top命令查看CPU情况,如果CPU比较高,则通过top -Hp 命令查看当前进程的各个线程运行情况,找出CPU过高的线程之后,将其线程id转换为十六进制的表现形式,然后在jstack日志中查看该线程主要在进行的工作。这里又分为两种情况
  • 如果是正常的用户线程,则通过该线程的堆栈信息查看其具体是在哪处用户代码处运行比较消耗CPU;
  • 如果该线程是VM Thread,则通过jstat -gcutil 命令监控当前系统的GC状况,然后通过jmap dump:format=b,file= 导出系统当前的内存数据。导出之后将内存情况放到eclipse的mat工具中进行分析即可得出内存中主要是什么对象比较消耗内存,进而可以处理相关代码;
  • 如果通过 top 命令看到CPU并不高,并且系统内存占用率也比较低。此时就可以考虑是否是由于另外三种情况导致的问题。具体的可以根据具体情况分析:
  • 如果是接口调用比较耗时,并且是不定时出现,则可以通过压测的方式加大阻塞点出现的频率,从而通过jstack查看堆栈信息,找到阻塞点;
  • 如果是某个功能突然出现停滞的状况,这种情况也无法复现,此时可以通过多次导出jstack日志的方式对比哪些用户线程是一直都处于等待状态,这些线程就是可能存在问题的线程;
  • 如果通过jstack可以查看到死锁状态,则可以检查产生死锁的两个线程的具体阻塞点,从而处理相应的问题。

Java高CPU占用排查步骤

  • top:找到占用CPU高的进程PID
  • jstack <PID> >> java_stack.log:导出CPU占用高进程的线程栈
  • top -Hp <PID>:找出PID的进程占用CPU过高的线程TID。(或使用命令 ps -mp PID -o THREAD,tid,time | sort -rn | less)
  • printf "%x\n" <TID>:将需要的线程ID转换为16进制格式。
  • less java_stack.log:查找转换成为16进制的线程TID,找到对应的线程栈,分析并处理问题。

Java高内存占用排查步骤

  • top:找到占用内存(RES列)高的Java进程PID
  • jmap -heap <PID>:查看heap内存使用情况。
  • jps -lv :查看JVM参数配置。
  • jstat -gc <PID>1000:收集每秒堆的各个区域具体占用大小的gc信息。
  • jmap -dump:live,format=b,file=heap_dump.hprof <PID> :导出堆文件。
  • 使用MAT打开堆文件,分析问题。

Java堆外内存泄漏排查步骤

  • top:找到占用内存(RES列)较高的Java进程PID
  • jstat -gcutil PID 1000 查看每秒各个区域占堆百分比,若gc正常,则分析堆外内存使用情况。
  • jcmd <PID>VM.native_memory detail,该命令需要添加JVM参数 -XX:NativeMemoryTracking=detail,并重启Java进程才能生效,该命令会显示内存使用情况,查看输出结果,总的committed的内存是否小于物理内存(RES),因为jcmd命令显示的内存包含堆内内存、Code区域、通过unsafe.allocateMemory和DirectByteBuffer申请的内存,但是不包含其他Native Code(C代码)申请的堆外内存。
  • pmap -x <PID>| sort -rn -k 3:查看内存分布,是否有地址空间不在jcmd命令所给出的地址空间中。
  • 用工具定位堆外内存,如gperftools、gdb、strace等。

Java面试
https://xiaoyu72.com/articles/97408b2e/
Author
XiaoYu
Posted on
February 12, 2023
Updated on
August 28, 2023
Licensed under