在关系型数据库领域,为人津津乐道的一个特性,便是数据库的锁设计及事务隔离级别。

本文通过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和写账户可并发操作,满足前面表格的场景。