在关系型数据库领域,为人津津乐道的一个特性,便是数据库的锁设计及事务隔离级别。
本文通过golang系统库sync,来实现简单的数据库数据读写操作。
场景说明
小明经营一家水果店,创业初始资金为100000元,所有的收入以及支出通过2个银行账户进行往来。
因交易频繁,可能存在并发更新账户数据及查账的需求,需要保障账户数据针对所有操作的一致性。
此处需要引入读写锁,保障读写的安全性及高效性。
需求分析
在MySQL中,使用InnoDB存储引擎,配合合适的事务隔离级别,可以做到数据行级锁定,也就是:
操作类型 | 查账户A | 查账户B | 写账户A | 写账户B |
---|---|---|---|---|
查账户A | 可并发 | 可并发 | 互斥 | 可并发 |
查账户B | 可并发 | 可并发 | 可并发 | 互斥 |
写账户A | 互斥 | 可并发 | 互斥 | 可并发 |
写账户B | 可并发 | 互斥 | 可并发 | 互斥 |
账户A和账户B的读写操作相互独立,最大化账户的并发操作。
那么,如何使用golang实现简单表格中的场景呢?另外, 是否可以设置读写的优先级呢?
我们下面先来介绍下golang的两个类:
- sync.RWMutex
读写锁,支持单写多读特性。区别于sync.Mutex的全局互斥锁特性(不支持同时读) - sync.WaitGroup
可通过Add方法,将请求分组,同一组的gorountine可通过Wait方法,控制组内全部结束,才能完成gorountine,否则一直阻塞主线程。
如下代码为实现样例,假设当前有如下数量请求并发:
5个A账户读,B账号读,3个A账号写,B账户写
其中A账户设置了低优先级读。
功能实现
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// 账户的初始数据
var accountTypeMap = map[string]int{
"A": 50000,
"B": 50000,
}
// init方法,设置seed,用于控制随机数生成的初始值,确保其随机性
func init() {
rand.Seed(time.Now().Unix())
}
func sleep() {
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
}
// 读取账号余额,低优先级的读,采取随机sleep的方式,等待请求
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毫秒,方便确认并发是否生效
time.Sleep(time.Duration(10) * time.Millisecond)
// 释放读锁
m.RUnlock()
wg.Done()
}
// 写入账号,低优先级的写,采取随机sleep的方式,等待请求
func writeAccount(accountType string, addMoney int, m *sync.RWMutex, wg *sync.WaitGroup, lowPriority ...string) {
if (len(lowPriority)) > 0 {
sleep()
}
// 使用写锁(排他锁)
m.Lock()
accountTypeMap[accountType] = accountTypeMap[accountType] + addMoney
fmt.Println("time:", time.Now().UnixNano()/1e6, "modify account ", accountType, " add money:", addMoney)
// sleep 10毫秒,方便确认并发是否生效
time.Sleep(time.Duration(10) * time.Millisecond)
m.Unlock()
wg.Done()
}
func main() {
var mutexA sync.RWMutex
var mutexB sync.RWMutex
wg := sync.WaitGroup{}
// 设置5个A账户读,B账号读,3个A账号写,B账户写
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"])
}
结果分析
返回的结果具体随机性,其中A读具有低优先级,返回在最后。以下为一种结果:
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行的返回结果看,写账户A和写账户B 可并发操作,
从第3,4,5 行看,读账户B和写账户可并发操作,满足前面表格的场景。
有疑问加站长微信联系(非本文作者)