2016年將推出的那些新能源汽車政策

2016年新能源汽車仍將高速增長,但還存在很多不確定因素,互聯網企業造車、電池企業准入、低速電動車企業能否借機“轉正”等懸念是否能揭曉?2016年,中國還將推出哪些新能源汽車政策?讓我們先來預測一下。

《電動汽車動力蓄電池回收利用技術政策》

狀態:徵求意見

預發佈時間:2016年初

2015年9月11日,國家發展改革委環資司、工業和資訊化部裝備司聯合發佈《電動汽車動力蓄電池回收利用技術政策》(徵求意見稿)。根據徵求意見稿要求,國家將推動建立統一的動力蓄電池產品編碼制度。動力蓄電池生產企業應對所生產(或進口)的所有動力蓄電池產 品進行編碼,並建立追溯系統追蹤動力蓄電池流向。編碼應與電池產品及整車具有惟一對應性,編碼應標識在動力蓄電池產品顯著位置,且具有較高的牢固性。
 

《乘用車企業平均燃料消耗量管理辦法(草稿)》

狀態:草稿階段

預發佈時間:2016年

據悉,《乘用車企業平均燃料消耗量管理辦法(草稿)》已經制定完成,即將徵求意見。草案中最重要的內容是企業 平均燃料消耗量積分轉結及交易制度,不達標企業將有巨額罰款。根據草稿,乘用車企業平均燃料消耗量不達標罰款納入預算內,作為汽車行業節能管理專項資金, 其中40%用於汽車節能管理,60%用於支援企業為改善燃料經濟性而進行的產品研發。

《電動汽車充電介面及通信協定5項國家標準》

實施時間:2016年1月1日

2015年12月28日,質檢總局、國家標準委聯合國家能源局、工信部、科技部在京發佈新修訂的電動汽車 充電介面及通信協定5項國家標準。在交流充電部分,禁止採用存在安全隱患的直通電纜加普通家用插頭的連接方式;針對大於16安培的充電方式強制要求在車輛 插座和供電插座安裝電子鎖和溫度感測器,防止帶電拉弧及過熱引起的設備損壞和火災等事故……

《低速電動汽車管理辦法》

狀態:未發佈

預發佈時間:2016年

工信部、發改委、公安部、環境部等部門已經對低速電動汽車進行調研,統一的“低速電動汽車管理辦法”有望出 台。據瞭解,在山東,僅德州一個地級市,低速電動汽車的年產量超過10萬輛,低速電動車製造行業和配套產業直接為德州市貢獻超過50億元的GDP,提供了 一大批工作崗位。全國低速電動汽車主要分佈于山東、河北、河南等省,湖北、四川、湖南、安徽、江西、山西等省份隨著市場的發展產銷量也在不斷攀升。

《私人用戶居住地充電基礎設施建設管理示範文本(操作指南)》

狀態:徵求意見

預發佈時間:2016年

2015年10月12日,國家能源局在江蘇常州舉辦了“電動汽車充電基礎設施促進聯盟成立暨建設經驗交流現場 會”,會上發佈了《私人用戶居住地充電基礎設施建設管理示範文本(操作指南)》(徵求意見稿)。該意見稿對具備固定車位和不具備固定車位的私人用戶需準備 的材料及建設流程做出了詳細規定,此外對配電容量不足情況、消防驗收方面也有具體要求,具體細節還需進一步調研討論。

《關於“十三五”新能源汽車充電設施獎勵政策及加強新能源汽車推廣應用的通知(徵求意見稿)》

狀態:徵求意見

預發佈時間:2016年上半年

12月15日,財政部、科技部、工業和資訊化部、發展改革委、國家能源局聯合發佈《關於“十三五”新能源汽車 充電設施獎勵政策及加強新能源汽車推廣應用的通知(徵求意見稿)》。徵求意見稿明確了拿到獎勵的門檻,例如:大氣污染治理重點區域和重點省市(包括北京、 上海、天津、河北、山西、江蘇、浙江、山東、廣東、海南),2016~2020年度新能源汽車(標準車,下同)推廣數量分別不低於3萬輛、3.5萬輛、 4.3萬輛、5.5萬輛、7萬輛,且推廣的新能源汽車數量占本地區新增及更新的汽車總量比例不低於3%、4%、5%、8%、10%。此外,還明確了新能源標準車折算關係,例如純電動乘用車(續駛里程(參配、圖片、詢價) ≥150km)與標準車折算比例為1:1等。
 

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

【其他文章推薦】

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

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

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

.NET Core 3.0 單元測試與 Asp.Net Core 3.0 集成測試

單元測試與集成測試

測試必要性說明

相信大家在看到單元測試與集成測試這個標題時,會有很多感慨,我們無數次的在實踐中提到要做單元測試、集成測試,但是大多數項目都沒有做或者僅建了項目文件。這裡有客觀原因,已經接近交付日期了,我們沒時間做白盒測試了。也有主觀原因,面對業務複雜的代碼我們不知道如何入手做單元測試,不如就留給黑盒測試吧。但是,當我們的代碼無法進行單元測試的時候,往往就是代碼開始散發出壞味道的時候。長此以往,將欠下技術債務。在實踐過程中,技術債務常常會存在,關鍵在於何時償還,如何償還。

上圖說明了隨着時間的推移開發/維護難度的變化。

測試框架選擇

在 .NET Core 中,提供了 xUnit 、NUnit 、 MSTest 三種單元測試框架。

MSTest UNnit xUnit 說明 提示
[TestMethod] [Test] [Fact] 標記一個測試方法
[TestClass] [TestFixture] n/a 標記一個 Class 為測試類,xUnit 不需要標記特性,它將查找程序集下所有 Public 的類
[ExpectedException] [ExpectedException] Assert.Throws 或者 Record.Exception xUnit 去掉了 ExpectedException 特性,支持 Assert.Throws
[TestInitialize] [SetUp] Constructor 我們認為使用 [SetUp] 通常來說不好。但是,你可以實現一個無參構造器直接替換 [SetUp]。 有時我們會在多個測試方法中用到相同的變量,熟悉重構的我們會提取公共變量,並在構造器中初始化。但是,這裏我要強調的是:在測試中,不要提取公共變量,這會破壞每個測試用例的隔離性以及單一職責原則。
[TestCleanup] [TearDown] IDisposable.Dispose 我們認為使用 [TearDown] 通常來說不好。但是你可以實現 IDisposable.Dispose 以替換。 [TearDown] 和 [SetUp] 通常成對出現,在 [SetUp] 中初始化一些變量,則在 [TearDown] 中銷毀這些變量。
[ClassInitialize] [TestFixtureSetUp] IClassFixture< T > 共用前置類 這裏 IClassFixture< T > 替換了 IUseFixture< T > ,
[ClassCleanup] [TestFixtureTearDown] IClassFixture< T > 共用後置類 同上
[Ignore] [Ignore] [Fact(Skip=”reason”)] 在 [Fact] 特性中設置 Skip 參數以臨時跳過測試
[Timeout] [Timeout] [Fact(Timeout=n)] 在 [Fact] 特性中設置一個 Timeout 參數,當允許時間太長時引起測試失敗。注意,xUnit 的單位時毫秒。
[DataSource] n/a [Theory], [XxxData] Theory(數據驅動測試),表示執行相同代碼,但具有不同輸入參數的測試套件 這個特性可以幫助我們少寫很多代碼。

以上寫了 MSTest 、UNnit 、 xUnit 的特性以及比較,可以看出 xUnit 在使用上相對其它兩個框架來說提供更多的便利性。但是這裏最終實現還是看個人習慣以選擇。

單元測試

  1. 新建單元測試項目

  2. 新建 Class

  3. 添加測試方法

            /// <summary>
            /// 添加地址
            /// </summary>
            /// <returns></returns>
            [Fact]
            public async Task Add_Address_ReturnZero()
            {
                DbContextOptions<AddressContext> options = new DbContextOptionsBuilder<AddressContext>().UseInMemoryDatabase("Add_Address_Database").Options;
                var addressContext = new AddressContext(options);
    
                var createAddress = new AddressCreateDto
                {
                    City = "昆明",
                    County = "五華區",
                    Province = "雲南省"
                };
                var stubAddressRepository = new Mock<IRepository<Domain.Address>>();
                var stubProvinceRepository = new Mock<IRepository<Province>>();
                var addressUnitOfWork = new AddressUnitOfWork<AddressContext>(addressContext);
    
                var stubAddressService = new AddressServiceImpl.AddressServiceImpl(stubAddressRepository.Object, stubProvinceRepository.Object, addressUnitOfWork);
                await stubAddressService.CreateAddressAsync(createAddress);
                int addressAmountActual = await addressContext.Addresses.CountAsync();
                Assert.Equal(1, addressAmountActual);
            }
    • 測試方法的名字包含了測試目的、測試場景以及預期行為。
    • UseInMemoryDatabase 指明使用內存數據庫。
    • 創建 createAddress 對象。
    • 創建 Stub 。在單元測試中常常會提到幾個概念 Stub , Mock 和 Fake ,那麼在應用中我們該如何選擇呢?
      • Fake – Fake 通常被用於描述 Mock 或 Stub ,如何判斷它是 Stub 還是 Mock 依賴於使用上下文,換句話說,Fake 即是 Stub 也是 Mock 。
      • Stub – Stub 是系統中現有依賴項的可控替代品。通過使用 Stub ,你可以不用處理依賴直接測試你的代碼。默認情況下, 偽造對象以stub 開頭。
      • Mock – Mock 對象是系統中的偽造對象,它決定單元測試是否通過或失敗。Mock 會以 Fake 開頭,直到被斷言為止。
    • Moq4 ,使用 Moq4 模擬我們在項目中依賴對象。
  4. 打開視圖 -> 測試資源管理器。

  5. 點擊運行,得到測試結果。

  6. 至此,一個單元測試結束。

集成測試

集成測試確保應用的組件功能在包含應用的基礎支持下是正確的,例如:數據庫、文件系統、網絡等。

  1. 新建集成測試項目。

  2. 添加工具類 Utilities 。

    using System.Collections.Generic;
    using AddressEFRepository;
    
    namespace Address.IntegrationTest
    {
        public static class Utilities
        {
            public static void InitializeDbForTests(AddressContext db)
            {
                List<Domain.Address> addresses = GetSeedingAddresses();
                db.Addresses.AddRange(addresses);
                db.SaveChanges();
            }
    
            public static void ReinitializeDbForTests(AddressContext db)
            {
                db.Addresses.RemoveRange(db.Addresses);
                InitializeDbForTests(db);
            }
    
            public static List<Domain.Address> GetSeedingAddresses()
            {
                return new List<Domain.Address>
                {
                    new Domain.Address
                    {
                        City = "貴陽",
                        County = "測試縣",
                        Province = "貴州省"
                    },
                    new Domain.Address
                    {
                        City = "昆明市",
                        County = "武定縣",
                        Province = "雲南省"
                    },
                    new Domain.Address
                    {
                        City = "昆明市",
                        County = "五華區",
                        Province = "雲南省"
                    }
                };
            }
        }
    }
  3. 添加 CustomWebApplicationFactory 類,

     using System;
     using System.IO;
     using System.Linq;
     using AddressEFRepository;
     using Microsoft.AspNetCore.Hosting;
     using Microsoft.AspNetCore.Mvc.Testing;
     using Microsoft.EntityFrameworkCore;
     using Microsoft.Extensions.Configuration;
     using Microsoft.Extensions.DependencyInjection;
     using Microsoft.Extensions.Logging;
    
     namespace Address.IntegrationTest
     {
         public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
         {
             protected override void ConfigureWebHost(IWebHostBuilder builder)
             {
                 string projectDir = Directory.GetCurrentDirectory();
                 string configPath = Path.Combine(projectDir, "appsettings.json");
                 builder.ConfigureAppConfiguration((context, conf) =>
                 {
                     conf.AddJsonFile(configPath);
                 });
    
                 builder.ConfigureServices(services =>
                 {
                     ServiceDescriptor descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<AddressContext>));
    
                     if (descriptor != null)
                     {
                         services.Remove(descriptor);
                     }
    
                     services.AddDbContextPool<AddressContext>((options, context) =>
                     {
                         //var configuration = options.GetRequiredService<IConfiguration>();
                         //string connectionString = configuration.GetConnectionString("TestAddressDb");
                         //context.UseMySql(connectionString);
                         context.UseInMemoryDatabase("InMemoryDbForTesting");
    
                     });
    
                     // Build the service provider.
                     ServiceProvider sp = services.BuildServiceProvider();
                     // Create a scope to obtain a reference to the database
                     // context (ApplicationDbContext).
                     using IServiceScope scope = sp.CreateScope();
                     IServiceProvider scopedServices = scope.ServiceProvider;
                     var db = scopedServices.GetRequiredService<AddressContext>();
                     var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
    
                     // Ensure the database is created.
                     db.Database.EnsureCreated();
    
                     try
                     {
                         // Seed the database with test data.
                         Utilities.ReinitializeDbForTests(db);
                     }
                     catch (Exception ex)
                     {
                         logger.LogError(ex, "An error occurred seeding the " + "database with test messages. Error: {Message}", ex.Message);
                     }
                 });
             }
         }
     }
    • 這裏為什麼要添加 CustomWebApplicationFactory 呢?
      WebApplicationFactory 是用於在內存中引導應用程序進行端到端功能測試的工廠。通過引入自定義 CustomWebApplicationFactory 類重寫 ConfigureWebHost 方法,我們可以重寫我們在 StartUp 中定義的內容,換句話說我們可以在測試環境中使用正式環境的配置,同時可以重寫,例如:數據庫配置,數據初始化等等。
    • 如何準備測試數據?
      我們可以使用數據種子的方式加入數據,數據種子可以針對每個集成測試做數據準備。
    • 除了內存數據庫,還可以使用其他數據庫進行測試嗎?
      可以。
  4. 添加集成測試 AddressControllerIntegrationTest 類。

     using System.Collections.Generic;
     using System.Linq;
     using System.Net.Http;
     using System.Threading.Tasks;
     using Address.Api;
     using Microsoft.AspNetCore.Mvc.Testing;
     using Newtonsoft.Json;
     using Xunit;
    
     namespace Address.IntegrationTest
     {
         public class AddressControllerIntegrationTest : IClassFixture<CustomWebApplicationFactory<Startup>>
         {
             public AddressControllerIntegrationTest(CustomWebApplicationFactory<Startup> factory)
             {
                 _client = factory.CreateClient(new WebApplicationFactoryClientOptions
                 {
                     AllowAutoRedirect = false
                 });
             }
    
             private readonly HttpClient _client;
    
             [Fact]
             public async Task Get_AllAddressAndRetrieveAddress()
             {
                 const string allAddressUri = "/api/Address/GetAll";
                 HttpResponseMessage allAddressesHttpResponse = await _client.GetAsync(allAddressUri);
    
                 allAddressesHttpResponse.EnsureSuccessStatusCode();
    
                 string allAddressStringResponse = await allAddressesHttpResponse.Content.ReadAsStringAsync();
                 var addresses = JsonConvert.DeserializeObject<IList<AddressDto.AddressDto>>(allAddressStringResponse);
                 Assert.Equal(3, addresses.Count);
    
                 AddressDto.AddressDto address = addresses.First();
                 string retrieveUri = $"/api/Address/Retrieve?id={address.ID}";
                 HttpResponseMessage addressHttpResponse = await _client.GetAsync(retrieveUri);
    
                 // Must be successful.
                 addressHttpResponse.EnsureSuccessStatusCode();
    
                 // Deserialize and examine results.
                 string addressStringResponse = await addressHttpResponse.Content.ReadAsStringAsync();
                 var addressResult = JsonConvert.DeserializeObject<AddressDto.AddressDto>(addressStringResponse);
                 Assert.Equal(address.ID, addressResult.ID);
                 Assert.Equal(address.Province, addressResult.Province);
                 Assert.Equal(address.City, addressResult.City);
                 Assert.Equal(address.County, addressResult.County);
             }
         }
     }
  5. 在測試資源管理器中運行集成測試方法。

  6. 結果。

  7. 至此,集成測試完成。需要注意的是,集成測試往往耗時比較多,所以建議能使用單元測試時就不要使用集成測試。

總結:當我們寫單元測試時,一般不會同時存在 Stub 和 Mock 兩種模擬對象,當同時出現這兩種對象時,表明單元測試寫的不合理,或者業務寫的太過龐大,同時,我們可以通過單元測試驅動業務代碼重構。當需要重構時,我們應盡量完成重構,不要留下欠下過多技術債務。集成測試有自身的複雜度存在,我們不要節約時間而打破單一職責原則,否則會引發不可預期後果。為了應對業務修改,我們應該在業務修改以後,進行回歸測試,回歸測試主要關注被修改的業務部分,同時測試用例如果有沒要可以重寫,運行整個和修改業務有關的測試用例集。

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

【其他文章推薦】

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

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

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

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

variable precision SWAR算法

      計算二進制形式中1的數量這種問題,在各種刷題網站上比較常見,以往都是選擇最笨的遍歷方法“矇混”過關。在了解Redis的過程中接觸到了variable precision SWAR算法(以下簡稱VP-SWAR算法),算法異常簡潔,是目前已知的同類方法中最快的。但如果對於位運算不是很熟悉的話,卻不一定容易理解,所以有必要記錄一下。

      下面先看看VP-SWAR算法的完整實現,然後再逐行解釋。

  public int vpSWAR(int i){
    i = (i & 0x55555555) + ((i>>1) & 0x55555555);
    i = (i & 0x33333333) + ((i>>2) & 0x33333333);
    i = (i & 0x0F0F0F0F) + ((i>>4) & 0x0F0F0F0F);
    i = (i * 0x01010101) >> 24;
    return i;
  }

      VP-SWAR算法分為四步,第一步

i = (i & 0x55555555) + ((i>>1) & 0x55555555);

      第一步的作用是計算每兩位為一組的二進制形式包含1的個數。要理解這句話,我們需要從二進制的角度看看到底發生了什麼。首先, 0x55555555 的二進製表示為 0101 0101 0101 0101 0101 0101 0101 0101 ,這個数字的規律是基數位為1,偶數位為0。為簡單起見,我們只考慮兩位,總共有四種情況,即:

 

i b i & b 結果
00 01 00
01 01 01
10 01 00
11 01 01

       觀察發現, i & (0b01) 是i的基數位對應b的1位,i的偶數位對應着b的0位, i & (0b01) 的結果會將I的偶數位置為0,而基數位保持不變,得到的結果就是i的基數位包含1的個數。 (i >> 1) & 0x55555555 先將i右移一位,也就是將i的基數位對應b的0位,i的偶數位對應着b的1位,然後再與 0x55555555 按位與,計算出來的是i的偶數位包含1的個數。兩個計算結果相加就得到i每兩位為一組中包含的1的數量,我們最後需要的就是這每兩位一組的和。

      第二步是在第一步的基礎上,計算每四位為一組包含1的個數。按照每2位為一組分組用到了 0x55555555 這個數,那麼自然的,按照每4位為一組分組自然就需要 0b0011 這種形式,這就是使用 0x33333333 的原因。理論上, i & (0b0011) 總共有16種情況,但是四位二進制位最多包含4個1,用二進製表示為 0b0100 ,所以經過第一步之後,i最多有5種取值,如下:

i b i & b 結果
0000 0011 0000
0001 0011 0001
0010 0011 0010
0011 0011 0011
0100 0011 0000

 

      觀察發現, i & (0b0011) 得到的是i的低兩位包含的1的個數,  (i >> 2) & 0b0011 )得到的是i的高兩位包含的1的個數,兩個結果相加得到每四位包含的1的個數。注意,這裏並不是說任何數與 0b0011 按位與得到的都是低兩位包含的1的個數,這裏的前提是第一步的計算,因為經過第一步計算之後,每兩位包含多少個1已經記錄了下來,再和 0b0011 按位與才得到正確的結果。例如, 0x0010 & 0x 0011=0x0010 ,但是我們不能說 0x0010 包含兩個1,但是如果 0x0010 是經過第一步的計算得來,那才說明 0x0010 記錄原始數據低兩位有兩個1。

      第三步在第二步基礎上,計算每8位有多少個1,由 0x010x0011 ,很自然想到 0x00001111 ,其對應的32位的十六進制數就是 0x0F0F0F0F

      第四步就很有意思了,它不再是計算每16位包含1的個數,而是直接計算32位包含1的個數。對於32位的數來說,可以將其按每8位一組分為4組,分別用ABCD表示,例如 0x01020304 用這種形式表示為:

 

 

      假設 0x01020304 是經過前三步計算之後得到的結果,那麼要計算其總共包含多少個1,只需計算A+B+C+D。而ABCD表示的是不同的位區間範圍,不能直接相加,該如何快速計算A+B+C+D的值呢?這裏又用到了移位運算,將B、C、D分別左移8位、16位、24位,使其分別與A對齊:

 

       我們發現,將数字i分別左移0位、8位、16位、24位然後相加的結果,就是 i * 0x01010101 ,因為 i + (i << 8) + (i << 16) + (i << 24) = i * (1 + 1 << 8 + 1 << 16 + 1 << 24) = i * 0x01010101 。對於32位数字來說,左移之後超過32位的部分會被捨棄,低位補0,將左移之後得到的四個数字相加,結果的高8位的值就是原32位數包含的1的個數,要得到這個值,只需要將結果右移24位,將值放在低8位即可。

      到這裏,整個算法就結束了,右移的結果就是1的數量。在Redis中,BITCOUNT命令同時使用了查表法和VP-SWAR這兩種方法。當要計算的位數小於128位時,使用查表法,否則使用VP-SWAR算法。其中查表法的做法是,程序先存一個256長度的表,按順序記錄從0-255(即 0b00000000 – 0b11111111) 數中二進制1的個數,然後對於輸入參數每8位查一次表。

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

【其他文章推薦】

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

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

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

SQlALchemy session詳解

系列文章:

概念

session用於創建程序和數據庫之間的會話,所有對象的載入和保存都需通過session對象 。

通過sessionmaker調用創建一個工廠,並關聯Engine以確保每個session都可以使用該Engine連接資源:

from sqlalchemy.orm import sessionmaker

# 創建session
DbSession = sessionmaker(bind=engine)
session = DbSession()

操作

session的常見操作方法包括:

  1. flush:預提交,提交到數據庫文件,還未寫入數據庫文件中
  2. commit:提交了一個事務,把內存的數據直接寫入數據庫
  3. rollback:回滾
  4. close:關閉

在事務處理時,需注意一下兩點:

  1. 在事務處理過程發生異常時,進行rollback操作,否則會在下次操作時報錯:
Can’t reconnect until invalid transaction is rolled back 
  1. 一般情況下,在一個事務處理完成之後要關閉session,以確保數據操作的準確性。

建議封裝上下文方法:

from contextlib import contextmanager

@contextmanager
def session_maker(session=session):
    try:
        yield session
        session.commit()
    except:
        session.rollback()
        raise
    finally:
        session.close()

調用:

def update_user():
    with session_maker() as db_session:
        db_session.query(Users).filter_by(name='test2').update({'email': 'test2@qq.com'})

線程安全

session不是線程安全的,並且我們一般session對象都是全局的,那麼在多線程情況下,當多個線程共享一個session時,數據處理就會發生錯誤。

為了保證線程安全,需使用scoped_session方法:

db_session = scoped_session(sessionmaker(bind=engine))

內部原理

session對象包含了三個重要的部分:

  1. 標識映射(Identity Map)
  2. 對象的狀態 / 狀態跟蹤
  3. 事務

標識映射

標識映射是與ORM關聯的集合,通過標識映射保證了數據庫操作的準確性。

具體的實現原理是:維護一個Python字典(IdentityMap),關聯這個Session對象到數據庫ID的映射,當應用程序想要獲取一個session對象時,若該對象不存在,標識映射會加載該對象並緩存,若該對象已存在,則直接獲取。這樣的好處是:

  1. 已經被請求過的session對象緩存下來,不需要連接加載多次,造成額外的開銷;
  2. 避免了數據不一致

狀態跟蹤

一個Session對象從創建到銷毀,依次經歷四種狀態,分別是:

  1. Transient:剛new出來的對象,還不在會話中,也沒有保存到數據庫。
  2. Pending:transient的對象調用add后,就會變成pending狀態,這時會加入sqlalchemy的監管範圍,數據並未更新到數據庫。
  3. Persistent:該狀態表明數據庫里已經記錄了該對象,在兩種情況下對象處於該狀態:一是通過flush()方法刷新pending對象,二是從數據庫query()得到對象。
  4. Detached:在會話中的事務提交之後,所有的對象都將是Detached狀態。

所謂的狀態跟蹤,就是跟蹤以上四個狀態,保證數據的準確性並在合理的時機丟棄對象以保證合理開銷,那麼具體是怎麼實現的呢?

我們可以看到,只有在pending狀態時,對象的內存數據和數據庫中的數據不一致,在Persistent狀態時,內存數據和數據庫數據已經一致,那麼此後任意時刻丟棄該對象數據都是可以的,這時就需要找個合適的時機丟棄對象,過早或過晚都有其缺陷。於是,就讓垃圾回收器來做決定,在內存不夠的時候釋放對象,回收內存。

Session對象採用了弱引用機制,所謂弱引用,就是說,在保存了對象的引用的情況下,對象仍然可能被垃圾回收器回收。在某一時刻通過引用訪問對象時,對象可能存在也可能不存在,如果對象不存在,就重新從數據庫中加載對象。而如果不希望對象被回收,只需要另外保存一個對象的強引用即可 。

session對象包括三個屬性:

  1. new:剛加入會話的對象
  2. dirty:剛被修改的對象
  3. deleted:在會話中被刪除的對象

三個屬性共同的特點就是內存的數據和數據庫數據不一致,也就是對象處於pending狀態,這也就表明了session保存了所有對象處於pending狀態的強引用。

以上。

代碼可參照:

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

【其他文章推薦】

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

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

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

spring 是如何注入對象的和bean 創建過程分析

文章目錄:

  1. 【本文】spring 是如何注入對象的

首先需要知道一個大致實現

  • 這個注入過程肯定是在 BeanPostProcessor 中實現的

  • spring 是在 beanFactory.getBean 進行 bean 實例化的,即懶加載

  • 根據第二條,也就是說在 getBean 的時候才會去調用所有 BeanPostProcessor

  • 第二篇文章說到,BeanFactory 的 refresh 過程只是註冊 BeanPostProcessor,真正執行在 getBean 方法中

  • MergedBeanDefinitionPostProcessor 也是一種 BeanPostProcessor 它重新弄了個一個生命周期函數,替代了 BeanPostProcessor 默認的生命周期函數,這麼看吧,我貼一小段源碼

    for (BeanPostProcessor bp : getBeanPostProcessors()) {
        if (bp instanceof MergedBeanDefinitionPostProcessor) {
            MergedBeanDefinitionPostProcessor bdp = (MergedBeanDefinitionPostProcessor) bp;
            bdp.postProcessMergedBeanDefinition(mbd, beanType, beanName);
        }
    }

    它允許你在非 BeanFactoryProcess 中去修改 Bean 定義

  • InstantiationAwareBeanPostProcessor 也是一種 BeanPostProcessor 它也重新定義了一個生命周期函數,它允許把屬性值注入到屬性對象中

@Autowired 加載定義的過程

我們先不看 bean 的創建過程,就看 MergedBeanDefinitionPostProcessor 的實現子類,這裏看名字猜測 AutowiredAnnotationBeanPostProcessor 應該就是干這件事的,所以我們接下來可以直接看 AutowiredAnnotationBeanPostProcessor 的 postProcessMergedBeanDefinition 方法的代碼。

順着方法的調用,可以知道在 buildAutowiringMetadata 是真正查找這些註解的地方,最後 checkConfigMembersMember 註冊進了 bean 定義,具體如何查找的讀者自行查看源碼。

這裏只是將 Member 註冊進了 bean 定義,真正實例化在填充 Bean 的過程中,下面說到 bean 的創建過程可以知道是何時注入的。

Bean 的創建過程

前面說到 spring 是在 getBean 的過程中進行 Bean 創建的,創建 bean 分為幾個步驟

  1. 獲取 bean 定義
  2. new Bean()
  3. 執行生命周期函數 (前)
  4. 創建依賴項
  5. 填充 bean
  6. 執行生命周期函數(后)

入口為 BeanFactory.getBean ,BeanFactory 的實現類為 DefaultListableBeanFactory 這些你可以在 BeanFactory 的 refresh 過程中找到

根據源碼,如果 bean 還不存在時,就會執行 bean 的創建流程

獲取 bean 定義在這段源碼中

final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);

緊跟着,根據 Bean 定義搜索其依賴項,並創建 bean ,可以看出是遞歸創建 bean

String[] dependsOn = mbd.getDependsOn();
for (String dep : dependsOn) {
    getBean(dep);
}

然後就創建 bean 了

if (mbd.isSingleton()) {
    createBean(beanName, mbd, args);
}

// 真正的執行在 doCreateBean 過程中
Object beanInstance = doCreateBean(beanName, mbdToUse, args);

創建 bean 第一步 new Bean

if (instanceWrapper == null) {
    instanceWrapper = createBeanInstance(beanName, mbd, args);
}

創建 bean 第二步,執行所有的 processor ,包含 MergedBeanDefinitionPostProcessor ,所以在這一步註冊注入選項

applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);

創建 bean 第三步,填充 bean ,這裏做的 @Autowired 注入

populateBean(beanName, mbd, instanceWrapper);

最終的處理過程在 AutowiredAnnotationBeanPostProcessor 的 postProcessPropertyValues 函數中

metadata.inject(bean, beanName, pvs);

因為在前面已經獲取過依賴項,並且把其丟進了容器,所以這裡是直接用反射寫進去就可以了

創建 bean 第四步,初始化 bean ,這裡有一個方法注入,方法注入原來發生在初始化 bean 過程中,還有就是生命周期函數執行了,包含 BeanPostProcessor 的前置後置生命周期,初始化方法等

小說明 :AutowiredAnnotationBeanPostProcessor 即是 一個 MergedBeanDefinitionPostProcessor 也是一個 InstantiationAwareBeanPostProcessor

一點小推廣

創作不易,希望可以支持下我的開源軟件,及我的小工具,歡迎來 gitee 點星,fork ,提 bug 。

Excel 通用導入導出,支持 Excel 公式
博客地址:
gitee:

使用模板代碼 ,從數據庫生成代碼 ,及一些項目中經常可以用到的小工具
博客地址:
gitee:

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

【其他文章推薦】

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

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

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

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

《中國電動汽車標準化工作線圖》將於近日發佈

1月11日,《中國電動汽車標準化工作路線圖》評審會在北京召開。《路線圖》由中國汽車技術研究中心等單位聯合制定,將於近日發佈。

路線圖的定位

本版路線圖的起止階段為2015年至2025年,通過對2015年至2025年電動汽車及其相關產業的技術發展、產業化和市場化發展的判斷和預測,制定2015年至2025年中國電動汽車標準化工作的具體目標和工作計畫。在對國內外電動汽車標準和法規進行總結和歸納的基礎上,著重分析和研究中國電動汽車在未來的10年中,電動汽車及其相關產業的技術發展、產業化和市場化發展的標準需求,以此作為中國電動汽車快速健康發展的主要技術支撐。路線圖中標準化工作實施期限分為:緊急(2015年1月至2015年12月)、短期(2016年1月至2017年12月)、中期(2018年1月至2020年12月)和長期(2021年1月至2025年12月)共4個階段。由上述內容構成的本路線圖,將成為指導從2015年至2025年中國電動汽車標準化工作的綱領性檔。

路線圖的範圍

a)電動車輛:純電動汽車、混合動力電動汽車(包括插電式和增程式)、燃料電池電動汽車以及電動車輛相關零部件等;

b)基礎設施:充電樁、充電站、換電站和加氫站等;

c)相關產業:電動汽車及電池的運輸、回收利用、教育培訓、搶險救援等。

d) 本路線圖覆蓋基礎設施和相關產業的建設、運營、管理等。

路線圖的目標

路線圖的目標是有目的、有計劃、有步驟地建立起聯繫緊密、相互協調、層次分明、構成合理、相互支持、滿足應用需求的整體性、系統性、開放性的電動汽車標準體系,加速制定一系列由標準體系確定的具體標準,從而降低研究、生產、使用、維護及管理的成本和風險,使標準化工作發揮最佳效益,支撐和引導我國電動汽車產業的技術創新和快速發展,即:

——提出系統、科學的電動汽車標準體系;

——提出未來十年需要制修訂的標準專案;

——提出我國電動汽車標準及體系建設的實施步驟;

——促進科技創新;

——改變能源結構和推動節能減排;

——促進電動汽車大規模產業化;

——滿足科研、產業化、市場化運行和政府管理的需要;

——積極參與國際標準化工作,爭取中國標準走向世界。

路線圖的實施

通過對國內外電動汽車標準化工作的現狀的分析,為了滿足電動汽車產業的發展,未來標準的缺項進行了梳理,路線圖將通過表格和圖形的形式,對標準缺項進行系統的整理,按照時間序列來闡述從2015年1月至2025年12月共11年的標準制定計劃,計畫為4個階段(優先順序),分為緊急(2015年1月至2015年12月)、短期(2106年1月至2017年12月、中期(2018年1月至2020年12月)、長期(2021年1月至2025年12月)。

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

【其他文章推薦】

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

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

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

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

重慶新建住宅將100%配備充電基礎設施

1月11日,重慶市政府辦公廳公佈《重慶市加快電動汽車充電基礎設施建設實施方案》。方案要求,新建住宅配建的停車庫必須100%建設電動汽車充電基礎設施或預留建設安裝條件。到2020年,主城區將累計建成不少於30座公共充換電站。

在主城區,凡新建的交通樞紐、超市賣場、商務樓宇,黨政機關、企事業單位辦公場所、學校、醫院、文化體育場館以及獨立用地的公共停車場、停車換乘(P+R)停車場等,按照不低於總停車位數量10%的比例,建設電動汽車充電基礎設施。

對已建成的住宅社區及上述場所,要通過改造、加裝等方式,到2020年,達到不低於10%的比例,提供充電基礎設施。

其他區縣(自治縣)城區要因地制宜,同樣按照不低於總停車位數量10%的比例,建設電動汽車充電基礎設施或預留建設安裝條件(包括預埋電力管線和預留電力容量)。

此外,還將建設電動汽車公共充換電站。到2020年,主城區原則上按服務半徑每1公里提供1座公共充換電站,累計建成不少於30座公共充換電站;其他每個區縣(自治縣)城區至少建成1座公共充換電站;每個重點旅遊景區至少建成1-2座公共充換電站;凡具備安全條件的加油站、加氣站、高速公路服務區等實現充換電設施全覆蓋。

不同場所執行不同電價 住宅區按合表用戶電價收費

對向電力企業直接報裝接電的經營性集中式電動汽車充換電設施用電,執行大工業用電價格,2020年前暫免收取基本電費;其他充電基礎設施用電按其所在場所執行分類目錄電價。其中,向電力企業報裝的居民住宅區充電基礎設施用電,執行居民用電價格中的合表用戶電價;對居民自用充電基礎設施的用電與家庭用電分表計量,執行居民生活用電價格,且不納入居民生活用電階梯電價計算範圍;黨政機關、企事業單位和社會公共停車場等場所的充電基礎設施用電,執行一般工商業及其他類用電價格。電動汽車充電基礎設施用電按重慶市峰穀分時電價相關政策執行。

電動汽車充電服務企業向使用者收取的充電服務費執行政府指導價,針對不同類別充電基礎設施,以電價為計費依據,服務費暫按每千瓦時不超過執行電價的50%收取,試行一年;試行期滿後,結合市場發展情況,逐步放開充電服務費,由市場競爭形成價格。

此外,在安裝充電設施時,也將簡化審批手續。個人在自有停車庫、停車位元,各居住區、單位在已有停車泊位安裝電動汽車充電設施的,無需辦理建設用地規劃許可證、建設工程規劃許可證和施工許可證。

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

【其他文章推薦】

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

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

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

js數組方法大全(下)

# js數組方法大全(下)

記錄一下整理的js數組方法,免得每次要找方法都找不到。圖片有點多,注意流量,嘻嘻!

本期分享

  • forEach()
  • map()
  • filer()
  • every()
  • some()
  • reduce()
  • reduceRight()
  • indexOf()
  • lastIndex()

上期分享

  • join()
  • reverse()
  • sort()
  • concat()
  • slice()
  • splice()
  • push()
  • pop()
  • unshift()
  • shift()
  • toString()
  • toLocaleString()

forEach() —>遍歷

  • 使用熱度:經常用
  • 是否改變原始數組:否
  • 返回:無
  • 參數:
參數位置 參數類型 是否必選 說明
1 function 三個參數分別是:數組元素、元素的索引、數組本身
  • 說明:該方法無法提前終止運行,如果要提前終止運行,只能使用try塊中,然後拋出一個異常。
  • 小技巧:如果數組是個數組對象形式可以直接操作數組元素改變原始數組本身,因為對象是個引用數據類型嘛!
  • 實例如下:
var log=console.log;
var data=[1,2,3,4,5];
var sum =0;
data.forEach(value=>{
  sum+=value;
})
log(sum);

data.forEach((v,i,a)=>{
  a[i]=v+1;
})
log(data);

var data_post=[{a:1},{a:2}]
data_post.forEach(value=>{
  value.a++;
})
log(data_post)

map() —>映射

  • 使用熱度:經常用
  • 是否改變原始數組:否
  • 返回:返回一個新函數
  • 參數:
參數位置 參數類型 是否必選 說明
1 function 三個參數分別是:數組元素、元素的索引、數組本身
  • 說明:傳遞給map函數應該有返回值,返回值是新數組的元素。
  • 實例如下:
var log=console.log;
var data=[1,2,3,4,5];
var b= data.map(x=>{
  return x*x;
})
log(b)

filter() —>過濾

  • 使用熱度:常用
  • 是否改變原始數組:否
  • 返回:返回過濾后的數組
  • 參數:
參數位置 參數類型 是否必選 說明
1 function 三個參數分別是:數組元素、元素的索引、數組本身
  • 說明:如果返回值是true或者可以轉化為true的值,那麼這個值就是新數組的元素。
  • 實例如下:
var log=console.log;
var data=[1,2,3,4,5];
var b= data.filter(x=>{
  return x<3;
})
log(b)

every() —>檢測

  • 使用熱度:不常用
  • 是否改變原始數組:否
  • 返回:true或者false
  • 參數:
參數位置 參數類型 是否必選 說明
1 function 三個參數分別是:數組元素、元素的索引、數組本身
  • 說明:當且僅當針對數組中的所有元素調用綁定函數都返回true時,它才返回true。
  • 注意:一旦every或者some已經確定了改返回什麼值得時候就不會遍曆數組了。根據數學上的慣例,在空數組調用時,every返回true,some返回false。
  • 實例如下:
var log=console.log;
var data=[1,2,3,4,5];
var b= data.every(x=>{
  return x<10;
})
log(b)

var c= data.every(x=>{
  return x%2===0;
})
log(c)

some() —>檢測

  • 使用熱度:不常用
  • 是否改變原始數組:否
  • 返回:true或者false
  • 參數:
參數位置 參數類型 是否必選 說明
1 function 三個參數分別是:數組元素、元素的索引、數組本身
  • 說明:當數組中至少有一個元素調用綁定函數返回true時,它就返回true。
  • 注意:一旦every或者some已經確定了改返回什麼值得時候就不會遍曆數組了。根據數學上的慣例,在空數組調用時,every返回true,some返回false。
  • 實例如下:
var log=console.log;
var data=[1,2,3,4,5];
var b= data.some(x=>{
  return x>10;
})
log(b)

var c= data.some(x=>{
  return x%2===0;
})
log(c)

reduce() —>簡化

  • 使用熱度:不常用
  • 是否改變原始數組:否
  • 返回:返回一個值
  • 參數:
參數位置 參數類型 是否必選 作用
1 function 四個參數分別是:初始化值/數組元素、數組元素、元素的索引、數組本身
2 number 供計算的初始化值
  • 說明:第一個參數是到目前為止的簡化操作累計的結果
  • 注意:如果沒有初始化值,第一次調用函數的第一個參數就是第一個數組元素,第二個參數則是第二個數組元素。如果有初始化值,第一次調用函數的第一個參數就是初始化值,二個參數則是第一個數組元素。
  • 實例如下:
var log=console.log;
var data=[1,2,3,4,5];
var b= data.reduce((x,y)=>{
  return x+y;
},0)
log(b)

var c= data.reduce((x,y)=>{
  return x*y;
},1)
log(c)

var d= data.reduce((x,y)=>{
  return x>y?x:y;
},1)
log(d)

reduceRight() —>簡化

  • 使用熱度:不常用
  • 是否改變原始數組:否
  • 返回:返回一個值
  • 參數:
參數位置 參數類型 是否必選 作用
1 function 四個參數分別是:初始化值/數組元素、數組元素、元素的索引、數組本身
2 number 供計算的初始化值
  • 說明:第一個參數是到目前為止的簡化操作累計的結果。不同於reduce這個僅僅是從右到左計算。
  • 注意:如果沒有初始化值,第一次調用函數的第一個參數就是第一個數組元素,第二個參數則是第二個數組元素。如果有初始化值,第一次調用函數的第一個參數就是初始化值,二個參數則是第一個數組元素。
  • 實例如下:
var log=console.log;
var data=[1,2,3,4,5];
data.reduceRight((x,y)=>{
  log(y)
  return x+y;
},0)

indexOf() —>搜索

  • 使用熱度:經常用
  • 是否改變原始數組:否
  • 返回:返回數組索引或者-1
  • 參數:
參數位置 參數類型 是否必選 作用
1 * 要搜索的數組元素
2 number 從數組哪個索引開始搜索
  • 說明:如果能搜索到結果將返回第一個索引,如果搜索不到就返回-1
  • 注意:如果第二個參數是負數,指的是從數組元素末尾的偏移量位置開始向後搜索,而不是到偏移量位置就截止搜索了,這裡是很容易和splice弄混的。
  • 實例如下:
var log=console.log;
var data=[1,2,3,4,5];
var b=data.indexOf(1,-5);
log(b);

lastIndexOf() —>搜索

  • 使用熱度:經常用
  • 是否改變原始數組:否
  • 返回:返回數組索引或者-1
  • 參數:
參數位置 參數類型 是否必選 作用
1 * 要搜索的數組元素
2 number 從數組哪個索引開始搜索
  • 說明:和indexOf不同的是lashIndexOf是反向搜索
  • 注意:因為是反向搜索,當第二個參數是負數的時候,就是指從末尾偏移量的位置向前搜索
  • 實例如下:
var log=console.log;
var data=[1,2,3,2,5];
var b=data.lastIndexOf(2,-3)
log(b)

var c=data.lastIndexOf(1,-5)
log(c)

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

【其他文章推薦】

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

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

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

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

6. SOFAJRaft源碼分析— 透過RheaKV看線性一致性讀

開篇

其實這篇文章我本來想在講完選舉的時候就開始講線性一致性讀的,但是感覺直接講沒頭沒尾的看起來比比較困難,所以就有了RheaKV的系列,這是RheaKV,終於可以講一下SOFAJRaft的線性一致性讀是怎麼做到了的。所謂線性一致性,一個簡單的例子是在 T1 的時間寫入一個值,那麼在 T1 之後讀一定能讀到這個值,不可能讀到 T1 之前的值。

其中部分內容參考SOFAJRaft文檔:

RheaKV讀取數據

RheaKV的讀取數據的入口是DefaultRheaKVStore的bGet。

DefaultRheaKVStore#bGet

public byte[] bGet(final String key) {
    return FutureHelper.get(get(key), this.futureTimeoutMillis);
}

bGet方法中會一直調用到DefaultRheaKVStore的一個get方法中:
DefaultRheaKVStore#get

private CompletableFuture<byte[]> get(final byte[] key, final boolean readOnlySafe,
                                      final CompletableFuture<byte[]> future, final boolean tryBatching) {
    //校驗started狀態
    checkState();
    Requires.requireNonNull(key, "key");
    if (tryBatching) {
        final GetBatching getBatching = readOnlySafe ? this.getBatchingOnlySafe : this.getBatching;
        if (getBatching != null && getBatching.apply(key, future)) {
            return future;
        }
    }
    internalGet(key, readOnlySafe, future, this.failoverRetries, null, this.onlyLeaderRead);
    return future;
}

get方法會根據傳入的參數來判斷是否採用批處理的方式來讀取數據,readOnlySafe表示是否開啟線程一致性讀,由於我們調用的是get方法,所以readOnlySafe和tryBatching都會返回true。
所以這裡會調用getBatchingOnlySafe的apply方法,將key和future傳入。
getBatchingOnlySafe是在我們初始化DefaultRheaKVStore的時候初始化的:
DefaultRheaKVStore#init

.....
this.getBatchingOnlySafe = new GetBatching(KeyEvent::new, "get_batching_only_safe",
        new GetBatchingHandler("get_only_safe", true));
.....

在初始化getBatchingOnlySafe的時候傳入的處理器是GetBatchingHandler。

然後我們回到getBatchingOnlySafe#apply中,看看這個方法做了什麼:

public boolean apply(final byte[] message, final CompletableFuture<byte[]> future) {
    //GetBatchingHandler
    return this.ringBuffer.tryPublishEvent((event, sequence) -> {
        event.reset();
        event.key = message;
        event.future = future;
    });
}

apply方法會向Disruptor發送一個事件進行異步處理,並把我們的key封裝到event的key中。getBatchingOnlySafe的處理器是GetBatchingHandler。

批量獲取數據

GetBatchingHandler#onEvent

public void onEvent(final KeyEvent event, final long sequence, final boolean endOfBatch) throws Exception {
    this.events.add(event);
    this.cachedBytes += event.key.length;
    final int size = this.events.size();
    //校驗一下數據量,沒有達到MaxReadBytes並且不是最後一個event,那麼直接返回
    if (!endOfBatch && size < batchingOpts.getBatchSize() && this.cachedBytes < batchingOpts.getMaxReadBytes()) {
        return;
    }

    if (size == 1) {
        reset();
        try {
            //如果只是一個get請求,那麼不需要進行批量處理
            get(event.key, this.readOnlySafe, event.future, false);
        } catch (final Throwable t) {
            exceptionally(t, event.future);
        }
    } else {
        //初始化一個剛剛好大小的集合
        final List<byte[]> keys = Lists.newArrayListWithCapacity(size);
        final CompletableFuture<byte[]>[] futures = new CompletableFuture[size];
        for (int i = 0; i < size; i++) {
            final KeyEvent e = this.events.get(i);
            keys.add(e.key);
            futures[i] = e.future;
        }
        //遍歷完events數據到entries之後,重置
        reset();
        try {
            multiGet(keys, this.readOnlySafe).whenComplete((result, throwable) -> {
                //異步回調處理數據
                if (throwable == null) {
                    for (int i = 0; i < futures.length; i++) {
                        final ByteArray realKey = ByteArray.wrap(keys.get(i));
                        futures[i].complete(result.get(realKey));
                    }
                    return;
                }
                exceptionally(throwable, futures);
            });
        } catch (final Throwable t) {
            exceptionally(t, futures);
        }
    }
}
}

onEvent方法首先會校驗一下當前的event數量有沒有達到閾值以及當前的event是不是Disruptor中最後一個event;然後會根據不同的events集合中的數量來走不同的實現,這裏做了一個優化,如果是只有一條數據那麼不會走批處理;最後將所有的key放入到keys集合中並調用multiGet進行批處理。

multiGet方法會調用internalMultiGet返回一個Future,從而實現異步的返回結果。
DefaultRheaKVStore#internalMultiGet

private FutureGroup<Map<ByteArray, byte[]>> internalMultiGet(final List<byte[]> keys, final boolean readOnlySafe,
                                                             final int retriesLeft, final Throwable lastCause) {
    //因為不同的key是存放在不同的region中的,所以一個region會對應多個key,封裝到map中
    final Map<Region, List<byte[]>> regionMap = this.pdClient
            .findRegionsByKeys(keys, ApiExceptionHelper.isInvalidEpoch(lastCause));
    //返回值
    final List<CompletableFuture<Map<ByteArray, byte[]>>> futures =
            Lists.newArrayListWithCapacity(regionMap.size());
    //lastCause傳入為null
    final Errors lastError = lastCause == null ? null : Errors.forException(lastCause);

    for (final Map.Entry<Region, List<byte[]>> entry : regionMap.entrySet()) {
        final Region region = entry.getKey();
        final List<byte[]> subKeys = entry.getValue();
        //重試次數減1,設置一個重試函數
        final RetryCallable<Map<ByteArray, byte[]>> retryCallable = retryCause -> internalMultiGet(subKeys,
                readOnlySafe, retriesLeft - 1, retryCause);
        final MapFailoverFuture<ByteArray, byte[]> future = new MapFailoverFuture<>(retriesLeft, retryCallable);
        //發送MultiGetRequest請求,獲取數據
        internalRegionMultiGet(region, subKeys, readOnlySafe, future, retriesLeft, lastError, this.onlyLeaderRead);
        futures.add(future);
    }
    return new FutureGroup<>(futures);
}

internalMultiGet里會根據key去組裝region,不同的key會對應不同的region,數據時存在region中的,所以要從不同的region中獲取數據,region和key是一對多的關係所以這裡會封裝成一個map。然後會遍歷regionMap,每個region所對應的數據作為一個批次調用到internalRegionMultiGet方法中,根據不同的情況獲取數據。

DefaultRheaKVStore#internalRegionMultiGet

private void internalRegionMultiGet(final Region region, final List<byte[]> subKeys, final boolean readOnlySafe,
                                    final CompletableFuture<Map<ByteArray, byte[]>> future, final int retriesLeft,
                                    final Errors lastCause, final boolean requireLeader) {
    //因為當前的是client,所以這裡會是null
    final RegionEngine regionEngine = getRegionEngine(region.getId(), requireLeader);
    // require leader on retry
    //設置重試函數
    final RetryRunner retryRunner = retryCause -> internalRegionMultiGet(region, subKeys, readOnlySafe, future,
            retriesLeft - 1, retryCause, true);
    final FailoverClosure<Map<ByteArray, byte[]>> closure = new FailoverClosureImpl<>(future,
            false, retriesLeft, retryRunner);
    if (regionEngine != null) {
        if (ensureOnValidEpoch(region, regionEngine, closure)) {
            //如果不是null,那麼會獲取rawKVStore,並從中獲取數據
            final RawKVStore rawKVStore = getRawKVStore(regionEngine);
            if (this.kvDispatcher == null) {
                rawKVStore.multiGet(subKeys, readOnlySafe, closure);
            } else {
                //如果是kvDispatcher不為空,那麼放入到kvDispatcher中異步執行
                this.kvDispatcher.execute(() -> rawKVStore.multiGet(subKeys, readOnlySafe, closure));
            }
        }
    } else {
        final MultiGetRequest request = new MultiGetRequest();
        request.setKeys(subKeys);
        request.setReadOnlySafe(readOnlySafe);
        request.setRegionId(region.getId());
        request.setRegionEpoch(region.getRegionEpoch());
        //調用rpc請求
        this.rheaKVRpcService.callAsyncWithRpc(request, closure, lastCause, requireLeader);
    }
}

因為我們這裡是client端調用internalRegionMultiGet方法的,所以是沒有設置regionEngine的,那麼會直接向server的當前region所對應的leader節點發送一個MultiGetRequest請求。

因為上面的這些方法基本上和put是一致的,我們已經在講過了,所以這裏不重複的講了。

server端處理MultiGetRequest請求

MultiGetRequest請求會被KVCommandProcessor所處理,KVCommandProcessor里會根據請求的magic方法返回值來判斷是用什麼方式來進行處理。我們這裡會調用到DefaultRegionKVService的handleMultiGetRequest方法中處理請求。

public void handleMultiGetRequest(final MultiGetRequest request,
                                  final RequestProcessClosure<BaseRequest, BaseResponse<?>> closure) {
    final MultiGetResponse response = new MultiGetResponse();
    response.setRegionId(getRegionId());
    response.setRegionEpoch(getRegionEpoch());
    try {
        KVParameterRequires.requireSameEpoch(request, getRegionEpoch());
        final List<byte[]> keys = KVParameterRequires.requireNonEmpty(request.getKeys(), "multiGet.keys");
        //調用MetricsRawKVStore的multiGet方法
        this.rawKVStore.multiGet(keys, request.isReadOnlySafe(), new BaseKVStoreClosure() {

            @SuppressWarnings("unchecked")
            @Override
            public void run(final Status status) {
                if (status.isOk()) {
                    response.setValue((Map<ByteArray, byte[]>) getData());
                } else {
                    setFailure(request, response, status, getError());
                }
                closure.sendResponse(response);
            }
        });
    } catch (final Throwable t) {
        LOG.error("Failed to handle: {}, {}.", request, StackTraceUtil.stackTrace(t));
        response.setError(Errors.forException(t));
        closure.sendResponse(response);
    }
}

handleMultiGetRequest方法會調用MetricsRawKVStore的multiGet方法來批量獲取數據。

MetricsRawKVStore#multiGet

public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
    //實例化MetricsKVClosureAdapter對象
    final KVStoreClosure c = metricsAdapter(closure, MULTI_GET, keys.size(), 0);
    //調用RaftRawKVStore的multiGet方法
    this.rawKVStore.multiGet(keys, readOnlySafe, c);
}

multiGet方法會傳入一個MetricsKVClosureAdapter實例,通過這個實例實現異步回調response。然後調用RaftRawKVStore的multiGet方法。

RaftRawKVStore#multiGet

public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
    if (!readOnlySafe) {
        this.kvStore.multiGet(keys, false, closure);
        return;
    }
    // KV 存儲實現線性一致讀
    // 調用 readIndex 方法,等待回調執行
    this.node.readIndex(BytesUtil.EMPTY_BYTES, new ReadIndexClosure() {

        @Override
        public void run(final Status status, final long index, final byte[] reqCtx) {
            //如果狀態返回成功,
            if (status.isOk()) {
                RaftRawKVStore.this.kvStore.multiGet(keys, true, closure);
                return;
            }
            //readIndex 讀取失敗嘗試應用鍵值讀操作申請任務於 Leader 節點的狀態機 KVStoreStateMachine
            RaftRawKVStore.this.readIndexExecutor.execute(() -> {
                if (isLeader()) {
                    LOG.warn("Fail to [multiGet] with 'ReadIndex': {}, try to applying to the state machine.",
                            status);
                    // If 'read index' read fails, try to applying to the state machine at the leader node
                    applyOperation(KVOperation.createMultiGet(keys), closure);
                } else {
                    LOG.warn("Fail to [multiGet] with 'ReadIndex': {}.", status);
                    // Client will retry to leader node
                    new KVClosureAdapter(closure, null).run(status);
                }
            });
        }
    });
}

multiGet調用node的readIndex方法進行一致性讀操作,並設置回調,如果返回成功那麼就直接調用RocksRawKVStore讀取數據,如果返回不是成功那麼申請任務於 Leader 節點的狀態機 KVStoreStateMachine。

線性一致性讀readIndex

所謂線性一致讀,一個簡單的例子是在 t1 的時刻我們寫入了一個值,那麼在 t1 之後,我們一定能讀到這個值,不可能讀到 t1 之前的舊值(想想 Java 中的 volatile 關鍵字,即線性一致讀就是在分佈式系統中實現 Java volatile 語義)。簡而言之是需要在分佈式環境中實現 Java volatile 語義效果,即當 Client 向集群發起寫操作的請求並且獲得成功響應之後,該寫操作的結果要對所有後來的讀請求可見。和 volatile 的區別在於 volatile 是實現線程之間的可見,而 SOFAJRaft 需要實現 Server 之間的可見。

SOFAJRaft提供的線性一致讀是基於 Raft 協議的 ReadIndex 實現用 ;Node#readIndex(byte [] requestContext, ReadIndexClosure done) 發起線性一致讀請求,當安全讀取時傳入的 Closure 將被調用,正常情況從狀態機中讀取數據返回給客戶端。

Node#readIndex

public void readIndex(final byte[] requestContext, final ReadIndexClosure done) {
    if (this.shutdownLatch != null) {
        //異步執行回調
        Utils.runClosureInThread(done, new Status(RaftError.ENODESHUTDOWN, "Node is shutting down."));
        throw new IllegalStateException("Node is shutting down");
    }
    Requires.requireNonNull(done, "Null closure");
    //EMPTY_BYTES
    this.readOnlyService.addRequest(requestContext, done);
}

readIndex會調用ReadOnlyServiceImpl#addRequest將requestContext和回調方法done傳入,requestContext傳入的是BytesUtil.EMPTY_BYTES
接着往下看

ReadOnlyServiceImpl#addRequest

public void addRequest(final byte[] reqCtx, final ReadIndexClosure closure) {
    if (this.shutdownLatch != null) {
        Utils.runClosureInThread(closure, new Status(RaftError.EHOSTDOWN, "Was stopped"));
        throw new IllegalStateException("Service already shutdown.");
    }
    try {
        EventTranslator<ReadIndexEvent> translator = (event, sequence) -> {
            event.done = closure;
            //EMPTY_BYTES
            event.requestContext = new Bytes(reqCtx);
            event.startTime = Utils.monotonicMs();
        };
        int retryTimes = 0;
        while (true) {
            //ReadIndexEventHandler
            if (this.readIndexQueue.tryPublishEvent(translator)) {
                break;
            } else {
                retryTimes++;
                if (retryTimes > MAX_ADD_REQUEST_RETRY_TIMES) {
                    Utils.runClosureInThread(closure,
                        new Status(RaftError.EBUSY, "Node is busy, has too many read-only requests."));
                    this.nodeMetrics.recordTimes("read-index-overload-times", 1);
                    LOG.warn("Node {} ReadOnlyServiceImpl readIndexQueue is overload.", this.node.getNodeId());
                    return;
                }
                ThreadHelper.onSpinWait();
            }
        }
    } catch (final Exception e) {
        Utils.runClosureInThread(closure, new Status(RaftError.EPERM, "Node is down."));
    }
}

addRequest方法里會將傳入的reqCtx和closure封裝成一個時間,傳入到readIndexQueue隊列中,事件發布成功後會交由ReadIndexEventHandler處理器處理,發布失敗會進行重試,最多重試3次。

ReadIndexEventHandler

private class ReadIndexEventHandler implements EventHandler<ReadIndexEvent> {
    // task list for batch
    private final List<ReadIndexEvent> events = new ArrayList<>(
                                                  ReadOnlyServiceImpl.this.raftOptions.getApplyBatch());

    @Override
    public void onEvent(final ReadIndexEvent newEvent, final long sequence, final boolean endOfBatch)
                                                                                                     throws Exception {
        if (newEvent.shutdownLatch != null) {
            executeReadIndexEvents(this.events);
            this.events.clear();
            newEvent.shutdownLatch.countDown();
            return;
        }

        this.events.add(newEvent);
        //批量執行
        if (this.events.size() >= ReadOnlyServiceImpl.this.raftOptions.getApplyBatch() || endOfBatch) {
            executeReadIndexEvents(this.events);
            this.events.clear();
        }
    }
}

ReadIndexEventHandler是ReadOnlyServiceImpl裏面的內部類,裏面有一個全局的events集合用來做事件的批處理,如果當前的event已經達到了32個或是整個Disruptor隊列里最後一個那麼會調用ReadOnlyServiceImpl的executeReadIndexEvents方法進行事件的批處理。

ReadOnlyServiceImpl#executeReadIndexEvents

private void executeReadIndexEvents(final List<ReadIndexEvent> events) {
    if (events.isEmpty()) {
        return;
    }
    //初始化ReadIndexRequest
    final ReadIndexRequest.Builder rb = ReadIndexRequest.newBuilder() //
        .setGroupId(this.node.getGroupId()) //
        .setServerId(this.node.getServerId().toString());

    final List<ReadIndexState> states = new ArrayList<>(events.size());

    for (final ReadIndexEvent event : events) {
        rb.addEntries(ZeroByteStringHelper.wrap(event.requestContext.get()));
        states.add(new ReadIndexState(event.requestContext, event.done, event.startTime));
    }
    final ReadIndexRequest request = rb.build();

    this.node.handleReadIndexRequest(request, new ReadIndexResponseClosure(states, request));
}

executeReadIndexEvents封裝好ReadIndexRequest請求和將ReadIndexState集合封裝到ReadIndexResponseClosure中,為後續的操作做裝備

NodeImpl#handleReadIndexRequest

public void handleReadIndexRequest(final ReadIndexRequest request, final RpcResponseClosure<ReadIndexResponse> done) {
    final long startMs = Utils.monotonicMs();
    this.readLock.lock();
    try {
        switch (this.state) {
            case STATE_LEADER:
                readLeader(request, ReadIndexResponse.newBuilder(), done);
                break;
            case STATE_FOLLOWER:
                readFollower(request, done);
                break;
            case STATE_TRANSFERRING:
                done.run(new Status(RaftError.EBUSY, "Is transferring leadership."));
                break;
            default:
                done.run(new Status(RaftError.EPERM, "Invalid state for readIndex: %s.", this.state));
                break;
        }
    } finally {
        this.readLock.unlock();
        this.metrics.recordLatency("handle-read-index", Utils.monotonicMs() - startMs);
        this.metrics.recordSize("handle-read-index-entries", request.getEntriesCount());
    }
}

因為線性一致讀在任何集群內的節點發起,並不需要強制要求放到 Leader 節點上,允許在 Follower 節點執行,因此大大降低 Leader 的讀取壓力。
當在Follower節點執行一致性讀的時候實際上Follower 節點調用 RpcService#readIndex(leaderId.getEndpoint(), newRequest, -1, closure) 方法向 Leader 發送 ReadIndex 請求,交由Leader節點實現一致性讀。所以我這裏主要介紹Leader的一致性讀。

繼續往下走調用NodeImpl的readLeader方法
NodeImpl#readLeader

private void readLeader(final ReadIndexRequest request, final ReadIndexResponse.Builder respBuilder,
                        final RpcResponseClosure<ReadIndexResponse> closure) {
    //1. 獲取集群節點中多數選票數是多少
    final int quorum = getQuorum();
    if (quorum <= 1) {
        // Only one peer, fast path.
        //如果集群中只有一個節點,那麼直接調用回調函數,返回成功
        respBuilder.setSuccess(true) //
                .setIndex(this.ballotBox.getLastCommittedIndex());
        closure.setResponse(respBuilder.build());
        closure.run(Status.OK());
        return;
    }

    final long lastCommittedIndex = this.ballotBox.getLastCommittedIndex();
    //2. 任期必須相等
    //日誌管理器 LogManager 基於投票箱 BallotBox 的 lastCommittedIndex 獲取任期檢查是否等於當前任期
    // 如果不等於當前任期表示此 Leader 節點未在其任期內提交任何日誌,需要拒絕只讀請求;
    if (this.logManager.getTerm(lastCommittedIndex) != this.currTerm) {
        // Reject read only request when this leader has not committed any log entry at its term
        closure
                .run(new Status(
                        RaftError.EAGAIN,
                        "ReadIndex request rejected because leader has not committed any log entry at its term, " +
                         "logIndex=%d, currTerm=%d.",
                        lastCommittedIndex, this.currTerm));
        return;
    }
    respBuilder.setIndex(lastCommittedIndex);

    if (request.getPeerId() != null) {
        // request from follower, check if the follower is in current conf.
        final PeerId peer = new PeerId();
        peer.parse(request.getServerId());
        //3. 來自 Follower 的請求需要檢查 Follower 是否在當前配置
        if (!this.conf.contains(peer)) {
            closure
                    .run(new Status(RaftError.EPERM, "Peer %s is not in current configuration: {}.", peer,
                     this.conf));
            return;
        }
    }

    ReadOnlyOption readOnlyOpt = this.raftOptions.getReadOnlyOptions();
    //4. 如果使用的是ReadOnlyLeaseBased,確認leader是否是在在租約有效時間內
    if (readOnlyOpt == ReadOnlyOption.ReadOnlyLeaseBased && !isLeaderLeaseValid()) {
        // If leader lease timeout, we must change option to ReadOnlySafe
        readOnlyOpt = ReadOnlyOption.ReadOnlySafe;
    }

    switch (readOnlyOpt) {
        //5
        case ReadOnlySafe:
            final List<PeerId> peers = this.conf.getConf().getPeers();
            Requires.requireTrue(peers != null && !peers.isEmpty(), "Empty peers");
            //設置心跳的響應回調函數
            final ReadIndexHeartbeatResponseClosure heartbeatDone = new ReadIndexHeartbeatResponseClosure(closure,
                    respBuilder, quorum, peers.size());
            // Send heartbeat requests to followers
            //向 Followers 節點發起一輪 Heartbeat,如果半數以上節點返回對應的
            // Heartbeat Response,那麼 Leader就能夠確定現在自己仍然是 Leader
            for (final PeerId peer : peers) {
                if (peer.equals(this.serverId)) {
                    continue;
                }
                this.replicatorGroup.sendHeartbeat(peer, heartbeatDone);
            }
            break;
        //6. 因為在租約期內不會發生選舉,確保 Leader 不會變化
        //所以直接返回回調結果
        case ReadOnlyLeaseBased:
            // Responses to followers and local node.
            respBuilder.setSuccess(true);
            closure.setResponse(respBuilder.build());
            closure.run(Status.OK());
            break;
    }
}
  1. 獲取集群節點中多數選票數是多少,即集群節點的1/2+1,如果當前的集群里只有一個節點,那麼直接返回成功,並調用回調方法
  2. 校驗 Raft 集群節點數量以及 lastCommittedIndex 所屬任期符合預期,那麼響應構造器設置其索引為投票箱 BallotBox 的 lastCommittedIndex
  3. 來自 Follower 的請求需要檢查 Follower 是否在當前配置,如果不在當前配置中直接調用回調方法設置異常
  4. 獲取 ReadIndex 請求級別 ReadOnlyOption 配置,ReadOnlyOption 參數默認值為 ReadOnlySafe。如果設置的是ReadOnlyLeaseBased,那麼會調用isLeaderLeaseValid檢查leader是否是在在租約有效時間內
  5. 配置為ReadOnlySafe 調用 Replicator#sendHeartbeat(rid, closure) 方法向 Followers 節點發送 Heartbeat 心跳請求,發送心跳成功執行 ReadIndexHeartbeatResponseClosure 心跳響應回調;ReadIndex 心跳響應回調檢查是否超過半數節點包括 Leader 節點自身投票贊成,半數以上節點返回客戶端Heartbeat 請求成功響應,即 applyIndex 超過 ReadIndex 說明已經同步到 ReadIndex 對應的 Log 能夠提供 Linearizable Read
  6. 配置為ReadOnlyLeaseBased,因為Leader 租約有效期間認為當前 Leader 是 Raft Group 內的唯一有效 Leader,所以忽略 ReadIndex 發送 Heartbeat 確認身份步驟,直接返回 Follower 節點和本地節點 Read 請求成功響應。Leader 節點繼續等待狀態機執行,直到 applyIndex 超過 ReadIndex 安全提供 Linearizable Read

無論是ReadOnlySafe還是ReadOnlyLeaseBased,最後發送成功響應都會調用ReadIndexResponseClosure的run方法。

ReadIndexResponseClosure#run

public void run(final Status status) {
    //fail
    //傳入的狀態不是ok,響應失敗
    if (!status.isOk()) {
        notifyFail(status);
        return;
    }
    final ReadIndexResponse readIndexResponse = getResponse();
    //Fail
    //response沒有響應成功,響應失敗
    if (!readIndexResponse.getSuccess()) {
        notifyFail(new Status(-1, "Fail to run ReadIndex task, maybe the leader stepped down."));
        return;
    }
    // Success
    //一致性讀成功
    final ReadIndexStatus readIndexStatus = new ReadIndexStatus(this.states, this.request,
        readIndexResponse.getIndex());
    for (final ReadIndexState state : this.states) {
        // Records current commit log index.
        //設置當前提交的index
        state.setIndex(readIndexResponse.getIndex());
    }

    boolean doUnlock = true;
    ReadOnlyServiceImpl.this.lock.lock();
    try {
        //校驗applyIndex 是否超過 ReadIndex
        if (readIndexStatus.isApplied(ReadOnlyServiceImpl.this.fsmCaller.getLastAppliedIndex())) {
            // Already applied, notify readIndex request.
            ReadOnlyServiceImpl.this.lock.unlock();
            doUnlock = false;
            //已經同步到 ReadIndex 對應的 Log 能夠提供 Linearizable Read
            notifySuccess(readIndexStatus);
        } else {
            // Not applied, add it to pending-notify cache.
            ReadOnlyServiceImpl.this.pendingNotifyStatus
                .computeIfAbsent(readIndexStatus.getIndex(), k -> new ArrayList<>(10)) //
                .add(readIndexStatus);
        }
    } finally {
        if (doUnlock) {
            ReadOnlyServiceImpl.this.lock.unlock();
        }
    }
}

Run方法首先會校驗一下是否需要響應失敗,如果響應成功,那麼會將所有封裝的ReadIndexState更新一下index,然後校驗一下applyIndex 是否超過 ReadIndex,超過了ReadIndex代表所有已經複製到多數派上的 Log(可視為寫操作)被視為安全的 Log,該 Log 所體現的數據就能對客戶端 Client 可見。

ReadOnlyServiceImpl#notifySuccess

private void notifySuccess(final ReadIndexStatus status) {
    final long nowMs = Utils.monotonicMs();
    final List<ReadIndexState> states = status.getStates();
    final int taskCount = states.size();
    for (int i = 0; i < taskCount; i++) {
        final ReadIndexState task = states.get(i);
        final ReadIndexClosure done = task.getDone(); // stack copy
        if (done != null) {
            this.nodeMetrics.recordLatency("read-index", nowMs - task.getStartTimeMs());
            done.setResult(task.getIndex(), task.getRequestContext().get());
            done.run(Status.OK());
        }
    }
}

如果是響應成功,那麼會調用notifySuccess方法,會將status里封裝的ReadIndexState集合遍歷一遍,調用當中的run方法。

這個run方法會調用到我們在multiGet中設置的run方法中
RaftRawKVStore#multiGet

public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
    if (!readOnlySafe) {
        this.kvStore.multiGet(keys, false, closure);
        return;
    }
    // KV 存儲實現線性一致讀
    // 調用 readIndex 方法,等待回調執行
    this.node.readIndex(BytesUtil.EMPTY_BYTES, new ReadIndexClosure() {

        @Override
        public void run(final Status status, final long index, final byte[] reqCtx) {
            //如果狀態返回成功,
            if (status.isOk()) {
                RaftRawKVStore.this.kvStore.multiGet(keys, true, closure);
                return;
            }
            //readIndex 讀取失敗嘗試應用鍵值讀操作申請任務於 Leader 節點的狀態機 KVStoreStateMachine
            RaftRawKVStore.this.readIndexExecutor.execute(() -> {
                if (isLeader()) {
                    LOG.warn("Fail to [multiGet] with 'ReadIndex': {}, try to applying to the state machine.",
                            status);
                    // If 'read index' read fails, try to applying to the state machine at the leader node
                    applyOperation(KVOperation.createMultiGet(keys), closure);
                } else {
                    LOG.warn("Fail to [multiGet] with 'ReadIndex': {}.", status);
                    // Client will retry to leader node
                    new KVClosureAdapter(closure, null).run(status);
                }
            });
        }
    });

這個run方法會調用RaftRawKVStore的multiGet從RocksDB中直接獲取數據。

總結

我們這篇文章從RheaKVStore的客戶端get方法一直講到,RheaKVStore服務端使用JRaft實現線性一致性讀,並講解了線性一致性讀是怎麼實現的,通過這個例子大家應該對線性一致性讀有了一個相對不錯的理解了。

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

【其他文章推薦】

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

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

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

Hazel,自動整理文件,讓你的 Mac 井井有條

原文地址

讓我們從實際需求出發,看看問題出在哪裡,並在此基礎上認識和學習使用 Hazel。

電腦隨着使用時間的增長,其中的文件也在瘋狂的增長,時間長了也就會出現各種混亂:大量文件堆放在一起,舊文件很少清理,分不清哪些文件還有用,找不到需要的文件等等。

今天我們就以「下載」和「桌面」為例,聊一聊如何整理我們的電腦。

Downloads:下載的文件很少處理,時間一長就各種堆積…… 

Desktop:經常把臨時文件存放在此,方便拖拽使用,但時間一長,就是各種凌亂……

既然知道了問題所在,那麼我們就來着手整理吧。

理清整理思路

首先是確定整理思路,比如如何界定一個文件是否還有用,如何界定它屬於什麼分類等,對應的操作一般是刪除(比如不再需要的或重複的文件)或存檔(學習資料或工作材料等分類存儲),知道如何處理一個文件就很好辦了,剩下的就都是體力活兒。

雖然這不是一件特別麻煩的事,但是我們也經常忘記或「懶得整理」。這有點類似於打掃房間,當我們沒有時間或者經常忘記時,可以買一台掃地機器人幫助我們打掃,同樣的,在 Mac 上也有這樣一台「機器人」,它就是 Hazel。

Hazel 是什麼?

Hazel 是一款可以自動監控並整理文件夾的工具,其官網的介紹就是簡單的一句話:Automated Organization for Your Mac。

它的使用有點類似於網絡服務 IFTTT,你可以設定一個 if 條件,如果被監控的文件夾出現符合條件的項,那麼對其執行 then 的操作(也可以通過郵箱的收件過濾規則來理解)。

Hazel 不是一款新工具,它已經有了很長的歷史,其第一個版本在 2006 年底就已經發布,在今年 5 月 4 號,Hazel 發布了 4.0 版本,新增了規則同步(文末會有介紹)、規則搜索等一系列實用功能。

Hazel 具體能做什麼?

先為大家簡單羅列一些 Hazel 能做到的事情:

  • 根據文件創建的時間,自動將文件進行顏色標記(比如將最近的文件標記為藍色)
  • 自動的用特定軟件打開某個特定文件(比如下載 BT 種子后,自動用迅雷打開下載)
  • 自動刪除已下載過的 BT 種子文件
  • 根據文件的類型,自動轉移到相應的文件夾中(比如圖片移動到照片文件夾,電影移動到視頻文件夾等)
  • 自動刪除某些特定文件(比如標題中含有固定內容且創建日期在很早以前的)
  • 自動將壓縮文件解壓
  • 自動幫你清理文件的緩存
  • 自動幫你整理照片,可以按照「年 – 月」來分類存儲到相應文件夾
  • 自動把文件夾中的內容上傳到 FTP 等網絡服務中
  • 自動將照片導入 Photos,自動將音樂導入 iTunes 
  • ……

以上只是列舉的一些場景能夠實現的功能,再加上 Hazel 支持 AppleScript、JavaScript、Automator workflow 等代碼指令,令其擴展性更上一層樓,可以做到的事情也可以說只剩下想象力這道門檻了。

介紹了不少,下面我們就從 Hazel 的安裝和實際設置來為大家做一個簡單的入門指南。

Hazel 的安裝

前往下載最新版本,按照提示安裝,完成后 Hazel 會出現在系統設置中(在應用程序中可找不到哦)。
Hazel 是一款收費軟件,初次安裝后可以免費試用 14 天,此時可以選擇加載一些簡單的默認規則以幫助你快速上手(當然看完這篇文章也就可以不用加載了)。

操作后 Hazel 會給我們彈出警告信息:在激活這些規則之前,一定要先檢查它們。具體的方法下面會提及。

Hazel 的界面和基礎應用

注:文末提供了文中所有 Hazel 規則的打包下載地址,如果你對文中介紹的規則感興趣,可以直接下載使用。

Hazel 的主界面包含三部分,分別是設置文件夾規則的 Folders 頁面,設置垃圾箱規則的 Trash 頁面和其他信息頁(Info),今天主要給大家講解文件夾規則設置頁面。

在 Folders 中包含三部份:設置監控的文件夾(圖中 1),設置該文件夾下的具體規則(圖中 2),設置該文件夾的重複文件處理(圖中 3),圖 1 部分右側的 icon 分別表示「暫停規則執行」和「同步」,建議嘗試新規則的時候先暫停執行再進行調試。

以整理「下載」文件夾為例,我個人的需求有如下幾條:

  • 最近的下載文件用顏色標籤提醒
  • 超過 3 天的文件不再是新文件,去掉顏色標籤
  • 對存放超過 3 周的文件需進行處理,將滿足此條件的文件用紅色標記提醒
  • 自動刪除已使用的 .torrent 文件
  • 將手機截屏的圖片單獨存放

上面幾條是梳理自己的整理需求后,選擇的可以被 Hazel 自動執行的。此時回到 Hazel,我們點擊左下角的加號新增「下載」文件夾,隨後在右側 Rules 區域點擊加號新增規則。

標記最新下載文件

下圖是規則設置界面,圖 1 部分設置規則名稱和註釋;圖 2 部分設置監控條件,此時設置的是文件添加時間在最後匹配時間之前(新文件添加后暫未被匹配,所以一定是早於匹配時間);圖 3 部分設置執行的動作,此時是將匹配出來的文件標記藍色標籤,並且同時可以被其他規則匹配。

標記舊文件

超過 3 天的文件,不再是我需要關注的內容,將其中的藍色標籤去掉:

標記待處理文件

對「下載」文件夾,我需要對超過 3 周未處理的文件進行處理,要麼歸檔要麼刪除,需要進行人工判斷的時候我使用紅色標記來提醒自己:

刪除 .torrent 文件

在使用 BT 下載之後,留在文件夾的種子文件也就沒有什麼用了,為了防止誤刪設置了 5 天的期限,注意圖中綠色符號,那是點擊了 Preview 后的效果,建議設置規則的時候多使用 Preview 功能來檢查條件設置是否正確,特別是那些複雜的符合條件。

自動移動手機截屏文件

工作關係,經常需要在手機上截屏上傳到電腦使用(使用 AirDrop 上傳到「下載」中),這類圖片的處理一般是超過一周后移動到桌面文件夾中再進行集中處理:

上面介紹了「下載」文件夾的整理思路和執行;對於「桌面」文件夾的整理,我的思路一般是不輕易自動刪除(防誤刪),而是統一到分類文件夾中集中處理。將文檔存放於「文檔」中,將圖片存放於「圖片」中等等,都是非常簡單和基礎的設置,就不做過多介紹;

下面說一下我對源文件的處理,這裏涉及到條件的嵌套使用:

圖中使用了嵌套條件,具體的操作是鼠標長按右側加號(也可按住 Option 後點擊),即可增加嵌套條件組。

附上桌面整理后截圖:

Hazel 中級應用

除了以上的基礎使用,Hazel 還可作用於更加廣泛的場景,下面以自動解壓自動清理緩存為例。

自動解壓

下載壓縮包后不用手動解壓,Hazel 會自動創建文件夾(按照壓縮包的名稱命名),並將壓縮包和解壓后的文件存放於此:

有三點需要為大家說明:

  • 設置標籤是為了防止壓縮文件有損壞而導致 Hazel 陷入循環執行中;
  • 不能設置自動刪除,因為 Hazel 會自動選中解壓后的文件,此時的刪除也只是把解壓后的文件刪掉;
  • 使用默認的「Unarchive」操作也可解壓,不過在解壓 .zip 文件後會自動將壓縮包刪掉,所以我這裏使用了第三方的免費解壓軟件  代替(注意:在第一次執行時需要權限設置);不介意刪除壓縮包的同學使用默認的解壓操作即可。

此規則參考了  的博客,特此感謝。

自動清理緩存

以 QQ 為例,QQ 會把群消息中的圖片自動保存到本地,時間一長這個文件夾就很容易達到幾個 G 的大小,這時候 Hazel 又可以派上用處了。

首先找到你的 QQ 文件夾,可嘗試如下路徑(本人 Mac 系統 10.11)

/Users/用戶名/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ

將路徑中的「用戶名」換成自己的,然後在 Finder 中按住「⌘ + Shift + G」,把路徑粘貼到輸入框中點擊「前往」即可。

如果路徑沒問題,就可以在 Hazel 中添加此文件夾了,點擊添加按鈕彈出選擇文件夾界面后,使用上述快捷鍵和路徑同樣可以快速選定,添加後設置如下兩條規則,第一條規則的作用是讓所有子文件夾都可以適配規則並執行操作;第二條規則是把超過 500M 的子文件夾進行刪除操作,且不會直接刪除父文件夾。

至此,QQ 緩存文件的自動清理就設置完成了,其他軟件緩存也可以進行類似的規則設計,不過一定要注意確保這裏面沒有你需要的文件,否則一旦刪除要找回也是頗為麻煩的。

更多用法

如前文所說,Hazel 能做到的不止這些場景,還有用戶用它來整理照片,利用 AppleScript 執行更加複雜的工作流程等等,這裏僅當作拋磚引玉,歡迎大家分享自己的用法,並且以後也會有更多關於 Hazel 使用技巧的文章。 

其他功能

管理垃圾箱

在 Hazel 的 Trash 頁面,可以進行一些垃圾箱的設置,比如將其中超過一周的文件刪除,保持垃圾箱大小控制在 2GB 左右,選擇刪除時是否使用安全刪除功能,以及卸載應用時檢測其附屬文件夾等等;這方面的功能筆者並不常用,在此不做過多介紹。

刪除應用時檢測相關文件,並可選擇一併刪除。作用類似於 。

同步規則

同步功能在 4.0 終於推出,現在也可以方便的使用在多台電腦上了。點擊左側面板中的齒輪圖標,選擇 Rule Sync Options 即可打開同步界面(也可在文件夾上右鍵選擇 Rule Sync Options)。

同步需要配合第三方同步網盤使用,當前文件夾若是第一次使用同步,需要設置同步文件存放路徑,點擊 Set up new sync file 即可。如果要使用同步的文件,在界面中點擊 Use existing sync file 即可。

Hazel 的下載

Hazel 是一款收費軟件(),五月初的時候發布了 4.0 版本,單獨購買是 $32,Family Pack $49,從 3.0 版本升級需要 $10。初次下載可以免費試用 14 天,建議大家先試用再購買。

最後給大家提供我自己的 Hazel 設置,你可以導入后調整為適合自己的規則再使用:。

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

【其他文章推薦】

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

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

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

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