一个程序员的成长之路

时间:2024-09-29 19:05:51

学习笔记

  • java
    • 基础类型与String相关
      • 基本类型范围
      • 基本类型的转换
        • byte计算自动转换int
        • 基本类型与包装类
        • equals与==的区别
    • 集合比较与常用集合原理
    • 反射机制与真实使用场景
    • 动态代理与使用范例
    • 异常
    • 类加载机制与热加载实现与反编译
    • 内存模型与threadLocal与syncronize
    • jvm的GC与调优处理
    • JUC设计原理
    • 文件流处理与网络相关
  • Maven
  • spring(ssm与springboot)
    • spring原理与bean生命周期
    • springboot原理与starter编写
    • springboot相关
    • springMVC原理与流程及过滤拦截
    • mybatis原理及缓存与分页
  • redis原理与zookeeper
  • mysql与oracle
  • 分库分表以及ES
  • mq
  • 算法
  • Dubbo与springcloud以及Golang等分布式处理
    • springcloud
  • docker与K8S及Jekins等容器部署相关
  • tomcat与nginx,Jboss等一系列服务器
  • CAP理论下的一些复杂场景解决方案
  • ChatGPT
  • 团队经验
  • 杂记

java

内容持续更新…

基础类型与String相关

在这里插入图片描述

基本类型范围

一个字节占八位,即8bit,byte取值为00000000-11111111,即0-255,由于java中是有符号类型,所以第一位作为符号
那么byte取值就是01111111为最大值127,11111111为最小值-127,此处引入原码,反码,补码的概念,反码用于负数的算数运算,而补码用于正负数跨零的算数运算,计算机都是用补码计算和存储的,因此多出了-128,原理如下:原码反码补码

int是基本数据类型,Integer是int的封装类,是引用类型。int默认值是0,Integer默认值是null,所以Integer能区分出0与null的情况,一旦java看到null,就知道这个引用没有指向某个对象,在任何引用使用前,必须为其指定一个对象,否则会报错
基本数据类型在声明时系统会自动给他分配空间(方法区的常量池),而引用类型声明时只分配了引用空间(线程的java虚拟机栈),必须通过实例化开辟数据空间后才可以赋值(实例化即在java堆中创建对象实例,将引用指向java堆中对象)
虽然定义了boolean这种数据类型,但java虚拟机中操作的所有boolean值,在编译之后都是用了int类型来代替,而boolean数组将会被编译为byte数组

基本类型的转换

byte计算自动转换int
	byte b1=3,b2=4,b;
    b=b1+b2; //错误 运算时会自动转换为int类型 而int类型的值不能赋值给byte 需要强制类型转换、
    b=3+4;//正确  常量具有常量类型优化机制 可以直接识别为byte(原因:常量运算,先把结果算出来再赋给一个变量)
    b1+=b2;//正确 +=操作会自动类型转换
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述
byte计算自动转换原因:jvm的32位架构最基础计算单元为int

基本类型与包装类

自动装箱与拆箱,java基本类型与其相应的包装类之间会自动进行类型转换

public class Main{
  public static void main(String[] args){
  	Integer i1=100;
  	Integer i2=100;
  	Integer i3=200;
  	Integer i4=200;
  	System.out.println(i1==i2);
  	System.out.println(i3==i4);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

运行结果:

true
false
  • 1
  • 2

原因是Integer的valueOf方法具体实现如下

public static Integer valueOf(int i){
 if(i>=-128 && i<=IntergerCache.high){
 	return IntegerCache.cache[i+128];
 else
 	return new Integr(i); 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在通过valueOf创建Integer对象时,如果在-128~127之间便会返回中已经存在的对象的引用,否则创建一个新的Integer对象

public class Main{
  public static void main(String[] args){
  	Double i1=100.0;
  	Double i2=100.0;
  	Double i3=200.0;
  	Double i4=200.0;
  	System.out.println(i1==i2);
  	System.out.println(i3==i4);
  }
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

运行结果:

false
false
  • 1
  • 2

原因:在一个范围中的浮点数个数不是有限的,存在精度问题,如果要进行比较,使用BigDecimal并使用BigDecimal提供的对象方法进行计算,构造时使用String作为入参防止丢失精度,若用数字比如1解析出来可能是1.0000000000000000000000013之类的,还有计算时要保留位数,尤其除法否则无限循环会报错,然后BigDecimal和String一样不可变,累加需要赋值。

equals与==的区别

equals一般比较对象内容是否相等,而==比较两者的栈中保存的对象堆地址值是否相等,即是否指向同一个对象,equals在object中就存在,如不重写的话两者没有区别,重写时一般使用hashcode方法结合,比较相等首先判断hashcode是否相等,相等再进行真实值的比较,大大降低比较时间,最多两次即可判断。

string stringbuffer stringbuilder
string是一个对象,底层为final类型的字符数组,所引用的字符串不可改变,每次+操作都是隐式的在堆上new一个与原字符串相同的stringbuilder对象进行append操作,stringbuffer加了同步锁,线程安全,stringbuilder非线程安全

集合比较与常用集合原理

List,Map,Set
Array是基于索引index的数据结构,因此getByIndex搜索复杂度是O(1),而且可以进行范围查找
ArrayList和LinkedList是List的两种实现,前者可以自动扩容,底层实现是array,自动扩容原理为新建一个两倍长度的数组再将数据复制到新数组中,LinkedList是一个双向链表,因此在添加和删除时会优于ArrayList,数据不经常更新可以使用ArrayList否则使用LinkedList,而如果不是为了范围查找,最常见还是使用map存储,数组还有一种线程安全的实现方式Vector,很少使用
List的队列和栈数据模型:List的队列和栈数据模型
map是以键值对的形式存储数据,主要的实现为hashmap,hashtable,其中hashmap实现为数组加链表的形式,添加数据的原理为每当数据来临,调用hashcode方法计算哈希并找到元素存储位置,若该处无数据则添加,若有数据则调用equals在链表中进行比较,随着hash冲突的增加,链表的比较开销较大,因此在1.8之后设计为HashMap设计原理
hashmap的死循环原因触发于多线程和扩容状态下,因为头插法扩容时原先的a-b-c就会变为c-b-a,扩容也是一个一个放置到新的map中,原map最上面的就先放入最终变成最下面的,这时候如果两个线程一个执行扩容变成c-b-a,另一个在扩容前是a-b-c那么该线程存储的a的next是b,等扩容完去获取b的next就是a,死循环发生,选择头插是认为新插入数据使用可能较大放前面搜索快,改为尾插不会发生死循环,但是并发不能保证
hashtable是同步的,内部方法使用synchronize修饰,效率很低,几乎不使用,syncronizedmap构造方法将this指向mutex,所有方法加synchronized锁住map,只能一个线程访问,如下:

...
private final Map<k,v> m;
final Object mutex;
SynchronizeMap(Map<k,v> m){
this.m=m;
mutex=this;
...
public int size(){
synchronized(mutex){return m.size();}
}
...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

concurrenthashmap使用分段锁来保证在多线程下的性能。一次锁住一个桶。默认将hash表分为16个桶, 诸如 get put remove等常见操作只锁当前需要用到的桶。能同时有16个写线程执行,并发性能的提升是显而易见的。HashMap在多线程情况下put超过了负载因子0.8总容量就会rehash扩容,此时可能发生死循环,虽然改为尾插不会死循环但多个put也会线程不安全,而hashTable方法都用了Synchronize效率太低,因此ConcurrentHashMap使用分段锁的概念,因为不同的数据集hash不同根本不需要去竞争同一个锁,1.7中数据结构为一个segement数组,默认长度16,然后每个Segement对于一个HashEntry,HashEntry就类似hashmap的结构,当put的时候会hash两次,首先hash确定放入哪个Segement中然后hash确定放入HashEntry哪个位置中,并且在放置的时候因为Segement继承了ReentrantLock所以使用tryLock来判断是否能获取锁,获取到的才会put获取不到就进入Lock的AQS中的CLH队列中等待了,get操作和HashMap类似只不过要两次hash,size操作,因为并发size不准确,提供了两个方案,先不加锁去算最多三次,比较size不变那就返回结果,如果不行那就给每个Segement上锁后再计算返回,这里让我想起了Innodb不像Myisam一样提供数据条数总量的字段的问题,因为有事务的情况所以条数不准确。而到了1.8就放弃了Segement的方式直接使用Node数组+链表+红黑树的结构来实现,保留Segement是为了兼容之前的代码。并发使用Synchronize和CAS来操作,Node是存储的基本单元,继承于HashMap中的Entry,就是一个链表,只能查询不能修改
TreeNode继承自Node,数据结构换成了二叉树,一条链路是一个Bucket,Bucket中可能是链表也可能是红黑树,取决于长度是否大于8。put操作先判断key和value是否为null,然后计算key的hash进入无限循环确保可以插入数据,先判断table是否空如果是初始化table数组表,然后后根据key的hash值取table中节点,不存在则CAS放入,否则判断节点hash是否是MOVED,是则说明在扩容,帮助转移,不是的话synchronize开始对该桶Bucket进行遍历并比较,相等就修改,遍历完都不想等就在末尾生成一个新结点,然后计算长度如果达到阈值就转为红黑树。get函数则是根据key的hash判断在哪个桶,如果首结点符合就返回,如果遇上遇到扩容会调用ForwardingNode的find方法取查找(如果该桶没扩容完可以找到,扩容完会把这个指向原来的引用供查找),如果都不符合就遍历节点,匹配就返回,不匹配就返回null。扩容原理是这样的,首先扩容是n<<1即原来的大小乘2,这样会对桶的结点重新计算存放的hash位置,因为N的最高位是1,哈希码和N的最高位一样都是1那么说明是新位置,否则是原来位置,只有这两种情况,因为以前少一位,如果现在第一位还是0说明就是以前,变为1才说明是到了新的位置,然后每个线程参加扩容都只会分配属于自己的区域。put或者get的时候就会判断节点状态并帮助扩容。
为什么线程安全的map都不允许key为null?
hashtable和concurrenthashmap都不允许key为null,而hashmap却可以,这是出于对并发的考虑,多线程情况下一个线程判断该位置有无元素得到null值时无法确定是没有元素还是其他元素在此时插入了一个null值数据
TreeMap 有序,元素无序要实现comparable
set用于非重复,保证机制还是hashcode加equals方法
Collection是集合类的上级接口包含有关集合操作的静态方法用于实现集合的搜索排序线程安全化操作,不能实例化。
跳表skiplist是一种有序的数据结构,可以作为平衡树的一种替代,因为平衡树比如红黑树之类的实现起来复杂而且要旋转变色在并发场景下锁的性能比较差,跳表是有序链表加稀疏索引的方式,首先底层是有序的链表,比如12345678,第一层索引比如有三个值
1-4-8分别可以指向链表中的148元素,再上一层索引可能只有1-8分别指向第一层索引的18,这样当搜索的时候通过几层索引就可以迅速找到数据在链表中的位置,而不需要一个个找下去,显然索引会随着链表的变化而要变化,因此将每层的中点作为索引不合适
这样每次一个元素修改就要更改索引,所以用随机化,比如设置N个数据的链表第k层有N/2^k个节点
BitMap位图,用每一个位表示某个状态,适用处理海量数据,本质是hash表,原理就是将一个int数据映射到对应的位上,将该位由0变为1,一个int存储要32bit,bitmap只要1bit所以节约了32倍
数据分布不均匀的话中间空值占了很多反而浪费了资源,这就需要用一位存数据,多几位存到下一个数据的跨度信息,合起来作为一个数据的整体,比如10位为一个数据。然后对于字符型的可能就要
使用布隆过滤器(比较难维护,生成之后如果值删除了要去除比较麻烦)。

stream中的flatMap是扁平化处理,list中的子list可以用flatMap来操作会得到该子list的所有数据
Optional用来解决校验问题,如果一个对象中多层嵌套的判断是否为空用if太多层了,用然后用map当一个if去链式处理最后用orElse或者orElseGet来收尾会更优雅
@Conditional注解的作用是给需要装载的bean添加一个条件判断只有满足的时候才加载,在bean的描述逻辑中加上这个注解并重写matchs方法自定义装载条件

反射机制与真实使用场景

java反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能调用它的任意一个方法,只要给定类的名字就可以获取它的全部信息,field,constructor,method都可以被获取,提高灵活性但是性能较低,可以通过setAcessible(True)来关闭jdk的安全检查来提高性能

动态代理与使用范例

异常

Exception于error,error是JVM出现的问题,不是代码原因,比如oom,exception有runtimeException和编译时异常,前者有空指针,数组溢出等,运行时才有可能发生,需要try/catch。后者有ParserException,IOExceptio等在编译的时候就要进行处理否则编译无法通过,我们也可以实现Throwable定义自己的异常

类加载机制与热加载实现与反编译

内存模型与threadLocal与syncronize

java的四类引用,强弱软虚
直接创建对象就是强引用,包括new,newInstance,clone和序列化,弱引用weakReference,只要GC就会回收,软引用SoftReference会在内存不足的时候回收,虚引用phantomReference回收机制和弱引用差不多,只要GC就回收,但回收前会被放入ReferenceQueue中,其他引用都是被jvm回收后才被传入ReferenceQueue中,由于这个机制,虚引用大多用于引用被销毁前的处理工作,因此创建虚引用必须带有ReferenceQueue
深拷贝与浅拷贝的区别:深拷贝将对象完全拷贝一份,包括属性和对其他对象的引用,浅拷贝只会拷贝属性,对象的引用复用,所以浅拷贝的两个对象在对引用属性修改时是共享的,深拷贝则完全独立。clone是浅拷贝,深拷贝需要自己实现,即每一层对象都用浅拷贝。
java中不想序列化的字段用transient关键字修饰,不会序列化,反序列化时也不会被持久化和恢复,只能修饰变量,不能用来修饰方法和类。
threadLocal的弱引用产生的内存泄漏隐患,与线程池的结合
syncronize的锁升级机制与AQS算法,monitorenter与monitorexit指令
AQS双向链表等待队列

jvm的GC与调优处理

JVM的内存结构主要源于数据在主存和工作内存的交流方式产生,分别是线程内私有的java虚拟机栈,用于存储局部变量表,方法操作栈等,本地方法栈用于执行native方法,程序计数器用于记录当线程的cpu时间片耗尽时程序执行的位置,执行native时置为空,线程间的共享有堆和方法区两块,堆用于存放几乎所有的对象实例,即新生代和老年代,方法区用于存储常量静态变量和类的信息,即永久代。类的加载和卸载遵循双亲委派机制,加载-验证-准备(设置初始值)-解析-初始化(赋值),该机制保证类不被重复加载,且能保证java核心api不被篡改,bootstrap-ext-application-自定义,以加载器+全类名为唯一标识,GC算法在新生代为标记赋值算法,eden-survivor1-survivor2是minorGC,老年代是标记清楚算法是FullGC,判断是否回收通过引用计数和可达性分析,引用计数简单但不能解决循环引用,可达性分析为从GCRoots向下搜索,搜索走过的路径称作引用链,当一个对象没有任何引用链,说明是不可达的,JVM性能调优通过-Xmx设置堆内存,不宜太小否则都去老年代了。
GC问题:cpu报警,接口失败率逐渐上升逐渐奔溃说明服务节点还在但是应用系统内有问题,cpu报警业务上无计算密集型也没有大量io所以判断是不是内存,查看触发GC算法fullGC频繁,所以查看原因发现新上的代码使用single线程池来插入数据,sql从入参来
入参很大导致sql很大然后一次插入要1秒并且后续的sql都在阻塞队列中作为大对象等待导致fullGC,然后限流没限住是因为请求并没到达阈值是处理太慢了,处理慢没触发降级熔断是因为用了异步线程去做rt没有增加。。

JUC设计原理

java解决多线程并发访问共享资源的时候提供了两种解决方案
一种是synchronize锁方式,用时间换空间,有无锁的CAS以及乐观锁方式,也有lock包下的锁与synchronize关键字,对共享变量的同时修改采用互斥的方式,synchronize利用monitor的机器指令,锁住的是对象,锁信息置于对象头中,所以没有公平锁的概念,释放资源后产生羊群效应,其中还包括锁优化即偏向锁到轻量级锁到重量级锁的锁升级机制,lock使用的AQS,锁住的是锁本身,用state去控制访问的权限,等待竞争的线程会在CLH队列中,所以可以保证公平锁。但是synchronize可以锁类,lock只能锁方法。
一种是ThreadLocal复制方式,用空间换时间,通过对共享变量的复制,使得每个线程访问并修改自身存储的值,ThreadLocal本身不存储数据,使用线程中threadlocals的一个属性,其类型由threadlocal中threadlocalmap定义,这个还要去看源码。

Condition类:主要用来替代Object中的wait和notify实现线程间的写作,相比Object来说Condition中提供awaitUntil的deadline等待以及awaitUninterrptibly的非中断等待等更有效的方式
场景例子有arrayBlockingQueue的实现:

public class ConTest {
    final Lock lock=new ReentrantLock();
    final Condition condition=lock.newCondition();
    final Lock lockFake=new ReentrantLock();//condition必须和对应lock配合使用如果用lockFake上锁,condition通知会报错
    public static void main (String args[]){
        //AbstractQueuedSynchronizer AQS抽象队列同步器
        //ArrayBlockingQueue 中Condition notEmpty在enqueue添加元素后signal,notFull在dequeue删除元素后signal,在take中count为0则,在put中count=则
        ConTest test=new ConTest();
        Producer p=test.new Producer();
        Consumer c=test.new Consumer();
        c.start();
        p.start();

    }
    class Consumer extends Thread{
        public void run(){
            try{
                lock.lock();
                System.out.println("This Thread is waiting a sign"+ currentThread().getName());
                condition.await();
            }catch (InterruptedException e){

            }finally {
                System.out.println("getting a sign"+currentThread().getName());
                lock.unlock();
            }
        }
    }
    class Producer extends Thread{
        public void run(){
            try{
                lock.lock();
                System.out.println("This Thread holding on the lock"+ currentThread().getName());
                condition.signalAll();
                System.out.println("advertise a sign"+ currentThread().getName());
            }finally {
                lock.unlock();
            }
        }
    }

}
  • 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

以Condition实现生产消费者模式:

public class ConTestTwo {
    private int queueSize = 10;
    private PriorityQueue<Integer> queue = new PriorityQueue<>(queueSize);
    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

    public static void main(String args[]) throws InterruptedException {
        ConTestTwo test = new ConTestTwo();
        ConTestTwo.Producer p = test.new Producer();
        ConTestTwo.Consumer c = test.new Consumer();
        c.start();
        p.start();
        p.interrupt();
        c.interrupt();

    }

    class Consumer extends Thread {
        private volatile boolean flag = true;

        public void run() {
            try {
                while (flag) {
                    lock.lock();
                    if (queue.isEmpty()) {
                        try {
                            System.out.println("queue is empty thread is waiting for data");
                            notEmpty.await();
                        } catch (InterruptedException e) {//由于signal不会抛出中断异常,await会被唤醒或者中断,放在这里保证中断时最后执行完一次操作,放外面的try/catch则中断直接就强制结束了
                            flag = false;
                        }
                    }
                    queue.poll();
                    notFull.signal();
                    System.out.println("take one data from queue queue size:" + queue.size());
                }

            } finally {
                lock.unlock();
            }
        }
    }

    class Producer extends Thread {
        private volatile boolean flag = true;

        public void run() {
            try {
                while (flag) {
                    lock.lock();
                    if (queue.size() == queueSize)
                        try {
                            System.out.println("queue has full now please wait");
                            notFull.await();
                        } catch (InterruptedException e) {
                            flag = false;
                        }
                    queue.offer(1);
                    notEmpty.signal();
                    System.out.println("put one data into queue queue rest:"+(queueSize-queue.size()));
                }

            } finally {
                lock.unlock();
            }
        }
    }
}
  • 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

Condition的实现基于AQS,AQS是一个抽象类。里面定义了同步器的基本框架和基本结构功能,只留下状态条件的维护由具体同步器根据场景自行定制,如常见的ReentrantLock,ReentrantReadWriteLock,CountDownLatch,Semaphore等,由三部分组成,state状态,Node组成的CLH队列,ConditionObject条件变量。对于状态由getState和setState和compateAndSetState使用unsafe的CAS,还有isHeldExclusively判断当前线程是否持有资源。CLH是AQS内部维护的FIFO双链表队列,当一个线程竞争资源失败,就会将其封装为一个Node节点通过CAS原子操作插入队列尾部,Node节点连接组成CLH队列,优点在于FIFO保证公平,CAS无锁保证非阻塞,Node中waitStatus来判断线程的状态,nextWaiter设置下一个节点是否shared可共享,流程为线程获取资源失败,封装成Node节点通过addWaiter加入CLH队列尾部并阻塞线程,哨兵节点即第一个节点释放资源时,将指向的线程唤醒,也就是CLH的第二个节点,如果第二个节点获取资源成功,将其设置为哨兵节点,原头部节点出队。acquireQueued方法,这里不需要CAS因为只有一个节点能获取资源。条件变量:其实就是Condition,有await和signal,相比Synchronize的Object的Condition可以有多个条件,ConditionObjec中维护了一个单向条件队列,这里只放置await的节点,也就是不获取资源的,再这里的节点不能在CLH中出现,这里出队的节点会加入到CLH中,当某个线程执行await函数,阻塞当前线程,线程封装成Node节点添加到条件队列末端,其他线程执行signal则将条件队列头部线程节点转移到CLH中参与资源竞争,CAS的方式用enq加入到CLH中,signalAll就是释放所有节点。
AQS采用了模板方法提供独占模板 acquire和release还有共享模版acquireShared和releaseShared,逻辑为执行tryAcquire(子类实现,代表资源获取是否成功),成功返回失败通过addWaiter进入CLH队列,执行acquireQueued自旋阻塞等待获取资源,如果获取成功判断中断状态决定是否执行中断逻辑。释放就是线程释放资源成功,唤醒CLH第二个线程节点。共享模板流程差不多,区别在于addWaiter的时候加入共享节点,获取资源成功后还会判断资源大于0唤醒后续节点,释放区别不大。这就满足semaphore信号量的功能,n个资源可获取,获取完了才等待。CountDownLatch类似于计数器的功能可以让一个或多个线程进入等待状态直至其他线程完成工作,持有countdown方法和await方法,当线程调用countdown的时候计数器将会减一,当线程调用await方法的时候,如果计数器不为0,则该线程进入阻塞状态直到计数器为0时就会直接退出,所以可以在线程执行完后countDown获取资源,然后用await等待直到所有资源被获取完说明都执行完了程序结束。
为什么Condition阻塞队列是单向的,CLH是双向的?CLH很多情况下当前节点操作要判断上一个节点的状态,不用双向链表就就要从头遍历,Conditon的不需要判断前一个节点状态所以单向就行了。

synchronize的锁升级机制:首先有四类锁状态,分别是无锁,偏向锁,轻量级锁和重量级锁。在这里要注意java的对象结构:
在这里插入图片描述
对象头:
Mark Word: 标记字,主要用来表示对象的线程锁状态,另外还可以用来配合GC存放该对象的hashCode
Class Pointer: 类对象指针,存放方法区Class对象的地址,虚拟机通过这个指针来确定这个对象是哪个类的实例
Array Length: 数组长度,如果对象是一个Java数组,那么此字段必须有,用于记录数组长度的数据,不是数组对象可以没有,可选字段
对象体:对象体包含对象的实例变量(成员变量)
对齐字节:对齐字节也叫作填充对齐,其作用是用来保证Java对象所占内存字节数为8的倍数
优先级是偏向锁-轻量级锁(先自旋不行再膨胀)-重量级锁。偏向锁是指锁对象的MarkWord不再存储锁记录地址而是存储线程ID,当发生锁重入时判断ID就可以不需要CAS,默认情况新建对象都是偏向锁(Synchronize情景下)轻量级锁指的是当A有偏向锁,A执行完后B又来获取锁此时发生锁膨胀进入轻量级锁,具体指的是线程的栈中会有一个锁记录的对象LockRecord。其中包含对象指针ObjectReference和锁记录地址LockRecord地址00,当这个线程执行到上锁的位置时会将对象头MaekWord和锁记录地址进行CAS交换,成功后对象头存放的就是锁记录地址和状态00,重量级锁指的是A已经有轻量级锁在执行中,B来竞争锁那么会锁膨胀变为重量级锁,也就是Monitor指令情况了。自旋优化指的是当A有锁时B来竞争不会立即进入阻塞而是多次尝试获取锁,如果期间A执行完了B就会获取锁这就是自旋,这样就不需要上下文切换就可以获取锁,如果获取不到B就进入阻塞状态。自旋耗费CPU时间,单核就是浪费只有多核才有用,自旋是自适应的,能成功就会多试几次,不行就少几次,不能控制。Synchronize锁住的代码实际上会加上Monitorenter和Monitorexit指令,会让对象在执行时进入加1退出减1,每一个对象同时刻只能于一个monitor关联,一个monitor同一时刻页只能被一个线程获得,一个对象在获取monitor时如果计数器为0,那么可以获取并将计数器+1,如果重入就会累加,如果不为0就等待。锁消除指的是如果虚拟机编译时发现要求同步的代码不可能出现共享那么就会消除锁,比如逃逸分析发现锁是方法中的局部变量。
流程:轻量级锁只是栈中一个锁对象,不是monitor级别,发生于多线程并发但是加锁时间是错开的,那么synchronize就是轻量级锁,每次执行到同步代码快时都会创建锁记录LockRecord每个线程都会包括一个锁记录的结构,锁记录内部可以存储对象的MarkWord和对象引用Reference,让锁记录中ObjectReference指向对象,并用CAS替换Object对象的MarkWord为LockRecord的指针并MarkWord信息存入锁记录中,这就完成了加锁。如果失败说明有其他线程持有了该Object的轻量级锁,发生了锁竞争那么就升级为重量级锁,如果是自己执行相当于重入,再添加一条LockRecord作为重入计数,且新的LockRecord中对象的MarkWord为null,当线程退出同步代码块时如果取值为null说明有重入,重置锁记录表示计数减一,如果不为null那么使用cas将MarkWord值恢复给Object,成功则解锁成功,失败说明已经锁膨胀进入重量级锁解锁过程。锁膨胀是当A加轻量级锁,此时B已经锁了,那么A的CAS失败,于是A为对象申请Monitor锁让Object的MarkWord指向重量级锁Monitor地址,然后自己进入Monitor的entryList变成BLOCKED阻塞状态,当B退出同步代码块的时候,使用CAS失败那么进入重量级锁解锁过程,按照Monitor地址找到Monitor对象,将Owner设置为null,唤醒EntryList中的A线程。偏向锁出现在于轻量级锁的时候重入也是要CAS的就耗时了,于是用偏向锁将第一次CAS时候将对象的MarkWord头设置为线程ID(没有锁记录这个东西了)之后来只要判断是不是自己ID就行了。
线程与线程池:
线程的创建继承runnable或者callable,executorService,futuretask的是怎么操作的,无非就是利用notify和wait来通知和等待呗,线程池主要参数有核心线程数,最大线程数,空闲过期时间,任务队列,拒绝策略,流程为任务来了创建线程直到核心线程数,然后将任务加入
任务队列,队列满了之后任务来了创建线程直到最大线程数,再有任务来就执行拒绝策略,若线程空闲达到空闲时间则销毁,线程池线程数要具体调试得出,一般为计算密集型那就比cpu多一个,io密集型就是cpu的两倍,拒绝策略有四种,不接受任务并返回报错
不接受任务但不返回信息,不接受任务返回给原线程自己执行,接受任务将任务队列中存在最久的任务丢弃,对于线程池盲盒监控可以用beforeExecute和afterExecute来做一些操作,对于每个任务也可以指定提交方式submit来返回Future查看结果
线程池创建使用threadpoolexecutor不要使用executorservice因为队列长度默认为会oom,常用线程池有newSingleThreadExecutor单个线程保证执行顺序,newCachedThreadExecutor线程池大小不用限制,依赖于JVM能创建的最大线程
线程的wait和sleep区别为wait会释放当前对象锁,sleep只是释放cpu时间片,并不会释放对象锁,中止线程stop和interrput,stop强制终止,不要使用,强制释放锁可能会导致原子性问题,interrput为线程设置一个中断标志,并不会中断线程,将会在
线程的自我判断中或者执行sleep,join,wait等阻塞方法时抛出interrputException,从而安全的处理或退出,线程顺序执行可在线程中使用join方法阻塞等待,wait,notify等方法不在thread中而在object中的原因是java的锁是对象锁,在每一个对象的java
对象头中有monitor,通过记录哪个线程获取了monitor来判断某个对象被哪一个线程锁住了,所以这些方法不在thread中,synchronize和reentrantlock的区别,使用上的区别,后者要lock与unlock配合trycatch锁本身没什么区别,一般用synchronize
除非要使用reentrantlock高级用法,等待可中断构造时候设置参数为true可变为公平锁,synchronize的原理是在锁住的代码处加上monitorenter和monitorexit指令,monitorenter为0则可以获取锁并加1,monitorexit就减一,重复进入再加一,直到最后减为0释放锁
synchronize与volidate实现双重校验锁,即单例模式的饿汉模式,饱汉模式为static的属性中new,这样如果不被用到就浪费了内存,饿汉在于需要创建示例的时候再创建,首先判断示例是否为null是的话加锁再判断是否为null为null则new,第二次加锁判断是
为了防止第一次判断时另一线程已经在创建中,这样就保证不了单例了,此处还要注意对象引用要使用volidate修饰,防止指令重排,因为创建对象有三步,为对象分配内存,初始化,将引用指向初始化后的内存地址,重排后可能变为引用指向了一个未初始化的
内存地址,而另一个线程来判断对象已经不为null就会返回实例对象,这时候就会出现问题

ThreadLocal使用场景,他可以在上下文中传递数据,所以在一些框架和中间件会传递用户信息或者请求id,还有数据库的连接管理,使用连接池的情况下把数据库连接存在threadlocal中,每个线程可以自己
管理避免了线程间的竞争和冲突,比如mybatis的sqlsession就是使用了threadLocal存储当前线程的数据库会话信息,还可以在需要手动管理事务的场景中使用threadLocal存储事务的上下文,spring中的
transactionSynchronizationManager就是使用threadLocal来存储上下文信息,使用threadLocal要注意弱引用只是对于key,value不是,所以要注意内存泄露的问题,用完要在finally中remove
CompletableFuture是1.8后有的,线程的异步执行不会阻塞等待返回结果,等线程处理完会异步回调事件方法,基于事件来驱动任务,thenCombine当两个任务都完成处理回调方法,thenCompose顺序执行两个任务
thenAccept第一个任务后做第二个任务,且第一个任务返回值作为第二个任务的入参,thenApply和前一个一样不过第二个任务会有返回值,thenRun执行完第一个任务后触发一个实现runnable的任务
rpc框架底层使用较多,提升异步处理的性能
ThreadLocal是用来线程内传递数据使用的,它的构造函数是空的不用看,主要就是三个方法get/set/remove,看之前先要了解数据结构,threadLocal是一个包装类,内部方法都是操作内部类threadLocalMap的
threadLocalMap有个内部类Entry继承弱引用并有一个value属性,map中有一个类型Entry的table数组在Thread中有两个类型为threadLocalMap的属性threadlocals和inheritablethreadlocal
所以ThreadLocal的使用都是首先new一个ThreadLocal类型的引用A然后在需要的线程中使用A的set方法,这时候会去获取当前线程的属性threadlocals去将A添加进map的数组中
get方法源码:首先拿到当前线程然后获取当前线程的threadlocals为map并判断如果map不为空,直接(this),this代表的是当前threadLocal,也就是上述引用A,getEntry的逻辑为得到A的hashcode
然后和table数组的长度与获取位置i,然后返回table[i],如果table[i]不存在那就往table后面查找,因为set的时候也是遇到hash冲突往后放,然后默认16个,也就是一个线程threadLocalMap可以设置16个引用
然后得到一个Entry为e判断,e不为空则拿到返回,如果任意一步是空的则执行初始化,也就是初始化一个map并添加this到entry中value为null
set方法源码:拿到table然后获取长度并计算当前threadlocal的hashcode和长度与之后的i,判断如果i位置有数据那就是hash冲突了就往后一位去放,放完之后判断size是不是超过默认的2/3决定是否调用rehash
进行扩容,扩容时会先清空table中变为null的数据,然后计算还需要扩容的话就直接new一个新数组是原来两倍然后遍历老数组重新hash到新数组,完成后将老数组指向新数组
remove方法源码:直接拿到threadlocal的引用然后在table数组中hash定位,如果没有顺序向后查找,如果找到和该引用进行比较,相等的话把这个引用置为null并清空一下table数组中值为null的数据
看完源码后怎么理解内存泄露问题?因为Entry中的位置threadLocal是弱引用,当new的threadLocal对象的引用A如果断了,那么entry中的threadLocal就会被回收从而导致该位置为null,但是entry还在引用value
所以value不会被回收但永远访问不到,打个比方线程处于线程池中,然后在线程的run方法中new的threadLocal对象并进行设置和操作,这样的话在设置的时候会用这个threadLocal对象作为entry的弱引用然后强
引用的value,等处理完了线程没有被回收因为是线程池,然后new的threadlocal对象引用断了,然后下一次GC只剩下entry在弱引用threadLocal对象,所以会被回收,但是线程没死,所以线程的threadlocals还在
所以threadlocals的table数组也在,value也在但是访问不到了,所以调用remove会清除entry为null的数据,set如果碰到扩容也会清除,但这个不一定,所以为防止泄露要手动remove或者把threadlocal对象的
创建写成static永远存在强引用就不会被回收。如果已经发生泄露那么该threadlocal的对象已经不存在所有方法都不可用了,只能用该线程别的threadlocal对象来remove
然后如果要父子线程共享数据的话,也就是在一个线程里设置,然后启动另一个线程去执行并要获取到刚才设置的值,这种可以在任务的传递中设置traceid之类的,这个就用到了inheritableThreadLocal,new
这个对象然后用这个引用来设置,就可以在父向子线程共享,存在inheritablethreadlocal里面,父线程可以给子线程,子线程设置是无法返回给父线程的,原理在于new thread构造器创建的时候会将父线程
inheritablethreadlocal设置到子线程中,所以线程池的时候也会有问题,线程池中都是复用,没有构建新的线程就不会进行赋值

并发编程的坑:simpleDateFormat一般用来对时间格式处理,如果定义format为静态常量,在多线程调用的情况下parser方法调用establish方法,其中清空数据再设置时间并不是原子操作,可能时间错误
解决方式为定义format为局部变量,用threadLocal保存数据或用java8的DateTimeFormater类,双重检查锁的漏洞,懒汉模式下先判断为null再上锁,再判断可能会被指令优化,所以被判断的对象要volatile
多线程情况下用HashMap在1.7之前resize扩容使用头插法可能会死循环导致内存溢出,1.8后改为数组+链表或者红黑树,用红黑树是因为链表O(n)而红黑树O(logn),不直接红黑树是因为树节点占用空间是链表2倍
1.8能解决死循环吗?不会,在扩容时候当链表转换为树时for循环无法跳出导致死循环,@Async注解使用它可以开启异步功能,只需要在springboot启动类加上@EnableAsync然后在需要异步执行的方法上@Async
这里会调用AsyncExecutionAspectSupport中的doSubmit方法,一般会走这个逻辑里的else下的处理每次创建一个新线程,可以指定线程池,不指定的话默认simpleAsyncTaskExecutor核心八个,最大线程数为
Integer.MAX_VALUE所以高并发下可能有OOM。自旋锁浪费cpu资源,最经典的就是CAS,用一个死循环来更新资源,高并发下失败率高则自旋浪费资源很多,用(int i)更新失败后休眠一下
类似于sleep。ThreadLocal高并发下设置之后在try中set和get在finally中remove

文件流处理与网络相关

xml sax socket
redis使用epoll的网络io模型,首先网络io发展历史为一开始BIO分为用户态和内核态,用户态为开发人员自己写的程序代码,内核态是操作系统内部的代码无法修改只能调用,在OSI七层模型中小于TCP传输
层属于内核态高于的三层属于用户态,BIO就是写代码调用内核暴露的接口,一致没有数据到达我们的代码就一直阻塞在这个接口调用上会产生浪费,于是有了NIO,相当于用户态的代码调用内核态查询网络是否
有数据达到,内核态会立即返回结果,如果有就用户态就调用内核的read去读取数据这就是多路复用,这里问题在于用户态是一个while死循环去查询调用内核成本太高,于是加了select模型去避免这个问题,首
先用户态把需要获取的连接注册到select,select去监听内核有无数据到达,然后有数据就告诉用户态哪些可用,相应的请求去调用内核的read,这里的问题就是用户态获取内核态数据需要拷贝降低性能,还有
用户态注册到select中select还要去内核开辟空间做一个映射浪费内存,所以有了epoll增加了mmp内存地址映射和零拷贝,首先mmp技术会将内核监听的连接与用户态使用同一个不用再开辟空间映射,然后零拷贝
也不需要用户态独立开辟空间存,当有数据后sendFile直接共享内存空间,mmp用红黑树存连接,用链表存数据事件,然后只需要查看链表有无数据就可以了

网络编程:首先了解TCP和UDP,链路层,网络层,传输层,应用层,而tcp协议是连接的可靠的基于字节流的传输层协议,通过三次握手建立连接,第一次客户端申请连接服务端,服务端收到后自己接收功能正常,第二次服务端返回消息,客户端确认了自己的发送
接收和对方的接收发送是ok的,第三次客户端发送,服务端就知道自己的发送是正常的,通过三次握手确认双方的发送和接受都是正常的。UDP是无连接的不可靠传输层协议,只管发不管收没收到,qq聊天视频直播就是。可一对多,TCP基于连接所以只能一对一
Http对应的是应用层,基于TCP连接,ip协议对应的是网络层,Socket是一个API,在应用层和传输层之间,通过它可以使用TCP/IP,理论上Socket是长连接,不会中断,但由于环境因素可能会断掉,为了判断连续需要发送心跳消息,http协议则是一个短链接,即客户端向服务端发一次
请求,服务器响应返回后就会断开,webSocket是应用层让客户端和服务端双向通信的工具,支持了服务端向客户端主动推送数据。Socket的使用需要出现成对的套接字,在服务端一个ServerSocket并执行accept方法阻塞等待输入流,,客户端Socket与服务端建立
连接并发送输出流,如何告知接收方已经发送完消息呢?一个是调用socket的shuitdownOutput而不是后者关闭输出流Socket会直接关闭,前者会告知已经完成消息写入,可以决定要返回消息还是关闭Socket,但这样要再发送消息要重新连接
所以可以通过约定符号的方式,这样不用关闭流,但是符号不好选取,太简单容易出现在正文,太复杂不好处理又占用带宽,所以可以采用指定长度的方式,约定两个字节读取,客户端先发字节长度,然后再发消息。服务端先读取两个字节长度,再读取消息。
一般情况下服务端要处理多个Socket请求,一般是使用多线程来处理,一个Socket请求就创建一个线程来处理它。
IO的方式,同步阻塞BIO,同步非阻塞NIO,异步非阻塞AIO,BIO是在服务端启动一个serverSocket然后在客户端启动socket进行通信,默认情况下服务端要建立多个线程等待请求,客户端发送请求后咨询是否有线程响应,如果没有则会一直等待或被拒绝,如果
有线程响应,那么客户端线程会等待请求结束后才继续执行。NIO是基于事件驱动思想来完成的,每个请求来了注册一个channel到selector中,selector主动轮询channel如果channel的buffer中准备就绪了,选择准备就绪的处理请求,selector后面只有一个线程
AIO是异步的,需要读写操作时直接调用read或者write即可,之后立即返回处理自己的事,等待操作系统准备完成后通知在进行操作,目前java中没有实现

高性能负载均衡-分类和算法
常见的负载均衡分三种:DNS负载,硬件负载,软件负载
DNS负载均衡:解析同一个域名返回不同的ip地址,一般用于地理级别的均衡,同一个域名南北方获取不同的地址,成本低,就近访问提升访问速度,不过更新不及时,扩展性差
硬件负载均衡:用单独硬件实现
软件负载均衡:Nginx,比较简单且灵活配置,但是不具备防火墙功能和硬件相比性能也一般
算法主要有四类:任务平分类,如轮询。负载均衡类,将任务分给负载最低的服务器,负载可以是连接数,cpu负载等等。性能最优类:将任务完分配给处理速度最快的。Hash类:根据任务的某些关键信息Hash运行,将值相同请求发到同一台服务器,例如同一事务

Maven

Maven安装流程比较简单,主要更新文件定制仓库路径和下载jar包路径还有镜像。配置进IDEA就可以方便使用了,然后主要命令有如下几个:

---------------实际应用-----------------------
# 1、刷新子模块版本号: 
mvn versions:update-child-modules
# 2、重新打包到maven本地库: 
mvn clean install -Dmaven.test.skip=true
mvn install
# 3、部署包到远程服务器
mvn clean deploy -Dmaven.test.skip=true

#---------------------一般常用命令-----------------------
# 该命令打印出所有的java系统属性和环境变量
mvn  help:system 自动在本用户下创建   ~/.m2/repository
# 清理输出目录默认target/
mvn clean
mvn clean compile     清理编译
# maven test,但实际执行的命令有:clean:clean,resource:resources,compiler:compile, resources:testResources, compiler:testCompile,maven在执行test之前,会先自动执行项目主资源处理,主代码编译,测试资源处理,测试代码编译等工作,测试代码编译通过之后默认在target/test-calsses目录下生成二进制文件,紧接着surefile:test 任务运行测试,并输出测试报告,显示一共运行了多少次测试,失败成功等等
mvn clean test  清理测试
mvn clean package 清理打包
mvn clean install  清理将打包好的jar存入 本地仓库  注意是本地仓库
mvn clean deploy  根据pom中的配置信息将项目发布到远程仓库中 

echo %MAVEN_HOME%:查看maven安装路径

---------------------创建项目-------------------------------
mvn -version/-v    显示版本信息
mvn archetype:generate  创建mvn项目,使用Archetype生成项目骨架
mvn archetype:create -DgroupId=com.oreilly -DartifactId=my-app 创建mvn项目
# 创建Maven的普通java项目:
mvn archetype:create -DgroupId=packageName -DartifactId=projectName 
# 创建MavenWeb项目:  
mvn archetype:create -DgroupId=packageName   -DartifactId=webappName-DarchetypeArtifactId=maven-archetype-webapp   

---------------------优化依赖命令-------------------------------
mvn dependency:list   显示所有已经解析的所有依赖
mvn dependency:tree  以目录树的形式展现依赖,  最高层为一层依赖 其次二层依赖 三层依赖....
mvn dependency:analyze  第一部分显示 已经使用但是未显示依赖的的  第二部分显示项目未使用的但是依赖的

---------------------第三方jar 发布到远程仓库---------------------
mvn deploy:deploy-file -DgroupId=com -DartifactId=client -Dversion=0.1.0 -Dpackaging=jar -Dfile=d:\client-0.1.0.jar -DrepositoryId=maven-repository-inner -Durl=ftp://xxxxxxx/opt/maven/repository/

---------------------第三方jar 安装到本地仓库---------------------
mvn install:install-file -DgroupId=com -DartifactId=client -Dversion=0.1.0 -Dpackaging=jar -Dfile=d:\client-0.1.0.jar -DdownloadSources=true -DdownloadJavadocs=true

#你是否因为记不清某个插件有哪些goal而痛苦过,你是否因为想不起某个goal有哪些参数而苦恼,那就试试这个命令吧,它会告诉你一切的.参数: 1. -Dplugin=pluginName   2. -Dgoal(-Dmojo)=goalName:-Dplugin一起使用,它会列出某个插件的goal信息,如果嫌不够详细,同样可以加-Ddetail.(:一个插件goal也被认为是一个 “Mojo)
mvn help:describe -Dplugin=help -Dmojo=describe

mvn -e    显示详细错误 信息.
mvn validate  验证工程是否正确,所有需要的资源是否可用。
mvn test-compile 编译项目测试代码。 。
mvn integration-test  在集成测试可以运行的环境中处理和发布包。
mvn verify    运行任何检查,验证包是否有效且达到质量标准。 
mvn generate-sources  产生应用需要的任何额外的源代码,如xdoclet。

  • 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

maven中文件优先获取本地仓库,获取不到使用配置的仓库地址和central远程仓库,如果也获取不到可以配置mirror镜像,镜像mirrorof对应仓库id,匹配到第一个就执行,不可覆盖,如使用*则所有仓库都用该镜像

spring(ssm与springboot)

spring原理与bean生命周期

spring的主要原理是IOC和AOP,ioc是典型的工厂模式和代理模式,applicationContext就是工厂类,所有的bean都在它的map中,我们可以通过bean的生命周期去理解和操作bean的构建,主要在beanpostprocessor和init-method中,bean的生命周期包含了三级缓存来解决循环依赖。而AOP就是代理模式一般用于日志监控等,@autowired和@resource一个是spring提供的一个是java提供的,前者是type查找,与之对应的按name查找的是Qualifier,后者默认name匹配不到就按type,依赖注入的方式有构造器注入,setter注入和注解,bean的生命周期为实例化bean-设置对象属性并依赖注入-处理Aware接口-beanpostprocessor-执行init-method方法-disposableBean接口调用destory方法,bean的作用域有singleton单例,prototype原型每次都请求创建,request每个网络请求创建一个,session每个session创建一个
spring中有两个相同id的bean的处理方式:在同一个xml中是不能存在相同id的bean的否则启动就会报错,因为spring启动会校验id的唯一性,在spring对xml解析为BeanDefinition的时候。但是多个spring配置文件那就会默认覆盖,加载过就不会在加载了,现在一般使用@Configuration声明配置类,然后用@Bean实现bean的声明取代xml形式,此时声明了相同id的bean只会注册第一个声明的实例

spring如何解决循环依赖:循环依赖是指一个或多个对象之间直接或间接的依赖关系形成了循环调用,如图:
循环依赖图示
解决的方式类似于JVM,JVM中类加载机制有加载,验证,准备,解析,初始化这么几个阶段,而且是在第一次用到类的时候才加载且只加载一次,循环依赖的情况下首先会加载A类然后验证然后准备,在准备阶段需要B类的时候加载B类,等B类到准备阶段的时候由于A类已经被加载,B就可以获取A类初始化完成,之后A类也可以初始化了,这是属性依赖,类加载发生的时间不是强制的,初始化会在new或者反射的时候才会触发,还有一种依赖是构造器依赖,这是无解的,会导致java虚拟机调用栈溢出,因为构造器之间构成了死循环,spring对属性依赖使用了三级缓存的方式,第一级缓存用于保存初始化完成的bean实例,第二级缓存用于保存实例化完成但没注入初始化的半成品bean实例,第三级缓存保存bean的创建工厂,便于后面获取bean对象,bean的加载流程为:执行获取单例A的操作依次从一二三级缓存中查找,找不到则将A加入到singletonCurrentlyInCreation的set中,标记A为创建中,然后反射获取A实例,此时属性都为null,创建玩之后将创建工厂放入singletonFactories也就是三级缓存中,然后为A属性赋值,发现需要B,于是B也走一遍上述流程直到对B属性进行赋值,发现在三级缓存中有A的构造工厂,然后判断A是否需要AOP,从而通过getObject获取earlyBeanReference返回原始bean或者代理对象bean,将这个bean放入二级缓存并删除三级缓存的A工厂(之后别人需要A就可以在二级缓存获取),然后进行B的初始化,完成后B会直接放入一级缓存并且删除B的二三级缓存以及singletonCurrentlyInCreation的set,然后A的属性赋值就可以继续了,A初始化完成后删除二三级缓存和set,此时循环依赖被解决。这里可以发现其实循环依赖解决只要两级缓存就够了,类似JVM只有实例和class信息就解决了,为什么spring要三层呢?原因在于AOP,如果只有两级缓存,那么二级缓存放入的就是普通对象,虽然也可以放入aop对象,但这样就打破了bean的生命周期,原本是在beanpostprocessor后置处理生成aop对象的,两级缓存则要在初始化之前就生成aop了,违反了spring原则。除了三级缓存外,还可以通过@Lazy注解来解决,一般bean都是在启动的时候加载,用了该注解就只会在调用到的时候才加载,也可以解决循坏依赖。

像beanpostprocessor去做功能增强只在bean的初始化流程中一次性的,而aop代理对象的invoke增强是每次调用都有的,它的判断为扫描所有@aspectj的bean然后判断有无@before,@afterReturning等注解的增强方法,通过pointcut路径扫描是否包含当前bean,如果有则生成代理对象,还有就是事务方法,事务也会生成代理对象
bean的生命周期,通过构造方法实例化一个对象,得到后判断有无@autowired属性,有就找出来赋值注入,依赖注入后判断有无实现Aware接口,实现了就表明当前对象要实现这些接口中定义的setBeanName,setBeanFactory等方法,spring就会调用这些方法并传入相应参数,aware回调完成后判断该对象有无@PostConstruct注解的方法,存在则调用,然后判断有无实现initializingBean,如果有就表明该对象要实现afterPropertiesSet方法进行初始化,最后判断是否要AOP。
至于构造方法的判断,只有一个构造方法就选该方法,多个构造方法默认使用无参构造方法,没有无参方法则报错,如果某个构造方法有@autowired,说明使用这个构造方法,这也是spring的构造器注入方式
spring中扩展点及其应用:spring启动时会先解析bean定义为BeanDefinition,在实例化bean,首先在解析阶段,可以用BeanFactoryPostProcessor或者@Import来注册bean的定义,bean生命周期阶段可以用beanPostProcessor来进行前后置化干预,Aware接口如BeanNameAware,BeanFactoryAware等在bean实例化过程中调用的方法,initializingBean在初始化时调用等于init-method,这些接口可以用在整合其他的组件与服务上,帮助架构和系统的搭建集成。
@value类似于@autowired,但是一般是读取配置文件的信息,@value(“something”)会将something赋值给属性,如果不是string或者无法转化为string会报错。@value(“${something}”)会吧something当作key从配置文件中读取,没找到则把双引号中值当作字符串注入,@value(“#{something}”)会被当作spring表达式解析,把something当作beanName从容器中寻找对应bean注入,没有则报错,@value要在spring管理下的类中才能使用,且不能对final和static的属性生效。
spring事务失效的原因:首先spring本身不支持事务,是利用了数据库的事务,所以mysql用myisam就会失效,它不支持事务,还有就是修饰非public方法会失效,因为computeTransactionAttribute类中会判断非public就返回null不支持事务,还有就是final和static修饰的方法失效,因为spring事务使用了动态代理,final无法重写方法,static无法动态代理,还有就是同一个类中非事务方法调用事务方法也会失效,因为在同一个类中调用方法会用this,就用不到代理对象,就不存在事务了,还有未被spring容器管理没有@component也会失效,多线程调用也会失效,因为同一个事物指的是同一个数据库链接,在不同线程中数据库链接也不同,所以是不同的事务。还有自己吞了try/catch,或者配置了不正确的事务特性,有七类特性,一个是存在就加入,不存在就新建,是默认的Required,一个是存在就加入,不存在就非事务运行,一个是存在就加入,不存在抛异常,一个是新建事务,之前有事务就把之前的挂起,一个是非事务运行,存在事务就挂起,一个是非事务运行,之前存在就抛出异常。还有失效的场景就是手动抛出别的异常,默认是runtimeException如果像触发其他的回滚就要在@Transactional(RollbackFor=)但是这个配置仅限于Throwable异常与其子类,spring事务在开始时通过aop生成一个connection连接,之后sql命令都通过该连接操作,然后才能得到回滚。

springboot原理与starter编写

springboot自动配置原理在于@springbootApplication中的EnableAutoConfiguration中使用了springFactoryLoader的loaderFactory方法读取了resource/META-INF/文件中配置的需要自动注入的bean的路径来进行自动配置,所以我们staters编写的时候需要自动注入的bean也要将路径也在这个文件中,controllerAdvice配合exceptionHandler可以处理异常配置,commandLineRunner可以在启动的时候做一些功能处理

springboot相关

spring接口多个实现类情况下会注入哪一个?不同环境下用不同实例等情况
那么如果你注入接口就会报错,spring不知道要用哪一个,这时候低级的办法是找一个实例加上@Primary设为默认加载的,或者用@Resource注入,因为默认先是按名字注入
@Resource
IDemoService demoServiceBeijing; 这样会先找demoServiceBeijing这个实例去注入
或者这样指定名字
@Resource(name = “demoServiceBeijing”) //使用resource注解明确指定名称
IDemoService demoService;
同理用@Qualifier也一样但是很耦合,不同自适应,情况变化代码也要跟着变化
用配置来动态获取
@ConditionalOnProperty(value=“”,havingValue = “beijing”)
使用Conditional放在每个实例本身上,然后配置文件中填的是哪个,注入的时候会自动注入哪个,这样就不同配置都不用改代码
上述为不同情况下只需要一个实例的场景,如果是同一场景下需要多个实例呢?比如用到策略模式的时候
首先多个实例实现接口方法,然后定义一个策略service,将所有策略实现类添加到map中,因为spring默认会把所有实例都注入构建成一个list的,取一个他就不知道是那个了,取list就会返回所有的

springboot内置tomcat的原理:基于SPI方式,首先要依赖springboot-starter-web则会自动添加ServletWebServerFactoryAutoConfiguration类,该类@import了ServletWebServerFactoryConfiguration
其中有tomcat,还有jetty和undertow,通过@ConditionOnClass选择容器默认是EmbeddedTomcat(要用别的那就是去除tomcat需要的class,加入别的容器需要的class就会启动别的容器了)
在EmbeddedTomcat中配置了一个tomcatServletWebServerFactory的bean,其中有个getWebServer方法,逻辑是new了一个tomcat,然后进行一些配置后通过()启动容器,然后启动完还需要挂起
等待请求,通过startDaemonAwaitThread启动一个等待守护线程,run方法逻辑为().await(),不挂起的话启动执行完线程结束了,所以await来等待请求
springboot启动的时候run方法会创建spring容器,然后调用refresh方法加载ioc容器,refresh逻辑是先invokeBeanFactoryPostProcessors解析自动配置类,然后tomcatServletWebServerFactory被加载进来
然后会调用一个onRefresh方法去刷新ioc容器,onRefresh中会调用createWebServer,里面回去获取webFactory也就是上面加载的配置类实例,然后就会调用这个实例的getWebServer方法启动tomcat
那么同样的springmvc怎么加载进来的呢,也是一样的springmvc就是最关键的dispatchServlet所以有一个DispatchServletAutoConfiguration自动配合类,@Bean一个DispatchServlet就可以处理请求了

AOP实现原理:具体整个流程太多了,大致分为三步,首先@EnableAspectJAutoProxy注解通过@Import注册一个BeanPostProcessor处理AOP,然后创建Bean的时候调用BeanPostProcesor解析切面@Aspect将切面中所有通知解析为Advisor排序放入List中缓存
然后在Bean初始化后调用BeanPostProcessor拿到之前缓存的advisor判断当前bean是否被切点表达式命中,如果匹配为Bean创建动态代理,然后进行调用,通过代理类执行方法增强。

控制SpringBean的创建顺序,用@DependsOn(“beanName”)适用于一个bean优先于一个bean的情况,如果有一个bean要优先于很多bean的话这个就很麻烦,那么要从原理来考虑,@Component是通过扫描后加到
BeanDefinition中的List里的并基于这个List来创建,那么就要控制List的顺序,可以在扫描前在这个List中先放一些类、要写一个容器初始化器实现ApplicationContextInitializer,然后这个类要写
文件里表明有这个初始化类,然后重写initialize去调用applicationContext的addBeanFactoryPostProcessor方法添加一个BeanPostProcessor,然后在postProcessorBeanDefinitionRegistry里写一下逻辑将
注册一下需要的bean就好了

spring的schedule方法,首先在springboot启动类加上@EnableSchedule然后在定时方法上@Scheduled,定时任务默认是使用一个定时任务线程执行的,所以为了避免等待,可以为定时任务配置线程池
实现SchedulingConfigure来定义不同任务用不同线程,但是相同任务还是在同一个线程,在定时任务上使用@Async则相同任务每次也用不同的线程

为什么springboot的jar包可以直接运行?java -jar xxx 会去找到jar包里的manifest文件其中指明了Main-Class然后开启线程去执行这个类里的Main方法,springboot在这一层上做了封装,他的Main-class为JarLauncher,jvm将这个类里的main方法运行起来后
main方法会找到manifest里写的Start-Class就是我们的@SpringbootApplication

在springboot中除了启动类的mainclass如果还有别的main方法,本地启动不会有问题但是打包package时会报错提示多个主方法,这时候就要在mavne的pom中指定打包的mainclass,具体在springboot-maven-plugin中

<build>
        <plugins>
  <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${springboot.maven.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <mainClass>com.hundsun.gaps.runtime.springboot.GapsSpringBootRunner</mainClass>
                </configuration>
            </plugin>
 
     </plugins>
       <finalName>bupps-ecs-acct-deploy-${project.parent.version}</finalName>       
 </build>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

这样就能确定打包的时候应用启动类是哪个
jar和war启动区别,jar先执行springbootapplication的run方法,然后启动ioc容器再启动嵌入式servlet容器,war先启动servlet容器,容器启动springboot(SpringBootServletInitializer)然后启动ioc容器
SpringBootServletInitializer实现WebApplicationInitializer由于现在都在用注解取代xml配置方式,WebApplicationInitializer就类似于如果不需要打war包部署到外部服务器是不需要操作的,springboot内嵌服务器使用jar包就可以执行,如果要
将springboot应用打成war包部署到外部容器,由于外部容器无法识别应用的启动类所以要继承SpringBootServletInitializer重写config方法,比如实现@SpringBootApplication的启动类去继承SpringBootServletInitializer然后重写configure
用(启动类.class)将启动类注册为配置类,然后如果打成jar包则正常运行,打成war就可以上传到外部服务器并会执行configure中的逻辑,内部原理如下
首先有一个类SpringServletContainerInitializer,它有个注解@HandlesTypes可以传入一个类,这里传入WebApplicationInitializer,然后有一个onStartup方法,主要逻辑就是遍历WebApplicationInitializer找到非interface和abstract的实现类加入到
LinkedList并根据annotationAwareOrderComparator进行排序然后循环调用WebApplicationInitializer实现类的onStartup方法,所以我们上述实现的SpringBootServletInitializer就会在onStartup中执行调用了,那SpringServletContainerInitializer又是
在哪里用到呢?它只有一个接口ServletContainerInitializer在下,通过SPI机制当启动web容器时自动到相应jar包下找到META-INF/services下以ServletContainerInitializer的全路径名称命名的文件,文件内容就是ServletContainerInitializer
的实现类的全路径将他们全部实例化,因为SpringServletContainerInitializer是它的一个实现类,所以你可以在SpringServletContainerInitializer所在jar包META-INF/services下找到文件内容是SpringServletContainerInitializer
的全路径类名,整个流程就是在web容器加载时候通过SPI机制将SpringServletContainerInitializer加载调用onStartup方法,然后这个方法会将WebApplicationInitializer的实现类加入到list中并按顺序执行他们的onStartup方法,其中SpringBootServletInitializer实现了
WebApplicationInitializer并重写了onStartup方法,所以执行的时候会通过createRootApplicationContext方法来执行application的run方法,接下来的过程就和jar一样了,在内部创建ioc容器只是以war形式应用在创建ioc时不再创建servlet容器
springboot内置Tomcat的启动原理:依赖了Springboot-starter-web后会在springboot中注入ServletWebServerFactoryAutoConfiguration的配置类,该类通过@import导入了可用(@ConditionalOnClass判断决定使用哪一个)的一个Web容器工厂,默认Tomcat
在内嵌Tomcat类中配置了一个TomcatServletWebServerFactory的bean会在springboot启动时加载ioc容器创建内嵌的Tomcat并启动

springMVC原理与流程及过滤拦截

springMVC流程在中设置servlet为dispatchServlet然后收到请求调用handleMapping根据配置找到具体执行的handle以及相应的handleInceptor返回,dispatchServlet再调用handlerAdapter适配找到具体的controller处理,处理完后返回ModelAndView,handlerAdapter将ModelAndView返回dispatchServlet,servlet提交给ViewResolver解析后返回具体的View,即JSP,常用的注解有@RequestMapping,@RequestBody等,其中put/get/post/delete对应增查改删,put幂等,post表单提交非幂等,get获取,当然也可以提交,只需将参数放在url的?后面

mybatis原理及缓存与分页

mybatis原理为sessionFactory读取xml配置文件中的mapper,entity和driver等配置进入Configuration中,然后底层还是jdbc的形式去调用。通过simpleExecutor去执行sql,一级缓存二级缓存很鸡肋,一级缓存是session级别的,将具体sql和sql的id绑定,查询缓存中的数据,如果有任何更新清除所有缓存,在分布式情况下多session易出现数据问题,二级缓存是mapper级别的,如果多个表联查也无法缓存,使用一级二级缓存熟练度不高极易数据出错。mybatis分页有PageHelper,本身支持三种方式,一个是limit关键字,一个是用rowbounds将所有数据查到内存中后在内存中分页,还有是使用Interceptor拦截器动态构建分页sql,像pageHelper也是基于拦截器的,在查询前先通过count查出所有条数,或者一些数据加密脱敏之类的也可以用拦截器做。
mybatis中${}会替换为?,使用prepareStatement,而#会直接替换,不要使用,不能防止sql注入
mybatis-plus对mybatis的优化原理:

redis原理与zookeeper

redis的持久化机制由RDB快照和AOF写入日志,redis快的原因在于单线程不需要上下文切换,内存中操作,渐进式rehash以及io多路复用,数据类型由string,hash,list,set,zset等,过期策略为定时删除加惰性删除,定期选一批数据判断,到期的就删除掉,不能全部扫描太耗性能。惰性删除就是每次查询先判断是否过期,过期了就删掉。内存淘汰策略为lru按最近使用次数,random随机删除,ttl选择最近要到期的,lfu时间加上使用频率,然后都有volatile和all两类,即在有过期时间的和全部的key中,redis使用单线程的原因在于cpu不是性能瓶颈,内存的大小和网络的io才是,10w/s的查询速度,在6.0之后变为多线程因为有些公司业务需要。redis的优化,首先key不要设置太大,还有就是RDB和AOF不要开启,如果数据重要的话就在slave中开启APF每秒同步一次,redis的nosql是一个巨大的map,渐进式rehash的原理是首先在查找中一般由两类解法,各类平衡树或者哈希表,哈希最快但是不能范围查询,平衡树有些可以范围查询,一般的map和字典都是哈希表查询,用key算hash然后用链解决hash冲突,在加载因子到一定值后扩容,redis的map太大了,如果一次扩容必然阻塞,因此有了渐进式rehash,需要扩容时,并不一次完成操作,而是在之后的每次redis操作中按顺序更新一小部分数据,直至完成扩容,是一种分治法的思想。
分布式锁:JUC提供的锁机制只能保证同一个JVM中的并发,微服务情况下多个JVM就需要一个中间人,分布式锁就是用来保证在同一时刻只有一个线程能执行操作
主要的特性:也是锁的特性,互斥,不会死锁,加解锁的线程是同一个,可重入,容错性(集群红锁)
常见的三种实现方式:数据库锁,redis分布式锁,zookeeper分布式锁
数据库锁比较简单,用版本号作为乐观锁即可实现
redis分布式锁:设计首先为了防止死锁,要在try中使用,finally中释放,上锁的逻辑是使用redisTemplate的setIfAbsent互斥上锁并且同时原子的设置过期时间,防止极端情况下锁无法释放,锁的超时时间设置为业务时间的三倍差不多。同时为了防止A上锁执行超时,然后自动释放,B上锁成功,A执行完毕释放了B的锁,然后C上锁的情况,解锁只能是上锁线程来执行,所以UUID作为value,解锁的时候比较和释放是两步操作,所以用lua脚本实现原子性操作。到此为止在单机情况下的普通redis分布式锁就实现了,但是还有很多问题,比如A超时执行的情况下,B还是可以获取到锁的,那么同时会有多个线程在执行,而且不能实现可重入,如果A的锁方法调用了X方法,X方法也需要锁,那么就无法实现了,超时的问题使用看门狗机制,为redis设置一个守护线程,执行定时任务去redis服务端做一个检查,如果查询发现锁还在,为它续签,也就是更新延长锁的过期时间,当然万一就是服务挂了这样一直续签是不能忍受的,所以可能还需要一个监控服务,类似于心跳机制去连接微服务还是否可连接,n次无法通信后可能就由监控服务去释放锁。对于可重入性不能直接在方法中传递参数,侵入性太强,所以value改为用一个静态全局UUID作为APP_ID,加上thread-id作为value,这样在确定了哪一个微服务中哪一个线程在执行,同一个线程可重入。但如果启动一个子线程去执行这样就不行了,这时候要使用InheritableThreadLocal,它的原理是在Thread类中启动子线程的时候会在init()方法中把InheritableThreadLocal的map复制过去,这样就可以获取父类的threa-id了,但要注意不能和线程池一起用,因为线程池线程复用,不会调用init(),无法复制。如果是微服务之间互相调用,可能又要使用微服务调用的traceid来处理,所以value要具体问题具体分析。重入也需要设置超时时间,要防止小于最初时间,而且微服务之间rpc传递的话也不能存在内存中,所以要用一个key存在redis中,最简单的方式就是放大,每次都往上加过期时间。还有就是锁重入要进行计数,否则任意一次锁的释放将把整个锁释放掉,这就计数也需要一个key,之后释放锁要先-1,到0才能释放锁。同时这两个key都是要设置过期时间的,不然异常情况锁就不可用了,他们都是附属于锁的,因此可以使用hash数据结构,key是锁,hashkey是锁的属性,hahsvalue是属性值,超时时间就只要设置key就行了。至此,单机下的redis分布式锁基本完成了,redission都实现了,所以推荐用redission。多机的redis有两类一个是哨兵模式,也就是主从模式,还有一种是集群模式。前者复制只能是RDB或者AOF,那么会有延迟的情况在,我们使用从redis做备份,当主redis宕机之后,从redis顶上接着服务,问题点:当A服务从主redis获取锁之后,主redis掉线了,但是因为还没有把A获得所得状态同步到从redis上去,这时候从redis升级为主redis,B服务又去申请了同一个分布式锁,因为并没有同步到A已经获得这把锁的状态,所以此时就有两个服务获得同一把锁,这样就会出问题了。所以集群模式的红锁方式解决了这个问题,当某个服务分别向整个集群中每个实例去获取锁,如果从大于等于n/2+1个实例获取锁成功,则获取分布式所就成功。也就是大于一半的节点响应就可以获取锁,但可能消耗时间会比较久,在于对集群建立通信。

redis持久化有RDB和AOF在4.0之后支持两者混合,简单来说AOF就是将写操作存下来,宕机则按顺序恢复,并且采用写后日志的模式,即redis先执行命令写内存后写日志,原因在于先写内存可以防止错误命令记录到日志,同时记录AOF日志也不用再进行
命令检查,而且命令执行完再写入也不会阻塞主进程写操作。redis为了避免AOF追加命令导致文件越写越大,提供了AOF重写机制,因为很多写操作执行完最后的效果可能只要简单几个命令就能实现,重写即把冗余命令替换,当然因为redis单线程,所以
大量重写可能阻塞就不能执行客户端请求了,所以是通过fork一个bgrewriteaof子进程进行aof日志重写,就不会影响主进程,但fork的子进程也是主进程实现,所以要复制主进程的页表结构等,若数据量过大就可能阻塞,然后这里就可能会有数据不一致
问题,子进程在写原来数据的时候主进程处理新命令,办法是主进程执行完命令会写入AOF缓冲区和AOF重写缓冲区,子进程重写完成后会异步向主进程发一个信号,主进程接收到信号将AOF重写缓冲区的内容追加到新的AOF文件中,即重写完的文件,然后修改
文件名原子切换新老文件,保证重写一致性

redis的散列表dict是键值对的集合类型,类似于HashMap由数组和链表构成,数组的每个位置叫做哈希桶,当出现hash冲突的时候在桶下挂一个链表,一般情况用dict结构,不同在于dict有两个hash表指针,一个是正常查询的,另一个是渐进式rehash使用的
这里就有点类似于concurrentHashMap里面的扩容时候有两个table,没迁移就正常取,迁移了就指向原来的。但当满足以下两个条件时会使用listpack,7之前是ziplist,会有连锁更新的问题
每个键值对中的key和value都小于hash-max-listpack-value的配置值(默认64)时以及每个dict桶键值对数量小于hash-max-listpack-entries的配置值(默认512)。每次向散列表写数据的时候都会判断是否需要转换底层数据,当不满足时候会转为dict,一旦
转为dict不能再退化,使用listpack虽然搜索不能O(1)但是大大减少内存,而且由于数据量小性能不会有很大影响,为了对上层屏蔽存储的不同结构,所以使用hashTypeIterator迭代器来实现散列表的查询。

redis和zookeeper的区别,同为key-value组件应用场景有很大不同,redis一般用于缓存,数据库和消息中间件,zookeeper一般用于配置中心,分布式同步,分组服务,即redis更注重于数据存储本身,zookeeper更注重于数据协同
zookeeper通过forceSync配置来控制是否开启WAL,默认开启,每次更新刷盘保证分布式系统可靠性,redis的aof默认每秒执行一次极端情况会有数据丢失,前者可靠但可用性低于后者,后者吞吐量高可靠性低于前者,至于分布式锁两者都可以实现
但一般redis在项目中必用而zookeeper如果配置中心用了apoll这些那就不会专门为了分布式锁引入新组件

mysql与oracle

数据库DQL语句指查询语句select,DML语句指操纵语句:就是更新,DDL定义语句就是结构的,建表,索引视图这些,这是隐式commit的不能回滚,DCL语句控制权限,GRANT,rollback,commit这些。mysql执行过程:首先是连接器,然后是分析器,然后优化器,然后执行器,这都是server层的,然后执行器调用引擎innodb进行数据的查询
mysql中MyISAM和InnoDB的区别是什么:InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务。InnoDB是聚集索引,使用B+Tree作为索引结构,数据文件是和(主键)索引绑在一起的(表数据文件本身就是按B+Tree组织的一个索引结构),必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。MyISAM是非聚集索引,也是使用B+Tree作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。InnoDB不保存表的具体行数,执行select count(1) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数。Innodb没有这个变量的原因在于事务,同一时刻不同事务的行数是不同的!
count是返回该列的不包含null的条数总数,所以count(1)不一定等于count(某个字段)。
mysql在RR级别下使用select for update当前读自动开启next-key-locks,意向锁指的是当一个事务想要锁表的时候,会先查询该表有无意向锁,若有表明有其他事务锁了表,就要等待释放,若无则加上意向锁之后再加表锁,这样就不用去查询表中每一条数据是否上锁了,相当于一个标记,意向锁是server提供的,用户无法操作意向锁。
mysql执行过程:首先是连接器,然后是分析器,然后优化器,然后执行器,这都是server层的,然后执行器调用引擎innodb进行数据的查询
连接器:是半双工模式,就是同一时刻只能客户端向服务端发送请求或反过来,不能同时进行,首先验证用户名和密码是否正确,然后验证权限,权限表一共四个,user,db,tables_priv和columns_priv,验证过程为先判断user是否是全局用户,通过就结束了,否则再判断db,是否由库权限,再判断tables表权限,再列权限的顺序。在这之后会查询缓存,缓存以key和value的形式存储,key为具体sql,value是结果集,如果无法命中缓存,会走到分析器,命中就直接返回,缓存在8.0后删除了,因为失效非常频繁,在写入较多的情况下,缓存的新增和失效非常频繁命中率极低。
分析器:主要作用对sql进行语义分析,解析语法和判断是否能执行,比如表和列是否存在等
优化器:进入到优化器阶段说明sql是符合语义且可以运行的,此处主要是sql的优化,explain就是模拟优化器的模式指定执行计划返回,因此不会执行,不过如果有子查询,子查询会被执行。否则主查询的计划生成不了了。这里的优化在于比如有个联合索引abc,sql查询where b=x and a=x看起来不满足最左前缀原则,不会走索引,优化后变为where a=x and b=x,在此阶段是自动按照执行计划进行预处理选择一个最佳执行计划
执行器:执行器阶段会调用存储引擎的API,常用的innodb,会根据语句是DQL还是DML不同的执行,DML会写undolog存储事务id和回滚指针,然后在bufferpool中查询是否目标页存在,存在的话DQL就返回,DML如果普通索引就写入changebuffer中,有唯一索引因为要比较唯一冲突因此会去磁盘,如果不在bufferpool中就去磁盘读取该页(16kb)所有数据进bufferpool,如果索引使用频率达到一定程度,会触发自适应hash的产生,以hash的形式查询是最快的,但是只能生效于const类型的查询,即一条数据的情况,执行完成写入redolog中,事务提交,将redolog置为prepare然后写入binlog中,写完后将redolog置为commit,这样二阶段提交是为了保证redolog和binlog数据一致
为什么要二阶段提交:因为每次操作都更新到磁盘很慢,于是引入了两个日志redolog重做日志和binlog归档日志,这种技术是WAL,write ahead logging ,redolog是物理格式的日志,记录了修改的信息,是有大小限制的,写满了就要刷盘之后再写入,有两个点writePos和checkpoint一个记录当前写到哪里了,一个记录刷盘刷到哪里了,这两个相遇就说明写满了。有了redolog,异常宕机重启后数据也不会丢失,但是redolog只能保证数据存在,不能保证事务正确性,所以引入了binlog,是一个二进制的逻辑日志,可以简单认为是执行过的sql,用于主从同步,是由server提供的,而redolog只有innodb有,binlog是追加写入的,一个文件写到一定大小后会切换到下一个,无限。再来看二阶段提交,为了保证两个日志的数据一致,如果只写了redolog,重启后redolog恢复了数据,binlog没有,库之间去同步就会出现从库数据少了,如果只写了binlog,重启后redolog没执行,那么库之间同步出现主库数据少了。这两者的刷盘频率可以设置,都设为1则每次都会提交最安全,设置为0,那么redolog会放入redologbuffer,binlog会放在binlogcache中,速度快但在内存中会丢失不安全
慢sql的优化:首先开启慢sql查询日志,找到执行慢的sql,然后使用explain查看执行计划,limit 1可以防止全表扫描,如果有唯一索引就没必要加了,本来就是const不会全表扫描,explain中的select_type有simple,表明是普通查询没有子查询,primary是复杂查询的最外层查询。Type字段有System,这是很少见的,表明只有一条数据,然后是const是唯一查询,ref表明使用了索引,range表明是范围扫描,index表明用了索引全表扫描,不需要排序,All就是完全的全表扫描最慢,Possible_key表明可能用到的索引并不是一定用到,rows列表明估算的要读取的行数,很重要,越少越好,extra列using index表明用了索引,using where表明用了条件查询而且查询列没用到索引,using fliesort用了外部排序,没用索引,一般是All查询
举例一句sql的执行:sql中每一步产生的结果以临时表提供给下一阶段使用,顺序为from-join/on-where-groupby-having-select-orderby-limit
select distinct from T t join S s on = where =“Yrion” group by having count(1)>2 order by s.create_time limit 5
第一步就是选择出from关键词后面跟的表,这也是sql执行的第一步:表示要从数据库中执行哪张表。join是表示要关联的表,on是连接的条件。通过from和join on选择出需要执行的数据库表T和S,产生笛卡尔积,生成T和S合并的临时中间表Temp1。on:确定表的绑定关系,通过on产生临时中间表表示筛选,根据where后面的条件进行过滤,按照指定的字段的值(如果有and连接符会进行联合筛选)从临时中间表Temp2中筛选需要的数据,注意如果在此阶段找不到数据,会直接返回客户端,不会往下进行.这个过程会生成一个临时中间表Temp3。注意在where中不可以使用聚合函数,聚合函数主要是(min\max\count\sum等函数)group by是进行分组,对where条件过滤后的临时表Temp3按照固定的字段进行分组,产生临时中间表Temp4,这个过程只是数据的顺序发生改变,而数据总量不会变化,表中的数据以组的形式存在对临时中间表Temp4进行聚合,这里可以为count等计数,然后产生中间表Temp5,在此阶段可以使用select中的别名在temp4临时表中找出条数大于2的数据,如果小于2直接被舍弃掉,然后生成临时中间表temp5对分组聚合完的表挑选出需要查询的数据,如果为*会解析为所有数据,此时会产生中间表Temp6在此阶段就是对temp5临时聚合表中S表中的id进行筛选产生Temp6,此时temp6就只包含有s表的id列数据,并且name=“Yrion”,通过mobile分组数量大于2的数据distinct对所有的数据进行去重,此时如果有min、max函数会执行字段函数计算,然后产生临时表Temp7此阶段对temp5中的数据进行去重,引擎API会调用去重函数进行数据的过滤,最终只保留id第一次出现的那条数据,然后产生临时中间表temp7会根据Temp7进行顺序排列或者逆序排列,然后插入临时中间表Temp8,这个过程比较耗费资源limit对中间表Temp8进行分页,产生临时中间表Temp9,返回给客户端,所以limit我们优化的时候用子查询传入id将上次最后一个id作为条件放入where中能加速的原因就是temp8变小了,limit的时候不用扫描过前面的无用数据。因为查询的时候不加id那么首先是随机查询出符合条件的二级索引然后再进行回表查询找到数据,如果查第三万到三万零五条实际是查了三万零五条然后忽略前三万条这就是越往后limit越慢的原因,而你用子查询限定主键之后只查询了五条,这个执行过程也可以用select index_name,count(1) from information_schema.INNODB_BUFFER_PAGE where index_name in(xxx) and table_name like ‘xxtable’ 的方式去查看两种查询实际的bufferpool数据页大小来证实。实际上这个过程也并不是绝对这样的,中间mysql会有部分的优化以达到最佳的优化效果。
索引下推:ICP,5.6之后的新特性,不使用的时候存储引擎通过索引检索到数据返回给服务器,服务器判断是否符合条件,使用后如果索引中的列有判断条件,服务器会将条件传递给存储引擎,引擎来判断索引是否符合条件,只有符合条件的数据才会返回给服务器,简而言之不符合条件的数据不用再回表查询了。
MySQL服务层负责SQL语法解析、生成执行计划等,并调用存储引擎层去执行数据的存储和检索。
索引下推的下推其实就是指将部分上层(服务层)负责的事情,交给了下层(引擎层)去处理
举个例子:创建联合索引(name, age)并查询 select *from tuser where name like ‘张%’ and age=10;
根据索引最左匹配原则,这个语句在搜索索引树的时候,只能用 张,找到的第一个满足条件的记录id为1
在这里插入图片描述
如果没有使用ICP:
存储引擎根据通过联合索引找到name like ‘张%’ 的主键id(1、4),逐一进行回表扫描,去聚簇索引找到完整的行记录,server层再对数据根据age=10进行筛选。
我们看一下示意图:在这里插入图片描述
可以看到需要回表两次,把我们联合索引的另一个字段age浪费了
如果使用了ICP:
存储引擎根据(name,age)联合索引找到,由于联合索引中包含列,所以存储引擎直接在联合索引里按照age=10过滤。按照过滤后的数据再一一进行回表扫描。
我们看一下示意图:在这里插入图片描述
可以看到只回表了一次。
除此之外我们还可以看一下执行计划,看到Extra一列里 Using index condition,这就是用到了索引下推,并不是Using index。因为并不能确定利用索引条件下推查询出的数据就是符合要求的数据,还需要通过其他的查询条件来判断。
索引下推适用条件:需要整表扫描的情况。比如:range, ref, eq_ref。InnDB引擎只适用于二级索引,因为InnDB的聚簇索引会将整行数据读到InnDB的缓冲区,这样一来索引条件下推的主要目的减少IO次数就失去了意义。因为数据已经在内存中了,不再需要去读取了。引用子查询的条件不能下推。
索引下推默认是开启的,索引下推优化技术其实就是充分利用了索引中的数据,尽量在查询出整行数据之前过滤掉无效的数据。由于需要存储引擎将索引中的数据与条件进行判断,所以这个技术是基于存储引擎的,只有特定引擎可以使用(InnoDB和Myisam)。并且判断条件需要是在存储引擎这个层面可以进行的操作才可以,比如调用存储过程的条件就不可以,因为存储引擎没有调用存储过程的能力

遇到慢sql怎么排查解决,慢可能有很多因素,可以从索引网络io吞吐量锁之类的方向去分析,首先要分情况讨论如果是大多数情况下正常偶然很慢,那可能是数据库在刷新脏页比如碰上redolog写满了要同步到磁盘或者执行遇到了表锁之类的
概率较小的原因可能是网络不好内存不足io吞吐量小形成的瓶颈,如果多次执行很慢,那可能是没有索引或者索引失效,字段没索引或者用了函数操作导致无法使用,%放前面不满足最左匹配,用or之类的,这时候通过慢sql查询日志,在中
添加几行去定义比如2s为慢sql,然后后通过explain去生成执行计划查看,至于优化,索引的建立要选择合适字段,然后尽量覆盖索引,即select的字段被索引包括,这样在索引中就有值了不需要回表,甚至还能结合索引下推,
写的多的情况下少用唯一索引,防止changebuffer失效,对于sql语句or可以改成union,insert多条可以写为一条,limit把偏移量用子查询换位某个位置开始的查询等,对于表来说,可以拆表,横向和纵向的,经常联合查询的表可以考虑建中间表
然后如果业务场景上数据量大压力大,那也可以考虑读写分离,分库分表,这又会引入新的问题,一致性问题等
数据库的MDL(meta-data-lock)元数据锁,针对DML和DQL会加MDL读锁,因为增删改查只会操作表内数据不会影响表结构,可以并行,而DDL会加MDL写锁只有获取写锁的线程可以读写元数据,其他线程对表无法执行任何操作,所以在表数据量过大或者读写太频繁的
表中执行DDL可能会导致大量线程等待锁释放而程序奔溃,在mysql5.6之后加入了onlineDDL获取MDL写锁之后降级为MDL读锁,然后执行DDL这时候会很慢,但是不影响DML和DQL完成DDL操作后升级为DML写锁再释放
数据库联表查询优化,小表驱动大表(其实查询就是for循环),联表查询就是双层for循环,小表在外层,则外层循环次数少,即表的连接次数少,资源消耗少,可通过explain查看驱动表是哪个,第一行的表就是驱动表
若使用left join左表驱动,right join右表驱动,inner join会选数据量小的做驱动表。所以就是先看where条件里驱动表的条件来查询,因此这些条件要索引,然后以驱动表的这个数据开始循环查询被驱动表和剩余的where条件
所以b表的查询条件以及on条件里的被驱动表字段都要建立索引,而驱动表的on条件字段不需要建立索引。然后就是少用子查询,因为子查询结果会存在一个临时表里,而临时表是没有索引的,所以要全表查询,因此最好用联表查询不要子查询除非子查询结果很少
同一个接口用同样的报文在测试和uat上执行同样的代码操作不同库的同名表,一个可以执行,一个字段长度超出限制,找了半天原因,等我找很自然想到数据有中文,oracle不同编码中文占不同长度,于是用
select * from v$nls_parameters where parameter=‘NLS_CHARACTERSET’ 查看库的编码字符集,用select lengthb(‘65002021122016002-协助执行通知书-’) from dual查看占用长度,解决办法
可以改编码集,但这样历史数据就不兼容了,风险比较大,还有就是建表的时候varchar2(50CHAR)最多存50个汉字,varchar2(50)就要看编码格式了,或者就增加字段长度
mysql查询二级索引如果需要查的数据在二级索引里有那直接查,没有的话需要回表查主键索引,这种在二级索引就能查到过程叫覆盖索引
在一致性保证中删除缓存失效的情况有两种方式应对,一个是引入消息队列做重试机制,还有就是先更新数据库再删除缓存,数据库更新成功会有一条binlog日志,通过订阅binlog日志拿到要操作的数据再执行缓存删除
Canal中间件也是基于这个实现的,它模拟mysql主从复制的交互协议把自己伪装成一个从库向主库发送dump请求,然后获取到主库的binlog后读取转为结构化数据
explain plan for select count(1)
from gaps5.bupps_busi_serial A
left join gaps5.bupps_eapy_serial B
on a.busi_req_no=b.busi_req_no
left join gaps5.bupps_merch_info C
on a.merch_no=c.inner_merch_no
where 1=1
and a.upps_trans_status in (‘281’,‘341’,‘201’)
and a.settle_status=‘0’
and b.out_merch_no=‘202308171156438’
and c.top_merch_no=‘202209271245363’
此sql生产环境执行需要接近30s,因为bupps_busi_serial存储业务交易流水1200w数据,bupps_eapy_serial支付流水1200w
此时应该使用分区,超过500w就可以使用,根据日期建立按月建立分区,查询时强制带上日期,但是分区需要在新表上做,那么
旧表的数据如何迁移,因为数据不只是新增,还会对状态进行变更,同步策略要如何设计,此处没有java来提供双写和告警人工
处理的方式,这个是后续优化,眼下通过执行计划查看发现bupps_epay_serial的out_merch_no条件使用了table access full全表
扫描,分析该字段为外部商户号,数据量大且分布均匀且唯一区分商户,符合建立索引条件,为该值建立索引,耗时48s,之后执行
计划变为table access index scan时间变为0.04s
对于三个月,十二个月,二十四个月无交易账户根据监管要求需要变更账户状态为只收不付,不收不付等,通过代码功能实现,最初的
设计通过交易码和交易状态作为条件,账户号与日期作为条件查询交易流水bupps_busi_serial中有无该账户的记录,全表扫描一个账户
将近30s,存量全量正常状态的账户有五六万,每个账户执行一次,预期无法接受,其次查询的交易码和交易状态用了很多in和or且这两个
值区分度太低在上千万数据中也只有几十个无法建立索引,因此修改上述设计,首先根据日期判断可以先一次查出所有数据的,然后放在
临时表中查询,当然临时表中是没有索引的需要建立,然后交易码和状态的对应逻辑选择可以放到java中处理,不用在sql中(如果每个账户的交易数据量
很少,执行的时候先执行走索引的然后得到的数据建立临时表,临时表数据很少,走in和or也很快,因为sql执行步骤中where条件会先用索引把数据量减少查到
数据后做临时表再走剩下的条件,所以不放java似乎也可以,但sql不擅长逻辑处理且要防止偶发的单个账户大数据量情况造成的耗时),最终改造是为账户号
建立索引,日期建立索引,然后查询条件只有账号和日期,这样一个账户查不到几条数据,原本通过count(1) over()函数查出当前条件查询的总数据量,但意外的大数据量
不能执行返回给java执行,因此分两次查询,先用select count(1)得到总数据然后通过分页的方式查询后执行java逻辑,这样一个账户处理在0.1s内(查询两次共0.04s算上java处理)
最终首次执行批量能在2小时内,相比之前的八天可以接受,执行完后账户状态已变更,再次执行从账户表中获取正常账户列表数量会下降,批量执行速度也会加快,生产环境大于10000笔
交易的账户有99个,因此考虑偶发大数据量是合理的
mvcc

分库分表以及ES

一般的搜索是在文章中搜索单词,而搜索引擎需要通过单词能找到文章,这种反向的搜索就是倒排索引,实现原理为将某个单词作为key,将它出现过的文章id集合作为value存下来,下次再搜索这个单词的时候就可以返回所有文章的id了,这是最初版,但是单词
太多了,所以为单词再添加一层字典树,字典树只存单词的前缀,将单词有序的排列,然后字典树通过字典树可以快速的找到单词所在的大概位置,即都是这个前缀的单词,然后在部分单词中找到准确的那一个

mq

削峰填谷
一个大任务的多线程处理
扩容消费者,broker生产者扩容,处理积压
丢弃mq,使用持久化在谷时恢复执行
事务消息顺序消息延迟消息死信队列

分布式事务2pc,3pc提交协议,本质还是加一个全局者,然后全局者和每个事务参与者确认状态最终确认事务的状态,2pc将事务的提交分为两个阶段,协调者先将事务发送给各参与者并等待处理结果,各参与者操作完将undo和redo保存,方便回滚和最后确认,
协调者收到所有事务做完的结果,再通知各参与者事务可提交,缺点在于同步阻塞一个事务要等所有完成才行,还有的问题就是单点问题,一阶段有人挂了还好说,二阶段有人挂了数据就不一致了,协调者挂了那所有人都锁住了,3pc是对2pc的一种优化,在
一阶段和二阶段之间增加了一个准备阶段,保证在之前的二阶段各参与者状态都一致,同时在参与者和协调者之间引入了超时机制,若未收到协调者的commit请求会自行commit不会阻塞,解决了单点故障,但还是没有完全解决数据一致性问题,这里还有一个ack
机制,在mq中保证消息一定发送和消费的机制,用日志保证存储中的消息丢失可恢复,还有mysql主从复制防止写丢失的半同步复制也有用到ack,主库写入后通知从库写binlog任一从库返回ack才算主库记录写入完成

为什么要用MQ,mq就是服务间消息的传递,那么rpc和springcloud都可以做到,还要用MQ是为什么?所以最主要的目的就是削峰,这两者都是实时响应的,或者说先返回响应然后逻辑处理完在回调等,这些
都是没办法处理短时间大量的请求以及保证请求的不丢失等问题,那么mq可以用ack做到请求必发必做,失败重试,业务自己保持幂等,然后最关键削峰,大量请求先积聚起来,慢慢等业务处理,然后还有就是
解耦,那两个都是要对于逻辑做些对应的处理的,mq就只管发和收别的不管了,还有异步,就是如果一个请求要多个接口返回,那上面的一个个来rt就是叠加,用mq同时发给多个接口就并行处理节约时间。
缺点就是多引入一个东西,挂了或者消息丢了影响比较大,还有数据要做幂等复杂度上升了,还有异步的一个数据一致性问题
四大主流MQ,activeMQ,RabbitMQ,RocketMQ,kafka首先前两个单机吞吐量是万级别,后两个是十万级别,Rocket的topic很多对性能影响较小,Kafka的topic太多性能会显著下降,响应延时RabbitMQ在微秒
其他三个都在毫秒,后两个都是分布式架构可用性很高,经过参数配置可以达到0丢失,所以推荐用RocketMQ如果是大数据类的实时计算日志采集推荐Kafka,用了zookeeper来做分布式架构

算法

数组的查找一般普通查找和二分法,二分法每次取中间值判断大小,然后再剩下的一半里一直重复直到找到,二分法需要有序,数组的排序算法,有冒泡排序,比较相邻两个数据,将大的那个换到后面,然后再继续往下,一次完成之后最后的元素就是最大的,然后再来
选择排序,和冒泡类似,从第一个开始和后面每个元素比,两个中更小的那个和后面的比,之后最后最小的和最后的换位置,然后再来(就换一次,冒泡换n次),快速排序,就是用一个基准值,将所有大于它的放到右边,小于它的放到左边,然后左右两边的数组再
按这种方式一次又一次排序最终就有序了,方式为以第一个值作为基准设置i=0,j=length-1,然后从末尾往前依次比较j–,有比基准小的值,和基准互换,然后i++和基准进行比较,如果有大的就和基准互换然后j–,就这样直到i==j然后一次排完了,这时候
基准两边的两段数组再重复上述操作,堆排序:堆是一个完全二叉树,利用完全二叉树的结构来维护一维数组,根据特点可以分为大顶堆小顶堆,前者每个结点的值都大于左右孩子节点,后者都小于,根据堆的定义,在一维数组的描述中就是如果父结点下标是i
那么左孩子结点下标是2i+1,右孩子结点是下标2i+2,这种特性很重要,可以快速访问到重要的元素,所以优先级队列使用了这种方式,即堆顶是k个元素中最大或最小,建堆从最后一个非叶子节点开始,计算为length/2-1,和他的子结点比较换个大的过来,然后
找上一个父结点比较,换大的上去就这样从左往右到最后完成构建,然后像查找数组中第k大的元素可以用一个优先级队列,容量为k,比较器设置为a-b>0这样会把最小的放在上面,把前k个数add进去,然后数组剩下的值来和堆顶比较,小于k的不要,大于
k则丢弃顶堆,将大的数据加入始终保持顶堆为目前的第k大,最后返回堆顶就行了
链表的查找一般有双指针法,一快一慢或者一前一后将两次循环变为一次,动态规划例子如下比如有面值1,3,5的硬币怎么用最少的硬币凑够11元,首先f(n)是拿n元的最小硬币数,则前一次f(n-h[i])其中h[i]等于1,3,5所以f(n)=min(f(n-h[i])+1)然后找到
底层边界问题f(1)=1,f(2)=2,f(3)=1,f(4)=2,f(5)=1,然后自底向上算出最小硬币数
这个题目如果改为有多少种拿硬币的方式,f(n)为拿n个硬币的所有方式f(n)=sum(f(n-h[i])+1)就是f(n)=f(n-1)+1+f(n-3)+1+f(n-5)+1,然后底层边界问题的定义之后进行递归计算

深度优先遍历DFS和广度优先遍历BFS,DFS主要思路是从一个图中未访问的顶点V开始沿着一条路一直走到底,然后再回到上一个节点走另一条路,直到所有的顶点都遍历完
实现方式有递归和非递归两种,递归的就按顺序遍历当前节点,左节点,右节点这样递归下去直到叶节点终止递归条件,不过层级过深容易栈溢出,非递归实现
对每个节点来说,先读取当前节点,然后如果有的话把右节点压栈,再压左节点,然后弹栈,拿到在栈顶的节点读取并判断有无左右节点,如果不为空就再走一遍前面的逻辑,如果为空弹出
举个例子二叉树12345,那第一层将1压栈,然后弹出得到第一个1并判断有无左右节点,先压3再压2,然后2是栈顶弹出,判断2有无左右节点,先压5再压4,弹出4判断有无,无再弹出5判断,在弹出3判断
所以得到12453
BFS是从一个节点出发遍历这个节点相邻节点,再依次遍历每个相邻节点的相邻节点,DFS是栈,BFS要用队列,思路为添加读取当前节点,然后将左节点入队,再将右节点入队,然后循环,先出队,然后判断
有无左右节点入队,直到队列为空就结束,举个例子二叉树123456,第一次将1加入队列,然后出队判断有无左右节点,将2入队再入3,然后2出队,判断2有无左右节点将4入队再入5,然后3出队将6入队,然后
4出5出6出,所以得到123456

DJ斯特拉算法是从一个顶点到其余顶点的最短路径算法,解决有权图中的最短路径问题,采用贪心算法的策略,每次遍历到开始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点
Prim普利姆算法,在有权图中搜索最小生成树,即包含所有顶点且所有边的权值之和也是最小的,通过矩阵即二维数组来定义图,每一个value表示对应的下标表示的两个节点之间的权值,随意选一个起点
然后在与起点相连但未被选中的节点中选一个权值最小的,用集合存放已经访问过的节点,计算未选择的节点与已选节点相连的权值以及相连的是那个节点,选择权值最小的并加入已选节点集合,重复直到未选节点为空
两个算法思路相同都是贪心算法,每次选现在最短的,区别在于普利姆算法的最低是对于已经选择的所有节点来说最低,而DJ斯特拉算法的最短是以和起点的距离相比最短

观察者模式observable,被观察者继承observable并实现Runnable,然后在run中写逻辑,在catch中调用setChange()设置修改标识,然后调用notifyObservers通知Observer调用update方法,观察者实现Observer的方法重写update通过update传递的observable
进行参数传递然后新建一个observable并addObserver新加一个观察者然后run启动新的线程
然后其实那个设计完全可以变成用mq的方式,然后可以控制多个线程去处理,如果是分库分表的情况下就很好的分散压力又快速写入而且有保障机制.如果项目不用mq那只用java来实现,就是需要考虑这种方式
策略模式,多实例的spring注入List接收
单例模式的饱汉饿汉
spring的applicationContext的工厂模式
AQS的acquire和release提供的模板模式
@aspect的代理模式

Dubbo与springcloud以及Golang等分布式处理

过滤器和拦截器有什么区别,都是对请求做处理,但是过滤器在调用servlet之前,而拦截器在调用controller之前,过滤器在中配置而拦截器在spring配置文件或注解中,过滤器只能对request和response
操作,拦截器因为到了springMVC所以还可以对异常,handler这些进行操作

拦截请求的三种方式,filter,interceptor,aspect,以web项目来说filter先执行然后在执行controller且我们只能操作原始的入参出参,不能知道这个请求让哪个controller哪个方法调用了,用interceptor
就可以获取到,通过handler参数就可以得到执行的controller和方法信息,但拿不到执行方法时候具体传入的参数是什么,aspect就可以做到,通过joinPoint拿到参数,这一切原因就是流程,filter在取controller
之前他当然不知道,interceptor在执行方法之前它当然不知道,aspect就是代理执行当然都知道,但是没法知道方法内部信息了。
springbooot配置filter顺序,按顺序写就顺序执行,(springboot要么用WebSevletInitializer的onStartUp方法,没试过,猜测),注解方式那自定义过滤器要实现Filter然后添加@WebFilter然后@Order
然后启动类添加 @ServletComponentScan,是Order是不生效的,原因是加载被@WebFilter修饰的类没用order,是通过类名所以要么限定类名xxFilter01,xxxFilter02然后01先于02,然后配置类配置
@Configuration
public class FilterConfig{

@Bean
public FilterRegistrationBean Filter01(){
	FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean();
	(new Filter01());//设置过滤器名称
	("/*");//配置过滤规则
	(1); //order的数值越小 则优先级越高
	return filterRegistrationBean;
}
@Bean
public FilterRegistrationBean Filter02(){
	FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean();
	(new Filter02());
	("/*");
	(2);
	return filterRegistrationBean;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

}
实现拦截器则需要实现HandlerInterceptor重写preHandle在调用前执行,postHandle在调用后渲染视图前执行,afterCompletion渲染视图后执行

使用AspectJ实现aop,通过@Aspect和@Component,通过@Pointcut(“execution(* ..(…))”)定义切入点,前置后置环绕异常配置
其中Pointcut表达式可以是路径也可以是@Pointcut(“this(.)”)就只能是BookServiceImpl这个类
@Pointcut(“target(.)”)则可以是这个类型,比如父类引用也可以
execution是指当执行匹配的那个方法时,within: 某个类里面,this: 指定AOP代理类的类型,target:指定目标对象的类型,args: 指定参数的类型,bean:指定特定的bean名称,可以使用通配符(Spring自带的)
@target: 带有指定注解的类型,@args: 指定运行时传的参数带有指定的注解,@within: 匹配使用指定注解的类,@annotation:指定方法所应用的注解
然后可以通过JoinPoint的getArgs方法获取入参,在环绕通知中用ProceedingJoinPoint的getArgs,和JoinPoint相比ProceedingJoinPoint还可以修改参数,因为环绕时会用()调用原始方法
这里可以传入参数的

ApplicationContextAware的使用,一般就是实现它之后可以用它来获取bean实例,像dubbo的filter不支持依赖注入,可以用这个方式来获取bean,ApplicationContextAware在应用启动就会加载
@Component
public class BeanUtils implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
= applicationContext;
}
}

像dubbo的filter使用也比较简单,在resources目录下添加纯文本文件META-INF/dubbo/内容为 xxxFilter=
然后xxxFilter实现Filter重写invoke方法就可以了,这个入参invoker和invocation一个是可以获取类和执行方法,一个可以获取参数列表很灵活,多个拦截器配置顺序可以用Order来越小越先执行
执行中调用链如果有就调用下一个filter如果没有就调用最后的service方法,包括spring的filter的chain也是这样
@Activate(group = , order = -110000)group表示在什么场景下provider表示被别人调用时,consumer表示调用别人时触发,order表示执行顺序
group: 所属组,String[],例如消费端、服务端。
value String[],如果指定该值,只有当消费者或服务提供者URL中包含属性名为value的键值对,该过滤器才处于激活状态
before:String[],用于指定执行顺序,before指定的过滤器在该过滤器之前执行
after:string[],用于指定执行顺序,after指定的过滤器在该过滤器之后执行

dubbo的SPI是从java的SPI机制上扩展来的,java的SPI会一次性实例化所有扩展点,没用到就会浪费,而且初始化耗时而且如果扩展点加载失败,后续使用出现报错很难定位查询,dubbo增加了ioc和aop支持
一个扩展点可以直接setter注入其他扩展点,dubbo最核心的三个概念分别是:Provider(服务提供者)、Consumer(服务消费者)与 Registry(注册中心),通过注册中心解耦了服务方与消费方的调用关系。
在服务发布与服务引用的时候 dubbo 分别提供了 ExporterListener 与 InvokerListener 对于服务进行不同的扩展,dubbo在进行服务调用的过程中最核心的概念就是Invoke就相当于Spring里面的bean一样
Java/Spring/Dubbo三种SPI机制,谁更好?SPI 机制应用在了大家项目中的很多地方,举个例子,为什么我们在项目中引入 mysql-connector 的 jar 包,就可以直接连接 MySQL 数据库了?
SPI 的本质是将接口的实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载对应接口的实现类这样就可以在运行时获取接口的实现类,动态就导致可以容易的通过SPI为程序提供扩展功能
Java SPI 是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。设计一个接口,将接口的实现类写在配置文件中,服务通过读取配置文件来发现实现类,进行加载实例化然后使用。
配置文件路径:classpath下的META-INF/services/,mysql就是这样得到了驱动类实例从而连接数据库,还有启动类springservletinitializer不也是么
JavaSPI实现原理在ServiceLoader主要就是取获取META-INF/services/的文件读取内容后反射实例化,缺点在于启动就全部加载浪费资源,且通过Iterator遍历全部获取不能用参数选择
Spring的SPI读取META-INF/下配置文件是key-value的形式,这也是springboot能够自动化配置的原因啊,key为接口的全限定名value为实现类的全限定名,且一个key可以对应多个value
springboot加载dubbo举例,spring boot启动过程中在(type, classLoader)这一步中会将EnableAutoConfiguration的实现类全部进行加载、解析、初始化
dubbo-spring-boot-starter的META-INF/有写key是EnableAutoConfiguration,value是dubboAutoConfiguration所以实例化dubboAutoConfiguration时会执行其中的逻辑将dubbo服务注册spring中
Dubbo的SPI包括Filter、Protocol、Cluster、LoadBalance等,dubbo SPI为每个拓展点(接口)单独设置一个文件,文件名为接口的全限定名
支持"别名的概念“,可以通过别名获取拓展点的某个实现。配置文件内容是key -value类型,key是别名,value是实现类的全限定名。只使用指定的 filter就不会实例化其他filter,可选择而上述两个都是全部加载
Dubbo还支持通过setter方法进行依赖注入Dubbo首先会通过反射获取到实例的所有方法,然后再遍历方法列表,检测方法名是否具有setter方法特征。若有则通过ObjectFactory获取依赖对象,最后通过反射调用setter方法将依赖设置到目标对象中
读取时候根据配置路径区分Dubbo内置SPI和外部service的SPI,优先加载内部的

sentinel,hystrix限流熔断降级,熔断指的是服务出错或者响应过慢直接返回错误信息或者默认信息或者历史数据,降级指的是干掉次要功能保留主要功能,使用方面hystrix已经停止更新而sentinel开源并不断发展
dubbo的filter不支持@autowired想要注入使用setter或者从applicationcontext里面去获取bean,可以自己实现ApplicationContextAware注入,然后在filter中获取或者使用dubbo提供的ServiceBean直接获取applicationcontext
group={}就是别人调用我会过滤group={}是我调用别人会过滤,@value在filter里只能在别的bean里获取好然后通过上面的方式在filter中获取
dubbo服务调用和服务暴露过程均会用到过滤器,两端的过滤器互不影响,过滤器之间构造调用栈进行链式调用
restTemplate不像jdbcTemplate一样直接@autowired就可以使用,因为restTemplate有一个参数ClientHttpRequestFactory要配置所以需要写一个restTemplate的config通过@Configuration和@Bean来实现注入

springcloud

springcloud是一整套微服务的解决方案,首先有注册中心,如果没有注册中心,微服务之间要调用一个服务要写死服务的路径,包括机器ip和接口路径,增加和修改都很耦合,所以用注册中心来解耦
应用向EurekaServer注册中心服务端注册服务,通过网关路由的请求找到接口并通过hystrix控制服务使用,若使用则通过fegin进行服务调用,走ribbon负载均衡,然后EurekaClient发现服务的一个流程
在Eureka中有个双层map的服务清单,第一层是服务名,第二层是实例名,value是服务加端口号,第二层可以作为负载均衡,同时对服务维持心跳监测,剔除不可用服务,集群各节点都有一样的服务清单,但不是强一致的
可能某个服务注册到集群某个节点,同步到其他节点还需要时间,服务第一次请求的时候就会将整个双重map服务清单缓存到本地后续就本地找到路径去访问,应该也有个过期策略去同步服务端?hystrix可以降级在fallback
里可以给友好的提示返回,也可以把参数拿一下发mq,记录日志或者说流量大的时候原来整个接口是ms级别返回的,走fallback的mq处理变成好几秒返回这样也是一种降级
微服务架构有zookeeper+dubbo和Netflix还有springcloudAlibaba,都相似的引入了一个注册中心,比如zookeeper,netflix的Eureka,springcloudAlibaba的Nacos,引入注册中心之后就不需要Nginx了,启动的服务注册到注册中心
访问的通过服务名称去注册中心找,甚至把注册表缓存一份到本地提高新能,然后访问哪个由负载均衡配置来决定,自己客户端就可以实现,每个客户端定制化,还不用像Nginx一样服务端配置
将nacos下载并在服务器上运行,然后就可以根据指定路径访问nacos注册中心前端界面了,然后项目使用的时候springboot中引入nacos的依赖,然后在spring配置中配置一下nacos的注册中心地址,和自己服务的名称
启动后就会自动注册上去,以服务名为第一层,实例个数第二层,在前端配置页可以查询查看状态。至此注册完成了,然后怎么发现调用呢,直接用服务名是找不到的,而且用哪台机器呢?这里alibaba的nacos中默认引入集成了ribbon
只需要在调用的restTemplate实例注入处加入@LoadBalanced注解就可以访问了,怎么实现的呢?ribbon底层主要就是通过一个拦截器LoadBalanceInterceptor,http访问会被拦截器解析服务名,到配置的nacos注册中心地址查询并缓存
列表清单到本地,之后都是服务名解析来转换请求路径去访问,既然用了缓存那就有数据不一致的问题,怎么实现动态获取最新注册表呢?有一个秒级的定时任务查询覆盖,其实实际上是双层保护,还有一但有新的机器增减,会通过UDP
方式通信,后来改为gRPC长连接的方式发送以防定时任务有问题,双重保险,而机器下线就是通过心跳任务,每个机器都要给注册中心发心跳,心跳失败几次就默认下线剔除。然后此时调用是要写路径的,http//服务名/接口名/参数这样
如果想像调用本地服务一样那要引入fegin组件,开启@EnableFeignClient然后在定义地方用@FeignClient确认服务名,在接口方法上定义url地址,然后其他地方使用的时候直接注入这个接口调用接口方法就可以了,底层逻辑就是feign通过
代理的方式生成代理对象,拼接出访问的路径来实现访问。现在多流程在一个事务流程中,当然可以通过事务回滚的方式,但这也导致可能一些小问题使得整个链路走不通,那么没做到高可用,所以做一些妥协,比如进行trycatch记录错误日志
先走通主要流程然后再日志中或者mq方式事后补偿,也就是服务降级,用到组件Sentinel,导入依赖,在sping配置中配置一下对feign可用为true,然后在@FeignClient的接口上在@FeignClient注解参数里添加fallback,指向一个class,这个class
需要实现这个@FeignClient的接口,作为他的子类,然后重写他的方法,也就是在降级的时候处理逻辑,sentinel还可以进行限流,就要下一个sentinel-dashboard的jar包和nacos一样,启动之后有前端界面访问进行配置,零业务代码。还可以实现
熔断,和降级的区别在于降级是远程请求,失败后使用fallback方法,原理就是代理对象报了一层trycatch,catch里面走fallback方法,熔断是我已知远端接口有问题,那就没必要去调用了,直接返回fallback处理就行了,可以在sentinel前端配置
异常比例或者异常数量等策略来决定熔断与否。sentinel限流的原理是滑动时间窗,以前gateway里面用过令牌桶,滑动时间窗大概就是有个数组每秒加一个对象,对象有个计数器,每次访问加1,加到阈值就不能再加,除非到下一秒新对象又从零开始。
熔断也是在这个对象里有总访问数和失败访问数,就可以计算了。再到分布式事务用seata,多流程可以用事务,但流程是微服务的话是不同的线程在做,事务不生效的。使用的时候加入seata的依赖包,然后在要事务的地方用注解@GlobalTransactional
就可以了,实现原理就是要找一个总的中心然后2pc提交咯,用seata的时候需要启动一个服务端,用来当协调中心,一阶段各服务事务提交然后报告结果,协调中心根据各服务undolog回滚日志生成反向补偿,如果有失败的就把成功的反向补偿。但是这个
支付宝会用,淘宝不会用,因为太慢了,要等待所有人提交,高可用做不到啊。所以可能用降级mq时候补偿这样的方式。
Hystrix会给每个command分配独立的线程池,这样A-C和B-C调用的C资源互不影响,隔离性好但是占用较多线程资源,还有就是信号量隔离,发起请求需要获取信号量(JUC的AQS),获取不到的请求会被拒绝进入fallback
ESB就是一种SOA,服务都注册在总线上,总线上的服务才能调用,微服务就没用总线,互相调用,但是有注册中心啊还是差不多的,不过注册中心挂了微服务还是可以调用,因为调用表在本地有缓存,但是到了缓存更新时间可能就不行了
区别就在于微服务用注册中心去发现,但是调用还是自己去调用的,SOA总线是在链路上的断了就发起不了了(需要总线根据不同协议转换报文和路由)
Dubbo和springcloud哪个好?出现时间的问题,都可以用。springcloud没有服务间的依赖一说,就是http调用的对吧,dubbo就不同了rpc调用是有依赖的,我本地调用需要知道远端的接口名定义,这时候就有facade层了,远端是具体实现,但消费者
不需要知道具体实现类,只需要知道接口即可,所以依赖facade层,里面只有接口定义,而不用依赖服务端jar包,因为接口可以有多个实现,所以定义的时候指定版本号,在使用的地方@Reference注入的时候也指定版本号就可以确定了
自己实现dubbo,有三块东西,第一个是provide系统,里面呢有api和impl还有Provider提供类,然后是消费系统,消费里面有Consumer消费类,然后还有framework系统,定义我的dubbo框架,有register注册和protocol通信协议这么两部分
首先实现provider,api里定义需要暴露出去的facade接口,然后实现在impl中,这两部分是业务逻辑,然后在provide中实现提供服务的功能,需要哪些步骤呢?首先能接收网络请求才能被调用到,然后定义invocation入参,即定义调用接口必须有的东西
比如服务名接口名版本号参数列表这些,然后还要定义序列化反序列化方式,然后获取到这些参数后反射生成对象调用方法并返回结果。所以一开始就是通过默认的配置选择接收网络请求的类型(tomcat,socket等等),比如就是在provider中先默认启动一个
tomcat然后在tomcat里使用自定义的dispatchservlet,dispatchservlet里面根据策略模式走不同的handler去处理请求(服务处理的请求,收集数据的请求,回声测试/心跳监测的请求等等),在handler中先反序列化请求的数据流,然后转换为通用的定义
的invocation,其中可以获取到接口名,怎么去对应到实现类,这个就是在注册map中去寻找的(这里provider用到的启动tomcat的client连接和dispatchservlet还有handler都是在protocol中的,然后register中有注册表map的类),在map中根据接口名拿到
实现类,然后通过反射进行method的invoke然后通过序列化机制返回结果。这时候provider提供者就完成了,Consumer只需要引入api层依赖,然后组装好invocation,向provide启动的服务器发送请求就可以返回结果了,但是consumer这边操作太麻烦了
最好情况是只要注入接口然后使用接口的方法就能得到返回的,就像本地调用一样,所以Consumer这边还要封装一下请求过程。即构建一个接口的引用指向,然后这个引用指向直接使用对应方法,但是构建引用指向因为Consumer是只有接口没有实现类的
所以直接获取对象是获取不到的,那就用动态代理的方式在运行的时候创建么,通过Proxy动态代理的方式去实现invoke方法,将上述invocation组装以及向服务器发送请求放在代理类中实现,那么Consumer中只要获取这个动态对象然后去调用方法就可以了
这时候只要再将改代理对象在启动时候加入到spring容器中管理起来,那么就可以通过注解的形式添加并使用了就和dubbo一样了,这时候还有一个问题就是服务器请求怎么动态得到?通过访问注册中心然后缓存到本地注册表然后负载均衡策略去发送
本地map缓存和注册中心缓存一致性用redis的话在于发布订阅机制,基于发布者订阅者和channel频道,不用直接通信,发布者往频道发消息,所有订阅该频道的订阅者都能收到消息
zookeeper注册中心的话他可以有watcher机制,当然redis也有watch机制(使用watch监视一个或多个key,如果key在事务执行前被修改则整个事务取消),基于节点可以绑定监听事件,会在数据发生变化时产生一个watcher事件发送到客户端
超时的话redis就是心跳监测来续签,没心跳就过期了,zookeeper就是临时节点,如果注册的服务器挂了那临时节点由于心跳监测不到也会被清除,这就是用这两个做注册中心的原因

docker与K8S及Jekins等容器部署相关

k8s缩扩容有两种方式,一种是修改的replices下的数字去手动控制容器数量,另一种是使用自动缩扩容技术HPA,HPA控制器根据cpu和内存以及并发数,包的传输大小等检测状态每隔30s运行一次来判断是否需要缩扩容

tomcat与nginx,Jboss等一系列服务器

CAP理论下的一些复杂场景解决方案

秒杀抢购设计:几个细节是瞬间高并发,页面静态化,读多写少,缓存库存一致性,分布式锁,MQ异步处理,限流等。首先在活动页面是用户流量第一入口并发量最大,那么这些流量如果都去访问服务端可能
要挂掉,所以活动页面大多数内容要设置为静态资源减少请求,只有到了时间用户点秒杀按钮才能访问服务端,这样可以过滤大部分无效请求,对于秒杀按钮也是点击之后比如5秒内置灰不能再点,秒杀过程中
一般是先检查库存是否足够,足够才下单,读多写少的情况下将库存缓存到redis中,查redis没有再去查库,这里用分布式锁只有拿到锁的才查库,防止高并发下大量请求打到数据库,拿不到锁自旋等待去缓存读
这个写自己的项目就行了,关键点在于库存一致性,这个写自己的项目就行了,关键点在于库存一致性,我秒杀前库存同步到redis了然后生成订单是预扣redis库存的,后面订单撤销是回退库存的这些都用lua
脚本原子操作,那么后面真正执行支付后需要落库了吧?这时候怎么处理库存一致性?以及秒杀到点或者手动取消后,怎么同步库存?这个还要看代码的。
mq异步操作,秒杀中核心流程是秒杀生成订单支付,真正并发大的是秒杀,生成订单和支付是比较小的并发,所以要把后两者从整个流程中拆分出来,将订单生成做成MQ异步处理,发送下单消息-MQ-消费消息
消息防丢失,可能MQ磁盘问题或者broker挂了或者io问题,要加一个消息发送表,秒杀成功发mq之前添加一条记录状态为待处理,然后发mq消费完回调生产者接口设置记录为已处理,用定时任务重试查询待处理
记录并重新发送mq消息,这里就有重复消费的问题,消费成功但一直没有返回,如果io等问题要保证幂等性,还需要做限流操作,比如对ip限流,对访问处理时间戳限流,或者加手机验证码这种,只有邀请注册
用户才能访问或者提前预约等

亿级用户的在线状态如何统计?用数据库加字段存用户登录状态就完蛋了,数据库频繁修改和查询直接爆了,所以存到redis中用login_status做key,用户id为set
登录就加,登出就减,最后用scard获取set的数量,但是如果大量增加很占用内存,再优化一下用bitmap,登录用setbit命令去将用户id作为一个位移值,然后将这个值设为1,查一下bitmap怎么用的
用户登出setbit设这个值为0,判断用户是否在线用getbit,统计数量用bitcount,相比于set四个字节,这里一个用户只有1个bit相差32倍,缺点在于bit位只能是数字所以用户id也只能是自增数字
一般分布式情况下数据库主键很少采用自增主键
但是这里有个问题,登录是可靠的,登出是不可靠的,可能用户直接但是这里有个问题,登录是可靠的,登出是不可靠的,可能用户直接
关机杀掉就没有反馈了,是否用心跳机制?以及用户的写入和过期的清理应该写入越轻量越好,websocket长连接相比心跳就太重,还有hyperloglog存uv的设计等
做一个访问在线计数功能,类似B站当前视频多少人同时观看这么个功能,首先这种计数实时性的但肯定不会要求特别准确,然后连接可靠但退出不可靠,可能是挂了所以要用心跳机制,用socket长连接就太重了
所以基本思路就是用redis来存储视频id用户id,建立连接加入退出删除并加入心跳机制考虑过期删除问题,然后统一计数,这样的话hash可以实现,key为视频id,field为用户id,然后value为进入的时间戳
并用一个分钟级过期的key为视频id的键值对作为清理时机,每个hash的field心跳更新时间戳或者加入或者退出的时候获取设置一下这个分钟过期的key如果失败那说明还没到需要清理过期数据的时候,如果
成功那就hgetall获取所有并清理掉时间戳过期的,比如心跳三秒一次那就清理时间戳和当前时间超过三秒的,这样实时性和准确度都还可以,然后清理也没很频繁,数据量十万级都没问题,百万级可以用hscan
来异步分批范围获取,获取数量用hlen,当然另一种方案可以用zset存储,视频id为key,成员为用户id,时间戳作为评分(zset用评分排序),然后心跳更新时间戳,zcard获取数量,然后清理机制和前一个相同,触发后通过评分
删除,这个方案于上一个方案相比,在心跳写入处变重了在清理处变轻了,现在观看属于高并发写,轻量的写入更合适。还有Redis有一种hyperloglog专门用于大数据的统计,比如页面访问统计用户数量统计
语法主要有pfadd和pfcount,是自带去重的,pfadd返回1就进入返回0说明有重复,pfcount也只统计去重,和bitmap相比去重比较方便,可以结合bitmap使用,bitmap标记用户活跃,hyperloglog计数,所以
上述方案可以为缓存key为视频id然后hyperloglog并设几分钟过期时间在观看的用户每分钟请求一次客户端将id加入,查询pfcount统计,因为hyperloglog过期时间是整个log不是内容,所以每分钟维护一个log,
pfcount查询前三分钟的pfmerge但是这样就是实时性很差,用bitmap也是用户id自增然后每一位去设置1然后心跳来设置,过期设置为0这样。

推动集成测试(在单元测试的基础上将所有模块按设计要求组装成系统进行测试)一般都要在本地启动完整的spring容器的就会非常耗时,于是排查耗时在哪里,用bean生命周期前后置处理器打一下耗时,就发现
耗时在于数据库的初始化,一个是数据库连接池初始化还有就是数据库元数据初始化,原来是因为分库分表,初始化就比较慢,其实本地跑测试只要覆盖几个表就行了不需要全量的数据,所以优化一下,底层用了
Druid那么在init的时候会初始化连接池并加入数组中,不过它提供了一个asyncInit变量,设置为true就会懒加载,启动的时候就不会初始化了,可以在Bean后置处理器初始化前置处理设置一下这个值,第二个shardingJDBC里
的SchemaMetaDataLoader加载字段和索引的元数据并且是以table维度的,如果一个库里面一个表分了八张表又分了八个库那就加载64次了,所以一个逻辑表就要遍历64次很耗时,最后也就是存到ColumnMetaData
中所以考虑修改逻辑通过读取Json文件的方式而不去走底层RPC,这是jar包里的代码所以要在项目里相同包名和类名写一个去覆盖掉jar中的这个类然后执行,Json来源可以第一次执行打印后获取写入本地文件
然后读取本地文件转换Json到ColumnDataMeta中,如果有DDL改变了表要重新获取一下。本地容器不想加到注册中心,就用bean后置处理器初始化前置获取后setRegister(false)就可以了,然后用@Conditional区分只有测试
环境才加载其他环境不执行就ok了,这里具体是一个问题处理的思路。

cpu使用率飙升,系统反应慢怎么排查,导致cpu飙升有两类,一个是上下文切换过多,一个是资源消耗,前者比如文件io网络io锁等待线程阻塞等都会触发上下文切换,后者有线程一直占用cpu,或者对象太多触发的GC算法,如果一直是同一个线程,可以jstack
获取线程的dump日志找到线程后定位问题代码,如果线程id不断的变化那就找几个id用jstack再去查看,有可能程序正常只是偶然访问量大

解决多服务之间的数据依赖问题:在一个供应链系统中存在商品,销售订单,采购订单三个微服务,数据的依赖情况为需要根据商品来查找订单,需要根据商品来查找采购单,初期按照严格微服务划分,商品相关都在商品系统中,这时候如果查询订单或采购单
时包含商品字段,则要先根据商品字段查询商品服务返回匹配商品信息,然后在订单或采购单中通过in匹配商品id来查询单据。随着商品增多,匹配到的商品也越来越多,in查询越来越慢,且商品作为一个核心服务,依赖它的服务越来越多,导致响应变慢,还
会请求超时,一旦请求超时,相关服务处理就经常失败。所以需要改造,首先想到数据冗余,就是在订单,采购单中保存一些商品字段,这样每次查询就不用再依赖商品服务了。但是这里有个问题就是如果商品进行了更新,如何同步数据?一种就是更新商品时
先调用订单和采购服务,再更新商品冗余。另一种就是每次更新商品时发一条mq消息,订单和采购消费后各自更新冗余。第一个会引发数据一致性问题,如果冗余数据更新失败,那么整个操作都要回滚,对于商品服务来说是不合理的,这不是商品的核心需求
不能因为边缘流程阻断自身核心流程。而且依赖商品的服务会越来越多,多一个服务就要加一个,太耦合了。第二个方案就会好一些,一般也会用这种,但mq消息类型较多,联调也不知道某个消息被哪个服务器用了。而且每个服务都要重新处理冗余数据的逻辑
会导致大量重复代码。所以再一次改造就是解耦业务逻辑的数据同步方案,思路是将商品和商品相关的表实时同步到依赖的服务的数据库并保持表结构不变,不允许订单,采购服务修改商品相关表,这样商品不用依赖别的服务,其他服务也无需关注冗余数据
缺点就是增加了订单和采购服务的数据库存储空间,不过相比于数据冗余来说更节省空间,加入订单N个,商品M条数据,N一定远大于M,数据冗余要冗余N条,而数据库同步只要冗余M条,实时同步工具要求为支持实时同步,支持增量同步,不用写业务逻辑
支持mysql之间同步,活跃度高等五个要求,使用Bifrost,缺点在于不支持集群

多线程导入的例子:分治法,大量数据excel的导入,单线程太慢,用多线程去解决,用单线程从excel导入5000条并存入数据库差不多要16s左右,用多线程200一批,就是开25个线程去做耗时是一点五秒左右
如果每批次调的更小多开线程去做会快吗?比如100条一次50个线程。答案是不会的,甚至变慢,因为CPU几核心几线程才是真实的线程数,超过这个数量就是虚拟线程了,反而要进行线程之间的上下文切换导致耗时增多
小文件都读取进内存然后200去启动一个,文件大就用POI的SAX方式分段读取去启动线程做,然后这里有个问题就是多线程的事务问题,你多线程插入失败怎么反馈呢?如果一定要保持完全的事务性,那么就很复杂类似于分布式事务了
那可能用到2pc的方式去做,就每个线程做完之后先不提交commit,而是输出一个状态到集合中然后wait,等主线程查看状态集合数据,如果都成功就唤醒提交,失败回滚。3pc的话就是这一步再加个countDownLatch都通知到了才执行
如果是不要求全部一致的话,那就成功插入的就成功呗,失败的在异常里面重试几次也可以,然后丢mq里记录下来去做也可以,或者报个错那几个失败也行。

高并发系统缓存设计方案:用到缓存了就会主要有缓存穿透,缓存击穿,缓存雪崩的问题,我们设置缓存一般设置一个超时时间,因为数据库和缓存之间强一致很难,用超时时间做兜底防止数据不一致
雪崩很简单,过期时间分散过期时间是个固定+随机数就好了,缓存穿透场景主要是防攻击,因为缓存和数据库都是不存在的,就一直查询数据库压力很大,解决就是查数据库null后加入缓存,这样的问题
就是大量null可能会把缓存内存撑爆,布隆过滤器也可以,但是这个东西删除很麻烦,他是个简单的单向hash,所以少了个数据你去删万一另一个key也占了同一个hash位咋办,可能也可以使用双层布隆过滤器
第二个存删除的key,但一样会有hash碰撞的只是概率小点,然后最好定期重建,但说实话真实场景真没用过这个布隆过滤器缓存击穿一般出现在热key失效情况,热key过期大量请求访问数据库导致,这个把DB当作临界资源,访问要加锁,用分布式锁,没抢到
锁的两种策略,阻塞等待,这个不推荐,既然用了缓存那么请求量应该比较大,阻塞多久呢?阻塞的时候大量请求等着会不会把连接池打爆了?所以返回一个默认值比较好,默认值怎么设计呢?在设置keyvalue
的时候在value里设计一个逻辑过期时间,获取缓存的时候用当前时间和逻辑过期时间比较,如果超时了那就加锁去请求数据库更新数据,别的没抢到锁的直接返回这个数据,对于频繁更新的数据,一般先改数据库
再改缓存,这样在所有场景中只有数据库成功和缓存失败情况下在缓存过期前这一段时间数据可能不一致,但最终保持一致性

交易开关页面和增改查功能很简单,只不过在增加的时候根据平台+交易码+是否有效作为唯一键(代码控制,不用数据库控制),然后在增加和修改数据成功后插入redis,key为平台+交易码,value为开关开始时间,过期时间为开关结束时间
这是加在账户模块系统下的功能,至于控制接口禁用去提供者模块系统,因为接口都是提供给外部访问的
根据需求来看是对存量逻辑的一个过滤校验,属于功能增强类,第一反应是动态代理aop
首先要保证对原来代码改动最小,因为之前的交易种类混乱方式繁多,想要使用aop最小的改动就是梳理出所有交易类,将所有交易实现一个主接口,然后通过主接口可以获取主接口多个注入实现的List列表,对于列表
这样相比于直接在原执行流程中加这一步好处就是解耦了,通过jointpoint获取getArgs得到原方法的参数去redis查询来判断是否要禁用,环绕round还可以修改原方法参数
定义一个Switch接口,然后找到对外接口,并实现Switch接口,然后定义一个@Aspect类,pointcut通过Target指定目标对象的类型为Switch,然后对所有方法执行@Before注解的方法,方法中通过JoinPoint的getClass获取类实例,获取参数到redis判断是否存在,无则继续执行,有则禁用
这里有个问题就是所有方法,禁用可能会过度,解决办法就是构造方法注入Switch获取Switch的实现列表,也即该服务模块中所有的对外接口实例对象,放入一个map中,梳理所有该禁用的方法名字并添加到map里,然后判断到哪个class再判断method才禁用
如果前期已经实现策略模式,交易都统一走的一个父类的一个方法,然后用aop织入交易方法的@before进行控制,去新增的表库中查询判断状态就很方便,这是最初如果设计的话会这样
那现在的设计对代码有了改动侵入性太强,所以考虑其他控制类,有网关,过滤器,拦截器等,网关有,但不是对某一个微服务,是对于整个微服务集群的,粒度太大了,于是考虑用过滤器/拦截器,这两个都差不多,区别在于拦截器可以操作springmvc的东西
但是我们项目不是一个web项目,底层通信是用Dubbo的RPC方式,所以这里使用Dubbp的filter来处理,在fliter中通过invocation拿到入参,然后取出相应参数到redis校验有则判断开始时间选择是否禁用,无则放过,redis若压力大可以添加Guava的localCache
这里有个点是送进来的是外部商户号,我们设置的是内部平台号,这两个有个映射,所以要根据外部商户号查一次内部平台号,这个要放redis,但是原来的对于这个表的操作是不会自动更新redis的,所以这里要做一个数据一致性的保证
那就肯定有过期机制,既然如此就要保证缓存雪崩缓存穿透缓存击穿,返回new RpcResult(Object o),这里提一点,微服务之间取数据应该不要依赖redis,通过rpc去调用账户模块的功能,而账户模块放在redis中,这样才能将数据的结构保护起来不至于混乱

视频通过埋点计算观看数量怎么设计?观看时长怎么设计?视频表里加一个字段每次点进来就加1,但是这个如果不停刷新页面就会不停的累加,这样会导致刷数据的情况出现,低质量视频可能被当作热门视频推广,那么需要进行风控,基于ip加账号一段时间内
只能算一次这样,一般都是用账号的,因为现在都要实名,注册一个账号是比较消耗成本的,基于一些判断比如短时间大量评论大量视频看起来像机器人的就添加到账号风控名单去忽略掉这个号的信息,那么除开这些,B站视频首先要性能高几十万qps然后要
高可用,至少要有兜底值可以服务降级,然后数量也不用完全准确并且可以有延时,读写都要在内存中,不能在磁盘里,内存有远程和本地,本地缓存的话集群下一致性比较困难,所以用远程缓存redis,redis是10万qps如果满足不了就挂主从,这样就可以多
机器响应,主从同步策略也可以做的比较松散,暂时数量有问题影响不大,所以前期大量请求都写在本地内存,这样写入性能很高,然后定期吧本地内存刷到redis,然后再用一个定时任务统一把redis刷到数据库里,这里可能会丢数据,没关系丢一点问题不大
方案很简单,成本也很低,只有一些极端情况丢失一些不重要的数据,这个方案就还可以

数据库迁移的巨坑:订单切换背景,一开始的系统有很多订单来源,每个来源单独有个订单表,所以订单表的维护成本就很高,但是每个表的结构其实又是差不多的,所以想要重构把多个表合为一张表。其中的难点在于订单表功能是toC的不能停机,还有就是
业务也在开发迭代中,因此不能修改数据源的访问,要保证对开发中的程序员访问透明,还有就是切换的一致性问题,订单属于核心链路,字段的完整性和一致性要求是很高的,不过值得欣慰的一点是这些表都是一个应用在访问,如果多个访问的话就比较麻烦了
比如商品服务要查库存,库存服务将库存放在redis中,那么商品服务可以直接redis拿也可以走库存服务,然后库存服务从redis拿,这里微服务原则来说是后者,但前者更方便,这样的话如果库存服务下次不想放在redis用es,改造量和范围就很大了而且redis中
某个库存kv不见了也不知道是哪个应用给你删了,查找问题也很麻烦,所以服务间不要暴露自己的数据结构。回到订单切换,大致分为几步,第一步先双写,每次的写操作先写到老库然后写到新库,然后确认双写数据是否一致。第二步历史数据的同步,也要确定
数据没问题,然后第三步切读的流量到新的数据库,通过灰度发布,持续一段时间发布几次来切换,这就是不停机平滑迁移,然后第二个问题怎么和业务解耦开发?因为用了mybatis框架,定义数据库访问的时候只定义了mapper接口,而这个mapper是动态的通过
xml文件生成的所以,在service层中都是通过dao接口注入的,那么如果我把dao的生成进行一层代理,在代理里面完成双写和切换的逻辑,那么就和上游service层完全解耦了,这里对mapper接口实现静态代理模式就行另外的就是多个表变成一个表,如果之前有通过id来查询的,可能会有不同表的id是
一样的,但我们是用雪花算法生成的流水号去当主键的,所以没这个问题,如果遇到这种问题的话就很麻烦了,对于保持数据一致性,写老表和写新表要在一个事务里面,这个针对insert比较简单,但是对于update因为我们是先增量写新老表,然后有一个别的
逻辑去处理历史数据的,这里极端情况可能同步的先读了老表,这时候update老表然后去update新表,发现新表没数据,然后这时候同步的线程又把老表的老数据insert到新表了,这样老表的是update后的新数据,而新表是update前的老数据,这样数据不一致了
所以对于历史数据同步完了之后要有个定时任务进行双边的对账,并且发出一个告警,然后看是程序自动更正还是人为更正,慢慢处理到告警越来越少,数据基本就能一致了,以上是写的逻辑,读的逻辑就是不能一把梭哈直接切换,灰度切流,比如按百分比或者
对于用户id取模100等于1才走新表之类的,慢慢的去放大流量,同时还要单独设置一个校验的模块,读的过程中先读老表再都新表后进行比较,数据一致返回新表,不一致返回老表并发出告警。这样运行一段时间之后稳定了就可以把老表的写入停掉下线了。
同库的话就双写,主从的话用canel增量的写同步
搞得简单点的话就是增量数据双写,然后查先查新库,查不到去老库,查到的结果添加到新库里,后期做历史数据迁移,通过canel中间件同步也可以或者自己写接口,然后给接口返回字段加一个数据来源,运行了一段时间来源都是新表那就直接老表下线。这里
如果有缓存的话也是一样的先用老的等稳定切新的。
数据库迁移的一个考虑点:业务不大有低峰期直接停机做主从同步最简单。如果不能停机,那么同步的异步延迟一致性要考虑好,主要是双读和双写
业务背景为交易有很多来源每个交易都有一个交易流水表所以维护起来很麻烦,每个表的结构又差不多一样,某些字段和具体场景有关,这样的背景下重构把多个表合为一个表,因为交易是toC的不能停机平滑迁移
还有就是业务一直在迭代所以不能控制别的开发人员对数据源的访问,修改需要保持对他人透明,别人用原来的数据源一样能访问到,还有就是切换数据的一致性考虑,数据和字段的完整性一致性因为是交易流水
所以要求也非常高一定要保证一致
分批做,上游的读取对用户进行打标,比如用户id取模或者用白名单,满足的去读取新表,不满足的读取原来的表这就是灰度切流,但有时候对用户有使用情况的问题全球性质的那就用接口
打标,在某些用户时区是睡觉的时候切流,然后怎么校验新老库和主从数据是不是一致的呢,用binlog每天做一个比对,所以到了切流的时候数据没问题,这时候如果有个函数里写了马上读,写在老库读新库
类似于主从延迟一样,怎么解决呢,读从库同时读主库比较两个版本号取较大的一个做双读,这种接口较少,梳理出来做特殊处理

交易要与核心同步,成功失败相应处理,但有时候会超时,一种处理办法就是没成功直接回滚,但这样可能核心只是响应的慢了实际是做了的,那就会导致不一致的情况,所以这里有三种方案,一种是回滚的,一种是重试的,还有一种是语义锁(也就是人为的合并为一个事务)
金额交易不是淘宝发货通知之类的,重试比较危险,选用回滚,因为交易可以等待可以失败所以超时用自动查证,即记录状态后发mq消息自动查询根据返回结果操作,若失败则需要冲正即回滚结果。语义锁就是业务层面来控制,比如用数据库表存储发起后的记录
状态未完成,等收到结果后再更新状态未完成,然后根据完成的记录再执行相应操作。但这个可能会有弱一致性问题就是有多条等待反馈的记录,在金额这种强一致的情况下可能不合适
交易先本地记账,记账异常根据流水号回滚限额,记账失败回滚,记账超时发mq进行自动查证,记账成功发送核心,根据结果再来一次成功失败和超时,失败回滚或者冲正也有超时发mq查证

ChatGPT

ChatGPT无魔法免费体验
国内Chat列表导航
awesome -prompts
ChatGPT相关原理与基础知识

团队经验

团队的管理首先就是知道自己的资源,然后据此需求分析和计划排期,之后要留有冗余预防突发情况,以及对任务排优先级做好最低期望,还有向上反馈需要的资源和困难之处,阶段性复盘提炼总结,对下也要以结果为导向,任务的优先级和问题的及时反馈
资源的需要等,都是一套模式,有预期的情况下1v1定每个人的计划,不定期不定量的任务则平衡分配然后根据产出来排绩效
工时制度有一定可取之处,但平时排期后并不是只有排期的工作,还有很多琐碎沟通,以及每个人的能力和擅长都不同,用同一标准衡量最后就是加班延期质量不保等上线问题更没时间做后续的进入恶性循环

虚拟账户商户体系有平台,平台下挂商户,商户下挂个人和商户这样三个层级,同时平台自己也可以作为商户
bupps_merch_info中保存商户信息,innerMerchNo是商户在虚拟账户的唯一标识,merchNo是给外部使用的商户号
topOutUserNo是商户的平台的外部商户号,topMerchNo是商户的平台的内部商户号,然后查询的话平台外部商户号topOutUserNo是必输的,商户外部商户号merchno可以不输
商户号在merchno在平台下是唯一的,平台间可能不是唯一的

手续费提现配置手续费外内扣如果不设置分账承担商户,则会统一内扣为个人
提现判断在途非在途资金
前端发起交易,我们只是进行记账,然后发往核心进行交易执行,等待交易完成结果通知然后进行相应的更新并返回结果给前端,所有调用都要有超时查证

外部平台,金融云系统(类似网关,内外部系统链接),虚拟账户(记账),泰惠收(核心支付),系统间请求流水号唯一,系统内微服务有系统内流水号来唯一标识

grep -rn可以显示行号,容易丢失日志
tail -f 只能边用边打
vim 文件然后?xxx来寻找可以查找所有但是很多
view 然后?可以查
vi 直接查过去压缩包日志
打包部署:/app/gaps/source
sh deploy all & 打包所有或sh deploy bupps-customer bupps-epay &
在看error完成之后到deploy_see找到zip包下载拿到see平台上传发布物然后部署
customer提供给esb外部系统使用所以用dubbo,dubbo怎么实现远程通信的?
而几个微服务之间调用gapsflow或者代码里直接FlowHelper使用flowid去调用,底层也是通过bean的统一注册到注册中心后
FlowHelper调用的方法内部使用GapsServiceComponent的init方法通过flowid使用读写锁,如果不存在写锁独占将flowid和根据flowid,version,application,group等从注册中心得到的bean后生成的代理对象存入GapsServiceComponent
然后读锁去获取bean定义并执行,那这样不是调用微服务啊?如果用的不是同一个库不是GG?数据库配置不一致都调不到,目前是一起的,就是硬拆的微服务呗,customer和外部才是真的微服务,gaps封装了bean之后还是会去rpc调用的,有没有文档

excel大数据量导入,ooxml图表绘制,easyExce的head和row读取,通过getAsResourece拿excel表头配置然后读取并通过注解和反射去校验
微信小程序对接,数据大批量入库,秒杀设计,仔细回想确实没做什么高级业务,所以工资低没什么原因纯粹是菜,现在这个虚拟账户很大,要花时间学会架构和GAPS5架构这是dubbo
现在让你接一个支付功能,这是核心业务,支付记账也是项目亮点啊

支付和退款,担保非担保这些都要知道,不然整个业务一知半解是做不了的
项目经理就是需求评审,整体架构流程设计,对接上下级,人力和部门间资源的分配,测试和运维上线这些,还有项目的管理

挂账清单查询一共就两类:商户批量清算入账和线下充值,因为交易也就这两类
挂账的意思就是交易发到核心处理后我们进行记账发现核心处理的和我们这里绑定的卡不一致于是无法记账,就进行自动挂账,然后由业务去和交易人协调或者怎么样的再进行手动的调账,选择退回或者转到相应的卡里

iar网关层会进行服务的注册和发现,然后在本地通过rpc方式invoke对象,找到对应服务执行后序列化返回结果值

业务服务内部子模块之间通过注册服务到zookeeper然后通过rpc调用,只需要通过网关不用走gxp,和其他系统的交互通过customer通过gxp标准文本转换后再走iar网关
网关使用nginx分接入网关和api网关两部分,接入网关访问配置信息进行流量控制http协议转换T2私有协议,参数校验,安全接入,api网关控制路由注册中心和业务服务调用,负载均衡,鉴权,降级和扩容
GXP实现tcp/xml,http,T2等方式转为http/json访问接入,然后走向iar网关处理,接出提供T2转换
流程编排flow,流程引擎启动时扫描所有流程在spring容器中单例生成,可通过http或rpc方式调用,每次调用根据flow名查找对应对象并创建上下文对象保证线程安全
日志收集:通过开源log4j日志组件,自定义Appenders (输出源)直连kafka(消息队列),通过fileBeat采集器,读取日志文件,推送kafka(消息队列)
日志加工:Storm异步从kafka读取数据,对内容格式进行统一加工。方便后期链路跟踪、数据统计、图表分析
日志存储:日志信息通过Elasticsearch进行实时文档存储,提供实时分析搜索功能
日志分析:通过统一运营终端进行日志检索排查问题、全链路跟踪、图表分析、仪表盘,也可以通过RESTful API 个性化开发
每次交易生成唯一流水id gaps_serial_id,行内通过agent采集器将日志推送至行内日志中心备份
配置中心可以支持分支配置,分组配置,回滚上一版本配置信息,热部署发布配置,实现依赖mysql和zookeeper,在配置中心管理平台配置参数后,实时存储到mysql数据库中
点击发布按钮,管理平台对zookeeper节点进行注册更新删除操作
应用端通过jar包引用依赖配置中心客户端,应用端启动时向zookeeper服务器注册信息同时注册watcher类型监听器到本地watchManger中,zookeeper中节点发生变化后触发watcher事件
通知给客户端,客户端线程从watchManger中取出相应watcher对象执行回调逻辑(动态参数调整)去刷新应用端spring环境变量
注册中心使用zookeeper微服务框架使用dubbo,注册中心存储provider注册的远程服务,并将管理的服务列表通知给consumer且注册中心与双方都保持长连接,可以获取Provider发布的服务的变化情况,并将最新的服务列表推送给Consumer
任务调用平台也是写flow然后将flow配置进任务调用xml,然后部署就可以搜索到,再在调度平台进行流程组合,平台就会存储信息到数据库定时去执行,不好用的地方在于集成的平台任务长时间执行无返回难以寻找问题,所以转用xxl-job
spring自带的是单线程的,用线程池之类的也是系统内部的,没法页面查看以及分布式情况下的保证等

杂记

mybatisPlus的lamada表达式自定义方式,用.select自定义选择展示结果集sql,用.apply自定义查询条件sql,包括连表查询等,在最后使用.last可以进行limit 1分页

并发秒杀设计:购买场景无非查看商品,生成订单,取消订单,付款四部分,查看商品用前端静态资源加载解决,秒杀在于高并发同时保证性能和数据正确性,数据库会阻塞影响性能,所以用redis
库存的增减正确性用lua脚本来执行,保证原子性,这样就基本达到性能与正确性要求,订单数量正确后,不会出现超卖现象,然后支付调用支付接口的pre-id流程即可
redis实现分布式锁在java中使用redistemplate来实现,获取锁用setifabsent并设置超时时间防止死锁,万一执行时间超过超时时间,被其他线程获取到然后被当前线程释放会有问题,因此value设为uuid
只能释放自己上的锁,由于判断和释放非原子性,使用lua脚本解决,没有用redisson是项目惯性,没有引入。注意在redis的集群模式下要加前缀{cluster:}防止hash不一致,jedis到2.9.0解决集群密码支持问题
上锁用setifabsent其中key为监听事件的唯一标志,value为当前时间加过期时间,因为redis单线程且setifabsent原子性,只会有一个获取到分布式锁,获取失败的根据key去获取value判断是否超时,防止原来
操作异常未释放锁产生死锁,若当前时间大于value说明有问题了,需要重置key的value,这里多线程更新会有问题所以用getandset去更新然后判断,getandset原子操作更新一个key的value并返回原来的value
所以只有第一个去更新的得到的value是旧值,别的线程并发更新得到的都是新值,判断得到旧value的获取锁,这样就完成了上锁。释放锁也简单,就判断value相等然后删除key然后trycatch就行。这是最初版
将分布式锁放在appCommandLineRunner的run中,继承自springboot的commandLineRunner,在启动前执行,帮助做一些启动前功能
项目架构:
前端通过ngnix负载均衡的发送到gateway,gateway根据请求分发到监管端,发行方或者微信小程序,这三者之间用mq进行通信,监管和发行方使用了redis,这三个后端会调用开放API的封装链操作,开放api会
调用区块链,区块链异步返回结果发送到结果同步系统,走http回调到监管或者发行方
由于NFT的火爆,研究院决定建立自己的nft平台进行售卖,由于nft敏感性需要相应的监管,基于此项目架构为前端通过负载均衡发到网关,网关限流过滤后进入业务系统,业务系统之间用redis和mq交互,对
区块链的操作封装到开放api系统,api向区块链发请求,结果由交易记录通知系统监听获取,并通过http形式回调业务系统,业务系统ack形式返回接受处理结果,监管方主要功能是校验nft合规性和审核上链
小程序主要是用户注册和秒杀场景的并发,上链主要处理在于十万条数据nft上链和插入数据库耗时所以同步进行,数据库用异步线程批量插入,通过redis存储执行进度,观察者模式simpleobservable保证异常
后重启线程执行,通过定时任务保证宕机后恢复,小程序注册主要使用redis分布式锁锁住手机号防止注册问题,具体流程在收藏里的invatation图,秒杀流程主要就是订单生成和取消还有支付,订单对应库存
并发场景下使用redis用lua脚本增减保证原子性,售卖结果在监管和小程序间用mq同步时,乐观锁失败率百分之六较高且内存占用较大,优化为售卖结果redis存储状态,incr原子计数减少数据库操作加快处理
并且减少对象创建,用定时任务判断redis计数去更新库,后续优化可以批量发送和批量消费mq,处理逻辑也简化为计数后返回,到达一定量统一发给自己一条mq去执行,保证执行的成功

springcloudgateway的jar与springboot版本要一致否则无法使用,还有网关中的url使用lb:是针对由注册中心的情况,没有的话统一使用http:在k8S中可通过的serviceName+port来寻找到服务提供者
在网关中是转发请求不用处理请求所以直接使用K8S,如果涉及到微服务的相互调用需要注册中心,用于序列化传输。用feign也可以,依赖K8S要运维在系统配置中开启ClusterRoleBinding,普通的路由功能
ipKeyResolver实现KeyResolver根据header中x-forwareded-for获取ip进行限流,统一ip访问数进行限制,实现GloableFilter,ordered来实现全局过滤,防重放(一定时间内只处理一个请求)
限流的requestRateLimit可代码也可以配置
cloud:
gateway:
routes:
-id:opennft-console-svc
uri:http://opennft-console-svc-service:30011/
predicates:
-path://**
filters:
-name:RequestRateLimiter
args:
redis-rate:10
key-resolver:“”

全局过滤器配置实现使用webMvcConfigurer配置类,注册一下拦截器Interceptors指定一下拦截路径,自定义拦截器AuthTokenInterceptors继承HandlerInterceptorAdapter重写preHandle处理前和PostHandle
处理后,在preHandle中校验token(token在登录时生成并返回给前端,保持redis中,之后其他接口访问都要拦截器验证token成功才执行)并设置为localThread后续要查调用者信息时可提供
利用@ControllerAdvice声明一些全局性的东西,结合@ExceptionHandler用于全局异常处理,统一捕获异常,设置相应异常码返回前端,@ModelAttribute则会在执行Controller之前执行
@Aspect和@Componet注解LogAspect类,用@pointCut(“execution(xxpath)”)定义横切点方法路径,@Before(value=“”)定义横切执行前增强,打印请求时间,入参,请求发起者等输入到log日志
@AfterReturning定义横切执行完返回逻辑增强,打印返回结果,请求时间等输入到log日志,就完成了全局的aop执行日志写入功能,入参在joinpoint对象里,序列化成string输出

MVCC版本控制:B修改且未提交A读取会读取到B修改前的数据,但A要修改必须等B提交,数据库在并发访问时可能会出现脏读,幻读,不可重复读,分别是读到不存在的数据(别的事务未提交)
读到数据无法支持后续操作(条数不对,修改一条被删除的数据),两次读取同一数据结果不同。MVCC在可重复读的隔离级别用快照解决幻读,使得不需要串行化执行,减少了锁的使用。普通的select不加锁
是快照读,select for update和insert,update,delete是当前读,会加行锁,实现原理是通过三部分,版本链,undolog和readview。每一条数据其实都有三个隐藏字段,分别记录最后一次修改事务的id
回滚到上一版本的指针和隐藏主键,在undolog中记录了记录的修改版本链,每个版本上一版本的指针以及该版本修改事务的id,有线程定期清理无用的undolog,然后readview是读视图,在事务进行快照读
时候生成的,记录事务相关信息,主要有三部分,trxList为生成视图时系统活跃的事务id列表,uplimitId为活跃事务最小id,lowlimitId为下一个未分配的事务id,然后根据记录的事务id和最小id比较
如果前者小,表明可以看到,否则和lowlimitid比较,大则不可见,小则判断是否在活跃链表中,在说明生成readview时该事务正活跃未commit不可见,反之已经commit则可见,而且为了保证可重复读
这里每个事务只会在第一次快照读时候生成readview,之后快照读沿用第一次生成的readview,但mvcc不能解决幻读问题,因为读有当前读和快照读。mvcc只能解决rr可重复读级别下快照读的幻读问题
如果需要实时数据进行当前读就要加锁,一种行锁,一种间隙锁,锁其实都是锁的索引,而非记录,即便没有索引,也有隐藏字段主键索引,行锁有s和x锁,一个事务获得s锁,其他事务可以继续获取s锁
但不能获取x锁,相当于读锁,可以一起读,但有人读就不能写,一个事务获取x锁,其他事务不可获取锁,即写独占锁。间隙锁是锁了索引之间的间隙,因为索引有序,比如有1,3,4那么我读取3,就会
在1,3和3,4的间隙锁住,这时候有事务插入2,是在1,3之间的就会失败,这可以规避掉行锁的一个问题就是第一次读的时候幻读数据不存在,就没法加行锁。要注意对没有索引的列当前读会加全表间隙锁
生产上要注意,所以幻读的两种读分别使用mvcc和next-keylocks解决的

消息队列如何保证消息可靠传输首先要保证消费端幂等性,一般根据唯一id或者记录状态等判断是否处理过,保证幂等后也无所谓生产者是否多发了,这是防止多做,那么少做怎么防止呢,对于中间件中心
存储消息的组件broker来说,生产者发送消息后等broker返回一个接受到的讯息,rocketmq的confirm或者kafaka的ack机制,这样保证发送成功,消费时也一样,做完了返回ack才说明做完了,否则按配置决定
继续发的策略,如何设计一个mq?rocketmq融合了kafka和rabbitmq所以基于rocketmq来说,首先要存消息,要有一个单机的队列数据结构,支持高效且可以扩展即缩容扩容,再将队列分布式到集群管理,用
nameserver记录broker,topic,生产者向nameserver拉取topic所在broker之后向broker发消息,broker与nameserver之间心跳链接并定期向nameserver集群注册topic,集群会有同步机制。消息在多个队列
中轮询且消费者和队列有N对一关系,保证一个队列内先进先出。最后还有日志文件commitlog持久化,用于丢失恢复宕机消息恢复,还有基于topic的所以查询consumerquene log查消息,扩展功能包含死信队列
延时队列,事务消息等。事务消息就模仿数据库,生产者向mq发,然后存在事务消息队列中,发完才commit放入,否则rollback,消费也一样,消费完commit那mq才能删除,幂等消费者自己做。顺序消息的原理
目前只有rocketmq有,只需要局部顺序就行,在生产者处注册messageselect保证消息会放到同一个队列,消费者设置registerlistener监听保证一定是同一个消费者取走所有消息。百万消息积压如何处理?
事前预估压测来分配合适资源,事中优先处理消息防止宕机,机器上加队列,如果是机器性能瓶颈那K8S自动扩容加机器,事后增加消费并行度,批量消费,多个结果汇总更新提交数据库响应速度,跳过非重要
消息,优化处理逻辑等

保障接口安全的五种方式:token授权,http协议无状态,一次请求结束断开链接下次请求并不知道谁发送的,有的模块需要权限访问的话就在第一次登录给客户端返回一个token,将token和用户id以键值对
形式保存redis中,后续所有操作要带上token进行验证,token应用内唯一且每次token生成不一样,半小时过期。生成方式用户id+时间戳+服务端密钥用MD5加密生成,第二种时间戳超时机制,每次请求带上
时间戳,收到后和当前时间比较,差值过大请求失效,可当时dos攻击,第三种url签名,防止请求参数被篡改,对通信参数加上私钥用MD5加密生成sign,随接口一起发送,服务端进行相同操作和sign比较,结果
相同则无修改,第四种防重放,将123联合,第五种用https加密协议

项目部署:jenkins构建完查看consoleoutput拿到镜像名修改中镜像地址,是配置文件,是容器部署文件,是容器暴露给外部访问地址文件,下的data是
系统参数相当于,apiversion版本号,kind是configmap类型,metadata下name是configmap的名字,在的volumes数据卷的configmap的name一致,中selector的app值是
中labels下app名字,用来找相应部署容器,所以就是deploy是部署容器,配置用configmap的,svc是映射部署的容器和暴露外部访问端口的。
vi修改三个文件内容通过k apply -f 覆盖,启动有问题就delete pod/name就会自动再部署一个pod,查看日志k logs 名字 动态日志 k logs -f 名字 查看pod属性 k describe pods/名字 可以查看node地址
k get svc查看node端口号 地址加端口号路径可以在网络*问,的spec下的replices设置几个就部署几个容器,设置为0后覆盖文件就会停止部署

springcloudkubernetes是springcloud和K8S结合产品,不需要再依赖其他组件进行服务注册发现,导入该jar之后应用直接部署K8S即可令服务之间自行找到路径微服务通信
@FeginClient(name=服务在K8S中的servicename)
publc interface feignDemo{
@GetMapping(“/get”)
String getMessage();
}
调用实例:
@Autowired
private feignDemo demo;

@GetMapping(“/get”)
public String demo(){
();
}

同时在springboot启动类加上@EnableDiscoveryClient和@EnableFeignClient

就是开启服务发现和注册功能,然后springcloud的feign那一套,只不过name写的是svc中那个servicename然后直接注入调用就行了,不用走注册中心,因为K8S会去读配置找到相应的容器

jenkins打包报no space left on device然后去服务器用df -h 查看内存使用发现挂载在/app下的文件系统占用99。然后用du -su *查看每个文件占用然后手动删除 或者去jenkins构建配置通用下面选择丢弃旧的构建 只保留三个这样每次构建自动删除前面的

springboot中component对的springutil在core和hutool包中都有,导致重复了,解决办法在core自己的代码中springutil的component中value写个别的名字,就不会冲突了,但使用会影响吗?不会,因为注入的时候要import类路径的,从util中获取的名字变了而已

10 30 * * * find /app/gaps/vas/logs/2024-03 -name “*.” -mtime +2 -exec rm {} ;
每天十点半删除/app/gaps/vas/logs/2024-03文件夹下的最后修改时间三天前的后缀为的文件

订单怎么优化流程啊,循环很慢那就循环里的查询校验提取出来一把查,然后循环是一个个等待,那并行计算forkjoinpool用一下,怎么用,再把流程拆开订单的和支付的结果之间异步的通知?看下怎么实现的,高可用?
纯计算就好,加逻辑判断甚至会阻塞要慎用,fork/join pool适合计算密集型任务且最好是非阻塞任务,用相对少的线程处理大量任务,本质就是将任务给按设置粒度不断分拆然后从最底层任务开始计算,往上一层层合并结果递归。所以不适合业务形式拆分。然后forkjoinpool特点是工作窃取模式,每个线程会在自己队列任务处理完后去其他线程的队列尾部获取任务来提供资源利用率,从尾部窃取可以防止与原线程竞争,只剩最后一个任务时,通过cas方式解决与原线程竞争问题。

rpc或者http方式远程调用,远程的实例是复制本地数据copy的还是直接本地的?这会导致数据在远程修改之后修改值和本地还是否一致的问题,rpc怎么传输数据的?
通过序列化传输,对象不能直接网络传递,根据协议将类型,属性类型,属性值等按规则转为二进制数据为序列化,服务调用者将对象转为二进制数据送达服务提供者,提供者根据二进制数据解码反序列化在本地创建新对象使用,使用完后将新对象序列化为二进制数据传输,服务调用者收到新对象的二进制数据反序列化创建新对象赋值给之前的引用。所以实例是直接本地新建的,但是数据修改变化是可感知的