時間序列神器之爭:Prophet VS LSTM

一、需求背景

我們福祿網絡致力於為廣大用戶提供智能化充值服務,包括各類通信充值卡(比如移動、聯通、電信的話費及流量充值)、遊戲類充值卡(比如王者榮耀、吃雞類點券、AppleStore充值、Q幣、鬥魚幣等)、生活服務類(比如肯德基、小鹿茶等),網娛類(比如QQ各類鑽等),作為一個服務提供商,商品質量的穩定、持續及充值過程的便捷一直是我們在業內的口碑。
在整個商品流通過程中,如何做好庫存的管理,以充分提高庫存運轉周期和資金使用效率,一直是個難題。基於此,我們提出了智能化的庫存管理服務,根據訂單數據及商品數據,來預測不同商品隨着時間推移的日常消耗情況。

二、算法選擇

目前成熟的時間序列預測算法很多,但商業領域性能優越的卻不多,經過多種嘗試,給大家推薦2種時間序列算法:facebook開源的Prophet算法和LSTM深度學習算法。
現將個人理解的2種算法特性予以簡要說明:

  • (1)、在訓練時間上,prophet幾十秒就能出結果,而lstm往往需要1個半小時,更是隨着網絡層數和特徵數量的增加而增加。
  • (2)、Prophet是一個為商業預測而生的時間序列預測模型,因此在很多方便都有針對性的優化,而lstm的初衷是nlp。
  • (3)、Prophet無需特徵處理即可使用,參數調優也明確簡單。而lstm則需要先進行必要的特徵處理,其次要進行正確的網絡結構設計,因此lstm相對prophet更為複雜。
  • (4)、Lstm需要更多的數據進行學習,否則無法消除欠擬合的情形。而prophet不同,prophet基於統計學,有完整的數學理論支撐,因此更容易從少量的數據中完成學習。
  • (5)、傳統的時間序列預測算法只支持單緯度,但LSTM能支持多緯度,也就是說LSTM能考慮促銷活動,目標用戶特性,產品特性等

三、數據來源

  • (1)、訂單數據
  • (2)、產品分類數據

四、數據形式

time,product,cnt
2019-10-01 00,**充值,6
2019-10-01 00,***遊戲,368
2019-10-01 00,***,1
2019-10-01 00,***,11
2019-10-01 00,***遊戲,17
2019-10-01 00
,三網***,39
2019-10-01 00,**網,6
2019-10-01 00,***,2

字段說明:

  • Time:小時級時間
  • Product:產品名稱或產品的分類名稱,目前使用的是產品2級分類,名稱
  • Cnt:成功訂單數量
    目前的時間序列是由以上time和cnt組成,product是用於區分不同時間序列的字段。

五、特徵處理

時間序列一般不進行特徵處理,當然可以根據具體情況進行歸一化處理或是取對數處理等。

六、算法選擇

目前待選的算法主要有2種:

  • (1)、Prophet
    Facebook開源的時間序列預測算法,考慮了節假日因素。
  • (2)、LSTM
    優化后的RNN深度學習算法。

七、算法說明

7.1 prophet

7.1.1Prophet的核心是調參,步驟如下:
  • 1、首先我們去除數據中的異常點(outlier),直接賦值為none就可以,因為Prophet的設計中可以通過插值處理缺失值,但是對異常值比較敏感。
  • 2、選擇趨勢模型,默認使用分段線性的趨勢,但是如果認為模型的趨勢是按照log函數方式增長的,可設置growth=’logistic’從而使用分段log的增長方式
  • 3、 設置趨勢轉折點(changepoint),如果我們知道時間序列的趨勢會在某些位置發現轉變,可以進行人工設置,比如某一天有新產品上線會影響我們的走勢,我們可以將這個時刻設置為轉折點。如果自己不設置,算法會自己總結changepoint。
  • 4、 設置周期性,模型默認是帶有年和星期以及天的周期性,其他月、小時的周期性需要自己根據數據的特徵進行設置,或者設置將年和星期等周期關閉。
    設置節假日特徵,如果我們的數據存在節假日的突增或者突降,我們可以設置holiday參數來進行調節,可以設置不同的holiday,例如五一一種,國慶一種,影響大小不一樣,時間段也不一樣。
  • 5、 此時可以簡單的進行作圖觀察,然後可以根據經驗繼續調節上述模型參數,同時根據模型是否過擬合以及對什麼成分過擬合,我們可以對應調節seasonality_prior_scale、holidays_prior_scale、changepoint_prior_scale參數。

以上是理論上的調參步驟,但我們在實際情況下在建議使用grid_search(網格尋參)方式,直接簡單效果好。當機器性能不佳時網格調參配合理論調參方法可以加快調參速度。建議初學者使用手動調參方式以理解每個參數對模型效果的影響。

holiday.csv

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from fbprophet import Prophet

data = pd.read_csv('../data/data2.csv', parse_dates=['time'], index_col='time')


def get_product_data(name, rule=None):
    product = data[data['product'] == name][['cnt']]
    product.plot()
    
if rule is not None:
        product = product.resample(rule).sum()
    product.reset_index(inplace=True)
    product.columns = ['ds', 'y']
    return product


holidays = pd.read_csv('holiday.csv', parse_dates=['ds'])
holidays['lower_window'] = -1

holidays = holidays.append(pd.DataFrame({
    'holiday': '雙11',
    'ds': pd.to_datetime(['2019-11-11', '2020-11-11']),
    'lower_window': -1,
    'upper_window': 1,
})).append(pd.DataFrame({
    'holiday': '雙12',
    'ds': pd.to_datetime(['2019-12-12', '2020-12-12']),
    'lower_window': -1,
    'upper_window': 1,
})
)

def predict(name, rule='1d', freq='d', periods=1, show=False):
    ds = get_product_data(name, rule=rule)
    if ds.shape[0] < 7:
        return None
    m = Prophet(holidays=holidays)
    m.fit(ds)
    future = m.make_future_dataframe(freq=freq, periods=periods)  # 建立數據預測框架,數據粒度為天,預測步長為一年
    forecast = m.predict(future)
    if show:
        m.plot(forecast).show()  # 繪製預測效果圖
        m.plot_components(forecast).show()  # 繪製成分趨勢圖
    mse = forecast['yhat'].iloc[ds.shape[0]] - ds['y'].values
    mse = np.abs(mse) / (ds['y'].values + 1)
    return [name, mse.mean(), mse.max(), mse.min(), np.quantile(mse, 0.9), np.quantile(mse, 0.8), mse[-7:].mean(),
            ds['y'].iloc[-7:].mean()]
if __name__ == '__main__':
    products = set(data['product'])
    p = []
    for i in products:
        y = predict(i)
        if y is not None:
            p.append(y)
    df = pd.DataFrame(p, columns=['product', 'total_mean', 'total_max', 'total_min', '0.9', '0.8', '7_mean',
       '7_real_value_mean'])
    df.set_index('product', inplace=True)
    product_sum: pd.DataFrame = data.groupby('product').sum()
    df = df.join(product_sum)
    df.sort_values('cnt', ascending=False, inplace=True)
    df.to_csv('result.csv', index=False)

結果如下:由於行數較多這裏只展示前1行

根據結果,對比原生數據,可以得出如下結論:
就算法與產品的匹配性可分為3個類型:

  • (1)與算法較為匹配,算法的歷史誤差8分為數<=0.2的
  • (2)與算法不太匹配的,算法的歷史誤差8分為數>0.2的
  • (3)數據過少的,無法正常預測的。目前僅top10就能佔到整體訂單數的90%以上。
7.1.2 部分成果展示

A. 因素分解圖

上圖中主要分為3個部分,分別對應prophet 3大要素,趨勢、節假日或特殊日期、周期性(包括年周期、月周期、week周期、天周期以及用戶自定義的周期)
下面依照上面因素分解圖的順序依次對圖進行說明:

  • (1)、Trend:
    即趨勢因素圖。描述時間序列的趨勢。Prophet支持線性趨勢和logist趨勢。通過growth參數設置,當然模型能自己根據時間序列的走勢判斷growth類型。這也是prophet實現的比較智能的一點。
  • (2)、Holidays
    即節假日及特殊日期因素圖。描述了節假日及用戶自定義的特殊日期對時間序列的影響。正值為正影響,負值為負影響。從圖中可以看出這個商品對節假日比較敏感。節假日是根據holidays參數設置的。
  • (3)、weekly
    星期周期性因素圖。正常情況下,如果是小時級別數據將會有天周期圖。有1年以上完整數據並且時間序列有典型的年周期性會有年周期圖。如果你覺得這個有年周期,但模型並不這麼認為,你可以通過設置yearly_seasonality設置一個具體的數值。這個數值默認情況下為10(weekly_seasonality默認為3),這個值代表的是傅里恭弘=叶 恭弘級數的項數,越大模型越容易過擬合,過小則會導致欠擬合,一般配合seasonality_prior_scale使用。
    B.預測曲線與實際值對比

7.2 lstm

LSTM(長短記憶網絡)主要用於有先後順序的序列類型的數據的深度學習網絡。是RNN的優化版本。一般用於自然語言處理,也可用於時間序列的預測。

簡單來說就是,LSTM一共有三個門,輸入門,遺忘門,輸出門, i 、o、 f 分別為三個門的程度參數, g 與RNN中的概念一致。公式里可以看到LSTM的輸出有兩個,細胞狀態c 和隱狀態 h,c是經輸入、遺忘門的產物,也就是當前cell本身的內容,經過輸出門得到h,就是想輸出什麼內容給下一單元。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torch
from torch import nn

from sklearn.preprocessing import MinMaxScaler

ts_data = pd.read_csv('../data/data2.csv', parse_dates=['time'], index_col='time')


def series_to_supervised(data, n_in=1, n_out=1, dropnan=True):
    n_vars = 1 if type(data) is list else data.shape[1]
    df = pd.DataFrame(data)
    cols, names = list(), list()
    # input sequence (t-n, ... t-1)
    for i in range(n_in, 0, -1):
        cols.append(df.shift(i))
        names += [('var%d(t-%d)' % (j + 1, i)) for j in range(n_vars)]
    # forecast sequence (t, t+1, ... t+n)
    for i in range(0, n_out):
        cols.append(df.shift(-i))
        if i == 0:
            names += [('var%d(t)' % (j + 1)) for j in range(n_vars)]
        else:
            names += [('var%d(t+%d)' % (j + 1, i)) for j in range(n_vars)]
    # put it all together
    agg = pd.concat(cols, axis=1)
    agg.columns = names
    # drop rows with NaN values
    if dropnan:
        agg.dropna(inplace=True)
    return agg


def transform_data(feature_cnt=2):
    yd = ts_data[ts_data['product'] == '移動話費'][['cnt']]
    scaler = MinMaxScaler(feature_range=(0, 1))
    yd_scaled = scaler.fit_transform(yd.values)
    yd_renamed = series_to_supervised(yd_scaled
, n_in=feature_cnt).values.astype('float32')

    n_row = yd_renamed.shape[0]

    n_train = int(n_row * 0.7)

    train_X, train_y = yd_renamed[:n_train, :-1], yd_renamed[:n_train, -1]
    test_X, test_y = yd_renamed[n_train:, :-1], yd_renamed[n_train:, -1]

    # 最後,我們需要將數據改變一下形狀,因為 RNN 讀入的數據維度是 (seq, batch, feature),所以要重新改變一下數據的維度,這裏只有一個序列,所以 batch 是 1,而輸入的 feature 就是我們希望依據的幾天,這裏我們定的是兩個天,所以 feature 就是 2.
    train_X = train_X.reshape((-1, 1, feature_cnt))
    test_X = test_X.reshape((-1, 1, feature_cnt))
    print(train_X.shape, train_y.shape, test_X.shape, test_y.shape)

    # 轉化成torch 的張量
    train_x = torch.from_numpy(train_X)
    train_y = torch.from_numpy(train_y)
    test_x = torch.from_numpy(test_X)
    test_y = torch.from_numpy(test_y)
    return scaler, train_x, train_y, test_x, test_y


scaler, train_x, train_y, test_x, test_y = transform_data(24)


# lstm 網絡
class lstm_reg(nn.Module):  # 括號中的是python的類繼承語法,父類是nn.Module類 不是參數的意思
    def __init__(self, input_size, hidden_size, output_size=1, num_layers=2):  # 構造函數
        # inpu_size 是輸入的樣本的特徵維度, hidden_size 是LSTM層的神經元個數,
        # output_size是輸出的特徵維度
        super(lstm_reg, self).__init__()  # super用於多層繼承使用,必須要有的操作

        self.rnn = nn.LSTM(input_size, hidden_size, num_layers)  # 兩層LSTM網絡,
        self.reg = nn.Linear(hidden_size, output_size)  # 把上一層總共hidden_size個的神經元的輸出向量作為輸入向量,然後回歸到output_size維度的輸出向量中

    
def forward(self, x):  # x是輸入的數據
        x, _ = self.rnn(x)  # 單個下劃線表示不在意的變量,這裡是LSTM網絡輸出的兩個隱藏層狀態
        s, b, h = x.shape
        x = x.view(s * b, h)
        x = self.reg(x)
        x = x.view(s, b, -1)  # 使用-1表示第三個維度自動根據原來的shape 和已經定了的s,b來確定
        return x


def train(feature_cnt, hidden_size, round, save_path='model.pkl'):
    # 我使用了GPU加速,如果不用的話需要把.cuda()給註釋掉
    net = lstm_reg(feature_cnt, hidden_size)
    criterion = nn.MSELoss()
    optimizer = torch.optim.Adam(net.parameters(), lr=1e-2)
    for e in range(round):
        # 新版本中可以不使用Variable了
        #     var_x = Variable(train_x).cuda()
        #     var_y = Variable(train_y).cuda()

        # 將tensor放在GPU上面進行運算
        var_x = train_x
        var_y = train_y

        out = net(var_x)
        loss = criterion(out, var_y)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if (e + 1) % 100 == 0:
            print('Epoch: {}, Loss:{:.5f}'.format(e + 1, loss.item()))
    # 存儲訓練好的模型參數
    torch.save(net.state_dict(), save_path)
    return net


if __name__ == '__main__':
    net = train(24, 8, 5000)
    # criterion = nn.MSELoss()
    # optimizer = torch.optim.Adam(net.parameters(), lr=1e-2)
    pred_test = net(test_x)  # 測試集的預測結果

    pred_test = pred_test.view(-1).data.numpy()  # 先轉移到cpu上才能轉換為numpy

    # 乘以原來歸一化的刻度放縮回到原來的值域
    origin_test_Y = scaler.inverse_transform(test_y.reshape((-1,1)))
    origin_pred_test = scaler.inverse_transform(pred_test.reshape((-1,1)))

    # 畫圖
    plt.plot(origin_pred_test, 'r', label='prediction')
    plt.plot(origin_test_Y, 'b', label='real')
    plt.legend(loc='best')
    plt.show()

    # 計算MSE
    # loss = criterion(out, var_y)?
    true_data = origin_test_Y
    true_data = np.array(true_data)
    true_data = np.squeeze(true_data)  # 從二維變成一維
    
MSE = true_data - origin_pred_test
    MSE = MSE * MSE
    MSE_loss = sum(MSE) / len(MSE)
    print(MSE_loss)

八、兩種算法的比較

  • (1)在訓練時間上,prophet幾十秒就能出結果,而lstm往往需要1個半小時,更是隨着網絡層數和特徵數量的增加而增加。
  • (2)Prophet是一個為商業預測而生的時間序列預測模型,因此在很多方便都有針對性的優化,而lstm的初衷是nlp。
  • (3)Prophet無需特徵處理即可使用,參數調優也明確簡單。而lstm則需要先進行必要的特徵處理,其次要進行正確的網絡結構設計,因此lstm相對prophet更為複雜。
  • (4)Lstm需要更多的數據進行學習,否則無法消除欠擬合的情形。而prophet不同,prophet基於統計學,有完整的數學理論支撐,因此更容易從少量的數據中完成學習。
    參考文獻:
    【1】Prophet官方文檔:https://facebook.github.io/prophet/
    【2】Prophet論文:https://peerj.com/preprints/3190/
    【3】Prophet-github:https://github.com/facebook/prophet
    【4】LSTM http://colah.github.io/posts/2015-08-Understanding-LSTMs/
    【5】基於LSTM的關聯時間序列預測方法研究 尹康 《北京交通大學》 2019年 cnki地址:http://cdmd.cnki.com.cn/Article/CDMD-10004-1019209125.htm

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

澳洲北領地邊界再封18個月 防原住民染疫

摘錄自2020年8月11日中央社報導

澳洲當局今天(11日)表示,將持續關閉北領地(Northern Territory)邊界18個月,以免其他疫情重災區的民眾進入,藉此保護當地龐大且弱勢的原住民族群。法新社報導,根據政府統計,北領地僅有約25萬人居住,其中30%為原住民。

外界認為澳洲原住民更容易受到武漢肺炎(COVID-19)等疾病的威脅,因為社會經濟與文化因素會影響醫療照護資源的取得及潛在健康問題。許多原住民族群擔憂疫情恐襲擊偏遠且醫療資源有限的原住民社區。

自澳洲爆發疫情以來,北領地確診病例很少,無人病歿。北領地近來不允許維多利亞州(Victoria)及雪梨的人進入當地。

土地利用
國際新聞
澳洲
原住民
疫情下的社會衝突
弱勢族群

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

蘇格蘭火車出軌意外 3人死亡、6人受傷

摘錄自2020年8月13日公視報導

日本連續一週的高溫已經造成10人死亡。韓國連下50多天的雨,也有42死或失聯。英國蘇格蘭東北部昨天則是發生一起嚴重的火車出軌意外,包括駕駛在內共3人死亡、6人受傷,由於事發當地連日豪雨引發土石災情,英國當局不排除出軌與天氣因素有關。

蘇格蘭東北部的「亞伯丁郡」12日上午9點43分發生列車出軌。這輛蘇格蘭鐵路的列車屬於雙車頭,並有四個車廂,原定終點站為「格拉斯哥皇后街」。不過卻在亞伯丁市區南邊15公里處的「斯冬希文鎮」出軌,當時車上9人,駕駛、調度員和一名乘客當場身亡,其餘六人受輕傷。

事發的斯冬希文鎮,近日來飽受水災所苦。當地媒體報導,可能是山崩引發這場意外,蘇格蘭首席大臣雖然不否認可能性,但希望交通警察部門能徹底調查原因。

國際新聞
蘇格蘭
火車

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

玻璃纖維船的全球污染 學者警告:大量棄置海上 威脅海洋生物生存

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

巴西疫情嚴峻 大沼澤野火煙塵污染雪上加霜

摘錄自2020年08月15日中央通訊社巴西報導

馬托格羅索(Mato Grosso)和南馬托格羅索(Mato Grosso do Sul)所在的中西部,連日來被濃煙壟罩,主因是位於兩州的生物群系大沼澤(Pantanal)發生近20年來最嚴重的野火。野火導致環境中大氣空氣品質惡化,通常伴隨人口呼吸和健康問題增加,再加上武漢肺炎疫情嚴峻,對居民來說更是雪上加霜。

今年1月至8月,整個中西部地區發生的火災數量較2019年同期增加20%,而全球面積最大的濕地大沼澤的現況,也令環保專家擔心。專家指出,目前大沼澤正遭遇近47年來最嚴重乾旱,今年上半年的雨量較預期低40%。

根據巴西國家太空署(Inpe),大沼澤也面臨1990年代末以來最凶猛的野火期,今年至8月12日在大沼澤發生的火災數量,較去年同期增加242%。專家表示,自從去年聯邦政府架空巴西環保署(Ibama)的權力後,大沼澤的監測就開始縮水。亞馬遜的情況也同樣讓人擔心,資料顯示,今年6、7月的火災數量也較去年同期有所增加。

氣候變遷
國際新聞
巴西
森林野火
空氣污染
沼澤

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

越南貓肉市場猖獗 估百萬貓遭吃下肚

摘錄自2020年8月18日公視報導

根據國際動保團體的最新調查,過去只在越南北部較為普遍的貓肉餐廳,現在已經擴展到全國各地,每年大約會吃掉100萬隻貓,而越南政府從1998年以來的吃貓肉禁令,已經在今年1月廢除,動保團體擔心情況將會雪上加霜。

越南人在二戰後民不聊生,經常抓貓狗充飢,雖然政府在1998年曾明令禁止,但民間吃貓肉的風氣仍舊存在。有老一輩的越南人認為,月初吃貓會獲得好運,避免遭遇不幸,還有的認為經常吃貓肉可以像貓一樣敏捷;而有的餐廳將貓肉稱作老虎寶寶,或小老虎之類的,讓人相信吃了可以強身壯陽。

今年1月間越南政府廢除吃貓禁令後,動保團體發現,越南各地貓肉餐廳頓時多了起來,遍及會安、胡志明市等地,而距離河內兩個小時車程的太平省,就是貓隻屠宰場的大本營,整個屠宰過程極不人道。

在揭發業者抓捕屠殺貓隻的殘忍行徑後,動保團體希望越南民眾別再吃貓肉。

生物多樣性
國際新聞
越南

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

「氣候倡議者」賀錦麗出任拜登副手 智庫:絕對有利氣候外交

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

以太坊節點發現協議

本檔前部分翻譯自以太坊定義的節點發現協議(版本4),後半部分給出了源碼實現的大致流程,以幫助理解。

以太坊節點信息的存儲採用的是Kademlia分佈式哈希表。理解節點發現協議主要是理解分佈式哈希表的原理,再加上定義的節點間通信的報文格式,節點ID的定義,距離的計算,加在一起就是以太坊的節點發現協議了。以太坊不同語言版本代碼實現上具體細節可能不同但大致流程思想是相同的。

第一部分——節點發現協議定義

節點ID

每個節點都有一個secp256k1橢圓曲線密碼學ID。節點的公鑰作為標識或節點ID。節點之間的距離為公鑰按位異或或者是公鑰的哈希值按位異或。計算公式如下:

distance(n₁, n₂) = keccak256(n₁) XOR keccak256(n₂)

節點表

節點表在節點發現協議中用於保存鄰節點信息。鄰節點被存在一個包含有K桶的路由表中。協議中\(k=16\),即每個K桶至多含有16個節點條目。每項按時間排序——最新發現更新的節點放在前,其他在後。

每當一個新節點\(N_1\)被發現,就可以插入相應的桶中。如果桶中少於\(k\)個條目,\(N_1\)可添加到桶中第一個條目。如果桶中已含有\(k\)項,桶中最早發現的節點\(N_2\),需要通過發送ping包重新檢測其有效性。如果沒有收到來自\(N_2\)的回復則認為該節點已失效(下線),從路由表中移除並將\(N_1\)添加到桶的前部。

以太坊文檔中Node Table一節有部分內容錯誤, For each 0 ≤ i < 256, every node keeps a k-bucket for nodes of distance between 2i and 2i+1 from itself. ,應該是\([2^i,2^{i+1})\) 。建議閱讀論文Kademlia——A Peer-to-peer Information System Based on the XOR Metric。

端點驗證

為了預防流量放大攻擊,必須驗證查詢的發送者是否參与了發現協議。如果數據包的發送者在過去12小時內發送了具有匹配ping哈希的有效pong響應,則認為該數據包的發送者已經過驗證。

遞歸查找

一次查找會找到\(k\)個距離目標節點最近的節點。節點查找發起后先選取\(a\)個距離目標節點最近的已知節點。隨後同時向這些節點發送FindNode包。其中,\(a\)是一個參數,通常可設為3。發起者繼續向先前查詢到的節點發送FindNode,如此不斷進行遞歸。對獲知的\(k\)個離目標節點最近的節點,選取\(a\)個尚未查詢過的節點向其發送FindNode。無法快速響應的節點將被排除在外,除非他們做出響應。

如果一輪FindNode查詢失敗,即沒有返回任何一個比目前節點中更近的節點,那麼將會繼續向\(k\)個最近節點未被查詢過的節點中發送FindNode

報文協議

節點發現協議報文都是UDP報文,報文中最大的是1280字節。

packet = packet-header || packet-data

數據包頭部:

packet-header = hash || signature || packet-type
hash = keccak256(signature || packet-type || packet-data)
signature = sign(packet-type || packet-data)

當在同一UDP端口上運行多個協議時,hash可使分組格式可識別。除此並無其他目的。每個包都由節點公鑰來簽名,簽名是一個編碼長度為65字節數組,簽名值r,s,簽名驗證值v

消息類型packet-type占單字節。包有效數據在消息類型後面。數據包頭部之後的數據用RLP進行編碼。根據EIP-8,實現應忽略列表中的任何其他元素以及列表后的任何額外數據。

Ping Packet (0x01)

packet-data = [version, from, to, expiration]
version = 4
from = [sender-ip, sender-udp-port, sender-tcp-port]
to = [recipient-ip, recipient-udp-port, 0]packet-data = [ver

expiration字段是UNIX時間戳,如果一個數據包的時戳過期了可能會無法處理。收到ping數據包后,接收節點應回復pong數據包。並可考慮將發送節點添加到節點表中。

如果在過去12小時內未與發送方進行任何通信,則除了pong之外還應發送ping以驗證對端節點。

Pong Packet (0x02)

packet-data = [to, ping-hash, expiration]

Pong是ping的響應。ping-hash須與相應的ping包hash一致。實現時應該忽略那些不含有ping包hash的pong包。

FindNode Packet (0x03)

packet-data = [target, expiration]

FindNode包用於請求距離目的節點近的節點。目標節點ID是一個65字節長度的secp256k1橢圓曲線公鑰。當接收到FindNode,接收端需要回復在本地節點表中距離請求目的節點最近的16個節點。

為了對抗流量放大攻擊,只有被驗證過的FindNode發送者才會被回復鄰節點信息。

Neighbors Packet (0x04)

packet-data = [nodes, expiration]
nodes = [[ip, udp-port, tcp-port, node-id], ... ]

FindNode包的響應。

存在的問題及建議

凡含有expiration字段的數據包都是用於防止數據重放的。因為是絕對時間戳,節點時鐘必要要十分準確以正確驗證時戳的有效性。自從2016年協議發布後起,已經接收到無數的因為用戶的時鐘不準確造成的錯誤報告。

端點驗證是不嚴密是因為FindNode的發送方永遠法確定接收端十分接收到足夠的pong。Geth按如下方式處理:如果在最近12小時內未與收件人進行通信,請通過發送ping啟動該過程。等待來自另一方的ping,回復它然後發送FindNode

第二部分——節點發現協議代碼實現流程
流程圖

節點如何加入到對應的K桶

計算節點之間的距離很簡單,直接按位異或后的值即為兩節點之間的距離值,但節點應該加入那個K桶呢?可以公鑰哈希值按位異或后最高位的值(例如: 異或值0000 ... 0000 0101,則桶距離為3 ),則將節點放入第3個桶中。

為什麼?

主要是要理解二叉樹的拆分過程:
對每一個節點,都可以按照自己的視角對整個二叉樹進行拆分。拆分的規則是:先從根節點開始,把不包含自己的那個子樹拆分出來;然後在剩下的子樹再拆分不包含自己的下一層子樹;以此類推,直到最後只剩下自己。

拆分的最後一個K桶(距離自己最近的那個K桶),只有最後1位不同,異或值為0000 ... 0000 0001,最高位為1,第一個K桶;拆分的倒數第二個K桶,異或值為0000 ... 0000 001x,最高位為2,第二個K桶;依此類推……

在具體實現細節上,以太坊節點節點公鑰是512位,計算距離時的ID是取節點公鑰的哈希,值為256位。所以節點路由表由256個K桶組成,每個K桶最多16個節點。

參考文檔:
Node Discovery Protocol v4
聊聊分佈式散列表(DHT)的原理——以 Kademlia(Kad) 和 Chord 為例
Kademlia——A Peer-to-peer Information System Based on the XOR Metric

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

【其他文章推薦】

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

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

【Spring註解驅動開發】在@Import註解中使用ImportSelector接口導入bean

寫在前面

在上一篇關於Spring的@Import註解的文章《【Spring註解驅動開發】使用@Import註解給容器中快速導入一個組件》中,我們簡單介紹了如何使用@Import註解給容器中快速導入一個組件,而我們知道,@Import註解總共包含三種使用方法,分別為:直接填class數組方式;ImportSelector方法(重點);ImportBeanDefinitionRegistrar方式。那麼,今天,我們就一起來學習關於@Import註解非常重要的第二種方式:ImportSelector方式。

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

ImportSelector接口概述

ImportSelector接口是至spring中導入外部配置的核心接口,在SpringBoot的自動化配置和@EnableXXX(功能性註解)都有它的存在。我們先來看一下ImportSelector接口的源碼,如下所示。

package org.springframework.context.annotation;

import java.util.function.Predicate;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.lang.Nullable;

public interface ImportSelector {
	String[] selectImports(AnnotationMetadata importingClassMetadata);
	@Nullable
	default Predicate<String> getExclusionFilter() {
		return null;
	}
}

該接口文檔上說的明明白白,其主要作用是收集需要導入的配置類,selectImports()方法的返回值就是我們向Spring容器中導入的類的全類名。如果該接口的實現類同時實現EnvironmentAware, BeanFactoryAware ,BeanClassLoaderAware或者ResourceLoaderAware,那麼在調用其selectImports方法之前先調用上述接口中對應的方法,如果需要在所有的@Configuration處理完在導入時可以實現DeferredImportSelector接口。

在ImportSelector接口的selectImports()方法中,存在一個AnnotationMetadata類型的參數,這個參數能夠獲取到當前標註@Import註解的類的所有註解信息。

注意:如果ImportSelector接口展開講的話,可以單獨寫一篇文章,那我就放在下一篇文章中講吧,這裏就不贅述了,嘿嘿。

ImportSelector接口實例

首先,我們創建一個MyImportSelector類實現ImportSelector接口,如下所示。

package io.mykit.spring.plugins.register.selector;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
/**
 * @author binghe
 * @version 1.0.0
 * @description 測試@Import註解中使用ImportSelector
 *              自定義邏輯,返回需要導入的組件
 */
public class MyImportSelector implements ImportSelector {
    /**
     * 返回值為需要導入到容器中的bean的全類名數組
     * AnnotationMetadata:當前標註@Import註解的類的所有註解信息
     */
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[0];
    }
}

接下來,我們在PersonConfig2類的@Import註解中,導入MyImportSelector類,如下所示。

@Configuration
@Import({Department.class, Employee.class, MyImportSelector.class})
public class PersonConfig2 {

至於使用MyImportSelector導入哪些bean,就需要在MyImportSelector類的selectImports()方法中進行設置了,只要在MyImportSelector類的selectImports()方法中返回要導入的類的全類名(包名+類名)即可。

我們繼承創建兩個Java bean對象,分別為User和Role,如下所示。

  • User類
package io.mykit.spring.plugins.register.bean;
/**
 * @author binghe
 * @version 1.0.0
 * @description 測試ImportSelector
 */
public class User {
}
  • Role類
package io.mykit.spring.plugins.register.bean;
/**
 * @author binghe
 * @version 1.0.0
 * @description 測試ImportSelector
 */
public class Role {
}

接下來,我們將User類和Role類的全類名返回到MyImportSelector類的selectImports()方法中,此時,MyImportSelector類的selectImports()方法如下所示。

/**
 * 返回值為需要導入到容器中的bean的全類名數組
 * AnnotationMetadata:當前標註@Import註解的類的所有註解信息
 */
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    return new String[]{
        User.class.getName(),
        Role.class.getName()
    };
}

接下來,我們運行SpringBeanTest類的testAnnotationConfig7()方法,輸出的結果信息如下所示。

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig2
io.mykit.spring.plugins.register.bean.Department
io.mykit.spring.plugins.register.bean.Employee
io.mykit.spring.plugins.register.bean.User
io.mykit.spring.plugins.register.bean.Role
person
binghe001

可以看到,輸出結果中多出了io.mykit.spring.plugins.register.bean.User和io.mykit.spring.plugins.register.bean.Role。

說明使用ImportSelector已經成功將User類和Role類導入到了Spring容器中。

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

MySQL觸發器的詳細教學與實戰分析

所有知識體系文章,GitHub已收錄,歡迎老闆們前來Star!

GitHub地址: https://github.com/Ziphtracks/JavaLearningmanual

MySQL觸發器

一、什麼是觸發器

觸發器(trigger)是MySQL提供給程序員和數據分析員來保證數據完整性的一種方法,它是與表事件相關的特殊的存儲過程,它的執行不是由程序調用,也不是手工啟動,而是由事件來觸發,比如當對一個表進行操作(insert,delete, update)時就會激活它執行。簡單理解為:你執行一條sql語句,這條sql語句的執行會自動去觸發執行其他的sql語句。

二、觸發器的作用

  • 可在寫入數據表前,強制檢驗或轉換數據。
  • 觸發器發生錯誤時,異動的結果會被撤銷。
  • 部分數據庫管理系統可以針對數據定義語言(DDL)使用觸發器,稱為DDL觸發器。
  • 可依照特定的情況,替換異動的指令 (INSTEAD OF)。

三、觸發器創建的四要素

  • 監視地點(table)
  • 監視事件(insert、update、delete)
  • 觸發時間(after、before)
  • 觸發事件(insert、update、delete)

四、觸發器的使用語法

語法:

before/after: 觸發器是在增刪改之前執行,還是之後執行

delete/insert/update: 觸發器由哪些行為觸發(增、刪、改)

on 表名: 觸發器監視哪張表的(增、刪、改)操作

觸發SQL代碼塊: 執行觸發器包含的SQL語句

1CREATE TRIGGER 觸發器名
2BEFORE|AFTER DELETE|INSERT|UPDATE
3ON 表名 FOR EACH ROW
4BEGIN
5觸發SQL代碼塊;
6END;

注意: 觸發器也是存儲過程程序的一種,而觸發器內部的執行SQL語句是可以多行操作的,所以在MySQL的存儲過程程序中,要定義結束符。

如果MySQL存儲過程不了解的小夥伴,可以參考此文面向MySQL存儲過程編程,文章中詳細講解了MySQL存儲過程的優勢和語法等等,相信你會在其中得以收穫。

1# 設置MySQL執行結束標誌,默認為;
2delimiter //

五、觸發器的基本使用

5.1 基本使用步驟

首先,我先展示一下創建的兩張表,因為創建的表很簡單,這裏我沒有提供庫表操作的SQL命令。

tb_class

image-20200611205404311

employee

image-20200611205435284

其次,創建了一個含有update操作的存儲過程

1delimiter //
2create procedure update_emp(in i intin p int)
3begin
4    update employee set phone = p where id = i;
5end //

再創建一個觸發器

分析: 觸發器名稱為t1,觸發時間為after,監視動作為update,監視表為employee表。匯總一起解釋這個觸發器就是:創建一個觸發器名稱為t1的觸發器,觸發器監視employee表執行update(更新)操作后,就開始執行觸發器內部SQL語句update tb_class set num = num + 1 where id = 1;

簡單來說就是一個監視一個表的增、刪、改操作並設置操作前後時間,在設置時間的範圍內對另外一個表進行其他操作。

如果你學到這裏還是一知半解,後面我會講解一個訂單與庫存的數據關係,到那時候你就會明白了!

 1delimiter //
2# 創建觸發器,觸發器名稱為t1
3create trigger t1
4    # 觸發器執行在update操作之後
5    after update
6    # 監視employee表
7    on employee
8    for each row
9begin
10    # 觸發執行的SQL語句
11    update tb_class set num = num + 1 where id = 1;
12end //

最後調用函數,並查看、分析結果

1call update_emp(2110);

觸發器在此場景的作用分析

當employee表發生update操作時,觸發器就對tb_class表中的num值做修改。

執行結果發現,我們在使用函數將employee表中id為2員工的phone修改為110后,觸發器監視到employee表中發生了update更新操作,就執行了內部SQL語句,也就是將tb_class表中id為1的num值自增1。

image-20200611213411229 image-20200611213432459

5.2 查看和刪除已有的觸發器

查看已有觸發器: show triggers

刪除已有觸發器: drop trigger 觸發器名稱

5.3 for each row

這裏擴展,在oracle觸發器中,觸發器分為行觸發器和語句觸發器。也就是說,假設你監視一個修改操作,它修改了1000行代碼,在Oracle中觸發器會觸發1000次。

在oracle中,for each row如果不寫,無論update語句一次影響了多少行,都只執行一次觸發事件。

而MySQL中,不支持語句級觸發器,所以在MySQL中並不需要在意。

六、訂單與庫存關係場景

訂單與庫存的關係: 用戶下訂單,意味着創建該商品訂單,該商品訂單中的商品數量為1,庫存中的該商品數量-1。往往訂單表和庫存表中的數量是同時操作的,所以我們這裏可以用觸發器。

觸發器應用: 關於訂單表,下訂單肯定是涉及到insert插入數據數量的操作。我們可以創建一個監視訂單表insert操作后執行庫存表數量-1的觸發器來完成訂單與庫存表的同時修改。

創建表,並在表中添加幾條數據:

 1create table goods(
2  gid int,
3  name varchar(20),
4  num smallint
5);
6create table ord(
7  oid int,
8  gid int,
9  much smallint
10);
11insert into goods values(1,'cat',40);
12insert into goods values(2,'dog',63);
13insert into goods values(3,'pig',87);

創建觸發器

1create trigger t1 
2after
3insert
4on ord
5for each row
6begin
7 update goods set num = num - 1 where gid = 1;
8end$

該觸發器意為,用戶不管下什麼訂單,都會把商品編號為1的商品的庫存減去1。

七、觸發器中引用行變量

7.1 old和new對象語法

  • 在觸發目標上執行insert操作後會有一個新行,如果在觸發事件中需要用到這個新行的變量,可以用new關鍵字表示
  • 在觸發目標上執行delete操作後會有一箇舊行,如果在觸發事件中需要用到這箇舊行的變量,可以用old關鍵字表示
  • 在觸發目標上執行update操作后原紀錄是舊行,新記錄是新行,可以使用new和old關鍵字來分別操作
觸發語句 old new
insert 所有字段都為空 將要插入的數據
update 更新以前該行的值 更新后的值
delete 刪除以前該行的值 所有字段都為空

7.2 old和new對象應用

關於old和new對象的應用,我在這裏沒有展開演示。只是將第八章的綜合案例結合了old和new對象實現。綜合案例中詳細講解了MySQL觸發器的使用!

八、綜合案例

8.1 創建表、插入表數據

tb_class為幼兒園班級表,其中cid為唯一主鍵,cname為大、中、小班班級標準,stuNo為班級標準內的學生個數。插入大、中、小班標準,初始化兩名學生在大班。

tb_stu為幼兒園學生表,其中sid為唯一主鍵,sname為學生性名,cno為所在班級標準的外鍵。插入兩條數據並初始化這兩名學生在大班,因為我們在班級表中初始化了兩名學生在大班嘛,所以要做此操作。

 1create table tb_class
2(
3    cid   int auto_increment
4        primary key,
5    cname varchar(32not null,
6    stuNo int         not null
7);
8
9INSERT INTO temp.tb_class (cname, stuNo) VALUES ('大班'2)
10INSERT INTO temp.tb_class (cname, stuNo) VALUES ('中班'0)
11INSERT INTO temp.tb_class (cname, stuNo) VALUES ('小班'0)
12
13create table tb_stu
14(
15    sid   int auto_increment
16        primary key,
17    sname varchar(32not null,
18    cno   int         not null
19);
20
21INSERT INTO temp.tb_stu (sname, cno) VALUES ('Ziph'1)
22INSERT INTO temp.tb_stu (sname, cno) VALUES ('Join'1)

8.2 添加學生案例

在此表結構中,如果一位新同學來到學校學習,意味着某一個班級中會多出一名學生。假設Marry同學去小班學習,其表結構的變化為:tb_stu表中添加一條Marry的記錄(注:cno = 3),tb_class表中小班記錄的stuNo = 0修改為stuNo = 1

先創建一個添加學生的存儲過程

1# 添加學生函數
2delimiter //
3# 創建存儲過程,傳入學生性名和班級參數
4create procedure add_stu(in in_sname varchar(32), in in_cno int)
5begin
6    # 插入記錄
7    insert into tb_stu (sname, cno) values (in_sname, in_cno);
8end //

創建觸發器

注意: 在更新學生數量SQL語句中,有一段cid = new.cno的SQL語句。這裏我解釋一下,new代表產生的新對象,將cid主鍵與添加Marry記錄后產生的新紀錄對象的cno外鍵關聯。(因為insert后產生的是新紀錄對象嘛,所以用new)

 1# 觸發器
2# 創建名稱為t_add_stu的觸發器
3create trigger t_add_stu
4    # 設置在insert操作之後觸發
5    after
6        insert
7    # 監視tb_stu的insert操作
8    on tb_stu
9    for each row
10begin
11    # 更新學生數量(cid為tb_class表中主鍵,cno為tb_stu表中外鍵)
12    update tb_class set stuNo = stuNo + 1 where cid = new.cno;
13end //

聲明回結束符

1delimiter ;

插入Marry學生記錄到數據庫表中

1call add_stu('Marry'3);

執行結果就是當插入Marry學生記錄的同時也修改了班級表中的小班學生數量。

8.3 刪除學生案例

刪除學生與添加學生十分相似,刪除學生相當於是添加學生的逆過程。如果以為學生退學了或者讀完了幼兒園離開學校了,就意味着班級中少了一位學生。假設Join同學讀完了大班結束了幼兒園階段的學習將要幼兒園去上小學,其表結構變化為:tb_stu刪除Join這條記錄(注:sid = 2),tb_class將修改Join所在大班班級級別的stuNo,即stuNo = stuNo – 1

先創建一個刪除學生的存儲過程

1# 刪除學生
2delimiter //
3create procedure delete_stu(in in_sid int)
4begin
5    delete from tb_stu where sid = in_sid;
6end //

創建觸發器

注意: 在更新學生數量的時候,書寫了此段SQL語句cid = OLD.cno。該語句使用old對象,意為Join學生的記錄沒有了,但是使用觸發器同步修改tb_class表中的大班學生數量還需要用到關聯Join學生所在記錄的外鍵cno,使用old來句點出來的cno就是刪除之前Join那一條學生記錄的cno。(如果我們用new,該記錄還存在嗎?該記錄的cno還存在嗎?答案是都不存在了!)

 1# 觸發器
2# 創建觸發器名稱為t_delete_stu的觸發器
3create trigger t_delete_stu
4    # 設置在delete操作之後觸發
5    after
6        delete
7    # 監視tb_stu表的delete操作
8    on tb_stu
9    for each row
10begin
11    # 更新學生數量(cid為tb_class表中主鍵,cno為tb_stu表中外鍵)
12    update tb_class set stuNo = stuNo - 1 where cid = OLD.cno;
13end //

聲明回結束符

1delimiter ;

刪除Jion學生記錄

1call delete_stu(2);

執行結果為Join記錄在數據庫的表中消失了,而大班的學生數量也減掉了1。

8.4 刪除班級案例

因為我已經詳細講解了添加學生與刪除學生,所以刪除班級我就不再作過多的贅述了。那就直接說核心內容吧。刪除一個班級級別比如:刪除小班之前要把小班內的所有學生也被刪除了,因為兩個表是主外鍵關聯的。如果只刪除了小班,而沒有刪除小班內的所有學生,那麼原小班內的所有學生現在屬於哪個班級呢,就不知道了吧!所以要在刪除小班之前刪除小班內的所有學生。

 1# 創建刪除班級的存儲過程
2delimiter //
3create procedure delete_class(in in_cid int)
4begin
5    delete from tb_class where cid = in_cid;
6end //
7
8# 創建觸發器名稱為t_delete_class的觸發器
9create trigger t_delete_class
10    # 作用在delete操作之前
11    before
12        delete
13    # 監視tb_class表中的delete操作
14    on tb_class
15    for each row
16begin
17    # 同時刪除所有該原班級cid的所有學生
18    delete from tb_stu where cno = OLD.cid;
19end //
20
21# 將結束符聲明為;
22delimiter ;
23
24# 刪除小班班級別
25call delete_class(3);

執行結果為既刪除了小班,又刪除小班內的所有學生。

8.5 觸發器衝突問題

觸發器衝突問題其實就是關聯問題。為什麼這麼說呢?就說以下剛才這三個案例中出現的觸發器衝突問題。

如果我們在寫觸發器的時候,將添加學生、刪除學生和刪除班級的觸發器都寫在一個查詢模板中。你會發現當你在刪除班級的時候,會報錯。显示如下信息:

image-20200612004546204

這是為什麼呢?

仔細想想,我們將在案例中有兩個是同一個表中的刪除觸發器。刪除班級的觸發器中定義的是刪除班級時觸發刪除學生,而刪除學生的觸發器中定義的是班級人數減一。你發現了沒,觸發器被連着觸發了。如下變化:

image-20200612005312835

我們通過刪除班級案例了解了,刪除班級之前需要把班級內所有學生刪除掉。正因為如此,我們在刪除班級之前已經把所有學生都刪除了,導致在刪除學生的時候觸發了班級人數減一的觸發器,該觸發器在執行過程中修改了已經被刪除班級的學生人數。這問題就出在這裏了,班級已經刪除了,怎麼修改一個本就沒有的班級內的人數呢?對吧!

解決觸發器衝突

為解決這個場景的觸發器衝突問題,我們只能取捨一個觸發器。於是,就通過命令刪除了刪除學生案例中使用的那個觸發器,刪除后刪除班級就可以成功執行觸發了!

1drop trigger t_delete_stu;

注意: 由於存在觸發器衝突問題,我們在實際開發中需要認真考量定義觸發器!

九、觸發器性能和使用分析(必讀)

各大論壇等等,相信在大家的文章中都不推薦使用觸發器,而是推薦使用存儲過程程序,這是為什麼呢?

首先,存儲過程程序分為存儲過程、儲存過程函數和觸發器。也就是說這三種都是存儲過程的使用都是存儲過程的表現形式。

如果場景在數據量和併發量都很大的情況下,使用觸發器、存儲過程再加上幾個事務等等,很容易出現死鎖。而且在使用觸發器的時候,也會出現衝突,出現問題時,我們需要追溯的代碼就需要從一個觸發器到另一個觸發器……從而影響開發效率。從性能上看,觸發器也是存儲過程程序的一種,它也並沒有展現出多少性能上的優勢。由於觸發器寫起來比較隱蔽,容易被開發人員忽略,而且隱式調用觸發器不易於排除依賴,對後期維護不是很友好!

所以在開發中,觸發器是很少用到的。那為什麼我還花時間大篇幅的講解MySQL觸發器呢?原因很簡單,是因為需要擴展自己的知識儲備。開發中的使用問題和是否被大家摒棄,不是你拒絕學習知識的理由。之所以存在就有它存在的道理,我們在學習的道路中不斷擴充自己的知識儲備即可。

假如有一天你的同事聊起觸發器,你也能和他們聊聊你對觸發器的見解是哈?如果你根據從未了解過此知識呢?那性質就不一樣了,相信大家都懂吧!

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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