12306技术内幕

公司内部做的一次技术分享

文章目录

  • 12306的成就
  • 12306系统特点
  • 12306系统难点
  • 解决思路
    • 产品角度
    • 技术角度
      • 余票库存的表如何设计?
  • 抢票软件推荐
  • 巨人的肩膀

对于未公开的技术部分,只能结合已公开的信息,去做大胆的猜想。

本文提到的一些解决方案,并不一定是标准的实现,一些观点旨在引发大家的思考。

12306的成就

  • 创下全球最大实时票务交易系统世界记录,春运一个月抵欧洲一年。
  • 最高可达百万并发,承受了这个世界上能秒杀任何系统的QPS。
  • 网站浏览量一天最高超1500亿次,峰值是双11的三倍。

12306系统特点

  • 跟淘宝天猫等相比,业务简单(卖票)。
  • 流量极大。
  • 动态库存。

12306系统难点

目前 12306 最大的难点,在于库存扣减。

它跟传统的电商网站,可能最大的不同在于它的库存, 它的库存是动态变化的,库存之间会互相影响。

比如现在有一个组合品的需求,A品是由B品和C品通过不同的比例混合而成,用户下单的时候传过来的是A品这个 SKU,但是库存扣减的时候是把它的组合品的单品(B和C),都去扣一遍的。

我们平时各种商品 sku 库存的话,它是表里面的一行行记录。

某个行程是:杭州 -> 武汉 -> 成都。

杭州 -> 成都 ,武汉 -> 成都。这两个是一个车次。那么每卖出一张 武汉 -> 成都 的票,杭州 -> 成都 的票也会少一张。

举个极端的例子,火车上就一个座位,车次是从 A -> B -> C。

如果卖出 B -> C 的车票,不去扣减 A-> C 库存的话,那么假设有两个用户分别买了 A-> C 和 B -> C 。那么当车行至站点B的时候,车上会有两个人,但是座位就一个。

所以不同车次之间的库存是会互相影响的。

A -> B -> C -> D 共 4 个车站,假如乘客买了 B -> C 的车票,那么同时会影响到 A->C,A->D,B->C,B->D。

这里不会影响 A -> B 的行程,因为乘客买的是 B -> C 的车票,站点 B 才上车,不占用 A -> B 行程的座位,我下车你上车,不冲突。

计算耗费性能

一些长途,中间会经过十几个站点,而且有些城市是没有直达的车次的,中间只能换乘。涉及了多个车站的排列组合,这里计算是比较耗费性能的。

行锁竞争会非常激烈

购买一个行程会涉及多个站点的扣减库存,有可能这些多个站点的库存扣减是放在一个事务中的,如果是在一个事务中,那么一次下单行为,可能要涉及到几十次库存扣减。

锁范围膨胀,事务就会被拉大,线程数可能迅速被占满,导致数据库可能成为性能瓶颈,并且接口性能也会有所下降。

热点问题

火车站不同的站之间都是一个具体库存,中间的库存扣了之后,那么远的这个站的库存也要扣减。

极端情况下,一些热门城市,中间一些站点可能比较火爆,那两端的人是不是永远买不到票。

所以说像 12306 这种库存,其实涉及到非常多的一个行锁竞争,而且事务是非常大的。一列火车之间的库存其实是互相影响的,动态变化的。

另外还会涉及到其他维度,比如 硬座、硬卧、软卧、无座 这种业务逻辑在里面。

业务逻辑加上库存之间相互影响,就导致库存扣减逻辑异常复杂。

解决思路

产品角度

  • 早期的 12306 是通过整点去抢票,整点就会产生非常高的流量峰值,对系统造成非常大的压力,后面采取了分时段售票,比如今天开抢 15 天之后的车票,将抢票的压力按照时间区间分散开,大大减低了峰值。
  • 候补车票。

在2019年5月份,12306 新增了“候补购票”功能,在“候补购票”功能没出来之前,放票时间一到,千万人同时刷新抢票,这便是春运火车票秒光的原因。

候补车票堪称抢了一票第三方软件的“饭碗”。

“黄牛”的秘密武器是外挂,用最快的服务器不断地刷新和监控12306,刷票速度往往是正常购票的几十倍。

实际上,市面上通行的“抢票软件”,原理与“黄牛”并无本质区别。

以前,合肥到上海的车票卖光了,有人退票或改签,车票会回到票池可供购买。抢票软件实时刷新监控,第一时间购买,这便是抢票软件比人快的原因。

现在,有了候补车票,合肥到上海的车票卖光了,乘客可以候补登记,有人退票或改签,车票按顺序优先卖给候补登记的人,而不是回到票池公开出售。这便是抢票软件无效的原因。

现在很多抢票软件反而没有“候补购票”功能抢票来得快。

候补车票在整个系统上相当于是一个异步过程。先排队,后面抢没抢到票再通知你。只要异步了,就可以通过消息,或者定时任务慢慢去消费,大大降低系统的压力。

12306 是有非常多的灰色流量的,像是一些抢票软件或者脚本。因为这里面涉及到的利益非常巨大,滋生了很多灰色流量,给12306本身带来了很多额外的压力。

“候补购票”功能可以降低黄牛刷票行为,拦截部分灰色流量。

  • 验证码机制。BT的验证码机制,可以过滤非常大的灰色流量。

12306 的验证码,是所有验证码中的一股绝对的“清流”。

2013 年起,铁道部为了应对黄牛抢票,以及各类抢票软件和插件,升级了购票验证码系统。

在12306官方网站上,从购票到付款,都需要输入验证码。从最开始的字母数字验证码,再到后来升级后的图形验证码,成为了一道“难过”的关卡。

各类奇葩验证码出现在了 12306 上。

到了2015年底,根据有关网站统计,12306上的图形验证码多达接近600种。再经过排列组合,总共有多达300000种。一次性输入准确的比例仅仅是8%,两次输入准确比例27%,三次以上输入准确的比例才勉强超过60%,如果一次性输入成功的平均用时为5秒的话,按照热门车票“秒光”的情况计算,每输错一次验证码,就意味着当次购票成功率下降80%左右。

直到 2018 年,各类奇葩验证码才陆续开始“下岗”。

上面的候补抢票和验证码机制,主要是为了对抗黄牛。

道高一尺,魔高一丈。黄牛也在不断进步。

推荐阅读: 为什么车票刚出就没了?揭秘AI神器抢票内幕 进化的12306与杀不死的“黄牛”

比如我是一个搞黑灰产的人,我可以雇佣一批大学生,去做图形验证码识别。简单验证码还是机器去执行。

那这样的话其实还是没办法防止,但是这样增加了灰产的成本,毕竟人工比机器成本高,从一定程度上还是能够降低灰产的流量占比。

12306 之所以能够使用如此变态的验证码机制的大前提是:没有把用户体验放在首位

12306 比较特殊,市面上几乎不存在竞争对手,而且火车票对于逢年过节,旅行返乡的人来说,几乎可以等同于必需品。

在这种情况下,12306 不必将用户体验放在首位,对于 12306 来说是可以牺牲部分用户体验来换取系统稳定性。

  • 12306 是读多写少的情况,查询流量占大头。

天量的火车票查询是影响 12306 性能的重要原因之一,大概占了90%以上的访问流量。更棘手的是:峰谷的查询有天壤之别,平时工作日跟这种节假日,春运。流量相差是巨大的,时间区间是非常明显的。

如果说完全用机器去堆,可能就会造成一个资源浪费。

还有至关重要一点是,假如完全用机器去堆,在实际业务峰值超出了初始评估量时,服务将面临无法完全承载而瘫痪,因为大规模服务器的采购、交付、部署到应用上线所耗费时间以月计,根本无法在业务量激增时"即插即用"

几乎没有办法在成本和并发能力之间做一个好的平衡。以往的一个做法是从几个关键入口流量控制,保障系统可用性,但是会影响用户体验。

淘宝/天猫大促的时候,也会增加服务器,但阿里的业务盘子大,这些新增的机器很快会被其他业务(包括阿里云)消化掉,可能还不够。但是对于 12306来说,就比较难做到这一点。

在 2015 年的时候,12306 跟阿里云达成合作,通过云的弹性和“按量付费”的计量方式,来支持巨量的查询业务,把架构中比较“重”(高消耗、低周转)的部分放在云上,将75%的余票查询业务切换到了阿里云上。

将余票查询模块和12306现有系统做分离,在云上独立部署一套余票查询系统。

通过动态的云计算,在高峰时段动态去扩容,可以达到分钟级的扩容,这样就避免在平时浪费大量的机器。

合作后,提高了网站的负载能力。2019年的春运,12306挺过了流量的高峰 297 亿次的日访问量。

推荐阅读: 秘密合作半年 阿里同12306关系被曝光

技术角度

历史背景:12306 是在 2010 年左右上线的,11年到12年之间,基本上一到节假日系统就崩溃。当时也是被喷的不行。12年之后在 7、8月份进行了大规模的重构,到了13年的春节,整个系统就比较稳定了,基本没有 down 机的情况发生。

面对 12306 这种读多写少的场景,可能我们平时会采用 Redis 这种缓存机制,12306 具体选择的解决方案,可能跟我们常见的解决方案有一些不一样。

12306 通过充分调研,并没有选择 Redis,而是选择了名叫 Pivotal GemFire 的产品。

没听过 Pivotal ,学 Java 的肯定都听过 Spring。

Spring 框架它归属于 Spring 团队。没错框架名和团队名是一样的,这个团队归属于 Pivotal 公司。

很多银行、投行,实时交易方面的系统都采用 Pivotal GemFire 作为解决方案。

GemFire 基于开源项目 Geode 进行研发的。Redis 是在 2010 年左右才发行的第一个版本,Geode 是更早的一个开源项目。

GemFire 本身是 Geode 的商用版本,可以理解为收费的 Redis(Redis的作者现在就在 GemFire 打工)。就像 Oracle 和 MySQL。

为什么选择 Pivotal GemFire 而不是 Redis?

https://redis.io/comparisons/redis-vs-gemfire/

Redis 是开源的缓存解决方案,而 GemFire 是商用的,我们在互联网项目中为什么使用 Redis 比较多呢,很大原因就是因为 Redis 是开源的,不要钱。

开源对应的也就是稳定性不是那么的强,并且开源社区也不会给你提供解决方案,毕竟你是白嫖的。

而在银行以及 12306 这些系统中,它们对可靠性要求非常的高,因此会选择商用的 GemFire,不仅性能强、高可用,而且 GemFire 还会提供一系列的解决方案。

12306 本身不缺钱,在资金预算充足的情况下,追求系统的稳定性和交易的绝对可靠,追求的是最好的解决方案。

而 GemFire 类似 Oracle 是一套完整的解决方案,不只是给你一套工具,让你私有化部署就不管了,而是需要后面持续去维护的。

据说 GemFire 同时做到了分布式系统里的CAP,违背了架构的一个常识。

当时 12306 也尝试了许多其他的解决方案,都扛不住查询的流量,而使用 GemFire 之后扛住了流量,因此就使用了 GemFire。

Redis 主要用作缓存存储,而当时 (12年左右) 12306 最大的瓶颈主要是在 IO 上面。

为什么 12306 最大的瓶颈会在 IO 上面呢?这跟 12306 使用读扩散有关。

读扩散和写扩散常见于 订阅/聊天/群聊 系统。

推荐阅读:读扩散与写扩散分析

读扩散,一般指牺牲了读的性能,去提升写的性能。

写扩散,一般指牺牲了写的性能,去提升读的性能。

对应 12306 来说:

  • 读扩散:扣减只需要关注列车站点之间的扣减,关注车次,查询的时候再去动态计算。

优点:扣减简单;缺点:计算余票复杂。

  • 写扩散:扣减直接把各个站点之间票都扣减,关注站点。

优点:扣减复杂;缺点:查询余票简单。

个人猜测 12306 使用读扩散的原因是对数据实时性有要求,当扩散队列很长的时候,写入时间存在延时,可能导致不同行程的余票对不齐。

而且涉及排列组合过多,使用写扩散,数据冗余会比较严重,浪费存储成本。

计算余票是一个数据密集型的运算,要关联很多的数据进行计算。一方面需要数据,一方面又要频繁的计算。计算跟数据本身是分开的(计算在CPU,数据存储在内存)。

GemFire 的定位是实时存储网格

一般分布式缓存,比如Redis,查询数据就算再快,还是要从缓存里取出数据,再CPU进行计算。

GemFire 最大的特点是将存储和计算放在了一起,它的存储和实时计算的性能目前还没有其他中间件可以取代。

扣减库存数据库选用的是 Sybase(收费,关系型数据库),相比查询的流量,扣减库存的流量是完全可以承载的。

db-engines.com 这个网站可以对比主流数据库之间的差异

扣减库存之后再同步至 GemFire,然后在 GemFire 里进行动态计算,整个 GemFire 承载的是查询的流量。引入 GemFire 之后,整个系统的查询扩散瓶颈基本上就解决了。

GemFire 将很多机器内存汇总成一个大的节点,作为整体去管理,尽量保证业务运算和业务数据是在同一个节点,尽量避免多节点的网络通信。

但是 GemFire 也存在不足的地方,对于扩容的支持不太友好。在 12306 中,也有过测试,需要几十个T的内存就可以将业务数据全部放到内存中来,因此直接将内存给加够,也就不需要很频繁的扩容。

余票库存的表如何设计?

这里的设计思路都是猜测的,并不一定是 12306 真实的设计方案。

12306 余票库存的表的设计是非常特色并且重要的

首先说一下需要哪几个表来表示余票的库存信息:

1、基础的车次表:表示车次的编号以及发车时间等具体的车次信息,属于比较稳定的数据。

2、车的座位表:表示每个座位的具体信息,包括在几车厢、几行、几列,以及 该座位的售卖情况

3、车的余票表:通过座位表可以计算出每个车位在各个车站区间还有多少余票,但是动态计算比较浪费性能,因此再添加余票表,通过定时计算余票信息放入到余票表中,提高查询的性能。

(其实还应该有站点表和车厢表,不过不太重要,这里直接就省略了)

这里说一下这 3 个表的对应关系:

比如车次为 K123,该车上有很多的座位,每个座位对应座位表中的一条数据。

而余票表指的是 K123 车次上,硬座、硬卧、软卧、无座各有多少张余票,余票表的信息可以由座位表来计算得到。

接下来说一下如何通过座位表来表示用户购买的车票:

12306 中的车票信息其实是比较复杂的,因为各个车站之间是有依赖关系的,比如 4 个车站 A->B->C->D

如果乘客购买 B->C 的车票的话,不仅 B->C 的库存要减一,B->D 的库存也要减一,这是排列组合的情况,可以考虑通过二进制去简化车票的表示。

在座位表中,我们设置一个字段 sell varchar(50) 表示该座位的售卖情况,如果该车次有 4 个站 A->B->C->D,那么 sell 字段的长度就为 3,sell 字段的第一位表示该座位 A->B 的票是否已经被买了,第二位表示 B->C 的票是否已经被买了…

如果乘客购买 B->C 的车票,则 sell 字段的值为:010

如果乘客购买 B->D 的车票,此时发现该座位在 B->C 已经被卖出去了,因此不能将该座位出售给这位乘客。

如果乘客购买 C->D 的车票,则 sell 字段的值为:011 ,表示 B->C,C->D 都已经有人了。

通过座位表来计算出余票,得到余票表。

通过余票表提升查询性能

这里余票表就相当于是数据库中的视图。

如果要去查询一个车次中某一个类型的余票还有多少,还需要去对座位表进行计算,这个消耗是比较大的 ,因此通过余票表来加快对于余票的查询。

可以定时去计算座位表中的数据,将每种类型的座位的余票给统计出来,比如:

硬卧:xx张

硬座:xx张

软卧:xx张

再将余票表的信息给放入到缓存中,大大提高查询的性能。

我们在使用 12306 的时候,也会发现,有时候显示的有票,但是真正去买的时候发现已经没有余票了,这就说明 12306 没有保证实时的一致性,只要保证了最终一致性即可,也就是用户真正去买的时候,保证对于余票数量的查询是准确的就可以了。

个人推测 12306 使用的是缓存+动态计算结合的方式,查询的时候使用的是缓存(最终一致性),等到真正下单的时候会再去动态计算一遍(实时一致性)。这样利用缓存就能隔绝掉很大的查询的流量,并且也能保证最终下单的准确性。

对于电商,如果商品销售小份额的超出库存,部分场景下可以通过补充库存进行弥补,仅要求弱一致性。而 12306 属于实时的交易型系统,库存资源的数量固定,对分布式事务要求强一致性。

中间的站点如果太过火爆,导致两边的站点买不到票怎么办?

比如 A->B->C->D,对于一个车次中的座位来说,如果 B->C 的乘客非常多,那么是不是就会导致 A->D 买不到票了?

这个是通过运营部来进行设计,首先考虑的肯定是要盈利,远途票价比较贵,因此比较倾向于远途的旅客,营业部根据具体的实际情况以及盈利情况来定一下各个区间预留多少票,给每个车站区间都留有一些余票,那么就不会因为某一个区间非常火爆,而导致其他乘客买不到长途的票了。

抢票软件推荐

https://www.bypass.cn/(亲测好用)

巨人的肩膀

  • 【高级进阶】12306未公开技术细节!技术专家必须理解的设计思想!
  • 【12306完结】铁道部技术内幕!为什么是Gemfire?如何强一致?预留票机制?
  • 读扩散与写扩散分析
  • 数据存储结构设计,是读扩散,还是写扩散?
  • 由12306.CN谈谈网站性能技术
  • 12306 架构设计难点
  • 12306的十年往事
  • 为什么车票刚出就没了?揭秘AI神器抢票内幕
  • 进化的12306与杀不死的“黄牛”
  • 12306订票系统技术内幕 源码
  • 架构必看:12306抢票亿级流量架构演进(图解+秒懂+史上最全)
  • 秘密合作半年 阿里同12306关系被曝光
  • 抢了个票,还以为发现了12306的系统BUG
  • 12306 技术分析
  • 从嗤之以鼻到“奇迹” 前淘宝工程师详解12306技术
  • Gemfire:分布式缓存利器
  • REDIS VS GEMFIRE

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/15315.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

测试基础05:软件测试的分类

课程大纲 1、两种架构(Architecture) 1.1、B/S(Browser/Server) 浏览器服务器架构(大体3步):用户通过浏览器向服务器发出请求,服务器处理请求,将结果通过网络返回到用户…

使用Webcam实现摄像头的开启和关闭,并保存和复制图片

实现思路 0,将webcam的jar文件传入项目中 1,显示摄像头的地方:创建一个画板,在画板上添加开启和关闭按钮 2,设置开启和关闭功能:创建一个类实现动作监听器,进而实现监听动作按钮 3&#xff…

【数据结构与算法篇】二叉树链式结构及实现

【数据结构与算法篇】二叉树链式结构及实现 🥕个人主页:开敲🍉 🔥所属专栏:每日刷题🍍 🌼文章目录🌼 4. 二叉树链式结构的实现 4.1 前置说明 4.2 二叉树的遍历 4.2.1 前序、中序以及…

OceanBase的存储架构与传统LSM-Tree架构的异同|OceanBase数据转储合并技术解读(二)

前篇博文将OceanBase的存储架构巧妙地与自然界中的“水生态”进行了类比,今日我们转变视角,聚焦在与拥有相同LSM-Tree架构的其他产品的比较,深入探讨OceanBase相较于它们所展现出的独特性能。 众所周知,OceanBase数据库的存储引擎…

element-ui 前端ui框架用法开发指南(2024-05-22)

Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库 1、npm安装 // npm安装:npm install element-ui --save 能更好地和 webpack 打包工具配合使用 2、cdn在线引入 访问最新版本的资源地址 - element-uiThe CDN for element-u…

RedHat9 | DNS剖析-配置主DNS服务器实例

一、实验环境 1、BIND软件包介绍 BIND软件是一款开放源码的DNS服务器软件,由美国加州大学Berkeley分校开发和维护,全称为Berkeley Internet Name Domain。该软件在DNS(域名系统)领域具有重要地位,是目前世界上使用最…

使用OpenCV dnn c++加载YOLOv8生成的onnx文件进行目标检测

在网上下载了60多幅包含西瓜和冬瓜的图像组成melon数据集,使用 LabelMe 工具进行标注,然后使用 labelme2yolov8 脚本将json文件转换成YOLOv8支持的.txt文件,并自动生成YOLOv8支持的目录结构,包括melon.yaml文件,其内容…

Python怎样定位并删除Sql语句中不确定的查询条件

1.问题场景描述: 在sql语句中经常会有查询条件是:查找多个订单签订日期范围的数据,但具体的日期范围是不确定,我们如何来查找定位 例如:查询条件语句的部分如下图: 目标是: 1)定位字符串:t_contract_order.sign_date 2)最终得到结果: 解决问题思路: 1)定位要找的字符串起始位置…

【学习心得】PyTorch的知识要点复习(持续更新)

PyTorch知识要点复习,目的是为了巩固PyTorch基础、快速回顾、深化理解PyTorch框架。这篇文章会持续更新。 一、本文的一些说明 知识点梳理:我将PyTorch的核心概念和高级技巧进行了系统化的整理,从基础的张量操作到复杂的模型构建与训练。这样…

【Linux】进程终止与进程等待

目录 进程终止 errno exit和_exit 进程等待 wait和waitpid 宏:WIFEXITED 非阻塞等待 进程终止 下面要谈的一个话题就是进程终止,就是说一个进程退出了,可能有三种情况 1.进程代码执行完,结果是正确的 2.进程代码执行完&…

【活动】开源与闭源大模型:探索未来趋势的双轨道路

🌈个人主页: 鑫宝Code 🔥热门专栏: 闲话杂谈| 炫酷HTML | JavaScript基础 ​💫个人格言: "如无必要,勿增实体" 文章目录 开源与闭源大模型:探索未来趋势的双轨道路引言一、开源大模型&#…

翻译《The Old New Thing》- The importance of the FORMAT_MESSAGE_IGNORE_INSERTS flag

The importance of the FORMAT_MESSAGE_IGNORE_INSERTS flag - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20071128-00/?p24353 Raymond Chen 2007年11月28日 FORMAT_MESSAGE_IGNORE_INSERTS 标志的重要性 简要 文章讨论了使用FormatMes…

评估企业的业务是否存在高风险的六个步骤

风险的幽灵使得组织别无选择,只能改善各种网络风险的总体管理。以下是一个基于信息安全论坛的IRAM2方法论的分步过程,网络安全和风险从业者可以利用它来评估和管理信息风险。 第1步:范围界定练习 范围界定练习的目标是提供一个以业务为中心…

基于springboot+vue的招聘信息管理系统

开发语言:Java框架:springbootJDK版本:JDK1.8服务器:tomcat7数据库:mysql 5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:…

K8s的常用命令以及yaml文件的创建

目录 一、声明式管理方法:YAML文件 1、yaml文件简介 2、yaml和json的主要区别: 3、YAML的语法格式 4、yaml文件组成部分 ①控制器定义 5、查看api资源版本标签 6、编写nginx-deployment.yaml资源配置清单 6.1创建资源对象 6.2查看创建的pod资源…

Unity-Sprite Atlas+UGUI系统的运行原理

每日一句:别听世俗耳语,看自己的风景就好 目录 SA的原理: SA的优点: SA的缺点: DrawCall是什么? 批处理是什么? 我们先了解一下UGUI系统的运行原理吧! 提到图集优化&#xff0…

cocosCreator动态生成二维码

cocosCreator 版本:3.7.2 开发语言:typeScript 我们在游戏开发中,经常会生成一个专属于玩家个人的二维码,比如说推广、充值等功能。 接到这个任务,在网上找了下,还是有很多教程的。但是这些教程大部分都是用…

Ollydbg动态分析MessageBoxA输出hellow world

一、目的 找到main函数找到调用的MessageBoxA函数 测试源码 #include <iostream> #include <windows.h>int main() {MessageBoxA(NULL, "Hellow World", "Title", MB_OK);return 1; }二、快捷键 指令快捷键说明RestartCtrlF2重新开始调试S…

buu[HCTF 2018]WarmUp(代码审计)

buu[HCTF 2018]WarmUp&#xff08;代码审计&#xff09; 题目 访问source.php <?phphighlight_file(__FILE__);class emmm{public static function checkFile(&$page){$whitelist ["source">"source.php","hint">"hint.php…

电脑键盘如何练习盲打?

电脑键盘如何练习盲打&#xff1f;盲打很简单&#xff0c;跟着我做&#xff0c;今天教会你。 请看【图1】&#xff1a; 【图1】中&#xff0c;红色方框就是8个基准键位&#xff0c;打字时我们左右手的8个手指就是放在这8个基准键位上&#xff0c;F键和J键上各有一个小突起&…