开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

闲鱼异地多活架构设计与实现

发表于 2021-10-14

作者:闲鱼技术——吴白

背景

首页和搜索一直以来都是闲鱼导购的主阵地,为了保证高可用业务上做了很多保护方案。但是随着原有地域的IDC日渐趋于饱和,一些更深层次的问题开始暴露出来:a)架构不具备扩展性。当服务量增大,单个IDC由于服务器部署、电力等物理因素无法满足诉求,不能简单的通过IDC部署来应对新增的流量。以算法为例,算法在现有IDC资源饱和的情况下,其上新模型之前不得不等老模型下线,严重制约业务的迭代效率。b)容灾问题。当现有IDC出现故障时,如何保证导购主链路依然高可用。​

常用高可用架构

•同城双活:系统从接入层以下在同城两个机房做部署,这样可以应对断电,断网等单机房故障。由于同城机房距离足够近,可以近似看成一个机房,因此在部署上和单机房相比没有特殊要求。但是遇到同区域灾害时,服务就会受到影响,而且扩展性较差。

image.png

•异地灾备:系统从接入层以下除了在同城两个机房做部署之外,在其他区域部署异地备份,底层数据根据实际要求做热备/冷备,但是不承担任何流量。当区域挂掉时,服务切至备份区域保证服务可用性。但异地灾备的问题是:a)另一个区域不跑流量,出了问题不敢切。b)备份全站,资源利用率低。c)存在跨地域访问。

image.png

•异地多活:异地多活从接入层开始做多区域多机房部署,各个区域之间没有主备的概念,均承担相应的流量。它的优势在于资源利用率高,扩展性较好。

image.png

前面提到闲鱼除了要解决容灾问题之外还需要解决算法同学的资源利用问题,因此异地多活很自然成为我们的不二选择。

多地部署带来的变化

当我们的系统从单地部署升级成多地域部署时,它并不是简单将整个系统搬到各个地域去做部署。当你的系统部署过去之后,需要考虑非常多的因素,比如

•流量调度,系统部署过去后流量怎么跟着怎么过去。•流量自闭环。由于距离的原因,跨地域的物理延时是没法避免的,流量过去之后怎么保证所有的操作都在本地完成,如果做不到那怎么将这种延时影响降到最低。•数据一致性,数据如何在多个地域之间保证一致性,针对一些如交易相关等数据时效性敏感的场景,如何解决多地域之间数据同步时延。•容灾切流。当某个机房出现故障时,如何快速把流量无损地切至其他机房。这里并不是说简单把流量切过去就完事,由于数据在多区域同步,流量切过去之后能否保证数据的一致性?

还有很多其他因素,这里不一一列举。可以预想到当我们的系统升级成多地域部署架构之后,给整个系统带来了很大的变化,同时也带来了相当大的挑战。​

多地域部署方案

面临的挑战

异地部署最大的特点是网络时延较高:一般来说同地域延时2~3ms,同机房延时小于1ms,而跨地域延时一般大于20ms。所以我们首先要解决的问题便是如何降低跨地域对导购链路的影响,这也是我们做异地容灾的一个大原则,这面临着几个难点

image.png

1.流量如何做到地域内闭环,流量闭环必然有代价,如何平衡这两者之间的关系。2.多地域部署带来的系统架构的复杂度。无论是流量调控,服务路由还是数据读写同步,都要在庞大的系统中做好精细化的调控,对系统带来的复杂度可想而知。3.如何做流量路由。如何识别流量的来源,控制流量的去向。4.系统部署规范。系统在一直不停的演进,如何避免架构快速腐化。5.流量如何做管控。流量调控规则需要统一做监控,管控等,一旦需要切流时,流量如何快速完成调整收敛。

用户数据是否做拆分

在介绍部署架构之前先讲一下闲鱼导购链路的一个大前提,后面的很多方案都是基于这个大前提之下做抉择的,那就是存储层的数据是否要做拆分。​

异地部署之后数据会存在多个区域中,区域之间的数据同步存在一定的延时,因此数据是否要做拆分取决于对数据一致性的要求。如果可以容忍对数据短时间不一致那么则不需要做数据拆分。但是在电商某些场景下,比如买家加入购物车操作,如果数据写在区域A,购物车列表读的却是区域B,那么很有可能就会导致买家看不到刚加入购物车的商品,这是非常糟糕的体验。因此在这种场景下就需要保证数据的读写都在相同的维度,这种情况下就需要对数据做拆分。​

但是前面提到闲鱼导购链路特点是:a)可以容忍短时间的数据不一致。b)不涉及到数据库的写操作。显而易见数据拆分显得并没有那么必要,因此我们决定不做数据拆分。

部署架构

整体部署方案中,物理上各个区域是对等的,不存在主备的概念,但是逻辑上还是区分出了中心区域和其他可用区域,这是因为:a)总有部分长尾依赖没法做多区域部署。b)并不是所有场景(非核心链路)都适合做多地域部署。我们把这些长尾依赖统一放在中心区域,做兜底部署。

image.png

流量路由方案

流量路由方案这里面包含了两个问题:

•流量分发的原则是什么,解决哪些流量应该到哪个地域。流量分发原则常见的有三种方式:a)完全随机。b)按照地域就近访问。c)按照用户维度切分。•流量在哪一层做分发,解决用户请求从哪里开始分流。

如前面数据拆分部分提到,流量分发理论上需要和数据拆分逻辑保持一致。由于闲鱼底层没有做数据拆分,因此流量分发原则相对较为灵活。

1.最简单的流量分发原则是完全随机。前面提到数据在多区域之间存在数据同步延时,虽然导购链路可以容忍短时间的数据延时,但是我们需要避免用户连续两次请求看到的数据存在不一致(如果俩次请求分别落在不同地域)。

1.按照地域就近访问能实现最低的访问延时,但是这种方案最大的问题是地域之间的流量严重不均衡,而且在不停变化(正常时段&节假日),这会给整个系统的负载均衡带来很高的复杂度。

1.按照用户进行切分,保证部分用户请求会固定路由到某个区域。

在我们导购场景下1和2都不适合,因为都有可能导致用户两次请求看到的数据不一致,最终我们选择按照用户进行切分,这也是公司内部成熟的路由方案。确定了流量分发原则,接下来需要决定流量在哪一层做分发。这点我们考虑了三个可选的方案

•方案一,在域名解析阶段做不同地域的流量分发。a)这种方式成本较高,需要有独立的DNS域名,独立的路由规则。b)当路由规则调整的时候,收敛周期较长(依赖端侧的缓存更新)。c)和运行环境绑定,不支持H5,Web等场景。其优点是经过公司内部验证,相对比较成熟。

image.png

•方案二,在统一接入层进行不同地域流量分发。这种方式成本低,可以复用现有的逻辑,只需要在统一接入层做规则配置,但是部分流量存在跨地域访问(从接入层到业务集群)。

image.png

•方案三,搭建边缘网关。通过原生的DNS做域名解析,然后就近选择边缘网关访问。切流等复杂逻辑放在边缘网关中完成。这个方案和运行时无关,支持app&小程序&Web等,而且规则调整收敛速度快,扩展能力强。其缺点是需要从0开始搭基建。

image.png

方案一的跨地域问题是我们需要避免的,方案三虽然比较适合,但是成本太高,而且没有成熟的经验借鉴,权衡之下我们最终决定采用方案二。

全链路升级改造

全链路改造的目的在于使我们的系统适应从单地部署到多地域部署的转变,改造涉及到的点非常多,主要包括

1.应用代码改造。导购链路所有的依赖是否都能做多地部署,如果没法多地部署跨地域时延是否会被放大。2.服务之间的流量路由策略。导购链路涉及到很多异构的子系统,这些异构系统之间的流量是否遵循同地域优先,当某个地域服务挂了之后流量是否允许自动切到其余地域。3.流量强纠偏。导购的请求链路较为复杂,会依赖众多异构的子系统。虽然域名解析时流量会路由至对应的区域,但是在后续链路仍然有可能发生流量窜到其余地域的情况,这种情况下理论上会对用户体验造成影响,所以在导购链路的每一跳节点都应该有纠偏策略。4.外部流量由于分发策略我们没法管控,会导致预期之外的流量流入。为了避免这种情况,我们也需要有一个流量纠偏的策略。

改造点3在数据强一致场景是必不可少的,但是对本次导购链路,由于改造成本和时间的关系,最终我们放弃了改造点3。因为改造点2保证了正常情况下流量路径是符合预期的,只有异常情况才有可能发生流量窜到其他区域,但是这种情况我们认为:a)低频且持续时间不长。b)短时间的不一致对业务影响可控。​

应用代码改造主要包括​

•对于那些没法做多地部署的依赖,评估其对数据一致性的约束,如果是弱一致性,则考虑使用富客户端模式,在富客户端模式中优先读缓存,不命中再走一次RPC,通过缓存降低跨地域请求的频率。

•没法做多地部署且要求数据强一致性的依赖,需要避免跨地域访问时延被放大。不存在跨地域延时的时候串行并行的区别并不明显,但是引入跨地域时延之后串行和并行的区别就会非常明显,因此对这部分依赖需要做并发改造。另一方面在改造过程中梳理出核心依赖&非核心依赖,核心依赖强制要求单元化,对于非核心依赖做到并发&可观测&可降级。

•缓存改造。由于以前对缓存的使用不够严谨,会导致单地域部署下被掩盖的问题在多地域部署之下暴露出来。比如下面这种场景,在某个场景写入某个key,然后在另一个场景下读取这个key。在单地域部署下不会有问题,但是一旦多地域部署之后就有可能出现读写不同地域的情况导致数据不一致。

image.png

这种情况下我们需要:a)强制写中心主节点。b)开通主节点到其他地域的数据同步。总的来说缓存改造两大原则

•如果是非持久化缓存,则不用做任何改造。因为这种场景缓存不命中会有数据加载过程。但是很多非持久化缓存场景滥用了持久化缓存,针对这类case需要规范使用,改造成非持久化缓存。•如果是持久化缓存,分为两种情况:a)强一致性,如分布式锁,这种情况强制读写中心主集群。b)非强一致性,则强制写中心主节点,就近读。

导购链路涉及到很多异构系统,包括各个子领域应用构成的微服务集群,以及众多搜索&推荐服务。异构主要体现在:a)编写语言以及部署&运维平台的差异。b)服务注册发现机制不一样,主要包括configserver/vipserver/zookeeper。因此主要改造内容在于规范对这些组件的使用,调整流量路由策略保证流量区域内自闭环。​

为了防止外部流量对闲鱼导购流量的影响,我们在统一接入层加了一条流量纠偏策略:对于外部非导购链路的流量,强制切回中心区域。这一点非常重要,因为对于部署范围之外的服务,如果因为这个原因导致流量到了其他可用区域,其返回数据的正确性我们没法做保证。

服务集群部署方案

image.png

微服务集群整体采用对等部署。微服务集群按照服务发现&注册机制的不同划分成三类:

•采用HSF作为RPC框架的业务服务,采用configserver做服务发现,configserver同时在多地域部署,彼此之间互相隔离,各地域部署的服务只拉取本地域内的configserver数据,通过这种方式实现地域之间的流量隔离。但是中心区域的数据会同步至其他区域(区域挂了流量可以路由到中心区域,保证服务可用)。

•采用HTTP调用的算法服务集群由于历史&异构原因,采用了两种服务注册&发现方式

•Zookeeper。Zookeeper在中心和区域都做单独部署,客户端请求的时候按照地域拉取对应的Zookeeper。通过这种方式实现流量的同机房访问,地域彼此间数据隔离,当单个地域服务出现问题时,只能通过将其他区域的服务数据挂载到故障地域对应的Zookeeper下面来进行恢复。•vipserver(阿里自研的一套集群路由软件负载均衡系统)。由于vipserver本身是分布式的负载均衡系统,且支持多种路由方式,故只部署一套。

导购链路使用缓存的地方很多,大致分成两种用法

•缓解持久层的访问压力。先访问缓存,缓存如果没有数据则请求持久化层并把数据加载至缓存中,缓存本身不做数据一致性保证。这种情况比较好处理,因为不涉及到多区域之间的同步,只需要简单做多地域部署即可。

image.png

•用作数据持久化。典型的如分布式锁,计数器等。这种场景会有中心和区域的概念,彼此间双向同步,这种场景在单区域部署的时候和上面的用法没有太大区别,但是在多地域部署架构下,就会因为双写导致数据出现不一致,因此需要保证同一个key同一时间不能在多区域同时写。

•区域同步至中心。因为数据需要做持久化,所以会在中心有一份完整的数据集,区域保证数据的最终一致性即可。•中心同步至区域。保证区域的数据和中心的数据一致。

image.png

数据库部署

按照分布式系统的CAP定理:Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。所以严格意义上来说,数据库的异地部署只能三选二。但是在分布式系统中必然是分区的,而且分区之间的网络我们没法控制,也就是说P是一个事实,我们只能从C和A中二选一,这分别对应着数据库的两种数据复制方式。

•主从复制模式的MySql:中心写成功即返回,从节点依赖主从之间的数据同步。这种模式下保证了A和P,牺牲了C。

image.png

•双向复制模式的MySql:没有主从节点之分,节点与节点之间实现数据最终一致性。这种模式下同样保证了A和P,牺牲了C。

image.png

•采用Paxos协议的分布式数据库如Google的Spanner等,采用Paxos协议来保证数据的强一致性,但是在Master节点挂了之后在新的Master选举出来之前不可用,即保证了C和P,牺牲了A。

一方面根据导购链路的特点(绝大部分都是数据读取操作,可以容忍短时间内的不一致)。另一方面原有的数据存储采用MySql,考虑到成本,最终选择主从复制模式MySql。​

总结

异地部署给系统带来的最大挑战是物理距离带来的网络延时,整个系统设计都围绕着这个展开。总的来说在解决跨地域延时过程中我们遵循两个大的原则:a)流量地域内自闭环。b)坚持可用性优先。在这两个大原则之下从接入层,服务层以及数据存储层做了相应的改造&部署。​

目前闲鱼部分链路已经实现了两地三机房部署,并且已经承接线上流量,具备了异地容灾的能力。同时经过本次改造,导购链路具备了较好的扩展性,能够以极低的成本快速部署至更多机房。​

但是一方面由于导购链路大部分都是只读场景,对数据要求弱一致性即可。对于数据强一致性场景带给系统的挑战会更大。另一方面业务是一个不停演进的过程,如何保证在演进过程中仍然能保证异地多活的部署架构,这是急需解决的问题。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

EasyC++02,C++常用语句

发表于 2021-10-14

github仓库

声明变量

在C++当中所有的变量都需要声明,如:

1
C++复制代码int wordCnt;

我们声明了一个int类型的变量wordCnt,这样的语句会告诉编译器两个关键信息。一个是变量所需要的内存,一个是这块内存的名称。比如在这个例子当中,我们声明了一个int型的变量。它占据32个二进制位,也就是4个字节,这块内存的名称被叫做wordCnt。

注:在有些语言(如basic)当中变量无须声明,可直接使用。但这会引起部分问题,如拼写错误时很难检查。

对于变量声明,C++ Primer推荐尽可能在首次使用变量之前就声明它。

赋值语句

变量被声明了之后,我们就可以通过赋值语句给它赋上我们想要的值。

例如:

1
C++复制代码wordCnt = 10;

C++当中支持连续赋值的写法,例如:

1
2
3
4
C++复制代码int wordCnt;
int personCnt;
int roomCnt;
wordCnt = personCnt = roomCnt = 10;

这就是一个连续赋值的操作,10先赋值给最右侧的roomCnt,再赋给personCnt,最后赋给wordCnt。

cin、cout语句

cin、cout同样是C++当中常用的语句。

cin顾名思义,表示读入,它可以从屏幕(终端)读入数据,流向我们指定的变量。例如:

1
2
C++复制代码int wordCnt;
cin >> wordCnt;

cin是输入数据的对象,数据从cin流向了wordCnt。即我们在终端输入的数据被读入到了wordCnt当中。

和cout一样,我们可以从终端读入多种类型的数据,如浮点数、整数、字符串等,cin会自动将读入的数据转化成对应的数据类型并完成赋值。

我们使用cout输出结果时可以通过多个<<符号进行拼接,如:

1
C++复制代码cout << "word count: " << wordCnt << "room count: " << roomCnt << endl;

库函数

C++官方提供了许多库函数,这些函数的实现往往分布在不同的头文件当中。我们需要首先include对应的头文件才能进行使用。

例如计算平方根的函数sqrt的实现在cmath库中,我们需要首先include cmath这个库,才能使用它。

1
2
3
4
C++复制代码#include <cmath>
using namespace std;

double a = sqrt(10.0);

对于库函数我们需要首先查找到它对应的头文件,将其include之后再进行使用。

自定义函数

C++当中函数同样分为声明和实现,函数的声明一定要写在main函数之前,否则main函数在调用的时候将会找不到对应的函数,报错error: use of undeclared identifier。

所以一种正确的写法是在main函数之前写上函数的声明,函数的实现写在main函数之后。其实只需要保证函数声明在main函数之前即可,函数的实现并不限制摆放位置。

对于函数的声明,和变量的声明类似,它分为三个部分。分别是函数返回类型,函数名和函数所需的外界参数。例如:

1
2
C++复制代码void test();
int getValue(int x, int y);

上面所写的都是函数的声明,如果函数无需外界参数,也需要保留小括号。

另外在函数的声明当中,变量名也可以省略,只需要标注类型即可,所以getValue的函数声明又可以写成:

1
C++复制代码int getValue(int, int);

如果怕麻烦,可以将函数的声明和实现写在一起,放在main函数之前即可。

例如:

1
2
3
4
5
6
7
8
C++复制代码int getValue(int x, int y) {
return x + y;
}

int main() {
cout << getValue(3, 5) << endl;
return 0;
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

聊聊如何设计百万级抽奖系统

发表于 2021-10-14

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

本文正在参与 “性能优化实战记录”话题征文活动


点赞再看,养成习惯。

本文收录于github-JavaExpert ,里面有我的系列文章、面试题库、自学资料、电子书等。

前言

哈喽,大家好,我是一条。

今天分享一个粉丝在美团二面遇到的问题——如何设计一个百万人抽奖系统?

思维导图

最近在交流群里和字节滴滴等专家聊怎么更好的把知识传递给粉丝的时候,大家一致觉得思维导图有利于构建知识网络,粉丝反馈也很喜欢思维导图,所以后面的文章都会尽量配上思维导图。

导图源文件:github.com/lbsys/JavaE…

导图按照由浅入深的方式进行讲解,架构从来不是设计出来的,而是演进而来的

从一个几百人的抽奖系统到几万人,再到到百万人,不断增加新的东西。

最后总结归纳一套设计思想,也是万能模板,这样面试官问任何高并发系统,只需从这几个方向去考虑就可以了。

[toc]

V0——单体架构

如果现在让你实现几十人的抽奖系统,简单死了吧,直接重拳出击!

两猫一豚走江湖,中奖入库,调通知服务,查库通知,完美!

相信大家学java时可能都做过这种案例,思考🤔一下存在什么问题?

  • 单体服务,一着不慎满盘皆输
  • 抽了再抽,一个人就是一支军队
  • 恶意脚本,没有程序员中不了的奖

接下来就聊聊怎么解决这些问题?

V1——负载均衡

当一台服务器的单位时间内的访问量越大时,服务器压力就越大,大到超过自身承受能力时,服务器就会崩溃。

为了避免服务器崩溃,让用户有更好的体验,我们通过负载均衡的方式来分担服务器压力。

负载均衡就是建立很多很多服务器,组成一个服务器集群,当用户访问网站时,先访问一个中间服务器,好比管家,由他在服务器集群中选择一个压力较小的服务器,然后将该访问请求引入该服务器。

如此以来,用户的每次访问,都会保证服务器集群中的每个服务器压力趋于平衡,分担了服务器压力,避免了服务器崩溃的情况。

负载均衡是用「反向代理」的原理实现的。具体负载均衡算法及其实现方式我们下文再续。

负载均衡虽然解决了单体架构一着不慎满盘皆输的问题,但服务器成本依然不能保护系统周全,我们必须想好一旦服务器宕机,如何保证用户的体验。

即如何缓解开奖一瞬间时的大量请求。

V2——服务限流

限流主要的作用是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用。

还可用于平滑请求。

在上一小节我们做好了负载均衡来保证集群的可用性,但公司需要需要考虑服务器的成本,不可能无限制的增加服务器数量,一般会经过计算保证日常的使用没问题。

限流的意义就在于我们无法预测未知流量,比如刚提到的抽奖可能遇到的:

  • 重复抽奖
  • 恶意脚本

其他一些场景:

  • 热点事件(微博)
  • 大量爬虫

这些情况都是无法预知的,不知道什么时候会有10倍甚至20倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的(弹性扩容都是虚谈,一秒钟你给我扩一下试试)

明确了限流的意义,我们再来看看如何实现限流

防止用户重复抽奖

重复抽奖和恶意脚本可以归在一起,同时几十万的用户可能发出几百万的请求。

如果同一个用户在1分钟之内多次发送请求来进行抽奖,就认为是恶意重复抽奖或者是脚本在刷奖,这种流量是不应该再继续往下请求的,在负载均衡层给直接屏蔽掉。

可以通过nginx配置ip的访问频率,或者在在网关层结合sentinel配置限流策略。

用户的抽奖状态可以通过redis来存储,后面会说。

拦截无效流量

无论是抽奖还是秒杀,奖品和商品都是有限的,所以后面涌入的大量请求其实都是无用的。

举个例子,假设50万人抽奖,就准备了100台手机,那么50万请求瞬间涌入,其实前500个请求就把手机抢完了,后续的几十万请求就没必要让他再执行业务逻辑,直接暴力拦截返回抽奖结束就可以了。

同时前端在按钮置灰上也可以做一些文章。

那么思考一下如何才能知道奖品抽完了呢,也就是库存和订单之前的数据同步问题。

服务降级和服务熔断

有了以上措施就万无一失了吗,不可能的。所以再服务端还有降级和熔断机制。

在此简单做个补充,详细内容请持续关注作者。

有好多人容易混淆这两个概念,通过一个小例子让大家明白:

假设现在一条粉丝数突破100万,冲上微博热搜,粉丝甲和粉丝乙都打开微博观看,但甲看到了一条新闻发布会的内容,乙却看到”系统繁忙“,过了一会,乙也能看到内容了。

(请允许一条幻想一下😎)

在上述过程中,首先是热点时间造成大量请求,发生了服务熔断,为了保证整个系统可用,牺牲了部分用户乙,乙看到的”系统繁忙“就是服务降级(fallback),过了一会有恢复访问,这也是熔断器的一个特性(hystrix)

V3 同步状态

接着回到上一节的问题,如何同步抽奖状态?

这不得不提到redis,被广泛用于高并发系统的缓存数据库。

我们可以基于Redis来实现这种共享抽奖状态,它非常轻量级,很适合两个层次的系统的共享访问。

当然其实用ZooKeeper也是可以的,在负载均衡层可以基于zk客户端监听某个znode节点状态。一旦抽奖结束,抽奖服务更新zk状态,负载均衡层会感知到。

V4线程优化

对于线上环境,工作线程数量是一个至关重要的参数,需要根据自己的情况调节。

众所周知,对于进入Tomcat的每个请求,其实都会交给一个独立的工作线程来进行处理,那么Tomcat有多少线程,就决定了并发请求处理的能力。

但是这个线程数量是需要经过压测来进行判断的,因为每个线程都会处理一个请求,这个请求又需要访问数据库之类的外部系统,所以不是每个系统的参数都可以一样的,需要自己对系统进行压测。

但是给一个经验值的话,Tomcat的线程数量不宜过多。因为线程过多,普通服务器的CPU是扛不住的,反而会导致机器CPU负载过高,最终崩溃。

同时,Tomcat的线程数量也不宜太少,因为如果就100个线程,那么会导致无法充分利用Tomcat的线程资源和机器的CPU资源。

所以一般来说,Tomcat线程数量在200~500之间都是可以的,但是具体多少需要自己压测一下,不断的调节参数,看具体的CPU负载以及线程执行请求的一个效率。

在CPU负载尚可,以及请求执行性能正常的情况下,尽可能提高一些线程数量。

但是如果到一个临界值,发现机器负载过高,而且线程处理请求的速度开始下降,说明这台机扛不住这么多线程并发执行处理请求了,此时就不能继续上调线程数量了。

V5业务逻辑

抽奖逻辑怎么做?

好了,现在该研究一下怎么做抽奖了

在负载均衡那个层面,已经把比如50万流量中的48万都拦截掉了,但是可能还是会有2万流量进入抽奖服务。

因为抽奖活动都是临时服务,可以阿里云租一堆机器,也不是很贵,tomcat优化完了,服务器的问题也解决了,还剩啥呢?

Mysql,是的,你的Mysql能抗住2万的并发请求吗?

答案是很难,怎么办呢?

把Mysql给替换成redis,单机抗2万并发那是很轻松的一件事情。

而且redis的一种数据结构set很适合做抽奖,可以随机选择一个元素并剔除。

V6流量削峰

由上至下,还剩中奖通知部分没有优化。

思考这个问题:假设抽奖服务在2万请求中有1万请求抽中了奖品,那么势必会造成抽奖服务对礼品服务调用1万次。

那也要和抽奖服务同样处理吗?

其实并不用,因为发送通知不要求及时性,完全可以让一万个请求慢慢发送,这时就要用到消息中间件,进行限流削峰。

也就是说,抽奖服务把中奖信息发送到MQ,然后通知服务慢慢的从MQ中消费中奖消息,最终完成完礼品的发放,这也是我们会延迟一些收到中奖信息或者物流信息的原因。

假设两个通知服务实例每秒可以完成100个通知的发送,那么1万条消息也就是延迟100秒发放完毕罢了。

同样对MySQL的压力也会降低,那么数据库层面也是可以抗住的。

看一下最终结构图:

答题模板

所谓答题模板,就是高并发问题的几个思考方向和解决方案。

单一职责

一个基本的设计思想,回想高中物理的串联和并联,串联一灭全灭,并联各自有一个通路。

一样的道理,高内聚,低耦合。

微服务之所以兴起就是因为把复杂的功能进行拆分,即使网站崩了,无法下单,但是浏览功能依然健康,而不是所有服务引起连锁反应,像雪崩一样,全面瘫痪。

URL动态加密

这说的是防止恶意访问,有些爬虫或者刷量脚本会造成大量的请求访问你的接口,你更加不知道他会传什么参数给你,所以我们定义接口时一定要多加验证,因为不止是你的朋友调你的接口,敌人也有可能。

静态资源——CDN

CDN全称内容分发网络,是建立并覆盖在承载网之上,由分布在不同区域的边缘节点服务器群组成的分布式网络。

通俗的讲,就是把经常访问又费时的资源放在你附近的服务器上。

淘宝的图片访问,有98%的流量都走了CDN缓存。只有2%会回源到源站,节省了大量的服务器资源。

但是,如果在用户访问高峰期,图片内容大批量发生变化,大量用户的访问就会穿透cdn,对源站造成巨大的压力。

所以,对于图片这种静态资源,尽可能都放入CDN。

服务限流

在上面已有讲解,可分为前端限流和后端限流。

  • 前端:按钮禁用,ip黑名单
  • 后端:服务熔断,服务降级,权限验证

数据预热

可以采用定时任务(elastic-job)实时查询Druid,把热点数据放入redis缓存中。

思考一个问题:

比如现在库存只剩下1个了,我们高并发嘛,4个服务器一起查询了发现都是还有1个,那大家都觉得是自己抢到了,就都去扣库存,那结果就变成了-3,是的只有一个是真的抢到了,别的都是超卖的。咋办?

回答:

可以用CAS+LUA脚本实现。

Lua脚本是类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些Redis事务性的操作。这点是关键。

写一个脚本把判断库存扣减库存的操作都写在一个脚本丢给Redis去做,那到0了后面的都Return False了是吧,一个失败了你修改一个开关,直接挡住所有的请求。

削峰填谷

精通一个中间件会给你加分很多

消息队列已经逐渐成为企业IT系统内部通信的核心手段。

它具有低耦合、可靠投递、广播、流量控制、最终一致性等一系列功能,成为异步RPC的主要手段之一。

当今市面上有很多主流的消息中间件,如老牌的ActiveMQ、RabbitMQ,炙手可热的Kafka,阿里巴巴自主开发RocketMQ等。

最原始的MQ,生产者先将消息投递一个叫做「队列」的容器中,然后再从这个容器中取出消息,最后再转发给消费者,仅此而已。

更详细的MQ下文再续。


今天就学这么多,相信大家都对高并发系统有了初步的认识,面试官问起来也不至于无话可说,但是想要学好任重而道远,希望大家关注一条,带各位一起学习!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

2021技术圈未解之谜:“毒瘤”低代码?只会营销云原生?万事

发表于 2021-10-14

每一年的年初与年终,技术圈总会有各种各样的趋势预测和年末总结,在循环往复的圈子里吹捧着此起彼伏的技术方向。

从软件开发的层次上看,技术的发展变化不可谓不快:

  • 云计算从虚拟机到容器再到云原生,十年时间已经迎来送往了多个时代;
  • 数据库从关系型到 NoSQL 再到 NewSQL,商业型数据库、开源数据库互相印证一路前行;
  • 运维们也从“人肉运维”到 DevOps、AIOps、DevSecOps,引发了行业性的被替代危机。

但那些关于技术的迷思却仍未被真正解决:

  • 低代码到底是不是“业界毒瘤”?
  • 云原生到底是不是营销出来的概念?
  • 我们是不是真正需要微服务?
  • 为什么分布式数据库这么挣钱?
  • 技术人为什么不写代码去搞管理?
  • 机器学习究竟能不能代替人力?
  • 大前端这么多年,有什么新进展?
  • 程序员如何培养自己的个人品牌?
  • Java 老矣,尚能饭否?
  • 混沌工程的这只“猴儿”该怎么玩?
  • 业务架构何时才能摆脱“背锅侠”身份?
  • 音视频技术会是直播短视频时代的新风口吗?

关于这些技术的迷思,或许仍将持续到明年、后年甚至下一个十年。但如果能给你我一个去思考技术本源和业务发展的机会,或许你我的下一个十年将跑在日益内卷的行业前面。

这是稀土掘金技术社区发起首届稀土开发者大会的初衷,我们期望通过这样一场集「国际性」、「前瞻性」、「实践性」为一体的开发者大会,面向开发者社区新生中坚力量,旨在帮助研发团队骨干拓宽视野,实现跨越式成长。

本届稀土开发者大会策划的专题涵盖低代码、混沌工程、微服务、云原生、人工智能、音视频、数据库、大前端等热点技术方向,由来自阿里巴巴、百度、贝壳、bilibili、美团、腾讯、网易、知乎、字节跳动等互联网大厂具备技术影响力与洞察力的优质出品人负责议题把控。

目前稀土开发者大会 14 大专题出品人、讲师已全部集齐,免费预约通道 即将关闭,报名从速!更多大会内容请参考:大会官网。

除了为期两天的沉浸式内容体验,稀土开发者大会还为万千开发者朋友们准备了丰厚的奖品,报名参会的开发者将有机会参与抽奖,赢取丰厚大奖,敬请期待!

  • 如果你想获得更多一线大厂实践经验,欢迎报名!
  • 如果你想看到更多前沿趋势与技术洞察,欢迎报名!
  • 如果你想与更多行业专家、同行交流,欢迎报名!

上.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【云计算】历史的选择-基础设施即代码IaC

发表于 2021-10-14

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

之前几篇讲到了基础设施代码的工具,但并没有讲什么是IaC,这篇文章简单说一下。

一、历史的选择

随着软件迭代速度快,且复杂度的上升,组织结构和职能发生了变化。

  • 第一阶段,开发(Dev)和运维(Ops)组织分开,开发负责编写软件,运维负责管理硬件。→问题: 跨团队协作,开发和运维环境不一致,运维人员大多数部署通过手动完成,代码冲突、服务故障和停机频繁。
  • 第二阶段,上云计算平台,运维团队任务从硬件管理转移到编写软件管理,需要编写代码,这样开发和运维都需要编写代码,所以Dev和Ops合并,演化成DevOps。

DevOps自动化的目标是将软件交付过程自动化。所以落实到管理基础设施方面,也要尽可能多地通过代码来实现,减少点击网页或手动执行Shell命令的方式,即采用IaC(基础设施即代码)。

二、走进IaC

IaC(Infrastructure as Code)即基础设施即代码,核心思想为通过编写和执行代码来定义、部署、更新和销毁基础设施。

它是一种观念的转变,其将运维的各个工作都视为与软件相关,甚至一些明显针对硬件的工作,即将所有事物都在代码中进行管理,包括

  • 服务器
  • 数据库
  • 网络
  • 日志文件
  • 应用程序配置
  • 文档
  • 自动化测试,部署过程

2.1 IaC解决方案的价值

  • 版本控制(可Review)
  • 建立CI/CD自动化(不依赖于UI操作,减少人为错误)
  • 重复使用(减少时间上的浪费)
  • 环境切换控制(保持生产、预发、测试环境的一致性,平滑切换)
  • 团队成长和协作(文档分享)

总的来说,周而复始的手动管理基础设施,难免枯燥乏味,如此工作,既无创意又无挑战,使用Iac在改进我们工作的同时,让我们关注更有价值的事情。

2.2 IaC工具分类

IaC是一种思想,实现这种思想的工具有很多,大致分为五大类。

  • 专项脚本(为每一项任务写一个专项脚本,比如配置docker环境,配置web服务等。其维护难度相当大,可针对小规模、一次性的任务。)
  • 配置管理工具(Chef、Puppet、Absible、SaltStack属于此类,目的是在现有服务器上安装和管理软件,优于专项脚本,可用作大规模、分布式、统一管理的场景)
  • 服务器模板工具(Docker、Packer、Vagrant属于此类,使用容器镜像、虚拟机(VM)的方式拥有一个完全独立的服务器,然后在所有服务器上统一安装)
  • 编排工具(管理服务器模板工具创建虚拟机和容器等,具体实现包括K8s,Marathon/Mesos、AmazonElastic Container Service(Amazon ECS)、Docker Swarm和Nomad等等)
  • 服务开通工具(创建云资源,比如服务器,网络、防火墙设施、路由规则、负载均衡等等,具体实现包括Terraform、CloudFormation、OpenStack Heat、Pulumi等等)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

密码学系列之 加密货币中的scrypt算法 简介 scryp

发表于 2021-10-14

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

「欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章」

简介

为了抵御密码破解,科学家们想出了很多种方法,比如对密码进行混淆加盐操作,对密码进行模式变换和组合。但是这些算法逐渐被一些特制的ASIC处理器打败,这些ASIC处理器不做别的,就是专门来破解你的密码或者进行hash运算。

最有名的当然是比特币了,它使用的是为人诟病的POW算法,谁的算力高,谁就可以挖矿,这样就导致了大量无意义的矿机的产生,这些矿机什么都不能干,就算是用来算hash值。结果浪费了大量的电力。

普通人更是别想加入这个只有巨头才能拥有的赛道,如果你想用一个普通的PC机来挖矿,那么我估计你挖到矿的几率可能跟被陨石砸中差不多。

为了抵御这种CPU为主的密码加密方式,科学家们发明了很多其他的算法,比如需要占用大量内存的算法,因为内存不像CPU可以疯狂提速,所以限制了很多暴力破解的场景,今天要将的scrypt算法就是其中一种,该算法被应用到很多新的加密货币挖矿体系中,用以表示他们挖矿程序的公平性。

scrypt算法

scrypt是一种密码衍生算法,它是由Colin Percival创建的。使用scrypt算法来生成衍生key,需要用到大量的内存。scrypt算法在2016年作为RFC 7914标准发布。

密码衍生算法主要作用就是根据初始化的主密码来生成系列的衍生密码。这种算法主要是为了抵御暴力破解的攻击。通过增加密码生成的复杂度,同时也增加了暴力破解的难度。

但是和上面提到的原因一样,之前的password-based KDF,比如PBKDF2虽然提高了密码生成的遍历次数,但是它使用了很少的内存空间。所以很容易被简单的ASIC机器破解。scrypt算法就是为了解决这样的问题出现的。

scrypt算法详解

scrypt算法会生成非常大的伪随机数序列,这个随机数序列会被用在后续的key生成过程中,所以一般来说需要一个RAM来进行存储。这就是scrypt算法需要大内存的原因。

接下我们详细分析一下scrypt算法,标准的Scrypt算法需要输入8个参数,如下所示:

  • Passphrase: 要被hash的输入密码
  • Salt: 对密码保护的盐,防止彩虹表攻击
  • CostFactor (N): CPU/memory cost 参数,必须是2的指数(比如: 1024)
  • BlockSizeFactor (r): blocksize 参数
  • ParallelizationFactor (p): 并行参数
  • DesiredKeyLen (dkLen): 输出的衍生的key的长度
  • hLen: hash函数的输出长度
  • MFlen: Mix函数的输出长度

这个函数的输出就是DerivedKey。

首先我们需要生成一个expensiveSalt。首先得到blockSize:

1
ini复制代码blockSize = 128*BlockSizeFactor

然后使用PBKDF2生成p个blockSize,将这p个block组合成一个数组:

1
scss复制代码[B0...Bp−1] = PBKDF2HMAC-SHA256(Passphrase, Salt, 1, blockSize*ParallelizationFactor)

使用ROMix对得到的block进行混合:

1
2
css复制代码   for i ← 0 to p-1 do
Bi ← ROMix(Bi, CostFactor)

将B组合成新的expensiveSalt:

1
复制代码expensiveSalt ← B0∥B1∥B2∥ ... ∥Bp-1

接下来使用PBKDF2和新的salt生成最终的衍生key:

1
kotlin复制代码return PBKDF2HMAC-SHA256(Passphrase, expensiveSalt, 1, DesiredKeyLen);

下面是ROMix函数的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码Function ROMix(Block, Iterations)

Create Iterations copies of X
X ← Block
for i ← 0 to Iterations−1 do
Vi ← X
X ← BlockMix(X)

for i ← 0 to Iterations−1 do
j ← Integerify(X) mod Iterations
X ← BlockMix(X xor Vj)

return X

其中BlockMix的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vbnet复制代码Function BlockMix(B):

The block B is r 128-byte chunks (which is equivalent of 2r 64-byte chunks)
r ← Length(B) / 128;

Treat B as an array of 2r 64-byte chunks
[B0...B2r-1] ← B

X ← B2r−1
for i ← 0 to 2r−1 do
X ← Salsa20/8(X xor Bi) // Salsa20/8 hashes from 64-bytes to 64-bytes
Yi ← X

return ← Y0∥Y2∥...∥Y2r−2 ∥ Y1∥Y3∥...∥Y2r−1

scrypt的使用

Scrypt被用在很多新的POW的虚拟货币中,比如Tenebrix、 Litecoin 和 Dogecoin。感兴趣的朋友可以关注一下。

本文已收录于 www.flydean.com/42-scrypt/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一招搞定 Spring Boot 可视化监控! 阅读原文

发表于 2021-10-14

阅读原文

1、简介

当某个应用程序在生产环境中运行时,监控其运行状况是必要的。通过实时了解应用程序的运行状况,你能在问题出现之前得到警告,也可以在客户注意到问题之前解决问题。

在本文中,我们将创建一个Spring Boot应用程序,在Spring Actuator,Micrometer,Prometheus和Grafana的帮助下来监控系统。其中,Spring Actuator和Micrometer是Spring Boot App的一部分。

图片

简要说明了不同组件的目的:

  • Spring Actuator:在应用程序里提供众多 Web 接口,通过它们了解应用程序运行时的内部状况。有关更多信息,请参见Spring Boot 2.0中的Spring Boot Actuator。
  • Micrometer:为 Java 平台上的性能数据收集提供了一个通用的 API,它提供了多种度量指标类型(Timers、Guauges、Counters等),同时支持接入不同的监控系统,例如 Influxdb、Graphite、Prometheus 等。Spring Boot Actuator对此提供了支持。
  • Prometheus:一个时间序列数据库,用于收集指标。
  • Grafana:用于显示指标的仪表板。

下面,我们将分别介绍每个组件。本文中使用的代码存档在GitHub上。

2、创建示例应用

首先要做的是创建一个可以监控的应用程序。通过Spring Initializr,并添加Spring Boot Actuator,Prometheus和Spring Web依赖项, 我们创建了一个如下所示的Spring MVC应用程序。

Spring Boot 基础就不介绍了,推荐下这个实战教程:

github.com/javastacks/…

1
2
3
4
5
6
7
8
9
10
11
12
13
typescript复制代码@RestController
public class MetricsController {

@GetMapping("/endPoint1")
public String endPoint1() {
return "Metrics for endPoint1";
}

@GetMapping("/endPoint2")
public String endPoint2() {
return "Metrics for endPoint2";
    }
}

启动应用程序:

1
arduino复制代码$ mvn spring-boot:run

验证接口是否正常:

1
shell复制代码$ curl http://localhost:8080/endPoint1Metrics for endPoint1$ curl http://localhost:8080/endPoint2Metrics for endPoint2

验证Spring Actuator接口。为了使响应信息方便可读,我们通过python -mjson.tool来格式化信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bash复制代码$ curl http://localhost:8080/actuator | python -mjson.tool
...
{
"_links":{
"self":{
"href":"http://localhost:8080/actuator",
"templated":false
      },
"health":{
"href":"http://localhost:8080/actuator/health",
"templated":false
      },
"health-path":{
"href":"http://localhost:8080/actuator/health/{*path}",
"templated":true
      },
"info":{
"href":"http://localhost:8080/actuator/info",
"templated":false
      }
   }
}

默认情况下,会显示以上信息。除此之外,Spring Actuator可以提供更多信息,但是你需要启用它。为了启用Prometheus,你需要将以下信息添加到application.properties文件中。

1
ini复制代码management.endpoints.web.exposure.include=health,info,prometheus

重启应用程序,访问http://localhost:8080/actuator/prometheus从Prometheus拉取数据,返回了大量可用的指标信息。我们这里只显示输出的一小部分,因为它是一个很长的列表。

1
2
3
4
5
6
ini复制代码$ curl http://localhost:8080/actuator/prometheus
# HELP jvm_gc_pause_seconds Time spent in GC pause
# TYPE jvm_gc_pause_seconds summary
jvm_gc_pause_seconds_count{action="end of minor GC",cause="G1 Evacuation Pause",} 2.0
jvm_gc_pause_seconds_sum{action="end of minor GC",cause="G1 Evacuation Pause",} 0.009
...

如前所述,还需要Micrometer。Micrometer为最流行的监控系统提供了一个简单的仪表板,允许仪表化JVM应用,而无需关心是哪个供应商提供的指标。它的作用和SLF4J类似,只不过它关注的不是Logging(日志),而是application metrics(应用指标)。简而言之,它就是应用监控界的SLF4J。

Spring Boot Actuator为Micrometer提供了自动配置。Spring Boot2在spring-boot-actuator中引入了micrometer,对1.x的metrics进行了重构,另外支持对接的监控系统也更加丰富(Atlas、Datadog、Ganglia、Graphite、Influx、JMX、NewRelic、Prometheus、SignalFx、StatsD、Wavefront)。

更新后的application.properties文件如下所示:

1
ini复制代码management.endpoints.web.exposure.include=health,info,metrics,prometheus

重启应用程序,并从http://localhost:8080/actuator/metrics中检索数据。

1
2
3
4
5
6
7
8
bash复制代码$ curl http://localhost:8080/actuator/metrics | python -mjson.tool
...
{
"names": [
"http.server.requests",
"jvm.buffer.count",
"jvm.buffer.memory.used",
...

可以直接通过指标名来检索具体信息。例如,如果查询http.server.requests指标,可以按以下方式检索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
php复制代码$ curl http://localhost:8080/actuator/metrics/http.server.requests | python -mjson.tool
...
{
"name": "http.server.requests",
"description": null,
"baseUnit": "seconds",
"measurements": [
        {
"statistic": "COUNT",
"value": 3.0
        },
        {
"statistic": "TOTAL_TIME",
"value": 0.08918682
        },
...

3、添加Prometheus

Prometheus是Cloud Native Computing Foundation的一个开源监控系统。由于我们的应用程序中有一个/actuator/Prometheus端点来供 Prometheus 抓取数据,因此你现在可以配置Prometheus来监控你的Spring Boot应用程序。

Prometheus有几种安装方法,在本文中,我们将在Docker容器中运行Prometheus。

你需要创建一个prometheus.yml文件,以添加到Docker容器中。

1
2
3
4
5
6
7
8
vbnet复制代码global:
scrape_interval:15s

scrape_configs:
- job_name: 'myspringmetricsplanet'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['HOST:8080']
  • scrape_interval:Prometheus多久轮询一次应用程序的指标
  • job_name:轮询任务名称
  • metrics_path:指标的URL的路径
  • targets:主机名和端口号。使用时,替换HOST为主机的IP地址

如果在Linux上查找IP地址有困难,则可以使用以下命令:

1
shell复制代码$ ip -f inet -o addr show docker0 | awk '{print $4}' | cut -d '/' -f 1

启动Docker容器并将本地prometheus.yml文件,映射到Docker容器中的文件。

1
2
3
4
shell复制代码$ docker run \
    -p 9090:9090 \
    -v /path/to/prometheus.yml:/etc/prometheus/prometheus.yml \
    prom/prometheus

成功启动Docker容器后,首先验证Prometheus是否能够通过 http://localhost:9090/targets收集数据。

图片

如上图所示,我们遇到context deadline exceeded错误,造成Prometheus无法访问主机上运行的Spring Boot应用程序。如何解决呢?

可以通过将Docker容器添加到你的主机网络来解决此错误,这将使Prometheus能够访问Spring Boot应用程序。

1
2
3
4
5
6
shell复制代码$ docker run \
    --name prometheus \
    --network host \
    -v /path/to/prometheus.yml:/etc/prometheus/prometheus.yml \
    -d \
    prom/prometheus

再次验证,状态指示为UP。

图片

现在可以显示Prometheus指标。通过访问http://localhost:9090/graph,在搜索框中输入http_server_requests_seconds_max并单击“执行”按钮,将为你提供请求期间的最长执行时间。

图片

4、添加Grafana

最后添加的组件是Grafana。尽管Prometheus可以显示指标,但Grafana可以帮助你在更精美的仪表板中显示指标。Grafana也支持几种安装方式,在本文中,我们也将在Docker容器中运行它。

1
css复制代码$ docker run --name grafana -d -p 3000:3000 grafana/grafana

点击 http://localhost:3000/,就可以访问Grafana。

图片

默认的用户名/密码为admin/admin。单击“登录”按钮后,你需要更改默认密码。

图片

接下来要做的是添加一个数据源。单击左侧边栏中的“配置”图标,然后选择“Data Sources(数据源)”。

图片

单击Add data source(添加数据源)按钮。

图片

Prometheus在列表的顶部,选择Prometheus。

图片

填写可访问Prometheus的URL,将HTTP Access设置为Browser,然后单击页面底部的Save&Test按钮。

关注公众号,学习更多 Java 干货!图片

图片

一切正常后,将显示绿色的通知标语,指示数据源正在工作。

图片

现在该创建仪表板了。你可以自定义一个,但也可以使用开源的仪表板。用于显示Spring Boot指标的一种常用仪表板是JVM仪表板。

在左侧边栏中,点击+号,然后选择导入。

图片

输入JVM仪表板的URL grafana.com/grafana/das…

图片

为仪表板输入一个有意义的名称(例如MySpringMonitoringPlanet),选择Prometheus作为数据源,然后单击Import按钮。

图片

到目前为止,你就可以使用一个很酷的Grafana仪表板。

图片

也可以将自定义面板添加到仪表板。在仪表板顶部,单击Add panel(添加面板)图标。

图片

单击Add new panel(添加新面板)。

图片

在Metrics 字段中,输入http_server_requests_seconds_max,在右侧栏中的Panel title字段中,可以输入面板的名称。

图片

最后,单击右上角的Apply 按钮,你的面板将添加到仪表板。不要忘记保存仪表板。

为应用程序设置一些负载,并查看仪表板上的http_server_requests_seconds_max指标发生了什么。

1
shell复制代码$ watch -n 5 curl http://localhost:8080/endPoint1$ watch -n 10 curl http://localhost:8080/endPoint2

图片

5、结论

在本文中,我们学习了如何为Spring Boot应用程序添加一些基本监控。这非常容易,只需要通过将Spring Actuator,Micrometer,Prometheus和Grafana组合使用。

当然,这只是一个起点,但是从这里开始,你可以为Spring Boot应用程序扩展和配置更多、更具体的指标。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java经典基础毕设项目--学生信息管理系统详细设计【附源码

发表于 2021-10-14

本文正在参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

临近学期结束,还是毕业设计,你还在做java程序网络编程,期末作业,老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里,你想解决的问题,在这都能满足你的需求。原始Jsp、SSM、SpringBoot、以及HTML+CSS+JS页面设计, web大学生网页设计作业源码等等都可以参考得到解决。话不多说直接拿一个学生信息管理系统来举例

)​

摘要设计:

本次系统设计以方便快捷和安全为出发点,放弃传统的人工记录对学生信息管理的缺陷和不足, 采用全新的方式使学校对学生信息进行存储和维护,增加管理效率。本系统大体分为管理员登录管理后台、学生登录选课及对老师授课等三大模块、并且赋予了管理员很多功能来操作这个系统,包括:学生管理,老师管理,选课管理,密码修改等功能;为学生用户提供了选修改课程查询 、选择、密码修改等功能。通过这些功能模块的设计,满足了老师对学生的信息进行管控所需的功能。系统采用 B/S 三层结构,对动态页面的制作采用了 JSP技术,为了实现管理系统的安全可靠以及对有些代码可以进行重复使用考虑, 对程序的重要代码进行封装时采用 Java Bean。本系统贯彻以人为本的思想,实用性高。

系统功能概述:

主要模块设计如下:

使用Shiro权限管理框架,实现登录验证和登录信息的储存,根据不同的登录账户,分发权限角色,对不同页面url进行角色设置。

管理员可对 教师信息、学生信息、课程信息 进行 增删改查 操作,管理员账户,可以重置非管理员账户的密码。

课程管理:当课程已经有学生选课成功时,将不能删除学生管理:添加学生信息时,其信息也会添加到登录表中教师管理:同上

账户密码重置:

教师登陆后,可以获取其,教授的课程列表,并可以给已经选择该课程的同学打分无法对已经给完分的同学进行二次操作

学生登录后,根据学生信息,获取其已经选择的课程,和已经修完的课程

所有课程: 在这里选修课程,选好后,将会自动跳转到已选课程选项

已选课程: 这里显示的是,还没修完的课程,也就是老师还没给成绩,由于还没有给成绩,所以这里可以进行退课操作

已修课程: 显示已经修完,老师已经给成绩的课程修改密码:

主要功能截图:

用户登录: 用户登录是选择角色进行登录:管理员、教师、学生

)​

系统主页: 管理员登录后具体功能模块可对 教师信息、学生信息、课程信息 进行 增删改查 操作,管理员账户,可以重置非管理员账户的密码。

)​

课程管理: 课程列表管理和添加课程等具体操作

)​

添加录入课程信息

)​

学生管理: 学生列表管理和添加学生等具体操作

)​

添加学生信息)​

教师管理:

)​

文件上传下载:

文件列表和下载文件

)​

文件上传

)​

账号相关:

)​

教师登录后主要页面展示:

查看授课列表

)​

查看该课程学生信息

)​

给学生成绩打分

​

学生用户登录:

根据学生信息,获取其已经选择的课程,和已经修完的课程)​

主要代码展示:

登录相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
java复制代码package com.system.controller;

import com.system.exception.CustomException;
import com.system.po.Userlogin;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
* Created by 李杨勇.
*/
@Controller
public class LoginController {

//登录跳转
@RequestMapping(value = "/login", method = {RequestMethod.GET})
public String loginUI() throws Exception {
return "../../login";
}

//登录表单处理
@RequestMapping(value = "/login", method = {RequestMethod.POST})
public String login(Userlogin userlogin) throws Exception {

//Shiro实现登录
UsernamePasswordToken token = new UsernamePasswordToken(userlogin.getUsername(),
userlogin.getPassword());
Subject subject = SecurityUtils.getSubject();

//如果获取不到用户名就是登录失败,但登录失败的话,会直接抛出异常
subject.login(token);
if (subject.hasRole("admin")&userlogin.getRole()==0) {
return "redirect:/admin/showStudent";
} else if (subject.hasRole("teacher")&userlogin.getRole()==1) {
return "redirect:/teacher/showCourse";
} else if (subject.hasRole("student")&userlogin.getRole()==2) {
return "redirect:/student/showCourse";
}else throw new CustomException("请选择正确的身份登陆");


}

}

文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
java复制代码package com.system.controller;

import com.system.po.FileVO;
import com.system.service.FileService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.UUID;

/**
* 文件上传下载
*/
@Controller
@RequestMapping("/file")
public class FileController {


@Resource(name = "fileServiceImpl")
private FileService fileService;

@RequestMapping("/upload")
public String fileUpload(@RequestParam MultipartFile file, FileVO filevo, HttpServletRequest request) throws IOException {
//上传路径保存设置
// 把文件写到磁盘
String fileName = file.getOriginalFilename();
String[] str = fileName.split("\.");
String uuid = UUID.randomUUID().toString().replaceAll("-","");
String headPath = "E://upload/" + uuid+ "."+str[str.length-1];
File dest = new File(headPath);
file.transferTo(dest);
filevo.setFileID(uuid);
filevo.setFilePath(headPath);
filevo.setUserID(null);
try {
fileService.save(filevo);
} catch (Exception e) {
e.printStackTrace();
}
return "redirect:/admin/showFile";
}


@RequestMapping("/downFile")
public void down(HttpServletRequest request, HttpServletResponse response,String fileID) throws Exception{
FileVO fileVO = fileService.findById(fileID);
String fileName = fileVO.getFilePath();
String[] str = fileName.split("\.");
InputStream bis = new BufferedInputStream(new FileInputStream(new File(fileName)));
String filename = fileVO.getFileName()+"\."+str[str.length-1];
filename = URLEncoder.encode(filename,"UTF-8");
response.addHeader("Content-Disposition", "attachment;filename=" + filename);
response.setContentType("multipart/form-data");
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
int len = 0;
while((len = bis.read()) != -1){
out.write(len);
out.flush();
}
out.close();
}

}

异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码package com.system.exception;

/**
* 系统自定义异常类,针对预期异常,需要在程序中抛出此类的异常
*/
public class CustomException extends Exception {

//异常信息
public String message;

public CustomException(String message) {
super(message);
this.message=message;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

主要数据库设计:

主要数据表有:专业表、课程表、文件信息表、角色表、学生选课表、老师表、学生表等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
sql复制代码CREATE TABLE `college` (
`collegeID` int(11) NOT NULL,
`collegeName` varchar(200) NOT NULL COMMENT '课程名',
PRIMARY KEY (`collegeID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `college` */

insert into `college`(`collegeID`,`collegeName`) values

(1,'计算机系'),

(2,'设计系'),

(3,'财经系');

/*Table structure for table `course` */

DROP TABLE IF EXISTS `course`;

CREATE TABLE `course` (
`courseID` int(11) NOT NULL,
`courseName` varchar(200) NOT NULL COMMENT '课程名称',
`teacherID` int(11) NOT NULL,
`courseTime` varchar(200) DEFAULT NULL COMMENT '开课时间',
`classRoom` varchar(200) DEFAULT NULL COMMENT '开课地点',
`courseWeek` int(200) DEFAULT NULL COMMENT '学时',
`courseType` varchar(20) DEFAULT NULL COMMENT '课程类型',
`collegeID` int(11) NOT NULL COMMENT '所属院系',
`score` int(11) NOT NULL COMMENT '学分',
PRIMARY KEY (`courseID`),
KEY `collegeID` (`collegeID`),
KEY `teacherID` (`teacherID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `course` */

insert into `course`(`courseID`,`courseName`,`teacherID`,`courseTime`,`classRoom`,`courseWeek`,`courseType`,`collegeID`,`score`) values

(1,'C语言程序设计',1001,'周二','科401',18,'必修课',1,3),

(2,'Python爬虫技巧',1001,'周四','X402',18,'必修课',1,3),

(3,'数据结构',1001,'周四','科401',18,'必修课',1,2),

(4,'Java程序设计',1002,'周五','科401',18,'必修课',1,2),

(5,'英语',1002,'周四','X302',18,'必修课',2,2),

(6,'服装设计',1003,'周一','科401',18,'选修课',2,2);

/*Table structure for table `role` */

DROP TABLE IF EXISTS `role`;

CREATE TABLE `role` (
`roleID` int(11) NOT NULL,
`roleName` varchar(20) NOT NULL,
`permissions` varchar(255) DEFAULT NULL COMMENT '权限',
PRIMARY KEY (`roleID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `role` */

insert into `role`(`roleID`,`roleName`,`permissions`) values

(0,'admin',NULL),

(1,'teacher',NULL),

(2,'student',NULL);

/*Table structure for table `selectedcourse` */

DROP TABLE IF EXISTS `selectedcourse`;

CREATE TABLE `selectedcourse` (
`courseID` int(11) NOT NULL,
`studentID` int(11) NOT NULL,
`mark` int(11) DEFAULT NULL COMMENT '成绩',
KEY `courseID` (`courseID`),
KEY `studentID` (`studentID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `selectedcourse` */

insert into `selectedcourse`(`courseID`,`studentID`,`mark`) values

(2,10001,12),

(1,10001,95),

(1,10002,66),

(2,10003,99),

(5,10001,NULL),

(3,10001,NULL),

(1,10003,NULL),

(4,10003,NULL);

/*Table structure for table `student` */

DROP TABLE IF EXISTS `student`;

CREATE TABLE `student` (
`userID` int(11) NOT NULL AUTO_INCREMENT,
`userName` varchar(200) NOT NULL,
`sex` varchar(20) DEFAULT NULL,
`birthYear` date DEFAULT NULL COMMENT '出生日期',
`grade` date DEFAULT NULL COMMENT '入学时间',
`collegeID` int(11) NOT NULL COMMENT '院系id',
PRIMARY KEY (`userID`),
KEY `collegeID` (`collegeID`)
) ENGINE=InnoDB AUTO_INCREMENT=10008 DEFAULT CHARSET=utf8;

/*Data for the table `student` */

insert into `student`(`userID`,`userName`,`sex`,`birthYear`,`grade`,`collegeID`) values

(9999,'mike1','男','1996-09-03','2019-11-13',3),

(10001,'小红','男','2020-03-02','2020-03-02',1),

(10002,'小绿','男','2020-03-10','2020-03-10',1),

(10003,'小陈','女','1996-09-02','2015-09-02',2),

(10005,'小左','女','1996-09-02','2015-09-02',2),

(10007,'MIke','男','1996-09-02','2015-09-02',2);

/*Table structure for table `teacher` */

DROP TABLE IF EXISTS `teacher`;

CREATE TABLE `teacher` (
`userID` int(11) NOT NULL AUTO_INCREMENT,
`userName` varchar(200) NOT NULL,
`sex` varchar(20) DEFAULT NULL,
`birthYear` date NOT NULL,
`degree` varchar(20) DEFAULT NULL COMMENT '学历',
`title` varchar(255) DEFAULT NULL COMMENT '职称',
`grade` date DEFAULT NULL COMMENT '入职时间',
`collegeID` int(11) NOT NULL COMMENT '院系',
PRIMARY KEY (`userID`),
KEY `collegeID` (`collegeID`)
) ENGINE=InnoDB AUTO_INCREMENT=1004 DEFAULT CHARSET=utf8;

/*Data for the table `teacher` */

insert into `teacher`(`userID`,`userName`,`sex`,`birthYear`,`degree`,`title`,`grade`,`collegeID`) values

(1001,'刘老师','女','1990-03-08','硕士','副教授','2015-09-02',2),

(1002,'张老师','女','1996-09-02','博士','讲师','2015-09-02',1),

(1003,'软老师','女','1996-09-02','硕士','助教','2017-07-07',1);

/*Table structure for table `userlogin` */

DROP TABLE IF EXISTS `userlogin`;

CREATE TABLE `userlogin` (
`userID` int(11) NOT NULL AUTO_INCREMENT,
`userName` varchar(200) NOT NULL,
`password` varchar(200) NOT NULL,
`role` int(11) NOT NULL DEFAULT '2' COMMENT '角色权限',
PRIMARY KEY (`userID`),
KEY `role` (`role`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8;

/*Data for the table `userlogin` */

insert into `userlogin`(`userID`,`userName`,`password`,`role`) values

(1,'admin','123',0),

(10,'10003','123',2),

(11,'10005','123',2),

(14,'1001','123',1),

(15,'1002','123',1),

(16,'1003','123',1),

(20,'9999','123',2),

(21,'10001','123',2),

(22,'10002','123',2);

论文结构目录设计 :

一、 绪论 4

1.1 研究背景 4

1.2 系统设计概述 4

1.3 研究的内容 5

二、相关技术介绍 5

2.1 spring 5

2.2 Spring MVC 6

2.3 mybatis 7

2.4 JSP技术 7

2.5 jQuery 8

2.6 Mysql 8

三、需求分析和可行性 11

3.1 系统功能概述 11

3.2 系统运行环境 11

3.3 技术设计 12

3.4 社会可行性 12

3.5 安全性可行性 12

3.6 经济可行性 12

3.7 法律可行性 12

四、系统设计 13

4.1 系统模式架构 13

4.2系统层次架构 13

4.3系统功能详情设计 14

4.4数据流图 14

4.5源码架构 15

五、系统实现 17

5.1 程序主要类 17

5.1.1用户登录类 17

5.1.2教师信息类 17

5.1.3角色权限类 17

5.1.4课程信息类 17

5.1.5学生信息类 18

5.1.6学生选课类 18

5.2系统功能主要实现模块截图 18

5.2.1关键代码实现 18

5.2.2部分功能截图 23

六、数据库设计 26

6.1表基本设计 26

6.2数据库三范式要求: 26

6.3数据库表ER图 27

6.4用户登录表设计 27

6.5老师表设计 28

6.6学生信息表设计 29

6.7学生选课表设计 30

6.8角色权限表设计 31

6.9课程表设计 31

6.10院系表设计 32

七、开发心得体会 33

八、测试实例 33

九、参考献文 34

好了,今天就到这儿吧,小伙伴们点赞、收藏、评论走起呀,下期见~~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

前端实现很哇塞的浏览器端扫码功能🌟

发表于 2021-10-14

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

背景

不久前我做了关于获取浏览器摄像头并扫码识别的功能,本文中梳理了涉及到知识点及具体代码实现,整理成此篇文章内容。

本文主要介绍,通过使用基于 vue 技术栈的前端开发技术,在浏览器端调起摄像头 📷,并进行扫码识别功能,对识别到的二维码进行跳转或其他操作处理。本文内容分为背景介绍、实现效果、技术简介、代码实现、总结等部分组成。

实现效果

本实例中主要有两个页面首页和扫码页,具体实现效果如下图所示。

  • 首页:点击 SCAN QRCODE 按钮,进入到扫码页。
  • 扫码页:首次进入时,或弹出 获取摄像头访问权限的系统提示框,点击允许访问,页面开始加载摄像头数据并开始进行二维码捕获拾取,若捕获到二维码,开始进行二维码解析,解析成功后加载识别成功弹窗。

📸 在线体验:dragonir.github.io/h5-scan-qrc…

📌 提示:需要在有摄像头设备的浏览器中竖屏访问。手机横竖屏检测小知识可前往我的另一篇文章《五十音小游戏中的前端知识》 中进行了解。

技术简介

WebRTC API

WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间 点对点(Peer-to-Peer) 的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC 包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建 点对点(Peer-to-Peer) 的数据分享和电话会议成为可能。

三个主要接口:

  • MediaStream:能够通过设备的摄像头及话筒获得视频、音频的同步流。
  • RTCPeerConnection:是 WebRTC 用于构建点对点之间稳定、高效的流传输的组件。
  • RTCDataChannel:使得浏览器之间建立一个高吞吐量、低延时的信道,用于传输任意数据。

🔗 前往 MDN 深入学习:WebRTC_API

WebRTC adapter

虽然 WebRTC 规范已经相对健全稳固了,但是并不是所有的浏览器都实现了它所有的功能,有些浏览器需要在一些或者所有的 WebRTC API上添加前缀才能正常使用。

WebRTC 组织在 github 上提供了一个 WebRTC适配器(WebRTC adapter) 来解决在不同浏览器上实现 WebRTC 的兼容性问题。这个适配器是一个 JavaScript垫片,它可以让你根据 WebRTC 规范描述的那样去写代码,在所有支持 WebRTC 的浏览器中不用去写前缀或者其他兼容性解决方法。

🔗 前往 MDN 深入学习:WebRTC adapter

核心的API navigator.mediaDevices.getUserMedia

网页调用摄像头需要调用 getUserMedia API,MediaDevices.getUserMedia() 会提示用户给予使用媒体输入的许可,媒体输入会产生一个 MediaStream,里面包含了请求的媒体类型的轨道。此流可以包含一个视频轨道(来自硬件或者虚拟视频源,比如相机、视频采集设备和屏幕共享服务等等)、一个音频轨道(同样来自硬件或虚拟音频源,比如麦克风、A/D转换器 等等),也可能是其它轨道类型。

它返回一个 Promise 对象,成功后会 resolve 回调一个 MediaStream对象;若用户拒绝了使用权限,或者需要的媒体源不可用,promise 会 reject 回调一个 PermissionDeniedError 或者 NotFoundError 。(返回的 promise对象 可能既不会 resolve 也不会 reject,因为用户不是必须选择允许或拒绝。)

通常可以使用 navigator.mediaDevices 来获取 MediaDevices ,例如:

1
2
3
4
5
6
7
js复制代码navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
// 使用这个stream
})
.catch(function(err) {
// 处理error
})

🔗 前往 MDN 深入学习:navigator.mediaDevices.getUserMedia

二维码解析库 JSQR

jsQR 是一个纯 JavaScript 二维码解析库,该库读取原始图像或者是摄像头,并将定位,提取和解析其中的任何 QR码。

如果要使用 jsQR 扫描网络摄像头流,则需要 ImageData 从视频流中提取,然后可以将其传递给 jsQR。

jsQR 导出一个方法,该方法接受 4 个参数,分别是解码的 图像数据,宽、高 以及 可选的对象 进一步配置扫描行为。

imageData:格式为 [r0, g0, b0, a0, r1, g1, b1, a1, ...] 的 Uint8ClampedArray( 8位无符号整型固定数组) 的 rgba 像素值。

1
2
3
4
js复制代码const code = jsQR(imageData, width, height, options);
if (code) {
console.log('找到二维码!', code);
}

🔗 前往 github 深入了解:jsQR

代码实现

流程

整个扫码流程如下图所示:页面初始化,先检查浏览器是否支持 mediaDevices 相关API,浏览器进行调去摄像头,调用失败,就执行失败回调;调用成功,进行捕获视频流,然后进行扫码识别,没有扫瞄到可识别的二维码就继续扫描,扫码成功后绘制扫描成功图案并进行成功回调。

下文内容对流程进行拆分,分别实现对应的功能。

扫码组件 Scaner

页面结构

我们先看下页面结构,主要由 4 部分组成:

  • 提示框。
  • 扫码框。
  • video:展示摄像头捕获视频流。
  • canvas: 绘制视频帧,用于二维码识别。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
html复制代码<template>
<div class="scaner" ref="scaner">
<!-- 提示框:用于在不兼容的浏览器中显示提示语 -->
<div class="banner" v-if="showBanner">
<i class="close_icon" @click="() => showBanner = false"></i>
<p class="text">若当前浏览器无法扫码,请切换其他浏览器尝试</p>
</div>
<!-- 扫码框:显示扫码动画 -->
<div class="cover">
<p class="line"></p>
<span class="square top left"></span>
<span class="square top right"></span>
<span class="square bottom right"></span>
<span class="square bottom left"></span>
<p class="tips">将二维码放入框内,即可自动扫描</p>
</div>
<!-- 视频流显示 -->
<video
v-show="showPlay"
class="source"
ref="video"
:width="videoWH.width"
:height="videoWH.height"
controls
></video>
<canvas v-show="!showPlay" ref="canvas" />
<button v-show="showPlay" @click="run">开始</button>
</div>
</template>

方法:绘制

  • 画线。
  • 画框(用于扫码成功后绘制矩形图形)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码// 画线
drawLine (begin, end) {
this.canvas.beginPath();
this.canvas.moveTo(begin.x, begin.y);
this.canvas.lineTo(end.x, end.y);
this.canvas.lineWidth = this.lineWidth;
this.canvas.strokeStyle = this.lineColor;
this.canvas.stroke();
},
// 画框
drawBox (location) {
if (this.drawOnfound) {
this.drawLine(location.topLeftCorner, location.topRightCorner);
this.drawLine(location.topRightCorner, location.bottomRightCorner);
this.drawLine(location.bottomRightCorner, location.bottomLeftCorner);
this.drawLine(location.bottomLeftCorner, location.topLeftCorner);
}
},

方法:初始化

  • 检查是否支持。
  • 调起摄像头。
  • 成功失败处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
js复制代码// 初始化
setup () {
// 判断了浏览器是否支持挂载在MediaDevices.getUserMedia()的方法
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
this.previousCode = null;
this.parity = 0;
this.active = true;
this.canvas = this.$refs.canvas.getContext("2d");
// 获取摄像头模式,默认设置是后置摄像头
const facingMode = this.useBackCamera ? { exact: 'environment' } : 'user';
// 摄像头视频处理
const handleSuccess = stream => {
if (this.$refs.video.srcObject !== undefined) {
this.$refs.video.srcObject = stream;
} else if (window.videoEl.mozSrcObject !== undefined) {
this.$refs.video.mozSrcObject = stream;
} else if (window.URL.createObjectURL) {
this.$refs.video.src = window.URL.createObjectURL(stream);
} else if (window.webkitURL) {
this.$refs.video.src = window.webkitURL.createObjectURL(stream);
} else {
this.$refs.video.src = stream;
}
// 不希望用户来拖动进度条的话,可以直接使用playsinline属性,webkit-playsinline属性
this.$refs.video.playsInline = true;
const playPromise = this.$refs.video.play();
playPromise.catch(() => (this.showPlay = true));
// 视频开始播放时进行周期性扫码识别
playPromise.then(this.run);
};
// 捕获视频流
navigator.mediaDevices
.getUserMedia({ video: { facingMode } })
.then(handleSuccess)
.catch(() => {
navigator.mediaDevices
.getUserMedia({ video: true })
.then(handleSuccess)
.catch(error => {
this.$emit("error-captured", error);
});
});
}
},

方法:周期性扫描

1
2
3
4
5
6
js复制代码run () {
if (this.active) {
// 浏览器在下次重绘前循环调用扫码方法
requestAnimationFrame(this.tick);
}
},

方法:成功回调

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码// 二维码识别成功事件处理
found (code) {
if (this.previousCode !== code) {
this.previousCode = code;
} else if (this.previousCode === code) {
this.parity += 1;
}
if (this.parity > 2) {
this.active = this.stopOnScanned ? false : true;
this.parity = 0;
this.$emit("code-scanned", code);
}
},

方法:停止

1
2
3
4
5
6
7
8
js复制代码
// 完全停止
fullStop () {
if (this.$refs.video && this.$refs.video.srcObject) {
// 停止视频流序列轨道
this.$refs.video.srcObject.getTracks().forEach(t => t.stop());
}
}

方法:扫描

  • 绘制视频帧。
  • 扫码识别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
js复制代码// 周期性扫码识别
tick () {
// 视频处于准备阶段,并且已经加载足够的数据
if (this.$refs.video && this.$refs.video.readyState === this.$refs.video.HAVE_ENOUGH_DATA) {
// 开始在画布上绘制视频
this.$refs.canvas.height = this.videoWH.height;
this.$refs.canvas.width = this.videoWH.width;
this.canvas.drawImage(this.$refs.video, 0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
// getImageData() 复制画布上制定矩形的像素数据
const imageData = this.canvas.getImageData(0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
let code = false;
try {
// 识别二维码
code = jsQR(imageData.data, imageData.width, imageData.height);
} catch (e) {
console.error(e);
}
// 如果识别出二维码,绘制矩形框
if (code) {
this.drawBox(code.location);
// 识别成功事件处理
this.found(code.data);
}
}
this.run();
},

父组件

Scaner 的父组件主要加载页面,并展示 Scaner 扫码结果的回调。

页面结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
html复制代码<template>
<div class="scan">
<!-- 页面导航栏 -->
<div class="nav">
<a class="close" @click="() => $router.go(-1)"></a>
<p class="title">Scan QRcode</p>
</div>
<div class="scroll-container">
<!-- 扫码子组件 -->
<Scaner
v-on:code-scanned="codeScanned"
v-on:error-captured="errorCaptured"
:stop-on-scanned="true"
:draw-on-found="true"
:responsive="false"
/>
</div>
</div>
</template>

父组件方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
js复制代码import Scaner from '../components/Scaner';

export default {
name: 'Scan',
components: {
Scaner
},
data () {
return {
errorMessage: "",
scanned: ""
}
},
methods: {
codeScanned(code) {
this.scanned = code;
setTimeout(() => {
alert(`扫码解析成功: ${code}`);
}, 200)
},
errorCaptured(error) {
switch (error.name) {
case "NotAllowedError":
this.errorMessage = "Camera permission denied.";
break;
case "NotFoundError":
this.errorMessage = "There is no connected camera.";
break;
case "NotSupportedError":
this.errorMessage =
"Seems like this page is served in non-secure context.";
break;
case "NotReadableError":
this.errorMessage =
"Couldn't access your camera. Is it already in use?";
break;
case "OverconstrainedError":
this.errorMessage = "Constraints don't match any installed camera.";
break;
default:
this.errorMessage = "UNKNOWN ERROR: " + error.message;
}
console.error(this.errorMessage);
alert('相机调用失败');
}
},
mounted () {
var str = navigator.userAgent.toLowerCase();
var ver = str.match(/cpu iphone os (.*?) like mac os/);
// 经测试 iOS 10.3.3以下系统无法成功调用相机摄像头
if (ver && ver[1].replace(/_/g,".") < '10.3.3') {
alert('相机调用失败');
}
}

完整代码

🔗 github: github.com/dragonir/h5…

总结

应用扩展

我觉得以下几个功能都是可以通过浏览器调用摄像头并扫描识别来实现的,大家觉得还有哪些 很哇塞🌟 的功能应用可以通过浏览器端扫码实现 😂?

  • 🌏 链接跳转。
  • 🛒 价格查询。
  • 🔒 登录认证。
  • 📂 文件下载。

兼容性

  • ❗ 即使使用了 adapter,getUserMedia API 在部分浏览器中也存在不支持的。
  • ❗ 低版本浏览器(如 iOS 10.3 以下)、Android 小众浏览器(如 IQOO 自带浏览器)不兼容。
  • ❗ QQ、微信 内置浏览器无法调用。

参考资料

  • [1]. Taking still photos with WebRTC
  • [2]. Choosing cameras in JavaScript with the mediaDevices API
  • [3]. 如何使用JavaScript访问设备前后摄像头

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

桀骜不驯的代码又搞事情?我找来 10 个开源项目帮你驯服它们

发表于 2021-10-14

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

你的编程能力从什么时候开始突飞猛进?

看到这个问题,我陷入了沉思。我开始回忆过往的编程经历…貌似我的编程能力没有突飞猛进过!但如果说被骂和嫌弃的次数变少就算是进步的话,那么我“突飞猛进”的转折点就应该是:自从我看得懂代码的报错和异常,不拿白痴的问题找我师傅后就很少挨骂了。从那以后我就开始自己解决代码的 BUG 和问题,然后被“委以重任”开始独立开发模块和负责项目了。自此我也算是吃上了程序员这口饭,感谢师傅没有放弃我~

如果说代码是一匹桀骜不驯的野马,一开始要先认识它不能“指鹿为马”,也就是搞懂语法能看懂代码。然后多数情况下运行代码就会出错,所以需要先驯服它。刚开始可以先:

  1. 尝试定位问题,把关键步骤的变量输出出来
  2. 学会用 IDE 调试代码,弄清楚每一步的值
  3. 拿着异常的关键字:错误代码、异常类别,去问搜索引擎

但如果是线上运行着的代码、需要在服务器上找问题、需要在真机上调试,那上面的方法就很难搞了。刚找到通往成功的钥匙,就发现锁换了。

不怕!今天 HelloGitHub 带来的开源项目,助你全方位搞定桀骜不驯的代码,让 BUG 无处遁形。

更好用的调试工具

编程语言都自带代码调试工具(Debugger),比如:C/C++(GDB)、Python(pdb)、Java(JDB)、LLDB 等,这些工具常用于服务器端调试代码。下面这些开源项目比自带的调试工具:操作更加简单、信息展示更丰富、更加人性化,好用的调试工具能够更加方便地调试代码,定位问题。

注意:不要在服务器上调试线上代码,风险巨大!

1、dbg-macro(C++)

Star:1.7k|语言:C++

兼容 C++11 的 header-only 单个头文件的打日志方式调试库。它提供了比 printf 和 std::cout 更好的宏函数。特点:

  • 美观的彩色输出
  • 支持基础类型和 STL 容器类型的输出
  • 除了基本信息外,还输出变量名和类型

github.com/sharkdp/dbg…

另外 Python 也有类似功能的库:PySnooper

github.com/cool-RR/PyS…

2、pudb(Python)

Star:2.2k|语言:Python

支持代码高亮的 Python 命令行可视化调试器。栈、断点、变量动态实时更新,支持 VIM 的操作方式,还兼容 pdb 的某些命令,更容易上手。

github.com/inducer/pud…

3、pylane(Python)

Star:292|语言:Python

Python 进程注入和调试工具。可以直接进入正在运行的 Python 进程,动态注入或执行代码片段。

github.com/NtesEyes/py…

4、arthas(Java)

Star:27.3k|语言:Java

简单易用的命令行 Java 诊断工具。支持 JVM 进程和资源监控,还能展示 GC、JDK 版本等信息,无需增加代码就可以加入日志,帮助快速定位问题。当线上出现了奇怪的异常时,无需发版就能截获运行时的数据,包括参数、返回值、异常、耗时等信息。

github.com/alibaba/art…

5、delve(Go)

Star:17.1k|语言:Go

简单且强大的 Go 源码调试器。支持线程和 goroutine,功能齐全。

github.com/go-delve/de…

目前很多 IDE 都支持远程调试啦(基于上述项目实现),这里就不再赘述了,主要是我没用过 IDE 远程调试😅。

移动端的调试工具

PC 端最常用的 Web 调试工具应该是 Chrome 浏览器的开发工具啦。

下面介绍的开源项目帮你开启移动端的“开发者工具”,全图挂那种!

不用盲猜啦,全图的感觉真爽。

6、eruda(手机网页)

Star:11.2k|语言:JavaScript

专为手机网页设计的前端调试工具。类似手机端迷你版开发者模式,可用于在手机端调试页面。主要功能包括:显示 console 日志、检查元素状态、捕获 XHR 请求、显示本地存储和 Cookie 等信息

github.com/liriliri/er…

7、FLEX(iOS)

Star:12.6k|语言:Objective-C

iOS 应用上的调试工具。通过它你几乎可以查看应用的所有状态并修改任意组件的数值。比如:调整布局、浏览文件、查看网络请求历史、本地数据库等

github.com/FLEXTool/FL…

8、DoraemonKit

Star:18k|语言:Java

支持多种客户端的调试工具。它功能强大、接入方便、便于扩展,能够让你在 Android、iOS、小程序等移动端应用,快速接入常用的调试、辅助开发、性能检测、视觉辅助等工具。

github.com/didi/Doraem…

9、insomnia

Star:18.1k|语言:JavaScript

支持 API、GraphQL、REST、gRPC 的调试工具,请求接口的桌面应用。不仅有简约漂亮的界面,还支持 Windows、Linux、macOS 主流操作系统。

github.com/Kong/insomn…

又多了一个奇怪的工具

调试代码也好,应用开挂也罢。都是为了找到问题原因,然后解决问题。

那么能远程协助下吗?

10、termpair

Star:1.2k|语言:Python

能够在命令行开启远程协助的工具。原理是命令行启动了一个 Web 服务,然后生成分享用的链接。最后只要拿到链接就能通过浏览器,远程操作服务器了。有了它再遇到问题,求助大佬就方便多了。

1
2
3
4
5
6
bash复制代码# 安装
pip install termpair
# 启动服务
termpair serve
# 生成远程控制终端的链接
termpair share

github.com/cs01/termpa…

最后

想要驯服桀骜不驯的代码,不是一件容易的事情。我的经验:

先定位问题,然后验证想法复现问题,最后在考虑解决方案。

弄懂每一行代码,知晓复杂系统下的数据流向和状态。

这期介绍的开源项目都是用来辅助你找到 Bug、定位问题的工具,有了它们相信你假以时日,驯服代码的能力定会突飞猛进。编程能力起飞!

以上就是本期的全部内容,这里是 HelloGitHub 分享 GitHub 上有趣、入门级的开源项目。感谢您的阅读和支持,求赞、分享,让优秀的开源项目被更多人发现和喜欢。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…491492493…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%