go语言高并发缓存.md

  • 在高并发场景下,可以在内存中先缓存一些只读的数据,减少方法数据库的请求,缓解数据库压力。但内存容量有限。需要算法移除一些数据
  • 常用的淘汰算法主要有:
    1. FIFO(first in first out,先进先出)
    2. LFU(least frequently used,最少使用)
    3. LRU(least recently used,最近最少使用)
  1. FIFO算法
    • 淘汰缓存中最早添加的记录,用一个双向队列实现,数据不断从队尾添加,当队列满时,移除队首元素
    • 缺点:部分记录虽然是最早添加都,但后面可能也会被访问,但最早添加的数据不断被移除,也使得这部分数据不断被添`加、移除。降低性能。
       //缓存接口
       type Cache interface {
           //添加数据。以key-value键值对的形式
           Set(key string,value interface{})
           //获取数据
           Get(key string) interface{}
           //删除数据
           Del(key string)
           //删除最老的数据
           DelOldest()
           //获取缓存队列长度
           Len()int
       }
    
       type Value interface {
           Len() int
       }
       //获取value数据的大小
       func CalcLen(value interface{}) int {
           var n int
    
           switch v:=value.(type) {
           case Value:
               n=v.Len()
           case string:
               if runtime.GOARCH=="amd64"{
                   n=16+len(v)
               }else {
                   n=8+len(v)
               }
           case bool,uint8,int8:
               n=1
           case int16,uint16:
               n=2
           case int32,uint32,float32:
               n=4
           case int64,uint64,float64:
               n=8
           case int, uint:
               if runtime.GOARCH=="amd64"{
                   n=8
               }else {
                   n=4
               }
           case complex64:
               n=8
           case complex128:
               n=16
           default:
               panic(fmt.Sprintf("元素不合法"))
           }
           return n
       }
       //实现
       
       type fifo struct {
           //最大储存字节数
           maxBytes int
           //回调方法。可以在删除数据的时候被调用
           onEvicted func(key string , value interface{})
           //当前已使用的字节数
           useBytes int
           //缓存队列
           ll *list.List
           //缓存map集合。方便获取数据
           cache map[string]*list.Element
       }
       //新增修改元素
       func (f fifo) Set(key string, value interface{}) {
           if e,ok := f.cache[key]; ok{
               f.ll.MoveToBack(e)
               en:=e.Value.(*entry)
               f.useBytes=f.useBytes-c.CalcLen(en.value)+c.CalcLen(value)
               en.value=value
           }
           en:=&entry{key: key,value: value}
           e := f.ll.PushBack(en)
           f.cache[key]=e
           f.useBytes+=en.Len()
           if f.useBytes>0&&f.useBytes>f.maxBytes{
               f.DelOldest()
           }
       }
       //获取缓存中的元素
       func (f fifo) Get(key string) interface{} {
           if e,ok := f.cache[key] ;ok{
               return e.Value.(*entry).value
           }
           return nil
       }
       //删除元素
       func (f fifo) Del(key string) {
           if e,ok:=f.cache[key] ;ok{
               f.removeElement(e)
           }
       }
       //删除最老元素
       func (f fifo) DelOldest() {
           f.removeElement(f.ll.Front())
       }
       //获取队列长度
       func (f fifo) Len() int {
           return  f.ll.Len()
       }
    
       func (f fifo) removeElement(e *list.Element) {
           if e==nil{
               return
           }
           f.ll.Remove(e)
           ebn:=e.Value.(*entry)
           f.useBytes-=ebn.Len()
           delete(f.cache,ebn.key)
           if f.onEvicted!=nil{
               f.onEvicted(ebn.key,ebn.value)
           }
       }
    
       type entry struct {
           key string
           value interface{}
       }
    
       func (e *entry)Len() int  {
           return c.CalcLen(e.value)
       }
       //生成缓存结构
       func New(maxBytes int,onEvicted func(key string,value interface{})) c.Cache  {
           return &fifo{
               maxBytes: maxBytes,
               onEvicted: onEvicted,
               ll: list.New(),
               cache: make(map[string]*list.Element),
           }
       }
    
    
  2. LFU算法
    • 淘汰缓存中访问次数最少的数据,为缓存中的每一个数据添加一个count,每次访问count+1,缓存队列以count值排序,每次count加一队列重新排序,在删除数据时删除count最小的数据。
    • 缺点:维护每个数据的访问次数对内存来说是一种浪费,并且如果数据的访问模式发送变化,则算法也需要时间重新排列,LFU算法受历史数据的影响比较大。
       //淘汰缓存中访问次数最少的数据
       type lfu struct {
           maxBytes int
           onEvicted func(key string,value interface{})
           usedBytes int
           queue *queue
           cache map[string]*enerty
       }
       //添加数据,如果key已经存在,更新数据,count+1
       func (l *lfu) Set(key string, value interface{}) {
           if e,ok:=l.cache[key];ok{
               l.usedBytes=l.usedBytes+caches.CalcLen(e.value)+caches.CalcLen(value)
               l.queue.update(e,value,e.count+1)
               return
           }
           //不存在则生成数据,添加到堆中
           en:= &enerty{key: key,value: value}
           heap.Push(l.queue,en)
           l.cache[key] = en
           l.usedBytes+=en.Len()
           //如果内存不足就删除权重最小的点,就是最小堆的顶点
           if l.maxBytes>0&&l.usedBytes>l.maxBytes{
               l.removeElement(heap.Pop(l.queue))
           }
       }
    
       func (l lfu) Get(key string) interface{} {
           if e,ok:=l.cache[key];ok{
               l.queue.update(e,e.value,e.count+1)
               return e.value
           }
           return nil
       }
       //更新数据并重建堆
       func (q *queue) update(e *enerty, value interface{}, i int) {
           e.value = value
           e.count=i
           heap.Fix(q,e.index)
       }
       func (l lfu) Del(key string) {
           if e,ok := l.cache[key];ok{
               heap.Remove(l.queue,e.index)
               l.removeElement(e)
           }
       }
    
       func (l lfu) DelOldest() {
           if l.queue.Len()==0{
               return
           }
           l.removeElement(heap.Pop(l.queue))
       }
    
       func (l lfu) Len() int {
           l.queue.Len()
       }
    
       func (l *lfu) removeElement(e interface{}) {
           if e==nil{
               return
           }
           en:=e.(*enerty)
           delete(l.cache,en.key)
           l.usedBytes-=en.Len()
           if l.onEvicted!=nil{
               l.onEvicted(en.key,en.value)
           }
       }
    
       type enerty struct {
           key string
           value interface{}
           count int
           index int
       }
       type queue []*enerty
    
       func (e *enerty)Len() int {
           return caches.CalcLen(e.value)+4+4
       }
    
       func(q queue)Len()int{
           return len(q)
       }
       func (q queue)Less(i,j int) bool  {
           return q[i].count<q[j].count
       }
       func (q *queue)Push( x interface{})  {
           n:=len(*q)
           en:=x.(*enerty)
           en.index=n
           *q=append(*q,en)
       }
       func (q queue)Swap(i,j int)  {
           q[i],q[j] = q[j],q[i]
           q[i].index = i
           q[j].index = j
       }
       func (q *queue)Pop() interface{}  {
           old := *q
           n:=len(old)
           en:= old[n-1]
           old[n-1]=nil
           en.index = -1
           *q = old[0:n-1]
           return en
       }
    
       func New(maxBytes int,onEvicted func(key string,value interface{})) caches.Cache {
    
           q:=make(queue,0,1024)
           return &lfu{
               maxBytes:  maxBytes,
               onEvicted: onEvicted,
               queue:     &q,
               cache:     make(map[string]*enerty),
           }
    
       }
    
  3. LRU算法
    • 维护一个队列,如果某条数据被访问了,则把这条数据添加到队尾,队首就是最近最少使用的数据,淘汰队首数据即可。
      //与FIFO算法类似,在查找元素时,只有有访问就将该数据添加到队尾
      //获取缓存中的元素
      func (f *lru) Get(key string) interface{} {
          if e,ok := f.cache[key] ;ok{
              f.ll.MoveToBack(e)
              return e.Value.(*entry).value
          }
          return nil
      }
      
  • 进程内缓存
  • 在缓存中添加锁,每一次set和get都要获取,释放锁。
        //进程内部缓存
        const DefaultMaxBytes = 1 << 29
        type safeCache struct {
            m sync.RWMutex
            cache Cache
            nhit ,nget int
        }
        func newSafeCache(cache Cache) *safeCache{
            return &safeCache{
                cache: cache,
            }
        }
        func (sc *safeCache)set(key string,value interface{})  {
            sc.m.Lock()
            defer sc.m.Unlock()
            sc.cache.Set(key ,value)
        }
        func (sc *safeCache)get(key string)  interface{}{
            sc.m.RLock()
            defer sc.m.RUnlock()
            sc.nget++
            if sc.cache==nil{
                return nil
            }
            v:=sc.cache.Get(key)
            if v!=nil{
                log.Println("hit!")
                sc.nhit++
            }
            return v
        }
    
        type Stat struct {
            NHit ,NGet int
        }
    
        func (sc *safeCache)stat() *Stat  {
            sc.m.RLock()
            defer sc.m.RUnlock()
            return &Stat{
                NHit: sc.nhit,
                NGet: sc.nget,
            }
        }
        ```
    
  • 缓存优化:
    • 在高并发场景下不断的加锁,释放锁非常影响性能,根据开源库bigCache,可以将整体数据划分为组,组的索引用哈希计算,
      对于每一个缓存对象,根据它的key计算它的哈希值: hash(key) % N, N是分片数量。 理想情况下N个 goroutine 每次请求正好平均落在各自的分片上,这样就不会有竞争了,即使有多个goroutine落在同一个分组上,如果hash比较平均的话,单个组的压力也会比较小。高并发下获取数据时先hash(key)获取到组,然后再对组内的数据进行操作,不同的协程对应不同的组,这样协程之间的竞争会少些。
    • GC优化,go语言的GC是把map当作一个对象扫描,在1.5之后,如果map的key-value都是基本类型,GC则不会扫描,通常是把key-value序列化存储
posted @ 2021-01-23 11:56  月下繁星杨  阅读(144)  评论(0编辑  收藏  举报