基於 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 開發博客項目 – 終結篇之發布項目

上一篇搭建了 Blazor 項目並將整體框架改造了一下,本篇將完成用 C# 代碼代替 JavaScript 實現幾個小功能,說是代替但並不能完全不用 JavaScript,應該說是盡量不用吧。

二維碼显示與隱藏

可以看到,當我鼠標移入的時候显示二維碼,移出的時候隱藏二維碼。

這個功能如果是用JavaScript來完成的話,肯定首先想到的是HTML的 Mouse 事件屬性,那麼在Blazor中也是一樣的,給我們實現了各種on*事件。

打開index.razor頁面,給微信圖標那個 NavLink 標籤添加兩個事件,@onmouseover和@onmouseout。

...
<NavLink class="link-item weixin" title="掃碼關注微信公眾號:『阿星Plus』查看更多。" @onmouseover="Hover" @onmouseout="Hover">
    <i class="iconfont iconweixin"></i>
</NavLink>
...

當鼠標移入移出的時候都執行我們自定義的一個方法Hover()。

C# 代碼寫在@code{}花括號中,實現显示和隱藏原理是利用css,默認是隱藏的,當显示的時候將具有隱藏屬性的class值去掉就可以了。

所以,可以添加兩個字段,一個用於判斷當前是否處於隱藏狀態,一個用來存儲class的值。

/// <summary>
/// 是否隱藏
/// </summary>
private bool IsHidden = true;

/// <summary>
/// 二維碼CSS
/// </summary>
private string QrCodeCssClass => IsHidden ? "hidden" : null;

當IsHidden = true,QrCodeCssClass = "hidden",當IsHidden = false,QrCodeCssClass = null。

那麼在Hover()方法中,不斷修改IsHidden的值就可以實現效果了。

/// <summary>
/// 鼠標移入移出操作
/// </summary>
private void Hover() => IsHidden = !IsHidden;

最後將QrCodeCssClass變量賦值給二維碼圖片所在的div上。

...
<div class="qrcode @QrCodeCssClass">
    <img src="https://static.meowv.com/images/wx_qrcode.jpg" />
</div>
...

大功告成,index.razor完整代碼如下:

@page "/"

<div class="main">
    <div class="container">
        <div class="intro">
            <div class="avatar">
                <a href="javascript:;"><img src="https://static.meowv.com/images/avatar.jpg"></a>
            </div>
            <div class="nickname">阿星Plus</div>
            <div class="description">
                <p>
                    生命不息,奮鬥不止
                    <br>Cease to struggle and you cease to live
                </p>
            </div>
            <div class="links">
                <NavLink class="link-item" title="Posts" href="posts">
                    <i class="iconfont iconread"></i>
                </NavLink>
                <NavLink target="_blank" class="link-item" title="Notes" href="https://notes.meowv.com/">
                    <i class="iconfont iconnotes"></i>
                </NavLink>
                <NavLink target="_blank" class="link-item" title="API" href="https://api.meowv.com/">
                    <i class="iconfont iconapi"></i>
                </NavLink>
                <NavLink class="link-item" title="Manage" href="/account/auth">
                    <i class="iconfont iconcode"></i>
                </NavLink>
                <NavLink target="_blank" class="link-item" title="Github" href="https://github.com/Meowv/">
                    <i class="iconfont icongithub"></i>
                </NavLink>
                <NavLink class="link-item weixin" title="掃碼關注微信公眾號:『阿星Plus』查看更多。" @onmouseover="Hover" @onmouseout="Hover">
                    <i class="iconfont iconweixin"></i>
                </NavLink>
                <div class="qrcode @QrCodeCssClass">
                    <img src="https://static.meowv.com/images/wx_qrcode.jpg" />
                </div>
            </div>
        </div>
    </div>
</div>

@code {
    /// <summary>
    /// 是否隱藏
    /// </summary>
    private bool IsHidden = true;

    /// <summary>
    /// 二維碼CSS
    /// </summary>
    private string QrCodeCssClass => IsHidden ? "hidden" : null;

    /// <summary>
    /// 鼠標移入移出操作
    /// </summary>
    private void Hover() => IsHidden = !IsHidden;
}

菜單显示與隱藏

菜單是在小屏幕上才會出現的,相信看完了二維碼的显示與隱藏,這個菜單的显示與隱藏就好辦了吧,實現方法是一樣的,菜單按鈕是在頭部組件Header.razor中的,包括主題切換功能,所以下面代碼都在Header.razor裏面。

@code {
    /// <summary>
    /// 下拉菜單是否打開
    /// </summary>
    private bool collapseNavMenu = false;

    /// <summary>
    /// 導航菜單CSS
    /// </summary>
    private string NavMenuCssClass => collapseNavMenu ? "active" : null;

    /// <summary>
    /// 显示/隱藏 菜單
    /// </summary>
    private void ToggleNavMenu() => collapseNavMenu = !collapseNavMenu;
}

默認是不打開的,collapseNavMenu = false。然後根據collapseNavMenu值為NavMenuCssClass給定不同的class。

...
<nav class="navbar-mobile">
    <div class="container">
        <div class="navbar-header">
            <div>
                <NavLink class="menu-item" href="" Match="NavLinkMatch.All">阿星Plus</NavLink>
                <NavLink>&nbsp;·&nbsp;Light</NavLink>
            </div>
            <div class="menu-toggle" @onclick="ToggleNavMenu">&#9776; Menu</div>
        </div>
        <div class="menu @NavMenuCssClass">
            <NavLink class="menu-item" href="posts">Posts</NavLink>
            <NavLink class="menu-item" href="categories">Categories</NavLink>
            <NavLink class="menu-item" href="tags">Tags</NavLink>
            <NavLink class="menu-item apps" href="apps">Apps</NavLink>
        </div>
    </div>
</nav>
...

與二維碼显示與隱藏唯一區別就是這裡是點擊按鈕,不是移入移出,所以菜單显示與隱藏需要用到@onclick方法。

主題切換

哇,這個主題切換真的是一言難盡,當切換主題的時候需要記住當前的主題是什麼,當刷新頁面或者跳轉其他頁面的時候,主題狀態是需要一致的,默認是白色主題,當切換暗黑色主題后其實是在body上加了一個class。

在Blazor實在是不知道用什麼辦法去動態控制body的樣式,所以這裏我想到了一個辦法,寫幾個全局的JavaScript方法,然後再Blazor中調用,要知道,他們是可以互相調用的,於是問題迎刃而解。

添加app.js文件,放在 /wwwroot/js/ 下面。

var func = window.func || {};

func = {
    setStorage: function (name, value) {
        localStorage.setItem(name, value);
    },
    getStorage: function (name) {
        return localStorage.getItem(name);
    },
    switchTheme: function () {
        var currentTheme = this.getStorage('theme') || 'Light';
        var isDark = currentTheme === 'Dark';

        if (isDark) {
            document.querySelector('body').classList.add('dark-theme');
        } else {
            document.querySelector('body').classList.remove('dark-theme');
        }
    }
};

這裏寫了三個方法,設置localStorage:setStorage(name,value),獲取localStorage:getStorage(name),切換主題:switchTheme(),localStorage 是瀏覽器以 name:value 形式的本地存儲對象。

switchTheme主要做的事情就是,判斷當前主題如果是暗黑,就給body加上對應的class,如果不是就去掉。

然後在 index.html 中引用。

...
<body>
    <app>
        <div class="loader"></div>
    </app>
    <script src="js/app.js"></script>
    <script src="_framework/blazor.webassembly.js"></script>
</body>
...

有了這個三個全局的JavaScript方法,切換主題就變得簡單多了,看代碼。

...
/// <summary>
/// 當前主題
/// </summary>
private string currentTheme;

/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
    currentTheme = await JSRuntime.InvokeAsync<string>("window.func.getStorage", "theme") ?? "Light";

    await JSRuntime.InvokeVoidAsync("window.func.switchTheme");
}
...

注意在Blazor調用JavaScript方法需要注入IJSRuntime接口,@inject IJSRuntime JSRuntime。

新建一個變量currentTheme,在生命周期函數初始化的時候去調用JavaScript中的getStorage方法,獲取當前主題,考慮到第一次訪問的情況,可以給一個默認值為Light,表示白色主題,然後再去調用switchTheme,執行切換主題的方法。這樣頁面就會根據localStorage的值來確定當前的主題。

...
/// <summary>
/// 切換主題
/// </summary>
private async Task SwitchTheme()
{
    currentTheme = currentTheme == "Light" ? "Dark" : "Light";

    await JSRuntime.InvokeVoidAsync("window.func.setStorage", "theme", currentTheme);

    await JSRuntime.InvokeVoidAsync("window.func.switchTheme");
}
...

SwitchTheme()是切換主題的方法,當我們點擊input按鈕時可以任意切換,並且主題還要實時跟着變化。

當點擊按鈕執行SwitchTheme()時候改變currentTheme的值,然後將currentTheme傳遞給JavaScript方法setStorage,最後再次執行切換主題的JavaScript方法即可。

此時變量currentTheme也發揮了不少作用,在小屏幕下會显示當前主題的名稱,Dark or Light,可以直接將currentTheme在HTML中賦值即可。

並且我們input是checkbox類型,當是黑色主題的時候需要時選中的狀態,白色主題的時候不選中,這裏就可以利用checked屬性這樣寫:checked="@(currentTheme == "Dark")"。

<nav class="navbar">
    <div class="container">
       ...
        <div class="menu navbar-right">
           ...
            <input id="switch_default" type="checkbox" class="switch_default" @onchange="SwitchTheme" checked="@(currentTheme == "Dark")" />
            <label for="switch_default" class="toggleBtn"></label>
        </div>
    </div>
</nav>
<nav class="navbar">
    <div class="container">
       ...
        <div class="menu navbar-right">
           ...
            <input id="switch_default" type="checkbox" class="switch_default" @onchange="SwitchTheme" checked="@(currentTheme == "Dark")" />
            <label for="switch_default" class="toggleBtn"></label>
        </div>
    </div>
</nav>
<nav class="navbar-mobile">
    <div class="container">
        <div class="navbar-header">
            <div>
                <NavLink class="menu-item" href="" Match="NavLinkMatch.All">阿星Plus</NavLink>
                <NavLink @onclick="SwitchTheme">&nbsp;·&nbsp;@currentTheme</NavLink>
            </div>
            <div class="menu-toggle" @onclick="ToggleNavMenu">&#9776; Menu</div>
        </div>
        <div class="menu @NavMenuCssClass">
            ...
        </div>
    </div>
</nav>

OK,搞定,快去試試吧。

優化代碼

現在看起來亂亂的,並且設置獲取localStorage屬於公共的方法,說不定以後也能用到,我們將其封裝一下,便於日後的調用,不然要寫好多重複的代碼。

在Blazor項目根目錄添加文件夾Commons,在文件夾下添加一個Common.cs,目前用到了IJSRuntime,用構造函數注入,然後寫幾個公共的方法。

//Common.cs
using Microsoft.JSInterop;
using System.Threading.Tasks;

namespace Meowv.Blog.BlazorApp.Commons
{
    public class Common
    {
        private readonly IJSRuntime _jsRuntime;

        public Common(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }

        /// <summary>
        /// 執行無返回值方法
        /// </summary>
        /// <param name="identifier"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        public async ValueTask InvokeAsync(string identifier, params object[] args)
        {
            await _jsRuntime.InvokeVoidAsync(identifier, args);
        }

        /// <summary>
        /// 執行帶返回值的方法
        /// </summary>
        /// <typeparam name="TValue"></typeparam>
        /// <param name="identifier"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        public async ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object[] args)
        {
            return await _jsRuntime.InvokeAsync<TValue>(identifier, args);
        }

        /// <summary>
        /// 設置localStorage
        /// </summary>
        /// <param name="name"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public async Task SetStorageAsync(string name, string value)
        {
            await InvokeAsync("window.func.setStorage", name, value);
        }

        /// <summary>
        /// 獲取localStorage
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public async Task<string> GetStorageAsync(string name)
        {
            return await InvokeAsync<string>("window.func.getStorage", name);
        }
    }
}

然後需要在Program.cs中注入。

using Meowv.Blog.BlazorApp.Commons;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace Meowv.Blog.BlazorApp
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");

            builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            builder.Services.AddSingleton(typeof(Common));

            await builder.Build().RunAsync();
        }
    }
}

緊接着在_Imports.razor中注入使用Common,@inject Commons.Common Common。

改造一下Header.razor,全部代碼如下:

<header>
    <nav class="navbar">
        <div class="container">
            <div class="navbar-header header-logo">
                <NavLink class="menu-item" href="/" Match="NavLinkMatch.All">
                    阿星Plus
                </NavLink>
            </div>
            <div class="menu navbar-right">
                <NavLink class="menu-item" href="posts">Posts</NavLink>
                <NavLink class="menu-item" href="categories">Categories</NavLink>
                <NavLink class="menu-item" href="tags">Tags</NavLink>
                <NavLink class="menu-item apps" href="apps">Apps</NavLink>
                <input id="switch_default" type="checkbox" class="switch_default" @onchange="SwitchTheme" checked="@(currentTheme == "Dark")" />
                <label for="switch_default" class="toggleBtn"></label>
            </div>
        </div>
    </nav>
    <nav class="navbar-mobile">
        <div class="container">
            <div class="navbar-header">
                <div>
                    <NavLink class="menu-item" href="" Match="NavLinkMatch.All">阿星Plus</NavLink>
                    <NavLink @onclick="SwitchTheme">&nbsp;·&nbsp;@currentTheme</NavLink>
                </div>
                <div class="menu-toggle" @onclick="ToggleNavMenu">&#9776; Menu</div>
            </div>
            <div class="menu @NavMenuCssClass">
                <NavLink class="menu-item" href="posts">Posts</NavLink>
                <NavLink class="menu-item" href="categories">Categories</NavLink>
                <NavLink class="menu-item" href="tags">Tags</NavLink>
                <NavLink class="menu-item apps" href="apps">Apps</NavLink>
            </div>
        </div>
    </nav>
</header>

@code {
    /// <summary>
    /// 下拉菜單是否打開
    /// </summary>
    private bool collapseNavMenu = false;

    /// <summary>
    /// 導航菜單CSS
    /// </summary>
    private string NavMenuCssClass => collapseNavMenu ? "active" : null;

    /// <summary>
    /// 显示/隱藏 菜單
    /// </summary>
    private void ToggleNavMenu() => collapseNavMenu = !collapseNavMenu;

    /// <summary>
    /// 當前主題
    /// </summary>
    private string currentTheme;

    /// <summary>
    /// 初始化
    /// </summary>
    /// <returns></returns>
    protected override async Task OnInitializedAsync()
    {
        currentTheme = await Common.GetStorageAsync("theme") ?? "Light";

        await Common.InvokeAsync("window.func.switchTheme");
    }

    /// <summary>
    /// 切換主題
    /// </summary>
    private async Task SwitchTheme()
    {
        currentTheme = currentTheme == "Light" ? "Dark" : "Light";

        await Common.SetStorageAsync("theme", currentTheme);

        await Common.InvokeAsync("window.func.switchTheme");
    }
}

實現過程比較簡單,相信你絕對學會了。本篇就到這裏了,未完待續…

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

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

10幾萬就想買個好看又踏實的家用車,怪不得大家都買他們…

58萬元,在推出第二年才推出了自動擋車型,可能因為自動變速箱實在不行,在往後的幾年推出新款就再也沒推出過自動擋車型了,直到15年的時候擁有一定的調校技術和資源才推出了自動擋車型,也開始陸續地“開枝散恭弘=叶 恭弘”,紅標、藍標、運動版逐漸現世,但依然不太推薦自動擋車型,兩百塊能跑四百公里和只能跑300公里相比,你會怎麼選。

“,我要買哈弗H6!”

“哦?那你想買哪個版本?”

“啊?怎麼說?”

“哈弗H6有紅標運動版、藍標運動版、升級版、柴油版、Coupe車型….”

“……”

11月份哈弗H6賣出了7.03萬輛

排在國內熱銷SUV排行榜榜首

銷量超過了第二名的傳祺GS4兩倍有餘

為什麼哈弗H6這麼多人買?

因為它賣得火啊!

全文終。

自2011年哈弗H6在長城天津工廠下線以來,一步步地成為國內SUV市場的爆款,在2015年哈弗H6累計銷量破百萬,這是自主品牌的銷量神話…

哈弗H6犹如是淘寶中慢慢蛻變爆款的商品,在SUV市場還沒全面興起的時候,恰好在這個時間上架,也憑藉一些實力做出了一些口碑,可以理解為“刷”好評。消費者要買想SUV,首先看的是SUV銷量排行榜,其次是看口碑,十個口碑中有六個好評其實已經引起消費者的好感和購買慾望,這是大部分中國消費者的消費習慣。

▲哈弗H6符合國人口味的外觀

▲哈弗H6不俗的內飾

口碑是可以“刷”出來的,但也需要用實力去支撐,哈弗H6也有它的“過人之處”,多重的因素令它成為了最成功的SUV車型之一。每一個商家不會放過任何一個有爆款潛力的商品,更何況是已經成為爆款的商品,更是加大投入推廣和改進,爭取獲得更好的銷量成績。

▲哈弗H6 COUpE車型

11年上市的哈弗H6隻有兩個版本,分別是2.0L和2.0T的發動機,只有手動擋配置,當時國內的自動變速箱技術還真不夠成熟,所以那時候大部分自主車型都是手動擋,畢竟机械離合器構造簡單成本低。價錢也不低,當時候最低配也要9.58萬元,在推出第二年才推出了自動擋車型,可能因為自動變速箱實在不行,在往後的幾年推出新款就再也沒推出過自動擋車型了,直到15年的時候擁有一定的調校技術和資源才推出了自動擋車型,也開始陸續地“開枝散恭弘=叶 恭弘”,紅標、藍標、運動版逐漸現世,但依然不太推薦自動擋車型,兩百塊能跑四百公里和只能跑300公里相比,你會怎麼選?手動擋剛開始接觸可能稍難上手一些,但用得久了,其實真沒什麼,這是很多開手動擋車的老司機原話。

現在很多更新換代的車型無非就是稍微改改外觀內飾,增加或減少配置控制整車成本,都是這樣的一個套路,哈弗H6這麼多款版本車型也不例外,無論是外觀和內飾配置怎麼變怎麼改,都是用那套原來舊的動力總成、懸架甚至是車身結構,並沒有“改頭”只是“換臉”而已。眾所周知大部分自主品牌使用的動力總成大多都是合資品牌的老舊技術,質量過關耐用,但就是跟不上時代的步伐,就拿國內的2.0T發動機和本田的2.0T發動機相比,真的是天地之別。

奈何,國人的用車習慣就是車能跑得快、空間夠大人坐得舒服也載得多人,其次還有外觀要好看霸氣有面子,重點還要價錢不能高,所以十來萬的SUV最符合國人的口味,哈弗H6就是這樣符合國情,但真要說人生第二台車會不會選哈弗,相信很多消費者都是否定的。

再來說說轎車吧,轎車一直都不是自主品牌的強悍,SUV市場一片繁華,但轎車方面還是十分中庸,11月份轎車銷量排行榜十中有九都是合資品牌,都是我們熟悉的軒逸、朗逸、速騰、英朗等等一直很火的十來萬緊湊型車。

▲日產軒逸-11月份轎車排行榜銷量冠軍

轎車銷量排行榜前十名的銷量並沒有SUV排行榜那麼懸殊,都是賣出三、四萬台這樣,日產軒逸賣出4.52萬輛排在第一,對於部分抵制日貨的消費者來說很氣,但日系車特別符合家庭用車習慣這是最大的事實,賣出一台日系車日本賺到的利潤遠遠比不上我們自己國家收的稅多,這裏就不往下說了,日系車還是賣得很好,證明大部分消費者都是理性消費的。

▲國人的用車還是以家庭為主

中國家庭就是需要一台看着大氣開起來舒服又省油的轎車,車價在10-15萬元是國人最能接受的區間,首付供車壓力不算大,全款支付也並不需要N+1年才能實現“有車一族”,所以轎車銷量排行榜中車型都涉及到這個車價區間,這也是國內汽車市場最受歡迎的價格區間。

你想問轎車銷量排行榜裏面哪款車比較好?不吹不黑告訴你們,這些車都差不多,很適合家庭用車,如果你想從中選一台,可以按以下步驟:

第一、從外觀入手,除了自己的審美之外,問問父母的意見(僅僅外觀一項)有女朋友的問女朋友有老婆的問老婆,從中挑幾款順眼好看的車型;

第二、到4S店靜態體驗,空間大小和座椅的舒適性比較,當然最好也能帶上父母或者老婆,畢竟以後他們都會坐上你的車;

第三、看手頭的預算,挑配置,配置不用多夠用就好,省點總比亂花好;

第四、道路試駕,其中十來萬的合資車開起來差不到哪裡去,省油才是最關鍵,其次感受一下車輛的濾震,挑開起來順手最舒服的那台絕對沒錯;

第五、看中哪個配置車型就可以跟銷售砍價了。

有個性的車,例如外觀極端,不管是好看還是難看,基本都賣不好,自然成為了小眾車型。國人的審美逐漸被常見的車型平淡化,你覺得這輛車個性又獨特,但當你真正需要買車的時候不會想到這些小眾的車型,除非是價錢吸引到你,大多人都會回歸到銷量排行榜中,這是一種依賴的心理效應,總覺得賣得好的車就是最靠譜的車,至少不會因為自己的選擇而後悔,就算是後悔也有一大把人陪自己呢,車子出問題了大不了跟“同病相憐”的一起抱怨罷了。

總結:SUV或轎車也好,其實不必要太糾結每個月的銷量排行榜,我們可以將它作為我們買車的參考,而不是將它作為我們買車的標準,選擇自己真心喜歡的車型,沒必要迎合他人的口味。榜上有名的車型自然有它們存在的道理,這裏不談它們的優點或缺點,畢竟人無完人,車也一樣嘛!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

當精工製造遇上了一雙慧眼,會造出一輛什麼車?

在整個越野路段對比體驗中,印象最深刻的就是傲虎的動力輸出特性,由於傲虎使用的是自然吸氣水平對置發動機與CVT變速箱,低轉速時的扭力輸出比渦輪增壓發動機要好很多,動力的輸出非常線性,簡單點來說就是很容易駕馭,同時對安全也有幫助,駕駛員能把更多的精力放在觀察路況與路線當中。

想要打造出一輛能夠大賣的爆款車型,或許並不難,只要它有足夠的賣點即可,例如帥氣靚麗的外形,又或者極致的駕駛樂趣,再或者是豐富的配置,相較於那些各有特色的車型,覺得“均衡”才是一輛好車的必備條件。

近日,就試駕到了兩款非常“均衡”的車,同時它們對於斯巴魯而言也具有里程碑意義,因為它們首次將斯巴魯的EyeSight技術帶進中國,這兩款車就是斯巴魯的力獅與傲虎。

每次提及到斯巴魯,首先就會想到它們引以為傲的“水平對置發動機”與“左右對稱全時四驅”,為什麼斯巴魯一直在堅持,覺得一方面是對駕駛員的尊重,另一方面是對自然界的致敬。

在這次的斯巴魯力獅性能體驗中,在廣東國際賽車場輪流試駕了斯巴魯力獅、奧迪A4L和奔馳C級,三款車型的驅動方式分別為:全時四驅、前驅和后驅。

三種驅動形式各有特色,在賽道上進行極限駕駛時,力獅的左右對稱全時四驅盡顯其優勢,最明顯的就是出彎極限非常高,同樣的一個高速彎,開着力獅能夠從容的全油門出彎,然而A4L和C級都無法這樣做,否則就會伴隨着轉向不足與轉向過度。

除了左右對稱全時四驅,水平對置發動機的低重心也對車輛操控有积極意義,除了出色的操控,在三車對比當中,力獅 2017款 2.5i 全驅榮耀版 EyeSight在賽道上的動力表現值得稱讚,對比車型的奧迪A4L 40 TFSI與奔馳C200L同樣是使用2.0T的直列四缸渦輪增壓發動機,力獅則是2.5L自然吸氣水平對置發動機。

還沒開始試駕之前,一度認為用2.0T渦輪增壓與2.5L自然吸氣對比,好像有點不太公平,可是輪流試駕完三輛車之後,徹底改觀。賽道是一個分秒必爭的地方,力獅的CVT變速箱在此時此刻發揮出了它的優點。

通過方向盤右側多功能按鍵設定運動駕駛模式后,油門踏板變得非常靈敏,出彎時深踩油門,發動機轉速迅速上升,隨後一直維持在最佳動力輸出的轉速,整個過程不僅免除了換檔時間,動力的傳遞還非常及時,這樣的表現與市面上的大多數CVT都不太一樣。

如果你現在還是認為城市SUV的四驅性能都是噱頭,那麼就大錯特錯了,其實在此之前,也是對城市SUV的四驅系統不太看好,但是親自體驗完斯巴魯傲虎、奧迪Q5和豐田漢蘭達之後,徹底感受到傲虎的優勢。

“穩定可靠,簡單易用”,這是對於傲虎越野性能的總結,越野場地的賽道長度很短,但是涵蓋了眾多項目,其中最具有參考意義的就是炮彈坑、岩石路段和陡坡環節。

在整個越野路段對比體驗中,印象最深刻的就是傲虎的動力輸出特性,由於傲虎使用的是自然吸氣水平對置發動機與CVT變速箱,低轉速時的扭力輸出比渦輪增壓發動機要好很多,動力的輸出非常線性,簡單點來說就是很容易駕馭,同時對安全也有幫助,駕駛員能把更多的精力放在觀察路況與路線當中。

除了線性的動力輸出,還有一個關鍵點不得不提,那就是非常高效率的电子限滑系統,當車輛開啟了X-MODE模式之後,电子限滑系統的控制邏輯就會改變,變得更加积極、靈敏,在此模式下通過大炮彈坑,雖然兩個對象車輪懸空,但是整個過程一氣呵成,电子限滑系統反應迅速,車輪幾乎沒有出現空轉。

斯巴魯傲虎的陡坡緩降功能同樣是非常好用,當我們開啟X-MODE模式后,陡坡緩降系統就會在我們下坡時自動控制車速,我們先需要通過剎車制動把車輛控制在我們想要的車速,然後鬆開剎車踏板,系統就是自動保持在當前時速,我們要做的就是專註於轉向的控制。

無論是力獅的賽道性能還是傲虎的越野性能體驗,回歸到我們實際用車當中,覺得最重要的還是——安全,這是親自體驗過後最直觀的感受,即使你是第一次駕駛它們,你也會發現並不需要什麼適應過程,車輛會幫你處理那些瑣碎的事情,我們只需專註於駕駛,專註於觀察路面的狀況即可,這就是智能化的深度表現。

從第一輛汽車的誕生開始,車輛安全永遠都是世人關注的焦點,斯巴魯為什麼堅持全時四輪驅動,為什麼堅持水平對置發動機,覺得這些都是基於安全這個大前提去考慮的。

2017款斯巴魯力獅和傲虎是國內首次搭載預防性安全技術——EyeSight駕駛輔助系統的車型,繼日本、澳大利亞、北美、歐洲上市后,中國是第五個投放的市場,EyeSight的實際表現如何呢,感觸最深的是——細膩。

體驗內容包含了EyeSight駕駛輔助系統的幾大功能,防碰撞制動系統(pCB)、起步油門誤操作預防功能、全車速自適應巡航控制系統(ACC)和警報與提示功能。只要是配備了EyeSight駕駛輔助系統的車型,每當車輛啟動,防碰撞制動系統就會處於開啟狀態,若車速小於50km/h,能夠避免車輛與前方靜止的障礙物發生碰撞。

車內視角

車外視角

在實際測試中,分別以10km/h、30km/h和50km/h的時速進行體驗,車速越高,防碰撞制動系統的介入就越早,當系統識別到前方有障礙物,並且駕駛員沒有進行減速時,就會在儀錶盤進行相關提示,並帶有蜂鳴聲音,如果駕駛員仍然沒有進行任何操作,隨後車輛就會自動剎車至靜止狀態,整個自動剎車過程中,車內人員不會感覺到非常突兀,因為系統會進行提前預先剎車。

全車速自適應巡航控制系統能夠在0~180km/h的車速範圍內工作,能夠實現跟車功能,當車輛靜止后,可以通過兩個途徑恢復之前設定的時速繼續巡航,輕踩油門踏板或者是向上撥動車速設定撥桿即可。

這套全車速自適應巡航控制系統有幾個細節點做得很好,首先是跟車距離的設定,即便是從最近車距調節至最遠車距,系統也不會猛然減速,整個調節過程比人為控製做得還要好。

其次是對於前方有車輛併入車道,識別度很高,減速過程也不會感覺到很突兀,剎車力度有一個漸進的過程。除了這些,儀錶盤的動畫显示也做得非常好,無論是車輛併入,還是跟車的過程中,只要是我們的車輛處於減速狀態,儀錶盤中的显示剎車燈亮起的狀態,從這些這些細節的設計能夠看出斯巴魯還是很走心的。

EyeSight駕駛輔助系統還有一個功能非常適合剛拿到駕照的新手,那就是“起步油門誤操作預防功能”,當車輛處於靜止狀態下,如果系統識別到前方有靜止的障礙物,同時駕駛員將檔位掛到前進檔,即便是深踩油門,車輛也不會快速往前行駛,造成事故。

為什麼會這呢,其實是因為系統對發動機輸出進行抑制,發動機處於低速運轉,車輛只會有輕微的蠕動,這時候儀錶盤會有相關的提示和警示音,這樣就能夠很好的避免駕駛員掛錯檔而造成不必要的損失。

回過頭來看到整個EyeSight駕駛輔助系統,認為斯巴魯之所以選擇雙攝像頭,主要還是想讓更多的消費者體驗到這種預防性安全系統,為什麼這樣說呢,目前為止,其實各種駕駛輔助系統的方案都存在弱項,EyeSight的立體攝像頭方案很重要的一點在於其性價比很高。

在其它廠商的車型中,配備駕駛輔助系統的車型絕大多數都是頂配車型,但是這次斯巴魯將EyeSight引入中國,並沒有投放在頂配車型上,而是中配車型,他們的目的就是讓我們不需要花大價錢就能夠使用上EyeSight,始終將汽車安全性放在首位,這就是斯巴魯的獨特魅力。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

買了小排量就鐵定省油?天真!

5排量啊,還帶T,聽起來像思域的那台哦。你跟我談油耗。看下F1的發動機怎麼噴油的。F1的百公里油耗差不多在50—70升。你跟我說小排量省油。所以發動機的油耗與排量沒有最直接的關係,而是發動機做功的多少,我們都知道能量守恆定律。

這两天由於各個地方的霧霾,大排量的汽車又被推上風口浪尖,甚至引起不少所謂的微博大V撕逼。還有所謂的大學生上街遊行抵制大排量。

以往說到大排量就會想到油老虎,確實,我們所見到的大排量的車要麼是跑車。

要麼是旗艦級的豪車。

還有“美國車”。

首先發動機的排量是個什麼東西呢?今天就不列公式裝逼了。打個簡單的比方,如果說小排量是瘦子。

那麼大排量就像胖子。

同樣吃一個大蛋糕,大排量每一口吃得更多,小排量每口吃得更少,所以胖子吃得更快。不過即使這樣,只要時間充足,瘦子最後也能把蛋糕吃完。

這也是我們大多數人的印象,大排量發動機動力更好,車跑得更快,但是油耗更高。

為什麼會有大排量就鐵定高耗油的定論呢?因為胖子嘴大,餓起來連自己都吃,所以每一口吃得多,你要讓豬八戒把人蔘果分兩口吃肯定不可能。

而瘦子則可以每次吃半口,如果感覺有點飽了接下來的半口可以留着不吃。理論上聽着確實很省。

但是為什麼現在很多朋友買了1.2T三缸和1.5T四缸,發現並沒有想象中省油呢?先不說那所謂的“工信部油耗”。

我們上面也說了,從A到B就像吃吃一塊蛋糕,如果每一口吃得多,那麼肯定吃得快,要是小排量也想和大排量一起吃完,那就必須吃得更快,所以像之前說的1050馬力的本田1.5T F1發動機工作轉速就高達14000轉。1.5排量啊,還帶T,聽起來像思域的那台哦。

你跟我談油耗?看下F1的發動機怎麼噴油的!

F1的百公里油耗差不多在50—70升。你跟我說小排量省油?所以發動機的油耗與排量沒有最直接的關係,而是發動機做功的多少,我們都知道能量守恆定律。要想馬兒跑得快肯定要讓馬多吃草。那麼市面上的小排量發動機的優勢在哪裡呢?我們會發現大排量的車發動機也是多缸數,3.0以上排量就肯定是6缸了,4.0以上至少8缸,再往上就12缸,還是那句老話,馬力大十匹不如體重減十斤。12缸畢竟缸數在這裏,所以像3缸4缸發動機在自重上佔有不少優勢。如果兩台發動機功率一樣,那麼重量更輕的在油耗上肯定佔有優勢。

所以發動機的油耗是看做功的多少,(做功的多少又關係發動機的熱效率)那麼做的功多少能用到輪上又考慮到變速箱和輪胎等多方面因素了,一台車是否省油還是看你怎麼開,只是小排量更容易開出低油耗。

如果你經常切二擋。

2.0也可以開出6.0的油耗。

如果你只是當做一個從A到B的工具,能動就可以,不追求什麼加速能力,推背感,6.0也可以開出2.0的油耗。

就像以前手機還是2G網絡的時候一個月30M都用不完,現在用上4G一個月10G也不夠。

而那些把空氣污染和霧霾全部歸咎於汽車排放的人,看了下面的成績單,是不是說明邯鄲的汽車排放是北京的3倍呢?

那如果霧霾的罪魁禍首不是汽車又是誰呢?

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

“小7系”降臨,E級顫抖了嗎?!全新寶馬5系Li揭開面紗!

而在動力總成方面,預計未來國產長軸距版車型將搭載不同功率調校的全新2。0T發動機以及3。0T發動機。與發動機匹配的有望是8速自動變速箱,部分車型還將搭載四驅系統。新5系標準軸距版海外的5系標準軸距版將於2017年2月在歐洲率先開售,起售價為36025英鎊,摺合人民幣29。

日前,華晨寶馬公布了一張國產全新5系Li長軸距版車型的官方圖片。新車針對中國消費者喜好,在全新一代寶馬5系標準軸距版的基礎上進行軸距加長打造而成。有消息稱全新5系Li有望在2017年4月的上海車展首發亮相,並且預計在2017年6月正式上市。

全新5系Li

在外觀方面,全新5系Li與標準軸距版幾乎保持一致,標誌性的雙腎型進氣格柵,開眼角式大燈,貫穿全車的側面腰線,前輪後方的氣流出口,儼然在向大哥7系看齊。

新5系標準軸距版

7系

內飾方面,預計也會和標準軸距版保持一致,延續新7系的設計語言,豪華之餘充滿科技感。略偏向駕駛者的中控布局,懸浮式中控大屏,全新設計的多功能方向盤,以及整體的飾料風格和色彩搭配,讓人一眼看上去很容易以為就是7系的內飾。

新5系標準軸距版

7系

配置方面,新車預計將配備多種新7繫上的高科技配置,如手勢控制、遙控泊車、遠程3D視角、HUD抬頭显示、夜視系統等。

而在動力總成方面,預計未來國產長軸距版車型將搭載不同功率調校的全新2.0T發動機以及3.0T發動機。與發動機匹配的有望是8速自動變速箱,部分車型還將搭載四驅系統。

新5系標準軸距版

海外的5系標準軸距版將於2017年2月在歐洲率先開售,起售價為36025英鎊,摺合人民幣29.67萬元,而國內售價則尚未公布。用5系的價格買到一輛“小7系”,這種越級的體驗不再是奔馳E級一家獨大,相信隨着全新寶馬5系Li的推出,國內C級豪華轎車市場的競爭會越發激烈。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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

帶你輕鬆了解C# Lock 關鍵字

  相信絕大多數.NET玩家和我一樣,常常使用Timer這個對象,而在WPF中使用DispatcherTimer的人也是很多,DispatcherTimer是在UI線程跑的。我們的程序中大多數都會充斥很多Timer,可以理解它是一個線程,它繼承自 System.Windows.Threading 。

  程序中也許會有一些靜態變量或是單例模式的對象來讓不同的頁面進行交互,但也就是這樣讓每個線程之間打架提供了基礎。因為資源是單獨的,就像是腳踩兩隻船的人,必定會翻車。例如一個List集合,你在一個線程中對它進行了操作,在同步瞬間的另線程中,如果不對它謹慎處理,就會造成 “集合已修改;可能無法執行枚舉操作”。當然我們說的不是關於集合的相關問題,而是關於資源分配的,當然在資源搶奪上,是在耗時的線程中才會出現的,例如下面的這張圖。

 

  這種耗時的操作,並且在同步線程中,沒有對線程進行封裝,很容易造成資源搶奪問題,假如Object是個集合,我在中間把它改了,下一秒的其它線程對它進行臟讀了,就會產生錯誤,我們可以通過Lock關鍵字。

  首先在Microsoft文檔中對Lock的說明是,lock 關鍵字可以用來確保代碼塊完成運行,而不會被其他線程中斷。這是通過在代碼塊運行期間為給定對象獲取互斥鎖來實現的。

  不過我們需要注意的是Lock本質上Monitor.Enter,Monitor.Enter會使值類型裝箱,每次Lock的是裝箱后的對象。Lock其實是類似編譯器的語法糖,因此編譯器直接限制住不能lock值類型,為啥呢,你仔細想想,每次裝箱后都是不同的對象,我怎麼判斷? object.ReferenceEquals 每次都是false…還有就是千萬不要Lock 字符串,簡單來說Lock字符串之後,只要是你以後有字符串匹配和你Lock里的內容有一樣的,那個該字符串也會被鎖定,相當於死鎖了。

  Lock和Monitor的區別不是很大,具體看以下代碼。

private static object obj = new object();
        public void LockSomething()
        {
            lock (obj)
            {
                dosomething();
            }
        }
        public void MonitorSomeThing()
        {
                Monitor.Enter(obj);
                dosomething();
                Monitor.Exit(obj);
        }
        public void dosomething()
        { 
            //做具體的事情
        }

   lock和Monitor是.NET用一個特殊結構實現的,Monitor對象是完全託管的、完全可移植的,並且在操作系統資源要求方面可能更為有效,同步速度較快,但不能跨進程同步。主要作用是鎖定臨界區,使臨界區代碼只能被獲得鎖的線程執行。Monitor.Wait和Monitor.Pulse用於線程同步,類似信號操作,個人感覺使用比較複雜,容易造成死鎖。

  lock就是封裝了Monitor.Enter和Monitor.Exit方法其實非常不難理解,只要確定Lock在啥時候用,該怎麼用就可以了,總結一句話。經常會應用於防止多線程操作導致公用變量值出現不確定的異常,用於確保操作的安全性。

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

【其他文章推薦】

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

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

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

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

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

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

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

redis源碼分析系列文章

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

為什麼要從Redis源碼分析 

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

Redis的雙向鏈表一文全知道

前言

hello,各位小可愛們,又見面了。今天這篇文章來自去年面試閱文的面試題,結果被虐了。這一part不說了,下次專門開一篇,寫下我面試被虐的名場面,尷尬的不行,全程尬聊。哈哈哈哈,話不多說,開始把。

 

今天要寫Redis的Hash類型,如果有對Redis不熟悉,或者對其他數據類型感興趣的,可以移步上面的系列文章。(最上面的最上面最上面,重要的事情說三遍)

在Redis中Hash類型的應用非常廣泛,其中key到value的映射就通過字典結構來維護的。記筆記,此處要考。

​

API使用

API的使用比較簡單,所以以下就粗略的寫了。

插入數據hset

使用hset命令往myhash中插入兩個key,value的鍵值對,分別是(name,zhangsan)和(age,20),返回值當前的myhash的長度。

獲取數據hget

使用hget命令獲取myhash中key為name的value值。

獲取所有數據hgetall

使用hgetall命令獲取myhash中所有的key和value值。

獲取所有key

使用hkeys命令獲取myhash中所有的key值。

獲取長度

使用hlen命令獲取myhash的長度。

獲取所有value

使用hvals命令獲取myhash中所有的value值。

具體邏輯圖

hash的底層主要是採用字典dict的結構,整體呈現層層封裝。

首先dict有四個部分組成,分別是dictType(類型,不咋重要),dictht(核心),rehashidx(漸進式hash的標誌),iterators(迭代器),這裏面最重要的就是dictht和rehashidx。

接下來是dictht,其有兩個數組構成,一個是真正的數據存儲位置,還有一個用於hash過程,包括的變量分別是真正的數據table和一些常見變量。

最後數據節點,和上篇說的雙向鏈表一樣,每個節點都有next指針,方便指向下一個節點,這樣目的是為了解決hash碰撞。具體的可以看下圖。

這邊看不懂沒關係,後面會針對每個模塊詳細說明。(千萬不要看到這裏就跳過啦)

​

雙向鏈表的定義

字典結構體dict

我們先看字典結構體dict,其包括四個部分,重點是dictht[2](真正的數據)和rehashidx(漸進式hash的標誌)。具體圖如下。

具體代碼如下:

//字典結構體 
 typedef struct dict {
    dictType *type;//類型,包括一些自定義函數,這些函數使得key和value能夠存儲 
    void *privdata;//私有數據 
    dictht ht[2];//兩張hash表 
    long rehashidx; //漸進式hash標記,如果為-1,說明沒在進行hash
    unsigned long iterators; //正在迭代的迭代器數量
} dict;

 

數組結構體dictht

dictht主要包括四個部分,1是真正的數據dictEntry類型的數組,裏面存放的是數據節點;2是數組長度size;3是進行hash運算的參數sizemask,這個不咋重要,只要記住等於size-1;4是數據節點數量used,當前有多少個數據節點。

具體代碼如下:

//hash結構體 
typedef struct dictht {
    dictEntry **table;//真正數據的數組 
    unsigned long size;//數組的大小 
    unsigned long sizemask;//用戶將hash映射到table的位置索引,他的值總是等於size-1 
    unsigned long used;//已用節點數量 
} dictht;

 

數據節點dictEntry

dictEntry為真正的數據節點,包括key,value和next節點。

//每個節點的結構體  
typedef struct dictEntry {
    void *key; //key
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;//value
    struct dictEntry *next; //下一個數據節點的地址
} dictEntry;

 

 

 

擴容過程和漸進式Hash圖解

我們先來第一個部分,dictht[2]為什麼會要2個數組存放,真正的數據只要一個數組就夠了?

這其實和Java的HashMap相似,都是數據加鏈表的結構,隨着數據量的增加,hash碰撞發生的就越頻繁,每個數組後面的鏈表就越長,整個鏈表顯得非常累贅。如果業務需要大量查詢操作,因為是鏈表,只能從頭部開始查詢,等一個數組的鏈表全部查詢完才能開始下一個數組,這樣查詢時間將無線拉長。

這無疑是要進行擴容,所以第一個數組存放真正的數據,第二個數組用於擴容用。第一個數組中的節點經過hash運算映射到第二個數組上,然後依次進行。那麼過程中還能對外提供服務嗎?答案是可以的,因為他可以隨時停止,這就到了下一個變量rehashidx。(一點都不生硬的轉場,哈哈哈)

rehashidx其實是一個標誌量,如果為-1說明當前沒有擴容,如果不為-1則表示當前擴容到哪個下標位置,方便下次進行從該下標位置繼續擴容。

這樣說是不是太抽象了,還是一臉懵逼,貼心的送上擴容過程全解,一定要點贊評論多誇誇我哦。(越來越不要臉了。。。)

步驟1

首先是未擴容前,rehashidx為-1,表示未擴容,第一個數組的dictEntry長度為4,一共有5個節點,所以used為5。

步驟2

當發生擴容了,rahashidx為第一個數組的第一個下標位置,即0。擴容之後的大小為大於used*2的2的n次方的最小值,即能包含這些節點*2的2的倍數的最小值。因為當前為5個數據節點,所以used*2=10,擴容后的數組大小為大於10的2的次方的最小值,為16。從第一個數組0下標位置開始,查找第一個元素,找到key為name,value為張三的節點,將其hash過,找到在第二個數組的下標為1的位置,將節點移過去,其實是指針的移動。這邊就簡單說了。

 

步驟3

key為name,value為張三的節點移動結束后,繼續移動第一個數組dictht[0]的下標為0的後續節點,移動步驟和上面相同。

步驟4

繼續移動第一個數組dictht[0]的下標為0的後續節點都移動完了,開始移動下標為1的節點,發現其沒有數據,所以移動下標為2的節點,同時修改rehashidx為2,移動步驟和上面相同。

整個過程的重點在於rehashidx,其為第一個數組正在移動的下標位置,如果當前內存不夠,或者操作系統繁忙,擴容的過程可以隨時停止。

停止之後如果對該對象進行操作,那是什麼樣子的呢?

  • 如果是新增,則直接新增后第二個數組,因為如果新增到第一個數組,以後還是要移過來,沒必要浪費時間
  • 如果是刪除,更新,查詢,則先查找第一個數組,如果沒找到,則再查詢第二個數組。

字典的實現(源碼分析)

創建並初始化字典

首先分配內存,接着調用初始化方法_dictInit,主要是賦值操作,重點看下rehashidx賦值為-1(這驗證了剛才的圖解,-1表示未進行hash擴容),最後返回是否創建成功。

/* 創建並初始化字典 */
dict *dictCreate(dictType *type,
        void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d));
    _dictInit(d,type,privDataPtr);
    return d;
}

/* Initialize the hash table */
int _dictInit(dict *d, dictType *type,
        void *privDataPtr)
{
    _dictReset(&d->ht[0]);
    _dictReset(&d->ht[1]);
    d->type = type;
    d->privdata = privDataPtr;
    d->rehashidx = -1;//賦值為-1,表示未進行hash
    d->iterators = 0;
    return DICT_OK;
}

 

擴容

dict裏面有一個靜態方法_dictExpandIfNeed,判斷是否需要擴容。

首先判斷通過dictIsRehashing方法,判斷是否處於hash狀態,其調用的是宏常量#define dictIsRehashing(d) ((d)->rehashidx != -1),即判斷rehashidx是否為-1,如果為-1,即不處於hash狀態,if條件為false,可以進行擴容,如果不為-1,即處於hash狀態,if條件為true,不可以進行擴容,直接返回常量DICT_OK。

接着判斷第一個數組的size是否為0,如果為0,則擴容為默認大小4,如果不為0,則執行下面的代碼。

再接着判斷是否需要擴容,if中有三個條件,具體的分析如下。

最後就是調用dictExpand擴容方法了,參數為數據節點的雙倍大小ht[0].used*2。此處驗證了上面擴容過程的數組大小16。

擴容方法比較簡單點,獲取擴容后的大小,將第二個設置新的大小。

這樣講感覺有點空,看下流程圖。

擴容流程圖

具體代碼:

static int _dictExpandIfNeeded(dict *d)
{
    //判斷是否處於擴容狀態中,通過調用宏常量#define dictIsRehashing(d) ((d)->rehashidx != -1)
    //來判斷是否可以擴容
    if (dictIsRehashing(d)) return DICT_OK;

    //判斷第一個數組size是否為0,如果為0,則調用擴容方法,大小為宏常量
    //#define DICT_HT_INITIAL_SIZE     4
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    //下面先列出if條件中所使用到的參數 
    // static int dict_can_resize = 1;數值為1表示可以擴容
    //static unsigned int dict_force_resize_ratio = 5;
    //我們來分析if條件,如果第一個數組的所有節點數量大於等於第一個數組的大小(表示節點數據已經有些多)
    //並且可用擴容(數值為1)或者所有節點數量除以數組大小大於5
    //這個條件表示擴容那個的條件,第一個就是節點必要大於等於數組長度,
    //第二點就再可以擴容和數據太多,超過5兩个中選其一
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {
        //調用擴容方法
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

int dictExpand(dict *d, unsigned long size)
{
    dictht n;
    //獲取擴容后真正的大小,找到比size大的最小值,且是2的倍數
    unsigned long realsize = _dictNextPower(size);

    //一些判斷條件
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    if (realsize == d->ht[0].size) return DICT_ERR;

    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    //第一個hash為null,說明在初始化 
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }
    //正在hash,給第二個hash的長度設置新的, 
    d->ht[1] = n;
    d->rehashidx = 0;//設置當前正在hash 
    return DICT_OK;
}

/* 找到比size大的最小值,且是2的倍數 */
static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE;

    if (size >= LONG_MAX) return LONG_MAX;
    while(1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}

 

 

漸進式hash

漸進式hash過程已經通過上面圖解說明,以下主要看下代碼是如何實現的,以及過程是不是對的。

擴容之後就是執行dictRehash方法,參數包括待移動的哈希表d和步驟数字n。

首先判斷標誌量rehashidx是否等於-1,如果等於-1,則表示hash完成,如果不等於-1,則執行下面的代碼。

接着進行循環,遍歷第一個數組上的每個下標,每次移動下標位置,都需要更新rehashidx值,每次加1。

再接着進行第二個循環,遍歷下標的鏈表每個節點,完成數據的遷移,主要是指針的移動和一些參數的修改。

最後,返回int數值,如果為0表示整個數據全部hash完成,如果返回1則表示部分hash結束,並沒有全部完成,下次可以通過rehashidx值繼續hash。

具體代碼如下:

//重新hash這個哈希表
 // Redis的哈希表結構公有兩個table數組,t0和t1,平常只使用一個t0,當需要重hash時則重hash到另一個table數組中
 //參數列表
 // 1. d: 待移動的哈希表,結構中存有目前已經重hash到哪個桶了
  //  2. n: N步進行rehash 
// 返回值 返回0說明整個表都重hash完成了,返回1代表未完成
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; 
    //如果當前rehashidx=-1,則返回0,表示hash完成 
    if (!dictIsRehashing(d)) return 0;
    //分n步,而且ht[0]還有沒有移動的節點 
    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        //第一個循環用來更新 rehashidx 的值,因為有些桶為空,所以 rehashidx並非每次都比原來前進一個位置,而是有可能前進幾個位置,但最多不超過 10。
        //將rehashidx移動到ht[0]有節點的下標,也就是table[d->rehashidx]非空
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];  //第二個循環用來將ht[0]表中每次找到的非空桶中的鏈表(或者就是單個節點)拷貝到ht[1]中

        /* 利用循環講數據節點移過去 */
        while(de) {
            unsigned int h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    return 1;
}

總結

該篇主要講了Redis的Hash數據類型的底層實現字典結構Dict,先從Hash的一些API使用,引出字典結構Dict,剖析了其三個主要組成部分,字典結構體Dict,數組結構體Dictht,數據節點結構體DictEntry,進而通過多幅過程圖解釋了擴容過程和rehash過程,最後結合源碼對字典進行描述,如創建過程,擴容過程,漸進式hash過程,中間穿插流程圖講解。

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

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

好了,拜拜咯。

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

【其他文章推薦】

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

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

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

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

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

從linux源碼看socket(tcp)的timeout

從linux源碼看socket(tcp)的timeout

前言

網絡編程中超時時間是一個重要但又容易被忽略的問題,對其的設置需要仔細斟酌。在經歷了數次物理機宕機之後,筆者詳細的考察了在網絡編程(tcp)中的各種超時設置,於是就有了本篇博文。本文大部分討論的是socket設置為block的情況,即setNonblock(false),僅在最後提及了nonblock socket(本文基於linux 2.6.32-431內核)。

connectTimeout

在討論connectTimeout之前,讓我們先看下java和C語言對於socket connect調用的函數簽名:

java:
 // 函數調用中攜帶有超時時間
 public void connect(SocketAddress endpoint, int timeout) ;
C語言:
 // 函數調用中並不攜帶超時時間
 int connect(int sockfd, const struct sockaddr * sockaddr, socklen_t socklent) 	 

操作系統提供的connect系統調用並沒有提供timeout的參數設置而java卻有,我們先考察一下原生系統調用的超時策略。

connect系統調用

我們觀察一下此系統調用的kernel源碼,調用棧如下所示:

connect[用戶態]
	|->SYSCALL_DEFINE3(connect)[內核態]
			|->sock->ops->connect

由於我們考察的是tcp的connect,其socket的內部結構如下圖所示:

最終調用的是tcp_connect,代碼如下所示:

int tcp_connect(struct sock *sk) {
	......
	// 發送SYN
	err = tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
	...
	/* Timer for repeating the SYN until an answer. */
	// 由於是剛建立連接,所以其rto是TCP_TIMEOUT_INIT
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
				inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
	return 0;	
}

又上面代碼可知,在tcp_connect設置了重傳定時器之後return回了tcp_v4_connect再return到inet_stream_connect。我們繼續考察:

int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
			int addr_len, int flags)
{
	......
	// tcp_v4_connect=>tcp_connect
	err = sk->sk_prot->connect(sk, uaddr, addr_len);
	// 這邊用的是sk->sk_sndtimeo
	timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
	......
	inet_wait_for_connect(sk, timeo));
	......
out:
	release_sock(sk);
	return err;

sock_error:
	err = sock_error(sk) ? : -ECONNABORTED;
	sock->state = SS_UNCONNECTED;
	if (sk->sk_prot->disconnect(sk, flags))
		sock->state = SS_DISCONNECTING;
	goto out
}

由上面代碼可見,可以採用設置SO_SNDTIMEO來控制connect系統調用的超時,如下所示:

setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);

不設置SO_SNDTIMEO

如果不設置SO_SNDTIMEO,那麼會由tcp重傳定時器在重傳超過設置的時候后超時,如下圖所示:

這個syn重傳的次數由:

cat /proc/sys/net/ipv4/tcp_syn_retries 筆者機器上是5 

來決定。那麼我們就來看一下這個重傳到底是多長時間:

tcp_connect中:
		// 設置的初始超時時間為icsk_rto=TCP_TIMEOUT_INIT為1s
		inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
				inet_csk(sk)->icsk_rto, TCP_RTO_MAX);

其重傳定時器的回掉函數為tcp_retransmit_timer:

void tcp_retransmit_timer(struct sock *sk)
{
	......
	// 檢測是否超時
	if (tcp_write_timeout(sk))
		goto out;
	......
	// icsk_rto = icsk_rto * 2,由於syn階段,所以isck_rto不會由於網絡傳輸而改變
	// 重傳的時候會以1,2,4,8指數遞增
	icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);
	// 重設timer
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);
out:;		
}

而計算tcp_write_timeout的邏輯則是在這篇blog中已經詳細描述過,

https://my.oschina.net/alchemystar/blog/1936433

只不過在connect時刻,重傳的計算以TCP_TIMEOUT_INIT為單位進行計算。而ESTABLISHED(read/write)時刻,重傳以TCP_RTO_MIN進行計算。那麼根據這段重傳邏輯,我們就可以計算出不同tcp_syn_retries最終表現的超時時間。如下圖所示:

那麼整理下錶格,對於系統調用,connect的超時時間為:

tcp_syn_retries timeout
1 min(so_sndtimeo,3s)
2 min(so_sndtimeo,7s)
3 min(so_sndtimeo,15s)
4 min(so_sndtimeo,31s)
5 min(so_sndtimeo,63s)

上述超時時間和筆者的實測一致。

kernel代碼版本細微變化

值得注意的是,linux本身官方發布的2.6.32源碼對於tcp_syn_retries2的解釋和RFC並不一致(至少筆者閱讀的代碼如此,這個細微的變化困擾了筆者好久,筆者下載了和機器對應的內核版本后才發現代碼改了)。而redhat發布的2.6.32-431已經修復了這個問題(不清楚具體哪個小版本修改的),並將初始RTO設置為1s(官方2.6.32為3s)。這也是,不同內核小版本上的實驗會有不同的connect timeout表現的原因(有的抓包到的重傳SYN時間間隔為3,6,12……)。以下為代碼對比:

========================>linux 內核版本2.6.32-431<========================
#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ))	/* RFC2988bis initial RTO value	*/

static inline bool retransmits_timed_out(struct sock *sk,
					 unsigned int boundary,
					 unsigned int timeout,
					 bool syn_set)
{
	......
	unsigned int rto_base = syn_set ? TCP_TIMEOUT_INIT : TCP_RTO_MIN;
	......
	timeout = ((2 << boundary) - 1) * rto_base;
	......

}
========================>linux 內核版本2.6.32.63<========================
#define TCP_TIMEOUT_INIT ((unsigned)(3*HZ))	/* RFC 1122 initial RTO value	*/

static inline bool retransmits_timed_out(struct sock *sk,
					 unsigned int boundary
{
	......
	timeout = ((2 << boundary) - 1) * TCP_RTO_MIN;
	......
}

另外,tcp_syn_retries重傳次數可以在單個socket中通過setsockopt設置。

JAVA connect API

現在我們考察下java的connect api,其connect最終調用下面的代碼:

Java_java_net_PlainSocketImpl_socketConnect(...){

    if (timeout <= 0) {
    	 ......
        connect_rv = NET_Connect(fd, (struct sockaddr *)&him, len);
    	 .....
    }else{
    	 // 如果timeout > 0 ,則設置為nonblock模式
        SET_NONBLOCKING(fd);
        /* no need to use NET_Connect as non-blocking */
        connect_rv = connect(fd, (struct sockaddr *)&him, len);
        /*
         * 這邊用系統調用select來模擬阻塞調用超時
         */
        while (1) {
            ......
            struct timeval t;
            t.tv_sec = timeout / 1000;
            t.tv_usec = (timeout % 1000) * 1000;
            connect_rv = NET_Select(fd+1, 0, &wr, &ex, &t);
            ......
        }
        ......
        // 重新設置為阻塞模式
        SET_BLOCKING(fd);
        ......
    }
}

其和connect系統調用的不同點是,在timeout為0的時候,走默認的系統調用不設置超時時間的邏輯。在timeout>0時,將socket設置為非阻塞,然後用select系統調用去模擬超時,而沒有走linux本身的超時邏輯,如下圖所示:

由於沒有java並沒有設置so_sndtimeo的選項,所以在timeout為0的時候,直接就通過重傳次數來控制超時時間。而在調用connect時設置了timeout(不為0)的時候,超時時間如下錶格所示:

tcp_syn_retries timeout
1 min(timeout,3s)
2 min(timeout,7s)
3 min(timeout,15s)
4 min(timeout,31s)
5 min(timeout,63s)

socketTimeout

write系統調用的超時時間

socket的write系統調用最後調用的是tcp_sendmsg,源碼如下所示:

int tcp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
		size_t size){
	......
	timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);
	......
	while (--iovlen >= 0) {
		......
		// 此種情況是buffer不夠了
		if (copy <= 0) {
	new_segment:
		  ......
		  if (!sk_stream_memory_free(sk))
			  goto wait_for_sndbuf;

		  skb = sk_stream_alloc_skb(sk, select_size(sk),sk->sk_allocation);
		  if (!skb)
			  goto wait_for_memory;
		}
		......
	}
	......
	// 這邊等待write buffer有空間
wait_for_sndbuf:
		set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
		if (copied)
			tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
			// 這邊等待timeo長的時間
		if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
			goto do_error;
		......
out:
	// 如果拷貝了數據,則返回
	if (copied)
		tcp_push(sk, flags, mss_now, tp->nonagle);
	TCP_CHECK_TIMER(sk);
	release_sock(sk);
	return copied;		
out_err:
	// error的處理
	err = sk_stream_error(sk, flags, err);
	TCP_CHECK_TIMER(sk);
	release_sock(sk);
	return err;		
}

從上面的內核代碼看出,如果socket的write buffer依舊有空間的時候,會立馬返回,並不會有timeout。但是write buffer不夠的時候,會等待SO_SNDTIMEO的時間(nonblock時候為0)。但是如果SO_SNDTIMEO沒有設置的時候,默認初始化為MAX_SCHEDULE_TIMEOUT,可以認為其超時時間為無限。那麼其超時時間會有另一個條件來決定,我們看下sk_stream_wait_memory的源碼:

int sk_stream_wait_memory(struct sock *sk, long *timeo_p){
		// 等待socket shutdown或者socket出現err
		sk_wait_event(sk, &current_timeo, sk->sk_err ||
						  (sk->sk_shutdown & SEND_SHUTDOWN) ||
						  (sk_stream_memory_free(sk) &&
						  !vm_wait));
}						 

在write等待的時候,如果出現socket被shutdown或者socket出現錯誤的時候,則會跳出wait進而返回錯誤。在不考慮對端shutdown的情況下,出現sk_err的時間其實就是其write的timeout時間,那麼我們看下什麼時候出現sk->sk_err。

SO_SNDTIMEO不設置,write buffer滿之後ack一直不返回的情況(例如,物理機宕機)

物理機宕機后,tcp發送msg的時候,ack不會返回,則會在重傳定時器tcp_retransmit_timer到期后timeout,其重傳到期時間通過tcp_retries2以及TCP_RTO_MIN計算出來。其源碼可見筆者的blog:

https://my.oschina.net/alchemystar/blog/1936433

tcp_retries2的設置位置為:

cat /proc/sys/net/ipv4/tcp_retries2 筆者機器上是5,默認是15

SO_SNDTIMEO不設置,write buffer滿之後對端不消費,導致buffer一直滿的情況

和上面ack超時有些許不一樣的是,一個邏輯是用TCP_RTO_MIN通過tcp_retries2計算出來的時間。另一個是真的通過重傳超過tcp_retries2次數來time_out,兩者的區別和rto的動態計算有關。但是可以大致認為是一致的。

上述邏輯如下圖所示:

write_timeout表格

tcp_retries2 buffer未滿 buffer滿
5 立即返回 min(SO_SNDTIMEO,(25.6s-51.2s)根據動態rto定
15 立即返回 min(SO_SNDTIMEO,(924.6s-1044.6s)根據動態rto定

java的SocketOutputStream的sockWrite0超時時間

java的sockWrite0沒有設置超時時間的地方,同時也沒有設置過SO_SNDTIMEOUT,其直接調用了系統調用,所以其超時時間和write系統調用保持一致。

readTimeout

ReadTimeout可能是最容易導致問題的地方。我們先看下系統調用的源碼:

read系統調用

socket的read系統調用最終調用的是tcp_recvmsg, 其源碼如下:

int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int nonblock, int flags, int *addr_len)
{
	......
	// 這邊timeo=SO_RCVTIMEO
	timeo = sock_rcvtimeo(sk, nonblock);
	......
	do{
		......
		// 下面這一堆判斷表明,如果出現錯誤,或者已經被CLOSE/SHUTDOWN則跳出循環
		if(copied) {
			if (sk->sk_err ||
			    sk->sk_state == TCP_CLOSE ||
			    (sk->sk_shutdown & RCV_SHUTDOWN) ||
			    !timeo ||
			    signal_pending(current))
				break;
		} else {
			if (sock_flag(sk, SOCK_DONE))
				break;

			if (sk->sk_err) {
				copied = sock_error(sk);
				break;
			}
			// 如果socket shudown跳出
			if (sk->sk_shutdown & RCV_SHUTDOWN)
				break;
			// 如果socket close跳出
			if (sk->sk_state == TCP_CLOSE) {
				if (!sock_flag(sk, SOCK_DONE)) {
					/* This occurs when user tries to read
					 * from never connected socket.
					 */
					copied = -ENOTCONN;
					break;
				}
				break;
			}
			.......
		}
		.......

		if (copied >= target) {
			/* Do not sleep, just process backlog. */
			release_sock(sk);
			lock_sock(sk);
		} else /* 如果沒有讀到target自己數(和水位有關,可以暫認為是1),則等待SO_RCVTIMEO的時間 */
			sk_wait_data(sk, &timeo);	
	} while (len > 0);
	......
}

上面的邏輯如下圖所示:

重傳以及探測定時器timeout事件的觸發時機如下圖所示:

如果內核層面ack正常返回而且對端窗口不為0,僅僅應用層不返回任何數據,那麼就會無限等待,直到對端有數據或者socket close/shutdown為止,如下圖所示:

很多應用就是基於這個無限超時來設計的,例如activemq的消費者邏輯。

java的SocketInputStream的sockRead0超時時間

java的超時時間由SO_TIMOUT決定,而linux的socket並沒有這個選項。其sockRead0和上面的java connect一樣,在SO_TIMEOUT>0的時候依舊是由nonblock socket模擬,在此就不再贅述了。

ReadTimeout超時表格

C系統調用:

tcp_retries2 對端無響應 對端內核響應正常
5 min(SO_RCVTIMEO,(25.6s-51.2s)根據動態rto定 SO_RCVTIMEO==0?無限,SO_RCVTIMEO)
15 min(SO_RCVTIMEO,(924.6s-1044.6s)根據動態rto定 SO_RCVTIMEO==0?無限,SO_RCVTIMEO)

Java系統調用

tcp_retries2 對端無響應 對端內核響應正常
5 min(SO_TIMEOUT,(25.6s-51.2s)根據動態rto定 SO_TIMEOUT==0?無限,SO_RCVTIMEO
15 min(SO_TIMEOUT,(924.6s-1044.6s)根據動態rto定 SO_TIMEOUT==0?無限,SO_RCVTIMEO

對端物理機宕機之後的timeout

對端物理機宕機后還依舊有數據發送

對端物理機宕機時對端內核也gg了(不會發出任何包通知宕機),那麼本端發送任何數據給對端都不會有響應。其超時時間就由上面討論的
min(設置的socket超時[例如SO_TIMEOUT],內核內部的定時器超時來決定)。

對端物理機宕機后沒有數據發送,但在read等待

這時候如果設置了超時時間timeout,則在timeout后返回。但是,如果僅僅是在read等待,由於底層沒有數據交互,那麼其無法知道對端是否宕機,所以會一直等待。但是,內核會在一個socket兩個小時都沒有數據交互情況下(可設置)啟動keepalive定時器來探測對端的socket。如下圖所示:

大概是2小時11分鐘之後會超時返回。keepalive的設置由內核參數指定:

cat /proc/sys/net/ipv4/tcp_keepalive_time 7200 即兩個小時后開始探測
cat /proc/sys/net/ipv4/tcp_keepalive_intvl 75 即每次探測間隔為75s
cat /proc/sys/net/ipv4/tcp_keepalve_probes 9 即一共探測9次

可以在setsockops中對單獨的socket指定是否啟用keepalive定時器(java也可以)。

對端物理機宕機后沒有數據發送,也沒有read等待

和上面同理,也是在keepalive定時器超時之後,將連接close。所以我們可以看到一個不活躍的socket在對端物理機突然宕機之後,依舊是ESTABLISHED狀態,過很長一段時間之後才會關閉。

進程宕后的超時

如果僅僅是對端進程宕機的話(進程所在內核會close其所擁有的所有socket),由於fin包的發送,本端內核可以立刻知道當前socket的狀態。如果socket是阻塞的,那麼將會在當前或者下一次write/read系統調用的時候返回給應用層相應的錯誤。如果是nonblock,那麼會在select/epoll中觸發出對應的事件通知應用層去處理。
如果fin包沒發送到對端,那麼在下一次write/read的時候內核會發送reset包作為回應。

nonblock

設置為nonblock=true后,由於read/write都是立刻返回,且通過select/epoll等處理重傳超時/probe超時/keep alive超時/socket close等事件,所以根據應用層代碼決定其超時特性。定時器超時事件發生的時間如上面幾小節所述,和是否nonblock無關。nonblock的編程模式可以讓應用層對這些事件做出響應。

總結

網絡編程中超時時間是個重要但又容易被忽略的問題,這個問題只有在遇到物理機宕機等平時遇不到的現象時候才會凸顯。筆者在經曆數次物理機宕機之後才好好的研究了一番,希望本篇文章可以對讀者在以後遇到類似超時問題時有所幫助。

公眾號

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

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

【String註解驅動開發】如何按照條件向Spring容器中註冊bean?這次我懂了!!

寫在前面

當bean是單實例,並且沒有設置懶加載時,Spring容器啟動時,就會實例化bean,並將bean註冊到IOC容器中,以後每次從IOC容器中獲取bean時,直接返回IOC容器中的bean,不再創建新的bean。

如果bean是單實例,並且使用@Lazy註解設置了懶加載,則Spring容器啟動時,不會實例化bean,也不會將bean註冊到IOC容器中,只有第一次獲取bean的時候,才會實例化bean,並且將bean註冊到IOC容器中。

如果bean是多實例,則Spring容器啟動時,不會實例化bean,也不會將bean註冊到IOC容器中,以後每次從IOC容器中獲取bean時,都會創建一個新的bean返回。

Spring支持按照條件向IOC容器中註冊bean,滿足條件的bean就會被註冊到IOC容器中,不滿足條件的bean就不會被註冊到IOC容器中。接下來,我們就一起來探討Spring中如何實現按照條件向IOC容器中註冊bean。

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

@Conditional註解概述

@Conditional註解可以按照一定的條件進行判斷,滿足條件向容器中註冊bean,不滿足條件就不向容器中註冊bean。

@Conditional註解是由 SpringFramework 提供的一個註解,位於 org.springframework.context.annotation 包內,定義如下。

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
	Class<? extends Condition>[] value();
}

從@Conditional註解的源碼來看,@Conditional註解可以添加到類上,也可以添加到方法上。在@Conditional註解中,存在一個Condition類型或者其子類型的Class對象數組,Condition是個啥?我們點進去看一下。

package org.springframework.context.annotation;

import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.core.type.AnnotatedTypeMetadata;
@FunctionalInterface
public interface Condition {
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

可以看到,Condition是一個函數式接口,對於函數式接口不了解的同學可以參見【Java8新特性】中的《【Java8新特性】還沒搞懂函數式接口?趕快過來看看吧!》一文。也可以直接查看《Java8新特性專欄》來系統學習Java8的新特性。

所以,我們使用@Conditional註解時,需要一個類實現Spring提供的Condition接口,它會匹配@Conditional所符合的方法,然後我們可以使用我們在@Conditional註解中定義的類來檢查。

@Conditional註解的使用場景如下所示。

  • 可以作為類級別的註解直接或者間接的與@Component相關聯,包括@Configuration類;
  • 可以作為元註解,用於自動編寫構造性註解;
  • 作為方法級別的註解,作用在任何@Bean方法上。

向Spring容器註冊bean

不帶條件註冊bean

我們在PersonConfig2類中新增person01()方法和person02()方法,併為兩個方法添加@Bean註解,如下所示。

@Bean("binghe001")
public Person person01(){
    return new Person("binghe001", 18);
}

@Bean("binghe002")
public Person person02(){
    return new Person("binghe002", 20);
}

那麼,這兩個bean默認是否會被註冊到Spring容器中呢,我們新建一個測試用例來測試一下。在SpringBeanTest類中新建testAnnotationConfig6()方法,如下所示。

@Test
public void testAnnotationConfig6(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    String[] names = context.getBeanNamesForType(Person.class);
    Arrays.stream(names).forEach(System.out::println);
}

我們運行testAnnotationConfig6()方法,輸出的結果信息如下所示。

person
binghe001
binghe002

從輸出結果可以看出,同時輸出了binghe001和binghe002。說明默認情況下,Spring容器會將單實例並且非懶加載的bean註冊到IOC容器中。

接下來,我們再輸出bean的名稱和bean實例對象信息,此時我們在testAnnotationConfig6()方法中添加相應的代碼片段,如下所示。

@Test
public void testAnnotationConfig6(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    String[] names = context.getBeanNamesForType(Person.class);
    Arrays.stream(names).forEach(System.out::println);

    Map<String, Person> beans = context.getBeansOfType(Person.class);
    System.out.println(beans);
}

再次運行SpringBeanTest類中的testAnnotationConfig6()方法,輸出結果如下所示。

person
binghe001
binghe002
給容器中添加Person....
{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18), binghe002=Person(name=binghe002, age=20)}

可以看到,輸出了註冊到容器的bean。

帶條件註冊bean

現在,我們就要提出新的需求了,比如,如果當前操作系統是Windows操作系統,則向Spring容器中註冊binghe001;如果當前操作系統是Linux操作系統,則向Spring容器中註冊binghe002。此時,我們就需要使用@Conditional註解了。

這裏,有小夥伴可能會問:如何獲取操作系統的類型呢,別急,這個問題很簡單,我們繼續向下看。

使用Spring的ApplicationContext接口就能夠獲取到當前操作系統的類型,如下所示。

ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
Environment environment = context.getEnvironment();
String osName = environment.getProperty("os.name");
System.out.println(osName);

我們將上述代碼整合到SpringBeanTest類中的testAnnotationConfig6()方法中,如下所示。

@Test
public void testAnnotationConfig6(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    Environment environment = context.getEnvironment();
    String osName = environment.getProperty("os.name");
    System.out.println(osName);

    String[] names = context.getBeanNamesForType(Person.class);
    Arrays.stream(names).forEach(System.out::println);

    Map<String, Person> beans = context.getBeansOfType(Person.class);
    System.out.println(beans);
}

接下來,我們運行SpringBeanTest類中的testAnnotationConfig6()方法,輸出的結果信息如下所示。

Windows 10
person
binghe001
binghe002
給容器中添加Person....
{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18), binghe002=Person(name=binghe002, age=20)}

由於我使用的操作系統是Windows 10操作系統,所以在結果信息中輸出了Windows 10。

到這裏,我們成功獲取到了操作系統的類型,接下來,就可以實現:如果當前操作系統是Windows操作系統,則向Spring容器中註冊binghe001;如果當前操作系統是Linux操作系統,則向Spring容器中註冊binghe002的需求了。此時,我們就需要藉助Spring的@Conditional註解來實現了。

要想使用@Conditional註解,我們需要實現Condition接口來為@Conditional註解設置條件,所以,這裏,我們創建了兩個實現Condition接口的類,分別為WindowsCondition和LinuxCondition,如下所示。

  • WindowsCondition
package io.mykit.spring.plugins.register.condition;


import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * @author binghe
 * @version 1.0.0
 * @description Windows條件,判斷操作系統是否是Windows
 */
public class WindowsCondition implements Condition {
    /**
     * ConditionContext:判斷條件使用的上下文環境
     * AnnotatedTypeMetadata:註釋信息
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //判斷是否是Linux系統
        //1.獲取到IOC容器使用的BeanFactory
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //2.獲取類加載器
        ClassLoader classLoader = context.getClassLoader();
        //3.獲取當前的環境信息
        Environment environment = context.getEnvironment();
        //4.獲取bean定義的註冊類,我們可以通過BeanDefinitionRegistry對象查看
        //Spring容器中註冊了哪些bean,也可以通過BeanDefinitionRegistry對象向
        //Spring容器中註冊bean,移除bean,查看bean的定義,查看是否包含某個bean的定義
        BeanDefinitionRegistry registry = context.getRegistry();
        String property = environment.getProperty("os.name");
        return property.contains("Windows");
    }
}
  • LinuxCondition
package io.mykit.spring.plugins.register.condition;

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * @author binghe
 * @version 1.0.0
 * @description Linux條件,判斷操作系統是否是Linux
 */
public class LinuxCondition implements Condition {
    /**
     * ConditionContext:判斷條件使用的上下文環境
     * AnnotatedTypeMetadata:註釋信息
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //判斷是否是Linux系統
        //1.獲取到IOC容器使用的BeanFactory
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //2.獲取類加載器
        ClassLoader classLoader = context.getClassLoader();
        //3.獲取當前的環境信息
        Environment environment = context.getEnvironment();
        //4.獲取bean定義的註冊類,我們可以通過BeanDefinitionRegistry對象查看
        //Spring容器中註冊了哪些bean,也可以通過BeanDefinitionRegistry對象向
        //Spring容器中註冊bean,移除bean,查看bean的定義,查看是否包含某個bean的定義
        BeanDefinitionRegistry registry = context.getRegistry();
        String property = environment.getProperty("os.name");
        return property.contains("linux");
    }
}

接下來,我們就需要在PersonConfig2類中使用@Conditional註解添加條件了。添加註解后的方法如下所示。

@Conditional({WindowsCondition.class})
@Bean("binghe001")
public Person person01(){
    return new Person("binghe001", 18);
}

@Conditional({LinuxCondition.class})
@Bean("binghe002")
public Person person02(){
    return new Person("binghe002", 20);
}

此時,我們再次運行SpringBeanTest類中的testAnnotationConfig6()方法,輸出的結果信息如下所示。

Windows 10
person
binghe001
給容器中添加Person....
{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18)}

可以看到,輸出結果中不再含有名稱為binghe002的bean了,說明程序中檢測到當前操作系統為Windows10,沒有向Spring容器中註冊名稱為binghe002的bean。

@Conditional註解也可以標註在類上,標註在類上含義為:滿足當前條件,這個類中配置的所有bean註冊才能生效,大家可以自行驗證@Conditional註解標註在類上的情況

@Conditional的擴展註解

@ConditionalOnBean:僅僅在當前上下文中存在某個對象時,才會實例化一個Bean。
@ConditionalOnClass:某個class位於類路徑上,才會實例化一個Bean。
@ConditionalOnExpression:當表達式為true的時候,才會實例化一個Bean。
@ConditionalOnMissingBean:僅僅在當前上下文中不存在某個對象時,才會實例化一個Bean。
@ConditionalOnMissingClass:某個class類路徑上不存在的時候,才會實例化一個Bean。
@ConditionalOnNotWebApplication:不是web應用,才會實例化一個Bean。
@ConditionalOnBean:當容器中有指定Bean的條件下進行實例化。
@ConditionalOnMissingBean:當容器里沒有指定Bean的條件下進行實例化。
@ConditionalOnClass:當classpath類路徑下有指定類的條件下進行實例化。
@ConditionalOnMissingClass:當類路徑下沒有指定類的條件下進行實例化。
@ConditionalOnWebApplication:當項目是一個Web項目時進行實例化。
@ConditionalOnNotWebApplication:當項目不是一個Web項目時進行實例化。
@ConditionalOnProperty:當指定的屬性有指定的值時進行實例化。
@ConditionalOnExpression:基於SpEL表達式的條件判斷。
@ConditionalOnJava:當JVM版本為指定的版本範圍時觸發實例化。
@ConditionalOnResource:當類路徑下有指定的資源時觸發實例化。
@ConditionalOnJndi:在JNDI存在的條件下觸發實例化。
@ConditionalOnSingleCandidate:當指定的Bean在容器中只有一個,或者有多個但是指定了首選的Bean時觸發實例化。

@Conditional 與@Profile 的對比

Spring3.0 也有一些和@Conditional 相似的註解,它們是Spring SPEL 表達式和Spring Profiles 註解 Spring4.0的@Conditional 註解要比@Profile 註解更加高級。@Profile 註解用來加載應用程序的環境。@Profile註解僅限於根據預定義屬性編寫條件檢查。 @Conditional註釋則沒有此限制。

Spring中的@Profile 和 @Conditional 註解用來檢查”If…then…else”的語義。然而,Spring4 @Conditional是@Profile 註解的更通用法。

  • Spring 3中的 @Profile僅用於編寫基於Environment變量的條件檢查。 配置文件可用於基於環境加載應用程序配置。
  • Spring 4 @Conditional註解允許開發人員為條件檢查定義用戶定義的策略。 @Conditional可用於條件bean註冊。

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

參考:
https://www.cnblogs.com/cxuanBlog/p/10960575.html

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

數據誤操作,教你使用ApexSQLLog工具從 SQLServer日誌恢複數據!

前幾天同事不小心誤操作,將SQLServer庫的一張表的一個狀態字段給刷成了一個統一狀態,由於是update執行所以原來的相關狀態無法確定。發生這種事情的時候我的小夥伴背後 一涼。

由於是在開發試運行中的項目,還沒來得及進行備份處理,所以從備份恢復宣告失敗。就算有備份那麼恢復的也是備份時間節點的數據,意味着使用平台做的數據需要從備份時間重新做過,而且有可能有遺漏。

小夥伴問我這咋辦,首先沒有備份,那麼只有從數據庫日誌查找,然後看能不能通過日誌找回之前的數據,再還原到刷狀態之前的數據。然後就找到了ApexSQLLog工具,接下來我介紹下這款工具的使用和如何恢複數據。ApexSQLLog有幾個版本,我是用的是ApexSQLLog2014支持SqlServer更高的版本,數據庫使用的是SqlSerVer2014。

ApexSQLLog2014
提取碼: np4f

  • 首先建一個測試庫,和一張測試表。
    測試庫ApexSQLLogTest和測試用的表TestUser,然後我手動編輯了三條數據進去,保存編輯的數據。

  • 用ApexSQLLog打開測試庫日誌
    選擇要連接的數據庫,也可以從最近的session中打開,打開篩選過的記錄可以保存未session。
    然後選中要篩選的日誌文件,如果有備份數據庫文件也會自動查找到並在這裏羅列出來,自己按情況選擇。

  • 條件篩選
    我們選擇日誌文件后就進入到篩選條件選擇,可以在篩選條件裏面自由組合。
    可以選擇時間段(Time range)、操作(operations)、表(tables)。

    高級選項(advanced options)裏面還有用戶、字段條件等可以選擇。

  • 查看日誌數據
    當我們組合完篩選條件后,就進入到日誌分析界面,可以看到我們之前手動插入的三條數據實際已經在日誌裏面了,分成了三條insert語句。在選中其中一條日誌的時候在下面可以看到執行的各字段值的修改情況。

    可以點擊 下面的Row history查看記錄,Redo script可以生成執行的操作, Undo script可以還原到之前的數據。我們恢複數據就是使用Undo script。

  • 恢複數據測試。
    我們使用update語句將Status狀態全都重置為3。

update TestUser set Status=3 

然後刷新下日誌,會看到多出了三條Update日誌記錄,點擊第一條看到下面的Status字段從0變為了3。

我們選中這三條記錄右鍵或者上面的菜單欄功能,用create undo script 生成恢復sql。

--	This UNDO script was generated with ApexSQL Log 2014.04.1133 on 2020-06-10 11:18:47.601
--	NOTE: Operations in UNDO scripts are always output in descending order.
--	SERVER VIP-966\SQLEXPRESS
--	DATABASE ApexSQLLogTest

--	UPDATE (00000024:000000A0:0004) done at 2020-06-10 11:09:36.293 by VIP-966\Administrator in transaction 0000:0000034B (Committed)
BEGIN TRANSACTION 
UPDATE [dbo].[TestUser] SET [Status] = 2 WHERE [Id] = 3
IF @@ROWCOUNT <= 1 COMMIT TRANSACTION ELSE BEGIN ROLLBACK TRANSACTION; PRINT 'ERROR: STATEMENT AFFECTED MORE THAN ONE ROW. ALL THE CHANGES WERE ROLLED BACK.' END
--	UPDATE (00000024:000000A0:0003) done at 2020-06-10 11:09:36.293 by VIP-966\Administrator in transaction 0000:0000034B (Committed)
BEGIN TRANSACTION 
UPDATE [dbo].[TestUser] SET [Status] = 1 WHERE [Id] = 2
IF @@ROWCOUNT <= 1 COMMIT TRANSACTION ELSE BEGIN ROLLBACK TRANSACTION; PRINT 'ERROR: STATEMENT AFFECTED MORE THAN ONE ROW. ALL THE CHANGES WERE ROLLED BACK.' END
--	UPDATE (00000024:000000A0:0002) done at 2020-06-10 11:09:36.293 by VIP-966\Administrator in transaction 0000:0000034B (Committed)
BEGIN TRANSACTION 
UPDATE [dbo].[TestUser] SET [Status] = 0 WHERE [Id] = 1
IF @@ROWCOUNT <= 1 COMMIT TRANSACTION ELSE BEGIN ROLLBACK TRANSACTION; PRINT 'ERROR: STATEMENT AFFECTED MORE THAN ONE ROW. ALL THE CHANGES WERE ROLLED BACK.' END
GO

--	FINISHED ON 2020-06-10 11:18:47.697
--	TOTAL OPERATIONS PROCESSED 3
--	END OF FILE

最後我們就可以使用這個腳本去恢複數據了。

注意

我們在使用日誌恢復的時候如果表有主鍵會根據主鍵生成sql,如上圖sql中 where後面的條件。如果表沒有主鍵那麼生成的sql後面的where條件會帶上所有的字段。在我幫小夥伴恢複數據的時候發現他的表沒有設置主鍵,而且字段有20多個,3萬多條數據生成的sql都是100多M,還要拆分執行。
比如我把Id主鍵去了再更新下Status狀態到4,生成的sql如下,會提示沒有主鍵。

以上就是一次數據恢復的分享,如果下次你也遇到這種情況希望能幫到你。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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