15

1、Nacos与Eureka的区别

nacos 和eureka的范围不同,nacos的阀值是针对某个具体Service的,而不是针对所有的服务的;但Eureka的自我保护阀值是针对所有服务的。nacos支持cp和Ap俩种;eureka支持AP。nacos使用netty,是长连接;eureka是短连接,定时发送。
Eureka和Nacos的保护方式不同: Eureka保护方式:在短时间内,统计续约失败的比例,如果达到一定的阀值,则会触发自我保护的机制。在该机制下,Eureka Server 不会剔除任何的微服务,等到正常后,在退出自我保护机制。

3、Ribbon的负载均衡算法有哪几种,如果要自定义手写一个负载均衡你的思路是怎么样的


ribbon主要负责请求分发,例如一个服务节点集群:多台服务器部署着订单服务,用户请求过来了就要根据不同的负载策略分发请求到不同机器上,起到一个缓解请求压力的作用。其自身不会发起请求,真正发起请求的还是Feign/openFeign。
ribbon是属于客户端的负载均衡,Nginx是属于服务端的负载均衡。
ribbon 负载均衡算法有七种:
1,RoundRobinRule轮询(默认):具体实现是一个负载均衡算法:第N次请求%服务器集群的总数=实际调用服务器位置的下标。
2:RandomRule随机
3:RetryRule轮询重试(重试采用的默认也是轮询)
4:WeightedResponseTimeRule响应速度决定权重
5:BestAvailableRule最优可用(底层也有RoundRobinRule);
6:AvailabilityFilteringRule可用性过滤原则
7:ZoneAvoidanceRule区域内可用性能最优

4、微服务集群环境下接口限流怎么实现

1,nginx限流;
2,guaua限流
3,sentinel限流

5、场景题:权限部分已经校验完token信息,现在feign与feign之间的调用,我要传递token参数,你有什么解决方案,方法
中绑定入参token或者对象加token字段或者对象去继承一个有token属性的父类,

1,通过redis获取,通过threadLocal传递;

6、场景题:不使用七牛云等文件服务器的情况下,feign与feign之间去传文件,发现文件根本传不过去,接收方得到的文件是null
这种情况下该怎么解决

1,Feign的参数注解必须改为@RequestPart;
2,路径后一定要指定Content-Type 为multipart/form-data

7、你们分库分表是怎么分的,分表的字段基于什么考虑,shareding-jdbc分表原理

通过shareding-jdbc分表分库,
shareding-jdbc分表原理:

8、为什么分页查询会越来越慢,我们经常用的百度查询,分页参数只有上一页和下一页为什么百度不会给用户展示首页和尾页的按钮

当我们翻到最好几页时,查询的sql通常是:select * from table where column=xxx order by xxx limit 100000,20。查询非常慢,但是当我们查询前几页的时候,速度并不慢。这是因为limit的偏移量太大导致的。mysql使用limit时的原理是(用上面的例子举例):
MySQL将查询出100020记录,然后舍掉前面的1000000条记录。返回剩下的20条记录
解决的方案就是尽量使用索引,就是我们select后面检出的是索引列,而不是所有列,而且这个索引的列最好是id,然后做一次关联查询返回所有的列,上述的sql写成:
SELECT  *  FROM  table t  INNER JOIN (SELECT  id  FROM table WHERE xxx_id = 143381 LIMIT 800000,20) t1 ON t.id = t1.id

11、RabbitMQ怎么保证投递消息不丢失

RabbitMQ一般情况下很少丢失,但是不能排除意外,为了保证我们自己的系统高可用,我们必须做好完善的措施,保证系统的稳定性。
  1,消息持久化
 2,ACK确认机制
 3,设置集群镜像模式
4,消息补偿机制
第一种: 消息持久化
RabbitMQ 的消息默认存放在内存上面,如果不特别声明设置,消息不会持久化保存到硬盘上面的,如果节点重启或者意外crash掉,消息就好丢失。所以也要对消息进行持久化处理:1,Exchange 设置持久化,2,Queue进行持久化,3,Message持久化发送: 发送消息设置发送模式deliveryMode=2,代表持久化消息。
第二种: Ack确认机制
 多个消费者同时收取消息,比如消息接收到一半的时候,一个消费者死掉了(逻辑复杂时间太长,超时了或者消费被停机了或者网络断开链接),为了保证消息不丢失就需要使用 Message acknowledgment机制,就是消费端消费完成要通知服务端,服务端才会把消息从内存删除。
 这样就解决了,及时一个消费者出了问题,没有同步消息给服务端,还有洽谈的消费端去消费,保证了消息不丢的case.
第三种:设置集群镜像模式:
RabbitMQ有三种部署模式分别是:1,单节点模式: 最简单的情况,非集群的模式,节点挂了,消息就不能用了。业务可能瘫痪只能等待。
2,普通模式: 默认的集群模式,某个节点挂了,改节点上的消息不能用,有影响的业务瘫痪,只能等待节点恢复重启(必须持久化消息情况下)。
3,镜像模式:把需要的队列做成镜像队列,存在于多个节点,属于RabbitMQ的HA方案
第四种:消息补偿机制
  

12、场景题:mysql主从复制,读写分离的设计中,从库同步主 库会有一定延迟,如果现有这样一个场景,
在写入数据的时候,马上读取数据,怎么保证读取的数据不为空?达到即写即读的要求

使用cannal

13、谈谈Redission分布式锁的实现原理

Redission是redis客户端和jedis,lettuce一样,但他提供方诸多如分布式锁这些方便的工具。
实际上就是分三种情况作了处理
1. 如果锁不存在:使用hest方法创建了锁,filed是线程的唯一标识value是重入次数,初始为1
2,如果锁存在且为自身持有:使用hincrby将value加1表示了一次重入
3,如果锁存在但非自身持有: 返回锁的过期时间(毫秒级别)
watch dog 自动延期机制
 在使用setnx实现的分布式锁中存在着一个这样的问题: 锁无法续期,在实际的业务中可能存在小锁中存在大事务的情况,如果事务的执行时间非常长,此次锁被已经超时释放掉了,就可能存在不可预料的问题产生。redission为我们提供的锁的默认时间是30秒,并且在客户端加锁成功后会启动一个watch dog 后台线程,没距离10秒检查一下,如果客户端还持有锁key,那么就不断的延长锁key生存时间。

1:redission自带的分布式锁作为附带的工具可以很好的满足我们在分布式系统上的加锁需求;
2:相较于setnx实现的分布式锁,其实现原理更为复杂一些,引入了watch dog机制,实现了锁的可续期,巧妙的利用hash 结构实现了锁的课重入;
3,无论是redission还是setnx实现的分布式锁,在redis的集群模式下,都存在主节点故障导致的问题;
A客户端在主节点写入自己的锁,此时主节点宕机,该数据未及时同步到从节点,而从节点被选为主节点,B客户端如果也去写入锁,同样也可以成功,此时存在并发写入导致的系列风险问题。

14、谈谈synchronized锁以及他的锁升级过程

synchronized在java中是一个关键字,他是由JVM实现的,所以Synchronized依赖于操作系统实现。但是Synchronized非常的智能,能通过判断整个程序的锁冲突来决定使用什么样的锁策略,对于程序员来说非常的友好。
首先synchronized的真个锁升级过程: 无锁-->偏向锁--->轻量级锁-->重量级锁,synchronized的升级过程就是以上四个步骤,当锁的竞争越来越激烈的时候synchronized会自动进行升级。
 无锁: 整个程序都不会发生冲突的时候就是无锁策略。
 偏向锁: 当我们的第一个线程拿到锁以后,系统会标记此线程,当此线程释放锁以后,在尝试拿锁会直接同行,并不需要直接加锁解锁操作。
轻量级锁: synchronized 升级成轻量级锁,这个轻量级锁就是一个基于cas实现的自旋锁,如果发现有锁冲突但是不严重的情况下,一个线程先获取到锁,然后另外一个线程也想要获取这个锁资源,这时候线程2不会进入进行阻塞等待,而是在CPU上空转,没有放弃CPU,然后每过一段时间就尝试获得锁,这可以使得在自旋的线程可以第一时间获取到锁资源,但是会占用CPU的资源。在我们的锁冲突比较不严重的情况下我们的synchronized就是这样的一个轻量级锁。
重量级锁:
当JVM判断我们整个程序的锁冲突严重的时候就会将synchronized升级成重量级锁,而这个重量级锁就是挂起等待锁。当一个线程获取到锁之后,其他线程会进入阻塞队列中等待,等待锁被释放,然后所有线程再去竞争锁,如果这个锁资源一直不被释放,其他线程就会一直在阻塞队列中一直死等,等到资源被释放。
synchronized只能进行锁升级,不能支持锁降级。

15、谈谈ReentrantLock锁的基本原理

 一个线程持有锁时,当其他的线程尝试获取该锁时,会被阻塞;而这个线程尝试获取自己持有锁时,如果成功说明锁是可以重入的,反之则不可重入。
  ReentrantLock使用内部类的Sync来管理锁,所以真正的获取锁是由Sync的实现类来控制的。Sync又有2个实现,分别为NonfairSync(非公平锁)和FairSync(公平锁)。Sync通过继承AQS实现,在AQS中维护了一个private volatile in state 来计算重入次数,避免频繁的持有释放操作带来的线程问题。
 synchronized 关键字经过编译后,会在同步快的前后分别形成monitorenter和monitoreit2个字节码指令。每个锁对象内部维护一个计时器,该计时器初始值为0,表示任何线程都可以获取该锁并执行相应的方法。根据虚拟规范要求,在执行minitorenter指令时,首先尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了对象的锁,把锁的计数器+1,相应的在执行monitorexit指令后锁计数器-1,当计数器为0时,锁就被释放,如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

16、ReentrantLock如何实现锁重入

一个线程持有锁时,当其他的线程尝试获取该锁时,会被阻塞;而这个线程尝试获取自己持有锁时,如果成功说明锁是可以重入的,反之则不可重入。
ReentrantLock使用内部类的Sync来管理锁,所以真正的获取锁是由Sync的实现类来控制的。Sync又有2个实现,分别为NonfairSync(非公平锁)和FairSync(公平锁)。Sync通过继承AQS实现,在AQS中维护了一个private volatile in state 来计算重入次数,避免频繁的持有释放操作带来的线程问题。
synchronized 关键字经过编译后,会在同步快的前后分别形成monitorenter和monitoreit2个字节码指令。每个锁对象内部维护一个计时器,该计时器初始值为0,表示任何线程都可以获取该锁并执行相应的方法。根据虚拟规范要求,在执行minitorenter指令时,首先尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了对象的锁,把锁的计数器+1,相应的在执行monitorexit指令后锁计数器-1,当计数器为0时,锁就被释放,如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止

17、ReentrantLock中公平锁和非公平锁分别是怎么实现的

ReentrantLock是J.U.C包下的一个类,默认是非公平的。
public class ReentrantLock implements Lock, java.io.Serializable {
   // 内部类
   private final Sync sync;

   public ReentrantLock() {
       // 默认使用非公平锁
       sync = new NonfairSync();
   }
   
   // 可通过传入参数(true)的形式使用公平锁
   public ReentrantLock(boolean fair) {
       sync = fair ? new FairSync() : new NonfairSync();
   }

其中内部类 Sync 继承了AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
   //...
}
而 NonfairSync 与FairSync都是继承自Sync,也均是内部类
static final class NonfairSync extends Sync
static final class FairSync extends Sync
一.非公平锁实现原理
ReentranLock的常用实现方式: 一般都是直接new一个对象,然后通过lock()方法获取锁,
   // 1. 获取对象,默认非公平
   public ReentrantLock() {
       sync = new NonfairSync();
   }

   // 2. 调用lock() 方法
   public void lock() {
       sync.lock();
   }

   // 3. 调用非公平内部类实现的 Lock() 方法
   static final class NonfairSync extends Sync {
       private static final long serialVersionUID = 7316153563782823691L;
       
       final void lock() {
           // 尝试获取锁
           if (compareAndSetState(0, 1))
               // 获取成功后设置当前线程信息
               setExclusiveOwnerThread(Thread.currentThread());
           else
               // 未获取到锁则执行以下方法
               acquire(1);
       }
       // 其他方法。。。
   }

   // acquire(1) 方法内部逻辑
   public final void acquire(int arg) {
       if (!tryAcquire(arg) && // 尝试获取锁
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 调用AQS 同步方法
           selfInterrupt();
   }
非公平锁的 acquire(int arg) ,逻辑比较简单,源码如下
       protected final boolean tryAcquire(int acquires) {
           return nonfairTryAcquire(acquires);
       }

       /**
        * Performs non-fair tryLock.  tryAcquire is implemented in
        * subclasses, but both need nonfair try for trylock method.
        */
       final boolean nonfairTryAcquire(int acquires) {
           final Thread current = Thread.currentThread();
           int c = getState();
           // 若锁状态为0,则直接执行获取锁逻辑
           if (c == 0) {
               if (compareAndSetState(0, acquires)) {
                   setExclusiveOwnerThread(current);
                   return true;
               }
           }
           // 若锁已经有线程执行,则判断是否为当前线程,若是则state + 1, 这也是可重入的实现
           else if (current == getExclusiveOwnerThread()) {
               int nextc = c + acquires;
               if (nextc < 0) // overflow
                   throw new Error("Maximum lock count exceeded");
               setState(nextc);
               return true;
           }
           return false;
       }
二: 公平锁的实现原理:
       protected final boolean tryAcquire(int acquires) {
           final Thread current = Thread.currentThread();
           int c = getState();
           if (c == 0) {
               // 与非公平锁的区别是加入了阻塞队列的判断
               if (!hasQueuedPredecessors() &&
                   compareAndSetState(0, acquires)) {
                   setExclusiveOwnerThread(current);
                   return true;
               }
           }
           else if (current == getExclusiveOwnerThread()) {
               int nextc = c + acquires;
               if (nextc < 0)
                   throw new Error("Maximum lock count exceeded");
               setState(nextc);
               return true;
           }
           return false;
       }
   }
ReentrantLock 引入了阻塞队列来实现排队等候,先来先执行,后来则排队,所以是公平的,
ReentLock的非公平锁就是不考虑阻塞队列的情况下直接获取锁。

18、自旋锁和自适应自旋锁有什么区别

  自旋锁的概念: 阻塞或唤醒一个java线程需要操作系统切换cpu状态来完成,这种状态装换需要耗费处理器的时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户执行代码的时间还要长。
 在许多场景下,同步资源的锁定时间较短,为了这一小段时间去切换线程,线程挂起和恢复线程花费可能会让系统得不偿失。如果物理机器有多个处理器,能让2个或者多个线程同时并行执行。我们可以让后面那个请求锁的线程不放弃cpu的执行时间,看看持有锁的线程是否会很快就会释放锁。
 而为了让当前线程 "稍等一下",我们需要让当前线程锁自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
   自旋锁的缺点: 自旋锁本身是有缺点的,他不能代替阻塞,自旋等待虽然避免了线程切换的开销,但是他要占用cpu处理器的时间,如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器的资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以用 -XX:PreBlockSpin来更改)没有成功获得锁,就应该挂起线程。自旋锁的原理同样也是cas。
  适应性自旋锁:自适应意味着自旋的时间(次数)不在固定,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在执行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而他讲允许自旋等待的持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那子啊以后尝试获取这个锁时将可能省略自旋过程,直接阻塞线程,避免浪费处理器资源。

19、AQS了解吗?说说你的理解

AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到对列中
CLH(Craig,Landin,and Hagersten)对列是一个虚拟的双向对列(虚拟的双向对列即不存在对列的实例,仅存在节点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁对列的一个节点(NODE),来实现锁的分配。
AQS 使用一个int 成员变量来表示同步状态,通过内置的FIFO对列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现其对值的修改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性。
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
//返回同步状态的当前值
protected final int getState() {
       return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
       state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
       return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2, AQS对资源的共享方式
AQS定义2种资源共享方式
Exclusive(独占): 只有一个线程能执行,如ReentrantLock。又可以分为公平锁和非公平锁:
 公平锁:按照线程在对列中的排队序列,先到者先拿到锁。
非公平锁:当线程要获取锁时,无视对列顺序直接去抢锁,谁抢到就是谁的
share(共享):多个线程可同时执行,如Semaphore/CountDownLatch,Semaphore,CountDownLatch,CyclicBarrier,ReadWriteLock。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一个资源进行行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取和释放方式即可,至于具体线程等待对列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

3,AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般是这样
使用者继承AbstractQueuedSynchronizer 并重写指定的方法(对共享资源state的获取和释放)
将aqs组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
默认情况下每个方法都抛出UnsupportedOperationException.这些方法的实现必须是内部线程安全的,并且通常应简短而不是阻塞。AQS类中的其他方法都是final,所以无法被其他类使用,只有这几个方法可以被其他类使用。
以ReentrantLock为例,state初始化为0,表示未锁定状态,A线程locak()时,会调用tryAcquire()独占该锁并将state+1. 此后,其他线程在tryAcquire()时就会失败,
直到A线程unlock()到state=0(即释放锁为止),其他线程才会有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可以重入的概念。但要注意,获取多少次就要释放多少次,才能保证state是能回到零态的。
在以CountDownLatch以例,任务分为N个子线程去执行,state 也初始化为N(注意N要以线程个数一致),这N个子线程是并行执行的,每个子线程执行完以后countDown()一次,state 会CAS(Compare and  swap)减1.等到所有的子线程都执行完后(state=0),会unpark()主调用线程,然后主线程就会从await()函数返回,继续后于余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock
4 AQS组件总结
Semaphore(信号量)-允许多个线程同时访问:synchronized和ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
countDownLatch(倒计时器):countDownLatch是一个同步工具类,用来协调多个线程之间的同步,这个工具通常用来控制线程等待,他可以让某一个线程等待直到倒计时结束,再开始执行。
CyclicBarrier(循环栅栏),CyclicBarrier和CountDownLatch非常相似,他可以实现线程之间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

20、springcloud框架中,我们写的controller是线程安全的吗?@autowired注解注入的bean是类的成员变量,
请问他是线程安全的吗?如果是它是怎么保证安全的?如果不是为什么我们还要这样写不安全的代码?


21、spring事务失效的几种场景说说看

 Spring事务会在以下几种特定的场景下失效。
1:数据库不支持事务,spring事务生效的前提是锁连接的数据库要支持事务,如果底层的数据库都不支持事务那么spring的事务肯定会失效。例如: 如果使用的数据库为MySQL,并且使用了MyISAM存储引擎,则spring的事务就会失效。
2: 【事务方法未被Spring关联】
 如果事务方法所在的类没有加载到Spring IOC容器中,也就是说,事务方法所在的类没有被Spring管理,则spring事务会失效。
3,方法没有被public修饰
如果事务所在的方法没有被public修饰,此时spring的事务会失效。
4: 同一类中方法调用
如果同一个类中的2个方法分别为A和B,方法A上没有添加事务注解,方法B上加了@Transactional事务注解,方法A调用方法B,则方法B的事务会失效。
5: 未配置事务管理器:
如果在项目中没有配置Spring的事务管理器,即使使用了Spring的事务管理功能,Spring的事务也不会生效。
6:方法的事务传播类型不支持事务。
7.不正确的捕获异常
不正确的捕获异常也会导致Spring的事务失效。
8.错误的标注异常类型
如果在@Transaction注解中标注了错误的异常类型,则Spring事务的回滚会失败。

21、spring中如何将原来的事务挂起重新开启一个新事务?

PROPAGATION_REQUIRED:默认的Spring事物传播级别,若当前存在事务,则加入该事务,若不存在事务,则新建一个事务。
PAOPAGATION_REQUIRE_NEW:若当前没有事务,则新建一个事务。若当前存在事务,则新建一个事务,新老事务相互独立。外部事务抛出异常回滚不会影响内部事务的正常提交。
PROPAGATION_NESTED:如果当前存在事务,则嵌套在当前事务中执行。如果当前没有事务,则新建一个事务,类似于REQUIRE_NEW。
PROPAGATION_SUPPORTS:支持当前事务,若当前不存在事务,以非事务的方式执行。
PROPAGATION_NOT_SUPPORTED:以非事务的方式执行,若当前存在事务,则把当前事务挂起。
PROPAGATION_MANDATORY:强制事务执行,若当前不存在事务,则抛出异常.
PROPAGATION_NEVER:以非事务的方式执行,如果当前存在事务,则抛出异常

22、乐观锁和悲观锁有什么区别?

乐观锁和悲观锁都是用于解决并发场景下的数据竞争问题,但是却是2种不同的思想。他们的使用非常的广泛,也不局限于某种语言或者数据库。

乐观锁的概念: 乐观锁指的是在操作数据的时候非常的乐观,乐观的认为被人不会同时修改数据,因此乐观锁默认不会上锁的,只有在执行更新的时候才会去判断在此期间别人是否修改了数据,如果别人修改了数据则放弃操作,否则执行操作。,
冲突比较少的时候,使用乐观锁,由于乐观锁的不上锁特性,所有在性能方面要比悲观锁好,比较适合在db的读大于写的业务场景。
悲观锁的概念:悲观锁指的是在操作数据库的时候比较悲观,悲观的认为别人一定会同时修改数据,因此悲观锁在操作数据库的时候是直接把数据上锁的,直到操作完成后才会释放锁,在上锁期间其他人不能操作数据。
冲突比较多的时候,使用悲观锁对于每一次数据修改都要上锁,如果在db读取需要比较大的情况下有线程在执行数据修改操作会导致读操作全部被挂载起来,等修改线程释放了锁才能读取到数据,体验感差。所以 比较适合用在db写大于读的时候

读取频繁使用乐观锁,写入频繁使用悲观锁。
乐观锁的实现方式:
乐观锁的实现方式主要有2种,一种是cas(Compare and Swap,比较并交换)机制,一种是版本号机制。
CAS机制:
CAS操作包括了三个操作数,分别是需要读取的内存位置(V)、进行比较的预期值(A)和拟写入的新值(B),操作逻辑是,如果内存位置V的值等于预期值A,则将该位置更新为新值B,否则不进行操作。另外,许多CAS操作都是自旋的,意思就是,如果操作不成功,就会一直重试,直到操作成功为止。
版本号机制:
版本号机制的基本思路,是在数据中增加一个version字段用来表示该数据的版本号,每当数据被修改版本号就会加1。当某个线程查询数据的时候,会将该数据的版本号一起读取出来,之后在该线程需要更新该数据的时候,就将之前读取的版本号与当前版本号进行比较,如果一致,则执行操作,如果不一致,则放弃操作
悲观锁的实现方式:
悲观锁的实现方式也就是加锁,加锁既可以在代码层面(比如java中的关键字synchronized关键字),也可以在数据库层面(比如MySQL中的排它锁)。
乐观锁和悲观锁并没有优劣之分,他们有各自的适合场景。

23、volatile是如何实现可见性和有序性的

可见性是指当多个线程访问同一个变量时,一个线程修改了变量的值,其他线程能够立即看得到修改的值。
缓存一致性协议
现代处理器为了提高处理速度,在处理器和内存之间增加了多级缓存。处理器不会直接去和内存通信,将数据读取到内部缓存中进行操作。由于引入了多级缓存,就存在缓存数据不一致的问题。
缓存一致性协议:
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改的时候,会强制重新从系统内存里把数据读取到处理器缓存里。
volatile 保证有序性的原理。
为了性能优化,jvm会在不改变数据依赖性的情况下,允许编译器和处理器对指令序列进行重排序,而有序性问题指的是程序代码执行的顺序与程序员编写程序的顺序不一致,导致结果不正确的问题。而加了volatile修饰的共享变量,则通过内存屏障解决了多线程下有序问题。

24、有没有用过java泛型,谈谈你对泛型的理解以及它的应用场景

 泛型的本质是为了参数化类型(通过泛型指定的不同类型来控制形参具体限制的类型)
java中的泛型,只在编译阶段有效。在编译的过程中,正确的检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。
泛型的作用
使用泛型能写出更加灵活通用的代码,使代码重用更容易实现‘。
  有时项目中存在对大量的重复代码,这些重复的代码中仅仅类型是不同的,泛型能很好的解决这个问题(通过继承一个使用泛型的通用父类)
泛型将代码安全性检查提前到编译期。
使用泛型后,能让编译器在编译的时候借助传入的类型参数检查对容器的插入,获取操作是否合法,从而将运行时ClassCastException转移到编译时。

泛型能够省去类型强制转换
在JDK1.5之前,Java容器都是通过将类型向上转型为Object类型来实现的,因此在从容器中取出来的时候需要手动的强制转换。加入泛型后,由于编译器知道了具体的类型,因此编译期会自动进行强制转换,使得代码更加简洁优雅。

25、一个类中两个有@Async注解的方法互相调用,请问会不会出现失效的问题?为什么?

会失效,把这2个方法分开到不同的类中,把注解加到类名上面

26、redis有哪些数据类型

redis常用的五大数据类型:
String(字符串):string 是redis的最基本的类型,一个key对应一个value.String类型是二进制最安全 ,意思是redis的String可以包含任何数据,比如jpg图片或者序列化的对象。String类型是Redis最基本的数据类型,String类型的值最大存储512Mb.
Hash(哈希)
Redis hash 是一个键值对集合
List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。
Set(集合)
Redis的set是String类型的无序集合
zset(sorted set: 有序集合)
Redis zset和set一样也是String类型元素的集合,且不允许有重复的成员。

28、场景题:如果现在要统计一个班级的学生分数排行用Redis的那种数据类型?

  redis有序集合 zset类似于java的SoretedSet和HashMap的结合体,一方面他是一个set,可以保证内部value的唯一性,另一方面他可以给每个value赋予一个score代表这个score的排序权重。
image.png

29、Redis如何保证数据不丢失

redis是内存数据库,所有操作都在内存上完成。内存的话,服务器断电,内存上面的数据就会丢失了,
Redis中引入了持久化来避免数据的丢失,主要是有2种持久化的方式RDB持久化和AOF持久化。
AOF(Append Only File):通过保存数据库执行的命令来记录数据库的状态。
AOF日志对数据库命令的保存顺序是,Redis先执行命令,把数据写入内存,然后在记录日志。
 1,后写记录日志,能够避免记录到错误的命令。因为先执行命令,后写入日志,只有命令执行成功了,命令才能被写入到日志中。
2,避免阻塞当前的写操作,是在命令执行后才记录日志,所以不会阻塞当前的写操作。
AOF的潜在风险
1,如果命令执行成功,写入日志的时候宕机了,命令没有写入到日志中,这时候就有丢失数据的风险了,因为这时候没有写入日志,服务断电后,这部分数据就丢失了。
 2, AOF的日志写入也是在主线程进行的,如果磁盘的压力很大,写入速度变慢了,会影响后续的操作。
 这2种情况可通过调整AOF文件的写入磁盘的时机来避免
AOF文件的写入和同步
AOF文件持久化的功能分成三个步骤,文件追加(append)文件写入,文件同步(sync)
AOF文件在写入磁盘之前是先写入到aof_buf缓冲区中,然后通过调用flushAppendOnlyFile将缓冲区中的内容保存到AOF文件中。
 写入的策略通过appendsync 来进行配置。
Always: 同步写回 每次写操作命令执行完后,同步将AOF日志数据写回硬盘;
Everysec:每秒写回 每次写操作命令执行完后,先将命令写入到AOF文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
No:操作系统控制的写回 Redis 不在控制命令的写回机制,交由系统制,每次写操作命令执行完成后,命令会被放入到AOF文件的内核缓冲区,之后什么时候写入到磁盘,交由系统控制。
AOF文件重写机制
因为每次执行的命令都会写入到AOF文件中,随着系统的运行,越来越多的文件会被写入到AOF文件中,这样AOF文件势必会变得很大,Redis中引入了重写的机制。
因为 AOF 文件中记录的是每个命令的操作记录,举个,比如当一个键值对被多条写命令反复修改时,AOF文件会记录相应的多条命令,那么重写机制,就是根据这个键值对当前的最新状态,为它生成对应的写入命令,保存成一行操作命令。这样就精简了 AOF 文件的大小。简单来讲就是多变一,就是把 AOF 中日志根据当前键值的状态,合并成一条操作命令。
RDB持久化
RDB实现方式是将存在Redis内存中的数据写入到RDB文件中保存到磁盘上从而实现持久化的。
和AOF不同的是RDB保存的是数据而不是操作,在进行数据恢复的时候,直接把RDB的文件读入到内存,即可完成数据恢复。
Redis中对于如何备份到RDB文件中,提供了2种方式;
1,save:在主线程中执行,不过这种会阻塞Redis服务进程;
2,bgsave: 主线程会fork出一个子进程来负责处理的RDB文件的创建,不会阻塞主线程的命令操作,这也是Redis中RDB文件生成的默认配置。
对于 save 和 bgsave 这两种快照方式,服务端是禁止这两种方式同时执行的,防止产生竞争条件。





Redis 中可以使用 save 选项,来配置服务端执行 BGSAVE 命令的间隔时间





## Save the DB on disk:##   save  ##   Will save the DB if both the given number of seconds and the given#   number of write operations against the DB occurred.##   In the example below the behaviour will be to save:#   after 900 sec (15 min) if at least 1 key changed#   after 300 sec (5 min) if at least 10 keys changed#   after 60 sec if at least 10000 keys changed##   Note: you can disable saving completely by commenting out all "save" lines.##   It is also possible to remove all the previously configured save#   points by adding a save directive with a single empty string argument#   like in the following example:##   save ""save 900 1save 300 10save 60 10000




save 300 10 就是服务端在300秒,读数据进行了至少10次修改,就会触发一次 BGSAVE 命令




image.png

32、Redis怎么保证数据同步问题

方案1
做缓存,就要遵循缓存的语义规定:
读:读缓存redis,没有,读mysql,并将mysql的值写入到redis。
写:写mysql,成功后,更新或者失效掉缓存redis中的值。
对于一致性要求高的,从数据库中读,比如金融,交易等数据。其他的从Redis读。

这种方案的好处是由mysql,常规的关系型数据库来保证持久化,一致性等,不容易出错。

方案2
这里还可以基于binlog使用mysql_udf_redis,将数据库中的数据同步到Redis。

但是很明显的,这将整体的复杂性提高了,而且本来我们在系统代码中能很轻易完成的功能,现在需要依赖第三方工具,而且系统的整个边界扩大了,变得更加不稳定也不好管理了。
Redis同步到数据库
也就是说将Redis中的数据变化同步到数据库,那么这里是将Redis做为db,而真的db,数据库只作为备份。(注意,这里是一种不同看待事物的方式)。

这样做的好处是:大大减小了数据库的压力,但是用redis做内存数据库,状态很不稳定。

虽然redis也有持久化机制,但是redis集群宕机后的重启,数据加热都很耗时。

另一方面,随着大量插入或者更新导致redis持久化操作会严重拖累作为内存KV数据库的优势。

方案1
将redis变更复制一份,丢到队列中,给mysql消费
很明显这种方案,只能保证最终一致性,而且变更数据复制,队列维护,这些杂七杂八的东西太复杂,抛弃。

具体做法是:写redis时,同时将数据写到redis维护的另外一个队列中,但这样又要增加内存消耗了。

其实还有一种方式是使用redis的pipeline通知机制,但是redis是不保证的一定通知到的(得到被通知方的ack)。

方案2
定时刷新redis中的最新数据到mysql。

很明显的,无论定时任务的间距有多小,都会留下时间缝隙,如果发生宕机,故障等都会造成数据的不一致性。

虽然可以通过:比较redis和数据库中的数据,同步那些需要同步的变化数据,但是会加大计算量和程序的复杂度。

33、Redis的集群(cluster模式)环境是怎么实现数据同步的?

Redis 有三种集群模式,分别是:主从模式,哨兵模式,CLuster模式;
 主从模式是三种模式中最简单的,在主从复制中分为主数据库(master)和从数据库(slave),若master出现宕机,需要手动配置slave转为master.
后来为了高可用提出来哨兵模式,可以选择出slave转为master,提升可用性,但不能动态扩充。
后来又有了cluster集群模式。
一,主从复制
命令: redis-server --replicaof 127.0.0.1 7001
执行该命令后,当前服务器得redis-server就会变成127.0.0.1 7001的从数据了,使用从数据库可以使得数据更加安全。从数据库具有只读属性,不能写。
主数据库要给从数据库同步数据才能使用。
二,数据同步
1,全量数据同步
 1,从数据库连接到主数据库;
 2,从数据库发送到ping命令,主数据回复pong命令
  发送ping主要主要为了检测主数据库是否有响应。
3,从数据库发送一个同步命令,主数据库进行异步发送数据
 无论主数据采用aof还是rdb的持久化方式,在主从复制的时候都以rdb的数据格式进行复制。
4, 从数据库根据rdb调整内存后,发送给主数据库完成同步命令。
2 增量数据同步
在从数据库连接着主数据库的情况下,由于网络原因,某些数据出现主从不一致的情况,这时候可以容忍数据不一致的情况,通过设置环形缓冲区,将网络抖动的时候的数据先写入环形缓冲区,从数据库会记录一个偏移值,在主数据发送数据前,从数据先把偏移值发送给主数据库,看偏移值是否在环形缓冲区中,然后在将环形缓冲区中便宜值到起始地址的数据发送到从数据库中。
3、服务器 RUN ID
无论主库还是从库都有自己的 RUN ID,RUN ID 启动时自动产生,RUN ID 由40个随机的十六进
制字符组成;
当从库对主库初次复制时,主库将自身的 RUN ID 传送给从库,从库会将 RUN ID 保存;
当从库断线重连主库时,从库将向主库发送之前保存的 RUN ID;

从库 RUN ID 和主库 RUN ID 一致,说明从库断线前复制的就是当前的主库;主库尝试执行 增量同步操作;
若不一致,说明从库断线前复制的主库并不时当前的主库,则主库将对从库执行全量同步操作;
相比全量数据同步,增量数据同步还多了数据检查。从数据库连接主数据库,使用了runid,而不仅仅是ip,port。因为如果从数据库所在服务器宕机了,换了另一台,使用ip、port,那又怎么知道该增量多少呢,复制到哪个阶段,而使用runid可以唯一标识从数据库。
4, 复制偏移量 offset
主从都会维护一个复制偏移量;
主库向从库发送N个字节的数据时,将自己的复制偏移量上加N;
从库接收到主库发送的N个字节数据时,将自己的复制偏移量加上N;
通过比较主从偏移量得知主从之间数据是否一致,偏移量想同则数据一致,偏移量不同则数据不一致
5,环形缓冲区:
本质: 固定长度先进先出对列
因某些原因(网络抖动或从库宕机),丛库与主库断开连接,避免重新连接后开始全量同步,在主库设置了一个环形缓冲区;该缓冲区会在从库失联期间累计主库的写操作;当从库重连,会发送自身的复制偏移量到主库,主库会比较主从的复制偏移量;
若从库offset还在复制积压缓冲区中,则进行增量同步;
否则,主库将对从库执行全量同步。
# redis.conf
# 环形缓冲区的大小 为1mb
repl-backlog-size 1mb
# 如果所有从库断开连接 3600 秒后没有从库连接,则释放环形缓冲区
repl-backlog-ttl 3600
大小确定: disconnect_time * write_size_per_second
disconnect_time :从库断线后重连主库所需的平均时间(以秒为单位);
write_size_per_second :主库平均每秒产生的写命令数据量;

偏移量会出现环绕回去,变成0的情况吗?偏移量最大为2 64 2^{64}2 
64
,就算一天增加1个亿,也要好几千年,不会存在这种问题。





34、场景题:商品秒杀环境下,100万用户下单抢夺10件件商品,怎么保证商品的库存不出现超卖并且cpu还不能飙高?

将数据存入到redis,100万用户去redis取数据。

34、场景题:用户下单购买商品,如果超过30分钟没有支付,该笔订单作废,这个功能该怎么设计?

使用死信队列;

35、谈谈分布式架构中hystrix的熔断机制

1,Hystrix遵循的设计原则是防止任何单独的依赖耗尽资源,过载立即切断并快速失败,防止排队;尽可能提供回退以保护免受故障。防止整个依赖客户端执行失败,而不仅仅是网络通信。我们可以把熔断器想象为一个保险丝,在电路系统中,一般在所有的家电系统连接外部供电的线路中间都会加一个保险丝,当外部电压过高,达到保险丝的熔点的时候,保险丝就会被熔断。
2 Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用是被,比如超时,异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

36、Sentinel的流控模式有哪几种

Sentinel 共有三种流控模式:
直接模式: 统计当前资源的请求,触发阀值时对当前资源直接限流,也是默认的模式。
关联模式: 统计与当前资源相关的另外一个资源,触发阀值时,对当前资源限流。
链路模式:统计从指定链路访问到本资源的请求,触发阀值时,对指定链路限流。


37、场景题:如用户支付时需要修改订单状态,同时用户要查询订单。
查询和修改操作会争抢数据库锁,产生竞争。业务需求是优先支付和更新订单的业务,因此当修改订单业务触发阈值时,需要对查询订单业务限流,这种场景下需要使用Sentinel的哪种流控模式?

关联模式: 统计与当前资源相关的另外一个资源,触发阀值时,对当前资源限流。

38、JWT技术如何实现活跃用户token过期自动续期,做到用户无感知,而不活跃的用户token过期则提示他重新登录

用双jwt技术,一个过期马上用新的。

39、场景题:一款app首页加载很慢,要5分钟才能全部加载出来,首页有10多个模块一直在转圈圈,
请你设计一个优化方案如何修改java代码(不考虑前端),才能提升用户体验(提示:数据库,java多线程)

添加缓存和多线程加载数据

40、场景题:现在有个模块的查询很慢,sql语句的where条件是动态的,是一条动态sql一定存在不走索引的情况,请你用你能想到的所有方法进行优化(不能将mysql表的所有字段都加索引),提升查询性能

缓存到redis,查询redis.

42、java 中动态加载和静态加载有什么区别?new 一个对象是动态还是静态?

class.forName("")是动态加载类,而 new class() 是静态加载类。本质区别是,静态加载类的时候编译时类必须存在,而动态加载时候类不一定存在。  


43、String类型有最大长度吗?

 String 的长度是有限制的。
 编译期的限制: 字符串的utf-8编码指的字节数不能超过65535,字符串的长度不能超过65534;
运行时的限制: 字符串的长度不能超过2^31-1,占用的内存数不能超过虚拟机能够提供的最大值。

44、java是面向对象的语言,请问在java中那些不是对象?

java 是面向对象的语言,但是java中有些不是对象。 
 基本类型不是对象,boolean char short int long float double 的值都不是对象。
但是所有的数组类型,无论是对象数组还上班基本类型的数据都扩展了object类
java 不是一种纯面向对象的语言。

45、java中接口可以多继承接口吗?

 在java编程语言中,java接口是一种抽象类型,是抽象方法的集合。接口通常以interfance来声明。并且java接口对于程序的运行有重大的意义。java接口可以继承多个接口,具体还有以下说明:
1,一个接口可以继承多个接口,
2, 一个类可以实现多个接口;
 3,一个类只能继承一个类,这就是java的继承特点。
接口的继承使用extends关键字

46、抽象类可以继承普通类吗?

抽象类可以继承普通类,抽象类与普通类的区别仅仅是抽象类本身不能实例化和允许含有抽象方法,必须通过有其他类继承他来工作。除此之外没有其他的特殊方法。

你可能感兴趣的:(15)