天天看點

一年經驗Java開發0713面試

@

目錄

  • 介紹一下你做的某些子產品,有些什麼比較複雜的地方?
  • 你們的檔案怎麼存儲的?
  • 怎麼沒有用檔案伺服器?
  • 檔案存儲有沒有做備份?
  • 在項目上有沒有什麼搞不定的問題?
  • 對搞不定的問題你是怎麼處理的?
  • 你們項目怎麼測試?
  • MyBatis#和$有什麼差別?
  • Redis你用到它那些結構?
  • 多線程你了解多少?
  • 怎麼保證線程安全?
  • 怎麼管理線程?
  • 假如有一個List,其中存的是使用者User對象,使用者對象有很多屬性,我要根據其中的年齡屬性對List排序,這個該怎麼辦?
  • jdk 1.8的Stream用的多嗎?
  • JWT知道嗎?
  • 資料庫事務知道嗎?
  • 你寫的代碼用到事務嗎?
  • 常用的檢索優化方式有哪些?
  • Linux了解多少?

略。

我們的檔案是存儲在MongoDB中的。

MongoDB單個文檔的存儲限制是16M,如果要存儲大于16M的檔案,就要用到MongoDB GridFS。

GridFS是Mongo的一個子子產品,使用GridFS可以基于MongoDB來持久存儲檔案。并且支援分布式應用(檔案分布存儲和讀取)。作為MongoDB中二進制資料存儲在資料庫中的解決方案,通常用來處理大檔案。

GridFS使用兩個集合(collection)存儲檔案。一個集合是chunks, 用于存儲檔案内容的二進制資料;一個集合是files,用于存儲檔案的中繼資料。

GridFS會将兩個集合放在一個普通的buket中,并且這兩個集合使用buket的名字作為字首。MongoDB的GridFs預設使用fs命名的buket存放兩個檔案集合。是以存儲檔案的兩個集合分别會命名為集合fs.files ,集合fs.chunks。

GridFS存儲檔案示意圖

一年經驗Java開發0713面試

直接将檔案使用通過FTP上傳到檔案伺服器,并将檔案位址存儲到MySQL資料庫。這種方式也是可行的。

但是,檔案系統到了後期會變的很難管理,同時不利于擴充,此外我想做分布式檔案系統也顯得不那麼容易。而GridFS卻正好相反,它基于MongoDB的檔案系統,便于管理和擴充。

當然了,還有其它的一些分布式檔案存儲系統如FastDFS,可以根據檔案存儲的實際情況來進行選擇。

目前是手動備份。

後面計劃寫一個自動備份的腳本來每日備份。

#{}是預編譯處理,${}是字元串替換。

(1)mybatis在處理#{}時,會将sql中的#{}替換為?号,調用PreparedStatement的set方法來指派。

(2)mybatis在處理${}時,就是把${}替換成變量的值。

(3)使用#{}可以有效的防止SQL注入,提高系統安全性。原因在于:預編譯機制。

預編譯是提前對SQL語句進行預編譯,而其後注入的參數将不會再進行SQL編譯。我們知道,SQL注入是發生在編譯的過程中,因為惡意注入了某些特殊字元,最後被編譯成了惡意的執行操作。而預編譯機制則可以很好的防止SQL注入。

預編譯完成之後,SQL的結構已經固定,即便使用者輸入非法參數,也不會對SQL的結構産生影響,進而避免了潛在的安全風險。

主要用到了String、Hash、Set。

String:正常key-value緩存應用。用來存一些計數。

Hash: 鍵值(key => value)對集合。用來存一些對象,對應Java集合中的HashMap。

Set: set是string類型的無序集合。對應Java中的HashSet,用來存一些需要去重的資料。

  • 先說說多線程是個什麼:

    要說線程,就得先講,程序:程序可以簡單的了解為一個可以獨立運作的程式機關,它是線程的集合,程序就是有一個或多個線程構成的。而線程是程序中的實際運作機關,是作業系統進行運算排程的最小機關。可了解為線程是程序中的一個最小運作單元。

    那麼多線程就很容易了解:多線程就是指一個程序中同時有多個線程正在執行。

  • 再說說為什麼要用多線程?

    簡單說來,使用多線程就是為了提高CPU的利用效率。

  • 最後簡單說說線程的建立:在Java中有三種線程建立方式。

    繼承 Thread 類建立線程類

    實作Runnable接口建立線程類

    使用 Callable 和 Future 建立線程

保證線程安全有以下幾種方式:

  • Synchronized 關鍵字:被 Synchronized 關鍵字描述的方法或代碼塊在多線程環境下同一時間隻能由一個線程進行通路,在持有目前 Monitor 的線程執行完成之前,其他線程想要調用相關方法就必須進行排隊,知道持有持有目前 Monitor 的線程執行結束,釋放 Monitor ,下一個線程才可擷取 Monitor 執行。
  • Volatile 關鍵字:被 Volatile 關鍵字描述變量的操作具有可見性和有序性(禁止指令重排)
  • java.util.concurrent.atomic原子操作:ava.util.concurrent.atomic 包提供了一系列的 AtomicBoolean、AtomicInteger、AtomicLong 等類。使用這些類來聲明變量可以保證對其操作具有原子性來保證線程安全。
  • Lock:Lock 也是 java.util.concurrent 包下的一個接口,定義了一系列的鎖操作方法。Lock 接口主要有 ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock 實作類。與 Synchronized 不同是 Lock 提供了擷取鎖和釋放鎖等相關接口,使得使用上更加靈活,同時也可以做更加複雜的操作。

通常,會使用線程池來管理線程。

在 JDK 1.5 之後推出了相關的 api,常見的建立線程池方式有以下幾種:

  • Executors.newCachedThreadPool():無限線程池。
  • Executors.newFixedThreadPool(nThreads):建立固定大小的線程池。
  • Executors.newSingleThreadExecutor():建立單個線程的線程池。

其實看這三種方式建立的源碼就會發現:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
           

實際上還是利用 ThreadPoolExecutor 類實作的。

通常我們都是使用:

threadPool.execute(new Job());

           

這樣的方式來送出一個任務到線程池中,是以核心的邏輯就是 execute() 函數了。

線程池一共有五種狀态, 分别是:

  • RUNNING :能接受新送出的任務,并且也能處理阻塞隊列中的任務;
  • SHUTDOWN:關閉狀态,不再接受新送出的任務,但卻可以繼續處理阻塞隊列中已儲存的任務。線上程池處于 RUNNING 狀态時,調用 shutdown()方法會使線程池進入到該狀态。(finalize() 方法在執行過程中也會調用shutdown()方法進入該狀态);
  • STOP:不能接受新任務,也不處理隊列中的任務,會中斷正在處理任務的線程。線上程池處于 RUNNING 或 SHUTDOWN 狀态時,調用 shutdownNow() 方法會使線程池進入到該狀态;
  • TIDYING:如果所有的任務都已終止了,workerCount (有效線程數) 為0,線程池進入該狀态後會調用 terminated() 方法進入TERMINATED 狀态。
  • TERMINATED:在terminated() 方法執行完後進入該狀态,預設terminated()方法中什麼也沒有做。

    進入TERMINATED的條件如下:

    • 線程池不是RUNNING狀态;
    • 線程池狀态不是TIDYING狀态或TERMINATED狀态;
    • 如果線程池狀态是SHUTDOWN并且workerQueue為空;
    • workerCount為0;
    • 設定TIDYING狀态成功。

下圖為線程池的狀态轉換過程:

一年經驗Java開發0713面試

再看看Excute方法的執行:

一年經驗Java開發0713面試

1、擷取目前線程池的狀态。

2、目前線程數量小于 coreSize 時建立一個新的線程運作。

3、如果目前線程處于運作狀态,并且寫入阻塞隊列成功。

4、雙重檢查,再次擷取線程狀态;如果線程狀态變了(非運作狀态)就需要從阻塞隊列移除任務,并嘗試判斷線程是否全部執行完畢。同時執行拒絕政策。

5、如果目前線程池為空就新建立一個線程并執行。

6、如果在第三步的判斷為非運作狀态,嘗試建立線程,如果失敗則執行拒絕政策。

目前SpringBoot比較流行,我們可以發揮Spring的特性,由Spring來替我們管理線程:
@Configuration
public class TreadPoolConfig {
    /**
     * 消費隊列線程
     * @return
     */
    @Bean(value = "consumerQueueThreadPool")
    public ExecutorService buildConsumerQueueThreadPool(){
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("consumer-queue-thread-%d").build();
        ExecutorService pool = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(5),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());
        return pool ;
    }
}
           

使用時:

@Resource(name = "consumerQueueThreadPool")
private ExecutorService consumerQueueThreadPool;
@Override
public void execute() {
    //消費隊列
    for (int i = 0; i < 5; i++) {
        consumerQueueThreadPool.execute(new ConsumerQueueThread());
    }
}
           

其實也挺簡單,就是建立了一個線程池的 bean,在使用時直接從 Spring 中取出即可。

可以通過Collections類的sort方法。但需要注意,使用sort方法的時候:

  • 要麼 User類實作Comparable接口,并在類中編寫public int compareTo(T o)方法
public class User implements Comparable<User> {
	private int age;
	private String name;
	private String sex;

	@Override
	public int compareTo(User o) {
		if (this.getAge() > o.getAge()) {
			return 1;
		} else if (this.getAge() < o.getAge()) {
			return -1;
		} else {
			return 0;
		}
	}
  // ……
}
           
List<User> userList=new ArrayList();
		userList.add(new User(10, "王二", "男"));
		userList.add(new User(8, "張三", "男"));
		userList.add(new User(17, "李四", "女"));
		Collections.sort(userList);
		System.out.println(userList);
           
  • 或者在排序的時候,給sort()方法傳入一個比較器。具體來說,就是傳入一個實作比較器接口的匿名内部類。
List<User> userList=new ArrayList();
		userList.add(new User(10, "王二", "男"));
		userList.add(new User(8, "張三", "男"));
		userList.add(new User(17, "李四", "女"));
		//Collections.sort(userList);
		Collections.sort(userList, new Comparator<User>() {

			@Override
			public int compare(User o1,User o2) {
				if(o1.getAge()>o2.getAge()) {
					return 1;
				}else if(o1.getAge()<o2.getAge()) {
					return -1;
				}else {
					return 0;
				}
			}
		});
		System.out.println(userList);
           

在Java8以後可以使用Lamda表達式來進行函數式地程式設計:

userList.sort((a, b) -> Integer.compare(a.getAge(), b.getAge()));
           

Stream 作為 Java 8 的一大亮點,它與 java.io 包裡的 InputStream 和 OutputStream 是完全不相關的東西。

Java 8 中的 Stream 是對集合(Collection)對象功能的增強,它專注于對集合對象進行各種非常便利、高效的聚合操作(aggregate operation),或者大批量資料操作 (bulk data operation)。

下面是使用流的過程:

一年經驗Java開發0713面試

下面是一個使用流的執行個體,用于List的疊代:

List<String> stringList = new ArrayList<String>();

		stringList.add("one");
		stringList.add("two");
		stringList.add("three");
		stringList.add("one");

		Stream<String> stream = stringList.stream();

		stream.forEach( element -> { System.out.println(element); });

           

JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案。

傳統的session認證一般是這樣的流程:

  • 1、使用者向伺服器發送使用者名和密碼。
* 2、伺服器驗證通過後,在目前對話(session)裡面儲存相關資料,比如使用者角色、登入時間等等。
           
  • 3、伺服器向使用者傳回一個 session_id,寫入使用者的 Cookie。
  • 4、使用者随後的每一次請求,都會通過 Cookie,将 session_id 傳回伺服器。
  • 5、伺服器收到 session_id,找到前期儲存的資料,由此得知使用者的身份。
一年經驗Java開發0713面試

這種模式的問題在于,擴充性(scaling)不好。單機當然沒有問題,如果是伺服器叢集,或者是跨域的服務導向架構,就要求 session 資料共享,每台伺服器都能夠讀取 session。

一種解決方案是 session共享,将session持久化或者存入緩存。各種服務收到請求後,都向持久層或緩存請求資料。這種方案的優點是架構清晰,缺點是工程量比較大。另外,持久層或者緩存萬一挂了,就會認證失敗。

另一種方案是伺服器索性不儲存 session 資料了,所有資料都儲存在用戶端,每次請求都發回伺服器。JWT 就是這種方案的一個代表。

JWT認證流程:

  • 1、 使用者使用賬号和密碼發出post請求;
  • 2、 伺服器使用私鑰建立一個jwt;
  • 3、 伺服器傳回這個jwt給浏覽器;
  • 4、 浏覽器将該jwt串在請求頭中像伺服器發送請求;
  • 5、 伺服器驗證該jwt;
  • 6、 傳回響應的資源給浏覽器。
一年經驗Java開發0713面試

事務(TRANSACTION)是作為單個邏輯工作單元執行的一系列操作, 這些操作作為一個整體一起向系統送出,要麼都執行、要麼都不執行 。

事務是一個不可分割的工作邏輯單元事務必須具備以下四個屬性,簡稱 ACID 屬性:

  • 原子性(Atomicity) :事務是一個完整的操作。事務的各步操作是不可分的(原子的);要麼都執行,要麼都不執行。
  • 一緻性(Consistency): 當事務完成時,資料必須處于一緻狀态。
  • 隔離性(Isolation) :對資料進行修改的所有并發事務是彼此隔離的, 這表明事務必須是獨立的,它不應以任何方式依賴于或影響其他事務。
  • 永久性(Durability) : 事務完成後,它對資料庫的修改被永久保持,事務日志能夠保持事務的永久性

通過在方法加注解 @Transactional 來實作聲明式的事務。

Spring 事務管理分為編碼式和聲明式的兩種方式。程式設計式事務指的是通過編碼方式實作事務;聲明式事務基于 AOP,将具體業務邏輯與事務處了解耦。聲明式事務管理使業務代碼邏輯不受污染, 是以在實際使用中聲明式事務用的比較多。聲明式事務有兩種方式,一種是在配置檔案(xml)中做相關的事務規則聲明,另一種是基于 @Transactional 注解的方式。

  • 1、查詢語句中不要使用select *
  • 2、盡量減少子查詢,使用關聯查詢(left join,right join,inner join)替代
  • 3、減少使用IN或者NOT IN ,使用exists,not exists或者關聯查詢語句替代
  • 4、or 的查詢盡量用 union或者union all 代替(在确認沒有重複資料或者不用剔除重複資料時,union all會更好)
  • 5、應盡量避免在 where 子句中使用!=或<>操作符,否則将引擎放棄使用索引而進行全表掃描。
  • 6、應盡量避免在 where 子句中對字段進行 null 值判斷,否則将導緻引擎放棄使用索引而進行全表掃描,如: select id from t where num is null 可以在num上設定預設值0,確定表中num列沒有null值,然後這樣查詢: select id from t where num=0

(這裡應該是想問用過的指令)日産工作中,下面這些指令經常用到:

  • 檢視目前目錄:pwd
  • 切換目錄 : cd
  • 檢視目錄下的檔案 :ls/ls -lh
  • 建立目錄:mkdir
  • 啟動war包:java -jar xx.war
  • 背景啟動war包:nohup java -jar * xx.war&
  • 查找程序:ps –aux|grep java
  • 殺死程序:kill -9 pid
參考:

【1】:SpringBoot學習筆記(十一:使用MongoDB存儲檔案 )

【2】:GridFS 基于 MongoDB 的分布式檔案存儲系統

【3】:Linux下shell腳本實作mongodb定時自動備份

【4】:Mybatis中#{}和${}的差別是什麼

【5】:Redis五種資料類型及應用場景

【6】:Redis五種資料類型及應用場景

【7】:面試官:說說什麼是線程安全?一圖帶你了解java線程安全

【8】:如何優雅的使用和了解線程池

【9】:深入了解 Java 線程池:ThreadPoolExecutor

【10】:透徹的掌握 Spring 中 @Transactional的使用

【11】:SpringBoot學習筆記(十三:JWT )

【12】:Java8 Stream

繼續閱讀