GGTalk 开源即时通讯系统源码剖析之:服务端全局缓存
继上篇《GGTalk 开源即时通讯系统源码剖析之:数据库设计》介绍了 GGTalk 数据库中所有表的结构后,接下来我们将进入GGTalk服务端的核心部分。
GGTalk 对需要频繁查询数据库的数据做了服务端全局缓存处理,这样做一来大大降低了数据库的读取压力,二来在客户端的请求到来时,服务端能更快地响应,极大地提升了用户体验。这篇文章将会详细剖析关于 GGTalk 服务端全局缓存的设计与实现。还没有GGTalk源码的朋友,可以到 GGTalk源码下载中心 下载。
一. GGTalk 服务端三大核心
首先,我们需要了解 GGTalk服务端 的三大核心,其分别是:
- 消息处理:处理来自客户端的消息;
- 全局缓存:将用户和群组的数据缓存在内存中;
- 数据库交互:对数据库中的信息进行增删改查。
1. 消息处理
此部分的代码位于
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分支,然后进行对应的处理。
2. 全局缓存
此部分的代码位于
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 方法清除用户的缓存,最后再更新数据库中用户的信息。
3. 数据库交互
此部分的代码位于
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 服务端全局缓存的设计。
1. 代码位置

由于在 GGTalk服务端 中对用户和群组信息查询过于频繁,故而 GGTalk 将用户和群组的信息缓存在服务端内存之中,进而达到减少资源消耗和更快的服务端响应的好处,但这样做同时也会增加编码的复杂度,那么 GGTalk 是如何在其中进行取舍的呢?下面将介绍具体实现。
2. ServerGlobalCache类
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类 中所有方法的核心,其作用如下:
- dbPersister:与数据库进行交互;
- userCache:与用户缓存相关;
- groupCache:与群组缓存相关。
关于这三个字段,在后面的具体场景会展开更加详细的介绍。
3. 缓存数据的实现
关于服务端缓存,最关键的就是 userCache 和 groupCache 字段了,其中 userCache 用于缓存用户的信息;而 groupCache 用于缓存群组的信息。
首先我们来看关于这两个字段的类型ObjectManager:
public class ObjectManager<TPKey, TObject>
ObjectManager是对Dictionary的二次封装,支持多线程安全,使用起来也更方便。这个类接受两个泛型参数,我们通过传入不同的泛型可以实现不同数据的管理(在 GGTalk服务端 中,仅管理了用户和群组的数据)。
其内部的Dictionary就是用来将用户或群组的数据存储在内存中,达到缓存数据的目的。
4. 将数据库中数据的读入内存(缓存数据)
我们来看 ServerGlobalCache类 中如下两个方法:

从名字上来看我们很容易就知道这两个方法的意思,预加载用户数据和预加载群组数据,这两个方法的主要作用就是将数据库中用户和群组的数据加载到内存中。首先通过 dbPersister 字段来从数据库中查询到所有用户和群组数据,通过foreach遍历,分别调用userCache和groupCache上的Add方法将每一条数据存储到前面提到的objectDictionary字段中,也是就存储在了服务端程序运行时的内存里面。
5. 数据库增删改查
看到这里,关于 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——C#开源即时通讯系统源码介绍系列(一)
坦白讲,我们公司其实没啥技术实力,之所以还能不断接到各种项目,全凭我们老板神通广大!要知道他每次的饭局上可都是些什么人物! 但是项目接下一大把,就凭咱哥儿几个的水平,想要独立自主.保质保量保期地一个个 ...
- 新一代开源即时通讯应用源码定制 运营级IM聊天源码
公司介绍:我们是专业的IM服务提供商!哇呼Chat是一款包含android客户端/ios客户端/pc客户端/WEB客户端的即时通讯系统.本系统完全自主研发,服务器端源码直接部署在客户主机.非任何第三方 ...
- muduo库源码剖析(二) 服务端
一. TcpServer类: 管理所有的TCP客户连接,TcpServer供用户直接使用,生命期由用户直接控制.用户只需设置好相应的回调函数(如消息处理messageCallback)然后TcpSer ...
- 即时通信系统中实现全局系统通知,并与Web后台集成【附C#开源即时通讯系统(支持广域网)——QQ高仿版IM最新源码】
像QQ这样的即时通信软件,时不时就会从桌面的右下角弹出一个小窗口,或是显示一个广告.或是一个新闻.或是一个公告等.在这里,我们将其统称为“全局系统通知”.很多使用C#开源即时通讯系统——GGTalk的 ...
- GGTalk ——C#开源即时通讯系统
http://www.cnblogs.com/justnow/ GGTalk ——C#开源即时通讯系统 下载中心 GGTalk(简称GG)是可在广域网部署运行的QQ高仿版,2013.8.7发布GG ...
- zookeeper源码分析之五服务端(集群leader)处理请求流程
leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...
- zookeeper源码分析之四服务端(单机)处理请求流程
上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...
- Netty 4源码解析:服务端启动
Netty 4源码解析:服务端启动 1.基础知识 1.1 Netty 4示例 因为Netty 5还处于测试版,所以选择了目前比较稳定的Netty 4作为学习对象.而且5.0的变化也不像4.0这么大,好 ...
- 即时通信系统中实现聊天消息加密,让通信更安全【低调赠送:C#开源即时通讯系统(支持广域网)——GGTalk4.5 最新源码】
在即时通讯系统(IM)中,加密重要的通信消息,是一个常见的需求.尤其在一些政府部门的即时通信软件中(如税务系统),对即时聊天消息进行加密是非常重要的一个功能,因为谈话中可能会涉及到机密的数据.我在最新 ...
- Netty源码剖析-关闭服务
参考文献:极客时间傅健老师的<Netty源码剖析与实战>Talk is cheap.show me the code! ----主线: ----源码: 先在服务端加个断点和修改下代码:如 ...
随机推荐
- 添加索引后SQL消耗量在执行计划中的变化
不同索引的执行效率也是不一样的,下面比较三条SQL语句在正常查询与建立普通索引与位图索引后的CPU消耗量的变化,目的为了是加强对索引的理解与运用 实验步骤:1.创建有特点的大数据表.为了保证索引产生前 ...
- 基于DotNetCoreNPOI封装特性通用导出excel
基于DotNetCoreNPOI封装特性通用导出excel 目前根据项目中的要求,支持列名定义,列索引排序,行合并单元格,EXCEL单元格的格式也是随着数据的类型做对应的调整. 效果图: 调用方式 可 ...
- HashMap实现原理和自动扩容
HashMap实现原理: JDK1.7:数组+单向链表(头插) 在并发情况下头插可能出现循环链表(死循环)问题.原因:因为头插,在新数组中链表的元素顺序发生了变化, 如上图,假设线程1在扩容,刚刚调整 ...
- Protobuf编码规则
支持类型 该表显示了在 .proto 文件中指定的类型,以及自动生成的类中的相应类型: .proto Type Notes C++ Type Java/Kotlin Type[1] Java/Kotl ...
- 2023-04-02:设计一个仓库管理器,提供如下的方法: 1) void supply(String item, int num, int price) 名字叫item的商品,个数num,价格pri
2023-04-02:设计一个仓库管理器,提供如下的方法: void supply(String item, int num, int price) 名字叫item的商品,个数num,价格price. ...
- 2022-10-27:设计一个数据结构,有效地找到给定子数组的 多数元素 。 子数组的 多数元素 是在子数组中出现 threshold 次数或次数以上的元素。 实现 MajorityChecker 类
2022-10-27:设计一个数据结构,有效地找到给定子数组的 多数元素 . 子数组的 多数元素 是在子数组中出现 threshold 次数或次数以上的元素. 实现 MajorityChecker 类 ...
- 2020-09-13:判断一个正整数是a的b次方,a和b是整数,并且大于等于2,如何求解?
福哥答案2020-09-13: 首先确定b的范围,b的范围一定在[2,logN]里.然后遍历b,求a的范围,如果范围长度等于0,说明这个正整数是a的b次方.1.遍历b范围.二分法求a,a初始范围是[2 ...
- 补充:C语言枚举类型
1.枚举类型 1.枚举数据类型是C语言中一种构造数据类型,可以让数据更加简洁,更易读,对于只有几个特定的数据,可以使用枚举类型 2.枚举对应英文enumeration,简写为enum 3.枚举是一组常 ...
- 2023-05-22:给定一个长度为 n 的字符串 s ,其中 s[i] 是: D 意味着减少; I 意味着增加。 有效排列 是对有 n + 1 个在 [0, n] 范围内的整数的一个排列 perm
2023-05-22:给定一个长度为 n 的字符串 s ,其中 s[i] 是: D 意味着减少: I 意味着增加. 有效排列 是对有 n + 1 个在 [0, n] 范围内的整数的一个排列 perm ...
- AcWing 243. 一个简单的整数问题2-(区间修改,区间查询)
给定一个长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一: C l r d,表示把 A[l],A[l+1],-,A[r]都加上 d. Q l r,表示询问数列中第 l∼r个数的和. ...