本篇着重理解緩存層的DBCacheComponent,使用了LRU算法,網上找到幾個感覺不錯的文章
1、用鏈表的目的是什么?省空間還是省時間? - invalid s的回答 - 知乎 https://www.zhihu.com/question/31082722/answer/1928249851
3、鏈表——最基本的數據結構之一 | 經典鏈表應用場景: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()}");
}
}
}