在關(guān)系型數(shù)據(jù)庫(kù)領(lǐng)域,為人津津樂(lè)道的一個(gè)特性,便是數(shù)據(jù)庫(kù)的鎖設(shè)計(jì)及事務(wù)隔離級(jí)別。
本文通過(guò)golang系統(tǒng)庫(kù)sync,來(lái)實(shí)現(xiàn)簡(jiǎn)單的數(shù)據(jù)庫(kù)數(shù)據(jù)讀寫(xiě)操作。
場(chǎng)景說(shuō)明
小明經(jīng)營(yíng)一家水果店,創(chuàng)業(yè)初始資金為100000元,所有的收入以及支出通過(guò)2個(gè)銀行賬戶進(jìn)行往來(lái)。
因交易頻繁,可能存在并發(fā)更新賬戶數(shù)據(jù)及查賬的需求,需要保障賬戶數(shù)據(jù)針對(duì)所有操作的一致性。
此處需要引入讀寫(xiě)鎖,保障讀寫(xiě)的安全性及高效性。
需求分析
在MySQL中,使用InnoDB存儲(chǔ)引擎,配合合適的事務(wù)隔離級(jí)別,可以做到數(shù)據(jù)行級(jí)鎖定,也就是:
| 操作類(lèi)型 | 查賬戶A | 查賬戶B | 寫(xiě)賬戶A | 寫(xiě)賬戶B |
|---|---|---|---|---|
| 查賬戶A | 可并發(fā) | 可并發(fā) | 互斥 | 可并發(fā) |
| 查賬戶B | 可并發(fā) | 可并發(fā) | 可并發(fā) | 互斥 |
| 寫(xiě)賬戶A | 互斥 | 可并發(fā) | 互斥 | 可并發(fā) |
| 寫(xiě)賬戶B | 可并發(fā) | 互斥 | 可并發(fā) | 互斥 |
賬戶A和賬戶B的讀寫(xiě)操作相互獨(dú)立,最大化賬戶的并發(fā)操作。
那么,如何使用golang實(shí)現(xiàn)簡(jiǎn)單表格中的場(chǎng)景呢?另外, 是否可以設(shè)置讀寫(xiě)的優(yōu)先級(jí)呢?
我們下面先來(lái)介紹下golang的兩個(gè)類(lèi):
- sync.RWMutex
讀寫(xiě)鎖,支持單寫(xiě)多讀特性。區(qū)別于sync.Mutex的全局互斥鎖特性(不支持同時(shí)讀) - sync.WaitGroup
可通過(guò)Add方法,將請(qǐng)求分組,同一組的gorountine可通過(guò)Wait方法,控制組內(nèi)所有g(shù)orountine全部結(jié)束,才能繼續(xù)主線程,否則一直阻塞主線程。
如下代碼為實(shí)現(xiàn)樣例,假設(shè)當(dāng)前有如下數(shù)量請(qǐng)求并發(fā):
5個(gè)A賬戶讀,B賬號(hào)讀,3個(gè)A賬號(hào)寫(xiě),B賬戶寫(xiě)
其中A賬戶設(shè)置了低優(yōu)先級(jí)讀。
功能實(shí)現(xiàn)
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// 賬戶的初始數(shù)據(jù)
var accountTypeMap = map[string]int{
"A": 50000,
"B": 50000,
}
// init方法,設(shè)置seed,用于控制隨機(jī)數(shù)生成的初始值,確保其隨機(jī)性
func init() {
rand.Seed(time.Now().Unix())
}
func sleep() {
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
}
// 讀取賬號(hào)余額,低優(yōu)先級(jí)的讀,采取隨機(jī)sleep的方式,等待請(qǐng)求
func readAccount(accountType string, m *sync.RWMutex, wg *sync.WaitGroup, lowPriority ...string) {
if (len(lowPriority)) > 0 {
sleep()
}
// 使用讀鎖
m.RLock()
fmt.Println("time:", time.Now().UnixNano()/1e6, " read account ", accountType, " left money:", accountTypeMap[accountType])
// sleep 10毫秒,方便確認(rèn)并發(fā)是否生效
time.Sleep(time.Duration(10) * time.Millisecond)
// 釋放讀鎖
m.RUnlock()
wg.Done()
}
// 寫(xiě)入賬號(hào),低優(yōu)先級(jí)的寫(xiě),采取隨機(jī)sleep的方式,等待請(qǐng)求
func writeAccount(accountType string, addMoney int, m *sync.RWMutex, wg *sync.WaitGroup, lowPriority ...string) {
if (len(lowPriority)) > 0 {
sleep()
}
// 使用寫(xiě)鎖(排他鎖)
m.Lock()
accountTypeMap[accountType] = accountTypeMap[accountType] + addMoney
fmt.Println("time:", time.Now().UnixNano()/1e6, "modify account ", accountType, " add money:", addMoney)
// sleep 10毫秒,方便確認(rèn)并發(fā)是否生效
time.Sleep(time.Duration(10) * time.Millisecond)
m.Unlock()
wg.Done()
}
func main() {
var mutexA sync.RWMutex
var mutexB sync.RWMutex
wg := sync.WaitGroup{}
// 設(shè)置5個(gè)A賬戶讀,B賬號(hào)讀,3個(gè)A賬號(hào)寫(xiě),B賬戶寫(xiě)
for i := 0; i < 5; i++ {
wg.Add(1)
go readAccount("A", &mutexA, &wg, "lowpriority")
}
for i := 0; i < 5; i++ {
wg.Add(1)
go readAccount("B", &mutexB, &wg)
}
for i := 0; i < 3; i++ {
wg.Add(1)
go writeAccount("A", 1000, &mutexA, &wg)
}
for i := 0; i < 3; i++ {
wg.Add(1)
go writeAccount("B", 3000, &mutexB, &wg)
}
wg.Wait()
fmt.Println("account A left: ", accountTypeMap["A"])
fmt.Println("account B left: ", accountTypeMap["B"])
}
結(jié)果分析
返回的結(jié)果具體隨機(jī)性,其中A讀具有低優(yōu)先級(jí),返回在最后。以下為一種結(jié)果:
time: 1565345040128 modify account B add money: 3000
time: 1565345040128 modify account A add money: 1000
time: 1565345040139 read account B left money: 53000
time: 1565345040139 read account B left money: 53000
time: 1565345040139 modify account A add money: 1000
time: 1565345040139 read account B left money: 53000
time: 1565345040139 read account B left money: 53000
time: 1565345040139 read account B left money: 53000
time: 1565345040149 modify account A add money: 1000
time: 1565345040149 modify account B add money: 3000
time: 1565345040160 modify account B add money: 3000
time: 1565345040493 read account A left money: 53000
time: 1565345040693 read account A left money: 53000
time: 1565345040693 read account A left money: 53000
time: 1565345040828 read account A left money: 53000
time: 1565345041091 read account A left money: 53000
account A left: 53000
account B left: 59000
從前面2行的返回結(jié)果看,寫(xiě)賬戶A和寫(xiě)賬戶B 可并發(fā)操作,
從第3,4,5 行看,讀賬戶B和寫(xiě)賬戶可并發(fā)操作,滿足前面表格的場(chǎng)景。