ET6.0數據庫模塊(二)


本篇着重理解緩存層的DBCacheComponent,使用了LRU算法,網上找到幾個感覺不錯的文章

1、用鏈表的目的是什么?省空間還是省時間? - invalid s的回答 - 知乎 https://www.zhihu.com/question/31082722/answer/1928249851

2、數據結構和算法:鏈表(Linked List)

3、鏈表——最基本的數據結構之一 | 經典鏈表應用場景:LRU 緩存淘汰算法

4、算法必知 --- LRU緩存淘汰算法

DBCacheComponent在傳奇Demo里面有,不過作者說取自SJET項目,SJET提供了一些實用的項目模塊值得參考,比如UI管理,紅點管理,BUFF組件等,可以自行前往Github下載SJET:https://github.com/susices/SJET


進入正題,DBCacheComponent考慮了以下幾個點:

1、緩存數據管理和查詢;

2、容量達到上限時,快速定位刪除最近最少使用的數據,核心;

3、回收池;

基於LRU算法,緩存數據節點有任何操作(新加入、查詢、更新)都會視為活躍數據,移到鏈表最前面,把非活躍數據排到后面應需淘汰。


下面以實際代碼案例來講解

DBCacheComponent定義了兩個映射關系LruCacheNodes、UnitCachePool用於查詢數據和判斷數據是否加入到緩存管理,HeadCacheNode、TailCacheNode輔助管理鏈表順序,LRUCapacity在Awake時讀取配置的緩存容量常量。

using System;
using System.Collections.Generic;

namespace ET
{
    public class DBCacheComponent : Entity
    {
        public int LRUCapacity; //緩存容量
        public Dictionary<long, LRUCacheNode> LruCacheNodes = new Dictionary<long, LRUCacheNode>(); //PlayerID和緩存節點的映射關系
        public LRUCacheNode HeadCacheNode;  //鏈表頭節點
        public LRUCacheNode TailCacheNode;  //鏈表尾節點
        public Dictionary<long, Dictionary<Type, Entity>> UnitCaches = new Dictionary<long, Dictionary<Type, Entity>>();    //PlayerID和緩存數據的映射關系
        public Pool<Dictionary<Type, Entity>> UnitCachePool = new Pool<Dictionary<Type, Entity>>(); //Entity緩存數據池
        public Pool<LRUCacheNode> CacheNodePool = new Pool<LRUCacheNode>(); //PlayerID緩存節點池
    }
}

DBCacheComponent相關方法定義如下,整個思路就是圍繞添加、刪除、更新、查詢緩存數據(脫不開增刪改查),以及維護鏈表順序,保證容量超出時能快速定位到最不活躍數據進行刪除,整個代碼太多就不貼了,只貼哈希鏈表比較重要的滿溢刪除部分,這是哈希表和鏈表的聯動點

/// <summary>
/// 添加緩存數據
/// </summary>
public static void AddCacheData<T>(this DBCacheComponent self, long playerId, T entity) where T : Entity
{
    if (self.UnitCaches.Count >= self.LRUCapacity)  //如果超過緩存容量,清理掉最舊的數據
    {
        self.ClearPlayerCache(self.TailCacheNode.Id);   //TailCacheNode.Id肯定是最舊的Key,傳過去刪除,哈希鏈表重要邏輯
    }

    var dic = self.UnitCachePool.Fetch();   //獲取UnitCachePool池的首位元素,添加緩存數據之后加入到UnitCaches緩存管理
    dic.Add(typeof(T), entity);
    self.UnitCaches.Add(playerId, dic);
    self.AddCacheNode(playerId);    //添加緩存節點,並建立映射關系
}

備注:案例代碼是自己寫了個雙向鏈表,C#有個內置的雙向鏈表對象LinkedList<T>,對鏈表的操作比較便利,應該也可以直接使用。

 

貼上完整代碼注釋版,有興趣可以自取

點擊查看代碼
using System.Collections.Generic;

namespace ET
{
    public class DBCacheComponentAwakeSystem : AwakeSystem<DBCacheComponent>
    {
        public override void Awake(DBCacheComponent self)
        {
            self.LRUCapacity = FrameworkConfigVar.LRUCapacity.IntVar();
        }
    }

    public static class DBCacheComponentSystem
    {
         public static async ETTask<T> Query<T>(this DBCacheComponent self, long playerId) where T : Entity
        {
            using (await CoroutineLockComponent.Instance.Wait(CoroutineLockType.DBCache, playerId))
            {
                
                if (!self.UnitCaches.ContainsKey(playerId)) //查詢不到就從DBComponent獲取數據,加入到緩存數據
                {
                    T entity = await self.Domain.GetComponent<DBComponent>().Query<T>(self.DomainZone(), playerId);
                    self.AddCacheData(playerId, entity);
                    return entity;
                }

                if (!self.UnitCaches[playerId].ContainsKey(typeof (T))) //同上
                {
                    T entity = await self.Domain.GetComponent<DBComponent>().Query<T>(self.DomainZone(), playerId);
                    self.UnitCaches[playerId].Add(typeof (T), entity);
                    self.MoveCacheToHead(playerId); //新加入玩家設為頭節點
                    return entity;
                }
                self.MoveCacheToHead(playerId); //查詢玩家設為頭節點
                Entity cacheEntity = self.UnitCaches[playerId][typeof (T)];
                return cacheEntity as T;
            }
        }

        public static async ETTask Save<T>(this DBCacheComponent self, long playerId, T entity) where T : Entity
        {
            using (await CoroutineLockComponent.Instance.Wait(CoroutineLockType.DBCache, playerId))
            {
                if (!self.UnitCaches.ContainsKey(playerId)) //查詢不到就新加入到緩存管理
                {
                    self.AddCacheData(playerId, entity);
                    return;
                }
                
                self.UpdateCacheData(playerId,entity);  //更新緩存數據
            }
        }

        public static async ETTask Save(this DBCacheComponent self, long playerId, List<Entity> entities)
        {
            using (await CoroutineLockComponent.Instance.Wait(CoroutineLockType.DBCache, playerId))
            {
            }
        }

        /// <summary>
        /// 添加緩存數據
        /// </summary>
        public static void AddCacheData<T>(this DBCacheComponent self, long playerId, T entity) where T : Entity
        {
            if (self.UnitCaches.Count >= self.LRUCapacity)  //如果超過緩存容量,清理掉最舊的數據
            {
                self.ClearPlayerCache(self.TailCacheNode.Id);   //TailCacheNode.Id肯定是最舊的Key,傳過去刪除,哈希鏈表重要邏輯
            }

            var dic = self.UnitCachePool.Fetch();   //獲取UnitCachePool池的首位元素,添加緩存數據之后加入到UnitCaches緩存管理
            dic.Add(typeof (T), entity);
            self.UnitCaches.Add(playerId, dic);
            self.AddCacheNode(playerId);    //添加緩存節點,並建立映射關系
        }

        /// <summary>
        /// 添加緩存節點,並建立PlayerID和緩存節點的映射關系
        /// </summary>
        public static void AddCacheNode(this DBCacheComponent self, long playerId)
        {
            LRUCacheNode cacheNode = self.CacheNodePool.Fetch();    //獲取CacheNodePool池的首位元素
            cacheNode.Id = playerId;
            self.LruCacheNodes.Add(playerId, cacheNode);    //建立PlayerID和緩存節點的映射關系

            //如果頭節點不為null,則cacheNode設為鏈表頭節點,null的話就只有cacheNode,頭尾都是它
            LRUCacheNode headCacheNode = self.HeadCacheNode;
            if (headCacheNode != null)
            {
                headCacheNode.Pre = cacheNode;
                cacheNode.Next = headCacheNode;
            }
            else
            {
                self.TailCacheNode = cacheNode;
            }
            self.HeadCacheNode = cacheNode;
            //Log.Info($"添加節點  playerId:{playerId.ToString()}");
        }

        /// <summary>
        /// 更新緩存數據
        /// </summary>
        public static void UpdateCacheData<T>(this DBCacheComponent self, long playerId, T entity) where T : Entity
        {
            if (!self.UnitCaches[playerId].ContainsKey(typeof(T))) //類似Mongo的Replace操作,如果找不到就添加,找得到就覆蓋數據
            {
                self.UnitCaches[playerId].Add(typeof(T), entity);
            }
            else
            {
                self.UnitCaches[playerId][typeof(T)] = entity;
            }
            self.MoveCacheToHead(playerId); //更新玩家設為頭節點
        }

        /// <summary>
        /// 移動緩存節點到頭部位置
        /// </summary>
        public static void MoveCacheToHead(this DBCacheComponent self, long playerId)
        {
            if (!self.LruCacheNodes.ContainsKey(playerId))
            {
                Log.Error($"DBCache 未找到 cacheNode playerId:{playerId.ToString()}");
                return;
            }

            if (self.HeadCacheNode.Id == playerId)  //已經是頭節點 跳過
            {
                return;
            }

            LRUCacheNode cacheNode = self.LruCacheNodes[playerId];
            if (self.TailCacheNode.Id == playerId)
            {
                //如果是尾節點,就移到頭節點位置
                self.TailCacheNode = cacheNode.Pre; //倒數第二個節點作為尾節點
                self.TailCacheNode.Next = null; //尾節點.Next指向null

                //處理頭節點
                LRUCacheNode oldHeadNode = self.HeadCacheNode;  //暫存當前頭節點
                self.HeadCacheNode = cacheNode; //cacheNode設為新頭節點
                self.HeadCacheNode.Pre = null;  //cacheNode.Pre指向null
                self.HeadCacheNode.Next = oldHeadNode;  //cacheNode.Next指向原頭節點
                oldHeadNode.Pre = self.HeadCacheNode;   //原頭節點.pre指向新頭節點cacheNode
            }
            else
            {
                //如果是中間節點,設為頭節點,連接中間斷開的前后節點
                LRUCacheNode preNode = cacheNode.Pre;
                LRUCacheNode nextNode = cacheNode.Next;
                preNode.Next = nextNode;
                nextNode.Pre = preNode;

                //處理頭節點
                LRUCacheNode oldHeadNode = self.HeadCacheNode;
                self.HeadCacheNode = cacheNode;
                self.HeadCacheNode.Pre = null;
                self.HeadCacheNode.Next = oldHeadNode;
                oldHeadNode.Pre = self.HeadCacheNode;
            }
            //Log.Info($"移動至頭節點  playerId:{playerId.ToString()}");
        }

        /// <summary>
        /// 清除指定player的緩存節點和數據
        /// </summary>
        public static void ClearPlayerCache(this DBCacheComponent self, long playerId)
        {
            if (!self.LruCacheNodes.ContainsKey(playerId))
            {
                return;
            }

            LRUCacheNode cacheNode = self.LruCacheNodes[playerId];
            if (cacheNode.Next == null)
            {
                // 尾節點 設置前一個為尾節點
                if (cacheNode.Pre != null)
                {
                    LRUCacheNode preNode = cacheNode.Pre;
                    preNode.Next = null;
                    self.TailCacheNode = preNode;
                }
                else
                {
                    self.HeadCacheNode = null;
                    self.TailCacheNode = null;
                }
            }
            else
            {
                // 中間節點  連接前后節點
                if (cacheNode.Pre != null)
                {
                    cacheNode.Pre.Next = cacheNode.Next;
                    cacheNode.Next.Pre = cacheNode.Pre;
                }
                else
                {
                    self.HeadCacheNode = cacheNode.Next;
                    self.HeadCacheNode.Pre = null;
                }
            }

            self.LruCacheNodes.Remove(playerId);
            cacheNode.Clear();
            self.CacheNodePool.Recycle(cacheNode);  //緩存節點放到回收池

            var dic = self.UnitCaches[playerId];
            self.UnitCaches.Remove(playerId);
            dic.Clear();
            self.UnitCachePool.Recycle(dic);  //緩存數據放到回收池
            //Log.Info($"清除節點  playerId:{playerId.ToString()}");
        }
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM