JAVA软件开发面试基础一

本文涵盖了Java的基础知识,包括代理、反射、多态、equals与hashcode、自动装箱拆箱、StringBuffer和StringBuilder。深入讲解了集合类如ArrayList、LinkedList、HashMap及其原理,分析了线程和进程的区别、synchronized、volatile关键字、多线程相关概念,如线程池和AQS。同时,讨论了JVM内存、对象创建、垃圾收集,以及数据库的索引、隔离级别等。此外,还涉及了Redis、Spring框架、网络模型、HTTP协议等。文章适合Java开发者阅读,为面试做准备。
摘要由CSDN通过智能技术生成

Java

基础知识

代理、反射和多态

equals和(==)和hashcode

  • 我们在用的时候,如果比较的是基本数据类型,比较的就是它们的值,如果比较的是引用数据类型,那我们比较的就是对象的的地址是否一致,equals的话,不能对基本数据类型进行比较,在没有重写equals方法的时候其实和==一样,比较的是对象的地址,但是我们可以通过从重写equals方法来让它比较两个对象的值,重写equals通常有必要重写hashcode方法。hashcode就是把对象的地址通过计算返回一个整数,它是针对散列存储结构的,比如hashmap、hashset、hashtable这些,我们比较对象的时候就是先比较它的hashcode是否相等,如果相等再进一步判断地址是否相等,如果hashcode都不相等,那这两个对象肯定就不相等了。

自动装箱和拆箱

StringBuffer和StringBuilder


集合

ArrayList和LinkedList的区别

  • ArrayList底层是一个object数组,而LinkedList是一个双向链表,ArrayList在知道index情况下查询和修改是效率最高的,而LinkedList则需要从头到尾遍历节点进行查询,但是在增加和删除的时候,ArrayList默认是插入到最后一位,如果我们指定了中间的某个位置ArrayList就需要开辟新的数组,然后移动把指定插入位置后的元素都往后移一位,然后再插入,开销比较大,而实际上LinkedList在插入的时候也只有头插和尾插快,如果是在中间插入的话,还是要一个一个遍历节点找到位置然后插入,开销远比ArrayList大,只有它的头插是比ArrayList的头插快的多的。除此之外LinkedList要消耗更多的空间来存放前驱后继数据等。然后这两个Collection都是线程不安全的。
  • 如果是想要线程安全的话,有一个比较老土的方法是Vector,它的所有方法都上了锁,实现了同步,一个线程访问Vector的时候要进行同步会产生的开销很大,所以现在用的比较少了。

HashMap

  • HashMap是我比较常用的一个集合,是由数组加上链表的结构组成的,实际上也可以说它是一个数组,使用了拉链发加上链表来解决哈希冲突的问题。在java1.7的时候是数组加上链表,然后使用的是头插法,1.8的时候改成了数组加上链表加上红黑树的结构,采用的是尾插法。因为使用头插法在rehash和resize的过程中会出现null值得丢失,会出现死循环。1.8的HashMap在链表的长度超过8也就是默认阈值的时候会自动转成红黑树,提高了效率。
  • HashMap的扩容机制是这样的,它的默认容量是16,负载因子是0.75,当存储的数量达到容量乘上负载因子的时候就会触发扩容,也就是说16×0.75=12的时候出触发扩容。扩容的时候就是创建一个新的数组,容量是容量是之前的两倍,再把所有的元素的Hash值都重新计算也就是rehash的过程,最后放进新的HashMap里。
  • HashMap是线程不安全的,如果涉及到线程安全的话,HashTable和ConcurrentHashMap都是线程安全的,但是HashTable现在已经基本弃用了,它的所有方法都经过了synchronized修饰,它用的是全表锁,就是一把锁锁整张表,如果有一个线程访问的时候,其他线程是不能访问的,只能进入阻塞或者轮询的状态,效率非常低。而ConcurrentHashMap在jdk1.7的时候采取了分段锁,是由Segment和HashEntry组成的,Segment实现了ReentrantLock,HashEntry是一个链表结构的元素,整个看起来有点像HashMap,只不过Segment是带了锁的,当我们要对Entry里面的元素进行修改时,首先要获得Segment的锁。然后在1.8的时候ConcurrentHashMap就取消了segment分段锁,改成了CAS加上synchronized来实现线程安全,它的数据结构也就变成了数组加链表加红黑树的结构,跟HashMap类似,链表长度超过8的时候就把链表转化为红黑树,synchronized锁住了链表或者红黑树的头节点,这样只要不发生hash冲突就不会产生并发,效率提高了很多。
  • Hash冲突是指我们在计算Hash值的时候可能会出现相同的情况,而HashMap采用拉链法解决这种问题,如果计算出Hash值重复就判断当前位置的key是否相同,如果相同的话就直接覆盖,不同的话再用链表的形式存储。HashMap长度是2的倍数的原因则是hash值的范围太大,内存放不下,我们是对其进行取模运算,就是n-1&hash,这是HashMap长度是2的倍数的原因。

Hash

  • Hash就是一种转换方法,把任意长度的输入通过一定的计算方法转换成固定长度的输出。解决Hash冲突的方法有四种,一个是开放定址法,就是为产生冲突的地址求一个地址序列,即把它放到后面的地址上去(线性探测,平方探测,随机探测)一个是HashMap里的链表散列法,如果遇到相同的Hash地址,就按链表来连接。一个是再哈希,就是重新计算hash值直到不冲突为止。还有一个就是计算公共溢出区,把冲突的都放在一个地方,不在表里。
  • Hash的一些使用:涉及到分布式的系统,就会有负载均衡和数据分布的问题。1、Hash取模,比如有三台服务器,我们要做负载均衡,就可以把请求的用户的ip地址计算出hash值,然后对3取模,余数为几就放到哪台服务器上。缺点就是如果新增了服务器,那所有的绝大多数的请求都要重新映射,变动太大了。2、一致性Hash,一致性哈希是以232为除数来取模,从0到232-1来形成一个环。我们先对服务器地址进行Hash计算,然后对2^32求余,得到它在环中的位置,然后来了一个请求,用同样的方式计算它在环中的位置,然后顺时针找到第一个节点,这个节点就处理这个请求。缺点就是节点数量比较少的时候会出现分布不均匀的情况,解决的办法是在Hash环上增加虚拟节点。

HashSet

  • HashSet它是一个由HashMap支持的容器,它存储唯一元素不允许为空值,并且不保证顺序插入,其实就是一个皮包公司,它对外接活,接到了就丢给HashMap处理,可以理解为是HashMap的包装,比如它的add方法就是基于HashMap的put方法实现的,只不过HashMap是kv存储,而HashSet是存储对象的,它把对象储存在HashMap的key上,value值默认为空,除此之外它的remove方法、contains方法,还有其它的一些方法,都是基于HashMap来实现的,因为是把对象存储在key上,所以HashSet内部的元素是无序的,如果要有序的话可以使用LinkedHashSet。

CopyOnWriteArrayList

  • 它其实就是运用了写入时复制(CopyOnWrite,简称COW)思想,就是如果有多个调用者对同一份资源呢进行操作的时候他们会获取相同的指针指向同一份资源,当进行修改时系统会创建一份专用副本给这个调用者,而其他调用者见到的还是初始的资源不变。CopyOnWriteArrayList在add的时候就是,在方法的最前面加了锁,这里用了ReentrantLock,然后创建副本数组进行操作,这样才能保证只创建单个副本,而读的时候不需要加锁,即便有多个线程进行写操作读到的还是旧数据。它内部有一个用volatile transient修饰的数组,一个线程在读取数组的时候总能看到其他数组对这个数组最后的写入,这样就保证了add前后的一致性,但是CopyOnWriteArrayList也有缺点,缺点在于内存占用比较大,并且只能保证数据的最终一致性,不能保证实时一致性。

多线程

线程和进程的区别

  • 进程时程序的一次执行过程,它是系统进行资源调度和分配的基本单位,进程的实体是由程序段数据段还有PCB块组成的,创建进程实际上就是创建实体对应的PCB块,撤销就是撤销PCB块。而线程是比进程更小的资源调用单位,也可以说是轻型进程,一个进程中可以拥有多个线程,与进程不同的地方在于线程共享进程的堆和方法区资源,私有自己的程序计数器,虚拟机栈和本地方法栈,线程在切换的时候负担比进程小得多。
  • 线程的生命周期有六个状态,分别是新建、就绪、运行、阻塞、死亡。
  • 上下文切换是指CPU时间片切换到另一个任务前会先保存自己的状态,以便下次再切换回这个任务是可以再加载这个任务的状态,也就是说任务的保存再到加载的过程就是一次上下文切换。

synchronized关键字

  • synchronized关键字是JVM层面的操作,它修饰实例方法的时候锁住的是一个对象,修饰静态方法和代码块的时候锁住的是当前类。synchronized同步语句块用的是monitorenter和monitorexit指令,两个分别指向代码块的头和尾,线程试图获取锁其实就是试图获取monitor对象,monitor对象存在于每个java对象的对象头中,如果锁计数器为0,执行monitorenter指令的时候锁计数器会置1,这时候其他线程试图获取就会失败,当我们执行到monitorexit指令的时候,锁计数器就会置0,表示锁被释放。synchronized同步方法的时候用的是ACC_SYNCHRONIZED标识,它告诉JVM这个方法是不是一个同步方法,然后再执行相应的同步操作。
  • 在jdk1.6的时候synchronized进行了优化,引入了偏向锁、轻量级锁、自旋锁、重量级锁这些,最开始的时候资源是无锁状态,当出现第一个线程要获取锁的时候锁就会升级为偏向锁,偏向于这个线程,意思就是在没有被其他尝试获取的时候取消同步。如果出现锁竞争比较激烈的情况,每次持有锁的线程可能都不一样,那锁就会升级为轻量级锁,轻量级锁是采用了CAS操作来替代互斥量。如果轻量级锁失败以后,为了避免线程在操作系统层面挂起,还加入了一层锁自旋的手段,就是争夺锁的线程进入忙循环的状态,因为一般线程持有锁的时间不会太长,所以让进行一个原地自旋等待能减小开销,但是如果超过一定的自旋次数还没获得,锁就会升级为重量级锁了,因为长时间的原地自旋对CPU也是一种消耗,这个升级的过程是不可逆的。
  • 自适应自旋就是根据上一次锁的自旋时间和拥有者的状态来决定这次的自旋时间,锁消除就是JVM在编译的时候检测到共享数据不存在竞争了,就把锁给消除。
  • 它和ReentrantLock的区别:两者都是可重入锁,就是自己可以再次获取自己内部的锁,获取之后锁计数器会再次加一,释放了减一,一直到锁计数器为零了算是完全释放。synchronized依赖于JVM而ReentrantLock依赖于API,也就是说ReentrantLock是JDK层面实现的,需要lock和unlock来手动实现,而synchronized则是自动释放。除此之外还有一个就是ReentrantLock增加了一些新的功能,比如等待可中断,也就是说等待的线程可以选择放弃等待改为处理其他事情。可实现公平锁,它可以指定锁是公平锁还是非公平锁,而synchronized只能是非公平锁,公平锁就是先等待的线程先获取锁。可实现选择性通知,就是ReentrantLock可以在Condition里指明要通知的线程对象,而synchronized则是在notify和notifyall的时候通知的对象由JVM选择。

volatile关键字

  • volatile关键字修饰的变量能保证对不同线程的可见性,它越过了本地内存,直接将变量写入主存中,读取也是直接从主存中读取。因为我们知道java的内存模型里,线程和主存之间还存在一个工作内存,如果一个线程在主存中修改了一个变量,而另一个线程还在使用这个变量在本地内存中的拷贝,就会造成数据的不一致,所以volatile关键字很好地解决了这种问题。它还有一个作用就是防止指令重排。但是volatile只能保证单词读写的原子性,对于i++这样的操作不能保证它的原子性,要解决这个问题我们可以使用锁或者是原子类。volatile关键字的底层是对总线上传输的数据不断进行嗅探和CAS循环,如果大量使用volatile可能会引起总线风暴。
  • 总线嗅探:这个要从缓存一致性原则说起,因为CPU和主存之间我们设置了多级缓存,而CPU操作的是缓存中的数据,这就有可能会出现多个CPU操作的缓存数据不一致的情况,因此设置了缓存一致性原则,就是每个处理器不断对总线上传播的数据进行嗅探,来检查自己的数据是不是过期了,如果发现缓存行对应的内存地址不一致,就会将缓存行置为无效,要操作这个数据时会从主存中重新读取。
  • 禁止指令重排:JVM会对指令进行重排序,在不改变程序结果的情况下提高效率,遵守as-if-serial原则(不管怎么重排序,单线程下的执行结果不能被改变)。而volatile关键字实现禁止指令重排是通过插入内存屏障的方式,volatile写是分别在指令前后加上内存屏障,读是在后面加上两个。
    -happens-before原则:一个操作happens-before于另一个操作那么第一个操作的结果将对第二个操作可见,如果两个操作之间存在happens-before关系,那么如果出现指令重排后的结果和happens-before一样,那么这种重排是允许的。

CAS

  • CAS就是字面上的意思,compare and swap ,它有三个操作数,内存地址V,旧的预期值A和新的目标值B,当我们在进行CAS操作的时候,会比较内存地址V里面的值和旧的预期值A是否相等,如果相等就可以把V里面的值修改为B,否则就什么都不做。它存在了一些问题,比如我们通常将CAS搭配循环来使用,循环时间长了CPU的开销会很大。还有就是只能保证一个变量的原子性,还有一个就是ABA问题,就是比如我在内存地址V中初次读取到的值为A,准备赋值的时候检查到它的值还是为A,但是实际上在这个中间它被修改过又改回来了,这样我们是检查不到的,解决这个问题的办法可以是加上一个标识符或者版本号来解决。

synchronized和volatile的区别

  • volatile关键字只能用于变量synchronized可以用在方法和代码块上。多线程访问volatile不会发生阻塞,synchronized就会。还有就是volatile能保证数据的可见性,但不能保证原子性,synchronized两个都能保证。

ThreadLocal

  • ThreadLocal就是线程专属本地变量,就是说如果我创建了一个ThreadLocal变量,那每一个访问这个变量的线程都会有这个变量的本地副本,并且通过get和set方法来获取或设置。它的底层原理是我们的ThreadLocal内部存在一个ThreadLocalMap静态内部类,它类似于HashMap,我们存储的key是当前的ThreadLocal对象,值是我们想要存储的任意值,Thread对象保持着对ThreadLocalMap的弱引用。ThreadLocal存在一个内存泄漏的问题,因为我们设置的key是ThreadLocal的弱引用,而value是强引用,在我们的ThreadLocal没有被外部强引用的情况下GC的时候key就会被清理,留下key为null的value,无法被访问,这时候就出现了内存泄漏。ThreadLocalMap其实考虑了这种情况,在调用set、get、remove方法的时候就会清理掉key为null的记录,不过还是在使用完ThreadLocal后手动调用一个remove方法比较好。
  • 为什么是弱引用?因为如果ThreadLocalMap持有ThreadLocal的强引用,如果ThreadLocal的对象回收了,如果我们不手动删除,ThreadLocal仍然不会被回收这里就出现了内存泄漏。而如果是弱引用,ThreadLocal的对象被回收了,留下了key为null的value,这里就出现了内存泄漏,但是两者不同的地方在于,因为ThreadLocalMap生命周期和Thread一样长,强引用如果不手动删除就会一直存在,而弱引用多了一层保障,就是在我们下次调用set、get、remove的时候就会清除。

线程池

  • 线程池好啊,它是管理线程的一种手段,通过重复利用已创建的线程来降低创建和销毁线程的损耗,提高响应速度还对线程进行统一的分配和监控。它有三个比较重要的参数,一个是核心线程数,一个是最大线程数,一个是缓冲队列。它的线程复用是通过重写Thread类,在start方法里不断循环调用传过来的Runnable对象来实现线程的复用。它的工作过程是,当我们调用excute方法添加一个任务时,如果这时的任务书小于核心线程数,那就马上创建线程运行这个任务,如果大于核心线程数,就把它放进缓冲队列里,如果缓冲队列满了,但小于最大线程数,那就创建非核心线程来执行,如果超过了最大线程数就会抛出拒绝添加的异常了,在线程池完成所有工作后,它最终会收缩到核心线程数的大小。
  • 拒绝策略吗?我了解的不多,知道一个是丢弃最老的一个请求,一个是默默丢弃无法处理的请求,还有一个是直接抛出异常。
  • 缓冲队列吗?我记得常用的有两个,一个是LinkedBlockingQueue,它是无界的,可能会内存溢出。一个是ArrayBlockingQueue,有界的,通过加锁保证安全,队列不满就唤醒。
  • 线程池参数设置:根据三个参数来判断,一个是tasks(每秒需要处理的最大任务数量),一个是tasktime(单线程执行任务耗时),一个是responsetime(系统允许任务的最大响应时间),举个例子就是比如我有100到1000个任务,每个人物的tasktime是0.1秒,那我就需要10到100个线程,那么我的核心线程数(corePoolSize)就设置为10个,具体数字还有个8020原则,就是80%的情况下每秒任务数,就是看80%的情况下每秒任务数是多少,如果是200,那corePoolSize就设置成20。然后第二个就是缓冲队列的长度,长度是根据你的核心线程数来设定的,比如刚才算出来的corePoolSize是20,那就用20/0.1再*2,就是400,最后一个就是最线程数,最大线程数就是用刚才最大的1000个任务减去缓冲队列的400个再乘上0.1,就是60个。keepalivetime可以根据任务峰值持续时间来设定。

AQS

  • AQS是AbstractQueuedSynchronizer,是一个构建锁和同步器的框架,内部是一个volatile修饰的state变量和一个FIFO队列,然后很多并发类比如ReentrantLock、CountDownLatch、CyclicBarrier都是基于它实现的,比如ReentrantLock,它把state初始化为0,表示为未锁状态,一个线程lock时调用tryAcquire方法独占这个锁并将state+1,然后其他线程再tryAcquire的时候就会失败,直到unlock到state=0。其他两个我就没有深入看过了。

JVM

Java内存

  • Java的内存空间分为五个部分,分别是堆,方法区,程序计数器,虚拟机栈,本地方法区。堆和方法区是线程共享的,虚拟机栈,程序计数器,本地方法区是线程私有的。堆存放我们的对象以及数组,方法区放的是我们的类信息、常量(static final)、静态变量(static)。程序计数器是线程的字节码解释器,它取出指定指令地址中的指令来执行,然后指向下一条地址,虚拟机栈中有我们的局部变量表、操作数栈、动态链接、方法出口等,局部变量表主要存放了我们的八种基本数据类型(short,int,long,float,double,char,boolean,byte)以及对象引用。

Java类加载过程

  • 类的加载过程是加载,验证,准备,解析,初始化,卸载,这个过程中我们还用到一个双亲委派原则,就是当类加载器收到一个类加载请求的时候,它首先会把这个请求交给父类加载器,如果父类加载器处理不了才会自己尝试加载。
  • Java的类加载器有,最顶层是Bootstrap ClassLoader,这是虚拟机识别的类库用户无法直接使用,然后是Extension ClassLoader用户可以使用,然后是Application ClassLoader,负责的是用户路径中的所有类库,如果用户没有自定义加载器那就默认使用这个,最后才到自定义类加载器。自定义类加载器的原因是系统加载器只加载指定目录下的class文件,如果想加载自己定义的class文件就可以自己定义一个。

Java对象创建的过程

  • 类加载检查分配内存初始化零值设置对象头到执行init方法
  • 首先是类加载的过程,JVM遇到一条new指令时首先会去检查这个指令的参数能不能在常量池中定位到这个类的符号引用,并且检查这个类有没有被加载解析初始化过,如果没有就要执行相应的加载过程。
  • 然后是分配内存,在堆的新生代里划分一块内存给新生的对象,分配方法有指针碰撞法和空闲列表法。
  • 然后是初始化零值,JVM将分配到的内存空间都初始化为零值。
  • 然后设置对象头,对象头包括了Mark Word,Klass Point,Monitor,Mark Word里有对象的HashCode,分代年龄还有锁标志位信息,Klass Point是指向类元数据的指针,Monitor就是之前说的synchronized方法操作的地方。
  • 最后就是执行init方法,把对象按照我们的写法初始化。

JavaGC

  • 从GC的角度我们的堆还分为新生代和老年代,新生代还分为eden、servivorFrom、servivorTo,Eden是Java新对象的初始地带,如果对象比较大就会直接进入老年区,当Eden区不够的时候就会触发一次MinorGC,结束之后如果对象还存活就会进入到servivorFrom或者servivorTo区,同时年龄加一,当年龄增加到一定程度之后就会进入老年区了。老年区存放的是比较稳定生命周期比较长的对象,老年区空间不够了就会触发MajorGC。最后还有一个就是永久代,永久代存放的就是类和元数据信息,永远不会被GC。
  • GC通过两种方法确定垃圾,一个是引用计数法一个是可达性分析法,引用计数法就是给对象添加一个引用计数器,每当有一个地方引用他计数器就加一,引用失效就减一,当计数器为零就是可以回收的对象了。而可达性分析法就是从GCRoot节点出发一直到我们的对象,走过的路径就是引用链,如果对象到GCRoot没有一条引用链就说明这个对象是可回收的。
  • 确定了垃圾之后就是回收算法,JVM主要有三种回收算法,一个是标记清除算法,就是把需要回收的对象打上标记,然后回收,缺点是碎片化比较严重,复制算法是MinorGC用的,它是把内存划分为等大小的两块,每次只用其中一块,当内存空间满了之后就把存活对象复制到另一块上区,然后把这一块全都清除,问题就是可用内存变为了原来的一半。标记整理算法是标记可回收对象,然后让所有存活对象向一端移动,然耨清理掉端以外的内存。
  • 对象引用有四种,强引用,软引用,弱引用,虚引用,一个对象如果具有强引用,它就不可被回收,如果具有的是软引用,那在内存空间足够的时候,它不会被回收,如果内存空间不足了,它就会被回收。而弱引用是存活的时间更短一点,如果被垃圾回收器扫描到,就会被回收。虚引用基本就跟没有一样,任何时候都有可能被回收。

垃圾收集器

垃圾收集器了解的不多,只知道

  • CMS收集器,CMS收集器采用多线程标记清除算法,分为四个阶段,首先是初始标记阶段,这个阶段很快,只是标记一下GCRoot可达对象,但是会暂停所有工作线程,然后时并发标记阶段,这个过程是进行GCRoots跟踪,能和用户线程一起工作,重新标记阶段,对并发标记阶段的记录进行修正,最后是并发清除,和用户线程一起工作,清除不可达对象。

数据库

数据库的索引

  • 数据库的索引其实就是为了让数据库查询更快的一种排好序的数据结构,分为普通索引、唯一索引、主键索引、聚合索引、全文索引。普通索引没有什么限制,是基本的索引类型,仅起到加速查询的作用,唯一索引不允许两行有相同的索引值,主键索引是一种特殊的唯一索引,一张表中只能有一个主键索引,联合索引是可以覆盖多个数据列。
  • 主键索引和普通索引的区别在于,主键索引在B+树上的叶子节点存放的是整行数据,而普通索引的叶子节点存放的是主键的值,主键索引我们直接搜寻B+树就可以得到,但普通索引我们得到了主键的值以后还要回表再搜寻一次,效率比较低。

数据库引擎

  • 数据库引擎分为MYISAM和InnoDB,MYISAM不支持外键、事务和MVCC,使用的是表锁,查询表行数的时候不需要全表扫描,InnoDB支持外键、事务和MVCC,使用的是行锁,查询表行数的时候需要全表扫描。这两个的区别除了上面说的几个以外还有索引方面的区别,InnoDB的数据文件本身就是索引文件,它的辅助索引data域存储的是主键的值而不是地址,而MYISAM的索引和数据文件是分开的,辅助索引和主索引没多大区别。
  • 我常用的是InnoDB,它用的数据结构是B+树,它是由B树优化而来的,B+树它的构造是这样的,它的非叶子节点上不保存指向关键字的指针,仅仅作为索引,因此能容纳的关键字数量就更多,树的高度就比B树更矮,然后我们磁盘IO的效率就更高。它的叶子节点上保存了所有父节点关键字的指针,叶子节点根据关键字的大小从小到大排列,叶子节点之间存在指针指向下一个节点,相当于一个有序链表,对B+树进行查找相当于二分查找,时间复杂度相对稳定,是logn,无论是否成功,都要经历从根节点到叶子节点的路径。
  • B+树和B树的区别在于,B树的每个节点都存储数据,非叶子节点存储关键字和指向关键字的指针,如果要存储大量的数据那树的高度就会上来,效率就变低了。B树的查询是要遍历每一层的节点,如果我们要查询的信息离根节点很近,那时间复杂度就很低,可能是O(1),如果很远,那就是O(n)了,而B+树的叶子节点保存了所有信息,非叶子节点作为索引不保存指向关键字的指针,因此能容纳更多的关键字,树的高度就更矮,磁盘IO次数就更少。它的叶子节点根据关键字的大小从小到大排列,并且叶子节点之间还有指向下一个节点的指针,查询的时候相当于对叶子节点进行遍历,在非叶子节点的帮助下我们的查询效率相当于二分查找,相对稳定,是O(logn)。
  • (为什么不用红黑树?B树?)红黑树高度太大了,查询效率不够高。

ACID

  • A是Atomicity原子性,原子性就是事务应该是一个不可分割的工作单位,事务中的操作要么都执行要么都不执行
  • C是Consistency一致性,事务前后数据的完整性都保持一致。
  • I是Isolation隔离性,是指事务并发的时候相互隔离互不干扰。
  • D是Durability持久性,指事物提交后对数据的操作应该是永久性的。

隔离级别

  • 数据库的隔离级别有四种,从低到高时读未提交、读已提交、重复读和串行化,因为不考虑隔离级别的话可能会出现脏读、不可重复读和幻读的出现,脏读就是一个事物读到了其他事务未提交的数据,也就是脏数据,不可重复读就是指一个事物多次查询同一个数据得到的结果是不同的,因为被其他事务修改过,幻读是也是读到了其他事物处理过的数据,但是和不可重复读的地方在于幻读针对是一批数据集合。再来看四种隔离级别就知道,读未提交就是读脏数据,并发最高但是一致性最差,读已提交就可以避免脏读,而可重复读就可以避免脏读和不可重复读,串行化就可以避免三种情况,但是因为所有的事务都是串行的,不存在并发,效率就非常低,所以我们的数据库默认得事可重复读。

数据库优化

  • 一个是读写分离,主库负责写,从库负责读,这里的主从复制有三种,异步复制,半同步复制,并行复制。一个是垂直分表,就是指表按列来拆分,比如一个表里面既有登录信息又有用户信息,我们就可以拆分成两张表。还有一个是水平分表,按行来分表,数据量非常大的时候可以用这种方法,但是实际上分片事务非常难进行,因为跨节点Join性能差,逻辑也复杂,通常都是用客户端分片,可以用客户端代理或者是中间件代理的方式。

SQL调优

  • 最基本的是表要有主键,有主键的表MySQL会创建聚簇索引,聚簇索引有一个好处就是主键和数据行是在一起的,你在explain查询的时候会发现它的type级别是constant,这是很高的一种级别,当有主键的时候一条SQL语句查询很慢可以查看它是否建立了相应的索引,索引的选择可以尽量是where、orderby、groupby以及join后面的字段作为索引列,并且这些索引列要排个序,符合最左匹配原则(最左优先,以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配),另外呢就是你选的时候要根据它们的索引选择器,你的非重复数据行和重复数据行间的排列,大的放左小的放右的形式,然后这种多列的形式尽量去建立联合索引而非多个单个索引。
  • 另外如果数据查询比较频繁,就可以使用覆盖索引。关于索引没有生效的问题,也可以考虑一下MySQL底层是不是出了什么问题,因为它底层是一个随机采样的过程,它会根据你的索引基数采样,但是它不可能给你全部标记上,它会根据随机采样来计算你的索引选择,有可能它采错了,虽然你的索引选择性比较大,但是它可能认为你的选择性比较小,然后就出错了,对于这种情况一般是用force index让它强制走看它行不行,如果不行的话就要刷新一下它的信息了用analyze table看它有没有重新走对。

MVCC

  • 是指多版本并发控制,就是我们在创建数据的时候添加了两个隐藏列,一个是存储上一个事物的id,一个是指向上一个版本的指针,这就是版本链,然后我们就通过版本链来控制我们的并发读和写。

Redis

redis的数据结构

  • redis有五种,分别是字符串,哈希表,列表,集合,有序集合

持久化机制(RDB和AOF)

  • 持久化机制的实现是,单独fork一个子进程,把当前父进程的数据库数据复制到子进程的内存中,然后由子进程写到临时文件中,持久化的过程结束了再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。
  • 主要有两种方式,RDB和AOF,RDB是默认的持久化方式,按照时间周期来定期把内存的数据保存到硬盘的二进制文件,即snapshot快照存储,对应的数据文件为dump.rdb,通过配置文件的save来设置时间周期。 AOF是将收到的每一个写命令都通过write函数追加到文件的最后,有点类似于MySQL的binlog,redis重启的时候会重新执行写命令来重新恢复数据库的内容,两种方式同时开启的时候,数据恢复redis会优先选者AOF恢复。

缓存穿透,雪崩,预热,更新,降级

  • 缓存穿透是指redis里没有,数据库里也没有的数据,查询的时候先发现缓存里没有,到数据库中查询发现也没有,于是本次请求失败,相当于绕过了缓存,而大量这样的缓存未命中然后请求持久层数据库就会给数据库带来很大的压力。解决的办法是布隆过滤器,布隆过滤器是一个bit数组,如果我们要映射一个值到布隆过滤器中,首先通过不同哈希函数计算多个哈希值,然后把这些哈希值对应在数组中的位置置为一。当有一个数据进来,我们通过哈希函数计算得到的哈希值,如果对应到这几个位置都为一,说明这个数据可能存在,如果有一个不为一,那就说明一定不存在。可能存在的原因是这些位置可能被其他的值通过计算置为了一。还有一种方法是把这个查询不到的数据的key-value短时间写为key-null,防止用户恶意暴力攻击。
  • 缓存击穿是指一个非常热点的key,高并发对这个key进行访问,在这个key过期的一瞬间,大量并发仍保持访问,但又读不到数据,于是高并发请求落在持久层数据库上,这样就发生了缓存穿透。解决办法是设置这个热点key永不过期,或者是用SETNX命令,key过期时对这个key上锁,等到缓存更新结束之后释放锁就行,但是SETNX命令也有过期时间,有可能请求执行还没结束SETNX命令就过期了,所以一般会加上一个守护线程,监听SETNX的状态,如果过期了就给它加上一个续命锁。
  • 缓存雪崩就是大量的缓存同时过期,然后高并发请求全都发向持久层的数据库,形成严重的后果。解决办法是设置key的过期时间不一致,或者设置热点数据永不过期,用队列来削峰也可以。
  • 缓存预热就是在系统上线后将相关的数据直接加载到缓存系统,然后用户就不用先访问数据库,再将数据缓存,直接访问预热加载好的缓存数据。

Spring

隔离级别

传播特性

生命周期

  • 我理解的是大致可以分为三个过程,首先是bean的一次加工是,它是通过BeanFactoryPostProcessor将对象实例化,执行Bean的构造器、bean的二次加工是通过依赖注入把我们配置好的属性注入给bean,然后调用BeanNameAware、BeanFactoryAware来设置容器,最后就是销毁了,调用destrory方法。

AOP

  • 面向切面编程,有两种实现方式,一种是静态代理,通过我们手动实现代理类,还有一种是动态代理就是通过JDK代理和cglib代理,如果代理对象是接口就走的是jdk代理,主要是用到了java反射中的两个类,一个是一个是Proxy一个是InvocationHandler,它通过bind的方式去绑定之前的被代理类,通过Proxy.newproxyinstance来创建代理对象,然后通过反射invoke去执行代理的方法,如果代理对象是实现类,走的是cglib,利用字节码在内存生成一个继承代理目标的类,然后生成实例化代理对象。
  • Spring AOP和AspectJ AOP 的区别在于,Spring AOP是基于代理的运行时增强,而AspectJ AOP是基于字节码的编译时增强,相比来说Spring AOP相对简单,AspectJ AOP功能更强大,如果切面较少的话,一般用Spring的就够了,如果切面很多的话会选择使用AspectJ AOP,因为它更快。

IOC

  • 控制反转和依赖注入,我的理解是它最重要的一个点就是容器,它管理bean的生命周期的控制bean的依赖注入,然后我们使用的时候就不需要自己new对象了,会由spring创建,并且它会拿到我们在xml里配置好的属性,然后自动注入。它有设计两个容器,一个是BeanFactory、一个是ApplicationContext,前面那个可以看成是一个HashMap,就k-v形式存储,key是BeanName,value是Bean实例,通常只提供put和get方法。ApplicationContext提供了一些新的功能比如资源的获取,支持多种消息。

MVC

  • MVC是一种设计模式,就是Model、View、Controller,而SpringMVC是一个很优秀的MVC框架,用SpringMVC的时候我们一般把项目分成Dao层,Entity层,Service层和Controller层。它的工作流程是这样的,首先是客户端发送来一个请求,发送到DispatcherServlet,DispatcherServlet根据请求调用HandlerMapping找到对应的Handler,然后通过HandlerAdapter来调用处理器处理请求,处理完成以后会返回ModelAndVie对象,Model是数据对象,View是逻辑对象,ViewResolver通过逻辑view找到实际的View,最后DispatcherServlet把Model传给View进行视图渲染,返还给客户端。

设计模式

  • 用到了工厂模式、单例模式、单例设计模式、模板方法模式、包装器设计模式、观察者模式、适配器模式。
//线程安全的单例模式
public class Single1{
  //懒汉式
  private static Single1 instance;
  private Single1(){}
  public static synchronized Single1 getInstance(){
    if(instance==null) instance = new Single1();
    return instance;
  }
}

public class Single2{
  //饿汉式
  private static Single2 instance = new Single2();
  private Single2(){}
  public static Single2 getInstance(){
    return instance;
  }
}
public class Single3{
  //双重锁
  public static volatile Single3 instance;
  private Single3(){}
  public static Single3 getInstance(){
    if(instance == null){
      synchronized(Single3.class){
        if(instance==null)
        instance = new Single3();
      }
    }
    return instance;
  }
}

计网

网络模型

  • 计算机网络的OSI七层模型为物理层,数据链路层,网络层,传输层,会话层,表示层,应用层,TCP/IP的五层模型则是物理层,数据链路层,网络层,传输层,应用层。

TCP/IP协议

  • TCP/IP不仅仅只有TCP和IP两个协议,它的五层模型是物理层,数据链路层,网络层,传输层,应用层。

TCP和UDP的区别

  • TCP是面向连接的可靠传输,UDP是无连接的传输不保证可靠交付。TCP的话我们通常是用在文件传输,网络会话,发送接收邮件这些地方,UDP就是用在语音,视频还有直播这些地方。然后TCP只支持一对一的连接,UDP就没那么多限制,一对一,一对多,多对多都可以。除此之外UDP分组首部开销比较小,只要八个字节,TCP要二十个字节。还有一个就是TCP是面向字节流的,它是把一连串数据转成字节流进行传输,而UDP是面向报文的,报文是它的最小分割单位。

三次握手四次挥手

  • 三次握手是为了防止已失效的连接请求报文突然又传到服务端,然后产生错误。第一次握手时客户端发送一个syn包,然后进入SYN_SEND状态,第二次握手时,服务端确认到SYN包,把SYN包加一返回的同时返回一个应答的ACK包,然后进入SYN_RECV状态。第三次握手时,客户端接收到SYN+ACK包,然后把ACK包加一返回,发送完毕之后就成功建立连接。 **四次挥手,**第一次挥手时,客户端发送一个FIN包,进入FIN_WAIT状态,第二次挥手时,服务端接收到FIN包之后发送一个ACK包应答进入CLOSE_WAIT状态,但是这个时候可能还有没有发送完的数据,客户端那边也还保持在被动接收状态,所以连接不会马上断开,当确认数据传输完毕之后才会进入第三次挥手,服务端发送一个FIN包,表示数据传输完毕不会再传了,进入LAST_ACK。第四次挥手时客户端接收到FIN包,进入TIME_WAIT状态,这个状态是为了防止丢包的,服务端接收到ACK后四次挥手就完成了。
  • 如果第三次握手失败了,服务端不会重发ACK报文,它会发送RST复位报文,并主动关闭至closed状态,防止syn泛洪攻击。泛洪攻击就是一种常见的DDOS攻击,客户端在短时间内伪造大量不存在的ip地址,发送大量的syn包,并且不做出三次握手响应,server会消耗大量资源直到超时,从而引起网络阻塞甚至瘫痪。检测syn泛洪攻击的方式很简单,就是如果server有大量半连接状态并且ip地址是随机的,就说明遭到了攻击。

TCP保证可靠传输的方式

  • 三次握手四次挥手
  • 数据分片,就是将数据截成TCP认为合理的长度,按字节编号发送。
  • 超时重发,就是TCP会设置一个计时器,如果不能及时收到确认就会重传前面的分组。
  • 确认应答,对于收到的请求给出确认响应。
  • 校验和,校验出包有错就丢弃报文段,不给出响应。
  • 序列号,对失序数据重新排序,然后才交给应用层。
  • 丢弃重复数据,如果收到了重复的数据就丢弃。
  • 流量控制,通过滑动窗口实现流量控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以接收数据。
  • 拥塞控制,就是当网络拥塞是减少数据的发送,主要采用了四种方法,
    • 慢开始(慢开始算法的思路由小到大逐渐增大拥塞窗口数值。cwnd初始值为1,每经过一个传播轮次,cwnd加倍。)
    • 拥塞避免(拥塞避免算法的思路是让拥塞窗口cwnd缓慢增大,即每经过一个往返时间就把发送放的cwnd加1)
    • 快重传和快恢复。和上面的计时器不一样,如果客户端接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。缺点是当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。
  • 滑动窗口的原理是把数据分为四个部分,从前到后分别是已发送并收到确认、已发送但未收到确认、允许发送但未发送、不允许发送,当前方的数据发送得到确认了窗口就会向后滑,后面的数据也会进入新的状态。

输入url之后的过程

  • 首先是DNS域名解析,找到域名的IP地址,这个过程是先从浏览器缓存开始,如果浏览器缓存不存在就查找路由器缓存,之后再到DNS缓存。然后就建立TCP连接,三次握手成功了之后,客户端这边发送一个HTTP请求,服务器处理请求并返回HTTP报文,浏览器解析并渲染页面,连接结束。
  • 这个过程中用到了DNS协议,TCP/IP协议,OSPF协议,ARP协议,HTTP协议。
    • DNS协议:就是将域名转成IP地址的协议。
    • TCP协议:用于建立连接
    • IP协议:TCP协议建立之后在网络层传输数据使用IP协议
    • OPSF协议:IP数据包在路由器之间传递使用的协议
    • ARP协议:路由器与服务器通信时,将IP地址转成MAC地址用到的协议。
    • HTTP协议:TCP连接建立之后,客户端与服务器之间使用HTTP协议。

DNS解析的过程

  • 首先是在浏览器缓存中搜寻有没有这个url被解析过的ip地址,如果没命中,就会到操作系统缓存中检查,这个缓存被保存在C盘的hosts文件中,如果没命中就请求本地域名服务器来解析,如果还没命中就向Root Server域名服务器中请求解析,解析之后返回一个url所在的主域名服务器,然后本地域名服务器发送请求解析,,解析完域名和对应的ip地址之后返回给用户。

HTTP和HTTPS

  • http是超文本传输协议,http1.0中默认使用短链接,就是客户端和服务器之间只进行一次连接,当任务结束了之后就中断连接。每当客户端访问一个资源就新建一个Http会话,1.1之后改成了长连接,在响应头加上了Connection:keep alive,在保持时间内TCP连接不会断开,客户端访问服务端其他资源都会使用这一条http连接,在2.0之后新增了多路复用以及头部数据压缩的功能。多路复用就是一个连接并发处理多个请求,头部数据压缩就是把状态行和头部的一些基本数据如Cookie进行压缩。
  • http和https的区别在于,http默认使用80端口,https使用443端口,然后http建立在TCP之上,所有的传输内容都是明文,客户端和服务器都无法验证对方的身份,而https是建立在SSL/TSL之上的协议,所有的内容都采用了对称加密,且对称加密的密钥经使用服务器方的证书进行了非对称加密。
    • SSL:SSL就是安全套接字层,是一个加密和验证客户端和浏览器之间传输的数据的协议。首先是将对话内容通过服务器发送过来的公钥进行加密,并且服务器端保存有唯一的私钥,数据发送过去之后服务器再通过私钥解密。
    • 保证公钥不被篡改的方法就是把公钥放在数字证书中,只要证书是可信的,公钥就是可信的。
    • 对称加密就是密钥只有一个,加密解密都是一个密码,速度快,典型的对称加密算法有DES、AES等。
    • 非对称加密就是密钥成对出现,并且公钥密钥无法推知对方,加密解密使用不同密钥,速度较慢,典型的非对称加密算法有RSA、DSA。
    • Https建立连接的过程:
    • 1、首先客户端访问https连接
    • 2、然后服务端发送证书,也就是公钥给客户端
    • 3、客户端收到以后验证证书的合法性以及地址是否一致,验证通过以后会生成一个随机字符串,用服务端的公钥进行加密,然后生成握手信息,用约定好的Hash算法对握手信息进行加密,然后再用随机字符串加密握手信息和握手信息的Hash值(这里之所以要加上Hash的原因:如果服务端对收到的信息进行Hash时发现和发送过来的不一样,那就说明信息被篡改过)
    • 4、服务端接收到信息之后先用私钥对信息进行解密(RSA解密)得到随机字符串,再用随机字符串进行解密得到握手信息和Hash值,然后对握手信息进行Hash,对比发送过来的Hash值,如果一致说明信息正确,验证完之后把同样使用随机字符串把加密握手信息和握手信息的Hash值发送给客户端
    • 5、客户端还是像那样,解密随机字符串验证加密信息,校验通过后握手完成,然后开始AES加密通信。

Cookies和Session

  • Cookies是用来弥补Http无状态的不足的,是保存在客户端浏览器里的一些kv,用来记录用户的一些信息,value只能是字符串类型。Session是保存在服务器端的kv,用来跟踪用户状态的,安全,value可以是Object类型的。在初次创建Session的时候服务端就会通过http协议在Cookies里保存一个Sessionid,之后的每次访问都通过Cookies中的Sessionid来辨别指定的客户端。如果浏览器禁用了Cookies,会通过URL重写来记录,就是在url后面加上一个sid==xxx这样的参数来识别用户。

一些常见的Web攻击

  • SQL注入:就是
select * from user where name = ‘lianggzone’ and password = ‘’ or ‘1’=‘1’

逻辑判断or后面成真,那么就能执行通过,然后查询出来信息。防范的方法是,在客户端进行有效性检验,限制字符串的输入长度。在服务端不使用拼接SQL字符串,使用PrepareStatement/

  • DDos攻击:就是在第三次握手的时候客户端不向服务端发送确认数据包,服务器一直等待客户端的确认。解决的办法是限制同时打开syn半链接的数目,以及缩短syn半链接的Time out时间。

负载均衡和反向代理

  • 负载均衡就是使用多个服务器构成的集群来响应客户端的请求。
    正向代理就是客户端通过代理服务器翻墙,反向代理就是服务端通过代理服务器接收请求,然后代理服务器转发,外界无法查看到服务端的信息。
  • 常见的负载均衡的算法有轮询(将请求依次循环发送到服务器上)、随机(随机发送)、最少链接(发送到进行最少连接处理的服务器上)、Hash(将IP地址进行Hash计算,然后同一IP地址的请求在会话期内发送到同一台服务器上,缺点是服务器宕机后会话会丢失)、加权(在上面几种算法的基础上,通过加权的方式进行服务器分配)。

IP地址

  • IP地址分为五类,ABCDE类,A类是8位网络地址加24位主机地址,B类是16位网络地址加16位主机地址,C类是24类网络地址加8位主机地址。D类是组播地址,E类是保留地址。A类的划分是从1.0.0.0到127.255.255.255,B类是从128.0.0.0到191.255.255.255,C类是从192.0.0.0到233.255.255.255,D类是从234.0.0.0到239.255.255.255,E类是从240.0.0.0到255.255.255.255。子网的划分是通过主机数来确认,首先我们知道子网掩码就是除了主机地址以外其他所有位都由1构成,比如一个B类网络的主机数有1024个,那他就是2的10次方,主机位就是10,然后子网掩码就是32-10=22位,那么它的子网掩码就是11111111.11111111.11111100.00000000,换算成十进制就是255.255.252.0。子网掩码就是用来掩饰我们划分的子网,在外界看起来网络还是没有变化。

操作系统

进程线程协程

  • 进程是操作系统资源分配的最小单位,有自己独立的地址空间和堆,是程序的一次执行过程,进程也可以看作是程序加上数据加上PCB控制块组成。操作系统最开始的时候是只有进程的,后来发现进程的创建和撤销都要涉及到资源的再分配,开销比较大,于是才设计了线程,线程是操作系统调度执行的最小单位,也可以被看作是轻量级进程,一个进程中可以有多个至少有一个线程,它们共享进程的所拥有的资源,有自己的堆栈和局部变量,但是没有单独的地址空间,一个进程死掉所有的线程都会死掉。协程就是比线程更轻量级的存在,就是不受操作系统内核管理,完全由程序控制,也就是在用户态直接执行,不会像线程切换那样消耗资源,好处就是执行效率很高,没有线程切换的开销,然后还不需要锁,因为只有一个线程,所以就不存在同时写变量的冲突,只需要判断状态就行。

死锁

  • 死锁就是指多个进程在运行过程中因为争夺资源而造成的一种僵局。简单来说就是有两个线程A和B,然后它们互相持有对方需要的资源,并且同时请求对方的资源,然后谁都不放手,然后就形成了死循环,也就是死锁。
  • 产生死锁有四个必要条件,就是互斥条件,请求和保持条件,不剥夺条件,和循环等待条件。互斥条件就是一个资源任意时刻只能由一个线程占用。请求和保持条件就是一个进程因请求资源阻塞而阻塞的时候,不会释放已获得的资源。不剥夺条件就是线程已获得的资源在未使用完之前不能被其他线程强行剥夺。循环等待条件就是线程和资源之间的一种头尾相接的环形等待关系。
  • 预防死锁就要从这四个方面下手,首先互斥条件是不能破坏的,因为我们使用锁本来就是要它们互斥,我们可以破坏请求与保持条件,就是一次性把一个线程需要的所有资源都分配好,这里还可以做一些改进,就是在线程使用完分配好的资源后主动释放出去。然后时破坏不可剥夺条件,在请求不到其他资源的时候,主动释放自己的资源。最后是破坏循环等待条件,申请资源按照一个顺序申请,释放资源按照逆序释放。不过这几种方法都会损害系统性能,有一个比较好的方法就是银行家算法,银行家算法就是定义了状态和安全状态,当一个新的进程进入系统的时候,它必须说明需要的每种资源的最大数量,这个数量不能超过系统资源的总和,如果超过了就处于不安全状态,系统不会分配资源,直到其他进程释放足够资源为止。
  • 解除死锁就俩方法,一个是从其他进程剥夺足够数量的资源给死锁进程,一个是撤销死锁进程或者是撤销代价最小的进程。
  • 死锁的检测有Jstack和Jconsole工具。

进程间通信、线程间通信/同步

  • 进程间通信有七种方式,分别是管道,命名管道,消息队列,共享内存,信号量,套接字,以及信号。管道是半双工的,也就是数据只能单向流动,只能用于父子进程或者是兄弟进程之间。命名管道也是半双工的方式,但是它允许无亲缘进程之间的通信。消息队列的消息的链表,存放在内核中,由队列ID标识。信号量是一个计数器是用于进程间互斥与同步的,可以用来控制多个进程对资源的访问,如果信号量的上限是1,就成为了一个锁,如果需要在进程之间传递数据还需要结合共享内存来实现

项目

二手平台

这个二手平台是和朋友一起做的一个前后端分离的项目,做这个项目的原因是…然后就搭建了这么个项目,最近我们在设计一个新的模块就是抢购模块,因为考虑到…所以就设计了这么个抢购模块,

用session保存 k-v存储 k是sessionid v是用户信息

redis做一级缓存缓存怎么做的 我们是用k-v形式存储,k是一些比如user、product之类的,value是我们的数据信息,要做缓存的对象从数据库里查出来之后放进缓存里,如果更改就刷新缓存或者删除的动作就删除缓存,ehchache做二级缓存防止缓存雪崩,先查redis再查ehchache,然后设置两个的key的过期时间不一致。

高并发处理,用redis做锁,用到了SETNX命令,谁执行成功了就代表谁抢到锁了,SETNX命令有过期时间,但是执行事务的逻辑可能超时,然后就可能会引起冲突,这里就添加了一个守护线程去监听锁的变化(while循环去检查getkey有没有值,没有值代表过期了,过期了就再set一遍)。数据库也加了乐观锁,防止多个人同时修改,乐观锁version通过版本号实现,每次update都会查询一次版本号,如果版本号一致才能操作。
SETNX 守护线程 数据库加version dubbo netty 消息队列

RPC

这是我手动实现的一个RPC框架,实现了一些简单基础的功能,我这个项目的架构是这样的,它分程三个部分,客户端、中间处理器、服务端,它们之间的传输部分我实现了两种,一个是原生的socket传输(BIO)方式,还有一个就是netty传输(NIO)方式,客户端通过了动态代理来实现我的Request请求,服务端就是通过反射来接收我的Request请求解析并调用然后生成Response对象返还回去。值得一说的还是netty部分,我自定义了协议和编码器,协议是由四个字节的魔数(区分文件类别是类文件),四个字节的包名(区分是Request还是Response)、四个字节的序列化类型(区分哪种序列化方式)、四个字节的信息长度(防止粘包),还有我的信息主体构成。然后就是那四种序列化方式,其实我最开始用的是Json(序列化工具我使用的是Jackson),但是Json有一个毛病就是它反序列化Object的时候会直接把它转成String类型,而且它是基于字符串的,占用空间比较大速度也慢,然后就又增加了Kryo,它避免了上面Json的缺点,唯一的缺点是可能存在线程安全的问题,所以我把技术文档上是推荐放在ThreadLocal里。Nacos是阿里巴巴的一款服务注册中心么,我最开始写的时候是一个客户端对应一个服务端,有个问题就是如果服务端挂了那客户端就找不到服务了,所以就用上了Nacos服务注册中心。负载均衡我是用了两种策略,一种是随机策略,这种比较Low就是随机数从Nacos提供的注册列表里选择一个服务端,轮转算法是用了个index表示当前选到了第几个服务端,然后选择完加一(对size求余)。自动注册是通过注解实现的,两个注解,Service和ServiceScan,Service是用在服务类上注明它是个服务端,ServiceScan是用来扫描同一个包下的Service注解,找到所有的Service然后通过反射注册就完成了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值