小林求职记(二):说好的问基础,为啥我感觉一点也不基础呢?

在上一轮的面试中,小林在mysql方面因为作答不够完善,被面试官吊打了一番。经过两天的自我复习之后,新的一轮面试又开始了。

面试官:你好,请简单介绍下自己吧。

小林:你好,我是xxxxxx,之前在深圳的xxx公司负责了xxx系统的研发设计。

面试官:嗯嗯,那我先来问你一些基础问题吧。

小林:嗯嗯,好的。

面试官:你了解arraylist吧,请说下内部的一些特性。

小林此时心里一下子乐开了花,这个简单啊。

小林:arraylist的底层主要是由数组组成,它和普通数组不太一样,arraylist具有自动扩容的功能。每次当我们add一个元素到队列里面的时候,都会有一步确认容量的机制判断(对应源码里面的ensureCapacityInternal函数)如果当数组内部的元素达到了数组阈值的时候,就会以1.5倍的体积去做扩容,底层是调用了才做系统内部的一个System.arraycopy方法。

又由于arraylist是采用数组存储的,在读取数据的时候可以借助数组位的下标去快速定位,写数据的时候需要涉及到挪动数组,所以读的性能平均要比写的性能更高一些。

面试官:嗯嗯,回答地挺全面的。那你觉得在使用arraylist的时候一般会注意些什么吗?

小林:嗯嗯,有的。一般我会根据代码的上下文给arraylist附一个初始值来定位这个数组的大小,防止其做过多不必要的扩容操作。另外在循环中进行删除操作的时候需要注意会有坑,一般建议采用迭代器的模式来处理。

ps:

如果使用以下这种方式进行元素的移除可能会导致出现删除元素不完整的情况:

public static void main(String[] args) {
    ArrayListApplication arrayListApplication = new ArrayListApplication();
    List<String> list = new ArrayList(3);
    list.add("a");
    list.add("c");
    list.add("c");
    list.add("d");
    list.add("e");
    System.out.println(list);
    System.out.println("==========");
    removeV1(list, "c");
    System.out.println(list);
}
public static void removeV1(List<String> list, String deleteItem) {
    for (int i = 0; i < list.size(); i++) {
        String item = list.get(i);
        if (item.equals(deleteItem)) {
            list.remove(item);
        }
    }
}

 

打印结果:

[a, c, c, d, e]
==========
[a, c, d, e]

 

此时由于删除掉list里面元素之后,list的size值也减少了,随之导致了数组元素的前移,因此会出现被删除元素的后一位直接绕开了if判断,没有被“命中”。如果采用foreach的方式删除,则会抛出一段异常信息,声明删除失败:

Exception in thread "main" java.util.ConcurrentModificationException
 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
 at java.util.ArrayList$Itr.next(ArrayList.java:859)

 

面试官:嗯嗯,你刚刚有说到迭代器删除,那么你有了解过迭代器模式吗?

小林:额,迭代器模式,让我思考一下…. 背了知识点,但是不记得了….

小林求职记(二):说好的问基础,为啥我感觉一点也不基础呢?

面试官:好吧…

ps:其实迭代器模式的好处在于,直接帮我们屏蔽了具体的实现细节,通过采用迭代器的方式来帮助我们编写一些可以复用的类。例如说arraylist,其实内部也有自己迭代器的具体实现,vector也有自己迭代器模式的具体实现。非常的方便,并且有助于减少代码里面的对于具体实现的强依赖性。

面试官:那我们切入下一个话题吧,能说下自己对于幻读的理解吗?

小林:嗯嗯,可以的。我在上家公司工作的时候,公司内部的事务隔离级别设置为了可重复读级别,这样能够保证当前事务读取的数据不会受到其他事务提交的影响,但是这种隔离级别会在事务提交完毕之后查询数据的时候出现幻读的场景,如果需要解决幻读的情况需要将事务的隔离级别提升为串行化等级。

面试官:哦,那你在工作中有试过提升为串行化吗?

小林:没有,因为串行化是强行在mysql层加锁,使得事务得排队执行,容易产生堵塞的情况,性能不佳。

面试官:那你是怎么解决幻读的情况呢?

小林:嗯….别的同事帮我解决的…..

此时,面试官脸上渐渐露出了诡异的笑容。

小林求职记(二):说好的问基础,为啥我感觉一点也不基础呢?

ps:其实幻读这种情况在工作中偶尔还是会遇到的,举个具体场景:

假设某一时刻,同时有两个事务访问了数据库,需要先从库里面查询订单是否存在,然后再插入新的订单记录。

a连接

mysql> select @@global.tx_isolation,@@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation  |
+-----------------------+-----------------+
| REPEATABLE-READ       | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set, 2 warnings (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_order_1 where id =100;
Empty set (0.00 sec)

 

b连接

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_order_01 where id =100;
ERROR 1146 (42S02): Table 'test-db01.t_order_01' doesn't exist
mysql> select * from t_order_1 where id =100;
Empty set (0.00 sec)

 

假设在两边事务都开启的一刻,a连接中的事务往数据库插入了一条id为100的数据,然后commit。a连接

mysql> INSERT INTO `test-db01`.`t_order_1` ( `id`, `order_no`, `product_id`, `user_id`, `create_time`, `update_time` )
    -> VALUES
    -> ( 100, 2, 2, 2, now(), now());
Query OK, 1 row affected (0.00 sec)
mysql> select * from t_order_1 where id=100;
+-----+----------+------------+---------+---------------------+---------------------+
| id  | order_no | product_id | user_id | create_time         | update_time         |
+-----+----------+------------+---------+---------------------+---------------------+
| 100 |        2 |          2 |       2 | 2020-07-14 22:57:37 | 2020-07-14 22:57:37 |
+-----+----------+------------+---------+---------------------+---------------------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

 

当a事务提交结束了,此时b事务开始执行select 查询校验的操作,判断不存在id为100的数据,此时打算执行插入数据的操作:

mysql> select * from t_order_1 where id=100;
Empty set (0.00 sec)
mysql> INSERT INTO `test-db01`.`t_order_1` ( `id`, `order_no`, `product_id`, `user_id`, `create_time`, `update_time` )
    -> VALUES
    -> ( 100, 2, 2, 2, now(), now());
ERROR 1062 (23000): Duplicate entry '100' for key 'PRIMARY'

 

结果出现了异常,这种情况我们通常称之为幻读。那么该如何解决这种场景的问题呢?其实mysql内部提供了一种叫做next-key-lock的加锁机制,可以供我们处理这类特殊情况:

mysql> select * from t_order_1 where id=100 for update;
+-----+----------+------------+---------+---------------------+---------------------+
| id  | order_no | product_id | user_id | create_time         | update_time         |
+-----+----------+------------+---------+---------------------+---------------------+
| 100 |        2 |          2 |       2 | 2020-07-14 23:06:03 | 2020-07-14 23:06:03 |
+-----+----------+------------+---------+---------------------+---------------------+
1 row in set (0.00 sec)

 

借助 **for update **语句,我们可以在应用程序的层面手工实现数据加锁保护操作。就是那些需要业务层面数据独占时,可以考虑使用** for update**。

其实 for update 可以理解为一把悲观锁,每次获取数据的时候,都担心会有其他线程修改当前的数据,因此在拿数据的时候就会加入一把锁,其他试图改写数据的请求将会处于堵塞情况。(读数据的请求不会堵塞)

面试官:那你知道mysql里面的mvcc机制吗?

小林:mvcc是啥?mvc我倒知道,之前工作中有使用过springmvc框架 blabla(希望把面试官绕开引到自己熟系的话题方向)

面试官满脸微笑地看着小林,似乎想缓解下尴尬的气氛。

面试官:好吧,你之前有对缓存了解过吗?

小林:嗯嗯,我在工作中一般喜欢使用redis作为缓存。当查询数据的时候先去redis中查询,如果redis没有再去mysql中读取数据。

面试官:嗯嗯,那你有了解过redis里面的哪些数据结构吗?

小林:嗯嗯,我在工作中有使用过string,list,hash,zset,set这几类数据结构,它们各自都有自己的特点,在使用的时候需要结合实际的业务场景来使用。

String类型可以用于存储一些简单的键值对数据,例如数字,字符串之类的。

List结构一般是采用了双端队列的结构,这类结构通常使用的命令有lpush,lpop,rpop等,如果想移除某个节点的前置和后置节点就比较简单(复杂度就是O(1)),但是搜索比较复杂。适合用于存储一些列表类型的数据信息,例如说用户的留言和评论信息。

Set 是一个无顺序的集合,比较常见的例如说交集查询,用于搜索两个好友之间共同阅读过的图书。或者两个人之间的共同好友等。使用命令sinter key [key…] 即可实现。

小林求职记(二):说好的问基础,为啥我感觉一点也不基础呢?

Hash是包含键值对的无序散列表,常用命令有:hget,hgetall等。

ZSet则是一套有序集合,通常会根据分值范围或者成员来获取元素并且计算一个键的排名。

面试官:嗯嗯,你讲的这些东西都只是停留在了表层,关于其内部的构造有去做过一些深入的了解吗?

小林:额,没有….

此时小林又一次被面试官打击了自信心

面试官:好吧,今天的一面主要只是问问基础,那么就先这样吧,我去找下我老大询问下。

小林:嗯嗯,好的。(似乎感觉还有戏)

小林一个人在前台坐着等待着下一场面试的到来,不仅内心感叹道,自己过去的工作中过多地安逸于写crud,很多java的基础问题也都记得不太清楚了,每天下班之后也没怎么学习,虽然一面只是问了些基础问题,结果却暴露了自己这么多的知识盲区。

过了不久一个陌生的男人慢慢走了过来,天啊,这家伙真的是聪明“绝顶” 了。小林一下子慌了,脑袋一片空白,二面似乎来了一位资深的大佬…..

二面面试官:你好,我是你的二面面试官,下边我可能会针对你的项目做一些询问。

(未完待续….)