├── .gitignore ├── LICENSE ├── issue └── issue.go ├── safe └── safe.go ├── optimistic └── optimistic.go ├── queue └── single_queue.go ├── optimistic_queue └── single_queue.go ├── multiple_queue └── multiple_queue.go ├── optimistic_multiple_queue └── multiple_queue.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 golang workshop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /issue/issue.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "math/rand" 8 | "net/http" 9 | "os" 10 | "time" 11 | 12 | "github.com/globalsign/mgo" 13 | "github.com/globalsign/mgo/bson" 14 | ) 15 | 16 | var globalDB *mgo.Database 17 | var account = "appleboy" 18 | 19 | type currency struct { 20 | ID bson.ObjectId `json:"id" bson:"_id,omitempty"` 21 | Amount float64 `bson:"amount"` 22 | Account string `bson:"account"` 23 | Code string `bson:"code"` 24 | } 25 | 26 | // Random get random value 27 | func Random(min, max int) int { 28 | rand.Seed(time.Now().UTC().UnixNano()) 29 | return rand.Intn(max-min+1) + min 30 | } 31 | 32 | func pay(w http.ResponseWriter, r *http.Request) { 33 | entry := currency{} 34 | // step 1: get current amount 35 | err := globalDB.C("bank").Find(bson.M{"account": account}).One(&entry) 36 | 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | wait := Random(1, 100) 42 | time.Sleep(time.Duration(wait) * time.Millisecond) 43 | 44 | //step 3: subtract current balance and update back to database 45 | entry.Amount = entry.Amount + 50.000 46 | err = globalDB.C("bank").UpdateId(entry.ID, &entry) 47 | 48 | if err != nil { 49 | panic("update error") 50 | } 51 | 52 | fmt.Printf("%+v\n", entry) 53 | 54 | io.WriteString(w, "ok") 55 | } 56 | 57 | func main() { 58 | port := os.Getenv("PORT") 59 | if port == "" { 60 | port = "8000" 61 | } 62 | session, _ := mgo.Dial("localhost:27017") 63 | globalDB = session.DB("queue") 64 | globalDB.C("bank").DropCollection() 65 | 66 | user := currency{Account: account, Amount: 1000.00, Code: "USD"} 67 | err := globalDB.C("bank").Insert(&user) 68 | 69 | if err != nil { 70 | panic("insert error") 71 | } 72 | 73 | log.Println("Listen server on " + port + " port") 74 | http.HandleFunc("/", pay) 75 | if err := http.ListenAndServe(":"+port, nil); err != nil { 76 | log.Fatal(err) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /safe/safe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "math/rand" 8 | "net/http" 9 | "os" 10 | "sync" 11 | "time" 12 | 13 | "github.com/globalsign/mgo" 14 | "github.com/globalsign/mgo/bson" 15 | ) 16 | 17 | var globalDB *mgo.Database 18 | var account = "appleboy" 19 | var mu = &sync.Mutex{} 20 | 21 | type currency struct { 22 | ID bson.ObjectId `json:"id" bson:"_id,omitempty"` 23 | Amount float64 `bson:"amount"` 24 | Account string `bson:"account"` 25 | Code string `bson:"code"` 26 | } 27 | 28 | // Random get random value 29 | func Random(min, max int) int { 30 | rand.Seed(time.Now().UTC().UnixNano()) 31 | return rand.Intn(max-min+1) + min 32 | } 33 | 34 | func pay(w http.ResponseWriter, r *http.Request) { 35 | entry := currency{} 36 | //Solution here, Lock other thread access this section of code until it's unlocked 37 | mu.Lock() 38 | defer mu.Unlock() 39 | // step 1: get current amount 40 | err := globalDB.C("bank").Find(bson.M{"account": account}).One(&entry) 41 | 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | wait := Random(1, 100) 47 | time.Sleep(time.Duration(wait) * time.Millisecond) 48 | 49 | //step 3: subtract current balance and update back to database 50 | entry.Amount = entry.Amount + 50.000 51 | err = globalDB.C("bank").UpdateId(entry.ID, &entry) 52 | 53 | if err != nil { 54 | panic("update error") 55 | } 56 | 57 | fmt.Printf("%+v\n", entry) 58 | 59 | io.WriteString(w, "ok") 60 | } 61 | 62 | func main() { 63 | port := os.Getenv("PORT") 64 | if port == "" { 65 | port = "8000" 66 | } 67 | session, _ := mgo.Dial("localhost:27017") 68 | globalDB = session.DB("logs") 69 | 70 | globalDB.C("bank").DropCollection() 71 | 72 | user := currency{Account: account, Amount: 1000.00, Code: "USD"} 73 | err := globalDB.C("bank").Insert(&user) 74 | 75 | if err != nil { 76 | panic("insert error") 77 | } 78 | 79 | log.Println("Listen server on " + port + " port") 80 | http.HandleFunc("/", pay) 81 | if err := http.ListenAndServe(":"+port, nil); err != nil { 82 | log.Fatal(err) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /optimistic/optimistic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "math/rand" 8 | "net/http" 9 | "os" 10 | "time" 11 | 12 | "github.com/globalsign/mgo" 13 | "github.com/globalsign/mgo/bson" 14 | ) 15 | 16 | var globalDB *mgo.Database 17 | var account = "appleboy" 18 | 19 | type currency struct { 20 | ID bson.ObjectId `json:"id" bson:"_id,omitempty"` 21 | Amount float64 `bson:"amount"` 22 | Account string `bson:"account"` 23 | Code string `bson:"code"` 24 | Version int `bson:"version"` 25 | } 26 | 27 | // Random get random value 28 | func Random(min, max int) int { 29 | rand.Seed(time.Now().UTC().UnixNano()) 30 | return rand.Intn(max-min+1) + min 31 | } 32 | 33 | func pay(w http.ResponseWriter, r *http.Request) { 34 | LOOP: 35 | entry := currency{} 36 | // step 1: get current amount 37 | err := globalDB.C("bank").Find(bson.M{"account": account}).One(&entry) 38 | 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | wait := Random(1, 100) 44 | time.Sleep(time.Duration(wait) * time.Millisecond) 45 | 46 | //step 3: subtract current balance and update back to database 47 | entry.Amount = entry.Amount + 50.000 48 | err = globalDB.C("bank").Update(bson.M{ 49 | "version": entry.Version, 50 | "_id": entry.ID, 51 | }, bson.M{"$set": map[string]interface{}{ 52 | "amount": entry.Amount, 53 | "version": (entry.Version + 1), 54 | }}) 55 | 56 | if err != nil { 57 | goto LOOP 58 | } 59 | 60 | fmt.Printf("%+v\n", entry) 61 | 62 | io.WriteString(w, "ok") 63 | } 64 | 65 | func main() { 66 | port := os.Getenv("PORT") 67 | if port == "" { 68 | port = "8000" 69 | } 70 | session, _ := mgo.Dial("localhost:27017") 71 | globalDB = session.DB("queue") 72 | globalDB.C("bank").DropCollection() 73 | 74 | user := currency{Account: account, Amount: 1000.00, Code: "USD", Version: 1} 75 | err := globalDB.C("bank").Insert(&user) 76 | 77 | if err != nil { 78 | panic("insert error") 79 | } 80 | 81 | log.Println("Listen server on " + port + " port") 82 | http.HandleFunc("/", pay) 83 | if err := http.ListenAndServe(":"+port, nil); err != nil { 84 | log.Fatal(err) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /queue/single_queue.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "os" 8 | "sync" 9 | 10 | "github.com/globalsign/mgo" 11 | "github.com/globalsign/mgo/bson" 12 | ) 13 | 14 | var globalDB *mgo.Database 15 | var account = "appleboy" 16 | var in chan Data 17 | 18 | // Data struct 19 | type Data struct { 20 | Account string 21 | Result *chan float64 22 | } 23 | 24 | type currency struct { 25 | ID bson.ObjectId `json:"id" bson:"_id,omitempty"` 26 | Amount float64 `bson:"amount"` 27 | Account string `bson:"account"` 28 | Code string `bson:"code"` 29 | } 30 | 31 | func pay(w http.ResponseWriter, r *http.Request) { 32 | wg := sync.WaitGroup{} 33 | wg.Add(1) 34 | 35 | go func(wg *sync.WaitGroup) { 36 | result := make(chan float64) 37 | in <- Data{ 38 | Account: account, 39 | Result: &result, 40 | } 41 | for { 42 | select { 43 | case result := <-result: 44 | log.Printf("account: %v, result: %+v\n", account, result) 45 | wg.Done() 46 | } 47 | } 48 | }(&wg) 49 | 50 | wg.Wait() 51 | 52 | io.WriteString(w, "ok") 53 | } 54 | 55 | func main() { 56 | port := os.Getenv("PORT") 57 | if port == "" { 58 | port = "8000" 59 | } 60 | in = make(chan Data) 61 | 62 | session, err := mgo.Dial("localhost:27017") 63 | 64 | if err != nil { 65 | panic("can't connect mongodb server") 66 | } 67 | 68 | globalDB = session.DB("logs") 69 | 70 | globalDB.C("bank").DropCollection() 71 | 72 | user := currency{Account: account, Amount: 1000.00, Code: "USD"} 73 | err = globalDB.C("bank").Insert(&user) 74 | 75 | if err != nil { 76 | panic("insert error") 77 | } 78 | 79 | go func(in *chan Data) { 80 | for { 81 | select { 82 | case data := <-*in: 83 | entry := currency{} 84 | // step 1: get current amount 85 | err := globalDB.C("bank").Find(bson.M{"account": data.Account}).One(&entry) 86 | 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | //step 3: subtract current balance and update back to database 92 | entry.Amount = entry.Amount + 50.000 93 | err = globalDB.C("bank").UpdateId(entry.ID, &entry) 94 | 95 | if err != nil { 96 | panic("update error") 97 | } 98 | 99 | *data.Result <- entry.Amount 100 | } 101 | } 102 | 103 | }(&in) 104 | 105 | log.Println("Listen server on " + port + " port") 106 | http.HandleFunc("/", pay) 107 | if err := http.ListenAndServe(":"+port, nil); err != nil { 108 | log.Fatal(err) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /optimistic_queue/single_queue.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "os" 8 | "sync" 9 | 10 | "github.com/globalsign/mgo" 11 | "github.com/globalsign/mgo/bson" 12 | ) 13 | 14 | var globalDB *mgo.Database 15 | var account = "appleboy" 16 | var in chan Data 17 | 18 | // Data struct 19 | type Data struct { 20 | Account string 21 | Result *chan float64 22 | } 23 | 24 | type currency struct { 25 | ID bson.ObjectId `json:"id" bson:"_id,omitempty"` 26 | Amount float64 `bson:"amount"` 27 | Account string `bson:"account"` 28 | Code string `bson:"code"` 29 | Version int `bson:"version"` 30 | } 31 | 32 | func pay(w http.ResponseWriter, r *http.Request) { 33 | wg := sync.WaitGroup{} 34 | wg.Add(1) 35 | 36 | go func(wg *sync.WaitGroup) { 37 | result := make(chan float64) 38 | in <- Data{ 39 | Account: account, 40 | Result: &result, 41 | } 42 | for { 43 | select { 44 | case result := <-result: 45 | log.Printf("account: %v, result: %+v\n", account, result) 46 | wg.Done() 47 | } 48 | } 49 | }(&wg) 50 | 51 | wg.Wait() 52 | 53 | io.WriteString(w, "ok") 54 | } 55 | 56 | func main() { 57 | port := os.Getenv("PORT") 58 | if port == "" { 59 | port = "8000" 60 | } 61 | in = make(chan Data) 62 | 63 | session, err := mgo.Dial("localhost:27017") 64 | 65 | if err != nil { 66 | panic("can't connect mongodb server") 67 | } 68 | 69 | globalDB = session.DB("logs") 70 | 71 | globalDB.C("bank").DropCollection() 72 | 73 | user := currency{Account: account, Amount: 1000.00, Code: "USD", Version: 1} 74 | err = globalDB.C("bank").Insert(&user) 75 | 76 | if err != nil { 77 | panic("insert error") 78 | } 79 | 80 | go func(in *chan Data) { 81 | for { 82 | select { 83 | case data := <-*in: 84 | LOOP: 85 | entry := currency{} 86 | // step 1: get current amount 87 | err := globalDB.C("bank").Find(bson.M{"account": data.Account}).One(&entry) 88 | 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | //step 3: subtract current balance and update back to database 94 | entry.Amount = entry.Amount + 50.000 95 | err = globalDB.C("bank").Update(bson.M{ 96 | "version": entry.Version, 97 | "_id": entry.ID, 98 | }, bson.M{"$set": map[string]interface{}{ 99 | "amount": entry.Amount, 100 | "version": (entry.Version + 1), 101 | }}) 102 | 103 | if err != nil { 104 | goto LOOP 105 | } 106 | 107 | *data.Result <- entry.Amount 108 | } 109 | } 110 | 111 | }(&in) 112 | 113 | log.Println("Listen server on " + port + " port") 114 | http.HandleFunc("/", pay) 115 | if err := http.ListenAndServe(":"+port, nil); err != nil { 116 | log.Fatal(err) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /multiple_queue/multiple_queue.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "math/rand" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | "sync" 11 | "time" 12 | 13 | "github.com/globalsign/mgo" 14 | "github.com/globalsign/mgo/bson" 15 | ) 16 | 17 | var globalDB *mgo.Database 18 | var in []chan Data 19 | var maxUser = 100 20 | var maxThread = 10 21 | 22 | // Data struct 23 | type Data struct { 24 | Account string 25 | Result *chan float64 26 | } 27 | 28 | type currency struct { 29 | ID bson.ObjectId `json:"id" bson:"_id,omitempty"` 30 | Amount float64 `bson:"amount"` 31 | Account string `bson:"account"` 32 | Code string `bson:"code"` 33 | } 34 | 35 | // Random get random value 36 | func Random(min, max int) int { 37 | rand.Seed(time.Now().UTC().UnixNano()) 38 | return rand.Intn(max-min+1) + min 39 | } 40 | 41 | func pay(w http.ResponseWriter, r *http.Request) { 42 | wg := sync.WaitGroup{} 43 | wg.Add(1) 44 | 45 | go func(wg *sync.WaitGroup) { 46 | number := Random(1, maxUser) 47 | channelNumber := number % maxThread 48 | account := "user" + strconv.Itoa(number) 49 | result := make(chan float64) 50 | in[channelNumber] <- Data{ 51 | Account: account, 52 | Result: &result, 53 | } 54 | select { 55 | case result := <-result: 56 | log.Printf("account: %v, result: %+v\n", account, result) 57 | wg.Done() 58 | } 59 | }(&wg) 60 | 61 | wg.Wait() 62 | 63 | io.WriteString(w, "ok") 64 | } 65 | 66 | func main() { 67 | port := os.Getenv("PORT") 68 | if port == "" { 69 | port = "8000" 70 | } 71 | in = make([]chan Data, maxThread) 72 | 73 | session, _ := mgo.Dial("localhost:27017") 74 | globalDB = session.DB("logs") 75 | 76 | globalDB.C("bank").DropCollection() 77 | 78 | for i := range in { 79 | in[i] = make(chan Data) 80 | } 81 | 82 | // create 100 user 83 | for i := 0; i < maxUser; i++ { 84 | account := "user" + strconv.Itoa(i+1) 85 | user := currency{Account: account, Amount: 1000.00, Code: "USD"} 86 | if err := globalDB.C("bank").Insert(&user); err != nil { 87 | panic("insert error") 88 | } 89 | } 90 | 91 | for i := range in { 92 | go func(in *chan Data, i int) { 93 | for { 94 | select { 95 | case data := <-*in: 96 | entry := currency{} 97 | // step 1: get current amount 98 | err := globalDB.C("bank").Find(bson.M{"account": data.Account}).One(&entry) 99 | 100 | if err != nil { 101 | panic(err) 102 | } 103 | 104 | //step 3: subtract current balance and update back to database 105 | entry.Amount = entry.Amount + 50.000 106 | err = globalDB.C("bank").UpdateId(entry.ID, &entry) 107 | 108 | if err != nil { 109 | panic("update error") 110 | } 111 | 112 | *data.Result <- entry.Amount 113 | } 114 | } 115 | 116 | }(&in[i], i) 117 | } 118 | 119 | log.Println("Listen server on " + port + " port") 120 | http.HandleFunc("/", pay) 121 | if err := http.ListenAndServe(":"+port, nil); err != nil { 122 | log.Fatal(err) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /optimistic_multiple_queue/multiple_queue.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "math/rand" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | "sync" 11 | "time" 12 | 13 | "github.com/globalsign/mgo" 14 | "github.com/globalsign/mgo/bson" 15 | ) 16 | 17 | var globalDB *mgo.Database 18 | var in []chan Data 19 | var maxUser = 100 20 | var maxThread = 10 21 | 22 | // Data struct 23 | type Data struct { 24 | Account string 25 | Result *chan float64 26 | } 27 | 28 | type currency struct { 29 | ID bson.ObjectId `json:"id" bson:"_id,omitempty"` 30 | Amount float64 `bson:"amount"` 31 | Account string `bson:"account"` 32 | Code string `bson:"code"` 33 | Version int `bson:"version"` 34 | } 35 | 36 | // Random get random value 37 | func Random(min, max int) int { 38 | rand.Seed(time.Now().UTC().UnixNano()) 39 | return rand.Intn(max-min+1) + min 40 | } 41 | 42 | func pay(w http.ResponseWriter, r *http.Request) { 43 | wg := sync.WaitGroup{} 44 | wg.Add(1) 45 | 46 | go func(wg *sync.WaitGroup) { 47 | number := Random(1, maxUser) 48 | channelNumber := number % maxThread 49 | account := "user" + strconv.Itoa(number) 50 | result := make(chan float64) 51 | in[channelNumber] <- Data{ 52 | Account: account, 53 | Result: &result, 54 | } 55 | select { 56 | case result := <-result: 57 | log.Printf("account: %v, result: %+v\n", account, result) 58 | wg.Done() 59 | } 60 | }(&wg) 61 | 62 | wg.Wait() 63 | 64 | io.WriteString(w, "ok") 65 | } 66 | 67 | func main() { 68 | port := os.Getenv("PORT") 69 | if port == "" { 70 | port = "8000" 71 | } 72 | in = make([]chan Data, maxThread) 73 | 74 | session, _ := mgo.Dial("localhost:27017") 75 | globalDB = session.DB("logs") 76 | 77 | globalDB.C("bank").DropCollection() 78 | 79 | for i := range in { 80 | in[i] = make(chan Data) 81 | } 82 | 83 | // create 100 user 84 | for i := 0; i < maxUser; i++ { 85 | account := "user" + strconv.Itoa(i+1) 86 | user := currency{Account: account, Amount: 1000.00, Code: "USD", Version: 1} 87 | if err := globalDB.C("bank").Insert(&user); err != nil { 88 | panic("insert error") 89 | } 90 | } 91 | 92 | for i := range in { 93 | go func(in *chan Data, i int) { 94 | for { 95 | select { 96 | case data := <-*in: 97 | LOOP: 98 | entry := currency{} 99 | // step 1: get current amount 100 | err := globalDB.C("bank").Find(bson.M{"account": data.Account}).One(&entry) 101 | 102 | if err != nil { 103 | panic(err) 104 | } 105 | 106 | //step 3: subtract current balance and update back to database 107 | entry.Amount = entry.Amount + 50.000 108 | err = globalDB.C("bank").Update(bson.M{ 109 | "version": entry.Version, 110 | "_id": entry.ID, 111 | }, bson.M{"$set": map[string]interface{}{ 112 | "amount": entry.Amount, 113 | "version": (entry.Version + 1), 114 | }}) 115 | 116 | if err != nil { 117 | log.Println("got errors: ", err) 118 | goto LOOP 119 | } 120 | 121 | *data.Result <- entry.Amount 122 | } 123 | } 124 | 125 | }(&in[i], i) 126 | } 127 | 128 | log.Println("Listen server on " + port + " port") 129 | http.HandleFunc("/", pay) 130 | if err := http.ListenAndServe(":"+port, nil); err != nil { 131 | log.Fatal(err) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-transaction-example 2 | 3 | Included examples to guide how to make transaction using Golang. 4 | 5 | ## Install dependency lib 6 | 7 | ``` 8 | $ go get github.com/globalsign/mgo/bson 9 | $ go get github.com/globalsign/mgo 10 | ``` 11 | 12 | ## Scenario 13 | 14 | It demonstrate a simple server that serve pay user monery from the bank. Example steps: 15 | 16 | 1. Init bank account with an amount is 1000USD. 17 | 2. If there is a request is called to server, user will get 50$. 18 | 3. If pay is ok, calculate remain balance then update to DB. 19 | 20 | So based on this example, The maximum times user can get pay is 20 times ( 20 X 50$ = 1000$ ), If user can be pay over 20 times, our system get fraud. 21 | 22 | ## Testing 23 | 24 | Install [vegeta](https://github.com/tsenart/vegeta) HTTP load testing tool and library. 25 | 26 | ``` 27 | $ go get -u github.com/tsenart/vegeta 28 | ``` 29 | 30 | Sytax: 31 | 32 | ``` 33 | echo "GET http://localhost:8000" | vegeta attack -rate=1000 -duration=1s | tee results.bin | vegeta report 34 | ``` 35 | 36 | ## Answer 37 | 38 | First solution is using `sync.Mutex` to Lock other thread access this section of code until it's unlocked. You can see the [example code](./safe/safe.go). 39 | 40 | ## Alternative method 41 | 42 | ### Queue [single_queue.go](./queue/single_queue.go) 43 | 44 | Each payment is processed one by one.Take a look at [single_queue.go](./queue/single_queue.go). I have implemented 2 channels. The first one is input channel (user acccount) and other one is output channel (Get user amount). 45 | 46 | ### Multiple queues [multiple_queue.go](./multiple_queue/multiple_queue.go) 47 | 48 | I have 100 users and there are 10( Q ) queues are listening are numbered from 0 -> 9 ( 10 -1 ). If user X ( 0-> 99 ) I calculate what queue it should be used. My rule is simple by get modulo of X by Q. 49 | 50 | * X = 41, Q = 10 -> The queue should be process for this request is 41 % 10 = 1 ( first queue ) 51 | * X = 33, Q = 10 -> The queue should be process for this request is 33 % 10 = 3 ( third queue ) 52 | 53 | ### Optimistic concurrency control [optimistic.go](./optimistic/optimistic.go) 54 | 55 | **If you run multiple application, please following the solutio.** 56 | 57 | [Optimistic concurrency control](http://en.wikipedia.org/wiki/Optimistic_concurrency_control) (or `optimistic locking`) is usually implemented as an application-side method for handling concurrency, often by object relational mapping tools like Hibernate. 58 | 59 | In this scheme, all tables have a version column or last-updated timestamp, and all updates have an extra WHERE clause entry that checks to make sure the version column hasn’t changed since the row was read. The application checks to see if any rows were affected by the UPDATE and if none were affected, treats it as an error and aborts the transaction. 60 | 61 | ## Benchmark log 62 | 63 | Testing in [Digital Ocean](https://www.digitalocean.com/) 64 | 65 | * OS: Ubuntu 16.04.4 x64 66 | * Memory: 4 GB 67 | 68 | **500 requests per second** 69 | 70 | [safe.go](./safe/safe.go) using `sync.Mutex` 71 | 72 | ``` 73 | $ echo "GET http://localhost:8000" | vegeta attack -rate=500 -duration=1s | tee results.bin | vegeta report 74 | Requests [total, rate] 500, 501.00 75 | Duration [total, attack, wait] 27.248468672s, 997.999728ms, 26.250468944s 76 | Latencies [mean, 50, 95, 99, max] 13.171447347s, 13.256585994s, 25.01617146s, 26.093162165s, 26.250468944s 77 | Bytes In [total, mean] 1000, 2.00 78 | Bytes Out [total, mean] 0, 0.00 79 | Success [ratio] 100.00% 80 | Status Codes [code:count] 200:500 81 | Error Set: 82 | ``` 83 | 84 | **500 requests per second** 85 | 86 | [optimistic.go](./optimistic/optimistic.go) using [Optimistic concurrency control](http://en.wikipedia.org/wiki/Optimistic_concurrency_control) 87 | 88 | ``` 89 | $ echo "GET http://localhost:8000" | vegeta attack -rate=500 -duration=1s | tee results.bin | vegeta report 90 | Requests [total, rate] 500, 501.00 91 | Duration [total, attack, wait] 5.285286131s, 997.999795ms, 4.287286336s 92 | Latencies [mean, 50, 95, 99, max] 1.903748023s, 1.983848904s, 4.049558826s, 4.516593338s, 5.016707396s 93 | Bytes In [total, mean] 1000, 2.00 94 | Bytes Out [total, mean] 0, 0.00 95 | Success [ratio] 100.00% 96 | Status Codes [code:count] 200:500 97 | Error Set: 98 | ``` 99 | 100 | **500 requests per second, run 60 seconds: total 30000 request** 101 | 102 | [single_queue.go](./queue/single_queue.go) using `goroutine` + `channel` 103 | 104 | ``` 105 | $ echo "GET http://localhost:8000" | vegeta attack -rate=500 -duration=60s | tee results.bin | vegeta report 106 | Requests [total, rate] 30000, 500.02 107 | Duration [total, attack, wait] 59.999000882s, 59.997999731s, 1.001151ms 108 | Latencies [mean, 50, 95, 99, max] 763.662µs, 678.816µs, 862.271µs, 1.570812ms, 66.078117ms 109 | Bytes In [total, mean] 60000, 2.00 110 | Bytes Out [total, mean] 0, 0.00 111 | Success [ratio] 100.00% 112 | Status Codes [code:count] 200:30000 113 | Error Set: 114 | ``` 115 | 116 | **500 requests per second, run 60 seconds: total 30000 request** 117 | 118 | [multiple_queue.go](./multiple_queue/multiple_queue.go) using `goroutine` + `channel` 119 | 120 | ``` 121 | $ echo "GET http://localhost:8000" | vegeta attack -rate=500 -duration=60s | tee results.bin | vegeta report 122 | Requests [total, rate] 30000, 500.02 123 | Duration [total, attack, wait] 59.9988601s, 59.997999803s, 860.297µs 124 | Latencies [mean, 50, 95, 99, max] 789.131µs, 723.723µs, 950.715µs, 1.516693ms, 49.270982ms 125 | Bytes In [total, mean] 60000, 2.00 126 | Bytes Out [total, mean] 0, 0.00 127 | Success [ratio] 100.00% 128 | Status Codes [code:count] 200:30000 129 | Error Set: 130 | ``` 131 | 132 | **500 requests per second, run 60 seconds: total 30000 request** 133 | 134 | [optimistic_queue.go](./optimistic_queue/single_queue.go) using `goroutine` + `channel` + `Optimistic concurrency control`。Run two application in PORT `8081` and `8082` 135 | 136 | ``` 137 | $ echo "GET http://localhost:8081" | vegeta attack -rate=500 -duration=60s | tee results.bin | vegeta report 138 | Requests [total, rate] 30000, 500.02 139 | Duration [total, attack, wait] 59.999107154s, 59.997999809s, 1.107345ms 140 | Latencies [mean, 50, 95, 99, max] 1.297197ms, 826.466µs, 1.568221ms, 3.047957ms, 139.045488ms 141 | Bytes In [total, mean] 60000, 2.00 142 | Bytes Out [total, mean] 0, 0.00 143 | Success [ratio] 100.00% 144 | Status Codes [code:count] 200:30000 145 | Error Set: 146 | ``` 147 | 148 | **500 requests per second, run 60 seconds: total 30000 request** 149 | 150 | [optimistic_multiple_queue.go](./optimistic_multiple_queue/multiple_queue.go) using `goroutine` + `channel` + `Optimistic concurrency control`。Run two application in PORT `8081` and `8082` 151 | 152 | ``` 153 | $ echo "GET http://localhost:8081" | vegeta attack -rate=500 -duration=60s | tee results.bin | vegeta report 154 | Requests [total, rate] 30000, 500.02 155 | Duration [total, attack, wait] 59.99868945s, 59.997999842s, 689.608µs 156 | Latencies [mean, 50, 95, 99, max] 924.951µs, 821.617µs, 1.2388ms, 1.978441ms, 51.268963ms 157 | Bytes In [total, mean] 60000, 2.00 158 | Bytes Out [total, mean] 0, 0.00 159 | Success [ratio] 100.00% 160 | Status Codes [code:count] 200:30000 161 | Error Set: 162 | ``` 163 | 164 | Conclustion: 165 | 166 | | | max Latencies | mean Latencies | user account | 167 | |---------------------------|---------------|----------------|--------------| 168 | | sync lock | 26.250468944s | 13.171447347s | 1 | 169 | | optimistic lock | 5.016707396s | 1.903748023s | 1 | 170 | | single queue | 66.078117ms | 763.662µs | 1 | 171 | | multiple queue | 49.270982ms | 789.131µs | 100 | 172 | | optimistic single queue | 139.045488ms | 1.297197ms | 1 | 173 | | optimistic multiple queue | 51.268963ms | 924.951µs | 100 | 174 | 175 | 176 | ref: [PostgreSQL anti-patterns: read-modify-write cycles](https://blog.2ndquadrant.com/postgresql-anti-patterns-read-modify-write-cycles/) 177 | --------------------------------------------------------------------------------