分類: 3C資訊

  • .Net Core微服務入門全紀錄(一)——項目搭建

    .Net Core微服務入門全紀錄(一)——項目搭建

    前言

    寫這篇博客主要目的是記錄一下自己的學習過程,只能是簡單入門級別的,因為水平有限就寫到哪算哪吧,寫的不對之處歡迎指正。
    代碼放在:https://github.com/xiajingren/NetCoreMicroserviceDemo

    什麼是微服務?

    關於微服務的概念解釋網上有很多…
    個人理解,微服務是一種系統架構模式,它和語言無關,和框架無關,和工具無關,和服務器環境無關…
    微服務思想是將傳統的單體系統按照業務拆分成多個職責單一、且可獨立運行的接口服務。至於服務如何拆分,沒有明確的定義。
    幾乎任何後端語言都能做微服務開發。
    微服務也並不是完美無缺的,微服務架構會帶來更多的問題,增加系統的複雜度,引入更多的技術棧…

    創建項目

    一個客戶端,一個產品服務,一個訂單服務。3個項目都是asp.net core web應用程序。創建項目的時候記得啟用一下Docker支持,或者後面添加也行。

    為產品、訂單服務添加一些基礎代碼,就簡單的返回一下 服務名稱,當前時間,服務的ip、端口。

    在Docker中運行服務

    為了方便,我使用Docker來運行服務,不用Docker也行,關於docker的安裝及基本使用就不介紹了。

    • build鏡像:

    在項目根目錄打開PowerShell窗口執行:docker build -t productapi -f ./Product.API/Dockerfile .

    Successfully代表build成功了。

    • 運行容器:

    執行:docker run -d -p 9050:80 --name productservice productapi

    執行:docker ps查看運行的容器:

    沒問題,使用瀏覽器訪問一下接口:

    也沒問題,其中的ip端口是Docker容器內部的ip端口,所以端口是80,這個無所謂。

    • 產品服務部署好了,下面部署一下訂單服務,也是同樣的流程,就把指令簡單貼一下吧:

    build鏡像:docker build -t orderapi -f ./Order.API/Dockerfile .
    運行容器:docker run -d -p 9060:80 --name orderservice orderapi
    瀏覽器訪問一下:

    OK,訂單服務也部署完成了。

    客戶端調用

    客戶端我這裏只做了一個web客戶端,實際可能是各種業務系統、什麼PC端、手機端、小程序。。。這個明白就好,為了簡單就不搞那麼多了。

    • 因為客戶端需要http請求服務端接口,所以需要一個http請求客戶端,我個人比較習慣RestSharp,安利一波:https://github.com/restsharp/RestSharp

    • 添加基礎代碼:

    IServiceHelper.cs:

        public interface IServiceHelper
        {
            /// <summary>
            /// 獲取產品數據
            /// </summary>
            /// <returns></returns>
            Task<string> GetProduct();
    
            /// <summary>
            /// 獲取訂單數據
            /// </summary>
            /// <returns></returns>
            Task<string> GetOrder();
        }
    

    ServiceHelper.cs:

        public class ServiceHelper : IServiceHelper
        {
            public async Task<string> GetOrder()
            {
                string serviceUrl = "http://localhost:9060";//訂單服務的地址,可以放在配置文件或者數據庫等等...
    
                var Client = new RestClient(serviceUrl);
                var request = new RestRequest("/orders", Method.GET);
    
                var response = await Client.ExecuteAsync(request);
                return response.Content;
            }
    
            public async Task<string> GetProduct()
            {
                string serviceUrl = "http://localhost:9050";//產品服務的地址,可以放在配置文件或者數據庫等等...
    
                var Client = new RestClient(serviceUrl);
                var request = new RestRequest("/products", Method.GET);
    
                var response = await Client.ExecuteAsync(request);
                return response.Content;
            }
        }
    

    Startup.cs:

        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddControllersWithViews();
                
                //注入IServiceHelper
                services.AddSingleton<IServiceHelper, ServiceHelper>();
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Home/Error");
                }
                app.UseStaticFiles();
    
                app.UseRouting();
    
                app.UseAuthorization();
    
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllerRoute(
                        name: "default",
                        pattern: "{controller=Home}/{action=Index}/{id?}");
                });
            }
        }
    

    HomeController.cs:

        public class HomeController : Controller
        {
            private readonly ILogger<HomeController> _logger;
            private readonly IServiceHelper _serviceHelper;
    
            public HomeController(ILogger<HomeController> logger, IServiceHelper serviceHelper)
            {
                _logger = logger;
                _serviceHelper = serviceHelper;
            }
    
            public async Task<IActionResult> Index()
            {
                ViewBag.OrderData = await _serviceHelper.GetOrder();
                ViewBag.ProductData = await _serviceHelper.GetProduct();
    
                return View();
            }
    
            public IActionResult Privacy()
            {
                return View();
            }
    
            [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
            public IActionResult Error()
            {
                return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
            }
        }
    

    Index.cshtml:

    @{
        ViewData["Title"] = "Home Page";
    }
    
    <div class="text-center">
        <h1 class="display-4">Welcome</h1>
        <p>
            @ViewBag.OrderData
        </p>
        <p>
            @ViewBag.ProductData
        </p>
    </div>
    

    代碼比較簡單,這裏就不用docker了,直接控制台啟動,使用瀏覽器訪問:

    • 一切正常。進行到這裏,各個服務也獨立運行了,客戶端也能正常調用了,貌似算是完成一個簡易的微服務了。但是,微服務架構最重要的原則就是——“高可用”。以上的做法明顯不能滿足高可用性,因為任何一個服務掛掉,所有依賴這個服務的業務系統都會受影響。

    停止一下訂單服務:docker stop orderservice

    訂單服務停止,導致客戶端業務系統無法獲取訂單數據。
    要解決這個問題,很容易想到:集群。

    簡單的服務集群

    既然單個服務實例有掛掉的風險,那麼部署多個服務實例就好了嘛,只要大家不同時全掛就行。

    • 使用docker運行多個服務實例:
    docker run -d -p 9061:80 --name orderservice1 orderapi
    docker run -d -p 9062:80 --name orderservice2 orderapi
    docker run -d -p 9051:80 --name productservice1 productapi
    docker run -d -p 9052:80 --name productservice2 productapi
    

    現在訂單服務和產品服務都增加到3個服務實例。

    • 那麼稍微改造一下客戶端代碼吧:
      ServiceHelper.cs:
    public class ServiceHelper : IServiceHelper
        {
            public async Task<string> GetOrder()
            {
                string[] serviceUrls = { "http://localhost:9060", "http://localhost:9061", "http://localhost:9062" };//訂單服務的地址,可以放在配置文件或者數據庫等等...
    
                //每次隨機訪問一個服務實例
                var Client = new RestClient(serviceUrls[new Random().Next(0, 3)]);
                var request = new RestRequest("/orders", Method.GET);
    
                var response = await Client.ExecuteAsync(request);
                return response.Content;
            }
    
            public async Task<string> GetProduct()
            {
                string[] serviceUrls = { "http://localhost:9050", "http://localhost:9051", "http://localhost:9052" };//產品服務的地址,可以放在配置文件或者數據庫等等...
    
                //每次隨機訪問一個服務實例
                var Client = new RestClient(serviceUrls[new Random().Next(0, 3)]);
                var request = new RestRequest("/products", Method.GET);
    
                var response = await Client.ExecuteAsync(request);
                return response.Content;
            }
        }
    

    當然拿到這些服務地址可以自己做複雜的負載均衡策略,比如輪詢,隨機,權重等等 都行,甚至在中間弄個nginx也可以。這些不是重點,所以就簡單做一個隨機吧,每次請求來了隨便訪問一個服務實例。

    • 瀏覽器測試一下:

      可以看到請求被隨機分配了。但是這種做法依然不安全,如果隨機訪問到的實例剛好掛掉,那麼業務系統依然會出問題。
      簡單處理思路是:
      1.如果某個地址請求失敗了,那麼換一個地址接着執行。
      2.如果某個地址的請求連續多次失敗了,那麼就移除這個地址,下次就不會訪問到它了。
      。。。。。。
      業務系統實現以上邏輯,基本上風險就很低了,也算是大大增加了系統可用性了。

    • 然後思考另一個問題:

    實際應用中,上層的業務系統可能非常多,為了保證可用性,每個業務系統都去考慮服務實例掛沒掛掉嗎?
    而且實際應用中服務實例的數量或者地址大多是不固定的,例如雙十一來了,流量大了,增加了一堆服務實例,這時候每個業務系統再去配置文件里配置一下這些地址嗎?雙十一過了又去把配置刪掉嗎?顯然是不現實的,服務必須要做到可靈活伸縮。

    • 這時候就引入一個名詞:服務註冊與發現

    未完待續…

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

    【其他文章推薦】

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

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

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

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

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

  • redis 數據刪除策略和逐出算法

    redis 數據刪除策略和逐出算法

    數據存儲和有效期

    redis 工作流程中,過期的數據並不需要馬上就要執行刪除操作。因為這些刪不刪除只是一種狀態表示,可以異步的去處理,在不忙的時候去把這些不緊急的刪除操作做了,從而保證 redis 的高效

    數據的存儲

    在redis中數據的存儲不僅僅需要保存數據本身還要保存數據的生命周期,也就是過期時間。在redis 中 數據的存儲結構如下圖:

    獲取有效期

    Redis是一種內存級數據庫,所有數據均存放在內存中,內存中的數據可以通過TTL指令獲取其狀態

    刪除策略

    在內存佔用與CPU佔用之間尋找一種平衡,顧此失彼都會造成整體redis性能的下降,甚至引發服務器宕機或內存泄漏。

    定時刪除

    創建一個定時器,當key設置過期時間,且過期時間到達時,由定時器任務立即執行對鍵的刪除操作

    優點

    節約內存,到時就刪除,快速釋放掉不必要的內存佔用

    缺點

    CPU壓力很大,無論CPU此時負載多高,均佔用CPU,會影響redis服務器響應時間和指令吞吐量

    總結

    用處理器性能換取存儲空間

    惰性刪除

    數據到達過期時間,不做處理。等下次訪問該數據,如果未過期,返回數據。發現已經過期,刪除,返回不存在。這樣每次讀寫數據都需要檢測數據是否已經到達過期時間。也就是惰性刪除總是在數據的讀寫時發生的。

    expireIfNeeded函數

    對所有的讀寫命令進行檢查,檢查操作的對象是否過期。過期就刪除返回過期,不過期就什麼也不做~。

    執行數據寫入過程中,首先通過expireIfNeeded函數對寫入的key進行過期判斷。

    /*
     * 為執行寫入操作而取出鍵 key 在數據庫 db 中的值。
     *
     * 和 lookupKeyRead 不同,這個函數不會更新服務器的命中/不命中信息。
     *
     * 找到時返回值對象,沒找到返回 NULL 。
     */
    robj *lookupKeyWrite(redisDb *db, robj *key) {
    
        // 刪除過期鍵
        expireIfNeeded(db,key);
    
        // 查找並返回 key 的值對象
        return lookupKey(db,key);
    }
    

    執行數據讀取過程中,首先通過expireIfNeeded函數對寫入的key進行過期判斷。

    /*
     * 為執行讀取操作而取出鍵 key 在數據庫 db 中的值。
     *
     * 並根據是否成功找到值,更新服務器的命中/不命中信息。
     *
     * 找到時返回值對象,沒找到返回 NULL 。
     */
    robj *lookupKeyRead(redisDb *db, robj *key) {
        robj *val;
    
        // 檢查 key 釋放已經過期
        expireIfNeeded(db,key);
    
        // 從數據庫中取出鍵的值
        val = lookupKey(db,key);
    
        // 更新命中/不命中信息
        if (val == NULL)
            server.stat_keyspace_misses++;
        else
            server.stat_keyspace_hits++;
    
        // 返回值
        return val;
    }
    

    執行過期動作expireIfNeeded其實內部做了三件事情,分別是:

    • 查看key判斷是否過期
    • 向slave節點傳播執行過期key的動作併發送事件通知
    • 刪除過期key
    /*
     * 檢查 key 是否已經過期,如果是的話,將它從數據庫中刪除。
     *
     * 返回 0 表示鍵沒有過期時間,或者鍵未過期。
     *
     * 返回 1 表示鍵已經因為過期而被刪除了。
     */
    int expireIfNeeded(redisDb *db, robj *key) {
    
        // 取出鍵的過期時間
        mstime_t when = getExpire(db,key);
        mstime_t now;
    
        // 沒有過期時間
        if (when < 0) return 0; /* No expire for this key */
    
        /* Don't expire anything while loading. It will be done later. */
        // 如果服務器正在進行載入,那麼不進行任何過期檢查
        if (server.loading) return 0;
    
        // 當服務器運行在 replication 模式時
        // 附屬節點並不主動刪除 key
        // 它只返回一個邏輯上正確的返回值
        // 真正的刪除操作要等待主節點發來刪除命令時才執行
        // 從而保證數據的同步
        if (server.masterhost != NULL) return now > when;
    
        // 運行到這裏,表示鍵帶有過期時間,並且服務器為主節點
    
        /* Return when this key has not expired */
        // 如果未過期,返回 0
        if (now <= when) return 0;
    
        /* Delete the key */
        server.stat_expiredkeys++;
    
        // 向 AOF 文件和附屬節點傳播過期信息
        propagateExpire(db,key);
    
        // 發送事件通知
        notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
            "expired",key,db->id);
    
        // 將過期鍵從數據庫中刪除
        return dbDelete(db,key);
    }
    

    判斷key是否過期的數據結構是db->expires,也就是通過expires的數據結構判斷數據是否過期。
    內部獲取過期時間並返回。

    /*
     * 返回字典中包含鍵 key 的節點
     *
     * 找到返回節點,找不到返回 NULL
     *
     * T = O(1)
     */
    dictEntry *dictFind(dict *d, const void *key)
    {
        dictEntry *he;
        unsigned int h, idx, table;
    
        // 字典(的哈希表)為空
        if (d->ht[0].size == 0) return NULL; /* We don't have a table at all */
    
        // 如果條件允許的話,進行單步 rehash
        if (dictIsRehashing(d)) _dictRehashStep(d);
    
        // 計算鍵的哈希值
        h = dictHashKey(d, key);
        // 在字典的哈希表中查找這個鍵
        // T = O(1)
        for (table = 0; table <= 1; table++) {
    
            // 計算索引值
            idx = h & d->ht[table].sizemask;
    
            // 遍歷給定索引上的鏈表的所有節點,查找 key
            he = d->ht[table].table[idx];
            // T = O(1)
            while(he) {
    
                if (dictCompareKeys(d, key, he->key))
                    return he;
    
                he = he->next;
            }
    
            // 如果程序遍歷完 0 號哈希表,仍然沒找到指定的鍵的節點
            // 那麼程序會檢查字典是否在進行 rehash ,
            // 然後才決定是直接返回 NULL ,還是繼續查找 1 號哈希表
            if (!dictIsRehashing(d)) return NULL;
        }
    
        // 進行到這裏時,說明兩個哈希表都沒找到
        return NULL;
    }
    

    優點

    節約CPU性能,發現必須刪除的時候才刪除。

    缺點

    內存壓力很大,出現長期佔用內存的數據。

    總結

    用存儲空間換取處理器性能

    定期刪除

    周期性輪詢redis庫中時效性數據,採用隨機抽取的策略,利用過期數據佔比的方式刪除頻度。

    優點

    CPU性能佔用設置有峰值,檢測頻度可自定義設置

    內存壓力不是很大,長期佔用內存的冷數據會被持續清理

    缺點

    需要周期性抽查存儲空間

    定期刪除詳解

    redis的定期刪除是通過定時任務實現的,也就是定時任務會循環調用serverCron方法。然後定時檢查過期數據的方法是databasesCron。定期刪除的一大特點就是考慮了定時刪除過期數據會佔用cpu時間,所以每次執行databasesCron的時候會限制cpu的佔用不超過25%。真正執行刪除的是 activeExpireCycle方法。

    時間事件

    對於持續運行的服務器來說, 服務器需要定期對自身的資源和狀態進行必要的檢查和整理, 從而讓服務器維持在一個健康穩定的狀態, 這類操作被統稱為常規操作(cron job

    在 Redis 中, 常規操作由 redis.c/serverCron() 實現, 它主要執行以下操作

    1 更新服務器的各類統計信息,比如時間、內存佔用、數據庫佔用情況等。

    2 清理數據庫中的過期鍵值對。

    3 對不合理的數據庫進行大小調整。

    4 關閉和清理連接失效的客戶端。

    5 嘗試進行 AOF 或 RDB 持久化操作。

    6 如果服務器是主節點的話,對附屬節點進行定期同步。

    7 如果處於集群模式的話,對集群進行定期同步和連接測試。

    因為 serverCron() 需要在 Redis 服務器運行期間一直定期運行, 所以它是一個循環時間事件: serverCron() 會一直定期執行,直到服務器關閉為止。

    在 Redis 2.6 版本中, 程序規定 serverCron() 每秒運行 10 次, 平均每 100 毫秒運行一次。 從 Redis 2.8 開始, 用戶可以通過修改 hz選項來調整 serverCron() 的每秒執行次數, 具體信息請參考 redis.conf 文件中關於 hz 選項的說明

    查看hz

    way1 : config get hz  # "hz" "10"
    way2 : info server  # server.hz 10
    

    serverCron()

    serverCron()會定期的執行,在serverCron()執行中會調用databasesCron() 方法(serverCron()還做了其他很多事情,但是現在不討論,只談刪除策略)

    int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
        // 略去多無關代碼
    
        /* We need to do a few operations on clients asynchronously. */
        // 檢查客戶端,關閉超時客戶端,並釋放客戶端多餘的緩衝區
        clientsCron();
    
        /* Handle background operations on Redis databases. */
        // 對數據庫執行各種操作
        databasesCron();   /* !我們關注的方法! */
    

    databasesCron()

    databasesCron() 中 調用了 activeExpireCycle()方法,來對過期的數據進行處理。(在這裏還會做一些其他操作~ 調整數據庫大小,主動和漸進式rehash)

    // 對數據庫執行刪除過期鍵,調整大小,以及主動和漸進式 rehash
    void databasesCron(void) {
    
        // 判斷是否是主服務器 如果是 執行主動過期鍵清除
        if (server.active_expire_enabled && server.masterhost == NULL)
            // 清除模式為 CYCLE_SLOW ,這個模式會盡量多清除過期鍵
            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
    
        // 在沒有 BGSAVE 或者 BGREWRITEAOF 執行時,對哈希表進行 rehash
        if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) {
            static unsigned int resize_db = 0;
            static unsigned int rehash_db = 0;
            unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
            unsigned int j;
    
            /* Don't test more DBs than we have. */
            // 設定要測試的數據庫數量
            if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum;
    
            /* Resize */
            // 調整字典的大小
            for (j = 0; j < dbs_per_call; j++) {
                tryResizeHashTables(resize_db % server.dbnum);
                resize_db++;
            }
    
            /* Rehash */
            // 對字典進行漸進式 rehash
            if (server.activerehashing) {
                for (j = 0; j < dbs_per_call; j++) {
                    int work_done = incrementallyRehash(rehash_db % server.dbnum);
                    rehash_db++;
                    if (work_done) {
                        /* If the function did some work, stop here, we'll do
                         * more at the next cron loop. */
                        break;
                    }
                }
            }
        }
    }
    

    activeExpireCycle()

    大致流程如下

    1 遍歷指定個數的db(默認的 16 )進行刪除操作

    2 針對每個db隨機獲取過期數據每次遍歷不超過指定數量(如20),發現過期數據並進行刪除。

    3 如果有多於25%的keys過期,重複步驟 2

    除了主動淘汰的頻率外,Redis對每次淘汰任務執行的最大時長也有一個限定,這樣保證了每次主動淘汰不會過多阻塞應用請求,以下是這個限定計算公式:

    #define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* CPU max % for keys collection */ ``... ``timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
    

    也就是每次執行時間的25%用於過期數據刪除。

    void activeExpireCycle(int type) {
        // 靜態變量,用來累積函數連續執行時的數據
        static unsigned int current_db = 0; /* Last DB tested. */
        static int timelimit_exit = 0;      /* Time limit hit in previous call? */
        static long long last_fast_cycle = 0; /* When last fast cycle ran. */
    
        unsigned int j, iteration = 0;
        // 默認每次處理的數據庫數量
        unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
        // 函數開始的時間
        long long start = ustime(), timelimit;
    
        // 快速模式
        if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
            // 如果上次函數沒有觸發 timelimit_exit ,那麼不執行處理
            if (!timelimit_exit) return;
            // 如果距離上次執行未夠一定時間,那麼不執行處理
            if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
            // 運行到這裏,說明執行快速處理,記錄當前時間
            last_fast_cycle = start;
        }
    
        /* 
         * 一般情況下,函數只處理 REDIS_DBCRON_DBS_PER_CALL 個數據庫,
         * 除非:
         *
         * 1) 當前數據庫的數量小於 REDIS_DBCRON_DBS_PER_CALL
         * 2) 如果上次處理遇到了時間上限,那麼這次需要對所有數據庫進行掃描,
         *     這可以避免過多的過期鍵佔用空間
         */
        if (dbs_per_call > server.dbnum || timelimit_exit)
            dbs_per_call = server.dbnum;
    
        // 函數處理的微秒時間上限
        // ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 默認為 25 ,也即是 25 % 的 CPU 時間
        timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
        timelimit_exit = 0;
        if (timelimit <= 0) timelimit = 1;
    
        // 如果是運行在快速模式之下
        // 那麼最多只能運行 FAST_DURATION 微秒 
        // 默認值為 1000 (微秒)
        if (type == ACTIVE_EXPIRE_CYCLE_FAST)
            timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
    
        // 遍曆數據庫
        for (j = 0; j < dbs_per_call; j++) {
            int expired;
            // 指向要處理的數據庫
            redisDb *db = server.db+(current_db % server.dbnum);
    
            // 為 DB 計數器加一,如果進入 do 循環之後因為超時而跳出
            // 那麼下次會直接從下個 DB 開始處理
            current_db++;
    
            do {
                unsigned long num, slots;
                long long now, ttl_sum;
                int ttl_samples;
    
                /* If there is nothing to expire try next DB ASAP. */
                // 獲取數據庫中帶過期時間的鍵的數量
                // 如果該數量為 0 ,直接跳過這個數據庫
                if ((num = dictSize(db->expires)) == 0) {
                    db->avg_ttl = 0;
                    break;
                }
                // 獲取數據庫中鍵值對的數量
                slots = dictSlots(db->expires);
                // 當前時間
                now = mstime();
    
                // 這個數據庫的使用率低於 1% ,掃描起來太費力了(大部分都會 MISS)
                // 跳過,等待字典收縮程序運行
                if (num && slots > DICT_HT_INITIAL_SIZE &&
                    (num*100/slots < 1)) break;
    
                /* 
                 * 樣本計數器
                 */
                // 已處理過期鍵計數器
                expired = 0;
                // 鍵的總 TTL 計數器
                ttl_sum = 0;
                // 總共處理的鍵計數器
                ttl_samples = 0;
    
                // 每次最多只能檢查 LOOKUPS_PER_LOOP 個鍵
                if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                    num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
    
                // 開始遍曆數據庫
                while (num--) {
                    dictEntry *de;
                    long long ttl;
    
                    // 從 expires 中隨機取出一個帶過期時間的鍵
                    if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                    // 計算 TTL
                    ttl = dictGetSignedIntegerVal(de)-now;
                    // 如果鍵已經過期,那麼刪除它,並將 expired 計數器增一
                    if (activeExpireCycleTryExpire(db,de,now)) expired++;
                    if (ttl < 0) ttl = 0;
                    // 累積鍵的 TTL
                    ttl_sum += ttl;
                    // 累積處理鍵的個數
                    ttl_samples++;
                }
    
                /* Update the average TTL stats for this database. */
                // 為這個數據庫更新平均 TTL 統計數據
                if (ttl_samples) {
                    // 計算當前平均值
                    long long avg_ttl = ttl_sum/ttl_samples;
                    
                    // 如果這是第一次設置數據庫平均 TTL ,那麼進行初始化
                    if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                    /* Smooth the value averaging with the previous one. */
                    // 取數據庫的上次平均 TTL 和今次平均 TTL 的平均值
                    db->avg_ttl = (db->avg_ttl+avg_ttl)/2;
                }
    
                // 我們不能用太長時間處理過期鍵,
                // 所以這個函數執行一定時間之後就要返回
    
                // 更新遍歷次數
                iteration++;
    
                // 每遍歷 16 次執行一次
                if ((iteration & 0xf) == 0 && /* check once every 16 iterations. */
                    (ustime()-start) > timelimit)
                {
                    // 如果遍歷次數正好是 16 的倍數
                    // 並且遍歷的時間超過了 timelimit
                    // 那麼斷開 timelimit_exit
                    timelimit_exit = 1;
                }
    
                // 已經超時了,返回
                if (timelimit_exit) return;
    
                // 如果已刪除的過期鍵占當前總數據庫帶過期時間的鍵數量的 25 %
                // 那麼不再遍歷
            } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
        }
    }
    

    hz調大將會提高Redis主動淘汰的頻率,如果你的Redis存儲中包含很多冷數據佔用內存過大的話,可以考慮將這個值調大,但Redis作者建議這個值不要超過100。我們實際線上將這個值調大到100,觀察到CPU會增加2%左右,但對冷數據的內存釋放速度確實有明顯的提高(通過觀察keyspace個數和used_memory大小)。

    可以看出timelimit和server.hz是一個倒數的關係,也就是說hz配置越大,timelimit就越小。換句話說是每秒鐘期望的主動淘汰頻率越高,則每次淘汰最長佔用時間就越短。這裏每秒鐘的最長淘汰佔用時間是固定的250ms(1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/100),而淘汰頻率和每次淘汰的最長時間是通過hz參數控制的。

    因此當redis中的過期key比率沒有超過25%之前,提高hz可以明顯提高掃描key的最小個數。假設hz為10,則一秒內最少掃描200個key(一秒調用10次*每次最少隨機取出20個key),如果hz改為100,則一秒內最少掃描2000個key;另一方面,如果過期key比率超過25%,則掃描key的個數無上限,但是cpu時間每秒鐘最多佔用250ms。

    當REDIS運行在主從模式時,只有主結點才會執行上述這兩種過期刪除策略,然後把刪除操作”del key”同步到從結點。

    if (server.active_expire_enabled && server.masterhost == NULL)  // 判斷是否是主節點 從節點不需要執行activeExpireCycle()函數。
            // 清除模式為 CYCLE_SLOW ,這個模式會盡量多清除過期鍵
            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
    

    隨機個數

    redis.config.ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 決定每次循環從數據庫 expire中隨機挑選值的個數

    逐出算法

    如果不限制 reids 對內存使用的限制,它將會使用全部的內存。可以通過 config.memory 來指定redis 對內存的使用量 。

    下面是redis 配置文件中的說明

     543 # Set a memory usage limit to the specified amount of bytes.
     544 # When the memory limit is reached Redis will try to remove keys
     545 # according to the eviction policy selected (see maxmemory-policy).
     546 #
     547 # If Redis can't remove keys according to the policy, or if the policy is
     548 # set to 'noeviction', Redis will start to reply with errors to commands
     549 # that would use more memory, like SET, LPUSH, and so on, and will continue
     550 # to reply to read-only commands like GET.
     551 #
     552 # This option is usually useful when using Redis as an LRU or LFU cache, or to
     553 # set a hard memory limit for an instance (using the 'noeviction' policy).
     554 #
     555 # WARNING: If you have replicas attached to an instance with maxmemory on,
     556 # the size of the output buffers needed to feed the replicas are subtracted
     557 # from the used memory count, so that network problems / resyncs will
     558 # not trigger a loop where keys are evicted, and in turn the output
     559 # buffer of replicas is full with DELs of keys evicted triggering the deletion
     560 # of more keys, and so forth until the database is completely emptied.
     561 #
     562 # In short... if you have replicas attached it is suggested that you set a lower
     563 # limit for maxmemory so that there is some free RAM on the system for replica
     564 # output buffers (but this is not needed if the policy is 'noeviction').
     
    將內存使用限制設置為指定的字節。當已達到內存限制Redis將根據所選的逐出策略(請參閱maxmemory策略)嘗試刪除數據。
    
    如果Redis無法根據逐出策略移除密鑰,或者策略設置為“noeviction”,Redis將開始對使用更多內存的命令(如set、LPUSH等)進行錯誤回復,並將繼續回復只讀命令,如GET。
    
    當將Redis用作LRU或LFU緩存或設置實例的硬內存限制(使用“noeviction”策略)時,此選項通常很有用。
    
    警告:如果將副本附加到啟用maxmemory的實例,則將從已用內存計數中減去饋送副本所需的輸出緩衝區的大小,這樣,網絡問題/重新同步將不會觸發收回密鑰的循環,而副本的輸出緩衝區將充滿收回的密鑰增量,從而觸發刪除更多鍵,依此類推,直到數據庫完全清空。
    
    簡而言之。。。如果附加了副本,建議您設置maxmemory的下限,以便系統上有一些空閑RAM用於副本輸出緩衝區(但如果策略為“noeviction”,則不需要此限制)。
    

    驅逐策略的配置

    Maxmemery-policy volatile-lru
    

    當前已用內存超過 maxmemory 限定時,觸發主動清理策略

    易失數據清理

    volatile-lru:只對設置了過期時間的key進行LRU(默認值)

    volatile-random:隨機刪除即將過期key

    volatile-ttl : 刪除即將過期的

    volatile-lfu:挑選最近使用次數最少的數據淘汰

    全部數據清理

    allkeys-lru : 刪除lru算法的key

    allkeys-lfu:挑選最近使用次數最少的數據淘汰

    allkeys-random:隨機刪除

    禁止驅逐

    (Redis 4.0 默認策略)

    noeviction : 永不過期,返回錯誤當mem_used內存已經超過maxmemory的設定,對於所有的讀寫請求都會觸發redis.c/freeMemoryIfNeeded(void)函數以清理超出的內存。注意這個清理過程是阻塞的,直到清理出足夠的內存空間。所以如果在達到maxmemory並且調用方還在不斷寫入的情況下,可能會反覆觸發主動清理策略,導致請求會有一定的延遲。

    清理時會根據用戶配置的maxmemory-policy來做適當的清理(一般是LRU或TTL),這裏的LRU或TTL策略並不是針對redis的所有key,而是以配置文件中的maxmemory-samples個key作為樣本池進行抽樣清理。

    maxmemory-samples在redis-3.0.0中的默認配置為5,如果增加,會提高LRU或TTL的精準度,redis作者測試的結果是當這個配置為10時已經非常接近全量LRU的精準度了,並且增加maxmemory-samples會導致在主動清理時消耗更多的CPU時間,建議:

    1 盡量不要觸發maxmemory,最好在mem_used內存佔用達到maxmemory的一定比例后,需要考慮調大hz以加快淘汰,或者進行集群擴容。

    2 如果能夠控制住內存,則可以不用修改maxmemory-samples配置;如果Redis本身就作為LRU cache服務(這種服務一般長時間處於maxmemory狀態,由Redis自動做LRU淘汰),可以適當調大maxmemory-samples。

    這裏提一句,實際上redis根本就不會準確的將整個數據庫中最久未被使用的鍵刪除,而是每次從數據庫中隨機取5個鍵並刪除這5個鍵里最久未被使用的鍵。上面提到的所有的隨機的操作實際上都是這樣的,這個5可以用過redis的配置文件中的maxmemeory-samples參數配置。

    數據逐出策略配置依據

    使用INFO命令輸出監控信息,查詢緩存int和miss的次數,根據業務需求調優Redis配置。

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

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

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

  • 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)

    基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)

    系列文章

    1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
    2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
    3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
    4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
    5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
    6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
    7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
    8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
    9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
    10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
    11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
    12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
    13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
    14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
    15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
    16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
    17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
    18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
    19. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
    20. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)
    21. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)
    22. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(二)
    23. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)
    24. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)
    25. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(五)
    26. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)
    27. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(七)
    28. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)
    29. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(九)
    30. 基於 abp vNext 和 .NET Core 開發博客項目 – 終結篇之發布項目

    上一篇完成了博客的分頁查詢文章列表頁面的數據綁定和分頁功能,本篇將繼續完成剩下的幾個頁面。

    在開始主題之前重新解決上一篇的最後一個問題,當點擊了頭部組件的/posts鏈接時直接強制刷新了頁面,經過查看文檔和實踐有了更好的解決方案。

    先將頭部組件Header.razor中的NavLink恢復成<NavLink class="menu-item" href="posts">Posts</NavLink>,不需要點擊事件了。

    然後在Posts.razor中添加生命周期函數OnParametersSetAsync(),在初始化完成后執行。

    /// <summary>
    /// 初始化完成后執行
    /// </summary>
    /// <returns></returns>
    protected override async Task OnParametersSetAsync()
    {
        if (!page.HasValue)
        {
            page = 1;
            await RenderPage(page);
        }
    }
    

    判斷當前page參數是否有值,有值的話說明請求肯定是來自於翻頁,當page沒有值的時候就說明是頭部的菜單點進來的。那麼此時給page賦值為1,調用API加載數據即可。

    分類列表

    Categories.razor是分類列表頁面,上篇文章已經實現了從API獲取數據的方法,所以這裏就很簡單了,指定接受類型,然後在生命周期初始化OnInitializedAsync()中去獲取數據。

    @code{
        /// <summary>
        /// categories
        /// </summary>
        private ServiceResult<IEnumerable<QueryCategoryDto>> categories;
    
        /// <summary>
        /// 初始化
        /// </summary>
        protected override async Task OnInitializedAsync()
        {
            // 獲取數據
            categories = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryCategoryDto>>>($"/blog/categories");
        }
    }
    

    當獲取到數據的時候進行綁定,沒有數據的時候還是显示加載中的組件<Loading />讓他轉圈圈。

    @if (categories == null)
    {
        <Loading />
    }
    else
    {
        <div class="container">
            <div class="post-wrap categories">
                <h2 class="post-title">-&nbsp;Categories&nbsp;-</h2>
                <div class="categories-card">
                    @if (categories.Success && categories.Result.Any())
                    {
                        @foreach (var item in categories.Result)
                        {
                            <div class="card-item">
                                <div class="categories">
                                    <a href="/category/@item.DisplayName/">
                                        <h3>
                                            <i class="iconfont iconcode" style="padding-right:3px"></i>
                                            @item.CategoryName
                                        </h3>
                                        <small>(@item.Count)</small>
                                    </a>
                                </div>
                            </div>
                        }
                    }
                    else
                    {
                        <ErrorTip />
                    }
                </div>
            </div>
        </div>
    }
    

    直接循環返回的數據列表categories.Result,綁定數據就好,當獲取失敗或者沒有返回數據的時候显示錯誤提示組件<ErrorTip />

    標籤列表

    Categories.razor是標籤列表頁面,和分類列表HTML結構差不多一樣的,除了返回類型和接口地址不一樣,將上面代碼複製過來改改即可。

    @code{
        /// <summary>
        /// tags
        /// </summary>
        private ServiceResult<IEnumerable<QueryTagDto>> tags;
    
        /// <summary>
        /// 初始化
        /// </summary>
        protected override async Task OnInitializedAsync()
        {
            // 獲取數據
            tags = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryTagDto>>>($"/blog/tags");
        }
    }
    
    @if (tags == null)
    {
        <Loading />
    }
    else
    {
        <div class="container">
            <div class="post-wrap tags">
                <h2 class="post-title">-&nbsp;Tags&nbsp;-</h2>
                <div class="tag-cloud-tags">
                    @if (tags.Success && tags.Result.Any())
                    {
                        @foreach (var item in tags.Result)
                        {
                            <a href="/tag/@item.DisplayName/">@item.TagName<small>(@item.Count)</small></a>
                        }
                    }
                    else
                    {
                        <ErrorTip />
                    }
                </div>
            </div>
        </div>
    }
    

    友鏈列表

    FriendLinks.razor是友情鏈接列表頁面,實現方式和上面兩個套路一模一樣。

    @code {
        /// <summary>
        /// friendlinks
        /// </summary>
        private ServiceResult<IEnumerable<FriendLinkDto>> friendlinks;
    
        /// <summary>
        /// 初始化
        /// </summary>
        protected override async Task OnInitializedAsync()
        {
            // 獲取數據
            friendlinks = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<FriendLinkDto>>>($"/blog/friendlinks");
        }
    }
    
    @if (friendlinks == null)
    {
        <Loading />
    }
    else
    {
        <div class="container">
            <div class="post-wrap categories">
                <h2 class="post-title">-&nbsp;FriendLinks&nbsp;-</h2>
                <div class="categories-card">
                    @if (friendlinks.Success && friendlinks.Result.Any())
                    {
                        @foreach (var item in friendlinks.Result)
                        {
                            <div class="card-item">
                                <div class="categories">
                                    <a target="_blank" href="@item.LinkUrl">
                                        <h3>@item.Title</h3>
                                    </a>
                                </div>
                            </div>
                        }
                    }
                    else
                    {
                        <ErrorTip />
                    }
                </div>
            </div>
        </div>
    }
    

    文章列表(分類)

    Posts.Category.razor是根據分類查詢文章列表頁面,他接受一個參數name,我們要根據name去API查詢數據然後綁定頁面即可。

    這裏的參數name實際上就是從標籤列表傳遞過來的DisplayName的值,它是一個比較友好的名稱,我們還要通過這個值去查詢真正的分類名稱進行展示,所以這裏需要調用兩個API,這點在設計API的時候沒有考慮好,我們其實可以將這兩個API合併變成一個,後續再進行優化吧,這裏就請求兩次。

    添加兩個接收參數:分類名稱和返回的文章列表數據。

    /// <summary>
    /// 分類名稱
    /// </summary>
    private string categoryName;
    
    /// <summary>
    /// 文章列表數據
    /// </summary>
    private ServiceResult<IEnumerable<QueryPostDto>> posts;
    

    然後在OnInitializedAsync()初始化方法中調用API獲取數據,賦值給變量。

    /// <summary>
    /// 初始化
    /// </summary>
    protected override async Task OnInitializedAsync()
    {
        // TODO:獲取數據,可以在API中合併這兩個請求。
        var category = await Http.GetFromJsonAsync<ServiceResult<string>>($"/blog/category?name={name}");
        posts = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryPostDto>>>($"/blog/posts/category?name={name}");
    
        if (category.Success)
        {
            categoryName = category.Result;
        }
    }
    

    有了數據,直接在頁面上進行循環綁定。

    @if (posts == null)
    {
        <Loading />
    }
    else
    {
        <div class="container">
            <div class="post-wrap tags">
                @if (categoryName != null)
                {
                    <h2 class="post-title">-&nbsp;Category&nbsp;·&nbsp;@categoryName&nbsp;-</h2>
                }
            </div>
            <div class="post-wrap archive">
                @if (posts.Success && posts.Result.Any())
                {
                    @foreach (var item in posts.Result)
                    {
                        <h3>@item.Year</h3>
                        @foreach (var post in item.Posts)
                        {
                            <article class="archive-item">
                                <NavLink href="@("/post"+post.Url)">@post.Title</NavLink>
                                <span class="archive-item-date">@post.CreationTime</span>
                            </article>
                        }
                    }
                }
                else
                {
                    <ErrorTip />
                }
            </div>
        </div>
    }
    

    文章列表(標籤)

    Posts.Tag.razor是根據標籤查詢文章列表,這個和分類查詢文章列表實現方式一樣,直接上代碼。

    @code {
        /// <summary>
        /// 標籤名稱參數
        /// </summary>
        [Parameter]
        public string name { get; set; }
    
        /// <summary>
        /// 標籤名稱
        /// </summary>
        private string tagName;
    
        /// <summary>
        /// 文章列表數據
        /// </summary>
        private ServiceResult<IEnumerable<QueryPostDto>> posts;
    
        /// <summary>
        /// 初始化
        /// </summary>
        protected override async Task OnInitializedAsync()
        {
            // TODO:獲取數據,可以在API中合併這兩個請求。
            var tag = await Http.GetFromJsonAsync<ServiceResult<string>>($"/blog/tag?name={name}");
            posts = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryPostDto>>>($"/blog/posts/tag?name={name}");
    
            if (tag.Success)
            {
                tagName = tag.Result;
            }
        }
    }
    
    @if (posts == null)
    {
        <Loading />
    }
    else
    {
        <div class="container">
            <div class="post-wrap tags">
                @if (tagName != null)
                {
                    <h2 class="post-title">-&nbsp;Tag&nbsp;·&nbsp;@tagName&nbsp;-</h2>
                }
            </div>
            <div class="post-wrap archive">
                @if (posts.Success && posts.Result.Any())
                {
                    @foreach (var item in posts.Result)
                    {
                        <h3>@item.Year</h3>
                        @foreach (var post in item.Posts)
                        {
                            <article class="archive-item">
                                <NavLink href="@("/post"+post.Url)">@post.Title</NavLink>
                                <span class="archive-item-date">@post.CreationTime</span>
                            </article>
                        }
                    }
                }
                else
                {
                    <ErrorTip />
                }
            </div>
        </div>
    }
    

    以上完成了以上幾個頁面的數據綁定,頁面之間的跳轉已經關聯起來了,然後還剩下文章詳情頁,大家可以先自己動手完成它,今天就到這裏,未完待續…

    開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

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

    【其他文章推薦】

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

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

    ※超省錢租車方案

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

    網頁設計最專業,超強功能平台可客製化

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

  • 小師妹學JavaIO之:MappedByteBuffer多大的文件我都裝得下

    小師妹學JavaIO之:MappedByteBuffer多大的文件我都裝得下

    目錄

    • 簡介
    • 虛擬地址空間
    • 詳解MappedByteBuffer
      • MapMode
    • MappedByteBuffer的最大值
    • MappedByteBuffer的使用
    • MappedByteBuffer要注意的事項
    • 總結

    簡介

    大大大,我要大!小師妹要讀取的文件越來越大,該怎麼幫幫她,讓程序在性能和速度上面得到平衡呢?快來跟F師兄一起看看吧。

    虛擬地址空間

    小師妹:F師兄,你有沒有發現,最近硬盤的價格真的是好便宜好便宜,1T的硬盤大概要500塊,平均1M五毛錢。現在下個電影都1G起步,這是不是意味着我們買入了大數據時代?

    沒錯,小師妹,硬件技術的進步也帶來了軟件技術的進步,兩者相輔相成,缺一不可。

    小師妹:F師兄,如果要是去讀取G級的文件,有沒有什麼快捷簡單的方法?

    更多精彩內容且看:

    • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
    • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
    • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
    • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

    還記得上次我們講的虛擬地址空間嗎?

    再把上次講的圖搬過來:

    通常來說我們的應用程序調用系統的接口從磁盤空間獲取Buffer數據,我們把自己的應用程序稱之為用戶空間,把系統的底層稱之為系統空間。

    傳統的IO操作,是操作系統講磁盤中的文件讀入到系統空間裏面,然後再拷貝到用戶空間中,供用戶使用。

    這中間多了一個Buffer拷貝的過程,如果這個量夠大的話,其實還是挺浪費時間的。

    於是有人在想了,拷貝太麻煩太耗時了,我們單獨劃出一塊內存區域,讓系統空間和用戶空間同時映射到同一塊地址不就省略了拷貝的步驟嗎?

    這個被劃出來的單獨的內存區域叫做虛擬地址空間,而不同空間到虛擬地址的映射就叫做Buffer Map。 Java中是有一個專門的MappedByteBuffer來代表這種操作。

    小師妹:F師兄,那這個虛擬地址空間和內存有什麼區別呢?有了內存還要啥虛擬地址空間?

    虛擬地址空間有兩個好處。

    第一個好處就是虛擬地址空間對於應用程序本身而言是獨立的,從而保證了程序的互相隔離和程序中地址的確定性。比如說一個程序如果運行在虛擬地址空間中,那麼它的空間地址是固定的,不管他運行多少次。如果直接使用內存地址,那麼可能這次運行的時候內存地址可用,下次運行的時候內存地址不可用,就會導致潛在的程序出錯。

    第二個好處就是虛擬空間地址可以比真實的內存地址大,這個大其實是對內存的使用做了優化,比如說會把很少使用的內存寫如磁盤,從而釋放出更多的內存來做更有意義的事情,而之前存儲到磁盤的數據,當真正需要的時候,再從磁盤中加載到內存中。

    這樣物理內存實際上可以看做虛擬空間地址的緩存。

    詳解MappedByteBuffer

    小師妹:MappedByteBuffer聽起來好神奇,怎麼使用它呢?

    我們先來看看MappedByteBuffer的定義:

    public abstract class MappedByteBuffer
        extends ByteBuffer
    

    它實際上是一個抽象類,具體的實現有兩個:

    class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
    
    class DirectByteBufferR extends DirectByteBuffer
    implements DirectBuffer
    

    分別是DirectByteBuffer和DirectByteBufferR。

    小師妹:F師兄,這兩個ByteBuffer有什麼區別呢?這個R是什麼意思?

    R代表的是ReadOnly的意思,可能是因為本身是個類的名字就夠長了,所以搞了個縮寫。但是也不寫個註解,讓人看起來十分費解….

    我們可以從RandomAccessFile的FilChannel中調用map方法獲得它的實例。

    我們看下map方法的定義:

     public abstract MappedByteBuffer map(MapMode mode, long position, long size)
            throws IOException;
    

    MapMode代表的是映射的模式,position表示是map開始的地址,size表示是ByteBuffer的大小。

    MapMode

    小師妹:F師兄,文件有隻讀,讀寫兩種模式,是不是MapMode也包含這兩類?

    對的,其實NIO中的MapMode除了這兩個之外,還有一些其他很有趣的用法。

    • FileChannel.MapMode.READ_ONLY 表示只讀模式
    • FileChannel.MapMode.READ_WRITE 表示讀寫模式
    • FileChannel.MapMode.PRIVATE 表示copy-on-write模式,這個模式和READ_ONLY有點相似,它的操作是先對原數據進行拷貝,然後可以在拷貝之後的Buffer中進行讀寫。但是這個寫入並不會影響原數據。可以看做是數據的本地拷貝,所以叫做Private。

    基本的MapMode就這三種了,其實除了基礎的MapMode,還有兩種擴展的MapMode:

    • ExtendedMapMode.READ_ONLY_SYNC 同步的讀
    • ExtendedMapMode.READ_WRITE_SYNC 同步的讀寫

    MappedByteBuffer的最大值

    小師妹:F師兄,既然可以映射到虛擬內存空間,那麼這個MappedByteBuffer是不是可以無限大?

    當然不是了,首先虛擬地址空間的大小是有限制的,如果是32位的CPU,那麼一個指針佔用的地址就是4個字節,那麼能夠表示的最大值是0xFFFFFFFF,也就是4G。

    另外我們看下map方法中size的類型是long,在java中long能夠表示的最大值是0x7fffffff,也就是2147483647字節,換算一下大概是2G。也就是說MappedByteBuffer的最大值是2G,一次最多只能map 2G的數據。

    MappedByteBuffer的使用

    小師妹,F師兄我們來舉兩個使用MappedByteBuffer讀寫的例子吧。

    善!

    先看一下怎麼使用MappedByteBuffer來讀數據:

    public void readWithMap() throws IOException {
            try (RandomAccessFile file = new RandomAccessFile(new File("src/main/resources/big.www.flydean.com"), "r"))
            {
                //get Channel
                FileChannel fileChannel = file.getChannel();
                //get mappedByteBuffer from fileChannel
                MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
                // check buffer
                log.info("is Loaded in physical memory: {}",buffer.isLoaded());  //只是一個提醒而不是guarantee
                log.info("capacity {}",buffer.capacity());
                //read the buffer
                for (int i = 0; i < buffer.limit(); i++)
                {
                    log.info("get {}", buffer.get());
                }
            }
        }
    

    然後再看一個使用MappedByteBuffer來寫數據的例子:

    public void writeWithMap() throws IOException {
            try (RandomAccessFile file = new RandomAccessFile(new File("src/main/resources/big.www.flydean.com"), "rw"))
            {
                //get Channel
                FileChannel fileChannel = file.getChannel();
                //get mappedByteBuffer from fileChannel
                MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096 * 8 );
                // check buffer
                log.info("is Loaded in physical memory: {}",buffer.isLoaded());  //只是一個提醒而不是guarantee
                log.info("capacity {}",buffer.capacity());
                //write the content
                buffer.put("www.flydean.com".getBytes());
            }
        }
    

    MappedByteBuffer要注意的事項

    小師妹:F師兄,MappedByteBuffer因為使用了內存映射,所以讀寫的速度都會有所提升。那麼我們在使用中應該注意哪些問題呢?

    MappedByteBuffer是沒有close方法的,即使它的FileChannel被close了,MappedByteBuffer仍然處於打開狀態,只有JVM進行垃圾回收的時候才會被關閉。而這個時間是不確定的。

    總結

    本文再次介紹了虛擬地址空間和MappedByteBuffer的使用。

    本文的例子https://github.com/ddean2009/learn-java-io-nio

    本文作者:flydean程序那些事

    本文鏈接:http://www.flydean.com/io-nio-mappedbytebuffer/

    本文來源:flydean的博客

    歡迎關注我的公眾號:程序那些事,更多精彩等着您!

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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

  • 【深度思考】JDK8中日期類型該如何使用?

    【深度思考】JDK8中日期類型該如何使用?

    在JDK8之前,處理日期時間,我們主要使用3個類,DateSimpleDateFormatCalendar

    這3個類在使用時都或多或少的存在一些問題,比如SimpleDateFormat不是線程安全的,

    比如DateCalendar獲取到的月份是0到11,而不是現實生活中的1到12,關於這一點,《阿里巴巴Java開發手冊》中也有提及,因為很容易犯錯:

    不過,JDK8推出了全新的日期時間處理類解決了這些問題,比如InstantLocalDateLocalTimeLocalDateTimeDateTimeFormatter,在《阿里巴巴Java開發手冊》中也推薦使用Instant

    LocalDateTimeDateTimeFormatter

    但我發現好多項目中其實並沒有使用這些類,使用的還是之前的DateSimpleDateFormatCalendar,所以本篇博客就講解下JDK8新推出的日期時間類,主要是下面幾個:

    1. Instant
    2. LocalDate
    3. LocalTime
    4. LocalDateTime
    5. DateTimeFormatter

    1. Instant

    1.1 獲取當前時間

    既然Instant可以代替Date類,那它肯定可以獲取當前時間:

    Instant instant = Instant.now();
    System.out.println(instant);
    

    輸出結果:

    2020-06-10T08:22:13.759Z

    細心的你會發現,這個時間比北京時間少了8個小時,如果要輸出北京時間,可以加上默認時區:

    System.out.println(instant.atZone(ZoneId.systemDefault()));
    

    輸出結果:

    2020-06-10T16:22:13.759+08:00[Asia/Shanghai]

    1.2 獲取時間戳

    Instant instant = Instant.now();
    
    // 當前時間戳:單位為秒
    System.out.println(instant.getEpochSecond());
    // 當前時間戳:單位為毫秒
    System.out.println(instant.toEpochMilli());
    

    輸出結果:

    1591777752

    1591777752613

    當然,也可以通過System.currentTimeMillis()獲取當前毫秒數。

    1.3 將long轉換為Instant

    1)根據秒數時間戳轉換:

    Instant instant = Instant.now();
    System.out.println(instant);
    
    long epochSecond = instant.getEpochSecond();
    System.out.println(Instant.ofEpochSecond(epochSecond));
    System.out.println(Instant.ofEpochSecond(epochSecond, instant.getNano()));
    

    輸出結果:

    2020-06-10T08:40:54.046Z

    2020-06-10T08:40:54Z

    2020-06-10T08:40:54.046Z

    2)根據毫秒數時間戳轉換:

    Instant instant = Instant.now();
    System.out.println(instant);
    
    long epochMilli = instant.toEpochMilli();
    System.out.println(Instant.ofEpochMilli(epochMilli));
    

    輸出結果:

    2020-06-10T08:43:25.607Z

    2020-06-10T08:43:25.607Z

    1.4 將String轉換為Instant

    String text = "2020-06-10T08:46:55.967Z";
    Instant parseInstant = Instant.parse(text);
    System.out.println("秒時間戳:" + parseInstant.getEpochSecond());
    System.out.println("豪秒時間戳:" + parseInstant.toEpochMilli());
    System.out.println("納秒:" + parseInstant.getNano());
    

    輸出結果:

    秒時間戳:1591778815

    豪秒時間戳:1591778815967

    納秒:967000000

    如果字符串格式不對,比如修改成2020-06-10T08:46:55.967,就會拋出java.time.format.DateTimeParseException異常,如下圖所示:

    2. LocalDate

    2.1 獲取當前日期

    使用LocalDate獲取當前日期非常簡單,如下所示:

    LocalDate today = LocalDate.now();
    System.out.println("today: " + today);
    

    輸出結果:

    today: 2020-06-10

    不用任何格式化,輸出結果就非常友好,如果使用Date,輸出這樣的格式,還得配合SimpleDateFormat指定yyyy-MM-dd進行格式化,一不小心還會出個bug,比如去年年底很火的1個bug,我當時還是截了圖的:

    這2個好友是2019/12/31關注我的,但我2020年1月2號查看時,卻显示成了2020/12/31,為啥呢?格式化日期時格式寫錯了,應該是yyyy/MM/dd,卻寫成了YYYY/MM/dd,剛好那周跨年,就显示成下一年,也就是2020年了,當時好幾個博主寫過文章解析原因,我這裏就不做過多解釋了。

    划重點:都說到這了,給大家安利下我新註冊的公眾號「申城異鄉人」,歡迎大家關注,更多原創文章等着你哦,哈哈。

    2.2 獲取年月日

    LocalDate today = LocalDate.now();
    
    int year = today.getYear();
    int month = today.getMonthValue();
    int day = today.getDayOfMonth();
    
    System.out.println("year: " + year);
    System.out.println("month: " + month);
    System.out.println("day: " + day);
    

    輸出結果:

    year: 2020

    month: 6

    day: 10

    獲取月份終於返回1到12了,不像java.util.Calendar獲取月份返回的是0到11,獲取完還得加1。

    2.3 指定日期

    LocalDate specifiedDate = LocalDate.of(2020, 6, 1);
    System.out.println("specifiedDate: " + specifiedDate);
    

    輸出結果:

    specifiedDate: 2020-06-01

    如果確定月份,推薦使用另一個重載方法,使用枚舉指定月份:

    LocalDate specifiedDate = LocalDate.of(2020, Month.JUNE, 1);
    

    2.4 比較日期是否相等

    LocalDate localDate1 = LocalDate.now();
    LocalDate localDate2 = LocalDate.of(2020, 6, 10);
    if (localDate1.equals(localDate2)) {
        System.out.println("localDate1 equals localDate2");
    }
    

    輸出結果:

    localDate1 equals localDate2

    2.5 獲取日期是本周/本月/本年的第幾天

    LocalDate today = LocalDate.now();
    
    System.out.println("Today:" + today);
    System.out.println("Today is:" + today.getDayOfWeek());
    System.out.println("今天是本周的第" + today.getDayOfWeek().getValue() + "天");
    System.out.println("今天是本月的第" + today.getDayOfMonth() + "天");
    System.out.println("今天是本年的第" + today.getDayOfYear() + "天");
    

    輸出結果:

    Today:2020-06-11

    Today is:THURSDAY

    今天是本周的第4天

    今天是本月的第11天

    今天是本年的第163天

    2.6 判斷是否為閏年

    LocalDate today = LocalDate.now();
    
    System.out.println(today.getYear() + " is leap year:" + today.isLeapYear());
    

    輸出結果:

    2020 is leap year:true

    3. LocalTime

    3.1 獲取時分秒

    如果使用java.util.Date,那代碼是下面這樣的:

    Date date = new Date();
    
    int hour = date.getHours();
    int minute = date.getMinutes();
    int second = date.getSeconds();
    
    System.out.println("hour: " + hour);
    System.out.println("minute: " + minute);
    System.out.println("second: " + second);
    

    輸出結果:

    注意事項:這幾個方法已經過期了,因此強烈不建議在項目中使用:

    如果使用java.util.Calendar,那代碼是下面這樣的:

    Calendar calendar = Calendar.getInstance();
    
    // 12小時制
    int hourOf12 = calendar.get(Calendar.HOUR);
    // 24小時制
    int hourOf24 = calendar.get(Calendar.HOUR_OF_DAY);
    int minute = calendar.get(Calendar.MINUTE);
    int second = calendar.get(Calendar.SECOND);
    int milliSecond = calendar.get(Calendar.MILLISECOND);
    
    System.out.println("hourOf12: " + hourOf12);
    System.out.println("hourOf24: " + hourOf24);
    System.out.println("minute: " + minute);
    System.out.println("second: " + second);
    System.out.println("milliSecond: " + milliSecond);
    

    輸出結果:

    注意事項:獲取小時時,有2個選項,1個返回12小時制的小時數,1個返回24小時制的小時數,因為現在是晚上8點,所以calendar.get(Calendar.HOUR)返回8,而calendar.get(Calendar.HOUR_OF_DAY)返回20。

    如果使用java.time.LocalTime,那代碼是下面這樣的:

    LocalTime localTime = LocalTime.now();
    System.out.println("localTime:" + localTime);
    
    int hour = localTime.getHour();
    int minute = localTime.getMinute();
    int second = localTime.getSecond();
    
    System.out.println("hour: " + hour);
    System.out.println("minute: " + minute);
    System.out.println("second: " + second);
    

    輸出結果:

    可以看出,LocalTime只有時間沒有日期。

    4. LocalDateTime

    4.1 獲取當前時間

    LocalDateTime localDateTime = LocalDateTime.now();
    System.out.println("localDateTime:" + localDateTime);
    

    輸出結果:

    localDateTime: 2020-06-11T11:03:21.376

    4.2 獲取年月日時分秒

    LocalDateTime localDateTime = LocalDateTime.now();
    System.out.println("localDateTime: " + localDateTime);
    
    System.out.println("year: " + localDateTime.getYear());
    System.out.println("month: " + localDateTime.getMonthValue());
    System.out.println("day: " + localDateTime.getDayOfMonth());
    System.out.println("hour: " + localDateTime.getHour());
    System.out.println("minute: " + localDateTime.getMinute());
    System.out.println("second: " + localDateTime.getSecond());
    

    輸出結果:

    4.3 增加天數/小時

    LocalDateTime localDateTime = LocalDateTime.now();
    System.out.println("localDateTime: " + localDateTime);
    
    LocalDateTime tomorrow = localDateTime.plusDays(1);
    System.out.println("tomorrow: " + tomorrow);
    
    LocalDateTime nextHour = localDateTime.plusHours(1);
    System.out.println("nextHour: " + nextHour);
    

    輸出結果:

    localDateTime: 2020-06-11T11:13:44.979

    tomorrow: 2020-06-12T11:13:44.979

    nextHour: 2020-06-11T12:13:44.979

    LocalDateTime還提供了添加年、周、分鐘、秒這些方法,這裏就不一一列舉了:

    4.4 減少天數/小時

    LocalDateTime localDateTime = LocalDateTime.now();
    System.out.println("localDateTime: " + localDateTime);
    
    LocalDateTime yesterday = localDateTime.minusDays(1);
    System.out.println("yesterday: " + yesterday);
    
    LocalDateTime lastHour = localDateTime.minusHours(1);
    System.out.println("lastHour: " + lastHour);
    

    輸出結果:

    localDateTime: 2020-06-11T11:20:38.896

    yesterday: 2020-06-10T11:20:38.896

    lastHour: 2020-06-11T10:20:38.896

    類似的,LocalDateTime還提供了減少年、周、分鐘、秒這些方法,這裏就不一一列舉了:

    4.5 獲取時間是本周/本年的第幾天

    LocalDateTime localDateTime = LocalDateTime.now();
    System.out.println("localDateTime: " + localDateTime);
    
    System.out.println("DayOfWeek: " + localDateTime.getDayOfWeek().getValue());
    System.out.println("DayOfYear: " + localDateTime.getDayOfYear());
    

    輸出結果:

    localDateTime: 2020-06-11T11:32:31.731

    DayOfWeek: 4

    DayOfYear: 163

    5. DateTimeFormatter

    JDK8中推出了java.time.format.DateTimeFormatter來處理日期格式化問題,《阿里巴巴Java開發手冊》中也是建議使用DateTimeFormatter代替SimpleDateFormat

    5.1 格式化LocalDate

    LocalDate localDate = LocalDate.now();
    
    System.out.println("ISO_DATE: " + localDate.format(DateTimeFormatter.ISO_DATE));
    System.out.println("BASIC_ISO_DATE: " + localDate.format(DateTimeFormatter.BASIC_ISO_DATE));
    System.out.println("ISO_WEEK_DATE: " + localDate.format(DateTimeFormatter.ISO_WEEK_DATE));
    System.out.println("ISO_ORDINAL_DATE: " + localDate.format(DateTimeFormatter.ISO_ORDINAL_DATE));
    

    輸出結果:

    如果提供的格式無法滿足你的需求,你還可以像以前一樣自定義格式:

    LocalDate localDate = LocalDate.now();
    
    System.out.println("yyyy/MM/dd: " + localDate.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")));
    

    輸出結果:

    yyyy/MM/dd: 2020/06/11

    5.2 格式化LocalTime

    LocalTime localTime = LocalTime.now();
    System.out.println(localTime);
    System.out.println("ISO_TIME: " + localTime.format(DateTimeFormatter.ISO_TIME));
    System.out.println("HH:mm:ss: " + localTime.format(DateTimeFormatter.ofPattern("HH:mm:ss")));
    

    輸出結果:

    14:28:35.230

    ISO_TIME: 14:28:35.23

    HH:mm:ss: 14:28:35

    5.3 格式化LocalDateTime

    LocalDateTime localDateTime = LocalDateTime.now();
    System.out.println(localDateTime);
    System.out.println("ISO_DATE_TIME: " + localDateTime.format(DateTimeFormatter.ISO_DATE_TIME));
    System.out.println("ISO_DATE: " + localDateTime.format(DateTimeFormatter.ISO_DATE));
    

    輸出結果:

    2020-06-11T14:33:18.303

    ISO_DATE_TIME: 2020-06-11T14:33:18.303

    ISO_DATE: 2020-06-11

    6. 類型相互轉換

    6.1 Instant轉Date

    JDK8中,Date新增了from()方法,將Instant轉換為Date,代碼如下所示:

    Instant instant = Instant.now();
    System.out.println(instant);
    
    Date dateFromInstant = Date.from(instant);
    System.out.println(dateFromInstant);
    

    輸出結果:

    2020-06-11T06:39:34.979Z

    Thu Jun 11 14:39:34 CST 2020

    6.2 Date轉Instant

    JDK8中,Date新增了toInstant方法,將Date轉換為Instant,代碼如下所示:

    Date date = new Date();
    Instant dateToInstant = date.toInstant();
    System.out.println(date);
    System.out.println(dateToInstant);
    

    輸出結果:

    Thu Jun 11 14:46:12 CST 2020

    2020-06-11T06:46:12.112Z

    6.3 Date轉LocalDateTime

    Date date = new Date();
    Instant instant = date.toInstant();
    LocalDateTime localDateTimeOfInstant = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
    System.out.println(date);
    System.out.println(localDateTimeOfInstant);
    

    輸出結果:

    Thu Jun 11 14:51:07 CST 2020

    2020-06-11T14:51:07.904

    6.4 Date轉LocalDate

    Date date = new Date();
    Instant instant = date.toInstant();
    LocalDateTime localDateTimeOfInstant = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
    LocalDate localDate = localDateTimeOfInstant.toLocalDate();
    System.out.println(date);
    System.out.println(localDate);
    

    輸出結果:

    Thu Jun 11 14:59:38 CST 2020

    2020-06-11

    可以看出,Date是先轉換為Instant,再轉換為LocalDateTime,然後通過LocalDateTime獲取LocalDate

    6.5 Date轉LocalTime

    Date date = new Date();
    Instant instant = date.toInstant();
    LocalDateTime localDateTimeOfInstant = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
    LocalTime toLocalTime = localDateTimeOfInstant.toLocalTime();
    System.out.println(date);
    System.out.println(toLocalTime);
    

    輸出結果:

    Thu Jun 11 15:06:14 CST 2020

    15:06:14.531

    可以看出,Date是先轉換為Instant,再轉換為LocalDateTime,然後通過LocalDateTime獲取LocalTime

    6.6 LocalDateTime轉Date

    LocalDateTime localDateTime = LocalDateTime.now();
    
    Instant toInstant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
    Date dateFromInstant = Date.from(toInstant);
    System.out.println(localDateTime);
    System.out.println(dateFromInstant);
    

    輸出結果:

    2020-06-11T15:12:11.600

    Thu Jun 11 15:12:11 CST 2020

    6.7 LocalDate轉Date

    LocalDate today = LocalDate.now();
    
    LocalDateTime localDateTime = localDate.atStartOfDay();
    Instant toInstant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
    Date dateFromLocalDate = Date.from(toInstant);
    System.out.println(dateFromLocalDate);
    

    輸出結果:

    Thu Jun 11 00:00:00 CST 2020

    6.8 LocalTime轉Date

    LocalDate localDate = LocalDate.now();
    LocalTime localTime = LocalTime.now();
    
    LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
    Instant instantFromLocalTime = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
    Date dateFromLocalTime = Date.from(instantFromLocalTime);
    
    System.out.println(dateFromLocalTime);
    

    輸出結果:

    Thu Jun 11 15:24:18 CST 2020

    7. 總結

    JDK8推出了全新的日期時間類,如InstantLocaleDateLocalTimeLocalDateTimeDateTimeFormatter,設計比之前更合理,也是線程安全的。

    《阿里巴巴Java開發規範》中也推薦使用Instant代替DateLocalDateTime 代替 CalendarDateTimeFormatter 代替 SimpleDateFormat

    因此,如果條件允許,建議在項目中使用,沒有使用的,可以考慮升級下。

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

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

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

  • 看完這些黑科技,我覺得寶馬的真實身份其實是科技公司!

    看完這些黑科技,我覺得寶馬的真實身份其實是科技公司!

    寶馬通過一台基於全新BMW 5系的原型車展示了未來高度自動駕駛汽車極為個性化、智能化的駕駛感受。在高度自動駕駛狀態下,駕駛者的雙手和雙腳都得到了解放,無需手握方向盤和控制油門、剎車。同時,BMW實時交通信號系統還可以預測前方下一組信號燈情況,讓駕駛者更好地進行選擇。

    (拉斯維加斯/北京)2017年1月5日,一年一度的科技盛會、2017國際消費电子展在拉斯維加斯拉開帷幕。寶馬集團攜一系列前瞻理念和科技成果亮相,展現了其在智能互聯、未來汽車內部設計、控制與显示系統、自動駕駛等創新領域蓬勃的創造力。

    寶馬集團正在成為引領数字出行生活的重要力量。在其願景中,自動駕駛技術將為駕駛者帶來更多選擇和自由;豐富的智能互聯服務圍繞人的需求,將車輛與用戶的数字生活無縫對接,讓出行和生活更高效、便捷、充滿樂趣。重要的是,這些離我們的生活並不遙遠,近年在CES上亮相的手勢控制、遠程3D環視影像等,均迅速應用於量產車型,體現了寶馬在行業內的領先地位。

    這不是科幻大片,是你未來的汽車

    在CES展台,寶馬集團通過BMW i Inside Future未來內室研究項目展示出,未來配備自動駕駛技術的汽車,其座艙將根據用戶需求,在休息室、辦公室和娛樂室之間實現自由切換。

    未來,車內空間的氛圍和控制方式將取決於駕駛模式。在主動駕駛模式下,主動駕駛的功能將處於車內中心位置。在高度自動駕駛模式下,系統將显示更多的舒適、信息娛樂和通訊功能。導航系統可以推薦適合高度或完全自動駕駛的行車路線,並在到達路口時發出提醒。未來,自動駕駛將首先應用於高速或單向行駛道路。

    對人機交互模式的創新是未來車輛的重要課題。寶馬在最近的兩屆CES上都帶來了創新的人機交互系統——手勢控制和AirTouch手勢控制系統。其中手勢控制已經應用於量產的新BMW 7系和全新BMW 5系車型。今年,寶馬又帶來了BMW HoloActive觸控系統,將人機交互體驗提升至新的高度。

    BMW HoloActive觸控系統的原理與平視显示系統類似,通過反射原理在中控台位置投射出一塊“懸浮”屏幕,駕駛者通過指尖“點擊”虛擬屏幕來控制車輛,呈現出科幻大片的既視感。這套系統通過高敏感度的攝像頭捕捉駕駛者指尖的動作。在超聲波裝置的配合下,客戶的手指可以感受到輕微的壓力,模擬了傳統觸控屏的體驗,讓操作更符合人們的習慣。

    未來的車輛內,駕駛者和乘客可以各自享受音樂而不互相干擾。首次展出的音效裝置BMW Sound Curtain通過座椅頭枕發射出不同的聲音信號,為座位上的用戶提供個性化的專屬娛樂信息。一個可摺疊的大尺寸屏幕可從車內頂篷延伸出來,進一步豐富後排乘客的車內互聯生活。

    擁有一台高度互聯的自動駕駛汽車是一種怎樣的體驗?

    寶馬通過一台基於全新BMW 5系的原型車展示了未來高度自動駕駛汽車極為個性化、智能化的駕駛感受。在高度自動駕駛狀態下,駕駛者的雙手和雙腳都得到了解放,無需手握方向盤和控制油門、剎車。同時,BMW實時交通信號系統還可以預測前方下一組信號燈情況,讓駕駛者更好地進行選擇。

    在自動泊車功能展示中,抵達停車場時,車輛自動與泊車管理服務進行連接,显示屏會提示駕駛者可以使用預約的停車位。駕駛者與乘客下車后,車輛隨即啟動自動泊車功能。BMW雲端互聯將在車輛停好後向駕駛者推送提示信息。通過全新BMW 5系中首次配備的環視影像系統,用戶還可以通過BMW雲端互聯應用實時查看車輛情況。

    在自動駕駛帶來的閑暇時間里,駕駛者可以盡情享受豐富的智能互聯功能。例如,在車輛行進途中,前排乘客可以通過BMW增強手勢控制系統、獲取途經場所的信息,如娛樂場所的節目單,還可以直接訂票。

    在開放式移動雲的支持下,BMW 雲端互聯可以整合豐富的應用,讓人們在車內便捷地處理各種事務。比如已經在家用電腦上推出的個人数字助理“微軟小娜(Cortana)”也可以在BMW汽車上使用。在駕駛過程中,用戶可以通過語音控制讓小娜推薦就餐地點並預定位置。

    通過與亞馬遜prime Now速遞服務合作,未來在行車途中預約收取快遞也成為可能。設想一下,用戶正前往一個生日聚會,但忘了購買禮物,通過這一功能,用戶可以從容地在車內在線購物。prime Now與開放式移動雲將根據車輛位置、路線和實時交通信息計算出最佳交付地點;車輛到達交付點之後,prime Now的員工將把貨物送到用戶的手中。BMW 雲端互聯無疑為智能、便捷的数字生活帶來了巨大的想象空間。BMW 雲端互聯已於2016年12月在中國正式上線,未來這些功能也將逐步升級到現有版本中。

    BMW不僅在車庫,還可以在客廳——智能出行和智能生活相結合

    在2017 CES上,寶馬集團還將全新的智能互聯科技從車內延伸到用戶的家中,標志著BMW 雲端互聯與家居環境的結合。未來,通過創新的智能終端BMW Conncted Window,用戶在家也可以享受BMW雲端互聯的豐富功能。

    當用戶開始新的一天時,BMW Connected Window的界面將显示溫暖的問候,與此同時,用戶每天的出行日程會按照時間軸進行显示,出行目的地,建議出發時間、天氣情況等信息一目瞭然。BMW Connected Window的虛擬界面通過手勢來操作,與觸摸屏一樣直觀。更新信息也可以通過BMW 雲端互聯方便地添加到日程中並與其他智能設備保持同步,幫助用戶胸有成竹地開始新一天的生活。

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

    【其他文章推薦】

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

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

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

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

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

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

  • 吉利大法好!沃爾沃去年銷量創紀錄,換髮第二春!

    吉利大法好!沃爾沃去年銷量創紀錄,換髮第二春!

    2016年沃爾沃汽車進一步確立了在自動駕駛、電氣化及安全領域的領先地位,建立了新的商業聯盟,並不斷推出全新的產品,打造立足全球的生產製造基地。2016年沃爾沃汽車斥資5億美元在美國南卡羅來納州興建工廠,將生產基於SpA架構的車型,初期將聘用2,000餘名員工。

    沃爾沃汽車集團近日發布2016年銷售業績显示,2016年沃爾沃汽車全球共實現銷量534,332輛,同比增長6.2%,連續三年創銷量紀錄。2016年沃爾沃汽車在全球各大市場銷量齊頭並進,在中國和北美兩大市場均實現了兩位數增幅,西歐市場表現強勁,沃爾沃汽車全球復興第二階段持續加速。

    S90

    沃爾沃全新90系車型2016年銷量飄紅,其中XC90車型銷量較2015年激增了125%,印證了沃爾沃全新的設計語言及創新科技在全球取得成功,為未來沃爾沃全新車型的上市,打下堅實的基礎。同時,沃爾沃XC60車型年銷量達到161,092輛,自2008年投放市場以來,連續九年屢創銷量紀錄。

    XC90

    2016年沃爾沃汽車在中國市場銷量達90,930輛,同比增長11.5%。中國依然是沃爾沃汽車全球最大單一市場。其中,國產沃爾沃XC60和S60L是沃爾沃汽車在中國市場最暢銷的車型。

    沃爾沃汽車2016年在美國市場銷量增幅達18.1%,是美國增速最快的豪華汽車品牌之一,實現年銷量82,726輛。其中沃爾沃XC90和XC60最受美國消費者歡迎,市場表現出眾。得益於德國、英國、法國和意大利等主要市場強勁業績的推動,2016年沃爾沃汽車在西歐銷量增長4.1%,達到206,144輛。

    沃爾沃汽車2016年實現銷量破紀錄的同時,通過全球復興和品牌重新定位持續強化與其他豪華品牌的競爭優勢。2016年沃爾沃汽車進一步確立了在自動駕駛、電氣化及安全領域的領先地位,建立了新的商業聯盟,並不斷推出全新的產品,打造立足全球的生產製造基地。

    2016年沃爾沃汽車斥資5億美元在美國南卡羅來納州興建工廠,將生產基於SpA架構的車型,初期將聘用2,000餘名員工。2016年沃爾沃汽車發布了中國製造戰略,在提升產能的同時,將中國打造成了面向全球市場的生產和出口基地。沃爾沃大慶工廠生產旗艦級全新S90系家族,成都工廠生產現款60系及未來全新60系車型,基於CMA架構的全新40系車型正在規劃中,將在距上海以南350公里的路橋工廠投產。

    2016年9月,隨着沃爾沃全新V90 Cross Country旅行越界車的上市,沃爾沃全新90系車型已全部完成換代。其中XC90車型更是榮獲120多個國際大獎,充分展現了沃爾沃全新SpA架構在設計、技術等方面的領先優勢。

    V90 Cross Country

    未來幾年,沃爾沃汽車將以每年兩款全新車型的速度完成全部產品換代。2017年將推出基於SpA架構的全新XC60車型,以及基於CMA架構的首款40系產品——全新XC40車型;在新能源領域,2016年沃爾沃汽車發布了全方位的電氣化戰略,將在全系車型中引入插電式混合動力系統,並在2019年之前推出首款純電動車,到2025年將實現新能源車型累計銷量100萬輛。

    2016年沃爾沃汽車與優步(Uber)公司攜手合作開發自動駕駛技術,與瑞典奧托立夫公司(Autoliv)合作建立了合資公司——Zenuity,致力於設計和開發自動駕駛軟件及高級駕駛輔助系統,將為快速發展的全球市場提供自動駕駛軟件等服務。這也是豪華汽車品牌首次與一線供應商聯手開發相關技術,將為汽車行業帶來重大變革。

    2017年沃爾沃汽車將在瑞典總部哥德堡進一步推動Drive Me自動駕駛測試項目。作為目前全球最先進、最前沿的自動駕駛測試項目,沃爾沃汽車將提供100輛XC90自動駕駛汽車用於普通居民在真實的日常環境中出行使用。未來,沃爾沃汽車還將在中國與英國啟動DriveMe自動駕駛測試項目。

    隨着全球復興進度的加快及全新商業模式的拓展,沃爾沃汽車不僅是全球豪華汽車製造商,還將成為了一家全球高端移動出行公司。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

  • 車主們說這款有着跑車外觀的車 油耗出奇地低

    車主們說這款有着跑車外觀的車 油耗出奇地低

    只是高速時的風噪略大、儲物空間比較少,應該在四個門板多做一些儲物格。關於空間,我的身高176cm,把駕駛位置調好后,我在後排還有一拳多的腿部空間。而我的車型是2。0L的平均百公里油耗是7。8L。車主:Bestss購買車型:三廂 1。

    10多萬的合資A級車選擇非常多,要論最個性、最具動感外觀的車型,馬自達3昂克賽拉絕對是最具實力的車型之一。下面我們就來看看這款車的車主們都對它有哪些評價。

    長安馬自達-馬自達3 Axela昂克賽拉

    指導價:11.49-15.99萬

    車主:人稱啊明

    購買車型:三廂 1.5L自動豪華型

    裸車價格:12.89萬

    每一個男人都有一個跑車夢,昂克賽拉的外觀很像跑車,還有聰明的變速箱、讓人滿意的低油耗、起步快等優點,所以我最終選擇了這款車。

    它的操控真的不錯,給人的感覺是穩、實、准,轉向手感不錯,採用了四輪獨立懸架,支撐性好,在同級車型中性價比很高。

    目前我的車行駛了快8000公里了,平均百公里油耗只有7.1L!創馳藍天技術真不是蓋的。

    車主:佛山小偉

    購買車型:三廂 2.0L自動旗艦型

    裸車價格:14.99萬

    我對“魂動”的設計外觀和駕駛體驗最滿意,它指向精準、換擋果斷、動力也充沛。只是高速時的風噪略大、儲物空間比較少,應該在四個門板多做一些儲物格。

    關於空間,我的身高176cm,把駕駛位置調好后,我在後排還有一拳多的腿部空間。而我的車型是2.0L的平均百公里油耗是7.8L。

    車主:Bestss

    購買車型:三廂 1.5L手動豪華型

    裸車價格:12.29萬

    外觀和內飾就不用我多說了,大多數買昂克賽拉的人都是奔着漂亮的外觀去的!它的1.5L缸內直噴發動機動力夠用,2000轉以後的動力有比較大的爆發。

    行駛起來胎噪有些大,可能是輪胎側重抓地力的原因,還有就是車漆有些薄。

    目前我的車行駛了15000公里了,我對它比較滿意,目前的平均油耗是7.2L,油耗不算高!

    編者點評:

    昂克賽拉是一款性格鮮明的車型,它堅持採用四輪獨立懸挂、偏向性能的輪胎、AT變速箱等,讓它的操控性出色,如果你喜歡駕駛感受好的A級車,昂克賽拉絕對是一個不錯的選擇。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

    ※回頭車貨運收費標準

  • 20萬就能買到全新寶馬轎車?!因為它真的來了

    20萬就能買到全新寶馬轎車?!因為它真的來了

    新車基於UKL前驅平台打造,1系三廂將會是前驅車型,新車的動力系統或為1。5T 136馬力+6速自動變速箱,2。0T 192馬力/231馬力+8擋手自一體變速箱,其中官方聲稱1。5T車型的百公里油耗最低可以達到5。5L。新車將會有無鑰匙啟動、抬頭显示、倒車影像、全景天窗、LED大燈、pM2。

    寶馬1系三廂自從誕生的那一刻就備受關注,吸睛無數,小編也和大家一樣,時刻關注着寶馬1系三廂的所有信息。畢竟,哪個男人心裏沒有這一個藍天白雲夢,開寶馬也是很多人的心愿。

    但是寶馬作為豪華品牌,價格不是每個消費者都能承受的起的。寶馬進口1系的價格不是很貴,但是國人就是對兩廂車有一定的排斥心理,比較鍾愛三廂車,所以即使進口1系兩廂的價格足夠便宜,但是買單的人照樣不多。

    當然,寶馬3系是三廂車,但是3系的價格在30萬左右,還是有點小貴。所以在20萬級別這個領域,出現了一個市場空缺。如果能有一台20萬的三廂寶馬,這應該是極好的選擇。

    看看奧迪A3就知道了,作為豪華品牌的A3將價格做到了20萬元左右,在這個沒有直接競爭對手的領域,A3的銷量好的一塌糊塗,A3在11月份交出了9883輛的銷量,將近萬輛的銷量足以看出來這個細分市場有很大的潛力。

    所以寶馬義不容辭的推出了國產版的1系三廂。

    寶馬1系三廂版基於Compact Sedan概念車打造出來的,新車的設計借鑒了概念車的設計元素,同時也具有着寶馬家族化的設計風格,前大燈的造型與3系比較相似,側麵線條比較平直,從外觀看像是縮小的3系,看起來短小精悍,富有運動感。

    內飾看起來還是那麼熟悉的感覺,畢竟也是採用了寶馬家族化的設計特徵,中控台造型很有層次感,並配備了8.8英寸液晶屏,同時還有大面積的鍍鉻裝飾,可以增強內飾的精緻感。

    新車基於UKL前驅平台打造,1系三廂將會是前驅車型,新車的動力系統或為1.5T 136馬力+6速自動變速箱,2.0T 192馬力/231馬力+8擋手自一體變速箱,其中官方聲稱1.5T車型的百公里油耗最低可以達到5.5L。

    新車將會有無鑰匙啟動、抬頭显示、倒車影像、全景天窗、LED大燈、pM2.5濾清器等,這些都是國人比較看重的配置。新車的預售價為20.5萬起,這就意味着也許不到20萬的價格就可以買到帥氣的三廂寶馬,估計很多消費者都會心動吧!如果實際售價能比預售價還低一點的話,那麼A3就會瞬間感到壓力了。

    競爭對手

    奧迪A3

    指導價:18.49-28.10萬

    A3在國內的成就有目共睹,但是它真正的競爭對手預計會在2月份正式上市銷售,憑藉著“藍天白雲”在國內的號召力,A3將會面臨很大壓力,那麼它能否守住自己的陣地,就讓我們拭目以待吧!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

  • 售價降低還換裝1.5T 這款合資轎車不輸思域

    售價降低還換裝1.5T 這款合資轎車不輸思域

    輪轂方面使用的是美規思域Si上的18英寸旋風式輪轂,樣式非常漂亮。內飾:基本沒有變化內飾方面也僅僅進行了一些小改動,基本保持着舊款的設計。但是增加了配色,導航也終於被加入到車機裏面。依舊使用着数字显示式儀錶盤,這是一種用過都說好的設計,数字显示的時速一目瞭然,而且較高的位置真有點抬頭显示的感覺。

    前言

    本田思域可謂是現今緊湊型轎車的當紅辣子雞,即使需要加價也難以阻擋人們對它的喜愛之情,其中最吸引他們的就是思域上那款動力油耗表現出色的1.5T發動機以及極高的配置。而最近本田為我們帶來2017款本田傑德,這輛大兩廂汽車更換了十代思域上的1.5T發動機,功率雖然有所降低,但是配置更為豐富,那麼它具體又有着怎樣的升級呢?又是否值得購買呢?

    傑德定位非常獨特,你可以將它當做一輛旅行車,也可以將其當做是MpV。而本田定位它是一輛緊湊型汽車,在筆者的眼中它是一輛大兩廂車,首先來自老款思域的平台讓它有着轎車般的駕駛感受,其次6座的布局實在和7座MpV有差異。這次2017款傑德的定價為12.99-17.99萬,購買門檻比起之前更低了。

    外觀:更換了LED光源以及新增配色,更為精神

    2017款傑德增加了新翠綠、波爾多紅等車身顏色,更為個性以及好看。在前臉方面進氣格柵增加了少許鍍鉻裝飾,看着更為時尚,而這種家族史設計使得傑德前臉更像是雅閣。

    而集成了LED日間行車燈的LED大燈非常醒目,亮度相當高,不過這是1.5T中高配車型的專利,1.8L以及1.5T最低配都與這LED大燈無緣。

    尾燈也同樣使用了LED燈源,辨識率更高,關鍵的一點是視覺效果更佳。除此以外,傑德使用了雙邊共雙出的排氣管設計,看着有一點性能味。

    離地間隙提升了10mm,舊款托底的毛病減弱不少,但是有一點需要注意,傑德是一輛兩廂車,對於兩廂車而言這個離地間隙已經是很大的了。輪轂方面使用的是美規思域Si上的18英寸旋風式輪轂,樣式非常漂亮。

    內飾:基本沒有變化

    內飾方面也僅僅進行了一些小改動,基本保持着舊款的設計。但是增加了配色,導航也終於被加入到車機裏面。

    依舊使用着数字显示式儀錶盤,這是一種用過都說好的設計,数字显示的時速一目瞭然,而且較高的位置真有點抬頭显示的感覺。

    配置:提升不少,但尚未完美

    這次配置上的提升是比較明顯的,讓傑德競爭力一下子提升不少,增加了LED大燈、日間行車燈、LED尾燈、18英寸輪轂、併線輔助系統、車道偏離系統、主動剎車、自適應巡航系統等,而且1.5T車型全系標配了全景天窗。

    傑德似乎是想要向思域看齊,但傑德卻沒有思域上的自動駐車以及电子手剎,稍顯遺憾,而且作為最低配車型並沒有配備ESp車身穩定系統。總的而言,傑德在配置上以及性價比上提升不少。

    動力:最大的亮點

    一直以來,傑德都是被當做是老款思域的重造產物,使用了同樣的平台,同樣的動力總成。底盤表現非常出色,但1.8L自然吸氣發動機卻難以挑起重擔,油耗以及耐用度優秀,卻給人白開水的感覺。這次加入的1.5T發動機,就像是辣椒一樣一下子把這款大兩廂車變得勁爆刺激味蕾。最大功率115千瓦,最大扭矩203牛米,比起思域上略有下降,依然是優秀水平,可以理解為思域上的1.5T低功率版本。

    和這款1.5T渦輪增壓發動機相配合的依然是CVT變速箱,和思域一樣,根據思域上平均8.5L的實測油耗,我們有理由相信使用同款發動機的傑德油耗也能同樣優秀。

    這一次2017款本田傑德的上市把之前的一些小問題幾乎都解決了,更低的價格、換上了更強悍的動力總成、加入了導航、更豐富的舒適性配置以及安全配置,這樣看來傑德是越趨完美。加上同價位並沒有相似的競爭對手,傑德可能會成為東本的第二個思域。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

    網頁設計最專業,超強功能平台可客製化