分類
發燒車訊

【Leetcode 做題學算法周刊】第四期

首發於微信公眾號《前端成長記》,寫於 2019.11.21

背景

本文記錄刷題過程中的整個思考過程,以供參考。主要內容涵蓋:

  • 題目分析設想
  • 編寫代碼驗證
  • 查閱他人解法
  • 思考總結

目錄

Easy

67.二進制求和

題目描述

給定兩個二進制字符串,返回他們的和(用二進製表示)。

輸入為非空字符串且只包含数字 10

示例:

輸入: a = "11", b = "1"
輸出: "100"

輸入: a = "1010", b = "1011"
輸出: "10101"

題目分析設想

這道題又是一道加法題,所以記住下,直接轉数字進行加法可能會溢出,所以不可取。所以我們需要遍歷每一位來做解答。我這有兩個大方向:補0后遍歷,和不補0遍歷。但是基本的依據都是本位相加,逢2進1即可,類似手寫10進制加法。

  • 補0后遍歷,可以採用先算出的位數推入數組最後反轉,也可以採用先算出的位數填到對應位置后直接輸出
  • 不補0遍歷,根據短數組的長度進行遍歷,長數組剩下的数字與短數組生成的進位進行計算

查閱他人解法

Ⅰ.補0后遍歷,先算先推

代碼:

/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
var addBinary = function(a, b) {
    let times = Math.max(a.length, b.length) // 需要遍歷次數
    // 補 0
    while(a.length < times) {
        a = '0' + a
    }
    while(b.length < times) {
        b = '0' + b
    }
    let res = []
    let carry = 0 // 是否進位
    for(let i = times - 1; i >= 0; i--) {
        const num = carry + (a.charAt(i) | 0) + (b.charAt(i) | 0)
        carry = num >= 2 ? 1 : 0
        res.push(num % 2)
    }
    if (carry === 1) {
        res.push(1)
    }
    return res.reverse().join('')
};

結果:

  • 294/294 cases passed (68 ms)
  • Your runtime beats 95.13 % of javascript submissions
  • Your memory usage beats 72.58 % of javascript submissions (35.4 MB)
  • 時間複雜度 O(n)

Ⅱ.補0后遍歷,按位運算

代碼:

/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
var addBinary = function(a, b) {
    let times = Math.max(a.length, b.length) // 需要遍歷次數
    // 補 0
    while(a.length < times) {
        a = '0' + a
    }
    while(b.length < times) {
        b = '0' + b
    }
    let res = []
    let carry = 0 // 是否進位
    for(let i = times - 1; i >= 0; i--) {
        res[i] = carry + (a.charAt(i) | 0) + (b.charAt(i) | 0)
        carry = res[i] >= 2 ? 1 : 0
        res[i] %= 2
    }
    if (carry === 1) {
        res.unshift(1)
    }
    return res.join('')
};

結果:

  • 294/294 cases passed (60 ms)
  • Your runtime beats 99.65 % of javascript submissions
  • Your memory usage beats 65.82 % of javascript submissions (35.5 MB)
  • 時間複雜度 O(n)

Ⅲ.不補0遍歷

當然處理方式還是可以選擇上面兩種,我這就採用先算先推來處理了。

代碼:

/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
var addBinary = function(a, b) {
    let max = Math.max(a.length, b.length) // 最大長度
    let min = Math.min(a.length, b.length) // 最大公共長度

    // 將長字符串拆成兩部分
    let left = a.length > b.length ? a.substr(0, a.length - b.length) : b.substr(0, b.length - a.length)
    let right = a.length > b.length ? a.substr(a.length - b.length) : b.substr(b.length - a.length)

    // 公共長度部分遍歷
    let rightRes = []
    let carry = 0
    for(let i = min - 1; i >= 0; i--) {
        const num = carry + (right.charAt(i) | 0) + (((a.length > b.length ? b : a)).charAt(i) | 0)
        carry = num >= 2 ? 1 : 0
        rightRes.push(num % 2)
    }

    let leftRes = []
    for(let j = max - min - 1; j >= 0; j--) {
        const num = carry + (left.charAt(j) | 0)
        carry = num >= 2 ? 1 : 0
        leftRes.push(num % 2)
    }

    if (carry === 1) {
        leftRes.push(1)
    }
    return leftRes.reverse().join('') + rightRes.reverse().join('')
};

結果:

  • 294/294 cases passed (76 ms)
  • Your runtime beats 80.74 % of javascript submissions
  • Your memory usage beats 24.48 % of javascript submissions (36.2 MB)
  • 時間複雜度 O(n)

查閱他人解法

看到一些細節上的區別,我這使用 '1' | 0 來轉数字,有的使用 ''1' - '0''。另外還有就是初始化結果數組長度為最大長度加1后,最後判斷首位是否為0需要剔除的,我這使用的是判斷最後是否還要進位補1。

這裏還看到用一個提案中的 BigInt 類型來解決的

Ⅰ.BigInt

代碼:

/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
var addBinary = function(a, b) {
    return (BigInt("0b"+a) + BigInt("0b"+b)).toString(2);
};

結果:

  • 294/294 cases passed (52 ms)
  • Your runtime beats 100 % of javascript submissions
  • Your memory usage beats 97.05 % of javascript submissions (34.1 MB)
  • 時間複雜度 O(1)

思考總結

通過 BigInt 的方案我們能看到,使用原生方法確實性能更優。簡單說一下這個類型,目前還在提案階段,看下面的等式基本就能知道實現原理自己寫對應 Hack 來實現了:

BigInt(10) = '10n'
BigInt(20) = '20n'
BigInt(10) + BigInt(20) = '30n'

雖然這種方式很友好,但是還是希望看到加法題的時候,能考慮到遍歷按位處理。

69.x的平方根

題目描述

實現 int sqrt(int x) 函數。

計算並返回 x 的平方根,其中 x 是非負整數。

由於返回類型是整數,結果只保留整數的部分,小數部分將被捨去。

示例:

輸入: 4
輸出: 2

輸入: 8
輸出: 2
說明: 8 的平方根是 2.82842...,
     由於返回類型是整數,小數部分將被捨去。

題目分析設想

同樣,這裏類庫提供的方法 Math.sqrt(x) 就不說了,這也不是本題想考察的意義。所以這裡有幾種方式:

  • 暴力法,這裏不用考慮溢出是因為x沒溢出,所以即使加到平方根加1,也會終止循環
  • 二分法,直接取中位數運算,可以快速排除當前區域一半的區間

編寫代碼驗證

Ⅰ.暴力法

代碼:

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    if (x === 0) return 0
    let i = 1
    while(i * i < x) {
        i++
    }
    return i * i === x ? i : i - 1
};

結果:

  • 1017/1017 cases passed (120 ms)
  • Your runtime beats 23 % of javascript submissions
  • Your memory usage beats 34.23 % of javascript submissions (35.7 MB)
  • 時間複雜度 O(n)

Ⅱ.二分法

代碼:

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    if (x === 0) return 0
    let l = 1
    let r = x >>> 1
    while(l < r) {
        // 這裏要用大於判斷,所以取右中位數
        const mid = (l + r + 1) >>> 1

        if (mid * mid > x) {
            r = mid - 1
        } else {
            l = mid
        }
    }
    return l
};

結果:

  • 1017/1017 cases passed (76 ms)
  • Your runtime beats 96.08 % of javascript submissions
  • Your memory usage beats 59.17 % of javascript submissions (35.5 MB)
  • 時間複雜度 O(log2(n))

查閱他人解法

這裏看見了兩個有意思的解法:

  • 2的冪次底層優化
  • 牛頓法

Ⅰ.冪次優化

稍微解釋一下,二分法需要做乘法運算,他這裏改用加減法

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    let l = 0
    let r = 1 << 16 // 2的16次方,這裏我猜是因為上限2^32所以取一半
    while (l < r - 1) {
        const mid = (l + r) >>> 1
        if (mid * mid <= x) {
            l = mid
        } else {
            r = mid
        }
    }
    return l
};

結果:

1017/1017 cases passed (72 ms)
Your runtime beats 98.46 % of javascript submissions
Your memory usage beats 70.66 % of javascript submissions (35.4 MB)

  • 時間複雜度 O(log2(n))

Ⅱ.牛頓法

算法說明:

在迭代過程中,以直線代替曲線,用一階泰勒展式(即在當前點的切線)代替原曲線,求直線與 xx 軸的交點,重複這個過程直到收斂。

首先隨便猜一個近似值 x,然後不斷令 x 等於 xa/x 的平均數,迭代個六七次后 x 的值就已經相當精確了。

公式可以寫為 X[n+1]=(X[n]+a/X[n])/2

代碼:

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    if (x === 0 || x === 1) return x

    let a = x >>> 1
    while(true) {
        let cur = a
        a = (a + x / a) / 2
        // 這裡是為了消除浮點運算的誤差,1e-5是我試出來的
        if (Math.abs(a - cur) < 1e-5) {
            return parseInt(cur)
        }
    }
};

結果:

  • 1017/1017 cases passed (68 ms)
  • Your runtime beats 99.23 % of javascript submissions
  • Your memory usage beats 9.05 % of javascript submissions (36.1 MB)
  • 時間複雜度 O(log2(n))

思考總結

這裏就提一下新接觸的牛頓法吧,實際上是牛頓迭代法,主要是迭代操作。由於在單根附近具有平方收斂,所以可以轉換成線性問題去求平方根的近似值。主要應用場景有這兩個方向:

  • 求方程的根
  • 求解最優化問題

70.爬樓梯

題目描述

假設你正在爬樓梯。需要 n 階你才能到達樓頂。

每次你可以爬 12 個台階。你有多少種不同的方法可以爬到樓頂呢?

注意:給定 n 是一個正整數。

示例:

輸入: 2
輸出: 2
解釋: 有兩種方法可以爬到樓頂。
1.  1 階 + 1 階
2.  2 階

輸入: 3
輸出: 3
解釋: 有三種方法可以爬到樓頂。
1.  1 階 + 1 階 + 1 階
2.  1 階 + 2 階
3.  2 階 + 1 階

題目分析設想

這道題很明顯可以用動態規劃和斐波那契數列來求解。然後我們來看看其他正常思路,如果使用暴力法的話,那麼複雜度將會是 2^n,很容易溢出,但是如果能夠優化成 n 的話,其實還可以求解的。所以這道題我就從以下三個方向來作答:

  • 哈希遞歸,也就是暴力運算的改進版,通過存下算過的值降低複雜度
  • 動態規劃
  • 斐波那契數列

編寫代碼驗證

Ⅰ.哈希遞歸

代碼:

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    let hash = {}
    return count(0)
    function count (i) {
        if (i > n) return 0
        if (i === n) return 1

        // 這步節省運算
        if(hash[i] > 0) {
            return hash[i]
        }

        hash[i] = count(i + 1) + count(i + 2)
        return hash[i]
    }
};

結果:

  • 45/45 cases passed (52 ms)
  • Your runtime beats 98.67 % of javascript submissions
  • Your memory usage beats 48.29 % of javascript submissions (33.7 MB)
  • 時間複雜度 O(n)

Ⅱ.動態規劃

代碼:

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    if (n === 1) return 1
    if (n === 2) return 2
    // dp[0] 多一位空間,省的後面做減法
    let dp = new Array(n + 1).fill(0)
    dp[1] = 1
    dp[2] = 2
    for(let i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2]
    }
    return dp[n]
};

結果:

  • 45/45 cases passed (48 ms)
  • Your runtime beats 99.48 % of javascript submissions
  • Your memory usage beats 21.49 % of javascript submissions (33.8 MB)
  • 時間複雜度 O(n)

Ⅲ.斐波那契數列

其實斐波那契數列就可以用動態規劃來實現,所以下面的代碼思路很相似。

代碼:

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    if (n === 1) return 1
    if (n === 2) return 2
    let num1 = 1
    let num2 = 2
    for(let i = 3; i <= n; i++) {
        let count = num1 + num2
        num1 = num2
        num2 = count
    }
    // 相當於fib(n)
    return num2
};

結果:

  • 45/45 cases passed (56 ms)
  • Your runtime beats 95.49 % of javascript submissions
  • Your memory usage beats 46.1 % of javascript submissions (33.7 MB)
  • 時間複雜度 O(n)

查閱他人解法

查看題解發現這麼幾種解法:

  • 斐波那契公式(原來有計算公式可以直接用,尷尬)
  • Binets 方法
  • 排列組合

Ⅰ.斐波那契公式

代碼:

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    const sqrt_5 = Math.sqrt(5)
    // 由於 F0 = 1,所以相當於需要求 n+1 的值
    const fib_n = Math.pow((1 + sqrt_5) / 2, n + 1) - Math.pow((1 - sqrt_5) / 2, n + 1)
    return Math.round(fib_n / sqrt_5)
};

結果:

  • 45/45 cases passed (52 ms)
  • Your runtime beats 98.67 % of javascript submissions
  • Your memory usage beats 54.98 % of javascript submissions (33.6 MB)
  • 時間複雜度 O(log(n))

Ⅱ.Binets 方法

算法說明:

使用矩陣乘法來得到第 n 個斐波那契數。注意需要將初始項從 fib(2)=2,fib(1)=1 改成 fib(2)=1,fib(1)=0 ,來達到矩陣等式的左右相等。

代碼:

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {

    function pow(a, n) {
        let ret = [[1,0],[0,1]] // 矩陣
        while(n > 0) {
            if ((n & 1) === 1) {
                ret = multiply(ret, a)
            }
            n >> 1
            a = multiply(a, a)
        }
        return ret;
    }
    function multiply(a, b) {
        let c = [[0,0], [0,0]]
        for (let i = 0; i < 2; i++) {
            for(let j = 0; j < 2; j++) {
                c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j]
            }
        }
        return c
    }

    let q = [[1,1], [1, 0]]
    let res = pow(q, n)
    return res[0][0]
};

結果:

測試用例可以輸出,提交發現超時。

這個筆者還沒完全理解,所以很抱歉,暫時沒有 js 相應代碼分析,後續會補上。也歡迎您補充給我,感謝!

Ⅲ.排列組合

代碼:

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    // n 個台階走 i 次1階和 j 次2階走到,推導出 i + 2*j = n
    function combine(m, n) {
        if (m < n) [m, n] = [n, m];
        let count = 1;
        for (let i = m + n, j = 1; i > m; i--) {
            count *= i;
            if (j <= n) count /= j++;
        }
        return count;
    }
    let total = 0;
    // 取出所有滿足條件的解
    for (let i = 0,j = n; j >= 0; j -= 2, i++) {
      total += combine(i, j);
    }
    return total;
};

結果:

  • 45/45 cases passed (60 ms)
  • Your runtime beats 87.94 % of javascript submissions
  • Your memory usage beats 20.72 % of javascript submissions (33.8 MB)
  • 時間複雜度 O(n^2)

思考總結

這種疊加的問題,首先就會想到動態規劃的解法,剛好這裏又滿足斐波那契數列,所以我是推薦首選這兩種解法。另外通過查看他人解法學到了斐波那契公式,以及站在排列組合的角度去解,開拓了思路。

83.刪除排序鏈表中的重複元素

題目描述

給定一個排序鏈表,刪除所有重複的元素,使得每個元素只出現一次。

示例:

輸入: 1->1->2
輸出: 1->2

輸入: 1->1->2->3->3
輸出: 1->2->3

題目分析設想

注意一下,給定的是一個排序鏈表,所以只需要依次更改指針就可以直接得出結果。當然,也可以使用雙指針來跳過重複項即可。所以這裡有兩個方向:

  • 直接運算,通過改變指針指向
  • 雙指針,通過跳過重複項

如果是無序鏈表,我會建議先得到所有值然後去重后(比如通過Set)生成新鏈表作答。

編寫代碼驗證

Ⅰ.直接運算

代碼:

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    // 複製一個用做操作,由於對象是傳址,所以改指針指向即可
    let cur = head
    while(cur !== null && cur.next !== null) {
        if (cur.val === cur.next.val) { // 值相等
            cur.next = cur.next.next
        } else {
            cur = cur.next
        }
    }
    return head
};

結果:

  • 165/165 cases passed (76 ms)
  • Your runtime beats 87.47 % of javascript submissions
  • Your memory usage beats 81.21 % of javascript submissions (35.5 MB)
  • 時間複雜度 O(n)

Ⅱ.雙指針法

代碼:

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    // 新建哨兵指針和當前遍歷指針
    if (head === null || head.next === null) return head
    let pre = head
    let cur = head
    while(cur !== null) {
        debugger
        if (cur.val === pre.val) {
            // 當前指針移動
            cur = cur.next
        } else {
            pre.next = cur
            pre = cur
        }
    }
    // 最後一項如果重複需要把head.next指向null
    pre.next = null
    return head
};

結果:

  • 165/165 cases passed (80 ms)
  • Your runtime beats 77.31 % of javascript submissions
  • Your memory usage beats 65.1 % of javascript submissions (35.7 MB)
  • 時間複雜度 O(n)

查閱他人解法

忘記了,這裏確實還可以使用遞歸來作答。

Ⅰ.遞歸法

代碼:

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    if(head === null || head.next === null) return head
    if (head.val === head.next.val) { // 值相等
        return deleteDuplicates(head.next)
    } else {
        head.next = deleteDuplicates(head.next)
    }
    return head
};

結果:

  • 165/165 cases passed (80 ms)
  • Your runtime beats 77.31 % of javascript submissions
  • Your memory usage beats 81.21 % of javascript submissions (35.5 MB)
  • 時間複雜度 O(n)

思考總結

關於鏈表的題目一般都是通過修改指針指向來作答,區分單指針和雙指針法。另外,遍歷也是可以實現的。

88.合併兩個有序數組

題目描述

給定兩個有序整數數組 nums1nums2,將 nums2 合併到 nums1 中,使得 num1 成為一個有序數組。

說明:

  • 初始化 nums1nums2 的元素數量分別為 mn
  • 你可以假設 nums1 有足夠的空間(空間大小大於或等於 m + n)來保存 nums2 中的元素。

示例:

輸入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6],       n = 3

輸出: [1,2,2,3,5,6]

題目分析設想

之前我們做過刪除排序數組中的重複項,其實這裏也類似。可以從這幾個方向作答:

  • 數組合併後排序
  • 遍曆數組並進行插入
  • 雙指針法,輪流比較

但是由於題目有限定空間都在 nums1 ,並且不要寫 return ,直接在 nums1 上修改,所以我這裏主要的思路就是遍歷,通過 splice 來修改數組。區別就在於遍歷的方式方法。

  • 從前往後
  • 從后往前
  • 合併後排序再賦值

編寫代碼驗證

Ⅰ.從前往後

代碼:

/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */
var merge = function(nums1, m, nums2, n) {
    // 兩個數組對應指針
    let p1 = 0
    let p2 = 0
    // 這裏需要提前把nums1的元素拷貝出來,要不然比較賦值后就丟失了
    let cpArr = nums1.splice(0, m)

    // 數組指針
    let p = 0
    while(p1 < m && p2 < n) {
        // 先賦值,再進行+1操作
        nums1[p++] = cpArr[p1] < nums2[p2] ? cpArr[p1++] : nums2[p2++]
    }
    // 已經有p個元素了,多餘的元素要刪除,剩餘的要加上
    if (p1 < m) {
        // 剩餘元素,p1 + m + n - p = m + n - (p - p1) = m + n - p2
        nums1.splice(p, m + n - p, ...cpArr.slice(p1, m + n - p2))
    }
    if (p2 < n) {
        // 剩餘元素,p2 + m + n - p = m + n - (p - p2) = m + n - p1
        nums1.splice(p, m + n - p, ...nums2.slice(p2, m + n - p1))
    }
};

結果:

  • 59/59 cases passed (48 ms)
  • Your runtime beats 100 % of javascript submissions
  • Your memory usage beats 64.97 % of javascript submissions (33.8 MB)
  • 時間複雜度 O(m + n)

Ⅱ.從后往前

代碼:

/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */
var merge = function(nums1, m, nums2, n) {
    // 避免 nums1 = [0,0,0,0], nums2 = [1,2] 這種 nums1.length > nums2.length 並且 m = 0
    nums1.splice(m, nums1.length - m)
    // 兩個數組對應指針
    let p1 = m - 1
    let p2 = n - 1
    // 數組指針
    let p = m + n - 1
    while(p1 >= 0 && p2 >= 0) {
        // 先賦值,再進行-1操作
        nums1[p--] = nums1[p1] < nums2[p2] ? nums2[p2--] : nums1[p1--]
    }
    // 可能nums2有剩餘,由於指針是下標,所以截取數量需要加1
    nums1.splice(0, p2 + 1, ...nums2.slice(0, p2 + 1))
};

結果:

  • 59/59 cases passed (52 ms)
  • Your runtime beats 99.76 % of javascript submissions
  • Your memory usage beats 78.3 % of javascript submissions (33.6 MB)
  • 時間複雜度 O(m + n)

Ⅲ.合併後排序再賦值

代碼:

/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */
var merge = function(nums1, m, nums2, n) {
    arr = [].concat(nums1.splice(0, m), nums2.splice(0, n))
    arr.sort((a, b) => a - b)
    for(let i = 0; i < arr.length; i++) {
        nums1[i] = arr[i]
    }
};

結果:

  • 59/59 cases passed (64 ms)
  • Your runtime beats 90.11 % of javascript submissions
  • Your memory usage beats 31.21 % of javascript submissions (34.8 MB)
  • 時間複雜度 O(m + n)

查閱他人解法

這裏看到一個直接用兩次 while ,然後直接用 m/n 來計算下標的,沒有額外空間,但是本質上也是從后往前遍歷。

Ⅰ.兩次while

代碼:

/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */
var merge = function(nums1, m, nums2, n) {
    // 避免 nums1 = [0,0,0,0], nums2 = [1,2] 這種 nums1.length > nums2.length 並且 m = 0
    // nums1.splice(m, nums1.length - m)
    // 從后開始賦值
    while(m !== 0 && n !== 0) {
        nums1[m + n - 1] = nums1[m - 1] > nums2[n - 1] ? nums1[--m] : nums2[--n]
    }
    // nums2 有剩餘
    while(n !== 0) {
        nums1[m + n - 1] = nums2[--n]
    }
};

結果:

  • 59/59 cases passed (56 ms)
  • Your runtime beats 99.16 % of javascript submissions
  • Your memory usage beats 64.26 % of javascript submissions (33.8 MB)
  • 時間複雜度 O(m + n)

思考總結

碰到數組操作,會優先考慮雙指針法,具體指針方向可以由題目邏輯來決定。

(完)

本文為原創文章,可能會更新知識點及修正錯誤,因此轉載請保留原出處,方便溯源,避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗
如果能給您帶去些許幫助,歡迎 ⭐️star 或 ️ fork
(轉載請註明出處:https://chenjiahao.xyz)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

分類
發燒車訊

最強新能源汽車 奧迪發出核動力超跑

為什麼奧迪MesarthimF-Tron Quattro是最強新能源汽車?因為它的複雜程度,已經超出了你的物理知識認知。如果對新能源汽車大概分個等級,那麼“二次電池”驅動的純電動車是一級,插電式混合動力是二級,太陽能汽車三級,燃料電池汽車四級,那麼最強的是?當然是核動力汽車。  
  據稱,奧迪未來的超級跑車即是一款核動力汽車,代號Mesarthim F-Tron Quattro,由俄羅斯工程師操刀設計。外型前衛十足,頗有蝙蝠車的味道。   核動力主要部件核反應爐與離子發射器位於前後軸之間,旁邊是發熱裝置,產生的蒸汽進而驅動電機發電,動力電池安裝在前艙,使用四個輪轂電機驅動車輪前進,同時還有電機驅動離子發射器、冷凝器等,除了核燃料供給,其它整個系統構成一個閉環生態。  
  由於動力系統是個獨立部分,導致其它部件的安裝位置是個問題,為解決這個問題,工程師設計了一套叫做“Solid Cage”的獨立底盤,這個底盤是聚合物材料,可以3D列印出來,而且可以獨立拆卸。   除此以外,底盤上還有一個平底罐,裡面裝有磁流體,在有磁性的路面上行駛時,車子就會產生下壓力,過彎的時候能抵制側傾力,直行的時候抑制抬頭點頭。   雖然奧迪暫時沒有給出這款車具體推出時間表,總體來說還是值得稱讚的。   文章來源:蓋世汽車

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

分類
發燒車訊

國務院:外商可獨資製造新能源汽車動力電池等領域

7月19日,國務院下發第2016年第41號文件,關於在自由貿易試驗區暫時調整有關行政法規、國務院文件和經國務院批准的部門規章規定的決定。  
  相關自由貿易試驗區涉及上海市、天津市、廣東省、福建省四區域。國務院決定,在自由貿易試驗區暫時調整《中華人民共和國外資企業法實施細則》等18部行政法規、《國務院關於投資體制改革的決定》等4件國務院檔、《外商投資產業指導目錄(2015年修訂)》等4件經國務院批准的部門規章的有關規定。   據分析,此次重大調整專案涉及51項。其中放開合資門檻,允許外商以獨資形式從事生產經營活動的項目多達12項。在新能源汽車關鍵零部件及整車領域,涉及3項,具體如下:   1、允許外商以獨資形式從事能量型動力電池(能量密度≥110Wh,迴圈壽命≥2000次)的製造; 2、允許外商以獨資形式從事汽車電子匯流排網路技術、電動助力轉向系統電子控制器的製造與研發; 3、允許外商以獨資形式從事摩托車生產;   由此可見,在新能源汽車重要零部件動力電池領域,國外一線大廠終於擺脫了合資電池廠的固定模式,三星SDI、松下、LG化學等電池大頭將獲益。   文章來源:上海蓋世

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

分類
發燒車訊

CSS(8)—通俗講解定位(position)

CSS(8)—通俗講解定位(position)

CSS有三種基本的定位機制: 普通流浮動定位。前面兩個之前已經講過,詳見博客:

1、

2、

3、

一、為什麼要用定位?

如果說浮動關鍵在一個 “浮” 字上面, 那麼 我們的定位,關鍵在於一個 “位” 上。

我們來思考下定位用於的場景。

1、打Log標籤

比如你想在商品的圖片想打個標籤比如:包郵、最新上架等等。

怎麼做比較好呢,如果你要粗暴那就直接ps在圖片上添加標籤。只是這樣有個很大的弊端,比如你要添加新標籤你需要重現修圖,比如商品之前包郵後面不包郵了,

那你又需要重新p圖。這樣肯定是不合適的。那怎麼做比較合適?

其實很簡單,將商品圖片和標籤的標籤分開來。然後通過css在商品圖片上添加標籤。這個時候通常會定位去完成。

2、切換Banner

有些商城的首頁都會有個Banner,這裏 左右的箭頭下面的小點點一般也是用定位來做。

3、廣告位窗口

有些位置在左右側會有固定的廣告窗口,不論怎麼滑動頁面這個廣告窗口都是在固定位置

這個就需要用到固定定位了。

二、定位概念

1、定位的分類

在CSS中,position 屬性用於定義元素的定位模式,其基本語法格式如下:

選擇器 {position:屬性值;}

屬性值

這裏還有個概念就是 邊偏移 因為你定位肯定要指定定位在哪裡,所以需要通過 邊偏移 來指定。

所以定位是要和邊偏移搭配使用的。不過對於static(靜態定位)設置邊偏移是無用的。

2、靜態定位(static)

static 是此屬性的默認值。這時候,我們稱那個元素沒有被定位。簡單來說就是網頁中所有元素都默認的是靜態定位。 其實就是標準流的特性。

所以如果需要使用定位那這裏就不能是這個默認值了。

注意 在靜態定位狀態下,此時 top, right, bottom, left 和 z-index 屬性無效。

3、相對定位(relative)

它的主要特點如下

1、 參照元素原來的位置進行移動。
2、 通過"left"、 "top"、 "right"、 "bottom" 屬性進行定位。
3、 元素原有的空間位保留。
4、 元素在移動時會蓋住其他元素。

舉例說明

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>相對定位</title>
        <style type="text/css">
            #one {
                width: 120px;
                height: 120px;
                background: #E19D59;
            }
            #two {
                width: 120px;
                height: 120px;
                background: #FF0000;
                position: relative;   /*設置相對定位*/
                left: 20px;           /*設置距離左邊偏移位置*/
                top: 20px;            /*設置距離頂部偏移位置*/
            }
            #three {
                width: 120px;
                height: 120px;
                background: #008000;
            }
        </style>
    </head>
    <body>      
        <div id="one">div1</div>
        <div id="two">div2</div>
        <div id="three">div3</div>        
    </body>
</html>

運行結果

通過我們這個示例我們可以看出

1、它的左右,上下邊偏移的量是根據這個div2原始位置基礎上進行移動的。
2、這個div2它還是個標準流,並沒有浮起來,所以這個位置它還是佔有的。(如果div2浮動那麼div3就會向上移動,這裏顯然沒有)
3、當它偏移后 如果和其它元素有重疊,它會覆蓋其它元素。(div2覆蓋了部分div3元素)

作用 我的理解相對定位主要用途是用來給絕對定位的一個盒子。(下面會解釋這句話)

4、絕對定位absolute

特點

1、參照距離他最近的有定位屬性的父級元素進行移動
2、通過"left"、 "top"、 "right"、 "bottom" 屬性進行定位
3、元素完全脫離文檔流,原有位置不再保留
4、元素在移動時會蓋住其他元素
5、一般我們設置絕對定位時,都會找一個合適的父級將其設置為相對定位。最好為這個具有相對定位屬性的父級設置寬高

舉例說明

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
        <style type="text/css">
           #father{
                width: 400px;
                height: 400px;
                margin: 100px;
               /*  position: relative;*/
                background: yellow;
            }
            #bd1{
                width: 120px;
                height: 120px;
                background: #E19D59;
            }
            #bd2{
                width: 120px;
                height: 120px;
                background: #FF0000;
                position: absolute;
                left: 20px;
                top: 20px;
            }
            #bd3{
                width: 120px;
                height: 120px;
                background: #008000;
            }
        </style>
    </head>
    <body>  
    <div id="father"> 
        <div id="bd1">div1</div>
        <div id="bd2">div2</div>
        <div id="bd3">div3</div>  
    </div>         
    </body>
</html>

運行結果

從這幅圖可以看出一點

這裏因為父div沒有設置定位,所以它的位置是相對於body進行邊偏移。

這個時候我們將父標籤設置 position: relative;

再刷新頁面

從這張圖很直觀看到:

1、因為父div設置了定位,所以這裏的邊偏移變成都是相對於父div進行偏移(正常貼標籤就是這樣)
2、我們可以看出當設置絕對定位后,該元素已經脫離文檔流,已經浮上來了(因為div2上浮所有div3才會上移)
3、元素在移動時會蓋住其他元素 (div2覆蓋了部分div3)

5、固定定位(fixed)

特點

1、以body為定位時的對象,總是根據瀏覽器的窗口來進行元素的定位
2、通過"left"、 "top"、 "right"、 "bottom" 屬性進行定位
3、元素完全脫離文檔流,原有位置不再保留
4、元素不會隨着文檔流的滑動而滑動

固定定位最大的特點就是第一點,可以理解成它是以可視區域為準,會一直显示在可視區域,屏幕滑動也會显示在定位的位置。

6、四種定位總結

還有比較重要的三點

定位模式轉換

跟 浮動一樣, 元素添加了 絕對定位和固定定位之後, 元素模式也會發生轉換, 都自動轉換為 行內塊元素。

絕對定位的盒子水平/垂直居中

注意 普通的盒子是左右margin 改為 auto就可, 但是對於絕對定位就無效了。

定位的盒子也可以水平或者垂直居中,有一個算法(下面會舉例說明)。

1. 首先left 50%   父盒子的一半大小
2. 然後走自己外邊距負的一半值就可以了 margin-left。

子絕父相

這句話的意思是 子級是絕對定位的話,父級要用相對定位

為什麼會有這個概念,那是因為絕對定位的邊偏移特點是

 如果父元素沒有設置定位,那麼它的位置是相對於body進行邊偏移。如果父元素設置定位,那就根據父元素偏移。

一般我們肯定是希望根據父元素偏移。就好比圖片打標籤,不可能跟着body偏移而是父元素進行定位。而且父元素相對定位最大的好處就是它會佔有位置,因此父親最好是 相對定位。

三、經典示例

1、打上log標記

示例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>log標籤</title>
    <style>
    div {
        width: 310px;
        height: 190px;
        border: 1px solid #ccc;
        margin: 100px auto; 
        position: relative;  /*父選擇相對定位*/
    }
    .top {
        position: absolute; /*子取相對定位*/
        top: 0;             /*位置 左上*/
        left: 0;
    }
    
    </style>
</head>
<body>
    <div>
        <img src="images/log.jpg" alt="" class="top">     <!-- log的圖片 -->
        <img src="images/goods.jpg" height="190" width="310" alt=""> <!-- 商品圖片,長和寬和父div大小一致 -->
    </div>
</body>
</html>

運行結果就是上面的最終結果。

2、定位水平居中

加了定位 浮動的的盒子 margin 0 auto 失效了

代碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>水平居中</title>
    <style>
    div {
        width: 200px;
        height: 200px;
        background-color: pink;
        position: absolute;
        /*加了定位 浮動的的盒子  margin 0 auto 失效了*/
        left: 50%;
        margin-left: -100px;  /*減去總寬度一般*/
        top: 50%;
        margin-top: -100px;   /*減去總高度一般*/
    }
    </style>
</head>
<body>
    <div></div>
</body>
</html>

這個這個div就處於整個頁面的居中了,這裏我們來說明下下面這兩個的意思

        left: 50%;
        margin-left: -100px;  /*減去總寬度一般*/

3、輪播圖

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>輪播圖</title>
    <style>
    * {
        margin: 0;
        padding: 0;
    }
    li {
        list-style: none;
    }
    .tb {
        width: 520px;
        height: 280px;
        background-color: pink;
        margin: 100px auto;
        position: relative;
    }
    .tb a {
        width: 24px;
        height: 36px;
        
        display: block;
        position: absolute;
        top: 50%;
        margin-top: -18px;
    }
    .left {
        left: 0;
        background: url(images/left.png) no-repeat;
    }
    .right {
        right: 0;
        background: url(images/right.png) no-repeat;
    }
    .tb ul {
        width: 70px;
        height: 13px;
        background: rgba(255, 255, 255, .3);
        position: absolute; /* 加定位*/
        bottom: 18px;
        left: 50%; /*水平走父容器的一半*/
        margin-left: -35px; /*左走自己的一半*/
        border-radius: 8px;
    }
    .tb ul li {
        width: 8px;
        height: 8px;
        background-color: #fff;
        float: left;
        margin: 3px;
        border-radius: 50%;
    }
    .tb .current {
        background-color: #f40;
    }
    </style>
</head>
<body>
    <div class="tb">
        <img src="images/tb.jpg" >
        <a href="#" class="left"></a>
        <a href="#" class="right"></a>
        <ul>
            <li class="current"></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
        </ul>

    </div>
</body>
</html>

運行結果

參考

1、

2、

你如果願意有所作為,就必須有始有終。(10)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

分類
發燒車訊

深入理解Kafka必知必會(2)

Kafka目前有哪些內部topic,它們都有什麼特徵?各自的作用又是什麼?

__consumer_offsets:作用是保存 Kafka 消費者的位移信息
__transaction_state:用來存儲事務日誌消息

優先副本是什麼?它有什麼特殊的作用?

所謂的優先副本是指在AR集合列表中的第一個副本。
理想情況下,優先副本就是該分區的leader 副本,所以也可以稱之為 preferred leader。Kafka 要確保所有主題的優先副本在 Kafka 集群中均勻分佈,這樣就保證了所有分區的 leader 均衡分佈。以此來促進集群的負載均衡,這一行為也可以稱為“分區平衡”。

Kafka有哪幾處地方有分區分配的概念?簡述大致的過程及原理

  1. 生產者的分區分配是指為每條消息指定其所要發往的分區。可以編寫一個具體的類實現org.apache.kafka.clients.producer.Partitioner接口。
  2. 消費者中的分區分配是指為消費者指定其可以消費消息的分區。Kafka 提供了消費者客戶端參數 partition.assignment.strategy 來設置消費者與訂閱主題之間的分區分配策略。
  3. 分區副本的分配是指為集群制定創建主題時的分區副本分配方案,即在哪個 broker 中創建哪些分區的副本。kafka-topics.sh 腳本中提供了一個 replica-assignment 參數來手動指定分區副本的分配方案。

簡述Kafka的日誌目錄結構

Kafka 中的消息是以主題為基本單位進行歸類的,各個主題在邏輯上相互獨立。每個主題又可以分為一個或多個分區。不考慮多副本的情況,一個分區對應一個日誌(Log)。為了防止 Log 過大,Kafka 又引入了日誌分段(LogSegment)的概念,將 Log 切分為多個 LogSegment,相當於一個巨型文件被平均分配為多個相對較小的文件。

Log 和 LogSegment 也不是純粹物理意義上的概念,Log 在物理上只以文件夾的形式存儲,而每個 LogSegment 對應於磁盤上的一個日誌文件和兩個索引文件,以及可能的其他文件(比如以“.txnindex”為後綴的事務索引文件)

Kafka中有那些索引文件?

每個日誌分段文件對應了兩個索引文件,主要用來提高查找消息的效率。
偏移量索引文件用來建立消息偏移量(offset)到物理地址之間的映射關係,方便快速定位消息所在的物理文件位置
時間戳索引文件則根據指定的時間戳(timestamp)來查找對應的偏移量信息。

如果我指定了一個offset,Kafka怎麼查找到對應的消息?

Kafka是通過seek() 方法來指定消費的,在執行seek() 方法之前要去執行一次poll()方法,等到分配到分區之後會去對應的分區的指定位置開始消費,如果指定的位置發生了越界,那麼會根據auto.offset.reset 參數設置的情況進行消費。

如果我指定了一個timestamp,Kafka怎麼查找到對應的消息?

Kafka提供了一個 offsetsForTimes() 方法,通過 timestamp 來查詢與此對應的分區位置。offsetsForTimes() 方法的參數 timestampsToSearch 是一個 Map 類型,key 為待查詢的分區,而 value 為待查詢的時間戳,該方法會返回時間戳大於等於待查詢時間的第一條消息對應的位置和時間戳,對應於 OffsetAndTimestamp 中的 offset 和 timestamp 字段。

聊一聊你對Kafka的Log Retention的理解

日誌刪除(Log Retention):按照一定的保留策略直接刪除不符合條件的日誌分段。
我們可以通過 broker 端參數 log.cleanup.policy 來設置日誌清理策略,此參數的默認值為“delete”,即採用日誌刪除的清理策略。

  1. 基於時間
    日誌刪除任務會檢查當前日誌文件中是否有保留時間超過設定的閾值(retentionMs)來尋找可刪除的日誌分段文件集合(deletableSegments)retentionMs 可以通過 broker 端參數 log.retention.hours、log.retention.minutes 和 log.retention.ms 來配置,其中 log.retention.ms 的優先級最高,log.retention.minutes 次之,log.retention.hours 最低。默認情況下只配置了 log.retention.hours 參數,其值為168,故默認情況下日誌分段文件的保留時間為7天。
    刪除日誌分段時,首先會從 Log 對象中所維護日誌分段的跳躍表中移除待刪除的日誌分段,以保證沒有線程對這些日誌分段進行讀取操作。然後將日誌分段所對應的所有文件添加上“.deleted”的後綴(當然也包括對應的索引文件)。最後交由一個以“delete-file”命名的延遲任務來刪除這些以“.deleted”為後綴的文件,這個任務的延遲執行時間可以通過 file.delete.delay.ms 參數來調配,此參數的默認值為60000,即1分鐘。

  2. 基於日誌大小
    日誌刪除任務會檢查當前日誌的大小是否超過設定的閾值(retentionSize)來尋找可刪除的日誌分段的文件集合(deletableSegments)。
    retentionSize 可以通過 broker 端參數 log.retention.bytes 來配置,默認值為-1,表示無窮大。注意 log.retention.bytes 配置的是 Log 中所有日誌文件的總大小,而不是單個日誌分段(確切地說應該為 .log 日誌文件)的大小。單個日誌分段的大小由 broker 端參數 log.segment.bytes 來限制,默認值為1073741824,即 1GB。
    這個刪除操作和基於時間的保留策略的刪除操作相同。
  3. 基於日誌起始偏移量
    基於日誌起始偏移量的保留策略的判斷依據是某日誌分段的下一個日誌分段的起始偏移量 baseOffset 是否小於等於 logStartOffset,若是,則可以刪除此日誌分段。

如上圖所示,假設 logStartOffset 等於25,日誌分段1的起始偏移量為0,日誌分段2的起始偏移量為11,日誌分段3的起始偏移量為23,通過如下動作收集可刪除的日誌分段的文件集合 deletableSegments:

從頭開始遍歷每個日誌分段,日誌分段1的下一個日誌分段的起始偏移量為11,小於 logStartOffset 的大小,將日誌分段1加入 deletableSegments。
日誌分段2的下一個日誌偏移量的起始偏移量為23,也小於 logStartOffset 的大小,將日誌分段2加入 deletableSegments。
日誌分段3的下一個日誌偏移量在 logStartOffset 的右側,故從日誌分段3開始的所有日誌分段都不會加入 deletableSegments。
收集完可刪除的日誌分段的文件集合之後的刪除操作同基於日誌大小的保留策略和基於時間的保留策略相同

聊一聊你對Kafka的Log Compaction的理解

日誌壓縮(Log Compaction):針對每個消息的 key 進行整合,對於有相同 key 的不同 value 值,只保留最後一個版本。
如果要採用日誌壓縮的清理策略,就需要將 log.cleanup.policy 設置為“compact”,並且還需要將 log.cleaner.enable (默認值為 true)設定為 true。

如下圖所示,Log Compaction 對於有相同 key 的不同 value 值,只保留最後一個版本。如果應用只關心 key 對應的最新 value 值,則可以開啟 Kafka 的日誌清理功能,Kafka 會定期將相同 key 的消息進行合併,只保留最新的 value 值。

聊一聊你對Kafka底層存儲的理解

頁緩存

頁緩存是操作系統實現的一種主要的磁盤緩存,以此用來減少對磁盤 I/O 的操作。具體來說,就是把磁盤中的數據緩存到內存中,把對磁盤的訪問變為對內存的訪問。

當一個進程準備讀取磁盤上的文件內容時,操作系統會先查看待讀取的數據所在的頁(page)是否在頁緩存(pagecache)中,如果存在(命中)則直接返回數據,從而避免了對物理磁盤的 I/O 操作;如果沒有命中,則操作系統會向磁盤發起讀取請求並將讀取的數據頁存入頁緩存,之後再將數據返回給進程。

同樣,如果一個進程需要將數據寫入磁盤,那麼操作系統也會檢測數據對應的頁是否在頁緩存中,如果不存在,則會先在頁緩存中添加相應的頁,最後將數據寫入對應的頁。被修改過後的頁也就變成了臟頁,操作系統會在合適的時間把臟頁中的數據寫入磁盤,以保持數據的一致性。

用過 Java 的人一般都知道兩點事實:對象的內存開銷非常大,通常會是真實數據大小的幾倍甚至更多,空間使用率低下;Java 的垃圾回收會隨着堆內數據的增多而變得越來越慢。基於這些因素,使用文件系統並依賴於頁緩存的做法明顯要優於維護一個進程內緩存或其他結構,至少我們可以省去了一份進程內部的緩存消耗,同時還可以通過結構緊湊的字節碼來替代使用對象的方式以節省更多的空間。

此外,即使 Kafka 服務重啟,頁緩存還是會保持有效,然而進程內的緩存卻需要重建。這樣也極大地簡化了代碼邏輯,因為維護頁緩存和文件之間的一致性交由操作系統來負責,這樣會比進程內維護更加安全有效。

零拷貝

除了消息順序追加、頁緩存等技術,Kafka 還使用零拷貝(Zero-Copy)技術來進一步提升性能。所謂的零拷貝是指將數據直接從磁盤文件複製到網卡設備中,而不需要經由應用程序之手。零拷貝大大提高了應用程序的性能,減少了內核和用戶模式之間的上下文切換。對 Linux 操作系統而言,零拷貝技術依賴於底層的 sendfile() 方法實現。對應於 Java 語言,FileChannal.transferTo() 方法的底層實現就是 sendfile() 方法。

聊一聊Kafka的延時操作的原理

Kafka 中有多種延時操作,比如延時生產,還有延時拉取(DelayedFetch)、延時數據刪除(DelayedDeleteRecords)等。
延時操作創建之後會被加入延時操作管理器(DelayedOperationPurgatory)來做專門的處理。延時操作有可能會超時,每個延時操作管理器都會配備一個定時器(SystemTimer)來做超時管理,定時器的底層就是採用時間輪(TimingWheel)實現的。

聊一聊Kafka控制器的作用

在 Kafka 集群中會有一個或多個 broker,其中有一個 broker 會被選舉為控制器(Kafka Controller),它負責管理整個集群中所有分區和副本的狀態。當某個分區的 leader 副本出現故障時,由控制器負責為該分區選舉新的 leader 副本。當檢測到某個分區的 ISR 集合發生變化時,由控制器負責通知所有broker更新其元數據信息。當使用 kafka-topics.sh 腳本為某個 topic 增加分區數量時,同樣還是由控制器負責分區的重新分配。

Kafka的舊版Scala的消費者客戶端的設計有什麼缺陷?

如上圖,舊版消費者客戶端每個消費組( )在 ZooKeeper 中都維護了一個 /consumers/ /ids 路徑,在此路徑下使用臨時節點記錄隸屬於此消費組的消費者的唯一標識(consumerIdString),/consumers/ /owner 路徑下記錄了分區和消費者的對應關係,/consumers/ /offsets 路徑下記錄了此消費組在分區中對應的消費位移。

每個消費者在啟動時都會在 /consumers/ /ids 和 /brokers/ids 路徑上註冊一個監聽器。當 /consumers/ /ids 路徑下的子節點發生變化時,表示消費組中的消費者發生了變化;當 /brokers/ids 路徑下的子節點發生變化時,表示 broker 出現了增減。這樣通過 ZooKeeper 所提供的 Watcher,每個消費者就可以監聽消費組和 Kafka 集群的狀態了。

這種方式下每個消費者對 ZooKeeper 的相關路徑分別進行監聽,當觸發再均衡操作時,一個消費組下的所有消費者會同時進行再均衡操作,而消費者之間並不知道彼此操作的結果,這樣可能導致 Kafka 工作在一個不正確的狀態。與此同時,這種嚴重依賴於 ZooKeeper 集群的做法還有兩個比較嚴重的問題。

  1. 羊群效應(Herd Effect):所謂的羊群效應是指ZooKeeper 中一個被監聽的節點變化,大量的 Watcher 通知被發送到客戶端,導致在通知期間的其他操作延遲,也有可能發生類似死鎖的情況。
  2. 腦裂問題(Split Brain):消費者進行再均衡操作時每個消費者都與 ZooKeeper 進行通信以判斷消費者或broker變化的情況,由於 ZooKeeper 本身的特性,可能導致在同一時刻各個消費者獲取的狀態不一致,這樣會導致異常問題發生。

消費再均衡的原理是什麼?(提示:消費者協調器和消費組協調器)

就目前而言,一共有如下幾種情形會觸發再均衡的操作:

  • 有新的消費者加入消費組。
  • 有消費者宕機下線。消費者並不一定需要真正下線,例如遇到長時間的GC、網絡延遲導致消費者長時間未向 GroupCoordinator 發送心跳等情況時,GroupCoordinator 會認為消費者已經下線。
  • 有消費者主動退出消費組(發送 LeaveGroupRequest 請求)。比如客戶端調用了 unsubscrible() 方法取消對某些主題的訂閱。
  • 消費組所對應的 GroupCoorinator 節點發生了變更。
  • 消費組內所訂閱的任一主題或者主題的分區數量發生變化。

GroupCoordinator 是 Kafka 服務端中用於管理消費組的組件。而消費者客戶端中的 ConsumerCoordinator 組件負責與 GroupCoordinator 進行交互。

第一階段(FIND_COORDINATOR)

消費者需要確定它所屬的消費組對應的 GroupCoordinator 所在的 broker,並創建與該 broker 相互通信的網絡連接。如果消費者已經保存了與消費組對應的 GroupCoordinator 節點的信息,並且與它之間的網絡連接是正常的,那麼就可以進入第二階段。否則,就需要向集群中的某個節點發送 FindCoordinatorRequest 請求來查找對應的 GroupCoordinator,這裏的“某個節點”並非是集群中的任意節點,而是負載最小的節點。

第二階段(JOIN_GROUP)

在成功找到消費組所對應的 GroupCoordinator 之後就進入加入消費組的階段,在此階段的消費者會向 GroupCoordinator 發送 JoinGroupRequest 請求,並處理響應。

選舉消費組的leader
如果消費組內還沒有 leader,那麼第一個加入消費組的消費者即為消費組的 leader。如果某一時刻 leader 消費者由於某些原因退出了消費組,那麼會重新選舉一個新的 leader

選舉分區分配策略

  1. 收集各個消費者支持的所有分配策略,組成候選集 candidates。
  2. 每個消費者從候選集 candidates 中找出第一個自身支持的策略,為這個策略投上一票。
  3. 計算候選集中各個策略的選票數,選票數最多的策略即為當前消費組的分配策略。

第三階段(SYNC_GROUP)

leader 消費者根據在第二階段中選舉出來的分區分配策略來實施具體的分區分配,在此之後需要將分配的方案同步給各個消費者,通過 GroupCoordinator 這個“中間人”來負責轉發同步分配方案的。

第四階段(HEARTBEAT)

進入這個階段之後,消費組中的所有消費者就會處於正常工作狀態。在正式消費之前,消費者還需要確定拉取消息的起始位置。假設之前已經將最後的消費位移提交到了 GroupCoordinator,並且 GroupCoordinator 將其保存到了 Kafka 內部的 __consumer_offsets 主題中,此時消費者可以通過 OffsetFetchRequest 請求獲取上次提交的消費位移並從此處繼續消費。

消費者通過向 GroupCoordinator 發送心跳來維持它們與消費組的從屬關係,以及它們對分區的所有權關係。只要消費者以正常的時間間隔發送心跳,就被認為是活躍的,說明它還在讀取分區中的消息。心跳線程是一個獨立的線程,可以在輪詢消息的空檔發送心跳。如果消費者停止發送心跳的時間足夠長,則整個會話就被判定為過期,GroupCoordinator 也會認為這個消費者已經死亡,就會觸發一次再均衡行為。

Kafka中的冪等是怎麼實現的?

為了實現生產者的冪等性,Kafka 為此引入了 producer id(以下簡稱 PID)和序列號(sequence number)這兩個概念。

每個新的生產者實例在初始化的時候都會被分配一個 PID,這個 PID 對用戶而言是完全透明的。對於每個 PID,消息發送到的每一個分區都有對應的序列號,這些序列號從0開始單調遞增。生產者每發送一條消息就會將 <PID,分區> 對應的序列號的值加1。

broker 端會在內存中為每一對 <PID,分區> 維護一個序列號。對於收到的每一條消息,只有當它的序列號的值(SN_new)比 broker 端中維護的對應的序列號的值(SN_old)大1(即 SN_new = SN_old + 1)時,broker 才會接收它。如果 SN_new< SN_old + 1,那麼說明消息被重複寫入,broker 可以直接將其丟棄。如果 SN_new> SN_old + 1,那麼說明中間有數據尚未寫入,出現了亂序,暗示可能有消息丟失,對應的生產者會拋出 OutOfOrderSequenceException,這個異常是一個嚴重的異常,後續的諸如 send()、beginTransaction()、commitTransaction() 等方法的調用都會拋出 IllegalStateException 的異常。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

分類
發燒車訊

五分鐘學會HTML5的WebSocket協議

1、背景

  很多網站為了實現推送技術,所用的技術都是Ajax輪詢。輪詢是在特定的的時間間隔由瀏覽器對服務器發出HTTP請求,然後由服務器返回最新的數據給客戶端的瀏覽器。這種傳統的模式帶來很明顯的缺點,即瀏覽器需要不斷的向服務器發出請求,然而HTTP請求可能包含較長的頭部,其中真正有效的數據可能只是很小的一部分,顯然這樣會浪費很多的帶寬等資源。HTML5新增的一些新協議WebSocket,可以提供在單個TCP連接上提供全雙工,雙向通信,能夠節省服務器資源和帶寬,並且能夠實時進行通信。

2、WebSocket介紹

  傳統的http也是一種協議,WebSocket是一種協議,使用http服務器無法實現WebSocket,

2.1.瀏覽器支持情況

基本主流瀏覽器都支持

2.2.優點

相對於http有如下好處:

  • 1.客戶端與服務器只建立一個TCP連接,可以使用更少的連接。
  • 2.WebSocket服務器端可以主動推送數據到客戶端,更靈活高效。
  • 3.更輕量級的協議頭,減少數據傳送量。

對比輪訓機制

3、WebSocket用法

  我們了解WebSocket是什麼,有哪些優點后,怎麼使用呢?

3.1.WebSocket創建

WebSocket使用了自定義協議,url模式與http略有不同,未加密的連接是ws://,加密的連接是wss://,WebSocket實例使用new WebSocket()方法來創建,

var ws = new WebSocket(url, [protocol] );

第一個參數 url, 指定連接的 URL。第二個參數 protocol 是可選的,指定了可接受的子協議。

3.2.WebSocket屬性

當創建ws對象后,readyState為ws實例狀態,共4種狀態

  • 0 表示連接尚未建立。

  • 1 表示連接已建立,可以進行通信。

  • 2 表示連接正在進行關閉。

  • 3 表示連接已經關閉或者連接不能打開。

Tips:在發送報文之前要判斷狀態,斷開也應該有重連機制。

3.3.WebSocket事件

在創建ws實例對象后,會擁有以下幾個事件,根據不同狀態可在事件回調寫方法。

  • ws.onopen 連接建立時觸發

  • ws.onmessage 客戶端接收服務端數據時觸發

  • ws.onerror 通信發生錯誤時觸發

  • ws.onclose 連接關閉時觸發

ws.onmessage = (res) => {
  console.log(res.data);
};

ws.onopen = () => {
  console.log('OPEN...');
};

ws.onclose=()=>{
 console.log('CLOSE...');
}

3.4.WebSocket方法

  • ws.send() 使用連接發送數據(只能發送純文本數據)

  • ws.close() 關閉連接

4、Demo演示

  了解WebSocket的一些API之後,趁熱打鐵,做一個小案例跑一下。

4.1.Node服務器端

WebSocket協議與Node一起用非常好,原因有以下兩點:

1.WebSocket客戶端基於事件編程與Node中自定義事件差不多。

2.WebSocket實現客戶端與服務器端長連接,Node基本事件驅動的方式十分適合高併發連接

創建一個webSocket.js如下:

const WebSocketServer = require('ws').Server;
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function (ws) {
    console.log('client connected');
    ws.on('message', function (message) {
        ws.send('我收到了' + message);
    });
});

打開windows命令窗口運行

4.2.HTML客戶端

新建一個index.html頁面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webSocket小Demo</title>
</head>
<body>
    <div class="container">
        <div>
            <input type="text" id="msg">
            <button onclick="sendMsg()">發送報文</button>
        </div>
    </div>
    <script>
        const ws = new WebSocket('ws://localhost:8080');
        ws.onmessage = (res) => {
            console.log(res);
        };
        ws.onopen = () => {
            console.log('OPEN...');
        };
        ws.onclose = () => {
            console.log('CLOSE...');
        }
        function sendMsg() {
            let msg = document.getElementById('msg').value;
            ws.send(msg);
        }
    </script>
</body>

打開瀏覽器依次輸入字符1,2,3,每次輸入完點擊發送報體,可見在ws.onmessage事件中res.data中返回來我們發的報文

5、問題與總結

  以上只是簡單的介紹了下WebSocket的API與簡單用法,在處理高併發,長連接這些需求上,例如聊天室,可能WebSocket的http請求更加合適高效。

   但在使用WebSocket過程中發現容易斷開連接等問題,所以在每次發送報文前要判斷是否斷開,當多次發送報文時,由於服務器端返回數據量不同,返回客戶端前後順序也不同,所以需要在客戶端收到上一個報文返回數據后再發送下一個報文,為了避免回調嵌套過多,通過Promise ,async ,await等同步方式解決。關於WebSocket就寫這麼多,如有不足,歡迎多多指正!

參考資料:
《JavaScript高級教程》
《深入檢出NodeJs》

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

分類
發燒車訊

小白學 Python 爬蟲(2):前置準備(一)基本類庫的安裝

人生苦短,我用 Python

前文傳送門:

本篇內容較長,各位同學可以先收藏后再看~~

在開始講爬蟲之前,還是先把環境搞搞好,工欲善其事必先利其器嘛~~~

本篇文章主要介紹 Python 爬蟲所使用到的請求庫和解析庫,請求庫用來請求目標內容,解析庫用來解析請求回來的內容。

開發環境

首先介紹小編本地的開發環境:

  • Python3.7.4
  • win10

差不多就這些,最基礎的環境,其他環境需要我們一個一個安裝,現在開始。

請求庫

雖然 Python 為我們內置了 HTTP 請求庫 urllib ,使用姿勢並不是很優雅,但是很多第三方的提供的 HTTP 庫確實更加的簡潔優雅,我們下面開始。

Requests

Requests 類庫是一個第三方提供的用於發送 HTTP 同步請求的類庫,相比較 Python 自帶的 urllib 類庫更加的方便和簡潔。

Python 為我們提供了包管理工具 pip ,使用 pip 安裝將會非常的方便,安裝命令如下:

pip install requests

驗證:

C:\Users\inwsy>python
Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests

首先在 CMD 命令行中輸入 python ,進入 python 的命令行模式,然後輸入 import requests 如果沒有任何錯誤提示,說明我們已經成功安裝 Requests 類庫。

Selenium

Selenium 現在更多的是用來做自動化測試工具,相關的書籍也不少,同時,我們也可以使用它來做爬蟲工具,畢竟是自動化測試么,利用它我們可以讓瀏覽器執行我們想要的動作,比如點擊某個按鈕、滾動滑輪之類的操作,這對我們模擬真實用戶操作是非常方便的。

安裝命令如下:

pip install selenium

驗證:

C:\Users\inwsy>python
Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import selenium

這樣沒報錯我們就安裝完成,但是你以為這樣就算好了么?圖樣圖森破啊。

ChromeDriver

我們還需要瀏覽器的支持來配合 selenium 的工作,開發人員嘛,常用的瀏覽器莫非那麼幾種:Chrome、Firefox,那位說 IE 的同學,你給我站起來,小心我跳起來打你膝蓋,還有說 360 瀏覽器的,你們可讓我省省心吧。

接下來,安裝 Chrome 瀏覽器就不用講了吧。。。。

再接下來,我們開始安裝 ChromeDriver ,安裝了 ChromeDriver 后,我們才能通過剛才安裝的 selenium 來驅動 Chrome 來完成各種騷操作。

首先,我們需要查看自己的 Chrome 瀏覽器的版本,在 Chrome 瀏覽器右上角的三個點鐘,點擊 幫助 -> 關於,如下圖:

將這個版本找個小本本記下來,小編這裏的版本為: 版本 78.0.3904.97(正式版本) (64 位)

接下來我們需要去 ChromeDriver 的官網查看當前 Chrome 對應的驅動。

官網地址:

因某些原因,訪問時需某些手段,訪問不了的就看小編為大家準備的版本對應表格吧。。。

ChromeDriver Version Chrome Version
78.0.3904.11 78
77.0.3865.40 77
77.0.3865.10 77
76.0.3809.126 76
76.0.3809.68 76
76.0.3809.25 76
76.0.3809.12 76
75.0.3770.90 75
75.0.3770.8 75
74.0.3729.6 74
73.0.3683.68 73
72.0.3626.69 72
2.46 71-73
2.45 70-72
2.44 69-71
2.43 69-71
2.42 68-70
2.41 67-69
2.40 66-68
2.39 66-68
2.38 65-67
2.37 64-66
2.36 63-65
2.35 62-64

順便小編找到了國內對應的下載的鏡像站,由淘寶提供,如下:

雖然和小編本地的小版本對不上,但是看樣子只要大版本符合應該沒啥問題,so,去鏡像站下載對應的版本即可,小編這裏下載的版本是:78.0.3904.70 ,ChromeDriver 78版本的最後一個小版本。

下載完成后,將可執行文件 chromedriver.exe 移動至 Python 安裝目錄的 Scripts 目錄下。如果使用默認安裝未修改過安裝目錄的話目錄是:%homepath%\AppData\Local\Programs\Python\Python37\Scripts ,如果有過修改,那就自力更生吧。。。

chromedriver.exe 添加后結果如下圖:

驗證:

還是在 CMD 命令行中,輸入以下內容:

C:\Users\inwsy>python
Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from selenium import webdriver
>>> browser = webdriver.Chrome()

如果打開一個空白的 Chrome 頁面說明安裝成功。

GeckoDriver

上面我們通過安裝 Chrome 的驅動完成了 Selenium 與 Chrome 的對接,想要完成 Selenium 與 FireFox 的對接則需要安裝另一個驅動 GeckoDriver 。

FireFox 的安裝小編這裏就不介紹了,大家最好去官網下載安裝,路徑如下:

FireFox 官網地址:

GeckoDriver 的下載需要去 Github (全球最大的同性交友網站),下載路徑小編已經找好了,可以選擇最新的 releases 版本進行下載。

下載地址:

選擇對應自己的環境,小編這裏選擇 win-64 ,版本為 v0.26.0 進行下載。

具體配置方式和上面一樣,將可執行的 .exe 文件放入 %homepath%\AppData\Local\Programs\Python\Python37\Scripts 目錄下即可。

驗證:

還是在 CMD 命令行中,輸入以下內容:

C:\Users\inwsy>python
Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from selenium import webdriver
>>> browser = webdriver.Firefox()

應該是可以正常打開一個空白的 FireFox 頁面的,結果如下:

注意: GeckoDriver 指出一點,當前的版本在 win 下使用有已知 bug ,需要安裝微軟的一個插件才能解決,原文如下:

You must still have the installed on your system for the binary to run. This is a known bug which we weren’t able fix for this release.

插件下載地址:

請各位同學選擇自己對應的系統版本進行下載安裝。

Aiohttp

上面我們介紹了同步的 Http 請求庫 Requests ,而 Aiohttp 則是一個提供異步 Http 請求的類庫。

那麼,問題來了,什麼是同步請求?什麼是異步請求呢?

  • 同步:阻塞式,簡單理解就是當發出一個請求以後,程序會一直等待這個請求響應,直到響應以後,才繼續做下一步。
  • 異步:非阻塞式,還是上面的例子,當發出一個請求以後,程序並不會阻塞在這裏,等待請求響應,而是可以去做其他事情。

從資源消耗和效率上來說,同步請求是肯定比不過異步請求的,這也是為什麼異步請求會比同步請求擁有更大的吞吐量。在抓取數據時使用異步請求,可以大大提升抓取的效率。

如果還想了解跟多有關 aiohttp 的內容,可以訪問官方文檔: 。

aiohttp 安裝如下:

pip install aiohttp

aiohttp 還推薦我們安裝另外兩個庫,一個是字符編碼檢測庫 cchardet ,另一個是加速DNS的解析庫 aiodns 。

安裝 cchardet 庫:

pip install cchardet

安裝 aiodns 庫:

pip install aiodns

aiohttp 十分貼心的為我們準備了整合的安裝命令,無需一個一個鍵入命令,如下:

pip install aiohttp[speedups]

驗證:

C:\Users\inwsy>python
Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import aiohttp

沒報錯就安裝成功。

解析庫

lxml

lxml 是 Python 的一個解析庫,支持 HTML 和 XML 的解析,支持 XPath 的解析方式,而且解析效率非常高。

什麼是 XPath ?

XPath即為XML路徑語言(XML Path Language),它是一種用來確定XML文檔中某部分位置的語言。
XPath基於XML的樹狀結構,提供在數據結構樹中找尋節點的能力。起初XPath的提出的初衷是將其作為一個通用的、介於XPointer與XSL間的語法模型。

以上內容來源《百度百科》

好吧,小編說人話,就是可以從 XML 文檔或者 HTML 文檔中快速的定位到所需要的位置的路徑語言。

還沒看懂?emmmmmmmmmmm,我們可以使用 XPath 快速的取出 XML 或者 HTML 文檔中想要的值。用法的話我們放到後面再聊。

安裝 lxml 庫:

pip install lxml

驗證:

C:\Users\inwsy>python
Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import lxml

沒報錯就安裝成功。

Beautiful Soup

Beautiful Soup 同樣也是一個 Python 的 HTML 或 XML 的解析庫 。它擁有強大的解析能力,我們可以使用它更方便的從 HTML 文檔中提取數據。

首先,放一下 Beautiful Soup 的官方網址,有各種問題都可以在官網查看文檔,各位同學養成一個好習慣,有問題找官方文檔,雖然是英文的,使用 Chrome 自帶的翻譯功能還是勉強能看的。

官方網站:

安裝方式依然使用 pip 進行安裝:

pip install beautifulsoup4

Beautiful Soup 的 HTML 和 XML 解析器是依賴於 lxml 庫的,所以在此之前請確保已經成功安裝好了 lxml 庫 。

驗證:

C:\Users\inwsy>python
Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from bs4 import BeautifulSoup

沒報錯就安裝成功。

pyquery

pyquery 同樣也是一個網頁解析庫,只不過和前面兩個有區別的是它提供了類似 jQuery 的語法來解析 HTML 文檔,前端有經驗的同學應該會非常喜歡這款解析庫。

首先還是放一下 pyquery 的官方文檔地址。

官方文檔:

安裝:

pip install pyquery

驗證:

C:\Users\inwsy>python
Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyquery

沒報錯就安裝成功。

本篇的內容就先到這裏結束了。請各位同學在自己的電腦上將上面介紹的內容都安裝一遍,以便後續學習使用。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

分類
發燒車訊

【Stream—5】MemoryStream相關知識分享

一、簡單介紹一下MemoryStream

MemoryStream是內存流,為系統內存提供讀寫操作,由於MemoryStream是通過無符號字節數組組成的,可以說MemoryStream的性能可以算比較出色,所以它擔當起了一些其他流進行數據交互安時的中間工作,同時可降低應用程序中對臨時緩衝區和臨時文件的需求,其實MemoryStream的重要性不亞於FileStream,在很多場合,我們必須使用它來提高性能

二、MemoryStream和FileStream的區別

前文中也提到了,FileStream主要對文件的一系列操作,屬於比較高層的操作,但是MemoryStream卻很不一樣,他更趨向於底層內存的操作,這樣能夠達到更快速度和性能,也是他們的根本區別,很多時候,操作文件都需要MemoryStream來實際進行讀寫,最後放入相應的FileStream中,不僅如此,在諸如XmlWriter的操作中也需要使用MemoryStream提高讀寫速度

三、分析MemoryStream最常見的OutOfMemory異常

先看一下下面一段簡單的代碼

 1             //測試byte數組 假設該數組容量是256M
 2             var testBytes = new byte[256 * 1024 * 1024];
 3             var ms = new MemoryStream();
 4             using (ms)
 5             {
 6                 for (int i = 0; i < 1000; i++)
 7                 {
 8                     try
 9                     {
10                         ms.Write(testBytes, 0, testBytes.Length);
11                     }
12                     catch
13                     {
14                         Console.WriteLine("該內存流已經使用了{0}M容量的內存,該內存流最大容量為{1}M,溢出時容量為{2}M",
15                             GC.GetTotalMemory(false) / (1024 * 1024),//MemoryStream已經消耗內存量
16                             ms.Capacity / (1024 * 1024), //MemoryStream最大的可用容量
17                             ms.Length / (1024 * 1024));//MemoryStream當前流的長度(容量)
18                         break;
19                     }
20                 }
21             }
22             Console.ReadLine();

輸出結果:

 

 

 從輸出結果來看,MemoryStream默認可用最大容量是1024M,發生異常時正好是其最大容量。

問題來了,假設我們需要操作比較大的文件,該怎麼辦呢?其實有2種方法可以搞定,一種是分段處理,我們將Byte數組分成等份進行處理,還有一種方式便是增加MomoryStream的最大可用容量(字節),我們可以在聲明MomoryStream的構造函數時利用它的重載版本:MemoryStream(int capacity)

到底使用哪種方法比較好呢?其實筆者認為具體項目具體分析,前者分段處理的確能夠解決大數據量操作的問題,但是犧牲了性能和時間(多線程暫時不考慮),後者可以得到性能上的優勢,但是其允許最大容量是 int.Max,所以無法給出一個明確的答案,大家在做項目時,按照需求自己定製即可,最關鍵的還是要取到性能和開銷的最佳點位,還有一種更噁心的溢出方式,往往會讓大家抓狂,就是不定時溢出,就是MemoryStream處理的文件可能只有40M或更小時,也會發生OutOfMemory的異常,關於這個問題,終於在老外的一篇文章中得到了解釋,運氣還不錯,可以看看這篇博文:,由於涉及到windows的內存機制,包括內存也,進程的虛擬地址空間等,比較複雜,所以大家看他的文章前,我先和大家簡單的介紹一下頁和進程的虛擬地址:

內存頁:內存頁分為:文件頁和計算頁

內存中的文件頁是文件緩存區,即文件型的內存頁,用於存放文件數據的內存頁(也稱永久頁),作用在於讀寫文件時可以減少對磁盤的訪問,如果它的大小設置的太小,會引起系統頻繁的訪問磁盤,增加磁盤I/O,設置太大,會浪費內存資源。

內存中的計算頁也稱為計算型的內存頁,主要用於存放程序代碼和臨時使用的數據。

進程的虛擬地址:每一個進程被給予它的非常自由的虛擬地址空間。對於32位的進程,地址空間是4G,因為一個32位指針能夠從0x00000000到0xffffffff之間的任意值,這個範圍允許指針從4294967296個值得一個,覆蓋了一個進程得4G範圍,對於64位進程,地址空間是16eb因為一個64位指針能夠指向18,446,744,073,709,551,616個值中的一個,覆蓋一個進程的16eb範圍,這是十分寬廣的範圍,上述概念都在自windows核心編程這本書,其實這本書對於我們程序員來說很重要,對於內存的操作,本人也是小白。

四、MemoryStream的構造函數

1、MemoryStream()

MemoryStream允許不帶參數的構造

2、MemoryStream(byte[] byte)

Byte數組是包含了一定數據的byte數組,這個構造很重要,初學者或者用的不是很多的程序員會忽略這個構造函數導致後面讀取或寫入數據時發現MemoryStream中沒有byte數據,會導致很鬱悶的感覺,大家注意一下就行,有時候也可能無需這樣,因為很多方法返回值已經是MemoryStream了。

3、MemoryStream(int capacity)

這個是重中之重,為什麼這麼說呢?我在本文探討關於OutMemory異常中也提到了,如果你想額外提高MemoryStream的吞吐量,也只能靠這個方法提升一定的吞吐量,最多也只能到int.Max,這個方法也是解決OutOfMemory的一個可行方案。

4、MemoryStream(byte[] byte,bool writeable)

writeable參數定義該流是否可寫

5、MemoryStream(byte[] byte,int index,int count)

index:參數定義從byte數組中的索引index

count:參數是獲取的數據量的個數

6、MemoryStream(byte[] byte,int index,int count,bool writeable,bool publiclyVisible)

publiclyVisible:參數表示true可以啟用GetBuffer方法,它返回無符號字節數組,流從該數組創建,否則為false。(大家一定覺得這個很難理解,別急,下面的方法中我會詳細的講一下這個東西)

五、MemoryStream的屬性

 Memory的屬性大致都是和其父類很相似,這些功能在我的這篇文章中已經詳細討論過,所以我簡單列舉以下其屬性:

 

 其獨有的屬性:

Capacity:這個前文其實已經提及,它表示該流的可支配容量(字節),非常重要的一個屬性。

六、MemoryStream的方法

對於重寫的方法,這裏不再重複說明,大家可以去看一下

 以下是MemoryStream獨有的方法

1、virtual byte[] GetBuffer()

這個方法使用時需要小心,因為這個方法返回無符號字節數組,也就是說,即使我只輸入幾個字符例如“HellowWorld”我們只希望返回11個數據就行,可是這個方法會把整個緩衝區的數據,包括那些已經分配但是實際上沒有用到的字符數據都返回回來了,如果想啟用這個方法那必須使用上面最後一個構造函數,將publiclyVisible屬性設置成true就行,這也是上面那個構造函數的錯用所在。

2、virtual void WriteTo(Stream stream)

這個方法的目的其實在本文開始的時候討論性能問題時已經指出,MemoryStream常用起中間流的作用,所以讀寫在處理完后將內存吸入其他流中。

七、示例:

1、XmlWriter中使用MemoryStream

 1         public static void UseMemoryStreamInXmlWriter()
 2         {
 3             var ms = new MemoryStream();
 4             using (ms)
 5             {
 6                 //定義一個XmlWriter
 7                 using (XmlWriter writer= XmlWriter.Create(ms))
 8                 {
 9                     //寫入xml頭
10                     writer.WriteStartDocument(true);
11                     //寫入一個元素
12                     writer.WriteStartElement("Content");
13                     //為這個元素增加一個test屬性
14                     writer.WriteStartAttribute("test");
15                     //設置test屬性的值
16                     writer.WriteValue("萌萌小魔王");
17                     //釋放緩衝,這裏可以不用釋放,但是在實際項目中可能要考慮部分釋放對性能帶來的提升
18                     writer.Flush();
19                     Console.WriteLine($"此時內存使用量為:{GC.GetTotalMemory(false)/1024}KB,該MemoryStream已使用容量為:{Math.Round((double)ms.Length,4)}byte,默認容量為:{ms.Capacity}byte");
20                     Console.WriteLine($"重新定位前MemoryStream所在的位置是{ms.Position}");
21                     //將流中所在當前位置往後移動7位,相當於空格
22                     ms.Seek(7, SeekOrigin.Current);
23                     Console.WriteLine($"重新定位后MemoryStream所存在的位置是{ms.Position}");
24                     //如果將流所在的位置設置位如下示的位置,則XML文件會被打亂
25                     //ms.Position = 0;
26                     writer.WriteStartElement("Content2");
27                     writer.WriteStartAttribute("testInner");
28                     writer.WriteValue("萌萌小魔王2");
29                     writer.WriteEndElement();
30                     writer.WriteEndElement();
31                     //再次釋放
32                     writer.Flush();
33                     Console.WriteLine($"此時內存使用量為:{GC.GetTotalMemory(false) / 1024}KB,該MemoryStream已使用容量為:{Math.Round((double)ms.Length, 4)}byte,默認容量為:{ms.Capacity}byte");
34                     //建立一個FileStream 文件創建目的地是f:\test.xml
35                     var fs=new FileStream(@"f:\test.xml",FileMode.OpenOrCreate);
36                     using (fs)
37                     {
38                         //將內存流注入FileStream
39                         ms.WriteTo(fs);
40                         if (ms.CanWrite)
41                         {
42                             //釋放緩衝區
43                             fs.Flush();
44                         }
45                     }
46                     Console.WriteLine();
47                 }
48             }
49         }

運行結果:

 

 咱看一下XML文本是什麼樣的?

 

 2、自定義處理圖片的HttpHandler

有時項目里我們必須將圖片進行一定的操作,例如:水印,下載等,為了方便和管理我們可以自定義一個HttpHandler來負責這些工作

後台代碼:

 1     public class ImageHandler : IHttpHandler
 2     {
 3         /// <summary>
 4         /// 實現IHttpHandler接口中ProcessRequest方法
 5         /// </summary>
 6         /// <param name="context"></param>
 7         public void ProcessRequest(HttpContext context)
 8         {
 9             context.Response.Clear();
10             //得到圖片名
11             var imageName = context.Request["ImageName"] ?? "小魔王";
12             //得到圖片地址
13             var stringFilePath = context.Server.MapPath($"~/Image/{imageName}.jpg");
14             //聲明一個FileStream用來將圖片暫時放入流中
15             FileStream stream=new FileStream(stringFilePath,FileMode.Open);
16             using (stream)
17             {
18                 //通過GetImageFromStream方法將圖片放入Byte數組中
19                 var imageBytes = GetImageFromStream(stream, context);
20                 //上下文確定寫道客戶端時的文件類型
21                 context.Response.ContentType = "image/jpeg";
22                 //上下文將imageBytes中的數組寫到前端
23                 context.Response.BinaryWrite(imageBytes);
24             }
25         }
26 
27         public bool IsReusable => true;
28 
29         /// <summary>
30         /// 將流中的圖片信息放入byte數組后返回該數組
31         /// </summary>
32         /// <param name="stream">文件流</param>
33         /// <param name="context">上下文</param>
34         /// <returns></returns>
35         private byte[] GetImageFromStream(FileStream stream, HttpContext context)
36         {
37             //通過Stream到Image
38             var image = Image.FromStream(stream);
39             //加上水印
40             image = SetWaterImage(image, context);
41             //得到一個ms對象
42             MemoryStream ms = new MemoryStream();
43             using (ms)
44             {
45                 //將圖片保存至內存流
46                 image.Save(ms,ImageFormat.Jpeg);
47                 byte[] imageBytes = new byte[ms.Length];
48                 ms.Position = 0;
49                 //通過內存流放到imageBytes
50                 ms.Read(imageBytes, 0, imageBytes.Length);
51                 //ms.Close();
52                 //返回imageBytes
53                 return imageBytes;
54             }
55         }
56 
57         private Image SetWaterImage(Image image, HttpContext context)
58         {
59             Graphics graphics = Graphics.FromImage(image);
60             Image waterImage = Image.FromFile(context.Server.MapPath("~/Image/logo.jpg"));
61             graphics.DrawImage(waterImage, new Point { X = image.Size.Width - waterImage.Size.Width, Y = image.Size.Height - waterImage.Size.Height });
62             return image;
63         }
64     }

別忘了,還要再web.config中進行配置,如下:

 

 這樣前台就能使用了

 

 讓我們來看一下輸出結果:

 

 哈哈,還不錯。

好了,MemoryStream相關的知識就先分享到這裏了。同志們,再見!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

分類
發燒車訊

Spring Boot2 系列教程(二十五)Spring Boot 整合 Jpa 多數據源

本文是 Spring Boot 整合數據持久化方案的最後一篇,主要和大夥來聊聊 Spring Boot 整合 Jpa 多數據源問題。在 Spring Boot 整合JbdcTemplate 多數據源、Spring Boot 整合 MyBatis 多數據源以及 Spring Boot 整合 Jpa 多數據源這三個知識點中,整合 Jpa 多數據源算是最複雜的一種,也是很多人在配置時最容易出錯的一種。本文大夥就跟着松哥的教程,一步一步整合 Jpa 多數據源。

工程創建

首先是創建一個 Spring Boot 工程,創建時添加基本的 Web、Jpa 以及 MySQL 依賴,如下:

創建完成后,添加 Druid 依賴,這裏和前文的要求一樣,要使用專為 Spring Boot 打造的 Druid,大夥可能發現了,如果整合多數據源一定要使用這個依賴,因為這個依賴中才有 DruidDataSourceBuilder,最後還要記得鎖定數據庫依賴的版本,因為可能大部分人用的還是 5.x 的 MySQL 而不是 8.x。完整依賴如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.28</version>
    <scope>runtime</scope>
</dependency>

如此之後,工程就創建成功了。

基本配置

在基本配置中,我們首先來配置多數據源基本信息以及 DataSource,首先在 application.properties 中添加如下配置信息:

#  數據源一
spring.datasource.one.username=root
spring.datasource.one.password=root
spring.datasource.one.url=jdbc:mysql:///test01?useUnicode=true&characterEncoding=UTF-8
spring.datasource.one.type=com.alibaba.druid.pool.DruidDataSource

#  數據源二
spring.datasource.two.username=root
spring.datasource.two.password=root
spring.datasource.two.url=jdbc:mysql:///test02?useUnicode=true&characterEncoding=UTF-8
spring.datasource.two.type=com.alibaba.druid.pool.DruidDataSource

# Jpa配置
spring.jpa.properties.database=mysql
spring.jpa.properties.show-sql=true
spring.jpa.properties.database-platform=mysql
spring.jpa.properties.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect

這裏 Jpa 的配置和上文相比 key 中多了 properties,多數據源的配置和前文一致,然後接下來配置兩個 DataSource,如下:

@Configuration
public class DataSourceConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.one")
    @Primary
    DataSource dsOne() {
        return DruidDataSourceBuilder.create().build();
    }
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.two")
    DataSource dsTwo() {
        return DruidDataSourceBuilder.create().build();
    }
}

這裏的配置和前文的多數據源配置基本一致,但是注意多了一個在 Spring 中使用較少的註解 @Primary,這個註解一定不能少,否則在項目啟動時會出錯,@Primary 表示當某一個類存在多個實例時,優先使用哪個實例。

好了,這樣,DataSource 就有了。

多數據源配置

接下來配置 Jpa 的基本信息,這裏兩個數據源,我分別在兩個類中來配置,先來看第一個配置:

@Configuration
@EnableJpaRepositories(basePackages = "org.javaboy.jpa.dao",entityManagerFactoryRef = "localContainerEntityManagerFactoryBeanOne",transactionManagerRef = "platformTransactionManagerOne")
public class JpaConfigOne {
    @Autowired
    @Qualifier(value = "dsOne")
    DataSource dsOne;
    @Autowired
    JpaProperties jpaProperties;
    @Bean
    @Primary
    LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBeanOne(EntityManagerFactoryBuilder builder) {
        return builder.dataSource(dsOne)
                .packages("org.javaboy.jpa.model")
                .properties(jpaProperties.getProperties())
                .persistenceUnit("pu1")
                .build();
    }
    @Bean
    PlatformTransactionManager platformTransactionManagerOne(EntityManagerFactoryBuilder builder) {
        LocalContainerEntityManagerFactoryBean factoryBeanOne = localContainerEntityManagerFactoryBeanOne(builder);
        return new JpaTransactionManager(factoryBeanOne.getObject());
    }
}

首先這裏注入 dsOne,再注入 JpaProperties,JpaProperties 是系統提供的一個實例,裡邊的數據就是我們在 application.properties 中配置的 jpa 相關的配置。然後我們提供兩個 Bean,分別是 LocalContainerEntityManagerFactoryBean 和 PlatformTransactionManager 事務管理器,不同於 MyBatis 和 JdbcTemplate,在 Jpa 中,事務一定要配置。在提供 LocalContainerEntityManagerFactoryBean 的時候,需要指定 packages,這裏的 packages 指定的包就是這個數據源對應的實體類所在的位置,另外在這裏配置類上通過 @EnableJpaRepositories 註解指定 dao 所在的位置,以及 LocalContainerEntityManagerFactoryBean 和 PlatformTransactionManager 分別對應的引用的名字。

好了,這樣第一個就配置好了,第二個基本和這個類似,主要有幾個不同點:

  • dao 的位置不同
  • persistenceUnit 不同
  • 相關 bean 的名稱不同

注意實體類可以共用。

代碼如下:

@Configuration
@EnableJpaRepositories(basePackages = "org.javaboy.jpa.dao2",entityManagerFactoryRef = "localContainerEntityManagerFactoryBeanTwo",transactionManagerRef = "platformTransactionManagerTwo")
public class JpaConfigTwo {
    @Autowired
    @Qualifier(value = "dsTwo")
    DataSource dsTwo;
    @Autowired
    JpaProperties jpaProperties;
    @Bean
    LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBeanTwo(EntityManagerFactoryBuilder builder) {
        return builder.dataSource(dsTwo)
                .packages("org.javaboy.jpa.model")
                .properties(jpaProperties.getProperties())
                .persistenceUnit("pu2")
                .build();
    }
    @Bean
    PlatformTransactionManager platformTransactionManagerTwo(EntityManagerFactoryBuilder builder) {
        LocalContainerEntityManagerFactoryBean factoryBeanTwo = localContainerEntityManagerFactoryBeanTwo(builder);
        return new JpaTransactionManager(factoryBeanTwo.getObject());
    }
}

接下來,在對應位置分別提供相關的實體類和 dao 即可,數據源一的 dao 如下:

package org.javaboy.jpa.dao;
public interface UserDao extends JpaRepository<User,Integer> {
    List<User> getUserByAddressEqualsAndIdLessThanEqual(String address, Integer id);
    @Query(value = "select * from t_user where id=(select max(id) from t_user)",nativeQuery = true)
    User maxIdUser();
}

數據源二的 dao 如下:

package org.javaboy.jpa.dao2;
public interface UserDao2 extends JpaRepository<User,Integer> {
    List<User> getUserByAddressEqualsAndIdLessThanEqual(String address, Integer id);

    @Query(value = "select * from t_user where id=(select max(id) from t_user)",nativeQuery = true)
    User maxIdUser();
}

共同的實體類如下:

package org.javaboy.jpa.model;
@Entity(name = "t_user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String username;
    private String address;
    //省略getter/setter
}

到此,所有的配置就算完成了,接下來就可以在 Service 中注入不同的 UserDao,不同的 UserDao 操作不同的數據源。

其實整合 Jpa 多數據源也不算難,就是有幾個細節問題,這些細節問題解決,其實前面介紹的其他多數據源整個都差不多。

好了,本文就先介紹到這裏。

相關案例已經上傳到 GitHub,歡迎小夥伴們們下載:

掃碼關注松哥,公眾號後台回復 2TB,獲取松哥獨家 超2TB 免費 Java 學習乾貨

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

分類
發燒車訊

蘋果傳將攜手韓廠研發電動車用電池

蘋果公司持續擴大業務範圍,除新成立的蘋果能源(Apple Energy)成功取得售電許可外,市場上關注度十足的蘋果電動車專案「泰坦計畫」(Project Titan)也時常傳出新消息。近期傳言蘋果將與南韓某企業合作,運用該南韓公司的電池專利,攜手開發電動車用電池。

根據日本蘋果情報網站iPhone Mania、南韓媒體ET News 等報導,蘋果看上某家南韓廠商所取得的鋰電池專利,打算與該廠合作開發電動車用電池。被看上的鋰電池專利技術,電池中央部位採中空設計,可使空氣流通來冷卻電池,等同降低冷卻系統的需求,騰出更多空間來增加電池容量。

日、韓媒體並未披露可能將與蘋果合作的韓廠名稱,但美國MacRumors 透剁歐洲專利局得知,這款「中央中空」的電池是由一家稱為Orange Power 的南韓廠商取得。該廠係一小廠,員工人數僅33人,其中多數為研發人員。

蘋果電動車計畫自曝光以來即備受矚目,但至今仍然只聞樓梯響。據了解,目前共有約1,000人參與蘋果電動車企劃,且蘋果已在柏林設立秘密開發實驗室。MoneyDJ引述The Information網站的說法,表示蘋果電動車可能在2021年亮相;但也有分析師認為,蘋果電動車可能會步上蘋果電視的後塵,胎死腹中。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選