slava是作者参与的一个github开源项目,项目的主要的工作是用Go语言构建一个高性能、K-V云数据库。 slava项目的连接
在slava(3)构建内存数据库的时候,设置了一个可以适用于高并发场景的concourrentMap,其可以保证对单个key进行操作时候的并发安全性,但是无法保证一些复杂操作的安全型,例如:MSETNX,需要判断所有的键是否都不存在数据库中,如果都不存在则将设置多个键值,如果有一个存在则不设置,所以我们在执行这个命令前,需要一次性对所有操作的key进行上锁。Incr的操作,要先对读取key的值,然后做加法,最后写入,要实现并发安全,所以需要在执行incr之前就锁住key所在的map。
如果这里我们采用的加锁方式是,对每一个key都设置一个锁,在操作命令前对所有操作的key加锁的话,那么就必须在解锁后释放锁,不然在高并发场景中会出现大量的内存泄漏。但是,由于解锁和释放锁并不是原子操作的,可能会存在锁误删的现象。
例如:我们给建立一个哈希表,表中存放每个键值的锁map[string]*sync.RWMutex
。锁误删现象如下表所示。
时间 | 协程A | 协程B |
---|---|---|
t1 | m["a"].Unlock() | |
t2 | m["a"] = &sync.RWMutex{} | |
t3 | delete(m, "a") | |
t4 | m["a"].Lock() |
如上表所示,协程A在t1时刻释放了锁,这时候(t2)协程B在这个时候初始化了键值key = “a”的锁,但是没来得及上锁,这时候(t3)协程A为了释放内存,防止内存泄漏,则删除了key = “a”的锁,然后再t4时刻协程B上锁报错,因为锁已经被协程A删除,这就是协程A误删了协程B的锁。
解决上面的锁误删的情况的办法是,不释放锁,但是不释放锁会引发内存泄漏问题。所以给每个key初始化锁的方式并不适合我们的项目场景。
既然没办法给每个key一个锁,那么作者考虑对给定的哈希槽进行加锁。原因是,我们可以设置一定数量的哈希槽,在进行复杂操作之前先对哈希槽进行加锁,哈希槽的数量远比我们的key数量要少,这时候给每个哈希槽初始化锁,即使锁不释放,不delete也不会造成太多的内存泄漏。在开始的时候哈希槽的数量设置为1024个,但是随着k-v的数量越来越多防止哈希碰撞的概率越来越大,后面会对哈希槽进行扩容,以此来减少加锁的粒度,有利于高并发。
根据这样的思想设置一个Locks的结构体,结构体中是哈希槽的数组。
// Lock结构体为key提供读写锁
type Locks struct {
table []*sync.RWMutex
}
// 创建一个lock map
func Make(tableSize int) *Locks {
table := make([]*sync.RWMutex, tableSize)
for i := 0; i < len(table); i++ {
table[i] = &sync.RWMutex{}
}
return &Locks{table: table}
}
与之前的类似,设置哈希函数
// 设置哈希函数
const ( // 用于哈希函数计算
prime32 = uint32(16777619)
)
func fnv32(key string) uint32 {
hash := uint32(2166136261)
for i := 0; i < len(key); i++ {
hash *= prime32
hash ^= uint32(key[i])
}
return hash
}
// 找到hash值对应table的索引
func (locks *Locks) spread(hashCode uint32) uint32 {
if locks == nil {
panic("dict is nil")
}
tableSize := uint32(len(locks.table))
return (tableSize - 1) & hashCode
}
实现加锁和解锁的功能
// 获取某个key的写锁
func (locks *Locks) Lock(key string) {
index := locks.spread(fnv32(key))
mu := locks.table[index]
mu.Lock()
}
// 获取key的读锁
func (locks *Locks) RLock(key string) {
index := locks.spread(fnv32(key))
mu := locks.table[index]
mu.RLock()
}
// 释放key的写锁
func (locks *Locks) UnLock(key string) {
index := locks.spread(fnv32(key))
mu := locks.table[index]
mu.Unlock()
}
// 释放key对应的读锁
func (locks *Locks) RUnLock(key string) {
index := locks.spread(fnv32(key))
mu := locks.table[index]
mu.RUnlock()
}
在锁定多个key时需要注意,若协程A持有键a的锁试图获得键b的锁,此时协程B持有键b的锁试图获得键a的锁则会形成死锁。解决方法是所有协程都按照相同顺序加锁,若两个协程都想获得键a和键b的锁,那么必须先获取键a的锁后获取键b的锁,这样就可以避免循环等待。
解开多个写锁,解开写锁的时候,按照从大到小的顺序解开。先从大的的锁开始解开,上锁从小的锁开始上锁,这样可以防止死锁。例如,A协程含有所有的资源锁a,b,c,此时B协程想要上锁,如果A协程先从小的锁开始释放的话。这时候A协程释放了a资源,这时候B协程获取了a资源,还要等待b,c 资源,然而这是A协程要处理某些情况又需要a资源。这时候A协程就会等待B协程释放a资源,而B协程等待A协程释放b,c资源,这样就形成相互等待对方的资源释放,而形成死锁。
// 锁定多个key,为了防止多个协程之间循环等待,让所有饿协程都按照一定的顺序获取keys的锁
// 例如,协程A想获得a和b的锁,此时协程A已经拥有a的锁,想获得b的锁,但是协程B也想获得a和b的锁
// 此时协程B已经获取了b的锁,等待a的锁,这种情况下就会出现相互等待对方资源释放而造成死锁的现象。
// 解决的办法,就是按照一定的的顺序进行加锁
// 获取多个keys所在的槽,并且根据reverse字段选择进行排序,如果reverse为true则按照从大到小排序,如果为false则按照从小到大排序
func (locks *Locks) toLockindices(keys []string, reverse bool) []uint32 {
// 用来存储keys所对应的所有索引
indexMap := make(map[uint32]struct{})
for _, key := range keys {
index := locks.spread(fnv32(key))
indexMap[index] = struct{}{}
}
// 预设容量,这一提升性能,避免底层频繁的扩容
indices := make([]uint32, 0, len(indexMap))
for index := range indexMap {
indices = append(indices, index)
}
sort.Slice(indices, func(i, j int) bool {
if !reverse {
return indices[i] < indices[j]
}
return indices[i] > indices[j]
})
return indices
}
// 对多个key进行上锁,按照从小到达的规则进行上锁,主要是为了防止协程相互等待对方的资源而导致死锁
// 这里对多个key值进行上写锁
func (locks *Locks) Locks(keys ...string) {
indices := locks.toLockindices(keys, false)
for _, index := range indices {
mu := locks.table[index]
mu.Lock()
}
}
// 对多个key上读锁,防止协程相互等待对方的资源而导致死锁
func (locks *Locks) RLocks(keys ...string) {
indices := locks.toLockindices(keys, false)
for _, index := range indices {
mu := locks.table[index]
mu.RLock()
}
}
// 解开多个写锁,解开写锁的时候,按照从大到小的顺序解开
// 先从大的的锁开始解开,上锁从小的锁开始上锁,这样可以防止死锁
// 例如,A协程含有所有的资源锁a,b,c,此时B协程想要上锁,如果A协程先从小的锁开始释放的话
// 这时候A协程释放了a资源,这时候B协程获取了a资源,还要等待b,c 资源,然而这是A协程要处理某些情况又需要a资源
// 这时候A协程就会等待B协程释放a资源,而B协程等待A协程释放b,c资源,这样就形成相互等待对方的资源释放,而形成死锁。
func (locks *Locks) UnLocks(keys ...string) {
indices := locks.toLockindices(keys, true)
for _, index := range indices {
mu := locks.table[index]
mu.Unlock()
}
}
// 释放多个key的读锁
func (locks *Locks) RUnLocks(keys ...string) {
indices := locks.toLockindices(keys, true)
for _, index := range indices {
mu := locks.table[index]
mu.RUnlock()
}
}
在对多个key进行操作上锁的时候,要判断这些key式只读还是可以写操作的key,如果只读的key则加读锁就可以了,这样可以提升读性能。
// 对写和读的操作进行上锁,在写和读的命令中允许存在重复的key
func (locks *Locks) RWLocks(writeKeys []string, readKeys []string) {
keys := append(writeKeys, readKeys...)
indices := locks.toLockindices(keys, false)
// 记录writeKeys中的key值的索引
writeKeysSet := make(map[uint32]struct{}, len(writeKeys))
for _, key := range writeKeys {
index := locks.spread(fnv32(key))
writeKeysSet[index] = struct{}{}
}
for _, index := range indices {
_, ok := writeKeysSet[index]
mu := locks.table[index]
if ok {
mu.Lock()
} else {
mu.RLock()
}
}
}
// 释放读写操作的锁,写和读的key可能会存在重复
func (locks *Locks) RWUnLocks(writeKeys []string, readKeys []string) {
keys := append(writeKeys, readKeys...)
indices := locks.toLockindices(keys, true)
writeKeysSet := make(map[uint32]struct{}, len(writeKeys))
for _, key := range writeKeys {
index := locks.spread(fnv32(key))
writeKeysSet[index] = struct{}{}
}
for _, index := range indices {
_, ok := writeKeysSet[index]
mu := locks.table[index]
if ok {
mu.Unlock()
} else {
mu.RUnlock()
}
}
}