web應用安全框架選型:Spring Security與Apache Shiro

一、 SpringSecurity 框架簡介

官網:

源代碼:

Spring Security 是強大的,且容易定製的,基於Spring開發的實現認證登錄與資源授權的應用安全框架。

SpringSecurity 的核心功能:

  • Authentication:認證,用戶登陸的驗證(解決你是誰的問題)
  • Authorization:授權,授權系統資源的訪問權限(解決你能幹什麼的問題)
  • 安全防護,防止跨站請求,session 攻擊等

二、比較一下shiro與Spring Security

目前在java web應用安全框架中,與Spring Security形成直接競爭的就是shiro,二者在核心功能上幾乎差不多,但從使用的角度各有優缺點。筆者認為:沒有最好的,只有最合適的。

2.1 用戶量

從使用情況上看,二者都在逐步提高使用量。shiro的使用量一直高於spring security.

2.2.使用的方便程度

通常來說,shiro入門更加容易,使用起來也非常簡單,這也是造成shiro的使用量一直高於Spring Security的主要原因。但是從筆者的角度來看,二者其實都簡單,我說說我的理由:

  • 在沒有Spring Boot之前,Spring Security的大部分配置要通過XML實現,配置還是還是非常複雜的。但是有了 Spring Boot之後,這一情況已經得到顯著改善。
  • Spring Security之所以看上去比shiro更複雜,其實是因為它引入了一些不常用的概念與規則。大家應該都知道2/8法則,這在Spring Security裏面體現的特別明顯,如果你只學Spring Security最重要的那20%,這20%的複雜度和shiro基本是一致的。也就是說,不重要的那80%,恰恰是Spring Security比shiro的“複雜度”。

也就是說,如果有人能幫你把Spring Security最重要的那20%摘出來,二者的入門門檻、複雜度其實是差不太多的。

2.3.社區支持

Spring Security依託於Spring龐大的社區支持,這點自不必多說。shiro屬於apache社區,因為它的廣泛使用,文檔也非常的全面。二者從社區支持來看,幾乎不相上下。

但是從社區發展的角度看,Spring Security明顯更佔優勢,隨着Spring Cloud、Spring Boot、Spring Social的長足進步,這種優勢會越來越大。因為Spring Security畢竟是Spring的親兒子,Spring Security未來在於Spring系列框架集成的時候一定會有更好的融合性,前瞻性、兼容性!這也是為什麼我們要學Spring Security的主要原因!

2.4.功能豐富性

Spring Security因為它的複雜,所以從功能的豐富性的角度更勝一籌。其中比較典型的如:

  • Spring Security默認含有對OAuth2.0的支持,與Spring Social一起使用完成社交媒體登錄也比較方便。shiro在這方面只能靠自己寫代碼實現。
  • 還有一種普遍說法:Spring Security在網絡安全的方面下的功夫更多,但是筆者並未有非常直接的感受,有可能出現安全問題的時候才會感到不夠安全的痛。

三、總結

如果你只是想實現一個簡單的web應用,shiro更加的輕量級,學習成本也更低。如果您正在開發一個分佈式的、微服務的、或者與Spring Cloud系列框架深度集成的項目,筆者還是建議您使用Spring Security。

期待您的關注

  • 博主最近新寫了一本書:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

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

【其他文章推薦】

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

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

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

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

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

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

[ASP.NET Core 3框架揭秘] 依賴注入[8]:服務實例的生命周期

生命周期決定了IServiceProvider對象採用怎樣的方式提供和釋放服務實例。雖然不同版本的依賴注入框架針對服務實例的生命周期管理採用了不同的實現,但總的來說原理還是類似的。在我們提供的中,我們已經模擬了三種生命周期模式的實現原理,接下來我們結合“服務範圍”的概念來對這個話題做進一步講述。

一、服務範圍(Service Scope)

對於依賴注入框架採用的三種生命周期模式(Singleton、Scoped和Transient)來說,Singleton和Transient都具有明確的語義,但是Scoped代表一種怎樣的生命周期模式,很多初學者往往搞不清楚。這裏所謂的Scope指的是由IServiceScope接口表示的“服務範圍”,該範圍由IServiceScopeFactory接口表示的“服務範圍工廠”來創建。如下面的代碼片段所示,IServiceProvider的擴展方法CreateScope正是利用提供的IServiceScopeFactory服務實例來創建作為服務範圍的IServiceScope對象。

public interface IServiceScope : IDisposable
{
    IServiceProvider ServiceProvider { get; }
}

public interface IServiceScopeFactory
{
    IServiceScope CreateScope();
}

public static class ServiceProviderServiceExtensions
{
   public static IServiceScope CreateScope(this IServiceProvider provider) => provider.GetRequiredService<IServiceScopeFactory>().CreateScope();
}

任何一個IServiceProvider對象都可以利用其註冊的IServiceScopeFactory服務創建一個代表服務範圍的IServiceScope對象,後者代表的“範圍”內具有一個新創建的IServiceProvider對象(對應着接口IServiceScope的ServiceProvider屬性),該對象與當前IServiceProvider在邏輯上具有如下圖所示的“父子關係”。

如上圖所示的樹形層次結構只是一種邏輯結構,從對象引用層面來看,通過某個IServiceScope封裝的IServiceProvider對象不需要知道自己的“父親”是誰,它只關心作為根節點的IServiceProvider在哪裡就可以了。下圖從物理層面揭示了IServiceScope / IServiceProvider對象之間的關係,任何一個IServiceProvider對象都具有針對根容器的引用。

二、服務實例的提供

只有在充分了解IServiceScope對象的創建過程以及它與IServiceProvider對象之間的關係之後,我們才會對三種生命周期管理模式(Singleton、Scoped和Transient)具有深刻的認識。就服務實例的提供方式來說,它們之間具有如下的差異:

  • Singleton:IServiceProvider對象創建的服務實例保存在作為根容器的IServiceProvider對象上,所以多個同根的IServiceProvider對象提供的針對同一類型的服務實例都是同一個對象。
  • Scoped:IServiceProvider對象創建的服務實例由自己保存,所以同一個IServiceProvider對象提供的針對同一類型的服務實例均是同一個對象。
  • Transient:針對每一次服務提供請求,IServiceProvider對象總是創建一個新的服務實例

三、服務實例的釋放

IServiceProvider除了為我們提供所需的服務實例之外,對於由它提供的服務實例,它還肩負起回收釋放的責任。這裏所說的回收釋放與.NET Core自身的垃圾回收機制無關,僅僅針對於自身類型實現了IDisposable或者IAsyncDisposable接口的服務實例(下面簡稱為Disposable服務實例),針對服務實例的釋放體現為調用它們的Dispose或者DisposeAsync方法。IServiceProvider對象針對服務實例採用的回收釋放策略取決於採用的生命周期模式,具體策略主要體現為如下兩點:

  • Singleton:提供Disposable服務實例保存在作為根容器的IServiceProvider對象上,只有在這個IServiceProvider對象被釋放的時候這些Disposable服務實例才能被釋放。
  • Scoped和Transient:當前IServiceProvider對象會保存由它提供的Disposable服務實例,當自己被釋放的時候,這些Disposable服務實例就會被釋放。

綜上所述,每個作為依賴注入容器的IServiceProvider對象都具有如下圖所示的兩個列表來存放服務實例,我們將它們分別命名為“Realized Services”和“Disposable Services”,對於一個作為非根容器的IServiceProvider對象來說,由它提供的Scoped服務保存在自身的Realized Services列表中,Singleton服務實例則會保存在根容器的Realized Services列表中。如果服務實現類型實現了IDisposable或者IAsyncDisposable接口,Scoped和Transient服務實例會被保存到自身的Disposable Services列表中,而Singleton服務實例則會保存到根容器的Disposable Services列表中。

對於作為根容器的IServiceProvider對象來說,Singleton和Scoped模式對它來說是兩種等效的生命周期模式,由它提供的Singleton和Scoped服務實例會被存放到自身的Realized Services列表中,而所有需要被釋放的服務實例則被存放到Disposable Services列表中。當某個IServiceProvider對象被用於提供針對指定類型的服務實例時,它會根據服務類型提取出表示服務註冊的ServiceDescriptor對象並根據它得到對應的生命周期模式:

  • 如果生命周期模式為Singleton,並且作為根容器的Realized Services列表中包含對應的服務實例,它將作為最終提供的服務實例。如果這樣的服務實例尚未創建,那麼新的服務將會被創建出來並作為提供的服務實例。這個服務實例會被添加到根容器的Realized Services列表中。如果實例類型實現了IDisposable或者IAsyncDisposable接口,創建的服務實例會被添加到根容器的Disposable Services列表中。
  • 如果生命周期為Scoped,那麼IServiceProvider會先確定自身的Realized Services列表中是否存在對應的服務實例,存在的服務實例將作為最終的返回值。如果Realized Services列表不存在對應的服務實例,那麼新的服務實例會被創建出來。在作為最終的服務實例被返回之前,創建的服務實例會被添加到自身的Realized Services列表中,如果實例類型實現了IDisposable或者IAsyncDisposable接口,創建的服務實例會被添加到自身的Disposable Services列表中。
  • 如果提供服務的生命周期為Transient,那麼IServiceProvider會直接創建一個新的服務實例。在作為最終的服務實例被返回之前,如果實例類型實現了IDisposable或者IAsyncDisposable接口,創建的服務實例會被添加到自身的Disposable Services列表中。

對於非根容器的IServiceProvider對象來說,它的生命周期是由“包裹”着它的IServiceScope對象控制的。從前面給出的定義可以看出IServiceScope實現了IDisposable接口,Dispose方法的執行不僅標志著當前服務範圍的終結,也意味着對應IServiceProvider對象生命周期的結束。

當代表服務範圍的IServiceScope對象的Dispose方法被調用的時候,它會調用對應IServiceProvider對象的Dispose方法。一旦IServiceProvider對象因自身Dispose方法的調用而被釋放的時候,它會從自身的Disposable Services列表中提取出所有需要被釋放的服務實例,並調用它們的Dispose或者DisposeAsync方法。在這之後,Disposable Services和Realized Services列表會被清空,列表中的服務實例和IServiceProvider對象自身會成為垃圾對象被GC回收。

四、ASP.NET Core應用

依賴注入框架所謂的服務範圍在ASP.NET Core應用中具有明確的邊界,指的是針對每個HTTP請求的上下文,也就是服務範圍的生命周期與每個請求上下文綁定在一起。如下圖所示,ASP.NET Core應用中用於提供服務實例的IServiceProvider對象分為兩種類型,一種是作為根容器並與應用具有相同生命周期的IServiceProvider對象,另一個類則是根據請求及時創建和釋放的IServiceProvider對象,我們一般將它們分別稱為ApplicationServicesRequestServices

在ASP.NET Core應用初始化過程(即請求管道構建過程)中使用的服務實例都是由ApplicationServices提供的。在具體處理每個請求時,ASP.NET Core框架會利用註冊的一个中間件來針對當前請求創建一個代表服務範圍的IServiceScope對象,該服務範圍提供的RequestServices用來提供當前請求處理過程中所需的服務實例。一旦服務請求處理完成,IServiceScoped對象代表的服務範圍被終結,在當前請求處理過程中的Scoped服務會變成垃圾對象並最終被GC回收。對於實現了IDisposable或者IAsyncDisposable接口的Scoped或者Transient服務實例來說,在變成垃圾對象之前,它們的Dispose或者DisposeAsync方法會被調用。

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

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

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

領導要求996,我拒絕了

互聯網公司程序員,前些天項目趕進度,被強制加班。 我們公司以前也是鼓勵員工加班,但比較隱晦, 不是強制的,而這次是上司直接發話,必須要加班,否則工作無法完成的責任會扣到你頭上。

被要求強行加班,無償的,而且是喪心病狂的996,我臉上顯得很平靜,但是內心一萬頭草泥馬在奔騰。當天晚上,我就發微信給上司,我說:

“趕項目我一定不拖進度,該完成的工作按時完成,但是你別讓我加班,第一天弄的太晚,直接影響第二天工作效率,這樣得不償失”

工作只是生活的一部分,如果每天9點以後下班,就表示沒時間陪家人,沒時間娛樂活動,沒時間弄好吃的,沒有時間做自己感興趣的事情,到家直接洗洗睡,第二天醒來繼續上班,這樣的生活豈不是很無趣,加班是罪魁禍首。

寫程序是腦力勞動與體力勞動的結合,聚精會神寫一整天代碼,效率很高,但到了下班點,整個人會非常疲乏,如果繼續工作,會影響第二天狀態,所以後面所謂的加班其實是在划水,根本做不了什麼東西。當然,也可以平均分佈工作和划水,正常上班時也不用那麼認真,那麼加班的時候好像還能做點東西, 但是一整天的總工作成果沒有變化。所以,與其把工作分佈到12個小時,還不如前8個小時多做產出,后4個小時下班回家,這樣有效工作量並不會減少,還有了自由時間, 只是看起來沒那麼积極,但不用怕完不成工作而被問責。

程序員俗稱碼農,也叫IT民工,這是自黑,可在不懂技術的領導眼中就跟搬磚工沒區別,在他們眼裡,程序員多加班一小時,就會多一小時工作成果,搬磚嘛,或多或少總能搬幾塊。他們不知道,寫程序雖然不像搞藝術,非常依賴靈感,沒靈感什麼都幹不成,但在精神良好、腦子靈活的狀態下,工作效率絕對要高於無精打采、混混沌沌的狀態,有時候幾小時搞不定的問題,忽然間靈光一閃就能解決,這就不是靠加班加出來的。良好的工作狀態下產生的工作成果不是靠堆時間可以趕超的。所以很多時候在一個不開明的領導指揮下,團隊所有人看似很努力的在加班工作,其實所花的時間都是沒有意義的冤枉時間,原本這些時間可以做更有意義的事情。

還有一種加班,更加無厘頭,這種加班叫做:我也不知道為什麼要加班,別人在加, 那麼我也加一會。

這種加班,到下班時間點后員工們手上的工作停下來了,但沒人動屁股,大家看網頁的看網頁,看視頻的看視頻,打遊戲的打遊戲,下班,不存在的。因為別人都在加班,我下班了,感覺就像在犯罪,有強烈的罪惡感。這種加班比前一種加班更加可惡,沒半分實際意義, 但是偏偏就很難打破。其實每個人都在抱怨,可又沒有人敢越雷池一步。

說實話我挺佩服能不加班的人,雖然加班有很多外部因素,比如工作真的忙,比如公司文化就是這樣,在比如領導犯渾,但加班表達出來的意思其實就是工作任務完不成了,要多花時間。那麼不加班也意味着在正常的工作時間內能游刃有餘的完成工作,是能力的體現。

我也想實現這個夢想,所以我拒絕加班,領導也同意了,他說:“那你自己看着辦吧, 我不強迫你”,然而我知道,我在這家公司只能在地板上混了,連天花板都別想碰到,更別談升級,但是我覺得值,因為我想要更多的自由生活時間。

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

【其他文章推薦】

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

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

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

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

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

【運維】記一次上線前的緊急定位與修復-獻上九條小經驗

1 簡介

本文介紹了作者所在團隊在某次上線前測試發現問題、定位問題並修復上線的過程,最後給出幾點經驗總結,希望對大家有用。

2 過程

(1)今天需要上線,但昨晚才合併了所有分支,時間很緊迫。不幸的是,打包測試后發現有一個Springboot應用(模塊R)啟動失敗,但進程沒有死,一直在輸出報錯日誌

(2)Google了相關的報錯日誌,並沒有找到相關信息。查看了模塊R的代碼變更,並沒有什麼改動,以為是環境問題;部署到其它環境后,發現問題依舊存在,而且這個問題從未出現過,基本排除環境問題,問題還是出在代碼上。

(3)啟動模塊R上一個版本(現生產環境)的代碼,正常啟動。所以問題還是出現模塊R的改動上。

(4)對比模塊R的發布包的新版本與生產版本的差異,發現第三方依賴包都一致,但自己項目的依賴包不同。

(5)想到一個有效的辦法,依次用生產版本替換掉自己項目的包,最終定位了問題出在通用模塊D上。

(6)查看模塊D的代碼變更記錄,改動比較大,比較難發現是哪裡的改動造成的。

(7)重新看日誌。為何要重看呢?並不是心血來潮,主要是想找關聯。既然已經知道了問題在哪個模塊代碼上,通過查看日誌與該模塊可能相關的信息,或許能找到蛛絲馬跡。

(8)果然!!!重新查看日誌發現,模塊R啟動時,報了一個其它錯誤ErrorA,但被後面不斷重複出現的錯誤ErrorB刷掉了,所以一開始並沒有注意到它。通過該報錯,與模塊D的代碼改動對比。終於定位出了問題!

(9)創建hotfix分支,修改代碼提交,重新merge,打包,測試通過,部署生產!!!

因為部署上線是有特定的時間窗口的,如果錯過了時間,就要下次再上線,還好及時定位,及時解決!

3 經驗總結

(1)不要放過任何日誌,特別是報錯的日誌,日誌是極其有用的。不要只看最後面的報錯,也不要只看最前面的報錯,任何報錯都可能提供新的方向和線索。如果日誌信息不夠,可以嘗試打開debug模式,會有大量的日誌信息,當然也要求你有足夠強的過濾和整理信息的能力。

(2)提取有用日誌,可以用greptailless等linux命令。

(3)組件化、模塊化很重要,能快速縮小問題範圍。能通過只回退某個模塊實現部分功能先上線。

(4)善用對比工具,如diff命令,BeyondCompare軟件等。

(5)善用代碼變更記錄,這是版本控制給我們帶來的好處,可以方便我們對比代碼改動了什麼,什麼時候改的,能加速問題定位;也能及時回退代碼。

(6)上線前要做充分的測試。這次問題的出現項目流程上的原因在於沒有進行充分的測試。(1)寫代碼的人修改了通用模塊,卻沒有測試依賴它的其它模塊的功能會不會受影響,而只測試了自己那一部分;(2)合併代碼后,沒有足夠的時間來進行測試。部署前一天,才合併了代碼打包測試。所以時間非常緊迫,在短時間要定位問題並解決,容易造成壓力。

(7)要有獨立的測試環境。這個是導致方向性錯誤的原因,經過是這樣的:A同學打包了自己的分支,這時剛好B同學稍晚一點也打包了分支,而打包的環境只有一個,B同學的包覆蓋了A同學的包。所以在A部署的時候,實際用了B同學的代碼打的包,導致啟動失敗。所以一直以為是A同學代碼的問題,這個方向性的錯誤浪費了很多時間。應該要讓每個分支可以同時打包,但不會覆蓋。

(8)不要先入為主。不要過早認定某個模塊就是有問題的,請參考上一條。

(9)團隊作戰,分工合作。整個過程全靠團隊一起協作才能快速定位並解決;打造一個開放包容、溝通順暢的團隊是多麼的重要。

If You Want to Go Fast, Go Alone. If You Want to Go Far, Go Together.

4 最後

運維和問題定位的知識很多,也非常重要,需要持續學習。本文僅講述了本次過程用到的方法。更多的知識以後慢慢再介紹…

歡迎關注公眾號<南瓜慢說>,將持續為你更新…

多讀書,多分享;多寫作,多整理。

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

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

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

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

小白學 Python(21):生成器基礎

人生苦短,我選Python

前文傳送門

生成器

我們前面聊過了為什麼要使用迭代器,各位同學應該還有印象吧(說沒有的就太過分了)。

列表太大的話會佔用過大的內存,可以使用迭代器,只拿出需要使用的部分。

生成器的設計原則和迭代器是相似的,如果需要一個非常大的集合,不會將元素全部都放在這個集合中,而是將元素保存成生成器的狀態,每次迭代的時候返回一個值。

比如我們要生成一個列表,可以採用如下方式:

list1 = [x*x for x in range(10)]
print(list1)

結果如下:

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

如果我們生成的列表非常的巨大,比如:

list2 = [x*x for x in range(1000000000000000000000000)]

結果如下:

Traceback (most recent call last):
  File "D:/Development/Projects/python-learning/base-generator/Demo.py", line 3, in <module>
    list2 = [x*x for x in range(1000000000000000000000000)]
  File "D:/Development/Projects/python-learning/base-generator/Demo.py", line 3, in <listcomp>
    list2 = [x*x for x in range(1000000000000000000000000)]
MemoryError

報錯了,報錯信息提示我們存儲異常,並且整個程序運行了相當長一段時間。友情提醒,這麼大的列表創建請慎重,如果電腦配置不夠很有可能會將電腦卡死。

如果我們使用生成器就會非常方便了,而且執行速度嗖嗖的。

generator1 = (x*x for x in range(1000000000000000000000000))
print(generator1)
print(type(generator1))

結果如下:

<generator object <genexpr> at 0x0000014383E85B48>
<class 'generator'>

那麼,我們使用了生成器以後,怎麼讀取生成器生成的數據呢?

當然是和之前的迭代器一樣的拉,使用 next() 函數:

generator2 = (x*x for x in range(3))
print(next(generator2))
print(next(generator2))
print(next(generator2))
print(next(generator2))

結果如下:

Traceback (most recent call last):
  File "D:/Development/Projects/python-learning/base-generator/Demo.py", line 14, in <module>
    print(next(generator2))
StopIteration

直到最後,拋出 StopIteration 異常。

但是,這種使用方法我們並不知道什麼時候會迭代結束,所以我們可以使用 for 循環來獲取每生成器生成的具體的元素,並且使用 for 循環同時也無需關心最後的 StopIteration 異常。

generator3 = (x*x for x in range(5))
for index in generator3:
    print(index)

結果如下:

0
1
4
9
16

generator 非常的強大,本質上, generator 並不會取存儲我們的具體元素,它存儲是推算的算法,通過算法來推算出下一個值。

如果推算的算法比較複雜,用類似列表生成式的 for 循環無法實現的時候,還可以用函數來實現。

比如我們定義一個函數,emmmmmm,還是簡單點吧,大家領會精神:

def print_a(max):
    i = 0
    while i < max:
        i += 1
        yield i

a = print_a(10)
print(a)
print(type(a))

結果如下:

<generator object print_a at 0x00000278C6AA5CC8>
<class 'generator'>

這裏使用到了關鍵字 yieldyieldreturn 非常的相似,都可以返回值,但是不同的是 yield 不會結束函數。

我們調用幾次這個用函數創建的生成器:

print(next(a))
print(next(a))
print(next(a))
print(next(a))

結果如下:

1
2
3
4

可以看到,當我們使用 next() 對生成器進行一次操作的時候,會返回一次循環的值,在 yield 這裏結束本次的運行。但是在下一次執行 next() 的時候,會接着上次的斷點接着運行。直到下一個 yield ,並且不停的循環往複,直到運行至生成器的最後。

還有一種與 next() 等價的方式,直接看示例代碼吧:

print(a.__next__())
print(a.__next__())

結果如下:

5
6

接下來要介紹的這個方法就更厲害了,不僅能迭代,還能給函數再傳一個值回去:

def print_b(max):
    i = 0
    while i < max:
        i += 1
        args = yield i
        print('傳入參數為:' + args)

b = print_b(20)
print(next(b))
print(b.send('Python'))

結果如下:

1
傳入參數為:Python
2

上面講了這麼多,可能各位還沒想到生成器能有什麼具體的作用吧,這裏我來提一個——協程。

在介紹什麼是協程之前先介紹下什麼是多線程,就是在同一個時間內可以執行多個程序,簡單理解就是你平時可能很經常的一邊玩手機一邊聽音樂(毫無違和感)。

協程更貼切的解釋是流水線,比如某件事情必須 A 先做一步, B 再做一步,並且這兩件事情看起來要是同時進行的。

def print_c():
    while True:
        print('執行 A ')
        yield None
def print_d():
    while True:
        print('執行 B ')
        yield None

c = print_c()
d = print_d()
while True:
    c.__next__()
    d.__next__()

結果如下:

...
執行 A 
執行 B 
執行 A 
執行 B 
執行 A 
執行 B 
執行 A 
執行 B 
執行 A 
執行 B 
...

因為 while 條件設置的是永真,所以這個循環是不會停下來的。

這裏我們定義了兩個生成器,並且在一個循環中往複的調用這兩個生成器,這樣看起來就是兩個任務在同時執行。

最後的協程可能理解起來稍有難度,有問題可以在公眾號後台問我哦~~~

示例代碼

本系列的所有代碼小編都會放在代碼管理倉庫 Github 和 Gitee 上,方便大家取用。

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

【其他文章推薦】

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

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

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

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

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

告別動態規劃,連刷 40 道題,我總結了這些套路,看不懂你打我(萬字長文)

動態規劃難嗎?說實話,我覺得很難,特別是對於初學者來說,我當時入門動態規劃的時候,是看 0-1 背包問題,當時真的是一臉懵逼。後來,我遇到動態規劃的題,看的懂答案,但就是自己不會做,不知道怎麼下手。就像做遞歸的題,看的懂答案,但下不了手,關於遞歸的,我之前也寫過一篇套路的文章,如果對遞歸不大懂的,強烈建議看一看:

對於動態規劃,春招秋招時好多題都會用到動態規劃,一氣之下,再 leetcode 連續刷了幾十道題

之後,豁然開朗 ,感覺動態規劃也不是很難,今天,我就來跟大家講一講,我是怎麼做動態規劃的題的,以及從中學到的一些套路。相信你看完一定有所收穫

如果你對動態規劃感興趣,或者你看的懂動態規劃,但卻不知道怎麼下手,那麼我建議你好好看以下,這篇文章的寫法,和之前那篇講遞歸的寫法,是差不多一樣的,將會舉大量的例子。如果一次性看不完,建議收藏,同時別忘了素質三連

為了兼顧初學者,我會從最簡單的題講起,後面會越來越難,最後面還會講解,該如何優化。因為 80% 的動規都是可以進行優化的。不過我得說,如果你連動態規劃是什麼都沒聽過,可能這篇文章你也會壓力山大。

一、動態規劃的三大步驟

動態規劃,無非就是利用歷史記錄,來避免我們的重複計算。而這些歷史記錄,我們得需要一些變量來保存,一般是用一維數組或者二維數組來保存。下面我們先來講下做動態規劃題很重要的三個步驟,

如果你聽不懂,也沒關係,下面會有很多例題講解,估計你就懂了。之所以不配合例題來講這些步驟,也是為了怕你們腦袋亂了

第一步驟:定義數組元素的含義,上面說了,我們會用一個數組,來保存歷史數組,假設用一維數組 dp[] 吧。這個時候有一個非常非常重要的點,就是規定你這個數組元素的含義,例如你的 dp[i] 是代表什麼意思?

第二步驟:找出數組元素之間的關係式,我覺得動態規劃,還是有一點類似於我們高中學習時的歸納法的,當我們要計算 dp[n] 時,是可以利用 dp[n-1],dp[n-2]…..dp[1],來推出 dp[n] 的,也就是可以利用歷史數據來推出新的元素值,所以我們要找出數組元素之間的關係式,例如 dp[n] = dp[n-1] + dp[n-2],這個就是他們的關係式了。而這一步,也是最難的一步,後面我會講幾種類型的題來說。

學過動態規劃的可能都經常聽到最優子結構,把大的問題拆分成小的問題,說時候,最開始的時候,我是對最優子結構一夢懵逼的。估計你們也聽多了,所以這一次,我將換一種形式來講,不再是各種子問題,各種最優子結構。所以大佬可別噴我再亂講,因為我說了,這是我自己平時做題的套路。

第三步驟:找出初始值。學過數學歸納法的都知道,雖然我們知道了數組元素之間的關係式,例如 dp[n] = dp[n-1] + dp[n-2],我們可以通過 dp[n-1] 和 dp[n-2] 來計算 dp[n],但是,我們得知道初始值啊,例如一直推下去的話,會由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我們必須要能夠直接獲得 dp[2] 和 dp[1] 的值,而這,就是所謂的初始值

由了初始值,並且有了數組元素之間的關係式,那麼我們就可以得到 dp[n] 的值了,而 dp[n] 的含義是由你來定義的,你想求什麼,就定義它是什麼,這樣,這道題也就解出來了。

不懂?沒事,我們來看三四道例題,我講嚴格按這個步驟來給大家講解。

二、案例詳解

案例一、簡單的一維 DP

問題描述:一隻青蛙一次可以跳上1級台階,也可以跳上2級。求該青蛙跳上一個n級的台階總共有多少種跳法。

(1)、定義數組元素的含義

按我上面的步驟說的,首先我們來定義 dp[i] 的含義,我們的問題是要求青蛙跳上 n 級的台階總共由多少種跳法,那我們就定義 dp[i] 的含義為:跳上一個 i 級的台階總共有 dp[i] 種跳法。這樣,如果我們能夠算出 dp[n],不就是我們要求的答案嗎?所以第一步定義完成。

(2)、找出數組元素間的關係式

我們的目的是要求 dp[n],動態規劃的題,如你們經常聽說的那樣,就是把一個規模比較大的問題分成幾個規模比較小的問題,然後由小的問題推導出大的問題。也就是說,dp[n] 的規模為 n,比它規模小的是 n-1, n-2, n-3…. 也就是說,dp[n] 一定會和 dp[n-1], dp[n-2]….存在某種關係的。我們要找出他們的關係。

那麼問題來了,怎麼找?

這個怎麼找,是最核心最難的一個,我們必須回到問題本身來了,來尋找他們的關係式,dp[n] 究竟會等於什麼呢?

對於這道題,由於情況可以選擇跳一級,也可以選擇跳兩級,所以青蛙到達第 n 級的台階有兩種方式

一種是從第 n-1 級跳上來

一種是從第 n-2 級跳上來

由於我們是要算所有可能的跳法的,所以有 dp[n] = dp[n-1] + dp[n-2]。

(3)、找出初始條件

當 n = 1 時,dp[1] = dp[0] + dp[-1],而我們是數組是不允許下標為負數的,所以對於 dp[1],我們必須要直接給出它的數值,相當於初始值,顯然,dp[1] = 1。一樣,dp[0] = 0.(因為 0 個台階,那肯定是 0 種跳法了)。於是得出初始值:

dp[0] = 0.
dp[1] = 1.
即 n <= 1 時,dp[n] = n.

三個步驟都做出來了,那麼我們就來寫代碼吧,代碼會詳細註釋滴。

int f( int n ){
    if(n <= 1)
    return n;
    // 先創建一個數組來保存歷史數據
    int[] dp = new int[n+1];
    // 給出初始值
    dp[0] = 0;
    dp[1] = 1;
    // 通過關係式來計算出 dp[n]
    for(int i = 2; i <= n; i++){
        dp[i] = dp[i-1] + dp[-2];
    }
    // 把最終結果返回
    return dp[n];
}
(4)、再說初始化

大家先想以下,你覺得,上面的代碼有沒有問題?

答是有問題的,還是錯的,錯在對初始值的尋找不夠嚴謹,這也是我故意這樣弄的,意在告訴你們,關於初始值的嚴謹性。例如對於上面的題,當 n = 2 時,dp[2] = dp[1] + dp[0] = 1。這顯然是錯誤的,你可以模擬一下,應該是 dp[2] = 2。

也就是說,在尋找初始值的時候,一定要注意不要找漏了,dp[2] 也算是一個初始值,不能通過公式計算得出。有人可能會說,我想不到怎麼辦?這個很好辦,多做幾道題就可以了。

下面我再列舉三道不同的例題,並且,再在未來的文章中,我也會持續按照這個步驟,給大家找幾道有難度且類型不同的題。下面這幾道例題,不會講的特性詳細哈。實際上 ,上面的一維數組是可以把空間優化成更小的,不過我們現在先不講優化的事,下面的題也是,不講優化版本。

案例二:二維數組的 DP

我做了幾十道 DP 的算法題,可以說,80% 的題,都是要用二維數組的,所以下面的題主要以二維數組為主,當然有人可能會說,要用一維還是二維,我怎麼知道?這個問題不大,接着往下看。

問題描述

一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記為“Start” )。

機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記為“Finish”)。

問總共有多少條不同的路徑?

這是 leetcode 的 62 號題:

還是老樣子,三個步驟來解決。

步驟一、定義數組元素的含義

由於我們的目的是從左上角到右下角一共有多少種路徑,那我們就定義 dp[i] [j]的含義為:當機器人從左上角走到(i, j) 這個位置時,一共有 dp[i] [j] 種路徑。那麼,dp[m-1] [n-1] 就是我們要的答案了。

注意,這個網格相當於一個二維數組,數組是從下標為 0 開始算起的,所以 右下角的位置是 (m-1, n – 1),所以 dp[m-1] [n-1] 就是我們要找的答案。

步驟二:找出關係數組元素間的關係式

想象以下,機器人要怎麼樣才能到達 (i, j) 這個位置?由於機器人可以向下走或者向右走,所以有兩種方式到達

一種是從 (i-1, j) 這個位置走一步到達

一種是從(i, j – 1) 這個位置走一步到達

因為是計算所有可能的步驟,所以是把所有可能走的路徑都加起來,所以關係式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]。

步驟三、找出初始值

顯然,當 dp[i] [j] 中,如果 i 或者 j 有一個為 0,那麼還能使用關係式嗎?答是不能的,因為這個時候把 i – 1 或者 j – 1,就變成負數了,數組就會出問題了,所以我們的初始值是計算出所有的 dp[0] [0….n-1] 和所有的 dp[0….m-1] [0]。這個還是非常容易計算的,相當於計算機圖中的最上面一行和左邊一列。因此初始值如下:

dp[0] [0….n-1] = 1; // 相當於最上面一行,機器人只能一直往左走

dp[0…m-1] [0] = 1; // 相當於最左面一列,機器人只能一直往下走

擼代碼

三個步驟都寫出來了,直接看代碼

public static int uniquePaths(int m, int n) {
    if (m <= 0 || n <= 0) {
        return 0;
    }

    int[][] dp = new int[m][n]; // 
    // 初始化
    for(int i = 0; i < m; i++){
      dp[i][0] = 1;
    }
    for(int i = 0; i < n; i++){
      dp[0][i] = 1;
    }
        // 推導出 dp[m-1][n-1]
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = dp[i-1][j] + dp[i][j-1];
        }
    }
    return dp[m-1][n-1];
}

O(n*m) 的空間複雜度可以優化成 O(min(n, m)) 的空間複雜度的,不過這裏先不講

案例三、二維數組 DP

寫到這裏,有點累了,,但還是得寫下去,所以看的小夥伴,你們可得繼續看呀。下面這道題也不難,比上面的難一丟丟,不過也是非常類似

問題描述

給定一個包含非負整數的 m x n 網格,請找出一條從左上角到右下角的路徑,使得路徑上的数字總和為最小。

說明:每次只能向下或者向右移動一步。

舉例:
輸入:
arr = [
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
輸出: 7
解釋: 因為路徑 1→3→1→1→1 的總和最小。

和上面的差不多,不過是算最優路徑和,這是 leetcode 的第64題:

還是老樣子,可能有些人都看煩了,哈哈,但我還是要按照步驟來寫,讓那些不大懂的加深理解。有人可能覺得,這些題太簡單了吧,別慌,小白先入門,這些屬於 medium 級別的,後面在給幾道 hard 級別的。

步驟一、定義數組元素的含義

由於我們的目的是從左上角到右下角,最小路徑和是多少,那我們就定義 dp[i] [j]的含義為:當機器人從左上角走到(i, j) 這個位置時,最下的路徑和是 dp[i] [j]。那麼,dp[m-1] [n-1] 就是我們要的答案了。

注意,這個網格相當於一個二維數組,數組是從下標為 0 開始算起的,所以 由下角的位置是 (m-1, n – 1),所以 dp[m-1] [n-1] 就是我們要走的答案。

步驟二:找出關係數組元素間的關係式

想象以下,機器人要怎麼樣才能到達 (i, j) 這個位置?由於機器人可以向下走或者向右走,所以有兩種方式到達

一種是從 (i-1, j) 這個位置走一步到達

一種是從(i, j – 1) 這個位置走一步到達

不過這次不是計算所有可能路徑,而是計算哪一個路徑和是最小的,那麼我們要從這兩種方式中,選擇一種,使得dp[i] [j] 的值是最小的,顯然有

dp[i] [j] = min(dp[i-1][j],dp[i][j-1]) + arr[i][j];// arr[i][j] 表示網格種的值
步驟三、找出初始值

顯然,當 dp[i] [j] 中,如果 i 或者 j 有一個為 0,那麼還能使用關係式嗎?答是不能的,因為這個時候把 i – 1 或者 j – 1,就變成負數了,數組就會出問題了,所以我們的初始值是計算出所有的 dp[0] [0….n-1] 和所有的 dp[0….m-1] [0]。這個還是非常容易計算的,相當於計算機圖中的最上面一行和左邊一列。因此初始值如下:

dp[0] [j] = arr[0] [j] + dp[0] [j-1]; // 相當於最上面一行,機器人只能一直往左走

dp[i] [0] = arr[i] [0] + dp[i] [0]; // 相當於最左面一列,機器人只能一直往下走

代碼如下
public static int uniquePaths(int[][] arr) {
    int m = arr.length;
    int n = arr[0].length;
    if (m <= 0 || n <= 0) {
        return 0;
    }

    int[][] dp = new int[m][n]; // 
    // 初始化
    dp[0][0] = arr[0][0];
    // 初始化最左邊的列
    for(int i = 1; i < m; i++){
      dp[i][0] = dp[i-1][0] + arr[i][0];
    }
    // 初始化最上邊的行
    for(int i = 1; i < n; i++){
      dp[0][i] = dp[0][i-1] + arr[0][i];
    }
        // 推導出 dp[m-1][n-1]
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + arr[i][j];
        }
    }
    return dp[m-1][n-1];
}

O(n*m) 的空間複雜度可以優化成 O(min(n, m)) 的空間複雜度的,不過這裏先不講

案例 4:編輯距離

這次給的這道題比上面的難一些,在 leetcdoe 的定位是 hard 級別。好像是 leetcode 的第 72 號題。

問題描述

給定兩個單詞 word1 和 word2,計算出將 word1 轉換成 word2 所使用的最少操作數 。

你可以對一個單詞進行如下三種操作:

插入一個字符
刪除一個字符
替換一個字符

示例:
輸入: word1 = "horse", word2 = "ros"
輸出: 3
解釋: 
horse -> rorse (將 'h' 替換為 'r')
rorse -> rose (刪除 'r')
rose -> ros (刪除 'e')

解答

還是老樣子,按照上面三個步驟來,並且我這裏可以告訴你,90% 的字符串問題都可以用動態規劃解決,並且90%是採用二維數組。

步驟一、定義數組元素的含義

由於我們的目的求將 word1 轉換成 word2 所使用的最少操作數 。那我們就定義 dp[i] [j]的含義為:當字符串 word1 的長度為 i,字符串 word2 的長度為 j 時,將 word1 轉化為 word2 所使用的最少操作次數為 dp[i] [j]

有時候,數組的含義並不容易找,所以還是那句話,我給你們一個套路,剩下的還得看你們去領悟。

步驟二:找出關係數組元素間的關係式

接下來我們就要找 dp[i] [j] 元素之間的關係了,比起其他題,這道題相對比較難找一點,但是,不管多難找,大部分情況下,dp[i] [j] 和 dp[i-1] [j]、dp[i] [j-1]、dp[i-1] [j-1] 肯定存在某種關係。因為我們的目標就是,**從規模小的,通過一些操作,推導出規模大的。對於這道題,我們可以對 word1 進行三種操作

插入一個字符
刪除一個字符
替換一個字符

由於我們是要讓操作的次數最小,所以我們要尋找最佳操作。那麼有如下關係式:

一、如果我們 word1[i] 與 word2 [j] 相等,這個時候不需要進行任何操作,顯然有 dp[i] [j] = dp[i-1] [j-1]。(別忘了 dp[i] [j] 的含義哈)。

二、如果我們 word1[i] 與 word2 [j] 不相等,這個時候我們就必須進行調整,而調整的操作有 3 種,我們要選擇一種。三種操作對應的關係試如下(注意字符串與字符的區別):

(1)、如果把字符 word1[i] 替換成與 word2[j] 相等,則有 dp[i] [j] = dp[i-1] [j-1] + 1;

(2)、如果在字符串 word1末尾插入一個與 word2[j] 相等的字符,則有 dp[i] [j] = dp[i] [j-1] + 1;

(3)、如果把字符 word1[i] 刪除,則有 dp[i] [j] = dp[i-1] [j] + 1;

那麼我們應該選擇一種操作,使得 dp[i] [j] 的值最小,顯然有

dp[i] [j] = min(dp[i-1] [j-1],dp[i] [j-1],dp[[i-1] [j]]) + 1;

於是,我們的關係式就推出來了,

步驟三、找出初始值

顯然,當 dp[i] [j] 中,如果 i 或者 j 有一個為 0,那麼還能使用關係式嗎?答是不能的,因為這個時候把 i – 1 或者 j – 1,就變成負數了,數組就會出問題了,所以我們的初始值是計算出所有的 dp[0] [0….n] 和所有的 dp[0….m] [0]。這個還是非常容易計算的,因為當有一個字符串的長度為 0 時,轉化為另外一個字符串,那就只能一直進行插入或者刪除操作了。

代碼如下
public int minDistance(String word1, String word2) {
    int n1 = word1.length();
    int n2 = word2.length();
    int[][] dp = new int[n1 + 1][n2 + 1];
    // dp[0][0...n2]的初始值
    for (int j = 1; j <= n2; j++) 
        dp[0][j] = dp[0][j - 1] + 1;
    // dp[0...n1][0] 的初始值
    for (int i = 1; i <= n1; i++) dp[i][0] = dp[i - 1][0] + 1;
        // 通過公式推出 dp[n1][n2]
    for (int i = 1; i <= n1; i++) {
        for (int j = 1; j <= n2; j++) {
            // 如果 word1[i] 與 word2[j] 相等。第 i 個字符對應下標是 i-1
            if (word1.charAt(i - 1) == word2.charAt(j - 1)){
                p[i][j] = dp[i - 1][j - 1];
            }else {
               dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
            }         
        }
    }
    return dp[n1][n2];  
}

最後說下,如果你要練習,可以去 leetcode,選擇動態規劃專題,然後連續刷幾十道,保證你以後再也不怕動態規劃了。當然,遇到很難的,咱還是得掛。

Leetcode 動態規劃直達:

三、如何優化?

前两天寫一篇長達 8000 子的關於動態規劃的文章

這篇文章更多講解我平時做題的套路,不過由於篇幅過長,舉了 4 個案例之後,沒有講解優化,今天這篇文章就來講解下,對動態規劃的優化如何下手,並且以前幾天那篇文章的題作為例子直接講優化,如果沒看過的建議看一下(不看也行,我會直接給出題目以及沒有優化前的代碼):

四、優化核心:畫圖!畫圖!畫圖

沒錯,80% 的動態規劃題都可以畫圖,其中 80% 的題都可以通過畫圖一下子知道怎麼優化,當然,DP 也有一些很難的題,想優化可沒那麼容易,不過,今天我要講的,是屬於不怎麼難,且最常見,面試筆試最經常考的難度的題。

下面我們直接通過三道題目來講解優化,你會發現,這些題,優化過後,代碼只有細微的改變,你只要會一兩道,可以說是會了 80% 的題。

O(n*m) 空間複雜度優化成 O(n)

上次那個青蛙跳台階的 dp 題是可以把空間複雜度 O( n) 優化成 O(1),本來打算從這道題講起的,但想了下,想要學習 dp 優化的感覺至少都是 小小大佬了,所以就不講了,就從二維數組的 dp 講起。

案例1:最多路徑數

問題描述

一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記為“Start” )。

機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記為“Finish”)。

問總共有多少條不同的路徑?

這是 leetcode 的 62 號題:

這道題的 dp 轉移公式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1],代碼如下

不懂的看我之前文章:

public static int uniquePaths(int m, int n) {
    if (m <= 0 || n <= 0) {
        return 0;
    }

    int[][] dp = new int[m][n]; // 
    // 初始化
    for(int i = 0; i < m; i++){
      dp[i][0] = 1;
    }
    for(int i = 0; i < n; i++){
      dp[0][i] = 1;
    }
        // 推導出 dp[m-1][n-1]
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = dp[i-1][j] + dp[i][j-1];
        }
    }
    return dp[m-1][n-1];
}

這種做法的空間複雜度是 O(n * m),下面我們來講解如何優化成 O(n)。

dp[i] [j] 是一個二維矩陣,我們來畫個二維矩陣的圖,對矩陣進行初始化

然後根據公式 dp[i][j] = dp[i-1][j] + dp[i][j-1] 來填充矩陣的其他值。下面我們先填充第二行的值。

大家想一個問題,當我們要填充第三行的值的時候,我們需要用到第一行的值嗎?答是不需要的,不行你試試,當你要填充第三,第四….第 n 行的時候,第一行的值永遠不會用到,只要填充第二行的值時會用到。

根據公式 dp[i][j] = dp[i-1][j] + dp[i][j-1],我們可以知道,當我們要計算第 i 行的值時,除了會用到第 i – 1 行外,其他第 1 至 第 i-2 行的值我們都是不需要用到的,也就是說,對於那部分用不到的值我們還有必要保存他們嗎?

答是沒必要,我們只需要用一個一維的 dp[] 來保存一行的歷史記錄就可以了。然後在計算機的過程中,不斷着更新 dp[] 的值。單說估計你可能不好理解,下面我就手把手來演示下這個過程。

1、剛開始初始化第一行,此時 dp[0..n-1] 的值就是第一行的值。

2、接着我們來一邊填充第二行的值一邊更新 dp[i] 的值,一邊把第一行的值拋棄掉。

為了方便描述,下面我們用arr (i,j)表示矩陣中第 i 行 第 j 列的值。從 0 開始哈,就是說有第 0 行。

(1)、顯然,矩陣(1, 0) 的值相當於以往的初始化值,為 1。然後這個時候矩陣 (0,0)的值不在需要保存了,因為再也用不到了。

這個時候,我們也要跟着更新 dp[0] 的值了,剛開始 dp[0] = (0, 0),現在更新為 dp[0] = (1, 0)。

(2)、接着繼續更新 (1, 1) 的值,根據之前的公式 (i, j) = (i-1, j) + (i, j- 1)。即 (1,1)=(0,1)+(1,0)=2。

大家看圖,以往的二維的時候, dp[i][j] = dp[i-1] [j]+ dp[i][j-1]。現在轉化成一維,不就是 dp[i] = dp[i] + dp[i-1] 嗎?

即 dp[1] = dp[1] + dp[0],而且還動態幫我們更新了 dp[1] 的值。因為剛開始 dp[i] 的保存第一行的值的,現在更新為保存第二行的值。

(3)、同樣的道理,按照這樣的模式一直來計算第二行的值,順便把第一行的值拋棄掉,結果如下

此時,dp[i] 將完全保存着第二行的值,並且我們可以推導出公式

dp[i] = dp[i-1] + dp[i]

dp[i-1] 相當於之前的 dp[i-1][j],dp[i] 相當於之前的 dp[i][j-1]。

於是按照這個公式不停着填充到最後一行,結果如下:

最後 dp[n-1] 就是我們要求的結果了。所以優化之後,代碼如下:

public static int uniquePaths(int m, int n) {
    if (m <= 0 || n <= 0) {
        return 0;
    }

    int[] dp = new int[n]; // 
    // 初始化
    for(int i = 0; i < n; i++){
      dp[i] = 1;
    }

        // 公式:dp[i] = dp[i-1] + dp[i]
    for (int i = 1; i < m; i++) {
        // 第 i 行第 0 列的初始值
        dp[0] = 1;
        for (int j = 1; j < n; j++) {
            dp[j] = dp[j-1] + dp[j];
        }
    }
    return dp[n-1];
}

案例2:編輯距離

接着我們來看昨天的另外一道題,就是編輯矩陣,這道題的優化和這一道有一點點的不同,上面這道 dp[i][j] 依賴於 dp[i-1][j] 和 dp[i][j-1]。而還有一種情況就是 dp[i][j] 依賴於 dp[i-1][j],dp[i-1][j-1] 和 dp[i][j-1]。

問題描述

給定兩個單詞 word1 和 word2,計算出將 word1 轉換成 word2 所使用的最少操作數 。

你可以對一個單詞進行如下三種操作:

插入一個字符
刪除一個字符
替換一個字符

示例:
輸入: word1 = "horse", word2 = "ros"
輸出: 3
解釋: 
horse -> rorse (將 'h' 替換為 'r')
rorse -> rose (刪除 'r')
rose -> ros (刪除 'e')

解答

昨天的代碼如下所示,不懂的記得看之前的文章哈:

public int minDistance(String word1, String word2) {
    int n1 = word1.length();
    int n2 = word2.length();
    int[][] dp = new int[n1 + 1][n2 + 1];
    // dp[0][0...n2]的初始值
    for (int j = 1; j <= n2; j++) 
        dp[0][j] = dp[0][j - 1] + 1;
    // dp[0...n1][0] 的初始值
    for (int i = 1; i <= n1; i++) dp[i][0] = dp[i - 1][0] + 1;
        // 通過公式推出 dp[n1][n2]
    for (int i = 1; i <= n1; i++) {
        for (int j = 1; j <= n2; j++) {
            // 如果 word1[i] 與 word2[j] 相等。第 i 個字符對應下標是 i-1
            if (word1.charAt(i - 1) == word2.charAt(j - 1)){
                p[i][j] = dp[i - 1][j - 1];
            }else {
               dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
            }         
        }
    }
    return dp[n1][n2];  
}

沒有優化之間的空間複雜度為 O(n*m)

大家可以自己動手做下,按照上面的那個模式,你會優化嗎?

對於這道題其實也是一樣的,如果要計算 第 i 行的值,我們最多只依賴第 i-1 行的值,不需要用到第 i-2 行及其以前的值,所以一樣可以採用一維 dp 來處理的。

不過這個時候要注意,在上面的例子中,我們每次更新完 (i, j) 的值之後,就會把 (i, j-1) 的值拋棄,也就是說之前是一邊更新 dp[i] 的值,一邊把 dp[i] 的舊值拋棄的,不過在這道題中則不可以,因為我們還需要用到它。

哎呀,直接舉例子看圖吧,文字繞來繞去估計會繞暈你們。當我們要計算圖中 (i,j) 的值的時候,在案例1 中,我們值需要用到 (i-1, j) 和 (i, j-1)。(看圖中方格的顏色)

不過這道題中,我們還需要用到 (i-1, j-1) 這個值(但是這個值在以往的案例1 中,它會被拋棄掉)

所以呢,對於這道題,我們還需要一個額外的變量 pre 來時刻保存 (i-1,j-1) 的值。推導公式就可以從二維的

dp[i][j] = min(dp[i-1][j] , dp[i-1][j-1] , dp[i][j-1]) + 1

轉化為一維的

dp[i] = min(dp[i-1], pre, dp[i]) + 1。

所以呢,案例2 其實和案例1 差別不大,就是多了個變量來臨時保存。最終代碼如下(但是初學者話,代碼也沒那麼好寫)

代碼如下
public int minDistance(String word1, String word2) {
    int n1 = word1.length();
    int n2 = word2.length();
    int[] dp = new int[n2 + 1];
    // dp[0...n2]的初始值
    for (int j = 0; j <= n2; j++) 
        dp[j] = j;
    // dp[j] = min(dp[j-1], pre, dp[j]) + 1
    for (int i = 1; i <= n1; i++) {
        int temp = dp[0];
        // 相當於初始化
        dp[0] = i;
        for (int j = 1; j <= n2; j++) {
            // pre 相當於之前的 dp[i-1][j-1]
            int pre = temp;
            temp = dp[j];
            // 如果 word1[i] 與 word2[j] 相等。第 i 個字符對應下標是 i-1
            if (word1.charAt(i - 1) == word2.charAt(j - 1)){
                dp[j] = pre;
            }else {
               dp[j] = Math.min(Math.min(dp[j - 1], pre), dp[j]) + 1;
            } 
            // 保存要被拋棄的值       
        }
    }
    return dp[n2]; 
}

總結

上面的這些題,基本都是不怎麼難的入門題,除了最後一道相對難一點。並且基本 80% 的二維矩陣 dp 都可以像上面的方法一樣優化成 一維矩陣的 dp,核心就是要畫圖,看他們的值依賴,當然,還有很多其他比較難的優化,但是,我遇到的題中,大部分都是我上面這種類型的優化。後面如何遇到其他的,我會作為案例來講,今天就先講最普遍最通用的優化方案。記住,畫二維 dp 的矩陣圖,然後看元素之間的值依賴,然後就可以很清晰着知道該如何優化了。

在之後的文章中,我也會按照這個步驟,在給大家講四五道動態規劃 hard 級別的題,會放在每天推文的第二條給大家學習。如果覺得有收穫,不放三連走起來(點贊、感謝、分享),嘻嘻。

有收穫?希望老鐵們來個三連擊,給更多的人看到這篇文章

1、點贊,可以讓更多的人看到這篇文章
2、關注我的原創微信公眾號『苦逼的碼農』,第一時間閱讀我的文章,已寫了 150+ 的原創文章。

公眾號後台回復『电子書』,還送你一份电子書大禮包哦。

作者簡潔

作者:帥地,一位熱愛、認真寫作的小伙,目前維護原創公眾號:『苦逼的碼農』,已寫了150多篇文章,專註於寫 算法、計算機基礎知識等提升你內功的文章,期待你的關注。
轉載說明:務必註明來源(註明:來源於公眾號:苦逼的碼農, 作者:帥地)

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

【其他文章推薦】

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

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

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

南投搬家前需注意的眉眉角角,別等搬了再說!

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

[UWP]使用CompositionAPI的翻轉動畫

1. 運行效果

在 這篇文章里我介紹了一個包含長陰影的番茄鍾,這個番茄鍾在狀態切換時用到了翻轉動畫,效果如上所示,還用到了彈簧動畫,可以看到翻轉後有點回彈。本來打算自己這個動畫效果寫的,但火火已經寫好了這個FlipSide控件,Github地址在,這篇文章就介紹下這個控件的部分原理。

2. TransformMatrix

Visual的 屬性是一個 的struct,它是應用於元素的轉換矩陣,可以進行動畫處理。它的默認值如下:

這時候動畫效果如下:

要使Visual可以正確旋轉需要按以下方式處理:

private void UpdateTransformMatrix(FrameworkElement element)
{
    var host = ElementCompositionPreview.GetElementVisual(element);
    var size = element.RenderSize.ToVector2();
    if (size.X == 0 || size.Y == 0) return;
    var n = -1f / size.X;

    Matrix4x4 perspective = new Matrix4x4(
        1.0f, 0.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 1.0f, n,
        0.0f, 0.0f, 0.0f, 1.0f);

    host.TransformMatrix =
        Matrix4x4.CreateTranslation(-size.X / 2, -size.Y / 2, 0f) *
        perspective *
        Matrix4x4.CreateTranslation(size.X / 2, size.Y / 2, 0f);
}

講真我也不明白為什麼要這麼寫,只知道是從微軟的 里抄的。每當SizeChanged事件發生時都需要調用這個函數重新設置TransformMatrix。

3. RotationAngleInDegrees

Visual包含兩個相似的屬性, 和 ,它們的定義如下:

//
// 摘要:
//     視覺對象的旋轉角度(以度為單位)。 可動畫處理。
//
// 返回結果:
//     The rotation angle of the visual in degrees.
public float RotationAngleInDegrees { get; set; }
//
// 摘要:
//     視覺對象的旋轉角度(以弧度為單位)。 可動畫處理。
//
// 返回結果:
//     The rotation angle in radians of the visual.
public float RotationAngle { get; set; }

這兩個屬性都用於控制Visua圍繞着RotationAxis和CenterPoint旋轉。在FlipSide這個控件里RotationAngleInDegrees比較適用:

float f1 = 0f, f2 = 0f;
if (IsFlipped)
{
    f1 = 180f;
    f2 = 360f;
    VisualStateManager.GoToState(this, "Slide2", false);
}
else
{
    f1 = 0f;
    f2 = 180f;
    VisualStateManager.GoToState(this, "Slide1", false);
}
if (springAnimation1 != null && springAnimation2 != null)
{
    springAnimation1.FinalValue = f1;
    springAnimation2.FinalValue = f2;
    s1Visual.StartAnimation("RotationAngleInDegrees", springAnimation1);
    s2Visual.StartAnimation("RotationAngleInDegrees", springAnimation2);
}

這段代碼用到了SpringAnimatin,所以有彈一下的效果。

4. RotationAxis

用於指定Visual旋轉的軸。FlipSide可以通過設置RotationAxis改變翻轉的角度,例如火火的Demo里使用根據鼠標改變RotationAxis:

private void OnFlipSidePointerReleased(object sender, PointerRoutedEventArgs e)
{
    var position = e.GetCurrentPoint(_FlipSide).Position;
    var v2 = (position.ToVector2() - _FlipSide.RenderSize.ToVector2() / 2);
    _FlipSide.Axis = new Vector2(-v2.Y, v2.X);
}

5. ExpressionAnimation

<controls:FlipSide x:Name="FlipSide" IsFlipped="True">
    <controls:FlipSide.Side1>
        <Grid Background="#FFE87A69" x:Name="InworkElement" CornerRadius="1">
            
        </Grid>
    </controls:FlipSide.Side1>
    <controls:FlipSide.Side2>
        <Grid Background="#FF5271c2" x:Name="BreakElement" CornerRadius="1">
            
        </Grid>
    </controls:FlipSide.Side2>
</controls:FlipSide>

上面XAML為FlipSide的調用代碼,它將Side1和Side2(這個命名超讓高達迷興奮)作為內容显示在UI上,當IsFlipped為False時显示Side1的內容,當IsFlipped為True時代表翻轉過去,此時显示Side2的內容。在翻轉動畫的過程中,何時隱藏Side1並显示Side2是個麻煩事。幸好UWP有強大的表達式動畫(ExpressionAnimation),FlipSide只用了下面幾句代碼處理這個問題:

s1Visual = ElementCompositionPreview.GetElementVisual(Side1Content);
s2Visual = ElementCompositionPreview.GetElementVisual(Side2Content);

var opacity1Animation = compositor.CreateExpressionAnimation("this.Target.RotationAngleInDegrees > 90 ? 0f : 1f");
var opacity2Animation = compositor.CreateExpressionAnimation("(this.Target.RotationAngleInDegrees - 180) > 90 ? 1f : 0f");

s1Visual.StartAnimation("Opacity", opacity1Animation);
s2Visual.StartAnimation("Opacity", opacity2Animation);

這段代碼的意思是當Side1的RotationAngleInDegrees大於90度時隱藏,否則显示;Side2則相反。其中,表達式中的this.Target表示使用這個表達式動畫的Vsual。

表達式動畫的話題很大,這篇文章就割愛了,可以參考下面給出的鏈接了解更多內容:

6. 結語

感謝火火提供了這個控件,讓我可以省下了不少功夫。其實我對TransformMatrix真的不理解,所以這部分只是用,沒辦法詳細介紹。而且我以前對UI里使用3D不感興趣,所以這方面真的沒法寫更多內容。期待火火為這方面補充一些博客。

7. 參考

8. 源碼

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

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

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

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

算法核心——空間複雜度和時間複雜度超詳細解析

算法核心——空間複雜度和時間複雜度超詳細解析

一、什麼是算法

算法

  • 一個有限指令集
  • 接受一些輸入(有些情況下不需要收入)
  • 產生輸出
  • 一定在有限步驟之後終止
  • 每一條指令必須:
  1. 有充分明確的目標,不可以有歧義
  2. 計算機能處理的範圍之內
  3. 描述應不依賴於任何一種計算機語言以及具體的實現手段

其實說白了,算法就是一個計算過程解決問題的方法。我們現在已經知道數據結構表示數據是怎麼存儲的,而“程序=數據結構+算法”,數據結構是靜態的,算法是動態的,它們加起來就是程序

對算法來說有輸入,有輸出,相當於函數參數返回值。我們寫算法的時候習慣把算法封裝到一個函數中。

二、什麼是好的算法

好,從上面我們知道了什麼是算法,下面我再說什麼是好的算法
在解決同一個問題的時候,我們通常會有很多種不一樣的算法,區別就在於,有的算法比較笨,有的算法比較聰明,那我們怎麼去衡量它們誰好誰壞呢?我們通常有下面兩個指標:

  • 空間複雜度:根據算法寫成的程序在執行時佔用存儲單元的長度。
  • 時間複雜度:根據算法寫成的程序在執行時耗費時間的長度。

先舉個例子說,如果讓你打印十個整數,你那個程序可能瞬間就給出結果了,如果讓你打印十萬個整數呢?這你就得多等一會了。所以這個程序運行的時間,就跟你要處理的數據是十個還是十萬個是相關的,這個十萬就是我們要處理的數據的規模。我們把它叫做n,是一個變量的話,那我們這個程序所用的時間空間都跟這個n是有直接關係的。解決一個問題有很多中不同的方法,你在設計這個方法的時候,一定要把這兩個要素考慮清楚。一不小心,如果空間複雜度太大的話,你那個程序就可能直接爆掉了,非正常中斷,我一會會在後面講,時間複雜度如果太大的話,你就可能等很長時間都等不出結果。

時間複雜度

先來看上面圖片中的幾組代碼,我是用Python表示的,你在看的時候考慮兩個問題:

 

  1. 四組代碼中,哪組的運行時間最短?
  2. 用什麼方式來體現算法運行的快慢?

剛才說n可以看作數據的規模,規模不一樣,運行時間肯定也不一樣,而且所用時間也不好確定,不同的n會得到不同的時間,所以我們用時間複雜度來表示算法運行的快慢。
先來看下面圖片中的幾個生活中的事件,估計時間:

這裏你會發現我們會用“”表示一個大概,後面還有相應的時間單位,那時間複雜度也參照類似的方法:
時間複雜度:用來評估算法運行效率的一個式子

看上面圖片所示,先說print(‘Hello World’),它的時間複雜度表示為O(1),O嚴格來說,它表示數學上一個式子的上界,我們可以簡單的理解為就是一個估計,大約,相當於上面說的“”。1可以理解為是個運行單位(類似於秒這樣的單位),為什麼是O(1),因為print(‘Hello World’)只執行了一次,同理分析第二個:

 

for i in range(n):
    print('Hello World')

它的時間複雜度表示為O(n),因為這組代碼執行了n次。n還是個單位,同理,分析第三個:

for i in range(n):
    for j in range(n):
        print('Hello World')

它的時間複雜度表示為O(),因為是有兩層循環,所以是,還是個單位。第四個你自己就可以分析了,我就不多此一舉了。但千萬不要以為就是這麼簡單,咱再看下面代碼圖片:

看到這個圖片,你是不是感覺很良好,和你猜的差不多是吧,哈哈,不要高興的太早,告訴你們,錯了,它們的時間複雜度不是這樣的。
為什麼?我說了,“1”是單位,但“3”不是單位,3是3乘1,就比如說在生活中,問你一壺水燒多長時間,沒有人回答說是三個幾分鐘或者幾個三分鐘。再說第二個,是單位,n也是個單位,但是比n大,所以我們在估計時用大單位,就好比生活中問你大概睡了多久,你一般說是幾個小時,而不是說幾個小時零幾分鐘,你強調的是一個大概的時間,明白了吧。
所以正確的時間複雜度是這樣的:

第一個為什麼是O(1),首先print(‘Hello World’)打印一次和打印三次實際的影響不大吧,就是不管執行幾次,只要它的規模不上升到n這麼大的時候,換句話說,1是個單位,所以不管怎樣,因為這是表示近似,不是表示精確的,所以是O(1).好,再看下面這個圖片:

當你的循環減半的時候,時間複雜度就會變為O(logn)。所以你可以這樣記,當算法過程出現循環折半的時候,複雜度式子中會出現logn。

 

時間複雜度小結

  • 時間複雜度是用來估計算法運行時間的一個式子(單位)
  • 一般來說,時間複雜度高的算法比時間複雜度低的算法慢
常見的時間複雜度(按效率排序)

複雜問題的時間複雜度

如何簡單快速地判斷算法複雜度

空間複雜度

在空間複雜度中需要注意的一點就是理解“空間換時間”,在研究一個算法的時候,時間比空間重要。

 

此篇完

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

【其他文章推薦】

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

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

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

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

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

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

系統架構設計師-軟件水平考試(高級)-論文-可靠性設計

系統架構設計師-軟件水平考試(高級)-論文-可靠性

前言

首先說一下為什麼這兩個月又沒消息了,因為這兩個月忙啊。

首先是接收上半年系統分析師的證書,並完成總結。其次是九月份PMP考試(4A通過,尚需努力),然後是十一月的軟考高項的考試。工作的事情就不談了,還好沒什麼私人事情需要處理。所以這兩個月沒什麼空寫博客,不過接下來應該會有一些時間來寫博客。

關於系統架構師這個分支,原本都打算完結了的。然後突然發現大家對系統架構師的論文比較感興趣,並且自從我上次透露了我有一個架構師/分析師的群后,陸陸續續不斷有人私信我加群。所以,就回過頭,再發一篇系統架構師的論文。並打算找時間,將自己系統分析師,PMP,項目管理師的知識整理出來。畢竟在過去的一年的時間,我連續通過系統架構師,系統分析師,PMP,並完成,參加了高項(雖然目前還不知道通過沒),我認為我的學習方法,知識體系等還是有一定作用的,希望對大家有所幫助。嘻嘻。

哦。差點忘了。由於我的架構師/分析師群是邀請制的,所以給你們群號,也是無法添加的。所以,如果有參加架構師/分析師的朋友,請私聊我。謝謝。

一,理論

(強調一下,圖片絕對清晰。如果看不清,請從新的頁面打開,或者下載下來)

論文

摘要:
本人於2015年11月參与浙江省某在線教育平台“外教一對一在線教育”項目,該項目為客戶提供了一對一歐美外教視頻教學,社交圈,公眾直播等功能提供全方位的軟件支撐,在該項目組中我擔任系統架構師崗位,主要負責整體架構設計與中間件選型。本文以該教育平台為例,主要討論了該系統有關可靠性方面的設計與應用,以及遇到的問題與解決方案。一方面通過負載均衡進行容錯技術中冗餘設計的實現,另一方面通過層次架構風格來明確系統結構體系,從而降低系統設計複雜度,提高系統可靠性。整個系統開發工作歷時18個月。目前,該系統已經穩定運行近一年半的時間。實踐證明,通過容錯設計,降低複雜度設計等,系統有效提高了可靠性,從而為公司業務提供持續穩定的服務支撐。

正文:
隨着國家對教育的越發重視,英語教育的市場份額逐步上升,其中用戶口語提升的需求越來越大。為此,一些公司開始提供與外國人聊天的平台。我所在公司決定從國際通訊領域進軍口語教育領域。為了這項戰略轉變,公司於2015年11月設計某在線教育平台系統(一下簡稱為“系統”)。該系統幫助人們與歐美外教進行面對面的口語交流和教學。其中隨意聊提供了一種類似QQ視頻通話,而正式課程還提供了H5互動課件與課後點評等,以提高教學質量。與此同時,還有公眾直播用於拉新,AI測試用於評定學院能力,降低成本。我參与了該項目的開發工作,擔任系統架構設計師職務,負責設計系統架構。本項目組全體成員共9人,我主要負責項目計劃制定,需求分析,整體架構設計與技術選型,以及部分底層設計。該項目的架構工作與次年2月完成,選擇了層次架構風格。整個項目耗時18個月,於2017年5月完成。
目前主流的可靠性設計技術有容錯設計,檢錯設計,降低複雜度設計等技術。容錯設計技術分為恢復塊設計,N版本程序設計和冗餘設計。其中恢復塊設計是選擇一組軟件操作作為容錯設計單元,將普通的程序塊編程恢復塊。N版本程序設計的核心是通過設計出多個模塊或不同版本,對於相同初始條件和相同輸入的操作結果,實現多數表決,防止其中某一軟件模塊/版本的故障提供錯誤的服務,以實現軟件容錯。冗餘設計是在一套完整的軟件系統之外,設計一種不同路徑,不同算法或不同實現方法的模塊或系統作為備份,在出現故障時可以使用冗餘的部分進行替換,從而維持軟件系統的正常運行。缺點是費用和資源的消耗會有所增加。檢錯技術是在軟件出現故障后能及時發現並報警。其缺點是不能自動解決故障。降低複雜度設計是因為軟件複雜性與軟件可靠性有着密切關係,是產生軟件缺陷的重要根源。在設計時考慮降低軟件的複雜性,是提高軟件可靠性的有效方法。

在了解系統需求后,我們決定聽從公司技術顧問的建議,容錯設計主要應用在冗餘設計方面,通過負載均衡,雙機容錯等機制完成冗餘設計。檢錯設計則是通過對Java異常處理機制的設計與封裝處理完成。至於降低複雜度方面,採用層次架構風格,使得系統的結構明確,立體,從而提高系統可靠性。接下來,我將從系統的冗餘設計,複雜度降低設計介紹可靠性在系統中的設計與應用,以及應用過程中遇到的問題與解決方案。

1.冗餘設計:

首先說冗餘設計,冗餘包含邏輯冗餘,數據冗餘,應用冗餘等。這裏以應用冗餘為例。為了提高系統的性能,可靠性,可拓展性等,我們採用了負載均衡技術。常見的負載均衡技術有F5硬件,LVS軟件,Nginx服務器配置等。出於便捷與成本的考慮,我們採用了Nginx服務器配置負載均衡技術。通過對Nginx服務器中upstream模塊的配置,就可以實現在多台服務器的反向代理家在負載均衡。採用負載均衡后,應用服務器集群存在Session問題無法統一的問題。解決方法有Session Sticky,Session Replication,Session數據集中存儲,Cookie Based四個方案。Session Sticky是通過確保同一個會話的請求都在同一個Web服務器上處理實現。Session Replication是增加Web服務器間會話數據的同步來保證不同Web服務器間的Session數據的一致。Cookie Based就是通過Cookie傳遞Session數據完成。經過考慮,我們採用了Session數據集中存儲。Session數據集中存儲通過令每台服務器從專門的session服務器獲取session數據來解決問題。優點是可靠性,可移植性與可拓展性的大幅提高。缺點是一方面讀寫Session數據引入了網絡操作,對數據讀取存在時延和不穩定性,但對於使用內網通信的系統並沒有太大影響。另一方面,如果Session服務器或集群出現問題,將會影響整個應用。我們通過雙機容錯機制解決該問題。除此之外,還有心跳線,看門狗等技術。限於篇幅,不再贅述。

2.降低複雜度設計:

再者就是降低複雜度設計,由於系統的複雜性和綜合性,我們決定採用層次架構風格,將系統架構分為接入層,應用層,服務層,數據層四個層次。這裏以應用層與服務層為例。應用層分為視圖層與業務邏輯層,視圖層負責App與網站的表現效果,業務邏輯層負責業務層的邏輯處理。為了解決系統日益複雜,應用日益臃腫問題,我們將系統按照應用橫向劃分,將系統劃分為課件管理系統,課程管理系統等十餘個子系統。如課件管理系統負責學員上課所用課件,有課件編輯,課件預覽,課件交互等多個功能模塊。功能模塊需調用服務層的服務支撐,如課件交互模塊需要調用stomp通信服務,實現學生與老師間課件的交互功能。另外,課件交互模塊通過對賬戶服務的調用,確立課件雙方的身份,從而明確雙方在課件交互過程中對課件交互部分的交互權限。該劃分使得系統體系變得清晰明了,極大降低系統複雜度,提高系統可靠性。應用層採用基於J2ee的MVC框架-Structs框架,主要通過Servlet和JSP技術實現。另外還有動靜分離,動態資源靜態化等,這裏不再贅述。

服務層提供通用服務。系統在應用層中按照應用橫向劃分,有效降低系統複雜度。但系統代碼仍然存在冗餘,比如用戶信息的調用在諸多應用子系統中都有相關模塊。另外應用的大小依舊十分巨大,複雜,而過小的應用劃分會增加數據庫連接數負擔,故我們提出服務化解決方案。服務化方案就是提取出各個應用的通用服務,如賬戶服務,Session服務等。出於技術成熟度與技術支持等考慮,我們最終採用了阿里的dubbo服務框架,建立服務層。開發過程中,產生了服務框架部署問題與實現服務框架的jar包和應用自身依賴的jar包衝突的問題。前者,我們通過Tomcat作為Web容器,而服務框架作為容器的一部分來解決。後者,我們通過Java的ClassLoader將服務框架自身用的類與應用的類進行隔離。除此之外,我們通過服務線程池隔離,分佈請求合併,服務調用端的流程控制來降低系統複雜度,提高系統可靠性。詳情限於篇幅,不再贅述。

最終項目成功上線,正常運行了近一年半,收到各方好評。尤其是H5課件的良好互動性,使得大量業界同行爭相模仿,改用H5製作課件。還有我們的服務化方案架構被作為許多傳統互聯網企業系統重構的經典方案。在系統的架構設計中,我們引入了層次架構的設計思想,有效地降低了維護成本,提高了系統的開放性,可擴展性,可重用性以及可移植性。當然還是存在一些問題的。如H5課件採用http協議,易被非法劫持,嵌入廣告,可以將協議修改為https來解決。還有我們採用的負載均衡算法是加權輪轉算法,過於簡單,常常出現資源分配不合理的現象,可以將算法改為加權最小連接數算法來解決。這些都是我在今後的系統設計和開發中需要注意與改進的地方,也是日後我應該努力的方向。

三,總結

這篇論文的項目,依舊是之前那片論文的項目-在線教育系統。但是其中很多技術,其實在原有項目中是沒有涉及的。

另外這篇論文與之前論文存在一個結構上的不同之處,那就是這次的核心論點只有兩個分論點。不過,第二個論點-降低複雜度設計,是通過兩個方面進行闡述的。這也算是論文中核心論點的一種回答方式。往往論文的核心論點,推薦使用三個分論點進行論述,而部分論文的核心論點就只能拆分為兩個分論點(或者,三個論點的拆分維度,自己不熟悉)。這時候就需要靈活的轉變自己的思想,將核心論點的兩個分論點氛圍主次論點回答,實際體現就是主論點兩個段落,次論點一個段落。

既然說到這裏,也說一下,如果核心論點可以拆分出多個分論點。如架構風格的層次架構完全可以拆分為接入層,應用層,服務層(基礎服務層,通用服務層,業務服務層),數據接入層,數據源等。那麼這種情況,我們完全可以從中挑選三點自己熟悉的部分,進行闡述。如果擔心這樣寫,文章顯得比較僵硬,就在相關位置寫上“此處,我們以XXX,XXX,XXX為重點,進行論述”這樣的話語即可。

附錄

早期未修改的論文:

摘要:
本人於2015年11月參与浙江省某在線教育平台“外教一對一在線教育”項目,該項目為客戶提供了一對一歐美外教視頻教學,社交圈,公眾直播等功能提供全方位的軟件支撐,在該項目組中我擔任系統架構師崗位,主要負責整體架構設計與中間件選型。本文以該教育平台為例,主要討論了該系統有關可靠性方面的設計與應用。一方面通過負載均衡與應用服務器集群實現容錯技術中冗餘設計的實現,另一方面通過建立了接入層,應用層,服務層,數據層四層層次的架構來降低明確系統結構,從而系統設計複雜度,提高系統可靠性。整個系統開發工作歷時18個月。目前,該系統已經穩定運行近一年半的時間。實踐證明,通過容錯設計,降低複雜度設計等,系統有效提高了可靠性,從而為公司業務提供持續穩定的服務支撐。

正文:
隨着國家對教育的越發重視,英語教育的市場份額逐步上升,其中用戶口語提升的需求越來越大。為此,一些公司開始提供與外國人聊天的平台。我所在公司決定從國際通訊領域進軍口語教育領域。為了這項戰略轉變,公司於2015年11月設計某在線教育平台系統(一下簡稱為“系統”)。該系統幫助人們與歐美外教進行面對面的口語交流和教學。其中隨意聊提供了一種類似QQ視頻通話,而正式課程還提供了H5互動課件與課後點評等,以提高教學質量。與此同時,還有公眾直播用於拉新,AI測試用於評定學院能力,降低成本。我參与了該項目的開發工作,擔任系統架構設計師職務,負責設計系統架構。本項目組全體成員共9人,我主要負責項目計劃制定,需求分析,整體架構設計與技術選型,以及部分底層設計。該項目的架構工作與次年2月完成,選擇了層次架構風格。整個項目耗時18個月,於2017年5月完成。

目前主流的可靠性設計技術有容錯設計,檢錯設計,降低複雜度設計等技術。容錯設計技術分為恢復塊設計,N版本程序設計和冗餘設計。其中恢復塊設計是選擇一組軟件操作作為容錯設計單元,將普通的程序塊編程恢復塊。N版本程序設計的核心是通過設計出多個模塊或不同版本,對於相同初始條件和相同輸入的操作結果,實現多數表決,防止其中某一軟件模塊/版本的故障提供錯誤的服務,以實現軟件容錯。冗餘設計是在一套完整的軟件系統之外,設計一種不同路徑,不同算法或不同實現方法的模塊或系統作為備份,在出現故障時可以使用冗餘的部分進行替換,從而維持軟件系統的正常運行。缺點是費用和資源的消耗會有所增加。檢錯技術是在軟件出現故障后能及時發現並報警。其缺點是不能自動解決故障。降低複雜度設計是因為軟件複雜性與軟件可靠性有着密切關係,是產生軟件缺陷的重要根源。在設計時考慮降低軟件的複雜性,是提高軟件可靠性的有效方法。

在了解系統需求后,我們決定聽從公司技術顧問的建議,在容錯設計,檢錯設計,降低複雜度設計三個主流方向分別作出相應處理和應用。容錯設計主要應用在冗餘設計方面,通過負載均衡,雙機容錯等機制完成冗餘設計。檢錯設計則是通過對Java異常處理機制的設計與封裝處理完成。至於降低複雜度,我們應用層次清晰的四層層次架構。通過將系統劃分為接入層,應用層,服務層,數據層,使得系統的結構明確,立體,從而降低系統複雜度。限於篇幅,接下來,我將從系統的冗餘設計,複雜度降低設計兩個方面介紹可靠性在系統中的設計與應用,以及應用過程中遇到的問題。

首先說冗餘設計,冗餘包含邏輯冗餘,數據冗餘,應用冗餘等。這裏以應用冗餘為例。一方面為了提高應用服務器性能,另一方面為了提高系統的可靠性,可拓展性等,我們採用了負載均衡技術。常見的負載均衡技術有F5硬件,LVS軟件,Nginx服務器配置等。出於便捷與成本的考慮,我們採用了Nginx服務器配置負載均衡技術。通過對Nginx服務器中upstream模塊的配置,就可以實現在多台服務器的反向代理家在負載均衡。為了提高負載均衡服務器可靠性,我們採用雙機熱備機制。但採用負載均衡后,應用服務器集群出現了Session問題無法統一的問題。解決方法有Session Sticky,Session Replication,Session數據集中存儲,Cookie Based四個方案。Session Sticky是通過確保同一個會話的請求都在同一個Web服務器上處理實現。Session Replication是增加Web服務器間會話數據的同步來保證不同Web服務器間的Session數據的一致。但一方面同步Session數據會造成網絡帶寬的開銷。另一方面,每台Web服務器都要保存所有Session數據,消耗大量內存。經過考慮,我們採用了第三種方案-Session數據集中存儲。Session數據集中存儲通過令每台服務器從專門的session服務器獲取session數據來解決問題。優點是可靠性,可移植性與可拓展性的大幅提高。缺點是一方面讀寫Session數據引入了網絡操作,對數據讀取存在時延和不穩定性,但對於使用內網通信的系統並沒有太大影響。另一方面,如果Session服務器或集群出現問題,將會影響整個應用。我們通過雙機容錯機制解決該問題。Cookie Based就是通過Cookie傳遞Session數據完成。實現簡單,但是存在如Cookie長度限制等問題。除此之外,還有心跳線,看門狗等諸多技術。限於篇幅,不再贅述。

再者就是降低複雜度設計,我們從架構風格選擇,技術選型等角度實現。由於系統的複雜性和綜合性,我們決定採用層次架構風格,將系統架構分為接入層,應用層,服務層,數據層四個層次。接入層負責多平台的接入,以及API網關,負載均衡等方面。API網關的使用使得對外資源與服務獲得統一,保持系統結構的明確,從而提高了系統可靠性。應用層分為視圖層與業務邏輯層,視圖層負責App與網站的表現效果,業務邏輯層負責業務層的邏輯處理。為了解決系統日益複雜,應用日益臃腫問題,我們將系統按照應用橫向劃分,將系統劃分為課件管理系統,課程管理系統等十餘個子系統。這樣的劃分使得系統體系變得清晰明了,極大降低系統複雜度,提高系統可靠性。應用層採用基於J2ee的MVC框架-Structs框架。服務層提供通用服務。系統在應用層中按照應用橫向劃分,有效降低系統複雜度。但系統代碼仍然存在冗餘,比如用戶信息的調用在諸多應用子系統中都有相關模塊。另外應用的大小依舊十分巨大,複雜,而過小的應用劃分會增加數據庫連接數負擔,故我們提出服務化解決方案。服務化方案就是提取出各個應用的通用服務,如賬戶服務,Session服務等。出於技術成熟度與技術支持等考慮,我們最終採用了阿里的dubbo服務框架,建立服務層。數據層涉及緩存,文件系統,數據庫,數據通知服務,搜索系統等模塊。由於用戶對數據訪問具有集中性,故我們基於Spring Cache與Redis實現緩存機制。數據訪問方面,Java已經有很多成熟技術,大致分為專用API方式,JDBC方式,給予ORM或類ORM接口方式三種。最終我們採用了成熟的ORM框架-Mybatis框架,再將框架包裝一層。這樣一方面提高系統開發效率,另一方面提高系統可移植性與可靠性。除此之外,還採用了solr作為數據層搜索引擎,數據訪問層物理部署採用Proxy方式。限於篇幅,不再贅述。

最終項目成功上線,正常運行了近一年半,收到各方好評。尤其是H5課件的良好互動性,使得大量業界同行爭相模仿,改用H5製作課件。還有我們的服務化方案架構被作為許多傳統互聯網企業系統重構的經典方案。在系統的架構設計中,我們引入了層次架構的設計思想,有效地降低了維護成本,提高了系統的開放性,可擴展性,可重用性以及可移植性。當然還是存在一些問題的。如H5課件採用http協議,易被非法劫持,嵌入廣告,可以將協議修改為https來解決。還有我們採用的負載均衡算法是加權輪轉算法,過於簡單,常常出現資源分配不合理的現象,可以將算法改為加權最小連接數算法來解決。這些都是我在今後的系統設計和開發中需要注意與改進的地方,也是日後我應該努力的方向。

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

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

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

撥亂反正-重構是門藝術活

前言

引用自: 《重構 改善既有代碼的設計》

重構是在不改變軟件可觀察行為的前提下改善其內部結構。當你面對一個最需要重構的遺留系統時,其規模之大、歷史之久、代碼質量之差,常會使得添加單元測試或者理解其邏輯都成為不可能的任務。此時你唯一能依靠的就是那些已經被證明是行為保持的重構手法: 用絕對安全的手法從焦油坑中整理出可測試的接口,給它添加測試,以此作為繼續重構的立足點。

因為我們部門內容平台的文章系統之前遺留了很多問題,急需解決這些具有”壞味道”的代碼。最後因為其他人手頭裡都有其他工作,最後這些任務就交給了我。以下是急需解決的問題。

  1. 內容平台新增/更新/取消/刪除文章,同步各集團下文章行為狀態,消息鏈路過長的問題。
  2. article分享錶停止規模新增,之前未做插入前的記錄判斷,通過新增的操作來進行記錄留存。
  3. 文章表拆除大字段到分表,如content、content_draft等字段。

鏈路過長概述

內容平台新增/更新/取消/刪除文章,同步各集團下文章行為狀態,消息鏈路過長的問題。

  • 問題導火索: 運營後台文章發布,發送消息到marketing-base

  • 慢鏈路,鏈路過長

    • mysql數據同步,單條執行n次

    • es索引數據同步,dubbo接口調用n次

圖1 鏈路圖

鏈路過長剖解及解決思路

具體問題,具體對待

//開啟同步開關的集團
        List<Integer> groupList = autoSyncStatusService.getAutoSyncGroupByManageType(MANAGE_TYPE_GROUP_ARTICLE); 

    for (Integer groupId : syncSubjectList) {
                SiteGroupInfoDTO siteGroupInfo = siteSPI.getGroupInfoById(groupId);
                Set<String> groupBrandSet = carOnSaleManage.getGroupBrandSet(siteGroupInfo);
                List<String> matchedBrandCodes = extractBrandCodesFromArticleLabel(article.getLabelInfos());
                if (CollectionUtils.isEmpty(matchedBrandCodes) || CollectionUtils.containsAny(groupBrandSet, matchedBrandCodes)) {
                    ArticleGroupMaterialBO groupMaterialBO =
                            ArticleBeanConverter.convertMaterial2GroupMaterial(article, groupId, groupList);
                    // 設置對應的集團主題id
                    ArticleGroupSubjectBO groupSubjectBO =
                            articleGroupSubjectService.getGroupSubjectBySoucheId(groupId, article.getSubjectId());
                    if (Objects.nonNull(groupSubjectBO.getId())) {
                        groupMaterialBO.setSubjectId(groupSubjectBO.getId());
                        groupMaterialBO.setMaterialId(myArticleId);
                        articleGroupMaterialService.addArticleGroupMaterial(groupMaterialBO);
                    }
            }
        } else {
                //查詢同步的文章數據是否存在
                List<ArticleGroupMaterialBO> list = articleGroupMaterialService.getListByMaterialId(myArticleId);
                for (ArticleGroupMaterialBO a : list) {
                    if (groupList.contains(a.getGroupId())) {
                        articleGroupMaterialService.changeRecommendStatus(a.getId(), a.getGroupId(), recommend, article.getLastOperatorName(), article.getLastOperatorName());
                    }
                }
        }
  • 第4行中我們可以看到這裡有一個for循環️,假設開啟同步開關的集體有1000家,則第18行中mysql插入操作就需要執行1000次。

  • 第24行這裏同樣有一個for循環體️,則26行內部的es數據同步則需要調用1000次。它的實現如下:

    @Override
        public boolean changeRecommendStatus(int id, int groupId, int recommended, String lastOperatorUserId, String lastOperatorName) {
            final boolean success = articleGroupMaterialDAO.changeRecommendStatus(
                    id, groupId, recommended, lastOperatorUserId, lastOperatorName) > 0;
            if (success) {
                //更新索引,更改推薦狀態
                articleSearchManage.updateArticleIndex(ArticleIndexUtil.getUpdateRecommendIndex(recommended, id, lastOperatorName));
            }
            return success;
        }

    解決思路

    Mybatis批量插入

    對於第一個循環️體中,我們需要將數據批量添加到數據庫,mybatis提供了將list集合循環添加到數據庫的方法。

    1. mapper層中創建 insertForeach(List < Fund > list) 方法,返回值是批量添加的數據條數
    public interface FundMapper {
      int insertForeach(List<Fund> list);
    }
    1. mybatis的xml文件中的insert語句如下
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    <mapper namespace="com.center.manager.mapper.FundMapper">
    
      <insert id="insertForeach" parameterType="java.util.List" useGeneratedKeys="false">
                  insert into fund
                  ( id,fund_name,fund_code,date_x,data_y,create_by,create_date,update_by,update_date,remarks,del_flag)
                  values
                  <foreach collection="list" item="item" index="index" separator=",">
                      (
                          #{item.id},
                          #{item.fundName},
                          #{item.fundCode},
                          #{item.dateX},
                          #{item.dataY},
                          #{item.createBy},
                          #{item.createDate},
                          #{item.updateBy},
                          #{item.updateDate},
                          #{item.remarks},
                          #{item.delFlag}
                      )
                   </foreach>     
        </insert>    
    </mapper>
    ES批量更新

    com.souche.elastic.search.api.IndexService

    方法:BulkUpdateResponse bulkUpdate(String index, Map<String, Object> event, String query, String origin)
    
    參數:
    
        index:要操作的索引
    
        event:更新的數據,可以只包含需要更新的字段,相當於mysql的update語句中的set語句中的字段
    
        query:query中的條件相當於mysql中的where,具體語法與下面的搜索接口中【querys:string 複雜的複合查詢 不同字段的OR 查詢】相同
    
        origin:操作源,一般寫調用方自己的應用名,用於區分不同調用方
    
    返回值:
    
        BulkUpdateResponse:
    
          {
    
            requestId:本次操作的唯一標示
    
            status:狀態,目前返回默認都是true
    
            updated:成功更新的條數
    
            failed:更新失敗的條數
    
            message:第一條更新失敗的原因
    
          }
    
    調用示例:
    1Map<String, Object> data = new HashMap<>();
    2        data.put("id", 20);
    3        data.put("title", "xue yin");
    4        data.put("content", "kuang dao");
    5        BulkUpdateResponse response = indexService.bulkUpdate("test_index", data, "address=bj AND contry=cn", "shenfl");

    這條更新將test_index索引中所有 address是bj並且contry是cn 的數據的 title更新成‘xue yin’ content更新成‘kuang dao’,注意:address和contry兩個字段在索引中需要加索引

Article表插入邏輯優化,停止規模新增概述

Article邏輯優化剖解及解決思路

具體問題及解決思路

當前article數據表數據量:

select count(*) as 總數 from article;

結果如下:

總數
369737
  @Override
    public String addSharedArticle(ArticleBO articleBO) {
        ArticleDO articleDO = new ArticleDO();
        BeanUtils.copyProperties(articleBO, articleDO);
        String shortUUID = UUIDUtil.getShortUUID();
        articleDO.setUid(shortUUID);
        if (articleDAO.addSharedArticle(articleDO) > 0) {
            return shortUUID;
        }
        return StringUtil.EMPTY_STRING;
    }

從上面這個業務邏輯實現類中,我們可以看到事實上我們想得到的是插入表數據的uid。但是之前的邏輯中,我們並沒有判斷該條數據是否已經存在,我們需要在上面代碼中判斷數據是否存在,已存在,查詢最後一天數據的uid返回給上層。不存在的話,執行插入操作。

文章表拆除大字段到分表

article_material表結構設計

article_material | CREATE TABLE `article_material` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `my_article_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '內容平台我的文章id',
  `status` tinyint(3) unsigned NOT NULL COMMENT '1-待發布、2-發布、3-取消發布',
  `subject_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '主題id',
  `platform_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '平台id',
  `source` varchar(32) NOT NULL DEFAULT '' COMMENT '版塊',
  `crawler_article_id` varchar(32) NOT NULL DEFAULT '0' COMMENT '爬蟲的文章id',
  `title` varchar(64) NOT NULL DEFAULT '' COMMENT '標題',
  `cover_img` varchar(128) NOT NULL COMMENT '封面圖',
  `summary` varchar(255) NOT NULL DEFAULT '' COMMENT '摘要',
  `labels` varchar(512) NOT NULL DEFAULT '' COMMENT '標籤',
  `label_infos` varchar(1024) NOT NULL DEFAULT '' COMMENT '標籤詳細信息',
  `content` text NOT NULL COMMENT '內容,用戶看到的',
  `content_imgs` text NOT NULL COMMENT '內容中圖片',
  `content_videos` varchar(255) NOT NULL DEFAULT '' COMMENT '內容中視頻',
  `content_draft` text NOT NULL COMMENT '草稿內容,編輯后保存到這裏,發布后內容會複製到content,此字段清空',
  `content_imgs_draft` text NOT NULL COMMENT '草稿內容的圖片,同上',
  `content_videos_draft` varchar(255) NOT NULL DEFAULT '' COMMENT '草稿內容的視頻',
  `recommended` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '0-不推薦、1-推薦',
  `author_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '作者userId',
  `author_name` varchar(16) NOT NULL COMMENT '作者名稱',
  `last_operator_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '最後操作人userId',
  `last_operator_name` varchar(16) NOT NULL COMMENT '最後操作人名字',
  `publish_date` datetime DEFAULT NULL COMMENT '發布時間',
  `publisher_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '發布者userId',
  `publisher_name` varchar(16) NOT NULL DEFAULT '' COMMENT '發布者名字',
  `pv` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '流量pv',
  `uv` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '流量uv',
  `share_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '分享次數',
  `share_people_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '分享人數',
  `date_create` datetime NOT NULL,
  `date_update` datetime NOT NULL,
  `date_delete` datetime DEFAULT NULL,
  `deleted` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '0 表示未刪除,刪除后是毫秒級時間戳',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_id` (`my_article_id`),
  KEY `idx_title_label_status` (`subject_id`,`platform_id`,`title`,`label_infos`(255),`source`)
) ENGINE=InnoDB AUTO_INCREMENT=861 DEFAULT CHARSET=utf8 COMMENT='文章素材庫,給集團提供文章素材'

上表中content, content_imgs,content_videos都是text類型等大字段,對於這種類型,我們需要把這種類型的表拆分成2張表 article_metedata和article_content 兩張表。

表拆分圖示

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

【其他文章推薦】

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

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

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

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

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