結合RBAC模型講解權限管理系統需求及表結構創建

在本號之前的文章中,已經為大家介紹了很多關於Spring Security的使用方法,也介紹了RBAC的基於角色權限控制模型。但是很多朋友雖然已經理解了RBAC控制模型,但是仍有很多的問題阻礙他們進一步開發。比如:

  • RBAC模型的表結構該如何創建?
  • 具體到某個頁面,某個按鈕權限是如何控制的?
  • 為了配合登錄驗證表,用戶表中應該包含哪些核心字段?
  • 這些字段與登錄驗證或權限分配的需求有什麼關係?

那麼本文就希望將這些問題,與大家進行一下分享。

一、回顧RBAC權限模型

  • 用戶與角色之間是多對多的關係,一個用戶有多個角色,一個角色包含多個用戶
  • 角色與權限之間是多對多關係,一個角色有多種權限,一個權限可以屬於多個角色

上圖中:

  • User是用戶表,存儲用戶基本信息
  • Role是角色表,存儲角色相關信息
  • Menu(菜單)是權限表,存儲系統包含哪些菜單及其屬性
  • UserRole是用戶和角色的關係表
  • RoleMenu是角色和權限的關係表

本文講解只將權限控制到菜單的訪問級別,即控制頁面的訪問權限。如果想控制到頁面中按鈕級別的訪問,可以參考Menu與RoleMenu的模式同樣的實現方式。或者乾脆在menu表裡面加上一個字段區別該條記錄是菜單項還是按鈕。

為了有理有據,我們參考一個比較優秀的開源項目:若依後台管理系統。

二、組織部門管理

2.1.需求分析

之所以先將部門管理提出來講一下,是因為部門管理沒有在我們上面的RBAC權限模型中進行提現。但是部門這樣一個實體仍然是,後端管理系統的一個重要組成部分。通常有如下的需求:

  • 部門要能體現出上下級的結構(如上圖中的紅框)。在關係型數據庫中。這就需要使用到部門id及上級部門id,來組合成一個樹形結構。這個知識是SQL學習中必備的知識,如果您還不知道,請自行學習。
  • 如果組織與用戶之間是一對多的關係,就在用戶表中加上一個org_id標識用戶所屬的組織。原則是:實體關係在多的那一邊維護。比如:是讓老師記住自己的學生容易,還是讓學生記住自己的老師更容易?
  • 如果組織與用戶是多對多關係,這種情況現實需求也有可能存在。比如:某人在某單位既是生產部長,又是技術部長。所以他及歸屬於技術部。也歸屬於生產部。對於這種情況有兩種解決方案,把該人員放到公司級別,而不是放到部門級別。另外一種就是從數據庫結構上創建User與Org組織之間的多對多關係。
  • 組織信息包含一些基本信息,如組織名稱、組織狀態、展現排序、創建時間
  • 另外,要有基本的組織的增刪改查功能

2.2 組織部門表的CreateSQL

以下SQL以MySQL為例:

CREATE TABLE `sys_org` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `org_pid` INT(11) NOT NULL COMMENT '上級組織編碼',
    `org_pids` VARCHAR(64) NOT NULL COMMENT '所有的父節點id',
    `is_leaf` TINYINT(4) NOT NULL COMMENT '0:不是恭弘=叶 恭弘子節點,1:是恭弘=叶 恭弘子節點',
    `org_name` VARCHAR(32) NOT NULL COMMENT '組織名',
    `address` VARCHAR(64) NULL DEFAULT NULL COMMENT '地址',
    `phone` VARCHAR(13) NULL DEFAULT NULL COMMENT '電話',
    `email` VARCHAR(32) NULL DEFAULT NULL COMMENT '郵件',
    `sort` TINYINT(4) NULL DEFAULT NULL COMMENT '排序',
    `level` TINYINT(4) NOT NULL COMMENT '組織層級',
    `status` TINYINT(4) NOT NULL COMMENT '0:啟用,1:禁用',
    PRIMARY KEY (`id`)
)
COMMENT='系統組織結構表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

注意:mysql沒有oracle中的start with connect by的樹形數據匯總SQL。所以通常需要為了方便管理組織之間的上下級樹形關係,需要加上一些特殊字段,如:org_pids:該組織所有上級組織id逗號分隔,即包括上級的上級;is_leaf是否是恭弘=叶 恭弘子結點;level組織所屬的層級(1,2,3)。

三、菜單權限管理

3.1 需求分析

  • 由上圖可以看出,菜單仍然是樹形結構,所以數據庫表必須有id與menu_pid字段
  • 必要字段:菜單跳轉的url、是否啟用、菜單排序、菜單的icon矢量圖標等
  • 最重要的是菜單要有一個權限標誌,具有唯一性。通常可以使用菜單跳轉的url路徑作為權限標誌。此標誌作為權限管理框架識別用戶是否具有某個頁面查看權限的重要標誌
  • 需要具備菜單的增刪改查基本功能
  • 如果希望將菜單權限和按鈕超鏈接相關權限放到同一個表裡面,可以新增一個字段。用戶標誌該權限記錄是菜單訪問權限還是按鈕訪問權限。

3.2 菜單權限表的CreateSQL

CREATE TABLE `sys_menu` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `menu_pid` INT(11) NOT NULL COMMENT '父菜單ID',
    `menu_pids` VARCHAR(64) NOT NULL COMMENT '當前菜單所有父菜單',
    `is_leaf` TINYINT(4) NOT NULL COMMENT '0:不是恭弘=叶 恭弘子節點,1:是恭弘=叶 恭弘子節點',
    `name` VARCHAR(16) NOT NULL COMMENT '菜單名稱',
    `url` VARCHAR(64) NOT NULL COMMENT '跳轉URL',
    `icon` VARCHAR(45) NULL DEFAULT NULL,
    `icon_color` VARCHAR(16) NULL DEFAULT NULL,
    `sort` TINYINT(4) NULL DEFAULT NULL COMMENT '排序',
    `level` TINYINT(4) NOT NULL COMMENT '菜單層級',
    `status` TINYINT(4) NOT NULL COMMENT '0:啟用,1:禁用',
    PRIMARY KEY (`id`)
)
COMMENT='系統菜單表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

四、角色管理

上圖為角色修改及分配權限的頁面

4.1.需求分析

  • 角色本身的管理需要注意的點非常少,就是簡單的增刪改查。重點在於角色分配該如何做。
  • 角色表包含角色id,角色名稱,備註、排序順序這些基本信息就足夠了
  • 為角色分配權限:以角色為基礎勾選菜單權限或者操作權限,然後先刪除sys_role_menu表內該角色的所有記錄,在將新勾選的權限數據逐條插入sys_role_menu表。
  • sys_role_menu的結構很簡單,記錄role_id與menu_id,一個角色擁有某一個權限就是一條記錄。
  • 角色要有一個全局唯一的標識,因為角色本身也是一種權限。可以通過判斷角色來判斷某用戶的操作是否合法。
  • 通常的需求:不會在角色管理界面為角色添加用戶,而是在用戶管理界面為用戶分配角色。

4.2.角色表與角色菜單權限關聯表的的CreateSQL

CREATE TABLE `sys_role` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `role_id` VARCHAR(16) NOT NULL COMMENT '角色ID',
    `role_name` VARCHAR(16) NOT NULL COMMENT '角色名',
    `role_flag` VARCHAR(64) NULL DEFAULT NULL COMMENT '角色標識',
    `sort` INT(11) NULL DEFAULT NULL COMMENT '排序',
    PRIMARY KEY (`id`)
)
COMMENT='系統角色表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;
CREATE TABLE `sys_role_menu` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `role_id` VARCHAR(16) NOT NULL COMMENT '角色ID',
    `menu_id` INT(11) NOT NULL COMMENT '菜單ID',
    PRIMARY KEY (`id`)
)
COMMENT='角色菜單多對多關聯表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

五、用戶管理

5.1.需求分析

  • 上圖中點擊左側的組織菜單樹結點,要能显示出該組織下的所有人員(系統用戶)。在組織與用戶是一對多的關係中,需要在用戶表加上org_id字段,用於查詢某個組織下的所有用戶。
  • 用戶表中要保存用戶的用戶名、加密后的密碼。頁面提供密碼修改或重置的功能。
  • 角色分配:實際上為用戶分配角色,與為角色分配權限的設計原則是一樣的。所以可以參考。
  • 實現用戶基本信息的增刪改查功能

5.2.sys_user 用戶信息表及用戶角色關係表的CreateSQL

CREATE TABLE `sys_user` (
        `id` INT(11) NOT NULL AUTO_INCREMENT,
        `org_id` INT(11) NOT NULL,
        `username` VARCHAR(64) NULL DEFAULT NULL COMMENT '用戶名',
        `password` VARCHAR(64) NULL DEFAULT NULL COMMENT '密碼',
        `enabled` INT(11) NULL DEFAULT '1' COMMENT '用戶賬戶是否可用',
        `locked` INT(11) NULL DEFAULT '0' COMMENT '用戶賬戶是否被鎖定',
        `lockrelease_time` TIMESTAMP NULL  '用戶賬戶鎖定到期時間',
        `expired_time` TIMESTAMP NULL  '用戶賬戶過期時間',
        `create_time` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '用戶賬戶創建時間',
    PRIMARY KEY (`id`)
)
COMMENT='用戶信息表'
ENGINE=InnoDB
;
CREATE TABLE `sys_user_role` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `role_id` VARCHAR(16) NULL DEFAULT NULL,
    `user_id` VARCHAR(18) NULL DEFAULT NULL,
    PRIMARY KEY (`id`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

在用戶的信息表中,體現了一些隱藏的需求。如:多次登錄鎖定與鎖定到期時間的關係。賬號有效期的設定規則等。

當然用戶表中,根據業務的不同還可能加更多的信息,比如:用戶頭像等等。但是通常在比較大型的業務系統開發中,業務模塊中使用的用戶表和在權限管理模塊使用的用戶表通常不是一個,而是根據某些唯一字段弱關聯,分開存放。這樣做的好處在於:經常發生變化的業務需求,不會去影響不經常變化的權限模型。

期待您的關注

  • 向您推薦博主的系列文檔:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

SpringBoot源碼學習系列之SpringMVC自動配置

目錄

源碼學習系列之WebMvc自動配置原理筆記

@

web的自動配置在SpringBoot項目中是一個很重要的方面,實現代碼在spring-boot-autoconfigure工程里:

按照官方文檔的說法,SpringBoot官方的說法,Springboot的SpringMVC自動配置,主要提供了如下自動配置:

WebMvcAutoConfiguration.java這個類很關鍵,這個就是SpringBoot Springmvc自動配置的一個很關鍵的配置類

@Configuration(proxyBeanMethods = false)//指定WebMvcAutoConfiguration不代理方法
@ConditionalOnWebApplication(type = Type.SERVLET)//在web環境(selvlet)才會起效
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })//系統有有Servlet,DispatcherServlet(Spring核心的分發器),WebMvcConfigurer的情況,這個自動配置類才起效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)//系統沒有WebMvcConfigurationSupport這個類的情況,自動配置起效
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
        ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
....
}

翻下源碼,可以看到WebMvcAutoConfiguration自動配置類里還有一個WebMvcConfigurer類型的配置類,2.2.1版本是implements WebMvcConfigurer接口,1.+版本是extends WebMvcConfigurerAdapter

@Configuration(proxyBeanMethods = false)//定義為配置類
    @Import(EnableWebMvcConfiguration.class)//spring底層註解,將EnableWebMvcConfiguration加到容器
    @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })//使WebMvcProperties、ResourceProperties配置類生效
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
    ....
}

1、ContentNegotiatingViewResolver

如圖,是視圖解析器的自動配置,這個類起效的情況是系統沒有ContentNegotiatingViewResolver類的情況,就調用改方法自動創建ContentNegotiatingViewResolver類

關鍵的是ContentNegotiatingViewResolver類,翻下ContentNegotiatingViewResolver類,找到如下重要的初始化方法

@Override
    protected void initServletContext(ServletContext servletContext) {
    //調用Spring的BeanFactoryUtils掃描容器里的所有視圖解析器ViewResolver類
        Collection<ViewResolver> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
        if (this.viewResolvers == null) {
            this.viewResolvers = new ArrayList<>(matchingBeans.size());
            //遍歷候選的viewResolvers,封裝到this.viewResolvers列表
            for (ViewResolver viewResolver : matchingBeans) {
                if (this != viewResolver) {
                    this.viewResolvers.add(viewResolver);
                }
            }
        }
        else {
            for (int i = 0; i < this.viewResolvers.size(); i++) {
                ViewResolver vr = this.viewResolvers.get(i);
                if (matchingBeans.contains(vr)) {
                    continue;
                }
                String name = vr.getClass().getName() + i;
                obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
            }

        }
        AnnotationAwareOrderComparator.sort(this.viewResolvers);
        this.cnmFactoryBean.setServletContext(servletContext);
    }

所以ContentNegotiatingViewResolver類的作用就是組合所有的視圖解析器,自動配置了ViewResolver(視圖解析器作用,根據方法返回值得到視圖對象view)

往下翻代碼,可以看到resolveViewName方法,裏面代碼是從this.viewResolvers獲取候選的視圖解析器,遍歷容器里所有視圖,然後通過如圖所標記的獲取候選視圖的方法,獲取候選的視圖列表,再通過getBestView獲取最合適的視圖

遍歷所有的視圖解析器對象,從視圖解析器里獲取候選的視圖,封裝成list保存

ok,跟了源碼就是只要將視圖解析器丟到Spring容器里,就可以加載到

寫個簡單的視圖解析類

DispatcherServlet是Spring核心分發器,找到doDispatch方法,debug,可以看到加的視圖解析器加載到了

2、靜態資源

也就是官方說的,如下圖所示:

翻譯過來就是支持靜態資源包括webjars的自動配置,webjars,就是以maven等等方式打成jar包的靜態資源,可以去看看文檔:

使用的話,直接去webjars官網負責對應的配置,加到項目里就可以

路徑都是在META-INF/webjars/**

WebMvcAutoConfiguration.addResourceHandlers,這個是比較重要的資源配置方法

@Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            if (!this.resourceProperties.isAddMappings()) {
                logger.debug("Default resource handling disabled");
                return;
            }
            Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
            //CacheControl是Spring框架提供的http緩存
            CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
            //讀取到webjars資源,將classpath:/META-INF/resources/webjars/的webjars資源都掃描出來
            if (!registry.hasMappingForPattern("/webjars/**")) {
                customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                        .addResourceLocations("classpath:/META-INF/resources/webjars/")
                        .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
            }
            String staticPathPattern = this.mvcProperties.getStaticPathPattern();
            if (!registry.hasMappingForPattern(staticPathPattern)) {
                customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                        .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                        .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
            }
        }

ok,通過源碼可以知道,Springboot支持webjars和其它等等靜態資源,其它的靜態資源要放在如下目錄里,Springboot就能自動加載到

  • classpath:/META-INF/resources/
  • classpath:/resources/
  • classpath:/static/
  • classpath:/public/
  • classpath:/

3、自動註冊 Converter, GenericConverter, and Formatter beans.

翻譯過來就是自動註冊了 Converter, GenericConverter, and Formatter beans.

  • Converter:轉換器 ,作用就是能自動進行類型轉換
    eg: public String hello(User user),這是一個方法,然後前端視圖傳來的參數通過轉換器能夠根據屬性進行映射,然後進行屬性類型轉換
  • Formatter :格式化器,eg:比如對前端傳來的日期2019/11/25,進行格式化處理

源碼在這裏,WebMvcAutoConfiguration.addFormatters方法是添加格式化器的方法

同理,也是從Spring容器里將這幾種類拿過來

當然,還有其它的,比如WebMvcAutoConfiguration.localeResolver方法是實現i18n國際化語言支持的自動配置

@Bean
        @ConditionalOnMissingBean//沒有自定義localeResolver的情況
        @ConditionalOnProperty(prefix = "spring.mvc", name = "locale")//application.properties有配置了spring.mvc.locale
        public LocaleResolver localeResolver() {
            if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
                return new FixedLocaleResolver(this.mvcProperties.getLocale());
            }
            //默認使用AcceptHeaderLocaleResolver 
            AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
            localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
            return localeResolver;
        }

具體的源碼參考我之前博客:,博客裏面有涉及源碼的

4、支持HttpMessageConverters

HttpMessageConverters :消息轉換器,Springmvc中用來轉換http請求和響應的

源碼里是通過configureMessageConverters方法實現,很顯然也是從容器里獲取的

官方文檔里也進行了比較詳細描述,Springboot已經為我們自動配置了json的、xml的自動轉換器,當然你也可以自己添加

5、支持MessageCodesResolver

MessageCodesResolver:是消息解析器,WebMvcAutoConfiguration.getMessageCodesResolver是實現Exception異常信息格式的

WebMvcProperties配置文件定義的一個異常枚舉值

格式為如圖所示,定了了錯誤代碼是生成規則:

6、首頁支持

Springboot默認的首頁是index.html,也就是你在classpath路徑丟個index.html文件,就被Springboot默認為首頁,或者說歡迎頁

如圖示代碼,就是遍歷靜態資源文件,然後獲取index.html作為歡迎頁面

7、網站logo設置

Springboot1.+版本,是有默認的logo圖標的,2.2.1版本,經過全局搜索,沒有發現給自定義的圖標,使用的話,是直接丟在classpath路徑,文件命名為favicon.ico,不過在2.2.1代碼並沒有找到相應的配置代碼,1.+版本是有的,不過文檔還是有描述了

8、ConfigurableWebBindingInitializer 初始綁定器

跟下源碼,也是從Spring容器里獲取的,然後注意到,如果沒有這個ConfigurableWebBindingInitializer ,代碼就會調用基類的getConfigurableWebBindingInitializer

源碼,這裏也是創建一個getConfigurableWebBindingInitializer

ConfigurableWebBindingInitializer 是Springboot為系統自動配置的,當然我們也可以自己定義一個ConfigurableWebBindingInitializer ,然後加載到容器里即可

初始化綁定的方法,ok,本博客簡單跟一下源碼

注意:
ok,Springboot官方文檔里還有這樣的描述,如圖所示

意思是,在使用webmvcConfigurer配置的時候,不要使用@EnableWebMvc註解,為什麼不要使用呢?因為使用了@EnableWebMvc,就是實現全面接管SpringMVC自動配置,也就是說其它的自動配置都會失效,全部自己配置

原理是為什麼?可以簡單跟一下源碼,如圖,SpringMVC自動配置類,有這個很關鍵的註解,這個註解的意思是@WebMvcConfigurationSupport註解不在系統時候自動配置才起效

然後為什麼加了@EnableWebMvc自動配置就可以被全面接管?點一下@EnableWebMvc源碼

很顯然,DelegatingWebMvcConfiguration類extends WebMvcConfigurationSupport類,所以這也就是為什麼@EnableWebMvc註解能實現全面接管自動配置的原理

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

ceph中rbd的增量備份和恢復

ceph中rbd的增量備份和恢復

ceph的文檔地址:

​ 在調研OpenStack中虛機的備份和恢復時,發現OpenStack和ceph緊密結合,使用ceph做OpenStack的後端簡直是不要太爽,於是調研了使用ceph中的塊設備rbd來對虛機進行增量備份和恢復。以下是虛機備份和恢復的實驗步驟:

1. 前言:

快照的功能一般是基於時間點做一個標記,然後在某些需要的時候,將狀態恢復到標記的那個點,這個有一個前提是底層的數據沒有破壞,舉個簡單的例子,Vmware 裏面對虛擬機做了一個快照,然後做了一些系統的操作,想恢復快照,前提是存儲快照的存儲系統沒用破壞,一旦破壞了是無法恢復的。

​ ceph也有快照功能,同樣,在這裏的快照是用來保存存儲系統上的狀態的,數據的快照能成功恢復的前提是存儲系統是好的,而一旦存儲系統壞了,快照同時會失效的,所以最好是能夠將數據備份下來。本篇博客主要是調研使用ceph的rbd命令來對存儲設備進行基於快照的增量備份。

2. ceph中rbd的常用命令:

2.1列出存儲池

ceph osd pool ls

2.2 查看存儲池的內容

rbd ls --pool pool_name
例子
rbd ls --pool volumes

2.3 打快照

rbd snap create {pool-name}/{image-name}@{snap-name}
例如
rbd snap create volumes/volume-c18b9782-dc71-4ddc-bb7f-bc0037105ac3@v1

2.4 羅列快照

rbd snap ls {pool-name}/{image-name}
例如:
rbd snap ls volumes/volume-c18b9782-dc71-4ddc-bb7f-bc0037105ac3

2.5 創建image

rbd create --size {pool-name}/{image-name}

3. Nova實例的備份與恢復

以ceph做後端,在創建實例時,需要選擇一個系統盤,系統盤即是我們的目標數據盤。

備份實驗步驟:

  1. 創建虛機。
  2. 在時間點v1對虛機打快照。
  3. 導出從開始創建image到快照v1那個時間點的差異數據,可以視為全量備份。
  4. 使用dd命令寫入文件test.txt
  5. 在時間點v2對虛機打快照。
  6. 導出從開始創建image到快照v2那個時間點的差異數據,可以視為全量備份。
  7. 導出了從v1快照時間點到v2快照時間點的差異數據,可以視為增量備份。

上文實驗過程的數據:

v1時間點數據 + v1_v2之間數據 = v2 時間點數據

虛機的備份

1. 實例第一次快照:

rbd snap create volumes/volume-c18b9782-dc71-4ddc-bb7f-bc0037105ac3@v1

2. 第一次全量備份:

rbd export-diff volumes/volume-c18b9782-dc71-4ddc-bb7f-bc0037105ac3@v1 testimage_v1

這個命令是導出了從開始創建image到快照v1那個時間點的差異數據導出來了testimage_v1,導出成本地文件testimage_v1

3. 寫入文件

dd

寫入文件,以此显示出v1和v2之間的數據變化,並沒有其他作用。

4. 實例第二次快照

rbd snap create volumes/volume-c18b9782-dc71-4ddc-bb7f-bc0037105ac3@v2

5. 第二次全量備份:

rbd export-diff volumes/volume-c18b9782-dc71-4ddc-bb7f-bc0037105ac3@v2  testimage_v2

這個命令是導出了從開始創建image到快照v2那個時間點的差異數據導出來了testimage_v2,導出成本地文件testimage_v2

6. 增量備份

增量備份(第二次和第一次的差異文件):

rbd export-diff volumes/volume-c18b9782-dc71-4ddc-bb7f-bc0037105ac3@v2 --from-snap v1 testimage_v1_v2

這個命令是導出了從v1快照時間點到v2快照時間點的差異數據,導出成本地文件testimage_v1_v2

注意:

rbd export-diff rbd/testimage testimage_now

這個是導出了從image創建到當前的時間點的差異數據。

虛機恢復

虛機的恢復過程使用的是剛剛上面提到的備份到本地的那些文件。

1.創建塊設備映像

2.將testimage_v1融入塊設備,恢復v1時間的狀態

3.將testimage_v2融入塊設備,恢復v2時間狀態

4.在2基礎上將v1_v2融入塊設備,恢復至v2時間狀態

上述實驗是全量恢復和增量恢復的兩種狀態。下文將詳細總結項目中增量備份和恢復的使用過程。

1. 創建塊設備映像image

首先隨便創建一個image,名稱大小都不限制,因為後面恢復的時候會覆蓋掉大小的信息

rbd create --size 2048 backups/testbacknew

2. 基於v2的時間點的快照做恢復

2.1 基於V2恢復

直接基於v2的時間點的快照做恢復

rbd import-diff testimage_v2 rbd/testbacknew
2.2 基於v1+ v1_v2數據恢復

直接基於v1的時間點的數據,和後面的增量的v1_v2數據(要按順序導入)

rbd import-diff testimage_v1 backups/testbacknew
rbd import-diff testimage_v1_v2 backups/testbacknew

​ 實際項目當中就是,定期做快照,然後導出某個時間點快照的數據,然後導出增量的快照的數據,就可以了

4. 實際使用

​ 在實際項目中使用就是,定期做快照,然後導出某個時間點快照的數據,然後導出增量的快照的數據。

例如:

備份:

​ 對所有的rbd的image做一個基礎快照,然後導出這個快照的數據,然後設置每天定時做快照,導出快照時間點之間的數據,這樣每天導出來的就是一個增量的數據了。

​ 設置循環周期,比如三天為一個周期。每三天循環一次,自動刪除三天前的備份。

恢復:

​ 從第一個快照導入,然後按照順序導入增量的快照即可。

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

go中的關鍵字-go(上)

1. goroutine的使用

  在Go語言中,表達式go f(x, y, z)會啟動一個新的goroutine運行函數f(x, y, z),創建一個併發任務單元。即go關鍵字可以用來開啟一個goroutine(協程))進行任務處理。

  創建單個goroutine

 1 package main
 2 
 3 import (
 4     "fmt"
 5 )
 6 
 7 func HelloWorld() {
 8     fmt.Println("Hello goroutine")
 9 }
10 
11 func main() {
12     go HelloWorld()      // 開啟一個新的併發運行
time.Sleep(1*time.Second)
13 fmt.Println("后輸出消息!") 14 }

  輸出

1 Hello goroutine
2 后輸出消息!

  這裏的sleep是必須的,否則你可能看不到goroutine裡頭的輸出,或者裏面的消息后輸出。因為當main函數返回時,所有的gourutine都是暴力終結的,然後程序退出。

  創建多個goroutine時

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func DelayPrint() {
 9     for i := 1; i <= 3; i++ {
10         time.Sleep(500 * time.Millisecond)
11         fmt.Println(i)
12     }
13 }
14 
15 func HelloWorld() {
16     fmt.Println("Hello goroutine")
17 }
18 
19 func main() {
20     go DelayPrint()     // 第一個goroutine
21     go HelloWorld()     // 第二個goroutine
22     time.Sleep(10*time.Second)
23     fmt.Println("main func")
24 }

  輸出

1 Hello  goroutine
2 1
3 2
4 3
5 4
6 
7 main func

  當去掉 DelayPrint() 函數里的sleep之後,輸出為:

1 1
2 2
3 3
4 4
5 Hello goroutine
6 main function

  說明第二個goroutine不會因為第一個而堵塞或者等待。事實是當程序執行go FUNC()的時候,只是簡單的調用然後就立即返回了,並不關心函數裡頭發生的故事情節,所以不同的goroutine直接不影響,main會繼續按順序執行語句。

goroutine阻塞

  場景一:

1 package main
2 
3 func main() {
4     ch := make(chan int)
5     <- ch // 阻塞main goroutine, 通道被鎖
6 }

  運行程序會報錯:

1 fatal error: all goroutines are asleep - deadlock!
2 
3 goroutine 1 [chan receive]:
4 main.main()

  場景二

 1 package main
 2 
 3 func main() {
 4     ch1, ch2 := make(chan int), make(chan int)
 5 
 6     go func() {
 7         ch1 <- 1 // ch1通道的數據沒有被其他goroutine讀取走,堵塞當前goroutine
 8         ch2 <- 0
 9     }()
10 
11     <- ch2 // ch2 等待數據的寫
12 }

  非緩衝通道上如果只有數據流入,而沒有流出,或者只流出無流入,都會引起阻塞。 goroutine的非緩衝通道裡頭一定要一進一出,成對出現。 上面例子,一:流出無流入;二:流入無流出。

  處理方式:

  1. 讀取通道數據

 1 package main
 2 
 3 func main() {
 4     ch1, ch2 := make(chan int), make(chan int)
 5 
 6     go func() {
 7         ch1 <- 1 // ch1通道的數據沒有被其他goroutine讀取走,堵塞當前goroutine
 8         ch2 <- 0
 9     }()
10 
11     <- ch1 // 取走便是
12     <- ch2 // chb 等待數據的寫
13 }

  2. 創建緩衝通道

 1 package main
 2 
 3 func main() {
 4     ch1, ch2 := make(chan int, 3), make(chan int)
 5 
 6     go func() {
 7         ch1 <- 1 // cha通道的數據沒有被其他goroutine讀取走,堵塞當前goroutine
 8         ch2 <- 0
 9     }()
10 
11     <- ch2 // ch2 等待數據的寫
12 }

2. goroutine調度器相關結構

  goroutine的調度涉及到幾個重要的數據結構,我們先逐一介紹和分析這幾個數據結構。這些數據結構分別是結構體G,結構體M,結構體P,以及Sched結構體。前三個的定義在文件runtime/runtime.h中,而Sched的定義在runtime/proc.c中。Go語言的調度相關實現也是在文件proc.c中。

2.1 結構體G

  g是goroutine的縮寫,是goroutine的控制結構,是對goroutine的抽象。看下它內部主要的一些結構:

 1 type g struct {
 2    //堆棧參數。
 3      //堆棧描述了實際的堆棧內存:[stack.lo,stack.hi)。
 4      // stackguard0是在Go堆棧增長序言中比較的堆棧指針。
 5      //通常是stack.lo + StackGuard,但是可以通過StackPreempt觸發搶佔。
 6      // stackguard1是在C堆棧增長序言中比較的堆棧指針。
 7      //它是g0和gsignal堆棧上的stack.lo + StackGuard。
 8      //在其他goroutine堆棧上為〜0,以觸發對morestackc的調用(並崩潰)。
9 //當前g使用的棧空間,stack結構包括 [lo, hi]兩個成員 10 stack stack // offset known to runtime/cgo
11 // 用於檢測是否需要進行棧擴張,go代碼使用 12 stackguard0 uintptr // offset known to liblink
13 // 用於檢測是否需要進行棧擴展,原生代碼使用的 14 stackguard1 uintptr // offset known to liblink
15 // 當前g所綁定的m 16 m *m // current m; offset known to arm liblink
17 // 當前g的調度數據,當goroutine切換時,保存當前g的上下文,用於恢復 18 sched gobuf
19 // goroutine運行的函數 20 fnstart *FuncVal 21 // g當前的狀態 22 atomicstatus uint32 23 // 當前g的id 24 goid int64
25 // 狀態Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead 26 status int16
27 // 下一個g的地址,通過guintptr結構體的ptr set函數可以設置和獲取下一個g,通過這個字段和sched.gfreeStack sched.gfreeNoStack 可以把 free g串成一個鏈表 28 schedlink guintptr
29 // 判斷g是否允許被搶佔 30 preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
31 // g是否要求要回到這個M執行, 有的時候g中斷了恢復會要求使用原來的M執行 32 lockedm muintptr
33 // 用於傳遞參數,睡眠時其它goroutine設置param,喚醒時此goroutine可以獲取
param *void
34 // 創建這個goroutine的go表達式的pc 35 uintptr gopc 36 }

  其中包含了棧信息stackbase和stackguard,有運行的函數信息fnstart。這些就足夠成為一個可執行的單元了,只要得到CPU就可以運行。goroutine切換時,上下文信息保存在結構體的sched域中。goroutine切換時,上下文信息保存在結構體的sched域中。goroutine是輕量級的線程或者稱為協程,切換時並不必陷入到操作系統內核中,很輕量級。

  結構體G中的Gobuf,其實只保存了當前棧指針,程序計數器,以及goroutine自身。

1 struct Gobuf
2 {
3     //這些字段的偏移是libmach已知的(硬編碼的)。
4     sp   uintper;
5     pc   *byte;
6     g    *G;
7     ...
8 };

  記錄g是為了恢復當前goroutine的結構體G指針,運行時庫中使用了一個常駐的寄存器extern register G* g,這是當前goroutine的結構體G的指針。這種結構是為了快速地訪問goroutine中的信息,比如,Go的棧的實現並沒有使用%ebp寄存器,不過這可以通過g->stackbase快速得到。”extern register”是由6c,8c等實現的一個特殊的存儲,在ARM上它是實際的寄存器。在linux系統中,對g和m使用的分別是0(GS)和4(GS)。鏈接器還會根據特定操作系統改變編譯器的輸出,每個鏈接到Go程序的C文件都必須包含runtime.h頭文件,這樣C編譯器知道避免使用專用的寄存器。

2.2 結構體P

  P是Processor的縮寫。結構體P的加入是為了提高Go程序的併發度,實現更好的調度。M代表OS線程。P代表Go代碼執行時需要的資源。

 1 type p struct {
 2    lock mutex
 3 
 4    id          int32
 5    // p的狀態,稍後介紹
 6    status      uint32 // one of pidle/prunning/...
 7 
 8    // 下一個p的地址,可參考 g.schedlink
 9    link        puintptr
10    // p所關聯的m
11    m           muintptr   // back-link to associated m (nil if idle)
12 
13    // 內存分配的時候用的,p所屬的m的mcache用的也是這個
14    mcache      *mcache
15   
16    // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.
17    // 從sched中獲取並緩存的id,避免每次分配goid都從sched分配
18      goidcache    uint64
19      goidcacheend uint64
20 
21    // Queue of runnable goroutines. Accessed without lock.
22    // p 本地的runnbale的goroutine形成的隊列
23    runqhead uint32
24    runqtail uint32
25    runq     [256]guintptr
26 
27    // runnext,如果不是nil,則是已準備好運行的G
28    //當前的G,並且應該在下一個而不是其中運行
29    // runq,如果運行G的時間還剩時間
30    //切片。它將繼承當前時間剩餘的時間
31    //切片。如果一組goroutine鎖定在
32    //交流等待模式,該計劃將其設置為
33    //單位並消除(可能很大)調度
34    //否則會由於添加就緒商品而引起的延遲
35    // goroutines到運行隊列的末尾。
36 
37    // 下一個執行的g,如果是nil,則從隊列中獲取下一個執行的g
38    runnext guintptr
39 
40    // Available G's (status == Gdead)
41    // 狀態為 Gdead的g的列表,可以進行復用
42    gfree    *g
43    gfreecnt int32
44 }

  跟G不同的是,P不存在waiting狀態。MCache被移到了P中,但是在結構體M中也還保留着。在P中有一個Grunnable的goroutine隊列,這是一個P的局部隊列。當P執行Go代碼時,它會優先從自己的這個局部隊列中取,這時可以不用加鎖,提高了併發度。如果發現這個隊列空了,則去其它P的隊列中拿一半過來,這樣實現工作流竊取的調度。這種情況下是需要給調用器加鎖的。

2.3 結構體M

  M是machine的縮寫,是對機器的抽象,每個m都是對應到一條操作系統的物理線程。

 1 type m struct {
 2      // g0是用於調度和執行系統調用的特殊g
 3    g0      *g             // goroutine with scheduling stack
 4      // m當前運行的g
 5    curg    *g             // current running goroutine
 6    // 當前擁有的p
 7    p        puintptr      // attached p for executing go code (nil if not executing go code)
8 // 線程的 local storage 9 tls [6]uintptr // thread-local storage 10 // 喚醒m時,m會擁有這個p 11 nextp puintptr 12 id int64 13 // 如果 !="", 繼續運行curg 14 preemptoff string // if != "", keep curg running on this m
15 // 自旋狀態,用於判斷m是否工作已結束,並尋找g進行工作 16 spinning bool // m is out of work and is actively looking for work
17 // 用於判斷m是否進行休眠狀態 18 blocked bool // m is blocked on a note 19 // m休眠和喚醒通過這個,note裏面有一個成員key,對這個key所指向的地址進行值的修改,進而達到喚醒和休眠的目的 20 park note
21 // 所有m組成的一個鏈表 22 alllink *m // on allm 23 // 下一個m,通過這個字段和sched.midle 可以串成一個m的空閑鏈表 24 schedlink muintptr 25 // mcache,m擁有p的時候,會把自己的mcache給p 26 mcache *mcache 27 // lockedm的對應值 28 lockedg guintptr 29 // 待釋放的m的list,通過sched.freem 串成一個鏈表 30 freelink *m // on sched.freem 31 }

  和G類似,M中也有alllink域將所有的M放在allm鏈表中。lockedg是某些情況下,G鎖定在這個M中運行而不會切換到其它M中去。M中還有一個MCache,是當前M的內存的緩存。M也和G一樣有一個常駐寄存器變量,代表當前的M。同時存在多個M,表示同時存在多個物理線程。

2.4 Sched結構體

  Sched是調度實現中使用的數據結構,該結構體的定義在文件proc.c中。

 1 type schedt struct {
 2    // 全局的go id分配
 3    goidgen  uint64
 4    // 記錄的最後一次從i/o中查詢g的時間
 5    lastpoll uint64
 6 
 7    lock mutex
 8 
 9    //當增加nmidle,nmidlelocked,nmsys或nmfreed時,應
10    //確保調用checkdead()。
11 
12      // m的空閑鏈表,結合m.schedlink 就可以組成一個空閑鏈表了
13    midle        muintptr // idle m's waiting for work
14    nmidle       int32    // number of idle m's waiting for work
15    nmidlelocked int32    // number of locked m's waiting for work
16    // 下一個m的id,也用來記錄創建的m數量
17    mnext        int64    // number of m's that have been created and next M ID
18    // 最多允許的m的數量
19    maxmcount    int32    // maximum number of m's allowed (or die)
20    nmsys        int32    // number of system m's not counted for deadlock
21    // free掉的m的數量,exit的m的數量
22    nmfreed      int64    // cumulative number of freed m's
23 
24    ngsys uint32 // 系統goroutine的數量;原子更新
25 
26    pidle      puintptr // 閑置的
27    npidle     uint32
28    nmspinning uint32 // See "Worker thread parking/unparking" comment in proc.go.
29 
30    // Global runnable queue.
31    // 這個就是全局的g的隊列了,如果p的本地隊列沒有g或者太多,會跟全局隊列進行平衡
32    // 根據runqhead可以獲取隊列頭的g,然後根據g.schedlink 獲取下一個,從而形成了一個鏈表
33    runqhead guintptr
34    runqtail guintptr
35    runqsize int32
36 
37    // freem是m等待被釋放時的列表
38    //設置了m.exited。通過m.freelink鏈接。
39 
40    // 等待釋放的m的列表
41    freem *m
42 }

  大多數需要的信息都已放在了結構體M、G和P中,Sched結構體只是一個殼。可以看到,其中有M的idle隊列,P的idle隊列,以及一個全局的就緒的G隊列。Sched結構體中的Lock是非常必須的,如果M或P等做一些非局部的操作,它們一般需要先鎖住調度器。

3. G、P、M相關狀態

g.status

  • _Gidle: goroutine剛剛創建還沒有初始化
  • _Grunnable: goroutine處於運行隊列中,但是還沒有運行,沒有自己的棧
  • _Grunning: 這個狀態的g可能處於運行用戶代碼的過程中,擁有自己的m和p
  • _Gsyscall: 運行systemcall中
  • _Gwaiting: 這個狀態的goroutine正在阻塞中,類似於等待channel
  • _Gdead: 這個狀態的g沒有被使用,有可能是剛剛退出,也有可能是正在初始化中
  • _Gcopystack: 表示g當前的棧正在被移除,新棧分配中

goroutine的狀態變化

  在newproc1中新建的goroutine被設置為Grunnable狀態,投入運行時設置成Grunning。Grunning狀態的goroutine會在entersyscall的時候goroutine的狀態被設置為Gsyscall,到出系統調用時根據它是從阻塞系統調用中出來還是非阻塞系統調用中出來,又會被設置成Grunning或者Grunnable的狀態。在goroutine最終退出的runtime.exit函數中,goroutine被設置為Gdead狀態。還會在進行I/O時可能會進入waiting狀態,主動讓出CPU,此時會被移到所屬P中的其他G後面,等待下一次輪到執行。

p.status

  • _Pidle: 空閑狀態,此時p不綁定m
  • _Prunning: m獲取到p的時候,p的狀態就是這個狀態了,然後m可以使用這個p的資源運行g
  • _Psyscall: 當go調用原生代碼,原生代碼又反過來調用go的時候,使用的p就會變成此態
  • _Pdead: 當運行中,需要減少p的數量時,被減掉的p的狀態就是這個了

m.status

m的status沒有p、g的那麼明確,但是在運行流程的分析中,主要有以下幾個狀態

  • 運行中: 拿到p,執行g的過程中
  • 運行原生代碼: 正在執行原聲代碼或者阻塞的syscall
  • 休眠中: m發現無待運行的g時,進入休眠,並加入到空閑列表中
  • 自旋中(spining): 當前工作結束,正在尋找下一個待運行的g

 

4. G、P、M的調度關係

  一個G就是一個gorountine,保存了協程的棧、程序計數器以及它所在M的信息。P全稱是Processor,處理器,它的主要用途就是用來執行goroutine的。M代表內核級線程,一個M就是一個線程,goroutine就是跑在M之上的。程序啟動時,會創建一個主G,而每使用一次go關鍵字也創建一個G。go func()創建一個新的G后,放到P的本地隊列里,或者平衡到全局隊列,然後檢查是否有可用的M,然後喚醒或新建一個M,M獲取待執行的G和空閑的P,將調用參數保存到g的棧,將sp,pc等上下文環境保存在g的sched域,這樣整個goroutine就準備好了,只要等分配到CPU,它就可以繼續運行,之後再清理現場,重新進入調度循環。

4.1 調度實現

  圖中有兩個物理線程,M0、M1每一個M都擁有一個處理器P,每一個P都有一個正在運行的G。P的數量可以通過GOMAXPROCS()來設置,它其實也代表了真正的併發度,即有多少個goroutine可以同時運行。圖中灰色goroutine都是處於ready的就緒態,正在等待被調度。由P維護這個就緒隊列(runqueue),go function每啟動一個goroutine,runqueue隊列就在其末尾加入一個goroutine,在下一個調度點,就從runqueue中取出一個goroutine執行。

  當一個OS線程M0陷入阻塞時,P轉而在M1上運行G,圖中的M1可能是正被創建,或者從線程緩存中取出。當MO返回時,它嘗試取得一個P來運行goroutine,一般情況下,它會從其他的OS線程那裡拿一個P過來執行,像M1獲取P一樣;如果沒有拿到的話,它就把goroutine放在一個global runqueue(全局運行隊列)里,然後自己睡眠(放入線程緩存里)。所有的P會周期性的檢查全局隊列並運行其中的goroutine,否則其上的goroutine永遠無法執行。

  另一種情況是P上的任務G很快就執行完了(分配不均),這個處理器P很忙,但是其他的P還有任務,此時如果global runqueue也沒有G了,那麼P就會從其他的P里拿一些G來執行。一般來說,如果一般就拿run queue的一半,這就確保了每個OS線程都能充分的使用。

  golang採用了m:n線程模型,即m個gorountine(簡稱為G)映射到n個用戶態進程(簡稱為P)上,多個G對應一個P,一個P對應一個內核線程(簡稱為M)。

4.2 P、M的數量

  P的數量:由啟動時環境變量$GOMAXPROCS或者是由runtime的方法GOMAXPROCS()決定(默認是1)。這意味着在程序執行的任意時刻都只有$GOMAXPROCS個goroutine在同時運行。在確定了P的最大數量n后,運行時系統會根據這個數量創建n個P。

   M的數量:go語言本身的限制:go程序啟動時,會設置M的最大數量,默認10000.但是內核很難支持這麼多的線程數,所以這個限制可以忽略。runtime/debug中的SetMaxThreads函數,設置M的最大數量。一個M阻塞了,會創建新的M。

  M與P的數量沒有絕對關係,一個M阻塞,P就會去創建或者切換另一個M,所以,即使P的默認數量是1,也有可能會創建很多個M出來。

4.2 P、G的調度細節

  P上G的調度:如果一個G不主動讓出cpu或被動block,所屬P中的其他G會一直等待順序執行。

  一個G執行IO時可能會進入waiting狀態,主動讓出CPU,此時會被移到所屬P中的其他G後面,等待下一次輪到執行。   一個G調用了runtime.Gosched()會進入runnable狀態,主動讓出CPU,並被放到全局等待隊列中。   一個G調用了runtime.Goexit(),該G將會被立即終止,然後把已加載的defer(有點類似析構)依次執行完。   一個G調用了允許block的syscall,此時G及其對應的P、其他G和M都會被block起來,監控線程M會定時掃描所有P,一旦發現某個P處於block syscall狀態,則通知調度器讓另一個M來帶走P(這裏的另一個M可能是新創建的,因此隨着G被不斷block,M數量會不斷增加,最終M數量可能會超過P數量),這樣P及其餘下的G就不會被block了,等被block的M返回時發現自己的P沒有了,也就不能再處理G了,於是將G放入全局等待隊列等待空閑P接管,然後M自己sleep。   通過實驗,當一個G運行了很久(比如進入死循環),會被自動切到其他CPU核,可能是因為超過時間片后G被移到全局等待隊列中,後面被其他CPU核上的M處理。

  M上P和G的調度:每當一個G要開始執行時,調度器判斷當前M的數量是否可以很好處理完G:如果M少G多且有空閑P,則新建M或喚醒一個sleep M,並指定使用某個空閑P;如果M應付得來,G被負載均衡放入一個現有P+M中。

  當M處理完其身上的所有G后,會再去全局等待隊列中找G,如果沒有就從其他P中分一半的G(以便保證各個M處理G的負載大致相等),如果還沒有,M就去sleep了,對應的P變為空閑P。 在M進入sleep期間,調度器可能會給其P不斷放入G,等M醒后(比如超時):如果G數量不多,則M直接處理這些G;如果M覺得G太多且有空閑P,會先主動喚醒其他sleep的M來分擔G,如果沒有其他sleep的M,調度器創建新M來分擔。

協程特點

  協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。因此,協程能保留上一次調用時的狀態(即所有局部狀態的一個特定組合),每次過程重入時,就相當於進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置。線程和進程的操作是由程序觸發系統接口,最後的執行者是系統;協程的操作執行者則是用戶自身程序,goroutine也是協程。

 

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

Java 數據持久化系列之JDBC

前段時間小冰在工作中遇到了一系列關於數據持久化的問題,在排查問題時發現自己對 Java 後端的數據持久化框架的原理都不太了解,只有不斷試錯,因此走了很多彎路。於是下定決心,集中精力學習了持久化相關框架的原理和實現,總結出這個系列。

上圖是我根據相關源碼和網上資料總結的有關 Java 數據持久化的架構圖(只代表本人想法,如有問題,歡迎留言指出)。最下層就是今天要講的 JDBC,上一層是數據庫連接池層,包括 HikariCP 和 Druid等;再上一層是分庫分表中間件,比如說 ShardingJDBC;再向上是對象關係映射層,也就是 ORM,包括 Mybatis 和 JPA;最上邊是 Spring 的事務管理。

本系列的文章會依次講解圖中各個開源框架的基礎使用,然後描述其原理和代碼實現,最後會着重分析它們之間是如何相互集成和配合的。

廢話不多說,我們先來看 JDBC。

JDBC 定義

JDBC是Java Database Connectivity的簡稱,它定義了一套訪問數據庫的規範和接口。但它自身不參与數據庫訪問的實現。因此對於目前存在的數據庫(譬如Mysql、Oracle)來說,要麼數據庫製造商本身提供這些規範與接口的實現,要麼社區提供這些實現。

如上圖所示,Java 程序只依賴於 JDBC API,通過 DriverManager 來獲取驅動,並且針對不同的數據庫可以使用不同的驅動。這是典型的橋接的設計模式,把抽象 Abstraction 與行為實現Implementation 分離開來,從而可以保持各部分的獨立性以及應對他們的功能擴展。

JDBC 基礎代碼示例

單純使用 JDBC 的代碼邏輯十分簡單,我們就以最為常用的MySQL 為例,展示一下使用 JDBC 來建立數據庫連接、執行查詢語句和遍歷結果的過程。

public static void connectionTest(){

    Connection connection = null;
    Statement statement = null;
    ResultSet resultSet = null;

    try {
        // 1. 加載並註冊 MySQL 的驅動
        Class.forName("com.mysql.cj.jdbc.Driver").newInstance();

        // 2. 根據特定的數據庫連接URL,返回與此URL的所匹配的數據庫驅動對象
        Driver driver = DriverManager.getDriver(URL);
        // 3. 傳入參數,比如說用戶名和密碼
        Properties props = new Properties();
        props.put("user", USER_NAME);
        props.put("password", PASSWORD);

        // 4. 使用數據庫驅動創建數據庫連接 Connection
        connection = driver.connect(URL, props);

        // 5. 從數據庫連接 connection 中獲得 Statement 對象
        statement = connection.createStatement();
        // 6. 執行 sql 語句,返回結果
        resultSet = statement.executeQuery("select * from activity");
        // 7. 處理結果,取出數據
        while(resultSet.next())
        {
            System.out.println(resultSet.getString(2));
        }

        .....
    }finally{
        // 8.關閉鏈接,釋放資源  按照JDBC的規範,使用完成后管理鏈接,
        // 釋放資源,釋放順序應該是: ResultSet ->Statement ->Connection
        resultSet.close();
        statement.close();
        connection.close();
    }
}

代碼中有詳細的註釋描述每一步的過程,相信大家也都對這段代碼十分熟悉。

唯一要提醒的是使用完之後的資源釋放順序。按照 JDBC 規範,應該依次釋放 ResultSet,Statement 和 Connection。當然這隻是規範,很多開源框架都沒有嚴格的執行,但是 HikariCP卻嚴格准守了,它可以帶來很多優勢,這些會在之後的文章中講解。

上圖是 JDBC 中核心的 5 個類或者接口的關係,它們分別是 DriverManager、Driver、Connection、Statement 和 ResultSet。

DriverManager 負責管理數據庫驅動程序,根據 URL 獲取與之匹配的 Driver 具體實現。Driver 則負責處理與具體數據庫的通信細節,根據 URL 創建數據庫連接 Connection。

Connection 表示與數據庫的一個連接會話,可以和數據庫進行數據交互。Statement 是需要執行的 SQL 語句或者存儲過程語句對應的實體,可以執行對應的 SQL 語句。ResultSet 則是 Statement 執行后獲得的結果集對象,可以使用迭代器從中遍曆數據。

不同數據庫的驅動都會實現各自的 Driver、Connection、Statement 和 ResultSet。而更為重要的是,眾多數據庫連接池和分庫分表框架也都是實現了自己的 Connection、Statement 和 ResultSet,比如說 HikariCP、Druid 和 ShardingJDBC。我們接下來會經常看到它們的身影。

接下來,我們依次看一下這幾個類及其涉及的操作的原理和源碼實現。

載入 Driver 實現

可以直接使用 Class#forName的方式來載入驅動實現,或者在 JDBC 4.0 后則基於 SPI 機制來導入驅動實現,通過在 META-INF/services/java.sql.Driver 文件中指定實現類的方式來導入驅動實現,下面我們就來看一下兩種方式的實現原理。

Class#forName 作用是要求 JVM 查找並加載指定的類,如果在類中有靜態初始化器的話,JVM 會執行該類的靜態代碼段。加載具體 Driver 實現時,就會執行 Driver 中的靜態代碼段,將該 Driver 實現註冊到 DriverManager 中。我們來看一下 MySQL 對應 Driver 的具體代碼。它就是直接調用了 DriverManager的 registerDriver 方法將自己註冊到其維護的驅動列表中。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        // 直接調用 DriverManager的 registerDriver 將自己註冊到其中
        DriverManager.registerDriver(new Driver());
    }
}

SPI 機制使用 ServiceLoader 類來提供服務發現機制,動態地為某個接口尋找服務實現。當服務的提供者提供了服務接口的一種實現之後,必須根據 SPI 約定在 META-INF/services 目錄下創建一個以服務接口命名的文件,在該文件中寫入實現該服務接口的具體實現類。當服務調用 ServiceLoader 的 load 方法的時候,ServiceLoader 能夠通過約定的目錄找到指定的文件,並裝載實例化,完成服務的發現。

DriverManager 中的 loadInitialDrivers 方法會使用 ServiceLoader 的 load 方法加載目前項目路徑下的所有 Driver 實現。

public class DriverManager {
    // 程序中已經註冊的Driver具體實現信息列表。registerDriver類就是將Driver加入到這個列表
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    // 使用ServiceLoader 加載具體的jdbc driver實現
    static {
        loadInitialDrivers();
    }
    private static void loadInitialDrivers() {
        // 省略了異常處理
        // 獲得系統屬性 jdbc.drivers 配置的值
        String drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                // 通過 ServiceLoader 獲取到Driver的具體實現類,然後加載這些類,會調用其靜態代碼塊
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
                return null;
            }
        });

        String[] driversList = drivers.split(":");
        // for 循環加載系統屬性中的Driver類。
        for (String aDriver : driversList) {
            println("DriverManager.Initialize: loading " + aDriver);
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        }
    }
}

比如說,項目引用了 MySQL 的 jar包 mysql-connector-java,在這個 jar 包的 META-INF/services 文件夾下有一個叫 java.sql.Driver 的文件,文件的內容為 com.mysql.cj.jdbc.Driver。而 ServiceLoader 的 load 方法找到這個文件夾下的文件,讀取文件的內容,然後加載出文件內容所指定的 Driver 實現。而正如之前所分析的,這個 Driver 類被加載時,會調用 DriverManager 的 registerDriver 方法,從而完成了驅動的加載。

Connection、Statement 和 ResultSet

當程序加載完具體驅動實現后,接下來就是建立與數據庫的連接,執行 SQL 語句並且處理返回結果了,其過程如下圖所示。

建立 Connection

創建 Connection 連接對象,可以使用 Driver 的 connect 方法,也可以使用 DriverManager 提供的 getConnection 方法,此方法通過 url 自動匹配對應的驅動 Driver 實例,然後還是調用對應的 connect 方法返回 Connection 對象實例。

建立 Connection 會涉及到與數據庫進行網絡請求等大量費時的操作,為了提升性能,往往都會引入數據庫連接池,也就是說復用 Connection,免去每次都創建 Connection 所消耗的時間和系統資源。

Connection 默認情況下,對於創建的 Statement 執行的 SQL 語句都是自動提交事務的,即在 Statement 語句執行完后,自動執行 commit 操作,將事務提交,結果影響到物理數據庫。為了滿足更好地事務控制需求,我們也可以手動地控制事務,手動地在Statement 的 SQL 語句執行後進行 commit 或者rollback。

connection = driver.connect(URL, props);
// 將自動提交關閉
connection.setAutoCommit(false);

statement = connection.createStatement();
statement.execute("INSERT INTO activity (activity_id, activity_name, product_id, start_time, end_time, total, status, sec_speed, buy_limit, buy_rate) VALUES (1, '香蕉大甩賣', 1, 530871061, 530872061, 20, 0, 1, 1, 0.20);");
// 執行後手動 commit
statement.getConnection().commit();

Statement

Statement 的功能在於根據傳入的 SQL 語句,將傳入 SQL 經過整理組合成數據庫能夠識別的執行語句(對於靜態的 SQL 語句,不需要整理組合;而對於預編譯SQL 語句和批量語句,則需要整理),然後傳遞 SQL 請求,之後會得到返回的結果。對於查詢 SQL,結果會以 ResultSet 的形式返回。

當你創建了一個 Statement 對象之後,你可以用它的三個執行方法的任一方法來執行 SQL 語句。

  • boolean execute(String SQL) : 如果 ResultSet 對象可以被檢索,則返回的布爾值為 true ,否則返回 false 。當你需要使用真正的動態 SQL 時,可以使用這個方法來執行 SQL DDL 語句。
  • int executeUpdate(String SQL) : 返回執行 SQL 語句影響的行的數目。使用該方法來執行 SQL 語句,是希望得到一些受影響的行的數目,例如,INSERT,UPDATE 或 DELETE 語句。
  • ResultSet executeQuery(String SQL) : 返回一個 ResultSet 對象。當你希望得到一個結果集時使用該方法,就像你使用一個 SELECT 語句。

對於不同類型的 SQL 語句,Statement 有不同的接口與其對應。

接口 | 介紹
-|-| Statement | 適合運行靜態 SQL 語句,不接受動態參數 PreparedStatement | 計劃多次使用並且預先編譯的 SQL 語句,接口需要傳入額外的參數 CallableStatement | 用於訪問數據庫存儲過程

Statement 主要用於執行靜態SQL語句,即內容固定不變的SQL語句。Statement每執行一次都要對傳入的SQL語句編譯一次,效率較差。而 PreparedStatement則解決了這個問題,它會對 SQL 進行預編譯,提高了執行效率。

PreparedStatement pstmt = null;
    try {
        String SQL = "Update activity SET activity_name = ? WHERE activity_id = ?";
        pstmt = connection.prepareStatement(SQL);
        pstmt.setString(1, "測試");
        pstmt.setInt(2, 1);
        pstmt.executeUpdate();
    }
    catch (SQLException e) {
    }
    finally {
        pstmt.close();
    }
}

除此之外, PreparedStatement 還可以預防 SQL 注入,因為 PreparedStatement 不允許在插入參數時改變 SQL 語句的邏輯結構。

PreparedStatement 傳入任何數據不會和原 SQL 語句發生匹配關係,無需對輸入的數據做過濾。如果用戶將”or 1 = 1”傳入賦值給佔位符,下述SQL 語句將無法執行:select * from t where username = ? and password = ?。

ResultSet

當 Statement 查詢 SQL 執行后,會得到 ResultSet 對象,ResultSet 對象是 SQL語句查詢的結果集合。ResultSet 對從數據庫返回的結果進行了封裝,使用迭代器的模式可以逐條取出結果集中的記錄。

while(resultSet.next()) {
    System.out.println(resultSet.getString(2));
}

ResultSet 一般也建議使用完畢直接 close 掉,但是需要注意的是關閉 ResultSet 對象不關閉其持有的 Blob、Clob 或 NClob 對象。 Blob、Clob 或 NClob 對象在它們被創建的的事務期間會一直持有效,除非其 free 函數被調用。

參考

    • https://blog.csdn.net/wl044090432/article/details/60768342
    • https://blog.csdn.net/luanlouis/article/details/29850811

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

深入理解static關鍵字

在開始講static之前,我想讓各位看一段有意思的代碼:

public class Test {
     
    static{
        System.out.println("test static 1");
    }
  
    static{
        System.out.println("test static 2");
    }
    
    public static void main(String[] args) {
         
    }
}

看完程序,小白童鞋發話了:啥玩意?main方法中啥都沒有,能運行啥?博主你個星星星…

運行結果:
test static 1
test static 2

小白童鞋:那啥…那啥…博主我說啥了,我啥都沒說…

其實,上面的代碼懂的自然懂,不懂的自然就不懂了,因為上面的代碼涉及到JVM的類加載了!當然不在本篇博客文章的範疇內,如果有興趣理解上面的程序,這篇文章可能會對你有所幫助

1、static存在的主要意義

static的主要意義是在於創建獨立於具體對象的域變量或者方法。以致於即使沒有創建對象,也能使用屬性和調用方法

static關鍵字還有一個比較關鍵的作用就是 用來形成靜態代碼塊以優化程序性能。static塊可以置於類中的任何地方,類中可以有多個static塊。在類初次被加載的時候,會按照static塊的順序來執行每個static塊,並且只會執行一次。

  為什麼說static塊可以用來優化程序性能,是因為它的特性:只會在類加載的時候執行一次。因此,很多時候會將一些只需要進行一次的初始化操作都放在static代碼塊中進行。

2、static的獨特之處

1、被static修飾的變量或者方法是獨立於該類的任何對象,也就是說,這些變量和方法不屬於任何一個實例對象,而是被類的實例對象所共享

怎麼理解 “被類的實例對象所共享” 這句話呢?就是說,一個類的靜態成員,它是屬於大夥的【大夥指的是這個類的多個對象實例,我們都知道一個類可以創建多個實例!】,所有的類對象共享的,不像成員變量是自個的【自個指的是這個類的單個實例對象】…我覺得我已經講的很通俗了,你明白了咩?

2、在該類被第一次加載的時候,就會去加載被static修飾的部分,而且只在類第一次使用時加載並進行初始化,注意這是第一次用就要初始化,後面根據需要是可以再次賦值的。

3、static變量值在類加載的時候分配空間,以後創建類對象的時候不會重新分配。賦值的話,是可以任意賦值的!

4、被static修飾的變量或者方法是優先於對象存在的,也就是說當一個類加載完畢之後,即便沒有創建對象,也可以去訪問。

3、static應用場景

因為static是被類的實例對象所共享,因此如果某個成員變量是被所有對象所共享的,那麼這個成員變量就應該定義為靜態變量

因此比較常見的static應用場景有:

1、修飾成員變量
2、修飾成員方法
3、靜態代碼塊
4、修飾類【只能修飾內部類也就是靜態內部類】
5、靜態導包

以上的應用場景將會在下文陸續講到…

4、靜態變量和實例變量的概念

靜態變量:
static修飾的成員變量叫做靜態變量【也叫做類變量】,靜態變量是屬於這個類,而不是屬於是對象。

實例變量:
沒有被static修飾的成員變量叫做實例變量,實例變量是屬於這個類的實例對象。

還有一點需要注意的是:static是不允許用來修飾局部變量,不要問我問什麼,因為java規定的!

5、靜態變量和實例變量區別【重點常用】

靜態變量:
靜態變量由於不屬於任何實例對象,屬於類的,所以在內存中只會有一份,在類的加載過程中,JVM只為靜態變量分配一次內存空間。

實例變量:
每次創建對象,都會為每個對象分配成員變量內存空間,實例變量是屬於實例對象的,在內存中,創建幾次對象,就有幾份成員變量。

我相信各位智商都比宜春智商要高,應該都能理解上面的話。下面舉了例子完全出於娛樂,理解了大可不必看,下面的例子僅供參考,僅供娛樂一下下氣氛,趕時間的熊dei大可略過!

怎麼理解呢?打個比喻吧…就比方說程序員小王是一個比較溫柔陽光的男孩子,這1024的這一天,老闆閑的沒事,非要拉着程序員小王來玩耍,怎麼個玩法呢?老闆和小王一人拿着一把菜刀,規則很簡單,互相傷害,一人一刀,你一刀,我一刀….遊戲一開始,老闆二話不說,跳起來就是一刀,程序員小王二話也沒說反手就是一菜刀回去,這個時候老闆發飆了,雙眼瞪得忒大,跳起來又是一刀,這個時候程序員小王不敢還手了,就沒動手。沒想到老闆越來越生猛,左一刀右一刀全程下來差不多砍個半個時….程序員小王一直沒有還過手,因為小王知道他是老闆…

這個程序員小王只會在老闆第一次揮刀的時候,回老闆一刀,之後就不還手了,這個時候我們把程序員小王看做是靜態變量,把老闆第一次向小王揮刀看做是類加載,把小王回老闆一刀看出是分配內存空間,而一人一刀這個回合過程看成是類加載的過程,之後老闆的每一刀都看成是創建一次對象。

連貫起來就是static變量值在類第一次加載的時候分配空間,以後創建類對象的時候不會重新分配

之後這個老闆挨了一刀之後躺醫院了一年,一出院回到公司第一件事就是拉程序員宜春出來玩耍,老闆殊不知其然,這個博主程序員宜春性格異常暴躁,老闆遞給程序員宜春一把菜刀,博主宜春一接過菜刀,猝不及防的被老闆跳起來就是一刀,程序員宜春痛的嗷了一聲,暴躁的程序員宜春還沒嗷完,在嗷的同時跳起來就是給老闆一刀,接着老闆跳起來又是一刀,程序員宜春嗷的一聲又是回一刀,老闆跳起來又一刀,程序員宜春嗷的一聲又是回一刀,只要老闆沒停程序員宜春就沒停,因為程序員宜春知道,就自己這曝脾氣,暴躁起來si都敢摸,肯定有幾個老鐵知道….

程序員宜春就類似實例變量,每次創建對象,都會為每個對象分配成員變量內存空間,就像老闆來一刀,程序員宜春都會回一刀這樣子的…

6、訪問靜態變量和實例變量的兩種方式

我們都知道靜態變量是屬於這個類,而不是屬於是對象,static獨立於對象。

但是各位有木有想過:靜態成員變量雖然獨立於對象,但是不代表不可以通過對象去訪問,所有的靜態方法和靜態變量都可以通過對象訪問【只要訪問權限足夠允許就行】,不理解沒關係,來個代碼就理解了

public class StaticDemo {

        static int value = 666;

        public static void main(String[] args) throws Exception{
            new StaticDemo().method();
        }

        private void method(){
            int value = 123;
            System.out.println(this.value);
        }

}

猜想一下結果,我猜你的結果是123,哈哈是咩?其實

運行結果: 666

回過頭再去品味一下上面的那段話,你就能非常客觀明了了,這個思想概念要有隻是這種用法不推薦!

因此小結一下訪問靜態變量和實例變量的兩種方法:

靜態變量:

類名.靜態變量

對象.靜態變量(不推薦)

靜態方法:

類名.靜態方法

對象.靜態方法(不推薦)

7、static靜態方法

static修飾的方法也叫做靜態方法,不知道各位發現咩有,其實我們最熟悉的static靜態方法就是main方法了~小白童鞋:喔好像真的是哦~。由於對於靜態方法來說是不屬於任何實例對象的,this指的是當前對象,因為static靜態方法不屬於任何對象,所以就談不上this了。

還有一點就是:構造方法不是靜態方法

8、static靜態代碼塊

先看個程序吧,看看自個是否掌握了static代碼塊,下面程序代碼繼承關係為 BaseThree——> BaseTwo——> BaseOne

BaseOne類

package com.gx.initializationblock;

public class BaseOne {

    public BaseOne() {
        System.out.println("BaseOne構造器");
    }

    {
        System.out.println("BaseOne初始化塊");
        System.out.println();
    }

    static {
        System.out.println("BaseOne靜態初始化塊");

    }

}

BaseTwo類

package com.gx.initializationblock;

public class BaseTwo extends BaseOne {
    public BaseTwo() {
        System.out.println("BaseTwo構造器");
    }

    {
        System.out.println("BaseTwo初始化塊");
    }

    static {
        System.out.println("BaseTwo靜態初始化塊");
    }
}

BaseThree 類

package com.gx.initializationblock;

public class BaseThree extends BaseTwo {
    public BaseThree() {
        System.out.println("BaseThree構造器");
    }

    {
        System.out.println("BaseThree初始化塊");
    }

    static {
        System.out.println("BaseThree靜態初始化塊");
    }
}

測試demo2類

package com.gx.initializationblock;

/*
     注:這裏的ABC對應BaseOne、BaseTwo、BaseThree 
 * 多個類的繼承中初始化塊、靜態初始化塊、構造器的執行順序
     在繼承中,先後執行父類A的靜態塊,父類B的靜態塊,最後子類的靜態塊,
     然後再執行父類A的非靜態塊和構造器,然後是B類的非靜態塊和構造器,最後執行子類的非靜態塊和構造器
 */
public class Demo2 {
    public static void main(String[] args) {
        BaseThree baseThree = new BaseThree();
        System.out.println("-----");
        BaseThree baseThree2 = new BaseThree();

    }
}

運行結果

BaseOne靜態初始化塊
BaseTwo靜態初始化塊
BaseThree靜態初始化塊
BaseOne初始化塊

BaseOne構造器
BaseTwo初始化塊
BaseTwo構造器
BaseThree初始化塊
BaseThree構造器
-----
BaseOne初始化塊

BaseOne構造器
BaseTwo初始化塊
BaseTwo構造器
BaseThree初始化塊
BaseThree構造器

至於static代碼塊運行結果不是很清晰的童鞋,詳細講解請看這篇

以上僅僅是讓各位明確代碼塊之間的運行順序,顯然還是不夠的,靜態代碼塊通常用來對靜態變量進行一些初始化操作,比如定義枚舉類,代碼如下:

public enum WeekDayEnum {
    MONDAY(1,"周一"),
    TUESDAY(2, "周二"),
    WEDNESDAY(3, "周三"),
    THURSDAY(4, "周四"),
    FRIDAY(5, "周五"),
    SATURDAY(6, "周六"),
    SUNDAY(7, "周日");
 
    private int code;
    private String desc;
 
    WeekDayEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }
 
    private static final Map<Integer, WeekDayEnum> WEEK_ENUM_MAP = new HashMap<Integer, WeekDayEnum>();
 
    // 對map進行初始化
    static {
        for (WeekDayEnum weekDay : WeekDayEnum.values()) {
            WEEK_ENUM_MAP.put(weekDay.getCode(), weekDay);
        }
    }
 
    public static WeekDayEnum findByCode(int code) {
        return WEEK_ENUM_MAP.get(code);
    }
 
    public int getCode() {
        return code;
    }
 
    public void setCode(int code) {
        this.code = code;
    }
 
    public String getDesc() {
        return desc;
    }
 
    public void setDesc(String desc) {
        this.desc = desc;
    }
} 

當然不僅僅是枚舉這一方面,還有我們熟悉的單例模式同樣也用到了靜態代碼塊,如下:

public class Singleton {
    private static Singleton instance;
 
    static {
        instance = new Singleton();
    }
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        return instance;
    }
}

9、static變量與普通變量區別

static變量也稱作靜態變量,靜態變量和非靜態變量的區別是:靜態變量被所有的對象所共享,在內存中只有一個副本,它當且僅當在類初次加載時會被初始化。而非靜態變量是對象所擁有的,在創建對象的時候被初始化,存在多個副本,各個對象擁有的副本互不影響。

還有一點就是static成員變量的初始化順序按照定義的順序進行初始化。

10、靜態內部類

靜態內部類與非靜態內部類之間存在一個最大的區別,我們知道非靜態內部類在編譯完成之後會隱含地保存着一個引用,該引用是指向創建它的外圍內,但是靜態內部類卻沒有。沒有這個引用就意味着:

1、它的創建是不需要依賴外圍類的創建。
2、它不能使用任何外圍類的非static成員變量和方法。

代碼舉例(靜態內部類實現單例模式)

public class Singleton {
    
   // 聲明為 private 避免調用默認構造方法創建對象
    private Singleton() {
    }
    
   // 聲明為 private 表明靜態內部該類只能在該 Singleton 類中被訪問
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Singleton 類加載時,靜態內部類 SingletonHolder 沒有被加載進內存。只有當調用 getUniqueInstance()方法從而觸發 SingletonHolder.INSTANCESingletonHolder 才會被加載,此時初始化 INSTANCE 實例,並且 JVM 能確保 INSTANCE 只被實例化一次。

這種方式不僅具有延遲初始化的好處,而且由 JVM 提供了對線程安全的支持。

11、靜態導包

靜態導包格式:import static

這兩個關鍵字連用可以指定導入某個類中的指定靜態資源,並且不需要使用類名調用類中靜態成員,可以直接使用類中靜態成員變量和成員方法

//  Math. --- 將Math中的所有靜態資源導入,這時候可以直接使用裏面的靜態方法,而不用通過類名進行調用
//  如果只想導入單一某個靜態方法,只需要將換成對應的方法名即可
 
import static java.lang.Math.;
//  換成import static java.lang.Math.max;具有一樣的效果
 
public class Demo {
    public static void main(String[] args) {
 
        int max = max(1,2);
        System.out.println(max);
    }
}

靜態導包在書寫代碼的時候確實能省一點代碼,可以直接調用裏面的靜態成員,但是會影響代碼可讀性,所以開發中一般情況下不建議這麼使用。

12、static注意事項

1、靜態只能訪問靜態。
2、非靜態既可以訪問非靜態的,也可以訪問靜態的。

13、final與static的藕斷絲連

到這裏文章本該結束了的,但是static的使用始終離不開final字眼,二者可謂藕斷絲連,常常繁見,我覺得還是很有必要講講,那麼一起來看看下面這個程序吧。

package Demo;

class FinalDemo {
    public final double i = Math.random();
    public static double t = Math.random();
}

public class DemoDemo {
    public static void main(String[] args) {

        FinalDemo demo1 = new FinalDemo();
        FinalDemo demo2 = new FinalDemo();
        System.out.println("final修飾的  i=" + demo1.i);
        System.out.println("static修飾的 t=" + demo1.t);
        System.out.println("final修飾的  i=" + demo2.i);
        System.out.println("static修飾的 t=" + demo2.t);

        System.out.println("t+1= "+ ++demo2.t );
//      System.out.println( ++demo2.i );//編譯失敗
      }
}
運行結果:
    final修飾的  i=0.7282093281367935
    static修飾的 t=0.30720545678577604
    final修飾的  i=0.8106990945706758
    static修飾的 t=0.30720545678577604
    t+1= 1.307205456785776

static修飾的變量沒有發生變化是因為static作用於成員變量只是用來表示保存一份副本,其不會發生變化。怎麼理解這個副本呢?其實static修飾的在類加載的時候就加載完成了(初始化),而且只會加載一次也就是說初始化一次,所以不會發生變化!

至於final修飾的反而發生變化了?是不是巔覆你對final的看法?關於final詳細講解博主也準備好了一篇文章

ok,文章就先到這裏了,希望這篇文章能夠幫助到你對static的認識,若有不足或者不正之處,希望諒解並歡迎批評指正!

如果本文章對你有幫助,哪怕是一點點,那就請點一個讚唄,謝謝~

參考:
《java編程思想》

如果本文對你有一點點幫助,那麼請點個讚唄,謝謝~

最後,若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回復!

歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術,說好了來了就是盆友喔…

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

京東物流出問題了?褥了30塊羊毛 & 淺析系統架構

本人親身經歷,但後續的流程分析都是個人猜測的,畢竟沒有實際做過這塊的業務。

訂單物流阻塞經過

火熱的雙11剛剛退去,截止今日,我在京東購買的礦泉水終於到貨啦,下單兩箱還只收到了一箱 🙁 ,從下單到收到貨過去了14天,足足兩周的時間。

我從11-20號開始與京東客服聯繫,直到11-25整個購物體驗才完成,也因為京東沒有按照約定重新發貨,算是補償了我3000個京豆。

朋友們,不會不知道京豆是幹啥的吧,100個京豆相當於一塊錢,1000個京豆相當於10塊錢,3000個京豆就是30塊錢。

可那不是現金有啥卵用,你不會不在京東購物吧,下單的時候就可以選擇用京豆來抵用一部分下單金額了。

所以一般購買商品后鼓勵你去評價,文字超過一定字數且上傳了購買商品的圖片,就能得到比如20個京豆。京豆積少成多,就可以下單抵用現金了。

廢話不多說,回到正題!

雙11我在京東下單,自營商品的訂單一般都是次日達,因為雙11物流緊張,所以下單后提示11-13日送達。

11-13日:

遺憾的是,11-13日並沒有如期送達,查看訂單物流,增加了一段溫馨提示:「由於銷售火爆,根據目前情況,訂單預計11月16日送達到您的手中」,額~,當然大家都能理解,原來京東商品這麼「火爆」,畢竟雙十一累積銷量2000億呢。

11-20日 周三:

問題是到了11-16日並沒有送達,我把這個訂單差點忘記了,11-20號突然想起來了這件事,上京東確認了訂單,才發現還是那個「銷售火爆…」的提示呀!竟然沒有給我送貨。。。

然後在線聯繫人工客服,說了一下情況,客服態度很好,兩箱水拆分下單后因已經拆分為了兩個訂單,有兩個訂單號,為了表示歉意,每個訂單號補了500個京豆,1000個京豆到手了。

然後,客服跟我說,已經給我催促倉儲發貨了,讓耐心等待一下,預計第二天就能到了。

11-21號 周四:

可惜到了第二天,並沒有像客服MM所說的那樣如期送達,反正已經晚了,心想也不差這一天兒,還贈送了京豆,再等等了…… 。

11-22號 周五:

然而,到了11-22號還是沒有配送物流通知,訂單中的分揀流程沒有完成,這是什麼操作??

當天繼續聯繫客服,問了是什麼原因,又來「話術」:小妹已為您催促正在發貨中,此時我有點懷疑了,可能這個流程本身就中斷了,需要人工來協助處理補單流程。

此刻,「客服的嘴,騙人的鬼」終於用到這了~

同時,京東客服升級來了個電話溝通,誠摯的表示歉意,說是倉儲這邊發貨有點問題,正在重新補貨中… ,預計明日就能送到,請注意查收!

11-23日 周六:

「客服的嘴,騙人的鬼」再一次用到這了~

周六仍然沒有收到貨,而且訂單里的物流配送流程一動也沒動~

11-23日 周日:

周日仍然沒有收到貨,而且訂單里的物流配送流程一動也沒動~

看來沒很好的注重用戶體驗嘛,再次在線聯繫客服,每次接線的不是同一個客服,所以每次都要求提供一下訂單號,很煩,此時很無語了,本用戶表示很生氣啊,自己查!

然後呢,客服又說已經重新補發貨了,並且這次竟然不給我大概的送貨時間點了,因為他不相信到底有沒有真的去補發貨操作了,補發貨這個操作多半客服是沒有權限的。

另外,解釋到因訂單延遲時間過長,又一次非常的抱歉,給申請了1000京豆,不過這次京豆並不是實時到賬的,需要經過審核流程。

11-25日 周一:

早上已經在地鐵上了,收到了京東快遞小哥的來電,但是只到了一個訂單的貨。查了一下另外一個訂單物流狀態仍然一動沒動 :(。

最後,客服專員再次電話聯繫,解釋到這個訂單給疏忽了,建議我重新下單,然後這個訂單走退款流程。並且再一次給予了1000京豆的補償 :)。

物流系統異常分析:

上述物流配送異常流程中,想了解故障原因,電話中我也有意識的去問一下客服,是不是某個環節有這樣的問題,但是從客服那裡只能給到說倉配流程有問題 ,具體他們也不是很清楚了,全都是針對用戶的話術,避免說錯話。

作為個技術人,通常得思考一下問題背後的原因:

  • 到底是哪個環節出現的問題
  • 出現這樣問題的原因
  • 對用戶的影響及應對方案
  • 如何能避免類似的問題發生
物流系統介紹

由此次問題引出,我還特意去查資料看了一下京東物流的系統架構演進過程。記得當時京東物流招人非常猛,作為一個內部非常重量級的項目投入了很多研發人力。

在2012年的時候京東內部開始對物流系統進行設計改造,那時訪問量應該還不算高,最初的系統還沒那麼複雜。新改造的物流系統:「青龍系統

青龍系統演進過程如上圖所示 ,它的系統發展至今,已經包含了分揀中心,運輸路由,終端,對外拓展,運營支持等五十餘個核心子系統,構建了完善的電商物流體系。

並且青龍系統中總結了一些最佳的實戰原則,如下所示:

這些系統設計原則我認為對任何系統都是通用的,值得我們一起學習的:

  • 高可用

選擇合適的架構方案;大系統小做,服務拆分;併發控制,服務隔離;灰度發布;全方位監控報警;核心服務,平滑降級。

  • 高性能

緩存和異步化,同步接口異步化設計;接口數據緩存化。

接口數據緩存化是非常重要的手段,對Redis緩存系統的很好的利用,構建了具有自己特色的緩存體系,很好的支撐了業務發展。同時,還發展了基於Redis的分佈式調度系統

  • 數據一致性

高實時性/高一致性,高實時性/低一致性,低實時性/高一致性,低實時性/低一致性。

針對具體的業務,可以匹配到具體的數據場景,找到對應的解決方案。要客觀的結合業務分析,選擇最適合的一致性方案,並不是高實時性/高一致性就好,成本是很更貴的。

  • 用戶體驗

東哥要求過任何人不能對用戶體驗提升的建議說No。用戶體驗主要遵循MVP原則和動態運營的原則。

MVP原則:也就是敏捷開發中的迭代思路。即快速迭代,核心需求線上,及時的反饋和改進。

動態運營:跟MVP原則強關聯,上線后收集並分析用戶數據,使得產品落地的設計符合用戶的需求,不符合設計要求的就要不斷的持續調整,是一個動態持續的過程。

物流分揀系統

簡單介紹完了物流系統的演進過程及架構原則,還是回到主題,到底是哪個環節出現了問題?

需要了解整個購物鏈路的各個環節:

用戶整個購物流程經過以上幾個關鍵的流程,已經生成訂單號並且已經支付了,流程到了訂單中心。

各個系統都是分佈式部署的,訂單中心會發送一個MQ消息給各個下游系統,積分系統增加積分京豆等,促銷系統發放優惠券等,倉儲系統接收到MQ消息進行處理,調用物流系統生成物流單,通知到配送站,由配送員送貨。

結合一個火爆的訂單,看一下訂單跟蹤過程:

該訂單在倉庫處理中已經打包完成,訂單在京東【北京李橋接貨倉庫】分揀完成。注意到了「分揀」二字,順便看了一下正常的訂單流程,會經過多個貨倉的分揀過程,最終會分揀到離用戶最近的貨倉。

所以,猜測,這筆訂單的問題就是在配送前的分揀系統處理過程中出現了異常情況。

青龍物流系統其中就包括了預分揀流程,如下所示:

當用戶下單后,首先必須是經過預分揀環節,但是根據最新的訂單跟蹤過程看,是先進行了倉庫打包處理,然後進入分揀流程。

分揀系統接收到訂單,根據不同的訂單進行規則匹配,分配站點,處理成功後生產包裹打印標籤。

訂單無法被正常分揀完成,將無法生成訂單:

想必我的訂單大概率就是在分揀環節出現了問題 ~

分揀系統的目標:

其中可用性要求是達到99.99%,4個9的可用率呢,看來很不幸啊,不可用的0.01%小概率事件偶發在了我的訂單上。

預分揀算法:

1、經驗值

只適用於同一個地址多次購買,依賴於第一妥投地址。

2、特徵值

需要提前人工維護關鍵字,依賴於關鍵字的準確性。

3、特殊配置

需要提前人工配置,依賴於該區域是否有特殊配置。

4、GIS

通過GIS技術精準的匹配地理位置。

上述都沒有匹配到,那麼只能走人工處理流程了。

預分揀系統架構:

訂單系統下發服務,默認會進入到預分揀系統,不同的訂單有不同的匹配規則,匹配規則使用開源的Drools來實現的,規則匹配完成,會按照預分揀算法匹配,優先匹配到離用戶最近的地址,返回自動預分揀的結果。

一旦回傳失敗,應該會有預警,需要人工介入來協助完成預分揀,將結果返回給訂單服務。

預分揀服務系統交互流程:

分揀服務使用Tomcat分佈式部署的Worker進程,完成后,將結果寫入到任務庫,回傳服務從人物庫抓取分揀結果回傳站點。

下圖來源於網絡,不是很清晰了:

其中預分揀服務接受訂單服務都是分佈式部署的,並且針對不同的訂單做了服務隔離,使用應用服務器是Tomcat;全文檢索使用的Solr,可能目前已改進為流行的ElasticSerach架構了;分佈式緩存使用了Redis集群;預分揀算法中的地址庫、特徵值、配置都對應了自己的Worker集群,也是做了服務隔離,每個服務分佈式部署,最終將結果寫入到MySQL數據庫中;預分揀回傳站點單獨的Worker集群,用來從數據庫抓取分揀數據,返回給用戶站點。

小結

經過以上過程猜測性分析,基本就清楚了自己的訂單問題出現的位置了。

大概率就是預分揀服務在某一個站點因為流量洪峰或異常出現了故障,可能服務恢復后沒有及時完成自動分揀數據校對。

與客服的溝通結果來看,當分揀過程出現問題后,可能並沒有及時預警並人工及時的去干預處理,導致分揀流程被阻塞,遲遲無法進入到分揀恢復階段。或許也是考慮到這種小概率事件,就由用戶來直接反饋,然後由人工介入處理。

但是,很明顯,客服用話術告知用戶結果,讓用戶耐心等待的同時。在後續的分揀系統訂單恢複流程並不是那麼順暢的,不一定是那麼簡單的人工直接快速處理,會經過一些校驗核對、人工審核等一系列流程,又或者讓技術人員協助恢復的,導致分揀流程流轉下去很慢,也進而影響了用戶體驗。

在線話術告知用戶結果算是A方案。

人工處理的第一筆訂單跟蹤:

而第二個訂單,客服根據情況執行了B方案,將問題升級到專員,電話聯繫用戶,建議用戶重新下單,並給予一定的補償。當你重新下單,分揀系統接收到新的訂單,就是進入了自動預分揀訂單處理過程了,自動化流程當然是很快的,無需人工干預。

總體來說,京東客服的做法可圈可點,整體售後服務流程較以前值得肯定,越來越完善。

同時,系統架構在未來方向上,肯定更趨向於更加的智能化,使用機器學習、人工智能等手段持續不斷優化物流的各環節,減少或避免小概率的事件發生。

ps:文章前半段真實發生,後半段僅作為問題分析參考。

歡迎關注我的公眾號,掃二維碼關注獲得更多精彩文章,與你一同成長~

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

eNSP仿真軟件之利用單臂路由實現VLAN間路由

1、 實驗原理

以太網中,通常會使用VLAN技術隔離二層廣播域來減少廣播的影響,並增強網絡的安全性和可管理性。其缺點是同時也嚴格地隔離了不同VLAN之間的任何二層流量,使分屬於不同VLAN的用戶不能直接互相通信。在現實中,經常會出現某些用戶需要跨越VLAN實現通信的情況,單臂路由技術就是解決VLAN間通信的一種方法。

單臂路由的原理是通過一台路由器, 使VLAN間互通數據通過路由器進行三層轉發。如果在路由器上為每個VLAN分配一個單獨的路由器物理接口,隨着VLAN數量的增加,必然需要更多的接口,而路由器能提供的接口數量比較有限,所以在路由器的一個物理接口上通過配置子接口(即邏輯接口)的方式來實現以一當多的功能,將是一種非常好的方式。路由器同一物理接口的不同子接口作為不同VLAN的默認網關,當不同VLAN間的用戶主機需要通信時,只需將數據包發送給網關,網關處理后再發送至目的主機所在VLAN,從而實現VLAN間通信。由於從拓撲結構圖上看,在交換機與路由器之間,數據僅通過一條物理鏈路傳輸,故被形象地稱之為“單臂路由”。

2、 實驗內容

本實驗模擬公司網絡場景。路由器R1是公司的出口網關,員工PC通過接入層交換機(如S2和S3)接入公司網絡,接入層交換機又通過匯聚交換機S1與路由器R1相連。公司內部網絡通過劃分不同的VLAN隔離了不同部門之間的二層通信,保證各部門間的信息安全,但是由於業務需要,經理、市場部和人事部之間需要能實現跨VLAN通信,網絡管理員決定藉助路由器的三層功能,通過配置單臂路由來實現。

3、 實驗步驟

(1)、新建實驗拓補圖

 

(2)根據實驗編址表進行路由器R1和PC1-3的IP地址,其中路由器的配置方式如下:

配置路由器子接口和IP地址:

★在R1上創建子接口GE 0/0/1.1,配置IP地址為192.168.1.254/24,作為人事部網關地址。

★同理創建子接口並且配置IP地址

(3)公司為保障各部門的信息安全,需保證隔離不同部門間的二層通信,規劃各部門的終端屬於不同的VLAN,併為PC配置相應IP地址。

★在S2上創建VLAN 10和VLAN20,把連接PC-1的E 0/0/1和連接PC-2的E 0/0/2接口配置為Access類型接口,並分別劃分到相應的VLAN中。

★交換機之間或交換機和路由器之間相連的接口需要傳遞多個VLAN信息,需要配置成Trunk接口。將S2和S3的GE 0/0/2接口配置成Trunk類型接口,並允許所有VLAN通過 

 

 

 

★在S1上創建VLAN10、VLAN20和VLAN30,並配置交換機和路由器相連的接口為Trunk,允許所有VLAN通過。

(4)測試PC1-3的連通性,發現仍然不能聯通。

(5)配置路由器子接口封裝VLAN

雖然目前已經創建了不同的子接口,並配置了相關IP地址,但是仍然無法通信。這是由於處於不同VLAN下,不同網段的PC間要實現互相通信,數據包必須通過路由器進行中轉。由S1發送到RI的數據都加上了VLAN標籤,而路由器作為三層設備,默認無法處理帶了VLAN標籤的數據包。因此需要在路由器上的子接口下配置對應VLAN的封裝,使路由器能夠識別和處理VLAN標籤,包括剝離和封裝VLAN標籤。

★在R1的子接口GE 0/0/1.1.上封裝VLAN 10,在子接口GE 0/0/1.2上封裝VLAN 20。在子接口GE 0/0/1.3上封裝VLAN30,並開啟子接口的ARP廣播功能。

使用dot1q termination vid命令配置子接口對一層tag報文的終結功能。即配置該命令后,路由器子接口在接收帶有VLAN tag的報文時,將剝掉tag進行三層轉發,在發送報文時,會將與該子接口對應VLAN的VLAN tag添加到報文中。

使用arp broadcast enable命令開啟子接口的ARP廣播功能。如果不配置該命令,將會導致該子接口無法主動發送ARP廣播報文,以及向外轉發IP報文。 

同理配置R1的子接口GE 0/0/1.2和GE 0/0/1.3。

(7)      配置完成后,在路由器R1上查看接口狀態,可以看到3個子接口的物理狀態和協議狀態都正常。

(8)      查看路由器R1的路由表,可以觀察到,路由表中已經有了192.168.1.0/24、 192.168.2.0/24、 192. 168.3.0/24的路由條目,並且都是路由器R1的直連路由,類似於路由器上的直連物理接口 。

(9)      測試連通性。可以看到PC1和PC2已經可以PING通

(10)      在PC-1上tracertPC-2,可以觀察到PC-1先把ping包發送給自身的網關192.168.1.254, 然後再由網關發送到PC-2。

現以PC-1pingPC-2為例,分析單臂路由的整個運作過程。

      兩台PC由於處於不同的網絡中,這時PC-1會將數據包發往自己的網關,即路由器R1的子接口GE 0/0/1.1的地址192.168.1.254。.

      數據包到達路由器R1后,由於路由器的子接口GE 0/0/1.1已經配置了VLAN封裝,當接收到PC-1發送的VLAN 10的數據幀時,發現數據幀的VLANID跟自身GE0/0/1.1接口配置的VLAN ID 一樣,便會剝離掉數據幀的VLAN標籤后通過三層路由轉發。

      通過查找路由表后,發現數據包中的目的地址192.168.2.1所屬的192.168.2.0/24 網段的路由條目,已經是路由器R1上的直連路由,且出接口為GE 0/0/1.2,便將該數據包發送至GE 0/0/1.2接口。

      當GE0/0/1.2接口接收到一個沒有帶VLAN標籤的數據幀時,便會加上自身接口所配置的VLAN ID 20后再進行轉發,然後通過交換機將數據幀順利轉發給PC-2。

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

ASP.NET Core gRPC 使用 Consul 服務註冊發現

一. 前言

gRPC 在當前最常見的應用就是在微服務場景中,所以不可避免的會有服務註冊與發現問題,我們使用gRPC實現的服務可以使用 Consul 或者 etcd 作為服務註冊與發現中心,本文主要介紹Consul。

二. Consul 介紹

Consul是一種服務網絡解決方案,可跨任何運行平台以及公共或私有雲來連接和保護服務。它可以讓你發現服務並保護網絡流量。它可以在Kubernetes中使用,實現服務發現和服務網格功能(k8s默認etcd)。 提供安全服務通訊,保護和觀察服務之間的通信,而無需修改其代碼。 提供動態負載平衡, 使用Consul和HAProxy,Nginx或F5自動執行負載均衡器配置。 Consul 可以用於服務發現和服務網格。

翻譯自官網

三. Consul 安裝配置

安裝

Consul 下載地址:

根據自己的系統來選擇,我這裏選擇的是 Windows 版本的,直接解壓即可運行。

啟動

consul agent -dev -ui

本文不詳細介紹Consul使用,如需請自行查看相關資料

四. .NET Core Consul 客戶端的選擇

Consul 提供了 HTTP API 的方式來進行通訊,我們可以直接調用API或者是使用第三方封裝好的客戶端組件,通過Nuget搜索可以發現許多。

這裏面我沒有一一測試,但是目前使用量最多的 Consul 組件是不支持設置 GRPC 健康檢查的,而且 github 也停止了更新。

所以我 Fork 了這個倉庫,然後添加了 GRPC 的健康檢查支持,本文也將使用這個庫,歡迎大家使用:

因為原倉庫已經 Archived 了,所以我才 Fork 了自己改一下,改動很小,不影響原來的穩定性。

Nuget:

Github:

求個star

關於支持 GPRC 健康檢查的好處:

偷個懶,不翻譯了,摘自GRPC官方文檔

五. 註冊GRPC服務與健康檢查

基於前文()的Demo

1.為服務端項目安裝 NConsul.AspNetCore ( )

這裏面對 AspNetCore 做了適配,使用簡單。

2.在 Startup 的 ConfigureServices方法內進行配置

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc();

    services.AddConsul("http://localhost:8500")
        .AddGRPCHealthCheck("localhost:5000")
        .RegisterService("grpctest","localhost",5000,new []{"xc/grpc/test"});
}

AddConsul 添加 Consul Server 地址。

AddGRPCHealthCheck 添加 GRPC 健康檢查,即健康檢查走的是 GRPC 協議,該值為 GRPC 服務的地址,不需要path不需要提供 http/https

RegisterService 註冊服務

到這步,還不能啟動運行,如果運行健康檢查是會失敗的。

3.編寫 Health Check 服務 **

對於 GRPC 的健康檢查,官方有標準的定義,新建一個 proto 文件,命名為 HealthCheck.proto

syntax = "proto3";

package grpc.health.v1;

message HealthCheckRequest {
    string service = 1;
}

message HealthCheckResponse {
    enum ServingStatus {
        UNKNOWN = 0;
        SERVING = 1;
        NOT_SERVING = 2;
    }
    ServingStatus status = 1;
}

service Health {
    rpc Check(HealthCheckRequest) returns (HealthCheckResponse);

    rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}

這裏面的內容不得更改,是官方標準,資料見後文

這裏編譯一下項目,以便自動生成代碼。

然後,添加一個服務的實現類 HealthCheckService

public class HealthCheckService:Health.HealthBase
{
    public override Task<HealthCheckResponse> Check(HealthCheckRequest request, ServerCallContext context)
    {
        //TODO:檢查邏輯
        return Task.FromResult(new HealthCheckResponse(){Status = HealthCheckResponse.Types.ServingStatus.Serving});
    }

    public override async Task Watch(HealthCheckRequest request, IServerStreamWriter<HealthCheckResponse> responseStream, ServerCallContext context)
    {
        //TODO:檢查邏輯
        await responseStream.WriteAsync(new HealthCheckResponse()
            {Status = HealthCheckResponse.Types.ServingStatus.Serving});
    }
}

示例代碼直接返回了檢查結果,實際使用中應該在這裏編寫檢查邏輯,然後根據情況返回相應的檢查結果。檢查結果有3種情況:

結果類型 說明
Unknown 未知狀態
Serving 正常
NotServing 異常,不能提供服務

最後別忘了註冊服務:

4.測試運行

啟動 GRPC 服務

然後訪問 http://localhost:8500/ui 訪問 Consul 控制台

可以看到服務成功註冊,並且健康檢查也是通過了的。通過控制台日誌,還可以看到健康檢查的請求:

六. 客戶端使用服務發現

客戶端項目安裝 Consul 組件,然後改造下代碼:

static async Task Main(string[] args)
{
    var serviceName = "grpctest";
    var consulClient = new ConsulClient(c => c.Address = new Uri("http://localhost:8500"));
    var services = await consulClient.Catalog.Service(serviceName);
    if (services.Response.Length == 0)
    {
        throw new Exception($"未發現服務 {serviceName}");
    }

    var service = services.Response[0];
    var address = $"http://{service.ServiceAddress}:{service.ServicePort}";

    Console.WriteLine($"獲取服務地址成功:{address}");

    //啟用通過http使用http2.0
    AppContext.SetSwitch(
        "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
    var channel = GrpcChannel.ForAddress(address);
    var catClient = new LuCat.LuCatClient(channel);
    var catReply = await catClient.SuckingCatAsync(new Empty());
    Console.WriteLine("調用擼貓服務:"+ catReply.Message);
    Console.ReadKey();
}

通過服務名稱獲取服務地址,然後來進行訪問。

運行測試:

可以看到,成功的從Consul獲取了我們的服務地址,然後調用。

六. 參考資料

  • gRPC in Asp.Net Core :

  • GPRC Health Check Doc:

  • 本文 Demo:

  • 本系列文章目錄:

  • NConsul:

  • by Edison Zhou

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

MySQL權限管理

目錄

設置用戶與權限

一個MySQL系統可能有許多用戶。為了安全起見,root用戶通常只用管理目的。對於每個需要使用該系統的用戶,應該為他們創建一個賬號和密碼。這些用戶名和密碼不必與MySQL之外的用戶名稱和密碼(例如,Linux或NT用戶名和密碼)相同。同樣的原則也適合於root用戶。對於系統用戶和MySQL用戶最好使用不同的密碼,這一點對root用戶尤其應該這樣。
為用戶設置密碼不是必須的,但是強烈建議為所有創建的用戶設定密碼。要建立一個Web數據庫,最好為每個網站應用程序建立一個用戶。你可能會問,“為什麼要這麼做呢?”—-答案在於權限。

用戶管理

我們的MySQL的用戶,都被記錄在了mysql數據庫的user表中,如下圖:

在MySQL數據庫中,一個完整的用戶賬號應該包含用戶名登錄主機,也就是說 用戶名@主機名才是一個完整的用戶賬號。

創建用戶基本語法:

CREATE USER 用戶名@主機名 IDENTIFIED BY '密碼';

刪除用戶基本語法:

DROP USER 用戶名@主機名;

修改用戶密碼:

- 修改自己的密碼
set password = password('新密碼');
- 修改別人的密碼(需要有該權限)
set password for 用戶名@主機名 = password('新密碼');

MySQL權限系統介紹

MySQL的最好特性之一是支持複雜的權限系統權限是對特定對象執行特定操作的權力,它與特定用戶相關。其概念非常類似於文件的權限。擋在MySQL中創建一個用戶時,就賦予了該用戶一定的權限,這些權限指定了用戶在本系統中可以做什麼和不可以做什麼。

最少權限原則

最少權限原則可以用來提高任何計算機系統的安全性。它是一個基本的、但又是非常重要的而且容易為我們忽略的原則。該原則包含如下內容:

一個用戶(或一個進程)應該擁有能夠執行分配給他的任務的最低級別的權限。

該原則同樣適用於MySQL,就像它應用於其他地方一樣。例如,要在網站上運行查詢,用戶並不需要root用戶所擁有的所有權限。因此,我們應該創建另一個用戶,這個用戶只有訪問我們剛剛建立的數據庫的必要權限。

創建用戶:GRANT命令

GRANT和REVOKE命令分別用來授予取消MySQL用戶的權限,這些權限分4個級別。它們分別是:

  • 全局
  • 數據庫

GRANT命令用來創建用戶並賦予他們權限。GRANT命令的常見形式是:

GRANT privileges [columns]
ON item
TO user_name [IDENTIFIED BY 'password']
[REQUIRE ssl_options]
[WITH [GRANT OPTION | limit_options]]

普通寫法可以簡略如下:

GRANT 權限列表 ON 數據庫.表 TO 用戶名@主機名 [IDENTIFIED BY '密碼'];

identified by可以省略,也可以寫出
(1)如果寫了,用戶存在,就是修改用戶的密碼
(2)如果寫了,該用戶不存在,就是創建用戶,同時指定密碼

方括號內的子句是可選的。在本語法中,出現了許多佔位符。第一個佔位符是 privileges,應該是由逗號分開的一組權限。MySQL已經有一組已定義的權限。它們在下一節詳細介紹。
佔位符 columns是可選的。可以用它對每一個列指定權限。也可以使用單列的名稱或者逗號分開的一組列的名稱。

佔位符 item是新權限的所應用於的數據庫或表。可以將項目指定為“.”,而將權限應用於所有數據庫。這叫做賦予全局權限。如果沒有使用在特定的數據庫,也可以通過只指定*完成賦予全局權限。更常見的是,以dbname.*的形式指定數據庫中所有的表,以dbname.tablename的形式指定單個表,或者通過指定tablename來指定特定的列。這些分別表示其他3個可以利用的權限:數據庫、表、列。如果在輸入命令的時候正在使用一個數據庫,tablename本身將被解釋成當前數據庫中的一個表。

user_name應該是用戶登錄MySQL所使用的用戶名。請注意,它不必與登錄系統時所使用的用戶名相同。MySQL中的user_name也可以包含一個主機名。可以用它來區分如itbsl(解釋成itbsl@localhost)和itbsl@somewhere.com。這是非常有用的一項能力,因為來自不同域的用戶經常可能使用同一個名字。這也提高了安全性能,因為可以指定用戶從什麼地方連接到本機,甚至可以指定它們在特定的地方可以訪問那些表和數據庫。

password應該是用戶登錄時使用的密碼。常見的密碼選擇規則在這裏都適用。我們後面將更詳細地講述安全問題,但是密碼應該不容易被猜出來。這意味着,密碼不應該是一個字段單詞或與用戶名相同。理想的密碼應該是大、小寫字母和非字母的組合。

REQUIRE子句允許指定用戶是否必須通過加密套接字連接,或者指定其它的SSL選項,關於SSL到MySQL連接的更多信息,請參閱MySQL手冊。

WITH GRANT OPTION選項,如果指定,表示允許指定的用戶向別人授予自己所擁有的權限。
我們也可以指定如下所示的WITH子句:
MAX_QUERIES_PER_HOUR n
或者
MAX_UPDATES_PER_HOUR n
或者
MAX_CONNECTIONS_PER_HOUR n
這些子句可以指定每個用戶每小時執行的查詢、更新和鏈接的數量。在共享的系統上限制單個用戶的負載時,這些子句是非常有用的。
權限存儲在名為mysql的數據庫中的5個系統中。這些表分別是mysql.user、mysql.db、mysql.host、mysql.tables_priv和mysql.columns_priv。作為GRANT命令的替代,可以直接修改這些表。

權限的類型和級別

MySQL中存在3個基本類型的權限:適用於賦予一般用戶的權限、適用於賦予管理員的權限和幾個特定的權限。任何用戶都可以被賦予這3類權限,但是根據最少權限原則,最好嚴格限定只將管理員類型的權限賦予管理員。

我們應該只賦予用戶訪問他們必須使用的數據庫和表的權限。而不應該將訪問mysql的權限賦予不是管理員的人。mysql數據庫是所有用戶名、密碼等信息存儲的地方。

常規用戶的權限直接與特定的SQL命令類型以及用戶是否被允許運行它們相關。下錶所示的是基本用戶權限。“應用於”列下面的對象給出了該類型權限可以授予的對象。

用戶的權限

權限 應用於 描述
SELECT 表、列 允許用戶從表中查詢行(記錄)
INSERT 表、列 允許用戶在表中插入新行
UPDATE 表、列 允許用戶修改現存表裡行中的值
DELETE 允許用戶刪除現存表的行
INDEX 允許用戶創建和拖動特定表索引
ALTER 允許用戶改變現存表的結構,例如,可添加列、重命名列或表、修改列的數據類型
CREATE 數據庫、表 允許用戶創建新數據庫或表。如果在GRANT中指定了一個特定的數據庫或表,它們只能夠創建該數據庫或表,即它們必須首先刪除(drop)它
DROP 數據庫、表 允許用戶拖動(刪除)數據庫或表

從系統的安全性方面考慮,適於常規用戶的權限大多數是相對無害的。ALTER權限通過重命名表可能會影響權限系統,但是大多數用戶需要它。安全性常常是可用性與保險性的折中。遇到ALTER的時候,應當做出自己的選擇,但是通常還是會將這個權限授予用戶。

下面的表給出了適用於管理員用戶使用的權限。
可以將這些權限授予非管理員用戶,這樣做的時候要非常小心。
FILE權限有些不同,它對普通用戶非常有用,因為它可以將數據從文件載入數據庫,從而可以節省許多時間,否則,每次將數據輸入數據庫都需要重新輸入,這很浪費時間。
然而,文件載入可以用來載入MySQL可識別的任何文件,包括屬於其他用戶的數據庫和潛在的密碼文件。授予該權限的時候需要小心,或者自己為用戶載入數據。

管理員權限

權限 描述
CREATE TEMPORARY TABLES 允許管理員在CREATE TABLE語句中使用TEMPORARY關鍵字
FILE 允許將數據從文件讀入表,或從表讀入文件
LOCK TABLES 允許使用LOCK TABLES語句
PROCESS 允許管理員查看屬於所有用戶的服務器進程
RELOAD 允許管理員重新載入授權表、清空授權、主機、日誌和表
REPLICATION CLIENT 允許管理員重新載入授權表、和從機(Slave)上使用SHOW STATUS
REPLICATION SLAVE 允許複製從服務器連接到主服務器
SHOW DATABASES 允許使用SHOW DATABASES語句查看所有數據庫列表。沒有這個權限,用戶只能看到他們能夠看到的數據庫
SHUTDOWN 允許管理員關閉MySQL服務器
SUPER 允許管理員關閉屬於任何用戶的的線程

特別的權限

權限 描述
ALL 授予上面兩個表列表的所有權限。也可以將ALL寫成ALL PRIVILEGES
USAGE 不授予權限。這創建一個用戶並允許他登錄,但是不允許進行任何操作。通常會在以後授予該用戶更多的權限

REVOKE命令

與GRANT相反的命令是REVOKE。它用來從一個用戶收回權限。在語法上與GRANT非常相似:

REVOKE privileges [(columns)]
ON item
FROM user_name

中文翻譯:

REVOKE 權限列表 ON 數據庫.對象 FROM 用戶名@主機名;

如果已經給出了WITH GRANT OPTION子句,可以按如下方式撤銷它(以及所有其他權限):

REVOKE ALL PRIVILEGES, GRANT
FROM user_name

權限立即生效

當我們修改用戶權限之後,如果想不重啟立即生效,需要執行以下flush privileges,這樣能快速刷新權限

FULSH PRIVILEGES;

使用GRANT和REVOKE的例子

要創建一個管理員,可以輸入如下所示的命令:

grant all on * to fred identified by 'mnb123' with grant option;

以上命令授予了用戶名為fred、密碼為mnb123的用戶使用所有數據庫的所有權限,並允許他向其他人授予這些權限。

如果不希望用戶在系統中存在,可以按如下方式撤銷:

revoke all privileges, grant from red;

現在,我們可以按如下方式創建一個沒有任何權限的常規用戶:

grant usage on books.* to sally identified by 'magic123';

在與sally交談后,我們對她需要進行的操作有了一定的了解,因此按如下方式可以授予她適當的權限:

grant select, insert, update, delete, index, alter, create, drop on books.* to sally;

請注意,要完成這些,並不需要指定sally密碼。

如果我們認為sally權限過高,可能會決定按如下方式減少一些權限:

revoke alter, create, drop on books.* from sally;

後來,當她不再需要使用數據庫時,可以按如下方式撤銷所有的權限:

revoke all on books.* from sally;

參考資料:

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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