分類

技術文章

技術文章

2020-02-27

如何編寫可怕的 Java 代碼?

 

  • 1. 對一切使用異常
  • 2. 不用擔心訪問修飾符
  • 3. 在 Java 中沒有什麼是真正的 final
  • 4. 使用 Java 序列化,干就對
  • 5. 將對象用於一切
  • 6. 充分擁抱便捷編程的藝術
  • 7. 不要學習任何新知識–你總是最了解

我決定告訴你如何編寫可怕的Java代碼。如果你厭倦了所有這些美麗的設計模式和最佳實踐,並且想寫些瘋狂的東西,請繼續閱讀。

如果你正在尋找有關如何編寫良好代碼的建議,請查看其它文章!

1. 對一切使用異常

你知道循環對嗎?差一錯誤(英語:Off-by-one error,縮寫 OBOE,是在計數時由於邊界條件判斷失誤導致結果多了一或少了一的錯誤,通常指計算機編程中循環多了一次或者少了一次的程序錯誤,屬於邏輯錯誤的一種)很容易犯。當你迭代一個集合時,很容易出錯。讓我們看看如何使用 Java 異常處理來解決該問題,而不用擔心這些討厭的差一錯誤!

public static void horribleIteration(String [] words){
    int i = 0;
    try {
        while(true){
            System.out.println(words[i]);
            i++;
        }
    } catch (IndexOutOfBoundsException e){
        //iteration complete
    }
}

2. 不用擔心訪問修飾符

你說什麼?Java 中的訪問修飾符,這不是浪費時間嘛!你是否知道將屬性/方法等設為私有隻是一個建議?如果你想修改它,那就去做吧!沒什麼能阻止你(除了缺乏知識之外)。如果是這種情況,請看如下代碼。

public static void readPrivate() throws NoSuchFieldException, IllegalAccessException {
    Field f = System.class.getDeclaredField("lineSeparator");
    f.setAccessible(true);
    String separator = (String) f.get(System.class);
    System.out.println("Line separator is " + separator + ".");
}

我們在這裡讀取 lineSeparator,這並沒有什麼。但是修改 lineSeparator 會帶來更多樂趣!在我們修改代碼中的 lineSeparator 之後,看看 System.out.println 發生了什麼:

public static void readWritePrivate() throws NoSuchFieldException, IllegalAccessException {
    Field f = System.class.getDeclaredField("lineSeparator");
    f.setAccessible(true);
    String separator = (String) f.get(System.class);
    System.out.println("Line separator is " + separator + ".");

    f.set(System.class ,"!!!");
    System.out.println("Line one");
    System.out.println("Line two");
    System.out.println("Line three");
}

輸出為:

Line separator is
WARNING: All illegal access operations will be denied in a future release
.
Line one!!!Line two!!!Line three!!!

看起來不錯!

3. 在 Java 中沒有什麼是真正的 final

一些開發人員認為他們通過將 final 關鍵字放在變數前面來以說明不會去更改這個值。事實是——有時候你真的想要改變一個 final 欄位的值,所以這是如何做的:

public static void notSoFinal() throws NoSuchFieldException, IllegalAccessException, InterruptedException {
    ExampleClass example = new ExampleClass(10);
    System.out.println("Final value was: "+ example.finalValue);
    Field f = example.getClass().getDeclaredField("finalValue");
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);
    f.setInt(example, 77);
    System.out.println("Final value was: "+ example.finalValue);
}

public static class ExampleClass {
    final int finalValue;

    public ExampleClass(int finalValue){
        this.finalValue = finalValue;
    }
}

注意,在構造函數中提供最終值時,這對我很有用。如果你在類中設置了 final 值,那麼它將不起作用。(可能是一些編譯器級別的優化破壞了所有的樂趣)

4. 使用 Java 序列化,干就對

這很簡單,用 Java 序列化,玩得開心,好好享受。

好吧,我想你想要一些理由。我看到 Java 平台首席架構師 Mark Reinhold 表示,他們後悔將序列化引入到 Java。顯然,Java 中大約 1/3 的安全漏洞僅來自於序列化。

5. 將對象用於一切

你知道類嗎?浪費時間!你是否想看到代碼重用的巔峰之作?你去!

public static void printThings (List things){
    int i = 0;
    try {
        while(true){
            System.out.println(things.get(i));
            i++;
        }
    } catch (IndexOutOfBoundsException e){
        //iteration complete
    }
}

List superList = new ArrayList();
superList.add(7);
superList.add("word");
superList.add(true);
superList.add(System.class);
printThings(superList);

您可以相信我們一直以來都擁有這種力量嗎?另外,組合兩個模式還有額外的好處!

這只是你使用 Object 進行操作的開始。如果有疑問,請記住-使用對象。如果需要,你隨時可以使用這種驚人的模式進行回退!

public static void printThingsUppercaseStrings (List things){
    int i = 0;
    try {
        while(true){
            Object o = things.get(i);
            System.out.println(o);
            if(o.getClass() == String.class){
                String so = (String) o;
                so = so.toUpperCase();
                System.out.println(so);
            }
            i++;
        }
    } catch (IndexOutOfBoundsException e){
        //iteration complete
    }
}

這還是類型安全的,多麼健壯的解決方案。

6. 充分擁抱便捷編程的藝術

你知道比爾·蓋茨更喜歡懶惰的開發人員嗎?比爾·蓋茨說過:

“I will always choose a lazy person to do a difficult job…because, he will find an easy way to do it. –Bill Gates”

“我總是會選擇一個懶人去完成一份困難的工作…因為,他會找到捷徑。” — 比爾蓋茨

因此,有了比爾·蓋茨(Bill Gates)的大力支持,我們可以完全接受我們的懶惰。你準備好了嗎?那就開始吧!

  • 永遠不要編寫測試,只是不要編寫錯誤!
  • 將所有都定義為 public -方便訪問!
  • 支持全局變數–您可能需要它們!
  • 大型介面優於小型專用介面–可以使用的方法越多越好!
  • 支持繼承而不是合成(使用介面中的默認方法從未如此簡單)!
  • 始終使用裝箱類型–它們也可以用作對象!
  • 儘可能使用最短的名字(a, b, n 最好)!

7. 不要學習任何新知識–你總是最了解

一個程序員最重要的品質就是對自己有信心。相信自己什麼都懂,沒有什麼可學的!

考慮到這一點,請確保不要學習:

  • 新類庫
  • 新語言
  • 新框架

這樣可以節省你的時間!你永遠都不應學習任何新知識,因為你已經是最好的了。

你有能力去做這件事,並不代表你應該做



2020-02-25

Java 14 令人期待的 5 大新特性,打包工具終於要來了!

  • 1. instanceof 模式匹配
  • 2. 文本塊再次作為預覽特性保留
  • 3. 記錄類型(Record Type)的引入
  • 4. 打包工具終於來了
  • 5. 一個組合垃圾收集器被棄用
  • 6. 結束語

隨著新的 Java 發布生命周期的到來,新版本預計將於 2020 年 3 月發布,本文將對其中的 5 個主要特性作些概述。

Java 13 剛剛發布給開發人員使用不久,最新版本的JDK於2019年9月發布。但是很少有公司現在改用了Java 13 ,因為這個版本看起來無論如何都不會從甲骨文公司獲得長期支持(LTS)。更不要說現在Java 14又已經出現了。

隨著新的Java發布生命周期的到來,新的Java版本預計將於2020年3月發布。時間已經所剩不多了,這就是為什麼甲骨文公司的Java首席架構師Mark Reinhold剛剛提出應該將JDK增強提案(JEP)中的5個主要特性包含進Java 14中的原因。

因此,在這篇文章里我將對這5個主要特性作些概述。這些特性應該作為Java 14的一部分,以便它們可以從2020年3月起提供給開發人員使用。

1. instanceof 模式匹配

作為預覽模式提供的這個新特性旨在通過向用戶提供instanceof操作符的模式匹配來改進Java。

模式匹配在其他語言中已經出現,它使得以一種更安全和更簡潔的方式來表達程序邏輯成為可能。

instanceof操作符的模式匹配將有助於從對象中有條件地提取組件。

在大多數Java程序中,都有下面這種類型的代碼:

if (obj instanceof Integer) {
    int intValue = (Integer) obj;
    // ... use intValue ...
}

在這段代碼中,我們通過instanceof運算符來檢查obj變數是否是Integer的實例。如果條件為真的話,我們不能將obj直接作為一個整數變數使用,因為必須首先對它進行轉換。

上面的代碼不簡潔,也不是很清晰。此外,在程序中重複這種類型的構造會增加出錯的風險。

而作為預覽狀態在Java 14中引入的instanceof操作符的模式匹配將允許對上述代碼作如下簡化:

if (x instanceof Integer i) {
    // ... use i as an Integer directly ...
}

在下面更複雜的例子中,我們可以更加理解Java 14中可以做到什麼:

String formatted = "unknown";if (obj instanceof Integer i) {
    formatted = String.format("int %d", i);
}
else if (obj instanceof Byte b) {
    formatted = String.format("byte %d", b);
}
else if (obj instanceof Long l) {
    formatted = String.format("long %d", l);
}
else if (obj instanceof Double d) {
    formatted = String.format(「double %f", d);
}
else if (obj instanceof String s) {
    formatted = String.format("String %s", s);
}// ... use formatted variable ...

這個例子中最有趣的地方是instanceof模式匹配可以擴展到其他的語法結構中。首先,我們應該可以想到switch表達式。

在未來的Java 15、16或17中,我們可以想象用以下的代碼替換先前的if/else序列是可能的:

String formatted =
    switch (obj) {
        case Integer i -> String.format("int %d", i);
        case Byte b -> String.format("byte %d", b);
        case Long l -> String.format("long %d", l);
        case Double d -> String.format("double %f", d);
        case String s -> String.format("String %s, s);
        default -> String.format("Object %s", obj);
    };// ... use formatted variable

2. 文本塊再次作為預覽特性保留

文本塊(Text Block)在Java 13 中作為預覽特性引入,在Java 14中再次作為預覽特性保留下來。

鑒於在Java 13 發布之後從各大Java社區收集上來的反饋,對文本塊添加了兩個新的轉義序列。

轉義序列符「/」顯式地消除了插入新行字元的需要。以下面這個使用位於小字元串之間的連接運算符「+」來拆分較大字元串的例子為例:

String literal = "This is a string splitted " +
                 "in several smaller " +
                 "strings.";

使用轉義序列符「/」,在Java 14中我們可以像下面這樣來改寫上面的代碼:

String text = """
                This is a string splitted /
                in several smaller /
                strings./
                """;

由於字元文本和傳統字元串文本不允許嵌入換行符,因此轉義序列符「/」僅適用於文本塊。

另一方面,新的轉義序列符「/s」可以被轉換為一個簡單的空白。這樣可以防止空白字元被清除掉。

基於這個轉義序列符,我們可以構建一個字元串,確保每一行的長度相同:

String colors = """
red /s
green/s
blue /s
""";

注意:這個將在Java 14中引入的新轉義序列符(/s)也可以用於傳統的字元串文本。

3. 記錄類型(Record Type)的引入

Java 14中應該可以看到記錄類型(Record Type)作為預覽特性被引入。記錄對象允許使用緊湊的語法來聲明類,而這些類是淺不變數據的透明持有者。

和枚舉類型一樣,記錄也是類的一種受限形式。記錄聲明其表示並提交到與此表示相對應的API。記錄對象放棄了Java類從中受益的自由:也就是將API和其表示分離的能力。作為回報,記錄對象在簡潔性方面提供了顯著的好處。

記錄對象擁有一個名字和一個聲明其組件的狀態描述。記錄對象的主體(body)是可選的。下面是創建一個記錄對象Point的例子:

record Point(int x, int y) { }

這個寫法相當於下面的類聲明:

final class Point {
    public final int x;
    public final int y;


    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }


    // state-based implementations of equals, hashCode, toString
    // nothing else
}

為了在Java 14中引入這種新類型,需要在Java.lang.Class對象中添加如下兩個新方法:

RecordComponent[] getRecordComponents()
boolean isRecord()

這兩個方法的目的是更新Java Reflection API,以便將記錄類型納入其中。

4. 打包工具終於來了

Java 13 發布前的最後一刻被從外圍移除的打包工具jpackage終於可以在Java 14中使用了。但是要注意,它只在Incubator版本中提供。

這個打包工具應該可以讓開發人員創建自主的Java應用程序,它以JavaFX javapackager這個打包工具為基礎。它的主要特點有如下三個:

  • 支持本地打包格式,為用戶提供自然的安裝體驗;
  • 打包時可以指定啟動參數;
  • 通過命令行或使用ToolProvider API的程序來啟動。

5. 一個組合垃圾收集器被棄用

Java 14中令人期待的最新創新顯然不是針對所有Java開發人員的,實際上,它打算棄用ParallelScavenge + SerialOld垃圾收集器組合。

支持這一改變的JEP 366清楚地表明,他們的目的不是要刪除掉這個組合,而是要棄用它。

棄用這種演算法組合的原因是:它很少被使用,同時需要大量的維護工作。

6. 結束語

預定於2020年3月發布的Java 14,對Java開發人員的日常工作影響相對較小。最前沿的新特性是instanceof的模式匹配,這是大多數開發人員都渴望嘗試的新特性。

但是,在生產環境中使用此特性之前,你必須要有耐心,因為它們在Java 14中僅僅作為預覽特性出現。

好消息是,instanceof的模式匹配代表了在Java 15、16或17中進行更廣泛的模式匹配的第一步。

所有這些改變都讓開發人員感到非常高興,因為他們所有人將在程序的可讀性和個人的開發效率方面獲得顯著的收益。title: Java 14 令人期待的 5 大新特性,打包工具終於要來了!date: 2020-03-10
tags:
categories: 精進
permalink: Fight/
author: CSDN 翻譯,譯者:蘇本如
from_url: medium.com/better-programming/top-5-new-features-expected-in-java-14-82c0d85b295e
wechat_url:


隨著新的 Java 發布生命周期的到來,新版本預計將於 2020 年 3 月發布,本文將對其中的 5 個主要特性作些概述。

Java 13 剛剛發布給開發人員使用不久,最新版本的JDK於2019年9月發布。但是很少有公司現在改用了Java 13 ,因為這個版本看起來無論如何都不會從甲骨文公司獲得長期支持(LTS)。更不要說現在Java 14又已經出現了。

隨著新的Java發布生命周期的到來,新的Java版本預計將於2020年3月發布。時間已經所剩不多了,這就是為什麼甲骨文公司的Java首席架構師Mark Reinhold剛剛提出應該將JDK增強提案(JEP)中的5個主要特性包含進Java 14中的原因。

因此,在這篇文章里我將對這5個主要特性作些概述。這些特性應該作為Java 14的一部分,以便它們可以從2020年3月起提供給開發人員使用。

1. instanceof 模式匹配

作為預覽模式提供的這個新特性旨在通過向用戶提供instanceof操作符的模式匹配來改進Java。

模式匹配在其他語言中已經出現,它使得以一種更安全和更簡潔的方式來表達程序邏輯成為可能。

instanceof操作符的模式匹配將有助於從對象中有條件地提取組件。

在大多數Java程序中,都有下面這種類型的代碼:

if (obj instanceof Integer) {
    int intValue = (Integer) obj;
    // ... use intValue ...
}

在這段代碼中,我們通過instanceof運算符來檢查obj變數是否是Integer的實例。如果條件為真的話,我們不能將obj直接作為一個整數變數使用,因為必須首先對它進行轉換。

上面的代碼不簡潔,也不是很清晰。此外,在程序中重複這種類型的構造會增加出錯的風險。

而作為預覽狀態在Java 14中引入的instanceof操作符的模式匹配將允許對上述代碼作如下簡化:

if (x instanceof Integer i) {
    // ... use i as an Integer directly ...
}

在下面更複雜的例子中,我們可以更加理解Java 14中可以做到什麼:

String formatted = "unknown";if (obj instanceof Integer i) {
    formatted = String.format("int %d", i);
}
else if (obj instanceof Byte b) {
    formatted = String.format("byte %d", b);
}
else if (obj instanceof Long l) {
    formatted = String.format("long %d", l);
}
else if (obj instanceof Double d) {
    formatted = String.format(「double %f", d);
}
else if (obj instanceof String s) {
    formatted = String.format("String %s", s);
}// ... use formatted variable ...

這個例子中最有趣的地方是instanceof模式匹配可以擴展到其他的語法結構中。首先,我們應該可以想到switch表達式。

在未來的Java 15、16或17中,我們可以想象用以下的代碼替換先前的if/else序列是可能的:

String formatted =
    switch (obj) {
        case Integer i -> String.format("int %d", i);
        case Byte b -> String.format("byte %d", b);
        case Long l -> String.format("long %d", l);
        case Double d -> String.format("double %f", d);
        case String s -> String.format("String %s, s);
        default -> String.format("Object %s", obj);
    };// ... use formatted variable

2. 文本塊再次作為預覽特性保留

文本塊(Text Block)在Java 13 中作為預覽特性引入,在Java 14中再次作為預覽特性保留下來。

鑒於在Java 13 發布之後從各大Java社區收集上來的反饋,對文本塊添加了兩個新的轉義序列。

轉義序列符「/」顯式地消除了插入新行字元的需要。以下面這個使用位於小字元串之間的連接運算符「+」來拆分較大字元串的例子為例:

String literal = "This is a string splitted " +
                 "in several smaller " +
                 "strings.";

使用轉義序列符「/」,在Java 14中我們可以像下面這樣來改寫上面的代碼:

String text = """
                This is a string splitted /
                in several smaller /
                strings./
                """;

由於字元文本和傳統字元串文本不允許嵌入換行符,因此轉義序列符「/」僅適用於文本塊。

另一方面,新的轉義序列符「/s」可以被轉換為一個簡單的空白。這樣可以防止空白字元被清除掉。

基於這個轉義序列符,我們可以構建一個字元串,確保每一行的長度相同:

String colors = """
red /s
green/s
blue /s
""";

注意:這個將在Java 14中引入的新轉義序列符(/s)也可以用於傳統的字元串文本。

3. 記錄類型(Record Type)的引入

Java 14中應該可以看到記錄類型(Record Type)作為預覽特性被引入。記錄對象允許使用緊湊的語法來聲明類,而這些類是淺不變數據的透明持有者。

和枚舉類型一樣,記錄也是類的一種受限形式。記錄聲明其表示並提交到與此表示相對應的API。記錄對象放棄了Java類從中受益的自由:也就是將API和其表示分離的能力。作為回報,記錄對象在簡潔性方面提供了顯著的好處。

記錄對象擁有一個名字和一個聲明其組件的狀態描述。記錄對象的主體(body)是可選的。下面是創建一個記錄對象Point的例子:

record Point(int x, int y) { }

這個寫法相當於下面的類聲明:

final class Point {
    public final int x;
    public final int y;


    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }


    // state-based implementations of equals, hashCode, toString
    // nothing else
}

為了在Java 14中引入這種新類型,需要在Java.lang.Class對象中添加如下兩個新方法:

RecordComponent[] getRecordComponents()
boolean isRecord()

這兩個方法的目的是更新Java Reflection API,以便將記錄類型納入其中。

4. 打包工具終於來了

Java 13 發布前的最後一刻被從外圍移除的打包工具jpackage終於可以在Java 14中使用了。但是要注意,它只在Incubator版本中提供。

這個打包工具應該可以讓開發人員創建自主的Java應用程序,它以JavaFX javapackager這個打包工具為基礎。它的主要特點有如下三個:

  • 支持本地打包格式,為用戶提供自然的安裝體驗;
  • 打包時可以指定啟動參數;
  • 通過命令行或使用ToolProvider API的程序來啟動。

5. 一個組合垃圾收集器被棄用

Java 14中令人期待的最新創新顯然不是針對所有Java開發人員的,實際上,它打算棄用ParallelScavenge + SerialOld垃圾收集器組合。

支持這一改變的JEP 366清楚地表明,他們的目的不是要刪除掉這個組合,而是要棄用它。

棄用這種演算法組合的原因是:它很少被使用,同時需要大量的維護工作。

6. 結束語

預定於2020年3月發布的Java 14,對Java開發人員的日常工作影響相對較小。最前沿的新特性是instanceof的模式匹配,這是大多數開發人員都渴望嘗試的新特性。

但是,在生產環境中使用此特性之前,你必須要有耐心,因為它們在Java 14中僅僅作為預覽特性出現。

好消息是,instanceof的模式匹配代表了在Java 15、16或17中進行更廣泛的模式匹配的第一步。

所有這些改變都讓開發人員感到非常高興,因為他們所有人將在程序的可讀性和個人的開發效率方面獲得顯著的收益。



2020-02-25

阿里巴巴 29 個屌炸天的開源項目!

前言

眾所周知,阿里巴巴是 apache基金會成員、Linux基金會成員,同時是Xen顧問委員會成員。上述身份可見阿里在開源方面的重視程度,阿里通過開源貢獻更多技術、分享更多理念。其開源的很多項目大受歡迎,今日就來盤點阿里29個開源項目,你用過幾個,哪個最好用,歡迎在留言區告訴小編。

1. 分散式應用服務開發的一站式解決方案 Spring Cloud Alibaba

Spring Cloud Alibaba 致力於提供分散式應用服務開發的一站式解決方案。此項目包含開發分散式應用服務的必需組件,方便開發者通過 Spring Cloud 編程模型輕鬆使用這些組件來開發分散式應用服務。

依託 Spring Cloud Alibaba,您只需要添加一些註解和少量配置,就可以將 Spring Cloud 應用接入阿里分散式應用解決方案,通過阿里中間件來迅速搭建分散式應用系統。

地址:https://github.com/spring-cloud-incubator/spring-cloud-alibaba

2. 設計語言 & 前端框架 Ant Design

Ant Design 是螞蟻金服開發和正在使用的一套企業級的前端設計語言和基於 React 的前端框架實現。

它的特性:企業級金融產品的交互語言和視覺體系;豐富實用的 React UI 組件;基於 React 的組件化開發模式;背靠 npm 生態圈;基於 webpack 的調試構建方案,支持 ES6。

地址:https://github.com/ant-design/ant-design

3. JDBC 連接池、監控組件 Druid

Druid是一個 JDBC 組件。

1.監控資料庫訪問性能。

2.提供了一個高效、功能強大、可擴展性好的資料庫連接池。

3.資料庫密碼加密。

4.SQL執行日誌。

地址:https://github.com/alibaba/druid

4. Java 的 JSON 處理器 fastjson

fastjson 是一個性能很好的 Java 語言實現的 JSON 解析器和生成器,來自阿里巴巴的工程師開發。

主要特點:快速FAST (比其它任何基於Java的解析器和生成器更快,包括jackson);強大(支持普通JDK類包括任意Java Bean Class、Collection、Map、Date或enum);零依賴(沒有依賴其它任何類庫除了JDK)。

地址:https://github.com/alibaba/fastjson

5. 服務框架 Dubbo

Apache Dubbo (incubating) |是阿里巴巴的一款高性能、輕量級的開源Java RPC框架,它提供了三大核心能力:面向介面的遠程方法調用,智能容錯和負載均衡,以及服務自動註冊和發現。

地址:https://github.com/alibaba/dubbo

6. 企業級流式計算引擎 JStorm

JStorm 是參考 Apache Storm 實現的實時流式計算框架,在網路IO、線程模型、資源調度、可用性及穩定性上做了持續改進,已被越來越多企業使用。JStorm 可以看作是 storm 的 java 增強版本,除了內核用純java實現外,還包括了thrift、python、facet ui。從架構上看,其本質是一個基於 zk 的分散式調度系統。

地址:https://github.com/alibaba/jstorm

7. apns4j

apns4j 是 Apple Push Notification Service 的 Java 實現。

地址:https://github.com/teaey/apns4j

8. 數據驅動的高交互可視化圖形語法 AntV – G2

G2 是一套基於可視化編碼的圖形語法,以數據驅動,具有高度的易用性和擴展性,用戶無需關注各種繁瑣的實現細節,一條語句即可構建出各種各樣的可交互的統計圖表。

同時,G2 也是 AntV 最重要的組成,始於《The Grammar of Graphics》一書描述的視覺編碼語法系統(這也是 G2 項目命名的由來)。

項目地址:https://github.com/antvis/g2

9. 前端構建和工程化工具 Dawn

Dawn 取「黎明、破曉」之意,原為「阿里雲·業務運營團隊」內部的前端構建和工程化工具,現已完全開源。

它通過 pipeline 和 middleware 將開發過程抽象為相對固定的階段和有限的操作,簡化並統一了開發人員的日常構建與開發相關的工作。

地址:https://alibaba.github.io/dawn/

10. 分散式數據層 TDDL

TDDL 是一個基於集中式配置的 jdbc datasource實現,具有主備,讀寫分離,動態資料庫配置等功能。

地址:https://github.com/alibaba/tb_tddl

11. 輕量級分散式數據訪問層 CobarClient

Cobar Client是一個輕量級分散式數據訪問層(DAL)基於iBatis(已更名為MyBatis)和Spring框架實現。

地址:https://github.com/alibaba/cobarclient

12. 淘寶定製 JVM:TaobaoJVM

TaobaoJVM 基於 OpenJDK HotSpot VM,是國內第一個優化、定製且開源的伺服器版Java虛擬機。目前已經在淘寶、天貓上線,全部替換了Oracle官方JVM版本,在性能,功能上都初步體現了它的價值。

地址:http://jvm.taobao.org

13. Java 圖片處理類庫 SimpleImage

SimpleImage是阿里巴巴的一個Java圖片處理的類庫,可以實現圖片縮略、水印等處理。

地址:https://github.com/alibaba/simpleimage

14. Redis 的 Java 客戶端 Tedis

Tedis 是另一個 redis 的 java 客戶端。Tedis 的目標是打造一個可在生產環境直接使用的高可用 Redis 解決方案。

地址:https://github.com/justified/tedis

15. 開源 Java 診斷工具 Arthas

Arthas(阿爾薩斯)是阿里巴巴開源的 Java 診斷工具,深受開發者喜愛。

Arthas 採用命令行交互模式,同時提供豐富的 Tab 自動補全功能,進一步方便進行問題的定位和診斷。

地址:https://alibaba.github.io/arthas/

16. 動態服務發現、配置和服務管理平台 Nacos

Nacos 致力於幫助您發現、配置和管理微服務。Nacos 提供了一組簡單易用的特性集,幫助您實現動態服務發現、服務配置管理、服務及流量管理。

Nacos 幫助您更敏捷和容易地構建、交付和管理微服務平台。Nacos 是構建以「服務」為中心的現代應用架構(例如微服務範式、雲原生範式)的服務基礎設施。

地址:https://nacos.io/en-us/

17. Java 解析 Excel 工具 easyexcel

Java 解析、生成 Excel 比較有名的框架有 Apache poi、jxl 。但他們都存在一個嚴重的問題就是非常的耗內存,poi 有一套 SAX 模式的 API 可以一定程度的解決一些內存溢出的問題,但 POI 還是有一些缺陷,比如 07 版 Excel 解壓縮以及解壓后存儲都是在內存中完成的,內存消耗依然很大。

easyexcel 重寫了 poi 對 07 版 Excel 的解析,能夠原本一個 3M 的 excel 用 POI sax 依然需要 100M 左右內存降低到 KB 級別,並且再大的 excel 不會出現內存溢出,03 版依賴 POI 的 sax 模式。在上層做了模型轉換的封裝,讓使用者更加簡單方便。

地址:https://github.com/alibaba/easyexcel

18. 高可用流量管理框架 Sentinel

Sentinel 是面向微服務的輕量級流量控制框架,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。

只要通過 Sentinel API 定義的代碼,就是資源,能夠被 Sentinel 保護起來。大部分情況下,可以使用方法簽名,URL,甚至服務名稱作為資源名來標示資源。

地址:https://github.com/alibaba/Sentinel

19. 基於多維度 Metrics 的系統度量和監控中間件 SOFALookout

Lookout 是一個利用多維度的 metrics 對目標系統進行度量和監控的項目。Lookout 的多維度 metrics 參考 Metrics 2.0 標準。Lookout 項目分為客戶端部分與伺服器端部分。

客戶端是一個 Java 的類庫,可以將它植入您的應用代碼中採集 metrics 信息,客戶端更多詳情。

服務端代碼部分,將於下一版本提供。通過 LOOKOUT 的服務,可以對 metrics 數據進行收集、加工、存儲和查詢等處理,另外結合 grafana,可做數據可視化展示。

地址:https://github.com/alipay/sofa-lookout

20. 基於 Spring Boot 的研發框架 SOFABoot

SOFABoot 是螞蟻金服開源的基於 Spring Boot 的研發框架,它在 Spring Boot 的基礎上,提供了諸如 Readiness Check,類隔離,日誌空間隔離等等能力。在增強了 Spring Boot 的同時,SOFABoot 提供了讓用戶可以在 Spring Boot 中非常方便地使用 SOFAStack 相關中間件的能力。

地址:https://github.com/alipay/sofa-boot

21. 輕量級 Java 類隔離容器 SOFAArk

SOFAArk 是一款基於 Java 實現的輕量級類隔離容器,由螞蟻金服公司開源貢獻;主要為應用程序提供類隔離和依賴包隔離的能力;基於 Fat Jar 技術,應用可以被打包成一個自包含可運行的 Fat Jar,應用既可以是簡單的單模塊 Java 應用也可以是 Spring Boot 應用。可訪問網址進入快速開始並獲取更多詳細信息。

地址:https://alipay.github.io/sofastack.github.io/

22. 分散式鏈路追蹤中間件 SOFATracer

SOFATracer 是一個用於分散式系統調用跟蹤的組件,通過統一的 traceId 將調用鏈路中的各種網路調用情況以日誌的方式記錄下來,以達到透視化網路調用的目的。這些日誌可用於故障的快速發現,服務治理等。

地址:https://github.com/alipay/sofa-tracer

23. 高性能 Java RPC 框架 SOFARPC

SOFARPC 是一個高可擴展性、高性能、生產級的 Java RPC 框架。在螞蟻金服 SOFARPC 已經經歷了十多年及五代版本的發展。SOFARPC 致力於簡化應用之間的 RPC 調用,為應用提供方便透明、穩定高效的點對點遠程服務調用方案。為了用戶和開發者方便的進行功能擴展,SOFARPC 提供了豐富的模型抽象和可擴展介面,包括過濾器、路由、負載均衡等等。同時圍繞 SOFARPC 框架及其周邊組件提供豐富的微服務治理方案。

地址:https://github.com/alipay/sofa-rpc

24. 基於 Netty 的網路通信框架 SOFABolt

SOFABolt 是螞蟻金融服務集團開發的一套基於 Netty 實現的網路通信框架。

為了讓 Java 程序員能將更多的精力放在基於網路通信的業務邏輯實現上,而不是過多的糾結於網路底層 NIO 的實現以及處理難以調試的網路問題,Netty 應運而生。

為了讓中間件開發者能將更多的精力放在產品功能特性實現上,而不是重複地一遍遍製造通信框架的輪子,SOFABolt 應運而生。

地址:https://github.com/alipay/sofa-bolt

25. 動態非侵入 AOP 解決方案 JVM-Sandbox

JVM-Sandbox,JVM 沙箱容器,一種基於 JVM 的非侵入式運行期 AOP 解決方案。

地址:https://github.com/alibaba/jvm-sandbox

26. 面向雲的分散式消息領域標準 OpenMessaging

OpenMessaging 是由阿里巴巴發起,與雅虎、滴滴出行、Streamlio 公司共同參與創立,旨在創立廠商無關、平台無關的分散式消息及流處理領域的應用開發標準。

地址:https://github.com/openmessaging/openmessaging-java

27. P2P 文件分發系統 Dragonfly

Dragonfly(蜻蜓)是阿里自研的 P2P 文件分發系統,用於解決大規模文件分發場景下分發耗時、成功率低、帶寬浪費等難題。大幅提升發布部署、數據預熱、大規模容器鏡像分發等業務能力。

開源版的 Dragonfly 可用於 P2P 文件分發、容器鏡像分發、局部限速、磁碟容量預檢等。它支持多種容器技術,對容器本身無需做任何改造,鏡像分發比 natvie 方式提速可高達 57 倍,Registry 網路出流量降低99.5%以上。

地址:https://github.com/alibaba/Dragonfly

28. LayoutManager 定製化布局方案 vlayout

VirtualLayout是一個針對RecyclerView的LayoutManager擴展, 主要提供一整套布局方案和布局間的組件復用的問題。

地址:https://github.com/alibaba/vlayout

29. Java 代碼規約掃描插件 P3C

項目包含三部分:PMD 實現、IntelliJ IDEA 插件、Eclipse 插件

地址:https://github.com/alibaba/p3c



2020-02-22

Vert.x!這是目前最快的 Java 框架

如果您搜索「最佳網路框架 」,您可能會偶然發現Techempower基準測試,其中排名超過300個框架,在那裡你可能已經注意到Vert.x是排名最高的。

Vert.x是一個多語言 Web 框架,它支持Java ,Kotlin,Scala,Ruby和Javascript支持的語言之間的共同功能。無論語言如何,Vert.x都在Java虛擬機(JVM)上運行。模塊化和輕量級,它面向微服務開發。

Techempower基準測試衡量從資料庫更新,獲取和交付數據的性能。每秒提供的請求越多越好。在這種涉及很少計算的IO場景中,任何非阻塞框架都會有優勢。近年來,這種範式幾乎與Node.js不可分割,Node.js通過其單線程事件循環來推廣它。

與Node類似,Vert.x運行單個事件循環。但Vert.x也利用了JVM。Node運行在單個核心上,而Vert.x維護的線程池大小可以與可用核心數相匹配。憑藉更強的併發支持,Vert.x不僅適用於IO,也適用於需要并行計算的CPU繁重流程。

然而,事件循環只是故事的一半。另一半與Vert.x幾乎沒有關係。Java必備的 15 個框架,推薦看下。

要連接到資料庫,客戶端需要連接器驅動程序。在Java領域,Sql最常見的驅動程序是JDBC。問題是,這個驅動程序阻塞了。它在套接字級別阻塞。一個線程總會卡在那裡,直到它返回一個響應。

毋庸置疑,驅動程序一直是實現完全無阻塞應用程序的瓶頸。幸運的是,在具有多個活動分叉的非同步驅動程序上取得了進展(儘管是非官方的),其中包括:

  • https://github.com/jasync-sql/jasync-sql(適用於Postgres和MySql)
  • https://github.com/reactiverse/reactive-pg-client(Postgres)

黃金法則

使用Vert.x非常簡單,只需幾行代碼即可啟動http伺服器。

val vertx = Vertx.vertx()
vertx.createHttpServer().requestHandler(req => {

}).listen(8080)

方法requestHandler是事件循環傳遞請求事件的地方。由於Vert.x沒有意見,處理它是自由的風格。但請記住非阻塞線程的唯一重要規則:不要阻止它。

在使用併發時,我們可以從如今的許多選項中獲取,例如Promise,Future,Rx,以及Vert.x自己的慣用方法。但隨著應用程序複雜性的增加,單獨使用非同步功能是不夠的。我們還需要輕鬆協調和鏈接調用,同時避免回調地獄,以及優雅地傳遞任何錯誤。

Scala Future滿足上述所有條件,並具有基於函數式編程原理的額外優勢。雖然本文不深入探討Scala Future,但我們可以通過一個簡單的應用程序來嘗試它。

假設該應用程序是一個API服務,用於查找給定其ID的用戶:

val vertx = Vertx.vertx()
vertx.createHttpServer().requestHandler(req => {

req.path() match {
  case p if p contains("/user") =>
  val f = for {
    f1 <- Future { req.getParam("id").get.toInt }
    f2 <- if (f1 < 100) Future.unit else Future.failed(CustomException())
    f3 <- Future { getUserFromDb(f1) }
  } yield f3
  f map (r => printout(req, r)) recover {case exception => printout(req, handleException(exception))}

  case _ => printout(req, "Default page")
}

})
.listen(8080)

def printout(req: HttpServerRequest, msg: String) = req.response().end(msg)

def handleException(e: Throwable): String = {
e match {
  case t: NoSuchElementException => "Missing parameter"
  case t: NumberFormatException => "Parameter not number"
  case t: CustomException => "Custom exception"
  case t: SQLException => "Database error"
  case _ => "Unknown error"
}
}

def getUserFromDb(id: Int) = "mock user name"

case class CustomException() extends Exception("custom exception")

涉及三個操作:檢查請求參數,檢查id是否有效以及獲取數據。我們將把這些操作包裝在Future中,並在「for comprehension」結構中協調執行。

第一步是將請求與服務匹配。

Scala具有強大的模式匹配功能,我們可以將其用於此目的。在這裡,我們攔截任何提及「/ user」並將其傳遞給我們的服務。

接下來是這項服務的核心,我們的期貨按順序排列。

第一個furture 未來f1包裝參數檢查。我們特別想從get請求中檢索id並將其轉換為int。(如果返回值是方法中的最後一行,Scala不需要顯式返回。)如您所見,此操作可能會拋出異常,因為id可能不是int或甚至不可用,但現在可以。

第二個furture f2檢查id的有效性。

我們通過使用我們自己的CustomException顯式調用Future.failed來阻止任何低於100的id。否則,我們以Future.unit的形式傳遞一個空的Future作為成功驗證。

最後的furture f3將使用f1提供的id檢索用戶。

由於這只是一個示例,我們並沒有真正連接到資料庫。我們只返回一些模擬字元串。

map運行從f3生成用戶數據的排列,然後將其列印到響應中。

現在,如果在序列的任何部分發生錯誤,則傳遞Throwable進行恢復。

在這裡,我們可以將其類型與合適的恢復策略相匹配。回顧一下我們的代碼,我們已經預料到了幾個潛在的失敗,例如缺少id,或者id不是int或者無效會導致特定異常。我們通過向客戶端傳遞錯誤消息來處理handleException中的每一個。

這種安排不僅提供從開始到結束的非同步流程,還提供處理錯誤的乾淨方法。由於它是跨處理程序的簡化,我們可以專註於重要的事情,如資料庫查詢。

Verticles,Event Bus和其他陷阱

Vert.x還提供了一個名為verticle的併發模型,類似於Actor系統。Verticle隔離其狀態和行為以提供線程安全的環境。與之通信的唯一方法是通過事件匯流排。

但是,Vert.x事件匯流排要求其消息為String或JSON。

這使得傳遞任意非POJO對象變得困難。在高性能系統中,處理JSON轉換是不可取的,因為它會帶來一些計算成本。如果您正在開發IO應用程序,最好不要使用Verticle或事件匯流排 ,因為這樣的應用程序幾乎不需要本地狀態。

使用某些Vert.x組件也非常具有挑戰性。

您可能會發現缺少文檔,意外行為甚至無法正常運行。Vert.x可能正在遭受其雄心壯志,因為開發新組件需要移植多種語言。這是一項艱巨的任務。因此,堅持核心將是最好的。

如果您正在開發公共API,那麼vertx-core就足夠了。如果它是一個Web應用程序,您可以添加vertx-web,它提供http參數處理和JWT / Session身份驗證。

無論如何,這兩個是主導基準的。在使用vertx-web的一些測試中,性能有所下降,但由於它似乎源於優化,因此可能會在後續版本中得到解決。



2020-02-17

阿里雲 Redis 開發規範深入解讀,別只會 set、get!

 

  • Key命名設計:可讀性、可管理性、簡介性
  • Value設計:拒絕bigkey
  • 控制Key的生命周期:設定過期時間
  • 時間複雜度為O(n)的命令需要注意N的數量
  • 禁用命令:KEYS、FLUSHDB、FLUSHALL等
  • 推薦使用批量操作提升操作效率
  • monitor命令控制使用時間
  • 寫在最後

Key命名設計:可讀性、可管理性、簡介性

規範建議使用冒號即:進行分割拼接,因為很多Redis客戶端是根據冒號分類的。比如有幾個Key:apps:app:1、apps:app:2和apps:app:3。Redis Desktop Manager能自動歸類到apps目錄下。如下圖所示:

Value設計:拒絕bigkey

規範建議String類型的Value控制在10KB範圍以內。這是因為Redis隨著Value不斷增長,在超過10KB后,有一個非常奇妙的性能拐點,如下圖所示(圖片來自Redis官網:http://redis.cn/topics/benchmarks.html

假設內網帶寬是千兆網卡,即1000MB。假設你的Redis中有一個大Key的Value長度是10KB,並且這個Key的QPS是10W,那麼這一個Key就會把帶寬打滿:10KB*100000=1000MB。

控制Key的生命周期:設定過期時間

儘可能對每一個Key都設置過期時間,這個是非常有益處的。否則,你想想一下,半年以後,一年以後,你的Redis集群中有上百G甚至更多的數據,誰都不知道這些數據哪些是有價值的,哪些已經成為垃圾。如果你的每個Key都設置了過期時間,那麼就不會出現這個問題了。集群在運行過程中,或自動淘汰那些已經不再使用的垃圾緩存數據。

時間複雜度為O(n)的命令需要注意N的數量

這個建議的意思是,以List類型為例,LINDEX、LREM等命令的時間複雜度就是O(n)。也就是說,隨著List中元素數量越來越多,這些命令的性能越來越差。而Redis又是單線程的,如果出現一個慢命令,會導致在這個命令之後執行的命令耗時也會增長,這是使用Redis的大忌。

事實上這也是JDK8為什麼要對HashMap進行鏈條衝突優化:當entry數量不少於64時,如果衝突鏈表長度達到8,就會將其轉成紅黑樹。因為鏈表長度越長,性能會越來越差。

禁用命令:KEYS、FLUSHDB、FLUSHALL等

這些命令應該在搭建Redis環境的時候就要禁用掉(在config配置文件中通過rename-command禁用)。FLUSHDB和FLUSHALL這兩個命令會清空數據,後果可想而知。

至於KEYS命令,還記得那個由於使用這個命令導致幾百萬損失的案例嘛?而且,這個命令的不當使用導致的損失,會隨著你的業務並大越大價值越大而導致損失越大:

推薦使用批量操作提升操作效率

批量命令主要分為兩類,原生命令和非原生命令:

  • 原生命令包括:例如mget、mset、hmget、hmset、LPUSH key value集合等。
  • 非原生命令包括:Pipeline。

合理使用這些命令對操作性能提升是極其巨大 的,尤其在單機Redis或者Sentinel模式下。因為這兩種架構不涉及跨Slot,Redis集群性能也有提升,但是使用會受到一些限制,例如不支持跨Slot的操作。

當然批量雖好,但不要貪多。俗話說的好,貪多嚼不爛。一般不要超過1000,具體限制還與操作數據大小有關。

monitor命令控制使用時間

monitor命令一般是用來觀察redis服務端都在執行哪些命令並實時輸出。例如在其他redis-cli中執行兩個set命令,在monitor中監控結果如下:

afeiMacBook-Pro:redis-3.2.11 afei$ src/redis-cli monitor
OK
1573915193.053188 [0 127.0.0.1:55357] "COMMAND"
1573915197.087383 [0 127.0.0.1:55357] "set" "name" "afei"
1573915217.938838 [0 127.0.0.1:55357] "set" "公 眾 號" "阿飛的博客"

之所以規範建議控制monitor命令的使用時間,是因為隨著monitor命令執行時間越來越長,會導致越來越多的數據積壓在輸出緩衝區,從而導致輸出緩衝區佔用內存越來越大。而且,這種影響會由於Redis併發越高,而更加放大。關於這個問題,美團有一個很經典的案例,感興趣的同學可以搜索關鍵詞:「美團在REDIS上踩過的一些坑-3.REDIS內存佔用飆升 」。

寫在最後

總而言之,任何一門技術都有利有弊,技術的世界里沒有銀彈 。所以,我們對使用到生產環境的任何一個技術,都要非常熟悉:知道它所擅長的和它的弱點,這樣才能結合自己的項目特點,設計出更合理的架構,編寫出最合理的代碼,不給生產環境造成影響,不給公司帶來損失 — 千萬不要成為那個執行KEYS命令導致給公司造成金錢損失的人!



2020-02-17

分散式系統領域,有哪些經典演算法?

身為後端工程師,你對這類招聘要求肯定不陌生:熟悉分散式系統的設計和應用;熟悉分散式、緩存、消息、搜索等機制;對分散式常用技術進行合理應用、解決問題等等。

 

而這其中,又以分散式協議與演算法尤甚。很多大公司在招聘架構師或高級工程師時,都要求熟悉這部分內容。面試官不僅要考察其原理和運行機制,還有面試者對這類問題在架構設計層面的理解,以及具體場景下的應用。

 

但以我作為面試官的經驗看,真正搞懂這部分的候選人少之又少

 

其實,分散式協議與演算法(下面簡稱分散式演算法)是分散式系統運行的核心規則和關鍵步驟,想參透分散式技術、開發一個分散式系統,最先要掌握的就是這部分知識

 

以 InfluxDB 為例,很多技術團隊試圖自己實現 InfluxDB 的集群功能,但最終都放棄了。因為這裡面的坑實在太多,甚至有人在接入性能敏感的場景,該使用反熵(Anti-Entropy)演算法時,卻用了 Raft 演算法,使得集群性能約等同於單機。

 

如果你要使用集群功能,又做不到基於開源版本自研,就只能購買人家的企業版。要知道,企業版每個節點的 License 授權費就要 1.5 萬美刀/年,具體貴在哪?它的護城河就是以分散式演算法為核心的分散式集群能力

 

由此可見,真正掌握分散式演算法的人並不多。大多數人只是會用分散式系統,卻並不具備分散式系統的獨立開發能力。

 

所以,不論是基於工作需要,還是想尋求長期職業發展、提升職場競爭力,分散式演算法作為分散式系統的核心,都是你在這個時代應該掌握的基本功。

如何高效學習分散式演算法?

 

如果你留心觀察,會發現有不少人看了很多資料和書籍,涉及到具體問題,照樣一頭霧水,比如:

 

•  拜占庭錯誤是怎麼回事?為什麼區塊鏈用拜占庭容錯演算法?Paxos 演算法不行嗎?能黑比特幣嗎?

•  想要實現數據副本的一致性,到底該選 Paxos 演算法,還是 Raft 演算法?

•  為什麼我的集群接入性能低?稍微出現峰值流量,為什麼業務就基本不可用了?
•  如何設計分散式系統架構呢?那麼多演算法,Paxos、Raft、Gossip、Nuorum NWR、PBFT 等等,究竟該選擇哪個?
 
其實,演算法相對抽象,即使是非常經典的論文,也有一些關鍵細節沒有交代清楚。而網上的信息大多是「複製粘貼」的結果,甚至有不少錯誤,給自主學習帶來了很多障礙和誤導。
 
在我看來,要掌握這部分內容,不僅要理解常用演算法的原理、特點和局限性,還要根據場景特點選擇適合的分散式演算法
 
剛好,極客時間上線了一個新專欄《分散式協議與演算法實戰》,作者是騰訊資深工程師韓健,我有幸提前看到了一部分內容,很想推薦給你。
 
在專欄中,他分享了自己支撐海量互聯網服務中的分散式演算法實戰心得,讓你學完就能在工作中根據場景特點,靈活地設計架構和運用分散式演算法,開發出適合該場景的分散式系統,對架構設計的理解,也會更上一層樓。
 

👆掃碼免費試讀
結算時輸入優惠口令「fenbushi6」到手僅 ¥50
僅限【前 200 個】名額有效
 
韓健是誰?
 
上面也提到了,韓健是騰訊資深工程師。
 
從重慶大學的軟體工程專業畢業后,他就開始和分散式系統打交道,至今有 10 多年了。早期,他接觸了電信級分散式系統,比如內核態 HA Cluster,現在是互聯網分散式系統,比如名字服務、NoSQL 存儲、監控大數平台等。
 
他做過創業公司的 CTO。加入騰訊后,曾負責 QQ 後台海量服務分散式中間件,現致力於時序資料庫 InfluxDB 自研集群系統的架構設計和研發工作。
 
他是如何講解分散式演算法的?
 
他將整個專欄劃分成三大模塊:
 
第一,理論篇,講解分散式架構設計的核心理論,讓你學完就能落地實踐。其中,涉及典型的分散式問題,分散式系統中的相互矛盾特性等,幫你在實戰中根據場景特點選擇適合的分散式演算法。
 
第二,協議和演算法篇,重點講解其原理、特點、適用場景和常見誤區。比如,你以為開發分散式系統使用 Raft 演算法就足夠了,其實它更適合性能要求不高的強一致性場景;又比如類似「Paxos 和 Raft 的區別在哪裡」等常見面試題,你都會在這部分找到答案。
 
第三,實戰篇,讓你掌握分散式基礎理論和分散式演算法在工程實踐中的應用。比如,剖析 InfluxDB 企業版的 CP 架構和 AP 架構的設計,以及 Raft、Quorum NWR、Anti-Entropy 等分散式演算法的具體實現。
 
我仔細看了下,學完實戰篇,你就真正了解如何根據場景特點選擇適合的分散式演算法,以及使用分散式演算法的實戰技巧。這樣,才能根據工作中的實際情況舉一反三,獨立思考、設計和開發。
 
除此之外,他還專門剖析了 Hashicorp Raft 的實現,並以一個分散式 KV 系統的開發實戰為例,帶你用 Raft 演算法開發一個分散式系統,讓你全面掌握分散式演算法的實戰能力。
 

2020-02-06

「12306」 是如何支撐百萬 QPS 的?

  • 12306搶票,極限併發帶來的思考?
  • 1. 大型高併發系統架構
    • 1.1 負載均衡簡介
    • 1.2 Nginx加權輪詢的演示
  • 2.秒殺搶購系統選型
    • 2.1 下單減庫存
    • 2.2 支付減庫存
    • 2.3 預扣庫存
  • 3. 扣庫存的藝術
  • 4. 代碼演示
    • 4.1 初始化工作
    • 4.2 本地扣庫存和統一扣庫存
    • 4.3 響應用戶信息
    • 4.4 單機服務壓測
  • 5.總結回顧

12306搶票,極限併發帶來的思考?

每到節假日期間,一二線城市返鄉、外出遊玩的人們幾乎都面臨著一個問題:搶火車票!雖然現在大多數情況下都能訂到票,但是放票瞬間即無票的場景,相信大家都深有體會。尤其是春節期間,大家不僅使用12306,還會考慮「智行」和其他的搶票軟體,全國上下幾億人在這段時間都在搶票。「12306服務」承受著這個世界上任何秒殺系統都無法超越的QPS,上百萬的併發再正常不過了!筆者專門研究了一下「12306」的服務端架構,學習到了其系統設計上很多亮點,在這裡和大家分享一下並模擬一個例子:如何在100萬人同時搶1萬張火車票時,系統提供正常、穩定的服務。github代碼地址

1. 大型高併發系統架構

高併發的系統架構都會採用分散式集群部署,服務上層有著層層負載均衡,並提供各種容災手段(雙火機房、節點容錯、伺服器災備等)保證系統的高可用,流量也會根據不同的負載能力和配置策略均衡到不同的伺服器上。下邊是一個簡單的示意圖:

img

1.1 負載均衡簡介

上圖中描述了用戶請求到伺服器經歷了三層的負載均衡,下邊分別簡單介紹一下這三種負載均衡:

  • OSPF(開放式最短鏈路優先)是一個內部網關協議(Interior Gateway Protocol,簡稱IGP)。OSPF通過路由器之間通告網路介面的狀態來建立鏈路狀態資料庫,生成最短路徑樹,OSPF會自動計算路由介面上的Cost值,但也可以通過手工指定該介面的Cost值,手工指定的優先於自動計算的值。OSPF計算的Cost,同樣是和介面帶寬成反比,帶寬越高,Cost值越小。到達目標相同Cost值的路徑,可以執行負載均衡,最多6條鏈路同時執行負載均衡。
  • LVS (Linux VirtualServer),它是一種集群(Cluster)技術,採用IP負載均衡技術和基於內容請求分發技術。調度器具有很好的吞吐率,將請求均衡地轉移到不同的伺服器上執行,且調度器自動屏蔽掉伺服器的故障,從而將一組伺服器構成一個高性能的、高可用的虛擬伺服器。
  • Nginx想必大家都很熟悉了,是一款非常高性能的http代理/反向代理伺服器,服務開發中也經常使用它來做負載均衡。Nginx實現負載均衡的方式主要有三種:輪詢、加權輪詢、ip hash輪詢,下面我們就針對Nginx的加權輪詢做專門的配置和測試

1.2 Nginx加權輪詢的演示

Nginx實現負載均衡通過upstream模塊實現,其中加權輪詢的配置是可以給相關的服務加上一個權重值,配置的時候可能根據伺服器的性能、負載能力設置相應的負載。下面是一個加權輪詢負載的配置,我將在本地的監聽3001-3004埠,分別配置1,2,3,4的權重:

#配置負載均衡
    upstream load_rule {
       server 127.0.0.1:3001 weight=1;
       server 127.0.0.1:3002 weight=2;
       server 127.0.0.1:3003 weight=3;
       server 127.0.0.1:3004 weight=4;
    }
    ...
    server {
    listen       80;
    server_name  load_balance.com www.load_balance.com;
    location / {
       proxy_pass http://load_rule;
    }
}

我在本地/etc/hosts目錄下配置了 www.load_balance.com的虛擬域名地址,接下來使用Go語言開啟四個http埠監聽服務,下面是監聽在3001埠的Go程序,其他幾個只需要修改埠即可:

package main

import (
    "net/http"
    "os"
    "strings"
)

func main() {
    http.HandleFunc("/buy/ticket", handleReq)
    http.ListenAndServe(":3001"nil)
}

//處理請求函數,根據請求將響應結果信息寫入日誌
func handleReq(w http.ResponseWriter, r *http.Request) {
    failedMsg :=  "handle in port:"
    writeLog(failedMsg, "./stat.log")
}

//寫入日誌
func writeLog(msg string, logPath string) {
    fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
    defer fd.Close()
    content := strings.Join([]string{msg, "/r/n"}, "3001")
    buf := []byte(content)
    fd.Write(buf)
}

我將請求的埠日誌信息寫到了./stat.log文件當中,然後使用ab壓測工具做壓測:

ab -n 1000 -c 100 http://www.load_balance.com/buy/ticket

統計日誌中的結果,3001-3004埠分別得到了100、200、300、400的請求量,這和我在nginx中配置的權重佔比很好的吻合在了一起,並且負載后的流量非常的均勻、隨機。具體的實現大家可以參考nginx的upsteam模塊實現源碼,這裡推薦一篇文章:Nginx 中 upstream 機制的負載均衡

2.秒殺搶購系統選型

回到我們最初提到的問題中來:火車票秒殺系統如何在高併發情況下提供正常、穩定的服務呢?

從上面的介紹我們知道用戶秒殺流量通過層層的負載均衡,均勻到了不同的伺服器上,即使如此,集群中的單機所承受的QPS也是非常高的。如何將單機性能優化到極致呢?要解決這個問題,我們就要想明白一件事:通常訂票系統要處理生成訂單、減扣庫存、用戶支付這三個基本的階段,我們系統要做的事情是要保證火車票訂單不超賣、不少賣,每張售賣的車票都必須支付才有效,還要保證系統承受極高的併發。這三個階段的先後順序改怎麼分配才更加合理呢?我們來分析一下:

2.1 下單減庫存

當用戶併發請求到達服務端時,首先創建訂單,然後扣除庫存,等待用戶支付。這種順序是我們一般人首先會想到的解決方案,這種情況下也能保證訂單不會超賣,因為創建訂單之後就會減庫存,這是一個原子操作。但是這樣也會產生一些問題,第一就是在極限併發情況下,任何一個內存操作的細節都至關影響性能,尤其像創建訂單這種邏輯,一般都需要存儲到磁碟資料庫的,對資料庫的壓力是可想而知的;第二是如果用戶存在惡意下單的情況,只下單不支付這樣庫存就會變少,會少賣很多訂單,雖然服務端可以限制IP和用戶的購買訂單數量,這也不算是一個好方法。

img

2.2 支付減庫存

如果等待用戶支付了訂單在減庫存,第一感覺就是不會少賣。但是這是併發架構的大忌,因為在極限併發情況下,用戶可能會創建很多訂單,當庫存減為零的時候很多用戶發現搶到的訂單支付不了了,這也就是所謂的「超賣」。也不能避免併發操作資料庫磁碟IO

img

2.3 預扣庫存

從上邊兩種方案的考慮,我們可以得出結論:只要創建訂單,就要頻繁操作資料庫IO。那麼有沒有一種不需要直接操作資料庫IO的方案呢,這就是預扣庫存。先扣除了庫存,保證不超賣,然後非同步生成用戶訂單,這樣響應給用戶的速度就會快很多;那麼怎麼保證不少賣呢?用戶拿到了訂單,不支付怎麼辦?我們都知道現在訂單都有有效期,比如說用戶五分鐘內不支付,訂單就失效了,訂單一旦失效,就會加入新的庫存,這也是現在很多網上零售企業保證商品不少賣採用的方案。訂單的生成是非同步的,一般都會放到MQ、kafka這樣的即時消費隊列中處理,訂單量比較少的情況下,生成訂單非常快,用戶幾乎不用排隊。

img

3. 扣庫存的藝術

從上面的分析可知,顯然預扣庫存的方案最合理。我們進一步分析扣庫存的細節,這裡還有很大的優化空間,庫存存在哪裡?怎樣保證高併發下,正確的扣庫存,還能快速的響應用戶請求?

在單機低併發情況下,我們實現扣庫存通常是這樣的:

img

為了保證扣庫存和生成訂單的原子性,需要採用事務處理,然後取庫存判斷、減庫存,最後提交事務,整個流程有很多IO,對資料庫的操作又是阻塞的。這種方式根本不適合高併發的秒殺系統。

接下來我們對單機扣庫存的方案做優化:本地扣庫存。我們把一定的庫存量分配到本地機器,直接在內存中減庫存,然後按照之前的邏輯非同步創建訂單。改進過之後的單機系統是這樣的:

img

這樣就避免了對資料庫頻繁的IO操作,只在內存中做運算,極大的提高了單機抗併發的能力。但是百萬的用戶請求量單機是無論如何也抗不住的,雖然nginx處理網路請求使用epoll模型,c10k的問題在業界早已得到了解決。但是linux系統下,一切資源皆文件,網路請求也是這樣,大量的文件描述符會使操作系統瞬間失去響應。上面我們提到了nginx的加權均衡策略,我們不妨假設將100W的用戶請求量平均均衡到100台伺服器上,這樣單機所承受的併發量就小了很多。然後我們每台機器本地庫存100張火車票,100台伺服器上的總庫存還是1萬,這樣保證了庫存訂單不超賣,下面是我們描述的集群架構:

img

問題接踵而至,在高併發情況下,現在我們還無法保證系統的高可用,假如這100台伺服器上有兩三台機器因為扛不住併發的流量或者其他的原因宕機了。那麼這些伺服器上的訂單就賣不出去了,這就造成了訂單的少賣。要解決這個問題,我們需要對總訂單量做統一的管理,這就是接下來的容錯方案。伺服器不僅要在本地減庫存,另外要遠程統一減庫存。有了遠程統一減庫存的操作,我們就可以根據機器負載情況,為每台機器分配一些多餘的「buffer庫存」用來防止機器中有機器宕機的情況。我們結合下面架構圖具體分析一下:

img

我們採用Redis存儲統一庫存,因為Redis的性能非常高,號稱單機QPS能抗10W的併發。在本地減庫存以後,如果本地有訂單,我們再去請求redis遠程減庫存,本地減庫存和遠程減庫存都成功了,才返回給用戶搶票成功的提示,這樣也能有效的保證訂單不會超賣。當機器中有機器宕機時,因為每個機器上有預留的buffer余票,所以宕機機器上的余票依然能夠在其他機器上得到彌補,保證了不少賣。buffer余票設置多少合適呢,理論上buffer設置的越多,系統容忍宕機的機器數量就越多,但是buffer設置的太大也會對redis造成一定的影響。雖然redis內存資料庫抗併發能力非常高,請求依然會走一次網路IO,其實搶票過程中對redis的請求次數是本地庫存和buffer庫存的總量,因為當本地庫存不足時,系統直接返回用戶「已售罄」的信息提示,就不會再走統一扣庫存的邏輯,這在一定程度上也避免了巨大的網路請求量把redis壓跨,所以buffer值設置多少,需要架構師對系統的負載能力做認真的考量。

4. 代碼演示

Go語言原生為併發設計,我採用go語言給大家演示一下單機搶票的具體流程。

4.1 初始化工作

go包中的init函數先於main函數執行,在這個階段主要做一些準備性工作。我們系統需要做的準備工作有:初始化本地庫存、初始化遠程redis存儲統一庫存的hash鍵值、初始化redis連接池;另外還需要初始化一個大小為1的int類型chan,目的是實現分散式鎖的功能,也可以直接使用讀寫鎖或者使用redis等其他的方式避免資源競爭,但使用channel更加高效,這就是go語言的哲學:不要通過共享內存來通信,而要通過通信來共享內存。redis庫使用的是redigo,下面是代碼實現:

...
//localSpike包結構體定義
package localSpike

type LocalSpike struct {
    LocalInStock     int64
    LocalSalesVolume int64
}
...
//remoteSpike對hash結構的定義和redis連接池
package remoteSpike
//遠程訂單存儲健值
type RemoteSpikeKeys struct {
    SpikeOrderHashKey string    //redis中秒殺訂單hash結構key
    TotalInventoryKey string    //hash結構中總訂單庫存key
    QuantityOfOrderKey string   //hash結構中已有訂單數量key
}

//初始化redis連接池
func NewPool() *redis.Pool {
    return &redis.Pool{
        MaxIdle:   10000,
        MaxActive: 12000// max number of connections
        Dial: func() (redis.Conn, error) {
            c, err := redis.Dial("tcp"":6379")
            if err != nil {
                panic(err.Error())
            }
            return c, err
        },
    }
}
...
func init() {
    localSpike = localSpike2.LocalSpike{
        LocalInStock:     150,
        LocalSalesVolume: 0,
    }
    remoteSpike = remoteSpike2.RemoteSpikeKeys{
        SpikeOrderHashKey:  "ticket_hash_key",
        TotalInventoryKey:  "ticket_total_nums",
        QuantityOfOrderKey: "ticket_sold_nums",
    }
    redisPool = remoteSpike2.NewPool()
    done = make(chan int1)
    done <- 1
}

4.2 本地扣庫存和統一扣庫存

本地扣庫存邏輯非常簡單,用戶請求過來,添加銷量,然後對比銷量是否大於本地庫存,返回bool值:

package localSpike
//本地扣庫存,返回bool值
func (spike *LocalSpike) LocalDeductionStock() bool{
    spike.LocalSalesVolume = spike.LocalSalesVolume + 1
    return spike.LocalSalesVolume < spike.LocalInStock
}

注意這裡對共享數據LocalSalesVolume的操作是要使用鎖來實現的,但是因為本地扣庫存和統一扣庫存是一個原子性操作,所以在最上層使用channel來實現,這塊後邊會講。統一扣庫存操作redis,因為redis是單線程的,而我們要實現從中取數據,寫數據並計算一些列步驟,我們要配合lua腳本打包命令,保證操作的原子性:

package remoteSpike
......
const LuaScript = `
        local ticket_key = KEYS[1]
        local ticket_total_key = ARGV[1]
        local ticket_sold_key = ARGV[2]
        local ticket_total_nums = tonumber(redis.call('HGET', ticket_key, ticket_total_key))
        local ticket_sold_nums = tonumber(redis.call('HGET', ticket_key, ticket_sold_key))
        -- 查看是否還有餘票,增加訂單數量,返回結果值
       if(ticket_total_nums >= ticket_sold_nums) then
            return redis.call('HINCRBY', ticket_key, ticket_sold_key, 1)
        end
        return 0
`
//遠端統一扣庫存
func (RemoteSpikeKeys *RemoteSpikeKeys) RemoteDeductionStock(conn redis.Conn) bool {
    lua := redis.NewScript(1, LuaScript)
    result, err := redis.Int(lua.Do(conn, RemoteSpikeKeys.SpikeOrderHashKey, RemoteSpikeKeys.TotalInventoryKey, RemoteSpikeKeys.QuantityOfOrderKey))
    if err != nil {
        return false
    }
    return result != 0
}

我們使用hash結構存儲總庫存和總銷量的信息,用戶請求過來時,判斷總銷量是否大於庫存,然後返回相關的bool值。在啟動服務之前,我們需要初始化redis的初始庫存信息:

 hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 0

4.3 響應用戶信息

我們開啟一個http服務,監聽在一個埠上:

package main
...
func main() {
    http.HandleFunc("/buy/ticket", handleReq)
    http.ListenAndServe(":3005"nil)
}

上面我們做完了所有的初始化工作,接下來handleReq的邏輯非常清晰,判斷是否搶票成功,返回給用戶信息就可以了。

package main
//處理請求函數,根據請求將響應結果信息寫入日誌
func handleReq(w http.ResponseWriter, r *http.Request) {
    redisConn := redisPool.Get()
    LogMsg := ""
    <-done
    //全局讀寫鎖
    if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) {
        util.RespJson(w, 1,  "搶票成功"nil)
        LogMsg = LogMsg + "result:1,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
    } else {
        util.RespJson(w, -1"已售罄"nil)
        LogMsg = LogMsg + "result:0,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
    }
    done <- 1

    //將搶票狀態寫入到log中
    writeLog(LogMsg, "./stat.log")
}

func writeLog(msg string, logPath string) {
    fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
    defer fd.Close()
    content := strings.Join([]string{msg, "/r/n"}, "")
    buf := []byte(content)
    fd.Write(buf)
}

前邊提到我們扣庫存時要考慮競態條件,我們這裡是使用channel避免併發的讀寫,保證了請求的高效順序執行。我們將介面的返回信息寫入到了./stat.log文件方便做壓測統計。

4.4 單機服務壓測

開啟服務,我們使用ab壓測工具進行測試:

ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket

下面是我本地低配mac的壓測信息

This is ApacheBench, Version 2.3 <$Revision: 1826891 

gt;
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:
Server Hostname:        127.0.0.1
Server Port:            3005

Document Path:          /buy/ticket
Document Length:        29 bytes

Concurrency Level:      100
Time taken for tests:   2.339 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      1370000 bytes
HTML transferred:       290000 bytes
Requests per second:    4275.96 [#/sec] (mean)
Time per request:       23.387 [ms] (mean)
Time per request:       0.234 [ms] (mean, across all concurrent requests)
Transfer rate:          572.08 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    8  14.7      6     223
Processing:     2   15  17.6     11     232
Waiting:        1   11  13.5      8     225
Total:          7   23  22.8     18     239

Percentage of the requests served within a certain time (ms)
  50%     18
  66%     24
  75%     26
  80%     28
  90%     33
  95%     39
  98%     45
  99%     54
 100%    239 (longest request)

根據指標顯示,我單機每秒就能處理4000+的請求,正常伺服器都是多核配置,處理1W+的請求根本沒有問題。而且查看日誌發現整個服務過程中,請求都很正常,流量均勻,redis也很正常:

//stat.log
...
result:1,localSales:145
result:1,localSales:146
result:1,localSales:147
result:1,localSales:148
result:1,localSales:149
result:1,localSales:150
result:0,localSales:151
result:0,localSales:152
result:0,localSales:153
result:0,localSales:154
result:0,localSales:156
...

5.總結回顧

總體來說,秒殺系統是非常複雜的。我們這裡只是簡單介紹模擬了一下單機如何優化到高性能,集群如何避免單點故障,保證訂單不超賣、不少賣的一些策略,完整的訂單系統還有訂單進度的查看,每台伺服器上都有一個任務,定時的從總庫存同步余票和庫存信息展示給用戶,還有用戶在訂單有效期內不支付,釋放訂單,補充到庫存等等。

我們實現了高併發搶票的核心邏輯,可以說系統設計的非常的巧妙,巧妙的避開了對DB資料庫IO的操作,對Redis網路IO的高併發請求,幾乎所有的計算都是在內存中完成的,而且有效的保證了不超賣、不少賣,還能夠容忍部分機器的宕機。我覺得其中有兩點特別值得學習總結:

  • 負載均衡,分而治之。通過負載均衡,將不同的流量劃分到不同的機器上,每台機器處理好自己的請求,將自己的性能發揮到極致,這樣系統的整體也就能承受極高的併發了,就像工作的的一個團隊,每個人都將自己的價值發揮到了極致,團隊成長自然是很大的。
  • 合理的使用併發和非同步。自epoll網路架構模型解決了c10k問題以來,非同步越來被服務端開發人員所接受,能夠用非同步來做的工作,就用非同步來做,在功能拆解上能達到意想不到的效果,這點在nginx、node.js、redis上都能體現,他們處理網路請求使用的epoll模型,用實踐告訴了我們單線程依然可以發揮強大的威力。伺服器已經進入了多核時代,go語言這種天生為併發而生的語言,完美的發揮了伺服器多核優勢,很多可以併發處理的任務都可以使用併發來解決,比如go處理http請求時每個請求都會在一個goroutine中執行,總之:怎樣合理的壓榨CPU,讓其發揮出應有的價值,是我們一直需要探索學習的方向。



2020-02-06

SQL 注入是什麼?一張圖告訴你!

來源:jizhi.im/blog/post/sql_injection_intro

 

先來看一副很有意思的漫畫:

相信大家對於學校們糟糕的網路環境和運維手段都早有體會,在此就不多做吐槽了。今天我們來聊一聊SQL注入相關的內容。

何謂SQL注入?

SQL注入是一種非常常見的資料庫攻擊手段,SQL注入漏洞也是網路世界中最普遍的漏洞之一。大家也許都聽過某某學長通過攻擊學校資料庫修改自己成績的事情,這些學長們一般用的就是SQL注入方法。

SQL注入其實就是惡意用戶通過在表單中填寫包含SQL關鍵字的數據來使資料庫執行非常規代碼的過程。簡單來說,就是數據「越俎代庖」做了代碼才能幹的事情。

這個問題的來源是,SQL資料庫的操作是通過SQL語句來執行的,而無論是執行代碼還是數據項都必須寫在SQL語句之中,這就導致如果我們在數據項中加入了某些SQL語句關鍵字(比如說SELECT、DROP等等),這些關鍵字就很可能在資料庫寫入或讀取數據時得到執行。

多言無益,我們拿真實的案例來說話。下面我們先使用SQLite建立一個學生檔案表。

SQL資料庫操作示例:

import sqlite3

連接資料庫:

conn = sqlite3.connect('test.db')

建立新的數據表:

conn.executescript('''DROP TABLE IF EXISTS students;
       CREATE TABLE students
       (id INTEGER PRIMARY KEY AUTOINCREMENT,
       name TEXT NOT NULL);''')

插入學生信息:

students = ['Paul','Tom','Tracy','Lily']

for name in students:
    query = "INSERT INTO students (name) VALUES ('%s')" % (name)
    conn.executescript(query);

檢視已有的學生信息:

cursor = conn.execute("SELECT id, name from students")
print('IDName')
for row in cursor:
    print('{0}{1}'.format(row[0], row[1]))

conn.close()

點擊運行按鈕將會列印目前表中的內容。上述程序中我們建立了一個test.db資料庫以及一個students數據表,並向表中寫入了四條學生信息。

那麼SQL注入又是怎麼一回事呢?我們嘗試再插入一條惡意數據,數據內容就是漫畫中的”Robert’);DROP TABLE students;–“,看看會發生什麼情況。

SQL資料庫注入示例:

conn = sqlite3.connect('test.db')

插入包含注入代碼的信息:

name = "Robert');DROP TABLE students;--"
query = "INSERT INTO students (nameVALUES ('%s')" % (name)

conn.executescript(query)

檢視已有的學生信息:

cursor = conn.execute("SELECT id, name from students")
print('IDName')
for row in cursor:
    print('{0}{1}'.format(row[0], row[1]))

conn.close()

你將會發現,運行后,程序沒有輸出任何數據內容,而是返回一條錯誤信息:表單students無法找到!

這是為什麼呢?問題就在於我們所插入的數據項中包含SQL關鍵字DROP TABLE,這兩個關鍵字的意義是從資料庫中清除一個表單。

而關鍵字之前的Robert’);使得SQL執行器認為上一命令已經結束,從而使得危險指令DROP TABLE得到執行。

也就是說,這段包含DROP TABLE關鍵字的數據項使得原有的簡單的插入姓名信息的SQL語句:

INSERT INTO students (nameVALUES ('Robert')

變為了同時包含另外一條清除表單命令的語句:

INSERT INTO students (nameVALUES ('Robert');DROP TABLE students;
而SQL資料庫執行上述操作后,students表單被清除,因而表單無法找到,所有數據項丟失。

如何防止SQL注入問題呢?

大家也許都想到了,注入問題都是因為執行了數據項中的SQL關鍵字,那麼,只要檢查數據項中是否存在SQL關鍵字不就可以了么?

的確是這樣,很多資料庫管理系統都是採取了這種看似『方便快捷』的過濾手法,但是這並不是一種根本上的解決辦法,如果有個美國人真的就叫做『Drop Table』呢?你總不能逼人家改名字吧。

合理的防護辦法有很多。首先,盡量避免使用常見的資料庫名和資料庫結構。在上面的案例中,如果表單名字並不是students,則注入代碼將會在執行過程中報錯,也就不會發生數據丟失的情況——SQL注入並不像大家想象得那麼簡單,它需要攻擊者本身對於資料庫的結構有足夠的了解才能成功,因而在構建資料庫時盡量使用較為複雜的結構和命名方式將會極大地減少被成功攻擊的概率。

使用正則表達式等字元串過濾手段限制數據項的格式、字元數目等也是一種很好的防護措施。理論上,只要避免數據項中存在引號、分號等特殊字元就能很大程度上避免SQL注入的發生。

另外,就是使用各類程序文檔所推薦的資料庫操作方式來執行數據項的查詢與寫入操作,比如在上述的案例中,如果我們稍加修改,首先使用execute()方法來保證每次執行僅能執行一條語句,然後將數據項以參數的方式與SQL執行語句分離開來,就可以完全避免SQL注入的問題,如下SQL資料庫反注入示例。

conn = sqlite3.connect('test.db')

以安全方式插入包含注入代碼的信息:

name = "Robert');DROP TABLE students;--"
query = "INSERT INTO students (nameVALUES (?)"

conn.execute(query, [name])

檢視已有的學生信息:

cursor = conn.execute("SELECT id, name from students")
print('IDName')
for row in cursor:
    print('{0}{1}'.format(row[0], row[1]))

conn.close()

而對於PHP而言,則可以通過mysql_real_escape_string等方法對SQL關鍵字進行轉義,必要時審查數據項目是否安全來防治SQL注入

當然,做好資料庫的備份,同時對敏感內容進行加密永遠是最重要的。某些安全性問題可能永遠不會有完美的解決方案,只有我們做好最基本的防護措施,才能在發生問題的時候亡羊補牢,保證最小程度的損失。

2020-02-05

js實現二級連動效果

實例講述了JS二級菜單。分享給大家供大家參考

 1<head>
 2    <meta charset="utf-8" />
 3    <title></title>
 4</head>
 5
 6<body>
 7    省/市:
 8    <select id="provice"></select>
 9    市/區:
10    <select id="city"></select>
11    <script type="text/javascript">
12        //定義省/直轄市數組
13        var arr_province = ["請選擇省/直轄市""北京市""上海市""廣東省"];
14        //定義市/區二維數組
15        var arr_city = [
16            ["請選擇市/區"],
17            ["東城區""西城區""朝陽區""宣武區""昌平區""大興區""丰台區""海淀區"],
18            ['寶山區''長寧區''豐賢區''虹口區''黃浦區''青浦區''南匯區''徐匯區''盧灣區'],
19            ['廣州市''惠州市''汕頭市''珠海市''佛山市''中山市''東莞市']
20        ];
21        var province=document.getElementById("provice");
22        var city=document.getElementById("city");
23        //初始化菜單
24        window.onload=function(){
25            province.length=arr_province.length;
26            for (var i=0;i<arr_province.length;i++) {
27                province.options[i].text=arr_province[i];
28            }
29            city.length=1;
30            city.options[0].text=arr_city[0][0];
31        }
32        //二級連動
33        province.onchange=function(){
34            var index=province.selectedIndex;
35            city.length=arr_city[index].length;
36            for (var i=0;i<city.length;i++) {
37                city.options[i].text=arr_city[index][i];
38            }
39        }
40    </script>
41</body>

                                                         

2020-02-05

php數據緩存memcached 集成可視化工具

Memcached安裝配置.exe 可能會報毒,可放心使用!
一、 安裝memcached服務
安裝啟動服務memcached.exe
二、 php_memcache 擴展
1、下載和 php 所需的 php_memcache.dll 擴展 ;對應PHP版本
2、解壓 php_memcache.dll 文件,到php的ext文件夾中
3、php.ini文件添加:
extension=php-xx_memcache.dll
[Memcache]
memcache.allow_failover = 1
memcache.max_failover_attempts=20
memcache.chunk_size =8192
memcache.default_port = 11211
4、重啟Apache,查看phpinfo,如果有 memcache 的說明
5、運行 example.php 文件,測試memcached服務
輸出:
Server’s version: 1.4.4-14-g9c660c0
Store data in the cache (data will expire in 10 seconds)
Data from the cache:
object(stdClass)#3 (2) { [“str_attr”]=> string(4) “test” [“int_attr”]=> int(123) }

鏈接: https://pan.baidu.com/s/1qkjKLIq010u_mUepX7KKWw 密碼: kxpp

修復win2008停止運行的情況

修復報毒情況

添加更新提醒

修復保存后不自動啟動的問題

2020-02-05

Laravel – 路由 [Route]

啥是路由 ?

如下圖:我們在瀏覽器中輸入 http://xslaravel.dev/ 我們可以訪問細說laravel網站的首頁界面,如果我們輸入 http://xslaravel.dev/users/1 呢? 我們會來到用戶個人中心的用戶頁面

上圖片中展示的這就是路由了,即路由系統會對用戶輸入的 URL 地址 進行解析,然後分配不同的工作,有點像路由器

Laravel是怎麼處理URL呢?

下圖路由中我們所看到的的那幾行代碼是項目自帶的指向歡迎界面的代碼

Route::get(‘/’, function(){

        return view(‘welcome’);

});

別小看這幾行代碼,通過這幾行代碼我們可以實現很有功能,我們慢慢道來

我們來改下上面展示的自帶的代碼

Route::get(‘xslaravel’, function(){

        return view(‘welcome’);

});

這時候再去訪問http://www.xslaravel.dev他就會報錯

大概的意思就是說我們沒有找到Url,因為我們沒有定義『/』 這條路由

此時我們在瀏覽器中訪問 http://www.xslaravel.dev/xslaravel 就可以輸出頁面了

通過上面的演示,我們應該對路由有了一個簡單的了解和認識了,那麼接下來我們來學習下基本路由

基本路由 ?

網站的大多數路由都定義在 app/Http/routes.php 文件中,這個路由文件將會被 App/Providers/RouteServiceProvider 類載入

 

最基本的 Laravel 路由僅接受 URI 和一個閉包,下面我們再定義兩條路由

 

文件位置:app/Http/routes.php

相關代碼:

app/Http/routes.php

Route::get(‘welcome’, function () {

    return view(‘welcome’);

});

Route::get(‘/’, function() {

    return ‘首頁頁面’;

});

Route::get(‘/help’, function() {

    return ‘幫助頁面’;

});

路由動作

一個Url請求有很多,如 GET POST PUT DELETE等多種類型,對應方法如下:

//該路由將匹配 post方法的 ‘/foo’ url

Route::post(‘/foo’, function() {

});

//該路由將匹配 put方法的 ‘/foo’ url

Route::put(‘/foo’, function() {

});

有時候你可能需要註冊一個可響應多個 HTTP 動作的路由。這時可通過 Route facade 的 match 方法來實現:

// 該路由將匹配 get 和 post 方法的 ‘foo’ url

Route::match([‘get’, ‘post’],’/foo’, function () {

});

甚至可以通過 any 方法來使用註冊路由並響應所有的 HTTP 動作:

 

// 該路由將匹配 所有 類型的 ‘foo’ url

Route::any(‘foo’, function () {

});

有時候你可能需要從 URI 中獲取一些參數

Route::get(‘user/{id}’, function ($id) {

return ‘User ‘.$id;

});

依照路由需要,定義任意數量的路由參數

Route::get(‘posts/{post}/comments/{comment}’, function ($postId, $commentId) {

});

當然你可以為該可選參數設定一個默認值,當 url 未傳參時,將顯示默認值

 

Route::get(‘Hi/{name?}’, function ($name = ‘devon’) {

return ‘Hi! ‘.$name;

});

路由命名

所謂命名路由,就是給路由起個名字,這樣我們就可以通過這個名字獲取到該條路由的相關信息,也更利於後期維護

 

建議在開發過程中給每個路由命名,使用下面兩種方式都可以為一個路由命名

 

名路由讓你可以更方便的為特定路由生成 URL 或進行重定向

 

Route::get(‘foo’, [‘as’ => ‘foo’, function () {

//

}]);

Route::get(‘foo’, function() {

//

})->name(‘foo’);

關於Laravel可講的其實還有很多,這裡不一一普及了,大家可以參考下官方文檔,後期也可以關注下《細說Laravel》書中會有詳細解讀

長按關注我們吧

2020-02-05

Laravel 用戶認證邏輯

預計閱讀 12 分鐘。

本文介紹 Laravel 用戶認證的使用、基本邏輯和底層的實現。

快速使用

在 Laravel 框架初始化后,運行 php artisan make:auth  和   php artisan migrate  就能啟用 Laravel 自帶的用戶認證功能。

  1. 用戶認證的配置文件在 config/auth.php 中,
1// 默認使用的配置。
2// guard 是用戶認證邏輯的實現。web 是網站用戶認證邏輯;
3// api 是 API 用戶認證邏輯
4'defaults' => [
5        'guard' => 'web',
6        'passwords' => 'users',
7],

guard 還需配置對應的 driverprovider
driver 配置對應具體實現 guard 的邏輯類;provider 是邏輯類使用的數據提供者(查詢、更新對應的用戶信息的具體實現類)。

1'guards' => [
2        'web' => [
3            'driver' => 'session',
4            'provider' => 'users',
5        ],
6        ...
7    ],
  1. 認證的路由信息在 Illuminate/Routing/Router.php 中,通過 Illuminate/Support/Facades/Authroutes() 方法引入。註冊和登錄部分如下:
 1public function auth()
 2{
 3        // Authentication Routes...
 4        $this->get('login''Auth/LoginController@showLoginForm')->name('login');
 5        $this->post('login''Auth/LoginController@login');
 6        $this->post('logout''Auth/LoginController@logout')->name('logout');
 7
 8        // Registration Routes...
 9        $this->get('register''Auth/RegisterController@showRegistrationForm')->name('register');
10        $this->post('register''Auth/RegisterController@register');
11
12        // Password Reset Routes...
13 }

由路由信息可知,註冊、登錄對應的控制器和方法。

  1. 限制認證用戶才能訪問
    通常我們有一些介面是需要登錄后才能訪問的,在 Laravel 中是給路由分組,並添加中間件來實現。
1// 請求到達路由中的 Controller 前,會先通過 auth 
2// 中間件驗證
3Route::middleware('auth')->group(function () {
4    Route::get('/user''UserController@index');
5    // ....
6});

App/Http/Kernel 中定義了 auth 中間件

1 protected $routeMiddleware = [
2        'auth' => /Illuminate/Auth/Middleware/Authenticate::class,
3        // ....
4]

註冊、登錄、登錄檢測邏輯

註冊邏輯

註冊邏輯在控制器 Auth/RegisterControllerregister 方法中,具體是由控制器中的 RegistersUsers   trait 類實現。

 1public function register(Request $request)
 2{
 3     // 調用控制器中的方法對參數進行驗證,包括 
 4     // name/email/password
 5     $this->validator($request->all())->validate();
 6     // 調用控制器中 create 方法創建用戶
 7     // 使用 $user 初始化 Redistered 對象
 8     // 通過 event() 函數觸發註冊事件
 9     event(new Registered($user = $this->create($request->all())));
10
11     // 獲取 guard 實例,並通過 guard 登錄創建的用戶
12     $this->guard()->login($user);
13     // 根據 $user 註冊信息進行頁面跳轉
14     return $this->registered($request, $user)
15                        ?: redirect($this->redirectPath());
16}
17
18protected function guard()
19{
20    // 通過 Illuminate/Support/Facades/Auth 
21    // 獲取對應 guard 對象
22    return Auth::guard();
23}

登錄邏輯

登錄邏輯在控制器 Auth/LoginControllerlogin 方法中,具體是由控制器中的 AuthenticatesUsers   trait 類實現。

 1public function login(Request $request)
 2{
 3   // 參數驗證, email/password 參數
 4   $this->validateLogin($request);
 5
 6   // 使用 ThrottlesLogins trait 對登錄進行限制(
 7   // 一分鐘內,登錄失敗超過配置的次數,將不能進行登錄),
 8   // 防止惡意的登錄嘗試 
 9   // 限制的依據是 登錄的 email 和 IP 地址。
10   if ($this->hasTooManyLoginAttempts($request)) {
11        // 觸發登錄鎖定事件
12        $this->fireLockoutEvent($request);
13        // 返回鎖定的響應信息
14        return $this->sendLockoutResponse($request);
15   }
16   // 根據 $request 中的登錄憑證嘗試登錄
17   // 這裡實際以是獲取 guard 對象進行登錄嘗試 
18   if ($this->attemptLogin($request)) {
19       // 登錄成功后,重新生成 session,
20       // 並跳轉到設置的登錄成功頁面
21       return $this->sendLoginResponse($request);
22   }
23
24   // 沒登錄成功,增加登錄失敗次數,返回登錄失敗的響應
25   $this->incrementLoginAttempts($request);
26
27   return $this->sendFailedLoginResponse($request);
28}
29
30protected function attemptLogin(Request $request)
31{
32    // 通過調用 Illuminate/Support/Facades/Auth 
33    // 獲取 guard 對象,並通過 guard 進行實際的登錄邏輯
34    return $this->guard()->attempt(
35       $this->credentials($request), $request->has('remember')
36    );
37}

登錄檢測邏輯

中間件中通過 authenticate 方法檢測用戶是否登錄

 1protected function authenticate(array $guards)
 2{  
 3    // 默認時,傳遞的 $guards 為空
 4    if (empty($guards)) {
 5        // 調用注入的 auth 對象的 authenticate 方法。
 6        // auth 會調用默認 guard 的 authenticate 方法
 7        return $this->auth->authenticate();
 8    }
 9
10    foreach ($guards as $guard) {
11        // 調用特定 guard 的 check 方法
12        if ($this->auth->guard($guard)->check()) {
13            return $this->auth->shouldUse($guard);
14        }
15    }
16
17    throw new AuthenticationException('Unauthenticated.', $guards);
18}

認證底層實現源碼閱讀

上面 Controller 中的登錄和註冊邏輯最終都是調用 Illuminate/Support/Facades/Auth 來獲取 guard ,並通過 guard 來進行實際的登錄、註冊邏輯。
Illuminate/Support/Facades/Auth 是應用為 Illuminate/Auth/AuthManager 類提供的一個靜態的介面,所以應用最終是使用 AuthManager 中的 guard() 方法來獲取 guard 實例的。Facade 的原理可以參考文檔。
下面為獲取 guard   實例的主要邏輯:

 1public function guard($name = null)
 2{
 3    // 如果沒有傳 $name,就獲取默認的 guard 。 
 4    // 默認的為配置中的 web
 5    $name = $name ?: $this->getDefaultDriver();
 6    // 如果 guards 已經被實例化的,就直接調用,否則通過
 7    // resolve 方法創建 guard 對象
 8    return isset($this->guards[$name])
 9                ? $this->guards[$name]
10                : $this->guards[$name] = $this->resolve($name);
11}
12
13protected function resolve($name)
14{
15    // 獲取名稱對應的 guard 配置,
16    // 前面說了,包括 driver/provider
17    $config = $this->getConfig($name);
18    // 配置異常判斷
19    if (is_null($config)) {
20        throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
21    }
22    // 如果有設置自定義的 guard 創建方法,則用自定義的方法創建
23    // 自定義的方法通過類中的 extend 方法定義
24    if (isset($this->customCreators[$config['driver']])) {
25        return $this->callCustomCreator($name, $config);
26    }
27
28    $driverMethod = 'create'.ucfirst($config['driver']).'Driver';
29    // 根據配置,判斷是否有對應創建 guard 的方法,有則調用
30    // 系統自帶 createSessionDriver/createTokenDriver 了兩個
31    // 創建 guard 方法 
32    if (method_exists($this, $driverMethod)) {
33        return $this->{$driverMethod}($name, $config);
34    }
35    // 拋出 guard driver 沒有定義的異常
36    throw new InvalidArgumentException("Auth guard driver [{$name}] is not defined.");
37}

默認情況下,web guard 的 driver 為 session。所以是通過 createSessionDriver 方法將會返回一個 SessionGuard 實例。然後通過 SessionGuard 實例進行用戶認證邏輯。
這裡最靈活的地方在於還提供了一個 extend 方法來自定義 guard。使得可以方便的擴展自己的用戶認證。
下面是用戶認證的相關的類圖。

用戶認證類圖.png

最後

有問題,歡迎留言交流。

參考文檔

https://laravel-china.org/docs/laravel/5.4/authentication/1239