惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
C
Cybersecurity and Infrastructure Security Agency CISA
N
News and Events Feed by Topic
C
Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
Scott Helme
Scott Helme
P
Palo Alto Networks Blog
S
Schneier on Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
量子位
G
Google Developers Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog RSS Feed
NISL@THU
NISL@THU
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
AWS News Blog
AWS News Blog
爱范儿
爱范儿
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
L
LINUX DO - 最新话题
Security Archives - TechRepublic
Security Archives - TechRepublic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Secure Thoughts
Cloudbric
Cloudbric
aimingoo的专栏
aimingoo的专栏
L
Lohrmann on Cybersecurity
TaoSecurity Blog
TaoSecurity Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Hacker News: Ask HN
Hacker News: Ask HN
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
有赞技术团队
有赞技术团队
S
Security @ Cisco Blogs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
G
GRAHAM CLULEY
P
Proofpoint News Feed
V
V2EX
Martin Fowler
Martin Fowler
C
CERT Recently Published Vulnerability Notes
Attack and Defense Labs
Attack and Defense Labs
C
CXSECURITY Database RSS Feed - CXSecurity.com
The Cloudflare Blog
SecWiki News
SecWiki News
罗磊的独立博客
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
小众软件
小众软件
The Last Watchdog
The Last Watchdog

重归混沌的BLOG

给silly实现了一个ernro模块 | 重归混沌的BLOG 给silly实现了一个ernro模块 | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 如何基于LanguageServerProtocol来编写lint工具 谈谈游戏服务器中RPC模块的设计 谈谈游戏服务器代码抽象 最近碰到的一个分布式一致性问题 谈谈游戏服务器的自动化测试 对Raft协议的一点理解 使用mmap来学习/proc/pid/smaps 2023(完) 再次实现了一个Lua性能分析器 终于给Silly的定时器增加了取消功能 一次虚拟内存排查经历 游戏服务器分布式数据的一种同步的思路 为silly增加了互斥锁 2022(完) Go语言之闭包篇 一例误用unsafe包引起的内存问题 Go语言之内存篇 初识Go语言 重新抽象图形API 给Lua实现了一个数学库 谈谈跨平台图形API的抽象 寻路和Flocking算法的结合 行为树的一种高效实现 内测过程中Shader出现的问题 彻底解决多国语言 谈谈数据库的选型 再谈Lua热更新(终) 初窥Rust 关于游戏服务器的服务拆分 ECS的初步实现 ECS初探 屏幕空间(SreenSpace)的想象力 一些对辐射度量学的理解 深度缓冲和半透明渲染 Mysql的间隙锁 更新一些GPU相关知识 2020 地形渲染之爬过的坑 Lua5.3 GC源码阅读(5) 实现一个数据库存储队列 再学计算机图形学入门 再谈分布式服务架构 游戏上线一个月后的反思 一次并发Bug 双向链表的三种实现 谈谈随机数的使用 再谈性能优化 2019 Lua中的函数式编程 重构登录逻辑 Unity资源管理(续) 谈谈Unity的资源管理 一次关于Cache的性能分析 历史之2018 DC3算法 移动平台native代码遭遇的坑 从CPU层面谈谈优化 开卷有益(UNIX编程艺术篇) GC竞争问题 通过Mesh投影来实现贴花系统 谈谈我对数据同步的理解 又一个类型提升引起的Bug Lua5.3 GC源码阅读(4) Lua5.3 GC源码阅读(3) Lua5.3 GC源码阅读(2) Lua5.3 GC源码阅读(1) 三角形光栅化时遇到的坑 一次git事故 再见2017 又一个lua调试器 客户端缓存落地方案 Paxos算法 一次性能优化经历 关于CPU分支预测 C程序中让两个不同版本的库共存 实现了一个AOI模块 一个高可伸缩的游戏服务器架构 关于网络协议封装的一些新想法
HTTP服务器的特点
重归混沌 · 2017-10-29 · via 重归混沌的BLOG

最近一个月在学习关系性数据库mysql, 顺便看了一下http服务器的编写。写了这么久的游戏服务器,数据库也一直使用nosql。因此在初次尝试使用mysql编写http服务器程序时,产生了强烈的反差。

在使用APP的验证过程中,经常会遇到短信验证码问题。

下面就以‘如何实现短信验证码服务’来对比一下http服务器和游戏服务器(mysql和nosql)解决此类问题的不同之处。主要涉及数据结构定义、优化、及逻辑代码的更新问题。

先来明确一下‘业务流程’,最简单的‘短信验证码服务’至少要提供3个操作:

1. APP向‘短信验证码服务’发送请求获取验证码
2. 其他服务向‘短信验证码服务’发送请求校验验证码是否正确(这里的‘其他服务和‘短信验证码服务’有可能是同一个服务,这里只是假定,‘短信验证码服务’被单独实现为一个微服务)
3. 定期删除超时的短信验证码(这里验证码超时时间为5分钟)

先说执行流程:

http服务器的每次请求都是相互独立的,即每个请求都是先查询当前DB(这里假设使用Mysql)的状态,然后将状态写入内存,然后根据内存中的状态执行逻辑,然后把处理结果写入DB,释放所有资源(这里仅仅是理论上,比如可能连接DB时会采用连接池,那么释放时也仅仅是把链接归还到链接池而已)。而这里要实现的‘短信验证服务’基本上属于纯DB操作,因此下面直接展示sql语句,逻辑代码直接略过。

而游戏服务器则不太一样,一般会把要操作的数据提前加载入内存,当处理请求时,直接根据当前内存的状态,来处理逻辑,最后把结果写入内存,并极据不同的策略更新到DB。也就是说,不管DB定义的数据结构是怎样的,进程内的数据结构才是直接影响请求处理效率的关键。

先来看按‘http服务器’如何实现‘短信验证服务’。

首先定义mysql中的数据结构:

create table verifycode (
	phone bigint unsigned not null primary key,
	code int unsigned not null,
	time timestamp not null
);

为电话号码为‘100000000’生成一个短信验证码:

insert into verifycode (phone,code) 
values (100000000, floor(rand() * 10000)) 
on duplicate key update 
time= current_timestamp(),code=floor(rand() * 10000);

为‘其他服务’提供电话号码为‘100000000’验证码为10005验证功能:

select count(*) from verifycode where phone = 100000000 and code = 10005;

定期删除过期的验证码(此段代码需要开个定时器执行):

delete from verifycode where 
time < DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 5 MINUTE);

再来看看如果按‘游戏服务器’的方式该如何实现(直接在进程内操作, 以C++的方式呈现)。

定义数据结构:

struct verifycode {
        long int phone;
        int code;
        time_t time;
};
std::unordered_map<long int, struct verifycode> verifycode_pool;

为电话号码为‘100000000’生成一个短信验证码:

auto &c = verifycode_pool[++codeidx];
c.code = rand() % 10000;
c.phone = 100000000;
c.time = time(nullptr);

为‘其他服务’提供电话号码为‘100000000’验证码为10005验证功能:

auto iter = verifycode_pool.find(100000000);
return (iter != verifycode_pool.end() && iter->second.code == 10005);

定期删除过期的验证码(此段代码需要开个定时器执行):

time_t now = time(nullptr) - 5 * 60;
for (auto iter = verifycode_pool; iter != verifycode_pool; ) {
        if (iter->second.time < now)
                iter = verifycode_pool.erase(iter);
        else
                ++iter;
}

对比一下mysql版本和C++版本的短信验证码服务。可以发现,C++版本的代码基本上就是mysql版本中sql语句的翻译。

也就是说到目前为止,除了http服务器每一个请求都会重新读取Mysql和写入mysql的设计原则与游戏服务器不同外,设计思路上完全一致。


但是上述实现有几个问题。

1. 定期删除过期验证码,需要扫描整张表,时间复杂度为O(N),在有大量验证码的情况下,一次清理就会卡整个进程或整张表,导致”Stop The World”,而mysql在不使用索引的情况下,扫描整张表会更慢。
2. sql语句使用了now(),rand()之类的函数,这将导致mysql查询缓存失效(mysql版’短信验证码服务’特有问题,这个可以通过在应用程序中生成常量值来替换sql语句中的now/rand函数,下面优化不再列出)

为了解决删除过期验证码过慢的问题。

‘http服务器’的优化版本需要做出如下修改,为time字段添加索引:

create table verifycode (
	phone bigint unsigned not null primary key,
	code int unsigned not null,
	time int not null,
        index(time)
);

而C++版本则需要重新设计数据结构, 并实现相应的算法。

采用Time Wheel的方式,将过期的Key直接为300组,超时直接删除整组。

time_t lastupdate = now();
std::unordered_map<long int, int> verifycode;
std::unordered_set verifyexpire[5 * 60];

为电话号码为‘100000000’生成一个短信验证码:

int idx = now() / (5 * 60);
verifycode[100000000] = rand() % 10000;
verifyexpire[idx] = 100000000

为‘其他服务’提供电话号码为‘100000000’验证码为10005验证功能:

auto iter = verifycode.find(100000000);
return (iter != verifycode.end() && iter->second == 10005);

定期删除过期的验证码(此段代码需要开个定时器执行):

time_t now = time(nullptr);
while (lastupdate <= now) {
        int idx = lastupdate % (5 * 60);
        auto &e = verifyexpire[idx];
        for (auto phone:e)
                verifycode.erase(phone);
        e.clear();
        lastupdate++;
}
lastupdate = now;

从优化后的代码,再来对比一下两者的优缺点。

HTTP服务器,特点:
1. 业务逻辑每个请求都需要读取写入数据库,因此相比游戏服务器的方式来讲会请求会慢很多
2. 业务逻辑均采用关系数据库来进行存储(这是并不是绝对的,现在很多服务器已经开始采用nosql来存储了), 一般只能维绕着sql语句和索引进行优化
3. 数据与逻辑分离,可以动态热更新逻辑代码
4. 数据与逻辑分离,当业务请求能力处理不足,而瓶颈不在DB时,可通过增加逻辑服务器来动态扩容,具有极大的可伸缩性
5. 合理的DB集群架构设计,当DB达到瓶颈时,可动态扩容

游戏服务器,特点:
1. 数据均在进程中内存进行操作,可扩展性差。
2. 由于进程中内存残留业务逻辑状态,几乎或很难进行逻辑代码的热更新。
3. 不同的业务模型需要根据情况设计,才可以使集群具有可伸缩性,不像HTTP天然具有可伸缩性。
4. 数据表现力强,可以充分利用编程语言提供的各种表现方式,优化形式多样。
5. 所的请求全部基于内存状态进行处理,处理速度快

ps.mysql中的InnoDB索引采用B+Tree来实现,因此time字段加上索引后,单纯的从算法上与使用TimeWheel实现的C++版本相比,时间复杂度上并不会有显著区别。在更复杂的应用场景,DB和编程语言实现过程中的优化形式可能会相差甚远,但其本质上也是相同的,都是尽可能快的提高访问速度。