招银科技面经

目录

1、线程池参数的意义,如何设置线程池参数。线程池调优怎么调优。

JAVA线程池调优

2、数据库连接池 最大连接数、最小连接数

3、敏捷开发

4、mybatis的使用 批量插入和分页

5、Class类的意义,以及反射的处理

6、如何在java/spring中创建全局变量

7、java如何做缓存(与redis、memcached无关)

8、系统卡顿应该从哪些方面尽量去查找。(他会让你一直想啊想,简直崩溃想不到)

9、手写LRU实现

10、海量数据查找问题(设计一种数据结构、给的是实际的POI点,所以我创建了是RTree+BTree 面试官好像还比较认可)

11、当Linux中存在很多处于WaitTime连接的是什么情况


原文链接:https://www.nowcoder.com/discuss/113040

主要是卡着你的项目问,每个都问的很深很深。。所以炸了

1、线程池参数的意义,如何设置线程池参数。线程池调优怎么调优。

maximumPoolSize:此参数的价值在于当coresize线程数达到最大,并且workQueue达到最大,此时正在执行的线程数小于coresize,就会创建新的线程去执行,直到线程数小于maxmumPoolSize。

corePoolSize:当线程数小于workQueue,线程池中最大的并发数。

keepAliveTime:线程池中的闲置空闲连接,超过这个时间将会被回收。

 

比如去火车站买票, 有10个售票窗口, 但只有5个窗口对外开放. 那么对外开放的5个窗口称为核心线程数, 而最大线程数是10个窗口.如果5个窗口都被占用, 那么后来的人就必须在后面排队, 但后来售票厅人越来越多, 已经人满为患, 就类似于线程队列已满.这时候火车站站长下令, 把剩下的5个窗口也打开, 也就是目前已经有10个窗口同时运行. 后来又来了一批人,10个窗口也处理不过来了, 而且售票厅人已经满了, 这时候站长就下令封锁入口,不允许其他人再进来, 这就是线程异常处理策略.而线程存活时间指的是, 允许售票员休息的最长时间, 以此限制售票员偷懒的行为.

ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(corePoolSize,// 核心线程数
						maximumPoolSize, // 最大线程数
						keepAliveTime, // 闲置线程存活时间
						TimeUnit.MILLISECONDS,// 时间单位
						new LinkedBlockingDeque<Runnable>(),// 线程队列
						Executors.defaultThreadFactory(),// 线程工厂
						new AbortPolicy()// 队列已满,而且当前线程数已经超过最大线程数时的异常处理策略
				);

 

JAVA线程池调优

    在JAVA中,线程可以使用定制的代码来管理,应用也可以利用线程池。在使用线程池时,有一个因素非常关键:调节线程池的大小对获得最好的性能至关重要。线程池的性能会随线程池大小这一基本选择而有所不同,在某些条件下,线程池过大对性能也有很多不利的影响。

    所有线程池的工作方式本质是一样的:有一个任务队列,一定数量的线程会从该任务队列获取任务然后执行。任务的结果可以发回客户端,或保存到数据库,或保存到某个内部数据结构中,等等。但是在执行完任务后,这个线程会返回任务队列,检索另一个任务并执行。

    线程池有最小线程数和最大线程数。池中会有最小数目的线程随时待命,等待任务指派给它们。因为创建线程的成本非常高昂,这样可以提高任务提交时的整体性能。线程池的最小线程数称作核心池大小,考虑ThreadPoolExecutor最简单的情况,如果有个任务要执行,而所有的并发线程都在忙于执行另一个任务,就会启动一个新线程,直到创建的线程达到最大线程数。

    一般我们会从以下几个方面对线程池进行设置:

  • 设置最大线程数

    对于给定硬件上的给定负载,最大线程数设置为多少最好呢?这个问题回答起来并不简单:它取决于负载特性以及底层硬件。特别是,最优线程数还与每个任务阻塞的频率有关。

    假设JVM有4个CPU可用,很明显最大线程数至少要设置为4。的确,除了处理这些任务,JVM还有些线程要做其他的事,但是它们几乎从来不会占用一个完整的CPU,至于这个数值是否要大于4,则需要进行大量充分的测试。

    有以下两点需要注意:

    一旦服务器成为瓶颈,向服务器增加负载时非常有害的;

    对于CPU密集型或IO密集型的机器增加线程数实际会降低整体的吞吐量;

  • 设置最小线程数

    一旦确定了线程池的最大线程数,就该确定所需的最小线程数了。大部分情况下,开发者会直截了当的将他们设置成同一个值。

    将最小线程数设置为其他某个值(比如1),出发点是为了防止系统创建太多线程,以节省系统资源。指定一个最小线程数的负面影响相当小。如果第一次就有很多任务要执行,会有负面影响:这是线程池需要创建一个新线程。创建线程对性能不利,这也是为什么起初需要线程池的原因。

    一般而言,对于线程数为最小值的线程池,一个新线程一旦创建出来,至少应该保留几分钟,以处理任何负载飙升。空闲时间应该以分钟计,而且至少在10分钟到30分钟之间,这样可以防止频繁创建线程。

  • 线程池任务大小

    等待线程池来执行的任务会被保存到某个队列或列表中;当池中有线程可以执行任务时,就从队列中拉出一个。这会导致不均衡:队列中任务的数量可能变得非常大。如果队列太大,其中的任务就必须等待很长时间,直到前面的任务执行完毕。

    对于任务队列,线程池通常会限制其大小。但是这个值应该如何调优,并没有一个通用的规则。若要确定哪个值能带来我们需要的性能,测量我们的真实应用是唯一的途径。不管是哪种情况,如果达到了队列限制,再添加任务就会失败。ThreadPoolExecutor有一个rejectedExecution方法,用于处理这种情况,默认会抛出RejectedExecutionExecption。应用服务器会向用户返回某个错误:或者是HTTP状态码500,或者是Web服务器捕获异常错误,并向用户给出合理的解释消息—其中后者是最理想的。

  • 设置ThreadPoolExecutor的大小

    线程池的一般行为是这样的:创建时准备最小数目的线程,如果来了一个任务,而此时所有的线程都在忙碌,则启动一个新线程(一直到达到最大线程数),任务就会立即执行。否则,任务被加入到等待队列,如果队列中已经无法加入新任务,则拒接之。

    根据所选任务队列的类型,ThreadPoolExecutor会决定何时会启动一个新线程。有以下三种可能:

    • SynchronousQueue

      如果ThreadPoolExecutor搭配的是SynchronousQueue,则线程池的行为和我们预期的一样,它会考虑线程数:如果所有的线程都在忙碌,而且池中的线程数尚未达到最大,则会为新任务启动一个新线程。然而这个队列没办法保存等待的任务:如果来了一个任务,创建的线程数已经达到最大值,而且所有的线程都在忙碌,则新的任务都会被拒绝,所以如果是管理少量的任务,这是个不错的选择,对于其他的情况就不适合了。

    • 无界队列

      如果ThreadPoolExecutor搭配的是无界队列,如LinkedBlockingQueue,则不会拒绝任何任务(因为队列大小没有限制)。这种情况下,ThreadPoolExecutor最多仅会按照最小线程数创建线程,也就是说最大线程池大小被忽略了。如果最大线程数和最小线程数相同,则这种选择和配置了固定线程数的传统线程池运行机制最为接近。

    • 有界队列

      搭配了有界队列,如ArrayBlockingQueue的ThreadPoolExecutor会采用一个非常负责的算法。比如假定线程池的最小线程数为4,最大为8所用的ArrayBlockingQueue最大为10。随着任务到达并被放到队列中,线程池中最多运行4个线程(即最小线程数)。即使队列完全填满,也就是说有10个处于等待状态的任务,ThreadPoolExecutor也只会利用4个线程。

      如果队列已满,而又有新任务进来,此时才会启动一个新线程,这里不会因为队列已满而拒接该任务,相反会启动一个新线程。新线程会运行队列中的第一个任务,为新来的任务腾出空间。

      这个算法背后的理念是:该池大部分时间仅使用核心线程(4个),即使有适量的任务在队列中等待运行。这时线程池就可以用作节流阀。如果挤压的请求变得非常多,这时该池就会尝试运行更多的线程来清理;这时第二个节流阀—最大线程数就起作用了。

    对于上面提到的每一种选择,都能找到很多支持或反对的依据,但是在尝试获得最好的性能时,可以应用KISS原则"Keep it simple,stupid"。可以将最小线程数和最大线程数设置为相同,在保存任务方面,如果适合无界队列,则选择LinkedBlockingQueue;如果适合有界队列,则选择ArrayBlockingQueue。

小结:

  • 有时对象池也是不错的选择,线程池就是情形之一:线程初始化的成本很高,线程池使得系统上的线程数容易控制。
  • 线程池必须需仔细调优,盲目的向池中添加新线程,在某些情况下对性能会有不利的影响。
  • 使用ThreadPoolExecutor时,选择更简单选项通常会带来最好的、最能预见的性能。

2、数据库连接池 最大连接数、最小连接数

  • 最小连接数是连接池一直保持的数据连接。如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费掉。
  • 最大连接数是连接池能申请的最大连接数。如果数据连接请求超过此数,后面的数据连接请求将被加入到等待队列中,这会影响之后的数据库操作。
  • 如果最小连接数与最大连接数相差太大,那么,最先的连接请求将会获利,之后超过最小连接数量的连接请求等价于建立一个新的数据库连接。不过,这些大于最小连接数的数据库连接在使用完不会马上被释放,它将被放到连接池中等待重复使用或是空闲超时后被释放。

上面的解释,可以这样理解:数据库池连接数量一直保持一个不少于最小连接数的数量,当数量不够时,数据库会创建一些连接,直到一个最大连接数,之后连接数据库就会等待。

3、敏捷开发

概念:从1990年代开始逐渐引起广泛关注,是一种以人为核心,迭代、循序渐进的开发方法。强调以人为本,专注于交付对客户有价值的软件。是一个用于开发和维持复杂产品的框架。

敏捷开发的流程:


2.1 产品负责人将整个产品设计成产品代办列表。就是一个个需求列表。(可以理解为需求或者要做的事情)


2.2 召开产品迭代计划会议,确定哪些需求是需要在第一个迭代中完成的,评估迭代的时间(建议是2-4周),得到相应的迭代周期任务列表。ps:提前发布功能需求列表,会议提倡所有团队人员参与


2.3 把迭代的功能需求写在纸条上贴在任务墙,让大家认领分配。(任务墙就是把未完成、正在做、已完成的工作状态贴到一个墙上,这样大家都可以看得到任务的状态 )–>举行每日站立会议,让大家在每日会议上总结昨天做的事情、遇到什么困难,今天开展什么任务。(每日站立会议,是在每天早上定时和大家在任务墙前站立讨论,时间控制在15分钟内)–>绘制燃尽图,保证任务的概况能够清晰看到。(燃尽图把当前的任务总数和日期一起绘制,每天记录一下,可以看到每天还剩多少个任务,直到任务数为0 ,这个迭代就完成了)ps:在开发人员开始开发一个任务时,需要找来对应的测试人员讲解该任务功能,以便测试人员有一致的理解,并且一开始就进行测试用例、自动化系统测试脚本的开发(若需要自动化测试的话)。


2.4 评审会议(演示会议)是在迭代完成时举行,要向客户演示自己完成的软件产品,并获得客户的反馈 。

ps:很多用户对软件开发是没有概念的,他只知道自己有某种需求。所以就要通过不断的让用户看到产品的模型,这个过程用户才会逐步的对产品产生概念。

2.5 最后是总结会议,以轮流发言方式进行,每个人都要发言,总结好的实践和教训,并落实到后续的开发中。不要流于形式。

敏捷开发适用原则:

1、个人与互动:重于流程与工具

强调人与人的沟通,所以尽可能要集中化办公,异地开发模式容易让人疲惫;

个人技能要提高,尤其对于架构师要求要高;

管理者要多参与项目有关的事情;

减少对开发人员的干扰;

2、可用的软件:重于详尽的文件

强调文档的作用。必要的文件是必须的,且文档要具有传承性;

3、与客户合作:重于合约协商

做好客户引导。客户都是想在尽可能短的时间内,交付尽可能多的功能,做好版本控制;

4、回应变化:重于遵循计划

无理变化,举棋不定的结果,比不是说都需要及时响应,否则会导致很多浪费。

4、mybatis的使用 批量插入和分页

环境描述 
- mybatis 3.2 ; 
- mysql数据库 
- java开发语言

1.批量插入
mapper 类方法

int insertBatch(List<ShoppingCartBean> goodsList);
1
XML 配置


<insert id="insertBatch" parameterType="java.util.List">

   insert into th_shopping_Cart 

    (itemCode, userId, number)

   values

   <foreach collection="list" item="item" index="index" separator=",">

    (#{item.itemCode},#{item.userId},#{item.number})

   </foreach>

</insert>
2. 分页查询
页面请求参数 
- 页面显示条数 size 
- 第page页面

返回结果 
- 所有记录条目总数:totalNum 
- 当前page页 
- 每页记录数pageSize 
- 记录列表list

mapper类方法

// 查询第page页的所有记录
List<ShoppingCartBean> selectByUserId(String userId, Integer start, Integer size);
// 查询所有记录数量
int selectCount(String userId);
* xml配置 *


  <select id="selectByUserId" resultMap="BaseResultMap">

    select 

    <include refid="Base_Column_List" />

    from th_shopping_Cart

    where userId =#{0}
    // start = (page - 1)* size 
    order by createTime DESC limit #{1},#{2}
  </select>

  <select id="selectCount" resultType="java.lang.Integer">

     SELECT COUNT(userId) AS _count 

     FROM `th_shopping_Cart`

     WHERE userId=#{0} 

  </select>
3.mybatis 批量修改
url配置 
在mysql数据库连接上增加 &allowMultiQueries=true

例如:

jdbc:mysql://*******:3306/tohome?autoReconnect=true&autoReconnectForPools=true&interactiveClient=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
1
mapper类方法

    //参数为List<T>
int updateBatch(List<ShoppingCartBean> list);
XML配置逻辑

<update id="updateBatch" parameterType="java.util.List">
    <foreach collection="list" item="item" index="index" open="" close="" separator=";">
        update th_shopping_Cart 
        <set>
            number=${item.number}
        </set>
        where userId='${item.userId}' and itemCode='${item.itemCode}'
    </foreach>
  </update>

4.批量删除
XML配置

<delete id="deleteBatch" parameterType="com.morning.star.tohome.store.entity.ShoppingItems">
    delete from th_shopping_Cart
    where userId =#{userId}

    <if test="null!=list and list.size > 0">
        and itemCode in

         <foreach collection="list" item="item" index="index" open="(" close=")" separator=",">
            #{item.itemCode}
    </foreach>
    </if>
  </delete>
其中我将userId字段和List封装到一个实体对象中了。
在If 中判断是否不为空

5、Class类的意义,以及反射的处理

1.Class类
类是程序的一部分,每个类都有一个Class对象,换而言之每编写一个新类,就会产生一个Class对象(更准确的来说,是被保存在一个同名的.class文件中)。当程序中需要使用到这个类的时候(包括需要这个类的对象或引用这个类的静态变量)就通过类加载器将类加到内存中来。 
在进行类加载的时候,类加载器会先检查这个类的Class对象是否已经加载,如果还未加载,则默认的类加载器就会根据类名查找.class文件(这里不一定都是从文件系统中获取,可以从数据库读取字节码或者接受网络中传输过来的字节码)。这些字节码在被加载的时候,会检查验证保证它们没有被破坏。一旦某个类的Class对象被载入内存,它就会被用来创建这个类的所有对象。下面来看下Class类一些操作。 
1.1获取Class对象的三种方式 
总共有三种方式可以获取一个类的Class对象:Class类的forName()方法、类字面常量、对象的getClass()方法。下面是一个示例,Student类是用来进行测试的对象。

package com.sankuai.lkl;
public class Student {

    public static final String test1 = "final_test";
    public static       String test2 = "static_test";

    static {
        System.out.println("init static");
    }

    private String name;
    private int    id;
    private String grade;
}

下面是一个测试方法:

public static void test1() throws Exception{
      //获取Class对象的三种方式
        try {
            //使用Class.forName()方法载入的时候,需要类的全限定名
            Class<?> cc = Class.forName("com.sankuai.lkl.Student");
            System.out.println(cc);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        //使用类字面常量
        Class<?> cc1 = Student.class;
        System.out.println(cc1);

        //通过对象的getClass()
        Student student = new Student();
        Class<?> cc = student.getClass();
        System.out.println(cc);
    }

真正测试的时候每次只会运行一个方法,下面是三种方式对应的输出: 
 
 
 
可以看到第二次的输出有点不同,没有输出static初始化块中的值。这是因为使用类字面常量方式获取Class对象的时候,类加载器虽然将类对应的class文件载入到内存中并创建了Class对象,但是没有给类进行初始化工作。这里稍微介绍下类加载的流程,分为三步:

加载。这一步是类加载器执行的,负责查找字节码(通常是在classpath所指定的路径中查找,但这不是必要的)。并从这些字节码中创建一个Class对象。
链接。在链接阶段将验证类中的字节码,并为类中的静态变量分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用(静态变量是引用并且直接初始化的情况)。
初始化。如果该类有父类,则先对其进行初始化,调用静态初始化器和静态初始化块。
在使用类字面常量获取Class对象的时候,初始化这一步被延迟到对类的静态方法或静态变量进行首次调用的时候(这里有一个很有意思的情况,其实构造器也是隐式静态的)。

1.2从Class对象中获取类信息 
上面讨论了如何获取一个类的Class对象,那么拿到这个Class对象之后,我们又能做些什么呢。Class对象存储了类的类型信息,对于一个类来说比较重要的信息有其中定义的属性、方法、构造器,还有就是它扩展了哪些父类和接口等等。而这些方法Class对象中都是包含的,并且还提供了获取的方法。下面是一些具体的例子,先给两个测试用的类定义:

package com.sankuai.lkl.reflect;

public interface Person {

    void read();
}

package com.sankuai.lkl.reflect;
public class Student implements Person {

    private String id;
    public String name;
    private String age;

    //构造函数1
    public Student() {

    }
    //构造函数2
    public Student(String id) {
        this.id = id;
    }
    //构造函数3
    public Student(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }
     public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getAge() {
        return age;
    }
    public void setAge(String age) {
        this.age = age;
    }
    //静态方法
    public static void update() {

    }
    @Override
    public void read() {

    }
}

可以看到Student类是继承自Person类的。 
首先来通过Class对象获取Student的所有属性:

public static void main(String[] args) {
        Class<?> cc = Student.class;

        //getFields返回的是申明为public的属性,包括父类中定义
        Field[] fields = cc.getFields();
        System.out.println("cc.getFields()");
        for (Field field : fields) {
            System.out.println(field);
        }
        System.out.println("\n");

        //getDeclaredFields返回的是指定类定义的所有定义的属性,不包括父类的。
        Field[] fields1 = cc.getDeclaredFields();
        System.out.println("cc.getDeclaredFields()");
        for (Field field : fields1) {
            System.out.println(field);
        }
    }

输出如下:
cc.getFields()
public java.lang.String com.sankuai.lkl.reflect.Student.name
public static final java.lang.String com.sankuai.lkl.reflect.Person.test

cc.getDeclaredFields()
private java.lang.String com.sankuai.lkl.reflect.Student.id
public java.lang.String com.sankuai.lkl.reflect.Student.name
private java.lang.String com.sankuai.lkl.reflect.Student.age

除了上面两个方法,还有getField(String name)和 getDeclaredField(String name)两个可以通过名称拿到指定属性Field对象的方法,它们的区别同上。

获取类所有的方法:

         Class<?> cc = Student.class;
        //获取所有pulbic类型方法,包括父类中定义的(Object和Person类中)
        //被子类重写过的方法,只算子类的
        Method[] methods = cc.getMethods();
        System.out.println("cc.getMethods()");
        for (Method method : methods) {
            System.out.println(method);
        }
        System.out.println("\n");

        //获取本类中所有方法
        Method[] methods1 = cc.getDeclaredMethods();
        System.out.println("cc.getDeclaredMethods()");
        for (Method method : methods1) {
            System.out.println(method);
        }
输出如下:
cc.getMethods()
public java.lang.String com.sankuai.lkl.reflect.Student.getName()
public void com.sankuai.lkl.reflect.Student.setName(java.lang.String)
public java.lang.String com.sankuai.lkl.reflect.Student.getId()
public void com.sankuai.lkl.reflect.Student.read()
public static void com.sankuai.lkl.reflect.Student.update()
public void com.sankuai.lkl.reflect.Student.setId(java.lang.String)
public java.lang.String com.sankuai.lkl.reflect.Student.getAge()
public void com.sankuai.lkl.reflect.Student.setAge(java.lang.String)
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()


cc.getDeclaredMethods()
public java.lang.String com.sankuai.lkl.reflect.Student.getName()
public void com.sankuai.lkl.reflect.Student.setName(java.lang.String)
public java.lang.String com.sankuai.lkl.reflect.Student.getId()
public void com.sankuai.lkl.reflect.Student.read()
public static void com.sankuai.lkl.reflect.Student.update()
public void com.sankuai.lkl.reflect.Student.setId(java.lang.String)
public java.lang.String com.sankuai.lkl.reflect.Student.getAge()
public void com.sankuai.lkl.reflect.Student.setAge(java.lang.String)

除了上面的批量方法,也有通过名称和参数来获取单个Method对象的方法: 

分别是getMethod()和getDeclaredMethod(),这里需要注意的是光通过名字是可能无法唯一确定一个方法的,所以这里还需要传入参数,两个方法都是传入参数的Class对象数组,注意int.class和Integer.class分别对应到参数中的int和Integer类型,不能通用。

获取类的所有构造器 
在展示Class对象的方法之前,先将Person类进行改造:将其改造成抽象类,并增加两个构造器。Student仍然是Person类的子类。

public abstract class Person {

    static String test = "test";

    private int age;

    public abstract void read();

    private Person() {
    }

    private Person(int age) {
        this.age = age;
    }
}

下面是具体方法和输出结果

  Class<?> cc = Student.class;
        //获取所有构造器
        Constructor<?>[] constructors = cc.getConstructors();
        System.out.println("cc.getConstructors()");
        for (Constructor<?> constructor : constructors) {
            System.out.println(constructor);
        }
        System.out.println("\n");

        Constructor<?>[] constructors1 = cc.getDeclaredConstructors();
        System.out.println("getDeclaredConstructors()");
        for (Constructor<?> constructor : constructors1)                 
        {
            System.out.println(constructor);
        }

输出:
cc.getConstructors()
public com.sankuai.lkl.reflect.Student(java.lang.String,java.lang.String)
public com.sankuai.lkl.reflect.Student(java.lang.String)
public com.sankuai.lkl.reflect.Student()


getDeclaredConstructors()
public com.sankuai.lkl.reflect.Student(java.lang.String,java.lang.String)
public com.sankuai.lkl.reflect.Student(java.lang.String)
public com.sankuai.lkl.reflect.Student()

这次看起来倒是两个方法表现一致,所有私有的构造器都没查找出来。对于构造器来说也有根据名字和参数获取类单个构造器Constructor对象的方法。

通过Class获取类继承的父类和实现的接口的Class对象:

  Class<?> cc = Student.class;
        //获取该类实现了的所有接口
        Class<?>[] ccs = cc.getInterfaces();
        System.out.println("getInterfaces");
        for(Class<?> c : ccs){
            System.out.println(c);
        }

        System.out.println("\n");

        //获取该类的直接父类,如果没有显示声明的父类,则是Object
        Class<?> cc1 = cc.getSuperclass();
        System.out.println(cc1);

通过Class对象判断当前类是不是接口并创建类的实例:

        Class<?> cc = Student.class;
        if (!cc.isInterface()) {
            //调用Class对象的newInstance创建对象,要求类必须具有无参构造器
            //并且创建出来的对象是Object类型,必须强制转型
            Student student = (Student) cc.newInstance();
            System.out.println(cc);
        }

 输出:
 com.sankuai.lkl.reflect.Student@19501026

通过上面几个方法的介绍,大致上明白了Class类的作用,下面看下它是如何和反射来结合的。

2.Java反射机制
通过Class对象可以拿到类的类型信息,方法、属性、构造器;但是如果只能拿到这些信息,没有办法对其进行操作其实意义也不大。Java的反射机制正是提供了对上面获取的Method、Field、Constructor等进行操作的能力。其实这些类本身就属于java.lang.reflect包下的一部分,都实现了Member接口。下面来看下如何对它们进行操作。

2.1通过Field改变对象属性

public static void test1() throws Exception{
      try{
          Class<?> cc = Class.forName("com.sankuai.lkl.reflect.Student");
          Object object =  cc.newInstance();

          Field field = cc.getDeclaredField("id");
          //id是private类型的,如果没有下面的设置,会抛出IllegalAccessException
          field.setAccessible(true);
          field.set(object,"12313");
          System.out.println(((Student)object).getId());
      }catch (ClassNotFoundException e){
          e.printStackTrace();
      }
    }
    //输出:12313

上面的id属性是私有的,但是通过将其对应Field的accessible设置为true,就可以对其进行修改了,这其实破坏了Java的封装。

2.2通过Method来调用对象的方法

 public static void test1() throws Exception{
      try{
          Class<?> cc = Class.forName("com.sankuai.lkl.reflect.Student");
          Object object =  cc.newInstance();
          Method method = cc.getDeclaredMethod("setName",String.class);
          method.invoke(object,"testName");
          Method method1 = cc.getDeclaredMethod("getName");
          //将getName()方法设置成private,但将accessible设置为true之后,仍然可以方法
          method1.setAccessible(true);
          System.out.println(method1.invoke(object));
      }catch (ClassNotFoundException e){
          e.printStackTrace();
      }
    }
   //输出:testName

上面展示了通过Class对象获得一个类实例,然后通过Method来操作这个类实例方法的过程。需要注意的是调用Class对象的getDeclaredMethod方法时,如果是想获得一个有参数的方法的Method对象,那么是需要传入参数类型的Class对象的,这里对顺序是有严格要求的并且int.class和Integer.class是不一样的,对其它基本类型也是同样的情况。并且和Field一样,只要将accessible设置为true,就可以实现对私有方法的调用。

2.3通过Constructor来构造对象

 public static void test1() throws Exception {
        try {
            Class<?> cc = Class.forName("com.sankuai.lkl.reflect.Student");
            Constructor<?> constructor = cc.getDeclaredConstructor();
            System.out.println(constructor.newInstance());
            Constructor<?> constructor1 = cc.getConstructor(int.class, String.class);
            constructor1.setAccessible(true);
            Student student = (Student) constructor1.newInstance(1, "2321");
            System.out.println(student.getId() + " " + student.getName());

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

输出:
com.sankuai.lkl.reflect.Student@24e5ddd0
1 2321

看起来构造器调用的形式和方法有些类似,不同的只是不再需要指定名字。并且对于私有构造器,也可以通过设置accessible为true来进行访问。

总的来说反射机制允许程序在运行时透过Reflection APIs取得任何一个已知名称的class的内部信息,包括其modifiers(诸如public, static 等)、superclass(例如Object)、实现之interfaces(例如Cloneable),也包括fields和methods的所有信息,并可于运行时改变fields内容或唤起methods。 
Java反射机制容许程序在运行时加载、探知、使用编译期间完全未知的classes。
--------------------- 

6、如何在java/spring中创建全局变量

普通引入properties方法(只介绍)
在spring的配置文件applicationContext.xml配置

<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:config.properties</value> </list> </property> </bean>
改进后的properties引入方法
在spring的配置文件applicationContext.xml配置

<bean id="configProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean"> <property name="locations"> <list> <value>classpath*:config.properties</value> </list> </property> </bean> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer"> <property name="properties" ref="configProperties"></property> </bean>
config.properties文件配置内容
# 变量
maxid=12342

JAVA类的使用示例
@Value("#{configProperties['maxid']}") private int maxid;

7、java如何做缓存(与redis、memcached无关)

注:https://cloud.tencent.com/developer/article/1028722

8、系统卡顿应该从哪些方面尽量去查找。(他会让你一直想啊想,简直崩溃想不到)

一般卡顿主要是以下几个方面:

1:内存使用,可以用free -g来查

当观察到free栏已为0的时候,表示内存基本被吃完了,那就释放内存吧。

释放内存:

sync

echo 3 > /proc/sys/vm/drop_caches

sync表示将内存缓存区内容立即同步到磁盘,为了保证安全可以多执行几次。

2:磁盘使用,  df-h

当发现磁盘使用率很高时,那就要释放磁盘空间了,删除一些不必要的文件(查看各个目录占用磁盘空间)

du命令:查看目录和文件的磁盘占用情况

查看指定目录的磁盘占用情况:

du -sh /home/zhubao

查看当前目录下的所有一级子目录和文件的磁盘使用情况:

du -sh *

-h或–human-readable 以K,M,G为单位,提高信息的可读性;

-s或–summarize 仅显示总计,即当前目录的大小。

3:磁盘I/O使用,  iostat-x 1

当发现最右侧%util很高时,表示IO就很高了,若想看哪个进程占用IO,执行iotop命令查看

4:CPU使用   使用top命令

图中红框里表是cpu使用情况,最右侧的%id表示剩余,若很低,则表示cpu被吃完了,在top界面按shift+p对进程使用cpu排序,能看到哪些进程占用cpu较多。

具体的Top可以参考https://blog.csdn.net/csdn066/article/details/77171018,这个写的很详细

9、手写LRU实现

我们的认识仅仅只停留在LinkedHashMap可以按照插入的顺序保存,这是不够的。它还可以实现按照访问顺序保存元素。用LinkedHashMap实现简单的LRU。

public class UseLinkedHashMapCache<K,V> extends     LinkedHashMap<K,V>{
    private int cacheSize;
    public useLinkedHashMapCache(int cacheSize){
        //构造函数一定要放在第一行
        super(16,0.74f,true);   //那个f如果不加,就是double类型。
        this.cacheSize = cacheSize;
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest){   //重写LinkedHashMap原方法
         return size()>cacheSize;  //临界条件不能有等于,否则会让缓存尺寸小1
    }
}

关键点,继承了LinkedHashMap并使用

public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

构造函数,重写了;

 protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

看看如何使用

public static void main(String[]args){
        UseLinkedHashMapCache<Integer,String> cache = new UseLinkedHashMapCache<Integer,String>(4);
        cache.put(1, "one");
        cache.put(2, "two");
        cache.put(3, "three");
        cache.put(4, "four");
        cache.put(2, "two");
        cache.put(3, "three");

        Iterator<Map.Entry<Integer,String>> it = cache.entrySet().iterator();
        while(it.hasNext()){
            Map.Entry<Integer, String> entry = it.next();
            Integer key = entry.getKey();
            System.out.print("Key:\t"+key);
            String Value = entry.getValue();  //这个无需打印...
            System.out.println();
        }
    }

结果是:

Key:    1
Key:    4
Key:    2
Key:    3

 

用数组实现

/**
 * 用数组写了一个
 * 
 * 有个疑问, 比如当缓存大小为5  这时候1、2、3、4、4  请问最后一个4是应该插入还是不处理呢? 
 * 
 * 我个人觉得如果这里理解为缓存的key ,那么就应该是不插入  结果应该还是1、2、3、4、null 
 * */

public class HandMakeCache {
    //添加次数 计数器
    static int count =0;
    //数组元素 计数器
    static int size=0;
    //最大长度
    int maxSize;
    //对象数组
    int [] listArray;  //为了简略比较

    //顺序表的初始化方法
    public HandMakeCache(int maxSize)
    {
        listArray = new int [maxSize];
        this.maxSize = maxSize;
    }

    public int getSize(){
        return size;
    }

    public void insert(int obj) throws Exception {
        // 插入过程不应该指定下标,对于用户来讲这应该是透明的,只需要暴露插入的顺序
        boolean exist = false; // 每次insert校验一下是否存在
        int location = 0; // 对于已有元素,记录其已存在的位置
        for (int i = 0; i < maxSize; i++) {
            if (obj == listArray[i]) {
                exist = true;
                location = i; // 记录已存在的位置
            }
        } // 遍历看是否已有,每次插入都要遍历,感觉性能很差
        if (size < this.maxSize) { // 当插入次数小于缓存大小的时候随意插入
            if (exist) {
                if (location == 0) {
                    moveArrayElements(listArray,0,size-2);
                } else if (location < size - 1) { // 已存在元素不在最新的位置
                    moveArrayElements(listArray,location,size-2);
                }
                listArray[size - 1] = obj; // 由于已存在
            } else {
                listArray[size] = obj;
                size++; // 数组未满时才计数
            }
        } else { // 此时缓存为满,这时候要保留最末端元素先
            if (!exist || obj == listArray[0]) { // 新元素添加进来,和最远元素添加进来效果一样
                moveArrayElements(listArray,0,maxSize-2);
            } else if (obj != listArray[maxSize - 1]) {
                moveArrayElements(listArray,location,maxSize-2);
            } // 如果添加的是上次添加的元素,则不管了。。
            listArray[maxSize - 1] = obj;
        }
        count++; // 计数
    }

    public Object get(int index) throws Exception {
        return listArray[index];
    }

    /**
     * 平移数组的方法,start是要移动至的头位置,end为最后被移动的位置。
     * */
    public void moveArrayElements(int [] arr, int start, int end){
        for(int i=start;i<=end;i++){
            arr[i] = arr[i+1];
        }
    }


    public static void main(String[] args) {
        int cacheSize = 5;
        HandMakeCache list = new HandMakeCache(cacheSize);
        try
        {
            list.insert(1);
            list.insert(2);
            list.insert(3);
            list.insert(1);
            list.insert(3);
            list.insert(4);
            list.insert(4);
            list.insert(5);
//          list.insert(3);

            for(int i=0;i<cacheSize;i++)
            {
                System.out.println(list.get(i));
            }
            System.out.println("成功插入"+count+"次元素.");

        }
        catch(Exception ex)
        {
            ex.printStackTrace();
        }

    }
}

非常重要的一点~ 写LRU之前你一定要知道LRU的正确的含义。。 
这里分为几种情况吧.. 
1. 当数组未满的情况下,随便插 
2. 数组满了之后,插入介于头和尾的元素,需要记录其之前存在的下标,然后将大于该下标的元素整体前移。 
3. 数组满了之后,插入最新的元素等于什么操作也没有。保持原样 
3. 数组满了之后,插入一个不存在的元素 等同于 插入数组最开始的元素。 
比如 1、2、3、4 之后插入5 和 1、2、3、4 之后插入1 结果分别为 2、3、4、5和 2、3、4、1。

缺点: 
如果利用数组来存储的话,当我们缓存的大小非常大的时候。比如10W,那么假设我们需要淘汰最远的元素,就需要将99999个元素整体往前移一位,这样还仅仅只是替换一次。大量这样的操作是非常低效的,所以我们还是考虑用链表来实现↓。

10、海量数据查找问题(设计一种数据结构、给的是实际的POI点,所以我创建了是RTree+BTree 面试官好像还比较认可)

题目
问题一:现有海量日志数据,要提取出某日访问百度次数最多的那个IP(可以将题干简化,假设日志中仅包含IP数据,也就是说待处理的文件中包含且仅包含全部的访问IP,但内存空间有限,不能全部加载,假设只有512MB)

解决方案:

这是一道典型的分治思想的题目,这种问题处理起来套路比较固定,对于大部分的数据量比较大的前提的问题而言,分治都是一个可选的解决方案,但不一定是最优的,解决方法基本划分为三步走:

第一:如何有效的划分数据

第二:如何在子集上解决问题

第三:如何合并结果
1
2
3
4
5


那么对于本问题就显得比较明显了:

首先解决如何划分,由于IP地地址范围从000.000.000.000~255.255.255.255共有2^32个大约4GB,那么我们可以通过取模的方式进行划分,直接将四段数据相连就得到一个IP对应的数字A,再对A模1024(模数取多少可以自己指定,保证每个小文件能被放到内存就好),这样大文件被划分成小文件了,并且相同的IP一定被划分到相同的文件中。

其次解决每个小文件中TOP1的问题: 
这里可以用很多方式进行处理,比如你可以构造自己的HashMap,key为IP,value为当前出现次数,最后找到value最大的Key即为当前文件中出现次数最多的IP。

最后要解决结果合并问题:

这里直接将1024个子文件的统计结果进行比较就好,不用排序,直接选择最大的一个就好。

注意:这里第二步是影响效率的地方,读者可以想想如何优化这里的Wordcount,可以用trie树等

问题二:有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。

解决方案:细心的读者可以发现这个和第一个问题应该没有太大别,区别就在于第一个问题是Top1,这个问题是TOP100。

那么对于这个问题,主要考虑的是如何划分(每个文件要小于1M),这里可以考虑划分为2028份。

对于第二步可以考虑用堆排序 
即常用的TopN手段。

第三部就是更为常见的归并排序

问题三:给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?

解决方案:这种题目,我首先想到的就是布隆过滤器(BloomFilter) 
当然使用布隆过滤器存在一定的错误率,所以面试者可以和面试官进行进一步的沟通,看是否允许有一定的错误率。

如果使用布隆过滤器,那么问题就很好办了,4G的内存足以容纳300多亿的bit,所以足够处理了,先将a文件中的url都放入布隆过滤器,之后遍历b文件,对每个url都询问布隆过滤器看其是否已经存在,如果存在,则此条URL输入结果文件。

关于布隆过滤器使用空间和错误率的关系可以看这篇博文

第二种方案就是采用更为通用的但对于这个问题效率较低的分治思想,对a,b进行分片,a,b文件大小都大约320G,那么每个文件都切分成1G的320个小文件。

lista->{a1,a2,a3...a320}

listb->{b1,b2,b3...b320}
1
2
3
伪代码如下:

resultlist = []
for tmpfile in lista:
    for infile in listb:
        for url in tmpfile:
            if url in infile:
                resultlist.add(url)
1
2
3
4
5
6
当然这个在实现的时候可以进一步优化,首先可以并行处理(这里只用了2G内存,所以至少可以2个线程并行)

其次,可以对a中URL进行小部分判重处理,如果之前已经有了, 
就不必处理。

问题四:有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求你按照query的频度排序。

解决方案:这种题又是典型的wordcount问题,首先这种题不用说都是内存有限制(最好问问面试官,内存限制是多少)。如果没内存限制直接一个trie树就搞定了(因为一般来说query的重复都比较多)。

如果内存限制比较大,那么可以尝试屡试不爽的分治方法。但在使用之前需要先对原来的文件进行一下处理通过hash(url)%A,这里A只分成多少分,这里可以也取10,就是将原来的10个文件转为新的10个文件,如果内存限制比较大,那么这个A可以根据内存大小调整,要让1G/A <= 内存大小。

接下来就是要对每个小文件进行统计,可以用hashmap(url,count),也可以用trie树。

之后采用归并排序的方法得到最终结果。

问题五:怎么在海量数据中找出重复次数最多的一个?

解决方案:如果看了前面四个题目,这里相信读者应该已经会自己解决了。

使用屡试不爽的分治思路,先划分成小文件,之后分别统计,之后再选取max,读者可以思考如何优化小文件内的处理方法。

问题六:在2.5亿个整数中找出不重复的整数,注,内存不足以容纳这2.5亿个整数。

解决方案:首先对于这种问题基本上都可以采用分治的思想,先进行划分后进行单独处理,最后进行结果合并。但对于这道题目显然分治思想比较牵强,效率不高。

那么再审视一下这个题目,其只需要找到不重复的整数,无需什么排序,而且又限定在整数,那么显然可以考虑位集的思想。 
2.5亿个整数,显然可以用一个250M的位集表示。

一个位集可以表示如下:

但简单的位集只能表示存在与否,因为每个bit表示一个整数,1个bit只能表示两种情况,但在本题目中不但需要表示存在性,还需要表示是否多次出现,即需要寻找出现次数>=2,那么至少需要两个bit才能表示这种情况,00->不出现,01->出现一次,10->出现至少2次,这样就能统计出那些数据出现重复。

总结一下就是本题目可以用位集的变形,每个数用2个bit表示,那么总共需要250M*2b = 500Mb的内存,完全满足题目,而且效率还是o(n)。

问题七:一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。

解决方案:分治思想这里就简要阐述一下,可以利用trie树进行词频统计,之后采用TopN的解决方法,关于排序的问题可以看这篇博文

堆排序的时间复杂度是n(longk),n是数据集合大小,k是topK中的k,那么复杂度是max(n*len(e),nlogk),其中len(e)表示字符串平均大小。

这里想讲一下另一种稍显技巧的方法,即用变形的位集表示,由于题目中只有1w的数据,那么一个词最多出现1w词,那么用18个bit就可以表示最多出现的次数,说道这里读者可以回顾一下第六个题目了,其实和题目六很相似的,这里每个数字用18个bit表示,那么剩下的问题就是解决如何将每个词转化为1~10000的数字,这里读者可以自行YY或者自行搜索,方法有很多。关键的问题是要想到可以用这种方式来解决,那么复杂度显然就是o(n)

问题八:给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中? 
(PS:腾讯面试题)

解决方案:这个题目限定的比较明确了,整数、不重复,那么理所当然可以用bitMap的思想去解决了。

申请一个40亿的bit数组,初始化为0,遍历数据集合,有的占位1,给定待验证的数据,直接看相应位置的是1还是0即可。

当然有人可能觉得40亿的bit内存装不下啊,那怎么办,当然可以分治了,写到N个文件中总可以了吧,根据待验证的数,直接查找相应的文件,在相应的文件中查看是1还是0。

比如分成1W份,被分割的文件按下标存储:

file1,file2,file3 ... file10000
1
那么相当于每个文件存储4Mb的位,待验证的数据为a,那么目标文件的index为

index*400000<=a<=(index+1)*400000
1
求出index,再在file(index)中查找。

问题九:上千万或上亿数据(有重复),统计其中出现次数最多的前N个数据。

解决方案:典型的TopN问题,请参考前面的问答题。

问题十: 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门),请统计出最热门的10个查询串,要求使用的内存不能超过1G。

解决方案:典型的TopN问题,请参考前面的问题的答案,可以考虑使用分治的思想。

总结:处理海量数据问题的四板斧
1.分治
基本上处理海量数据的问题,分治思想都是能够解决的,只不过一般情况下不会是最优方案,但可以作为一个baseline,可以逐渐优化子问题来达到一个较优解。传统的归并排序就是分治思想,涉及到大量无法加载到内存的文件、排序等问题都可以用这个方法解决。

适用场景:数据量大无法加载到内存

技能链接:归并排序

2.哈希(Hash)
个人感觉Hash是最为粗暴的一种方式,但粗暴却高效,唯一的缺点是耗内存,需要将数据全部载入内存。

适用场景:快速查找,需要总数据量可以放入内存

3.bit(位集或BitMap)
位集这种思想其实简约而不简单,有很多扩展和技巧。比如多位表示一个数据(能够表示存在和数量问题),BloomFilter(布隆过滤器就是一个典型的扩展),在实际工作中应用场景很多,比如消息过滤等,读者需要掌握,但对于布隆过滤器使用有一些误区和不清楚的地方,读者可以看下面这篇博客避免这些性能上的误区。

适用场景:可进行数据的快速查找,判重

技能链接:布隆过滤器使用的性能误区

4.堆(Heap)
堆排序是一种比较通用的TopN问题解决方案,能够满足绝大部分的求最值的问题,读者需要掌握堆的基本操作和思想。

适用场景:处理海量数据中TopN的问题(最大或最小),要求N不大,使得堆可以放入内存

技能链接:排序算法-Heap排序

11、当Linux中存在很多处于WaitTime连接的是什么情况

客户端与服务器端建立TCP/IP连接后关闭SOCKET后,服务器端连接的端口状态为TIME_WAIT.主动关闭的一方在发送最后一个 ack 后

就会进入 TIME_WAIT 状态 停留2MSL(max segment lifetime)时间

这个是TCP/IP必不可少的,也就是“解决”不了的,也就是TCP/IP设计者本来是这么设计的

主要有两个原因

1。防止上一次连接中的包,迷路后重新出现,影响新连接

(经过2MSL,上一次连接中所有的重复包都会消失)

2。可靠的关闭TCP连接

在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。

TIME_WAIT 并不会占用很大资源的,除非受到攻击。

还有,如果一方 send 或 recv 超时,就会直接进入 CLOSED 状态

 

1

netstat -an

查看下,发现系统中有很多time_wait的连接。因此直接用一下命令查看详细情况

 

1

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

具体的解决方式

 

vim /etc/sysctl.conf

net.ipv4.tcp_syncookies = 1

// 表示开启SYN cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭

net.ipv4.tcp_tw_reuse = 1

//表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;

net.ipv4.tcp_tw_recycle = 1

//表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭

net.ipv4.tcp_fin_timeout = 30

//修改系統默认的 TIMEOUT 时间

/sbin/sysctl -p   //保存后生效

 

目前看来最好的办法是让每个TIME_WAIT早点过期。

在linux上可以这么配置:

#让TIME_WAIT状态可以重用,这样即使TIME_WAIT占满了所有端口,也不会拒绝新的请求造成障碍
echo "1" > /proc/sys/net/ipv4/tcp_tw_reuse
#让TIME_WAIT尽快回收,我也不知是多久,观察大概是一秒钟
echo "1" > /proc/sys/net/ipv4/tcp_tw_recycle

很多文档都会建议两个参数都配置上,但是我发现只用修改tcp_tw_recycle就可以解决问题的了,TIME_WAIT重用TCP协议本身就是不建议打开的。

不能重用端口可能会造成系统的某些服务无法启动,比如要重启一个系统监控的软件,它用了40000端口,而这个端口在软件重启过程中刚好被使用了,就可能会重启失败的。linux默认考虑到了这个问题,有这么个设定:

#查看系统本地可用端口极限值
cat /proc/sys/net/ipv4/ip_local_port_range

用这条命令会返回两个数字,默认是:32768 61000,说明这台机器本地能向外连接61000-32768=28232个连接,注意是本地向外连接,不是这台机器的所有连接,不会影响这台机器的80端口的对外连接数。但这个数字会影响到代理服务器(nginx)对app服务器的最大连接数,因为nginx对app是用的异步传输,所以这个环节的连接速度很快,所以堆积的连接就很少。假如nginx对app服务器之间的带宽出了问题或是app服务器有问题,那么可能使连接堆积起来,这时可以通过设定nginx的代理超时时间,来使连接尽快释放掉,一般来说极少能用到28232个连接。

12、在生产环境出现BUG一般是怎么解决的?

posted @ 2019-03-08 13:40  strawqqhat  阅读(417)  评论(0编辑  收藏  举报
#home h1{ font-size:45px; } body{ background-image: url("放你的背景图链接"); background-position: initial; background-size: cover; background-repeat: no-repeat; background-attachment: fixed; background-origin: initial; background-clip: initial; height:100%; width:100%; } #home{ opacity:0.7; } .wall{ position: fixed; top: 0; left: 0; bottom: 0; right: 0; } div#midground{ background: url("https://i.postimg.cc/PP5GtGtM/midground.png"); z-index: -1; -webkit-animation: cc 200s linear infinite; -moz-animation: cc 200s linear infinite; -o-animation: cc 200s linear infinite; animation: cc 200s linear infinite; } div#foreground{ background: url("https://i.postimg.cc/z3jZZD1B/foreground.png"); z-index: -2; -webkit-animation: cc 253s linear infinite; -o-animation: cc 253s linear infinite; -moz-animation: cc 253s linear infinite; animation: cc 253s linear infinite; } div#top{ background: url("https://i.postimg.cc/PP5GtGtM/midground.png"); z-index: -4; -webkit-animation: da 200s linear infinite; -o-animation: da 200s linear infinite; animation: da 200s linear infinite; } @-webkit-keyframes cc { from{ background-position: 0 0; transform: translateY(10px); } to{ background-position: 600% 0; } } @-o-keyframes cc { from{ background-position: 0 0; transform: translateY(10px); } to{ background-position: 600% 0; } } @-moz-keyframes cc { from{ background-position: 0 0; transform: translateY(10px); } to{ background-position: 600% 0; } } @keyframes cc { 0%{ background-position: 0 0; } 100%{ background-position: 600% 0; } } @keyframes da { 0%{ background-position: 0 0; } 100%{ background-position: 0 600%; } } @-webkit-keyframes da { 0%{ background-position: 0 0; } 100%{ background-position: 0 600%; } } @-moz-keyframes da { 0%{ background-position: 0 0; } 100%{ background-position: 0 600%; } } @-ms-keyframes da { 0%{ background-position: 0 0; } 100%{ background-position: 0 600%; } }