剛剛過了一下阿里Java開發手冊,發現很多內容都滿重要.這裏挑了比較重要,或者容易踩的坑進行了記錄.
看到條數不要害怕,其實很多都是常識,爲了加深一下印象而已.
Table of Contents
1.POJO 類中布爾類型變量都不要加 is 前綴,否則部分框架解析會引起序列化錯誤。
2.Object 的 equals 方法容易拋空指針異常,應使用常量或確定有值的對象來調用equals。
3.所有整型包裝類對象之間值的比較,全部使用 equals 方法比較。
4.浮點數之間的等值判斷,基本數據類型不能用==來比較,包裝數據類型不能用 equals 來判斷。
5.定義數據對象 DO 類時,屬性類型要與數據庫字段類型相匹配。
6.禁止使用構造方法 BigDecimal(double)的方式把 double 值轉化爲 BigDecimal 對象。
8.關於 hashCode 和 equals 的處理,遵循如下規則:
9.使用集合轉數組的方法,必須使用集合的 toArray(T[] array),傳入的是類型完全一致、長度爲 0 的空數組。
10.在使用 Collection 接口任何實現類的 addAll()方法時,都要對輸入的集合參數進行 NPE 判斷。
12. 不要在 foreach 循環裏進行元素的 remove/add 操作。
14. 使用 entrySet 遍歷 Map 類集合 KV,而不是 keySet 方式進行遍歷。
15. Map 類集合 K/V 能不能存儲 null 值的情況
19. 在使用嘗試機制來獲取鎖的方式中,進入業務代碼塊之前,必須先判斷當前線程是否持有鎖。鎖的釋放規則與鎖的阻塞等待方式相同
26.當 switch 括號內的變量類型爲 String 並且此變量爲外部參數時,必須先進行 null 判斷。
27.在高併發場景中,避免使用”等於”判斷作爲中斷或退出的條件。
28.注意 Math.random() 這個方法返回是 double 類型
30.日期格式化時,傳入 pattern 中表示年份統一使用小寫的 y。
31.finally 塊必須對資源對象、流對象進行關閉,有異常也要做 try-catch
36.避免重複打印日誌,浪費磁盤空間,務必在 log4j.xml 中設置 additivity=false。
38.單元測試中不準使用 System.out 來進行人肉驗證,必須使用 assert 來驗證。
39.小數類型爲 decimal,禁止使用 float 和 double。
41.單錶行數超過 500 萬行或者單表容量超過 2GB,才推薦進行分庫分表。
42.業務上具有唯一特性的字段,即使是多個字段的組合,也必須建成唯一索引。
44.在 varchar 字段上建立索引時,必須指定索引長度,沒必要對全字段建立索引,根據實際文本區分度決定索引長度即可。
45.頁面搜索嚴禁左模糊或者全模糊,如果需要請走搜索引擎來解決。
47.SQL 性能優化的目標:至少要達到 range 級別,要求是 ref 級別,如果可以是 consts 最好。
48.不要使用 count(列名)或 count(常量)來替代 count(*).
49.count(distinct col) 計算該列除 NULL 之外的不重複行數
50.當某一列的值全是 NULL 時,count(col)的返回結果爲 0,但 sum(col)的返回結果爲 NULL,因此使用 sum()時需注意 NPE 問題。
53.禁止使用存儲過程,存儲過程難以調試和擴展,更沒有移植性
54.in 操作能避免則避免,若實在避免不了,需要仔細評估 in 後邊的集合元素數量,控制在 1000 個之內。
55.如果有國際化需要,所有的字符存儲與表示,均以 utf-8 編碼,注意字符統計函數的區別。
58.POJO 類的布爾屬性不能加 is,而數據庫字段必須加 is_,要求在 resultMap 中進行字段與屬性之間的映射。
59. sql.xml 配置參數使用:#{},#param# 不要使用${} 此種方式容易出現 SQL 注入。
1.POJO 類中布爾類型變量都不要加 is 前綴,否則部分框架解析會引起序列化錯誤。
說明:在本文 MySQL 規約中的建表約定第一條,表達是與否的值採用 is_xxx 的命名方式,所以,需要在<resultMap>設置從 is_xxx 到 xxx 的映射關係。
反例:定義爲基本數據類型 Boolean isDeleted 的屬性,它的方法也是 isDeleted(),RPC 框架在反向解析的時候,“誤以爲”對應的屬性名稱是 deleted,導致屬性獲取不到,進而拋出異常。
2.Object 的 equals 方法容易拋空指針異常,應使用常量或確定有值的對象來調用equals。
正例:"test".equals(object);
反例:object.equals("test");
說明:推薦使用 java.util.Objects#equals(JDK7 引入的工具類)。
3.所有整型包裝類對象之間值的比較,全部使用 equals 方法比較。
說明:對於 Integer var = ? 在-128 至 127 範圍內的賦值,Integer 對象是在 IntegerCache.cache 產生,會複用已有對象,這個區間內的 Integer 值可以直接使用==進行判斷,但是這個區間之外的所有數據,都會在堆上產生,並不會複用已有對象,這是一個大坑,推薦使用 equals 方法進行判斷。
4.浮點數之間的等值判斷,基本數據類型不能用==來比較,包裝數據類型不能用 equals 來判斷。
說明:浮點數採用“尾數+階碼”的編碼方式,類似於科學計數法的“有效數字+指數”的表示方式。二進制無法精確表示大部分的十進制小數
反例:
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
if (a == b) {
// 預期進入此代碼快,執行其它業務邏輯
// 但事實上 a==b 的結果爲 false
}
Float x = Float.valueOf(a);
Float y = Float.valueOf(b);
if (x.equals(y)) {
/ / 預期進入此代碼快,執行其它業務邏輯
// 但事實上 equals 的結果爲 false
}
正例:
1.指定一個誤差範圍,兩個浮點數的差值在此範圍之內,則認爲是相等的。
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
float diff = 1e-6f;
if (Math.abs(a - b) < diff) {
System.out.println("true");
}
2.使用 BigDecimal 來定義值,再進行浮點數的運算操作。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
if (x.equals(y)) {
System.out.println("true");
}
5.定義數據對象 DO 類時,屬性類型要與數據庫字段類型相匹配。
正例:數據庫字段的 bigint 必須與類屬性的 Long 類型相對應。
反例:某個案例的數據庫表 id 字段定義類型 bigint unsigned,實際類對象屬性爲 Integer,隨着 id 越來越大,超過 Integer 的表示範圍而溢出成爲負數。
6.禁止使用構造方法 BigDecimal(double)的方式把 double 值轉化爲 BigDecimal 對象。
說明:BigDecimal(double)存在精度損失風險,在精確計算或值比較的場景中可能會導致業務邏輯異常。
正例:優先推薦入參爲 String 的構造方法,或使用 BigDecimal 的 valueOf 方法,此方法內部其實執行了 Double 的 toString,而 Double 的 toString 按 double 的實際能表達的精度對尾數進行了截斷。
代碼示例如下:
BigDecimal g = new BigDecimal(0.1f);
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
System.out.println(g);
System.out.println(recommend1);
System.out.println(recommend2);
------------- 輸出結果 --------------------------------------------
0.100000001490116119384765625
0.1
0.1
7.慎改serialVersionUID 字段
序列化類新增屬性時,請不要修改 serialVersionUID 字段,避免反序列失敗;如果完全不兼容升級,避免反序列化混亂,那麼請修改 serialVersionUID 值。
說明:注意 serialVersionUID 不一致會拋出序列化運行時異常。
8.關於 hashCode 和 equals 的處理,遵循如下規則:
1.只要覆寫 equals,就必須覆寫 hashCode。
2.因爲 Set 存儲的是不重複的對象,依據 hashCode 和 equals 進行判斷,所以 Set 存儲的對象必須覆寫這兩個方法。
3.如果自定義對象作爲 Map 的鍵,那麼必須覆寫 hashCode 和 equals。說明:String 已覆寫 hashCode 和 equals 方法,所以我們可以愉快地使用 String 對象作爲 key 來使用。
9.使用集合轉數組的方法,必須使用集合的 toArray(T[] array),傳入的是類型完全一致、長度爲 0 的空數組。
反例:直接使用 toArray 無參方法存在問題,此方法返回值只能是 Object[]類,若強轉其它類型數組將出
現 ClassCastException 錯誤。
正例:
List<String> list = new ArrayList<>(2);
list.add("guan");
list.add("bao");
String[] array = list.toArray(new String[0]);
說明:使用 toArray 帶參方法,數組空間大小的 length:
1.等於 0,動態創建與 size 相同的數組,性能最好。
2.大於 0 但小於 size,重新創建大小等於 size 的數組,增加 GC 負擔。
3.等於 size,在高併發情況下,數組創建完成之後,size 正在變大的情況下,負面影響與上相同。
4.大於 size,空間浪費,且在 size 處插入 null 值,存在 NPE 隱患。
10.在使用 Collection 接口任何實現類的 addAll()方法時,都要對輸入的集合參數進行 NPE 判斷。
說明:在 ArrayList#addAll 方法的第一行代碼即 Object[] a = c.toArray(); 其中 c 爲輸入集合參數,如果爲 null,則直接拋出異常。
11. 使用工具類 Arrays.asList()把數組轉換成集合時,不能使用其修改集合相關的方法,它的 add/remove/clear 方法會拋出 UnsupportedOperationException 異常。
說明:asList 的返回對象是一個 Arrays 內部類,並沒有實現集合的修改方法。Arrays.asList 體現的是適配器模式,只是轉換接口,後臺的數據仍是數組。
String[] str = new String[] { "yang", "hao" };
List list = Arrays.asList(str);
//報錯 Exception java.lang.UnsupportedOperationException
list.add("yangguanbao");
// 會更改 list中的結果
str[0] = "changed";
System.out.println(list.get(0));
12. 不要在 foreach 循環裏進行元素的 remove/add 操作。
remove 元素請使用 Iterator 方式,如果併發操作,需要對 Iterator 對象加鎖。
正例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("1".equalsIgnoreCase(item)) {
iterator.remove();
}
}
反例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
說明:以上代碼的執行結果, 移除"1," 不會報錯 . 移除"2", 拋出: java.util.ConcurrentModificationException
13. 集合初始化時,指定集合初始值大小
說明:HashMap 使用 HashMap(int initialCapacity) 初始化。
正例:initialCapacity = (需要存儲的元素個數 / 負載因子) + 1。注意負載因子(即 loader factor)默認爲 0.75,如果暫時無法確定初始值大小,請設置爲 16(即默認值)。
反例:HashMap 需要放置 1024 個元素,由於沒有設置容量初始大小,隨着元素不斷增加,容量 7 次被迫擴大,resize 需要重建 hash 表,嚴重影響性能。
14. 使用 entrySet 遍歷 Map 類集合 KV,而不是 keySet 方式進行遍歷。
說明:keySet 其實是遍歷了 2 次,一次是轉爲 Iterator 對象,另一次是從 hashMap 中取出 key 所對應的 value。
而 entrySet 只是遍歷了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用 Map.forEach 方法。
正例:values()返回的是 V 值集合,是一個 list 集合對象;keySet()返回的是 K 值集合,是一個 Set 集合對象;entrySet()返回的是 K-V 值組合集合。
Map map = new HashMap();
map.put(1,2);
map.put(2,2);
map.put(3,2);
map.put(4,2);
map.forEach(new BiConsumer() {
@Override
public void accept(Object o, Object o2) {
System.out.println(o + "=>" + o2);
}
});
map.forEach((k,v)->{
System.out.println(k + " = > " + v);
});
15. Map 類集合 K/V 能不能存儲 null 值的情況
集合類 |
Key |
Value |
Super |
說明 |
Hashtable |
不允許爲 null |
不允許爲 null |
Dictionary |
線程安全 |
ConcurrentHashMap |
不允許爲 null |
不允許爲 null |
AbstractMap |
鎖分段技術(JDK8:CAS) |
TreeMap |
不允許爲 null |
允許爲 null |
AbstractMap |
線程不安全 |
HashMap |
允許爲 null |
允許爲 null |
AbstractMap |
線程不安全 |
反例:由於 HashMap 的干擾,很多人認爲 ConcurrentHashMap 是可以置入 null 值,而事實上,存儲 null 值時會拋出 NPE 異常。
16. 線程池不允許使用 Executors 去創建
線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。
說明:Executors 返回的線程池對象的弊端如下:
1.FixedThreadPool 和 SingleThreadPool:
允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2.CachedThreadPool:
允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。
17. SimpleDateFormat 是線程不安全的類
- SimpleDateFormat 是線程不安全的類,一般不要定義爲 static 變量,如果定義爲 static,必須加鎖,或者使用 DateUtils 工具類。正例:注意線程安全,使用 DateUtils。亦推薦如下處理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
說明:如果是 JDK8 的應用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,
DateTimeFormatter 代替 SimpleDateFormat,官方給出的解釋:simple beautiful strong immutable thread-safe。
LocalDateTime rightNow= LocalDateTime.now();
String date=DateTimeFormatter.ISO_LOCAL_DATE.format(rightNow);
System.out.println(date);
DateTimeFormatter formatter=DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss");
System.out.println(formatter.format(rightNow));
18. 必須回收自定義的 ThreadLocal 變量
必須回收自定義的 ThreadLocal 變量,尤其在線程池場景下,線程經常會被複用,如果不清理自定義的 ThreadLocal 變量,可能會影響後續業務邏輯和造成內存泄露等問題。
儘量在代理中使用 try-finally 塊進行回收。
正例:
objectThreadLocal.set(userInfo);
try {
// ...
//
} finally {
objectThreadLocal.remove();
}
19. 在使用嘗試機制來獲取鎖的方式中,進入業務代碼塊之前,必須先判斷當前線程是
否持有鎖。鎖的釋放規則與鎖的阻塞等待方式相同
說明:Lock 對象的 unlock 方法在執行時,它會調用 AQS 的 tryRelease 方法(取決於具體實現類),如果
當前線程不持有鎖,則拋出 IllegalMonitorStateException 異常
正例:
public static void main(String[] args) {
Lock lock = new ReentrantLock();
// ...
boolean isLocked = lock.tryLock();
if (isLocked) {
try {
//todo
} finally {
lock.unlock();
}
}
}
20. 併發修改同一記錄時,避免更新丟失, 需要加鎖。
併發修改同一記錄時,避免更新丟失, 需要加鎖。 要麼在應用層加鎖,要麼在緩存加鎖,要麼在數據庫層使用樂觀鎖,使用 version 作爲更新依據。
說明:如果每次訪問衝突概率小於 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於3 次
21.Timer 異常情況注意
多線程並行處理定時任務時,Timer 運行多個 TimeTask 時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行,如果在處理定時任務時使用ScheduledExecutorService 則沒有這個問題。
22. CountDownLatch 異常情況注意
使用 CountDownLatch 進行異步轉同步操作,每個線程退出前必須調用 countDown 方法,線程執行代碼注意 catch 異常,確保 countDown 方法被執行到,避免主線程無法執行至 await 方法,直到超時才返回結果。
說明:注意,子線程拋出異常堆棧,不能在主線程 try-catch 到。
23.避免 Random 實例被多線程使用
避免 Random 實例被多線程使用,雖然共享該實例是線程安全的,但會因競爭同一 seed 導致的性能下降。
說明:Random 實例包括 java.util.Random 的實例或者 Math.random()的方式。
正例:在 JDK7 之後,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要編碼保證每個線程持有一個實例。
24. Double-Checked Locking is Broken
在併發場景下,通過雙重檢查鎖(double-checked locking)實現延遲初始化的優化
問題隱患(可參考 The "Double-Checked Locking is Broken" Declaration),推薦解決方案中較爲簡單一種(適用於 JDK5 及以上版本),將目標屬性聲明爲 volatile 型。
public class LazyInitDemo {
// 注意一定要加volatile
private volatile Object helper = null;
public Object getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
helper = new Object();
}
}
return helper;
}
}
25.volatile 解決多線程內存不可見問題
對於一寫多讀,是可以解決變量同步問題,但是如果多寫,同樣無法解決線程安全問題。
說明:如果是 count++操作,使用如下類實現:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數)。
26.當 switch 括號內的變量類型爲 String 並且此變量爲外部參數時,必須先進行 null 判斷。
public static void main(String[] args) {
method(null);
}
public static void method(String param) {
switch (param) { // 在這行會直接報錯
// 肯定不是進入這裏
case "sth":
System.out.println("it's sth"); break;
// 也不是進入這裏
case "null":
System.out.println("it's null"); break;
// 也不是進入這裏
default:
System.out.println("default");
}
}
----------------------------------------------
Exception in thread "main" java.lang.NullPointerException
at com.alibaba.SwitchTest.method(SwitchTest.java:10)
at com.alibaba.SwitchTest.main(SwitchTest.java:5)
27.在高併發場景中,避免使用”等於”判斷作爲中斷或退出的條件。
在高併發場景中,避免使用”等於”判斷作爲中斷或退出的條件。
說明:如果併發控制沒有處理好,容易產生等值判斷被“擊穿”的情況,使用大於或小於的區間判斷條件來代替。
反例:判斷剩餘獎品數量等於 0 時,終止發放獎品,但因爲併發處理錯誤導致獎品數量瞬間變成了負數,這樣的話,活動無法終止。
28.注意 Math.random() 這個方法返回是 double 類型
注意 Math.random() 這個方法返回是 double 類型,注意取值的範圍 0≤x<1(能夠取到零值,注意除零異常),如果想獲取整數類型的隨機數,不要將 x 放大 10 的若干倍然後取整,直接使用 Random 對象的 nextInt 或者 nextLong 方法。
29.獲取當前毫秒數
獲取當前毫秒數 System.currentTimeMillis(); 而不是 new Date().getTime();
說明:如果想獲取更加精確的納秒級時間值,使用 System.nanoTime()的方式。
在 JDK8 中,針對統計時間等場景,推薦使用 Instant 類。
30.日期格式化時,傳入 pattern 中表示年份統一使用小寫的 y。
日期格式化時,傳入 pattern 中表示年份統一使用小寫的 y。
說明:日期格式化時,yyyy表示當天所在的年,而大寫的YYYY代表是week in which year
(JDK7 之後引入的概念),意思是當天所在的周屬於的年份,一週從週日開始,週六結束,只要本週跨年,返回的YYYY就是下一年。另外需要注意:
-
- 表示月份是大寫的 M
- 表示分鐘則是小寫的 m
- 24 小時制的是大寫的 H
- 12 小時制的則是小寫的 h
正例:表示日期和時間的格式如下所示:
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
31.finally 塊必須對資源對象、流對象進行關閉,有異常也要做 try-catch
說明:如果 JDK7 及以上,可以使用 try-with-resources 方式。
32.不要在 finally 塊中使用 return。
說明:try 塊中的 return 語句執行成功後,並不馬上返回,而是繼續執行 finally 塊中的語句,如果此處存在 return 語句,則在此直接返回,無情丟棄掉 try 塊中的返回點。
private int x = 0;
public int checkReturn() {
try {
// x 等於 1,此處不返回
return ++x;
} finally {
// 返回的結果是 2
return ++x;
}
}
33.防止 NPE,注意 NPE 產生的場景:
返回類型爲基本數據類型,return 包裝數據類型的對象時,自動拆箱有可能產生 NPE。
反例:public int f() { return Integer 對象}, 如果爲 null,自動解箱拋 NPE。
- 數據庫的查詢結果可能爲 null。
- 集合裏的元素即使 isNotEmpty,取出的數據元素也可能爲 null。
- 遠程調用返回對象時,一律要求進行空指針判斷,防止 NPE。
- 對於 Session 中獲取的數據,建議進行 NPE 檢查,避免空指針。
- 級聯調用 obj.getA().getB().getC();一連串調用,易產生 NPE。
正例:使用 JDK8 的 Optional 類來防止 NPE 問題。
34.日誌存儲規約
所有日誌文件至少保存 15 天,因爲有些異常具備以“周”爲頻次發生的特點。網絡運行狀態、安全相關信息、系統監測、管理後臺操作、用戶敏感操作需要留存相關的網絡日誌不少於 6 個月。
35.在日誌輸出時,字符串變量之間的拼接使用佔位符的方式。
說明:因爲 String 字符串的拼接會使用 StringBuilder 的 append()方式,有一定的性能損耗。使用佔位符僅是替換動作,可以有效提升性能。
正例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
36.避免重複打印日誌,浪費磁盤空間,務必在 log4j.xml 中設置 additivity=false。
正例:<logger name="com.taobao.dubbo.config" additivity="false">
37.生產環境禁止輸出 debug 日誌
謹慎地記錄日誌。生產環境禁止輸出 debug 日誌;有選擇地輸出 info 日誌;如果使用 warn 來記錄剛上線時的業務行爲信息,一定要注意日誌輸出量的問題,避免把服務器磁盤撐爆,並記得及時刪除這些觀察日誌。
說明:大量地輸出無效日誌,不利於系統性能提升,也不利於快速定位錯誤點。記錄日誌時請思考:這些日誌真的有人看嗎?看到這條日誌你能做什麼?能不能給問題排查帶來好處
如非必要,請不要在此場景打出 error 級別,避免頻繁報警。
可以使用 warn 日誌級別來記錄用戶輸入參數錯誤的情況,避免用戶投訴時,無所適從。
38.單元測試中不準使用 System.out 來進行人肉驗證,必須使用 assert 來驗證。
39.小數類型爲 decimal,禁止使用 float 和 double。
說明:在存儲的時候,float 和 double 都存在精度損失的問題,很可能在比較值的時候,得到不正確的結果。如果存儲的數據範圍超過 decimal 的範圍,建議將數據拆成整數和小數並分開存儲。
40.varchar優化
varchar 是可變長字符串,不預先分配存儲空間,長度不要超過 5000,如果存儲長度大於此值,定義字段類型爲 text,獨立出來一張表,用主鍵來對應,避免影響其它字段索引效率。
41.單錶行數超過 500 萬行或者單表容量超過 2GB,才推薦進行分庫分表。
說明:如果預計三年後的數據量根本達不到這個級別,請不要在創建表時就分庫分表。
42.業務上具有唯一特性的字段,即使是多個字段的組合,也必須建成唯一索引。
說明:不要以爲唯一索引影響了 insert 速度,這個速度損耗可以忽略,但提高查找速度是明顯的;另外,即使在應用層做了非常完善的校驗控制,只要沒有唯一索引,根據墨菲定律,必然有髒數據產生。
43.超過三個表禁止 join。
需要 join 的字段,數據類型必須絕對一致;多表關聯查詢時,保證被關聯的字段需要有索引。
說明:即使雙表 join 也要注意表索引、SQL 性能。
44.在 varchar 字段上建立索引時,必須指定索引長度,沒必要對全字段建立索引,根據實際文本區分度決定索引長度即可。
說明:索引的長度與區分度是一對矛盾體,一般對字符串類型數據,長度爲 20 的索引,區分度會高達
90%以上,可以使用 count(distinct left(列名, 索引長度))/count(*)的區分度來確定。
45.頁面搜索嚴禁左模糊或者全模糊,如果需要請走搜索引擎來解決。
說明:索引文件具有 B-Tree 的最左前綴匹配特性,如果左邊的值未確定,那麼無法使用此索引。
46.利用延遲關聯或者子查詢優化超多分頁場景。
說明:MySQL 並不是跳過 offset 行,而是取 offset+N 行,然後返回放棄前 offset 行,返回 N 行,那當 offset 特別大的時候,效率就非常的低下,要麼控制返回的總頁數,要麼對超過特定閾值的頁數進行 SQL 改寫。
正例:先快速定位需要獲取的 id 段,然後再關聯:
SELECT a.* FROM 表 1 a, (select id from 表 1 where 條件 LIMIT 100000,20 ) b where a.id=b.id
47.SQL 性能優化的目標:至少要達到 range 級別,要求是 ref 級別,如果可以是 consts 最好。
說明:
- consts 單表中最多隻有一個匹配行(主鍵或者唯一索引),在優化階段即可讀取到數據。
- ref 指的是使用普通的索引(normal index)。
- range 對索引進行範圍檢索。
反例:explain 表的結果,type=index,索引物理文件全掃描,速度非常慢,這個 index 級別比較 range 還低,與全表掃描是小巫見大巫。
48.不要使用 count(列名)或 count(常量)來替代 count(*).
count(*)是 SQL92 定義的標準統計行數的語法,跟數據庫無關,跟 NULL 和非 NULL 無關。
說明:count(*)會統計值爲 NULL 的行,而 count(列名)不會統計此列爲 NULL 值的行。
49.count(distinct col) 計算該列除 NULL 之外的不重複行數
注意 count(distinct col1, col2) 如果其中一列全爲 NULL,那麼即使另一列有不同的值,也返回爲 0。
50.當某一列的值全是 NULL 時,count(col)的返回結果爲 0,但 sum(col)的返回結果爲 NULL,因此使用 sum()時需注意 NPE 問題。
正例:使用如下方式來避免 sum 的 NPE 問題:SELECT IFNULL(SUM(column), 0) FROM table;
51.使用 ISNULL()來判斷是否爲 NULL 值。
說明:NULL 與任何值的直接比較都爲 NULL。
- NULL<>NULL 的返回結果是 NULL,而不是 false。
- NULL=NULL 的返回結果是 NULL,而不是 true。
- NULL<>1 的返回結果是 NULL,而不是 true。
52. 不得使用外鍵與級聯,一切外鍵概念必須在應用層解決。
說明:以學生和成績的關係爲例,學生表中的 student_id 是主鍵,那麼成績表中的 student_id 則爲外鍵。如果更新學生表中的 student_id,同時觸發成績表中的 student_id 更新,即爲級聯更新。
外鍵與級聯更新適用於單機低併發,不適合分佈式、高併發集羣;級聯更新是強阻塞,存在數據庫更新風暴的風險;
外鍵影響數據庫的插入速度。
53.禁止使用存儲過程,存儲過程難以調試和擴展,更沒有移植性
54.in 操作能避免則避免,若實在避免不了,需要仔細評估 in 後邊的集合元素數量,控制在 1000 個之內。
55.如果有國際化需要,所有的字符存儲與表示,均以 utf-8 編碼,注意字符統計函數的區別。
說明:
SELECT LENGTH("輕鬆工作"); 返回爲 12
SELECT CHARACTER_LENGTH("輕鬆工作"); 返回爲 4
如果需要存儲表情,那麼選擇 utf8mb4 來進行存儲,注意它與 utf-8 編碼的區別。
56.TRUNCATE 不建議在開發代碼中使用。
TRUNCATE TABLE 比 DELETE 速度快,且使用的系統和事務日誌資源少,但TRUNCATE 無事務且不觸發 trigger,有可能造成事故,故不建議在開發代碼中使用此語句。
說明:TRUNCATE TABLE 在功能上與不帶 WHERE 子句的 DELETE 語句相同。
57.在表查詢中,一律不要使用 * 作爲查詢的字段列表
在表查詢中,一律不要使用 * 作爲查詢的字段列表,需要哪些字段必須明確寫明。
說明:
1)增加查詢分析器解析成本。
2)增減字段容易與 resultMap 配置不一致。
3)無用字段增加網絡消耗,尤其是 text 類型的字段。
58.POJO 類的布爾屬性不能加 is,而數據庫字段必須加 is_,要求在 resultMap 中進行字段與屬性之間的映射。
說明:參見定義 POJO 類以及數據庫字段定義規定,在<resultMap>中增加映射,是必須的。在
MyBatis Generator 生成的代碼中,需要進行對應的修改。
59. sql.xml 配置參數使用:#{},#param# 不要使用${} 此種方式容易出現 SQL 注入。
60.@Transactional 事務不要濫用。
事務會影響數據庫的 QPS,另外使用事務的地方需要考慮各方面的回滾方案,包括緩存回滾、搜索引擎回滾、消息補償、統計修正等。