標籤: 新北清潔

  • 推薦一種通過刷leetcode來增強技術功底的方法

    推薦一種通過刷leetcode來增強技術功底的方法

    背景

    如果前人認為這個一種學習提高或者檢驗能力的成功實踐。而自己目前又沒有更好的方法,那就不妨試一試。

    而不管作為面試官還是被面試者,編碼題最近越來越流行。而兩種角色都需要思考的問題是希望考察什麼能力,通過什麼題目,需要達到怎樣的程度可以說明面試者具有了這樣的能力。

    而要找到上面這些問題的答案,比較好的方式除了看一些理論性文章和接受培訓之外,自己動手刷一刷leetcode切身實踐一下不失為一個不錯的方式。而既然要花精力去做這件事情,那就需要解決一個問題:我從中可以獲得什麼提高。以下是個人的一些經驗和感受。

     

    收益

    對底層有了更深入的了解和思考

    leetcode一些常見題也是平時工作中常用的一些底層數據結構的實現方法。 

    先舉個大家使用比較多的算法:LRU(最近最少使用),在Java的實現中實現特別簡單。只是使用了LinkedHashMap這種數據結構。而如果看一些開源包里LRU的實現,發現也是這樣實現的。實際上動手實現一遍,LRU就再也不會忘了。

    再舉個數據結構的例子:字典樹又叫前綴樹。它是搜索和推薦的基礎。標準點的定義是:

    字典樹又稱單詞查找樹,Tire樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計,排序和保存大量的字符串(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計。它的優點是:利用字符串的公共前綴來減少查詢時間,最大限度地減少無謂的字符串比較,查詢效率比哈希樹高。

    因為之前做過搜索引擎,一直也對這塊很有興趣,所以對它底層知識的補充對個人而言,感覺深度有增加。

     

      

    養成評估時空開銷的習慣

    我刷leetcode必看官方解答里估算的時間和空間複雜度。這也是作為一個架構師的必備基本能力。

    數組、哈希這些因為數據的位置不需要進行查找,只需要算數計算就可以得到,所以它們的時間複雜度是O(1)。

    鏈表如果直接在頭部或者尾部插入,因為不需要查找,所以時間複雜度也是O(1),但是定位的話因為涉及查找,按遍歷查找來算是log(n)。所以對於jdk1.7之前,hashmap底層採用的是數組+鏈表的數據結構。所以如果不算哈希衝突和擴容的話,獲取和插入數據的時間複雜度是O(1);如果出現了哈希衝突,則插入因為是頭部插入,時間複雜度還是O(1);獲取時間複雜度因為涉及先查找,所以是O(n),這個n代表衝突的數量。

    對於在有序數據中進行查找,因為可採用二分查找等優化,時間複雜度可降到log(n).

    對於遞歸調用,如果遞歸方法內進行2次調用。對於層數n來說,時間複雜度是2的n次方。舉個例子就是一個數等於前面兩個數之和。當然,如果是前面3個數之和,不進行優化的情況下時間複雜度就是3的n次方。

    對於一個n*m的二維數組等需要進行嵌套循環遍歷的,時間複雜度是O(n*m),有個特殊情況是n*m,這時候時間複雜度是n的平方。

    對於全排列的情況,時間複雜度是O(n!)。

     

    代碼簡化的方法

     

    我習慣的一種學習方法是先做題,有了一定自己的總結和思考之後,再看書學習別人的總結思考方法。對於刷leetcode相關性高,也比較受認可的書是《Cracking the Coding interview(6th)》,中文版翻譯是《程序員面試金典》。這本書對於面試官和面試者來說讀了都會有一定的收穫。

     

    我讀了這本書,對我印象最深的是介紹了兩種代碼優化的方法:BUD和BCR。

     

    BUD 

    BUD是瓶頸、不必要工作、重複工作 三個詞組首字母的縮寫。

     

    作者提出拿到一道編程題,可先嘗試用暴力解法將題目寫出來,之後找到解法的性能瓶頸,針對瓶頸進行優化,之後在去掉不必要的工作,最後去掉重複的工作。

    這個經典的編程優化方法不只可應用於編程,還可應用於整個程序的優化,也是最常規的優化方法。

     

    BCR

    BCR是Best Conceivable Runtime的縮寫,意思是想知道自己可以優化到什麼程度,先估算可達到的最優情況。

    比如:在一個無序數組中,查找兩個兩個相同的數。直覺來說如果找到這兩個數,最起碼需要將每個數都遍歷一遍,所以可達到的最優情況是O(n),無論怎麼優化,都不可能比這個更好。所以這就是優化的上限。

    這本書里還介紹了其他的優化方法如:使用額外數據結構、通過構建測試用例、根據題目的限制和提示來尋找線索,大家看這本書的時候可以了解下。

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

    【其他文章推薦】

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

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

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

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

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

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

  • 【asp.net core 系列】14 .net core 中的IOC

    【asp.net core 系列】14 .net core 中的IOC

    0.前言

    通過前面幾篇,我們了解到了如何實現項目的基本架構:數據源、路由設置、加密以及身份驗證。那麼在實現的時候,我們還會遇到這樣的一個問題:當我們業務類和數據源越來越多的時候,我們無法通過普通的構造對象的方法為每個實例進行賦值。同時,傳統意義上的賦值遇到底層切換或者其他修改的時候,就需要修改大量的代碼,對改變不友好。為了改變這種現狀,我們基於面向接口編程,然後使用一些DI功能和IOC框架。

    1. IOC和DI

    先來給大家解釋幾個概念,IOC全稱Inversion of Control,翻譯過來就是控制反轉,是面向對象編程的一種設計原則,用來降低代碼之間的耦合度。所謂的控制反轉簡單來講就是將類中屬性或者其他參數的初始化交給其他方處理,而不是直接使用構造函數。

    public class Demo1
    {
    }
    
    public class Demo2
    {
    	public Demo1 demo;
    }
    

    對於以上簡單示例代碼中,在Demo2類中持有了一個Demo1的實例。如果按照之前的情況來講,我們會通過以下方法為demo賦值:

    // 方法一
    public Demo1 demo = new Demo1();
    // 方法二
    public Demo2()
    {
        demo = new Demo1();
    }
    

    這時候,如果Demo1變成下面的樣子:

    public class Demo1
    {
        public Demo1(Demo3 demo3)
        {
            // 隱藏
        }
    }
    public class Demo3
    {
    }
    

    那麼,如果Demo2 沒有持有一個Demo3的實例對象,這時候創建Demo1的時候就需要額外構造一個Demo3。如果Demo3需要持有另外一個類的對象,那麼Demo2中就需要多創建一個對象。最後就會發現這樣就陷入了一個構造“地獄”(我發明的詞,指這種為了一個對象卻得構造一大堆其他類型的對象)。

    實際上,對於Demo2並不關心Demo1的實例對象是如何獲取的,甚至都不關心它是不是Demo1的子類或者接口實現類。我在示例中使用了類,但這裏可以同步替換成Interface,替換之後,Demo2在調用Demo1的時候,還需要知道Demo1有實現類,以及實現類的信息。

    為了解決這個問題,一些高明的程序員們提出了將對象的創建這一過程交給第三方去操作,而不是調用類來創建。於是乎,上述代碼就變成了:

    public class Demo2
    {
        public Demo1 Demo {get;set;}
        public Demo2(Demo1 demo)
        {
            Demo = demo;
        }
    }
    

    似乎並沒有什麼變化?對於Demo2來說,Demo2從此不再負責Demo1的創建,這個步驟交由Demo2的調用方去創建,Demo2從此從負責維護Demo1這個對象的大麻煩中解脫了。

    但實際上構造地獄的問題還是沒有解決,只不過是通過IOC的設計將這一步后移了。這時候,那些大神們想了想,不如開發一個框架這些實體對象吧。所以就出現了很多IOC框架:AutoFac、Sping.net、Unity等。

    說到IOC就不得不提一下DI(Dependency Injection)依賴注入。所謂的依賴注入就是屬性對應實例通過構造函數或者使用屬性由第三方進行賦值。也就是最後Demo2的示例代碼中的寫法。

    早期IOC和DI是指一種技術,後來開始確定這是不同的描述。IOC描述的是一種設計模式,而DI是一種行為。

    2. 使用asp.net core的默認IOC

    在之前的ASP.NET 框架中,微軟並沒有提供默認的IOC支持。在最新的asp.net core中微軟提供了一套IOC支持,該支持在命名空間:

    Microsoft.Extensions.DependencyInjection
    

    里,在代碼中引用即可。

    主要通過以下幾組方法實現:

    public static IServiceCollection AddScoped<TService>(this IServiceCollection services) where TService : class;
    public static IServiceCollection AddSingleton<TService>(this IServiceCollection services) where TService : class;
    public static IServiceCollection AddTransient<TService>(this IServiceCollection services) where TService : class;
    

    這裏只列出了這三組方法的一種重載版本。

    這三組方法分別代表三種生命周期:

    • AddScored 表示對象的生命周期為整個Request請求
    • AddTransient 表示每次從服務容器進行請求時創建的,適合輕量級、 無狀態的服務
    • AddSingleton 表示該對象在第一次從服務容器請求后獲取,之後就不會再次初始化了

    這裏每組方法只介紹了一個版本,但實際上每個方法都有以下幾個版本:

    public static IServiceCollection AddXXX<TService>(this IServiceCollection services) where TService : class;
    public static IServiceCollection AddXXX(this IServiceCollection services, Type serviceType, Type implementationType);
    public static IServiceCollection AddXXX(this IServiceCollection services, Type serviceType, Func<IServiceProvider, object> implementationFactory);
    public static IServiceCollection AddXXX<TService, TImplementation>(this IServiceCollection services)
                where TService : class
                where TImplementation : class, TService;
    public static IServiceCollection AddXXX(this IServiceCollection services, Type serviceType);
    public static IServiceCollection AddXXX<TService>(this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class;
    public static IServiceCollection AddXXX<TService, TImplementation>(this IServiceCollection services, Func<IServiceProvider, TImplementation> implementationFactory)
                where TService : class
                where TImplementation : class, TService;
    

    其中:implementationFactory 表示通過一個Provider實現TService/TImplementation 的工廠方法。當方法指定了泛型的時候,會自動依據泛型參數獲取要注入的類型信息,如果沒有使用泛型則必須手動傳入參數類型。

    asp.net core如果使用依賴注入的話,需要在Startup方法中設置,具體內容可以參照以下:

    public void ConfigureServices(IServiceCollection services)
    {
        //省略其他代碼
        services.AddScoped<ISysUserAuthRepository,SysUserAuthRepository>();
    }
    

    asp.net core 為DbContext提供了不同的IOC支持,AddDbContext:

    public static IServiceCollection AddDbContext<TContext>(
          this IServiceCollection serviceCollection,
          Action<DbContextOptionsBuilder> optionsAction = null,
          ServiceLifetime contextLifetime = ServiceLifetime.Scoped,
          ServiceLifetime optionsLifetime = ServiceLifetime.Scoped)
          where TContext : DbContext;
    

    使用方法如下:

    services.AddDbContext<DefaultContext>();
    

    3. AutoFac 使用

    理論上,asp.net core的IOC已經足夠好了,但是依舊原諒我的貪婪。如果有二三百個業務類需要我來設置的話,我寧願不使用IOC。因為那配置起來就是一場極其痛苦的過程。不過,可喜可賀的是AutoFac可以讓我免收這部分的困擾。

    這裏簡單介紹一下如何使用AutoFac作為IOC管理:

    cd Web  # 切換目錄到Web項目
    dotnet package add Autofac.Extensions.DependencyInjection # 添加 AutoFac的引用
    

    因為asp.net core 版本3更改了一些邏輯,AutoFac的引用方式發生了改變,現在不介紹之前版本的內容,以3為主。使用AutoFac需要先在 Program類里設置以下代碼:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        		Host.CreateDefaultBuilder(args)
        		.UseServiceProviderFactory(new AutofacServiceProviderFactory()) // 添加這行代碼
        		.ConfigureWebHostDefaults(webBuilder =>
    			{
                    webBuilder.UseStartup<Startup>();
                });
    

    在Program類里啟用AutoFac的一個Service提供工廠類。然後在Startup類里添加如下方法:

    public void ConfigureContainer(ContainerBuilder builder)
    {
        builder.RegisterType<DefaultContext>().As<DbContext>()
                    .WithParameter("connectStr","Data Source=./demo.db")
                    .InstancePerLifetimeScope();
                
    
        builder.RegisterAssemblyTypes(Assembly.Load("Web"))
            .Where(t => t.BaseType.FullName.Contains("Filter"))
            .AsSelf();
    
        builder.RegisterAssemblyTypes(Assembly.Load("Domain"),
                        Assembly.Load("Domain.Implements"), Assembly.Load("Service"), Assembly.Load("Service.Implements"))
                    .AsSelf()
                    .AsImplementedInterfaces()
                    .InstancePerLifetimeScope()
                    .PropertiesAutowired();
    }
    
    

    修改:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews(options =>
                {
                    options.Filters.Add<UnitOfWorkFilterAttribute>();
                }).AddControllersAsServices();// 這行新增
        // 省略其他
    }
    

    4. 總結

    這一篇簡單介紹了如何在Asp.net Core中啟用IOC支持,並提供了兩種方式,可以說是各有優劣。小夥伴們根據自己需要選擇。後續會為大家詳細深入AutoFac之類IOC框架的核心秘密。

    更多內容煩請關注我的博客《高先生小屋》

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

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

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

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

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

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

    ※回頭車貨運收費標準

  • 重學 Java 設計模式:實戰中介者模式「按照Mybatis原理手寫ORM框架,給JDBC方式操作數據庫增加中介者場景」

    重學 Java 設計模式:實戰中介者模式「按照Mybatis原理手寫ORM框架,給JDBC方式操作數據庫增加中介者場景」

    作者:小傅哥
    博客:https://bugstack.cn – 原創系列專題文章

    沉澱、分享、成長,讓自己和他人都能有所收穫!

    一、前言

    同齡人的差距是從什麼時候拉開的

    同樣的幼兒園、同樣的小學、一樣的書本、一樣的課堂,有人學習好、有人學習差。不只是上學,幾乎人生處處都是賽道,發令槍響起的時刻,也就把人生的差距拉開。編程開發這條路也是很長很寬,有人跑得快有人跑得慢。那麼你是否想起過,這一點點的差距到遙不可及的距離,是從哪一天開始的。摸摸肚子的肉,看看遠處的路,別人講的是故事,你想起的都是事故

    思想沒有產品高才寫出一片的ifelse

    當你承接一個需求的時候,比如;交易、訂單、營銷、保險等各類場景。如果你不熟悉這個場景下的業務模式,以及將來的拓展方向,那麼很難設計出良好可擴展的系統。再加上產品功能初建,說老闆要的急,儘快上線。作為程序員的你更沒有時間思考,整體一看現在的需求也不難,直接上手開干(一個方法兩個if語句),這樣確實滿足了當前需求。但老闆的想法多呀,產品也跟着變化快,到你這就是改改改,加加加。當然你也不客氣,回首掏就是1024個if語句!

    日積月累的技術沉澱是為了厚積薄發

    粗略的估算過,如果從上大學開始每天寫200行,一個月是6000行,一年算10個月話,就是6萬行,第三年出去實習的是時候就有20萬行的代碼量。如果你能做到這一點,找工作難?有時候很多事情就是靠時間積累出來的,想走捷徑有時候真的沒有。你的技術水平、你的業務能力、你身上的肉,都是一點點積累下來的,不要浪費看似很短的時間,一年年堅持下來,留下印刻青春的痕迹,多給自己武裝上一些能力。

    二、開發環境

    1. JDK 1.8
    2. Idea + Maven
    3. mysql 5.1.20
    4. 涉及工程一個,可以通過關注公眾號bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
    工程 描述
    itstack-demo-design-16-01 使用JDBC方式連接數據庫
    itstack-demo-design-16-02 手寫ORM框架操作數據庫

    三、中介者模式介紹

    中介者模式要解決的就是複雜功能應用之間的重複調用,在這中間添加一層中介者包裝服務,對外提供簡單、通用、易擴展的服務能力。

    這樣的設計模式幾乎在我們日常生活和實際業務開發中都會見到,例如;飛機降落有小姐姐在塔台喊話、無論哪個方向來的候車都從站台上下、公司的系統中有一个中台專門為你包裝所有接口和提供統一的服務等等,這些都運用了中介者模式。除此之外,你用到的一些中間件,他們包裝了底層多種數據庫的差異化,提供非常簡單的方式進行使用。

    四、案例場景模擬

    在本案例中我們通過模仿Mybatis手寫ORM框架,通過這樣操作數據庫學習中介者運用場景

    除了這樣的中間件層使用場景外,對於一些外部接口,例如N種獎品服務,可以由中台系統進行統一包裝對外提供服務能力。也是中介者模式的一種思想體現。

    在本案例中我們會把jdbc層進行包裝,讓用戶在使用數據庫服務的時候,可以和使用mybatis一樣簡單方便,通過這樣的源碼方式學習中介者模式,也方便對源碼知識的拓展學習,增強知識棧。

    五、用一坨坨代碼實現

    這是一種關於數據庫操作最初的方式

    基本上每一個學習開發的人都學習過直接使用jdbc方式連接數據庫,進行CRUD操作。以下的例子可以當做回憶。

    1. 工程結構

    itstack-demo-design-16-01
    └── src
        └── main
            └── java
                └── org.itstack.demo.design
                    └── JDBCUtil.java
    
    • 這裏的類比較簡單隻包括了一個數據庫操作類。

    2. 代碼實現

    public class JDBCUtil {
    
        private static Logger logger = LoggerFactory.getLogger(JDBCUtil.class);
    
        public static final String URL = "jdbc:mysql://127.0.0.1:3306/itstack-demo-design";
        public static final String USER = "root";
        public static final String PASSWORD = "123456";
    
        public static void main(String[] args) throws Exception {
            //1. 加載驅動程序
            Class.forName("com.mysql.jdbc.Driver");
            //2. 獲得數據庫連接
            Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
            //3. 操作數據庫
            Statement stmt = conn.createStatement();
            ResultSet resultSet = stmt.executeQuery("SELECT id, name, age, createTime, updateTime FROM user");
            //4. 如果有數據 resultSet.next() 返回true
            while (resultSet.next()) {
                logger.info("測試結果 姓名:{} 年齡:{}", resultSet.getString("name"),resultSet.getInt("age"));
            }
        }
    
    }
    
    • 以上是使用JDBC的方式進行直接操作數據庫,幾乎大家都使用過這樣的方式。

    3. 測試結果

    15:38:10.919 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:水水 年齡:18
    15:38:10.922 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:豆豆 年齡:18
    15:38:10.922 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:花花 年齡:19
    
    Process finished with exit code 0
    
    • 從測試結果可以看到這裏已經查詢到了數據庫中的數據。只不過如果在全部的業務開發中都這樣實現,會非常的麻煩。

    六、中介模式開發ORM框架

    `接下來就使用中介模式的思想完成模仿Mybatis的ORM框架開發~

    1. 工程結構

    itstack-demo-design-16-02
    └── src
        ├── main
        │   ├── java
        │   │   └── org.itstack.demo.design
        │   │       ├── dao
        │   │       │	├── ISchool.java
        │   │       │	└── IUserDao.java
        │   │       ├── mediator
        │   │       │	├── Configuration.java
        │   │       │	├── DefaultSqlSession.java
        │   │       │	├── DefaultSqlSessionFactory.java
        │   │       │	├── Resources.java
        │   │       │	├── SqlSession.java
        │   │       │	├── SqlSessionFactory.java
        │   │       │	├── SqlSessionFactoryBuilder.java
        │   │       │	└── SqlSessionFactoryBuilder.java
        │   │       └── po
        │   │         	├── School.java
        │   │         	└── User.java
        │   └── resources
        │       ├── mapper
        │       │   ├── School_Mapper.xml
        │       │   └── User_Mapper.xml
        │       └── mybatis-config-datasource.xml
        └── test
             └── java
                 └── org.itstack.demo.design.test
                     └── ApiTest.java
    

    中介者模式模型結構

    • 以上是對ORM框架實現的核心類,包括了;加載配置文件、對xml解析、獲取數據庫session、操作數據庫以及結果返回。
    • 左上是對數據庫的定義和處理,基本包括我們常用的方法;<T> T selectOne<T> List<T> selectList等。
    • 右側藍色部分是對數據庫配置的開啟session的工廠處理類,這裏的工廠會操作DefaultSqlSession
    • 之後是紅色地方的SqlSessionFactoryBuilder,這個類是對數據庫操作的核心類;處理工廠、解析文件、拿到session等。

    接下來我們就分別介紹各個類的功能實現過程。

    2. 代碼實現

    2.1 定義SqlSession接口

    public interface SqlSession {
    
        <T> T selectOne(String statement);
    
        <T> T selectOne(String statement, Object parameter);
    
        <T> List<T> selectList(String statement);
    
        <T> List<T> selectList(String statement, Object parameter);
    
        void close();
    }
    
    • 這裏定義了對數據庫操作的查詢接口,分為查詢一個結果和查詢多個結果,同時包括有參數和沒有參數的方法。

    2.2 SqlSession具體實現類

    public class DefaultSqlSession implements SqlSession {
    
        private Connection connection;
        private Map<String, XNode> mapperElement;
    
        public DefaultSqlSession(Connection connection, Map<String, XNode> mapperElement) {
            this.connection = connection;
            this.mapperElement = mapperElement;
        }
    
        @Override
        public <T> T selectOne(String statement) {
            try {
                XNode xNode = mapperElement.get(statement);
                PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
                ResultSet resultSet = preparedStatement.executeQuery();
                List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
                return objects.get(0);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        @Override
        public <T> List<T> selectList(String statement) {
            XNode xNode = mapperElement.get(statement);
            try {
                PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
                ResultSet resultSet = preparedStatement.executeQuery();
                return resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        // ...
    
        private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {
            List<T> list = new ArrayList<>();
            try {
                ResultSetMetaData metaData = resultSet.getMetaData();
                int columnCount = metaData.getColumnCount();
                // 每次遍歷行值
                while (resultSet.next()) {
                    T obj = (T) clazz.newInstance();
                    for (int i = 1; i <= columnCount; i++) {
                        Object value = resultSet.getObject(i);
                        String columnName = metaData.getColumnName(i);
                        String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
                        Method method;
                        if (value instanceof Timestamp) {
                            method = clazz.getMethod(setMethod, Date.class);
                        } else {
                            method = clazz.getMethod(setMethod, value.getClass());
                        }
                        method.invoke(obj, value);
                    }
                    list.add(obj);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return list;
        }
    
        @Override
        public void close() {
            if (null == connection) return;
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 這裏包括了接口定義的方法實現,也就是包裝了jdbc層。
    • 通過這樣的包裝可以讓對數據庫的jdbc操作隱藏起來,外部調用的時候對入參、出參都有內部進行處理。

    2.3 定義SqlSessionFactory接口

    public interface SqlSessionFactory {
    
        SqlSession openSession();
    
    }
    
    • 開啟一個SqlSession, 這幾乎是大家在平時的使用中都需要進行操作的內容。雖然你看不見,但是當你有數據庫操作的時候都會獲取每一次執行的SqlSession

    2.4 SqlSessionFactory具體實現類

    public class DefaultSqlSessionFactory implements SqlSessionFactory {
    
        private final Configuration configuration;
    
        public DefaultSqlSessionFactory(Configuration configuration) {
            this.configuration = configuration;
        }
    
        @Override
        public SqlSession openSession() {
            return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
        }
    
    }
    
    • DefaultSqlSessionFactory,是使用mybatis最常用的類,這裏我們簡單的實現了一個版本。
    • 雖然是簡單的版本,但是包括了最基本的核心思路。當開啟SqlSession時會進行返回一個DefaultSqlSession
    • 這個構造函數中向下傳遞了Configuration配置文件,在這個配置文件中包括;Connection connectionMap<String, String> dataSourceMap<String, XNode> mapperElement。如果有你閱讀過Mybatis源碼,對這個就不會陌生。

    2.5 SqlSessionFactoryBuilder實現

    public class SqlSessionFactoryBuilder {
    
        public DefaultSqlSessionFactory build(Reader reader) {
            SAXReader saxReader = new SAXReader();
            try {
                saxReader.setEntityResolver(new XMLMapperEntityResolver());
                Document document = saxReader.read(new InputSource(reader));
                Configuration configuration = parseConfiguration(document.getRootElement());
                return new DefaultSqlSessionFactory(configuration);
            } catch (DocumentException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        private Configuration parseConfiguration(Element root) {
            Configuration configuration = new Configuration();
            configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
            configuration.setConnection(connection(configuration.dataSource));
            configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
            return configuration;
        }
    
        // 獲取數據源配置信息
        private Map<String, String> dataSource(List<Element> list) {
            Map<String, String> dataSource = new HashMap<>(4);
            Element element = list.get(0);
            List content = element.content();
            for (Object o : content) {
                Element e = (Element) o;
                String name = e.attributeValue("name");
                String value = e.attributeValue("value");
                dataSource.put(name, value);
            }
            return dataSource;
        }
    
        private Connection connection(Map<String, String> dataSource) {
            try {
                Class.forName(dataSource.get("driver"));
                return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
            } catch (ClassNotFoundException | SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        // 獲取SQL語句信息
        private Map<String, XNode> mapperElement(List<Element> list) {
            Map<String, XNode> map = new HashMap<>();
    
            Element element = list.get(0);
            List content = element.content();
            for (Object o : content) {
                Element e = (Element) o;
                String resource = e.attributeValue("resource");
    
                try {
                    Reader reader = Resources.getResourceAsReader(resource);
                    SAXReader saxReader = new SAXReader();
                    Document document = saxReader.read(new InputSource(reader));
                    Element root = document.getRootElement();
                    //命名空間
                    String namespace = root.attributeValue("namespace");
    
                    // SELECT
                    List<Element> selectNodes = root.selectNodes("select");
                    for (Element node : selectNodes) {
                        String id = node.attributeValue("id");
                        String parameterType = node.attributeValue("parameterType");
                        String resultType = node.attributeValue("resultType");
                        String sql = node.getText();
    
                        // ? 匹配
                        Map<Integer, String> parameter = new HashMap<>();
                        Pattern pattern = Pattern.compile("(#\\{(.*?)})");
                        Matcher matcher = pattern.matcher(sql);
                        for (int i = 1; matcher.find(); i++) {
                            String g1 = matcher.group(1);
                            String g2 = matcher.group(2);
                            parameter.put(i, g2);
                            sql = sql.replace(g1, "?");
                        }
    
                        XNode xNode = new XNode();
                        xNode.setNamespace(namespace);
                        xNode.setId(id);
                        xNode.setParameterType(parameterType);
                        xNode.setResultType(resultType);
                        xNode.setSql(sql);
                        xNode.setParameter(parameter);
    
                        map.put(namespace + "." + id, xNode);
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
    
            }
            return map;
        }
    
    }
    
    • 在這個類中包括的核心方法有;build(構建實例化元素)parseConfiguration(解析配置)dataSource(獲取數據庫配置)connection(Map<String, String> dataSource) (鏈接數據庫)mapperElement (解析sql語句)
    • 接下來我們分別介紹這樣的幾個核心方法。

    build(構建實例化元素)

    這個類主要用於創建解析xml文件的類,以及初始化SqlSession工廠類DefaultSqlSessionFactory。另外需要注意這段代碼saxReader.setEntityResolver(new XMLMapperEntityResolver());,是為了保證在不聯網的時候一樣可以解析xml,否則會需要從互聯網獲取dtd文件。

    parseConfiguration(解析配置)

    是對xml中的元素進行獲取,這裏主要獲取了;dataSourcemappers,而這兩個配置一個是我們數據庫的鏈接信息,另外一個是對數據庫操作語句的解析。

    connection(Map<String, String> dataSource) (鏈接數據庫)

    鏈接數據庫的地方和我們常見的方式是一樣的;Class.forName(dataSource.get("driver"));,但是這樣包裝以後外部是不需要知道具體的操作。同時當我們需要鏈接多套數據庫的時候,也是可以在這裏擴展。

    mapperElement (解析sql語句)

    這部分代碼塊內容相對來說比較長,但是核心的點就是為了解析xml中的sql語句配置。在我們平常的使用中基本都會配置一些sql語句,也有一些入參的佔位符。在這裏我們使用正則表達式的方式進行解析操作。

    解析完成的sql語句就有了一個名稱和sql的映射關係,當我們進行數據庫操作的時候,這個組件就可以通過映射關係獲取到對應sql語句進行操作。

    3. 測試驗證

    在測試之前需要導入sql語句到數據庫中;

    • 庫名:itstack-demo-design
    • 表名:userschool
    CREATE TABLE school ( id bigint NOT NULL AUTO_INCREMENT, name varchar(64), address varchar(256), createTime datetime, updateTime datetime, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    insert into school (id, name, address, createTime, updateTime) values (1, '北京大學', '北京市海淀區頤和園路5號', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
    insert into school (id, name, address, createTime, updateTime) values (2, '南開大學', '中國天津市南開區衛津路94號', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
    insert into school (id, name, address, createTime, updateTime) values (3, '同濟大學', '上海市彰武路1號同濟大廈A樓7樓7區', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
    CREATE TABLE user ( id bigint(11) NOT NULL AUTO_INCREMENT, name varchar(32), age int(4), address varchar(128), entryTime datetime, remark varchar(64), createTime datetime, updateTime datetime, status int(4) DEFAULT '0', dateTime varchar(64), PRIMARY KEY (id), INDEX idx_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (1, '水水', 18, '吉林省榆樹市黑林鎮尹家村5組', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200309');
    insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (2, '豆豆', 18, '遼寧省大連市清河灣司馬道407路', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 1, null);
    insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (3, '花花', 19, '遼寧省大連市清河灣司馬道407路', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200310');
    

    3.1 創建數據庫對象類

    用戶類

    public class User {
    
        private Long id;
        private String name;
        private Integer age;
        private Date createTime;
        private Date updateTime;
        
        // ... get/set
    }
    

    學校類

    public class School {
    
        private Long id;
        private String name;
        private String address;
        private Date createTime;
        private Date updateTime;  
      
        // ... get/set
    }
    
    • 這兩個類都非常簡單,就是基本的數據庫信息。

    3.2 創建DAO包

    用戶Dao

    public interface IUserDao {
    
         User queryUserInfoById(Long id);
    
    }
    

    學校Dao

    public interface ISchoolDao {
    
        School querySchoolInfoById(Long treeId);
    
    }
    

    3.3 ORM配置文件

    鏈接配置

    <configuration>
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack_demo_design?useUnicode=true"/>
                    <property name="username" value="root"/>
                    <property name="password" value="123456"/>
                </dataSource>
            </environment>
        </environments>
    
        <mappers>
            <mapper resource="mapper/User_Mapper.xml"/>
            <mapper resource="mapper/School_Mapper.xml"/>
        </mappers>
    
    </configuration>
    
    • 這個配置與我們平常使用的mybatis基本是一樣的,包括了數據庫的連接池信息以及需要引入的mapper映射文件。

    操作配置(用戶)

    <mapper namespace="org.itstack.demo.design.dao.IUserDao">
    
        <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.design.po.User">
            SELECT id, name, age, createTime, updateTime
            FROM user
            where id = #{id}
        </select>
    
        <select id="queryUserList" parameterType="org.itstack.demo.design.po.User" resultType="org.itstack.demo.design.po.User">
            SELECT id, name, age, createTime, updateTime
            FROM user
            where age = #{age}
        </select>
    
    </mapper>
    

    操作配置(學校)

    <mapper namespace="org.itstack.demo.design.dao.ISchoolDao">
    
        <select id="querySchoolInfoById" resultType="org.itstack.demo.design.po.School">
            SELECT id, name, address, createTime, updateTime
            FROM school
            where id = #{id}
        </select>
    
    </mapper>
    

    3.4 單個結果查詢測試

    @Test
    public void test_queryUserInfoById() {
        String resource = "mybatis-config-datasource.xml";
        Reader reader;
        try {
            reader = Resources.getResourceAsReader(resource);
            SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
            SqlSession session = sqlMapper.openSession();
            try {
                User user = session.selectOne("org.itstack.demo.design.dao.IUserDao.queryUserInfoById", 1L);
                logger.info("測試結果:{}", JSON.toJSONString(user));
            } finally {
                session.close();
                reader.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    • 這裏的使用方式和Mybatis是一樣的,都包括了;資源加載和解析、SqlSession工廠構建、開啟SqlSession以及最後執行查詢操作selectOne

    測試結果

    16:56:51.831 [main] INFO  org.itstack.demo.design.demo.ApiTest - 測試結果:{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}
    
    Process finished with exit code 0
    
    • 從結果上看已經滿足了我們的查詢需求。

    3.5 集合結果查詢測試

    @Test
    public void test_queryUserList() {
        String resource = "mybatis-config-datasource.xml";
        Reader reader;
        try {
            reader = Resources.getResourceAsReader(resource);
            SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
            SqlSession session = sqlMapper.openSession();
            try {
                User req = new User();
                req.setAge(18);
                List<User> userList = session.selectList("org.itstack.demo.design.dao.IUserDao.queryUserList", req);
                logger.info("測試結果:{}", JSON.toJSONString(userList));
            } finally {
                session.close();
                reader.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    • 這個測試內容與以上只是查詢方法有所不同;session.selectList,是查詢一個集合結果。

    測試結果

    16:58:13.963 [main] INFO  org.itstack.demo.design.demo.ApiTest - 測試結果:[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}]
    
    Process finished with exit code 0
    
    • 測試驗證集合的結果也是正常的,目前位置測試全部通過。

    七、總結

    • 以上通過中介者模式的設計思想我們手寫了一個ORM框架,隱去了對數據庫操作的複雜度,讓外部的調用方可以非常簡單的進行操作數據庫。這也是我們平常使用的Mybatis的原型,在我們日常的開發使用中,只需要按照配置即可非常簡單的操作數據庫。
    • 除了以上這種組件模式的開發外,還有服務接口的包裝也可以使用中介者模式來實現。比如你們公司有很多的獎品接口需要在營銷活動中對接,那麼可以把這些獎品接口統一收到中台開發一個獎品中心,對外提供服務。這樣就不需要每一個需要對接獎品的接口,都去找具體的提供者,而是找中台服務即可。
    • 在上述的實現和測試使用中可以看到,這種模式的設計滿足了;單一職責開閉原則,也就符合了迪米特原則,即越少人知道越好。外部的人只需要按照需求進行調用,不需要知道具體的是如何實現的,複雜的一面已經有組件合作服務平台處理。

    八、推薦閱讀

    • 1. 重學 Java 設計模式:實戰工廠方法模式「多種類型商品不同接口,統一發獎服務搭建場景」
    • 2. 重學 Java 設計模式:實戰原型模式「上機考試多套試,每人題目和答案亂序排列場景」
    • 3. 重學 Java 設計模式:實戰橋接模式「多支付渠道(微信、支付寶)與多支付模式(刷臉、指紋)場景」
    • 4. 重學 Java 設計模式:實戰組合模式「營銷差異化人群發券,決策樹引擎搭建場景」
    • 5. 重學 Java 設計模式:實戰外觀模式「基於SpringBoot開發門面模式中間件,統一控制接口白名單場景」
    • 6. 重學 Java 設計模式:實戰享元模式「基於Redis秒殺,提供活動與庫存信息查詢場景」

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

    【其他文章推薦】

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

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

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

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

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

  • 為什麼建議你使用枚舉?

    為什麼建議你使用枚舉?

    枚舉是 JDK 1.5 新增的數據類型,使用枚舉我們可以很好的描述一些特定的業務場景,比如一年中的春、夏、秋、冬,還有每周的周一到周天,還有各種顏色,以及可以用它來描述一些狀態信息,比如錯誤碼等。

    枚舉類型不止存在在 Java 語言中,在其它語言中也都能找到它的身影,例如 C# 和 Python 等,但我發現在實際的項目中使用枚舉的人很少,所以本文就來聊一聊枚舉的相關內容,好讓朋友們對枚舉有一個大概的印象,這樣在編程時起碼還能想到有“枚舉”這樣一個類型。

    本文的結構目錄如下:

    枚舉的 7 種使用方法

    很多人不使用枚舉的一個重要的原因是對枚舉不夠熟悉,那麼我們就先從枚舉的 7 種使用方法說起。

    用法一:常量

    在 JDK 1.5 之前,我們定義常量都是 public static final... ,但有了枚舉,我們就可以把這些常量定義成一個枚舉類了,實現代碼如下:

    public enum ColorEnum {  
      RED, GREEN, BLANK, YELLOW  
    } 
    

    用法二:switch

    將枚舉用在 switch 判斷中,使得代碼可讀性更高了,實現代碼如下:

    enum ColorEnum {
        GREEN, YELLOW, RED
    }
    public class ColorTest {
        ColorEnum color = ColorEnum.RED;
    
        public void change() {
            switch (color) {
                case RED:
                    color = ColorEnum.GREEN;
                    break;
                case YELLOW:
                    color = ColorEnum.RED;
                    break;
                case GREEN:
                    color = ColorEnum.YELLOW;
                    break;
            }
        }
    }
    

    用法三:枚舉中增加方法

    我們可以在枚舉中增加一些方法,讓枚舉具備更多的特性,實現代碼如下:

    public class EnumTest {
        public static void main(String[] args) {
            ErrorCodeEnum errorCode = ErrorCodeEnum.SUCCESS;
            System.out.println("狀態碼:" + errorCode.code() + 
                               " 狀態信息:" + errorCode.msg());
        }
    }
    
    enum ErrorCodeEnum {
        SUCCESS(1000, "success"),
        PARAM_ERROR(1001, "parameter error"),
        SYS_ERROR(1003, "system error"),
        NAMESPACE_NOT_FOUND(2001, "namespace not found"),
        NODE_NOT_EXIST(3002, "node not exist"),
        NODE_ALREADY_EXIST(3003, "node already exist"),
        UNKNOWN_ERROR(9999, "unknown error");
    
        private int code;
        private String msg;
    
        ErrorCodeEnum(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    
        public int code() {
            return code;
        }
    
        public String msg() {
            return msg;
        }
    
        public static ErrorCodeEnum getErrorCode(int code) {
            for (ErrorCodeEnum it : ErrorCodeEnum.values()) {
                if (it.code() == code) {
                    return it;
                }
            }
            return UNKNOWN_ERROR;
        }
    }
    

    以上程序的執行結果為:

    狀態碼:1000 狀態信息:success

    用法四:覆蓋枚舉方法

    我們可以覆蓋一些枚舉中的方法用於實現自己的業務,比如我們可以覆蓋 toString() 方法,實現代碼如下:

    public class EnumTest {
        public static void main(String[] args) {
            ColorEnum colorEnum = ColorEnum.RED;
            System.out.println(colorEnum.toString());
        }
    }
    
    enum ColorEnum {
        RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
        //  成員變量
        private String name;
        private int index;
    
        //  構造方法
        private ColorEnum(String name, int index) {
            this.name = name;
            this.index = index;
        }
    
        //覆蓋方法
        @Override
        public String toString() {
            return this.index + ":" + this.name;
        }
    }
    

    以上程序的執行結果為:

    1:紅色

    用法五:實現接口

    枚舉類可以用來實現接口,但不能用於繼承類,因為枚舉默認繼承了 java.lang.Enum 類,在 Java 語言中允許實現多接口,但不能繼承多個父類,實現代碼如下:

    public class EnumTest {
        public static void main(String[] args) {
            ColorEnum colorEnum = ColorEnum.RED;
            colorEnum.print();
            System.out.println("顏色:" + colorEnum.getInfo());
        }
    }
    
    interface Behaviour {
        void print();
    
        String getInfo();
    }
    
    enum ColorEnum implements Behaviour {
        RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
        private String name;
        private int index;
    
        private ColorEnum(String name, int index) {
            this.name = name;
            this.index = index;
        }
    
        @Override
        public void print() {
            System.out.println(this.index + ":" + this.name);
        }
    
        @Override
        public String getInfo() {
            return this.name;
        }
    }
    

    以上程序的執行結果為:

    1:紅色

    顏色:紅色

    用法六:在接口中組織枚舉類

    我們可以在一個接口中創建多個枚舉類,用它可以很好的實現“多態”,也就是說我們可以將擁有相同特性,但又有細微實現差別的枚舉類聚集在一個接口中,實現代碼如下:

    public class EnumTest {
        public static void main(String[] args) {
            // 賦值第一個枚舉類
            ColorInterface colorEnum = ColorInterface.ColorEnum.RED;
            System.out.println(colorEnum);
            // 賦值第二個枚舉類
            colorEnum = ColorInterface.NewColorEnum.NEW_RED;
            System.out.println(colorEnum);
        }
    }
    
    interface ColorInterface {
        enum ColorEnum implements ColorInterface {
            GREEN, YELLOW, RED
        }
        enum NewColorEnum implements ColorInterface {
            NEW_GREEN, NEW_YELLOW, NEW_RED
        }
    }
    

    以上程序的執行結果為:

    RED

    NEW_RED

    用法七:使用枚舉集合

    在 Java 語言中和枚舉類相關的,還有兩個枚舉集合類 java.util.EnumSetjava.util.EnumMap,使用它們可以實現更多的功能。

    使用 EnumSet 可以保證元素不重複,並且能獲取指定範圍內的元素,示例代碼如下:

    import java.util.ArrayList;
    import java.util.EnumSet;
    import java.util.List;
    
    public class EnumTest {
        public static void main(String[] args) {
            List<ColorEnum> list = new ArrayList<ColorEnum>();
            list.add(ColorEnum.RED);
            list.add(ColorEnum.RED);  // 重複元素
            list.add(ColorEnum.YELLOW);
            list.add(ColorEnum.GREEN);
            // 去掉重複數據
            EnumSet<ColorEnum> enumSet = EnumSet.copyOf(list);
            System.out.println("去重:" + enumSet);
    
            // 獲取指定範圍的枚舉(獲取所有的失敗狀態)
            EnumSet<ErrorCodeEnum> errorCodeEnums = EnumSet.range(ErrorCodeEnum.ERROR, ErrorCodeEnum.UNKNOWN_ERROR);
            System.out.println("所有失敗狀態:" + errorCodeEnums);
        }
    }
    
    enum ColorEnum {
        RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
        private String name;
        private int index;
    
        private ColorEnum(String name, int index) {
            this.name = name;
            this.index = index;
        }
    }
    
    enum ErrorCodeEnum {
        SUCCESS(1000, "success"),
        ERROR(2001, "parameter error"),
        SYS_ERROR(2002, "system error"),
        NAMESPACE_NOT_FOUND(2003, "namespace not found"),
        NODE_NOT_EXIST(3002, "node not exist"),
        NODE_ALREADY_EXIST(3003, "node already exist"),
        UNKNOWN_ERROR(9999, "unknown error");
    
        private int code;
        private String msg;
    
        ErrorCodeEnum(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    
        public int code() {
            return code;
        }
    
        public String msg() {
            return msg;
        }
    }
    

    以上程序的執行結果為:

    去重:[RED, GREEN, YELLOW]

    所有失敗狀態:[ERROR, SYS_ERROR, NAMESPACE_NOT_FOUND, NODE_NOT_EXIST, NODE_ALREADY_EXIST, UNKNOWN_ERROR]

    EnumMapHashMap 類似,不過它是一個專門為枚舉設計的 Map 集合,相比 HashMap 來說它的性能更高,因為它內部放棄使用鏈表和紅黑樹的結構,採用數組作為數據存儲的結構。

    EnumMap 基本使用示例如下:

    import java.util.EnumMap;
    
    public class EnumTest {
        public static void main(String[] args) {
            EnumMap<ColorEnum, String> enumMap = new EnumMap<>(ColorEnum.class);
            enumMap.put(ColorEnum.RED, "紅色");
            enumMap.put(ColorEnum.GREEN, "綠色");
            enumMap.put(ColorEnum.BLANK, "白色");
            enumMap.put(ColorEnum.YELLOW, "黃色");
            System.out.println(ColorEnum.RED + ":" + enumMap.get(ColorEnum.RED));
        }
    }
    
    enum ColorEnum {
        RED, GREEN, BLANK, YELLOW;
    }
    

    以上程序的執行結果為:

    RED:紅色

    使用注意事項

    阿里《Java開發手冊》對枚舉的相關規定如下,我們在使用時需要稍微注意一下。

    【強制】所有的枚舉類型字段必須要有註釋,說明每個數據項的用途。

    【參考】枚舉類名帶上 Enum 後綴,枚舉成員名稱需要全大寫,單詞間用下劃線隔開。說明:枚舉其實就是特殊的常量類,且構造方法被默認強制是私有。正例:枚舉名字為 ProcessStatusEnum 的成員名稱:SUCCESS / UNKNOWN_REASON。

    假如不使用枚舉

    在枚舉沒有誕生之前,也就是 JDK 1.5 版本之前,我們通常會使用 int 常量來表示枚舉,實現代碼如下:

    public static final int COLOR_RED = 1;
    public static final int COLOR_BLUE = 2;
    public static final int COLOR_GREEN = 3;
    

    但是使用 int 類型可能存在兩個問題:

    第一, int 類型本身並不具備安全性,假如某個程序員在定義 int 時少些了一個 final 關鍵字,那麼就會存在被其他人修改的風險,而反觀枚舉類,它“天然”就是一個常量類,不存在被修改的風險(原因詳見下半部分);

    第二,使用 int 類型的語義不夠明確,比如我們在控制台打印時如果只輸出 1…2…3 這樣的数字,我們肯定不知道它代表的是什麼含義。

    那有人就說了,那就使用常量字符唄,這總不會還不知道語義吧?實現示例代碼如下:

    public static final String COLOR_RED = "RED";
    public static final String COLOR_BLUE = "BLUE";
    public static final String COLOR_GREEN = "GREEN";
    

    但是這樣同樣存在一個問題,有些初級程序員會不按套路出牌,他們可能會直接使用字符串的值進行比較,而不是直接使用枚舉的字段,實現示例代碼如下:

    public class EnumTest {
        public static final String COLOR_RED = "RED";
        public static final String COLOR_BLUE = "BLUE";
        public static final String COLOR_GREEN = "GREEN";
        public static void main(String[] args) {
            String color = "BLUE";
            if ("BLUE".equals(color)) {
                System.out.println("藍色");
            }
        }
    }
    

    這樣當我們修改了枚舉中的值,那程序就涼涼了。

    枚舉使用場景

    枚舉的常見使用場景是單例,它的完整實現代碼如下:

    public class Singleton {
        // 枚舉類型是線程安全的,並且只會裝載一次
        private enum SingletonEnum {
            INSTANCE;
            // 聲明單例對象
            private final Singleton instance;
            // 實例化
            SingletonEnum() {
                instance = new Singleton();
            }
            private Singleton getInstance() {
                return instance;
            }
        }
        // 獲取實例(單例對象)
        public static Singleton getInstance() {
            return SingletonEnum.INSTANCE.getInstance();
        }
        private Singleton() {
        }
        // 類方法
        public void sayHi() {
            System.out.println("Hi,Java.");
        }
    }
    class SingletonTest {
        public static void main(String[] args) {
            Singleton singleton = Singleton.getInstance();
            singleton.sayHi();
        }
    }
    

    因為枚舉只會在類加載時裝載一次,所以它是線程安全的,這也是《Effective Java》作者極力推薦使用枚舉來實現單例的主要原因。

    知識擴展

    枚舉為什麼是線程安全的?

    這一點要從枚舉最終生成的字節碼說起,首先我們先來定義一個簡單的枚舉類:

    public enum ColorEnumTest {
        RED, GREEN, BLANK, YELLOW;
    }
    

    然後我們再將上面的那段代碼編譯為字節碼,具體內容如下:

    public final class ColorEnumTest extends java.lang.Enum<ColorEnumTest> {
      public static final ColorEnumTest RED;
      public static final ColorEnumTest GREEN;
      public static final ColorEnumTest BLANK;
      public static final ColorEnumTest YELLOW;
      public static ColorEnumTest[] values();
      public static ColorEnumTest valueOf(java.lang.String);
      static {};
    }
    

    從上述結果可以看出枚舉類最終會被編譯為被 final 修飾的普通類,它的所有屬性也都會被 staticfinal 關鍵字修飾,所以枚舉類在項目啟動時就會被 JVM 加載並初始化,而這個執行過程是線程安全的,所以枚舉類也是線程安全的類。

    小貼士:代碼反編譯的過程是先用 javac 命令將 java 代碼編譯字節碼(.class),再使用 javap 命令查看編譯的字節碼。

    枚舉比較小技巧

    我們在枚舉比較時使用 == 就夠了,因為枚舉類是在程序加載時就創建了(它並不是 new 出來的),並且枚舉類不允許在外部直接使用 new 關鍵字來創建枚舉實例,所以我們在使用枚舉類時本質上只有一個對象,因此在枚舉比較時使用 == 就夠了。

    並且我們在查看枚舉的 equlas() 源碼會發現,它的內部其實還是直接調用了 == 方法,源碼如下:

    public final boolean equals(Object other) {
        return this==other;
    }
    

    總結

    本文我們介紹了枚舉類的 7 種使用方法:常量、switch、枚舉中添加方法、覆蓋枚舉方法、實現接口、在接口中組織枚舉類和使用枚舉集合等,然後講了如果不使用枚舉類使用 int 類型和 String 類型存在的一些弊端:語義不夠清晰、容易被修改、存在被誤用的風險,所以我們在適合的環境下應該盡量使用枚舉類。並且我們還講了枚舉類的使用場景——單例,以及枚舉類為什麼是安全的,最後我們講了枚舉比較的小技巧,希望本文對你有幫助。

    查看 & 鳴謝

    https://www.iteye.com/blog/softbeta-1185573

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

    【其他文章推薦】

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

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

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

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

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

  • 特斯拉將公布儲能電池計畫 F-貿聯可望受惠

    特斯拉傳將於 4 月 30 日公布儲能電池計畫,市場推測,該計畫將包括住宅與公共規模市場,業界傳出,本來就是特斯拉重要夥伴的 F-貿聯,有機會獲新電力線訂單。而今年在特斯拉需求持續強勁,與 Type C 等外接式擴充介面的需求爆發下,法人預期,該公司今年營運將逐季走高,全年營收看增 15%。   F-貿聯首季營收 19.04 億元,年增 10%,創下歷年同期新高。而特斯拉傳出將於下周公布最新的儲能電池計畫,據市場傳言表示,貿聯本來就是特斯拉在電動車電池模組用線與超級充電樁的線束的主力供應商,未來不排除大型公共規模用儲能電池,將與現有充電站結合。而貿聯有機會順勢切入取得新產品,並以公用市場應用的電力線為主,近日有望已小量出貨。   法人預估,F-貿聯今年除來自特斯拉的需求成長外,另在其他車用佈局方面,包括全地形車大客戶全車線束、擴大歐美客戶於倒車雷達等新產品的開發;另外針對美國官方對公營單位節能環保的需求,也配合客戶開發出得以改裝現有車為電動車的配備裝置,隨未來該商業模式成型,貢獻將逐步擴大。  

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

    【其他文章推薦】

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

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

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

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

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

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

  • 最新的一波Vue實戰技巧,不用則已,一用驚人

    Vue中,不同的選項有不同的合併策略,比如 data,props,methods是同名屬性覆蓋合併,其他直接合併,而生命周期鈎子函數則是將同名的函數放到一個數組中,在調用的時候依次調用

    Vue中,提供了一個api, Vue.config.optionMergeStrategies,可以通過這個api去自定義選項的合併策略。

    在代碼中打印

    console.log(Vue.config.optionMergeStrategies)
    

      

     通過合併策略自定義生命周期函數

    背景

    最近客戶給領導反饋,我們的系統用一段時間,瀏覽器就變得有點卡,不知道為什麼。問題出來了,本來想甩鍋到後端,但是瀏覽器問題,沒法甩鍋啊,那就排查吧。

    後來發現頁面有許多定時器,ajax輪詢還有動畫,打開一個瀏覽器頁簽沒法問題,打開多了,瀏覽器就變得卡了,這時候我就想如果能在用戶切換頁簽時候將這些都停掉,不久解決了。百度裏面上下檢索,找到了一個事件visibilitychange,可以用來判斷瀏覽器頁簽是否显示。

    有方法了,就寫唄

    export default {
      created() {
        window.addEventListener('visibilitychange', this.$_hanldeVisiblityChange)
        // 此處用了hookEvent,可以參考小編前一篇文章
        this.$on('hook:beforeDestroy', () => {
          window.removeEventListener(
            'visibilitychange',
            this.$_hanldeVisiblityChange
          )
        })
      },
      methods: {
        $_hanldeVisiblityChange() {
          if (document.visibilityState === 'hidden') {
            // 停掉那一堆東西
          }
          if (document.visibilityState === 'visible') {
            // 開啟那一堆東西
          }
        }
      }
    }
    

    通過上面的代碼,可以看到在每一個需要監聽處理的文件都要寫一堆事件監聽,判斷頁面是否显示的代碼,一處兩處還可以,文件多了就頭疼了,這時候小編突發奇想,定義一個頁面显示隱藏的生命周期鈎子,把這些判斷都封裝起來

    自定義生命周期鈎子函數

    定義生命周期函數 pageHidden 與 pageVisible

    import Vue from 'vue'
    
    // 通知所有組件頁面狀態發生了變化
    const notifyVisibilityChange = (lifeCycleName, vm) => {
      // 生命周期函數會存在$options中,通過$options[lifeCycleName]獲取生命周期
      const lifeCycles = vm.$options[lifeCycleName]
      // 因為使用了created的合併策略,所以是一個數組
      if (lifeCycles && lifeCycles.length) {
        // 遍歷 lifeCycleName對應的生命周期函數列表,依次執行
        lifeCycles.forEach(lifecycle => {
          lifecycle.call(vm)
        })
      }
      // 遍歷所有的子組件,然後依次遞歸執行
      if (vm.$children && vm.$children.length) {
        vm.$children.forEach(child => {
          notifyVisibilityChange(lifeCycleName, child)
        })
      }
    }
    
    // 添加生命周期函數
    export function init() {
      const optionMergeStrategies = Vue.config.optionMergeStrategies
      // 定義了兩個生命周期函數 pageVisible, pageHidden
      // 為什麼要賦值為 optionMergeStrategies.created呢
      // 這個相當於指定 pageVisible, pageHidden 的合併策略與 created的相同(其他生命周期函數都一樣)
      optionMergeStrategies.pageVisible = optionMergeStrategies.beforeCreate
      optionMergeStrategies.pageHidden = optionMergeStrategies.created
    }
    
    
    // 將事件變化綁定到根節點上面
    // rootVm vue根節點實例
    export function bind(rootVm) {
      window.addEventListener('visibilitychange', () => {
        // 判斷調用哪個生命周期函數
        let lifeCycleName = undefined
        if (document.visibilityState === 'hidden') {
          lifeCycleName = 'pageHidden'
        } else if (document.visibilityState === 'visible') {
          lifeCycleName = 'pageVisible'
        }
        if (lifeCycleName) {
          // 通過所有組件生命周期發生變化了
          notifyVisibilityChange(lifeCycleName, rootVm)
        }
      })
    }
    

    應用

    1. main.js主入口文件引入
    import { init, bind } from './utils/custom-life-cycle'
    
    // 初始化生命周期函數, 必須在Vue實例化之前確定合併策略
    init()
    
    const vm = new Vue({
      router,
      render: h => h(App)
    }).$mount('#app')
    
    // 將rootVm 綁定到生命周期函數監聽裏面
    bind(vm)
    

      2. 在需要的地方監聽生命周期函數

    export default {
      pageVisible() {
        console.log('頁面显示出來了')
      },
      pageHidden() {
        console.log('頁面隱藏了')
      }
    }
    

      

    provideinject,不止父子傳值,祖宗傳值也可以

    Vue相關的面試經常會被面試官問道,Vue父子之間傳值的方式有哪些,通常我們會回答,props傳值,$emit事件傳值,vuex傳值,還有eventbus傳值等等,今天再加一種provideinject傳值,離offer又近了一步。(對了,下一節還有一種)

    使用過React的同學都知道,在React中有一個上下文Context,組件可以通過Context向任意後代傳值,而Vueprovideinject的作用於Context的作用基本一樣

    先舉一個例子

    使用過elemment-ui的同學一定對下面的代碼感到熟悉

    <template>
      <el-form :model="formData" size="small">
        <el-form-item label="姓名" prop="name">
          <el-input v-model="formData.name" />
        </el-form-item>
        <el-form-item label="年齡" prop="age">
          <el-input-number v-model="formData.age" />
        </el-form-item>
        <el-button>提交</el-button>
      </el-form>
    </template>
    <script>
    export default {
      data() {
        return {
          formData: {
            name: '',
            age: 0
          }
        }
      }
    }
    </script>
    

      

    看了上面的代碼,貌似沒啥特殊的,天天寫啊。在el-form上面我們指定了一個屬性size="small",然後有沒有發現表單裏面的所有表單元素以及按鈕的 size都變成了small,這個是怎麼做到的?接下來我們自己手寫一個表單模擬一下

    自己手寫一個表單

    自定義表單custom-form.vue

    <template>
      <form class="custom-form">
        <slot></slot>
      </form>
    </template>
    <script>
    export default {
      props: {
        // 控製表單元素的大小
        size: {
          type: String,
          default: 'default',
          // size 只能是下面的四個值
          validator(value) {
            return ['default', 'large', 'small', 'mini'].includes(value)
          }
        },
        // 控製表單元素的禁用狀態
        disabled: {
          type: Boolean,
          default: false
        }
      },
      // 通過provide將當前表單實例傳遞到所有後代組件中
      provide() {
        return {
          customForm: this
        }
      }
    }
    </script>
    

      

    在上面代碼中,我們通過provide將當前組件的實例傳遞到後代組件中,provide是一個函數,函數返回的是一個對象

    自定義表單項custom-form-item.vue

    沒有什麼特殊的,只是加了一個label,element-ui更複雜一些

    <template>
      <div class="custom-form-item">
        <label class="custom-form-item__label">{{ label }}</label>
        <div class="custom-form-item__content">
          <slot></slot>
        </div>
      </div>
    </template>
    <script>
    export default {
      props: {
        label: {
          type: String,
          default: ''
        }
      }
    }
    </script>

    自定義輸入框 custom-input.vue

    <template>
      <div
        class="custom-input"
        :class="[
          `custom-input--${getSize}`,
          getDisabled && `custom-input--disabled`
        ]"
      >
        <input class="custom-input__input" :value="value" @input="$_handleChange" />
      </div>
    </template>
    <script>
    /* eslint-disable vue/require-default-prop */
    export default {
      props: {
        // 這裏用了自定義v-model
        value: {
          type: String,
          default: ''
        },
        size: {
          type: String
        },
        disabled: {
          type: Boolean
        }
      },
      // 通過inject 將form組件注入的實例添加進來
      inject: ['customForm'],
      computed: {
        // 通過計算組件獲取組件的size, 如果當前組件傳入,則使用當前組件的,否則是否form組件的
        getSize() {
          return this.size || this.customForm.size
        },
        // 組件是否禁用
        getDisabled() {
          const { disabled } = this
          if (disabled !== undefined) {
            return disabled
          }
          return this.customForm.disabled
        }
      },
      methods: {
        // 自定義v-model
        $_handleChange(e) {
          this.$emit('input', e.target.value)
        }
      }
    }
    </script>
    

      


    form中,我們通過
    provide返回了一個對象,在
    input中,我們可以通過
    inject獲取
    form中返回對象中的項,如上代碼
    inject:['customForm']所示,然後就可以在組件內通過
    this.customForm調用
    form實例上面的屬性與方法了

    在項目中使用

    <template>
      <custom-form size="small">
        <custom-form-item label="姓名">
          <custom-input v-model="formData.name" />
        </custom-form-item>
      </custom-form>
    </template>
    <script>
    import CustomForm from '../components/custom-form'
    import CustomFormItem from '../components/custom-form-item'
    import CustomInput from '../components/custom-input'
    export default {
      components: {
        CustomForm,
        CustomFormItem,
        CustomInput
      },
      data() {
        return {
          formData: {
            name: '',
            age: 0
          }
        }
      }
    }
    </script>
    

      執行上面代碼,運行結果為:

    <form class="custom-form">
      <div class="custom-form-item">
        <label class="custom-form-item__label">姓名</label>
        <div class="custom-form-item__content">
          <!--size=small已經添加到指定的位置了-->
          <div class="custom-input custom-input--small">
            <input class="custom-input__input">
          </div>
        </div>
      </div>
    </form>
    

      

    通過上面的代碼可以看到,input組件已經設置組件樣式為custom-input--small

    inject格式說明

    除了上面代碼中所使用的inject:['customForm']寫法之外,inject還可以是一個對象。且可以指定默認值

    修改上例,如果custom-input外部沒有custom-form,則不會注入customForm,此時為customForm指定默認值

    {
      inject: {
        customForm: {
          // 對於非原始值,和props一樣,需要提供一個工廠方法
          default: () => ({
            size: 'default'
          })
        }
      }
    }
    

      

    使用限制

    1.provideinject的綁定不是可響應式的。但是,如果你傳入的是一個可監聽的對象,如上面的customForm: this,那麼其對象的屬性還是可響應的。

    2.Vue官網建議provideinject 主要在開發高階插件/組件庫時使用。不推薦用於普通應用程序代碼中。因為provideinject在代碼中是不可追溯的(ctrl + f可以搜),建議可以使用Vuex代替。 但是,也不是說不能用,在局部功能有時候用了作用還是比較大的。

     

    插槽,我要鑽到你的懷裡

    插槽,相信每一位Vue都有使用過,但是如何更好的去理解插槽,如何去自定義插槽,今天小編為你帶來更形象的說明。

    默認插槽

    <template>
      <!--這是一個一居室-->
      <div class="one-bedroom">
        <!--添加一個默認插槽,用戶可以在外部隨意定義這個一居室的內容-->
        <slot></slot>
      </div>
    </template>
    

      

    <template>
      <!--這裏一居室-->
      <one-bedroom>
        <!--將傢具放到房間裏面,組件內部就是上面提供的默認插槽的空間-->
        <span>先放一個小床,反正沒有女朋友</span>
        <span>再放一個電腦桌,在家還要加班寫bug</span>
      </one-bedroom>
    </template>
    <script>
    import OneBedroom from '../components/one-bedroom'
    export default {
      components: {
        OneBedroom
      }
    }
    </script>

    具名插槽

    <template>
      <div class="two-bedroom">
        <!--這是主卧-->
        <div class="master-bedroom">
          <!---主卧使用默認插槽-->
          <slot></slot>
        </div>
        <!--這是次卧-->
        <div class="secondary-bedroom">
          <!--次卧使用具名插槽-->
          <slot name="secondard"></slot>
        </div>
      </div>
    </template>
    

      

    <template>
      <two-bedroom>
        <!--主卧使用默認插槽-->
        <div>
          <span>放一個大床,要結婚了,嘿嘿嘿</span>
          <span>放一個衣櫃,老婆的衣服太多了</span>
          <span>算了,還是放一個電腦桌吧,還要寫bug</span>
        </div>
        <!--次卧,通過v-slot:secondard 可以指定使用哪一個具名插槽, v-slot:secondard 也可以簡寫為 #secondard-->
        <template v-slot:secondard>
          <div>
            <span>父母要住,放一個硬一點的床,軟床對腰不好</span>
            <span>放一個衣櫃</span>
          </div>
        </template>
      </two-bedroom>
    </template>
    <script>
    import TwoBedroom from '../components/slot/two-bedroom'
    export default {
      components: {
        TwoBedroom
      }
    }
    </script>

    作用域插槽

    <template>
      <div class="two-bedroom">
        <!--其他內容省略-->
        <div class="toilet">
          <!--通過v-bind 可以向外傳遞參數, 告訴外面衛生間可以放洗衣機-->
          <slot name="toilet" v-bind="{ washer: true }"></slot>
        </div>
      </div>
    </template>
    

      

    <template>
      <two-bedroom>
        <!--其他省略-->
        <!--衛生間插槽,通過v-slot="scope"可以獲取組件內部通過v-bind傳的值-->
        <template v-slot:toilet="scope">
          <!--判斷是否可以放洗衣機-->
          <span v-if="scope.washer">這裏放洗衣機</span>
        </template>
      </two-bedroom>
    </template>  

    插槽默認值

    <template>
      <div class="second-hand-house">
        <div class="master-bedroom">
          <!--插槽可以指定默認值,如果外部調用組件時沒有修改插槽內容,則使用默認插槽-->
          <slot>
            <span>這裡有一張水床,玩的夠嗨</span>
            <span>還有一個衣櫃,有點舊了</span>
          </slot>
        </div>
        <!--這是次卧-->
        <div class="secondary-bedroom">
          <!--次卧使用具名插槽-->
          <slot name="secondard">
            <span>這裡有一張嬰兒床</span>
          </slot>
        </div>
      </div>
    </template>
    

      

    <second-hand-house>
        <!--主卧使用默認插槽,只裝修主卧-->
        <div>
          <span>放一個大床,要結婚了,嘿嘿嘿</span>
          <span>放一個衣櫃,老婆的衣服太多了</span>
          <span>算了,還是放一個電腦桌吧,還要寫bug</span>
        </div>
      </second-hand-house>
    

    dispatchbroadcast,這是一種有歷史的組件通信方式

    dispatch
    broadcast是一種有歷史的組件通信方式,為什麼是有歷史的,因為他們是
    Vue1.0提供的一種方式,在
    Vue2.0中廢棄了。但是廢棄了不代表我們不能自己手動實現,像許多UI庫內部都有實現。本文以
    element-ui實現為基礎進行介紹。同時看完本節,你會對組件的
    $parent,
    $children,
    $options有所了解。

    方法介紹

    $dispatch: $dispatch會向上觸發一個事件,同時傳遞要觸發的祖先組件的名稱與參數,當事件向上傳遞到對應的組件上時會觸發組件上的事件偵聽器,同時傳播會停止。

    $broadcast: $broadcast會向所有的後代組件傳播一個事件,同時傳遞要觸發的後代組件的名稱與參數,當事件傳遞到對應的後代組件時,會觸發組件上的事件偵聽器,同時傳播會停止(因為向下傳遞是樹形的,所以只會停止其中一個恭弘=叶 恭弘子分支的傳遞)。

    $dispatch實現與應用

    1. 代碼實現

     // 向上傳播事件
     // @param {*} eventName 事件名稱
     // @param {*} componentName 接收事件的組件名稱
     // @param {...any} params 傳遞的參數,可以有多個
     
    function dispatch(eventName, componentName, ...params) {
      // 如果沒有$parent, 則取$root
      let parent = this.$parent || this.$root
      while (parent) {
        // 組件的name存儲在組件的$options.componentName 上面
        const name = parent.$options.name
        // 如果接收事件的組件是當前組件
        if (name === componentName) {
          // 通過當前組件上面的$emit觸發事件,同事傳遞事件名稱與參數
          parent.$emit.apply(parent, [eventName, ...params])
          break
        } else {
          // 否則繼續向上判斷
          parent = parent.$parent
        }
      }
    }
    
    // 導出一個對象,然後在需要用到的地方通過混入添加
    export default {
      methods: {
        $dispatch: dispatch
      }
    }  

    2. 代碼應用

    • 在子組件中通過$dispatch向上觸發事件

      import emitter from '../mixins/emitter'
      export default {
        name: 'Chart',
        // 通過混入將$dispatch加入進來
        mixins: [emitter],
         mounted() {
           // 在組件渲染完之後,將組件通過$dispatch將自己註冊到Board組件上
          this.$dispatch('register', 'Board', this)
        }
      }
    • Board組件上通過$on監聽要註冊的事件

    $broadcast實現與應用

    1. 代碼實現

      //向下傳播事件
      // @param {*} eventName 事件名稱
      // @param {*} componentName 要觸發組件的名稱
      // @param  {...any} params 傳遞的參數
     
    function broadcast(eventName, componentName, ...params) {
      this.$children.forEach(child => {
        const name = child.$options.name
        if (name === componentName) {
          child.$emit.apply(child, [eventName, ...params])
        } else {
          broadcast.apply(child, [eventName, componentName, ...params])
        }
      })
    }
    
    // 導出一個對象,然後在需要用到的地方通過混入添加
    export default {
      methods: {
        $broadcast: broadcast
      }
    }  

    2. 代碼應用

    在父組件中通過$broadcast向下觸發事件

    import emitter from '../mixins/emitter'
    export default {
      name: 'Board',
      // 通過混入將$dispatch加入進來
      mixins: [emitter],
      methods:{
      	//在需要的時候,刷新組件
      	$_refreshChildren(params) {
      		this.$broadcast('refresh', 'Chart', params)
      	}
      }
    }
    

    在後代組件中通過$on監聽刷新事件

    export default {
      name: 'Chart',
      created() {
        this.$on('refresh',(params) => {
          // 刷新事件
        })
      }
    }
    

    總結

    通過上面的例子,同學們應該都能對$dispatch$broadcast有所了解,但是為什麼Vue2.0要放棄這兩個方法呢?官方給出的解釋是:”因為基於組件樹結構的事件流方式實在是讓人難以理解,並且在組件結構擴展的過程中會變得越來越脆弱。這種事件方式確實不太好,我們也不希望在以後讓開發者們太痛苦。並且 $dispatch$broadcast 也沒有解決兄弟組件間的通信問題。“

    確實如官網所說,這種事件流的方式確實不容易讓人理解,而且後期維護成本比較高。但是在小編看來,不管黑貓白貓,能抓老鼠的都是好貓,在許多特定的業務場景中,因為業務的複雜性,很有可能使用到這樣的通信方式。但是使用歸使用,但是不能濫用,小編一直就在項目中有使用。

     

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

    【其他文章推薦】

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

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

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

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

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

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

  • 深入解讀Dictionary

    深入解讀Dictionary

    Dictionary<TKey,TValue>是日常.net開發中最常用的數據類型之一,基本上遇到鍵值對類型的數據時第一反應就是使用這種散列表。散列表特別適合快速查找操作,查找的效率是常數階O(1)。那麼為什麼這種數據類型的查找效率能夠這麼高效?它背後的數據類型是如何支撐這種查找效率的?它在使用過程中有沒有什麼局限性?一起來探究下這個數據類型的奧秘吧。

    本文內容針對的是.Net Framework 4.5.1的代碼實現,在其他.Net版本中或多或少都會有些差異,但是基本的原理還是相同的。

    本文的內容主要分為三個部分,第一部分是從代碼的角度來分析並以圖文並茂的方式通俗的解釋Dictionary如何解決的散列衝突並實現高效的數據插入和查找。第二部分名為“眼見為實”,由於第一部分是從代碼層面分析Dictionary的實現,側重於理論分析,因此第二部分使用windbg直接分析內存結構,跟第一部分的理論分析相互印證,加深對於這種數據類型的深入理解。最後是從數據結構的時間複雜度的角度進行分析並提出了幾條實踐建議。

    本文內容:

    • 第一部分 代碼分析
      • 散列衝突
      • Dictionary圖文解析
      • Dictionary的初始化
      • 添加第四個元素
    • 第二部分 眼見為實
      • 添加第一個元素后的內存結構
      • 添加第四個元素后的內存結構
    • 第三部分
      • 時間複雜度分析
      • 實踐建議

    散列衝突

    提到散列表,就不能不提散列衝突。由於哈希算法被計算的數據是無限的,而計算后的結果範圍有限,因此總會存在不同的數據經過計算后得到的值相同,這就是哈希衝突。(兩個不同的數據計算后的結果一樣)。散列衝突的解決方案有好幾個,比如開放尋址法、鏈式尋址法。

    Dictionary使用的是鏈式尋址法,也叫做拉鏈法。拉鏈法的基本思想是將散列值相同的數據存在同一個鏈表中,如果有散列值相同的元素,則加到鏈表的頭部。同樣道理,在查找元素的時候,先計算散列值,然後在對應散列值的鏈表中查找目標元素。

    用圖來表達鏈式尋址法的思想:

    Dictionary<TKey,TValue>的內部數據結構

    Dictionary的內部存儲數據主要是依賴了兩個數組,分別是int[] bucketsEntry[] entries。其中buckets是Dictionary所有操作的入口,類似於上文中解說拉鏈法所用的圖中的那個豎著的數據結構。Entry是一個結構體,用於封裝所有的元素並且增加了next字段用於構建鏈表數據結構。為了便於理解,下文中截取了一段相關的源代碼並增加了註釋。

    //數據在Dictionary<TKey,TValue>的存儲形式,所有的鍵值對在Dictionary<TKey,TValue>都是被包裝成一個個的Entry保存在內存中
    private struct Entry {
        public int hashCode;    // 散列計算結果的低31位數,默認值為-1
        public int next;        // 下一個Entry的索引值,鏈表的最後一個Entry的next為-1
        public TKey key;        // Entry對象的Key對應於傳入的TKey
        public TValue value;    // Entry對象的Value對應與傳入的TValue
    }
    
    private int[] buckets;      //hashCode的桶,是查找所有Entry的第一級數據結構
    private Entry[] entries;    //保存真正的數據
    

    下文中以Dictionary<int,string>為例,分析Dictionary在使用過程中內部數據的組織方式。

    Dictionary初始化

    初始化代碼:

    Dictionary<int, string> dictionary = new Dictionary<int, string>();
    

    Dictionary的初始化時,bucketsentries的長度都是0。

    添加一個元素

    dictionary.Add(1, "xyxy");
    

    向Dictionary中添加這個新元素大致經過了7個步驟:

    1. 首先判斷數組長度是否需要擴容(原長度為0,需要擴容);
    2. 對於數組進行擴容,分別創建長度為3的bucket數組和entries數組(使用大於原數組長度2倍的第一個素數作為新的數組長度);
    3. 整數1的hashcode為1;
    4. 取低31位數值為1(計算公式:hashcode & 0x7FFFFFFF=1);
    5. 該key的hashcode落到bucket下標為1的位置(計算公式:hashCode % buckets.Length=1);
    6. 將hashcode、key、value包裝起來(封裝到entries數組下標為0的結構體中);
    7. 設置bucket[1]的值為0(因為新元素被封裝到了entries數組下標為0的位置);

    當向Dictionary中添加一個元素后,內部數據結構如下圖(為了便於理解,圖上將bucket和entries中各個鏈表頭結點用線標出了關聯關係):

    添加第二個元素

    dictionary.Add(7, "xyxy");
    

    向Dictionary中添加這個元素大致經過了6個步驟:

    1. 計算7的hashcode是7;
    2. 取低31位數值為7(計算公式:hashcode & 0x7FFFFFFF=1);
    3. 該key的hashcode落到bucket下標為1的位置(計算公式:hashCode % buckets.Length=1);
    4. 將hashcode、key、value包裝起來(封裝到entries數組下標為1的結構體中,跟步驟3計算得到的1沒有關係,只是因為entries數組下標為1的元素是空着的所以放在這裏);
    5. 原bucket[1]為0,所以設置當前結構體的Entry.next為0;
    6. 設置bucket[1]為1(因為鏈表的頭部節點位於entries數組下標為1的位置)

    當向Dictionary中添加第二個元素后,內部數據結構是這樣的:

    添加第三個元素

    dictionary.Add(2, "xyxy");
    

    向Dictionary添加這個元素經過了如下5個步驟:

    1. 整數2計算的hashcode是2;
    2. hashcode取低31位數值為2(計算公式:hashcode & 0x7FFFFFFF=2);
    3. 該key的hashcode落到bucket下標為2的位置(計算公式:hashCode % buckets.Length=2);
    4. 將hashcode、key、value包裝起來(封裝到entries數組下標為2的結構體中,到此entries的數組就存滿了);
    5. 原bucket[2]上為-1,所以bucket[2]節點下並沒有對應鏈表,設置當前結構體的Entry.next為-1;
    6. 設置bucket[2]為2(因為鏈表的頭部節點位於entries數組下標為2的位置)

    當向Dictionary中添加第三個元素后,內部數據結構:

    添加第四個元素

    dictionary.Add(4, "xyxy");
    

    通過前面幾個操作可以看出,當前數據結構中entries數組中的元素已滿,如果再添加元素的話,會發生怎樣的變化呢?

    假如再對於dictionary添加一個元素,原來申請的內存空間肯定是不夠用的,必須對於當前數據結構進行擴容,然後在擴容的基礎上再執行添加元素的操作。那麼在解釋這個Add方法原理的時候,分為兩個場景分別進行:數組擴容和元素添加。

    數組擴容

    在發現數組容量不夠的時候,Dictionary首先執行擴容操作,擴容的規則與該數據類型首次初始化的規則相同,即使用大於原數組長度2倍的第一個素數7作為新數組的長度(3*2=6,大於6的第一個素數是7)。

    擴容步驟:

    1. 新申請一個容量為7的數組,並將原數組的元素拷貝至新數組(代碼:Array.Copy(entries, 0, newEntries, 0, count);
    2. 重新計算原Dictionary中的元素的hashCode在bucket中的位置(注意新的bucket數組中數值的變化);
    3. 重新計算鏈表(注意entries數組中結構體的next值的變化);

    擴容完成后Dictionary的內容數據結構:

    添加元素

    當前已經完成了entriesbucket數組的擴容,有了充裕的空間來存儲新的元素,所以可以在新的數據結構的基礎上繼續添加元素。

    當向Dictionary中添加第四個元素后,內部數據結構是這樣的:

    添加這個新的元素的步驟:

    1. 整數4計算的hashcode是4;
    2. hashcode取低31位數值為4(計算公式:hashcode & 0x7FFFFFFF=4);
    3. 該key的hashcode落到bucket下標為4的位置(計算公式:hashCode % buckets.Length=4);
    4. 將hashcode、key、value包裝起來;(封裝到entries數組下標為3的結構體中);
    5. 原bucket[4]上為-1,所以當前節點下並沒有鏈表,設置當前結構體的Entry.next為-1;
    6. 設置bucket[4]為3(因為鏈表的頭部節點位於entries數組下標為3的位置)

    眼見為實

    畢竟本文的主題是圖文並茂分析Dictionary<Tkey,Tvalue>的原理,雖然已經從代碼層面和理論層面分析了Dictionary<Tkey,Tvalue>的實現,但是如果能夠分析這個數據類型的實際內存數據結果,可以獲得更直觀的感受並且對於這個數據類型能夠有更加深入的認識。由於篇幅的限制,無法將Dictionary<Tkey,Tvalue>的所有操作場景結果都進行內存分析,那麼本文中精選有代表性的兩個場景進行分析:一是該數據類型初始化后添加第一個元素的內存結構,二是該數據類型進行第一次擴容后的數據結構。

    Dictionary添加第一個元素后的內存結構

    執行代碼:

    Dictionary<int, string> dic = new Dictionary<int, string>();
    dic.Add(1, "xyxy");
    Console.Read();
    

    打開windbg附加到該進程(由於使用的是控制台應用程序,當前線程是0號線程,因此如果附加進程后默認的不是0號線程時執行~0s切換到0號線程),執行!clrstack -l查看當前線程及線程上使用的所有變量:

    0:000> !clrstack -l
    OS Thread Id: 0x48b8 (0)
            Child SP               IP Call Site
    0000006de697e998 00007ffab577c134 [InlinedCallFrame: 0000006de697e998] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
    0000006de697e998 00007ffa96abc9c8 [InlinedCallFrame: 0000006de697e998] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
    0000006de697e960 00007ffa96abc9c8 *** ERROR: Module load completed but symbols could not be loaded for C:\WINDOWS\assembly\NativeImages_v4.0.30319_64\mscorlib\5c1b7b73113a6f079ae59ad2eb210951\mscorlib.ni.dll
    DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
    
    0000006de697ea40 00007ffa972d39ec System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean, Boolean, Int32 ByRef)
        LOCALS:
            <no data>
            <no data>
            <no data>
            <no data>
            <no data>
            <no data>
    
    0000006de697ead0 00007ffa972d38f5 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
        LOCALS:
            <no data>
            <no data>
    
    0000006de697eb30 00007ffa96a882d4 System.IO.StreamReader.ReadBuffer()
        LOCALS:
            <no data>
            <no data>
    
    0000006de697eb80 00007ffa97275f23 System.IO.StreamReader.Read()
        LOCALS:
            <no data>
    
    0000006de697ebb0 00007ffa9747a2fd System.IO.TextReader+SyncTextReader.Read()
    
    0000006de697ec10 00007ffa97272698 System.Console.Read()
    
    0000006de697ec40 00007ffa38670909 ConsoleTest.DictionaryDebug.Main(System.String[])
        LOCALS:
            0x0000006de697ec70 = 0x00000215680d2dd8
    
    0000006de697ee88 00007ffa97ba6913 [GCFrame: 0000006de697ee88] 
    
    

    通過對於線程堆棧的分析很容易看出當前線程上使用了一個局部變量,地址為:0x000001d86c972dd8,使用!do命令查看該變量的內容:

    0:000> !do 0x00000215680d2dd8
    Name:        System.Collections.Generic.Dictionary`2[[System.Int32, mscorlib],[System.String, mscorlib]]
    MethodTable: 00007ffa96513328
    EEClass:     00007ffa9662f610
    Size:        80(0x50) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                  MT    Field   Offset                 Type VT     Attr            Value Name
    00007ffa964a8538  4001887        8       System.Int32[]  0 instance 00000215680d2ee8 buckets
    00007ffa976c4dc0  4001888       10 ...non, mscorlib]][]  0 instance 00000215680d2f10 entries
    00007ffa964a85a0  4001889       38         System.Int32  1 instance                1 count
    00007ffa964a85a0  400188a       3c         System.Int32  1 instance                1 version
    00007ffa964a85a0  400188b       40         System.Int32  1 instance               -1 freeList
    00007ffa964a85a0  400188c       44         System.Int32  1 instance                0 freeCount
    00007ffa96519630  400188d       18 ...Int32, mscorlib]]  0 instance 00000215680d2ed0 comparer
    00007ffa964c6ad0  400188e       20 ...Canon, mscorlib]]  0 instance 0000000000000000 keys
    00007ffa977214e0  400188f       28 ...Canon, mscorlib]]  0 instance 0000000000000000 values
    00007ffa964a5dd8  4001890       30        System.Object  0 instance 0000000000000000 _syncRoot
    
    

    從內存結構來看,該變量中就是我們查找的Dic存在buckets、entries、count、version等字段,其中buckets和entries在上文中已經有多次提及,也是本文的分析重點。既然要眼見為實,那麼buckets和entries這兩個數組的內容到底是什麼樣的呢?這兩個都是數組,一個是int數組,另一個是結構體數組,對於這兩個內容分別使用!da命令查看其內容:

    首先是buckets的內容:

    0:000> !da -start 0 -details 00000215680d2ee8 
    Name:        System.Int32[]
    MethodTable: 00007ffa964a8538
    EEClass:     00007ffa966160e8
    Size:        36(0x24) bytes
    Array:       Rank 1, Number of elements 3, Type Int32
    Element Methodtable: 00007ffa964a85a0
    [0] 00000215680d2ef8
        Name:        System.Int32
        MethodTable: 00007ffa964a85a0
        EEClass:     00007ffa96616078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  40005a2        0             System.Int32      1     instance                   -1     m_value
    [1] 00000215680d2efc
        Name:        System.Int32
        MethodTable: 00007ffa964a85a0
        EEClass:     00007ffa96616078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  40005a2        0             System.Int32      1     instance                    0     m_value
    [2] 00000215680d2f00
        Name:        System.Int32
        MethodTable: 00007ffa964a85a0
        EEClass:     00007ffa96616078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  40005a2        0             System.Int32      1     instance                   -1     m_value
    
    
    

    當前buckets中有三個值,分別是:-1、0和-1,其中-1是數組初始化后的默認值,而下錶為1的位置的值0則是上文中添加dic.Add(1, "xyxy");這個指令的結果,代表其對應的鏈表首節點在entries數組中下標為0的位置,那麼entries數組中的數值是什麼樣子的呢?

    0:000> !da -start 0 -details 00000215680d2f10 
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]][]
    MethodTable: 00007ffa965135b8
    EEClass:     00007ffa9662d1f0
    Size:        96(0x60) bytes
    Array:       Rank 1, Number of elements 3, Type VALUETYPE
    Element Methodtable: 00007ffa96513558
    [0] 00000215680d2f20
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    1     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    1     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000215680d2db0     value
    [1] 00000215680d2f38
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value
    [2] 00000215680d2f50
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value
    
    

    通過對於entries數組的分析可以看出,這個數組也有三個值,其中下標為0的位置已經填入相關內容,比如hashCode為1,key為1,其中value的內容是一個內存地址:000001d86c972db0,這個地址指向的就是字符串對象,它的內容是xyxy,使用!do指令來看下具體內容:

    0:000> !do  00000215680d2db0
    Name:        System.String
    MethodTable: 00007ffcc6b359c0
    EEClass:     00007ffcc6b12ec0
    Size:        34(0x22) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    String:      xyxy
    Fields:
                  MT    Field   Offset                 Type VT     Attr            Value Name
    00007ffcc6b385a0  4000283        8         System.Int32  1 instance                4 m_stringLength
    00007ffcc6b36838  4000284        c          System.Char  1 instance               78 m_firstChar
    00007ffcc6b359c0  4000288       e0        System.String  0   shared           static Empty
    
    

    簡要分析擴容后的內存結構

    此次執行的代碼為:

    Dictionary<int, string> dic = new Dictionary<int, string>();
    dic.Add(1, "xyxy");
    dic.Add(7, "xyxy");
    dic.Add(2, "xyxy");
    dic.Add(4, "xyxy");
    Console.Read();
    

    同樣採取附加進程的方式分析這段代碼執行后的內存結構,本章節中忽略掉如何查找Dictionary變量地址的部分,直接分析buckets數組和entries數組的內容。

    首先是buckets數組的內存結構:

    0:000> !da -start 0 -details 0000019a471a54f8 
    Name:        System.Int32[]
    MethodTable: 00007ffcc6b38538
    EEClass:     00007ffcc6ca60e8
    Size:        52(0x34) bytes
    Array:       Rank 1, Number of elements 7, Type Int32
    Element Methodtable: 00007ffcc6b385a0
    [0] 0000019a471a5508
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                    1     m_value
    [1] 0000019a471a550c
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                    0     m_value
    [2] 0000019a471a5510
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                    2     m_value
    [3] 0000019a471a5514
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                   -1     m_value
    [4] 0000019a471a5518
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                    3     m_value
    [5] 0000019a471a551c
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                   -1     m_value
    [6] 0000019a471a5520
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                   -1     m_value
    
    

    然後是entries的內存結構:

    0:000> !da -start 0 -details 00000237effb2fa8 
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]][]
    MethodTable: 00007ffa965135b8
    EEClass:     00007ffa9662d1f0
    Size:        192(0xc0) bytes
    Array:       Rank 1, Number of elements 7, Type VALUETYPE
    Element Methodtable: 00007ffa96513558
    [0] 00000237effb2fb8
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    1     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    1     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000237effb2db0     value
    [1] 00000237effb2fd0
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    7     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    7     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000237effb2db0     value
    [2] 00000237effb2fe8
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    2     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    2     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000237effb2db0     value
    [3] 00000237effb3000
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    4     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    4     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000237effb2db0     value
    [4] 00000237effb3018
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value
    [5] 00000237effb3030
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value
    [6] 00000237effb3048
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value
    
    

    從內存的結構來看,擴容后bucket數組中使用了下標為0、1、2和4這四個位置,entries中使用了0~3存儲了示例中添加的數據,符合前文中理論分析的結果,兩者相互之間具有良好的印證關係。

    時間複雜度分析

    時間複雜度表達的是數據結構操作數據的時候所消耗的時間隨着數據集規模的增長的變化趨勢。常用的指標有最好情況時間複雜度、最壞情況時間複雜度和均攤時間複雜度。那麼對於Dictionary<Tkey,TValue>來說,插入和查找過程中這些時間複雜度分別是什麼樣的呢?

    最好情況時間複雜度:對於查找來說最好的是元素處於鏈表的頭部,查找效率不會隨着數據規模的增加而增加,因此該複雜度為常量階複雜度,即O(1);插入操作最理想的情況是數組中有空餘的空間,不需要進行擴容操作,此時時間複雜度也是常量階的,即O(1);

    最壞情況時間複雜度:對於插入來說,比較耗時的操作場景是需要順着鏈表查找符合條件的元素,鏈表越長,查找時間越長(下文稱為場景一);而對於插入來說最壞的情況是數組長度不足,需要動態擴容並重新組織鏈表結構(下文稱為場景二);

    場景一中時間複雜度隨着鏈表長度的增加而增加,但是Dictionary中規定鏈表的最大長度為100,如果有長度超過100的鏈表就需要擴容並調整鏈表結構,所以順着鏈表查找數據不會隨着數據規模的增長而增長,最大時間複雜度是固定的,因此時間複雜度還是常量階複雜度,即O(1);

    場景二中時間複雜度隨着數組中元素的數量增加而增加,如果原來的數組元素為n個,那麼擴容時需要將這n個元素拷貝到新的數組中並計算其在新鏈表中的位置,因此該操作的時間複雜度是隨着數組的長度n的增加而增加的,屬於線性階時間複雜度,即O(n)。

    綜合場景一和場景二的分析結果得出最壞情況時間複雜度出現在數據擴容過程中,時間複雜度為O(n)。

    最好情況時間複雜度和最壞情況時間複雜度都過於極端,只能描述最好的情況和最壞的情況,那麼在使用過程中如何評價數據結構在大部分情況下的時間複雜度?通常對於複雜的數據結構可以使用均攤時間複雜度來評價這個指標。均攤時間複雜度適用於對於一個數據進行連續操作,大部分情況下時間複雜度都很低,只有個別情況下時間複雜度較高的場景。這些操作存在前後連貫性,這種情況下將較高的複雜度攤派到之前的操作中,一般均攤時間複雜度就相當於最好情況時間複雜度。

    通過前面的分析可以看出Dictionary恰好就符合使用均攤時間複雜度分析的場景。以插入操作為例,假設預申請的entries數組長度為n,在第n+1次插入數據時必然會遇到一次數組擴容導致的時間消耗較高的場景。將這次較為複雜的操作的時間均攤到之前的n次操作后,每次操作時間的複雜度也是常量階的,因此Dictionary插入數據時均攤時間複雜度也是O(1)。

    實踐建議

    首先,Dictionary這種類型適合用於對於數據檢索有明顯目的性的場景,比如讀寫比例比較高的場景。其次,如果有大數據量的場景,最好能夠提前聲明容量,避免多次分配內存帶來的額外的時間和空間上的消耗。因為不進行擴容的場景,插入和查找效率都是常量階O(1),在引起擴容的情況下,時間複雜度是線性階O(n)。如果僅僅是為了存儲數據,使用Dictionary並不合適,因為它相對於List<T>具有更加複雜的數據結構,這樣會帶來額外的空間上面的消耗。雖然Dictionary<TKey,TValue>的TKey是個任意類型的,但是除非是對於判斷對象是否相等有特殊的要求,否則不建議直接使用自定義類作為Tkey。

    總結

    C#中的Dictionary<TKey,TValue>是藉助於散列函數構建的高性能數據結構,Dictionary解決散列衝突的方式是使用鏈表來保存散列值存在衝突的節點,俗稱拉鏈法。本文中通過圖文並茂的方式幫助理解Dictionary添加元素和擴容過程這兩個典型的應用場景,在理論分析之後使用windbg分析了內存中的實際結構,以此加深對於這種數據類型的深入理解。隨後在此基礎上分析了Dictionary的時間複雜度。Dictionary的最好情況時間複雜度是O(1),最壞情況複雜度是O(n),均攤時間複雜度是O(1),Dictionary在大多數情況下都是常量階時間複雜度,在內部數組自動擴容過程中會產生明顯的性能下降,因此在實際實踐過程中最好在聲明新對象的時候能夠預估容量,儘力避免數組自動擴容導致的性能下降。

    參考資料

    • Dictionary<TKey,TValue>源代碼(.net framework4.8版本)
    • .NET中Dictionary<TKey, TValue>淺析
    • 解決hash衝突的三個方法
    • 算法複雜度分析(下):最好、最壞、平均、均攤等時間複雜度概述

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

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

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

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

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

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

    ※回頭車貨運收費標準

  • 解Bug之路-中間件”SQL重複執行”

    解Bug之路-中間件”SQL重複執行”

    前言

    我們的分庫分表中間件在線上運行了兩年多,到目前為止還算穩定。在筆者將精力放在處理各種災難性事件(例如中間件物理機宕機/數據庫宕機/網絡隔離等突發事件)時。竟然發現還有一些奇怪的corner case。現在就將排查思路寫成文章分享出來。

    Bug現場

    應用拓撲

    應用通過中間件連後端多個數據庫,sql會根據路由規則路由到指定的節點,如下圖所示:

    錯誤現象

    應用在做某些數據庫操作時,會發現有比較大的概率失敗。他們的代碼邏輯是這樣:

    	int count = updateSql(sql1);
    	...
    	// 偽代碼
    	int count = updateSql("update test set value =1 where id in ("100","200") and status = 1;
    	if( 0 == count ){
    		throw new RuntimeException("更新失敗");
    	}
    	......
    	int count = updateSql(sql3);
    	...
    

    即每做一次update之後都檢查下是否更新成功,如果不成功則回滾並拋異常。
    在實際測試的過程中,發現經常報錯,更新為0。而實際那條sql確實是可以更新到的(即報錯回滾后,我們手動執行sql可以執行並update count>0)。

    中間件日誌

    筆者根據sql去中間件日誌裏面搜索。發現了非常奇怪的結果,日誌如下:

    2020-03-13 11:21:01:440 [NIOREACTOR-20-RW] frontIP=>ip1;sqlID=>12345678;rows=>0;sql=>update test set value =1 where id in ("1","2") and status = 1;start=>11:21:01:403;time=>24266;
    2020-03-13 11:21:01:440 [NIOREACTOR-20-RW] frontIP=>ip1;sqlID=>12345678;rows=>2;sql=>update test set value =1 where id in ("1","2") and status = 1;start=>11:21:01:403;time=>24591;
    

    由於中間件對每條sql都標識了唯一的一個sqlID,在日誌表現看來就好像sql執行了兩遍!由於sql中有一個in,很容易想到是否被拆成了兩條執行了。如下圖所示:

    這條思路很快被筆者否決了,因為筆者explain並手動執行了一下,這條sql確實只路由到了一個節點。真正完全否決掉這條思路的是筆者在日誌裏面還發現,同樣的SQL會打印三遍!即看上去像執行了三次,這就和僅僅只in了兩個id的sql在思路上相矛盾了。

    數據庫日誌

    那到底數據真正執行了多少條呢?找DBA去撈一下其中的sql日誌,由於線下環境沒有日誌切割,日誌量巨大,搜索時間太慢。沒辦法,就按照現有的數據進行分析吧。

    日誌如何被觸發

    由於當前沒有任何思路,於是筆者翻看中間件的代碼,發現在update語句執行后,中間件會在收到mysql okay包后打印上述日誌。如下圖所示:

    注意到所有出問題的update出問題的時候都是同一個NIOREACTOR線程先後打印了兩條日誌,所以筆者推斷這兩個okay大概率是同一個後端連接返回的。

    什麼情況會返回多個okay?

    這個問題筆者思索了很久,因為在筆者的實際重新執行出問題的sql並debug時,永遠只有一個okay返回。於是筆者聯想到,我們中間件有個狀態同步的部分,而這些狀態同步是將set auto_commit=0等sql拼接到應用發送的sql前面。即變成如下所示:

    sql可能為
    set auto_commit=0;set charset=gbk;>update test set value =1 where id in ("1","2") and status = 1;
    

    於是筆者細細讀了這部分的代碼,發現處理的很好。其通過計算出前面拼接出的sql數量,再在接收okay包的時候進行遞減,最後將真正執行的那條sql處理返回。其處理如下圖所示:

    但這裏確給了筆者一個靈感,即一條sql文本確實是有可能返回多個okay包的。

    真相大白

    在筆者發現(sql1;sql2;)這樣的拼接sql會返回多個okay包后,就立刻聯想到,該不會業務自己寫了這樣的sql發給中間件,造成中間件的sql處理邏輯錯亂吧。因為我們的中間件只有在對自己拼接(同步狀態)的sql做處理,明顯是無法處理應用傳過來即為拼接sql的情況。
    由於看上去有問題的那條sql並沒有拼接,於是筆者憑藉這條sql打印所在的reactor線程往上搜索,發現其上面真的有拼接sql!

    2020-03-1311:21:01:040[NIOREACTOR-20RW]frontIP=>ip1;sqlID=>12345678;rows=>1;
    sql=>update test_2 set value =1 where id=1 and status = 1;update test_2 set value =1 where id=2 and status = 1;
    

    如上圖所示,(update1;update2)中update1的okay返回被驅動認為是所有的返回。然後應用立即發送了update3。前腳剛發送,update2的okay返回就回來了而其剛好是0,應用就報錯了(要不是0,這個錯亂邏輯還不會提前暴露)。那三條”重複執行”也很好解釋了,就是之前的拼接sql會有三條。

    為何是概率出現

    但奇怪的是,並不是每次拼接sql都會造成update3″重複執行”的現象,按照筆者的推斷應該前面只要是多條拼接sql就會必現才對。於是筆者翻了下jdbc驅動源碼,發現其在發送命令之前會清理下接收buffer,如下所示:

    MysqlIO.java
    final Buffer sendCommand(......){
    	......
    	// 清理接收buffer,會將殘存的okay包清除掉
    	clearInputStream();
    	......
    	send(this.sendPacket, this.sendPacket.getPosition());
    	......
    }
    

    正是由於clearInputStream()使得錯誤非必現(暴露),如果okay(update2)在應用發送第三條sql前先到jdbc驅動會被驅動忽略!
    讓我們再看一下不會讓update3″重複執行”的時序圖:

    即根據okay(update2)返回的快慢來決定是否暴露這個問題,如下圖所示:

    同時筆者觀察日誌,確實這種情況下”update1;update2″這條語句在中間件裏面日誌有兩條。

    臨時解決方案

    讓業務開發不用這些拼接sql的寫法后,再也沒出過問題。

    為什麼不連中間件是okay的

    業務開發這些sql是就在線上運行了好久,用了中間件后才出現問題。
    既然不連中間件是okay的,那麼jdbc必然有這方面的完善處理,筆者去翻了下mysql-connect-java(5.1.46)。由於jdbc裏面存在大量的兼容細節處理,筆者這邊只列出一些關鍵代碼路徑:

    MySQL JDBC 源碼
    MySQLIO
    stack;
    executeUpdate
    	|->executeUpdateInternel
    		|->executeInternal
    			|->execSQL
    				|->sqlQueryDirect
    					|->readAllResults (MysqlIO.java)
    readAllResults: //核心在這個函數的處理裏面
    ResultSetImpl readAllResults(......){
    		......
           while (moreRowSetsExist) {
    			  ......
    			  // 在返回okay包的保中其serverStatus字段中如果SERVER_MORE_RESULTS_EXISTS置位
    			  // 表明還有更多的okay packet
                moreRowSetsExist = (this.serverStatus & SERVER_MORE_RESULTS_EXISTS) != 0;
            }
            ......
    }
    

    正確的處理流程如下圖所示:

    而我們中間件的源碼確實這麼處理的:

    @Override
    public void okResponse(byte[] data, BackendConnection conn) {
    	......
    	// 這邊僅僅處理了autocommit的狀態,沒有處理SERVER_MORE_RESULTS_EXISTS
    	// 所以導致了不兼容拼接sql的現象
    	ok.serverStatus = source.isAutocommit() ? 2 : 1;
    	ok.write(source);
    	......
    }
    

    select也”重複執行”了

    解決完上面的問題后,筆者在日誌里竟然發現select盡然也有重複的,這邊並不會牽涉到okay包的處理,難道還有問題?日誌如下所示:

    2020-03-13 12:21:01:040[NIOREACTOR-20RW]frontIP=>ip1;sqlID=>12345678;rows=>1;select abc;
    2020-03-13 12:21:01:045[NIOREACTOR-21RW]frontIP=>ip2;sqlID=>12345678;rows=>1;select abc;
    

    從不同的REACTOR線程號(20RW/21RW)和不同的frontIP(ip1,ip2)來看是兩個連接執行了同樣的sql,但為何sqlID是一樣的?任何一個詭異的現象都必須一查到底。於是筆者登錄到應用上看了下應用日誌,確實應用有兩個不同的線程運行了同一條sql。
    那肯定是中間件日誌打印的問題了,筆者很快就想通了其中的關竅,我們中間件有個對同樣sql緩存其路由節點結構體的功能(這樣下一次同樣sql就不必解析,降低了CPU),而sqlID信息正好也在那個路由節點結構體裏面。如下圖所示:

    這個緩存功能感覺沒啥用(因為線上基本是沒有相同sql的),於是筆者在筆者優化的閃電模式下(大幅度提高中間件性能)將這個功能禁用掉了,沒想到為了排查問題而開啟的詳細日誌碰巧將這個功能開啟了。

    總結

    任何系統都不能說百分之百穩定可靠,尤其是不能立flag。在線上運行了好幾年的系統也是如此。只有對所有預料外的現象進行細緻的追查與深入的分析並解決,才能讓我們的系統越來越可靠。

    公眾號

    關注筆者公眾號,獲取更多乾貨文章:

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

    【其他文章推薦】

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

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

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

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

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

  • 多圖解釋Redis的整數集合intset升級過程

    多圖解釋Redis的整數集合intset升級過程

    redis源碼分析系列文章

    [Redis源碼系列]在Liunx安裝和常見API 

    為什麼要從Redis源碼分析 

    String底層實現——動態字符串SDS 

    雙向鏈表都不懂,還說懂Redis?

    面試官:說說Redis的Hash底層 我:……(來自閱文的面試題)

    Redis的跳躍表確定不了解下

     

    前言

    大噶好,今天仍然是元氣滿滿的一天,拋開永遠寫不完的需求,拒絕要求賊變態的客戶,單純的學習技術,感受技術的魅力。(哈哈哈,皮一下很開森)

    前面幾周我們一起看了Redis底層數據結構,如動態字符串SDS雙向鏈表Adlist字典Dict跳躍表,如果有對Redis常見的類型或底層數據結構不明白的請看上面傳送門。

    今天來說下set的底層實現整數集合,如果有對set不明白的,常見的API使用這篇就不講了,看上面的傳送門哈。

    整數集合概念

    整數集合是Redis設計的一種底層結構,是set的底層實現,當集合中只包含整數值元素,並且這個集合元素數據不多時,會使用這種結構。但是如果不滿足剛才的條件,會使用其他結構,這邊暫時不講哈。

    下圖為整數集合的實際組成,包括三個部分,分別是編碼格式encoding,包含元素數量length,保存元素的數組contents。(這邊只需要簡單看下,下面針對每個模塊詳細說明哈)

    整數集合的實現

    我們看下intset.h裏面關於整數集合的定義,上代碼哈:

    //整數集合結構體
    typedef struct intset {
        uint32_t encoding;  //編碼格式,有如下三種格式,初始值默認為INTSET_ENC_INT16
        uint32_t length;    //集合元素數量
        int8_t contents[];  //保存元素的數組,元素類型並不一定是ini8_t類型,柔性數組不佔intset結構體大小,並且數組中的元素從小到大排列。
    } intset;               
    
    #define INTSET_ENC_INT16 (sizeof(int16_t))   //16位,2個字節,表示範圍-32,768~32,767
    #define INTSET_ENC_INT32 (sizeof(int32_t))   //32位,4個字節,表示範圍-2,147,483,648~2,147,483,647
    #define INTSET_ENC_INT64 (sizeof(int64_t))   //64位,8個字節,表示範圍-9,223,372,036,854,775,808~9,223,372,036,854,775,807

     

     

    編碼格式encoding

    包括INTSET_ENC_INT16,INTSET_ENC_INT32,INTSET_ENC_INT64三種類型,其分別對應着不同的範圍,具體看上面代碼的註釋信息。

    因為插入的數據的大小是不一樣的,為了盡可能的節約內存(畢竟都是錢,平時要省着點用),所以我們需要使用不同的類型來存儲數據。

    集合元素數量length

    記錄了保存數據contents的長度,即有多少個元素。

    保存元素的數組contents

    真正存儲數據的地方,數組是按照從小到大有序排序的,並且不包含任何重複項(因為set是不含重複項,所以其底層實現也是不含包含項的)。

    整數集合升級過程(重點,手動標星)

    上面的圖我們重新看下,編碼格式encoding為INTSET_ENC_INT16,即每個數據佔16位。長度length為4,即數組content裏面有四個元素,分別是1,2,3,4。如果我們要添加一個数字位40000,很明顯超過編碼格式為INTSET_ENC_INT16的範圍-32,768~32,767,應該是編碼格式為INTSET_ENC_INT32。那麼他是如何升級的呢,從INTSET_ENC_INT16升級到INTSET_ENC_INT32的呢?

    1.了解舊的存儲格式

    首先我們看下1,2,3,4這四個元素是如何存儲的。首先要知道一共有多少位,計算規則為length*編碼格式的位數,即4*16=64。所以每個元素佔用了16位。

    2.確定新的編碼格式

    新的元素為40000,已經超過了INTSET_ENC_INT16的範圍-32,768~32,767,所以新的編碼格式為INTSET_ENC_INT32。

    3.根據新的編碼格式新增內存

    上面已經說明了編碼格式為INTSET_ENC_INT32,計算規則為length*編碼格式的位數,即5*32=160。所以新增的位數為64-159。

    4.根據編碼格式設置對應的值

    從上面知道按照新的編碼格式,每個數據應該佔用32位,但是舊的編碼格式,每個數據佔用16位。所以我們從後面開始,每次獲取32位用來存儲數據。

    這樣說太難懂了,看下圖。

    首先,那最後32位,即128-159存儲40000。那麼第49-127是空着的。

    接着,取空着的49-127最後的32位,即96到127這32位,用來存儲4。那麼之前4存儲的位置48-6349-127剩下的64-95這兩部分組成了一個大部分,即48-95,現在空着啦。

    在接着在48-95這個大部分,再取后32位,即64-95,用來存儲3。那麼之前3存儲位置32-4748-95剩下的48-63這兩部分組成了一個大部分,即32-63,現在空着啦。

    再接着,將32-63這個大部分,再取后32位,即還是32-63,用來存儲2。那麼之前2存儲位置16-31空着啦。

    最後,將16-31和原來0-31合起來,存儲1。

    至此,整個升級過程結束。整體來說,分為3步,確定新的編碼格式,新增需要的內存空間,從后往前調整數據。

    這邊有個小問題,為啥要從后往前調整數據呢?

    原因是如果從前往後,數據可能會覆蓋。也拿上面個例子來說,數據1在0-15位,數據2在16-31位,如果從前往後,我們知道新的編碼格式INTSET_ENC_INT32要求每個元素佔用32位,那麼數據1應該佔用0-31,這個時候數據2就被覆蓋了,以後就不知道數據2啦。

    但是從后往前,因為後面新增了一些內存,所以不會發生覆蓋現象。

    升級的優點

     節約內存

    整數集合既可以讓集合保存三種不同類型的值,又可以確保升級操作只在有需要的時候進行,這樣就節省了內存。 

    不支持降級

    一旦對數組進行升級,編碼就會一直保存升級后的狀態。即使後面把40000刪掉了,編碼格式還是不會將會INTSET_ENC_INT16。

    整數集合的源碼分析

    創建一個空集合 intsetnew

    這個方法比較簡單,是初始化整數集合的步驟,即下圖部分。

    主要的步驟是分配內存空間,設置默認編碼格式,以及初始化數組長度length。

    intset *intsetNew(void) {
        intset *is = zmalloc(sizeof(intset));//分配內存空間 
        is->encoding = intrev32ifbe(INTSET_ENC_INT16);//設置默認編碼格式INTSET_ENC_INT16 
        is->length = 0;//初始化length 
        return is;
    }

    添加元素並升級insetAdd流程圖(重點)

    添加元素並升級insetAdd源碼分析

    可以根據上面的流程圖,對照着下面的源碼分析,這邊就不寫啦哈。

    //添加元素
    //輸入參數*is為原整數集合
    //value為要添加的元素
    //*success為是否添加成功的標誌量 ,1表示成功,0表示失敗 
    intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
        //確定要添加的元素的編碼格式 
        uint8_t valenc = _intsetValueEncoding(value);
        
        uint32_t pos;
        //如果success沒有初始值,則初始化為1 
        if (success) *success = 1;
    
       //如果新的編碼格式大於現在的編碼格式,則升級並添加元素 
        if (valenc > intrev32ifbe(is->encoding)) {
            //調用另一個方法 
            return intsetUpgradeAndAdd(is,value);
        } else {
            //如果編碼格式不變,則調用查詢方法 
            //輸入參數is為原整數集合 
            //value為要添加的數據
            //pos為位置 
            if (intsetSearch(is,value,&pos)) {//如果找到了,則直接返回,因為數據是不可重複的。 
                if (success) *success = 0;
                return is;
            }
    
            //設置length 
            is = intsetResize(is,intrev32ifbe(is->length)+1);
            if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
        }
        //設置數據 
        _intsetSet(is,pos,value);
        is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
        return is;
    }
    
    
    //#define INT8_MAX 127
    //#define INT16_MAX 32767
    //#define INT32_MAX 2147483647
    //#define INT64_MAX 9223372036854775807LL 
    static uint8_t _intsetValueEncoding(int64_t v) {
        if (v < INT32_MIN || v > INT32_MAX)
            return INTSET_ENC_INT64;
        else if (v < INT16_MIN || v > INT16_MAX)
            return INTSET_ENC_INT32;
        else
            return INTSET_ENC_INT16;
    }
    
    
    //根據輸入參數value的編碼格式,對整數集合is的編碼格式升級 
    static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
        //當前集合的編碼格式 
        uint8_t curenc = intrev32ifbe(is->encoding);
        //根據對value解析獲取新的編碼格式 
        uint8_t newenc = _intsetValueEncoding(value);
        //獲取集合元素數量 
        int length = intrev32ifbe(is->length);
        //如果要添加的數據小於0,則prepend為1,否則為0 
        int prepend = value < 0 ? 1 : 0;
    
       //設置集合為新的編碼格式,並根據編碼格式重新設置內存 
        is->encoding = intrev32ifbe(newenc);
        is = intsetResize(is,intrev32ifbe(is->length)+1);
    
        //逐步循環,直到length小於0,挨個重新設置每個值,從后往前 
        while(length--)
            _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
    
        //如果value為負數,則放在最前面 
        if (prepend)
            _intsetSet(is,0,value);
        else//如果value為整數,設置最末尾的元素為value 
            _intsetSet(is,intrev32ifbe(is->length),value);
        //重新設置length 
        is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
        return is;
    }
    
    
    //找到is集合中值為value的下標,返回1,並保存在pos中,沒有找到返回0,並將pos設置為value可以插入到數組的位置
    static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
        int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
        int64_t cur = -1;
    
        //如果集合為空,那麼位置pos為0 
        if (intrev32ifbe(is->length) == 0) { 
            if (pos) *pos = 0;
            return 0;
        } else {
            //因為數據是有序集合,如果要添加的數據大於最後一個数字,那麼直接把要添加的值放在最後即可,返回最大值下標 
            if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
                if (pos) *pos = intrev32ifbe(is->length);
                return 0;
            } else if (value < _intsetGet(is,0)) { //如果這個數據小於數組下標為0的數據,即為最小值 ,返回0 
                if (pos) *pos = 0;
                return 0;
            }
        }
        //有序集合採用二分法 
        while(max >= min) {
            mid = ((unsigned int)min + (unsigned int)max) >> 1;
            cur = _intsetGet(is,mid);
            if (value > cur) {
                min = mid+1;
            } else if (value < cur) {
                max = mid-1;
            } else {
                break;
            }
        }
    
        //確定找到 
        if (value == cur) {
            if (pos) *pos = mid;//設置參數pos,返回1,即找到位置 
            return 1;
        } else {//如果沒找到,則min和max相鄰,隨便設置都行,並返回0 
            if (pos) *pos = min; 
            return 0;
        }
    }

     

    結語

    該篇主要講了Redis的SET數據類型的底層實現整數集合,先從整數集合是什麼,,剖析了其主要組成部分,進而通過多幅過程圖解釋了intset是如何升級的,最後結合源碼對整數集合進行描述,如創建過程,升級過程,中間穿插例子和過程圖。

    如果覺得寫得還行,麻煩給個贊,您的認可才是我寫作的動力!

    如果覺得有說的不對的地方,歡迎評論指出。

    好了,拜拜咯。

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

    【其他文章推薦】

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

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

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

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

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

  • 印尼蘇門答臘象身首異處慘死 象牙遭盜採

    摘錄自2019年11月8日自由時報屏東報導

    印尼保育官員今天表示,1頭列為極危(critically endangered)物種的蘇門答臘象屍體被發現,牠的頭被砍下且象牙被拔走,顯然是一宗盜獵案件。

    蘇門答臘島廖內省(Riau)一名農園工人昨天發現這頭40歲公象腐爛的屍體。當地保育機關主管蘇哈尤諾(Suharyono)在聲明中說:「大象的頭被砍下,切斷的象鼻落在距離象身1公尺處。」當局正在追查犯案人士。

    森林濫伐造成蘇門答臘象的天然棲地縮減,導致牠們和人類的衝突加劇。另一方面,蘇門答臘象象牙在野生動物黑市交易中價值連城。

    去年在印尼亞齊省(Aceh)也發現一具顯然被毒死的蘇門答臘象屍體,當時牠的象牙也不見。印尼環境部估計,境內野生蘇門答臘象只剩不到2000頭。

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

    【其他文章推薦】

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

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

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

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

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

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