java面试题
java面试题
一贱书生 发表于1年前
java面试题
  • 发表于 1年前
  • 阅读 59
  • 收藏 0
  • 点赞 0
  • 评论 0

腾讯云 十分钟定制你的第一个小程序>>>   

1、现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行

答: thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。 使用线程的join方法,该方法的作用是“等待线程执行结束”,即join()方法后面的代码块都要等待现场执行结束后才能执行 。比如在线程B中调用了线程AJoin()方法,直到线程A执行完毕后,才会继续执行线程B
t.join();      //使调用线程 t 在此之前执行完毕。
t.join(1000);  //等待 t 线程,等待时间是1000毫秒

package cglib;


public class List1
{   
    
     public static void main(String[] args)
        {
            final Thread t1 = new Thread(new Runnable() {

                @Override
                public void run() {
                    System.out.println("t1");
                }
            });
            final Thread t2 = new Thread(new Runnable() {

                @Override
                public void run() {
                    try {
                        //引用t1线程,等待t1线程执行完
                        t1.join();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t2");
                }
            });
            Thread t3 = new Thread(new Runnable() {

                @Override
                public void run() {
                    try {
                        //引用t2线程,等待t2线程执行完
                        t2.join();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t3");
                }
            });
            t3.start();
            t2.start();
            t1.start();
        }
    }

 

输出:

t1
t2
t3

2、3*0.1==0.3返回false

因为 3*0.1 java生成0.30000000000000004,

修改成  float i=(float) (3*0.1);

i==0.3 ;


不管是什么数, 在计算机中最终都会被转化为 0 和 1 进行存储, 所以需要弄明白以下几点问题

  • 一个小数如何转化为二进制
  • 浮点数的二进制如何存储


浮点数的二进制表示

首先我们要了解浮点数二进制表示, 有以下两个原则:

  • 整数部分对 2 取余然后逆序排列
  • 小数部分乘 2 取整数部分, 然后顺序排列


0.1 的表示是什么?

我们继续按照浮点数的二进制表示来计算
0.1 * 2 = 0.2 整数部分取 0
0.2 * 2 = 0.4 整数部分取 0
0.4 * 2 = 0.8 整数部分取 0
0.8 * 2 = 1.6 整数部分取 1
0.6 * 2 = 1.2 整数部分取 1
0.2 * 2 = 0.4 整数部分取 0

所以你会发现, 0.1 的二进制表示是 0.00011001100110011001100110011……0011
0011作为二进制小数的循环节不断的进行循环.

这就引出了一个问题, 你永远不能存下 0.1 的二进制, 即使你把全世界的硬盘都放在一起, 也存不下 0.1 的二进制小数.


浮点数的二进制存储

Python 和 C 一样, 采用 IEEE 754 规范来存储浮点数. IEEE 754 对双精度浮点数的存储规范将 64 bit 分为 3 部分.

  • 第 1 bit 位用来存储 符号, 决定这个数是正数还是负数
  • 然后使用 11 bit 来存储指数部分
  • 剩下的 52 bit 用来存储尾数
    Double-precision_floating-point_format

而且可以指出的是, double 能存储的数的个数是有限的, double 能代表的数必然不超过 2^64 个, 那么现实世界上有多少个小数呢? 无限个. 计算机能做的只能是一个接近这个小数的值, 是这个值在一定精度下与逻辑认为的值相等. 换句话说, 每个小数的存储(但是不是所有的), 都会伴有精度的丢失.

0.1 在计算机存储中真正的数字是 0.1000000000000000055511151231257827021181583404541015625
0.2 是

0.200000000000000011102230246251565404236316680908203125
0.3 是

 

0.299999999999999988897769753748434595763683319091796875

这不是bug,原因在与十进制到二进制的转换导致的精度问题!其次这几乎出现在很多的编程

语言中:C、C++、Java、Javascript、Python中,准确的说:“使用了IEEE754浮点数格式”来存储浮点类型(float 32,double 64)的任何编程语言都有这个问题!

简要介绍下IEEE 754浮点格式:它用科学记数法以底数为2的小数来表示浮点数。IEEE浮点数(共32位)用1位表示数字符号,用8为表示指数,用23为来表示尾数(即小数部分)。此处指数用移码存储,尾数则是原码(没有符号位)。之所以用移码是因为移码的负数的符号位为0,这可以保证浮点数0的所有位都是0。双精度浮点数(64位),使用1位符号位、11位指数位、52位尾数位来表示。

因为科学记数法有很多种方式来表示给定的数字,所以要规范化浮点数,以便用底数为2并且小数点左边为1的小数来表示(注意是二进制的,所以只要不为0则一定有一位为1),按照需要调节指数就可以得到所需的数字。例如:十进制的1.25 => 二进制的1.01 => 则存储时指数为0、尾数为1.01、符号位为0.(十进制转二进制)

回到开头,为什么“0.1+0.2=0.30000000000000004”?首先声明这是javascript语言计算的结果(注意Javascript的数字类型是以64位的IEEE 754格式存储的)。正如同十进制无法精确表示1/3(0.33333...)一样,二进制也有无法精确表示的值。例如1/10。64位浮点数情况下:
十进制0.1=> 二进制0.00011001100110011...(循环0011)
=>尾数为1.1001100110011001100...1100(共52位,除了小数点左边的1),指数为-4(二进制移码为00000000010),符号位为0=> 存储为:0 00000000100 10011001100110011...11001=> 因为尾数最多52位,所以实际存储的值为0.00011001100110011001100110011001100110011001100110011001

十进制0.2=> 二进制0.0011001100110011...(循环0011)
=>尾数为1.1001100110011001100...1100(共52位,除了小数点左边的1),指数为-3(二进制移码为00000000011),符号位为0=> 存储为:0 00000000011 10011001100110011...11001
因为尾数最多52位,所以实际存储的值为0.00110011001100110011001100110011001100110011001100110011

3、说下java堆空间结构及常用的jvm内存分析命令和工具

空间结构:

1.        New Generation

又称为新生代,程序中新建的对象都将分配到新生代中,新生代又由Eden Space和两块Survivor Space构成,可通过-Xmn参数来指定其大小,Eden Space的大小和两块Survivor Space的大小比例默认为8,即当New Generation的大小为10M时,Eden Space的大小为8M,两块Survivor Space各占1M,这个比例可通过-XX:SurvivorRatio来指定。

2.        Old Generation

又称为旧生代,用于存放程序中经过几次垃圾回收还存活的对象,例如缓存的对象等,旧生代所占用的内存大小即为-Xmx指定的大小减去-Xmn指定的大小。

常用的jvm内存分析命令和工具

1:gc日志输出

       在jvm启动参数中加入 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimestamps -XX:+PrintGCApplicationStopedTime,jvm将会按照这些参数顺序输出gc概要信息,详细信息,gc时间信息,gc造成 的应用暂停时间。如果在刚才的参数后面加入参数 -Xloggc:文件路径,gc信息将会输出到指定的文件中。其他参数还有

-verbose:gc和-XX:+PrintTenuringDistribution等。

 

2:jconsole

      jconsole是jdk自带的一个内存分析工具,它提供了图形界面。可以查看到被监控的jvm的内存信息,线程信息,类加载信息,MBean信息。

      jconsole位于jdk目录下的bin目录,在windows下是jconsole.exe,在unix和linux下是 jconsole.sh,jconsole可以监控本地应用,也可以监控远程应用。 要监控本地应用,执行jconsole pid,pid就是运行的java进程id,如果不带上pid参数,则执行jconsole命令后,会看到一个对话框弹出,上面列出了本地的java进 程,可以选择一个进行监控。如果要远程监控,则要在远程服务器的jvm参数里加入一些东西,因为jconsole的远程监控基于jmx的

 

 

4、哪些情况下索引会失效

  1. 如果条件中有or,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)

  

  注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引

  2.对于多列索引,不是使用的第一部分,则不会使用索引

  3.like查询是以%开头

  4.如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引

  

  5.如果mysql估计使用全表扫描要比使用索引快,则不使用索引,

如查询谓词没有使用索引的主要边界,可能会导致不走索引。比如,你查询的是SELECT * FROM T WHERE Y=XXX;假如你的T表上有一个包含Y值的组合索引,但是优化器会认为需要一行行的扫描会更有效,这个时候,优化器可能会选择TABLE ACCESS FULL,但是如果换成了SELECT Y FROM T WHERE Y = XXX,优化器会直接去索引中找到Y的值,因为从B树中就可以找到相应的值。

6. 对索引列进行运算导致索引失效,我所指的对索引列进行运算包括(+,-,*,/,! 等)

 错误的例子:select * from test where id-1=9;

 正确的例子:select * from test where id=10;

7. 使用Oracle内部函数导致索引失效.对于这样情况应当创建基于函数的索引.

      错误的例子:select * from test where round(id)=10; 说明,此时id的索引已经不起作用了

      正确的例子:首先建立函数索引,create index test_id_fbi_idx on test(round(id));然后 select * from test where round(id)=10; 这时函数索引起作用了

8. 以下使用会使索引失效,应避免使用;

 a. 使用 <> 、not in 、not exist、!=

b、当变量采用的是times变量,而表的字段采用的是date变量时.或相反情况。

   9. 不要将空的变量值直接与比较运算符(符号)比较。

   如果变量可能为空,应使用 IS NULL 或 IS NOT NULL 进行比较,或者使用 ISNULL 函数。

此外,查看索引的使用情况。

10、 如果在B树索引中有一个空值,那么查询诸如SELECT COUNT(*) FROM T 的时候,因为HASHSET中不能存储空值的,所以优化器不会走索引,有两种方式可以让索引有效,一种是SELECT COUNT(*) FROM T WHERE XXX IS NOT NULL或者是不能为空

 

11、 在Oracle的初始化参数中,有一个参数是一次读取的数据块的数目,比如你的表只有几个数据块大小,而且可以被Oracle一次性抓取,那么就没有使用 索引的必要了,因为抓取索引还需要去根据rowid从数据块中获取相应的元素值,因此在表特别小的情况下,索引没有用到是情理当中的事情。

12、很长时间没有做表分析,或者重新收集表状态信息了,在数据字典中,表的统计信息是不准确的,这个情况下,可能会使用错误的索引,这个效率可能也是比较低的。

show status like ‘Handler_read%’;

handler_read_key:这个值越高越好,越高表示使用索引查询到的次数

handler_read_rnd_next:这个值越高,说明查询低效

 

5、

在JDBC中使用PreparedStatement代替Statement,预防SQL注入,

这是因为PreparedStatement不允许在插入时改变查询的逻辑结构

Statement和PreparedStatement的关系与区别在于:

  ① PreparedStatement类是Statement类的子类,拥有更多强大的功能。

  ② PreparedStatement类可以防止SQL注入攻击的问题

  ③ PreparedStatement会对SQL语句进行预编译,以减轻数据库服务器的压力,而Statement则无法做到。

Statement主要用于执行静态SQL语句,即内容固定不变的SQL语句。Statement每执行一次都要对传入的SQL语句编译一次,效率较差。

Statement :

String sql = "select * from user where name='" +name+ "'";

页面带过来的表单数据name值为' or 1=1 or name='

我们若将表单填写的数据带到代码中的SQL语句,就形成如下的SQL命令:

  select * from user where name='' or 1=1 or name=''

      可以看到使用Statement对象就是将两个字符串拼接形成的SQL语句,这样做很可能会将判断条件改变,如上面的命令,在where语句中出现了 or  1=1 这样一定会返回true的语句,就如同程序发送一条“select * from user where true”的句子,那么数据库执行这条语句根本不需要筛选条件,只要数据库有任意用户,都可以告诉程序你找到了该指定用户,那么我们连密码都不用填的只需 要恶意SQL语句即可登录网站。这就是一个SQL注入的典型例子。

PreparedStatement :

sql = "select * from users where NAME = ? and PWD = ?";

而使用PreparedStatement则不会,因为 PreparedStatement的预编译,会将表单中所填写的数据进行编译,这种编译是包含字符过滤的编译,就好像对html进行过滤转义一样,这字 符过滤最关键的因素在于PreparedStatement使用的是占位符,而不会像Statement那样因为拼接字符串而引入了引号, 可以看到在PreparedStatement中即使接收的表单数据中SQL语句以引号包围,由于程序中的SQL语句使用占位符,因此就相当于条件为 where name=' or 1=1 or name=',显然数据库并没有这样的记录,因此防止了SQL注入的问题。
     某些情况下,SQL语句只是其中的参数有所不同,其余子句完全相同,适用于PreparedStatement。   PreparedStatement 实例包含已事先编译的 SQL 语句,SQL 语句可有一个或多个 IN 参数,IN参数的值在 SQL 语句创建时未被指定。该语句为每个 IN 参数保留一个问号(“?”)作为占位符。
     每个问号的值必须在该语句执行之前,通过适当的setInt或者setString 等方法提供。
     由于 PreparedStatement 对象已预编译过,所以其执行速度要快于 Statement 对象。因此,多次执行的 SQL 语句经常创建为 PreparedStatement 对象,以提高效率。
通常批量处理时使用PreparedStatement。

6、

Servlet —— 只有一个实例

Servlet的生命周期:
       (1)装载Servlet。该操作一般是动态执行。然而,Server通常会提供一个管理的选项,用于在Server启动时强制装载和初始化特定的Servlet。
       (2)Server创建一个Servlet的实例
       (3)Server调用Servlet的init()方法
       (4)一个客户端的请求到达Server
       (5)Server创建一个请求对象
       (6)Server创建一个响应对象
       (7)Server激活Servlet的Service()方法,传递请求、响应对象作为参数
       (8)service()方法获得关于请求对象的信息,处理请求,访问其他资源,获得需要的信息
       (9)service()方法使用响应对象的方法,将响应传回Server,最终到达客户端。Service()方法可以激活其他方法以处理请求,如doGet()或doPost()或程序员自己开发的新的方法。
       (10)对于更多的客户端请求,Server创建新的请求和响应对象,仍然激活此Servlet的service()方法,将这两个对象作为参数传递给 该方法。如此重复以上的循环,但无需再调用init()方法。因为,一般Servlet只初始化一次(只有一个实例),而当Server不再需要 Servlet时(如异常或Server关闭),Server将调用Servlet的destroy()方法。


       这个生命周期是相当好理解的。唯一的一点,就是,为什么Servlet只有一个实例?

 

       出于性能的考虑:特别的对于门户网站而言,每一个Servlet在每一秒内的并发访问量都可以是成千上万的。在一个面向模块化开发的现在,常常一个点击 操作就被定义为一个Servlet的实现,而如果Servlet的每一次被访问,都创建一个新的实例的话,服务器的可用资源消耗量将是一个相当重要的问 题。退一步,一般Servlet的访问是很快的,每一个实例被快速的创建,又被快速的回收,GC的回收速度也跟不上,频繁的内存操作也将可能带来次生的问 题。所以,Servlet的“单一实例化”是一个很重要的策略。

7、编写一个基于guava中的缓存组件,有哪些场景需要考虑,怎么解决

为什么要有本地缓存?

在 系统中,有些数据,数据量小,但是访问十分频繁(例如国家标准行政区域数据),针对这种场景,需要将数据搞到应用的本地缓存中,以提升系统的访问效率,减 少无谓的数据库访问(数据库访问占用数据库连接,同时网络消耗比较大),但是有一点需要注意,就是缓存的占用空间以及缓存的失效策略。

 

为什么是本地缓存,而不是分布式的集群缓存?

         目前的数据,大多是业务无关的小数据缓存,没有必要搞分布式的集群缓存,目前涉及到订单和商品的数据,会直接走DB进行请求,再加上分布式缓存的构建,集群维护成本比较高,不太适合紧急的业务项目。

         这里介绍一下缓存使用的三个阶段(摘自info架构师文档)

          

 

本地缓存在那个区域?

         目前考虑的是占用了JVM的heap区域,再细化一点的就是heap中的old区,目前的数据量来看,都是一些小数据,加起来没有几百兆,放在heap区 域最快最方便。后期如果需要放置在本地缓存的数据大的时候,可以考虑在off-heap区域,但是off-heap区域的话,需要考虑对象的序列化(因为 off-heap区域存储的是二进制的数据),另外一个的话就是off-heap的GC问题。其实,如果真的数据量比较大,那其实就可以考虑搞一个集中式 的缓存系统,可以是单机,也可以是集群,来承担缓存的作用。

 

搞一个单例模式,里面有个Map的变量来放置数据

关于单例模式,一个既简单又复杂的模式(http://iamzhongyong.iteye.com/blog/1539642

非常典型的代码如下:

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

public class SingletonMap {

    //一个本地的缓存Map

    private Map<String,Object> localCacheStore = new HashMap<String,Object>(); 

 

    //一个私有的对象,非懒汉模式

    private static SingletonMap singletonMap = new SingletonMap(); 

 

    //私有构造方法,外部不可以new一个对象

    private SingletonMap(){

    }  

 

    //静态方法,外部获得实例对象

    public static SingletonMap getInstance(){

        return singletonMap;

    }

 

    //获得缓存中的数据

    public Object getValueByKey(String key){

        return localCacheStore.get(key);

    }

    //向缓存中添加数据

    public void putValue(String key , Object value){

        localCacheStore.put(key, value);

    }

}

 这种能不能用?可以用,但是非常局限

1

2

3

4

5

6

7

8

但是这种的就是本地缓存了吗?答案显然不是,为啥呢?

1、  没有缓存大小的设置,无法限定缓存体的大小以及存储数据的限制(max size limit);

2、  没有缓存的失效策略(eviction policies);

3、  没有弱键引用,在内存占用吃紧的情况下,JVM是无法回收的(weak rererences keys);

4、  没有监控统计(statistics);

5、  持久性存储(persistent store);

所以,这种就直接废掉了。。。

 

引入EhCache来构建缓存(详细介绍:  http://raychase.iteye.com/blog/1545906

EhCahce的核心类:

A、CacheManager:Cache的管理类;

B、Cache:具体的cache类信息,负责缓存的get和put等操作

C、CacheConfiguration :cache的配置信息,包含策略、最大值等信息

D、Element:cache中单条缓存数据的单位

典型的代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

public static void main(String[] args) {

        //EhCache的缓存,是通过CacheManager来进行管理的

        CacheManager cacheManager = CacheManager.getInstance();

         

        //缓存的配置,也可以通过xml文件进行

        CacheConfiguration conf = new CacheConfiguration();

        conf.name("cache_name_default");//设置名字

        conf.maxEntriesLocalHeap(1000);//最大的缓存数量

        conf.memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LRU);//设置失效策略

         

        //创建一个缓存对象,并把设置的信息传入进去

        Cache localCache = new Cache(conf);

         

        //将缓存对象添加到管理器中

        cacheManager.addCache(localCache);

                 

        localCache.put(new Element("iamzhongyong"new Date()));

         

        System.out.println(localCache.getSize());

        System.out.println(localCache.getStatistics().toString());

        System.out.println(localCache.getName());

        System.out.println(localCache.get("iamzhongyong").toString());

        System.out.println(localCache.get("iamzhongyong").getObjectValue());   

    }

当然,Cache的配置信息,可以通过配置文件制定了。。。

优点:功能强大,有失效策略、最大数量设置等,缓存的持久化只有企业版才有,组件的缓存同步,可以通过jgroup来实现

缺点:功能强大的同时,也使其更加复杂

 

引入guava的cacheBuilder来构建缓存

这个非常强大、简单,通过一个CacheBuilder类就可以满足需求。

缺点就是如果要组件同步的话,需要自己实现这个功能。

典型的代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

public class GuavaCacheBuilderTest {

    public static void main(String[] args) throws Exception{

        GuavaCacheBuilderTest cache = new GuavaCacheBuilderTest();

        cache.getNameLoadingCache("bixiao");

    }

    public void getNameLoadingCache(String name) throws Exception{

        LoadingCache<String, String> cache = CacheBuilder.newBuilder()       

            .maximumSize(20)//设置大小,条目数        

            .expireAfterWrite(20, TimeUnit.SECONDS)//设置失效时间,创建时间      

            .expireAfterAccess(20, TimeUnit.HOURS) //设置时效时间,最后一次被访问       

            .removalListener(new RemovalListener<String, String>() { //移除缓存的监听器

                public void onRemoval(RemovalNotification<String, String> notification) {

                    System.out.println("有缓存数据被移除了");

                }})

            .build(new CacheLoader<String, String>(){ //通过回调加载缓存

                @Override

                public String load(String name) throws Exception {

                    return name + "-" "iamzhongyong";

                }

        });

        System.out.println(cache.get(name));

        //cache.invalidateAll();

    }

}

 

缓存预热怎么搞?

A、全量预热,固定的时间段移除所有,然后再全量预热

适用场景:

1、数据更新不频繁,例如每天晚上3点更新即可的需求;

 2、数据基本没有变化,例如全国区域性数据;

B、增量预热(缓存查询,没有,则查询数据库,有则放入缓存)

适用场景:

1、  数据更新要求缓存中同步更新的场景

 

​集群内部,缓存的一致性如何保证?

如果采用ehcache的话,可以使用框架本身的JGroup来实现组内机器之间的缓存同步。

如果是采用google的cacheBuilder的话,需要自己实现缓存的同步。

A、非实时生效数据:数据的更新不会时时发生,应用启动的时候更新即可,然后定时程序定时去清理缓存;

B、需要实时生效数据:启动时可预热也可不预热,但是缓存数据变更后,集群之间需要同步

内存缓存需要考虑很多问题,包括并发问题,缓存失效机制,内存不够用时缓存释放,缓存的命中率,缓存的移除等等。 Cache在真实场景中有着相当广的使用范围。例如,当一个值要通过昂贵的计算和检索来获取,并且这个结果会被多次使用到,这个时候你就应该考虑使用缓存 了。 Cache有点类似于ConcurrentMap,但并不是完全一样。最根本的区别在于ConcurrentMap会持有添加到Map中的所有元素直到元 素被移除为止。Cache通过相关的配置项可以自动驱逐相关的缓存项,达到约束缓存占用的目的。有些情景中LoadingCache通过自动的内存载入甚 至可以不进行驱逐缓存项。 通常,Guava缓存工具适用于如下的情景: 1、你愿意牺牲一些内存来提升速度。 2、你期待缓存项的查询大于一次。 3、 你的缓存数据不超过内存(Guava缓存是单个应用中的本地缓存。它不会将数据存储到文件中,或者外部服务器。如果不适合你,可以考虑一下 Memcached)  注意:如果你不需要缓存的这些特性,那么使用ConcurrentHashMap会有更好的内存效率,但是如果想基于旧有的ConcurrentMap复制实现Cache的一些特性,那么可能是非常困难或者根本不可能。

使用缓存,就存在缓存数据一致性的问题,和缓存数据的更新敏感度的问题,这个就是缓存的数据更新问题。

如果是分布式缓存,就另外涉及到分布式的数据一致性问题,这里仅针对本地缓存进行讨论。

针对本地缓存,更新方法有很多种,比如最常用的:

  • 被动更新: 是先从缓存获取,没有则回源取,再放回缓存;
  • 主动更新: 发现数据改变后直接更新缓存(在多机环境下,一般不会采用)

在高并发场景下,被动更新的回源是要格外小心的,也就是雪崩穿透问题: 如果有太多请求在同一时间回源,后端服务如果无法支撑这么高并发,容易引发后端服务崩溃。

这时Guava Cache上场了,Guava Cache里的CacheLoader在回源的load方法上加了控制,对于同一个key,只让一个请求回源load,其他线程阻塞等待结果。同时,在 Guava里可以通过配置expireAfterAccess/expireAfterWrite设定key的过期时间,key过期后就单线程回源加载并 放回缓存。

样通过Guava Cache简简单单就较为安全地实现了缓存的被动更新操作。

为什么是”较为安全”呢?因为如果同一时间仍有太多的不同key过期,还是会有大量请求穿透缓存而请求到后端服务上,仍然有可能使后端服务崩溃,有什么办法解决这个问题呢?

1.将key的过期时间加个随机值,避免大家一起过期(前提是对业务不影响),
2.自己控制回源的并发数,即使有一万个key要更新,也只让100个可以回源,其余的9900个等着,(可以通过Guava的Striped实现)
3.在过期前主动更新,更新完成后将过期时间延长

另外,如果对刚才说的对于同一个key,只让一个请求回源,其他线程等待觉得还不爽,虽然对后端服务不会造成压力,但我的请求都还是blocked了,整个请求还是会被堵一下。

别急,Guava Cache还提供了一个refreshAfterWrite的配置项,定时刷新数据,刷新时仍只有一个线程回源取数据,但其他线程只会稍微等一会,没等到 就返回旧值,整个请求看起来就比较平滑了。为什么又是“比较平滑”呢?因为默认的刷新回源线程是同步的,如果想达到全过程平滑的效果,可以将刷新回源线程 做成异步方式。

这样数据的更新都是在后台异步做了,但这样也是有一定的代价的,比如过了刷新时间,仍可能拿到旧值,直到拿回数据更新缓存后才会返回新值。

因为这个refresh动作并不是主动发起的: 比如设置了5秒refresh一下,Guava的做法并不是真的每5秒刷一次,而是等请求到了之后,发现需要refresh时才会真的更新。所以,这一点 需要注意,比如虽然设置了5秒刷新,但如果超过1分钟都没有请求(假设key没有过期),当1分零1秒有请求来时,仍有可能返回旧值。

以下是关于设置Expire过期和Refresh刷新(sync/async)两种方式,Guava Cache对请求回源的处理示意图:

 

 

共有 人打赏支持
粉丝 14
博文 722
码字总数 600072
×
一贱书生
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: