ET6.0数据库模块(二)
本篇着重理解缓存层的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()}");
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律