面试总结

项目遇到的问题

报关出区系统自动脚本

  • 版本1: webdriver脚本 解析excel数据到园区系统

  • 版本2 : 包装成服务,报文系统请求该服务,由该服务自动提交表单

  • htmlparser , htmllexer ,MapStruct

  • 表头(formid,报关出库单号) -> 明细

电信DNS劫持

  • 备案问题,求助电信
  • isp问题,投诉 (切换vpn没出现这样的问题)
  • 改dns , https

font 字体样式

  • 版权问题,法务

动态销量看板

  • 性能问题,查询归档
  • websocket http2.0

微信头像同步

​ utf8mb4,异步多线程

砍价算法

FTPPool

  • 带name的ftp连接池 不同目标ftpserver 解决发送慢的问题

MySql 8小时

  • 连接池配置 :checkouttime checksql ,idle_time my.ini配置

    wait_timeout=28800

    interactive_timeout=28800

    testOnBorrow和testOnReturn在生产环境一般是不开启的,主要是性能考虑。失效连接主要通过testWhileIdle保证,如果获取到了不可用的数据库连接,一般由应用处理异常。

    一般在生产环境这个数值会被设置为7天甚至30天,目的是保证mysql不会因为流量稀少而主动关闭session. 至于是否会导致大量的sleep连接,这个请在理解以上原理后,自行思考吧。

    老版本中可以拼接 &autoReconnect=true

  • 连接池的大小的设置

    还是要结合实际的业务场景来说事

    比如说,你的系统同时混合了长事务短事务,这时,根据上面的公式来计算就很难办了。正确的做法应该是创建两个连接池,一个服务于长事务,一个服务于"实时"查询,也就是短事务。

    还有一种情况,比方说一个系统执行一个任务队列,业务上要求同一时间内只允许执行一定数量的任务,这时,我们就应该让并发任务数去适配连接池连接数,而不是连接数大小去适配并发任务数。

    连接数 = ((核心数 * 2) + 有效磁盘数)

    • N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。

      ( waittime+cputime /cputime )*corenum + disknum

      • 等待时间 (非cpu; db,rpc,cache)

    可以用shell脚本监控长事务(通过查询innodb相关表 locks_wait ...)

接口幂等性

乱码

  • java内部采用的就是Unicode编码

java运行的过程中就必然存在从Unicode编码与相应的计算机操作系统或者浏览器支持的编码格式相互转化的过程,这个转换的过程有一系列的步骤,如果某个步骤出现错误,则输出的文字就会是乱码。

  • 所以产生java乱码的问题就在于JVM与对应的操作系统/浏览器进行编码格式转换时出现了错误。

  • utf-8 为Unicode实现之一

  • java源文件是采用操作系统默认支持的file.encoding编码格式保存的。下面代码可以查看系统的file.encoding参数值。

    System.out.println(System.getProperty("file.encoding"));
    
  • javac.exe 把我们编写的java源程序从file.encoding编码格式转化为JAVA内部默认的UNICODE格式放入内存中

  • JDK将上面编译好的且保存在内存中信息写入class文件中,形成.class文件。此时.class文件是Unicode编码的,也就是说我们常见的.class文件中的内容无论是中文字符还是英文字符,他们都已经转换为Unicode编码格式了。

    在这一步中对对JSP源文件的处理方式有点儿不同:WEB容器调用JSP编译器,JSP编译器首先会查看JSP文件是否设置了文件编码格式,如果没有设置则JSP编译器会调用调用JDK采用默认的编码方式将JSP文件转化为临时的servlet类,然后再编译为.class文件并保持到临时文件夹中。

  • 运行编译的类:在这里会存在一下几种情况

    1、直接在console上运行。

    2、JSP/Servlet类。

    3、java类与数据库之间。

    这三种情况每种情况的方式都会不同,

    • Servlet类 、在数据库保存数据时(JDBC) 默认采用ISO-8859-1编码格式
  • javaweb一般解决乱码

    • post contentType || request.setCharacterEncoding(charset)

    • url javascript编码的三个方法:escape()、encodeURI()、encodeURIComponent()

    • javascript有一次转码、两次转码两种解决方法

      //1次转码
      String name = request.getParameter("name");
      System.out.println("传入参数:" + name);
      name  = new String(name.getBytes("ISO-8859-1"),"UTF-8");
      System.out.println("解码后参数:" + name);
      // 2次转码
      //前端
      window.location.href = encodeURI(encodeURI(url));
      // 后台
       String name = request.getParameter("name");
       name  = URLDecoder.decode(name,"UTF-8");
      
  • filter request.setCharacterEncoding(encoding);

支付安全

  • 对传递的金钱,数量等对最后支付金额会产生影响的所有参数做签名。
  • 并且注意签名算法不可被猜测到。
  • 这样攻击者修改数据的时候验证便不会通过。
  • 同时注意对已经交易的订单不可重复而造成重复重置的漏洞

库存超卖问题

利用Redis increment 的原子操作 + SQL 乐观锁

  1. 先查询redis中是否有库存信息,如果没有就去数据库查,这样就可以减少访问数据库的次数。 获取到后把数值填入redis,以商品id为key,数量为value。 注意要设置序列化方式为StringRedisSerializer,不然不能把value做加减操作。 还需要设置redis对应这个key的超时时间,以防所有商品库存数据都在redis中。

  2. 比较下单数量的大小,如果够就做后续逻辑。

3)执行redis客户端的increment,参数为负数,则做减法。因为redis是单线程处理,并且因为increment让key对应的value 减少后返回的是修改后的值。 有的人会不做第一步查询直接减,其实这样不太好,因为当库存为1时,很多做减3,或者减30情况,其实都是不够,这样就白减。

  1. 扣减数据库的库存,这个时候就不需要再select查询,直接乐观锁update,把库存字段值减1 。

  2. 做完扣库存就在订单系统做下单。

/* update mysql 使用乐观锁 version*/
update Product set count = count - #{购买数量} where id = #{id} and count - #{购买数量} >= 0 and version = #{preversion};

/* 虽然redis已经防止了超卖,但是数据库层面,为了也要防止超卖,以防redis崩溃时无法使用或者不需要redis处理时,则用乐观锁,因为不一定全部商品都用redis。
利用sql每条单条语句都是有事务的,所以两条sql同时执行,也就只会有其中一条sql先执行成功,另外一条后执行,也如上文提及到的场景一样。*/

用户访问下单是,前端ui可以让用户触发结算后,把按钮置灰色,防止重复触发。

可以按照库存数量来选定是否要用redis,因为如果库存数量少,或者说最近下单次数少的商品,就不用放redis,因为少人看和买的情况下,不必放redis导致占用内存。

如果到时间点抢购时,可以使用mq队列形式,用户触发购买商品后,进入队列,让用户的页面一直在转圈圈,等轮到他买的时候再进入结算页面,结算页面的后续流程和本文一致。

新零售

rfid

零售业的数字营销是非常先进的,比如说电商的本质就是在于把零售业的数字营销那条线做到极致,但更多零售业的数字管理程度还不够高,所以我是挺认同新零售的常见定义,

以消费者体验为中心的数据驱动的泛零售形态」,本质就是把数字营销、数字管理、数字力量有机结合,从而形成了新零售业态。这个角度看来,盒马鲜生我觉得确实能在 IT 里面起到标杆效应

人才方面,我觉得产业互联网核心在于产业认知,需要有对于产业的充分理解,知道整个业务如何去理解以及和不同的环节配合。从人的特质上,我最喜欢的是具有领导力、思辨力的人才,既能驱动事情前进,又能探索事物的本质。在这个行业里面,我认为技术的深度不是最重要的,能够跨界学习的软性能力则是非常重要的,思辨胜于经验。

ONES 等效率工具

「外部视角」观点,我觉得很深刻。

在公司的内部作出的决策和工作方法,其实都属于内部视角。有时候内部思想很难说明白,大家「屁股决定脑袋」嘛,其实很多事情在内部是很难推动的。

接触的新名词

  • CPS(按销售付费)CPS:英文全称Cost Per Sales。按销量付费
  • 学习
    1. 学基础知识
    2. 哲学方法论
    3. 去表面 存核心
    4. 方法使用
  • R.I.P.(Requiescat in peace简称)是一种简短的碑铭,或是希望逝者永享安宁的短句。通常以全称“Rest in peace”或缩写的形式刻在墓碑上,作为碑铭的一部分。“R.I.P.”一般见于天主教墓园

JAVA 基础&高级

final

  • final修饰的变量也是可以变的哦

    private final int final_field = random.nextInt(50);    //使用随机数来进行初始化
    

    所以不要以为某些变量是final就可以在编译期知道其值,使用随机数其进行初始化,他要在运行期才能知道其值

  • final方法

    • 防止被子类修改
    • 如果将一个方法指明为final,就是同意编译器将针对该方法的所有调用都转为内嵌调用 ,提高性能(不是很懂 要再去jvm规范了解下)
  • final类(最终类,比如:String)

  • final参数

    • 在匿名内部类中,为了保持参数的一致性,若所在的方法的形参需要被内部类里面使用时,该形参必须为final (原因可以用javap -p inner.class了解)

    • 从代码来看好像是那个内部类直接调用的a参数和b参数,但是实际上不是,在java编译器编译以后实际的操作代码是

      class Outer$Dosome
      {
       public Dosome(final String a,final int b)
         	{
         this.Dosome$a=a;
             this.Dosome$b=b; 
       } 
         public void dosome(){ 
             System.out.println(this.Dosome$a + this.Dosome$b);   
         }
      }
      

      从以上代码看来,内部类并不是直接调用方法传进来的参数,而是内部类将传进来的参数通过自己的构造器备份到了自己的内部,自己内部的方法调用的实际是自己的属性而不是外部类方法的参数。

      这样理解就很容易得出为什么要用final了,因为两者从外表看起来是同一个东西,实际上却不是这样,如果内部类改掉了这些参数的值也不可能影响到原参数,然而这样却失去了参数的一致性,因为从编程人员的角度来看他们是同一个东西,如果编程人员在程序设计的时候在内部类中改掉参数的值,但是外部调用的时候又发现值其实没有被改掉,这就让人非常的难以理解和接受,为了避免这种尴尬的问题存在,所以编译器设计人员把内部类能够使用的参数设定为必须是final来规避这种莫名其妙错误的存在。”

      (简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变)

堆、栈,方法区

    • 虚拟机栈

      • Java虚拟机栈也是线程私有的 ,它的生命周期与线程相同
    • 本地方法栈与虚拟机的作用相似,不同之处在于虚拟机栈为虚拟机执行的Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。有的虚拟机直接把本地方法栈和虚拟机栈合二为一

  • 栈和堆都是用来从底层操作系统中获取内存的。

  • .class加载到方法区,而方法区现在放在堆上

  • 每一个线程都有一个栈,但是每一个应用程序通常都只有一个堆

  • 栈更快因为所有的空闲内存都是连续的,因此不需要对空闲内存块通过列表来维护。只是一个简单的指向当前栈顶的指针。

  • 堆包含一个链表来维护已用和空闲的内存块

  • 栈经常与 sp 寄存器一起工作,最初 sp 指向栈顶

  • 方法区

    方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

    • 在目前已经发布的JDK 1.7 的HotSpot中,已经把原本放在永久代的字符串常量池移至本地内存中

    • 这么说以前的字符串常量池也被移到堆中了

    • JDK 1.8 中,已经没有方法区(永久代),而是将方法区直接放在一个与堆不相连的本地内存区域(Native Memory),这个区域被叫做元空间。

    • 通过上面分析,大家应该大致了解了 JVM 的内存划分,也清楚了 JDK 8 中永久代向元空间的转换。不过大家应该都有一个疑问,就是为什么要做这个转换?所以,最后给大家总结以下几点原因:

      1、字符串存在永久代中,容易出现性能问题和内存溢出。

        2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

        3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

        4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

  • Java程序运行时

首先会通过类装载器载入类文件的字节码信息,经过解析后将其装入方法区。类的各种信息(包括方法)都在 [方法区] 存储。

Foo foo = new Foo(); foo.f();

以上代码的内存实现原理为:
  1.Foo类首先被装载到JVM的方法区,其中包括类的信息,包括方法和构造等。
  2.在栈内存中分配引用变量foo。
  3.在堆内存中按照Foo类型信息分配实例变量内存空间;然后,将栈中引用foo指向foo对象堆内存的首地址。
  4.使用引用foo调用方法,根据foo引用的类型Foo调用f方法。

定义 成员变量 局部变量 静态变量
定义位置 在类中,方法外 方法中,或形参 类中,方法外(java只有静态成员变量)
初始化值 有默认初始化值 无,先定义,后赋值 默认初始化值
存储位置 方法区
声明周期 与对象共存亡 与方法(线程)共存亡 与类共存亡
  • 其实,移除永久代的工作从JDK1.7就开始了。字符串内部池,已经在JDK7中从永久代中移除,JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。我们可以通过一段程序来比较 JDK 1.6 与 JDK 1.7及 JDK 1.8 的区别,以字符串常量为例:

    import java.util.ArrayList;
    import java.util.List;
     
    public class StringOomMock {
        static String  base = "string";
        public static void main(String[] args) {
            List<String> list = new ArrayList<String>();
            for (int i=0;i< Integer.MAX_VALUE;i++){
                String str = base + base;
                base = str;
                list.add(str.intern());
            }
        }
    }
    
    // jdk 6 
    java.lang.OutOfMemoryError: PermGen space
      at java.lang.String.intern(Native Method)
        …………
    // jdk 7 
    java.lang.OutOfMemoryError: Java heap space
      at java.lang.util.Arrays.copyOf(Arrays.java:2367)
        …………   
    // jdk 8 
    java.lang.OutOfMemoryError: Metaspace
      at java.lang.classloader.defineClass1(Native Method)
        …………    
    

GC机制

gc回收的是无用对象,而对象创建后在jvm堆中所以我们要先来看jvm堆
JVM堆分为

  • (1) 新域:存储所有新成生的对象(使用“停止-复制”算法进行清理)

    新生代内存分为2部分,1部分 Eden区较大,1部分Survivor比较小,并被划分为两个等量的部分。

    它们之间划分有个比例8:1:1什么鬼的忘记了 ,可以通过参数指定

  • (2) 旧域:新域中的对象,经过了一定次数的GC循环后,被移入旧域(算法是标记-整理算法)

  • (3)永久域:存储类和方法对象,从配置的角度看,这个域是独立的,不包括在JVM堆内。默认为4M。

    • 方法区(永久域):
      永久域的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。

      在目前已经发布的JDK 1.7 的HotSpot中,已经把原本放在永久代的字符串常量池移至堆中

      对于无用的类进行回收,必须保证3点:

      • 类的所有实例都已经被回收
      • 加载类的ClassLoader已经被回收
      • 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)
    • 永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。

      先是触发young GC --> 当old满了,触发full GC。full GC很消耗内存 ,需要让它尽量少发生

对象的拷贝

  • 浅拷贝

    浅拷贝只是Java提供的一种简单的拷贝机制,一般没有什么实用价值

    如果有引用对象需要深拷贝,还是得重写clone方法

  • 深拷贝

    新建大量的对象

  • 序列化拷贝

    originObj对象只要实现Serializable接口就可实现克隆,无须继承Cloneable接口实现clone()方法。

    public class CloneUtils {
        public static <T extends Serializable> T clone(T originObj){
            T cloneObj = null;
            try {
                //写入字节流
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                ObjectOutputStream obs = new ObjectOutputStream(out);
                obs.writeObject(originObj);
                obs.close();
                
                //分配内存,写入原始对象,生成新对象
                ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
                ObjectInputStream ois = new ObjectInputStream(ios);
                //返回生成的新对象
                cloneObj = (T) ois.readObject();
                ois.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return cloneObj;
        }
    }
    

    1、 基本类型

       如果变量是基本很类型,则拷贝其值,比如int、float等。
    
    2、 对象
    
       如果变量是一个实例对象,则拷贝其地址引用,也就是说此时新对象与原来对象是公用该实例变量。
    
    3、 String字符串
    
       若变量为String字符串,则拷贝其地址引用。但是在修改时,它会从字符串池中重新生成一个新的字符串,原有字符串对象保持不变。
    

多继承

  • 内部类可以继承一个与外部类无关的类,因为这一点,多重继承才会成为可能
// 这个儿子有点叼
public class Son {
    
    /**
     * 内部类继承Father类
     */
    class Father_1 extends Father{
        // 帅气值
        public int handsome(){
            return super.handsome() + 6;
        }
    }
    /**
     * 内部类继承Mother类
     */
    class Mother_1 extends  Mother{
        //温和
        public int mild(){
            return super.mild() - 2;
        }
    }
    
    public int getHandsome(){
        return new Father_1().handsome();
    }
    
    public int getMild(){
        return new Mother_1().mild();
    }
}

// 儿子继承了父亲,变得比父亲更加帅气
// 同时也继承了母亲,但是没有那么温和了
// 内部类可以继承一个与外部类无关的类,因为此,多重继承才会成为可能。



equals

在java中进行比较,我们需要根据比较的类型来选择合适的比较方式:

 **1)** 对象域,使用equals方法 。 
   **2)** 类型安全的枚举,使用equals或== 。 
  **3)** 可能为null的对象域 : 使用 == 和 equals 。 
 **4)** 数组域 : 使用 Arrays.equals 。 
 **5)** 除float和double外的原始数据类型 : 使用 == 。 
 **6)** float类型: 使用Float.foatToIntBits转换成int类型,然后使用==。  
  **7)** double类型: 使用Double.doubleToLongBit转换成long类型,然后使用==。

至于6)、7)为什么需要进行转换,我们可以参考他们相应封装类的equals()方法,下面的是Float类的:

However, there are two exceptions:
If f1 and f2 both represent
Float.NaN, then the equals method returns
true, even though Float.NaN==Float.NaN
has the value false.
If <code>f1 represents +0.0f while
f2 represents -0.0f, or vice
versa, the equal test has the value
false, even though 0.0f==-0.0f
has the value true.

我们在覆写equals()方法时,一般都是推荐使用getClass来进行类型判断,不是使用instanceof。我们都清楚instanceof的作用是判断其左边对象是否为其右边类的实例,返回boolean类型的数据。可以用来判断继承中的子类的实例是否为父类的实现。注意后面这句话:可以用来判断继承中的子类的实例是否为父类的实现,正是这句话在作怪。我们先看如下实例(摘自《高质量代码 改善java程序的151个建议》)。

public class Person {
    protected String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public Person(String name){
        this.name = name;
    }
    
    public boolean equals(Object object){
        if(object instanceof Person){
            Person p = (Person) object;
            if(p.getName() == null || name == null){
                return false;
            }
            else{
                return name.equalsIgnoreCase(p.getName());
            }
        }
        return false;
    }
}


public class Employee extends Person{
    private int id;
    
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Employee(String name,int id){
        super(name);
        this.id = id;
    }
    
    /**
     * 重写equals()方法
     */
    public boolean equals(Object object){
        if(object instanceof Employee){
            Employee e = (Employee) object;
            return super.equals(object) && e.getId() == id;
        }
        return false;
    }
}

//test
public class Test {
    public static void main(String[] args) {
        Employee e1 = new Employee("chenssy", 23);
        Employee e2 = new Employee("chenssy", 24);
        Person p1 = new Person("chenssy");
        
        System.out.println(p1.equals(e1));//true
        System.out.println(p1.equals(e2));//true
        System.out.println(e1.equals(e2));//false
    }
}

/* 对于那e1!=e2我们非常容易理解,因为他们不仅需要比较name,还需要比较id。但是p1即等于e1也等于e2,这是非常奇怪的,因为e1、e2明明是两个不同的类,但为什么会出现这个情况?首先p1.equals(e1),是调用p1的equals方法,该方法使用instanceof关键字来检查e1是否为Person类,这里我们再看看instanceof:判断其左边对象是否为其右边类的实例,也可以用来判断继承中的子类的实例是否为父类的实现。他们两者存在继承关系,肯定会返回true了,而两者name又相同,所以结果肯定是true。

      所以出现上面的情况就是使用了关键字instanceof,这是非常容易“专空子”的。故在覆写equals时推荐使用getClass进行类型判断。而不是使用instanceof。
*/

NIO

  • select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
  • select 有打开文件描述符数量限制,默认1024(2048 for x64),100万并发,就要用1000个进程、切换开销大;poll采用链表结构,没有数量限制。
  • select,poll “醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,通过回调机制节省大量CPU时间;select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,而epoll只要一次拷贝。
  • poll会随着并发增加,性能逐渐下降,epoll采用红黑树结构,性能稳定,不会随着连接数增加而降低。

SimpleDateFormat线程安全问题

SimpleDateFormat维护的用于format和parse方法计算日期-时间的calendar被清空了,
如果此时线程A将calendar清空且没有设置新值,线程B也进入parse方法用到了SimpleDateFormat对象中的calendar对象,此时就会产生线程安全问题!

使用SimpleDateFormat对象进行日期-时间计算时,如果SimpleDateFormat是多个线程共享的就会有线程安全问题!
应该让每一个线程都有一个独立的SimpleDateFormat对象用于日期-时间的计算!此时就可以使用ThreadLocal将SimpleDateFormat绑定到线程上,
这样也可以避免线程安全问题!

双亲委派模型

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。下面举一个大家都知道的例子说明为什么要使用双亲委派模型。

黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。

而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。

或许你会想,我在自定义的类加载器里面强制加载自定义的java.lang.String类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。

举个简单例子:

ClassLoader1、ClassLoader2都加载java.lang.String类,对应Class1、Class2对象。那么Class1对象不属于ClassLoad2对象加载的java.lang.String类型。

如何实现双亲委派模型

    双亲委派模型的原理很简单,实现也简单。每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载。其实ClassLoader类默认的loadClass方法已经帮我们写好了,我们无需去写。

破坏双亲委派模型

 双亲委派模型并不是一个强制性约束,而是java设计者推荐给开发者的类加载器的实现方式,在一定条件下,为了完成某些操作,可以“破坏”模型。
     1.重写loadClass方法
     2.利用线程上下文加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的 setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承 一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
     3.为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换

线程池打满

#jstack dump
jstack qid > thread.txt
分析dump线程信息

​ 发起 http 请求的队列 ,这个使用队列形成的 pool 的场景是侧重 IO 的操作,IO 操作的一个特性是需要有较长的等待时间,那我们就可以为了提高吞吐量,而适当的调大 pool active size(反正大家就一起等等咯),这和线程池的的 maximum pool size 有着异曲同工之处。那调大至多少合适呢?可以根据这个接口调用情况,平均 QPS 是多少,峰值 QPS 是多少,rt 是多少等等元素,来调出一个合适的值,这一定是一个过程,而不是一次性决定的。那又有同学会问了,我是一个新接口,我不知道历史数据怎么办呢?对于这种情况,如果条件允许的话,使用压测是一个不错的办法。根据改变压测条件,来调试出一个相对靠谱的值,上线后对其观察,再决定是否需要调整。

4.2 dubbo 线程池

在本案中,对于这个线程池的问题有两个,队列长度与拒绝策略。队列长度的问题显而易见,一个应用的负载能力,是可以通过各种手段衡量出来的。就像我们去餐厅吃饭一样,顾客从上桌到下桌的平均时间(rt)是已知的,餐厅一天存储的食物也是已知的(机器资源)。当餐桌满了的时候,新来的客人需要排队,如果不限制队列的长度,一个餐厅外面排上个几万人,队列末尾的老哥好不容易轮到了他,但他已经饿死了或者餐厅已经没吃的了。这个时候,我们就需要学会拒绝。可以告诉新来的客人,你们今天晚上是排不上的,去别家吧。也可以把那些吃完了,但是赖在餐桌上聊天的客人赶赶走(虽然现实中这么挺不礼貌,但也是一些自助餐限时2小时的原因)。回到本案,如果我们调低了队列的长度,增加了适当的拒绝策略,并且可以把长时间排队的任务移除掉(这么做有一定风险),可以一定程度的提高系统恢复的速度。

最后补一句,我们在使用一些第三方工具包的时候(就算它是 spring 的),需要了解其大致的实现,避免因参数设置不全,带来意外的“收获”。

JVM

http://blog.cuzz.site/2019/05/10/JVM%E9%9D%A2%E8%AF%95/

GC Roots

  1. 虚拟机栈中引用的对象
  2. 本地方法栈中引用的对象
  3. 方法区中类静态属性引用的对象
  4. 方法区中常量引用的对象

JVM参数


## 两个经典参数:-Xms 和 - Xmx(如 -Xms1024m)
-Xms 等价于 -XX:InitialHeapSize
	* 初始大小内存,默认为物理内存 1/64
-Xmx 等价于 -XX:MaxHeapSize
	* 最大分配内存,默认为物理内存的 1/4
-Xss
	* 设置单个线程栈的大小,一般默认为 512-1024k
	* 等价于 -XX:ThreadStackSize
-Xmn
	* 设置年轻代的大小
	* 整个JVM内存大小 = 年轻代大小 + 年老代大小 + 持久代大小,持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
	
-XX:MetaspaceSize
	* 设置元空间大小(元空间的本质和永久代类似,都是对 JVM 规范中的方法区的实现,不过元空间于永久代之间最大区别在于,元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受本地内存限制)
元空间默认比较小,我们可以调大一点
	* Java 8 之后的版本使用元空间(Metaspace)代替了永久代,元空间是方法区在 HotSpot 中的实现,它与持久代最大的区别是:元空间并不在虚拟机中的内存中而是使用本地内存。
        1)元空间存放的信息:
        2)虚拟机加载的类信息
        3)常量池
        4)静态变量
        5)即时编译后的代码

-XX:+PrintGCDetails
	* 输出详细 GC 收集日志信息
	
-XX:SurvivorRatio
	* 设置新生代中 eden 和 S0/S1 空间比例
	* 默认 -XX:SurvivorRatio=8,Eden : S0 : S1 = 8 : 1 : 1
-XX:NewRatio
	* 配置年轻代和老年代在堆结构的占比
	* 默认 -XX:NewRatio=2 新生代占1,老年代占2,年轻代占整个堆的 1/3
-XX:MaxTenuringThreshold
	* 设置垃圾最大年龄

对象引用

除了基本数据类型外,其他的都是指向各类对象的对象引用;Java中根据其生命周期的长短,将引用分为4类。

  • 强引用、软引用、弱引用和虚引用

OOM

  • java.lang.StackOverflowError
    • 递归调用
  • java.lang.OutOfMemoryError : Java heap space
    • new 一个很大对象
  • java.lang.OutOfMemoryError : GC overhead limit exceeded
    • 执行垃圾收集的时间比例太大, 有效的运算量太小,默认情况下,如果GC花费的时间超过 98%, 并且GC回收的内存少于 2%, JVM就会抛出这个错误。
  • java.lang.OutOfMemoryError : Direct buffer memory
  • 创建线程数太多了 : java.lang.OutOfMemoryError : unable to create new native thread
  • java.lang.OutOfMemoryError : Metaspace

GC算法

  • 四种 GC 垃圾回收算法
    • 引用计数
    • 复制回收
    • 标记清除
    • 标记整理
  • GC 算法是内存回收的方法论,垃圾收集就是算法的落实的实现。
  • 目前为止还没有完美的收集器的出现,更加没有万能的收集器,只是针对具体应用最适合的收集器,进行分代收集。
  • 串行垃圾回收器(Serial)
    • 它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程,所以不适合服务环境。
  • 并行垃圾回收器(Parallel)
    • 多个垃圾收集线程并行工作,此时用户线程是暂停的,用于科学计算、大数据处理等弱交互场景。
  • 并发垃圾回收器(CMS)
    • 用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程,互联网公司多用它,适用对响应时间有要求的场景。
  • G1 垃圾回收器
    • G1 垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。
    • Region 区域化垃圾收集器:最大好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可。

JVM工作的模式

-Xint, -Xcomp, 和 -Xmixed

-Xmixed代表混合模式(mixed mode),前面也提到了,混合模式是JVM的默认工作模式。它会同时使用编译模式和解释模式。对于字节码中多次被调用的部分,JVM会将其编译成本地代码以提高执行效率;而被调用很少(甚至只有一次)的方法在解释模式下会继续执行,从而减少编译和优化成本。JIT编译器在运行时创建方法使用文件,然后一步一步的优化每一个方法,有时候会主动的优化应用的行为。这些优化技术,比如积极的分支预测(optimistic branch prediction),如果不先分析应用就不能有效的使用。这样将频繁调用的部分提取出来,编译成本地代码,也就是在应用中构建某种热点(即HotSpot,这也是HotSpot JVM名字的由来)。使用混合模式可以获得最好的执行效率。

-Xint代表解释模式(interpreted mode),-Xint标记会强制JVM以解释方式执行所有的字节码,当然这会降低运行速度,通常低10倍或更多。

-Xcomp代表编译模式(compiled mode),与它(-Xint)正好相反,JVM在第一次使用时会把所有的字节码编译成本地代码,从而带来最大程度的优化。这听起来不错,因为这完全绕开了缓慢的解释器。然而,很多应用在使用-Xcomp也会有一些性能损失,但是这比使用-Xint损失的少,原因是-Xcomp没有让JVM启用JIT编译器的全部功能。因此在上图中,我们并没有看到-Xcomp比-Xmixed快多少。

在都使用Client JVM的前提下,混合模式下,平均耗时150ms,然而在解释模式下,平均耗时超过1600ms,这基本上是10倍以上的差距。

Server/Client 类型

  • 最主要的差别在于:-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。
  • 当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器,C2比C1编译器编译的相对彻底,服务起来之后,性能更高。
  • 所以通常用于做服务器的时候我们用服务端模式,如果你的电脑只是运行一下java程序,就客户端模式就可以了。当然这些都是我们做程序优化程序才需要这些东西的,普通人并不关注这些专业的东西了。其实服务器模式即使编译更彻底,然后垃圾回收优化更好,这当然吃的内存要多点相对于客户端模式。

服务器变慢诊断思路

  • 整机:top
  • CPU:vmstat
  • 内存:free
  • 硬盘:df
  • 磁盘IO:iostat
  • 网络IO:ifstat

CPU过高如何定位?

  • 先用 top 命令找出 CPU 占比最高的
  • ps -ef 或者 jps 进一步定位,得知是一个怎么样的一个后台程序
  • 定位到具体的线程或代码
    • ps -mp 11111 -o THREAD,tid,time
    • -m 显示所有的线程
    • -p 进程使用cpu的时间
    • -o 该参数后是用户自定义格式
  • 将需要的线程 ID 转化为 16 进制格式
  • jstat <进程ID> | grep <线程ID(16进制)> -A60

this逃逸分析

  • this逃逸经常发生在构造函数中启动线程或注册监听器时

  • this逃逸是指当一个对象还没有完成构造(构造方法尚未返回)的时候,其他线程就已经可以获得到该对象的引用,并可以通过该引用操作该对象

解决方案

知道了造成this逃逸的原因,那么解决方案也就显而易见,总的原则是:不要在对象的构造方法中使this引用逃逸

对一个对象来说,被正确的构造是什么意思呢?简单来说,它意味着这个正在构造的对象的引用在构造期间没有被允许逸出。(参见安全构造技术)。换句话说,不要让其他线程在其他地方能够看见一个构造期间的对象引用。不要指派给一个静态字段,不要作为一个listener注册给其他对象等等。这些操作应该在构造方法之后完成,而不是构造方法中来完成。

class FinalFieldExample {
  final int x;
  int y;
  static FinalFieldExample f;
  public FinalFieldExample() {
    x = 3;
    y = 4;
  }

  static void writer() {
    f = new FinalFieldExample();
  }

  static void reader() {
    if (f != null) {
      int i = f.x;
      int j = f.y;
    }
  }
}
/*
上面的类展示了final字段应该如何使用。一个正在执行reader方法的线程保证看到f.x的值为3,因为它是final字段。它不保证看到f.y的值为4,因为f.y不是final字段。如果FinalFieldExample的构造方法像这样:

*/

public FinalFieldExample() { // bad!
x = 3;
y = 4;
// bad construction - allowing this to escape
global.obj = this;
}

/*

那么,从global.obj中读取this的引用线程不会保证读取到的x的值为3。

能够看到字段的正确的构造值固然不错,但是,如果字段本身就是一个引用,那么,你还是希望你的代码能够看到引用所指向的这个对象(或者数组)的最新值。如果你的字段是final字段,那么这是能够保证的。因此,当一个final指针指向一个数组,你不需要担心线程能够看到引用的最新值却看不到引用所指向的数组的最新值。重复一下,这儿的“正确的”的意思是“对象构造方法结尾的最新的值”而不是“最新可用的值”。

现在,在讲了如上的这段之后,如果在一个线程构造了一个不可变对象之后(对象仅包含final字段),你希望保证这个对象被其他线程正确的查看,你仍然需要使用同步才行。例如,没有其他的方式可以保证不可变对象的引用将被第二个线程看到。使用final字段的程序应该仔细的调试,这需要深入而且仔细的理解并发在你的代码中是如何被管理的。

如果你使用JNI来改变你的final字段,这方面的行为是没有定义的。

*/

错误说法:如果在一个线程构造了一个不可变对象之后(对象仅包含final字段),就可以保证了这个对象被其他线程正确的查看

栈分配

​  逃逸分析是目前Java虚拟机中比较前沿的优化技术,它并不是直接优化代码的手段,而是为其他优化手段提供依据的分析技术。
逃逸分析的主要作用就是分析对象作用域。
 当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种行为就叫做 方法逃逸。甚至该对象还可能被外部线程访问到,例如赋值被类变量或可以在其他线程中访问的实例变量,称为 线程逃逸
 通过逃逸分析技术可以判断一个对象不会逃逸到方法或者线程之外。根据这一特点,就可以让这个对象在栈上分配内存,对象所占用的内存空间就可以随帧栈出栈而销毁。在一般应用中,不会逃逸的局部对象所占比例很大,如果能使用栈上分配,那么大量的对象就会随着方法的结束而自动销毁了,垃圾收集系统的压力就会小很多。

除此之外,逃逸分析的作用还包括 标量替换同步消除 ;
标量替换 指:若一个对象被证明不会被外部访问,并且这个对象可以被拆解成若干个基本类型的形式,那么当程序真正执行的时候可以不创建这个对象,而是采用直接创建它的若干个被这个方法所使用到的成员变量来代替,将对象拆分后,除了可以让对象的成员变量在栈上分配和读写之外,还可以为后续进一步的优化手段创造条件。
同步消除 指:若一个变量被证明不会逃逸出线程,那么这个变量的读写就肯定不会出现竞争的情况,那么对这个变量实施的同步措施也就可以消除掉。
  说了逃逸分析的这些作用,那么Java虚拟机是否有对对象做逃逸分析呢?

答案是否。

关于逃逸分析的论文在1999年就已经发表,但直到Sun JDK 1.6才实现了逃逸分析,而且直到现在这项优化尚未足够成熟,仍有很大的改进余地。不成熟的原因主要是不能保证逃逸分析的性能收益必定高于它的消耗。因为逃逸分析本身就是一个高耗时的过程,假如分析的结果是没有几个不逃逸的对象,那么这个分析所花费时候比优化所减少的时间更长,这是得不偿失的。
 所以目前虚拟机只能采用不那么准确,但时间压力相对较小的算法来完成逃逸分析。还有一点是,基于逃逸分析的一些优化手段,如上面提到的“栈上分配”,由于HotSpot虚拟机目前的实现方式导致栈上分配实现起来比较复杂,因此在HotSpot中暂时还没有做这项优化
事实上,在java虚拟机中,有一句话是这么写的:

The heap is the runtime data area from which memory for all class instances and arrays is allocated。
堆是所有的对象实例以及数组分配内存的运行时数据区域。

所以,忘掉Java栈上分配对象内存的想法吧,至少在目前的HotSpot中是不存在的。也就是说Java的对象分配只在堆上。

公司面试问题集锦

平安基础面试题

项目中的技术难点
1.如果急先问同事,圈内朋友
2.google,github,oschina
3.如果还不能解决,向上级报告,寻求替代方案

1.首先我认为分歧是学习的机会
2.正在情绪上的时候不轻易做决定,或者人(格)身(体)攻击对方
3.如果事情发生在其它同事身上,不点火,不吹风
4.避免:尽量不把生活上情绪带到工作中
5.如果是技术上的分歧,很多时候是件好事,不损害公司利益的前提下 我觉得可以继续讨论下去
6.最后想说不好的可能:如果实在不能调解(公司或个人原则问题),可以请求规避(换部门,换环境)

产生背景 -> * 使用场景 -> 大致原理 -> hello world demo * -> 同类型技术比较 * -> 交流运用 -> 剖析原理 * -> 模拟实现 *
*号表示:学习笔记 + 分享

压力主要来自于个人诉求与自身状态不平衡 ,工作与生活不平衡
排解方式: 运动调节 + 先降低要求 + 进步式学习调节

1.如果我是测试,我必须向开发伙伴或者上级说。公司形象由其它部门负责
2.如果是责任开发,就看能不能及时解决bug
3.自己所在团队解决不了,请求外援
4.启用紧急事故处理方式

hash映射表,底层实现为 数组+链表
数组可通过 加载因子*元素个数 扩容
链表解决hash冲突,且基于其长度进行 链表和红黑树的转换

多态:
运行时程序体现的多种形态的结果,产生于子父类(包括实现)之间
好处:代码复用,扩展性,依赖抽象 降低耦合

读未提交 (会产生脏读)
读已提交(不可重复读)
可重复读(幻读)
串行化(性能影响极大,有范围锁不会产生以上问题)

1.是否记录log ,truncate不会记录,不适用于可修复的操作
2.delete会保留业务自增id ,truncate不会

1.索引的字段能带来性能提升
2.区分度
3.最少需要原则(因为索引也要占用空间)
4.mysql基于业务和explain分析 再建立
5.索引不是越多越好

乐观锁:修改数据时,认为别人不会修改(版本号,cas)
悲观锁:for update ,每次修改时都加锁,防止别人修改 (写>读时 这样做也挺好)

ps -ef|grep xxx
ls,cat ,find,vi ,cd ,mkdir,pwd,chmod,chown
pkill -f
yum,rpm

tomcat的jvm调优其实就是启动命令行中的优化参数,JAVA_OPTS,CATALINA_OPTS ,
配置 -server方式
根据主机内存和业务规格 配置栈区,新生代,等jmm内存区域的起始和最大内存
配置GC等相关参数

fastjson,dbutils,okhhtp,lombok,dom4j ,apache poi,netty

接口:可以实现多继承,完全抽象的类,可以什么都不干 就起一个标识作用
抽象类:一般可以做为接口的默认实现类,定义模板方法,实现通用业务方法,约束子类行为

1.加载驱动:forname(driver.class) |set property jdbc.driver | registerDriver |
2.获取连接:getconnection(host-port,user,pwd)
3.创建会话:statement,preparestatment,callableStatement
4.执行ddl,dml ,callable
5.根据result处理 // todo

对于高级岗位本人自知还有很多知识欠缺(不够深入),但这是我以后工作学习努力的方向
1.工作十分细心,能做实事
2.乐于沟通分享
3.渴望加入一个优秀的平台,和优秀的人共事,共同进步!!

丰巢科技

技术问题:
1.bio与nio的区别
2.select与poll的区别
多路复用模型来实现并发服务程序
epoll 事件驱动机制

6.线程池的参数配置,为什么java官方提供工厂方法给线程池
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
maximumPoolSize :
线程池中允许的最大线程数。线程池的阻塞队列满了之后,如果还有任务提交,
如果当前的线程数小于 maximumPoolSize,则会新建线程来执行任务。注意,如果使用的是无界队列,该参数也就没有什么效果了
keepAliveTime:线程空闲的时间,默认情况下,该参数只有在线程数大于corePoolSize时才会生效
RejectedExecutionHandler : 线程池的拒绝策略,饱和策略

FixedThreadPool SingleThreadExecutor 无界队列LinkedBlockingQueue
corePoolSize == maximumPoolSize都设置为创建FixedThreadPool时指定的参数nThreads
由于FixedThreadPool使用的是“无界队列”LinkedBlockingQueue,那么maximumPoolSize参数无效,
同时指定的拒绝策略AbortPolicy也将无效。而且该线程池也不会拒绝提交的任务,如y果客户端提交任务的速度快于任务的执行,那么keepAliveTime也是一个无效参数。

CachedThreadPool corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE
如果主线程提交任务的速度远远大于CachedThreadPool的处理速度,则CachedThreadPool会不断地创建新线程来执行任务,这样有可能会导致系统耗尽CPU和内存资源,
所以在使用该线程池是,一定要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题。

7.分布式框架dubbo的好处,不用dubbo可不可以。为什么要使用分布式

http://www.cnblogs.com/shan1393/p/9338530.html

8.七个垃圾回收器之间如何搭配使用
9.接口限流方案
redis url_userid , count ,timeout
令牌桶法
信号量
漏斗

注解+interceptor
通用到每个方法限流

10.ConcurrentHashMap使用原理
分段锁 ConcurrentHashMap的主干是个Segment数组 Segment继承了ReentrantLock
一个Segment维护着一个HashEntry数组 transient volatile HashEntry<K,V>[] table;

11.解决map的并发问题方案
HashMap :先说HashMap,HashMap是线程不安全的,在并发环境下,
可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),
导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的

使用synchronize ,或者使用collection.synchronizeXXX方法。或者使用concurrentHashmap来解决

12.什么是协程,以及实现要点

* 历史上是先有协程,是OS用来模拟多任务并发,(有点像以前军队里的旅长)
但是因为它是非抢占式的,导致多任务时间片不能公平分享,所以后来全部废弃了协程改成抢占式的线程。

假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A
看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
* 最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,
线程数量越多,协程的性能优势就越明显。(因为线程多会有切换开销)
* 第二大优势就是不需要多线程的锁机制

用户态线程是协作式的,即是协程。

Java由于还没有官方的协程支持
实际上Java中还是有一些第三方的协程实现是可以用的,在对非阻塞IO依赖很高的业务中可以考虑尝试一下,目前可用性比较高的有Quasar和ea-async.

这些方案基本上都是通过byte code Instrument,把编译后同步程序class文件修改为异步的操作。
这里面有两个关键因素,一个是现场(主要是stack, 操作数栈)的保存和恢复,一个是能够在某些点暂停执行和重入执行。
其中前者通过Instrument 字节码进去很容易修改,后者虽然JVM中也有PC(程序计数器)的概念,但是却不能通过字节码修改,
所以一般都要更麻烦一些,用一个状态机来实现,退出时保存状态,重入时根据状态跳转到不同的代码位置。
Instrument可以在编译后来做(如果用maven配置个maven插件就可以),在可以在启动的之后,加在agent来做。

13.lru cash 使用hash map 的实现(算法)
14.图的深度遍历和广度遍历(算法)
15.基本排序(算法)
16.设计模式的使用
TransactionTemplate,其实是模板+策略的双剑合璧。
针对不同的厂商,只需要提供不同的PlatformTransactionManager实现即可。
比如对于MyBatis,就用DataSourceTxManager,对于Hibernate,就用HibernateTxManager:

17.java 8 流式使用
18.java的对象一致性
19.操作系统的读写屏障
20.java 域的概念
21.分布式设计领域的概念
22.如何实现双11的购物限流(redis实现方案)
限流 ,公式验证码,ip(key) + 请求次数

23.mysql调优
安全 remoteuser root用户 ,专门的dba管理 rds

cpu,io,网络,大事务,慢查询,大表

cpu:max_connections = 100 ,可以设置大点
io: 搞活动时主库上的备份切换到从库上进行
网络:业务和服务网络分开
大表:分库分表,数据归档
大事务:分批处理

慢查询日志收集
Slow_query_log : on 
Slow_query_log_file  指定日志存储路径
Long_query_time 指定记录慢查询sql执行的时间阈值 (默认为10s 但是太长了) 
Log_queries_not_using_indexes 是否记录未使用索引的sql

24.cdn(异地多活)
25.进程之间的通信方式
管道(pipe):管道是一种半双工的通信方式

不同机器间:
套接字(socket):套接口也是一种进程间的通信机制,与其他通信机制不同的是它可以用于不同机器间的进程通信。
http ,soap ,rpc
26.tcp/ip协议
27.http协议
28.二级交换器传输协议
29.spring 事务的传播
事务方法发生嵌套调用时,事务如何进行传播,
即协调已经有事务标识的方法之间发生调用时的事务上下文的规则(是否要有独立的事务隔离级别和锁)

除了事务的传播行为外,事务的其他特性Spring是借助底层资源的功能来完成的,Spring无非只充当个代理的角色。但是事务的传播行为却是Spring凭借自身的框架提供的功能 ;

Spring*事务传播属性*:

所谓事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring支持以下7种事务传播行为

Spring事务传播属性:

1.propagation-required: 支持当前事务,如果有就加入当前事务中;如果当前方法没有事务,就新建一个事务;

2.propagation-supports: 支持当前事务,如果有就加入当前事务中;如果当前方法没有事务,就以非事务的方式执行;
3.propagation-mandatory: 支持当前事务,如果有就加入当前事务中;如果当前没有事务,就抛出异常;

4.propagation-requires_new: 新建事务,如果当前存在事务,就把当前事务挂起;如果当前方法没有事务,就新建事务;
5.propagation-not-supported: 以非事务方式执行,如果当前方法存在事务就挂起当前事务;如果当前方法不存在事务,就以非事务方式执行;
6.propagation-never: 以非事务方式执行,如果当前方法存在事务就抛出异常;如果当前方法不存在事务,就以非事务方式执行;
7.propagation-nested: 如果当前方法有事务,则在嵌套事务内执行;如果当前方法没有事务,则与required操作类似;
前六个策略类似于EJB CMT,第七个(PROPAGATION_NESTED)是Spring所提供的一个特殊变量。

它要求事务管理器或者使用JDBC 3.0 Savepoint API提供嵌套事务行为(如Spring的DataSourceTransactionManager)

在一个Service内部,普通方法和事务方法之间的嵌套调用,不会开启新的事务.

设:serviceA调用serviceB()
当 serviceA()运行在一个事务下(如设置为PROPAGATION_REQUIRED),
而 serviceB()设置为 PROPAGATION_NESTED时,如果底层数据源支持保存点,Spring将为内部的 serviceB()方法产生的一个内嵌的事务。
如果 serviceB()对应的内嵌事务执行失败,事务将回滚到 serviceB()方法执行前的点,并不会将整个事务回滚。
内嵌事务是外层事务的1部分,	所以只有外层事务提交时,嵌套事务才能一并提交。
嵌套事务不能够提交,它必须通过外层事务来完成提交的动作,外层事务的回滚也会造成内部事务的回滚。

当serviceB()设置为 PROPAGATION_REQUIRES_NEW  时启动一个新的、和外层事务无关的“内部”事务。
该事务拥有自己的独立隔离级别和锁,不依赖于外部事务,独立地提交和回滚。
当内部事务开始执行时,外部事务 将被挂起,内务事务结束时,外部事务才继续执行。

由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于:
PROPAGATION_REQUIRES_NEW 将创建一个全新的事务,它和外层事务没有任何关系,
而嵌套事务不能够提交,它必须通过外层事务来完成提交的动作,外层事务的回滚也会造成内部事务的回滚

30.spring事务
了解完Spring是如何实现统一的事务模型,不知道你是否也有疑问:既然是事务,那就要保证事务里的所有dao操作,
都要使用同一个数据库连接进行操作,但是我们在写代码的时候,并不需要给dao传入connection对象:
Spring又是怎么做到的?
答案是ThreadLocal。

通过ThreadLocal,在同一个线程中共享connection。

这很好理解,关键是,这是一个什么样的ThreadLocal?填空题。
ThreadLocal<Map<Object,Object>>
实际上,Spring在ThreadLocal里头,放的是一个Map。key是dataSource,value才是connection.

如何新开一个事务
Spring是支持在事务里面新开一个事务的,最简单的方式就是使用声明式事务模型:PROPAGATION_REQUIRES_NEW
@Transactional((propagation = Propagation.REQUIRES_NEW))

然而,按照之前的理论,如果每次都是从ThreadLocal里去获取connection,那么永远拿到的都是旧的事务,不会创建新事务。

Spring又是如何实现新开事务的呢?

很简单,链表。
1.一开始,旧事务绑定在当前线程
2.当需要新开事务时,先将原来的事务解绑
3.然后new一个新的事务,接着将新的事务指向旧事务  new conn -> old conn
4.最后将新事务绑定到当前线程 thread -> [ new conn -> old conn ]
之所以需要将新事务指向旧事务,形成一个事务链,是因为新事务在提交或者回滚之后,还需要恢复旧事务

总结一下:

	Spring如何实现统一的事务模型:Template + Strategy
	如何在方法间共享Connection:ThreadLocal
	如何挂起和恢复线程:链表
	提到的类:
	TransactionTemplate 事务模板
	PlatformTransactionManager 事务操作策略接口
	AbstractPlatformTransactionManager 事务操作策略抽象类
	DataSourceTxManager 具体策略,适用于JDBC/MyBatis
	HibernateTxManager 具体策略,适用于Hibernate
	TxSynManager 事务同步管理器,在线程中同步数据库连接等信息
	DataSourceUtils 数据库操作Utils


29.分布式下down机的处理方案(心跳检测)
30.分布式弱一致性下down机的处理方案

1、内存泄漏memory leak :是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
2、内存溢出 out of memory :指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,
但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。
3、二者的关系

内存泄漏的堆积最终会导致内存溢出

内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。

内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),
而系统也不能再次将它分配给需要的程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之后,把钥匙丢了或者没有将钥匙还回去,
那么结果就是这个柜子将无法供给任何人使用,也无法被垃圾回收器回收,因为找不到他的任何信息。

内存溢出:一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出。
比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。
就是分配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了那么多,那我就报错,

mapper文件中要定位到sql,需要两个条件,一个是namespace,一个是sql id.要想用这种mapper接口的方式调用也必须遵循一个约定,
那就是namespace等于接口的权限定名.接口的方法名等于xml文件中的sql id,这就是为什么图中封装MapperMethod的时候,需要把这两个传进去的原因.
确定了sql,传入参数mapperMethod.execute(args),拼接成一条完成sql,执行之


本文来自 糖妹儿 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/ruiruihahaha/article/details/70270574?utm_source=copy

https://mp.weixin.qq.com/s?__biz=MjM5NTA0Njc0NA==&mid=2654513296&idx=2&sn=00ab1f6949df31ad7696f78d59aec6af&chksm=bd327ca78a45f5b17c4bb298c6070c7ff5127ce11e9cf4f1d344d1fe7269aed3b7ada304cfb7&mpshare=1&scene=23&srcid=0522SYdOYWe2XCIfKEGv1dnR#rd

顺丰科技

  • 订单系统对接的设计模式

  • 支付网关

    • 支付路由,支付渠道, (签约,代扣,验证,支付,风控,查询,回调)
    • 分支模式是聚合模式的扩展,可以允许同时调用两个或者更多的微服务。
    • 如上,采用分支模式, 使得数据校验和风控可以并发执行。由于风控相对耗时较长,而订单中需要校验的数据较多,这两个操作有必要并发执行。
  • 跨职能团队

  • mq 用本地事务实现落地 也会有瓶颈怎么处理

    • 本地事务实现落地(为了可靠投递)
  • 落地redis ?

  • 多平台扩展

  • 分片策略

    • 商家 (多平台+代理点+互联网)
  • 可靠投递

    • 生产者到broker(exange) - confirmCallback

    • 生产者exange->queue returnCallback(没存到queue的情况触发)

    • 消费者手动ack(签收)

      • channel.basicAck
      • channel.basicNack channel.basicReject
  • mq消费时出现异常怎么处理

    • 消费者瓶颈可以扩展多个服务,加多线程

    • 出现异常:

      • catch到之后记录到表中,然后照常ack,最后统一另外处理这些失败的消息

      • 手工处理异常ack

      • 死信队列

  • mq消费堆积

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

    • 假设1万个订单积压在mq里面,没有处理,其中1000个订单都丢了,你只能手动写程序把那1000个订单给查出来,手动发到mq里去再补一次。
  • TCP

  • jvm 老年代,GC,线上问题

百丽国际新零售

  • 10个平台3个店铺5个业务 (横向扩展,故障转移)
    来源平台*业务(订单,支付,库存,优惠券)
    假如天猫订单量大 可以横向扩展天猫平台的服务

  • 百丽后面喜欢问业务线 ,流程故障处理,大数据量下的扩展性方案这些

  • 线程池执行步骤

  • 订单逆向流程

宝能国际WMS

* 编译步骤
* aof重写过程
* redis主从同步过程
* redis字符串
* netty buffer  
* 事务传播特性及适合场景
* rocketmq对nettty有哪些封装
* 逃逸分析

喜茶

  • nio
    • netty channel 池化
  • 线程池
    • 线程预热
    • tomcat对线程池的优化
  • spring事务
    • 怎么让spring事务失效
  • zk redis
  • 库存超卖
  • 消息可靠投递
  • 秒杀设计
  • 分库分表
    • 雪花算法
  • 覆盖索引
    • 单值or 复合索引,name+age 储存数据,name,age分开建立索引要不要回表
  • 冷热数据处理
  • 工作突出表现

阿里面试

UNKONW

  • HashMap 为什么使用红黑树而不使用平衡树?(菜鸟)

  • 6大设计原则,什么是高内聚低耦合(菜鸟)

  • 衡量垃圾收集器的指标有哪些,说说CMS收集器,CMS 为什么需要暂停 - stop the world?(菜鸟)

  • Dubbo 有哪些传输协议?(阿里)

  • 最短路径?(阿里)

  • HTTPS 加密过程,是怎么保证信息可靠的?(头条)

    • 非对称加密(协商过程进行加密)

    新问题来了,如何对协商过程进行加密?密码学领域中,有一种称为“非对称加密”的加密算法,特点是私钥加密后的密文,只要是公钥,都可以解密,但是公钥加密后的密文,只有私钥可以解密。私钥只有一个人有,而公钥可以发给所有的人

    • 协商什么加密算法

    要达到Web服务器针对每个客户端使用不同的对称加密算法,同时,我们也不能让第三者知道这个对称加密算法是什么,怎么办?

    使用随机数,就是使用随机数来生成对称加密算法。这样就可以做到服务器和客户端每次交互都是新的加密算法、只有在交互的那一该才确定加密算法。这下,你明白为什么HTTPS协议握手阶段会有这么多的随机数了吧。

    • 如何得到公钥?

    细心的人可能已经注意到了如果使用非对称加密算法,我们的客户端A,B需要一开始就持有公钥,要不没法开展加密行为啊。

    这下,我们又遇到新问题了,如何让A、B客户端安全地得到公钥?

    我能想到的方案只有这些:

    方案1. 服务器端将公钥发送给每一个客户端

    方案2. 服务器端将公钥放到一个远程服务器,客户端可以请求得到

    我们选择方案1,因为方案2又多了一次请求,还要另外处理公钥的放置问题。

      * 显然,让每个客户端的每个浏览器默认保存所有网站的公钥是不现实的。
    

    HTTPS要使客户端与服务器端的通信过程得到安全保证,必须使用的对称加密算法,但是协商对称加密算法的过程,需要使用非对称加密算法来保证安全,然而直接使用非对称加密的过程本身也不安全,会有中间人篡改公钥的可能性,所以客户端与服务器不直接使用公钥,而是使用数字证书签发机构颁发的证书来保证非对称加密过程本身的安全。这样通过这些机制协商出一个对称加密算法,就此双方使用该算法进行加密解密。从而解决了客户端与服务器端之间的通信安全问题。

  • Dubbo 有哪些负载均衡策略?(头条)

咸鱼游戏

  • redis排行版,评论点赞相同的根据时间怎么排
  • 接到一个需求怎么办,怎么和客户经理有效沟通,怎么可靠交付
  • 自己实现断点续传

职业规划

T型人才,技术专家的深度,知识面的广度
工作者的角度:确定地点、确定行业、确定公司、确定老板
程序员的角度:技术专家,管理岗

职业规划还涉及到「技术栈」的选择。究竟要怎么选择「技术栈」才能让自己的「收益最大化」呢?
“林恩效应”
这一点我参考「黑客与画家」中的说法,如果这个技术是短期的、昙花一现的,几年后就会被抛弃的,那么往往学习的收益不会高。
但是,如果这个技术是在若干年后依然有用武之地的,例如很多「长青」的算法,那么,学习它们绝对是有收益的。

做人

做人比会做事还要重要百倍。找到一个合适的职业目标,对于很多迷茫中的人来说可能并不是容易的事;
但学会做人,则是你在任何公司、任何时候都可以修炼的一种职业素养。
在做人的修炼中,有四项要素,是能够对你以后的职业发展起到致命性影响的:
1)你的人际关系处理能力
2)不断学习的能力
3)树立职业化精神
4)要有强大的内心

完成你的专业化品牌构建
1)知识结构

  • 在职学习、进修
    2)就是树立自己在行业内的影响力
  • 分享 ,文章

架构设计

DDD(领域驱动设计)

充血,贫血,失血模型

在面向事物脚本编程中所使用的对象就是我们今天的主角之一“贫血模型”,当然领域模型自然也就对应着“充血模型”。那么他们最大的区别在哪里呢?其实Marth Folwer的贫血模型一文中已明确说明贫血模型是一种反模式,它根本就不是面向对象的产物,违反面向对象的核心思想,而充血模型则契合面向对象的思想。

换句话说,贫血模型中只有属性,不存在领域操作,仅仅是被当做一种数据结构来使用。而充血模型有血有肉,既有属性,又有领域操作,贯彻面向对象的思想。

贫血模型

MVC模式中的pojo,entity

充血模型

https://blog.csdn.net/No_Endless/article/details/85067375

private static final long serialVersionUID = -4767597926507768285L;

private AccountRepository accountRepository;

private String accountNum;
private int totalAcount;
private String name;
private String cardNum;

public void deposit(int mony){

    if(mony <= 0)
        return;

    this.totalAcount = this.totalAcount + mony;
    accountRepository.updateAccount(this);
}

public void withdrawal(int mony){

    if(mony <= 0)
        return;
    if(mony > this.totalAcount)
        return;

    this.totalAcount = this.totalAcount - mony;
    accountRepository.updateAccount(this);
}

不知道大家看出来区别了没有,充血模型是有血有肉的,核心领域方法都放到模型去去,而不是把领域方法放到模型之上的service层中去。

引用别人对贫血模型和充血模型的总结(没记错的话应该是javaeye的robbin总结的):

对于Java来说,更加适合采用贫血的模型,Java比较适合于把一个复杂的业务逻辑分离到n个小对象中去,每个小对象描述单一的职责,n个对象 互相协作来表达一个复杂的业务逻辑,这n个对象之间的依赖和协作需要通过外部的容器例如IoC来显式的管理。但对于每个具体的对象来说,他们毫无疑问是贫血的。

这种贫血的模型好处是:

1、每个贫血对象职责单一,所以模块解藕程度很高,有利于错误的隔离。

2、非常重要的是,这种模型非常适合于软件外包和大规模软件团队的协作。每个编程个体只需要负责单一职责的小对象模块编写,不会互相影响。

贫血模型的坏处是:

1、由于对象状态和行为分离,所以一个完整的业务逻辑的描述不能够在一个类当中完成,而是一组互相协作的类共同完成的。因此可复用的颗粒度比较 小,代码量膨胀的很厉害,最重要的是业务逻辑的描述能力比较差,一个稍微复杂的业务逻辑,就需要太多类和太多代码去表达(针对我们假定的这个简单的工时管 理系统的业务逻辑实现,ruby使用了50行代码,但Java至少要上千行代码)。

2、对象协作依赖于外部容器的组装,因此裸写代码是不可能的了,必须借助于外部的IoC容器。

对于Ruby来说,更加适合充血模型。因为ruby语言的表达能力非常强大,现在用ruby做企业应用的DSL是一个很热门的领域,DSL说白了就是用来描述某个行业业务逻辑的专用语言。

充血模型的好处是:

1、对象自洽程度很高,表达能力很强,因此非常适合于复杂的企业业务逻辑的实现,以及可复用程度比较高。

2、不必依赖外部容器的组装,所以RoR没有IoC的概念。

充血模型的坏处是:

1、对象高度自洽的结果是不利于大规模团队分工协作。一个编程个体至少要完成一个完整业务逻辑的功能。对于单个完整业务逻辑,无法再细分下去了。

2、随着业务逻辑的变动,领域模型可能会处于比较频繁的变动状态中,领域模型不够稳定也会带来web层代码频繁变动。

好像在里面掺杂了好多东西,如果大家觉得罗嗦,可以看看这篇讲领域模型的文章,比我写的好,哈哈。链接在此:http://www.blogjava.net/GandofYan/archive/2006/05/30/48954.html

其实仅仅就这个例子的两种实现,还有太多太多的东西要去讲,但不是本篇的重点。学海无涯啊,领域驱动设计这方面的文章,在看书的过程中有任何感悟时我会陆续发到社区中。

参考资料:http://www.infoq.com/cn/articles/cjq-ddd

定义&why

DDD(Domain-Driver Design),TDD(测试驱动设计),数据驱动设计

  • 关注精简的业务模型及实现的匹配
  • 软件开发通常被应用到真实世界中已经存在的自动化流程,或者给真
    实的业务问题提供解决方案,即要自动化的业务流程或者可以用软
    件解决的现实问题。从一开始,我们就必需明白软件脱胎于领域,
    并跟领域密切相关
  • 为了创建一个好软件,你必须知道这个软件究竟是什么。在你充分
    了解金融业务是什么之前,你是做不出一个好的银行业软件系统
    的,你必须理解银行业的领域

DDD四层结构

四层定义 职责分配
用户界面、展现层 负责向用户展现信息
应用层 协调应用活动,不包含业务逻辑
领域层 包含领域的信息,业务软件的核心所在,保留业务对象的状态,依赖基础设施层(如持久化)
基础设施层 层间通信,业务对象持久化 其它层的支撑作用

模型-领域的抽象

  • 模型是软件设计中最基础部分
  • 我们需要建立领域的抽象。当我们跟领域专家交流时,我
    们会学到好多领域知识,但这些未加工的知识不能被容易地转换成
    软件构造,除非我们为它建立一个抽象——在脑海中建立一个蓝
    图。开始时,这个蓝图总是不完整的,但随着时间的推移,经过不
    断的努力,我们会让它越来越好,让它看上去越来越清晰。这个抽
    象是什么?它是一个模型,一个关于领域的模型。按照 Eric Evans
    的观点,领域模型不是一幅具体的图,它是那幅图要极力去传达的
    那个思想。它也不是一个领域专家头脑中的知识,而是一个经过严
    格组织并进行选择性抽象的知识。一幅图能够描绘和传达一个模
    型,同样,经过精心编写的代码和一段英语句子都能达到这个目
    的。模型是我们对目标领域的内部展现方式,它是非常必须的,会贯穿
    设计和开发的全过程。

领域建模法

  1. 用例分析法 (抽象)

  2. 4色建模

  3. 事件风暴

  4. 是一个不断迭代和探索的过程

  5. COLA 2.0框架 github开源了

数据驱动
  • 需求分析 - 数据建模(ER)- 建库建表(sql DAO) - 业务逻辑
DDD(领域模型驱动)
  • 领域划分 边界

  • 领域分析 - 领域模型 - 技术细节

  • 分离业务复杂度和技术复杂度

    • domain layer (纯粹)
    • 外围技术细节
    • controller application domain infrastructure(基础设施)

软件设计方法

模型可以理解成软件的架构,有了模型之后就可以开始代码细节设计了,代码设计建立在软件设计之上

瀑布式设计
  • 业务专家->业务分析人员-(创建模型)->开发人员
  • 是单一流程,没有反馈 。业务专家得不到分析人员的反馈信息,分析人员也得不到开
    发人员的反馈信息。
敏捷设计(XP 极限编程)
  • 产生背景是预先很难确定所有的需求,特别是需求经常变化的情况
  • 缺乏了真实可见的设计原则,由开发人员执行地持续重构会导致代码更难理解或者更难改

领域驱动原则

  • 设计和开发如何协同工作以创建一个更好的解决方案。优良的设计会加速开发的过程,而开发过程中的反馈也会进一步优化设计。
  • 领域驱动设计的一个核心的原则是使用一种基于模型的语言

通用语言

对通用语言的需要

为克服交流方式(业务专家和分析人员)的不同,在建立模型时,我们必须通过沟通来交换对模型和模型中涉及到的元素的想法,应该如何连接它们,哪些是有关的,哪些不是?在这种层次上的交流对一个成功的项目而言是极为重要的。如果一个人说了什么事情,其他的人不能理解,或者更糟的是错误理解成其他事情,又有什么机会来保证项目成功呢

UML 图在只涉及少量元素时很有帮助。但 UML 会像雨后的的蘑菇那样快速增长。当你的数以百计的类充斥在一张像密西西比河那样的长纸上时,你应该怎么办?即使是对软件专家而言,它也很难阅读,就更不用说领域专了。当它变大时,就会很难理解,一个中等规模项目的类图也会如此。同时,UML 擅长表现类,它们的属性和相互之间的关系。但类的行为和约束并不容易表现。为此 UML 求助于文字,将它们作为注释放在图上。所以 UML 不传达一个模型很重要的两个方面:它所要表现的概念的意义和对象准备做什么。但那依然很好,因为我们可以增加其他的沟通工具来做这些。

灰度发布|蓝绿发布

  • 蓝绿发布(资源要求高)
    3台老版本,3台新版本

  • 滚动发布
    假如有3台应用服务器需要更新,需要准备4台服务器(3台老版本,1台新版本),一次替换一台
    滚动发布能够解决掉蓝绿部署时对硬件要求增倍的问题。

  • 灰度发布(自动化要求高)

    也叫金丝雀发布(测试者)

在灰度发布开始后,先启动一个新版本应用,但是并不直接将流量切过来,而是测试人员对新版本进行线上测试,启动的这个新版本应用,就是我们的金丝雀。如果没有问题,那么可以将少量的用户流量导入到新版本上,然后再对新版本做运行状态观察,收集各种运行时数据,如果此时对新旧版本做各种数据对比,就是所谓的A/B测试。

当确认新版本运行良好后,再逐步将更多的流量导入到新版本上,在此期间,还可以不断地调整新旧两个版本的运行的服务器副本数量,以使得新版本能够承受越来越大的流量压力。直到将100%的流量都切换到新版本上,最后关闭剩下的老版本服务,完成灰度发布。

如果在灰度发布过程中(灰度期)发现了新版本有问题,就应该立即将流量切回老版本上,这样,就会将负面影响控制在最小范围内。

灰度发布就说金丝雀发布。就是实现了流量控制的滚动发布。
有一个金丝雀发布的具体实现flagger项目 基于istio的可以 具体了解一下。

三种方式均可以做到平滑式升级,在升级过程中服务仍然保持服务的连续性,升级对外界是无感知的。那生产上选择哪种部署方法最合适呢?这取决于哪种方法最适合你的业务和技术需求。如果你们运维自动化能力储备不够,肯定是越简单越好,建议蓝绿发布,如果业务对用户依赖很强,建议灰度发布。如果是K8S平台,滚动更新是现成的方案,建议先直接使用。

蓝绿发布:两套环境交替升级,旧版本保留一定时间便于回滚。

灰度发布:根据比例将老版本升级,例如80%用户访问是老版本,20%用户访问是新版本。

滚动发布:按批次停止老版本实例,启动新版本实例。

开发中的Session

可能有的问题

  • session超时

    一般存在服务中间件上(如:tomcat) ,server.session.timeout = 600 s

  • session并发控制

    一个人只能有一个会话,不让他在两个地方登录 。

    • 后面登录会话替换前面登录
    • 不让后面的登录,因为已经有活跃的会话存在

    implements SessionInformationExpiredStrategy

  • session集群管理

    不要在每个服务器上放置session,抽出来放在独立的存储上(专门的session服务器)

    spring session

    • storeType(Redis,MongoDB,JDBC,MAP …… )
    • Redis ,需要放置的对象都需要实现序列化(implements Serializable)

Zookeeper

https://www.douban.com/note/208430424/

CAP

这个理论的前提是: 分布式

  • Availability:可用性

    Every request receives a (non-error) response – without guarantee that it contains the most recent write

    我一定会给您返回数据,不会给你返回错误,但不保证数据最新,强调的是不出错

  • Consistency:一致性

    Every read receives the most recent write or an error

    对于任何从客户端发达到分布式系统的数据读取请求,要么读到最新的数据要么失败

    要么我给您返回一个错误,要么我给你返回绝对一致的最新数据,不难看出,其强调的是数据正确

  • Partition tolerance:分区容忍性

    The system continues to operate despite an arbitrary number of messages being dropped (or delayed) by the network between nodes

    分布式系统应该一直持续运行,即使在不同节点间同步数据的时候,出现了大量的数据丢失或者数据同步延迟

    我会一直运行,不管我的内部出现何种数据同步问题,强调的是不挂掉

  • 总结

    CAP is frequently misunderstood as if one has to choose to abandon one of the three guarantees at all times. In fact, the choice is really between consistency and availability only when a network partition or failure happens; at all other times, no trade-off has to be made.

    翻译如下:

    CAP经常被误解,好像一个人不得不选择放弃三个保证中的任何一个。实际上,只有在发生网络分区或故障时,才能在一致性和可用性之间做出选择;在其他任何时候,都不必进行权衡。

    对于一个分布式系统而言,P是前提,必须保证,因为只要有网络交互就一定会有延迟和数据丢失,这种状况我们必须接受,必须保证系统不能挂掉。

    试想一下,如果稍微出现点数据丢包,我们的整个系统就挂掉的话,我们为什么还要做分布式呢?所以,按照CAP理论三者中最多只能同时保证两者的论断,对于任何分布式系统,设计时架构师能够选择的只有C或者A,要么保证数据一致性(保证数据绝对正确),要么保证可用性(保证系统不出错)。

Nginx & Tomcat

  • 在工作方式上,Nginx分为单工作进程和多工作进程两种模式。在单工作进程模式下,除主进程外,还有一个工作进程,工作进程是单线程的;在多工作进程模式下,每个工作进程包含多个线程。Nginx默认为单工作进程模式。

静态资源如果让 Tomcat 处理的话 Tomcat 的性能会被损耗很多,所以我们一般都是采用:Nginx+Tomcat 实现动静分离

调优 Tomcat 线程池 配置Executor

NIO Tomcat8 以上版本,默认使用的就是NIO模式「非阻塞式 IO」。 APR 全称 Apache Portable Runtime,是Tomcat生产环境运行的首选方式,如果操作系统未安装 APR 或者 APR 路径未指到 Tomcat 默认可识别的路径,则 APR 模式无法启动,自动切换启动 NIO 模式。所以必须要安装 APR 和 Native,直接启动就支持 APR,APR是从操作系统级别解决异步 IO 问题,APR 的本质就是使用 JNI 技术调用操作系统底层的 IO 接口,所以需要提前安装所需要的依赖

JVM 调优 目的是最小化垃圾收集的时间,以在特定的时间内最大化处理客户的请求

在实际工作中,我们可以直接将初始堆大小(-Xms)与最大堆大小(-Xmx) 配置相等,这样的好处就是可以减少程序运行时垃圾回收的次数,从而提高效率。

新生代与老年代的设置比例:1:3 或者 1:4

Mybatis

mybatis延迟加载的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如用 a.getB().getName(),拦截器 invoke()方法发现 a.getB()是null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName()方法的调用。这就是延迟加载的基本原理。当然了,不光是 Mybatis,几乎所有的包括 Hibernate,支持延迟加载的原理都一样的。

消息队列

什么时候不使用MQ

  • 当上游实时关注执行结果
上游关注执行结果,但执行时间很长

(典型的是调用离线处理,或者跨公网调用),也经常使用回调网关+MQ来解耦。

  • https://www.w3cschool.cn/architectroad/architectroad-message-queue.html

  • eg. 举个栗子,微信支付,跨公网调用微信的接口,执行时间会比较长,但调用方又非常关注执行结果,此时一般怎么玩呢?

    上游需要支付的服务 --(调用)> wechat-pay-service -- (调用成功)->调用方

    wechat-pay-service -- (支付结果)-> http-gateway(网关)

    http-gateway(网关) --(发送保存结果)->MQ

    上游需要支付的服务 --> 订阅(支付结果)-->MQ

    一般采用“回调网关+MQ”方案来解耦:
    1)调用方直接跨公网调用微信接口
    2)微信返回调用成功,此时并不代表支付成功,只表示调用成功
    3)微信执行完成后,回调统一网关
    4)网关将返回结果通知MQ
    5)请求方收到结果通知

    这里需要注意的是,不应该由回调网关来调用上游来通知结果,如果是这样的话,每次新增调用方,回调网关都需要修改代码,仍然会反向依赖,使用回调网关+MQ的方案,新增任何对微信支付的调用,都不需要修改代码啦。

消息时序性

  • 在分布式消息队列里,有时候最需要做的就是恰恰不做,比如顺序性,我们只需要把强相关的两条消息基于相同的路由就行了,也就是说经过m1和m2的在路由表里的路由是一样的,那自然m1会优先于m2去投递。

  • 多个客户端消费时可能出现顺序性问题

  • 在消费端加入线程之后,就会出现顺序不一致的情况。

  • 高并发高性能的消息处理,如果有顺序依赖的消息,要保证消息有一个hashKey,类似于数据库表分区的的分区key列。保证对同一个key的消息发送到相同的队列。
    在此例中,A用户产生的消息(包括创建消息和删除消息)都按A的hashKey分发到同一个队列。这样再消费就可以保证业务顺序了。

  • 单队列多线程的架构,要想任务有序只能一个单一线程去处理你的有序任务,也就是说你往队列里扔的任务本身又是一个小队列,这个队列只能被一个线程执行。

  • 在消费端使用内存队列,队列里的数据使用 hash 进行分发,每个线程对应一个队列,这样可以保证数据的顺序

  • 假设我们要用N个线程处理消息队列,接收消息并处理的服务暂且称为ServiceA,ServiceA中维护N个队列(queue),在ServiceA的主线程中对微博ID取 %N 得到余数 X,

    根据X的值放入对应的队列(queue),每个线程只从一个队列(queue)中取元素并执行操作。

顺序消息放入同一Queue
  • 一般是通过对消息的ID进行hash来决定对应的消息队列

  • 缺点: 极端情况下,所有的消息都是针对这个记录(eg. 热点微博评论)都分发到同一个队列,而其它队列为空 。

    • 所以一般实践中,这种评论记录都会放到不同的队列中去
  • 但是如果评论中有依赖引用关系 ,eg . 对微博B1的评论处于队列A,对微博B2的评论处于队列B,但是后者引用了前者,所以必须保证前者首先被处理。

    • 假设消息M1的SN=100,消息M2的SN=110。为了保证消息M1在M2之前被处理,关键是,处理M2之前,必须先检查M1是否已经被处理,如果没有,就等待。因此有了方案二
其它队列加一个特殊消息

在将M1加入消息队列A时,同时对其它每一个队列加入一个特殊消息(Block Message,BM)。当某个消息队列处理BM时,检查M1是否已经被处理。如果M1已经被处理,则继续处理后面的消息。如果没有,则等待,直到M1被处理。

  • 缺点就是在M1加入时必须知道有其它依赖M1的操作进行

在MQ层面支持消息的顺序处理开销太大,为了极少量的需求,增加整体上的复杂度得不偿失。所以,还是在应用层面处理比较好。

如果queue只有单一worker,且能通过发布端控制或者通过MQ本身的routing机制将相同组的消息发到同一个queue,那就相对比较简单。

如果queue有多个worker,那恐怕只能通过在worker端通过共享的临时存储确保同一组的消息按顺序处理了,上一个消息没处理完,如果worker收到后面的消息的话先不处理,稍后重发到queue上,直到按顺序一个个处理完或者等到消息过期。

“恰好”地不解决

阿里消息服务ONS(Notify,MetaQ)

  • 解决任何问题都要付出代价

  • 消息队列的无序不意味地消息的无序

    • TCP协议
    • 发送端加一个编号 ,接收端再恢复顺序
  • 订单为例

    • 订单与订单之间是无序的
    • 单个订单的多个消息可能有序,可以把同一订单放入同一队列

推送系统设计

cim

https://www.cnblogs.com/imstudy/p/10028923.html

http://www.52im.net/forum.php?mod=viewthread&tid=602&highlight=����ϵͳ

posted @ 2021-06-27 15:58  沉梦匠心  阅读(294)  评论(0)    收藏  举报