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()}");
        }
    }
}
posted @   qianxun0975  阅读(516)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示