TCP協議粘包問題詳解

TCP協議粘包問題詳解

前言

  在本章節中,我們將探討TCP協議基於流式傳輸的最大一個問題,即粘包問題。本章主要介紹TCP粘包的原理與其三種解決粘包的方案。並且還會介紹為什麼UDP協議不會產生粘包。

 

基於TCP協議的socket實現遠程命令輸入

  我們準備做一個可以在Client端遠程執行Server端shell命令並拿到其執行結果的程序,而涉及到網絡通信就必然會出現socket模塊,關於如何抉擇傳輸層協議的選擇?我們選擇使用TCP協議,因為它是可靠傳輸協議且數據量支持比UDP協議要大。好了廢話不多說直接上代碼了。

 

  Server端代碼如下:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# ==== 基於TCP協議的socket實現遠程命令輸入之Server ====

import subprocess
from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("0.0.0.0",6666))  # 放在遠程填入0.0.0.0,放在本地填入127.0.0.1
server.listen(5)

while 1:  # 鏈接循環
    conn,client_addr = server.accept()
    while 1:  # 通信循環
        try:  # 防止Windows平台下Client端異常關閉導致雙向鏈接崩塌Server端異常的情況發生
            cmd = conn.recv(1024)
            if not cmd:  # 防止類Unix平台下Client端異常關閉導致雙向鏈接崩塌Server端異常的情況發生
                break
            res = subprocess.Popen(cmd.decode("utf-8"),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,)

            stdout_res = res.stdout.read()  # 正確結果
            stderr_res = res.stderr.read()  # 錯誤結果
            # subprocess模塊拿到的是bytes類型,所以直接發送即可

            cmd_res = stdout_res if stdout_res else stderr_res  # 因為兩個結果只有一個有信息,所以我們只拿到有結果的那個
            conn.send(cmd_res)

        except Exception:
            break

    conn.close()  # 由於client端鏈接異常,故關閉鏈接循環

 

  Client端代碼如下:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# ==== 基於TCP協議的socket實現遠程命令輸入之Client ====

from socket import *

client = socket(AF_INET,SOCK_STREAM)
client.connect(("xxx.xxx.xxx.xxx",6666))  # 填入Server端公網IP

while 1:
    cmd = input("請輸入命令>>>:").strip()
    if not cmd:
        continue
    if cmd == "quit":
        break
    client.send(cmd.encode("utf-8"))
    cmd_res = client.recv(1024)  # 本次接收1024字節數據
    print(cmd_res.decode("utf-8"))  # 如果Server端是Windows則用gbk解碼,類Unix用utf-8解碼

client.close()

 

  測試結果:

 

粘包問題及其原理

  上面的測試一切看起來都非常完美,但是是有一個BUG的。當我們如果讀取一條非常長的命令實際上是會出問題的,比如:

  這種現象被稱之為粘包,那麼為何會產生這樣的現象呢?

 

  這是由於recv()沒有一次性讀取完整個內核緩衝區的內容導致的。其實歸根結底還是怪TCP是字節流方式傳輸數據。

 

  我們來解析一下這種現象產生的原因:

 

  由於我們的recv()只是按照固定的1024去讀取數據,那麼一旦整體內核緩衝區中所存儲的整體數據大於1024,就會產生粘包現象。所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。

 

  這裏我還畫了一幅圖,可以方便讀者理解:

 

  那麼我們可以通過不斷的增大recv()中的讀取範圍來解決這個問題嗎?就像對應上圖中的,一次性把快遞櫃包裹全取完,答案是不可以!你再大你也不可能大過內核緩衝區,這個東西都是有一個一定的閾值。一旦超出了這個閾值就會引發異常或者乾脆無效。那麼有什麼好的辦法呢?哈,下面會教給你一些解決辦法的。不過在此之前我們要先看一個TCP協議特有的Nagle算法。

 

Nagle算法與粘包

 

  基於TCP協議的socket通信有一個特點,即:一方的send()與另一方的recv()可以沒有任何關係,即:一方send()三次,另一方recv()一次就可以將數據全部取出來。

 

  TCP協議的發送方有一個特徵。他會進行組包,如果一次發送的數據量很小,比如第一次發送10個字節,第二次發生2個字節,第三次發生3個字節。他可能會將這15個字節湊到一塊發送出去,這是採用了Nagle算法來進行的,這麼做有一個弊端就是接收方想要將這個大的數據包按照發送方的發送次數精確無誤的接收拆分成10 2 3必須要有發送方提供的拆包機制才行。

 

  如下圖組所示

 

  發送方:

from socket import *
ip_port = ("127.0.0.1",12306)
buffer_size = 1024
back_log = 5

server = socket(AF_INET,SOCK_STREAM)
server.bind(ip_port)
server.listen(back_log)

conn,addr = server.accept()
conn.send("hello,".encode("utf-8"))  # 第一次發送是6Bytes的數據
conn.send("world,".encode("utf-8"))     # 第二次也是6Bytes的數據
conn.send("yunyaGG!!".encode("utf-8"))  # 第三次是9Bytes的數據

 

  接收方:

from socket import *
ip_port = ("127.0.0.1",12306)
buffer_size = 1024

client = socket(AF_INET,SOCK_STREAM)
client.connect(ip_port)

data_1 = client.recv(buffer_size)  # 我們讀取數據時統一用設定的 buffer_size 來讀取
print("這是第一次的數據包:",data_1.decode("utf-8"))
data_2 = client.recv(buffer_size)
print("這是第二次的數據包:",data_2.decode("utf-8"))
data_3 = client.recv(buffer_size)
print("這是第三次的數據包:",data_3.decode("utf-8"))

 

  接收結果:

# ==== 執行結果 ====
​
"""
這是第一次的數據包: hello,
這是第二次的數據包: world,yunyaGG!!
這是第三次的數據包: 
"""

 

  和預想的有點不太一樣哈,居然把第二次和第三次組成了一個大的數據包發送過來了。這就是Nagle算法,這樣的組包策略很容易就會產生粘包。我不知道你是以什麼樣的方式發過來的,所以我recv()就只能按照自己設定的方式去接收。

 

  現在思考一下粘包的思路,我們的發送方需要將切分解包的規則告訴給接收方。

  我們嘗試改一下每一次的buffer_size接收大小:

 

  接收方:

from socket import *
ip_port = ("127.0.0.1",12306)
buffer_size = 1024

client = socket(AF_INET,SOCK_STREAM)
client.connect(ip_port)

data_1 = client.recv(6)  # 我們手動的按照對方發送時的規則來進行拆包
print("這是第一次的數據包:",data_1.decode("utf-8"))
data_2 = client.recv(6)
print("這是第二次的數據包:",data_2.decode("utf-8"))
data_3 = client.recv(9)
print("這是第三次的數據包:",data_3.decode("utf-8"))

 

  接收結果:

# ==== 執行結果 ====
​
"""
這是第一次的數據包: hello,
這是第二次的數據包: world,
這是第三次的數據包: yunyaGG!!
"""

 

  粘包被我們手動的計算字節數來精確的分割數據接受量的大小給解決了,但是這樣做是不現實的..我們不可能知道對方發送的數據到底是怎麼樣的,更不用說手動計算。所以有沒有更好的解決方案呢?

 

解決方案1:預先發送消息長度

  好了,其實上面關於解決粘包的思路已經出來了。我們需要做的就是讓接收方知道本次發送內容的大小,接收方才能夠精確的將所有數據全部提取出來不產生遺漏。其實實現方式很簡單,可以嘗試以下思路:

 

  1.發送方發送一個此次數據固定的長度

  2.接收方接收到該數據長度並且回應

  3.發送方收到回應並且發送真正的數據

  4.接收方不斷的用默認的buffer_size值接收新的數據並存儲起來直到超出整個數據的長度,代表此處數據全部接收完畢

 

  Server端:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# ==== 基於TCP協議的socket實現遠程命令輸入之Server ====

import subprocess
from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("0.0.0.0", 6666))  # 放在遠程填入0.0.0.0 放在本地測試填入127.0.0.1
server.listen(5)

while 1:  # 鏈接循環
    conn, client_addr = server.accept()
    while 1:  # 通信循環
        try:  # 防止Windows平台下Client端異常關閉導致雙向鏈接崩塌Server端異常的情況發生
            cmd = conn.recv(1024)
            if not cmd:  # 防止類Unix平台下Client端異常關閉導致雙向鏈接崩塌Server端異常的情況發生
                break
            res = subprocess.Popen(cmd.decode("utf-8"),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE, )

            stdout_res = res.stdout.read()  # 正確結果
            stderr_res = res.stderr.read()  # 錯誤結果
            # subprocess模塊拿到的是bytes類型,所以直接發送即可

            cmd_res = stdout_res if stdout_res else stderr_res  # 因為兩個結果只有一個有信息,所以我們只拿到有結果的那個
            msg_length = len(cmd_res)  # 本次數據的長度
            conn.send(str(msg_length).encode("utf-8"))  # 先將要發的整體內容長度發送過去
            if conn.recv(1024) == b"ready":  # 如果接收方回應了ready則開始發送真正的數據體
                conn.send(cmd_res)

        except Exception:
            break

    conn.close()  # 由於client端鏈接異常,故關閉鏈接循環

 

  Client端:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# ==== 基於TCP協議的socket實現遠程命令輸入之Client ====

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(("xxx.xxx.xxx.xxx", 6666))  # 填入Server端公網IP

while 1:
    cmd = input("請輸入命令>>>:").strip()
    if not cmd:
        continue
    if cmd == "quit":
        break
    client.send(cmd.encode("utf-8"))

    msg_length = int(client.recv(1024).decode("utf-8"))  # 接收到此次發送內容的整體長度
    recv_length = 0  # 代表已接收的內容長度
    cmd_res = b""

    client.send(b"ready")  # 發送給Server端,代表自己已經接收到此次內容長度,可以發送真正的數據啦

    while recv_length < msg_length:
        cmd_res += client.recv(1024)  # 本次接收1024字節數據,可能是一小節數據
        recv_length += len(cmd_res)  # 添加上本次讀取的長度,當全部讀取完后應該 recv_length == msg_length

    else:
        print(cmd_res.decode("utf-8"))  # 如果Server端是Windows則用gbk解碼,類Unix用utf-8解碼

client.close()

 

  結果如下:

 

解決方案2:json+struct方案

  其實上面的解決方案還是有一些弊端,因為Server端是發送了2次send(),第1次發送數據整體長度,第2次發送數據內容主體,這樣其實是不太好的(Server端可能同時處理多個鏈接,所以send()次數越少越好),而且如果Server端傳的是一個文件的話那麼局限性就太強了。因為我們只能將整體的消息長度發送過去而諸如文件名,文件大小之內的信息就發送不過去。

  所以我們需要一個更加完美的解決方案,即Server端發送一次send()就將本次的數據整體長度發送過去(還可以包括文件姓名,文件大小等信息。)

 

  struct模塊使用介紹

 

  struct模塊可以將其某一種數據格式序列化為固定長度的Bytes類型,其中最重要的兩個方法就是pack()、unpack()。

 

  pack(fmt,*args): 根據格式將其轉換為Bytes類型

  unpack(fmt,string):根據格式將Bytes類型數據反解為其原本的形式

 

格式 C語言類型 Python類型 字節數大小
x 填充字節 沒有值  
c char 字節長度為1 1
b signed char 整數 1
B unsigned char 整數 1
? _Bool bool 1
h short 整數 2
H unsigned short 整數 2
i int 整數 4
I unsigned int 整數 4
l long 整數 4
L unsigned long 整數 4
q long long 整數 8
Q unsigned long long 整數 8
n ssize_t 整數  
N size_t 整數  
f float 浮點數 4
d double 浮點數 8
s char[] 字節  
p char[] 字節  
P void * 整數  

 

  使用演示:

>>> import struct
>>> b1 = struct.pack("i",12)  # 嘗試將 int類型的12進行序列化,得到一個4字節的對象
>>> b1
b'\x0c\x00\x00\x00'
>>> struct.unpack("i",b1)  # 嘗試將12的序列化對象字節進行反解,得出元組,第1位就是需要的數據。
(12,)
>>>

 

  好了,了解到這裏我們就可以開始進行改寫了。

  Server端代碼如下:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# ==== 基於TCP協議的socket實現遠程命令輸入之Server ====

import json
import struct
import subprocess
from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("0.0.0.0", 6666))  # 放在遠程填入0.0.0.0 放在本地測試填入127.0.0.1
server.listen(5)

while 1:  # 鏈接循環
    conn, client_addr = server.accept()
    while 1:  # 通信循環
        try:  # 防止Windows平台下Client端異常關閉導致雙向鏈接崩塌Server端異常的情況發生
            cmd = conn.recv(1024)
            if not cmd:  # 防止類Unix平台下Client端異常關閉導致雙向鏈接崩塌Server端異常的情況發生
                break
            res = subprocess.Popen(cmd.decode("utf-8"),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE, )

            stdout_res = res.stdout.read()  # 正確結果
            stderr_res = res.stderr.read()  # 錯誤結果
            # subprocess模塊拿到的是bytes類型,所以直接發送即可

            cmd_res = stdout_res if stdout_res else stderr_res  # 因為兩個結果只有一個有信息,所以我們只拿到有結果的那個

            # 解決粘包:構建字典,包含數據主體長度,這個就相當於其頭部信息
            head_msg = {
                "msg_length": len(cmd_res), # 包含數據主體部分的長度
                # 如果是文件,還可以添加file_name,file_size等屬性。
            }

            # 序列化成json格式,並且統計其頭部的長度
            head_data = json.dumps(head_msg).encode("utf-8")
            head_length = struct.pack("i", len(head_data))  # 得到4字節的頭部信息,裡面包含頭部的長度

            # 發送頭部長度信息,頭部數據,與真實數據部分
            conn.send(head_length + head_data + cmd_res)

        except Exception:
            break

    conn.close()  # 由於client端鏈接異常,故關閉鏈接循環

 

  Client端代碼如下:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# ==== 基於TCP協議的socket實現遠程命令輸入之Client ====

import json
import struct
from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(("xxx.xxx.xxx.xxx", 6666))  # 填入Server端公網IP

while 1:
    cmd = input("請輸入命令>>>:").strip()
    if not cmd:
        continue
    if cmd == "quit":
        break
    client.send(cmd.encode("utf-8"))  # 發送終端命令

    # 解決粘包
    head_length = struct.unpack("i", client.recv(4))[0]  # 接收到頭部的長度信息
    head_data = json.loads(client.recv(head_length))  # 接收到真實的頭部信息

    msg_length = head_data["msg_length"]  # 獲取到數據主體的長度信息
    recv_length = 0  # 代表已接收的內容長度
    cmd_res = b""

    # 開始獲取真正的數據主體信息
    while recv_length < msg_length:
        cmd_res += client.recv(1024)  # 本次接收1024字節數據,可能是一小節數據
        recv_length += len(cmd_res)  # 添加上本次讀取的長度,當全部讀取完后應該 recv_length == msg_length

    else:
        print(cmd_res.decode("utf-8"))  # 如果Server端是Windows則用gbk解碼,類Unix用utf-8解碼


client.close()

 

  思想如下:

    1.Server端構建自身的數據頭部分,其中包含數據體整體長度,如果傳輸的是文件的話還可以包含文件名,文件大小等信息

    2.將數據頭部分json序列化后再轉換為Bytes類型

    3.使用struct.pack()模塊獲取數據頭的長度,得到一個長度為4的Bytes類型

    4.Server端將 數據頭長度 + 數據頭部分 + 數據體部分 全部發送給Client端

    5. Client端recv()接收值改為4,拿到數據頭長度Bytes類型

    6. Client端使用struct.unpack(數據頭長度Bytes類型)模塊反解出數據頭真實的長度

    7. Client端使用recv()接收值為數據頭真實的長度拿到真正的數據頭

    8. 通過json反序列化出真正的數據頭,在到其中取出數據體的長度

    9. 開始while循環不斷的讀取真實的數據體數據

 

 

解決方案3:iter()與偏函數(失敗案例)

 

  上面那麼做看似完美但還是美中不足。因為內存緩衝區本來就是只能取一次值,和迭代器很像,只能迭代一次便不能繼續迭代了。基於這一點我們來做一個終極優化:

  還記得iter()方法嗎?iter()方法除開創建迭代器外實際上還有一個參數:

 

def iter(source, sentinel=None):  # known special case of iter
    """
    iter(iterable) -> iterator
    iter(callable, sentinel) -> iterator

    Get an iterator from an object.  In the first form, the argument must
    supply its own iterator, or be a sequence.
    In the second form, the callable is called until it returns the sentinel.
    """
    pass

 

  我們來試試這個參數做什麼用的。

li = [1, 2, 3, 4]

def my_iter():
    return li.pop()

res = iter(my_iter, 2)  # 代表這個迭代器沒__next__一下就會執行my_iter函數,並且該函數返回值如果是2則終止迭代
print(res.__next__())  # 4
print(res.__next__())  # 3
print(res.__next__())  # StopIteration

 

  第二個參數看來可以設置迭代的終點。

 

  那麼偏函數是什麼呢?偏函數可以設定一個固定的參數給第一個位置的值

  效果如下:

from functools import partial  # 導入偏函數

def add(x, y):
    return x + y

func = partial(add, 1)  # 設置辨寒暑綁定的第一個參數的值
print(func(1))  # 2
print(func(5))  # 6

 

  現在我們仔細回想,當緩衝區的消息接收完畢後為空的狀態是會變成 b""的形式。那麼這個時候我們可以使用iter()方法設置為不斷的取出緩存中的值直到出現b"",而偏函數可以對recv()函數進行設置讓它始終取一個值,最後通過join來拼接出取出的所有值即可。

  可以使用 "".join(iter(partial(tcp_clien.recv,back_log)),b"")

 

  我們嘗試用函數來查看一下效果:

from functools import partial  # 導入偏函數

li = [b"","1","2","3","4","5"]  # 模擬內核緩衝區

def test(buffer_size):
    if buffer_size:  # 模擬recv的數據大小
        return li.pop()
    print("buffer_size必須為一個int類型的值")

res = "".join(iter(partial(test,1024),b""))
print(res)  # 54321

# join()方法會不斷的調用iter()下的__next__,每調用一次就執行一次偏函數。知道出現b""停止

 

  最後我們發現,這樣的做法是會產生recv()阻塞的,總體來說還是不能夠成功。因為join()方法會不斷的執行,即使內核緩衝區的數據被recv()讀完了也不會終止迭代而是繼續阻塞下次的recv(),故這種方式宣告失敗。(還是iter()的第二個參數導致的,或許讀取完后內核緩衝區中的數據並不是b"")

 

  測試的Server端代碼如下:

from socket import *
import subprocess
import struct
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024

tcp_server=socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

while True:
    conn,addr=tcp_server.accept()
    print('新的Client鏈接',addr)
    while True:
        #收
        try:
            cmd=conn.recv(buffer_size)
            if not cmd:break
            print('收到Client的命令',cmd)

            #執行命令,得到命令的運行結果cmd_res
            res=subprocess.Popen(cmd.decode('utf-8'),shell=True,
                                 stderr=subprocess.PIPE,
                                 stdout=subprocess.PIPE,
                                 stdin=subprocess.PIPE)
            err=res.stderr.read()
            if err:
                cmd_res=err
            else:
                cmd_res=res.stdout.read()

            #發
            if not cmd_res:
                cmd_res='執行成功'.encode('gbk')

            length=len(cmd_res)

            data_length=struct.pack('i',length)
            conn.send(data_length)
            conn.send(cmd_res)
        except Exception as e:
            print(e)
            break

 

  測試的Client代碼如下:

from socket import *
import struct
from functools import partial   #偏函數
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024

tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)

while True:
    cmd=input('>>: ').strip()
    if not cmd:continue
    if cmd == 'quit':break

    tcp_client.send(cmd.encode('utf-8'))


    #解決粘包
    length_data=tcp_client.recv(4)
    length=struct.unpack('i',length_data)[0]
   
  #第一種方法
    recv_size=0
    recv_msg=b''
    while recv_size < length:
        #為何recv里是buffer_size,不是length,因為length如果為24G,系統內存沒有那麼大
        #所以每次buffer_size,當recv_size < length時,循環接收,直到recv_size =length,退出循環
        recv_msg += tcp_client.recv(buffer_size)
        recv_size=len(recv_msg) #1024

    #第二種方法 失敗版本,會引發recv()的阻塞,而不會終止迭代。因為join()方法會不斷的調用其iter()方法產生的迭代器,也就是調用其__next__方法,所以第二次沒消息的recv()會阻塞住。
    #recv_msg=''.join(iter(partial(tcp_client.recv, buffer_size), b''))
    print('命令的執行結果是 ',recv_msg.decode('gbk'))
tcp_client.close()

 

UDP協議為何不會產生粘包

 

  UDP協議是面向消息的協議,每一次的sendto()與recvfrom()必須一一對應,否則就會收不到消息。

 

  UDP是面向消息的協議,每個UDP段都是一條消息,每sendto()一次就是發送一次消息,而不管接收方有沒有收到消息發送方只管自己的發送任務,這也是UDP被稱為不可靠傳輸協議的由來。接收端的套接字緩衝區採用了鏈式的結構來記錄每一個到達的UDP包,在每一個UDP包中都有了消息頭,包括端口,消息源等等..於是UDP就能夠去區分出一個明確的消息定義,即面向消息的通信是有消息邊界的,所以UDP的傳輸叫做數據報的形式。

 

  並且每一次recvform()的buffer_size最大值如果不夠獲取完全部的內核緩衝區里的數據的話,那麼只會收夠指定的最大字節數量(即buffer_size的設定值),剩餘的就不要了。所以UDP不會存在粘包,多麼乾脆利落…

 

  我們還是用一個快遞員的那個圖來進行演示:

  還有一點需要注意一下。使用UDP協議進行通信的時候不管首先啟動哪一方都不會報錯,因為它只管發,不管有沒有人接收。

  所以,這也是我稱UDP協議比較隨便的原因。

 

  那麼隨便有沒有什麼好處呢?有的,速度快。不用建立雙向鏈接通道,但是其代價就是數據可靠性與安全性的問題,效率和安全從來都是相對的,這個也只能在從中做取捨。

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

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

Salesforce LWC學習(十八) datatable展示 image

本篇參看:

https://developer.salesforce.com/docs/component-library/bundle/lightning-datatable/documentation

https://www.lightningdesignsystem.com/components/data-tables/

我們在salesforce中,經常會使用 listview,標準的 lightning listview會有排序,filter展示chart等等標準功能。當然,某些情況標準搞定不了情況下,我們便需要使用自定義的開發。自定義開發我們就可以使用 lightning-datatable組件。一個 lightning-datatable組件用來展示表格數據,它可以根據數據類型展示每一列。比如一個 email類型的字段會展示一個點開以後是一個 mail:to的超鏈接。默認展示的類型是text類型。那 lightning-datatable有哪些特性呢?

  • 展示和格式化焗油適當類型的列
  • 具有無限滾動行功能
  • 指定的數據類型允許 inline編輯
  • 可以定義header的action
  • 可以定義行級別的action
  • 重置每一列展示的大小
  • 選擇行操作
  • 指定的列通過升序或者降序排列
  • 文字換行和剪切
  • 對行進行編號
  • 設置單元格對齊方式

lightning-datatable有很多屬性,其他的請自行查看文檔,這裏只例舉幾個核心屬性或者方法。比較核心的如下:

  • columns:一組列的object用來定義數據類型,這個object有很多的屬性,其中有幾個必填的配置: label & fieldName & type。分別對應着字段的展示名稱,字段的API name以及字段的類型,不同的字段類型將會自動的轉換成不同的展示形式,默認是text。其他的屬性我們後期會有簡單介紹。
  • data:一組數據用來展示,通常 comulns設置表單的頭以及每個單元列的類型,data設置內容;
  • hide-checkbox-column:標籤用來設定是否展示左側的checkbox,值為true/false,true即隱藏checkbox;
  • selected-rows:用來初始化時展示哪些行是被選中的;
  • key-field:用來指定每行的unique id;
  • getSelectedRows:這個是lightning-datatable提供的方法,用來獲取哪些行用戶選中的。

簡單介紹完畢,下面通過幾點來慢慢帶入datatable的功能。

一. 基礎 Datatable展示數據

demo中展示,我們來看官方提供的一個demo。

dataTableExample1.html:用來展示一個 datatable,我們看到屬性中的data / columns / key-field都是上面描述過的, onrowselection方法為當有行選擇的時候的調用

<template>
    <lightning-datatable
            data={data}
            columns={columns}
            key-field="id"
            onrowselection={getSelectedName}>
    </lightning-datatable>
</template>

 dataTableExample1.js:有兩種方式可以獲取選擇的行信息,demo中有兩個。datatable有一個標準事件rowselection,調用以後可以通過event.detail.

selectedRows獲取選中的行的信息,另外一種就是找到datatable然後調用其getSelectedRows方法。

除此之外,我們看到columns的設置方式,除了 label / fieldName / type這三個基礎信息以外,還可以看到有 typeAttributes 以及 cellAttributes兩種設置。

  • typeAttributes:用於對指定的列的類型格式化dataCell,不同的type可能擁有不同的設置。salesforce針對不同的type可以設置不同的 typeAttributes,這裏不再詳細展開,具體查看文檔;
  • cellAttributes:typeAttributes用於設置針對 type的格式化,cellAttributes通常用於列中附加圖標信息,設置圖標位置等信息。
import { LightningElement } from 'lwc';

const columns = [
     {label: 'Opportunity name', fieldName: 'opportunityName', type: 'text'},
     {label: 'Confidence', fieldName: 'confidence', type: 'percent', cellAttributes:{ iconName: { fieldName: 'trendIcon' }, iconPosition: 'right' }},
     {label: 'Amount', fieldName: 'amount', type: 'currency', typeAttributes: { currencyCode: 'EUR'}},
     {label: 'Contact Email', fieldName: 'contact', type: 'email'},
     {label: 'Contact Phone', fieldName: 'phone', type: 'phone'},
];

const data = [{
                    id: 'a',
                    opportunityName: 'Cloudhub',
                    confidence: 0.2,
                    amount: 25000,
                    contact: 'jrogers@cloudhub.com',
                    phone: '2352235235',
                    trendIcon: 'utility:down'
                },
                {
                    id: 'b',
                    opportunityName: 'Quip',
                    confidence: 0.78,
                    amount: 740000,
                    contact: 'quipy@quip.com',
                    phone: '2352235235',
                    trendIcon: 'utility:up'
                }];

export default class DatatableExample1 extends LightningElement {
    data = data;
    columns = columns;

    getSelectedName(event) {
        //兩種方式獲取選中的行,第一種是使用 event.detail.selectedRows,另外一種是使用querySelector找到 lightning-datatable,然後使用datatable封裝的方法
        //const selectedRows = event.detail.selectedRows;
        const dataTable = this.template.querySelector('lightning-datatable');
        const selectedRows = dataTable.getSelectedRows();
        // Display that fieldName of the selected rows
        for (let i = 0; i < selectedRows.length; i++){
            console.log("You selected: " + selectedRows[i].opportunityName);
        }
    }
}

 簡單展示效果如下:

 二. datatable中展示父表中的字段值

上個demo中簡單介紹了datatable的使用,數據全是寫死的demo,接下來通過一個包含後台的邏輯進行datatable展示數據。

DataTableExampleController:用於獲取 Case表中的相關字段以及其父表的字段

public with sharing class DataTableExampleController {
    @AuraEnabled(cacheable=true)
    public static List<Case> getCaseList(){
        List<Case> caseList = [SELECT Id,CaseNumber,Account.Name,
                                    Priority,Status
                                FROM Case
                                LIMIT 100];
        return caseList;
    }
}

datatableExample2.js:用於展示相關的字段,通過wire adapter獲取後台數據放在data中

import { LightningElement, track, wire } from 'lwc';
import getCaseList from '@salesforce/apex/DataTableExampleController.getCaseList';
const COLUMNS = [
    {label: 'Case Number', fieldName: 'CaseNumber', type: 'text'},
    {label: 'Account Name', fieldName: 'Account.Name', type: 'text'},
    {label: 'Priority', fieldName: 'Priority', type: 'text'},
    {label: 'Status', fieldName: 'Status', type: 'text'}
];
export default class DataTableExample2 extends LightningElement {
    columns = COLUMNS;
    @track datas;

    @wire(getCaseList)
    wiredCaseList({ error, data }) {
        if(data) {
            this.datas = data;
        } else if(error) {
            //TODO
            console.log(JSON.stringify(error));
        }
    }
}

dataTableExample.html:展示datatable

<template>
    <lightning-datatable
            data={datas}
            columns={columns}
            key-field="id"
            >
    </lightning-datatable>
</template>

結果展示:通過下圖我們可以看到,其他的字段可以正常展示,但是父表的數據獲取確實失敗的。

 來剖析一下原因,datatable針對columns的fieldName只支持一級結構的獲取,針對獲取父的方式沒法通過 各種點的方式獲取到,那麼如何獲取呢,查看下面的方式,我們對js的代碼進行一下改動。按照下面的步驟兩步走的修改。

 結果展示:我們可以看到 Account Name列的值也順利的展示出來了,按照這種操作做一個拓展,其實我們也可以去做一些字段間的邏輯操作拼出一個符合我們邏輯的列,不僅僅局限於獲取父的對象值這種簡單操作。

 三. 實現datatable展示 formula類型是圖片的功能

自己看文檔的小夥伴可能發現datatable的columns的type沒有類似 image類型,但是在object field創建時我們很容易創建一些IMAGE的字段,那樣的話使用標準的datatable便無法展示,那要如何操作呢?還好lwc提供了一個自定義類型的操作。首先我們在Case表裡面新增一個formula 字段,展示圖片。

 接下來一步接着一步搞定。

dataTableWithImage.js:這裏需要繼承 LightningDatatable,並且引入 imageRow.html,其用於做template

import LightningDatatable from 'lightning/datatable';
import imageRow from './imageRow.html';
export default class DataTableWithImage extends LightningDatatable {
    static customTypes = {
        image: {
            template: imageRow
        }
    };
}

imageRow.html:默認要求 imageRow需要和 dataTableWithImage在同一個目錄下,如果不在一個目錄,需要改上面的文件目錄信息。

<template>
    <c-data-table-image-template
        url={value}
        >
    </c-data-table-image-template>
</template>

dataTableImageTemplate.html:用來通過 img標籤展示圖片

<template>
    <img src={url} class="image"/>
</template>

dataTableImageTemplate.css:用來設置圖片大小的初始值,這種是一種偷懶的方式,正常應該通過 attribute注入進來指定大小的寬和高。

.image {
  height: 30px;
  width: 30px;
}

dataTableImageTemplate.js

import { LightningElement,api } from 'lwc';

export default class DataTableImageTemplate extends LightningElement {
    @api url;
}

OK,通過上面的這些步驟我們實現和繼承了自定義的datatable組件。接下來是調用操作。我們在class裏面搜索一下Priority_Image__c這個字段,apexClass這裏不再寫。

dataTableExample3.js:這裡有幾個關鍵點。

  • 設置 type為 image類型,image類型是我們上文自定義創建的
  • 對 formula類型進行解析,找到他的src部分。比如針對<img src=”/resource/priority/priority/medium.png” alt=”Priority Flag” border=”0″/>這個字符串,我們需要的是/resource/priority/priority/medium.png這個字符串
import { LightningElement, track, wire } from 'lwc';
import getCaseList from '@salesforce/apex/DataTableExampleController.getCaseList';

const COLUMNS = [
    {label: 'Case Number', fieldName: 'CaseNumber', type: 'text'},
    {label: 'Account Name', fieldName: 'AccountName', type: 'text'},
    {label: 'Priority', fieldName: 'Priority', type: 'text'},
    { label: 'Priority Image',fieldName:'Priority_Image__c',type: 'image' },
    {label: 'Status', fieldName: 'Status', type: 'text'}
];
export default class DataTableExample3 extends LightningElement {
    columns = COLUMNS;
    @track datas;

    @wire(getCaseList)
    wiredCaseList({ error, data }) {
        if(data) {
            //this.datas = data;
            let resultList = [];
            let objectTemp;
            data.forEach(item => {
                objectTemp = Object.assign({},item);
                if(item.Account) {
                    objectTemp.AccountName = item.Account.Name;
                }

                const srcIndex = objectTemp.Priority_Image__c.indexOf('src=');
                if(srcIndex !== -1) {
                    const imgSrcSubstring = objectTemp.Priority_Image__c.substring(srcIndex + 5);
                    objectTemp.Priority_Image__c = imgSrcSubstring.substring(0,imgSrcSubstring.indexOf('"'))
                }
                console.log(JSON.stringify(objectTemp));
                resultList.push(objectTemp);
            });
            this.datas = resultList;
        } else if(error) {
            //TODO
            console.log(JSON.stringify(error));
        }
    }
}

dataTableExample3.html

<template>
    <c-data-table-with-image
                    key-field="id"
                    data={datas}
                    columns={columns}
                    >
                </c-data-table-with-image>
</template>

結果展示:針對不同的Priority展示其對應的formula的圖片。

總結:篇中講了datatable的簡單的應用以及相應的擴展,datatable很強悍,這裏不逐點講述,感興趣的可以自行查看 inline edit,action等操作。篇中有錯誤地方歡迎指出,有不懂的歡迎留言。

後記:工作原因這篇博客準備了好久才成型,不知不覺學了半年的lwc的開發,也整理總結了一些博客供自己和他人學習。前半年的主旋律大部分時間都在學習lwc,後半年的時間大部分會放在考證和學習其他技術,不出意外應該是今年的最後一篇lwc的博文。有其他lwc的問題歡迎留言探討,希望各位salesforce開發者可以養成獨立思考的能力,先思考,在查,最後再問。加油!

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

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

Jmeter(十三) – 從入門到精通 – JMeter定時器 – 上篇(詳解教程)

1.簡介

  用戶實際操作時,並非是連續點擊,而是存在很多停頓的情況,例如:用戶需要時間閱讀文字內容、填表、或者查找正確的鏈接等。為了模擬用戶實際情況,在性能測試中我們需要考慮思考時間。若不認真考慮思考時間很可能會導致測試結果的失真。例如,估計的可支撐用戶數偏小。在性能測試中,訪問請求之間的停頓時間被稱之為思考時間,那麼如何模擬這種停頓呢?我們可以藉助JMeter的定時器實現。

  JMeter中的定時器一般被我們用來設置延遲與同步。定時器的執行優先級高於Sampler(取樣器),在同一作用域(例如控制器下)下有多個定時器存在時,每一個定時器都會執行,如果想讓某一定時器僅對某一Sampler有效,則可以把定時器加在此Sampler節點下。

2.預覽定時器

首先我們來看一下JMeter的定時器,路徑:線程組(用戶)->添加->定時器(Timer);我們可以清楚地看到JMeter5中共有9個定時器,如下圖所示:

如果上圖您看得不是很清楚的話,宏哥總結了一個思維導圖,關於JMeter5的邏輯控制器類型,如下圖所示: 

 通過以上的了解,我們對定時器有了一個大致的了解和認識。下面宏哥就給小夥伴或則童鞋們分享講解一些通常在工作中會用到的定時器。 

4.常用定時器詳解

這一小節,宏哥就由上而下地詳細地講解一下常用的定時器。

4.1Constant Timer

固定定時器,看名稱大家也知道是一個固定定時器,多用來模擬思考時間,顧名思義是:請求之間的間隔時間為固定值。

作用:通過ThreadDelay設定每個線程請求之前的等待時間(單位為毫秒)。注意:固定定時是有作用域的,放到線程組下其作用域是所有請求都會延遲固定器設置的時間,如果放到請求內,作用域是單個請求延遲時間(常用)。

1、我們先來看看這個Constant Timer長得是啥樣子,路徑:線程組 > 添加 > 定時器 > 固定定時器,如下圖所示: 

2、關鍵參數說明如下:

Name:名稱,可以隨意設置,甚至為空;

Comments:註釋,可隨意設置,可以為空;

Thread Delay(in milliseconds):線程等待時間,單位毫秒。

用法(場景),更真實的模擬用戶場景,需要設置等待時間,或是等待上一個請求的時間,才執行,給sampler之間的思考時間;

4.1.1實例

場景應用:性能測試中,根據用戶操作預估時間,或者需要等待一段時間來加載數據。
PS:在實際模擬用戶請求的過程中,會失去靈活性,不推薦大量使用

1、新建測試計劃,線程組下添加2個取樣器 訪問博客園首頁、訪問度娘,如下圖所示:

2、然後再添加固定定時器,設置延遲時間3000ms,即3s,如下圖所示:

3、配置好以後,點擊“保存”,運行JMeter,查看錶格結果(取樣器訪問博客園首頁和訪問度娘間隔3s),如下圖所示:

4.2Uniform Random Timer

統一(均勻)隨機定時器,也是讓線程暫停一個隨機時間,只不過力求隨機時間能夠更均勻,都會出現。均勻隨機定時器,顧名思義,它產生的延遲時間是個隨機值,而各隨機值出現的概率均等。總的延遲時間等於一個隨機延遲時間加上一個固定延遲時間,用戶可以設置隨機延遲時間和固定延遲時間。

作用:它產生的延遲時間是個隨機值,而各隨機值出現的概率均等。總的延遲時間等於一個隨機延遲時間加上一個固定延遲時間,用戶可以設置隨機延遲時間和固定延遲時間。每個線程的延遲時間是符合標準正態分佈的隨機時間停頓,那麼使用這個定時器,總延遲 = 高斯分佈值(平均0.0和標準偏差1.0)* 指定的偏差值+固定延遲偏移(Math.abs((this.random.nextGaussian() * 偏差值) + 固定延遲偏移))

總延遲時間 = 指定範圍內的隨機時間(在範圍內各隨機值等概率)+ 固定延遲時間

1、我們先來看看這Uniform Random Timer長得是啥樣子,路徑:線程組 > 添加 > 定時器 > 統一隨機定時器,如下圖所示: 

2、關鍵參數說明如下:

Name:名稱,可以隨意設置,甚至為空;

Comments:註釋,可隨意設置,可以為空;

Random Delay Maximum:最大隨機延遲時間;

Constant Delay Offset: 固定延遲時間。

4.2.1實例

1、新建測試計劃,線程組下添加2個取樣器 訪問博客園首頁、訪問度娘,如下圖所示:

2、然後再添加統一隨機定時器,設置延遲時間3s,如下圖所示:

3、配置好以後,點擊“保存”,運行JMeter,查看錶格結果(取樣器訪問博客園首頁和訪問度娘間隔4s = 1000ms + 3000ms),如下圖所示:

4.3Precise Throughput Timer

準確的吞吐量定時器,顧名思義,這個就是控制吞吐量的。和Constant Throughput Timer類似,但是能更精準的控制請求。區別就是Constant Throughput Timer根據時間來做定時器(到了多少秒就發請求);Precise Throughput Timer是根據吞吐量在做計時器(到了多少量就發請求)。也就是能做到控制請求的速度和個數。

1、我們先來看看這個Precise Throughput Timer長得是啥樣子,路徑:線程組 > 添加 > 定時器 > 準確的吞吐量定時器,如下圖所示: 

2、關鍵參數說明如下:

Name:名稱,可以隨意設置,甚至為空;

Comments:註釋,可隨意設置,可以為空;

Thread Delay:忽略子控制器,即子控制器失效,由交替控制器接管。

Target Throught:目標吞吐量

Throught Period:表示在多長時間內發送Target Throught指定的請求數(以秒為單位)

Test Druation:指定測試運行時間(以秒為單位)

Number of threads in the bath:用來設置集合點,等到指定個數的請求后併發執行

4.3.1實例

1、新建測試計劃,線程組(設置線程組,保證有足夠的時間)下添加2個取樣器 訪問博客園首頁(已禁用)、訪問度娘,如下圖所示:

2、然後再添加準確的吞吐量定時器,設置10個吞吐量,設置10s啟動完10個請求,設置運行時間20s,如下圖所示:

3、配置好以後,點擊“保存”,運行JMeter,查看錶格結果(大約用了20秒啟動了21個線程),如下圖所示:

4、設置集合點在Precise Throughput Timer中設置集合點為10,其它參數不變,如下圖所示:

5、在Thread Group中設置線程數為10,如下圖所示: 

6、配置好以後,點擊“保存”,運行JMeter,查看錶格結果(可以看到,每10個線程為1組,同時啟動。),如下圖所示: 

4.4Constant Throughput Timer

固定吞吐量定時器,這個定時器引入了變量暫停,通過計算使總吞吐量(以每分鐘去楊樹計)盡可能接近給定的数字。當然,如果服務器不能處理它,或者如果其他定時器或耗時的測試原件阻止它,那麼吞吐量將更低。
雖然計時器被稱為常數吞吐量定時器,但吞吐量值並不一定是常數。它可以根據變量或函數調用定義,並且可以在測試期間改變該值。通過以下多種方式都可以改變:
使用計數器變量
使用一個 __jexl3, __groovy 函數來提供一個變化的值
使用遠程BeeShell服務器更改Jmeter屬性
請注意,在測試期間,不應該頻繁地更改吞吐量值——新值生效需要一段時間。

常數吞吐量定時器作用:控制吞吐量(線上壓測時候,避免一下就上百上千的吞吐量影響線上性能,加上這個之後較安全,可以一點一點往上加); 按指定的吞吐量執行,以每分鐘為單位。計算吞吐量依據是最後一次線程的執行時延。

作用域:此定時器放在請求的下級,只對它的上級請求起作用

1、我們先來看看這個Constant Throughput Timer長得是啥樣子,路徑:線程組 > 添加 > 定時器 > 常數吞吐量定時器,如下圖所示: 

2、關鍵參數說明如下:

Name:名稱,可以隨意設置,甚至為空;

Comments:註釋,可隨意設置,可以為空;

Target throughput(in samples per minute):目標吞吐量。注意這裡是每分鐘發送的請求數,可以選擇作用的線程:當前線程、當前線程組、所有線程組等,具體含義如下:

this thread only: 設置每個線程的吞吐量。總的吞吐量=線程數*該值。

all active threads in current thread group:吞吐量被分攤到當前線程組所有的活動線程上。每個線程將根據上次運行時間延遲。

all active threads:吞吐量被分配到所有線程組的所有活動線程的總吞吐量。每個線程將根據上次運行時間延遲。在這種情況下,每個線程組需要一個具有相同設置的固定吞吐量定時器。(不常用)

all active threads in current thread group (shared):同上,但是每個線程是根據組中的線程的上一次運行時間來延遲。相當於線程組組內排隊。(不常用)

all active threads (shared):同上,但每個線程是根據線程的上次運行時間來延遲。相當於讓所有線程組整體排隊。(不常用)

 4.4.1實例

1、新建測試計劃,線程組下添加1個取樣器 訪問博客園首頁(已禁用)、訪問度娘,如下圖所示:

2、然後再添加常數吞吐量定時器,設置目標吞吐量為300,如下圖所示:

3、配置好以後,點擊“保存”,運行JMeter,查看jp@gc – Transactions per Second(常數吞吐量定時器設置300/分鐘,也就是5/秒,故tps最大5,這裏的tps大約都是5,說明已經超過5,可以往上增加了),如下圖所示:

5. 定時器的作用域

1. 定時器是在每個sampler(採樣器)之前執行的,而不是之後(無論定時器位置在sampler之前還是下面);
2. 當執行一個sampler之前時,所有當前作用域內的定時器都會被執行;
3. 如果希望定時器僅應用於其中一個sampler,則把定時器作為子節點加入;
4. 如果希望在sampler執行完之後再等待,則可以使用Test Action;

6.小結

6.1安裝插件管理

1、安裝前查看選項,沒有看到插件管理,如下圖所示:

2、想安裝一個jmeter的插件,到官網(http://jmeter-plugins.org)上去下載插件安裝包,但是頁面一直都是搜索狀態,如下圖所示:

3、然後宏哥找了一個下載一個jmeter的插件管理工具 地址: http://jmeter-plugins.org/get/

4、將下載的文件拷貝的你的JMeter根目錄下的 lib/ext 目錄,如下圖所示:

5、 重啟jmeter,在選項中可以看到插件管理工具已經安裝成功,如下圖所示:

 6、勾選要下載的插件,點擊Apply changes and restart JMeter按鈕就完成了

Installed Plugins:用於查看已安裝的插件,並可通過 取消勾選 – 應用操作 來卸載插件

Available Plugins:用於查看和安裝可用的插件,通過 勾選-應用操作(右下側有按鈕Apply changes and restart JMeter) 來安裝插件

Upgrades:用於升級插件

   好了,今天關於定時器的上篇就講解到這裏,這一篇主要介紹了 Constant Timer 、Uniform Random Timer、Precise Throughput Timer和 Constant Throughput Timer。感謝你耐心的閱讀和學習。

 

您的肯定就是我進步的動力。如果你感覺還不錯,就請鼓勵一下吧!記得隨手點波  推薦  不要忘記哦!!!

別忘了點 推薦 留下您來過的痕迹

 

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

【其他文章推薦】

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※新北清潔公司,居家、辦公、裝潢細清專業服務

Spring Cloud Alibaba系列(五)sentinel實現服務限流降級

一、sentinel是什麼

sentinel的官方名稱叫分佈式系統的流量防衛兵。Sentinel 以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。在Spring Cloud項目中最開始我們使用的是Hystrix,目前已停止更新了。現在Spring Cloud官方推薦的是rensilience4j。當然還有我們今天學習的sentinel。

Sentinel 具有以下特徵:

  • 豐富的應用場景:Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即突發流量控制在系統容量可以承受的範圍)、消息削峰填谷、集群流量控制、實時熔斷下游不可用應用等。
  • 完備的實時監控:Sentinel 同時提供實時的監控功能。您可以在控制台中看到接入應用的單台機器秒級數據,甚至 500 台以下規模的集群的匯總運 行情況。
  • 廣泛的開源生態:Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊,例如與 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入 Sentinel。
  • 完善的 SPI 擴展點:Sentinel 提供簡單易用、完善的 SPI 擴展接口。您可以通過實現擴展接口來快速地定製邏輯。例如定製規則管理、適配動態數據源等。

二、sentinel實現限流

2.1 安裝sentinel控制台

  • 下載地址:https://github.com/alibaba/Sentinel/releases

這裏我們直接下載jar包即可,下載后通過命令行啟動:

java -jar sentinel-dashboard-1.7.2.jar
  • 默認端口:8080
  • 默認用戶名:sentinel
  • 默認密碼:sentinel

啟動成功后,我們瀏覽器訪問http://localhost:8080,出現如下界面。

2.2 微服務繼承sentinel

  • 引入sentinel依賴
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
  • 添加sentinel的相關配置
server:
  port: 7003
spring:
  application:
    name: sentinel-provider
  cloud:
	nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080
  • 提供個接口用來測試限流
@SpringBootApplication
public class SentinelApplication {

    public static void main(String[] args) {
        SpringApplication.run(SentinelApplication.class, args);
    }
}

@RestController
class TestController{
    @GetMapping("/test")
    public String test(){
        return "hello! sentinel!";
    }
}

我們請求幾次這個接口后,打開sentinel控制台,就可以實時監控到這個sentinel-provider服務接口調用情況了。

2.3 配置限流規則

我們這裏做一個簡單的規則配置:

  • 閥值類型:QPS

  • 單機閥值:2

意思就是:該接口每秒最多允許進入兩個請求。

點擊新增后,在流控規則里發現了一條規則:

現在,我們繼續請求3次這個接口。第三次響應的內容如下:

Blocked by Sentinel (flow limiting)

我們打開控制台發現拒絕了一條請求。

三、Sentinel規則介紹

不管是限流還是降級,它都是按照某種規則進行的,下面具體介紹一下sentinel支持的幾種規則。

3.1 流控規則

流量控制,其原理是監控應用流量的QPS(每秒查詢率) 或併發線程數等指標,當達到指定的閾值時

對流量進行控制,以避免被瞬時的流量高峰衝垮,從而保障應用的高可用性。

資源名:唯一名稱,默認是請求路徑,可自定義

針對來源:指定對哪個微服務進行限流,默認指default,意思是不區分來源,全部限制

閾值類型/單機閾值:

  • QPS(每秒請求數量): 當調用該接口的QPS達到閾值的時候,進行限流

  • 線程數:當調用該接口的線程數達到閾值的時候,進行限流

3.2 降級規則

降級規則就是當滿足什麼條件時,對服務降級——即將請求轉發到另外接口上,這個接口與業務無關,只是為了保證系統的完整性。

  • RT(平均響應時間) :當資源的平均響應時間超過閾值(以 ms 為單位)之後,資源進入准降級狀態。如果接下來 1s 內持續進入 5 個請求,它們的 RT都持續超過這個閾值,那麼在接下的時間窗口(以 s 為單位)之內,就會對這個方法進行服務降級。

    注意 Sentinel 默認統計的 RT 上限是 4900 ms,超出此閾值的都會算作 4900 ms,若需要變更此上限可以通過啟動配置項 -Dcsp.sentinel.statistic.max.rt=xxx 來配置。

  • 異常比例:當資源的每秒異常總數占通過量的比值超過閾值之後,資源進入降級狀態,即在接下的時間窗口(以 s 為單位)之內,對這個方法的調用都會自動地返回。異常比率的閾值範圍是 [0.0,1.0]。

  • 異常數 :當資源近 1 分鐘的異常數目超過閾值之後會進行服務降級。注意由於統計時間窗口是分鐘級別的,若時間窗口小於 60s,則結束熔斷狀態后仍可能再進入熔斷狀態。

3.3 熱點規則

熱點規則允許將規則具體到參數上。

我們用個例子來看看效果。

  • 編寫接口
@GetMapping("/myTest")
@SentinelResource("test3")
public String test123(String name,String age){
    return  name + "----"+ age;
}
  • 添加規則
  • 運行效果

結果显示,第一個參數被限流了,而第二個參數正常。

3.4 系統規則

系統保護規則是從應用級別的入口流量進行控制,從單台機器的總體 Load、RT、入口 QPS 、CPU使用率和線程數五個維度監控應用數據,讓系統盡可能跑在最大吞吐量的同時保證系統整體的穩定性。

系統保護規則是應用整體維度的,而不是資源維度的,並且僅對入口流量 (進入應用的流量) 生效。

  • Load(僅對 Linux/Unix-like 機器生效):當系統 load1 超過閾值,且系統當前的併發線程數超過系統容量時才會觸發系統保護。系統容量由系統的 maxQps * minRt 計算得出。設定參考值一般是 CPU cores * 2.5。

  • RT:當單台機器上所有入口流量的平均 RT 達到閾值即觸發系統保護,單位是毫秒。

  • 線程數:當單台機器上所有入口流量的併發線程數達到閾值即觸發系統保護。

  • 入口 QPS:當單台機器上所有入口流量的 QPS 達到閾值即觸發系統保護。

  • CPU使用率:當單台機器上所有入口流量的 CPU使用率達到閾值即觸發系統保護。

3.5 授權規則

很多時候,我們需要根據調用來源來判斷該次請求是否允許放行,這時候可以使用 Sentinel 的來源問控制的功能。來源訪問控制根據資源的請求來源(origin)限制資源是否通過:

  • 若配置白名單,則只有請求來源位於白名單內時才可通過;

  • 若配置黑名單,則請求來源位於黑名單時不通過,其餘的請求通過。

流控應用:sentinel提供了RequestOriginParser來處理接口來源。

我們運行abc來源的請求訪問/test接口。

@Component
class requestOrigin implements RequestOriginParser{

    @Override
    public String parseOrigin(HttpServletRequest httpServletRequest) {
        String server = httpServletRequest.getParameter("server");
        return server;
    }
}

我們請求http://localhost:7003/test?server=abc 和 http://localhost:7003/test?server=ab來分別看看效果。

@SentinelResource的使用

@SentinelResource 用於定義資源,並提供可選的異常處理和 fallback 配置項。

主要參數有以下幾個

屬性 作用
value 資源名稱
entryType entry類型,標記流量的方向,取值IN/OUT,默認是OUT
blockHandler 處理BlockException的函數名稱,函數要求:1. 必須是 public;2.返回類型 參數與原方法一致;3. 默認需和原方法在同一個類中。若希望使用其他類的函數,可配置blockHandlerClass ,並指定blockHandlerClass裏面的方法。
blockHandlerClass 存放blockHandler的類,對應的處理函數必須static修飾。
fallback 1. 返回類型與原方法一致;2. 參數類型需要和原方法相匹配;3. 默認需和原方法在同一個類中。若希望使用其他類的函數,可配置fallbackClass
fallbackClass 存放fallback的類。對應的處理函數必須static修飾。
defaultFallback 若同時配置了 fallback 和 defaultFallback,以fallback為準。
exceptionsToIgnore 指定排除掉哪些異常。排除的異常不會計入異常統計,也不會進入fallback邏輯,而是原樣拋出。
exceptionsToTrace 需要trace的異常

@sentinelResource可結合blockHandler用於限流處理,結合fallback用於降級處理。具體規則可通過sentinel控制台配置,具體我就不演示了,在下一章內容中,我會分別演示限流和降級的應用。

public class MySentinelResource {

    @SentinelResource(value="message",blockHandler="blockHandler",fallback="fallback")
    public String message(String str){
        if(StringUtils.isBlank(str)){
            throw new RuntimeException();
        }
        return str;
    }
    /**
     * 限流處理
     * @param str
     * @param ex
     * @return
     */
    public String blockHandler(String str, BlockedException ex){
        return str + "--"+ ex;
    }
    /**
     * 降級處理
     * @param str
     * @return
     */
    public String fallback(String str){
        return null;
    }
}

代碼示例

gitee:https://gitee.com/zhixie/spring-cloud-alibaba-learning/tree/master/sentinel-server

github:https://github.com/binzh303/spring-cloud-alibaba-learning/tree/master/sentinel-server

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

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

LG 化學將為長城汽車供應電動車電池

據 LG 化學 17 日公布的消息指出,近日該公司與中國最大 SUV (運動型多用途汽車) 和皮卡制造商簽署了電動車電池供應合約。根據合約,LG 化學將為長城汽車自 2017 年起量產的插電式混合動力車 (phev) 車型提供電池。   由此,LG 化學將為中國 20 萬輛電動車提供電池,在中國市場搶占先機,並奠定進入中國 SUV 市場的堅實基礎。2014 年中國 SUV 銷量約達 410 萬輛,預計在 2016 年將達 620 萬輛。

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

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

易到用車APP上線數千輛新能源車 最低5元人民幣即可搭乘

日前,易到用車啟動了「E-Car計畫」,將陸續在北京、上海、廣州、深圳上線數千輛新能源車型,打造新能源出行平台;與此同時易到用車還宣佈將與普天新能源、龐大集團、騰勢等品牌展開深度合作,共同打造新能源出行聯盟,共建新能源城市。   屆時,易到用車App將上線開通「新能源」車型選項。此次加入易到「E-Car計畫」的車型包括混合動力的雙擎普銳斯、沃爾沃S60L、北汽EV系列以及特斯拉,用戶可通過易到用車App,在「新能源」車型中按需選擇,價格最低5元人民幣(下同)起步,0元/分鐘,0.99元/公里。   易到方面介紹,作為聯盟計畫的一部分,易到用車將與普天合作,在未來共同建設1000個充電樁設施,也同時尋找更多合作夥伴,提供場地共同建樁,而且充電樁建好後,將面向社會全面開放使用。

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

加速電動車佈局!首爾3年內增設10萬個充電站

為了加速電動車(electric vehicles)普及率,南韓政府宣佈將主導建設名為「EV-Line」的行動充電站,在首都首爾地區擴增10萬個充電設施;此計畫預計在2018年完工,由當地業者Power Cube和KT南韓電信承包建設業務。充電站將包括停車場、公園、住宅區等各式建築物內。   Power Cube董事表示,首爾當地有8成民眾都住在公寓,而非獨棟房屋,因此家用車都停在公用停車廠。每一個「EV-Line」充電站每小時約可充3.3 KW電量,要充飽整台車約需耗費6至8小時。有些充電站則可提供每小時8KW電量,雖可以縮短一半以上的充電時間,但充電站內的安全考慮成為最大考驗。

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

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

xeus-clickhouse: Jupyter 的 ClickHouse 內核

在科學計算領域,Jupyter 是一個使用非常廣泛的集成開發環境,它支持多種主流的編程語言比如 Python, C++, R 或者 Julia。同時,數據科學最重要的還是數據,而 SQL 是操作數據最直觀的語言。前段時間看到一篇文章[1],有人給 sqlite 做了一個 jupyter 的內核,感覺很有意思。所以我嘗試給 ClickHouse 做了一個 jupyter 的內核,目前已經有了一個可以試用的版本,下面做一個簡單介紹。

clickhouse

現狀

新內核允許用戶用 ClickHouse SQL 的語法直接操作遠程 CH 數據庫,通過一些擴展操作比如 %CONNECT 支持與 ch cli 一樣的連接參數,後續也有計劃使用 jupyter magics 支持更多的數據可視化操作。

項目參考了 jupyter sqlite 內核的實現方式,是基於 xeus [2]框架來實現的。xeus 是一個 c++ 的 lib 庫,它對 jupyter 的內核做了很好的封裝,我們只需要專註於內核相關的功能就可以了。目前對於 ch 的操作基於 clickhouse-cpp 來實現,它是 ch 的 cpp 客戶端。

ch-sql

目前實現處於早期階段,但是基礎功能已經可用。它支持了幾乎 CH 所有 SQL 語法,具體例子可以參考 examples/clickhouse.ipynb [4]。xeus-clickhouse 在 jupyter notebook 和 jupyter lab 中以 HTML 表格的形式展示數據;在 jupyter console 中,我們使用 tabulate 庫只做純文本的表格。

未來

對於 xeus-clickhouse 未來的規劃是,先打磨好穩定性,目前已知的還有一個非法字符導致內核崩潰的問題,已經提交 issue 給 xeus 倉庫;另外clickhouse-cpp 不支持 ssl 連接。除了基礎功能的打磨,還計劃通過支持更多的 jupyter magic 來實現數據的可視化渲染,提供更方便的數據可視化能力。

使用

我製作了一個 Docker 鏡像發布在 docker-hub [3] ,不需要安裝任何環境就可以試用:

# start jupyter with clickhouse kernal
docker run -p 8888:8888 wangfenjin/xeus-clickhouse:v0.1.0

# start a local clickhouse for testing
docker run -d --name jupyter-clickhouse-server -p 8123:8123 --ulimit nofile=262144:262144 yandex/clickhouse-server

# open the example/clickhouse.ipynb and connect to local server by
# %CONNECT --host host.docker.internal --port 8123

在 docker 裏面連接另外一個 docker 中的 ch 可能會有問題,感覺是目前 clickhouse-cpp 對於網絡的處理不太完善。感興趣的同學也可以下載代碼自己編譯,具體的編譯流程見 github 倉庫 [4]。歡迎大家試用!

鏈接

  1. https://blog.jupyter.org/a-jupyter-kernel-for-sqlite-9549c5dcf551
  2. https://github.com/jupyter-xeus/xeus
  3. https://hub.docker.com/r/wangfenjin/xeus-clickhouse
  4. https://github.com/wangfenjin/xeus-clickhouse

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

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

併發07–線程池及Executor框架

一、JAVA中的線程池

線程池的實現原理及流程如下圖所示:

 

 

   如上圖所示,當一個線程提交到線程池時(execute()或submit()),先判斷核心線程數(corePoolSize)是否已滿,如果未滿,則直接創建線程執行任務;如果已滿,則判斷隊列(BlockingQueue)是否已滿,如果未滿,則將線程添加到隊列中;如果已滿,則判斷線程池(maximumPoolSize)是否已滿,如果未滿,則創建線程池執行任務;如果線程池已滿,則交給飽和策略(RejectedExecutionHandler.rejectExcution())來處理。

  可以看下線程池ThreadPoolExecutor的全參構造函數源碼:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

對其入參釋義如下:

參數 描述 作用
coolPoolSize 線程核心線程數 當一個任務提交到線程池時,線程池會創建一個線程來執行任務,即使其他的核心線程足夠執行新任務,也會創建線程,直到需要執行的任務數大於核心線程數后才不再創建;如果線程池先調用了preStartAllCoreThread()方法,則會先啟動所有核心線程。
maximumPoolSize 線程池最大線程數 如果隊列滿了,並且已創建的線程數小於該值,則會創建新的線程執行任務。這裏需要說明一點,如果使用的隊列時無界隊列,那麼該值無用。
keepAliveTime 存活時間 當線程池中線程超過超時時間沒有新的任務進入,則停止該線程;只會停止多於核心線程數的那幾個線程。
unit 線程存活的時間單位 可以有天、小時、分鐘、秒、毫秒、微妙、納秒
workQueue 任務隊列

用於保存等待執行任務的阻塞隊列。可以選擇如下幾個隊列:數組結構的有界隊列ArrayBlockingQueue、鏈表結果的有界隊列LinkedBlockingQueue、不存儲元素的阻塞隊列SynchronousQueue、一個具有優先級的無界阻塞隊列PriortyBlockingQueue

threadFactory 創建線程的工廠

可以通過工廠給每個線程創建更有意義的名字。使用Guava提供的ThreadFactoryBuilder可以快速的給線程池裡的線程創建有意義的名字,代碼如下

new ThreadFactoryBuilder().setNameFormat(“aaaaaaaa”).build();

handler 包和策略

當隊列和線程都滿了,說明線程池處於飽和狀態,那麼必須採取一種策略來處理新提交的任務。

AbortPolicy(默認),表示無法處理新任務時拋出異常。

CallerRunsPolicy:只有調用者所在線程來運行

DiscardOldestPolicy:丟棄隊列里最近的一個任務,並執行當前任務

DiscardPolicy:不處理,直接丟棄

  上面說到,向線程池提交任務有兩種方法,分別是execute()和submit(),兩者的區別主要是execute()提交的是不需要有返回值的任務,而submit提交的是需要有返回值的任務,並且submit()會返回一個Furure對象,並且可以使用future.get()方法獲取返回值,並且get方法會阻塞,直到有返回值。

  線程池的關閉有shutdown()和shutdownNow兩個方法,他們的原理是遍歷線程池中的工作線程,然後逐個調用interrupt方法來中斷線程,所以無法中斷的線程可能永遠無法終止;但是二者也有區別,shutdownNow是將線程池的狀態設置為STOP,然後嘗試停止所有正在執行或者暫停的線程,並返回等待執行任務列表;而shutdown只是將線程池的狀態設置成SHUTDOWN,然後中斷所有沒有正在執行的任務。當調用這兩個方法中的任何一個后,isShutdown方法就會返回true,當所有任務都已經關閉后,調用isTerminaed方法會返回true。

  使用線程池時,需要從任務的性質(IO密集型還是CPU密集型或是混合型)、任務的優先級、任務的執行時常、任務的依賴性(是否依賴其他系統資源,如數據庫連接等)來綜合判斷,比如說,CPU密集型,就可以就可以配置N+1個線程個數,其中N為CPU核數,如果是IO密集型,則可以配置2*N個線程數;如果是混合型的任務,可以將其拆分成IO密集型和CPU密集型,但是如果兩個任務的執行時間相差較大,則沒有必要進行拆分;優先級不同的任務可以使用優先級隊列PriortyBlockingQueue來處理;依賴數據出等其它資源的線程池,比如說依賴數據庫,那麼就可以加大線程數量,因為在等待sql執行的時候,線程是處於空閑狀態;另外,最好使用有界隊列,因為無界隊列,因為有界隊列可以增加系統的穩定性和預警能力。

  對於線程的監控,還有以下幾個方法可以使用:

方法 描述
taskCount() 線程池需要執行的任務數量
completedTskCount 線程池運行過程中已經執行完畢的任務數量
IarestPoolSize 線程池中曾經創建過的最大線程數
getPoolSize 線程池的線程數量
getActiveCount 獲取活動的線程數

二、Exector框架

   在java中,是用線程來異步執行任務,java線程的創建與銷毀需要一定的開銷。如果我們為每一個任務創建一個線程的話,這些線程就會消耗大量的計算資源,會使處於高負荷的應用崩潰。

  在HotSpot虛擬機中,JAVA線程被一對一的映射為本地操作系統線程。JAVA線程啟動時會創建一個本地操作系統線程,當該JAVA線程終止時,這個操作系統線程也會被收回,操作系統會調用多有線程並將他們分配給可用的CPU。

 

   Executor框架的兩級調度模型如上圖所示,應用程序通過Executor控制上層的調度,而下層的調用由操作系統內核控制,將線程映射到硬件處理器上,下層的調用不受應用程序的控制。

   關於Executor的組成部分如下所示:

元素 描述
任務 包括被執行任務需要實現的接口Runnable和Callable接口
任務的執行 包括任務執行機制的核心接口Executor,以及繼承自Executor的ExecutorService接口。Executor接口有兩個關鍵的實現類實現了ExecutorService接口:ThreadPoolExecutor和ScheduledThreadPoolExecutor
異步計算的結果 包括接口Future和實現Future接口的FurureTask類

  Executor框架使用示意圖如下:

 

 

   如上圖所示,主線程首先創建實現Runnable或Callable接口的任務對象,然後把任務對象提交給ExecutorService執行,如果使用的是submit提交,執行完畢后將返回一個實現Future接口的對象,最後,主線程可以執行FutureTask.get()方法來獲取返回值;主線程也可以調用FutureTask.cancel()方法來取消此任務的執行。

  Executor框架的成員如下:

成員 描述 子類 描述
ThreadPoolExecutor

通常使用工廠類Executors來創建,Executors可以創建三種類型的ThreadPoolExecutor

固定線程數的FixedThreadPool

適用於為了滿足資源管理的需求,而需要限制當前線程數量的應用場景,它適用於負載比較重的應用。
單一線程的SingleThreadPool 適用於需要保證順序的執行各個任務,並且在任意時間點都不會有多個線程活動的場景。
根據需要創建線程的CacheThreadPool 這是一個無界的線程池,適用於執行很多短期異步任務的小程序,或者是負載比較輕的服務器。
ScheduledThreadPoolExecutor 通常使用工廠類Executors創建,Executors可以創建兩種類型的ScheduledThreadPoolExecutor 包含若干線程的ScheduledThreadPoolExecutor 適用於需要多個後台線程執行周期任務,同時為了滿足資源管理的需求而需要限制後台線程數量的應用場景。
只包含一個線程的SingleThreadScheduledExecutor                         適用於需要單個後台線程執行周期任務,同時需要保證順序的執行各個任務的場景。                                               
ForkJoinsPool

 newWorkStealingPool適合使用在很耗時的操作,但是newWorkStealingPool不是ThreadPoolExecutor的擴展,它是新的線程池類ForkJoinPool的擴展,但是都是在統一的一個Executors類中實現,由於能夠合理的使用CPU進行對任務操作(并行操作),所以適合使用在很耗時的任務中 

   
Future Future接口和實現了該接口的FutureTask類來表示異步計算的結果    
Runnable和Callable接口

Runnable和Callable接口的實現類,都可以被ThreadPoolExecutor、ScheduledThreadPool、ForkJoinThred執行;除了可以自己實現Callable接口外,我們還可以使用工廠類Executors來把一個Runnable包裝成一個Callable

   

 

ThreadPoolExecutor詳解

1、ThreadPoolExecutor

  (1)FixedThreadPool

  構造函數如下:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

  構造函數中,核心線程數和最大線程數一致,keepAliveTime為0,隊列使用的是無界阻塞隊列LinkedBlockingQueue(最大值是Integer.MAX_VALUE);

  核心線程數和最大線程數保持一致,表明:如果隊列滿了之後,不會再創建新的線程;

  keepAliveTime為0,表明:如果運行線程數大於核心線程數時,如果線程執行完畢,空閑線程立刻被終止;

  使用無界阻塞隊列,表明:當運行線程到達核心線程數時,不會再創建線程,只會將任務加入阻塞隊列;因此最大線程數參數無效;因此keepAliveTime參數無效;且不會拒絕任務(既不會執行包和策略)

  (2)SingleThreadExecutor

  構造函數如下:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

  構造函數中,核心線程數和最大線程數均為1,keepAliveTime為0,隊列使用的是無界阻塞隊列LinkedBlockingQueue(最大值是Integer.MAX_VALUE)

  除了固定了核心線程數和最大線程數為1外,其餘的參數均與FixedThreadPool一致,那麼就是只有一個線程會反覆循環從阻塞隊列中獲取任務執行

  (3)CacheThreadPool

  構造函數如下:

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

  構造函數中,核心線程數為0,最大線程數為Integer.MAX_VALUE,意味着無界,keepAliveTime為60秒,阻塞隊列使用沒有存儲空間的SynchronousQueue

  核心線程數為0,最大線程數為無界,表明:只要隊列滿了,就會創建新的線程放入線程池

  使用沒有存儲空間的SynchronousQueue表明:線程提交的速度高於線程被消費的速度,那麼線程會被不斷的創建,最終會因為線程創建過多而耗盡CPU和內存資源

2、ScheduledThreadPoolExecutor

  ScheduledThreadPoolExecutor的運行機制如下:

   (1)當調用ScheduledTreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法時會向ScheduledThreadPoolExecutor的DelayQueue添加一個實現了RunnableScheduledFuture接口的ScheduledFutureTask

  (2)線程池中的線程從DelayQueue中獲取ScheduledFutureTask,然後執行任務。

  ScheduledFutureTask主要包含以下三個成員變量

成員變量 描述
long time 表示這個任務要被執行的時間
long sequenceNumber 表示該任務被添加到ScheduledThreadPoolExecutor中的序號
long period 表示任務執行的間隔周期

  DelayQueue封裝了一個PriorityQueue,當添加任務時,這個PriorityQueue會對隊列中的ScheduledFutureTask進行排序,time最小的在最前面(最先被執行),如果time一致,就比較sequenceNumber,sequenceNumber小的排在前面。

  當線程執行任務時,先從DelayQueue隊列中獲取已經到期的任務(time大於當前時間),然後執行該任務,執行完畢后,根據任務的執行周期,修改任務下次的執行時間time,並重新將任務添加到DelayQueue

 

 

  FutureTask詳解

  Future接口和實現該接口的FutureTask類,代表異步計算的結果。

  FutureTask的使用方法是將其交給Executor執行,也可以通過ExecutorService.submit()方法返回一個FutureTask,然後執行FutureTask.get()方法或FutureTask.cancel()方法,除此之外,還可以但是使用FutureTask。

  FutureTask有三種狀態:未啟動(FutureTask.run()沒有被執行之前的狀態)、已啟動(FutureTask.run()方法執行過程中)、已完成(FutureTask.run()方法執行完成或被取消),這三種狀態的流轉如下圖所示:

 

  FutureTask的實現是基於AQS(AbstractQueuedSynchrouizer)來實現的,之前已經說過,每一個基於AQS實現的同步器都會至少包含一個acquire操作和至少一個release操作。AQS被作為模板方法模式的基礎類提供給FutureTask的內部子類Sync實現了AQS的tryAcquireShared(int)方法和tryReleaseShared(int)方法,Sync通過這兩個方法來檢查和更新同步狀態。

  FutureTask涉及示意圖如下圖所示:

 

  如上圖所示,FutureTask.get()方法會調用AQS的acquireSharedInterruptibly(int)方法,該方法首先會回調在子類Sync中的tryAcquireShared()方法來判斷acquire操作是否成功(state狀態狀態是否為執行完成RAN或取消狀態CANCELED&runner不為null),如果成功則get()方法立刻返回,如果失敗則到線程等待隊列中去等待其他線程執行release操作;當其他線程執行release操作(比如FutureTask.run()或FutureTask.cancel())喚醒當前線程后,當前線程再次執行tryAcquireShared()將返回正值1,當前線程將離開線程等待隊列並喚醒它的後繼線程。

  Run方法執行過程如下:

  執行在構造函數中指定的任務(Callable.call()),然後以原子方式來更新狀態(調用AQS.compareAndSetState(int expect, int update),設置state的狀態為RAN),如果這個原子操作成功,就設置代表計算結果的變量result的值為Callable.call()的返回值,然後調用AQS.release(int)。

  AQS.rease首先會調用子類Sync中實現的tryReleaseShared方法來執行release操作(設置運行任務的線程為null,然後返回false),然後喚醒等待隊列中的第一個線程。

  最後調用Future.done()方法。

 

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※新北清潔公司,居家、辦公、裝潢細清專業服務

Spring Redis開啟事務支持錯誤用法導致服務不可用

 

 

1.事故背景

在APP訪問服務器接口時需要從redis中獲取token進行校驗,服務器上線后發現一開始可以正常訪問,但只要短時間內請求量增長服務則無法響應

2.排查流程

(1)使用top指令查看CPU資源佔用還遠遠達不到瓶頸,排查因為CPU資源不足導致服務不可用的可能

(2)查看tomcat線程池配置,默認最大線程數為200,理論上可以支持目前服務器的訪問量

(3)使用jmap指令保存堆棧信息,jmap -dump:format=b,file=dump.log pid,pid為進程號

(4)使用Java visualVM打開保存的堆棧日誌dump.log,發現絕大部分的線程都阻塞在從redis連接池中獲取連接的代碼,如下圖所示

 3.原理分析

 根據堆棧日誌所显示得知線程訪問redis前需要從連接池隊列中推出一個連接,當連接池沒有連接時,則會阻塞等待,阻塞等待的時間可以自行設置MAA_WAIT參數,默認是-1表示不限時等待,目前項目使用默認配置,所以所有的線程都會一直阻塞在獲取連接的步驟,如果設置了最大等待時間,當超過最大等待時間會報出Could not get a resource from the pool的異常

(1)在spring配置文件中的stringRedisTemplate對象配置參數中打開了事務支持,而redis的事務支持是用MUTI和EXEC指令來支持,以下事務實例截圖來自菜鳥教程 https://www.runoob.com/redis/redis-transactions.html

 

(2)如果要保證在事務能正常執行,那麼在一個方法中多次操作redis必須是同一條連接,這樣才能保證事務能正常執行,所以在stringRedisTemplate會將連接綁定在當前線程,當第二次訪問redis時直接從當前線程中獲取連接,綁定連接源碼如下:

 

 

 (3)按照流程,先綁定連接,最後在finally代碼塊中釋放連接,看起來並沒有問題,但跳進去releaseConnection方法的代碼發現連接需要在事務提交后才能釋放,也就是說service方法上必須使用@Transation註解修飾,但因為業務方法上少寫了@Transation註解導致連接將一直綁定第一次獲取他的線程上,當線程池的線程被獲取完之後,其他線程就會就如阻塞等待狀態,導致服務不可用

 

 (4)如果加上@Transation註解,那麼方法執行完之後將會執行TransactionSynchronizationUtils.invokeAfterCompletion這個方法,mysql事務也是在這個方法執行commit操作,如下圖所示方法的第一個參數是List<TransactionSynchronization> synchronizations,代表可以有多個事務,redis,mysql等,都會此進行事務提交操作,這裏使用多態,根據對象的具體類型執行不同的方法,redis則執行redis的事務提交操作,mysql則執行mysql的事務提交操作

(5)以下為redis事務提交的代碼,也跟我們上面提到的一樣,發送exec指令提交事務

 

 4.如何修改代碼

(1)確認實際需求是否需要事務支持,如果需要則在對應方法上加上@Transaction註解

(2)如果不需要事務支持則將enableTransactionSupport設置為false

 

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

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?