.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是一種內存級數據庫,所有數據均存放在內存中,內存中的數據可以通過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 實戰系列(四)

系列文章

  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多大的文件我都裝得下

目錄

  • 簡介
  • 虛擬地址空間
  • 詳解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之前,處理日期時間,我們主要使用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

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

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

售價降低還換裝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地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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

好開、省油還有科技感,教授把這輛車推薦給挑剔的朋友們

聽了的介紹,三位老同學都下定決心要買17款新蒙迪歐了,我想既然他們都將是17款新蒙迪歐的車主了,所以約了他們提車三個月之後回來探討一些用車的經驗。三個月後,和三位老同學再次相聚,討論一下蒙迪歐的用車心得。智行第一位同學,也就是那位駕齡不長的女司機,她說讓她印象最深刻的還是17款新蒙迪歐的LKA車道保持輔助系統,一旦車輛在無意識情況下偏離車道,系統將通過震動方向盤和儀錶盤信息提醒,並輔助她重回當前行駛車道,這讓她在高速公路上行駛時避免了偏離車道所造成的危險。

身邊的朋友知道喜歡玩車

於是經常能收到他們的諮詢

什麼十萬買啥車、二十萬買啥車

諸如此類的問題

其實都可以輕鬆應對的

然而就像廣東話說的

“上得山多終遇虎”

也會有天被問題難住

三個月前的同學聚會

有三位老同學說他們感情好

想買同一輛車,都要二十萬出頭而且夠大氣的

首先女同學是一名女司機

作為職業女性的她,要經常跑高速

而且她還是新手

所以她要我推薦一輛好開、安全的車給她

第二位同學則是環保主義者

什麼都不管,就要油耗低

(大哥,外觀大氣的車好難找油耗低的吧)

第三位同學是一名技術宅

典型的IT男

需要一輛有技術含量的車子

聽了他們的要求后,第一時間是吐槽根本沒有這樣的車。但兢兢業業鍥而不舍的我在網上做了大量的搜索之後,發現有一輛車非常適合這三位老同學,那就是長安福特的17款新蒙迪歐。

17款新蒙迪歐指導價17.98萬起,首先在價格上是符合要求的,然後在外觀上,馬丁式的家族前臉、LED大燈、2850mm的長軸距、以及肌肉感十足的車身線條,時尚個性又不失沉穩的外觀能讓不同年齡層的人接受。

內飾方面,採用了對稱式的布局,整體的內飾簡潔明了,多媒體系統也已經升級到第三代的SYNC操作系統,操作更加流暢,功能也更加完善。除此以外,旋鈕式換擋的設計也讓車內的檔次感提升不少。

行駛方面,拿2.0T車型來說,最大馬力達到245ps,搭配調校成熟的6AT手自一體變速箱,動力表現充沛且平順,絲毫不用擔心有小馬拉大車的感覺;底盤方面採用了多連桿獨立后懸架,整體偏向於舒適的調校,對細碎顛簸的過濾十分徹底。可以看出,實際的行駛表現也很符合蒙迪歐的定位。

聽了的介紹,三位老同學都下定決心要買17款新蒙迪歐了,我想既然他們都將是17款新蒙迪歐的車主了,所以約了他們提車三個月之後回來探討一些用車的經驗。

三個月後,和三位老同學再次相聚,討論一下蒙迪歐的用車心得。

智行

第一位同學,也就是那位駕齡不長的女司機,她說讓她印象最深刻的還是17款新蒙迪歐的LKA車道保持輔助系統,一旦車輛在無意識情況下偏離車道,系統將通過震動方向盤和儀錶盤信息提醒,並輔助她重回當前行駛車道,這讓她在高速公路上行駛時避免了偏離車道所造成的危險。

除此以外,對於這位新手司機來說,帶行人識別功能的智能感應制動保護系統、以及只需控制油門和剎車,即可輕鬆把車停在理想位置的主動泊車輔助系統和ACC全速智能自適應巡航控制系統都讓她駕駛蒙迪歐的過程更加輕鬆。另外,車廂內的10個安全氣囊的環繞式防護也讓她駕駛的過程中更加安心。

智擎

至於那位作為環保主義者的老同學,關注油耗的他對17款新蒙迪歐的EcoBoost雙渦流渦輪增壓直噴發動機贊不絕口,因為先進的雙渦流渦輪增壓技術能有效緩解低速時的遲滯性,使峰值扭矩提早爆發,帶來更佳的燃油經濟性。

而為了達到更好的節能環保效果,他選擇了Hybrid車型,17款新蒙迪歐的混合動力版本採用阿特金森循環發動機配合電動機,並搭載eCVT電控無級變速器,讓燃油與電力良好融合,高效環保,帶來煥然一新的駕駛體驗與樂趣。

智聯

最後一位老同學就是做IT男的技術宅,他最滿意的就是17款新蒙迪歐搭載的福特派TM互聯技術,只需要在手機中下載福特派TM應用,就可以遠程控制車輛並且通過手機來查看燃油剩餘量等車輛概況,十分方面。

在多媒體功能方面,車主可以通過藍牙或者USB數據線將手機和車輛相連,來拓展多媒體的功能,同時,17款新蒙迪歐的多媒體系統支持智能聲控以及智能中文手寫輸入,支持多字連寫,讓人機之間的交互變得更加簡單。

總結

其實早就知道17款新蒙迪歐的三大亮點能夠讓我的這三位老同學滿足,“智行”讓駕駛更加輕鬆而且安全、“智擎”在提供充沛動力的同時又保證了良好的燃油經濟性、“智聯”則結合了時下流行的智能手機,既提供了用車的便利,又豐富了多媒體功能。三管齊下,相信不只是我的這三位老同學,廣大的消費者都會被17款新蒙迪歐吸引到。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

不要小看這些買車細節,去4S店裝老司機全靠這幾招…

總結:買車畢竟是一件大事,最怕就是花了大價錢結果掉進了別人一早挖好的陷阱里,起不來活不起,當了冤大頭還要替別人數錢,所以在我們買車的時候還是多加留意,多留個心眼,將主動權抓在自己的手上。

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

大發現!國3汽車的PM2.5居然比國5低?

接下來是國三的車。所以僅從pM2。5的指數上來看,老車的成績不一定差,新車的成績也不一定好,反而車況才是更重要的因素。而那些所謂的專家。其實之前其他網友測試汽車尾氣pM2。5指數得出驚人的結果。(汽車尾氣比空氣乾淨,聽着很扯,畢竟你不可能整天去吸尾氣),就有專家出來表明,汽車的排放導致的污染並非只是直接的pM排放污染,而是“二次反應”。

一到冬季,pM指數和霧霾就成了熱門話題,這個時候汽車就會慣例被推上輿論焦點,近期國六排放標準的出台也進一步表明了相關部門的觀點—“霧霾的很大成因就是因為汽車的排放”。每次說到空氣污染提得最多的就是pM2.5指數。

pM指數那麼多,為什麼偏偏是pM2.5?

pM的英文全稱為particulate Matter (顆粒物),而数字2.5是指這種顆粒物的空氣動力學直徑(aerodynamic diameter)為2.5微米( pM10則是指粒徑等於、小於10微米的顆粒物)。粒徑越小、密度越低,顆粒沉降得越慢。如果再有一些外力引起空氣微小的擾動,這些細小的顆粒即使在無風的條件下,最終也很難沉降到地面上,它們會一直在空中“遊盪”,成為危害人體健康的罪魁禍首。

而我們常說的pM2.5的濃度或其他污染氣體的濃度是指每立方米空氣中這種污染物的質量含量,它反映了空氣的污染程度,這個值越高,就代表空氣污染越嚴重。由於空氣中污染物的種類很多,如:可吸入顆粒物、二氧化硫、氮氧化物、一氧化碳及臭氧等,為了統一評估,我國的環保部門,將每一種污染物的濃度都換算成統一的空氣污染指數,然後對外發布。俗話說病從口入,那麼我們今天就從源頭-用儀器來測試一下汽車尾氣的pM2.5指數。

在此之前我們先看下香煙的pM指數,直接爆表,所以能戒煙朋友趕緊戒煙了。

接着測試一下國4的車。(所測汽車均為公司同事車輛)

然後是國5的車!

接下來是國三的車。

所以僅從pM2.5的指數上來看,老車的成績不一定差,新車的成績也不一定好,反而車況才是更重要的因素。

而那些所謂的專家。。。

其實之前其他網友測試汽車尾氣pM2.5指數得出驚人的結果。。。

(汽車尾氣比空氣乾淨,聽着很扯,畢竟你不可能整天去吸尾氣),就有專家出來表明,汽車的排放導致的污染並非只是直接的pM排放污染,而是“二次反應”。

這樣一群吃瓜群眾就懵逼了,你是博士后,你說什麼都對,就像一直聽說F1產生的下壓力足以讓其貼在牆上跑,但是從來沒有見過,不相信這件事的人也找不到反駁的理由。

不管汽車的排放是不是元兇(因為現在霧霾這個鍋汽車已經背定了),相關部門對新車排放的要求越來越高並沒錯,畢竟排放再少也是排放。假設之前國三標準滿分是100分,當年生產的汽車再優秀,也只能得100。

現在國五標準出來了,直接就說滿分是150,100分不及格,也不給你重考的機會(限制進京、限制遷入等),這種把能夠達到要求的老車也一刀切那就有點想不通了,對於車迷來說最大的損失就是中國就永遠不會有老爺車文化,因為在相關部門眼中只有報廢車。

最後通過數據計算可以知道廣州的車輛密度比北京還要高,但是空氣質量卻要好不少,所以愛吃烤鴨的小夥伴趕緊把烤鴨吃個夠,說不定哪天烤鴨也分黃標鴨!!!

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

為什麼朋友說他夢想的車是艾母雞?看完本文後終於懂了

說到奔馳許多人最初的印象都是寬、大、貴外加後排坐着一個人肥,牙黃,地中海的老闆似乎奔馳一向跟年輕、運動、操控這些詞扯不上什麼關係所以坊間也流傳這麼一個說法——甚至有一些“自信狂人”遇上大奔時會抑制不住腎上腺

說到奔馳

許多人最初的印象都是

寬、大、貴

外加後排坐着一個

人肥,牙黃,地中海的老闆

似乎奔馳一向跟

年輕、運動、操控這些詞

扯不上什麼關係

所以坊間也流傳這麼一個說法——

甚至有一些“自信狂人”

遇上大奔時

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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