邁莫coding

邁莫coding 查看完整檔案

北京編輯  |  填寫畢業院校公眾號  |  邁莫coding 編輯填寫個人主網站
編輯

校招面試/go/公眾號同名文章,每天分享優質文章、大廠經驗、大廠面經,助力面試,是每個程序員值得關注的平臺。

個人動態

邁莫coding 發布了文章 · 3月2日

go語言十分鐘入門教程

在這里插入圖片描述

導語|這是一篇go基本語法快速入門文章,學習該文章時,默認讀者已安裝成功Golang環境,若環境未安裝成功,可自行百度。
原文地址:https://mp.weixin.qq.com/s/zvVzP0juPb4xk-GSuTNlOA

目錄

  • 環境安裝
  • 輸出語句
  • Go語言關鍵字
  • 類型

    • 數據類型
    • 變量定義

      • var關鍵字定義
      • 簡短模式
      • 多變量賦值
    • 常量
    • iota關鍵字
    • 運算符
  • 函數
  • 條件語句和循環語句

    • 條件語句
    • 循環語句
  • 數據

    • 數組
    • 字符串
    • 切片

      • 初始化slice
      • 示例
    • map字典
    • 結構體struct
  • 接口

    • 語法
    • 示例
  • 總結

環境安裝

安裝地址:[https://www.cnblogs.com/aaron...
](https://www.cnblogs.com/aaron...

輸出語句

無論學那一門語言,首先先學該語言的輸出語句。俗話說得好,輸出"Hello, World!",代表你入門成功?。?!

package main

import "fmt"

func main() {
  fmt.Println("Hello, World!")
}

接下來,一起學習go的基本語法,十分鐘解決完戰斗,走起?。?!

Go語言關鍵字

首先先認識一下Go語言中關鍵字,心里有個印象,讓初學者有個眼熟就行。記不住沒關系,我會在下面語法反復提到。在這里之所以提出來,就是讓你們看一下,看的看的就記住了。
在這里插入圖片描述

類型

數據類型

在 Go 編程語言中,數據類型用于聲明函數和變量。

數據類型的出現是為了把數據分成所需內存大小不同的數據,編程的時候需要用大數據的時候才需要申請大內存,就可以充分利用內存。

Go 語言按類別有以下幾種數據類型:
在這里插入圖片描述

變量定義

在數學概念中,變量表示沒有固定值且可改變的數。但從計算機系統實現角度來看,變量是一段或多段用來存儲數據的內存。

作為靜態類型語言,go變量總是有固定的數據類型,類型決定了變量內存的長度和存儲格式。我們只能修改變量值,無法改變類型。

var關鍵字定義

關鍵字var用于定義變量,和C不同,類型被放在變量后面。若顯式提供初始值,可省略變量類型,由編譯器推斷。

var X int // 自動初始化為零
var y = false // 自動推斷為bool的類型

可一次性定義多個變量,類型可相同也可不相同。

var x,y int
var a,b = 100, "abc"

簡短模式

變量定義時,除var關鍵字外,還可使用更加簡短的變量定義和初始化語法。

package main

import "fmt"

func main() {
  x := 10 // 使用 := 進行定義并初始化
  fmt.Println(x) // 輸出語句 10
}

使用簡短模式的一些限制:

  • 定義變量,同時顯式初始化。
  • 不能提供數據類型。
  • 只能用在函數內部,不能用在全局變量中。

多變量賦值

進行多變量賦值操作時,首先計算出等號右邊值,然后再依次完成賦值操作。

package main

import "fmt"

func main(){
  x, y := 10, 20
  x, y = y+3, x+2  // 先計算等號右邊值,然后再對x、y變量賦值
  fmt.Println(x, y) // 輸出語句  結果為:23 12
}

常量

常量表示運行時恒定不可改變的值,通常是一些字面量。使用常量就可用一個易于閱讀理解的標識符號來代替"魔法數字",也使得在調整常量值時,無須修改所有引用代碼。

常量值必須是編譯期可確定的字符、字符串、數字或布爾值??芍付ǔA款愋?,或由編譯器通過初始化推斷。

在go語言中,使用關鍵字const來定義常量。

const x, y int = 10, 20
const a,b = "邁莫coding", "歡迎小伙伴"

示例:

package main

import "fmt"

const (
   a, b string = "邁莫coding", "歡迎小伙伴"
)

func main() {
   fmt.Println(a,b) // 邁莫coding 歡迎小伙伴
}

iota關鍵字

Go中沒有明確意思上的enum(枚舉)定義,不過可以借用iota標識符實現一組自增常量值來實現枚舉類型。

const (
  a = iota // 0
  b        // 1
  c        // 2
)

變量a、b、c的值分別為0、1、2,原因是因為使用iota進行自增時,后續自增值按照序遞增。通俗點是每新增一行,iota值加一。

若在中途中斷iota自增,則必須顯示恢復,如下所示:

const (
  a = iota // 0
  b        // 1
  c = 100  // 100
  d        // 100 (與上一行常量值表達式一致)
  e = iota // 4 (恢復iota自增,計數包括c、d)
  f        // 5
)

運算符

運算符使用方式和其他語言基本一樣,在這里就不一一介紹了。

package main
import "fmt"
func main() {
   var a int = 21
   var b int = 10
   var c int
   c = a + b
   fmt.Println(c) // 31
   c = a - b
   fmt.Println(c) // 11
   c = a / b
   fmt.Println(c) // 2
   c = a % b
   fmt.Println(c) // 1
   a++
   fmt.Println(a) // 22
   a=21   // 為了方便測試,a 這里重新賦值為 21
   a--
   fmt.Println(a) // 20
}

函數

函數就是將復雜的算法過程分解為若干較小任務,進行拆分,易于維護。函數被設計成相對獨立,通過接收輸入參數完成一段算法指令,輸出或存儲相關結果。因此,函數還是代碼復用和測試的基本單元。

關鍵字func用于定義函數。

package main

import "fmt"

// 定義 Write函數 返回值有兩個,一個為name,一個age為
func Write() (name string, age int) {
   return "邁莫coding", 1
}

// 定義 Read函數
func Read(name string, age int) {
   fmt.Println(name, " 已經 ", age, " 歲了")
}

func main() {
   Read(Write()) // 邁莫coding  已經  1  歲了
}

條件語句和循環語句

條件語句

條件語句需要開發者通過指定一個或多個條件,并通過測試條件是否為 true 來決定是否執行指定語句,并在條件為 false 的情況在執行另外的語句。

下圖展示了程序語言中條件語句的結構:

在這里插入圖片描述

package main

import "fmt"

func main() {
  x := 3
  
  if x > 5 {
    fmt.Println("a")
  } else if x < 5 && x > 0 {
    fmt.Println("b")
  } else {
    fmt.Println("c")
  }
}

循環語句

在不少實際問題中有許多具有規律性的重復操作,因此在程序中就需要重復執行某些語句。

以下為大多編程語言循環程序的流程圖:

在這里插入圖片描述

package main

import "fmt"

func main() {
  for i := 0; i < 5; i++ {
    if i == 4 {
      continue
    } else if i == 5 {
      break
    }     
    fmt.Println(i)
  }
}

數據

數組

Go 語言提供了數組類型的數據結構。

數組是具有相同唯一類型的一組已編號且長度固定的數據項序列,這種類型可以是任意的原始類型例如整型、字符串或者自定義類型。

在這里插入圖片描述

package main

import "fmt"

func main() {
  var arr1 [4]int // 元素自動初始化為零
  fmt.Println(arr1) // [0 0 0 0]
  
  arr2 := [4]int{1,2} // 其他未初始化的元素為零
  fmt.Println(arr2) // [1 2 0 0]
  
  arr3 := [4]int{5, 3:10} // 可指定索引位置初始化
  fmt.Println(arr3) // [5 0 0 10]
  
  arr4 := [...]int{1,2,3} // 編譯器按照初始化值數量確定數組長度
  fmt.Println(arr4) // [1 2 3]
  
  t := len(arr4) // 內置函數len(數組名稱)表示數組的長度
  fmt.Println(t) // 3
}

字符串

字符串默認值不是nil,而是""。

package main

import "fmt"

func main() {
  var str string
  str = "邁莫coding歡迎小伙伴"
  fmt.Println(str)
}

切片

切片(slice)本身不是動態數組或動態指針。只是它內部采用數組存儲數據,當數組長度達到數組容量時,會進行動態擴容。

大白話就是切片功能和Java中的List集合類似,動態添加數據。不像數組(array)長度是固定的,需要事先知道數據長度。

初始化slice

x := make([]int, 1) // 通過make關鍵字進行slice初始化

示例

package main

import "fmt"

func main() {
    // 方式一
    a := make([]int,5) // 初始化長度為5的slice,默認值為零
    for i := 0; i <5; i++ {
       a = append(a, i)
    }
    a = append(a, 6)
    fmt.Println(a) // [0 0 0 0 0 0 1 2 3 4 6] 

    // 方式二    
    var a []int
    for i := 0; i < 5; i++ {
       a = append(a, i)
    }
    fmt.Println(a) // [0 1 2 3 4]
}

map字典

map字典也是使用頻率比較高的數據結構。將其作為語言內置類型,從運行時層面進行優化,可獲得更高效類型。

作為無序鍵值對集合,字典key值必須是支持相等運算符的數據類型,比如數字、字符串、指針、數組、結構體,以及對應接口類型。

map字典功能和Java中的map集合功能類似。

字典是應用類型,使用make函數或初始化表達語句來創建。

package main

import "fmt"

func main() {
   // 定義 變量strMap
   var strMap map[int]string
   // 進行初始化
   strMap = make(map[int]string)
   
   // 給map 賦值
   for i := 0; i < 5; i++ {
      strMap[i]  = "邁莫coding"
   }
   
   // 打印出map值
   for k, v := range strMap{
      fmt.Println(k, ":", v)
   }
  
  // 打印出map 長度
  fmt.Println(len(strMap))   
}

結構體struct

結構體(struct)將多個不同類型命名字段(field)序列打包成一個復合類型。

字段名必須唯一,可用"_"補位,支持使用自身指針類型成員。字段屬性為基本數據類型。

學過Java就可以進行類比,結構體struct可以類比為Java中的類,結構體struct中字段屬性可以類比為Java中類成員變量,結構體struct的方法可以類比為Java中類成員方法。

結構體(struct)語法如下:

type user struct {
  name string // 字段name 其數據類型為string
  age int // 字段age 其數據類型為int 
}

示例:

package main

import "fmt"

type user struct {
   name string
   age  int
}

// 結構體user Read方法
func (u *user) Read() string {
   return fmt.Sprintf("%s 已經 %d 歲了", u.name, u.age)
}

func main() {
   // 初始化
   u := &user{
      name: "邁莫coding",
      age:  1,
   }
   fmt.Println(u.name, "已經", u.age, "歲了")
   // 調用結構體user的 Read方法
   fmt.Println(u.Read()) // 邁莫coding 已經 1 歲了
}

接口

接口代表一個調用契約,是多個方法聲明的集合。

接口解除了類型依賴,有助于減少用戶可視方法,屏蔽內部結構和實現細節。在Go語言中,只要目標類型方法集內包含接口聲明的全部方法,就被視為實現了該接口,無須做顯示聲明。當然,目標類型可實現多個接口。

大白話,接口是多個方法聲明的集合,若一個struct類實現接口中所有方法,即表示該類實現了指定接口。

語法

type user interface{
}

示例

package main

import "fmt"

// 定義接口 包含公共方法
type user interface{
  talking()
}

// 定義一個struct類
type memo struct{
}

// 實現接口user中方法talking
func (m *memo) talking() {
  fmt.Println("邁莫coding歡迎您...")
}

func main() {
  mm := memo{}
  mm.talking()
}

總結

文章介紹了Go語言的基本語法,適合零小白查看,使其快速上手Go語言項目開發,但文章畢竟是快速入門,有許多沒講過的知識點,需讀者自行學習,也可關注我,和我一起學習Go語言。

文章也會持續更新,可以微信搜索「 邁莫coding 」第一時間閱讀。每天分享優質文章、大廠經驗、大廠面經,助力面試,是每個程序員值得關注的平臺。
查看原文

贊 0 收藏 0 評論 0

邁莫coding 發布了文章 · 2月26日

深度解析Golang sync.Once源碼

目錄

  • 什么是sync.Once
  • 如何使用sync.Once
  • 源碼分析
文章始發于公眾號【邁莫coding】https://mp.weixin.qq.com/s/b89PmljELaPaVuLw-YIQKg

什么是sync.Once

Once 可以用來執行且僅僅執行一次動作,常常用于單例對象的初始化場景。

Once 常常用來初始化單例資源,或者并發訪問只需初始化一次的共享資源,或者在測試的時候初始化一次測試資源。

sync.Once 只暴露了一個方法 Do,你可以多次調用 Do 方法,但是只有第一次調用 Do 方法時 f 參數才會執行,這里的 f 是一個無參數無返回值的函數。

如何使用sync.Once

就拿我負責的一個項目來說,因為項目的配置是掛在第三方平臺上,所以在項目啟動時需要獲取資源配置,因為需要一個方法來保證配置僅此只獲取一次,因此,我們考慮使用 sync.Once 來獲取資源。這樣的話,可以防止在其他地方調用獲取資源方法,該方法僅此執行一次。

下面我簡單寫個Demo來演示一個sync.Once如何使用

package main
import (
   "fmt"
   "sync"
)
var once sync.Once
var con string
func main() {
   once.Do(func() {
      con = "hello Test once.Do"
   })
   fmt.Println(con)
}

代碼說明:

代碼的話比較簡單,就是通過調用Do方法,采用閉包方式,將字符串("hello Test once.Do")賦值給con,進而打印出值,這就是 sync.Once 的使用,比較容易上手。

但我們用一個方法或者框架時,如果不對其了如指掌,總有點不太靠譜,感覺心里不踏實。為此,我們來聊一聊 sync.Once 的源碼實現,讓他無處可遁。

源碼分析

接下來分析 sync.Do 究竟是如何實現的,它存儲在包sync下 once.go 文件中,源代碼如下:

// sync/once.go

type Once struct {
   done uint32 // 初始值為0表示還未執行過,1表示已經執行過
   m    Mutex 
}
func (o *Once) Do(f func()) {
   // 判斷done是否為0,若為0,表示未執行過,調用doSlow()方法初始化
   if atomic.LoadUint32(&o.done) == 0 {
      // Outlined slow-path to allow inlining of the fast-path.
      o.doSlow(f)
   }
}

// 加載資源
func (o *Once) doSlow(f func()) {
   o.m.Lock()
   defer o.m.Unlock()
   // 采用雙重檢測機制 加鎖判斷done是否為零
   if o.done == 0 {
      // 執行完f()函數后,將done值設置為1
      defer atomic.StoreUint32(&o.done, 1)
      // 執行傳入的f()函數
      f()
   }
}

接下來會分為兩大部分進行分析,第一部分為 Once 的結構體組成結構,第二部分為 Do 函數的實現原理,我會在代碼上加上注釋,保證用心閱讀完都有收獲。

結構體

type Once struct {
   done uint32 // 初始值為0表示還未執行過,1表示已經執行過
   m    Mutex 
}

首先定義一個struct結構體 Once ,里面存儲兩個成員變量,分別為 donem 。

done成員變量

  • 1表示資源未初始化,需要進一步初始化
  • 0表示資源已初始化,無需初始化,直接返回即可

m成員變量

  • 為了防止多個goroutine調用 doSlow() 初始化資源時,造成資源多次初始化,因此采用 Mutex 鎖機制來保證有且僅初始化一次

Do

func (o *Once) Do(f func()) {
   // 判斷done是否為0,若為0,表示未執行過,調用doSlow()方法初始化
   if atomic.LoadUint32(&o.done) == 0 {
      // Outlined slow-path to allow inlining of the fast-path.
      o.doSlow(f)
   }
}

// 加載資源
func (o *Once) doSlow(f func()) {
   o.m.Lock()
   defer o.m.Unlock()
   // 采用雙重檢測機制 加鎖判斷done是否為零
   if o.done == 0 {
      // 執行完f()函數后,將done值設置為1
      defer atomic.StoreUint32(&o.done, 1)
      // 執行傳入的f()函數
      f()
   }

調用 Do 函數時,首先判斷done值是否為0,若為1,表示傳入的匿名函數 f() 已執行過,無需再次執行;若為0,表示傳入的匿名函數 f() 還未執行過,則調用 doSlow() 函數進行初始化。

在 doSlow() 函數中,若并發的goroutine進入該函數中,為了保證僅有一個goroutine執行 f() 匿名函數。為此,需要加互斥鎖保證只有一個goroutine進行初始化,同時采用了雙檢查的機制(double-checking),再次判斷 o.done 是否為 0,如果為 0,則是第一次執行,執行完畢后,就將 o.done 設置為 1,然后釋放鎖。

即使此時有多個 goroutine 同時進入了 doSlow 方法,因為雙檢查的機制,后續的 goroutine 會看到 o.done 的值為 1,也不會再次執行 f。

這樣既保證了并發的 goroutine 會等待 f 完成,而且還不會多次執行 f。

文章也會持續更新,可以微信搜索「 邁莫coding 」第一時間閱讀。每天分享優質文章、大廠經驗、大廠面經,助力面試,是每個程序員值得關注的平臺。
查看原文

贊 0 收藏 0 評論 0

邁莫coding 發布了文章 · 2月26日

深度解析go context實現原理及其源碼

目錄

  • Context 基本使用方法
  • Context 使用場景
  • valueCtx

    • 使用示例
    • 結構體
    • WithValue
  • cancleCtx

    • 使用示例
    • 結構體
    • WitCancel
  • WithTimeout
  • WithDeadline

    • 使用示例
    • WithDeadline
  • 總結

Context 基本使用方法

首先,我們來看一下 Context 接口包含哪些方法,這些方法都是干什么用的。

包 context 定義了 Context 接口,Context 的具體實現包括 4 個方法,分別是Deadline、Done、Err 和 Value,如下所示:

type Context interface { 
  Deadline() (deadline time.Time, ok bool)
  Done() <-chan struct{} Err()
  error 
  Value(key interface{}) interface{}
}

Deadline 方法會返回這個 Context 被取消的截止日期。如果沒有設置截止日期,ok 的值是 false。后續每次調用這個對象的 Deadline 方法時,都會返回和第一次調用相同的結果。

Done 方法返回一個 Channel 對象。在 Context 被取消時,此 Channel 會被 close,如果沒被取消,可能會返回 nil。后續的 Done 調用總是返回相同的結果。當 Done 被 close 的時候,你可以通過 ctx.Err 獲取錯誤信息。Done 這個方法名其實起得并不好,因為名字太過籠統,不能明確反映 Done 被 close 的原因,因為 cancel、timeout、deadline 都可能導致 Done 被 close,不過,目前還沒有一個更合適的方法名稱。

關于 Done 方法,你必須要記住的知識點就是:如果 Done 沒有被 close,Err 方法返回 nil;如果 Done 被 close,Err 方法會返回 Done 被 close 的原因。

Context使用場景

  • 上下文信息傳遞 (request-scoped),比如處理 http 請求、在請求處理鏈路上傳遞信息
  • 控制子 goroutine 的運行
  • 超時控制的方法調用
  • 可以取消的方法調用

valueCtx

valueCtx 是基于 parent Context 生成一個新的 Context,保存了一個key-value鍵值對。它主要用來傳遞上下文信息。

使用示例

ctx := context.Background()
ctx = context.WithValue(ctx, "key1", "0001")
ctx = context.WithValue(ctx, "key2", "0001")
ctx = context.WithValue(ctx, "key3", "0001")
ctx = context.WithValue(ctx, "key4", "0004")
fmt.Println(ctx.Value("key1")) // 0001

查找過程如圖所示:

在這里插入圖片描述

結構體

type valueCtx struct {
   Context  // parent Context
   key, val interface{}  // key-value
}

func (c *valueCtx) Value(key interface{}) interface{} {
   // 若key值 等于 當前valueCtx存儲的key值 
   // 則取出其value并返回
   if c.key == key {
      return c.val
   }
   // 否則遞歸調用valueCtx中Value方法,獲取其parent Context中存儲的key-value
   return c.Context.Value(key)
}

通過觀察 valueCtx 結構體,它利用一個 Context 變量表示其父節點的 context ,這樣 valueCtx 也繼承了父節點的所有信息;并且它持有一個 key-value 鍵值對,說明它還可以攜帶額外的信息。它還覆蓋了 Value 方法,優先從自己的存儲中檢查這個 key,不存在的話會從 parent 中繼續檢查。

WithValue

WithValue 就是向 context 中添加鍵值對:

func WithValue(parent Context, key, val interface{}) Context {
   if parent == nil {
      panic("cannot create context from nil parent")
   }
   if key == nil {
      panic("nil key")
   }
   if !reflectlite.TypeOf(key).Comparable() {
      panic("key is not comparable")
   }
   return &valueCtx{parent, key, val}
}

通過代碼可以看出,向 context 中添加鍵值對并不是在原 context 基礎上添加的,而是新建一個 valueCtx 子節點,將原 context 作為父節點。以此類推,就會形成一個 context 鏈。在查找過程中,如果當前 valueCtx 不存在key值,還會向 parent Context 去查找,如果 parent 還是 valueCtx 的話,還是遵循相同的原則:valueCtx 會嵌入 parent,所以還是會查找 parent 的 Value 方法的。

在這里插入圖片描述

cancleCtx

在我們開發過程中,我們常常會遇到一些場景,需要主動取消長時間的任務或者中止任務,這個時候就可以使用cancelCtx。通過調用cancel函數就可中止goroutine,進而去釋放所占用的資源。

需要注意的是,不是只有中途中止任務時才調用cancel函數,只要任務執行完畢后,就需要調用 cancel,這樣,這個 Context 才能釋放它的資源(通知它的 children 處理 cancel,從它的 parent 中把自己移除,甚至釋放相關的 goroutine)。

使用示例

func main() {
  // gen 在單獨的 goroutine 中生成整數 然后將它們發送到返回的管道
  gen := func(ctx context.Context) <-chan int {
     dst := make(chan int)
     n := 1
     go func() {
        for {
           select {
           case <-ctx.Done():
              return // returning not to leak the goroutine
           case dst <- n:
              n++
           }
        }
     }()
     return dst
  }
  ctx, cancel := context.WithCancel(context.Background())
  // 代碼完畢后調用cancel函數釋放goroutine所占用的資源
  defer cancel() // cancel when we are finished consuming integers
  // 遍歷循環獲取管道中的值
  for n := range gen(ctx) {
     fmt.Println(n)
     if n == 5 {
        break
     }
  }
}

創建一個 gen函數,在gen函數中創建一個goroutine,專門用來生成整數,然后將他們發送到返回的管道。通過 context.WithCancel 創建可取消的 context ,最后遍歷循環獲取管道中值,當n的值為5時,退出循環,結束進程。最后調用cancel函數釋放goroutine所占用的資源。

結構體

type cancelCtx struct {
    Context
    mu       sync.Mutex            
    done     chan struct{}         
    children map[canceler]struct{} 
    err      error                 
}

cancelCtx和valueCtx類似,結構體中都有一個Context作為其父節點;變量done表示關閉信號傳遞;變量children表示當前節點所擁有的子節點,err用于存儲錯誤信息表示任務結束的原因。

接下來,看看cancelCtx實現的方法:

type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

func (c *cancelCtx) Done() <-chan struct{} {
   c.mu.Lock()
   if c.done == nil {
      c.done = make(chan struct{})
   }
   d := c.done
   c.mu.Unlock()
   return d
}

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
   if err == nil {
      panic("context: internal error: missing cancel error")
   }
   c.mu.Lock()
   if c.err != nil {
      c.mu.Unlock()
      return // already canceled
   }
   c.err = err
   // 設置一個關閉的channel或者將done channel關閉,用以發送關閉信號
   if c.done == nil {
      c.done = closedchan
   } else {
      close(c.done)
   }
   // 遍歷循環將字節點context取消
   for child := range c.children {
      // NOTE: acquiring the child's lock while holding parent's lock.
      child.cancel(false, err)
   }
   c.children = nil
   c.mu.Unlock()
   if removeFromParent {
      // 將當前context節點從父節點上移除
      removeChild(c.Context, c)
   }
}

cancelCtx結構體實現Done和cancel方法,Done方法實現了將done初始化。cancel方法用于將當前節點從父節點上移除以及移除當前節點下的 所有子節點。

cancelCtx 被取消時,它的 Err 字段就是下面這個 Canceled 錯誤:

var Canceled = errors.New("context canceled")

WithCancel

WithCancel函數用來創建一個可取消的context,即cancelCtx類型的context。

WithCancel函數返回值有兩個,一個為parent 的副本Context,另一個為觸發取消操作的CancelFunc。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
   if parent == nil {
      panic("cannot create context from nil parent")
   }
   c := newCancelCtx(parent)
   propagateCancel(parent, &c) // 把c朝上傳播
   return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
   // 將parent作為父節點context生成一個新的子節點
   return cancelCtx{Context: parent}
}

func propagateCancel(parent Context, child canceler) {
   done := parent.Done()
   if done == nil {
      return // parent is never canceled
   }
   
   select {
   case <-done:
      // parent is already canceled
      child.cancel(false, parent.Err())
      return
   default:
   }
   
   // 獲取最近的類型為cancelCtx的祖先節點
   if p, ok := parentCancelCtx(parent); ok {
      p.mu.Lock()
      if p.err != nil {
         // parent has already been canceled
         child.cancel(false, p.err)
      } else {
         if p.children == nil {
            p.children = make(map[canceler]struct{})
         }
         // 將當前子節點加入最近cancelCtx祖先節點的children中
         p.children[child] = struct{}{}
      }
      p.mu.Unlock()
   } else {
      atomic.AddInt32(&goroutines, +1)
      go func() {
         select {
         case <-parent.Done():
            child.cancel(false, parent.Err())
         case <-child.Done():
         }
      }()
   }
}

調用 WithCancel函數時,首先會調用 newCancelCtx函數創建一個以parent作為父節點的context。然后調用propagateCancel函數,用來建立當前context節點與parent節點之間的關系。

在propagateCancel函數中,如果parent節點為nil,說明parent以上的路徑沒有可取消的cancelCtx,則不需要處理。

否則通過parentCancelCtx函數過去當前節點最近的類型為cancelCtx的祖先節點,首先需要判斷該祖先節點是否被取消,若已被取消就取消當前節點;否則將當前節點加入祖先節點的children列表中。

否則的話,則需要新起一個 goroutine,由它來監聽 parent 的 Done 是否已關閉。一旦parent.Done()返回的channel關閉,即context鏈中某個祖先節點context被取消,則將當前context也取消。

WithTimeout

WithTimeout 其實是和 WithDeadline 一樣,只不過一個參數是超時時間,一個參數是截止時間。超時時間加上當前時間,其實就是截止時間,因此,WithTimeout 的實現是:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { 
  // 當前時間+timeout就是deadline
  return WithDeadline(parent, time.Now().Add(timeout))
}

WithDeadline

WithDeadline 會返回一個 parent 的副本,并且設置了一個不晚于參數 d 的截止時間,類型為 timerCtx(或者是 cancelCtx)。

如果它的截止時間晚于 parent 的截止時間,那么就以 parent 的截止時間為準,并返回一個類型為 cancelCtx 的 Context,因為 parent 的截止時間到了,就會取消這個 cancelCtx。

如果當前時間已經超過了截止時間,就直接返回一個已經被 cancel 的 timerCtx。否則就會啟動一個定時器,到截止時間取消這個 timerCtx。

綜合起來,timerCtx 的 Done 被 Close 掉,主要是由下面的某個事件觸發的:

  • 截止時間到了
  • cancel 函數被調用
  • parent 的 Done 被 close

使用示例

func main() {
  d := time.Now().Add(time.Second * 3)
  ctx, cancel := context.WithDeadline(context.Background(), d)
  defer cancel()
  select {
  case <-time.After(3 * time.Second):
     fmt.Println("overslept")
  case <-ctx.Done():
     fmt.Println(ctx.Err())
  }
}

WithDeadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
   if parent == nil {
      panic("cannot create context from nil parent")
   }
   // 如果parent的截止時間更早,直接返回一個cancelCtx即可
   if cur, ok := parent.Deadline(); ok && cur.Before(d) {
      return WithCancel(parent)
   }
   c := &timerCtx{
      cancelCtx: newCancelCtx(parent),
      deadline:  d,
   }
   // 建立新建context與可取消context祖先節點的取消關聯關系
   propagateCancel(parent, c)
   dur := time.Until(d)
   if dur <= 0 { //當前時間已經超過了截止時間,直接cancel
      c.cancel(true, DeadlineExceeded) 
      return c, func() { c.cancel(false, Canceled) }
   }
   c.mu.Lock()
   defer c.mu.Unlock()
   if c.err == nil {
      // 設置一個定時器,到截止時間后取消
      c.timer = time.AfterFunc(dur, func() {
         c.cancel(true, DeadlineExceeded)
      })
   }
   return c, func() { c.cancel(true, Canceled) }
}

調用 WithDeadline函數,首先判斷parent的截止時間是否早于當前timerCtx,若為true的話,直接返回一個cancelCtx即可。否則需要調用propagateCancel函數建議新建context與可取消context祖先節點的取消關聯關系,建立關聯關系之后,若當前時間已經超過截止時間后,直接cancel。否則的話,需設置一個定時器,到截止時間后取消。

總結

context主要用于父子任務之間的同步取消信號,本質上是一種協程調度的方式。另外在使用context時有兩點值得注意:上游任務僅僅使用context通知下游任務不再需要,但不會直接干涉和中斷下游任務的執行,由下游任務自行決定后續的處理操作,也就是說context的取消操作是無侵入的;context是線程安全的,因為context本身是不可變的(immutable),因此可以放心地在多個協程中傳遞使用。

到這里,Context 的源碼已解讀完畢,希望對您有收獲,咱們下期再見。

文章也會持續更新,可以微信搜索「 邁莫coding 」第一時間閱讀。每天分享優質文章、大廠經驗、大廠面經,助力面試,是每個程序員值得關注的平臺。
查看原文

贊 0 收藏 0 評論 0

邁莫coding 發布了文章 · 2月23日

深度解析sync WaitGroup源碼及其實現原理

目錄

  • WaitGroup介紹
  • WaitGroup的實現

    • Add
    • Done
    • Wait

WaitGroup介紹

waitGroup ,也是在go語言并發中比較常用的語法,所以在這里我們一起剖析 waitGroup 的使用方式及其源碼解讀。

WaitGroup 也是sync 包下一份子,用來解決任務編排的一個并發原語。它主要解決了并發-等待問題:比如現在有三個goroutine,分別為goroutineA,goroutineB,goroutineC,而goroutineA需要等待goroutineBgoroutineC這一組goroutine全部執行完畢后,才可以執行后續業務邏輯。此時就可以使用 WaitGroup 輕松解決。

在這個場景中,goroutineA為主goroutine,goroutineBgoroutineC為子goroutine。goroutineA則需要在檢查點(checkout point) 等待goroutineBgoroutineC全部執行完畢,如果在執行任務的goroutine還沒全部完成,那么goroutineA就會阻塞在檢查點,直到所有goroutine都完成后才能繼續執行。

代碼實現:

package main

import (
  "fmt"
  "sync"
)

func goroutineB(wg *sync.WaitGroup) {
  defer wg.Done()
  fmt.Println("goroutineB Execute")
  time.Sleep(time.Second)
}

func goroutineC(wg *sync.WaitGroup) {
  defer wg.Done()
  fmt.Println("goroutineC Execute")
  time.Sleep(time.Second)
}

func main() {
  var wg sync.WaitGroup
  wg.Add(2)
  go goroutineB(&wg)
  go goroutineC(&wg)
  wg.Wait()
  fmt.Println("goroutineB and goroutineC finished...")
}

運行結果:

goroutineC Execute
goroutineB Execute
goroutineB and goroutineC finished...

上述就是WaitGroup 的簡單操作,它的語法也是比較簡單,提供了三個方法,如下所示:

func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
  • Add:用來設置WaitGroup的計數值(子goroutine的數量)
  • Done:用來將WaitGroup的計數值減1,起始就是調用Add(-1)
  • Wait:調用這個方法的goroutine會一直阻塞,直到WaitGroup的技術值變為0

接下來,我們進行剖析 WaitGroup 的源碼實現,讓其無處可遁,它源碼比較少,除去注釋,也就幾十行,對新手來說也是一種不錯的選擇。

WaitGroup的實現

首先,我們看看 WaitGroup 的數據結構,它包括了一個noCopy 的輔助字段,一個具有復合意義的state1字段。

  • noCopy 的輔助字段:主要就是輔助 vet 工具檢查是否通過 copy 賦值這個 WaitGroup 實例。我會在后面和你詳細分析這個字段
  • state1:具有復合意義的字段,包含WaitGroup計數值,阻塞在檢查點的主gooutine和信號量
type WaitGroup struct {
    // 避免復制使用的一個技巧,可以告訴vet工具違反了復制使用的規則
    noCopy noCopy
    // 64bit(8bytes)的值分成兩段,高32bit是計數值,低32bit是waiter的計數
    // 另外32bit是用作信號量的
    // 因為64bit值的原子操作需要64bit對齊,但是32bit編譯器不支持,所以數組中的元素在不同的架構中不一樣,具體處理看下面的方法
    // 總之,會找到對齊的那64bit作為state,其余的32bit做信號量
    state1 [3]uint32
}


// 得到state的地址和信號量的地址
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
    if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
        // 如果地址是64bit對齊的,數組前兩個元素做state,后一個元素做信號量
        return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
    } else {
        // 如果地址是32bit對齊的,數組后兩個元素用來做state,它可以用來做64bit的原子操作,第一個元素32bit用來做信號量
        return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
    }
}    

因為對 64 位整數的原子操作要求整數的地址是 64 位對齊的,所以針對 64 位和 32 位環境的 state 字段的組成是不一樣的。

在 64 位環境下,state1 的第一個元素是 waiter 數,第二個元素是 WaitGroup 的計數值,第三個元素是信號量。

在這里插入圖片描述

在 32 位環境下,如果 state1 不是 64 位對齊的地址,那么 state1 的第一個元素是信號量,后兩個元素分別是 waiter 數和計數值。

在這里插入圖片描述

接下里,我們一一看 Add 方法、 Done 方法、 Wait 方法的實現原理。

Add

Add方法實現思路:

Add方法主要操作的state1字段中計數值部分。當Add方法被調用時,首先會將delta參數值左移32位(計數值在高32位),然后內部通過原子操作將這個值加到計數值上。需要注意的是,delta的取值范圍可正可負,因為調用Done()方法時,內部通過Add(-1)方法實現的。

代碼實現如下:

func (wg *WaitGroup) Add(delta int) {
  // statep表示wait數和計數值
  // 低32位表示wait數,高32位表示計數值
   statep, semap := wg.state()
   // uint64(delta)<<32 將delta左移32位
    // 因為高32位表示計數值,所以將delta左移32,增加到技術上
   state := atomic.AddUint64(statep, uint64(delta)<<32)
   // 當前計數值
   v := int32(state >> 32)
   // 阻塞在檢查點的wait數
   w := uint32(state)
   if v > 0 || w == 0 {
      return
   }
   
   // 如果計數值v為0并且waiter的數量w不為0,那么state的值就是waiter的數量
    // 將waiter的數量設置為0,因為計數值v也是0,所以它們倆的組合*statep直接設置為0即可。此時需要并喚醒所有的waiter
   *statep = 0
   for ; w != 0; w-- {
      runtime_Semrelease(semap, false, 0)
   }
}

Done

內部就是調用Add(-1)方法,這里就不細講了。

// Done方法實際就是計數器減1
func (wg *WaitGroup) Done() { 
  wg.Add(-1)
}

Wait

wait實現思路:

不斷檢查state值。如果其中的計數值為零,則說明所有的子goroutine已全部執行完畢,調用者不必等待,直接返回。如果計數值大于零,說明此時還有任務沒有完成,那么調用者變成等待者,需要加入wait隊列,并且阻塞自己。

代碼實現如下:

func (wg *WaitGroup) Wait() {
   // statep表示wait數和計數值
   // 低32位表示wait數,高32位表示計數值
   statep, semap := wg.state()
   for {
      state := atomic.LoadUint64(statep)
      // 將state右移32位,表示當前計數值
      v := int32(state >> 32)
      // w表示waiter等待值
      w := uint32(state)
      if v == 0 {
         // 如果當前計數值為零,表示當前子goroutine已全部執行完畢,則直接返回
         return
      }
      // 否則使用原子操作將state值加一。
      if atomic.CompareAndSwapUint64(statep, state, state+1) {
         // 阻塞休眠等待
         runtime_Semacquire(semap)
         // 被喚醒,不再阻塞,返回
         return
      }
   }
}

到此,waitGroup的基本使用和實現原理已介紹完畢了,相信大家已有不一樣的收獲,咱們下期見。

文章也會持續更新,可以微信搜索「 邁莫coding 」第一時間閱讀。每天分享優質文章、大廠經驗、大廠面經,助力面試,是每個程序員值得關注的平臺。

在這里插入圖片描述

查看原文

贊 0 收藏 0 評論 0

邁莫coding 發布了文章 · 2月20日

騰訊一面問我SQL語句中where條件為什么寫上1=1

在項目編寫中,經常會在代碼中使用到“where 1=1”,這是為什么呢?

目錄

  • where后面加"1=1"還是不加
  • 不用where 1=1 在多條件查詢的困惑
  • 使用where 1=1 的好處
  • 使用where 1=1 的壞處

where后面加"1=1"還是不加

比如現在有一個場景,DB數據庫中有一張博客表(blog),想把blog表中的所有記錄查詢出來,那么可以有兩種方式操作。一種寫法是where關鍵詞什么也不加,另一種寫法是where關鍵詞后面加"1=1",寫法如下:

  • where關鍵詞什么也不加
select * from blog;
  • where關鍵詞后面加 "1=1"
select * from blog where 1 = 1;

這兩種SQL語句查詢所得到的結果完全沒有區別。那為什么要在where關鍵字后面添加"1=1"呢?

我們知道1=1表示true,即永真。如果使用不恰當會造T0級錯誤。例如在編寫SQL語句時進行where條件查詢時配合or運算符會得到意向不到的結果,結果會讓你哆嗦。不信,往下看:

例如,當我們要刪除博客ID稱為“202102111501”的記錄,我們可以這樣寫:

delete from blog where blogId = "202102111501"

如果這個時候如果在where語句后面加上 or 1=1會是什么后果?

delete from blog where blogId = "202102111501" or 1 = 1

本來只要博客ID稱為“202102111501”的記錄,結果因為添加了or 1=1的永真條件,會導致整張表里的記錄都被刪除了。那你可就闖禍了。

不用where 1=1 在多條件查詢的困擾

舉個例子,如果你想查看當前博客中某條評論記錄時,那么按照平時的查詢語句的 動態構造,代碼大體如下:

String sql="select * from blog where";
if ( condition 1) {
  sql = sql + " blogID = 202102111501";
}
if (condition 2) {
  sql = sql + " and commentID = 150101";
}

如果上述的兩個if判斷語句均為true時,那么最終的動態SQL語句為:

select * from table_name where blogID = 202102111501 and commentID = 150101;

可以看出來這是一條完整的正確的SQL查詢語句,能夠正確執行。

如果上述的兩個if判斷語句均為false時,那么最終的動態SQL語句為:

select * from table_name where;

此時我們看看這條生成的SQL語句,由于where關鍵字后面需要使用條件,但是這條語句根本不存在,所以該語句就是一條錯誤語句,不能被執行,不僅報錯,同時還查不到任何數據。

使用where 1=1 的好處

如果我們在where條件后加上1=1,看看它的真面目:

String sql="select * from blog where 1=1";
if ( condition 1) {
  sql = sql + " and blogID = 202102111501";
}
if (condition 2) {
  sql = sql + " and commentID = 150101";
}

當condition 1和condition 2都為真時,上面被執行的SQL代碼為:

select * from blog where 1=1 and blogID = "202102111501" and commentID = 150101;

可以看出來這是一條完整的正確的SQL查詢語句,能夠正確執行。

如果上述的兩個if判斷語句均為false時,那么最終的動態SQL語句為:

select * from table_name where 1=1;

現在,我們來看這條語句,由于where 1=1 是為True的語句,因此,該條語句語法正確,能夠被正確執行,它的作用相當于:sql="select * from table",即返回表中所有數據。

當在where關鍵字后面添加1=1時,如果此時查詢時不選擇任何字段時,那么必將返回表中的所有數據。如果是按照某個字段進行單條查詢時,那么就會此時的條件進行查詢。

說到這里,不知道您是否已明白,其實,where 1=1的應用,不是什么高級的應用,也不是所謂的智能化的構造,僅僅只是為了滿足多條件查詢頁面中不確定的各種因素而采用的一種構造一條正確能運行的動態SQL語句的一種方法。

使用where 1=1 的壞處

我們在寫SQL時,加上了1=1后雖然可以保證語法不會出錯!

select * from table_name where 1=1;

但是因為table中根本就沒有名稱為1的字段,該SQL其實等效于select * from table,這個SQL語句很明顯是全表掃描,需要大量的IO操作,數據量越大越慢。

所以在查詢時,where1=1的后面需要增加其它條件,并且給這些條件建立適當的索引,效率就會大大提高。

文章也會持續更新,可以微信搜索「 邁莫coding 」第一時間閱讀。每天分享優質文章、大廠經驗、大廠面經,助力面試,是每個程序員值得關注的平臺。
查看原文

贊 3 收藏 0 評論 0

邁莫coding 發布了文章 · 2月3日

mysql那些事兒|mysql鎖總結

圖片: https://uploader.shimo.im/f/y...

往期文章推薦

目錄

  • 鎖定義
  • 鎖分類
  • 讀鎖和寫鎖
  • 表鎖和行鎖
  • InnoDB共享鎖和排他鎖
  • InnoDB意向鎖和排他鎖
  • InnoDB行鎖
  • InnoDB間隙鎖

    • 概念
    • InnoDB使用間隙鎖目的
  • InnoDB行鎖實現方式
  • 閑聊
  • 歡迎加入我的公眾號【邁莫coding】 一起pk大廠

鎖定義

鎖是計算機協調多個進程或線程并發訪問某一資源的機制。

在數據庫中,除了傳統的計算資源(如CPU, RAM, I/O等)的爭用以外,數據也是一種供需要用戶共享的資源。鎖沖突也是影響數據庫并發訪問性能的一個重要因素。

鎖分類

  • 從性能上分為樂觀鎖(用版本對比來實現)和悲觀鎖
  • 從數據庫操作的類型分為讀鎖和寫鎖(都屬于悲觀鎖)
  • 從對數據操作的粒度分:分為表鎖和行鎖

讀鎖和寫鎖

  • 讀鎖(共享鎖):針對同一份數據,多個讀操作可以同時進行而不會互相影響,但是會阻塞寫操作
  • 寫鎖(互斥鎖):當前寫操作沒有完成前,它會阻斷其他寫鎖和讀鎖

表鎖和行鎖

表鎖

  • 每次操作時會鎖住整張表。開銷小,加鎖快;不會發生死鎖;鎖定粒度大,發生鎖沖突的概率最高;并發度最低;
  • 表鎖更適合于以查詢為主,并發用戶少,只有少量按索引條件更新數據的應用,如Web應用

行鎖

  • 每次操作鎖住一行數據。開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低;并發度最高;
  • 行級鎖只在存儲引擎層實現,而mysql服務器沒有實現。
  • 行級鎖更適合于有大量按索引條件并發更新少量不同數據,同時又有并發查詢的應用,如一些- 在線事務處理(OLTP)系統

InnoDB共享鎖和排他鎖

InnoDB實現了兩種類型的行鎖:

  • 共享鎖(S): 允許一個事務去讀一行,阻止其他事務獲得相同數據集的排他鎖
  • 排他鎖(X): 允許獲得排他鎖的事務更新數據,阻止其他事務取得相同數據集的共享讀鎖和排他寫鎖

為了允許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖:

  • 意向共享鎖(IS):事務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖
  • 意向排他鎖(IX):事務打算給數據行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖

InnoDB意向鎖和排他鎖

  • 意向鎖是 InnoDB 引擎自動加的,不需要用戶干預
  • 對于 UPDATEINSERTDELETE 語句,InnoDB引擎會自動給涉及數據集加排他鎖(X)
  • 對于普通 SELECT 語句,InnoDB不會加任何鎖
  • 事務可以通過以下語句顯式地給結果集加共享鎖或排它鎖

    • 共享鎖(S): SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE 。 其他 session 仍然可以查詢記錄,并也可以對該記錄加 share mode 的共享鎖。但是如果當前事務需要對該記錄進行更新操作,則很有可能造成死鎖。
    • 排它鎖(X): SELECT * FROM table_name WHERE ... FOR UPDATE 。其他 session 可以查詢該記錄,但是不能對該記錄加共享鎖或排他鎖,而是等待獲得鎖

InnoDB行鎖

  • innoDB行鎖通過索引上的索引項加鎖來實現的,這一點 MySQL 與 Oracle 不同,后者是通過在數據塊中對相應數據行加鎖來實現的。InnoDB 這種行鎖實現特點意味著:只有通過索引條件檢索數據,InnoDB 才使用行級鎖,否則,InnoDB 將使用表鎖!
  • 不論是使用主鍵索引、唯一索引或普通索引,InnoDB 都會使用行鎖來對數據加鎖。
  • 只有執行計劃真正使用了索引,才能使用行鎖:即便在條件中使用了索引字段,但是否使用索引來檢索數據是由 MySQL 通過判斷不同執行計劃的代價來決定的,如果 MySQL 認為全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下 InnoDB 將使用表鎖,而不是行鎖。
  • 由于 MySQL 的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然多個session訪問不同行的記錄, 但是如果是使用相同的索引鍵, 是會出現鎖沖突的(后使用這些索引的session需要等待先使用索引的session釋放鎖后,才能獲取鎖)。 應用設計的時候要注意這一點。

InnoDB間隙鎖

概念

當我們用范圍條件而不是相等條件檢索數據,并請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的索引項加鎖;對于鍵值在條件范圍內但并不存在的記錄,叫做“間隙(GAP)”,InnoDB也會對這個“間隙”加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖)。

很顯然,在使用范圍條件檢索并鎖定記錄時,InnoDB這種加鎖機制會阻塞符合條件范圍內鍵值的并發插入,這往往會造成嚴重的鎖等待。因此,在實際應用開發中,尤其是并發插入比較多的應用,我們要盡量優化業務邏輯,盡量使用相等條件來訪問更新數據,避免使用范圍條件。

InnoDB使用間隙鎖的目的:

  • 防止幻讀,以滿足相關隔離級別的要求;滿足恢復和復制的需要
  • MySQL 通過 BINLOG 錄入執行成功的 INSERT 、 UPDATE 、 DELETE 等更新數據的 SQL 語句,并由此實現 MySQL 數據庫的恢復和主從復制。MySQL 的恢復機制(復制其實就是在 Slave Mysql 不斷做基于 BINLOG 的恢復)有以下特點:

    • 一是 MySQL 的恢復是 SQL 語句級的,也就是重新執行 BINLOG 中的 SQL 語句。
    • 二是 MySQL 的 Binlog 是按照事務提交的先后順序記錄的, 恢復也是按這個順序進行的。

由此可見,MySQL 的恢復機制要求:在一個事務未提交前,其他并發事務不能插入滿足其鎖定條件的任何記錄,也就是不允許出現幻讀。

InnoDB加鎖方式

- 隱式鎖定:

InnoDB在事務執行過程中,使用兩階段鎖協議:

隨時都可以執行鎖定,InnoDB會根據隔離級別在需要的時候自動加鎖;

鎖只有在執行commit或者rollback的時候才會釋放,并且所有的鎖都是在同一時刻被釋放。

- 顯式鎖定 :

select ... lock in share mode //共享鎖 
select ... for update //排他鎖 

- select for update:

在執行這個 select 查詢語句的時候,會將對應的索引訪問條目進行上排他鎖(X 鎖),也就是說這個語句對應的鎖就相當于update帶來的效果。

- select for update 的使用場景*

為了讓自己查到的數據確保是最新數據,并且查到后的數據只允許自己來修改的時候,需要用到 for update 子句。

- select lock in share mode

in share mode 子句的作用就是將查找到的數據加上一個 share 鎖,這個就是表示其他的事務只能對這些數據進行簡單的select 操作,并不能夠進行 DML 操作。

- select lock in share mode 使用場景*

為了確保自己查到的數據沒有被其他的事務正在修改,也就是說確保查到的數據是最新的數據,并且不允許其他人來修改數據。但是自己不一定能夠修改數據,因為有可能其他的事務也對這些數據 使用了 in share mode 的方式上了 S 鎖。

- 性能影響

  • select for update 語句,相當于一個 update 語句。在業務繁忙的情況下,如果事務沒有及時的commit或者rollback 可能會造成其他事務長時間的等待,從而影響數據庫的并發使用效率。
  • select lock in share mode 語句是一個給查找的數據上一個共享鎖(S 鎖)的功能,它允許其他的事務也對該數據上S鎖,但是不能夠允許對該數據進行修改。如果不及時的commit 或者rollback 也可能會造成大量的事務等待。

閑聊

  • 讀完文章,自己是不是和mysql鎖的cp率又提高了
  • 我是邁莫,歡迎大家和我交流
文章也會持續更新,可以微信搜索「 邁莫coding 」第一時間閱讀。每天分享優質文章、大廠經驗、大廠面經,助力面試,是每個程序員值得關注的平臺。
查看原文

贊 1 收藏 1 評論 0

邁莫coding 發布了文章 · 2月3日

阿里終面:談談微服務架構之服務注冊中心

服務注冊中心

在微服務的架構中, 服務注冊中心是一個核心的概念。 就像上節所講, 服務注冊中心是服務發現中不可缺少的一部分。

服務注冊中心, 通俗來講, 是一個存儲網絡實例的網絡地址和數據庫, 一個服務注冊中心應該是高可用的, 而且其數據是最新的。

客戶端在查詢服務注冊中心后, 會緩存一部分網絡地址的數據, 但是, 這些信息需要設置過期時間, 因為數據會實時的發生變化。

因此, 一個服務注冊中心, 應包含一個服務器的集群, 在這個集群中, 各個機器中的數據需要保持一致, 機器之間通過replication協議來實現這個功能。

Netflix Eureka是服務注冊中心的一個很好的實例。它提供了一個用于注冊和查詢服務實例的REST API。服務實例使用POST請求注冊其網絡位置。每30秒,它必須使用PUT請求刷新注冊。通過使用HTTP刪除請求或實例注冊超時來刪除注冊??蛻舳丝梢允褂肏TTP GET請求檢索已注冊的服務實例。

Netflix 通過在多個Amazon服務器上運行多個Eureka來保證服務注冊中心的高可用性,每個Eureka服務器運行在一個EC2實例上, 而且具有一個可變IP地址。DNS TEXT 用于存儲Eureka集群配置,這個是個映射關系,用來得到可用的Eureka服務器的網絡位置列表。

當Eureka服務器啟動時,它查詢DNS來檢索Eureka集群配置,定位它的對應節點,并為自己分配一個未使用的IP地址。

Eureka客戶端通過查詢DNS來發現Eureka服務器的網絡地址, 客戶端更傾向于訪問相同區域的Eureka服務器, 當然, 如果沒有找到服務器, 也會訪問其他區域的服務器。

其他的比較好的服務注冊中心是:

  • etcd: 一個高可用的,分布式的,一致性key-value結構, 用于共享配置信息和服務發現, Kubernetes使用了etcd。
  • consul: 一個發現和配置服務的工具, 提供API供注冊和發現服務, 為了確??捎眯?, consul會執行健康檢查。
  • zookeeper: 一個被廣泛使用的分布式的高性能服務。

下面探討下服務注冊中心的實現機制:

主要有兩種模式: 一種是自注冊模式; 另一種是第三方的注冊模式。

自注冊模式

自注冊模式,就是每個服務實例都需要負責向服務注冊中心來注冊和解除注冊,同時, 還需要發送心跳來保持注冊信心不被過期。

在這里插入圖片描述

Netflix OSS Eureka Client 使用了這種模式。

Eureka Client負責注冊和解除注冊, 在Spring Cloud工程中,向Eureka注冊服務是很方便的,只需要加個注解即可@EnableEurekaClient

自注冊模式的好處是: 簡單,不需要其他的組件。

不過, 缺點就是: 耦合度比較高,需要和服務注冊中心適配, 使用編程語言和框架會
受到限制。

第三方注冊模式

使用第三方的注冊模式, 服務實例將不再負責直接向服務注冊中心注冊和解除注冊, 實際上, 另外一個第三方的系統組件來做這個注冊的操作。

這個第三方組件通過輪詢部署環境或者訂閱一個事件來實現對運行實例集群的監控, 當它發現有新的實例出現的時候, 它會注冊到服務注冊中心, 同樣,也會解除注冊。

在這里插入圖片描述

有一個開源軟件叫Registrator,就實現了這個注冊組件的功能,它會自動注冊和解除注冊部署在Docker環境中的服務實例。支持etcd和consul。

另外一個就是NetflixOSS的Prana, 支持非JVM語言,支持用于Eureka。

使用第三方的模式,好處就是: 很好的實現了和服務注冊中心的解耦,不需要按照服務注冊中心的要求來實現代碼。

缺點就是: 這個第三方的注冊組件必須保證高可用,這樣增加了管理的復雜度。

來源:https://www.imooc.com/article...

文章也會持續更新,可以微信搜索「 邁莫coding 」第一時間閱讀。每天分享優質文章、大廠經驗、大廠面經,助力面試,是每個程序員值得關注的平臺。
查看原文

贊 0 收藏 0 評論 0

邁莫coding 關注了問題 · 2月2日

深入淺出mysql索引總結(下) 一文就OK

在這里插入圖片描述

目錄

  • 聚集索引和非聚集索引

    • 聚集索引
    • 聚集索引使用場景
    • 非聚集索引
    • 非聚集索引使用場景
  • 什么是回表
  • 覆蓋索引
  • 索引失效問題
  • 索引最左匹配原則
  • 索引總結

聚集索引和非聚集索引

《數據庫原理》里面的解釋:聚集索引的順序就是數據的物理存儲順序,而非聚集索引的順序和數據物理排列無關。因為數據在物理存放時只能有一種排列方式,所以一個表只能有一個聚集索引。在SQL SERVER中,索引是通過二叉樹的數據結構來描述的;我們可以如此理解這個兩種索引:聚集索引的葉節點就是數據節點,而非聚集索引的葉節點仍然是索引節點,只不過其包含一個指向對應數據塊的指針。

聚集索引

聚集索引中鍵值的邏輯順序決定了表中相應行的物理順序。

聚集索引確定表中數據的物理順序。聚集索引類似于電話簿,后者按姓氏排列數據。由于聚集索引規定數據在表中的物理存儲順序,因此一個表只能包含一個聚集索引。但該索引可以包含多個列(組合索引),就像電話簿按姓氏和名字進行組織一樣。

聚集索引對于那些經常要搜索范圍值的列特別有效。使用聚集索引找到包含第一個值的行后,便可以確保包含后續索引值的行在物理相鄰。

例如,如果應用程序執行 的一個查詢經常檢索某一日期范圍內的記錄,則使用聚集索引可以迅速找到包含開始日期的行,然后檢索表中所有相鄰的行,直到到達結束日期。這樣有助于提高此 類查詢的性能。

同樣,如果對從表中檢索的數據進行排序時經常要用到某一列,則可以將該表在該列上聚集(物理排序),避免每次查詢該列時都進行排序,從而節省成本。

當索引值唯一時,使用聚集索引查找特定的行也很有效率。例如,使用唯一雇員 ID 列 emp_id 查找特定雇員的最快速的方法,是在 emp_id 列上創建聚集索引或 PRIMARY KEY 約束。

聚集索引使用場景

  • 此列包含有限數目的不同值
  • 查詢的結果返回一個區間的值
  • 查詢的結果返回某值相同的大量結果集

非聚集索引

一種索引,該索引中索引的邏輯順序與磁盤上行的物理存儲順序不同。

索引是通過二叉樹的數據結構來描述的,我們可以這么理解聚簇索引:索引的葉節點就是數據節點。而非聚簇索引的葉節點仍然是索引節點,只不過有一個指針指向對應的數據塊。

非聚集索引指定了表中記錄的邏輯順序,但記錄的物理順序和索引的順序不一致,聚集索引和非聚集索引都采用了B+樹的結構,但非聚集索引的葉子層并不與實際的數據頁相重疊,而采用葉子層包含一個指向表中的記錄在數據頁中的指針的方式。

非聚集索引比聚集索引層次多,添加記錄不會引起數據順序的重組。

非聚集索引使用場景

  • 此列包含了大量數目不同的值
  • 查詢的結束返回的是少量的結果集
  • order by 子句中使用了該列

什么是回表

假設,我們有一個主鍵列為ID的表,表中有字段k,并且在k上有索引。

這個表的建表語句是:

mysql> create table T(
id int primary key, 
k int not null, 
name varchar(16),
index (k))engine=InnoDB;

表中R1~R5的(ID,k)值分別為(100,1)、(200,2)、(300,3)、(500,5)和(600,6),兩棵樹的示例示意圖如下
在這里插入圖片描述

SQL語句 select * from T where k between 3 and 5 執行過程:

  1. 在 k 索引樹上找到 k=3 的記錄,取得 ID = 300;
  2. 再到 ID 索引樹查到 ID=300 對應的 R3;
  3. 在 k 索引樹取下一個值 k=5,取得 ID=500;
  4. 再回到 ID 索引樹查到 ID=500 對應的 R4;
  5. 在 k 索引樹取下一個值 k=6,不滿足條件,循環結束。

在這個過程中,回到主鍵索引樹搜索的過程,我們稱為回表??梢钥吹?,這個查詢過程讀了 k 索引樹的 3 條記錄(步驟 1、3 和 5),回表了兩次(步驟 2 和 4)。

也就是說,基于非主鍵索引的查詢需要多掃描一棵索引樹。因此,我們在應用中應該盡量使用主鍵查詢。

覆蓋索引

如果執行一條SQL語句 select ID from T where k = 3 ,這時只需要查找到iD的值即可,而ID值恰好存在與k索引樹上,不需要進行回表。也就是說,在這個查詢里面,索引 k 已經“覆蓋了”我們的查詢需求,我們稱為覆蓋索引。

由于覆蓋索引可以減少樹的搜索次數,顯著提升查詢性能,所以使用覆蓋索引是一個常用的性能優化手段。

索引失效問題

  • 不在索引列上做任何操作(計算、函數、(自動or手動)類型轉換),會導致索引失效而轉向全表掃描
EXPLAIN SELECT * FROM employees WHERE name = 'LiLei';
EXPLAIN SELECT * FROMemployees WHERE left(name,3) = 'LiLei';

在這里插入圖片描述

  • 給hire_time增加一個普通索引:
ALTER TABLE `employees`
ADD INDEX `idx_hire_time` (`hire_time`) USING BTREE 
EXPLAIN  select * from employees where date(hire_time) ='2018-09-30';

在這里插入圖片描述

轉化為日期范圍查詢,會走索引:

EXPLAIN  select * from employees where hire_time >='2018-09-30 00:00:00'  and hire_time <='2018-09-30 23:59:59';

在這里插入圖片描述

  • 存儲引擎不能使用索引中范圍條件右邊的列
EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22 AND position ='manager';
EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age > 22 AND
position ='manager';

在這里插入圖片描述

  • mysql在使用不等于(!=或者<>)的時候無法使用索引會導致全表掃描
EXPLAIN SELECT * FROM employees WHERE name != 'LiLei';

在這里插入圖片描述

  • is null,is not null 也無法使用索引
EXPLAIN SELECT * FROM employees WHERE name is null

在這里插入圖片描述

  • like以通配符開頭('$abc...')mysql索引失效會變成全表掃描操作
EXPLAIN SELECT * FROM employees WHERE name like '%Lei'

在這里插入圖片描述

EXPLAIN SELECT * FROMemployees WHERE name like 'Lei%'

在這里插入圖片描述

問題:解決like'%字符串%'索引不被使用的方法?

  1. 使用覆蓋索引,查詢字段必須是建立覆蓋索引字段
EXPLAIN SELECT name,age,position FROM employees WHERE name like '%Lei%';

在這里插入圖片描述

  1. 如果不能使用覆蓋索引則可能需要借助搜索引擎
  • 字符串不加單引號索引失效
EXPLAIN SELECT * FROM employees WHERE name = '1000';
EXPLAIN SELECT * FROM employees WHERE name = 1000;

在這里插入圖片描述

  • or 連接索引失效
explain select * from user where name = ‘2000’ or age = 20 or pos =‘cxy’;

在這里插入圖片描述

  • order by

正常(索引參與了排序),沒有違反最左匹配原則。

explain select * from user where name = 'zhangsan' and age = 20 order by age,pos;

在這里插入圖片描述

違反最左前綴法則,導致額外的文件排序(會降低性能)。

explain select name,age from user where name = 'zhangsan' order by pos;

在這里插入圖片描述

  • group by

正常(索引參與了排序)。

explain select name,age from user where name = 'zhangsan' group by age;

違反最左前綴法則,導致產生臨時表(會降低性能)。

explain select name,age from user where name = 'zhangsan' group by pos,age;

在這里插入圖片描述

索引最左匹配原則

最左前綴匹配原則:在MySQL建立聯合索引時會遵守最左前綴匹配原則,即最左優先,在檢索數據時從聯合索引的最左邊開始匹配。

要想理解聯合索引的最左匹配原則,先來理解下索引的底層原理。索引的底層是一顆B+樹,那么聯合索引的底層也就是一顆B+樹,只不過聯合索引的B+樹節點中存儲的是鍵值。由于構建一棵B+樹只能根據一個值來確定索引關系,所以數據庫依賴聯合索引最左的字段來構建。

舉例:創建一個(a,b)的聯合索引,那么它的索引樹就是下圖的樣子。

在這里插入圖片描述

可以看到a的值是有順序的,1,1,2,2,3,3,而b的值是沒有順序的1,2,1,4,1,2。但是我們又可發現a在等值的情況下,b值又是按順序排列的,但是這種順序是相對的。這是因為MySQL創建聯合索引的規則是首先會對聯合索引的最左邊第一個字段排序,在第一個字段的排序基礎上,然后在對第二個字段進行排序。所以b=2這種查詢條件沒有辦法利用索引。

索引總結

假設index(a,b,c)

在這里插入圖片描述

like KK%相當于=常量,%KK和%KK% 相當于范圍

文章也會持續更新,可以微信搜索「 邁莫coding 」第一時間閱讀。每天分享優質文章、大廠經驗、大廠面經,助力面試,是每個程序員值得關注的平臺。

關注 1 回答 0

邁莫coding 提出了問題 · 2月2日

深入淺出mysql索引總結(下) 一文就OK

在這里插入圖片描述

目錄

  • 聚集索引和非聚集索引

    • 聚集索引
    • 聚集索引使用場景
    • 非聚集索引
    • 非聚集索引使用場景
  • 什么是回表
  • 覆蓋索引
  • 索引失效問題
  • 索引最左匹配原則
  • 索引總結

聚集索引和非聚集索引

《數據庫原理》里面的解釋:聚集索引的順序就是數據的物理存儲順序,而非聚集索引的順序和數據物理排列無關。因為數據在物理存放時只能有一種排列方式,所以一個表只能有一個聚集索引。在SQL SERVER中,索引是通過二叉樹的數據結構來描述的;我們可以如此理解這個兩種索引:聚集索引的葉節點就是數據節點,而非聚集索引的葉節點仍然是索引節點,只不過其包含一個指向對應數據塊的指針。

聚集索引

聚集索引中鍵值的邏輯順序決定了表中相應行的物理順序。

聚集索引確定表中數據的物理順序。聚集索引類似于電話簿,后者按姓氏排列數據。由于聚集索引規定數據在表中的物理存儲順序,因此一個表只能包含一個聚集索引。但該索引可以包含多個列(組合索引),就像電話簿按姓氏和名字進行組織一樣。

聚集索引對于那些經常要搜索范圍值的列特別有效。使用聚集索引找到包含第一個值的行后,便可以確保包含后續索引值的行在物理相鄰。

例如,如果應用程序執行 的一個查詢經常檢索某一日期范圍內的記錄,則使用聚集索引可以迅速找到包含開始日期的行,然后檢索表中所有相鄰的行,直到到達結束日期。這樣有助于提高此 類查詢的性能。

同樣,如果對從表中檢索的數據進行排序時經常要用到某一列,則可以將該表在該列上聚集(物理排序),避免每次查詢該列時都進行排序,從而節省成本。

當索引值唯一時,使用聚集索引查找特定的行也很有效率。例如,使用唯一雇員 ID 列 emp_id 查找特定雇員的最快速的方法,是在 emp_id 列上創建聚集索引或 PRIMARY KEY 約束。

聚集索引使用場景

  • 此列包含有限數目的不同值
  • 查詢的結果返回一個區間的值
  • 查詢的結果返回某值相同的大量結果集

非聚集索引

一種索引,該索引中索引的邏輯順序與磁盤上行的物理存儲順序不同。

索引是通過二叉樹的數據結構來描述的,我們可以這么理解聚簇索引:索引的葉節點就是數據節點。而非聚簇索引的葉節點仍然是索引節點,只不過有一個指針指向對應的數據塊。

非聚集索引指定了表中記錄的邏輯順序,但記錄的物理順序和索引的順序不一致,聚集索引和非聚集索引都采用了B+樹的結構,但非聚集索引的葉子層并不與實際的數據頁相重疊,而采用葉子層包含一個指向表中的記錄在數據頁中的指針的方式。

非聚集索引比聚集索引層次多,添加記錄不會引起數據順序的重組。

非聚集索引使用場景

  • 此列包含了大量數目不同的值
  • 查詢的結束返回的是少量的結果集
  • order by 子句中使用了該列

什么是回表

假設,我們有一個主鍵列為ID的表,表中有字段k,并且在k上有索引。

這個表的建表語句是:

mysql> create table T(
id int primary key, 
k int not null, 
name varchar(16),
index (k))engine=InnoDB;

表中R1~R5的(ID,k)值分別為(100,1)、(200,2)、(300,3)、(500,5)和(600,6),兩棵樹的示例示意圖如下
在這里插入圖片描述

SQL語句 select * from T where k between 3 and 5 執行過程:

  1. 在 k 索引樹上找到 k=3 的記錄,取得 ID = 300;
  2. 再到 ID 索引樹查到 ID=300 對應的 R3;
  3. 在 k 索引樹取下一個值 k=5,取得 ID=500;
  4. 再回到 ID 索引樹查到 ID=500 對應的 R4;
  5. 在 k 索引樹取下一個值 k=6,不滿足條件,循環結束。

在這個過程中,回到主鍵索引樹搜索的過程,我們稱為回表??梢钥吹?,這個查詢過程讀了 k 索引樹的 3 條記錄(步驟 1、3 和 5),回表了兩次(步驟 2 和 4)。

也就是說,基于非主鍵索引的查詢需要多掃描一棵索引樹。因此,我們在應用中應該盡量使用主鍵查詢。

覆蓋索引

如果執行一條SQL語句 select ID from T where k = 3 ,這時只需要查找到iD的值即可,而ID值恰好存在與k索引樹上,不需要進行回表。也就是說,在這個查詢里面,索引 k 已經“覆蓋了”我們的查詢需求,我們稱為覆蓋索引。

由于覆蓋索引可以減少樹的搜索次數,顯著提升查詢性能,所以使用覆蓋索引是一個常用的性能優化手段。

索引失效問題

  • 不在索引列上做任何操作(計算、函數、(自動or手動)類型轉換),會導致索引失效而轉向全表掃描
EXPLAIN SELECT * FROM employees WHERE name = 'LiLei';
EXPLAIN SELECT * FROMemployees WHERE left(name,3) = 'LiLei';

在這里插入圖片描述

  • 給hire_time增加一個普通索引:
ALTER TABLE `employees`
ADD INDEX `idx_hire_time` (`hire_time`) USING BTREE 
EXPLAIN  select * from employees where date(hire_time) ='2018-09-30';

在這里插入圖片描述

轉化為日期范圍查詢,會走索引:

EXPLAIN  select * from employees where hire_time >='2018-09-30 00:00:00'  and hire_time <='2018-09-30 23:59:59';

在這里插入圖片描述

  • 存儲引擎不能使用索引中范圍條件右邊的列
EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22 AND position ='manager';
EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age > 22 AND
position ='manager';

在這里插入圖片描述

  • mysql在使用不等于(!=或者<>)的時候無法使用索引會導致全表掃描
EXPLAIN SELECT * FROM employees WHERE name != 'LiLei';

在這里插入圖片描述

  • is null,is not null 也無法使用索引
EXPLAIN SELECT * FROM employees WHERE name is null

在這里插入圖片描述

  • like以通配符開頭('$abc...')mysql索引失效會變成全表掃描操作
EXPLAIN SELECT * FROM employees WHERE name like '%Lei'

在這里插入圖片描述

EXPLAIN SELECT * FROMemployees WHERE name like 'Lei%'

在這里插入圖片描述

問題:解決like'%字符串%'索引不被使用的方法?

  1. 使用覆蓋索引,查詢字段必須是建立覆蓋索引字段
EXPLAIN SELECT name,age,position FROM employees WHERE name like '%Lei%';

在這里插入圖片描述

  1. 如果不能使用覆蓋索引則可能需要借助搜索引擎
  • 字符串不加單引號索引失效
EXPLAIN SELECT * FROM employees WHERE name = '1000';
EXPLAIN SELECT * FROM employees WHERE name = 1000;

在這里插入圖片描述

  • or 連接索引失效
explain select * from user where name = ‘2000’ or age = 20 or pos =‘cxy’;

在這里插入圖片描述

  • order by

正常(索引參與了排序),沒有違反最左匹配原則。

explain select * from user where name = 'zhangsan' and age = 20 order by age,pos;

在這里插入圖片描述

違反最左前綴法則,導致額外的文件排序(會降低性能)。

explain select name,age from user where name = 'zhangsan' order by pos;

在這里插入圖片描述

  • group by

正常(索引參與了排序)。

explain select name,age from user where name = 'zhangsan' group by age;

違反最左前綴法則,導致產生臨時表(會降低性能)。

explain select name,age from user where name = 'zhangsan' group by pos,age;

在這里插入圖片描述

索引最左匹配原則

最左前綴匹配原則:在MySQL建立聯合索引時會遵守最左前綴匹配原則,即最左優先,在檢索數據時從聯合索引的最左邊開始匹配。

要想理解聯合索引的最左匹配原則,先來理解下索引的底層原理。索引的底層是一顆B+樹,那么聯合索引的底層也就是一顆B+樹,只不過聯合索引的B+樹節點中存儲的是鍵值。由于構建一棵B+樹只能根據一個值來確定索引關系,所以數據庫依賴聯合索引最左的字段來構建。

舉例:創建一個(a,b)的聯合索引,那么它的索引樹就是下圖的樣子。

在這里插入圖片描述

可以看到a的值是有順序的,1,1,2,2,3,3,而b的值是沒有順序的1,2,1,4,1,2。但是我們又可發現a在等值的情況下,b值又是按順序排列的,但是這種順序是相對的。這是因為MySQL創建聯合索引的規則是首先會對聯合索引的最左邊第一個字段排序,在第一個字段的排序基礎上,然后在對第二個字段進行排序。所以b=2這種查詢條件沒有辦法利用索引。

索引總結

假設index(a,b,c)

在這里插入圖片描述

like KK%相當于=常量,%KK和%KK% 相當于范圍

文章也會持續更新,可以微信搜索「 邁莫coding 」第一時間閱讀。每天分享優質文章、大廠經驗、大廠面經,助力面試,是每個程序員值得關注的平臺。

關注 1 回答 0

邁莫coding 關注了標簽 · 2月2日

關注 89270

認證與成就

  • 獲得 4 次點贊
  • 獲得 1 枚徽章 獲得 0 枚金徽章, 獲得 0 枚銀徽章, 獲得 1 枚銅徽章

擅長技能
編輯

開源項目 & 著作
編輯

  • torm

    對象-關系映射(Object-Relational Mapping,簡稱ORM),面向對象的開發方法是當今企業級應用開發環境中的主流開發方法,關系數據庫是企業級應用環境中永久存放數據的主流數據存儲系統。

  • 博客系統

    適合學生搭建的個人博客

注冊于 2月2日
個人主頁被 584 人瀏覽

一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕 <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>