- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
继上篇《 GGTalk 开源即时通讯系统源码剖析之:数据库设计 》介绍了 GGTalk 数据库中所有表的结构后,接下来我们将进入GGTalk服务端的核心部分.
GGTalk 对需要频繁查询数据库的数据做了服务端全局缓存处理,这样做一来大大降低了数据库的读取压力,二来在客户端的请求到来时,服务端能更快地响应,极大地提升了用户体验。这篇文章将会详细剖析关于 GGTalk 服务端全局缓存的设计与实现。还没有GGTalk源码的朋友,可以到 GGTalk源码下载中心 下载.
首先,我们需要了解 GGTalk服务端 的三大核心,其分别是:
此部分的代码位于 GGTalk/TalkBase/Server/Core/ServerHandle.cs 。
当一个客户端的请求进来时,首先会进入消息处理环节,根据用户传递的消息号,进入不同的逻辑分支。以修改用户信息为例:
//(客户端逻辑代码)
/// <summary>
/// 修改个人资料。
/// </summary>
public void ChangeMyBaseInfo(string name, string signature, string department) {
//...
this.rapidPassiveEngine.SendMessage(null, this.talkBaseInfoTypes.ChangeMyBaseInfo, data, "", true);
//...
}
当一个用户信息被修改时,会调用如上方法,然后通过调用 rapid客户端引擎 上的 SendMessage 方法发送一条消息(其中 data 为用户信息的 byte[]数组 ).
//(服务端逻辑代码)
public void Initialize() {
//...
this.rapidServerEngine.MessageReceived += new ESBasic.CbGeneric<string,ClientType, int, byte[], string>(rapidServerEngine_MessageReceived);
//...
}
客户端发送消息会触发 rapid服务端引擎 上的 MessageReceived 事件,最终程序流程会来到如下图的地方.
根据客户端传递在消息号来匹配对应的 if分支 ,然后进行对应的处理.
此部分的代码位于 GGTalk/TalkBase/Server/Core/ServerGlobalCache.cs 。
接着前面修改用户信息的例子:
if (informationType == this.talkBaseInfoTypes.ChangeMyBaseInfo) {
//...
this.serverGlobalCache.UpdateUserInfo(sourceUserID, contract.Name, contract.Signature, contract.OrgID);
TUser user = this.serverGlobalCache.GetUser(sourceUserID);
//...
}
消息处理后会来到如上 if分支 ,其中分别调用了 serverGlobalCache 上的 UpdateUserInfo 和 GetUser 方法,下面是这两个方法的具体实现.
/// <summary>
/// 获取目标用户,如果缓存中不存在,则从DB加载。
/// </summary>
public TUser GetUser(string userID) {
TUser user = this.userCache.Get(userID);
if (user == null) {
user = this.dbPersister.GetUser(userID);
if (user != null) {
this.userCache.Add(userID, user);
}
}
return user;
}
此方法会从全局缓存获取用户数据,若缓存中不存在,则会从数据库中查询,并将查询到的用户数据存入缓存中,方法最终返回用户数据.
// 更新用户信息
public void UpdateUserInfo(string userID, string name, string signature, string orgID) {
TUser user = this.GetUser(userID);
if (user == null) {
return;
}
user.Name = name;
user.Signature = signature;
user.OrgID = orgID;
user.Version += 1;
user.DeletePartialCopy();
this.dbPersister.UpdateUserInfo(userID, name, signature, orgID, user.Version);
}
此方法先去获取用户的信息,修改用户信息,然后通过调用 user 上的 DeletePartialCopy 方法清除用户的缓存,最后再更新数据库中用户的信息.
此部分的代码位于 GGTalk/GGTalk.Server/DBPersister.cs 。
同样在这个修改用户信息的例子中,在前面的讲解中有涉及到两处与数据库的交互,分别是 GetUser 和 UpdateUserInfo 方法的调用。下面是这两个方法的具体实现:
// 获取用户信息
public GGUser GetUser(string userID) {
GGUser user = null;
user = db.Queryable<GGUser>().Where(it => it.UserID == userID).First();
return user;
}
// 更新用户信息
public void UpdateUserInfo(string userID, string name, string signature, string orgID, int version) {
db.Updateable<GGUser>(it => new GGUser() { Signature = signature, Name = name, OrgID = orgID, Version = version }).Where(it => it.UserID == userID).ExecuteCommand();
}
在数据库的交互环节,我们使用的是 sqlsugar 来操作数据库(这是一个开源的ORM框架,若想了解其详细用法,请移步 sqlsugar文档 ).
看到这里,相信你对 GGTalk服务端 的三大核心有了一定的了解,接下来将会详细介绍关于 GGTalk 服务端全局缓存的设计.
由于在 GGTalk服务端 中对用户和群组信息查询过于频繁,故而 GGTalk 将用户和群组的信息缓存在服务端内存之中,进而达到减少资源消耗和更快的服务端响应的好处,但这样做同时也会增加编码的复杂度,那么 GGTalk 是如何在其中进行取舍的呢?下面将介绍具体实现.
public class ServerGlobalCache<TUser, TGroup>
where TUser : TalkBase.IUser
where TGroup : TalkBase.IGroup
{
private IDBPersister<TUser, TGroup> dbPersister;
//...
private ObjectManager<string, TUser> userCache = new ObjectManager<string, TUser>(); // key:用户ID 。 Value:用户信息
private ObjectManager<string, TGroup> groupCache = new ObjectManager<string, TGroup>(); // key:组ID 。 Value:Group信息
//...
}
ServerGlobalCache类 就是 GGTalk 服务端全局缓存的核心实现了,这个类接受两个泛型参数, TUser 和 TGroup ,并要求 TUser 必须是TalkBase命名空间中的 IUser 接口的实现类或子类。 TGroup 必须是TalkBase命名空间中的 IGroup 接口的实现类或子类.
IUser
:用户基础接口,定义了关于用户一系列的属性和方法。
/// <summary>
/// 用户基础接口。
/// </summary>
public interface IUser : IUnit {
List<string> GroupList { get; }
UserStatus UserStatus { get; set; }
string GetFriendCatalog(string friendID);
string GetUnitCommentName(string unitID);
string Signature { get; set; }
string OrgID { get; set; }
/// <summary>
/// 用户使用状态
/// </summary>
UserState UserState { get; set; }
bool IsFriend(string userID);
List<string> GetAllFriendList();
void ChangeHeadImage(int defaultHeadImageIndex, byte[] customizedHeadImage);
DateTime PcOfflineTime { get; set; }
DateTime MobileOfflineTime { get; set; }
}
IGroup
:群/讨论组的基础接口,定义了一系列关于群/讨论组的属性和方法。
/// <summary>
/// 群、讨论组 基础接口。
/// </summary>
public interface IGroup : IUnit {
GroupType GroupType { get; }
string CreatorID { get; }
DateTime CreateTime { get; }
List<string> MemberList { get; }
void AddMember(string userID);
void RemoveMember(string userID);
string Announce { get; set; }
void ChangeMembers(List<string> members);
}
除了这两个泛型参数外,我们可以发现 ServerGlobalCache类 中还有三个字段,这三个字段是 ServerGlobalCache类 中所有方法的核心,其作用如下:
关于这三个字段,在后面的具体场景会展开更加详细的介绍.
关于服务端缓存,最关键的就是 userCache 和 groupCache 字段了,其中 userCache 用于缓存用户的信息;而 groupCache 用于缓存群组的信息.
首先我们来看关于这两个字段的类型 ObjectManager : public class ObjectManager<TPKey, TObject> 。
ObjectManager是对Dictionary的二次封装,支持多线程安全,使用起来也更方便。这个类接受两个泛型参数,我们通过传入不同的泛型可以实现不同数据的管理(在 GGTalk服务端 中,仅管理了用户和群组的数据).
其内部的Dictionary就是用来将用户或群组的数据存储在内存中,达到缓存数据的目的.
我们来看 ServerGlobalCache类 中如下两个方法:
从名字上来看我们很容易就知道这两个方法的意思,预加载用户数据和预加载群组数据,这两个方法的主要作用就是将数据库中用户和群组的数据加载到内存中。首先通过 dbPersister 字段来从数据库中查询到所有用户和群组数据,通过foreach遍历,分别调用 userCache 和 groupCache 上的 Add 方法将每一条数据存储到前面提到的 objectDictionary 字段中,也是就存储在了服务端程序运行时的内存里面.
看到这里,关于 ServerGlobalCache类 的基础设施你已经了解的七七八八了,接下来都是基于这些基础设施而实现的方法了。在这里我要纠正一个你可能感到疑惑的点,本篇文章不是介绍服务端缓存吗,这里怎么扯到数据库的增删改查呢?
因为往往数据缓存和数据源之间存在着一些联动,所以 ServerGlobalCache类 的作用不仅仅是缓存数据,同时也存在大量获取数据库中的数据的方法,这也是为什么在类里面会有一个 dbPersister 字段,当然关于具体从数据库中读取数据的方法不在这个类里边(回顾 GGTalk三大核心 ).
接下来,让我们看看 ServerGlobalCache类 还有什么:
我们可以看到,这些折叠的部分的代码行数几乎占据了 ServerGlobalCache类 的百分之九十,这是正是对数据库和数据缓存的操作,每个折叠代码块的注释都对应着 GGTalk数据库 的一张表.
接下来我们主要分析一下关于用户和群组的部分操作,看看 GGTalk服务端 是如何对数据库和数据缓存进行操作的.
首先来看一个简单的,添加新用户操作:
/// <summary>
/// 插入一个新用户。
/// </summary>
public void InsertUser(TUser user) {
this.userCache.Add(user.ID, user);
this.dbPersister.InsertUser(user);
}
这个方法接受一个 TUser 类型的参数,参数中包含用户的必要信息,然后分别添加到用户缓存和插入到数据库中.
接下来,再看最开始讲三大核心的那个例子:
/// <summary>
/// 获取目标用户,如果缓存中不存在,则从DB加载。
/// </summary>
public TUser GetUser(string userID) {
TUser user = this.userCache.Get(userID);
if (user == null) {
user = this.dbPersister.GetUser(userID);
if (user != null) {
this.userCache.Add(userID, user);
}
}
return user;
}
现在再来看是不是很清晰了呢,这个方法用于查询单个用户,接受一个用户ID,首先会从用户缓存中查找这个用户,如果缓存中不存在,则从数据库中查找,在用户存在的情况下再将其存入内存之中.
接下来再分析两个关于群组操作的方法.
1、根据群组ID获取群组信息:
/// <summary>
/// 获取某个组
/// </summary>
public TGroup GetGroup(string groupID) {
TGroup group = this.groupCache.Get(groupID);
if (group == null) {
group = this.dbPersister.GetGroup(groupID);
if (group != null) {
this.groupCache.Add(groupID, group);
}
}
return group;
}
和获取用户信息类似,此方法首先会在群组缓存中查找对应ID的群组,若群组不存在,则会从数据库读取对应ID的群组,并且在群组存在的情况下将其存入内存之中.
2、解散群组操作 。
public void DeleteGroup(string groupID) {
TGroup group = this.GetGroup(groupID);
if (group == null) {
return;
}
foreach (string userID in group.MemberList) {
TUser user = this.GetUser(userID);
if (user != null) {
user.QuitGroup(groupID);
this.dbPersister.UpdateUserGroups(user);
}
}
this.groupCache.Remove(groupID);
this.dbPersister.DeleteGroup(groupID);
this.dbPersister.DeleteAddGroupRequest(groupID);
}
这个方法接受群组ID作为参数,首先会调用 GetCroup 方法依次从内存和数据库中读取关于目标群组的数据(如果缓存中没有的话)。若群组存在,则从群组的 MemberList 属性中遍历用户ID,再通过 GetUser 方法查询用户数据,通过用户的 QuitGroup 退出群组,然后在数据库中更新用户的信息。在这个群组中的每一个存在的用户都退出群组后,从群组缓存中清除该群组的数据。然后再同步数据库中的群组表的数据,以及在数据库中申请加入群组表中删除加入此群组的记录.
将数据库中的数据缓存在内存中是一把双刃剑,若是将大量的数据保存在内存中,这会大大加大内存的占用,存在程序因为内存不足而导致程序崩溃的风险。如何避免这样的事情发生,这要求我们对内存保持足够的敏感。最后,希望这篇文章能够对你有所帮助。 在接下来的一篇我们将介绍GGTalk服务端的虚拟数据库。 敬请期待:《GGTalk 开源即时通讯系统源码剖析之:虚拟数据库》 。
最后此篇关于GGTalk开源即时通讯系统源码剖析之:服务端全局缓存的文章就讲到这里了,如果你想了解更多关于GGTalk开源即时通讯系统源码剖析之:服务端全局缓存的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我有一个使用 css 列的下拉菜单,当我使用 jquery slide() 时,它会调整下拉框的大小,并重排内容直到达到完整高度。 这是一个工作示例 https://codepen.io/peterg
我有一个带有嵌套 ScrollViewer 的 Expander,如下所示: 代码(简化版)
我想在所有 ajax 调用之后调用一些 javascript 函数。我知道如何调用每个单独的 ajax 调用中的函数,如下所示: function xyz() { if (window.XMLHttp
我想将值从应用程序端传递到 api。在此 api 调用中传递图像、名字、电子邮件、电话和位置。在 Debug模式下,检查值不会被传递。 代码下方 File file = null;
我正在尝试在使用reportlab生成的pdf中的表格后插入分页符,我正在使用以下函数生成pdf: def render_to_pdf(template_src, context_dict): t
CBPeripheralManager 是否有推荐的方法来终止连接。到目前为止我发现的最好的方法就是干脆不响应动态值,然后 BLE 堆栈似乎关闭了连接,但这似乎很粗糙。 一定有更好的方法吗? 最佳答案
我的 API 的 REST 端位于以下地址:http://test.jll.aplikacje-dedykowane.pl/rest/warehouse/all 。现在,我尝试返回在此页面准备的 JS
我有以下 CPP 代码。我想做的是,当我的 native 端发生错误时,我会通知 Java 该错误。我用了How can I catch SIGSEGV (segmentation fault) an
限制对象的方法之一是像这样给函数添加限制 def ten_objects(): obj = Model.objects.all()[0:10] # limit to 10 retur
我目前在电信公司实习,这是一个专业,也是本科生。我有很多选择。据我所知,我知道独立应用程序端的 c、c++、c#、java 语言,在移动端我尝试进入 android 世界,也知道 php、mysql、
我想让我的边框底部看起来像这样: 有一个 flex 的末端。目前它看起来像这样: 我尝试使用的 CSS 是 border-bottom-right-radius: 10px;。代码如下所示:
我有一个 Flutter 项目,突然间,据我所知,我没有做任何特别的事情..Android 端开始显示错误,我完全迷路了,我完全不知道哪里出了问题,也不知道为什么会这样。 这就是我打开 android
我有一个自定义对象列表 (List) 。我需要将此数据发送到 React Native 端以显示在平面列表中。我该怎么做?这个列表出现在 类 NativeToReact(reactContext:Re
我有这个代码: #if defined(NOT_STANDALONE) JNIEXPORT void JNICALL sumTraces (JNIEnv* env, jclass caller,
我有一个定义一对多模型关系的 Django 应用程序。模型如下所示: from django.db import models # Create your models here. class Str
我有以下代码,它根据 IFrame 内容的大小调整 IFrame 的大小: function setIframeHeight(id) {
如何创建自定义过滤器 angularjs javascript Controller 端?我想通过 SegmentId 在名为段的数组中搜索,以创建过滤器,该过滤器通过 SegmentId 在段数组搜
我的代码在 netbeans 8.0.2 中我几乎尝试了所有方法,但没有结果。请帮助我。如何在 netbeans 中显示它? 最佳答案 您只需单击源包(源文件),它就会显示您的项目文件。 关于java
我想这是纯 C++ 问题和 OpenGL 问题之间的一种交叉。我有一个统一的缓冲区,并在其中分配 sizeof(ShaderData) 字节的空间。我在着色器的 GPU 端使用 std140 布局。
我对 Hadoop 中 reduce 端的文件合并过程的理解有问题,因为它在“Hadoop:权威指南”(Tom White)中有所描述。引用它: When all the map outputs ha
我是一名优秀的程序员,十分优秀!