分類: 3C資訊

  • js數組方法大全(下)

    js數組方法大全(下)

    # js數組方法大全(下)

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

    本期分享

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

    上期分享

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

    forEach() —>遍歷

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

    map() —>映射

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

    filter() —>過濾

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

    every() —>檢測

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

    some() —>檢測

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

    reduce() —>簡化

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

    reduceRight() —>簡化

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

    indexOf() —>搜索

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

    lastIndexOf() —>搜索

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

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

    【其他文章推薦】

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

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

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

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

  • Ansible之playbook拓展

      一、handlers和notify結合使用觸發條件

      handlers同tasks是屬同級,相當於一個特殊任務列表,這些任務同前文說的tasks里的任務沒有本質的不同,用於當關注的資源發生變化時,才會採取一定的操作。notify此action可用於在每一個play的最後被觸發,這樣可避免多次有改變發生時都執行指定的操作,僅在所有的變化發生完成后一次性地執行指定操作,在notify中列出的操作稱為handler,換句話說當所關注的資源發生變化時notify將調用handlers中定義的操作。其中notify所在任務就是被監控的任務資源變化的任務,notify可以調用多個handlers定義的操作,一個handlers里可以定義很多任務。

    ---
    - hosts: websers
      remote_user: root
    
      tasks:
        - name: create apache group
          group: name=apache gid=80 system=yes
        - name: create apache user
          user: name=apache uid=80 group=apache system=yes shell=/sbin/nologin home=/var/www/html 
        - name: install httpd
          yum: name=httpd
        - name: copy config file
          copy: src=/tmp/httpd.conf dest=/etc/httpd/conf/
          notify: restart httpd service
    
        - name: start httpd service
          service: name=httpd state=started enabled=yes
    
      handlers:
        - name: restart httpd service
          service: name=httpd state=restarted   
    

      說明:notify后指定的名稱必須要和handlers里的任務名稱相同,如不同handlers所定義的任務將不會執行,相當於沒有notify調用handlers里的任務。

      在某些情況下,我們可能同時需要調用多個handlers,或者需要使用handlers其他handlers,ansible可以很簡單的實現這些功能,如下所示

      1)調用多個handlers

    ---
    - hosts: websers
      remote_user: root
    
      tasks:
        - name: create apache group
          group: name=apache gid=80 system=yes
        - name: create apache user
          user: name=apache uid=80 group=apache system=yes shell=/sbin/nologin home=/var/www/html 
        - name: install httpd
          yum: name=httpd
        - name: copy config file
          copy: src=/tmp/httpd.conf dest=/etc/httpd/conf/
          notify: 
            - restart httpd service
            - check httpd process
    
        - name: start httpd service
          service: name=httpd state=started enabled=yes
    
      handlers:
        - name: restart httpd service
          service: name=httpd state=restarted
        - name: check httpd process                                                                                      
          shell: /usr/bin/killall -0 httpd &> /tmp/httpd.log
    

      說明:調用多個handlers我們需要在notify中寫成列表的形式,同樣我們被觸發的任務名稱需要同handlers里的被調用的任務名稱完全相同

      2)handlers調用handlers

    ---
    - hosts: websers
      remote_user: root
    
      tasks:
        - name: create apache group
          group: name=apache gid=80 system=yes
        - name: create apache user
          user: name=apache uid=80 group=apache system=yes shell=/sbin/nologin home=/var/www/html 
        - name: install httpd
          yum: name=httpd
        - name: copy config file
          copy: src=/tmp/httpd.conf dest=/etc/httpd/conf/
          notify: restart httpd service
    
        - name: start httpd service
          service: name=httpd state=started enabled=yes
    
      handlers:
        - name: restart httpd service
          service: name=httpd state=restarted
          notify: check httpd process                                                                                    
        - name: check httpd process
          shell: /usr/bin/killall -0 httpd &> /tmp/httpd.log
    

      說明:handlers調用handlers,則直接在handlers中使用notify選項就可以。

    在使用handlers我們需要注意一下幾點:

      1)handlers只有在其所在任務被執行時才會被運行,handlers定義的任務它不會像task任務那樣,自動會從上至下依次執行,它只會被notify所在的任務發生狀態改變時才會觸發handlers 的任務執行,如果一個任務中定義了notify調用handlers,但由於條件的判斷等原因,該任務尚未執行,那麼notify調用的handlers同樣也不會執行。

      2)handlers只會在play的末尾運行一次;如果想要在一個playbook的中間運行handlers,則需要使用meta模塊來實現,如:-mate: flush_handlers

      二、playbook中變量的使用

    ansible中變量的命名規範同其他語言或系統中變量命名規則非常類似。變量名以英文大小寫字母開頭,中間可以包含下劃線和数字,ansible變量的來源有很多,具體有以下幾點:

      1)ansible setup模塊,這個模塊可以從遠程主機上獲取很多遠程主機的基本信息,它所返回的所有變量都可以直接調用,有關setup說明請參考本人博客

      2)在/etc/ansible/hosts中定義,此文件是ansible執行名時默認加載的主機清單文件,在裏面除了可定義我們要管理的主機外,我們還可以定義針對單個主機定義單獨的變量,我們把針對單獨某一台主機定義的變量叫做普通變量(也可叫做主機變量);還有一種變量它不是針對單獨一個主機,它針對某一個組裡的所有主機,我們把這種變量叫做公共組變量。主機清單中定義的變量優先級是普通變量高於公共變量。

        2.1)主機變量,可以在主機清單中定義主機時為其添加主機變量以便於在playbook中使用,如下所示

    [websers]
    192.168.0.128 http_port=80 maxRequestsPerChild=808
    192.168.0.218 http_port=81 maxRequestsPerChild=909
    

        2.2)主機組變量,組變量是指定賦予給指定組內所有主機上的在playbook中可使用的變量,如下所示

    [websers]
    192.168.0.128 http_port=80 
    192.168.0.218 http_port=81 
    [websers:vars]
    maxRequestsPerChild=909

      3)通過命令行指定變量(-e指定變量賦值,可以說多個但需要用引號引起或者一個變量用一個-e指定賦值),這種在命令行指定的優先級最高。如下所示

    ansible-playbook -e 'package_name1=httpd package_name2=nginx' test_vars.yml

      4)在playbook中定義變量,最常見的定義變量的方法是使用vars代碼塊,如下所示

    ---
    - hosts: websers
      remote_user: root
      vars:
        - abc: xxx 
        - bcd: aaa  
    

      5)在獨立的變量yml文件中定義,在playbook中使用vars_files代碼塊引用其變量文件,如下所示

    [root@test ~]#cat vars.yml 
    ---
    package_name1: vsftpd
    package_name2: nginx
    [root@test ~]#cat test_vars.yml 
    ---
    - hosts: websers
      remote_user: root
      vars_files:
        - vars.yml
      tasks:
        - name: install package1
          yum: name={{ package_name1 }}
        - name: install package2
          yum: name={{ package_name2 }}
    [root@test ~]#

      6)在role中定義,這個後續說到角色在做解釋

      變量的調用方式:第一種在playbook中使用變量需要用“{{}}”將變量括起來,表示括號里的內容是一個變量,有時用“{{  variable_name }}”才生效;第二種是ansible-playbook -e 選項指定其變量,ansible-playbook -e “hosts=www user=xxxx” test.yml

      在主機清單中定義變量的方法雖然簡單直觀,但是當所需要定義的變量有很多時,並且被多台主機使用時,這種方法顯得非常麻煩,事實上ansible的官方手冊中也不建議我們把變量直接定義到hosts文件中;在執行ansible命令時,ansible會默認會從/etc/ansible/host_vars/和/etc/ansible/group_vars/兩個目錄下讀取變量定義文件,如果/etc/ansible/下沒有以上這兩個目錄,我們可以手動創建,並且可以在這兩個目錄下創建與hosts文件中的主機名或主機組同名的文件來定義變量。比如我們要給192.168.0.218 這個主機定義個變量文件,我們可以在/etc/ansible/host_vars/目錄下創建一個192.168.0.218的空白文件,然後在文件中以ymal語法來定義所需變量即可。如下所示

    [root@test ~]#tail -6 /etc/ansible/hosts 
    ## db-[99:101]-node.example.com
    [websers]
    192.168.0.128 
    192.168.0.218 
    [appsers]
    192.168.0.217
    [root@test ~]#cat /etc/ansible/host_vars/192.168.0.218 
    ---
    file1: abc
    file2: bcd
    [root@test ~]#cat test.yml 
    ---
    - hosts: 192.168.0.218
      remote_user: root
      
      tasks:
        - name: touch file1
          file: name={{ file1 }} state=touch
        - name: toch file2
          file: name={{ file2 }} state=touch
    [root@test ~]#ansible-playbook test.yml 
    
    PLAY [192.168.0.218] ************************************************************************************************
    
    TASK [Gathering Facts] **********************************************************************************************
    ok: [192.168.0.218]
    
    TASK [touch file1] **************************************************************************************************
    changed: [192.168.0.218]
    
    TASK [toch file2] ***************************************************************************************************
    changed: [192.168.0.218]
    
    PLAY RECAP **********************************************************************************************************
    192.168.0.218              : ok=3    changed=2    unreachable=0    failed=0   
    
    [root@test ~]#ansible 192.168.0.218 -m shell -a 'ls -l /root'
    192.168.0.218 | SUCCESS | rc=0 >>
    總用量 12
    -rw-r--r--. 1 root   root    0 11月 17 16:49 abc
    -rw-r--r--. 1 root   root    0 11月 17 16:49 bcd
    drwxr-xr-x. 2 qiuhom root 4096 11月 11 19:18 scripts
    drwxr-xr-x. 3 qiuhom root 4096 11月 11 19:28 test
    -rw-r--r--. 1 root   root   57 11月 13 19:15 test_cron_file
    
    [root@test ~]#
    

      說明:可看到我們定義在/etc/ansible/host_vars/下的主機變量文件中的變量生效了。

    同理,我們要想針對某個組的主機定義一些變量,我們只需要在/etc/ansible/group_vars/目錄下創建與主機清單中的主機組同名的文件即可。

      三、使用高階變量

      對於普通變量,例如由ansible命令行設定的,hosts文件中定義的以及playbook中定義的和變量文件中定義的,這些變量都被稱為普通變量或者叫簡單變量,我們可以在playbook中直接用雙大括號加變量名來讀取變量內容;除此以外ansible還有數組變量或者叫做列表變量,如下所示:

    [root@test ~]#cat vars.yml 
    ---
    packages_list:
      - vsftpd
      - nginx
    [root@test ~]#
    

      列表定義完成后我們要使用其中的變量可以列表名加下標的方式去訪問,有點類似shell腳本里的數組的使用,如下所示

    [root@test ~]#cat test.yml 
    ---
    - hosts: 192.168.0.218
      remote_user: root
      
      vars_files:
        - vars.yml
      tasks:
        - name: touch file
          file: name={{ packages_list[0] }} state=touch
        - name: mkdir dir
          file: name={{ packages_list[1] }} state=directory
    [root@test ~]#
    

      說明:我們要使用列表中的第一個元素變量,我們可以寫成vars_list[0],使用第二個變量則下標就是1,依此類推

    [root@test ~]#ansible *218 -m shell -a 'ls -l /root'
    192.168.0.218 | SUCCESS | rc=0 >>
    總用量 12
    -rw-r--r--. 1 root   root    0 11月 17 16:49 abc
    -rw-r--r--. 1 root   root    0 11月 17 16:49 bcd
    drwxr-xr-x. 2 qiuhom root 4096 11月 11 19:18 scripts
    drwxr-xr-x. 3 qiuhom root 4096 11月 11 19:28 test
    -rw-r--r--. 1 root   root   57 11月 13 19:15 test_cron_file
    
    [root@test ~]#ansible-playbook test.yml 
    
    PLAY [192.168.0.218] ************************************************************************************************
    
    TASK [Gathering Facts] **********************************************************************************************
    ok: [192.168.0.218]
    
    TASK [touch file] ***************************************************************************************************
    changed: [192.168.0.218]
    
    TASK [mkdir dir] ****************************************************************************************************
    changed: [192.168.0.218]
    
    PLAY RECAP **********************************************************************************************************
    192.168.0.218              : ok=3    changed=2    unreachable=0    failed=0   
    
    [root@test ~]#ansible *218 -m shell -a 'ls -l /root'
    192.168.0.218 | SUCCESS | rc=0 >>
    總用量 16
    -rw-r--r--. 1 root   root    0 11月 17 16:49 abc
    -rw-r--r--. 1 root   root    0 11月 17 16:49 bcd
    drwxr-xr-x. 2 root   root 4096 11月 17 17:23 nginx
    drwxr-xr-x. 2 qiuhom root 4096 11月 11 19:18 scripts
    drwxr-xr-x. 3 qiuhom root 4096 11月 11 19:28 test
    -rw-r--r--. 1 root   root   57 11月 13 19:15 test_cron_file
    -rw-r--r--. 1 root   root    0 11月 17 17:23 vsftpd
    
    [root@test ~]#
    

      說明:可看到我們創建的文件和目錄在目標主機已經生成

    上面的用法是典型的python列表的用法,在python中讀取列表中的元素就是用下標的表示來讀取相應的元素的值。接下我們將介紹另外一種更為複雜的變量,它類似python中的字典概念,但比字典的維度要高,更像是二維字典。ansible內置變量ansible_eth0就是這樣一種,它用來保存遠端主機上面eth0接口的信息,包括ip地址和子網掩碼等。如下所示

    [root@test ~]#cat test.yml     
    ---
    - hosts: 192.168.0.218
      remote_user: root
      
      tasks:
        - debug: var=ansible_eth0 
    [root@test ~]#ansible-playbook test.yml 
    
    PLAY [192.168.0.218] ************************************************************************************************
    
    TASK [Gathering Facts] **********************************************************************************************
    ok: [192.168.0.218]
    
    TASK [debug] ********************************************************************************************************
    ok: [192.168.0.218] => {
        "ansible_eth0": {
            "active": true, 
            "device": "eth0", 
            "features": {
                "fcoe_mtu": "off [fixed]", 
                "generic_receive_offload": "on", 
                "generic_segmentation_offload": "on", 
                "highdma": "off [fixed]", 
                "large_receive_offload": "off [fixed]", 
                "loopback": "off [fixed]", 
                "netns_local": "off [fixed]", 
                "ntuple_filters": "off [fixed]", 
                "receive_hashing": "off [fixed]", 
                "rx_checksumming": "on", 
                "rx_vlan_filter": "on [fixed]", 
                "rx_vlan_offload": "on [fixed]", 
                "scatter_gather": "on", 
                "tcp_segmentation_offload": "on", 
                "tx_checksum_fcoe_crc": "off [fixed]", 
                "tx_checksum_ip_generic": "on", 
                "tx_checksum_ipv4": "off", 
                "tx_checksum_ipv6": "off", 
                "tx_checksum_sctp": "off [fixed]", 
                "tx_checksum_unneeded": "off", 
                "tx_checksumming": "on", 
                "tx_fcoe_segmentation": "off [fixed]", 
                "tx_gre_segmentation": "off [fixed]", 
                "tx_gso_robust": "off [fixed]", 
                "tx_lockless": "off [fixed]", 
                "tx_scatter_gather": "on", 
                "tx_scatter_gather_fraglist": "off [fixed]", 
                "tx_tcp6_segmentation": "off", 
                "tx_tcp_ecn_segmentation": "off", 
                "tx_tcp_segmentation": "on", 
                "tx_udp_tnl_segmentation": "off [fixed]", 
                "tx_vlan_offload": "on [fixed]", 
                "udp_fragmentation_offload": "off [fixed]", 
                "vlan_challenged": "off [fixed]"
            }, 
            "hw_timestamp_filters": [], 
            "ipv4": {
                "address": "192.168.0.218", 
                "broadcast": "192.168.0.255", 
                "netmask": "255.255.255.0", 
                "network": "192.168.0.0"
            }, 
            "ipv6": [
                {
                    "address": "fe80::20c:29ff:fee8:f67b", 
                    "prefix": "64", 
                    "scope": "link"
                }
            ], 
            "macaddress": "00:0c:29:e8:f6:7b", 
            "module": "e1000", 
            "mtu": 1500, 
            "pciid": "0000:02:01.0", 
            "promisc": false, 
            "speed": 1000, 
            "timestamping": [
                "rx_software", 
                "software"
            ], 
            "type": "ether"
        }
    }
    
    PLAY RECAP **********************************************************************************************************
    192.168.0.218              : ok=2    changed=0    unreachable=0    failed=0   
    
    [root@test ~]#
    

      說明:以上playbook就實現了對ansible_eth0這個變量進行調試並打印,可以看到ansible_eth0是一個相對比較複雜的變量,裡面包含了字典,列表混合一起的一個大字典。

    我們可以看到ansible_eht0裡面包含了很多內容,我們要想讀取其中的IPV4地址,我們可以採用“.”或者下標的方式去訪問,如下所示

    [root@test ~]#cat test.yml 
    ---
    - hosts: 192.168.0.218
      remote_user: root
      
      tasks:
        - name: print ipv4  
          shell: echo {{ ansible_eth0["ipv4"]["address"] }} 
        - name: print mac
          shell: echo  {{ ansible_eth0.macaddress }}
    [root@test ~]#ansible-playbook test.yml -v
    Using /etc/ansible/ansible.cfg as config file
    
    PLAY [192.168.0.218] ************************************************************************************************
    
    TASK [Gathering Facts] **********************************************************************************************
    ok: [192.168.0.218]
    
    TASK [print ipv4] ***************************************************************************************************
    changed: [192.168.0.218] => {"changed": true, "cmd": "echo 192.168.0.218", "delta": "0:00:00.001680", "end": "2019-11-17 18:30:21.926368", "rc": 0, "start": "2019-11-17 18:30:21.924688", "stderr": "", "stderr_lines": [], "stdout": "192.168.0.218", "stdout_lines": ["192.168.0.218"]}
    
    TASK [print mac] ****************************************************************************************************
    changed: [192.168.0.218] => {"changed": true, "cmd": "echo 00:0c:29:e8:f6:7b", "delta": "0:00:00.001746", "end": "2019-11-17 18:30:22.650541", "rc": 0, "start": "2019-11-17 18:30:22.648795", "stderr": "", "stderr_lines": [], "stdout": "00:0c:29:e8:f6:7b", "stdout_lines": ["00:0c:29:e8:f6:7b"]}
    
    PLAY RECAP **********************************************************************************************************
    192.168.0.218              : ok=3    changed=2    unreachable=0    failed=0   
    
    [root@test ~]#

      說明:由此可以看出ansible多級變量的調用,使用中括號和點號都是可以的

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

    【其他文章推薦】

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

    ※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

    ※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

  • 【algo&ds】2.線性表

    【algo&ds】2.線性表

    1.線性表

    線性表(英語:Linear List)是由n(n≥0)個元素()a[0],a[1],a[2]…,a[n-1]組成的。

    其中:

    • 數據元素的個數n定義為表的長度 = “list”.length() (”list”.length() = 0(表裡沒有一個元素)時稱為空表)
    • 將非空的線性表(n>=1)記作:(a[0],a[1],a[2],…,a[n-1])
    • 數據元素a[i](0≤i≤n-1)只是個抽象符號,其具體含義在不同情況下可以不同

    一個數據元素可以由若干個數據項組成。數據元素稱為記錄,含有大量記錄的線性表又稱為文件。這種結構具有下列特點:存在一個唯一的沒有前驅的(頭)數據元素;存在一個唯一的沒有後繼的(尾)數據元素;此外,每一個數據元素均有一個直接前驅和一個直接後繼數據元素。

    2.線性表的存儲結構

    • 鏈表
      • 單鏈表
        • 動態單鏈表
        • 靜態單鏈表
      • 循環鏈表
        • 單循環鏈表
        • 雙循環鏈表
      • 靜態鏈表

    3.順序表

    利用數組的連續存儲空間順序存放線性表的各元素

    3.1結構體定義

    如果需要使用自定義的結構體來維護一個順序表,通常來講結構體的元素一般是一個固定大小的數組(可用長度足夠大),以及當前數組存放的元素個數,也即數組的長度

    typedef struct LNode *List;
    struct LNode {
        ElementType Data[MAXSIZE];
        int Last;//記錄順序表的最後一個元素的下標
    } ;
    struct LNode L;
    List PtrL;

    訪問結構體的成員

    • 訪問下標為 i 的元素:L.Data[i] 或 PtrL->Data[i]
    • 線性表的長度:L.Last+1 或 PtrL->Last+1
    • 指針變量PtrL還可以這樣訪問兩個屬性(*PtrL).Data[i](*PtrL).Last,不過這種訪問方式並不常用

    而一般來講,為了簡單,不會去維護這樣一個結構體,(因為一旦維護了這個結構體,就需要去封裝相應的函數,比如說常見的插入、刪除、查找等操作),而是直接類似下面這樣

    ElementType data[MaxSize];
    int length;

    定義一個足夠大的數組,然後定義一個對應關聯的變量來時刻維護數組的長度。

    這兩種定義方式沒有什麼區別,一種是把常用操作封裝好,方便調用,另一種則是需要時刻自己維護對應的屬性。因為順序表的結構足夠簡單,所以不定義結構體也是可以的。

    3.2順序表的常見操作

    為了方便,這一節內容記錄的都是在定義的結構體基礎上,封裝的常見操作。

    1.初始化

    List MakeEmpty( ) {
        List PtrL;
        PtrL = (List )malloc( sizeof(struct LNode) );
        PtrL->Last = -1;
        return PtrL;
    }

    初始化的順序表,長度為0,所以Last為-1

    2.查找

    int Find( ElementType X, List PtrL ) {
        int i = 0;
        while( i <= PtrL->Last && PtrL->Data[i]!= X )
            i++;
        if (i > PtrL->Last) return -1; /* 如果沒找到,返回-1 */
        else return i; /* 找到后返回的是存儲位置 */
    }

    查找操作比較簡單,從順序表的第一個元素(下標為0開始)開始遍歷。

    還有一種更加巧妙一點的實現方式,就是引入哨兵思想。

    int Find( ElementType X, List PtrL ) {
        PtrL->Data[0] = x;//順序表第一個元素就是哨兵,賦值為x
        int i = PtrL->Last;//從最後一個元素開始遍歷
        while( PtrL->Data[i]!= X )
            i--;
        return i;
    }

    這樣做的好處很明顯,少了邊界的判斷,可以優化時間複雜度,編碼也更加簡單。

    注意:這裏把下標為0的元素設置為哨兵,則要求順序表從下標為1開始存儲。而且,函數如果沒有找到,則一定返回i=0

    3.插入

    看圖示應該要注意,移動的方向是從后往前移,如果從前往後移,則Data[i]=Data[i+1]=…=Data[n],因為後面的元素都被前面移過來的元素給覆蓋了。

    void Insert( ElementType X, int i, List PtrL ) {
        int j;
        if ( PtrL->Last == MAXSIZE-1 ) { /* 表空間已滿,不能插入*/
            printf("表滿");
            return;
        }
        if ( i < 1 || i > PtrL->Last+2) { /*檢查插入位置的合法性*/
            printf("位置不合法");
            return;
        }
        for ( j = PtrL->Last; j >= i-1; j-- )
            PtrL->Data[j+1] = PtrL->Data[j]; /*將 ai~ an倒序向後移動*/
        PtrL->Data[i-1] = X; /*新元素插入*/
        PtrL->Last++; /*Last仍指向最後元素*/
        return;
    }

    為什麼這裏需要判斷順序表的空間是否已滿?

    因為這個數組,是在初始化之後就固定了數組可容納的元素個數MaxSize,一旦超出,則程序下標就會越界。C++提供了動態數組vector,可以很方便的支持動態擴展數組長度,而且基本的插入刪除等操作都封裝好了,可以很方便的使用。

    4.刪除

    同樣的,需要注意元素移動的方向,如果從后往前移,則最後面的元素會一直覆蓋到Data[i]。

    void Delete( int i, List PtrL ) {
        int j;
        if( i < 1 || i > PtrL->Last+1 ) { /*檢查空表及刪除位置的合法性*/
            printf (“不存在第%d個元素”, i );
            return ;
        }
        for ( j = i; j <= PtrL->Last; j++ )
            PtrL->Data[j-1] = PtrL->Data[j]; /*將 ai+1~ an順序向前移動*/
        PtrL->Last--; /*Last仍指向最後元素*/
        return;
    }

    5.排序

    因為排序算法比較多,本文不展開講解,可以參考以下博文,內容包括了常見的十大排序算法的算法分析,時間複雜度和空間複雜度分析以及c實現和動圖圖解。

    4.鏈表

    不要求邏輯上相鄰的兩個元素物理上也相鄰;通過“鏈”建立起數據元素之間的邏輯關係。插入、刪除不需要移動數據元素,只需要修改“鏈”。

    4.1單鏈表

    typedef struct LNode *List;
    struct LNode {
        ElementType Data;
        List Next;
    };
    struct Lnode L;
    List PtrL;
    1.求表長
    int Length ( List PtrL ) {
        List p = PtrL; /* p指向表的第一個結點*/
        int j = 0;
        while ( p ) {
            p = p->Next;
            j++; /* 當前p指向的是第 j 個結點*/
        }
        return j;
    }

    時間複雜度O(n)

    2.查找

    按序查找

    List FindKth( int K, List PtrL ) {
        List p = PtrL;
        int i = 1;
        while (p !=NULL && i < K ) {
            p = p->Next;
            i++;
        }
        if ( i == K ) return p;
        /* 找到第K個,返回指針 */
        else return NULL;
        /* 否則返回空 */
    }

    時間複雜度O(n)

    按值查找

    List Find( ElementType X, List PtrL ) {
        List p = PtrL;
        while ( p!=NULL && p->Data != X )
            p = p->Next;
        return p;
    }

    時間複雜度O(n)

    3.插入

    (1)先構造一個新結點,用s指向;
    (2)再找到鏈表的第 i-1個結點,用p指向;
    (3)然後修改指針,插入結點 ( p之後插入新結點是 s)

    List Insert( ElementType X, int i, List PtrL ) {
        List p, s;
        if ( i == 1 ) { /* 新結點插入在表頭 */
            s = (List)malloc(sizeof(struct LNode)); /*申請、填裝結點*/
            s->Data = X;
            s->Next = PtrL;
            return s; /*返回新表頭指針*/
        }
        p = FindKth( i-1, PtrL ); /* 查找第i-1個結點 */
        if ( p == NULL ) { /* 第i-1個不存在,不能插入 */
            printf("參數i錯");
            return NULL;
        } else {
            s = (List)malloc(sizeof(struct LNode)); /*申請、填裝結點*/
            s->Data = X;
            s->Next = p->Next; /*新結點插入在第i-1個結點的後面*/
            p->Next = s;
            return PtrL;
        }
    }
    4.刪除

    (1)先找到鏈表的第 i-1個結點,用p指向;
    (2)再用指針s指向要被刪除的結點(p的下一個結點);
    (3)然後修改指針,刪除s所指結點;
    (4)最後釋放s所指結點的空間。

    List Delete( int i, List PtrL ) {
        List p, s;
        if ( i == 1 ) { /* 若要刪除的是表的第一個結點 */
            s = PtrL; /*s指向第1個結點*/
            if (PtrL!=NULL) PtrL = PtrL->Next; /*從鏈表中刪除*/
            else return NULL;
            free(s); /*釋放被刪除結點 */
            return PtrL;
        }
        p = FindKth( i-1, PtrL ); /*查找第i-1個結點*/
        if ( p == NULL ) {
            printf("第%d個結點不存在", i-1);
            return NULL;
        } else if ( p->Next == NULL ) {
            printf("第%d個結點不存在", i);
            return NULL;
        } else {
            s = p->Next; /*s指向第i個結點*/
            p->Next = s->Next; /*從鏈表中刪除*/
            free(s); /*釋放被刪除結點 */
            return PtrL;
        }
    }

    4.2雙鏈表

    雙向鏈表,又稱為雙鏈表,是的一種,它的每個數據結點中都有兩個,分別指向直接後繼和直接前驅。所以,從雙向鏈表中的任意一個結點開始,都可以很方便地訪問它的前驅結點和後繼結點。

    typedef struct DuLNode {
        ElemType data;
        struct DuLNode *prior, *next;
    } DuLNode, *DuLinkList;

    4.3循環鏈表

    4.3.1單循環鏈表

    存儲結構和單鏈表相同。

    typedef struct LNode {
        ElemType data;
        struct LNode *next;
    } LNode, *LinkList;
    
    // 設立尾指針的單循環鏈表的12個基本操作
    void InitList(LinkList *L) { // 操作結果:構造一個空的線性表L
        *L = (LinkList)malloc(sizeof(struct LNode)); // 產生頭結點,並使L指向此頭結點
        if (!*L) // 存儲分配失敗
            exit(OVERFLOW);
        (*L)->next = *L; // 指針域指向頭結點
    }
    
    void DestroyList(LinkList *L) { // 操作結果:銷毀線性表L
        LinkList q, p = (*L)->next; // p指向頭結點
        while (p != *L) { // 沒到表尾
            q = p->next;
            free(p);
            p = q;
        }
        free(*L);
        *L = NULL;
    }
    
    void ClearList(LinkList *L) /* 改變L */ { // 初始條件:線性表L已存在。操作結果:將L重置為空表
        LinkList p, q;
        *L = (*L)->next; // L指向頭結點
        p = (*L)->next; // p指向第一個結點
        while (p != *L) { // 沒到表尾
            q = p->next;
            free(p);
            p = q;
        }
        (*L)->next = *L; // 頭結點指針域指向自身
    }
    
    Status ListEmpty(LinkList L) { // 初始條件:線性表L已存在。操作結果:若L為空表,則返回TRUE,否則返回FALSE
        if (L->next == L) // 空
            return TRUE;
        else
            return FALSE;
    }
    
    int ListLength(LinkList L) { // 初始條件:L已存在。操作結果:返回L中數據元素個數
        int i = 0;
        LinkList p = L->next; // p指向頭結點
        while (p != L) { // 沒到表尾
            i++;
            p = p->next;
        }
        return i;
    }
    
    Status GetElem(LinkList L, int i, ElemType *e) { // 當第i個元素存在時,其值賦給e並返回OK,否則返回ERROR
        int j = 1; // 初始化,j為計數器
        LinkList p = L->next->next; // p指向第一個結點
        if (i <= 0 || i > ListLength(L)) // 第i個元素不存在
            return ERROR;
        while (j < i) { // 順指針向後查找,直到p指向第i個元素
            p = p->next;
            j++;
        }
        *e = p->data; // 取第i個元素
        return OK;
    }
    
    int LocateElem(LinkList L, ElemType e, Status(*compare)(ElemType, ElemType)) { // 初始條件:線性表L已存在,compare()是數據元素判定函數
        // 操作結果:返回L中第1個與e滿足關係compare()的數據元素的位序。
        //           若這樣的數據元素不存在,則返回值為0
        int i = 0;
        LinkList p = L->next->next; // p指向第一個結點
        while (p != L->next) {
            i++;
            if (compare(p->data, e)) // 滿足關係
                return i;
            p = p->next;
        }
        return 0;
    }
    
    Status PriorElem(LinkList L, ElemType cur_e, ElemType *pre_e) { // 初始條件:線性表L已存在
        // 操作結果:若cur_e是L的數據元素,且不是第一個,則用pre_e返回它的前驅,
        //           否則操作失敗,pre_e無定義
        LinkList q, p = L->next->next; // p指向第一個結點
        q = p->next;
        while (q != L->next) { // p沒到表尾
            if (q->data == cur_e) {
                *pre_e = p->data;
                return TRUE;
            }
            p = q;
            q = q->next;
        }
        return FALSE; // 操作失敗
    }
    
    Status NextElem(LinkList L, ElemType cur_e, ElemType *next_e) { // 初始條件:線性表L已存在
        // 操作結果:若cur_e是L的數據元素,且不是最後一個,則用next_e返回它的後繼,
        //           否則操作失敗,next_e無定義
        LinkList p = L->next->next; // p指向第一個結點
        while (p != L) { // p沒到表尾
            if (p->data == cur_e) {
                *next_e = p->next->data;
                return TRUE;
            }
            p = p->next;
        }
        return FALSE; // 操作失敗
    }
    
    Status ListInsert(LinkList *L, int i, ElemType e) /* 改變L */ { // 在L的第i個位置之前插入元素e
        LinkList p = (*L)->next, s; // p指向頭結點
        int j = 0;
        if (i <= 0 || i > ListLength(*L) + 1) // 無法在第i個元素之前插入
            return ERROR;
        while (j < i - 1) { // 尋找第i-1個結點
            p = p->next;
            j++;
        }
        s = (LinkList)malloc(sizeof(struct LNode)); // 生成新結點
        s->data = e; // 插入L中
        s->next = p->next;
        p->next = s;
        if (p == *L) // 改變尾結點
            *L = s;
        return OK;
    }
    
    Status ListDelete(LinkList *L, int i, ElemType *e) /* 改變L */ { // 刪除L的第i個元素,並由e返回其值
        LinkList p = (*L)->next, q; // p指向頭結點
        int j = 0;
        if (i <= 0 || i > ListLength(*L)) // 第i個元素不存在
            return ERROR;
        while (j < i - 1) { // 尋找第i-1個結點
            p = p->next;
            j++;
        }
        q = p->next; // q指向待刪除結點
        p->next = q->next;
        *e = q->data;
        if (*L == q) // 刪除的是表尾元素
            *L = p;
        free(q); // 釋放待刪除結點
        return OK;
    }
    
    void ListTraverse(LinkList L, void(*vi)(ElemType)) { // 初始條件:L已存在。操作結果:依次對L的每個數據元素調用函數vi()
        LinkList p = L->next->next; // p指向首元結點
        while (p != L->next) { // p不指向頭結點
            vi(p->data);
            p = p->next;
        }
        printf("\n");
    }

    4.3.2雙循環鏈表

    // 線性表的雙向鏈表存儲結構
    typedef struct DuLNode {
        ElemType data;
        struct DuLNode *prior, *next;
    } DuLNode, *DuLinkList;
    
    // 帶頭結點的雙向循環鏈表的基本操作(14個)
    void InitList(DuLinkList *L) {
        // 產生空的雙向循環鏈表L
        *L = (DuLinkList)malloc(sizeof(DuLNode));
        if (*L)
            (*L)->next = (*L)->prior = *L;
        else
            exit(OVERFLOW);
    }
    
    void DestroyList(DuLinkList *L) {
        // 操作結果:銷毀雙向循環鏈表L
        DuLinkList q, p = (*L)->next; // p指向第一個結點
        while (p != *L) { // p沒到表頭
            q = p->next;
            free(p);
            p = q;
        }
        free(*L);
        *L = NULL;
    }
    
    void ClearList(DuLinkList L) { // 不改變L
        // 初始條件:L已存在。操作結果:將L重置為空表
        DuLinkList q, p = L->next; // p指向第一個結點
        while (p != L) { // p沒到表頭
            q = p->next;
            free(p);
            p = q;
        }
        L->next = L->prior = L; // 頭結點的兩個指針域均指向自身
    }
    
    Status ListEmpty(DuLinkList L) {
        // 初始條件:線性表L已存在。操作結果:若L為空表,則返回TRUE,否則返回FALSE
        if (L->next == L && L->prior == L)
            return TRUE;
        else
            return FALSE;
    }
    
    int ListLength(DuLinkList L) {
        // 初始條件:L已存在。操作結果:返回L中數據元素個數
        int i = 0;
        DuLinkList p = L->next; // p指向第一個結點
        while (p != L) { // p沒到表頭
            i++;
            p = p->next;
        }
        return i;
    }
    
    Status GetElem(DuLinkList L, int i, ElemType *e) {
        // 當第i個元素存在時,其值賦給e並返回OK,否則返回ERROR
        int j = 1; // j為計數器
        DuLinkList p = L->next; // p指向第一個結點
        while (p != L && j < i) { // 順指針向後查找,直到p指向第i個元素或p指向頭結點
            p = p->next;
            j++;
        }
        if (p == L || j > i) // 第i個元素不存在
            return ERROR;
        *e = p->data; // 取第i個元素
        return OK;
    }
    
    int LocateElem(DuLinkList L, ElemType e, Status(*compare)(ElemType, ElemType)) {
        // 初始條件:L已存在,compare()是數據元素判定函數
        // 操作結果:返回L中第1個與e滿足關係compare()的數據元素的位序。
        // 若這樣的數據元素不存在,則返回值為0
        int i = 0;
        DuLinkList p = L->next; // p指向第1個元素
        while (p != L) {
            i++;
            if (compare(p->data, e)) // 找到這樣的數據元素
                return i;
            p = p->next;
        }
        return 0;
    }
    
    Status PriorElem(DuLinkList L, ElemType cur_e, ElemType *pre_e) {
        // 操作結果:若cur_e是L的數據元素,且不是第一個,則用pre_e返回它的前驅,
        // 否則操作失敗,pre_e無定義
        DuLinkList p = L->next->next; // p指向第2個元素
        while (p != L) { // p沒到表頭
            if (p->data == cur_e) {
                *pre_e = p->prior->data;
                return TRUE;
            }
            p = p->next;
        }
        return FALSE;
    }
    
    Status NextElem(DuLinkList L, ElemType cur_e, ElemType *next_e) {
        // 操作結果:若cur_e是L的數據元素,且不是最後一個,則用next_e返回它的後繼,
        // 否則操作失敗,next_e無定義
        DuLinkList p = L->next->next; // p指向第2個元素
        while (p != L) { // p沒到表頭
            if (p->prior->data == cur_e) {
                *next_e = p->data;
                return TRUE;
            }
            p = p->next;
        }
        return FALSE;
    }
    
    DuLinkList GetElemP(DuLinkList L, int i) { // 另加
        // 在雙向鏈表L中返回第i個元素的地址。i為0,返回頭結點的地址。若第i個元素不存在,
        // 返回NULL
        int j;
        DuLinkList p = L; // p指向頭結點
        if (i < 0 || i > ListLength(L)) // i值不合法
            return NULL;
        for (j = 1; j <= i; j++)
            p = p->next;
        return p;
    }
    
    Status ListInsert(DuLinkList L, int i, ElemType e) {
        // 在帶頭結點的雙鏈循環線性表L中第i個位置之前插入元素e,i的合法值為1≤i≤表長+1
        // 改進算法2.18,否則無法在第表長+1個結點之前插入元素
        DuLinkList p, s;
        if (i < 1 || i > ListLength(L) + 1) // i值不合法
            return ERROR;
        p = GetElemP(L, i - 1); // 在L中確定第i個元素前驅的位置指針p
        if (!p) // p=NULL,即第i個元素的前驅不存在(設頭結點為第1個元素的前驅)
            return ERROR;
        s = (DuLinkList)malloc(sizeof(DuLNode));
        if (!s)
            return OVERFLOW;
        s->data = e;
        s->prior = p; // 在第i-1個元素之後插入
        s->next = p->next;
        p->next->prior = s;
        p->next = s;
        return OK;
    }
    
    Status ListDelete(DuLinkList L, int i, ElemType *e) {
        // 刪除帶頭結點的雙鏈循環線性表L的第i個元素,i的合法值為1≤i≤表長
        DuLinkList p;
        if (i < 1) // i值不合法
            return ERROR;
        p = GetElemP(L, i); // 在L中確定第i個元素的位置指針p
        if (!p) // p = NULL,即第i個元素不存在
            return ERROR;
        *e = p->data;
        p->prior->next = p->next; // 此處並沒有考慮鏈表頭,鏈表尾
        p->next->prior = p->prior;
        free(p);
        return OK;
    }
    
    void ListTraverse(DuLinkList L, void(*visit)(ElemType)) {
        // 由雙鏈循環線性表L的頭結點出發,正序對每個數據元素調用函數visit()
        DuLinkList p = L->next; // p指向頭結點
        while (p != L) {
            visit(p->data);
            p = p->next;
        }
        printf("\n");
    }
    
    void ListTraverseBack(DuLinkList L, void(*visit)(ElemType)) {
        // 由雙鏈循環線性表L的頭結點出發,逆序對每個數據元素調用函數visit()
        DuLinkList p = L->prior; // p指向尾結點
        while (p != L) {
            visit(p->data);
            p = p->prior;
        }
        printf("\n");
    }

    4.4靜態鏈表

    前面講解的都是動態鏈表,即需要指針來建立結點之間的連接關係。而對有些問題來說結點的地址是比較小的整數(例如5位數的地址),這樣就沒有必要去建立動態鏈表,而應使用方便得多的靜態鏈表。
    靜態鏈表的實現原理是hash,即通過建立一個結構體數組,並令數組的下標直接表示結點的地址,來達到直接訪問數組中的元素就能訪問結點的效果。另外,由於結點的訪問非常方便,因此靜態鏈表是不需要頭結點的。靜態鏈表結點定義的方法如下:

    struct Node{
        typename data;//數據域
        int next;//指針域
    }node[size];

    參考資料:

    • 《算法筆記》
    • 《數據結構和算法》-極客時間專欄

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

    【其他文章推薦】

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

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

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

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

  • X-Admin&ABP框架開發-RBAC

    X-Admin&ABP框架開發-RBAC

      在業務系統需求規劃過程中,通常對於諸如組織機構、用戶和角色等這種基礎功能,通常是將這部分功能規劃到通用子域中,這也說明了,對於這部分功能來講,是系統的基石,整個業務體系是建立於這部分基石之上的,當然,還有諸如多語言、設置管理、認證和授權等。對於這部分功能,ABP中存在這些概念,並且通過Module Zero模塊完成了這些概念。

     

    一、角色訪問控制之RBAC

      RBAC:Role Based Access Control,基於角色的訪問控制,這在目前大多數軟件中來講已經算得上是普遍應用了,最常見的結構如下,結構簡單,設計思路清晰。

      

      但是也存在其它升級版的設計,諸如用戶權限表、角色組、用戶組的概念等,具體分類有RBAC0、RBAC1、RBAC2等,後者功能越來越強大,也越來越複雜。

    • RBAC0:是RBAC的核心思想。
    • RBAC1:是把RBAC的角色分層模型。
    • RBAC2:增加了RBAC的約束模型。
    • RBAC3:整合RBAC2 + RBAC1。

     

    二、ABP中的RBAC

      在Abp中,已經集成了這些概念,並在ModuleZero模塊中實現了這些概念,基於IdentityServer4的ModuleZero模塊完成了封裝。對於我們大多數以業務為中心的開發人員來講,不應該又去造一個輪子,而是應該開好這輛車。首先看下Abp中的RBAC模型

      

      在這其中權限表中記錄了用戶與權限,角色與權限兩部分。對於權限通常指的是功能權限和數據權限兩部分,一般來講,大多指的是功能權限,這種通過角色與權限進行管理即可,如還有用戶部分的功能區分,則可以再使用上用戶與權限,而對於數據權限,可以利用用戶與權限部分,個人用的比較少,但是,可以想象到這麼一個場景,針對於一家門店內的多個店長,角色相同即相應的權限相同,但各自關心的數據來源不同,關心東部、南部等數據,而不關心西部、北部數據,因此可以在數據層面進行劃分,比如設置數據來源,東南西北,對於數據來源進行權限關聯,這樣一來用戶本身如果擁有東部數據權限,則只能看到東部數據。

     

    1、權限聲明及應用

      在Abp中,需要首先在Core層/Authorization/PermissionNames.cs中聲明權限,Abp權限部分設計原則是:先聲明再使用

    /// <summary>
    /// 權限命名
    /// </summary>
    public static class PermissionNames
    {
        #region 頂級權限
        public const string Pages = "Pages";
        #endregion
    
        #region 基礎支撐平台
        public const string Pages_Frame = "Pages.Frame";
    
        #region 租戶管理
        public const string Pages_Frame_Tenants = "Pages.Frame.Tenants";
        #endregion
    
        #region 組織機構
        public const string Pages_Frame_OrganizationUnits = "Pages.Frame.OrganizationUnits";
        public const string Pages_Frame_OrganizationUnits_Create = "Pages.Frame.OrganizationUnits.Create";
        public const string Pages_Frame_OrganizationUnits_Update = "Pages.Frame.OrganizationUnits.Update";
        public const string Pages_Frame_OrganizationUnits_Delete = "Pages.Frame.OrganizationUnits.Delete";
        #endregion
    
        #region 用戶管理
        public const string Pages_Frame_Users = "Pages.Frame.Users";
        public const string Pages_Frame_Users_Create = "Pages.Frame.Users.Create";
        public const string Pages_Frame_Users_Update = "Pages.Frame.Users.Update";
        public const string Pages_Frame_Users_Delete = "Pages.Frame.Users.Delete";
        public const string Pages_Frame_Users_ResetPassword = "Pages.Frame.Users.ResetPassword";
        #endregion
    
        #region 角色管理
        public const string Pages_Frame_Roles = "Pages.Roles";
        public const string Pages_Frame_Roles_Create = "Pages.Frame.Roles.Create";
        public const string Pages_Frame_Roles_Update = "Pages.Frame.Roles.Update";
        public const string Pages_Frame_Roles_Delete = "Pages.Frame.Roles.Delete";
        #endregion
    
    }

      然後在Core層/Authorization/XXXAuthorizationProvider.cs中設置具體權限,在此處設置權限時,可以根據權限設計時候的職責劃分,比如如果僅僅是多租戶需要這部分,那便設置權限範圍為多租戶即可。

    public class SurroundAuthorizationProvider : AuthorizationProvider
    {
        public override void SetPermissions(IPermissionDefinitionContext context)
        {
            #region 頂級權限
            var pages = context.CreatePermission(PermissionNames.Pages, L("Pages"));
            #endregion
    
            #region 基礎支撐平台
            var frame = pages.CreateChildPermission(PermissionNames.Pages_Frame, L("Frame"));
    
            #region 租戶管理
            frame.CreateChildPermission(PermissionNames.Pages_Frame_Tenants, L("Tenants"), multiTenancySides: MultiTenancySides.Host);
            #endregion
    
            #region 組織機構
            var organizationUnits = frame.CreateChildPermission(PermissionNames.Pages_Frame_OrganizationUnits, L("OrganizationUnits"));
            organizationUnits.CreateChildPermission(PermissionNames.Pages_Frame_OrganizationUnits_Create, L("CreateOrganizationUnit"));
            organizationUnits.CreateChildPermission(PermissionNames.Pages_Frame_OrganizationUnits_Update, L("EditOrganizationUnit"));
            organizationUnits.CreateChildPermission(PermissionNames.Pages_Frame_OrganizationUnits_Delete, L("DeleteOrganizationUnit"));
            #endregion
    
            #region 用戶管理
            var users = frame.CreateChildPermission(PermissionNames.Pages_Frame_Users, L("Users"));
            users.CreateChildPermission(PermissionNames.Pages_Frame_Users_Create, L("CreateUser"));
            users.CreateChildPermission(PermissionNames.Pages_Frame_Users_Update, L("UpdateUser"));
            users.CreateChildPermission(PermissionNames.Pages_Frame_Users_Delete, L("DeleteUser"));
            users.CreateChildPermission(PermissionNames.Pages_Frame_Users_ResetPassword, L("ResetPassword"));
            #endregion
    
            #region 角色管理
            var roles = frame.CreateChildPermission(PermissionNames.Pages_Frame_Roles, L("Roles"));
            roles.CreateChildPermission(PermissionNames.Pages_Frame_Roles_Create, L("CreateRole"));
            roles.CreateChildPermission(PermissionNames.Pages_Frame_Roles_Update, L("UpdateRole"));
            roles.CreateChildPermission(PermissionNames.Pages_Frame_Roles_Delete, L("DeleteRole"));
            #endregion
        }
    }

      在設置完畢后,需要將該類集成到Core層/XXXCoreModule當前模塊中,才能使得該部分權限設置生效。

    //配置權限管理
    Configuration.Authorization.Providers.Add<SurroundAuthorizationProvider>();

       作為業務的入口,菜單是較為直觀的體現方式,現在可以,為菜單分配權限了,擁有權限的人才能看的到菜單,同時後台方法中也要有權限判定,菜單僅作為前端入口上的控制,權限判定作為後端的控制。在MVC層的Startup/XXXNavigationProvider.cs中完成菜單的配置工作,可以配置多級菜單,每個菜單可以配置相應的權限,在生成菜單判定時,如果父級菜單權限不足,則直接會跳過子級菜單的判定。

    new MenuItemDefinition(//基礎支撐
        PageNames.FrameManage,
        L(PageNames.FrameManage),
        icon: "&#xe828;",
        requiredPermissionName: PermissionNames.Pages_Frame
    ).AddItem(
        new MenuItemDefinition(//組織機構
            PageNames.OrganizationUnits,
            L(PageNames.OrganizationUnits),
            url: "/OrganizationUnits",
            icon: "&#xe6cb;",
            requiredPermissionName: PermissionNames.Pages_Frame_OrganizationUnits
        )
    ).AddItem(
        new MenuItemDefinition(//用戶管理
            PageNames.Users,
            L(PageNames.Users),
            url: "/Users",
            icon: "&#xe6cb;",
            requiredPermissionName: PermissionNames.Pages_Frame_Users
        )
    ).AddItem(
        new MenuItemDefinition(//角色管理
            PageNames.Roles,
            L(PageNames.Roles),
            url: "/Roles",
            icon: "&#xe6cb;",
            requiredPermissionName: PermissionNames.Pages_Frame_Roles
        )
    ).AddItem(
        new MenuItemDefinition(//系統設置
            PageNames.HostSettings,
            L(PageNames.HostSettings),
            url: "/HostSettings",
            icon: "&#xe6cb;",
            requiredPermissionName: PermissionNames.Pages_Frame_HostSettings
        )
    )

      在前端頁面上,對於按鈕級別的控制也通過權限判定,Abp提供了判定方法,利用Razor語法進行按鈕控制

    @if (await PermissionChecker.IsGrantedAsync(PermissionNames.Pages_Core_DataDictionary_Create))
    {
        <button class="layui-btn layuiadmin-btn-dataDictionary" data-type="addDataDictionary">添加類型</button>
    }

      在後端方法上,通常我喜歡直接在應用服務中的方法上做權限判定(當然也可以前移到MVC層,但是這樣一來,針對於WebApi形式的Host層,又得多加一次判定了),利用AbpAuthorize特性,判定該方法需要哪幾個權限才能訪問,而在mvc的控制器上做訪問認證。

    [AbpAuthorize(PermissionNames.Pages_Core_DataDictionary_Create)]
    private async Task CreateDataDictionaryAsync(CreateOrUpdateDataDictionaryInput input)
    {
    
    }

     

    2、角色與權限

       在Abp中,角色信息存儲在abprole表中,角色與權限間的關聯存儲在abppermission這張表中,一個角色有多個權限,如果某個角色的權限被去掉了,這張表中的相關記錄將由abp負責刪除,我們只需要完成掌控哪些權限是這個角色有的就行。Abp中已經完成了角色的所有操作,但是前端部分採用的是bootstrap弄的,將其改造一波,成為layui風格。

      

      在創建角色中,主要是將選中的權限掛鈎到具體的某個角色上,該部分代碼沿用abp中自帶的角色權限處理方法。

    private async Task CreateRole(CreateOrUpdateRoleInput input)
    {
        var role = ObjectMapper.Map<Role>(input.Role);
        role.SetNormalizedName();
    
        CheckErrors(await _roleManager.CreateAsync(role));
    
        var grantedPermissions = PermissionManager
            .GetAllPermissions()
            .Where(p => input.PermissionNames.Contains(p.Name))
            .ToList();
    
        await _roleManager.SetGrantedPermissionsAsync(role, grantedPermissions);
    }

      指定角色Id,租戶Id及之前聲明的權限名稱,在abppermission中可查看到具體角色權限。

      

     

    3、用戶與角色

       一個用戶可以承擔多個角色,履行不同角色的義務,作為一個業務系統最基本的單元,abp中提供了這些概念並在Module Zero模塊中已經完成了對用戶的一系列操作,用戶信息存儲在AbpUsers表中,用戶直接關聯的角色保存在AbpUserRoles表中,abp中MVC版本採用的是bootstrap風格,因此,用layui風格完成一次替換,並且,改動一些頁面布局。

      

      Abp版本中,由於是土耳其大佬所開發的習慣,針對於姓和名做了拆分,因此對於我們的使用要做一次處理,我這先簡單處理了一下,並且在業務系統中,郵箱時有時無,因此也需要進行考慮。

    [AbpAuthorize(PermissionNames.Pages_Frame_Users_Create)]
    private async Task CreateUser(CreateOrUpdateUserInput input)
    {
        var user = ObjectMapper.Map<User>(input.User);
        user.TenantId = AbpSession.TenantId;
        user.IsEmailConfirmed = true;
        user.Name = "Name";
        user.Surname = "Surname";
        //user.EmailAddress = string.Empty;
    
        await UserManager.InitializeOptionsAsync(AbpSession.TenantId);
        foreach (var validator in _passwordValidators)
        {
            CheckErrors(await validator.ValidateAsync(UserManager, user, AppConsts.DefaultPassword));
        }
    
        user.Password = _passwordHasher.HashPassword(user, AppConsts.DefaultPassword);
    
        await _userManager.InitializeOptionsAsync(AbpSession.TenantId);
    
        CheckErrors(await _userManager.CreateAsync(user, AppConsts.DefaultPassword));
    
        if (input.AssignedRoleNames != null)
        {
            CheckErrors(await _userManager.SetRoles(user, input.AssignedRoleNames));
        }
    
        if (input.OrganizationUnitIds != null)
        {
            await _userManager.SetOrganizationUnitsAsync(user, input.OrganizationUnitIds);
        }
    
        CurrentUnitOfWork.SaveChanges();
    }

      此處對用戶個人單獨的權限沒有去做處理,依照Abp的文檔有那麼一句話,大多數應用程序中,基於角色的已經足夠使用了,如果想聲明特定權限給用戶,那麼針對於用戶本身的角色權限則被覆蓋。    

     

     至此,修改整合用戶、角色和權限加入到系統中初步完成了,至於一些更為豐富的功能,待逐步加入中,車子再好,司機也得睡覺。

     

     倉庫地址:

    2019-11-17,望技術有成后能回來看見自己的腳步

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

    【其他文章推薦】

    ※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

    ※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

    ※帶您來看台北網站建置台北網頁設計,各種案例分享

  • 如何給HTML標籤中的文本設置修飾線

    如何給HTML標籤中的文本設置修飾線

    text-decoration屬性介紹

    • text-decoration屬性是用來設置文本修飾線呢,text-decoration屬性一共有4個值。

    text-decoration屬性值說明表

    作用
    none 去掉文本修飾線
    underline 設置下劃線
    overline 設置上劃線
    line-through 設置刪除線

    HTML標籤自帶修飾線

    • 在開始實踐text-decoration屬性之前,筆者先給大家普及下HTML中的標籤自帶修飾線如:u標籤s標籤,若有不全大家可以在下面評論中告訴筆者,畢竟筆者也是前端的一個小白,希望和大家相互交流,互幫互助,共同進步。

    u標籤

    • 下面讓我們進入u標籤的實踐,u標籤自帶的是文本下劃線。
    • 代碼塊

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>設置文本修飾線</title>
      
    </head>
    <body>
        <u>成功不是擊敗別人,而是改變自己</u>
    </body>
    </html>
    • 結果圖

    • 注意:u標籤也可以配合HTML中的其他標籤使用,舉例:將u標籤嵌套到h1標籤中使用。

    • 代碼塊

    <h1><u>成功不是擊敗別人,而是改變自己</u></h1>

    s標籤

    • 下面讓我們進入s標籤的實踐,s標籤自帶的是文本刪除線。
    • 代碼塊

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>設置文本修飾線</title>
      
    </head>
    <body>
        <s>成功不是擊敗別人,而是改變自己</s>
    </body>
    </html>
    • 結果圖

    • 注意:s標籤也可以嵌套,和u標籤一致,筆者就不過多的介紹了。

    none去除修飾線

    • 讓我們進入text-decoration屬性的none值實踐,實踐內容如:筆者將HTML頁面中的s標籤自帶的刪除線給去除掉。

    • 代碼塊

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>設置文本修飾線</title>
        <style>
            s{
                text-decoration: none;
            }
        </style>
    </head>
    <body>
        <s>成功不是擊敗別人,而是改變自己</s>
    </body>
    </html>
    • 結果圖

    • 注意:u標籤、s標籤、包括text-decoration屬性值的所有的修飾線都可以去掉哦。

    underline設置下劃線

    • 讓我們進入text-decoration屬性的underline值實踐,實踐內容如:筆者將HTML頁面中的h2標籤中的文本設置一個下劃線。
    • 代碼塊

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>設置文本修飾線</title>
        <style>
            h2{
                text-decoration: underline;
            }
        </style>
    </head>
    <body>
        <h2>成功不是擊敗別人,而是改變自己</h2>
    </body>
    </html>
    • 結果圖

    overline設置上劃線

    • 讓我們進入text-decoration屬性的overline值實踐,實踐內容如:筆者將HTML頁面中的h2標籤中的文本設置一個上劃線。

    • 代碼塊

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>設置文本修飾線</title>
        <style>
            h2{
                text-decoration: overline;
            }
        </style>
    </head>
    <body>
        <h2>成功不是擊敗別人,而是改變自己</h2>
    </body>
    </html>
    • 結果圖

    line-through設置刪除線

    • 讓我們進入text-decoration屬性的line-through值實踐,實踐內容如:筆者將HTML頁面中的h2標籤中的文本設置一個刪除線。

    • 代碼塊

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>設置文本修飾線</title>
        <style>
            h2{
                text-decoration: line-through;
            }
        </style>
    </head>
    <body>
        <h2>成功不是擊敗別人,而是改變自己</h2>
    </body>
    </html>
    • 結果圖

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

    【其他文章推薦】

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

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

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

    南投搬家前需注意的眉眉角角,別等搬了再說!

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

    開篇

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

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

    RheaKV讀取數據

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

    DefaultRheaKVStore#bGet

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

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

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

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

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

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

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

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

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

    批量獲取數據

    GetBatchingHandler#onEvent

    public void onEvent(final KeyEvent event, final long sequence, final boolean endOfBatch) throws Exception {
        this.events.add(event);
        this.cachedBytes += event.key.length;
        final int size = this.events.size();
        //校驗一下數據量,沒有達到MaxReadBytes並且不是最後一個event,那麼直接返回
        if (!endOfBatch && size < batchingOpts.getBatchSize() && this.cachedBytes < batchingOpts.getMaxReadBytes()) {
            return;
        }
    
        if (size == 1) {
            reset();
            try {
                //如果只是一個get請求,那麼不需要進行批量處理
                get(event.key, this.readOnlySafe, event.future, false);
            } catch (final Throwable t) {
                exceptionally(t, event.future);
            }
        } else {
            //初始化一個剛剛好大小的集合
            final List<byte[]> keys = Lists.newArrayListWithCapacity(size);
            final CompletableFuture<byte[]>[] futures = new CompletableFuture[size];
            for (int i = 0; i < size; i++) {
                final KeyEvent e = this.events.get(i);
                keys.add(e.key);
                futures[i] = e.future;
            }
            //遍歷完events數據到entries之後,重置
            reset();
            try {
                multiGet(keys, this.readOnlySafe).whenComplete((result, throwable) -> {
                    //異步回調處理數據
                    if (throwable == null) {
                        for (int i = 0; i < futures.length; i++) {
                            final ByteArray realKey = ByteArray.wrap(keys.get(i));
                            futures[i].complete(result.get(realKey));
                        }
                        return;
                    }
                    exceptionally(throwable, futures);
                });
            } catch (final Throwable t) {
                exceptionally(t, futures);
            }
        }
    }
    }

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

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

    private FutureGroup<Map<ByteArray, byte[]>> internalMultiGet(final List<byte[]> keys, final boolean readOnlySafe,
                                                                 final int retriesLeft, final Throwable lastCause) {
        //因為不同的key是存放在不同的region中的,所以一個region會對應多個key,封裝到map中
        final Map<Region, List<byte[]>> regionMap = this.pdClient
                .findRegionsByKeys(keys, ApiExceptionHelper.isInvalidEpoch(lastCause));
        //返回值
        final List<CompletableFuture<Map<ByteArray, byte[]>>> futures =
                Lists.newArrayListWithCapacity(regionMap.size());
        //lastCause傳入為null
        final Errors lastError = lastCause == null ? null : Errors.forException(lastCause);
    
        for (final Map.Entry<Region, List<byte[]>> entry : regionMap.entrySet()) {
            final Region region = entry.getKey();
            final List<byte[]> subKeys = entry.getValue();
            //重試次數減1,設置一個重試函數
            final RetryCallable<Map<ByteArray, byte[]>> retryCallable = retryCause -> internalMultiGet(subKeys,
                    readOnlySafe, retriesLeft - 1, retryCause);
            final MapFailoverFuture<ByteArray, byte[]> future = new MapFailoverFuture<>(retriesLeft, retryCallable);
            //發送MultiGetRequest請求,獲取數據
            internalRegionMultiGet(region, subKeys, readOnlySafe, future, retriesLeft, lastError, this.onlyLeaderRead);
            futures.add(future);
        }
        return new FutureGroup<>(futures);
    }

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

    DefaultRheaKVStore#internalRegionMultiGet

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

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

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

    server端處理MultiGetRequest請求

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

    public void handleMultiGetRequest(final MultiGetRequest request,
                                      final RequestProcessClosure<BaseRequest, BaseResponse<?>> closure) {
        final MultiGetResponse response = new MultiGetResponse();
        response.setRegionId(getRegionId());
        response.setRegionEpoch(getRegionEpoch());
        try {
            KVParameterRequires.requireSameEpoch(request, getRegionEpoch());
            final List<byte[]> keys = KVParameterRequires.requireNonEmpty(request.getKeys(), "multiGet.keys");
            //調用MetricsRawKVStore的multiGet方法
            this.rawKVStore.multiGet(keys, request.isReadOnlySafe(), new BaseKVStoreClosure() {
    
                @SuppressWarnings("unchecked")
                @Override
                public void run(final Status status) {
                    if (status.isOk()) {
                        response.setValue((Map<ByteArray, byte[]>) getData());
                    } else {
                        setFailure(request, response, status, getError());
                    }
                    closure.sendResponse(response);
                }
            });
        } catch (final Throwable t) {
            LOG.error("Failed to handle: {}, {}.", request, StackTraceUtil.stackTrace(t));
            response.setError(Errors.forException(t));
            closure.sendResponse(response);
        }
    }

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

    MetricsRawKVStore#multiGet

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

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

    RaftRawKVStore#multiGet

    public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
        if (!readOnlySafe) {
            this.kvStore.multiGet(keys, false, closure);
            return;
        }
        // KV 存儲實現線性一致讀
        // 調用 readIndex 方法,等待回調執行
        this.node.readIndex(BytesUtil.EMPTY_BYTES, new ReadIndexClosure() {
    
            @Override
            public void run(final Status status, final long index, final byte[] reqCtx) {
                //如果狀態返回成功,
                if (status.isOk()) {
                    RaftRawKVStore.this.kvStore.multiGet(keys, true, closure);
                    return;
                }
                //readIndex 讀取失敗嘗試應用鍵值讀操作申請任務於 Leader 節點的狀態機 KVStoreStateMachine
                RaftRawKVStore.this.readIndexExecutor.execute(() -> {
                    if (isLeader()) {
                        LOG.warn("Fail to [multiGet] with 'ReadIndex': {}, try to applying to the state machine.",
                                status);
                        // If 'read index' read fails, try to applying to the state machine at the leader node
                        applyOperation(KVOperation.createMultiGet(keys), closure);
                    } else {
                        LOG.warn("Fail to [multiGet] with 'ReadIndex': {}.", status);
                        // Client will retry to leader node
                        new KVClosureAdapter(closure, null).run(status);
                    }
                });
            }
        });
    }

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

    線性一致性讀readIndex

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

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

    Node#readIndex

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

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

    ReadOnlyServiceImpl#addRequest

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

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

    ReadIndexEventHandler

    private class ReadIndexEventHandler implements EventHandler<ReadIndexEvent> {
        // task list for batch
        private final List<ReadIndexEvent> events = new ArrayList<>(
                                                      ReadOnlyServiceImpl.this.raftOptions.getApplyBatch());
    
        @Override
        public void onEvent(final ReadIndexEvent newEvent, final long sequence, final boolean endOfBatch)
                                                                                                         throws Exception {
            if (newEvent.shutdownLatch != null) {
                executeReadIndexEvents(this.events);
                this.events.clear();
                newEvent.shutdownLatch.countDown();
                return;
            }
    
            this.events.add(newEvent);
            //批量執行
            if (this.events.size() >= ReadOnlyServiceImpl.this.raftOptions.getApplyBatch() || endOfBatch) {
                executeReadIndexEvents(this.events);
                this.events.clear();
            }
        }
    }

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

    ReadOnlyServiceImpl#executeReadIndexEvents

    private void executeReadIndexEvents(final List<ReadIndexEvent> events) {
        if (events.isEmpty()) {
            return;
        }
        //初始化ReadIndexRequest
        final ReadIndexRequest.Builder rb = ReadIndexRequest.newBuilder() //
            .setGroupId(this.node.getGroupId()) //
            .setServerId(this.node.getServerId().toString());
    
        final List<ReadIndexState> states = new ArrayList<>(events.size());
    
        for (final ReadIndexEvent event : events) {
            rb.addEntries(ZeroByteStringHelper.wrap(event.requestContext.get()));
            states.add(new ReadIndexState(event.requestContext, event.done, event.startTime));
        }
        final ReadIndexRequest request = rb.build();
    
        this.node.handleReadIndexRequest(request, new ReadIndexResponseClosure(states, request));
    }

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

    NodeImpl#handleReadIndexRequest

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

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

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

    private void readLeader(final ReadIndexRequest request, final ReadIndexResponse.Builder respBuilder,
                            final RpcResponseClosure<ReadIndexResponse> closure) {
        //1. 獲取集群節點中多數選票數是多少
        final int quorum = getQuorum();
        if (quorum <= 1) {
            // Only one peer, fast path.
            //如果集群中只有一個節點,那麼直接調用回調函數,返回成功
            respBuilder.setSuccess(true) //
                    .setIndex(this.ballotBox.getLastCommittedIndex());
            closure.setResponse(respBuilder.build());
            closure.run(Status.OK());
            return;
        }
    
        final long lastCommittedIndex = this.ballotBox.getLastCommittedIndex();
        //2. 任期必須相等
        //日誌管理器 LogManager 基於投票箱 BallotBox 的 lastCommittedIndex 獲取任期檢查是否等於當前任期
        // 如果不等於當前任期表示此 Leader 節點未在其任期內提交任何日誌,需要拒絕只讀請求;
        if (this.logManager.getTerm(lastCommittedIndex) != this.currTerm) {
            // Reject read only request when this leader has not committed any log entry at its term
            closure
                    .run(new Status(
                            RaftError.EAGAIN,
                            "ReadIndex request rejected because leader has not committed any log entry at its term, " +
                             "logIndex=%d, currTerm=%d.",
                            lastCommittedIndex, this.currTerm));
            return;
        }
        respBuilder.setIndex(lastCommittedIndex);
    
        if (request.getPeerId() != null) {
            // request from follower, check if the follower is in current conf.
            final PeerId peer = new PeerId();
            peer.parse(request.getServerId());
            //3. 來自 Follower 的請求需要檢查 Follower 是否在當前配置
            if (!this.conf.contains(peer)) {
                closure
                        .run(new Status(RaftError.EPERM, "Peer %s is not in current configuration: {}.", peer,
                         this.conf));
                return;
            }
        }
    
        ReadOnlyOption readOnlyOpt = this.raftOptions.getReadOnlyOptions();
        //4. 如果使用的是ReadOnlyLeaseBased,確認leader是否是在在租約有效時間內
        if (readOnlyOpt == ReadOnlyOption.ReadOnlyLeaseBased && !isLeaderLeaseValid()) {
            // If leader lease timeout, we must change option to ReadOnlySafe
            readOnlyOpt = ReadOnlyOption.ReadOnlySafe;
        }
    
        switch (readOnlyOpt) {
            //5
            case ReadOnlySafe:
                final List<PeerId> peers = this.conf.getConf().getPeers();
                Requires.requireTrue(peers != null && !peers.isEmpty(), "Empty peers");
                //設置心跳的響應回調函數
                final ReadIndexHeartbeatResponseClosure heartbeatDone = new ReadIndexHeartbeatResponseClosure(closure,
                        respBuilder, quorum, peers.size());
                // Send heartbeat requests to followers
                //向 Followers 節點發起一輪 Heartbeat,如果半數以上節點返回對應的
                // Heartbeat Response,那麼 Leader就能夠確定現在自己仍然是 Leader
                for (final PeerId peer : peers) {
                    if (peer.equals(this.serverId)) {
                        continue;
                    }
                    this.replicatorGroup.sendHeartbeat(peer, heartbeatDone);
                }
                break;
            //6. 因為在租約期內不會發生選舉,確保 Leader 不會變化
            //所以直接返回回調結果
            case ReadOnlyLeaseBased:
                // Responses to followers and local node.
                respBuilder.setSuccess(true);
                closure.setResponse(respBuilder.build());
                closure.run(Status.OK());
                break;
        }
    }
    1. 獲取集群節點中多數選票數是多少,即集群節點的1/2+1,如果當前的集群里只有一個節點,那麼直接返回成功,並調用回調方法
    2. 校驗 Raft 集群節點數量以及 lastCommittedIndex 所屬任期符合預期,那麼響應構造器設置其索引為投票箱 BallotBox 的 lastCommittedIndex
    3. 來自 Follower 的請求需要檢查 Follower 是否在當前配置,如果不在當前配置中直接調用回調方法設置異常
    4. 獲取 ReadIndex 請求級別 ReadOnlyOption 配置,ReadOnlyOption 參數默認值為 ReadOnlySafe。如果設置的是ReadOnlyLeaseBased,那麼會調用isLeaderLeaseValid檢查leader是否是在在租約有效時間內
    5. 配置為ReadOnlySafe 調用 Replicator#sendHeartbeat(rid, closure) 方法向 Followers 節點發送 Heartbeat 心跳請求,發送心跳成功執行 ReadIndexHeartbeatResponseClosure 心跳響應回調;ReadIndex 心跳響應回調檢查是否超過半數節點包括 Leader 節點自身投票贊成,半數以上節點返回客戶端Heartbeat 請求成功響應,即 applyIndex 超過 ReadIndex 說明已經同步到 ReadIndex 對應的 Log 能夠提供 Linearizable Read
    6. 配置為ReadOnlyLeaseBased,因為Leader 租約有效期間認為當前 Leader 是 Raft Group 內的唯一有效 Leader,所以忽略 ReadIndex 發送 Heartbeat 確認身份步驟,直接返回 Follower 節點和本地節點 Read 請求成功響應。Leader 節點繼續等待狀態機執行,直到 applyIndex 超過 ReadIndex 安全提供 Linearizable Read

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

    ReadIndexResponseClosure#run

    public void run(final Status status) {
        //fail
        //傳入的狀態不是ok,響應失敗
        if (!status.isOk()) {
            notifyFail(status);
            return;
        }
        final ReadIndexResponse readIndexResponse = getResponse();
        //Fail
        //response沒有響應成功,響應失敗
        if (!readIndexResponse.getSuccess()) {
            notifyFail(new Status(-1, "Fail to run ReadIndex task, maybe the leader stepped down."));
            return;
        }
        // Success
        //一致性讀成功
        final ReadIndexStatus readIndexStatus = new ReadIndexStatus(this.states, this.request,
            readIndexResponse.getIndex());
        for (final ReadIndexState state : this.states) {
            // Records current commit log index.
            //設置當前提交的index
            state.setIndex(readIndexResponse.getIndex());
        }
    
        boolean doUnlock = true;
        ReadOnlyServiceImpl.this.lock.lock();
        try {
            //校驗applyIndex 是否超過 ReadIndex
            if (readIndexStatus.isApplied(ReadOnlyServiceImpl.this.fsmCaller.getLastAppliedIndex())) {
                // Already applied, notify readIndex request.
                ReadOnlyServiceImpl.this.lock.unlock();
                doUnlock = false;
                //已經同步到 ReadIndex 對應的 Log 能夠提供 Linearizable Read
                notifySuccess(readIndexStatus);
            } else {
                // Not applied, add it to pending-notify cache.
                ReadOnlyServiceImpl.this.pendingNotifyStatus
                    .computeIfAbsent(readIndexStatus.getIndex(), k -> new ArrayList<>(10)) //
                    .add(readIndexStatus);
            }
        } finally {
            if (doUnlock) {
                ReadOnlyServiceImpl.this.lock.unlock();
            }
        }
    }

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

    ReadOnlyServiceImpl#notifySuccess

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

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

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

    public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
        if (!readOnlySafe) {
            this.kvStore.multiGet(keys, false, closure);
            return;
        }
        // KV 存儲實現線性一致讀
        // 調用 readIndex 方法,等待回調執行
        this.node.readIndex(BytesUtil.EMPTY_BYTES, new ReadIndexClosure() {
    
            @Override
            public void run(final Status status, final long index, final byte[] reqCtx) {
                //如果狀態返回成功,
                if (status.isOk()) {
                    RaftRawKVStore.this.kvStore.multiGet(keys, true, closure);
                    return;
                }
                //readIndex 讀取失敗嘗試應用鍵值讀操作申請任務於 Leader 節點的狀態機 KVStoreStateMachine
                RaftRawKVStore.this.readIndexExecutor.execute(() -> {
                    if (isLeader()) {
                        LOG.warn("Fail to [multiGet] with 'ReadIndex': {}, try to applying to the state machine.",
                                status);
                        // If 'read index' read fails, try to applying to the state machine at the leader node
                        applyOperation(KVOperation.createMultiGet(keys), closure);
                    } else {
                        LOG.warn("Fail to [multiGet] with 'ReadIndex': {}.", status);
                        // Client will retry to leader node
                        new KVClosureAdapter(closure, null).run(status);
                    }
                });
            }
        });

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

    總結

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

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

    【其他文章推薦】

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

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

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

  • 讀《MySQL必知必會》我學到了什麼?

    讀《MySQL必知必會》我學到了什麼?

    前言

    最近在寫項目的時候發現自己的SQL基本功有些薄弱,遂上知乎查詢MYSQL關鍵字,期望得到某些高贊答案的指點,於是乎發現了

    https://www.zhihu.com/question/34840297/answer/272185020 這位老兄的建議的書單,根據他的建議首先拜讀了《MYSQL必知必會》這本書,整體講的很基礎,頁數也不多一共 253 頁,適合基礎比較薄弱的同學進行食用。然後循序漸進,閱讀更深層次的書籍進行自我提升。這裏記載了自己在閱讀的過程中記錄的一些關鍵內容,分享給大家。書本 PDF 可以在上面的知乎鏈接獲取,或者點擊 http://www.notedeep.com/note/38/page/282 前往老哥的深度筆記進行下載。

    閱讀心得

    SQL語句和大小寫

    SQL語句不區分大小寫,並且在 Windows 環境下,4.1.1版本之後(現在常用的都是 5.6/5.7/8.0+),MYSQL表名,字段名也是不區分大小寫的,因此我們在命名的時候建議使用單個單詞_單個單詞的形式命名,如:mysql_crash_course user_role

    這裏附上阿里代碼規範的一條強制要求:

    【強制】表名、字段名必須使用小寫字母或数字,禁止出現数字開頭,禁止兩個下劃線中間只出現数字。數據庫字段名的修改代價很大,因為無法進行預發布,所以字段名稱需要慎重考慮。
    說明: MySQL 在 Windows 下不區分大小寫,但在 Linux 下默認是區分大小寫。因此,數據庫名、表名、字段名,都不允許出現任何大寫字母,避免節外生枝。

    不能部分使用DISTINCT

    • DISTINCT 關鍵字應用於所有列而不僅是前置它的列。如果給出 SELECT DISTINCT vend_id,prod_price ,除非指定的兩個列都不同,否則所有行都將被檢索出來。
    • DISTINCT 不會返回其後面跟的所有字段都相同的列。即兩行中SELECT查詢的任何一個字段都相同才會被去重。不能通過DISTINCT加括號 () 等方式來對單個字段進行去重。

    比如項目中有這麼一個需求:

    需要分頁查詢綁了某收費標準的房屋,因為是房屋列表查詢,我們默認相同ID的房屋只出現一次,錯誤的SQL如下:

    房屋表

    收費標準表

    SELECT DISTINCT h.id, h.num, s.name 
    FROM house h 
    LEFT JOIN standard s 
    ON h.id = s.room_id 
    WHERE s.name = '物業費' OR s.name = '暖氣費';

    這條語句並不能返回想要的結果,即每套房屋只出現一次,因為不同的收費標準名稱不一樣,DISTINCT 不能對部分查詢條件去重。可以看到房號為1-001的記錄出現了兩次。

    不過其實按照需求的描述我這裏僅查詢房屋的信息,對於查詢結果來說同一條記錄的房屋信息肯定完全相同,因此 DISTINCT 在我的業務中滿足要求。而有其他業務需要此關鍵字的時候,請大家慎重使用,切記不能部分使用該關鍵字

    區分大小寫和排序順序

    在對文本性的數據進行排序時,A與a相同嗎?a位於B之前還是位於Z之後?

    在創建字段時可以指定字符集,一般使用 utf8mb4, 此時可以選擇相應的排序規則。

    1. utf8mb4_general_ci ci即大小寫不敏感,排序時忽略大小寫,A a 視作相同
    2. utf8mb4_bin / 帶 cs 的即大小寫敏感,相應的升序排列的話, A~Z 在前,小a~z在後
      相應的,在設置大小寫敏感后,查詢條件 where cs = ‘a’ 只能查找到表中該字段為小寫 a 的行。而不敏感,即ci時,A,a都可以被查詢出來。

    BETWEEN關鍵字的注意事項

    在區間查詢時,我們最關注的不應該是區間內的能否被匹配到,因為這是肯定的。而區間的邊界能否被匹配才是我們應該注意的知識點,BETWEEN AND 關鍵字匹配區間時,包含左右邊界條件。如下面的 SQL 執行結果如下:

    SELECT prod_name, prod_price 
    FROM products p
    WHERE p.`prod_price` BETWEEN 5.99 AND 10

    NULL 與不匹配

    在通過過濾選擇出不具有特定值的行時,你可能希望返回具有 NULL 值的行。但是,不行。常見的錯誤會發生在is_xxx 字段上,我經常有這個毛病,

    對於 is_delete 字段,我認為為 null 或者 0 都是未刪除的房屋,所以當我使用

    SELECT * FROM house WHERE is_delete = '0';

    查詢未刪除的房屋時,我只能查到 id 為 3 的房屋,這顯然與我的預期是不符的,解決辦法是where後面加 or is_delete is null ,或者給 is_delete 列默認值 0;建議使用後者。

    這裏附上阿里代碼手冊的一條強制項目:

    【強制】表達是與否概念的字段,必須使用 is_xxx 的方式命名,數據類型是 unsigned tinyint(1 表示是,0 表示否)。說明:任何字段如果為非負數,必須是 unsigned 。

    解釋:tinyint 相當於 Java 中的 byte,取值範圍 -128 ~ 127 ,用來表達是否長度已經足夠,也可以用來表示人的年齡。而 unsigned 表示無符號的,對於確定為非負數的字段,使用 unsigned 可以將取值範圍擴大一倍。

    AND 和 OR 的計算次序

    舉個例子:假如需要列出價格為10美元(含)以上且由 1002 或 1003 製造的所有產品。下面的 SELECT 語句使用 AND 和 OR 操作符的組合建立了一個WHERE 子句:

    SELECT *
    FROM products
    WHERE vend_id = 1002 OR vend_id = 1003 AND prod_price >= 10;

    查詢結果如下:

    而被我用紅框標註的行很顯然不是我們需要的行,為什麼會這樣呢?原因在於計算的次序。SQL(像多數語言一樣)在處理 OR 操作符前,優先處理 AND 操作符。當SQL看到上述 WHERE 子句時,它理解為由供應商 1003 製造的任何價格為10美元(含)以上的產品,或者由供應商 1002 製造的任何產品,而不管其價格如何。換句話說,由於 AND 在計算次序中優先級更高,操作符被錯誤地組合了。解決方法就是加 ();正確的 SQL 如下:

    SELECT *
    FROM products
    WHERE (vend_id = 1002 OR vend_id = 1003) AND prod_price >= 10;

    建議在任何時候使用具有 AND 和 OR 操作符的 WHERE 子句,都應該使用圓括號明確地分組操作符。不要過分依賴默認計算次序,即使它確實是你想要的東西也是如此。使用圓括號沒有什麼壞處,它能消除歧義。

    UNION 組合查詢

    • 利用 UNION ,可給出多條SELECT 語句,將它們的結果組合成單個結果集。
    • UNION 中的每個查詢必須包含相同的列、表達式或聚集函數。
    • 在使用UNION 時,重複的行被自動取消。這是 UNION 的默認行為,但是如果需要,可以改變它。事實上,如果想返回所有匹配行,可使用 UNION ALL 而不是 UNION 。
    • 在用 UNION 組合查詢時,只能使用一條 ORDER BY 子句,它必須出現在最後一條 SELECT 語句之後

    INSERT語句總是使用列的列表

    一般不要使用沒有明確給出列的列表的 INSERT 語句。使用列的列表能使SQL代碼繼續發揮作用,即使表結構發生了變化。實際開發中有可能由於業務的需要,對錶結構進行修改,添加/刪除某一列。這時如果代碼中使用的SQL語句是沒有明確列表的插入語句就會報錯。當然一般我們使用逆向工程生成的 insertSelective(POJO) 並不存在這個問題,因為它對應生成的 SQL 會為我們生成列的列表。

    小心使用更新和刪除語句

    MySQL 沒有撤銷按鈕,因此在使用 UPDATE / DELETE 時一定要加上 WHERE 條件,並且在執行更新/刪除操作之前先進行 SELECT 操作,開啟事務。在執行結束后核對影響的行數和 SELECT 查詢出來的行數一致后再 COMMIT;

    另外,使用 ALTER TABLE 要極為小心,應該在進行改動前做一個完整的備份(模式和數據的備份)。數據庫表的更改不能撤銷,如果增加了不需要的列,可能不能刪除它們。類似地,如果刪除了不應該刪除的列,可能會丟失該列中的所有數據。

    視圖的規則和限制

    • 與表一樣,視圖必須唯一命名(不能給視圖取與別的視圖或表相同的名字)。
    • 對於可以創建的視圖數目沒有限制。
    • 為了創建視圖,必須具有足夠的訪問權限。這些限制通常由數據庫管理人員授予。
    • 視圖可以嵌套,即可以利用從其他視圖中檢索數據的查詢來構造一個視圖。
    • ORDER BY 可以用在視圖中,但如果從該視圖檢索數據 SELECT 中也含有 ORDER BY ,那麼該視圖中的 ORDER BY 將被覆蓋。
    • 視圖不能索引,也不能有關聯的觸發器或默認值。
    • 視圖可以和表一起使用。例如,編寫一條聯結表和視圖的 SELECT語句。

    其中視圖不能索引這一點要格外注意,在我開發過程中遇到過這樣一個視圖:使用 UNION 連結了好幾張表進行查詢,對查詢的結果使用 WHERE 條件再過濾,這裏雖然對於被連接的表對於 WHERE 條件后的字段都建立了索引,但是使用 UNION 連結生成視圖的臨時表並不能擁有索引。因此查詢的效率會很慢!所以建議在 UNION 查詢中將 WHERE 條件放在每個 SELECT 語句中。

    改善性能的一些建議

    • 首先,MySQL(與所有DBMS一樣)具有特定的硬件建議。在學習和研究MySQL時,使用任何舊的計算機作為服務器都可以。但
      對用於生產的服務器來說,應該堅持遵循這些硬件建議。

    • 一般來說,關鍵的生產DBMS應該運行在自己的專用服務器上。

    • MySQL是用一系列的默認設置預先配置的,從這些設置開始通常是很好的。但過一段時間后你可能需要調整內存分配、緩衝區大小等。(為查看當前設置,可使用 SHOW VARIABLES; 和 SHOWSTATUS; )

    • MySQL一個多用戶多線程的DBMS,換言之,它經常同時執行多個任務。如果這些任務中的某一個執行緩慢,則所有請求都會執
      行緩慢。如果你遇到顯著的性能不良,可使用 SHOW PROCESSLIST显示所有活動進程(以及它們的線程ID和執行時間)。你還可以用KILL 命令終結某個特定的進程(使用這個命令需要作為管理員登錄)。

    • 總是有不止一種方法編寫同一條 SELECT 語句。應該試驗聯結、並、子查詢等,找出最佳的方法。

    • 使用 EXPLAIN 語句讓MySQL解釋它將如何執行一條 SELECT 語句。

    • 一般來說,存儲過程執行得比一條一條地執行其中的各條MySQL語句快。但存儲過程一般難以調試和擴展,並且沒有移植性,因此阿里代碼規約裏面強制禁止使用存儲過程

    • 應該總是使用正確的數據類型。

    • 決不要檢索比需求還要多的數據。換言之,不要用 SELECT * (除非你真正需要每個列)。

    • 有的操作(包括 INSERT )支持一個可選的 DELAYED 關鍵字,如果使用它,將把控制立即返回給調用程序,並且一旦有可能就實際執行該操作。

      ​ 延遲插入,當插入和查詢併發執行時,插入被放入等待隊列中。直至所有查詢執行完畢后執行插入。並且MYSQL會在收到插入請求后直接返回給客戶端狀態信息,既是INSERT語句還在隊列中

    • 在導入數據時,應該關閉自動提交。你可能還想刪除索引(包括FULLTEXT 索引),然後在導入完成后再重建它們。

    • 必須索引數據庫表以改善數據檢索的性能。確定索引什麼不是一件微不足道的任務,需要分析使用的 SELECT 語句以找出重複的WHERE 和 ORDER BY 子句。如果一個簡單的 WHERE 子句返回結果所花的時間太長,則可以斷定其中使用的列(或幾個列)就是需要索引的對象。

    • 你的 SELECT 語句中有一系列複雜的 OR 條件嗎?通過使用多條SELECT 語句和連接它們的 UNION 語句,你能看到極大的性能改進。

    • 索引改善數據檢索的性能,但損害數據插入、刪除和更新的性能。如果你有一些表,它們收集數據且不經常被搜索,則在有必要之前不要索引它們。(索引可根據需要添加和刪除。)

    • LIKE 很慢。一般來說,最好是使用 FULLTEXT 而不是 LIKE 。但是 MYSQL FULLTEXT 對漢字並不友好,如果需要使用全文索引,建議使用搜索引擎 ES,可以參考我的另一篇博客: https://www.cnblogs.com/keatsCoder/p/11341835.html

    • 數據庫是不斷變化的實體。一組優化良好的表一會兒后可能就面目全非了。由於表的使用和內容的更改,理想的優化和配置也會改變。

    • 最重要的規則就是,每條規則在某些條件下都會被打破。

    實際開發過程中,我們需要根據業務的需要,開啟慢查詢日誌,然後針對慢SQL,不斷地進行 EXPLAIN 與修改SQL和索引,以求達到 ref 級別,至少達到 range 級別。這就需要強大的內功支持而不是每次都通過百度來解決,希望閱讀此篇文章的你和我一起不斷修鍊。加油!

    常用的函數

    文本處理函數

    日期和時間處理函數

    數值處理函數

    聚合函數

    • 雖然 MAX() 一般用來找出最大的數值或日期值,但MySQL允許將它用來返回任意列中的最大值,包括返迴文本列中的最大值。在用於文本數據時,如果數據按相應的列排序,則 MAX() 返回最後一行。MIN() 函數類似
    • MAX() 函數忽略列值為 NULL 的行。MIN() 函數類似,SUM() 也是

    附錄

    由於數中所附的附件內容(建表語句即數據插入語句)需要外網才能訪問,為了方便大家使用。這裏我已經下載下來附在了這篇博客裏面。如果不需要這些語句,可以直接通過網頁右邊的目錄跳躍到下一章進行瀏覽

    建表語句

    ########################################
    # MySQL Crash Course MYSQL必知必會建表語句
    # http://www.forta.com/books/0672327120/
    # 提供者博客園:后青春期的Keats 複製請註明出處
    ########################################
    
    
    ########################
    # Create customers table
    ########################
    CREATE TABLE customers
    (
      cust_id      int       NOT NULL AUTO_INCREMENT,
      cust_name    char(50)  NOT NULL ,
      cust_address char(50)  NULL ,
      cust_city    char(50)  NULL ,
      cust_state   char(5)   NULL ,
      cust_zip     char(10)  NULL ,
      cust_country char(50)  NULL ,
      cust_contact char(50)  NULL ,
      cust_email   char(255) NULL ,
      PRIMARY KEY (cust_id)
    ) ENGINE=InnoDB;
    
    #########################
    # Create orderitems table
    #########################
    CREATE TABLE orderitems
    (
      order_num  int          NOT NULL ,
      order_item int          NOT NULL ,
      prod_id    char(10)     NOT NULL ,
      quantity   int          NOT NULL ,
      item_price decimal(8,2) NOT NULL ,
      PRIMARY KEY (order_num, order_item)
    ) ENGINE=InnoDB;
    
    
    #####################
    # Create orders table
    #####################
    CREATE TABLE orders
    (
      order_num  int      NOT NULL AUTO_INCREMENT,
      order_date datetime NOT NULL ,
      cust_id    int      NOT NULL ,
      PRIMARY KEY (order_num)
    ) ENGINE=InnoDB;
    
    #######################
    # Create products table
    #######################
    CREATE TABLE products
    (
      prod_id    char(10)      NOT NULL,
      vend_id    int           NOT NULL ,
      prod_name  char(255)     NOT NULL ,
      prod_price decimal(8,2)  NOT NULL ,
      prod_desc  text          NULL ,
      PRIMARY KEY(prod_id)
    ) ENGINE=InnoDB;
    
    ######################
    # Create vendors table
    ######################
    CREATE TABLE vendors
    (
      vend_id      int      NOT NULL AUTO_INCREMENT,
      vend_name    char(50) NOT NULL ,
      vend_address char(50) NULL ,
      vend_city    char(50) NULL ,
      vend_state   char(5)  NULL ,
      vend_zip     char(10) NULL ,
      vend_country char(50) NULL ,
      PRIMARY KEY (vend_id)
    ) ENGINE=InnoDB;
    
    ###########################
    # Create productnotes table
    ###########################
    CREATE TABLE productnotes
    (
      note_id    int           NOT NULL AUTO_INCREMENT,
      prod_id    char(10)      NOT NULL,
      note_date datetime       NOT NULL,
      note_text  text          NULL ,
      PRIMARY KEY(note_id),
      FULLTEXT(note_text)
    ) ENGINE=MyISAM;
    
    
    #####################
    # Define foreign keys
    #####################
    ALTER TABLE orderitems ADD CONSTRAINT fk_orderitems_orders FOREIGN KEY (order_num) REFERENCES orders (order_num);
    ALTER TABLE orderitems ADD CONSTRAINT fk_orderitems_products FOREIGN KEY (prod_id) REFERENCES products (prod_id);
    ALTER TABLE orders ADD CONSTRAINT fk_orders_customers FOREIGN KEY (cust_id) REFERENCES customers (cust_id);
    ALTER TABLE products ADD CONSTRAINT fk_products_vendors FOREIGN KEY (vend_id) REFERENCES vendors (vend_id);

    數據語句

    ########################################
    # MySQL Crash Course MYSQL必知必會數據語句
    # http://www.forta.com/books/0672327120/
    # 提供者博客園:后青春期的Keats 複製請註明出處
    ########################################
    
    
    ##########################
    # Populate customers table
    ##########################
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)
    VALUES(10001, 'Coyote Inc.', '200 Maple Lane', 'Detroit', 'MI', '44444', 'USA', 'Y Lee', 'ylee@coyote.com');
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact)
    VALUES(10002, 'Mouse House', '333 Fromage Lane', 'Columbus', 'OH', '43333', 'USA', 'Jerry Mouse');
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)
    VALUES(10003, 'Wascals', '1 Sunny Place', 'Muncie', 'IN', '42222', 'USA', 'Jim Jones', 'rabbit@wascally.com');
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)
    VALUES(10004, 'Yosemite Place', '829 Riverside Drive', 'Phoenix', 'AZ', '88888', 'USA', 'Y Sam', 'sam@yosemite.com');
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact)
    VALUES(10005, 'E Fudd', '4545 53rd Street', 'Chicago', 'IL', '54545', 'USA', 'E Fudd');
    
    
    ########################
    # Populate vendors table
    ########################
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1001,'Anvils R Us','123 Main Street','Southfield','MI','48075', 'USA');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1002,'LT Supplies','500 Park Street','Anytown','OH','44333', 'USA');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1003,'ACME','555 High Street','Los Angeles','CA','90046', 'USA');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1004,'Furball Inc.','1000 5th Avenue','New York','NY','11111', 'USA');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1005,'Jet Set','42 Galaxy Road','London', NULL,'N16 6PS', 'England');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1006,'Jouets Et Ours','1 Rue Amusement','Paris', NULL,'45678', 'France');
    
    
    #########################
    # Populate products table
    #########################
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('ANV01', 1001, '.5 ton anvil', 5.99, '.5 ton anvil, black, complete with handy hook');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('ANV02', 1001, '1 ton anvil', 9.99, '1 ton anvil, black, complete with handy hook and carrying case');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('ANV03', 1001, '2 ton anvil', 14.99, '2 ton anvil, black, complete with handy hook and carrying case');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('OL1', 1002, 'Oil can', 8.99, 'Oil can, red');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('FU1', 1002, 'Fuses', 3.42, '1 dozen, extra long');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('SLING', 1003, 'Sling', 4.49, 'Sling, one size fits all');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('TNT1', 1003, 'TNT (1 stick)', 2.50, 'TNT, red, single stick');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('TNT2', 1003, 'TNT (5 sticks)', 10, 'TNT, red, pack of 10 sticks');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('FB', 1003, 'Bird seed', 10, 'Large bag (suitable for road runners)');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('FC', 1003, 'Carrots', 2.50, 'Carrots (rabbit hunting season only)');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('SAFE', 1003, 'Safe', 50, 'Safe with combination lock');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('DTNTR', 1003, 'Detonator', 13, 'Detonator (plunger powered), fuses not included');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('JP1000', 1005, 'JetPack 1000', 35, 'JetPack 1000, intended for single use');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('JP2000', 1005, 'JetPack 2000', 55, 'JetPack 2000, multi-use');
    
    
    
    #######################
    # Populate orders table
    #######################
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20005, '2005-09-01', 10001);
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20006, '2005-09-12', 10003);
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20007, '2005-09-30', 10004);
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20008, '2005-10-03', 10005);
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20009, '2005-10-08', 10001);
    
    
    ###########################
    # Populate orderitems table
    ###########################
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20005, 1, 'ANV01', 10, 5.99);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20005, 2, 'ANV02', 3, 9.99);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20005, 3, 'TNT2', 5, 10);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20005, 4, 'FB', 1, 10);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20006, 1, 'JP2000', 1, 55);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20007, 1, 'TNT2', 100, 10);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20008, 1, 'FC', 50, 2.50);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20009, 1, 'FB', 1, 10);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20009, 2, 'OL1', 1, 8.99);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20009, 3, 'SLING', 1, 4.49);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20009, 4, 'ANV03', 1, 14.99);
    
    #############################
    # Populate productnotes table
    #############################
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(101, 'TNT2', '2005-08-17',
    'Customer complaint:
    Sticks not individually wrapped, too easy to mistakenly detonate all at once.
    Recommend individual wrapping.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(102, 'OL1', '2005-08-18',
    'Can shipped full, refills not available.
    Need to order new can if refill needed.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(103, 'SAFE', '2005-08-18',
    'Safe is combination locked, combination not provided with safe.
    This is rarely a problem as safes are typically blown up or dropped by customers.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(104, 'FC', '2005-08-19',
    'Quantity varies, sold by the sack load.
    All guaranteed to be bright and orange, and suitable for use as rabbit bait.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(105, 'TNT2', '2005-08-20',
    'Included fuses are short and have been known to detonate too quickly for some customers.
    Longer fuses are available (item FU1) and should be recommended.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(106, 'TNT2', '2005-08-22',
    'Matches not included, recommend purchase of matches or detonator (item DTNTR).'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(107, 'SAFE', '2005-08-23',
    'Please note that no returns will be accepted if safe opened using explosives.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(108, 'ANV01', '2005-08-25',
    'Multiple customer returns, anvils failing to drop fast enough or falling backwards on purchaser. Recommend that customer considers using heavier anvils.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(109, 'ANV03', '2005-09-01',
    'Item is extremely heavy. Designed for dropping, not recommended for use with slings, ropes, pulleys, or tightropes.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(110, 'FC', '2005-09-01',
    'Customer complaint: rabbit has been able to detect trap, food apparently less effective now.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(111, 'SLING', '2005-09-02',
    'Shipped unassembled, requires common tools (including oversized hammer).'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(112, 'SAFE', '2005-09-02',
    'Customer complaint:
    Circular hole in safe floor can apparently be easily cut with handsaw.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(113, 'ANV01', '2005-09-05',
    'Customer complaint:
    Not heavy enough to generate flying stars around head of victim. If being purchased for dropping, recommend ANV02 or ANV03 instead.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(114, 'SAFE', '2005-09-07',
    'Call from individual trapped in safe plummeting to the ground, suggests an escape hatch be added.
    Comment forwarded to vendor.'
    );

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

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

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

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

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

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

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

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

    原文地址

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

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

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

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

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

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

    理清整理思路

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

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

    Hazel 是什麼?

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

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

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

    Hazel 具體能做什麼?

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

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

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

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

    Hazel 的安裝

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

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

    Hazel 的界面和基礎應用

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

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

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

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

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

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

    標記最新下載文件

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

    標記舊文件

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

    標記待處理文件

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

    刪除 .torrent 文件

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

    自動移動手機截屏文件

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

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

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

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

    附上桌面整理后截圖:

    Hazel 中級應用

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

    自動解壓

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

    有三點需要為大家說明:

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

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

    自動清理緩存

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

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

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

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

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

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

    更多用法

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

    其他功能

    管理垃圾箱

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

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

    同步規則

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

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

    Hazel 的下載

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

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

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

    【其他文章推薦】

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

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

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

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

  • 『嗨威說』算法設計與分析 – PTA 程序存儲問題 / 刪數問題 / 最優合併問題(第四章上機實踐報告)

    『嗨威說』算法設計與分析 – PTA 程序存儲問題 / 刪數問題 / 最優合併問題(第四章上機實踐報告)

    本文索引目錄:

    一、PTA實驗報告題1 : 程序存儲問題

      1.1  實踐題目

      1.2  問題描述

      1.3  算法描述

      1.4  算法時間及空間複雜度分析

    二、PTA實驗報告題2 : 刪數問題

      2.1  實踐題目

      2.2  問題描述

      2.3  算法描述

      2.4  算法時間及空間複雜度分析

    三、PTA實驗報告題3 : 最優合併問題

      3.1  實踐題目

      3.2  問題描述

      3.3  算法描述

      3.4  算法時間及空間複雜度分析

    四、實驗心得體會(實踐收穫及疑惑)

     

     

    一、PTA實驗報告題1 : 程序存儲問題

      1.1  實踐題目:

     

      1.2  問題描述:

          題意是,題干給定磁盤總容量和各個文件的佔用空間,詢問該磁盤最多能裝幾個文件。

     

      1.3  算法描述:

          簽到題,只需要將各個文件從小到大排序,並拿一個變量存儲已佔用的容量總和,進行對比即可得到結果。

    #include<bits/stdc++.h>
    #include<algorithm>
    using namespace std;
    #define MAXLENGTH 1000
    int interger[MAXLENGTH];
    int main()
    {
        int num,length;
        int sum = 0;
        int counter = 0;
        int m = 0;
        cin>>num>>length;
        for(int i=0;i<num;i++){
            cin>>interger[i];
        }
        sort(interger,interger+num);
        while(true){
            if(sum+interger[m]>length||counter==num)
                break;
            sum+=interger[m];
            counter++;
            m++;
        }
        cout<<counter<<endl;
        return 0;
     } 

     

      1.4  算法時間及空間複雜度分析:

         整體算法上看,輸入需要O(n)的時間進行輸入,最快用O(nlogn)的時間複雜度進行排序,使用O(n)的時間進行結果疊加,總時間複雜度為O(nlogn),時間複雜度花費在排序上。

        空間上,只需要一個臨時變量存儲當前佔用容量總和即可。

     

     

    二、PTA實驗報告題2 : 刪數問題

      2.1  實踐題目:

     

      2.2  問題描述:

        第二題題意是指,在給定的数字串以及可刪數個數的條件下,刪數指定k個數,得到的數是最小的。

     

      2.3  算法描述:

        首先,分析題目,刪數問題,可以用一個比較方便的函數,String類的erase函數,這個函數可以刪除字符串內的單個或多個字符,可以比較方便的處理刪數問題。

        第二,我們注意到這道題有個坑點,那就是前導零,我們需要注意100000,刪除1后結果應為0

        第三,確定我們的貪心策略:噹噹前的數,比后一位數大時,刪去當前的數。

        如:樣例178543

        用一個index時刻從頭往後掃,不滿足就后移。

     

         當滿足之後,刪除當前的值。

     

        得到17543,這時將index重新置0,並記錄已刪數+1,直到滿足最大刪數。以此類推,直接輸出string便是結果。

        AC代碼:

    #include<iostream>
    #include<algorithm>
    #include<string>
    using namespace std;
    #define MAXLENGTH 1005
    int main(){
        int k;
        string a;
        cin>>a>>k;
        int len = a.size();
        while(k>0){
            for(int i = 0;(i<a.size()-1);i++){
                if(a[i]>a[i+1])
                {
                    a.erase(i,1);
                    break;
                }
            }
            k--;
        }
        while(a.size()>1&&a[0]=='0'){
            a.erase(0,1);
        }
        cout<<a<<endl;
        return 0;
    }

     

      2.4  算法時間及空間複雜度分析:

        時間複雜度為O(n^2),即開銷在不斷的刪數和回溯到字符串頭的過程。

        空間複雜度需要一個String字符串長度,因此空間複雜度是O(n)

     

     

    三、PTA實驗報告題3 : 最優合併問題

      3.1  實踐題目:

     

      3.2  問題描述:

        該題目為:題目用 2 路合併算法將這k 個序列合併成一個序列,並且合併 2 個長度分別為m和n的序列需要m+n-1 次比較,輸出某段合併的最大比較次數和最小比較次數。

     

      3.3  算法描述:

        這道題算是哈夫曼算法的一道裸題,很容易解決,只需要使用優秀隊列不斷維護最小值或最大值即可。

        哈夫曼樹:是一顆最優二叉樹。給定n個權值作為n個恭弘=叶 恭弘子的結點,構造一棵二叉樹,若樹的帶權路徑長度達到最小,這棵樹則被稱為哈夫曼樹。

        因此本題根據哈夫曼算法,我們以最小比較次數為例:

     

     

         首先從隊列中選出兩個最小的數進行合併,並在隊列中刪除這兩個數,並將新合成數加入隊列中。

     

     

         再取最小的兩個數再進行合併,以此類推,得到最終的大數如下

        因此最小比較次數為:( 7 – 1 ) + ( 18 – 1 ) + ( 30 – 1 ) =  52,即為所得。最大比較次數也是同理。

       AC代碼如下:

    #include<bits/stdc++.h>
    using namespace std;
    priority_queue<int> Haff;
    priority_queue<int, vector<int>, greater<int> > Haff2;
    int n,ans1,ans2;
    
    int main()
    {
        cin>>n;
        for(int i = 0;i<n;i++)
        {
            int temp;
            cin>>temp;
            Haff.push(temp);
            Haff2.push(temp);
        }
    
        while(1)
        {
            if(Haff.size() == 1)
                break;
            int temp1 = Haff.top();
            Haff.pop();
            int temp2 = Haff.top();
            Haff.pop();
            Haff.push(temp1+temp2);
            ans1 += temp1+temp2-1;
        }
        
        while(1)
        {
            if(Haff2.size() == 1)
                break;
            int temp1 = Haff2.top();
            Haff2.pop();
            int temp2 = Haff2.top();
            Haff2.pop();
            Haff2.push(temp1+temp2);
            ans2 += temp1+temp2-1;
        }
        cout<<ans1<<" "<<ans2;
        return 0;
     } 

     

      3.4  算法時間及空間複雜度分析:

        由分析易知,雖然進行了兩次優先隊列維護,但是總的時間複雜度數量級是不變的,用O(n/2)的時間pop和push合成樹。在優先隊列裏面用紅黑樹對順序進行維護,時間複雜度為O(nlogn),最後將統計的結果輸出,總的時間複雜度為O(nlogn)。

       空間複雜度為兩棵紅黑樹,即T(2n) = O(n)。

     

     

    四、實驗心得體會(實踐收穫及疑惑):

        經過動態規劃的肆虐之後,貪心算法變得稍微容易很多,和三木小哥哥的合作很愉快,能夠很好較快及時的解決三道實踐問題,暫無太多的問題,繼續加油。

     

     

    如有錯誤不當之處,煩請指正。

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

    【其他文章推薦】

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

    ※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

    ※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

  • 【集合系列】- 紅黑樹實現分析

    【集合系列】- 紅黑樹實現分析

    一、故事的起因

    JDK1.8最重要的就是引入了紅黑樹的設計(當衝突的鏈表長度超過8個的時候),為什麼要這樣設計呢?好處就是避免在最極端的情況下衝突鏈表變得很長很長,在查詢的時候,效率會非常慢。

    • 紅黑樹查詢:其訪問性能近似於折半查找,時間複雜度O(logn);
    • 鏈表查詢:這種情況下,需要遍歷全部元素才行,時間複雜度O(n);

    本文主要是講解紅黑樹的實現,只有充分理解了紅黑樹,對於後面的分析才會更加順利。

    簡單的說,紅黑樹是一種近似平衡的二叉查找樹,其主要的優點就是“平衡“,即左右子樹高度幾乎一致,以此來防止樹退化為鏈表,通過這種方式來保障查找的時間複雜度為log(n)。

    關於紅黑樹的內容,網上給出的內容非常多,主要有以下幾個特性:

    • 1、每個節點要麼是紅色,要麼是黑色,但根節點永遠是黑色的;
    • 2、每個紅色節點的兩個子節點一定都是黑色;
    • 3、紅色節點不能連續(也即是,紅色節點的孩子和父親都不能是紅色);
    • 4、從任一節點到其子樹中每個恭弘=叶 恭弘子節點的路徑都包含相同數量的黑色節點;
    • 5、所有的恭弘=叶 恭弘節點都是是黑色的(注意這裏說恭弘=叶 恭弘子節點其實是上圖中的 NIL 節點);

    在樹的結構發生改變時(插入或者刪除操作),往往會破壞上述條件3或條件4,需要通過調整使得查找樹重新滿足紅黑樹的條件。

    二、調整方式

    上面已經說到當樹的結構發生改變時,紅黑樹的條件可能被破壞,需要通過調整使得查找樹重新滿足紅黑樹的條件。

    調整可以分為兩類:一類是顏色調整,即改變某個節點的顏色,這種比較簡單,直接將節點顏色進行轉換即可;另一類是結構調整,改變檢索樹的結構關係。結構調整主要包含兩個基本操作:左旋(Rotate Left)右旋(RotateRight)

    2.1、左旋

    左旋的過程是將p的右子樹繞p逆時針旋轉,使得p的右子樹成為p的父親,同時修改相關節點的引用,使左子樹的深度加1,右子樹的深度減1,通過這種做法來調整樹的穩定性。過程如下:

    以jdk1.8為例,打開HashMap的源碼部分,紅黑樹內部類TreeNode屬性分析:

    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
            //指向父節點的指針
            TreeNode<K,V> parent;
            //指向左孩子的指針
            TreeNode<K,V> left;
            //指向右孩子的指針
            TreeNode<K,V> right;
            //前驅指針,跟next屬性相反的指向
            TreeNode<K,V> prev;
            //是否為紅色節點
            boolean red;
            ......
    }

    左旋方法rotateLeft如下:

    /*
     * 左旋邏輯
     */
    static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                                  TreeNode<K,V> p) {
                //root:表示根節點
                //p:表示要調整的節點
                //r:表示p的右節點
                //pp:表示p的parent節點
                //rl:表示p的右孩子的左孩子節點
                TreeNode<K,V> r, pp, rl;
                //r判斷,如果r為空則旋轉沒有意義
                if (p != null && (r = p.right) != null) {
                    //多個等號的連接操作從右往左看,設置rl的父親為p
                    if ((rl = p.right = r.left) != null)
                        rl.parent = p;
                    //判斷p的父親,為空,為根節點,根節點的話就設置為黑色
                    if ((pp = r.parent = p.parent) == null)
                        (root = r).red = false;
                    //判斷p節點是左兒子還是右兒子
                    else if (pp.left == p)
                        pp.left = r;
                    else
                        pp.right = r;
                    r.left = p;
                    p.parent = r;
                }
                return root;
    }

    2.2、右旋

    了解了左旋轉之後,相應的就會有右旋,邏輯基本也是一樣,只是方向變了。右旋的過程是將p的左子樹繞p順時針旋轉,使得p的左子樹成為p的父親,同時修改相關節點的引用,使右子樹的深度加1,左子樹的深度減1,通過這種做法來調整樹的穩定性。實現過程如下:

    同樣的,右旋方法rotateRight如下:

    /*
     * 右旋邏輯
     */
    static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                                   TreeNode<K,V> p) {
                //root:表示根節點
                //p:表示要調整的節點
                //l:表示p的左節點
                //pp:表示p的parent節點
                //lr:表示p的左孩子的右孩子節點
                TreeNode<K,V> l, pp, lr;
                //l判斷,如果l為空則旋轉沒有意義
                if (p != null && (l = p.left) != null) {
                    //多個等號的連接操作從右往左看,設置lr的父親為p
                    if ((lr = p.left = l.right) != null)
                        lr.parent = p;
                    //判斷p的父親,為空,為根節點,根節點的話就設置為黑色
                    if ((pp = l.parent = p.parent) == null)
                        (root = l).red = false;
                    //判斷p節點是右兒子還是左兒子
                    else if (pp.right == p)
                        pp.right = l;
                    else
                        pp.left = l;
                    l.right = p;
                    p.parent = l;
                }
                return root;
    }

    三、操作示例介紹

    3.1、插入調整過程圖解

    3.2、刪除調整過程圖解

    3.3、查詢過程圖解

    四、總結

    至此,紅黑樹的實現就基本完成了,關於紅黑樹的結構,有很多種情況,情況也比較複雜,但是整體調整流程,基本都是先調整結構然後調整顏色,直到最後滿足紅黑樹特性要求為止。整篇文章,如果有理解不當之處,歡迎指正!

    五、參考

    1、
    2、

    作者:炸雞可樂
    出處:

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

    【其他文章推薦】

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

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

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

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