手撕分布式缓存---互斥锁的优化

相信学习本第二章节的朋友都是已经对分布式缓存的核心数据结构与LRU算法的实现了熟于心了的,那么基于我们已经掌握的知识要如何做优化与新增功能呢?毕竟仅仅简单的定义缓存的数据结构与实现基础功能并不能满足我们对分布式缓存的功能需求。带着对未知功能实现的向往,下面我们将要开始学习了。

我们先达成一个共识,使用分布式方式的项目一定是高并发的,是对QPS、TPS、RT有要求的,也就是数据量不是普通测试环境的小规模。 那么我们提出几个场景,并基于此寻找解决方式:


  1. 当有大量对缓存的读与写操作时,应该如何保证最终的结果是按照预期的顺序进行的? 上锁的方式可以实现我们的资源同一时间只被一个线程或协程操作,而因为读写锁比互斥锁的性能更高,且对资源的访问不需要锁,因此我们使用读写锁来实现这一效果。

  2. 查询性能优化的方式还有一种,分组。 拿一个我们最经常见的例子举例:数据库表结构的定义,通常我们在定义多对多关系的表结构时,会引入一个中间表,并将两个对象分别定义为两张表,这样做的好处是每张表的数据量都很小,尽管查询的时候需要关联查询,但是在数据量大的情况下这样的优化是必要的。分组同理,通过将数据根据命名空间分开存储在不同的cache中,来降低数据量的大小,进而减少查询数据的时间。


前文链接:

手撕分布式缓存之一 | 定义缓存结构体与实现底层功能函数


系列目录

  • (1)互斥锁的优化以及命名空间的实现
    • (1.1)前言
    • (1.2)Add & Get 功能的上锁
    • (1.3)命名空间结构的设计与实现
    • (1.4)代码实现
  • (2)细节整理
    • (2.1)接口型函数的讨论

(1)互斥锁的优化以及命名空间的实现

(1.1)前言

通过这篇文章,大家可以学习到读写锁与互斥锁的区别功能封装的结构设计(需要结合上一章节)
实现命名空间功能的数据结构接口型函数等

(1.2)Add & Get 功能的上锁

锁的作用对于单线程来说是没有意义的,因为一个线程执行的程序不可能发生对资源使用的冲突,只有多线程的情况下,在可能发生资源使用冲突的临界区使用上锁的操作来预防冲突。可以预想到的两种会发生冲突的操作,查询(读)与新增(写)
查询操作相关的限制是:可以允许多个线程进行查询;但是不允许存在既有线程查询又有线程新增,因为当一个线程正在修改时,执行读取/修改操作的线程应该等待上一个修改操作结束之后再进行读取。值得注意的是:将逻辑落实到代码中时,是要人为的去判断当前的临界区中是只读、只写还是读写操作,进而使用对应的锁进行限制。(之前写代码的时候都是通过代码实现逻辑,代码本身就拥有逻辑,突然写锁的时候,脑子没有转过来,没有理解声明锁的时候需要人为去指定)

(1.3)命名空间结构的设计与实现

命名空间最基础的两个属性,名称name和存储主体mainCache,name使用string类型,存储主体mainCache的类型为上一章节实现的cacha类型。另外在group层为用户提供了自定义处理未命中数据的函数参数,在数据未命中时可以调用此函数进行数据获取,比如之后的章节中如果实现了分布式存储的话,这里的自定义函数getter就可以实现从多个节点中查询数据。

(1.4)代码实现

规定的值value的数据结构

  • 定义存储的内容与方法返回的值类型为ByteView,由于底层的限制,因此需要实现Len方法
package geecache
// insert and get's value all is []byte
type ByteView struct {
	b []byte
}

// Len returns the view's length, if not inherit its func, Value can't transform to this ByteView type
func (v ByteView) Len() int {
	return len(v.b)
}


对Get与Add方法上锁

  • 对下层的结构再封装,Cache由互斥锁Mutex、自定义缓存结构CacheLRU、最大缓存限制int64,三个数据结构构成Cache类型。
  • 对Get方法上读锁,因为Get方法内部没有对共享资源的修改,允许多个线程同一时间内访问。内部功能实现通过调用对底层封装的功能 .Get 实现。
  • 对Add方法上写锁,因为Add方法内部有对共享资源的修改,不允许多个线程同一时间内访问。
package geecache

import (
	"gee-cache/my_gee_cache/geecache/lru"
	"sync"
)

type Cache struct {
	// set the sync lock
	mu       sync.Mutex
	cache    *lru.CacheLRU
	maxBytes int64
}

func (c *Cache) Get(key string) (value ByteView, ok bool) {
	// it also can substitute for RLock, cause it only have read operation
	c.mu.Lock()

	// if not the correct result, directly return
	if c.cache == nil {
		return
	}

	// correct result, return's value is ByteView
	if ele, ok := c.cache.Get(key); ok {
		return ele.(ByteView), true
	}
	defer c.mu.Unlock()
	return
}

func (c *Cache) Add(key string, value ByteView) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if c.cache == nil {
		// here is an Encapsulation of lower layers
		nc := lru.NewCacheLRU(c.maxBytes, nil)
		c.cache = nc
	}
	// the type ByteView, which has implements func Len()
	c.cache.Add(key, value)
}


命名空间的数据结构与功能封装

  • 定义了两个全局变量,类型为RWMutex与map[string]*Group;Group的数据结构定义如上文介绍所述。
  • NewGroupItem,使用名称、命名空间下缓存的最大长度以及未命中时的操作。
  • GetGroup,根据命名空间获取其下的缓存。
  • Get,根据已经获取到的Group,通过key查找对应的value。
  • Load、FindLocally,两个函数起到的功能均是根据具体情况,在未命中的背景下,去对应的位置查找key对应的value,可以自定义。
package geecache

import (
	"fmt"
	"sync"
)

type Getter interface {
	// Get func Gets val type is string
	Get(key string) ([]byte, error)
}

type FuncGetter func(key string) ([]byte, error)

func (f FuncGetter) Get(key string) ([]byte, error) {
	return f(key)
}

type Group struct {
	name      string
	mainCache Cache
	getter    Getter
}

var (
	mu     sync.RWMutex
	groups = make(map[string]*Group)
)

// NewGroupItem initialize group item
func NewGroupItem(name string, byteCache int64, getter Getter) (*Group, error) {
	mu.Lock()
	defer mu.Unlock()
	if group := groups[name]; group != nil {
		return nil, fmt.Errorf("group [%s] aleadry exist", name)
	}
	g := &Group{
		name:      name,
		getter:    getter,
		mainCache: Cache{maxBytes: byteCache},
	}

	groups[name] = g
	return g, nil
}

// GetGroup get group according to name
func GetGroup(name string) (*Group, error) {
	mu.RLock()
	defer mu.RUnlock()

	if g := groups[name]; g == nil {
		return nil, fmt.Errorf("group [%s] not exist", name)
	}

	return groups[name], nil
}

// Get get value according to key from group
func (g *Group) Get(key string) (ByteView, error) {
	if key == "" {
		return ByteView{}, fmt.Errorf("not fount key [%s]", key)
	}

	mu.RLock()
	defer mu.RUnlock()

	// if hit then return, else find the key in another region
	if bv, ok := g.mainCache.Get(key); ok {
		return bv, nil
	}

	bv, err := g.Load(key)
	if err != nil {
		return ByteView{}, err
	}

	return bv, nil
}

func (g *Group) Load(key string) (ByteView, error) {
	bv, err := g.FindLocally(key)
	if err != nil {
		return ByteView{}, err
	}
	return bv, nil
}

func (g *Group) FindLocally(key string) (ByteView, error) {
	return ByteView{b: []byte("not found")}, nil
}

(2)细节整理

(2.1)接口型函数的讨论

这种方式是之前在c、java、python中都没有遇到的,本以为很新颖、有什么特别的过人之处,但是转了一圈大家的理解都是实现了接口型函数后,原本的类型传递既可以传递接口类型,也可以传递函数类型,因为没有具体使用,还没有发现特别之处。

你可能感兴趣的:(分布式缓存,分布式,缓存,go,读写锁,性能优化,读写优化,分布式缓存)