- HashMap、LinkedHashMap、ConcurrentHashMap、ArrayList、LinkedList的底層實現。 答:HashMap是基於哈希表的Map接口的非同步實現,它容許null鍵和null值,且HashMap依託於它的數據結構的設計,存儲效率特別高,這是我用它的緣由 HashMap是基於hash算法實現的,經過put(key,value)存儲對象到HashMap中,也能夠經過get(key)從HashMap中獲取對象。當咱們使用put的時候,首先HashMap會對key的hashCode()的值進行hash計算,根據hash值獲得這個元素在數組中的位置,將元素存儲在該位置的鏈表上。當咱們使用get的時候,首先HashMap會對key的hashCode()的值進行hash計算,根據hash值獲得這個元素在數組中的位置,將元素從該位置上的鏈表中取出
- HashMap和Hashtable的區別。 Hashtable方法是同步的 HashMap方法是非同步的 Hashtable基於Dictionary類 HashMap基於AbstractMap,而AbstractMap基於Map接口的實現 Hashtable中key和value都不容許爲null,遇到null,直接返回 NullPointerException HashMap中key和value都容許爲null,遇到key爲null的時候,調用putForNullKey方法進行處理,而對value沒有處理 Hashtable中hash數組默認大小是11,擴充方式是old*2+1 HashMap中hash數組的默認大小是16,並且必定是2的指數
- ArrayList、LinkedList、Vector的區別。 LinkedList底層是雙向鏈表 ArrayList底層是可變數組 LinkedList不容許隨機訪問,即查詢效率低 ArrayList容許隨機訪問,即查詢效率高 LinkedList插入和刪除效率快 ArrayList插入和刪除效率低
Vector是基於可變數組的List接口的同步實現 Vector是有序的 Vector容許null鍵和null值 Vector已經不建議使用了java
- LinkedHashMap與HashMap的區別 LinkedHashMap有序的,有插入順序和訪問順序 HashMap無序的 LinkedHashMap內部維護着一個運行於全部條目的雙向鏈表 HashMap內部維護着一個單鏈表
- HashMap和ConcurrentHashMap的區別。 ConcurrentHashMap基於雙數組和鏈表的Map接口的同步實現 ConcurrentHashMap中元素的key是惟一的、value值可重複 ConcurrentHashMap不容許使用null值和null鍵 ConcurrentHashMap是無序的線程安全的
- HashMap和LinkedHashMap的區別。 LinkedHashMap有序的,有插入順序和訪問順序 HashMap無序的 LinkedHashMap內部維護着一個運行於全部條目的雙向鏈表 HashMap內部維護着一個單鏈表
LinkedHashMap底層使用哈希表與雙向鏈表來保存全部元素,它維護着一個運行於全部條目的雙向鏈表(若是學過雙向鏈表的同窗會更好的理解它的源代碼),此鏈表定義了迭代順序,該迭代順序能夠是插入順序或者是訪問順序 1.按插入順序的鏈表:在LinkedHashMap調用get方法後,輸出的順序和輸入時的相同,這就是按插入順序的鏈表,默認是按插入順序排序 2.按訪問順序的鏈表:在LinkedHashMap調用get方法後,會將此次訪問的元素移至鏈表尾部,不斷訪問能夠造成按訪問順序排序的鏈表。簡單的說,按最近最少訪問的元素進行排序(相似LRU算法)linux
- HashMap是線程安全的嗎。 不是線程安全的,方法一:經過Collections.synchronizedMap()返回一個新的Map,這個新的map就是線程安全的. 這個要求你們習慣基於接口編程,由於返回的並非HashMap,而是一個Map的實現. 方法二:從新改寫了HashMap,具體的能夠查看java.util.concurrent.ConcurrentHashMap. 這個方法比方法一有了很大的改進. 方法一使用的是的synchronized方法,是一種悲觀鎖.在進入以前須要得到鎖,確保獨享當前對象,而後作相應的修改/讀取. 方法二使用的是樂觀鎖,只有在須要修改對象時,比較和以前的值是否被人修改了,若是被其餘線程修改了,那麼就會返回失敗.鎖的實現,使用的是NonfairSync. 這個特性要確保修改的原子性,互斥性,沒法在JDK這個級別獲得解決,JDK在這次須要調用JNI方法,而JNI則調用CAS指令來確保原子性與互斥性. 當若是多個線程剛好操做到ConcurrentHashMap同一個segment上面,那麼只會有一個線程獲得運行,其餘的線程會被LockSupport.park(),稍後執行完成後,會自動挑選一個線程來執行LockSupport.unpark(). 如何獲得/釋放鎖 獲得鎖: 方法一:在Hashmap上面,synchronized鎖住的是對象(不是Class),因此第一個申請的獲得鎖,其餘線程將進入阻塞,等待喚醒. 方法二:檢查AbstractQueuedSynchronizer.state,若是爲0,則獲得鎖,或者申請者已經獲得鎖,則也能再辭獲得鎖,而且state也加1. 釋放鎖:都是獲得鎖的逆操做,而且使用正確,二種方法都是自動選取一個隊列中的線程獲得鎖能夠得到CPU資源. 優缺點 方法一:優勢:代碼實現十分簡單,一看就懂. 缺點:從鎖的角度來看,方法一直接使用了鎖住方法,基本上是鎖住了儘量大的代碼塊.性能會比較差. 方法二:優勢:須要互斥的代碼段比較少,性能會比較好. ConcurrentHashMap把整個Map切分紅了多個塊,發生鎖碰撞的概率大大下降,性能會比較好. 缺點:代碼實現稍稍複雜些.
- ConcurrentHashMap是怎麼實現線程安全的。 ConcurrentHashMap的數據結構爲一個Segment數組,Segment的數據結構爲HashEntry的數組,而HashEntry存的是咱們的鍵值對,能夠構成鏈表。能夠簡單的理解爲數組裏裝的是HashMap ConcurrentHashMap定位一個元素的過程須要進行兩次Hash操做,第一次Hash定位到Segment,第二次Hash定位到元素所在的鏈表的頭部,所以,這一種結構的帶來的反作用是Hash的過程要比普通的HashMap要長,可是帶來的好處是寫操做的時候能夠只對元素所在的Segment進行加鎖便可,不會影響到其餘的Segment。正是由於其內部的結構以及機制,ConcurrentHashMap在併發訪問的性能上要比Hashtable和同步包裝以後的HashMap的性能提升不少。在理想狀態下,ConcurrentHashMap 能夠支持 16 個線程執行併發寫操做(若是併發級別設置爲 16),及任意數量線程的讀操做
- HashMap 的長度爲何是2的冪次方 由於HashMap的數據結構是數組和鏈表的結合,其中裏面數組的默認長度是16,HashMap的長度爲2的冪次方的緣由是爲了減小Hash碰撞,儘可能使Hash算法的結果均勻分佈。
多線程併發相關問題(必問)redis
- 建立線程的3種方式。 1、繼承Thread類建立線程類 (1)定義Thread類的子類,並重寫該類的run方法,該run方法的方法體就表明了線程要完成的任務。所以把run()方法稱爲執行體。 (2)建立Thread子類的實例,即建立了線程對象。 (3)調用線程對象的start()方法來啓動該線程。 2、經過Runnable接口建立線程類 (1)定義runnable接口的實現類,並重寫該接口的run()方法,該run()方法的方法體一樣是該線程的線程執行體。 (2)建立 Runnable實現類的實例,並依此實例做爲Thread的target來建立Thread對象,該Thread對象纔是真正的線程對象。 (3)調用線程對象的start()方法來啓動該線程。 3、經過Callable和Future建立線程 (1)建立Callable接口的實現類,並實現call()方法,該call()方法將做爲線程執行體,而且有返回值。 (2)建立Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。 (3)使用FutureTask對象做爲Thread對象的target建立並啓動新線程。 (4)調用FutureTask對象的get()方法來得到子線程執行結束後的返回值
2、建立線程的三種方式的對比 採用實現Runnable、Callable接口的方式創見多線程時,優點是: 線程類只是實現了Runnable接口或Callable接口,還能夠繼承其餘類。 在這種方式下,多個線程能夠共享同一個target對象,因此很是適合多個相同線程來處理同一份資源的狀況,從而能夠將CPU、代碼和數據分開,造成清晰的模型,較好地體現了面向對象的思想。算法
劣勢是: 編程稍微複雜,若是要訪問當前線程,則必須使用Thread.currentThread()方法。 使用繼承Thread類的方式建立多線程時優點是: 編寫簡單,若是須要訪問當前線程,則無需使用Thread.currentThread()方法,直接使用this便可得到當前線程。spring
劣勢是: 線程類已經繼承了Thread類,因此不能再繼承其餘父類。數據庫
- 什麼是線程安全。 線程安全就是在多線程環境下也不會出現數據不一致,而非線程安全就有可能出現數據不一致的狀況。
- Runnable接口和Callable接口的區別。 不一樣之處: 1.Callable能夠返回一個類型,而Runnable不能夠 2.Callable可以拋出checked exception,而Runnable不能夠。 3.Runnable是自從java1.1就有了,而Callable是1.5以後才加上去的 4.Callable和Runnable均可以應用於executors。而Thread類只支持Runnable.
- wait方法和sleep方法的區別。 sleep()方法必須傳入參數,參數就是休眠時間,時間到了就會自動醒來 wait()方法能夠傳入參數也能夠不傳入參數,傳入參數就是在參數結束的時間後開始等待,不穿如參數就是直接等待。 sleep方法必需要捕獲異常,而wait方法不須要捕獲異常。
- synchronized、Lock、ReentrantLock、ReadWriteLock。 synchronized: 是JVM實現的一種鎖, 用於同步方法和代碼塊,執行完後自動釋放鎖。其中鎖的獲取和釋放分別是monitorenter和monitorexit指令,該鎖在實現上分爲了偏向鎖、輕量級鎖和重量級鎖,其中偏向鎖在1.6是默認開啓的,輕量級鎖在多線程競爭的狀況下會膨脹成重量級鎖,有關鎖的數據都保存在對象頭中。 Lock:Lock是一個鎖的接口,提供獲取鎖和解鎖的方法(lock,trylock,unlock) ReentrantLock:ReentrantLock是k是Lock接口的實現類,基於AQS實現的,在AQS內部會保存一個狀態變量state,經過CAS修改該變量的值,修改爲功的線程表示獲取到該鎖,沒有修改爲功,或者發現狀態state已是加鎖狀態,則經過一個Waiter對象封裝線程,添加到等待隊列中,並掛起等待被喚醒。 ReadWriteLock :能夠實現讀寫鎖,當讀取的時候線程會得到read鎖,其餘線程也能夠獲 得read鎖同時併發的去讀取,可是寫程序運行獲取到write鎖的時候,其餘線程是不能進行操做的,由於write是排它鎖,而上面介紹的兩種無論你是read仍是write沒有搶到鎖的線程都會被阻塞或者中斷,它也是個接口,裏面定義了兩種方法readLock()和writeLock(),他的一個實現類是ReentrantReadWriteLock。
- 介紹下CAS(無鎖技術)。 CAS(Compare and Swap),即比較並替換,實現併發算法時經常使用到的一種技術,CAS是經過unsafe類的compareAndSwap方法實現的。CAS的思想很簡單:三個參數,一個當前內存值V、舊的預期值A、即將更新的值B,當且僅當預期值A和內存值V相同時,將內存值修改成B並返回true,不然什麼都不作,並返回false。 缺點:CAS存在一個很明顯的問題,即ABA問題。 問題:若是變量V初次讀取的時候是A,而且在準備賦值的時候檢查到它仍然是A,那能說明它的值沒有被其餘線程修改過了嗎? 若是在這段期間曾經被改爲B,而後又改回A,那CAS操做就會誤認爲它歷來沒有被修改過。針對這種狀況,java併發包中提供了一個帶有標記的原子引用類AtomicStampedReference,它能夠經過控制變量值的版原本保證CAS的正確性。
- volatile關鍵字的做用和原理。 做用:保持內存可見性(內存可見性(Memory Visibility):全部線程都能看到共享內存的最新狀態。) 原理:觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令,lock前綴指令實際上至關於一個內存屏障(也成內存柵欄)它會強制將對緩存的修改操做當即寫入主存若是是寫操做,它會致使其餘CPU中對應的緩存行無效
- 什麼是ThreadLocal。 線程變量,一個以ThreadLocal對象爲鍵、任意對象爲值的存儲結構,每一個ThreadLocal能夠放一個線程級別的變量,可是它本事能夠被多個線程共享使用,並且又能夠達到線程安全的目的,且絕對線程安全。 經常使用的3個方法:set()、get()、remove()。都是線程安全的。
- 建立線程池的4種方式。 1.newSingleThreadExecutor 建立一個單線程的線程池。這個線程池只有一個線程在工做,也就是至關於單線程串行執行全部任務。若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它。此線程池保證全部任務的執行順序按照任務的提交順序執行。 2.newFixedThreadPool 建立固定大小的線程池。每次提交一個任務就建立一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,若是某個線程由於執行異常而結束,那麼線程池會補充一個新線程。 3.newCachedThreadPool 建立一個可緩存的線程池。若是線程池的大小超過了處理任務所須要的線程, 那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增長時,此線程池又能夠智能的添加新線程來處理任務。此線程池不會對線程池大小作限制,線程池大小徹底依賴於操做系統(或者說JVM)可以建立的最大線程大小。 4.newScheduledThreadPool 建立一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。
- ThreadPoolExecutor的內部工做原理。
- 分佈式環境下,怎麼保證線程安全。 向線程池提交一個任務後,它的主要處理流程以下圖所示 一個線程從被提交(submit)到執行共經歷如下流程: 線程池判斷核心線程池裏是的線程是否都在執行任務,若是不是,則建立一個新的工做線程來執行任務。若是核心線程池裏的線程都在執行任務,則進入下一個流程 線程池判斷工做隊列是否已滿。若是工做隊列沒有滿,則將新提交的任務儲存在這個工做隊列裏。若是工做隊列滿了,則進入下一個流程。 線程池判斷其內部線程是否都處於工做狀態。若是沒有,則建立一個新的工做線程來執行任務。若是已滿了,則交給飽和策略來處理這個任務。
JVM相關問題編程
-
介紹下垃圾收集機制(在何時,對什麼,作了什麼)。 標記-清除算法、複製算法、標記-整理算法、分代收集算法 主要是對一些靜態對象作清理和整理的工做bootstrap
-
垃圾收集有哪些算法,各自的特色。 標記-清除算法、複製算法、標記-整理算法、分代收集算法小程序
-
類加載的過程。 整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載七個階段。微信小程序
-
雙親委派模型。 雙親委託模型的工做過程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委託給父類加載器去完成,每個層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父類加載器反饋本身沒法完成這個加載請求(它的搜索範圍中沒有找到所須要加載的類)時,子加載器纔會嘗試本身去加載。 使用雙親委託機制的好處是:可以有效確保一個類的全局惟一性,當程序中出現多個限定名相同的類時,類加載器在執行加載時,始終只會加載其中的某一個類。
-
有哪些類加載器。 分別是 bootstrapClassLoader (主要加載java核心api) , ExtClassLoaders是擴展類的類加載器,AppClassLoader 程序類加載器,還有一個是用戶繼承ClassLoader重寫的類加載器。
-
能不能本身寫一個類叫java.lang.String。 能夠,可是即便你寫了這個類,也沒有用。 這個問題涉及到加載器的委託機制,在類加載器的結構圖(在下面)中, BootStrap是頂層父類,ExtClassLoader是BootStrap類的子類,ExtClassLoader又是AppClassLoader的父類 這裏以java.lang.String爲例,當我是使用到這個類時,Java虛擬機會將java.lang.String類的字節碼加載到內存中。
數據庫相關問題,針對MySQL(必問)
- 給題目讓你手寫SQL。
- 有沒有SQL優化經驗。
- MySQL索引的數據結構。
- SQL怎麼進行優化。
- SQL關鍵字的執行順序。 from->on->join->where->group by->having->select->distinct->union
- 有哪幾種索引。 普通索引:僅加速查詢 惟一索引:加速查詢 + 列值惟一(能夠有null) 主鍵索引:加速查詢 + 列值惟一(不能夠有null)+ 表中只有一個 組合索引:多列值組成一個索引,專門用於組合搜索,其效率大於索引合併 全文索引:對文本的內容進行分詞,進行搜索
- 何時該(不應)建索引。 通常來講,在WHERE和JOIN中出現的列須要創建索引,但也不徹底如此,由於MySQL只對<,<=,=,>,>=,BETWEEN,IN,以及某些時候的LIKE纔會使用索引。
- Explain包含哪些列。 expain出來的信息有10列,分別是id、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra,
框架相關問題
-
Hibernate和Mybatis的區別。
-
Spring MVC和Struts2的區別。
-
Spring用了哪些設計模式。 1.工廠模式,這個很明顯,在各類BeanFactory以及ApplicationContext建立中都用到了; 2.模版模式,這個也很明顯,在各類BeanFactory以及ApplicationContext實現中也都用到了; 3.代理模式,在Aop實現中用到了JDK的動態代理; 4.單例模式,這個好比在建立bean的時候。 5.策略模式在Java中的應用,這個太明顯了,由於Comparator這個接口簡直就是爲策略模式而生的。Comparable和Comparator的區別一文中,詳細講了Comparator的使用。比方說Collections裏面有一個sort方法,由於集合裏面的元素有多是複合對象,複合對象並不像基本數據類型,能夠根據大小排序,複合對象怎麼排序呢?基於這個問題考慮,Java要求若是定義的複合對象要有排序的功能,就自行實現Comparable接口或Comparator接口. 6.原型模式:使用原型模式建立對象比直接new一個對象在性能上好得多,由於Object類的clone()方法是一個native方法,它直接操做內存中的二進制流,特別是複製大對象時,性能的差異很是明顯。 7.迭代器模式:Iterable接口和Iterator接口 這兩個都是迭代相關的接口,能夠這麼認爲,實現了Iterable接口,則表示某個對象是可被迭代的;Iterator接口至關因而一個迭代器,實現了Iterator接口,等於具體定義了這個可被迭代的對象時如何進行迭代的
-
Spring中AOP主要用來作什麼。 1.Spring聲明式事務管理配置。 2.Controller層的參數校驗。 3.使用Spring AOP實現MySQL數據庫讀寫分離案例分析 4.在執行方法前,判斷是否具備權限。 5.對部分函數的調用進行日誌記錄。監控部分重要函數,若拋出指定的異常,能夠以短信或郵件方式通知相關人員。 6.信息過濾,頁面轉發等等功能,博主一我的的力量有限,只能列舉這麼多,歡迎評論區對文章作補充。
-
Spring注入bean的方式。 使用屬性的setter方法注入 這是最經常使用的方式; 使用構造器注入; 使用Filed注入(接口注入).
-
什麼是IOC,什麼是依賴注入。 inverse of controll IOC所謂控制反轉就是把建立對象(bean)和維護對象(bean)的關係的權利從程序中轉移到spring的容器文件(就是spring的配置文件),程序再也不維護 依賴注入實在編譯階段商未知所需的功能來自哪一個類的狀況下,將其餘的對象所依賴的功能對象實例化的模式。這就須要一種極致來激活相應的組件已提供特定的功能,所依賴注入是控制反轉的基礎。不然若是在組件不受框架控制的狀況下,框架有怎麼知道要建立那個組件?
-
Spring是單例仍是多例,怎麼修改。 spring默認是單例模式(就每一個請求都是用的同一對象,對於dao層確定是棒棒的),可是有的時候,咱們須要每一個請求都 產生一個新的對象,就如作微信小程序,用scoket、不可能一直都用一個來接收的,由於須要分配房間,因此須要使用到多例。 能夠修改成多例,在此bean節點中添加一個屬性,scope=「prototype",例如
-
Spring事務隔離級別和傳播性。
-
介紹下Mybatis/Hibernate的緩存機制。
-
Mybatis的mapper文件中#和$的區別。
-
Mybatis的mapper文件中resultType和resultMap的區別。
其餘遇到問題
- 介紹下棧和隊列。
- IO和NIO的區別。 Java NIO和IO之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取全部字節,它們沒有被緩存在任何地方。 Java NIO的緩衝導向方法略有不一樣。數據讀取到一個它稍後處理的緩衝區,須要時可在緩衝區中先後移動。這就增長了處理過程當中的靈活性。
- 接口和抽象類的區別。 接口和抽象類的概念不同。接口是對動做的抽象,抽象類是對根源的抽象。 一、抽象類和接口都不能直接實例化,若是要實例化,抽象類變量必須指向實現全部抽象方法的子類對象,接口變量必須指向實現全部接口方法的類對象。 二、抽象類要被子類繼承,接口要被類實現。 三、接口只能作方法申明,抽象類中能夠作方法申明,也能夠作方法實現 四、接口裏定義的變量只能是公共的靜態的常量,抽象類中的變量是普通變量。 五、抽象類裏的抽象方法必須所有被子類所實現,若是子類不能所有實現父類抽象方法,那麼該子類只能是抽象類。一樣,一個實現接口的時候,如不能所有實現接口方法,那麼該類也只能爲抽象類。 六、抽象方法只能申明,不能實現,接口是設計的結果 ,抽象類是重構的結果 七、抽象類裏能夠沒有抽象方法 八、若是一個類裏有抽象方法,那麼這個類只能是抽象類 九、抽象方法要被實現,因此不能是靜態的,也不能是私有的。 十、接口可繼承接口,並可多繼承接口,但類只能單根繼承。
- int和Integer的自動拆箱/裝箱相關問題。
- 常量池相關問題。
- ==和equals的區別。
- 什麼是JDK?什麼是JRE?什麼是JVM?三者之間的聯繫與區別
- Java和C++的區別
- 重載和重寫的區別。
- String和StringBuilder、StringBuffer的區別。 一、首先說運行速度,或者說是執行速度,在這方面運行速度快慢爲:StringBuilder > StringBuffer > String String最慢的緣由: String爲字符串常量,而StringBuilder和StringBuffer均爲字符串變量,即String對象一旦建立以後該對象是不可更改的,但後二者的對象是變量,是能夠更改的。 二、在線程安全上,StringBuilder是線程不安全的,而StringBuffer是線程安全的 總結一下 String:適用於少許的字符串操做的狀況 StringBuilder:適用於單線程下在字符緩衝區進行大量操做的狀況 StringBuffer:適用多線程下在字符緩衝區進行大量操做的狀況
- 靜態變量、實例變量、局部變量線程安全嗎,爲何。 靜態變量:線程非安全。加static關鍵字的變量,只能放在類裏,不能放到方法裏。靜態變量有默認初始化值。一旦靜態變量被修改,其餘對象均對修改可見,故線程非安全。 實例變量:單例模式(只有一個對象實例存在)線程非安全,非單例線程安全。 局部變量:線程安全。 一、局部變量只定義在局部範圍內,如:函數內,for循環語句內等,只在所屬的區域有效。 二、局部變量存在於棧內存中,做用的範圍結束,變量空間會自動釋放。 三、局部變量沒有默認初始化值 四、在使用變量時須要遵循的原則爲:就近原則,先找局部變量,再找實例變量(若是同名,實例變量須要用this關鍵字引用) 五、局部變量不能逐級重名,好比函數內定義過一個變量,就不能在for循環內定義相同的變量(兩層for循環一個用i一個用j也是這個道理)因爲每一個線程執行時將會把局部變量放在各自棧幀的工做內存中,線程間不共享,故不存在線程安全問題。
- try、catch、finally都有return語句時執行哪一個。 任何執行try 或者catch中的return語句以前,都會先執行finally語句,若是finally存在的話。 若是finally中有return語句,那麼程序就return了,因此finally中的return是必定會被return的, 編譯器把finally中的return實現爲一個warning。
- 介紹下B樹、二叉樹。
- 分佈式鎖的實現。 基於數據庫實現分佈式鎖 基於緩存(Redis,memcached,tair)實現分佈式鎖 基於Zookeeper實現分佈式鎖 基於數據庫表
要實現分佈式鎖,最簡單的方式可能就是直接建立一張鎖表,而後經過操做該表中的數據來實現了。 當咱們要鎖住某個方法或資源時,咱們就在該表中增長一條記錄,想要釋放鎖的時候就刪除這條記錄。 建立這樣一張數據庫表: CREATE TABLE methodLock
( id
int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', method_name
varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的方法名', desc
varchar(1024) NOT NULL DEFAULT '備註信息', update_time
timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存數據時間,自動生成', PRIMARY KEY (id
), UNIQUE KEY uidx_method_name
(method_name
) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法'; 當咱們想要鎖住某個方法時,執行如下SQL: insert into methodLock(method_name,desc) values (‘method_name’,‘desc’) 由於咱們對method_name作了惟一性約束,這裏若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功,那麼咱們就能夠認爲操做成功的那個線程得到了該方法的鎖,能夠執行方法體內容。 當方法執行完畢以後,想要釋放鎖的話,須要執行如下Sql: delete from methodLock where method_name ='method_name' 上面這種簡單的實現有如下幾個問題: 一、這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會致使業務系統不可用。 二、這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在數據庫中,其餘線程沒法再得到到鎖。 三、這把鎖只能是非阻塞的,由於數據的insert操做,一旦插入失敗就會直接報錯。沒有得到鎖的線程並不會進入排隊隊列,要想再次得到鎖就要再次觸發得到鎖操做。 四、這把鎖是非重入的,同一個線程在沒有釋放鎖以前沒法再次得到該鎖。由於數據中數據已經存在了。 固然,咱們也能夠有其餘方式解決上面的問題。 數據庫是單點?搞兩個數據庫,數據以前雙向同步。一旦掛掉快速切換到備庫上。 沒有失效時間?只要作一個定時任務,每隔必定時間把數據庫中的超時數據清理一遍。 非阻塞的?搞一個while循環,直到insert成功再返回成功。 非重入的?在數據庫表中加個字段,記錄當前得到鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,若是當前機器的主機信息和線程信息在數據庫能夠查到的話,直接把鎖分配給他就能夠了。 基於數據庫排他鎖
除了能夠經過增刪操做數據表中的記錄之外,其實還能夠藉助數據中自帶的鎖來實現分佈式的鎖。 咱們還用剛剛建立的那張數據庫表。能夠經過數據庫的排他鎖來實現分佈式鎖。 基於MySQL的InnoDB引擎,可使用如下方法來實現加鎖操做: public boolean lock(){ connection.setAutoCommit(false) while(true){ try{ result = select * from methodLock where method_name=xxx for update; if(result==null){ return true; } }catch(Exception e){
}
sleep(1000);
}
return false;
複製代碼
} 在查詢語句後面增長for update,數據庫會在查詢過程當中給數據庫表增長排他鎖。當某條記錄被加上排他鎖以後,其餘線程沒法再在該行記錄上增長排他鎖。 咱們能夠認爲得到排它鎖的線程便可得到分佈式鎖,當獲取到鎖以後,能夠執行方法的業務邏輯,執行完方法以後,再經過如下方法解鎖: public void unlock(){ connection.commit(); } 經過connection.commit()操做來釋放鎖。 這種方法能夠有效的解決上面提到的沒法釋放鎖和阻塞鎖的問題。 阻塞鎖? for update語句會在執行成功後當即返回,在執行失敗時一直處於阻塞狀態,直到成功。 鎖定以後服務宕機,沒法釋放?使用這種方式,服務宕機以後數據庫會本身把鎖釋放掉。 可是仍是沒法直接解決數據庫單點和可重入問題。 總結
總結一下使用數據庫來實現分佈式鎖的方式,這兩種方式都是依賴數據庫的一張表,一種是經過表中的記錄的存在狀況肯定當前是否有鎖存在,另一種是經過數據庫的排他鎖來實現分佈式鎖。 數據庫實現分佈式鎖的優勢 直接藉助數據庫,容易理解。 數據庫實現分佈式鎖的缺點 會有各類各樣的問題,在解決問題的過程當中會使整個方案變得愈來愈複雜。 操做數據庫須要必定的開銷,性能問題須要考慮。 基於緩存實現分佈式鎖
相比較於基於數據庫實現分佈式鎖的方案來講,基於緩存來實如今性能方面會表現的更好一點。並且不少緩存是能夠集羣部署的,能夠解決單點問題。 目前有不少成熟的緩存產品,包括Redis,memcached以及咱們公司內部的Tair。 這裏以Tair爲例來分析下使用緩存實現分佈式鎖的方案。關於Redis和memcached在網絡上有不少相關的文章,而且也有一些成熟的框架及算法能夠直接使用。 基於Tair的實現分佈式鎖其實和Redis相似,其中主要的實現方式是使用TairManager.put方法來實現。 public boolean trylock(String key) { ResultCode code = ldbTairManager.put(NAMESPACE, key, "This is a Lock.", 2, 0); if (ResultCode.SUCCESS.equals(code)) return true; else return false; } public boolean unlock(String key) { ldbTairManager.invalid(NAMESPACE, key); } 以上實現方式一樣存在幾個問題: 一、這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在tair中,其餘線程沒法再得到到鎖。 二、這把鎖只能是非阻塞的,不管成功仍是失敗都直接返回。 三、這把鎖是非重入的,一個線程得到鎖以後,在釋放鎖以前,沒法再次得到該鎖,由於使用到的key在tair中已經存在。沒法再執行put操做。 固然,一樣有方式能夠解決。 沒有失效時間?tair的put方法支持傳入失效時間,到達時間以後數據會自動刪除。 非阻塞?while重複執行。 非可重入?在一個線程獲取到鎖以後,把當前主機信息和線程信息保存起來,下次再獲取以前先檢查本身是否是當前鎖的擁有者。 可是,失效時間我設置多長時間爲好?如何設置的失效時間過短,方法沒等執行完,鎖就自動釋放了,那麼就會產生併發問題。若是設置的時間太長,其餘獲取鎖的線程就可能要平白的多等一段時間。這個問題使用數據庫實現分佈式鎖一樣存在 總結
可使用緩存來代替數據庫來實現分佈式鎖,這個能夠提供更好的性能,同時,不少緩存服務都是集羣部署的,能夠避免單點問題。而且不少緩存服務都提供了能夠用來實現分佈式鎖的方法,好比Tair的put方法,redis的setnx方法等。而且,這些緩存服務也都提供了對數據的過時自動刪除的支持,能夠直接設置超時時間來控制鎖的釋放。 使用緩存實現分佈式鎖的優勢 性能好,實現起來較爲方便。 使用緩存實現分佈式鎖的缺點 經過超時時間來控制鎖的失效時間並非十分的靠譜。 基於Zookeeper實現分佈式鎖
基於zookeeper臨時有序節點能夠實現的分佈式鎖。 大體思想即爲:每一個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個惟一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只須要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除便可。同時,其能夠避免服務宕機致使的鎖沒法釋放,而產生的死鎖問題。 來看下Zookeeper能不能解決前面提到的問題。 鎖沒法釋放?使用Zookeeper能夠有效的解決鎖沒法釋放的問題,由於在建立鎖的時候,客戶端會在ZK中建立一個臨時節點,一旦客戶端獲取到鎖以後忽然掛掉(Session鏈接斷開),那麼這個臨時節點就會自動刪除掉。其餘客戶端就能夠再次得到鎖。 非阻塞鎖?使用Zookeeper能夠實現阻塞的鎖,客戶端能夠經過在ZK中建立順序節點,而且在節點上綁定監聽器,一旦節點有變化,Zookeeper會通知客戶端,客戶端能夠檢查本身建立的節點是否是當前全部節點中序號最小的,若是是,那麼本身就獲取到鎖,即可以執行業務邏輯了。 不可重入?使用Zookeeper也能夠有效的解決不可重入的問題,客戶端在建立節點的時候,把當前客戶端的主機信息和線程信息直接寫入到節點中,下次想要獲取鎖的時候和當前最小的節點中的數據比對一下就能夠了。若是和本身的信息同樣,那麼本身直接獲取到鎖,若是不同就再建立一個臨時的順序節點,參與排隊。 單點問題?使用Zookeeper能夠有效的解決單點問題,ZK是集羣部署的,只要集羣中有半數以上的機器存活,就能夠對外提供服務。 能夠直接使用zookeeper第三方庫Curator客戶端,這個客戶端中封裝了一個可重入的鎖服務。 public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { try { return interProcessMutex.acquire(timeout, unit); } catch (Exception e) { e.printStackTrace(); } return true; } public boolean unlock() { try { interProcessMutex.release(); } catch (Throwable e) { log.error(e.getMessage(), e); } finally { executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS); } return true; } Curator提供的InterProcessMutex是分佈式鎖的實現。acquire方法用戶獲取鎖,release方法用於釋放鎖。 使用ZK實現的分佈式鎖好像徹底符合了本文開頭咱們對一個分佈式鎖的全部指望。可是,其實並非,Zookeeper實現的分佈式鎖其實存在一個缺點,那就是性能上可能並無緩存服務那麼高。由於每次在建立鎖和釋放鎖的過程當中,都要動態建立、銷燬瞬時節點來實現鎖功能。ZK中建立和刪除節點只能經過Leader服務器來執行,而後將數據同不到全部的Follower機器上。 總結
使用Zookeeper實現分佈式鎖的優勢 有效的解決單點問題,不可重入問題,非阻塞問題以及鎖沒法釋放的問題。實現起來較爲簡單。 使用Zookeeper實現分佈式鎖的缺點 性能上不如使用緩存實現分佈式鎖。 須要對ZK的原理有所瞭解。 三種方案的比較
從理解的難易程度角度(從低到高)
數據庫 > 緩存 > Zookeeper 從實現的複雜性角度(從低到高)
Zookeeper >= 緩存 > 數據庫 從性能角度(從高到低)
緩存 > Zookeeper >= 數據庫 從可靠性角度(從高到低)
Zookeeper > 緩存 > 數據庫
- 分佈式session存儲解決方案。
- 經常使用的linux命令。
- redis集羣原理。 1、設置redis服務器先通過CRC16哈希到一個指定的Node上範圍是0-16384 (平均分配,不能重複也不能缺失,不然會致使對象重複存儲或沒法存儲,好比:三臺啊服務器:節點1分配0-5600,節點二分配應該書5601-12000,節點3,12001-16384). 2、當數據要保存到redis時,經過CRC16哈希到一個指定RC16哈希值,保存在對應的節點上。 3、獲取,當要獲取一個數據時,先經過key獲取到RC16哈希值,再經過RC16哈希值找到對應的節點,而後就能在對應的節點立刻找到kye的值了。