環境資訊中心綜合外電;姜唯 編譯;林大利 審校
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※超省錢租車方案
※別再煩惱如何寫文案,掌握八大原則!
※回頭車貨運收費標準
※教你寫出一流的銷售文案?
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
synchronized鎖的原理也是大廠面試中經常會涉及的問題,本文主要通過對以下問題進行分析講解,來幫助大家理解synchronized鎖的原理。
1.synchronized鎖是什麼?鎖的對象是什麼?
2.偏向鎖,輕量級鎖,重量級鎖的執行流程是怎樣的?
3.為什麼說是輕量級,重量級鎖是不公平的?
4.重量級鎖為什麼需要自旋操作?
5.什麼時候會發生鎖升級,鎖降級?
6.偏向鎖,輕量鎖,重量鎖的適用場景,優缺點是什麼?
synchronized的英文意思就是同步的意思,就是可以讓synchronized修飾的方法,代碼塊,每次只能有一個線程在執行,以此來實現數據的安全。
一般可以修飾同步代碼塊、實例方法、靜態方法,加鎖對象分別為同步代碼塊塊括號內的對象、實例對象、類。
在實現原理上,
public class SyncTest {
private Object lockObject = new Object();
public void syncBlock(){
//修飾代碼塊,加鎖對象為lockObject
synchronized (lockObject){
System.out.println("hello block");
}
}
//修飾實例方法,加鎖對象為當前的實例對象
public synchronized void syncMethod(){
System.out.println("hello method");
}
//修飾靜態方法,加鎖對象為當前的類
public static synchronized void staticSyncMethod(){
System.out.println("hello method");
}
}
在JVM中,一個Java對象其實由對象頭+實例數據+對齊填充三部分組成,而對象頭主要包含Mark Word+指向對象所屬的類的指針組成(如果是數組對象,還會包含長度)。像下圖一樣:
Mark Word:存儲對象自身的運行時數據,例如hashCode,GC分代年齡,鎖狀態標誌,線程持有的鎖等等。在32位系統佔4字節,在64位系統中佔8字節,所以它能存儲的數據量是有限的,所以主要通過設立是否偏向鎖的標誌位和鎖標誌位用於區分其他位數存儲的數據是什麼,具體請看下圖:
鎖信息都是存在鎖對象的Mark Word中的,當對象狀態為偏向鎖時,Mark Word存儲的是偏向的線程ID;當狀態為輕量級鎖時,Mark Word存儲的是指向線程棧中Lock Record的指針;當狀態為重量級鎖時,Mark Word為指向堆中的monitor對象的指針。
這是網上找到的一個流程圖,可以先看流程圖,結合著文字來了解執行流程
Hotspot的作者經過以往的研究發現大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,於是引入了偏向鎖。
簡單的來說,就是主要鎖處於偏向鎖狀態時,會在Mark Word中存當前持有偏向鎖的線程ID,如果獲取鎖的線程ID與它一致就說明是同一個線程,可以直接執行,不用像輕量級鎖那樣執行CAS操作來加鎖和解鎖。
線程發現是匿名偏向狀態(也就是鎖對象的Mark Word沒有存儲線程ID),則會用CAS指令,將mark word中的thread id由0改成當前線程Id。如果成功,則代表獲得了偏向鎖,繼續執行同步塊中的代碼。否則,將偏向鎖撤銷,升級為輕量級鎖。
發現鎖對象存儲的線程ID就是當前線程的ID,會往當前線程的棧中添加一條Displaced Mark Word為空的Lock Record中,然後繼續執行同步塊的代碼,因為操縱的是線程私有的棧,因此不需要用到CAS指令;由此可見偏向鎖模式下,當被偏向的線程再次嘗試獲得鎖時,僅僅進行幾個簡單的操作就可以了,在這種情況下,synchronized關鍵字帶來的性能開銷基本可以忽略。
當沒有獲得鎖的線程進入同步塊時,發現當前是偏向鎖狀態,並且存儲的是其他線程ID(也就是其他線程正在持有偏向鎖),則會進入到撤銷偏向鎖的邏輯里,一般來說,會在safepoint中去查看偏向的線程是否還存活
mark word改為無鎖狀態(unlocked)由此可見,偏向鎖升級的時機為:當一個線程獲得了偏向鎖,在執行時,只要有另一個線程嘗試獲得偏向鎖,並且當前持有偏向鎖的線程還在同步塊中執行,則該偏向鎖就會升級成輕量級鎖。
因此偏向鎖的解鎖很簡單,其僅僅將線程的棧中的最近一條lock record的obj字段設置為null。需要注意的是,偏向鎖的解鎖步驟中並不會修改鎖對象Mark Word中的thread id,簡單的說就是鎖對象處於偏向鎖時,Mark Word中的thread id 可能是正在執行同步塊的線程的id,也可能是上次執行完已經釋放偏向鎖的thread id,主要是為了上次持有偏向鎖的這個線程在下次執行同步塊時,判斷Mark Word中的thread id相同就可以直接執行,而不用通過CAS操作去將自己的thread id設置到鎖對象Mark Word中。
這是偏向鎖執行的大概流程:
重量級鎖依賴於底層的操作系統的Mutex Lock來實現的,但是由於使用Mutex Lock需要將當前線程掛起並從用戶態切換到內核態來執行,這種切換的代價是非常昂貴的,而在大部分時候可能並沒有多線程競爭,只是這段時間是線程A執行同步塊,另外一段時間是線程B來執行同步塊,僅僅是多線程交替執行,並不是同時執行,也沒有競爭,如果採用重量級鎖效率比較低。以及在重量級鎖中,沒有獲得鎖的線程會阻塞,獲得鎖之後線程會被喚醒,阻塞和喚醒的操作是比較耗時間的,如果同步塊的代碼執行比較快,等待鎖的線程可以進行先進行自旋操作(就是不釋放CPU,執行一些空指令或者是幾次for循環),等待獲取鎖,這樣效率比較高。所以輕量級鎖天然瞄準不存在鎖競爭的場景,如果存在鎖競爭但不激烈,仍然可以用自旋鎖優化,自旋失敗后再升級為重量級鎖。
JVM會為每個線程在當前線程的棧幀中創建用於存儲鎖記錄的空間,我們稱為Displaced Mark Word。如果一個線程獲得鎖的時候發現是輕量級鎖,會把鎖的Mark Word複製到自己的Displaced Mark Word裏面。
然後線程嘗試用CAS操作將鎖的Mark Word替換為自己線程棧中拷貝的鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示Mark Word已經被替換成了其他線程的鎖記錄,說明在與其它線程競爭鎖,當前線程就嘗試使用自旋來獲取鎖。
自旋:不斷嘗試去獲取鎖,一般用循環來實現。
自旋是需要消耗CPU的,如果一直獲取不到鎖的話,那該線程就一直處在自旋狀態,白白浪費CPU資源。
JDK採用了適應性自旋,簡單來說就是線程如果自旋成功了,則下次自旋的次數會更多,如果自旋失敗了,則自旋的次數就會減少。
自旋也不是一直進行下去的,如果自旋到一定程度(和JVM、操作系統相關),依然沒有獲取到鎖,稱為自旋失敗,那麼這個線程會阻塞。同時這個鎖就會升級成重量級鎖。
在釋放鎖時,當前線程會使用CAS操作將Displaced Mark Word的內容複製回鎖的Mark Word裏面。如果沒有發生競爭,那麼這個複製的操作會成功。如果有其他線程因為自旋多次導致輕量級鎖升級成了重量級鎖,那麼CAS操作會失敗,此時會釋放鎖並喚醒被阻塞的線程。
輕量級鎖的加鎖解鎖流程圖:
當多個線程同時請求某個重量級鎖時,重量級鎖會設置幾種狀態用來區分請求的線程:
Contention List:所有請求鎖的線程將被首先放置到該競爭隊列,我也不知道為什麼網上的文章都叫它隊列,其實這個隊列是先進后出的,更像是棧,就是當Entry List為空時,Owner線程會直接從Contention List的隊列尾部(后加入的線程中)取一個線程,讓它成為OnDeck線程去競爭鎖。(主要是剛來獲取重量級鎖的線程是回進行自旋操作來獲取鎖,獲取不到才會進從Contention List,所以OnDeck線程主要與剛進來還在自旋,還沒有進入到Contention List的線程競爭)
Entry List:Contention List中那些有資格成為候選人的線程被移到Entry List,主要是為了減少對Contention List的併發訪問,因為既會添加新線程到隊尾,也會從隊尾取線程。
Wait Set:那些調用wait方法被阻塞的線程被放置到Wait Set。
OnDeck:任何時刻最多只能有一個線程正在競爭鎖,該線程稱為OnDeck。
Owner:獲得鎖的線程稱為Owner
。
!Owner:釋放鎖的線程
流程圖如下:
步驟1是線程在進入Contention List時阻塞等待之前,程會先嘗試自旋使用CAS操作獲取鎖,如果獲取不到就進入Contention List隊列的尾部。
步驟2是Owner線程在解鎖時,如果Entry List為空,那麼會先將Contention List中隊列尾部的部分線程移動到Entry List
步驟3是Owner線程在解鎖時,如果Entry List不為空,從Entry List中取一個線程,讓它成為OnDeck線程,Owner線程並不直接把鎖傳遞給OnDeck線程,而是把鎖競爭的權利交給OnDeck,OnDeck需要重新競爭鎖,JVM中這種選擇行為稱為 “競爭切換”。(主要是與還沒有進入到Contention
List,還在自旋獲取重量級鎖的線程競爭)
步驟4就是OnDeck線程獲取到鎖,成為Owner線程進行執行。
步驟5就是Owner線程調用鎖對象的wait()方法進行等待,會移動到Wait Set中,並且會釋放CPU資源,也同時釋放鎖,
步驟6.就是當其他線程調用鎖對象的notify()方法,之前調用wait方法等待的這個線程才會從Wait Set移動到Entry List,等待獲取鎖。
偏向鎖由於不涉及到多個線程競爭,所以談不上公平不公平,輕量級鎖獲取鎖的方式是多個線程進行自旋操作,然後使用用CAS操作將鎖的Mark Word替換為指向自己線程棧中拷貝的鎖記錄的指針,所以誰能獲得鎖就看運氣,不看先後順序。重量級鎖不公平主要在於剛進入到重量級的鎖的線程不會直接進入Contention List隊列,而是自旋去獲取鎖,所以後進來的線程也有一定的幾率先獲得到鎖,所以是不公平的。
因為那些處於ContetionList、EntryList、WaitSet中的線程均處於阻塞狀態,阻塞操作由操作系統完成(在Linxu下通過pthread_mutex_lock函數)。線程被阻塞后便進入內核(Linux)調度狀態,這個會導致系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能。如果同步塊中代碼比較少,執行比較快的話,後進來的線程先自旋獲取鎖,先執行,而不進入阻塞狀態,減少額外的開銷,可以提高系統吞吐量。
偏向鎖升級為輕量級鎖:
就是有不同的線程競爭鎖時。具體來看就是當一個線程發現當前鎖狀態是偏向鎖,然後鎖對象存儲的Thread id是其他線程的id,並且去Thread id對應的線程棧查詢到的lock record的obj字段不為null(代表當前持有偏向鎖的線程還在執行同步塊)。那麼該偏向鎖就會升級成輕量級鎖。
輕量級鎖升級為重量級鎖:
就是在輕量級鎖中,沒有獲取到鎖的線程進行自旋,自旋到一定次數還沒有獲取到鎖就會進行鎖升級,因為自旋也是佔用CPU的,長時間自旋也是很耗性能的。
鎖降級
因為如果沒有多線程競爭,還是使用重量級鎖會造成額外的開銷,所以當JVM進入SafePoint安全點(可以簡單的認為安全點就是所有用戶線程都停止的,只有JVM垃圾回收線程可以執行)的時候,會檢查是否有閑置的Monitor,然後試圖進行降級。
篇幅有限,下面是各種鎖的優缺點,來自《併發編程的藝術》:
| 鎖 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。 | 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 | 適用於只有一個線程訪問同步塊場景。 |
| 輕量級鎖 | 競爭的線程不會阻塞,提高了程序的響應速度。 | 如果始終得不到鎖競爭的線程使用自旋會消耗CPU。 | 追求響應時間。同步塊執行速度非常快。 |
| 重量級鎖 | 線程競爭不使用自旋,不會消耗CPU。 | 線程阻塞,響應時間緩慢。 | 追求吞吐量。同步塊執行速度較長。 |
參考鏈接:
https://github.com/farmerjohngit/myblog/issues/12
http://redspider.group:4000/article/02/9.html
https://blog.csdn.net/bohu83/article/details/51141836
https://blog.csdn.net/Dev_Hugh/article/details/106577862
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※想知道最厲害的網頁設計公司"嚨底家"!
※別再煩惱如何寫文案,掌握八大原則!
※產品缺大量曝光嗎?你需要的是一流包裝設計!
【摘要】對於mysql主備實例,seconds_behind_master是衡量master與slave之間延時的一個重要參數。通過在slave上執行”show slave status;”可以獲取seconds_behind_master的值。
對於mysql主備實例,seconds_behind_master是衡量master與slave之間延時的一個重要參數。通過在slave上執行”show slave status;”可以獲取seconds_behind_master的值。
Definition:The number of seconds that the slave SQL thread is behind processing the master binary log.
Type:time_t(long)
計算方式如下:
rpl_slave.cc::show_slave_status_send_data() if ((mi->get_master_log_pos() == mi->rli->get_group_master_log_pos()) && (!strcmp(mi->get_master_log_name(), mi->rli->get_group_master_log_name()))) { if (mi->slave_running == MYSQL_SLAVE_RUN_CONNECT) protocol->store(0LL); else protocol->store_null(); } else { long time_diff = ((long)(time(0) - mi->rli->last_master_timestamp) - mi->clock_diff_with_master); protocol->store( (longlong)(mi->rli->last_master_timestamp ? max(0L, time_diff) : 0)); }
主要分為以下兩種情況:
• SQL線程等待IO線程獲取主機binlog,此時seconds_behind_master為0,表示備機與主機之間無延時;
• SQL線程處理relay log,此時seconds_behind_master通過(long)(time(0) – mi->rli->last_master_timestamp) – mi->clock_diff_with_master計算得到;
定義:
• 主庫binlog中事件的時間。
• type: time_t (long)
計算方式:
last_master_timestamp根據備機是否并行複製有不同的計算方式。
rpl_slave.cc:exec_relay_log_event() if ((!rli->is_parallel_exec() || rli->last_master_timestamp == 0) && !(ev->is_artificial_event() || ev->is_relay_log_event() || (ev->common_header->when.tv_sec == 0) || ev->get_type_code() == binary_log::FORMAT_DESCRIPTION_EVENT || ev->server_id == 0)) { rli->last_master_timestamp= ev->common_header->when.tv_sec + (time_t) ev->exec_time; DBUG_ASSERT(rli->last_master_timestamp >= 0); }
在該模式下,last_master_timestamp表示為每一個event的結束時間,其中when.tv_sec表示event的開始時間,exec_time表示事務的執行時間。該值的計算在apply_event之前,所以event還未執行時,last_master_timestamp已經被更新。由於exec_time僅在Query_log_event中存在,所以last_master_timestamp在應用一個事務的不同event階段變化。以一個包含兩條insert語句的事務為例,在該代碼段的調用時,打印出event的類型、時間戳和執行時間
create table t1(a int PRIMARY KEY AUTO_INCREMENT ,b longblob) engine=innodb; begin; insert into t1(b) select repeat('a',104857600); insert into t1(b) select repeat('a',104857600); commit;
10T06:41:32.628554Z 11 [Note] [MY-000000] [Repl] event_type: 33 GTID_LOG_EVENT 2020-02-10T06:41:32.628601Z 11 [Note] [MY-000000] [Repl] event_time: 1581316890 2020-02-10T06:41:32.628614Z 11 [Note] [MY-000000] [Repl] event_exec_time: 0 2020-02-10T06:41:32.628692Z 11 [Note] [MY-000000] [Repl] event_type: 2 QUERY_EVENT 2020-02-10T06:41:32.628704Z 11 [Note] [MY-000000] [Repl] event_time: 1581316823 2020-02-10T06:41:32.628713Z 11 [Note] [MY-000000] [Repl] event_exec_time: 35 2020-02-10T06:41:32.629037Z 11 [Note] [MY-000000] [Repl] event_type: 19 TABLE_MAP_EVENT 2020-02-10T06:41:32.629057Z 11 [Note] [MY-000000] [Repl] event_time: 1581316823 2020-02-10T06:41:32.629063Z 11 [Note] [MY-000000] [Repl] event_exec_time: 0 2020-02-10T06:41:33.644111Z 11 [Note] [MY-000000] [Repl] event_type: 30 WRITE_ROWS_EVENT 2020-02-10T06:41:33.644149Z 11 [Note] [MY-000000] [Repl] event_time: 1581316823 2020-02-10T06:41:33.644156Z 11 [Note] [MY-000000] [Repl] event_exec_time: 0 2020-02-10T06:41:43.520272Z 0 [Note] [MY-011953] [InnoDB] Page cleaner took 9185ms to flush 3 and evict 0 pages 2020-02-10T06:42:05.982458Z 11 [Note] [MY-000000] [Repl] event_type: 19 TABLE_MAP_EVENT 2020-02-10T06:42:05.982488Z 11 [Note] [MY-000000] [Repl] event_time: 1581316858 2020-02-10T06:42:05.982495Z 11 [Note] [MY-000000] [Repl] event_exec_time: 0 2020-02-10T06:42:06.569345Z 11 [Note] [MY-000000] [Repl] event_type: 30 WRITE_ROWS_EVENT 2020-02-10T06:42:06.569376Z 11 [Note] [MY-000000] [Repl] event_time: 1581316858 2020-02-10T06:42:06.569384Z 11 [Note] [MY-000000] [Repl] event_exec_time: 0 2020-02-10T06:42:16.506176Z 0 [Note] [MY-011953] [InnoDB] Page cleaner took 9352ms to flush 8 and evict 0 pages 2020-02-10T06:42:37.202507Z 11 [Note] [MY-000000] [Repl] event_type: 16 XID_EVENT 2020-02-10T06:42:37.202539Z 11 [Note] [MY-000000] [Repl] event_time: 1581316890 2020-02-10T06:42:37.202546Z 11 [Note] [MY-000000] [Repl] event_exec_time: 0
rpl_slave.cc mts_checkpoint_routine ts = rli->gaq->empty() ? 0 : reinterpret_cast<Slave_job_group *>(rli->gaq->head_queue())->ts; rli->reset_notified_checkpoint(cnt, ts, true); /* end-of "Coordinator::"commit_positions" */
在該模式下備機上存在一個分發隊列gaq,如果gaq為空,則設置last_commit_timestamp為0;如果gaq不為空,則此時維護一個checkpoint點lwm,lwm之前的事務全部在備機上執行完成,此時last_commit_timestamp被更新為lwm所在事務執行完成后的時間。該時間類型為time_t類型。
ptr_group->ts = common_header->when.tv_sec + (time_t)exec_time; // Seconds_behind_master related rli->rli_checkpoint_seqno++;
if (update_timestamp) { mysql_mutex_lock(&data_lock); last_master_timestamp = new_ts; mysql_mutex_unlock(&data_lock); }
在并行複製下,event執行完成之後才會更新last_master_timestamp,所以非并行複製和并行複製下的seconds_behind_master會存在差異。
定義:
• The difference in seconds between the clock of the master and the clock of the slave (second – first). It must be signed as it may be <0 or >0. clock_diff_with_master is computed when the I/O thread starts; for this the I/O thread does a SELECT UNIX_TIMESTAMP() on the master.
• type: long
rpl_slave.cc::get_master_version_and_clock() if (!mysql_real_query(mysql, STRING_WITH_LEN("SELECT UNIX_TIMESTAMP()")) && (master_res= mysql_store_result(mysql)) && (master_row= mysql_fetch_row(master_res))) { mysql_mutex_lock(&mi->data_lock); mi->clock_diff_with_master= (long) (time((time_t*) 0) - strtoul(master_row[0], 0, 10)); DBUG_EXECUTE_IF("dbug.mts.force_clock_diff_eq_0", mi->clock_diff_with_master= 0;); mysql_mutex_unlock(&mi->data_lock); }
該差值僅被計算一次,在master與slave建立聯繫時處理。
定義:
• the difference from the statement’s original start timestamp and the time at which it completed executing.
• type: unsigned long
struct timeval end_time; ulonglong micro_end_time = my_micro_time(); my_micro_time_to_timeval(micro_end_time, &end_time); exec_time = end_time.tv_sec - thd_arg->query_start_in_secs();
(1)time_t time(time_t timer) time_t為long類型,返回的數值僅精確到秒;
(2)int gettimeofday (struct timeval *tv, struct timezone *tz) 可以獲得微秒級的當前時間;
(3)timeval結構
#include <time.h> stuct timeval { time_t tv_sec; /*seconds*/ suseconds_t tv_usec; /*microseconds*/ }
使用seconds_behind_master衡量主備延時只能精確到秒級別,且在某些場景下,seconds_behind_master並不能準確反映主備之間的延時。主備異常時,可以結合seconds_behind_master源碼進行具體分析。
點擊關注,第一時間了解華為雲新鮮技術~
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※別再煩惱如何寫文案,掌握八大原則!
※教你寫出一流的銷售文案?
※超省錢租車方案
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※產品缺大量曝光嗎?你需要的是一流包裝設計!
轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。
原文出處:https://dzone.com/articles/dry-dont-repeat-yourself
我們之前就發過一篇相關的文章:https://www.cnblogs.com/powertoolsteam/p/12758496.html 其中也提到了包括DRY在內的一些軟件開發的原則。
DRY 是軟件開發的原則之一,其目的主要是為了避免代碼重複,指導開發者盡量以抽象的思維去解決重複,基本上是,當您發現自己一遍又一遍地編寫相同的代碼時,可能會有更好的方法。
讓我們先看一個例子,看看這個例子是否可以改進,以及如何通過重構來避免代碼重複。
這裡有一個簡單的Report類,該類接收一些數據並通過控制台以格式化的方式直接輸出。
我們這裏使用php的一個代碼片段來舉例,相信大家對代碼的結構和想要完成的工作都不難理解,所以為了大家更容易理解,我只對一些下面用到的php函數定義做一個解釋:
class Report
{
public function show(array $data)
{
echo "Report: " . ucwords(strtolower($data["name"])) . "\n";
echo "Product: " . ucwords(strtolower($data["product"])) . "\n";
echo "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
echo "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
echo "Total: " . $data["total"] . "\n";
echo "Average x day: " . floor($data["total"] / 365) . "\n";
echo "Average x week: " . floor($data["total"] / 52) . "\n";
}
}
可以看到,上面的代碼完成目標是沒有任何問題的。
這時我們對Report類提出一個新的需求:把所有字符串也可以保存到文件中。
我們經過一通複製和粘貼上面的代碼,新建一個名為saveToFile的函數,就可以很快的完成這個需求,代碼如下:
class Report
{
public function show(array $data)
{
echo "Report: " . ucwords(strtolower($data["name"])) . "\n";
echo "Product: " . ucwords(strtolower($data["product"])) . "\n";
echo "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
echo "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
echo "Total: " . $data["total"] . "\n";
echo "Average x day: " . floor($data["total"] / 365) . "\n";
echo "Average x week: " . floor($data["total"] / 52) . "\n";
echo "Average x month: " . floor($data["total"] / 12) . "\n";
}
public function saveToFile(array $data)
{
$report = '';
$report .= "Report: " . ucwords(strtolower($data["name"])) . "\n";
$report .= "Product: " . ucwords(strtolower($data["product"])) . "\n";
$report .= "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
$report .= "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
$report .= "Total: " . $data["total"] . "\n";
$report .= "Average x day: " . floor($data["total"] / 365) . "\n";
$report .= "Average x week: " . floor($data["total"] / 52) . "\n";
$report .= "Average x month: " . floor($data["total"] / 12) . "\n";
file_put_contents("./report.txt", $report);
}
}
那麼,上面的代碼能夠滿足我們提出的需求嗎?答案當然“是的”。但是從技術角度來看,這段代碼似乎是有些問題的,它的重複代碼到處都是。無論是對代碼閱讀及後期維護來講,這都是一場噩夢。
所以我們需要進行一些重構,抽象能抽象的方法,讓冗繁的代碼變得更簡潔。
首先,我們對Report類進行功能上的抽象,生成報告並輸出一共可以分為兩個功能,一個只負責創建Report,一個只負責如何處理Report,那麼讓我們開始重構吧。
class Report
{
public function show(array $data)
{
echo $this->createReport($data);
}
public function saveToFile(array $data)
{
file_put_contents("./report.txt", $this->createReport($data));
}
private function createReport(array $data): string
{
$report = '';
$report .= "Report: " . ucwords(strtolower($data["name"])) . "\n";
$report .= "Product: " . ucwords(strtolower($data["product"])) . "\n";
$report .= "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
$report .= "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
$report .= "Total: " . $data["total"] . "\n";
$report .= "Average x day: " . floor($data["total"] / 365) . "\n";
$report .= "Average x week: " . floor($data["total"] / 52) . "\n";
$report .= "Average x month: " . floor($data["total"] / 12) . "\n";
return $report;
}
}
現在看起來更清楚一些,對嗎?
下面我們還有函數使用重複的問題要解決,例如,Report和Products的名稱函數使用重複:
$report .= "Report: " . ucwords(strtolower($data["name"])) . "\n"; $report .= "Product: " . ucwords(strtolower($data["product"])) . "\n";
我們可以將這些轉換抽象為一個新的函數:
private function normalizeName($name): string
{
return ucwords(strtolower($name));
}
另一個重複:日期格式。
$report .= "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
$report .= "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
讓我們將其抽象為:
private function formatDate($date): string
{
return date("Y/m/d", $date);
}
最後一個:平均值計算。
$report .= "Average x day: " . floor($data["total"] / 365) . "\n"; $report .= "Average x week: " . floor($data["total"] / 52) . "\n"; $report .= "Average x month: " . floor($data["total"] / 12) . "\n";
儘管計算結果並不完全相同,但執行的操作大家是一致的,所以可以抽象為如下:
private function calculateAverage(array $data, $period): string
{
return floor($data["total"] / $period);
}
所以,經過了一番重構,最終的Report類變為了如下:
class Report
{
public function show(array $data)
{
echo $this->createReport($data);
}
public function saveToFile(array $data)
{
file_put_contents("./report.txt", $this->createReport($data));
}
private function createReport(array $data)
{
$report = '';
$report .= "Report: " . $this->normalizeName($data["name"]) . "\n";
$report .= "Product: " . $this->normalizeName($data["product"]) . "\n";
$report .= "Start date: " . $this->formatDate($data["startDate"]) . "\n";
$report .= "End date: " . $this->formatDate($data["endDate"]) . "\n";
$report .= "Total: " . $data["total"] . "\n";
$report .= "Average x day: " . $this->calculateAverage($data, 365) . "\n";
$report .= "Average x week: " . $this->calculateAverage($data, 52) . "\n";
$report .= "Average x month: " . $this->calculateAverage($data, 12) . "\n";
return $report;
}
private function formatDate($date): string
{
return date("Y/m/d", $date);
}
private function calculateAverage(array $data, $period): string
{
return floor($data["total"] / $period);
}
private function normalizeName($name): string
{
return ucwords(strtolower($name));
}
}
這是一個簡單的例子,實際情況可能比這要更加複雜的多,但我僅想通過這個實例向大家說明一個問題,那就是避免重複代碼的重要性及我們如何通過重構去處理重複代碼。
有時候重複一次相同的代碼可能沒問題,但是當第三次我們寫出相同的代碼時,那就說明是時候重構你的代碼了。
請記住DRY原則,並隨時抱着不要重複自己代碼的想法去完成開發工作。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※別再煩惱如何寫文案,掌握八大原則!
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※超省錢租車方案
※教你寫出一流的銷售文案?
※網頁設計最專業,超強功能平台可客製化
※產品缺大量曝光嗎?你需要的是一流包裝設計!
在上一篇關於Spring的@Import註解的文章《【Spring註解驅動開發】使用@Import註解給容器中快速導入一個組件》中,我們簡單介紹了如何使用@Import註解給容器中快速導入一個組件,而我們知道,@Import註解總共包含三種使用方法,分別為:直接填class數組方式;ImportSelector方法(重點);ImportBeanDefinitionRegistrar方式。那麼,今天,我們就一起來學習關於@Import註解非常重要的第二種方式:ImportSelector方式。
項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation
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接口展開講的話,可以單獨寫一篇文章,那我就放在下一篇文章中講吧,這裏就不贅述了,嘿嘿。
首先,我們創建一個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,如下所示。
package io.mykit.spring.plugins.register.bean;
/**
* @author binghe
* @version 1.0.0
* @description 測試ImportSelector
*/
public class User {
}
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註解驅動開發不再迷茫。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※教你寫出一流的銷售文案?
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※回頭車貨運收費標準
※別再煩惱如何寫文案,掌握八大原則!
※超省錢租車方案
※產品缺大量曝光嗎?你需要的是一流包裝設計!
所有知識體系文章,GitHub已收錄,歡迎老闆們前來Star!
GitHub地址: https://github.com/Ziphtracks/JavaLearningmanual
觸發器(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 //
首先,我先展示一下創建的兩張表,因為創建的表很簡單,這裏我沒有提供庫表操作的SQL命令。
tb_class
image-20200611205404311
employee
image-20200611205435284
其次,創建了一個含有update操作的存儲過程
1delimiter //
2create procedure update_emp(in i int, in 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(2, 110);
觸發器在此場景的作用分析
當employee表發生update操作時,觸發器就對tb_class表中的num值做修改。
執行結果發現,我們在使用函數將employee表中id為2員工的phone修改為110后,觸發器監視到employee表中發生了update更新操作,就執行了內部SQL語句,也就是將tb_class表中id為1的num值自增1。
image-20200611213411229 image-20200611213432459
查看已有觸發器:
show triggers刪除已有觸發器:
drop trigger 觸發器名稱
這裏擴展,在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。
- 在觸發目標上執行insert操作後會有一個新行,如果在觸發事件中需要用到這個新行的變量,可以用new關鍵字表示
- 在觸發目標上執行delete操作後會有一箇舊行,如果在觸發事件中需要用到這箇舊行的變量,可以用old關鍵字表示
- 在觸發目標上執行update操作后原紀錄是舊行,新記錄是新行,可以使用new和old關鍵字來分別操作
| 觸發語句 | old | new |
|---|---|---|
| insert | 所有字段都為空 | 將要插入的數據 |
| update | 更新以前該行的值 | 更新后的值 |
| delete | 刪除以前該行的值 | 所有字段都為空 |
關於old和new對象的應用,我在這裏沒有展開演示。只是將第八章的綜合案例結合了old和new對象實現。綜合案例中詳細講解了MySQL觸發器的使用!
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(32) not 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(32) not 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)
在此表結構中,如果一位新同學來到學校學習,意味着某一個班級中會多出一名學生。假設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學生記錄的同時也修改了班級表中的小班學生數量。
刪除學生與添加學生十分相似,刪除學生相當於是添加學生的逆過程。如果以為學生退學了或者讀完了幼兒園離開學校了,就意味着班級中少了一位學生。假設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。
因為我已經詳細講解了添加學生與刪除學生,所以刪除班級我就不再作過多的贅述了。那就直接說核心內容吧。刪除一個班級級別比如:刪除小班之前要把小班內的所有學生也被刪除了,因為兩個表是主外鍵關聯的。如果只刪除了小班,而沒有刪除小班內的所有學生,那麼原小班內的所有學生現在屬於哪個班級呢,就不知道了吧!所以要在刪除小班之前刪除小班內的所有學生。
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);
執行結果為既刪除了小班,又刪除小班內的所有學生。
觸發器衝突問題其實就是關聯問題。為什麼這麼說呢?就說以下剛才這三個案例中出現的觸發器衝突問題。
如果我們在寫觸發器的時候,將添加學生、刪除學生和刪除班級的觸發器都寫在一個查詢模板中。你會發現當你在刪除班級的時候,會報錯。显示如下信息:
image-20200612004546204
這是為什麼呢?
仔細想想,我們將在案例中有兩個是同一個表中的刪除觸發器。刪除班級的觸發器中定義的是刪除班級時觸發刪除學生,而刪除學生的觸發器中定義的是班級人數減一。你發現了沒,觸發器被連着觸發了。如下變化:
image-20200612005312835
我們通過刪除班級案例了解了,刪除班級之前需要把班級內所有學生刪除掉。正因為如此,我們在刪除班級之前已經把所有學生都刪除了,導致在刪除學生的時候觸發了班級人數減一的觸發器,該觸發器在執行過程中修改了已經被刪除班級的學生人數。這問題就出在這裏了,班級已經刪除了,怎麼修改一個本就沒有的班級內的人數呢?對吧!
解決觸發器衝突
為解決這個場景的觸發器衝突問題,我們只能取捨一個觸發器。於是,就通過命令刪除了刪除學生案例中使用的那個觸發器,刪除后刪除班級就可以成功執行觸發了!
1drop trigger t_delete_stu;
注意: 由於存在觸發器衝突問題,我們在實際開發中需要認真考量定義觸發器!
各大論壇等等,相信在大家的文章中都不推薦使用觸發器,而是推薦使用存儲過程程序,這是為什麼呢?
首先,存儲過程程序分為存儲過程、儲存過程函數和觸發器。也就是說這三種都是存儲過程的使用都是存儲過程的表現形式。
如果場景在數據量和併發量都很大的情況下,使用觸發器、存儲過程再加上幾個事務等等,很容易出現死鎖。而且在使用觸發器的時候,也會出現衝突,出現問題時,我們需要追溯的代碼就需要從一個觸發器到另一個觸發器……從而影響開發效率。從性能上看,觸發器也是存儲過程程序的一種,它也並沒有展現出多少性能上的優勢。由於觸發器寫起來比較隱蔽,容易被開發人員忽略,而且隱式調用觸發器不易於排除依賴,對後期維護不是很友好!
所以在開發中,觸發器是很少用到的。那為什麼我還花時間大篇幅的講解MySQL觸發器呢?原因很簡單,是因為需要擴展自己的知識儲備。開發中的使用問題和是否被大家摒棄,不是你拒絕學習知識的理由。之所以存在就有它存在的道理,我們在學習的道路中不斷擴充自己的知識儲備即可。
假如有一天你的同事聊起觸發器,你也能和他們聊聊你對觸發器的見解是哈?如果你根據從未了解過此知識呢?那性質就不一樣了,相信大家都懂吧!
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※超省錢租車方案
※別再煩惱如何寫文案,掌握八大原則!
※回頭車貨運收費標準
※教你寫出一流的銷售文案?
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
摘錄自2020年8月26日民視新聞報導
非洲烏干達保育區的25歲明星大猩猩「拉飛奇」,六月被盜獵者射殺,7月1日保育員發現牠失蹤,搜救隊隔天就發現牠的遺體。驗屍發現,牠腹部和內臟有遭利器穿刺的痕跡。
嫌犯自稱是因為武漢肺炎觀光收入銳減、沒錢買食物吃,盜獵小動物的時候以為猩猩要攻擊,出於自衛以長茅殺死大猩猩。警方發現他已獵殺一隻羚羊,麂羚和野豬,住處還發現多種野生動物的肉,被判處11年有期徒刑,成為該國首例。
大猩猩屬瀕危動物,剛果,烏干達和盧安達保護區內僅剩約1000隻,去年光是大猩猩觀光收入就多達2550萬美金。今年97%行程因疫情邊境封鎖而取消,多數業者不堪負荷倒閉。
生物多樣性
國際新聞
烏干達
大猩猩
盜獵
武漢肺炎
動物與大環境變遷
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※別再煩惱如何寫文案,掌握八大原則!
※教你寫出一流的銷售文案?
※超省錢租車方案
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※產品缺大量曝光嗎?你需要的是一流包裝設計!
摘錄自2020年8月27日中央社報導
阿富汗官員今(26日)表示,暴雨引發的暴洪,肆虐首都喀布爾北方一座城市,造成至少100人罹難、約100人受傷以及將近500棟民宅被毀,罹難者包括許多小孩。
軍方救難人員從巴萬省(Parwan)首府查里喀爾市(Charikar)倒塌建築物的瓦礫堆中,拉出數十名受害人。當地昨晚的暴雨,助長猛烈洪水侵襲這座城市。
阿富汗國家災難管理局表示,罹難人數攀升至100人,另有約100人受傷以及將近500棟民宅被洪水摧毀。
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※別再煩惱如何寫文案,掌握八大原則!
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※超省錢租車方案
※教你寫出一流的銷售文案?
※網頁設計最專業,超強功能平台可客製化
※產品缺大量曝光嗎?你需要的是一流包裝設計!