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

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


  • 首页

  • 归档

  • 搜索

Kubernetes 已经成为云原生时代的安卓,这就够了吗?

发表于 2021-11-24

简介:本文将介绍如何在 Kubernetes 上构建新的应用管理平台,提供一层抽象以封装底层逻辑,只呈现用户关心的接口,使用户可以只关注自己的业务逻辑,管理应用更快更安全。

作者:司徒放

导语:云原生时代,直接使用 Kubernetes 和云基础设施过于复杂,如用户需要学习很多底层细节、应用管理的上手成本高、容易出错、故障频频。随着云计算的普及,不同云又有不同的细节,进一步加剧了上述问题。

本文将介绍如何在 Kubernetes 上构建新的应用管理平台,提供一层抽象以封装底层逻辑,只呈现用户关心的接口,使用户可以只关注自己的业务逻辑,管理应用更快更安全。

云原生时代是一个非常好的时代,我们所面临的是整体技术的颠覆性革新,全面地对应用做端到端重构。目前,云原生在演进过程中产生了三个关键技术:

  • 一是容器化,容器作为标准化交互的介质,在运维效率、部署密度和资源隔离性方面相比传统方式有很大改进,据 CNCF 最新调研报告显示,目前已有 92% 的企业生产系统在使用容器;
  • 二是 Kubernetes,它对基础设施进行了抽象和管理,现已成为云原生的标配;
  • 三是 Operator 自动化运维,通过控制器和定制资源的机制,使 Kubernetes 不仅可以运维无状态应用,还可以执行由用户定义的运维能力,实现更复杂的自动化运维应用进行自动化部署和交互。

这三项关键技术其实是逐渐演进的关系,另外,在应用交付领域,也有与之对应的理论在跟随上述技术不断地演进。云原生的崛起,带来了交付介质、基础设施管理、运维模型和持续交付理论的全面升级和突破,加速了云计算时代的到来。

图 1 云原生技术全景图

从 CNCF 发布的云原生技术全景图(见图 1)中,可以看到云原生的蓬勃生态,细数图中这 900 + Logo,其中不乏开源项目、创业公司,未来云原生的技术都会在这些地方诞生。

云原生 “操作系统” Kubernetes带来的应用交付挑战

上文提到,Kubernetes 已成为云原生的标配,其对下封装基础设施的差异,对上支持各种应用的运维部署,如无状态应用、微服务,再如有状态、批处理、大数据、AI、区块链等新技术的应用,在 Kubernetes 上面都有办法部署。Kubernetes 已经成为了现实意义的 “操作系统” 。它在云原生的地位正如移动设备中的 Android。为什么这样讲?Android 不仅仅装在我们的手机上,它还进一步渗透到汽车、电视、天猫精灵等智能终端里,移动应用可以通过 Android 运行在这些设备上。而 Kubernetes 也有这样的潜力或发展趋势,当然它不是出现在智能家电中,而是出现在各家公有云、自建机房,以及边缘集群。可以预想,Kubernetes 未来会像 Android 一样无处不在。

那么,有了 Kubernetes 这层交付以后,容器 + Kubernetes 这层界面是不是就可以解决掉所有的交付问题了?答案肯定不是。试想,如果我们的手机中只有 Android 系统,它能够满足我们工作和生活需求吗?不能,必须有各种各样的软件应用才行。对应到云原生,它除了 Kubernetes 这个 “操作系统” 外,也需要一套应用的交付能力。在手机中,软件应用可以通过类似“豌豆荚”这样的应用以便用户安装,同样在云原生时代,我们也需要将应用部署到不同的 Kubernetes 集群上。但由于 Kubernetes 海量琐碎的设施细节与复杂各异的操作语言,导致部署过程中会遇到各种各样的问题,这时就需要云原生的“豌豆荚”来解决这个问题,也就是应用管理平台,去屏蔽交付的复杂性。

应用管理平台在业界有两种主流模式,第一种是传统平台模式,在 Kubernetes 上 “盖一个大帽子” ,将所有复杂度屏蔽,在此之上,再根据需求自己提供一层简化的应用抽象。通过这种方式,虽然应用平台变得易用了,但新的能力需要由平台开发实现,也就带来了扩展难、迭代缓慢的问题,无法满足日益增长的应用管理诉求。

另一种解法是容器平台模式。这种模式比较云原生,组件是开放的,扩展性强,但是,它缺乏应用层的抽象,导致了很多问题,比如开发者学习路线陡峭。举个例子,当一个业务开发者把自己的代码提交到应用平台时,他需要写 Deployment 部署应用、写 Prometheus 规则配置监控、写 HPA 设置弹性伸缩,写 Istio 规则控制路由等,这些都不是业务开发希望去做的。

所以,不论是哪种解法,都有优缺点,需要取舍。那么,到底怎么做才能封装平台的复杂性,还能拥有良好的扩展性?这是我们一直在探索的。

通过应用管理平台,屏蔽云原生应用交付的复杂性

2012 年,阿里巴巴已经开始做容器化相关的调研,起初主要是为了提高资源利用率,开始了自研容器虚拟化技术之路。随着应对大促的机器资源量不断增多,在 2015 年开始采用容器的混合云弹性架构,并使用阿里云的公有云计算资源,支撑大促流量高峰。这也是阿里巴巴做云原生的早期阶段。

转折发生在 2018 年,阿里巴巴的底层调度采用开源的 Kubernetes 后,我们从面对虚拟机的脚本化安装部署模式,转变为基于标准的容器调度系统部署应用,全面推进阿里巴巴基础设施的 Kubernetes 升级。但很快,新的问题就出现了:应用平台没有标准、不统一,大家“各自为政”。

因此,我们在 2019 年携手微软发布了开放应用模型——OAM(Open Application Model),并开始做 OAM 平台化改造。一切都比较顺利,2020 年 OAM 的实现引擎 KubeVela 正式开源,在内部推进多套应用管理平台基于 OAM 和 KubeVela 演进。并推动了三位一体战略,不仅阿里内部的核心系统全面使用这套技术,而且在面向客户的商业化云产品以及在开源时,都使用同样的技术。通过全面拥抱开源,让整个 OAM 和 KubeVela 社区参与共建。在这段探索历程中,我们走了不少弯路,也累积了许多踩坑经验,接下来将作具体介绍,同时分享 KubeVela 的设计原理和使用方法,帮助开发者了解云原生应用管理平台的完整解决方案,提高应用开发者的使用体验和应用交付效率。

云原生应用管理平台的解决方案

在探索云原生应用管理平台解决方案的过程中,我们主要遇到 4 项重大挑战,并总结了 4 个基本原则,下文将一一介绍。

挑战 1:不同场景的应用平台接口不统一,重复建设。

虽然,云原生有了 Kubernetes 系统,但在不同场景它会构建不一样的应用平台,且接口完全不统一,交付能力存在很大差异,比如 AI、中间件、Serverless 和电商在线业务都有各自不同的服务平台。因此,在构建应用管理平台时,难免重复开发和重复运维。最理想的状况当然是实现复用,但运维平台架构模式各有不同,没办法做到互通。另外,业务开发者在不同场景对接应用交付时,对接的 API 完全不同,交付能力存在很大差异。这是我们遇到的第一个挑战。

挑战 2:“面向终态”无法满足过程式的交付方式。

在云原生时代,面向终态的设计很受欢迎,因为它能减少使用者对实现过程的关心。使用者只需要描述自己想要什么,不需要详细规划执行路径,系统就能自动把事情做好。但在实际使用过程中,交付过程通常需要审批、暂停观察、调整等人为干预。举个例子,我们的 Kubernetes 系统在交付过程中处于强管护的状态,要审批发布。在《阿里集团变更管理规范》中明确 “线上变更,前 x 个线上生产环境批次,每个批次变更后观察时间应大于 y 分钟。” “必须先在安全生产环境(SPE)内进行发布,只有在 SPE 验证无问题后,才能在线上生产环境进行灰度发布。” 因此,应用交付是一个面向过程而非面向终态的执行流程,我们必须考虑,怎样让它更好地适应面向过程的流程。

挑战 3:平台能力扩展复杂度太高。

上文提到,传统模式下的应用平台扩展性差,那么在云原生时代,有哪些常见扩展平台的机制?在 Kubernetes 系统中,可以直接用 Go Template 等模板语言做部署,但缺点是灵活性不够,整个模板写下来结构复杂,难以做大规模的维护。有些高手可能会说 “我可以自定义一套 Kubernetes Controller,扩展性一定很好!” 没错,但是,了解 Kubernetes 及 CRD 扩展机制的人比较少。即使高手把 Controller 写出来了,他还有后续的许多工作要做,比如需要编译并将其安装在 Kubernetes 上运行,另外,Controller 数量也不能一直这样膨胀上去。因此,要想做一个高可扩展的应用平台有很大挑战。

挑战 4:不同环境不同场景,交付差异巨大。

在应用交付过程中,对于不同用途的环境,其运维能力差异特别大。比如开发测试环境,重视开发和联调效率,每次修改采用热加载,不重新打包、走镜像部署的一套流程,同时为开发人员部署按需创建的独立环境。再比如预发联调环境,有攻防演练、故障注入的日常运维诉求。以及在生产环境,需要加入安全生产、服务高可用方面的运维能力。此外,同一个应用,组件依赖也有巨大差异,数据库、负载均衡、存储,在不同云上存在诸多差异。

针对以上四项挑战,我们总结了现代应用管理平台的 4 点核心设计原则:

  1. 统一的、基础设施无关的开放应用模型。
  1. 围绕工作流的声明式交付。
  1. 高度可扩展,易编程。
  1. 面向混合环境的设计。

原则1:统一的、基础设施无关的开放应用模型。

怎样提炼统一的、基础设施无关的开放应用模型呢?以开放应用模型,即 OAM 为例,首先,它的设计非常简单,且能够大幅简化我们对管理平台的使用:原来使用者要面对上百个 API,OAM 将其抽象成 4 类交付模型。其次,OAM 从业务开发者视角描述要交付的组件,要用到的运维能力和交付策略,由平台开发者提供运维能力和交付策略的实现,从而对开发者屏蔽基础设施细节与差异性。通过组件模型,OAM 可以用来描述容器、虚拟机、云服务、Terraform 组件、Helm 等制品。

图 2 用开放应用模型描述的一个应用交付示例

如图 2,这是用 OAM 描述的一个 KubeVela 应用交付示例,里面包含上述 4 类模型。首先,要描述一个应用部署时包含的待交付组件(Component),一般是镜像、制品包、云服务等形式;其次,要描述应用部署后用到的运维能力(Trait),比如路由规则、自动扩缩容规则等,运维能力都作用于组件上;再次,是交付策略(Policy),比如集群分发策略、健康检查策略、防火墙规则等,任何一个部署前需要遵守的规则都可以在这个阶段声明和执行;最后,是工作流(Workflow)的定义,比如蓝绿部署、带流量的渐进式部署、手动审批等任意的管道式持续交付策略。

原则 2:围绕工作流做声明式的交付。

上面 4 类模型中最核心的是工作流,应用交付本质上是一次编排,将组件、运维能力、交付策略、工作流步骤等按顺序定义在一个有向无环图 DAG 里面。

图 3 KubeVela 通过工作流编排应用交付的示例

举个例子,应用交付前的第一步,比如安装系统部署依赖、初始化检查等,通过交付策略描述并在交付最开始的时候执行;第二步是依赖的部署,比如应用依赖了数据库,我们可以通过组件创建相关的云资源,也可以引用一个已有的数据库资源,将数据库连接串作为环境参数注入到应用环境中;第三步是用组件部署应用本身,包括镜像版本、开放端口等;第四步是应用的运维能力,比如设置监控方式、弹性伸缩策略、负载均衡等;第五步是在线上环境插入一个人工审核,检查应用启动是否有问题,人工确认没问题之后再继续让工作流往下走;第六步是将剩下的资源并行部署完,然后通过钉钉消息做回调,将部署完的消息告诉开发人员。这就是我们在真实场景中的交付流程。

这个工作流最大的价值在于,它把一个复杂的、面向不同环境的交付过程通过标准化的程序,较为规范地描述了出来。

原则 3:高度可扩展、易编程。

我们一直希望能够像乐高积木一样构建应用模块,平台开发者可以使用平台的业务开发轻松扩展应用平台的能力。但前文提到,用模板语言这种方式,灵活性不够、扩展性不足,而写 Kubernetes Controller 又太复杂、对开发者的专业能力要求极高。那怎么才能既有高度可扩展性,又有编程的灵活性?我们最后借鉴了谷歌 Borg 的 CUElang,这是一个适合做数据模板化、数据传递的配置语言。它天然适合调用 Go 语言,很容易与 Kubernetes 生态融合,具备高灵活性。而且 CUElang 是动态配置语言,不需要编译发布,响应速度快,只要将规则发布到 Kubernetes,就立马生效。

图 4 KubeVela 动态扩展机制

以 KubeVela 的动态扩展机制为例,平台开发者编写完 Web 服务、定时任务等组件模板,以及弹性伸缩、滚动升级等运维能力模板后,将这些能力模板(OAM X-Definition)注册到对应的环境。KubeVela 根据能力模板内容将能力运行时需要的依赖安装到对应环境的集群上。此时,应用开发者就可以使用平台开发者刚才编写的这些模板,他通过选择组件和运维能力构建出一个应用 Application yaml,并将 yaml 发布到 KubeVela 控制面上。KubeVela 通过 Application yaml 编排应用,运行对应选取的能力模板,最终把应用发布到 Kubernetes 集群中。整个从能力定义、应用描述,到最终完成交付的过程就完成了。

原则4:面向混合环境的设计。

在 KubeVela 设计之初,我们就考虑到未来可能是在混合环境(混合云/多云/分布式云/边缘)中做应用的交付,且不同环境、不同场景的交付差异较大。我们做了两件事。第一,将 KubeVela 控制平面完全独立,不入侵业务集群。可以在业务集群中使用任何来自社区的 Kubernetes 插件运维和管理应用,由 KubeVela 负责在控制平面管理和操作这些插件。第二,不使用 KubeFed 等会生成大量联邦对象的技术,而是直接向多集群进行交付,保持和单集群管理一致的体验。通过集成 OCM/Karmada 等多容器集群管理方案支持 Push 和 Pull 模式。在中央管控、异构网络等场景下,KubeVela 可以实现安全集群治理、环境差异化配置、多集群灰度发布等能力。

以阿里云内部边缘计算产品的方案为例,开发人员只需将编写的镜像和 KubeVela 的文件直接发布到 KubeVela 控制平面,控制平面会将应用组件分发到中心托管集群或边缘集群。边缘集群可以采用 OpenYurt 等边缘集群管理方案。因为 KubeVela 是多集群统一的控制平面,所以它可以实现应用组件的统一编排、云-边集群差异配置,以及汇聚所有底层的监控信息,实现统一可观测和绘制跨集群资源拓扑等目的。

总结

总的来说,上述 4 个 KubeVela 核心设计原则可以简单囊括为:

1.基于 OAM 抽象基础设施底层细节,用户只需要关心 4 个交付模型。

2.围绕工作流的声明式交付,工作流无需额外启动进程或容器,交付流程标准化。

3.高度可扩展、易编程:将运维逻辑用 CUE 语言代码化,比模板语言更灵活,比写 Controller 简单一个量级。

4.面向混合环境的设计,提供环境和集群等围绕应用的概念抽象,统一管控所有应用依赖的资源 (包含云服务等)。

图 5 KubeVela 在阿里云原生基础设施的位置

目前,KubeVela 已经成为阿里云原生基础设施一部分。从图 5 可见,我们在 Kubernetes 之上做了很多扩展,包括资源池、节点、集群管理能力,对工作负载和自动化运维能力也做了很多支持。KubeVela 在这些能力之上做了一层统一的应用交付和管理层,以便集团业务能够适用不同场景。

未来云原生将如何演进呢?回顾近十年的云原生发展,**一个不可逆转的趋势是标准化界面不断上移。**为什么?从 2010 年左右云计算崭露头角到如今站稳脚跟,云的算力得到普及;2015 年前后容器大范围铺开,带来了交付介质的标准化;2018 年左右,Kubernetes 通过对集群调度和运维抽象,实现了基础设施管理的标准化;近两年 Prometheus 和 OpenTelemetry 逐渐让监控走向统一,Envoy/Istio 等 Service Mesh 技术在让流量管理更加通用。从这些云原生发展历程中,我们看到了云原生领域技术碎片化和应用交付复杂性的问题,提出开放应用模型 OAM 并开源 KubeVela 试图解决这个问题。我们相信,应用层标准化将是云原生时代的趋势。

作者介绍:

司徒放,花名“姬风”|阿里云资深技术专家,阿里云应用 PaaS 及 Serverless 产品线负责人。2010 年加入阿里巴巴后一直深度参与服务化和云原生架构的多次跨代演进,如链路跟踪、容器虚拟化、全链路压测、异地多活、中间件云产品化、云原生上云等。负责并主导了阿里巴巴在微服务、可观测性、Serverless 等领域的开源技术和商业化产品建设,致力于通过云原生技术,为外部企业提供成熟稳定的互联网架构解决方案和产品。参与或主导设计的开源项目包括 KubeVela、Spring Cloud Alibaba、Apache Dubbo、Nacos 等。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

springboot+websocket实现并发抢红包功能

发表于 2021-11-24

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」

概述

抢红包功能作为几大高并发场景中典型,应该如何实现?

源码地址:gitee.com/tech-famer/…

分析

参考微信抢红包功能,将抢红包分成一下几个步骤:

  1. 发红包;主要填写红包信息,生成红包记录
  2. 红包支付回调;用户发红包支付成功后,收到微信支付付款成功的回调,生成指定数量的红包。
  3. 抢红包;用户并发抢红包。
  4. 拆红包;记录用户抢红包记录,转账抢到的红包金额。

效果展示

项目使用sessionId模拟用户,示例打开俩个浏览器窗口模拟两个用户。

20211124_110334.gif

设计开发

表结构设计

红包记录在 redpacket 表中,用户领取红包详情记录在 redpacket_detail 表中。

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
sql复制代码CREATE DATABASE  `redpacket`;

use `redpacket`;

CREATE TABLE `redpacket`.`redpacket` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`packet_no` varchar(32) NOT NULL COMMENT '订单号',
`amount` decimal(5,2) NOT NULL COMMENT '红包金额最高10000.00元',
`num` int(11) NOT NULL COMMENT '红包数量',
`order_status` int(4) NOT NULL DEFAULT '0' COMMENT '订单状态:0初始、1待支付、2支付成功、3取消',
`pay_seq` varchar(32) DEFAULT NULL COMMENT '支付流水号',
`create_time` datetime NOT NULL COMMENT '创建时间',
`user_id` varchar(32) NOT NULL COMMENT '用户ID',
`update_time` datetime NOT NULL COMMENT '更新时间',
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='红包订单表';

CREATE TABLE `redpacket`.`redpacket_detail` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`packet_id` bigint(20) NOT NULL COMMENT '红包ID',
`amount` decimal(5,2) NOT NULL COMMENT '红包金额',
`received` int(1) NOT NULL DEFAULT '0' COMMENT '是否领取0未领取、1已领取',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '更新时间',
`user_id` varchar(32) DEFAULT NULL COMMENT '领取用户',
`packet_no` varchar(32) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='红包详情表';

发红包设计

用户需要填写红包金额、红包数量、备注信息等,生成红包记录,微信收银台下单,返回用户支付。

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
ini复制代码public RedPacket generateRedPacket(ReqSendRedPacketsVO data,String userId) {
final BigDecimal amount = data.getAmount();
//红包数量
final Integer num = data.getNum();

//初始化订单
final RedPacket redPacket = new RedPacket();
redPacket.setPacketNo(UUID.randomUUID().toString().replace("-", ""));
redPacket.setAmount(amount);
redPacket.setNum(num);
redPacket.setUserId(userId);
Date now = new Date();
redPacket.setCreateTime(now);
redPacket.setUpdateTime(now);
int i = redPacketMapper.insertSelective(redPacket);
if (i != 1) {
throw new ServiceException("生成红包出错", ExceptionType.SYS_ERR);
}

//模拟收银台下单
String paySeq = UUID.randomUUID().toString().replace("-", "");

//拿到收银台下单结果,更新订单为待支付状态
redPacket.setOrderStatus(1);//待支付
redPacket.setPaySeq(paySeq);
i = redPacketMapper.updateByPrimaryKeySelective(redPacket);
if (i != 1) {
throw new ServiceException("生成红包出错", ExceptionType.SYS_ERR);
}
return redPacket;
}

微信截图_20211124140547.png

红包支付成功回调设计

用户支付成功后,系统接收到微信回调接口。

  1. 更新红包支付状态
  2. 二倍均值法生成指定数量红包,并批量入库。 红包算法参考:Java实现4种微信抢红包算法,拿走不谢!
  3. 红包总数入redis,设置红包过期时间24小时
  4. websocket通知在线用户收到新的红包
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
scss复制代码@Transactional(rollbackFor = Exception.class)
public void dealAfterOrderPayCallback(String userId,ReqOrderPayCallbackVO data) {
RedPacketExample example = new RedPacketExample();
final String packetNo = data.getPacketNo();
final String paySeq = data.getPaySeq();
final Integer payStatus = data.getPayStatus();
example.createCriteria().andPacketNoEqualTo(packetNo)
.andPaySeqEqualTo(paySeq)
.andOrderStatusEqualTo(1);//待支付状态
//更新订单支付状态
Date now = new Date();
RedPacket updateRedPacket = new RedPacket();
updateRedPacket.setOrderStatus(payStatus);
updateRedPacket.setUpdateTime(now);
updateRedPacket.setPayTime(now);
int i = redPacketMapper.updateByExampleSelective(updateRedPacket, example);
if (i != 1) {
throw new ServiceException("订单状态更新失败", ExceptionType.SYS_ERR);
}

if (payStatus == 2) {
RedPacketExample query = new RedPacketExample();
query.createCriteria().andPacketNoEqualTo(packetNo)
.andPaySeqEqualTo(paySeq)
.andOrderStatusEqualTo(2);
final RedPacket redPacket = redPacketMapper.selectByExample(query).get(0);
final List<BigDecimal> detailList = getRedPacketDetail(redPacket.getAmount(), redPacket.getNum());
final int size = detailList.size();
if (size <= 100) {
i = detailMapper.batchInsert(detailList, redPacket);
if (size != i) {
throw new ServiceException("生成红包失败", ExceptionType.SYS_ERR);
}
} else {
int times = size % 100 == 0 ? size / 100 : (size / 100 + 1);
for (int j = 0; j < times; j++) {
int fromIndex = 100 * j;
int toIndex = 100 * (j + 1) - 1;
if (toIndex > size - 1) {
toIndex = size - 1;
}
final List<BigDecimal> subList = detailList.subList(fromIndex, toIndex);
i = detailMapper.batchInsert(subList, redPacket);
if (subList.size() != i) {
throw new ServiceException("生成红包失败", ExceptionType.SYS_ERR);
}
}
}

final String redisKey = REDPACKET_NUM_PREFIX + redPacket.getPacketNo();

String lua = "local i = redis.call('setnx',KEYS[1],ARGV[1])\r\n" +
"if i == 1 then \r\n" +
" local j = redis.call('expire',KEYS[1],ARGV[2])\r\n" +
"end \r\n" +
"return i";
//优化成lua脚本
final Long execute = redisTemplate.execute(new DefaultRedisScript<>(lua, Long.class), Arrays.asList(redisKey), size, 3600 * 24);
if (execute != 1L) {
throw new ServiceException("生成红包失败", ExceptionType.SYS_ERR);
}
//websocket通知在线用户收到新的红包
Websocket.sendMessageToUser(userId, JSONObject.toJSONString(redPacket));
}
}


/**
* 红包随机算法
*
* @param amount 红包金额
* @param num 红包数量
* @return 随机红包集合
*/
private List<BigDecimal> getRedPacketDetail(BigDecimal amount, Integer num) {
List<BigDecimal> redPacketsList = new ArrayList<>(num);
//最小红包金额
final BigDecimal min = new BigDecimal("0.01");
//最少需要红包金额
final BigDecimal bigNum = new BigDecimal(num);
final BigDecimal atLastAmount = min.multiply(bigNum);
//出去最少红包金额后剩余金额
BigDecimal remain = amount.subtract(atLastAmount);
if (remain.compareTo(BigDecimal.ZERO) == 0) {
for (int i = 0; i < num; i++) {
redPacketsList.add(min);
}
return redPacketsList;
}

final Random random = new Random();
final BigDecimal hundred = new BigDecimal("100");
final BigDecimal two = new BigDecimal("2");
BigDecimal redPacket;
for (int i = 0; i < num; i++) {
if (i == num - 1) {
redPacket = remain;
} else {
//100内随机获得的整数
final int rand = random.nextInt(100);
redPacket = new BigDecimal(rand).multiply(remain.multiply(two).divide(bigNum.subtract(new BigDecimal(i)), 2, RoundingMode.CEILING)).divide(hundred, 2, RoundingMode.FLOOR);
}
if (remain.compareTo(redPacket) > 0) {
remain = remain.subtract(redPacket);
} else {
remain = BigDecimal.ZERO;
}
redPacketsList.add(min.add(redPacket));
}

return redPacketsList;
}

页面加载成功后初始化websocket,监听后端新红包生成成功,动态添加红包到聊天窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
javascript复制代码$(function (){
var websocket;
if('WebSocket' in window) {
console.log("此浏览器支持websocket");
websocket = new WebSocket("ws://127.0.0.1:8082/websocket/${session.id}");
} else if('MozWebSocket' in window) {
alert("此浏览器只支持MozWebSocket");
} else {
alert("此浏览器只支持SockJS");
}
websocket.onopen = function(evnt) {
console.log("链接服务器成功!")
};
websocket.onmessage = function(evnt) {
console.log(evnt.data);
var json = eval('('+evnt.data+ ')');
obj.addPacket(json.id,json.packetNo,json.userId)

};
websocket.onerror = function(evnt) {};
websocket.onclose = function(evnt) {
console.log("与服务器断开了链接!")
}
});

抢红包设计

抢红包设计高并发,本地单机项目,通过原子Integer控制抢红包接口并发限制为20,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
less复制代码private AtomicInteger receiveCount = new AtomicInteger(0);

@PostMapping("/receive")
public CommonJsonResponse receiveOne(@Validated @RequestBody CommonJsonRequest<ReqReceiveRedPacketVO> vo) {
Integer num = null;
try {
//控制并发不要超过20
if (receiveCount.get() > 20) {
return new CommonJsonResponse("9999", "太快了");
}
num = receiveCount.incrementAndGet();
final String s = orderService.receiveOne(vo.getData());
return StringUtils.isEmpty(s) ? CommonJsonResponse.ok() : new CommonJsonResponse("9999", s);
} finally {
if (num != null) {
receiveCount.decrementAndGet();
}
}
}

对于没有领取过该红包的用户,在红包没有过期且红包还有剩余的情况下,抢红包成功,记录成功标识入redis,设置标识过期时间为5秒。

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
kotlin复制代码public String receiveOne(ReqReceiveRedPacketVO data) {
final Long redPacketId = data.getPacketId();
final String redPacketNo = data.getPacketNo();
final String redisKey = REDPACKET_NUM_PREFIX + redPacketNo;
if (!redisTemplate.hasKey(redisKey)) {
return "红包已经过期";
}
final Integer num = (Integer) redisTemplate.opsForValue().get(redisKey);
if (num <= 0) {
return "红包已抢完";
}
RedPacketDetailExample example = new RedPacketDetailExample();
example.createCriteria().andPacketIdEqualTo(redPacketId)
.andReceivedEqualTo(1)
.andUserIdEqualTo(data.getUserId());
final List<RedPacketDetail> details = detailMapper.selectByExample(example);
if (!details.isEmpty()) {
return "该红包已经领取过了";
}
final String receiveKey = REDPACKET_RECEIVE_PREFIX + redPacketNo + ":" + data.getUserId();

//优化成lua脚本
String lua = "local i = redis.call('setnx',KEYS[1],ARGV[1])\r\n" +
"if i == 1 then \r\n" +
" local j = redis.call('expire',KEYS[1],ARGV[2])\r\n" +
"end \r\n" +
"return i";
//优化成lua脚本
final Long execute = redisTemplate.execute(new DefaultRedisScript<>(lua, Long.class), Arrays.asList(receiveKey), 1, 5);
if (execute != 1L) {
return "太快了";
}
return "";
}

拆红包设计

在用户抢红包成功标识未过期的状态下,且红包未过期红包未领完时,从数据库中领取一个红包,领取成功将领取记录写入redis以供查询过期时间为48小时。

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
ini复制代码@Transactional(rollbackFor = Exception.class)
public String openRedPacket(ReqReceiveRedPacketVO data) {
final Long packetId = data.getPacketId();
final String packetNo = data.getPacketNo();
final String userId = data.getUserId();
final String redisKey = REDPACKET_NUM_PREFIX + packetNo;
Long num = null;
try {
final String receiveKey = REDPACKET_RECEIVE_PREFIX + packetNo + ":" + userId;
if (!redisTemplate.hasKey(receiveKey)) {
log.info("未获取到红包资格,packet:{},user:{}", packetNo, userId);
throw new ServiceException("红包飞走了", ExceptionType.SYS_ERR);
}
redisTemplate.delete(receiveKey);
if (!redisTemplate.hasKey(redisKey)) {
log.info("红包过期了,packet:{}", packetNo);
throw new ServiceException("红包飞走了", ExceptionType.SYS_ERR);
}
num = redisTemplate.opsForValue().increment(redisKey, -1);
if (num < 0L) {
log.info("红包领完了,packet:{}", packetNo);
throw new ServiceException("红包飞走了", ExceptionType.SYS_ERR);
}
final int i = detailMapper.receiveOne(packetId, packetNo, userId);
if (i != 1) {
log.info("红包真的领完了,packet:{}", packetNo);
throw new ServiceException("红包飞走了", ExceptionType.SYS_ERR);
}
RedPacketDetailExample example = new RedPacketDetailExample();
example.createCriteria().andPacketIdEqualTo(packetId)
.andReceivedEqualTo(1)
.andUserIdEqualTo(userId);
final List<RedPacketDetail> details = detailMapper.selectByExample(example);
if (details.size() != 1) {
log.info("已经领取过了,packet:{},user:{}", packetNo, userId);
throw new ServiceException("红包飞走了", ExceptionType.SYS_ERR);
}
//处理加款
log.info("抢到红包金额{},packet:{},user:{}", details.get(0).getAmount(), packetNo, userId);
final String listKey = REDPACKET_LIST_PREFIX + packetNo;
redisTemplate.opsForList().leftPush(listKey,details.get(0));
redisTemplate.expire(redisKey, 48, TimeUnit.HOURS);
return "" + details.get(0).getAmount();
} catch (Exception e) {
if (num != null) {
redisTemplate.opsForValue().increment(redisKey, 1L);
}
log.warn("打开红包异常", e);
throw new ServiceException("红包飞走了", ExceptionType.SYS_ERR);
}
}

其中 detailMapper.receiveOne(packetId, packetNo, userId); sql如下,将指定红包记录下未领取的红包更新一条未当前用户已经领取,若成功更新一条则表示领取成功,否则领取失败。

1
2
3
4
5
6
7
csharp复制代码update redpacket_detail d
set received = 1,update_time = now(),user_id = #{userId,jdbcType=VARCHAR}
where received = 0
and packet_id = #{packetId,jdbcType=BIGINT}
and packet_no = #{packetNo,jdbcType=VARCHAR}
and user_id is null
limit 1

获取红包领取记录设计

直接充redis中获取用户领取记录,没有则直接获取数据库并同步至redis。

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
scss复制代码public RespReceiveListVO receiveList(ReqReceiveListVO data) {
//红包记录redisKey
final String packetNo = data.getPacketNo();
final String redisKey = REDPACKET_LIST_PREFIX + packetNo;
if (!redisTemplate.hasKey(redisKey)) {
RedPacketDetailExample example = new RedPacketDetailExample();
example.createCriteria().andPacketNoEqualTo(packetNo)
.andReceivedEqualTo(1);
final List<RedPacketDetail> list = detailMapper.selectByExample(example);
redisTemplate.opsForList().leftPushAll(redisKey, list);
redisTemplate.expire(redisKey, 24, TimeUnit.HOURS);
}
List retList = redisTemplate.opsForList().range(redisKey, 0, -1);
final Object collect = retList.stream().map(item -> {
final JSONObject packetDetail = (JSONObject) item;
return ReceiveRecordVO.builder()
.amount(packetDetail.getBigDecimal("amount"))
.receiveTime(packetDetail.getDate("updateTime"))
.userId(packetDetail.getString("userId"))
.packetId(packetDetail.getLong("redpacketId"))
.packetNo(packetDetail.getString("redpacketNo"))
.build();
}).collect(Collectors.toList());
return RespReceiveListVO.builder().list((List) collect).build();
}

jmeter并发测试抢红包、查红包接口

设置jmeter参数1秒中并发请求50个抢11个红包,可以看到,前面的请求都是成功的,中间并发量上来后有部分达到并发上限被拦截,后面红包抢完请求全部失败。
微信截图_20211124145311.png

微信截图_20211124145340.png

微信截图_20211124145356.png

微信截图_20211124145410.png

本文转载自: 掘金

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

redis之bitmap使用及项目应用 bitmap的使用

发表于 2021-11-24

bitmap的使用

本章主要讲解bitmap的使用,bitmap底层也是string类型,通俗可以理解为每个key都是字符串,但是有特殊的命令对该字符串进行位操作。本章的命令对于没有接触过的同学可能会有点生疏,建议从应用场景开始查看以提高兴趣。

注:下列的截图测试数据添加没有完全说明,展示的时候有时候会不理解,有疑惑可以评论区留言

一、bitmap特性说明

1、底层结构为string类型

2、设置的bit位长度最多为2^32-1,意味着每个键值的最大存储为512M

3、新增的bit位默认位0

二、相关命令使用

本章介绍依旧是从命令开始介绍,若觉得无聊建议从应用场景开始看起,再回头看看命令的使用。

(一)setbig

命令格式: SETBIT key offset value

功能:设置一个bit位值

demo说明:
image.png

(二)getbit

命令格式: GETBIT key offset

功能:获取一个bit位值

demo说明:
image.png

(三)bitcount

命令格式: BITCOUNT key [start end]

功能:统计字符串被设置为1的bit数

demo说明:
image.png

(四)bitop

命令格式: BITOP operation destkey key [key …]

功能:对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上

demo说明:

首先设置给键为test1000和test4000设置一批数据
image.png

对test1000和test4000做逻辑并,并将结果保存到resultand
image.png
同理其他位运算就不一一演示

(五)BITFIELD

命令格式:# key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]

功能:本命令会把Redis字符串当作位数组,并能对变长位宽和任意未字节对齐的指定整型位域进行寻址。

demo说明:
表示对test1000取出五位无符号数,二进制累加起来刚好29
image.png

(六)bitpos

命令格式:BITPOS key bit [start] [end]

功能:返回字符串里面第一个被设置为1或者0的bit位。start、end表示的是字节,一字节的8bit。start=0且end=2表示在前三个字节内寻找

demo说明:

首先设置一批数据(这里从1设置,但其实数据是从零开始,没设置默认为0)
image.png

查看范围在第一字节到第二字节首次设为为1的bit位
image.png

查看首次设为为0的bit位
image.png

查看范围在第一字节到第二字节首次设为为0的bit位
image.png

三、应用场景

(一) 实现签到功能

1、需求

前台用户:

  • 1
    复制代码用户签到、今日是否已签到、每月的签到记录

后台管理:

  • 1
    复制代码统计每天有多少用户签到

2、设计

  • 采用redis的bitmap存储
  • 存储每天所有用户签到:bitmap的key设计为 userSign+日期,如userSign_20211122,偏移量offen采用用户的id
  • 存储单个用户每月签到:bitmap的key设计为 userSign+id+日期,如userSign_用户id_202111,偏移量offen采用每月的第几天
  • 用户签到则在对应的键以及对应的偏移量设置值为1

3、代码开发

前台功能接口

下面接口实现比较细化,主要是为了方便查看。正常项目开发肯定会将多种数据封装在一个接口返回。如当日是否已签到、某月签到日历、连续签到次数这几个接口会封装成一个接口

a、用户签到接口
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
java复制代码@ApiOperation("签到")
@PostMapping(value = "/userSign")
public RestResponse userSign() throws Exception{
//用户id,平时我们得从token或者session中获取
Long userId = 123456L;
LocalDate localDate = LocalDate.now();

//用户每月的签到键
String userKey = "userSign_"+ Long.valueOf(userId)+"_"+String.valueOf(localDate.getYear()) + localDate.getMonth().getValue();
//所有用户每月的签到键
String signKey = "userSign_"+"_"+String.valueOf(localDate.getYear()) + localDate.getMonth().getValue();

//今天是当月的第几天
int date = localDate.getDayOfMonth();
//用户每月的签到集合
Boolean result = redisTemplate.opsForValue().setBit(userKey, new Long(date), true);

if(result){
//首先判断该用户是否已签到 setBit 会返回原始值,若原本是true,则表示已签到
return new RestResponse(60001,"你今天已签到了哦!明天再来");

}else{
//所有用户每月的签到集合
redisTemplate.opsForValue().setBit(signKey, userId, true);
//返回信息
return new RestResponse(0,"签到成功");
}
}
b、获取用户每月的签到数据
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
ini复制代码@ApiOperation("获取用户每月的签到数据")
@PostMapping(value = "/getSignData")
public RestResponse getSignData(Integer year, Integer month) throws Exception{

LocalDate selectLocalDate = LocalDate.of(year,month,1);
//查询的月份有少天
int lengthMonth = selectLocalDate.lengthOfMonth();

//用户id,平时我们得从token或者session中获取
Long userId = 123456L;
String userKey = "userSign_"+ Long.valueOf(userId)+"_"+String.valueOf(year) +String.valueOf(month);

long mask = 0b1;
//用bitfield命令取出第一天到当月最后一天的数据
List<Long> signList = (List<Long>)redisTemplate.execute((RedisCallback<List<Long>>) con -> con.bitField(userKey.getBytes(), BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(lengthMonth+1)).valueAt(0)));
//签到数据
List<Long> userSignList = new ArrayList<>();

if (!CollectionUtils.isEmpty(signList)) {
long sign = signList.get(0) == null ? 0 : signList.get(0);
for (int i = selectLocalDate.lengthOfMonth(); i > 0; i--) {
//从最后一天往前算
userSignList.add((sign & mask));

//最低位前进一天
sign >>= 1;
}
}
//翻转list
Collections.reverse(userSignList);
//打印结果
System.out.println(userSignList);
return new RestResponse(0,"获取成功",userSignList);
}
c、今日是否已签到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@ApiOperation("今日是否已签到")
@GetMapping(value = "/checkSign")
public RestResponse checkSign() throws Exception{
//用户id,平时我们得从token或者session中获取
Long userId = 123456L;
LocalDate localDate = LocalDate.now();

//用户每月的签到键
String userKey = "userSign_"+ Long.valueOf(userId)+"_"+String.valueOf(localDate.getYear()) + localDate.getMonth().getValue();
//今天是当月的第几天
int date = localDate.getDayOfMonth();
//用户每月的签到集合
Boolean result = redisTemplate.opsForValue().getBit(userKey, new Long(date));

//自己将数据封装返回 我这里只是给个提示
if(result){
return new RestResponse(0,"今日已签到");

}else{
return new RestResponse(0,"今日未签到");
}
}
d、用户当月总签到次数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
less复制代码@ApiOperation("当月总签到次数")
@GetMapping(value = "/countSignTimes")
public RestResponse countSignTimes() throws Exception{
//用户id,平时我们得从token或者session中获取
Long userId = 123456L;
LocalDate localDate = LocalDate.now();

//用户每月的签到键
String userKey = "userSign_"+ Long.valueOf(userId)+"_"+String.valueOf(localDate.getYear()) + localDate.getMonth().getValue();

Long count = (Long) redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount(userKey.getBytes()));

return new RestResponse(0,"当月已签到"+String.valueOf(count)+"次");

}

后台功能接口

a、统计每天有多少用户签到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@ApiOperation("统计每天有多少用户签到")
@GetMapping(value = "/countAllUserSignTimes")
public RestResponse countAllUserSignTimes(Integer year, Integer month,Integer day) throws Exception{
//用户id,平时我们得从token或者session中获取
LocalDate localDate = LocalDate.now();

//所有用户每天的签到键
String signKey = "userSign_"+String.valueOf(localDate.getYear()) + localDate.getMonth().getValue() + localDate.getDayOfMonth();

Long count = (Long) redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount(signKey.getBytes()));

return new RestResponse(0,"该天有"+String.valueOf(count)+"用户签到");

}

4、测试

这里用工具postman简单地测试一下功能,按顺序从上往下

(1)用户签到

image.png

(2)获取用户每月的签到数据

image.png

(3)今日是否已签到

image.png

(4) 当月签到次数

image.png

(5) 统计每天有多少用户签到

image.png

(二) 实现用户活跃度

有需要可以评论,后续补充

(三) 实现记录登陆状态

有需要可以评论,后续补充

本文转载自: 掘金

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

华为云企业级Redis揭秘第15期:Redis为什么需要强一

发表于 2021-11-24

摘要:其实开源Redis的弱一致性已经不满足很多应用场景的诉求。怎么,不信?

本文分享自华为云社区《华为云企业级Redis揭秘第15期:Redis为什么需要强一致?》,作者: GaussDB 数据库。

有人说,开源Redis的最终一致性已经能满足大部分应用场景,也有人说,多副本的强一致代价太大,没有必要实现。要笔者说,其实弱一致性已经不满足很多应用场景的诉求。怎么,不信?请听笔者娓娓道来。

  1. 不一致带来的困扰

1.1 秒杀变秒崩

分享一个电商秒杀活动中限流器的例子,在电商的秒杀活动中,为了扛住前端对数据库的超大流量冲击,一般使用两种方案来保护系统,一个是缓存,另一个则是限流。缓存这个容易实现,只需要在数据库前加一层缓存服务器,而对于限流来说,最简单的可以使用Redis的计数器来实现限流功能。

具体来说,假设我们需要对某个接口限定流量为5000QPS,即每秒钟访问的次数不能超过5000。那么我们可以这么做:在一开始的时候设置一个计数器counter为5000,并且过期时间为1s,即1s后计数器失效。每当一个请求过来的时候,counter的值减1,判断当前counter的值是否等于0,如果等于,则说明请求次数过多,直接拒绝请求。如果counter计数器不存在,则重置计数器为5000,开始新一秒的接口限流,注意并发情况下计数器需要加锁。

正常情况下,这种方案不会出现问题,但是针对这种秒杀活动,不怕一万,就怕万一,万一Redis突然宕机怎么办,那岂不是限流器形同虚设,所有流量全部涌向后端的数据库,瞬间系统崩溃。此时聪明的你肯定会想到,给Redis搞一个备用服务器不就解决了,主服务器如果宕机,备用服务器顶上。没错,这种方案是对的,但是只正确了一半。为什么呢,如下图所示。

当给Redis配置从服务器之后,如果主服务器出现宕机,可以立刻切换到从服务器,但是由于开源Redis主从服务器之间的数据是异步复制的,如果网络不畅,经常发生主从数据不一致,如果此时主服务器发生宕机,切换到从服务器之后,因为限流器的判断出错,流量压力很容易超出阈值,一下子涌向数据库服务器,同样会造成系统崩溃。

仔细探究这个问题产生,根因是在于开源Redis的一致性机制为弱一致性,在某些时间内,主从副本数据不一致。而要彻底解决这个问题,只有真正的强一致才能解决。

1.2 难以维护的MySQL组件

其实不止Redis,就连大名鼎鼎的MySQL也逃不过弱一致的坑。MySQL的部署中,为了保证高可用性,主从热备份是MySQL常用的部署方式。但是如果发生故障时,仅仅靠MySQL自身的同步机制,是无法保证主库和从库之前的数据一致的,于是出现了重要的辅助组件MHA(Master High Availability),它的部署方式如下:

MHA由管理服务和Node服务组成,Node服务部署在每个MySQL节点上,MHA组件负责让MySQL的从库尽可能的追平主库,提供主从一致的状态。发生故障进行主从切换时,Manager首先为从库补充落后的数据,然后再将用户访问切换到从库,这个过程可能长达数十秒。

MHA的部署和维护都相当复杂,如未能顺利执行故障切换或发生数据丢失,运维面临的场面都将很棘手。其实运维同学何尝不希望手中的系统稳定运行呢?要是数据库自身能提供强一致保障,何苦再依赖复杂的辅助组件!

  1. 什么是强一致

上一节中笔者介绍了弱一致带来各种问题,接下来这一节具体介绍下什么是强一致。在“分布式系统”和“数据库”这两个领域中,一致性都是重要概念,但它表达的内容却并不相同。对于分布式系统而言,一致性是在探讨当系统内的一份逻辑数据存在多个物理的数据副本时,对其执行读写操作会产生什么样的结果,这也符合 CAP 理论对一致性的表述。而在数据库领域,“一致性”与事务密切相关,又进一步细化到 ACID 四个方面。因此,当我们谈论分布式数据库的一致性时,实质上是在谈论事务一致性和数据一致性两个方面。

2.1 事务一致性

事务的一致性主要是指的事务的ACID,分别是原子性、一致性、隔离性和持久性,如下图所示:

  • **原子性:**事务中的所有变更要么全部发生,要么一个也不发生,通过日志技术实现;
  • 一致性:事务要保持数据的完整性,它是应用程序的属性,依赖原子性和隔离属性来实现;
  • 隔离性:多事务并行执行所得到的结果,与串行执行(一个接一个)完全相同,通过并发控制技术来实现;
  • 持久性:一旦事务提交,它对数据的改变将被永久保留,不应受到任何系统故障的影响,通过日志技术实现。

2.2 数据一致性

在分布式系统中,为了避免网络不可靠带来的问题,通常会存储多个数据副本,逻辑上的一份数据存储在多个物理副本上,自然带来了数据一致性问题。

(1)状态视角

从状态的视角来看,任何变更操作后,数据只有两种状态,所有副本一致或者不一致。在某些条件下,不一致的状态是暂时,还会转换到一致的状态,而那些永远不一致的情况几乎不会去讨论,所以习惯上大家会把不一致称为“弱一致”。相对的,一致就叫做“强一致”了。以一个一主两备的MySQL集群为例,“强一致”的交互过程如下:

在该模式下,主库与备库同步 binlog 时,主库只有在收到两个备库的成功响应后,才能够向客户端反馈提交成功。显然,用户获得响应时,主库和备库的数据副本已经达成一致,所以后续的读操作肯定是没有问题的,这就是状态视角的“强一致”的模型。

但是状态视角的这种强一致副作用很大:第一个是性能很差,主库必须要等备库1和备库2成功返回后才能返回;第二个是可用性问题,如果主备节点很多,出现故障的概率非常高。因此,状态视角的强一致代价非常大,所以很少使用。

(2)操作视角

状态视角的强一致降低了系统的可用性,因此很多系统选择状态视角的弱一致性模型,通过额外的算法(如Raft、Paxos)在不保证所有节点状态的一致的情况下,来保证操作视角的一致性,同时提高了系统的可用性。通过加入一些限定条件,衍生出了若干种一致性模型:

  • 线性一致性:操作视角实现真正的强一致
  • 顺序一致性:一致性强度弱于线性一致性
  • 因果一致性:一致性强度弱于顺序一致性
  • 写后读一致性:一致性强度相当,弱于因果一致性

这些一致性模型的介绍参考《高斯Redis与强一致》这篇文章。

  1. 强一致的刚需场景

上一节我们介绍了什么是强一致,这一节我们介绍下强一致的典型应用场景。

在常见的互联网应用中,如果数据库服务器只部署在单个节点上,那么应用程序所有的读和写都只会访问单个节点,一份逻辑数据在物理上也只有一份,这种场景下就谈不上强一致的问题。

但是随着系统中业务访问量的增加,如果是单机部署数据库,就会导致I/O访问频率过高,数据库就会成为系统的瓶颈。此时,为了降低单机磁盘的I/O访问频率,提高单个机器的I/O性能,通常会增加多个数据存储节点,形成一主一从或者一主多重的架构。

此时,我们可以将负载分布在多个从节点上,一方面可以实现读写分离,写请求访问主库,读请求访问备库。另一方面,还可以在主库如果出现宕机的情况下进行主备切换,增强系统的稳定性。在以上两个场景中,由于一份逻辑数据在物理上有多个副本,那么如何保证多个副本之间的数据一致呢,这就是强一致需要解决的问题。

3.1 读写分离场景

以关系型数据库MySQL为例,典型的部署方案为一主两从三节点方案,主节点负责处理写操作,两个从节处理读操作,分担主库的压力,如下图所示:

此时,如果系统没有实现强一致,就有可能会遇到执行完写操作后,立刻去读,然后发现读不到或者读到旧状态的尴尬场景,比如操作顺序为以下操作:

  • 客户端首先通过代理向主节点 Master 进行了写入操作,此时由于没有实现强一致,写操作写完后立即返回;
  • 紧接着第二步去从节点 Slave A 执行读操作,此时Master和Slave A之间的同步还未完成,系统处于非强一致的状态,所以第二步的读操作读取到了旧状态。

可以看出,在一主多备读写分离的场景下,如果想要保证写入和读取操作的准确无误,系统实现强一致是非常重要的。

3.2 主备切换场景

主备切换的场景也需要强一致来保证,以目前业内使用最广泛的内存数据库Redis为例,Redis的主从同步如下图所示:

从上图可以看出,当Redis客户端向Master服务器发送一条命令时,Master服务器立即回复客户端命令的执行结果,并不等待命令同步到从服务器再回复,也就是说Redis的主从同步其实是异步的。

由于Master节点存在宕机的可能,在这种情况下,如果在Master收到命令但是还没同步到Slave服务器时发生了宕机,Redis就会发生主备切换,然后此时Master服务器和Slave服务器的数据还没有同步,就导致了数据丢失的情况。可见,开源Redis弱一致性本身的缺陷和不足,而要解决这个问题,必须实现强一致性才能解决。

  1. 高斯Redis强一致

由于开源的Redis不具备强一致的特性,导致开源Redis的应用也受到了诸多限制,为了解决开源Redis弱一致的问题,GaussDB(for Redis)应运而生。GaussDB(for Redis) 是华为云数据库团队自主研发的兼容Redis协议的云原生数据库,彻底解决了开源Redis一致性问题带来的痛点。

4.1 高斯Redis架构

高斯Redis的整体架构如下:

相比开源Redis,高斯Redis采用存算分离的设计思想,计算层负责计算和协议的处理,聚焦服务。而存储层负责副本管理、扩缩容等处理,聚焦数据本身。高斯Redis的优势如下:

  • 数据强一致:存储层使用分布式存储DFV,轻松实现了3副本强一致;
  • 超可用:N个节点的集群最多可以挂掉N – 1个节点;
  • 低成本:数据采用磁盘存储并且进行压缩,每GB的成本不到开源Redis的十分之一;
  • 秒扩容:计算层仅需修改路由映射,无需数据搬迁,实现秒级扩容;
  • 自动备份:高斯Redis可以实现MVCC快照备份和定期自动备份。

4.2 高斯Redis强一致的实现

开源Redis和高斯Redis的架构如下图所示:

开源Redis或者传统的主从结构如左图所示,如果在读写分离的场景或者主节点出现宕机发生主从切换的时候,都会导致数据不一致的情况。

高斯Redis采用存算分离的架构,如右图所示,在存储层DFV的副本管理中采用分布式共识算法实现了3副本的强一致。计算层调用存储层的接口时,如果返回OK,那么即表示存储层已经实现副本强一致的复制。

  1. 结语

我们在做架构设计的时候,其实很多场景都隐藏着强一致的诉求。如朋友圈这类应用,如果没有实现强一致,朋友圈的评论很容易乱序。再比如限流器的场景,如果没有强一致的保证,也极容易造成数据库的崩溃。因此,必须在系统设计之初就认识到强一致的重要性,才能设计出更加稳定和可靠的系统。而高斯Redis基于存算分离的架构设计,实现了数据的强一致,为业务的稳定可靠提供了超强保障。

  1. 附录

  • 本文作者:华为云数据库GaussDB(for Redis)团队
  • 杭州/西安/深圳简历投递:yuwenlong4@huawei.com
  • 更多产品信息:GaussDB(for Redis)官网
  • 更多技术文章:GaussDB(for Redis)博客

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

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

工具 一条 SQL 实现 PostgreSQL 数据找回

发表于 2021-11-24

作者:张连壮 PostgreSQL 研发工程师

从事多年 PostgreSQL 数据库内核开发,对 citus 有非常深入的研究。

快速找回丢失数据,是数据库的一项重要功能需求,一般建议使用官方推荐的工具。面向开源数据库,生态中也出现很多好用的开源工具。

PostgreSQL 是非常流行的开源数据库,接下来介绍一款近期在社区开源的 PostgreSQL 数据找回工具 pg_recovery ,并实例演示如何找回误操作而丢失的数据。

| 什么是 pg_recovery?

pg_recovery 是一款 PostgreSQL 数据找回工具。可以恢复 COMMIT / DELETE / UPDATE / ROLLBACK / DROP COLUMN 操作后导致的数据变化,并以表的形式返回。安装方便,操作简单。仓库地址:github.com/radondb/pg_…

快速安装

根据环境配置 PG_CONFIG。

1
2
3
4
5
6
7
8
9
10
11
bash复制代码$ make PG_CONFIG=/home/lzzhang/PG/postgresql/base/bin/pg_config
gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -Wno-format-truncation -Wno-stringop-truncation -g -g -O0 -fPIC -I. -I./ -I/home/lzzhang/PG/postgresql/base/include/server -I/home/lzzhang/PG/postgresql/base/include/internal -D_GNU_SOURCE -c -o pg_recovery.o pg_recovery.c
gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -Wno-format-truncation -Wno-stringop-truncation -g -g -O0 -fPIC -shared -o pg_recovery.so pg_recovery.o -L/home/lzzhang/PG/postgresql/base/lib -Wl,--as-needed -Wl,-rpath,'/home/lzzhang/PG/postgresql/base/lib',--enable-new-dtags

$ make install PG_CONFIG=/home/lzzhang/PG/postgresql/base/bin/pg_config
/usr/bin/mkdir -p '/home/lzzhang/PG/postgresql/base/lib'
/usr/bin/mkdir -p '/home/lzzhang/PG/postgresql/base/share/extension'
/usr/bin/mkdir -p '/home/lzzhang/PG/postgresql/base/share/extension'
/usr/bin/install -c -m 755 pg_recovery.so '/home/lzzhang/PG/postgresql/base/lib/pg_recovery.so'
/usr/bin/install -c -m 644 .//pg_recovery.control '/home/lzzhang/PG/postgresql/base/share/extension/'
/usr/bin/install -c -m 644 .//pg_recovery--1.0.sql '/home/lzzhang/PG/postgresql/base/share/extension/'

初始化插件成功,返回如下信息。

1
2
sql复制代码$ create extension pg_recovery ;
CREATE EXTENSION

| 数据找回演示

  1. 准备初始化数据

准备一个表和一些数据。

1
2
3
4
5
6
sql复制代码$ create table lzzhang(id int, dp int);
CREATE TABLE
# insert into lzzhang values(1, 1);
INSERT 0 1
$ insert into lzzhang values(2, 2);
INSERT 0 1
  1. 找回 UPDATE 数据

对数据进行变更操作,不加 WHERE 条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码$ update lzzhang set id=3, dp=3;
UPDATE 2
lzzhang=# select * from pg_recovery('lzzhang') as (id int, dp int);
id | dp
----+----
1 | 1
2 | 2
(2 rows)

$ select * from lzzhang;
id | dp
----+----
3 | 3
3 | 3
(2 rows)
  1. 找回 DELETE 数据

尝试恢复 DELETE 的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码$ delete from lzzhang;
DELETE 2
lzzhang=# select * from lzzhang;
id | dp
----+----
(0 rows)

$ select * from pg_recovery('lzzhang') as (id int, dp int);
id | dp
----+----
1 | 1
2 | 2
3 | 3
3 | 3
(4 rows)
  1. 找回 ROLLBACK 数据

尝试恢复回滚操作之前的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sql复制代码$ begin ;
BEGIN
$ insert into lzzhang values(4, 4);
INSERT 0 1
$ rollback ;
ROLLBACK
$ select * from lzzhang;
id | dp
----+----
(0 rows)

$ select * from pg_recovery('lzzhang') as (id int, dp int);
id | dp
----+----
1 | 1
2 | 2
3 | 3
3 | 3
4 | 4
(5 rows)
  1. 找回 DROP COLUMN 数据

尝试恢复表中被删除的列及数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
sql复制代码$ alter table lzzhang drop column dp;
ALTER TABLE
$ select attnum from pg_attribute, pg_class where attrelid = pg_class.oid and pg_class.relname='lzzhang' and attname ~ 'dropped';
attnum
--------
2
(1 row)

$ select * from lzzhang;
id
----
(0 rows)

$ select * from pg_recovery('lzzhang') as (id int, dropped_attnum_2 int);
id | dropped_attnum_2
----+------------------
1 | 1
2 | 2
3 | 3
3 | 3
4 | 4
(5 rows)

-- dropped_attnum_2: if the drop attnum is 5, set dropped_attnum_2 to dropped_attnum_5
  1. 显示找回数据

显示该表历史上所有写入过的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sql复制代码$ insert into lzzhang values(5);
INSERT 0 1
$ select * from lzzhang;
id
----
5
(1 row)

$ select * from pg_recovery('lzzhang', recoveryrow => false) as (id int, recoveryrow bool);
id | recoveryrow
----+-------------
1 | t
2 | t
3 | t
3 | t
4 | t
5 | f
(6 rows)

注意事项

  • 支持的 PostgreSQL 版本

目前 pg_revovery工具已支持 PostgreSQL 12/13/14 。

  • 可恢复事务数

PostgreSQL 通过参数 vacuum_defer_cleanup_age 值大小,可限制可恢复的事务数。如果预期需要恢复的数据量较大,可通过配置参数值,提高可恢复的事务数。

pg_recovery 通过读取 PostgreSQL dead 元组来恢复不可见的表数据。如果元组被 vacuum 清除掉,那么 pg_recovery 便不能恢复数据。

  • 锁请求

pg_recovery 使用期间,支持正常的读表的锁请求。此外 pg_recovery未使用期间,不会对数据库造成任何额外的开销或是影响,无需暂停服务。

本文转载自: 掘金

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

如何通过任务调度实现百万规则报警 01 问题背景 02 一次

发表于 2021-11-24

简介: 报警是一个公司的日常需求,常见的形态除了满足运维过程中的基础设施监控报警(CPU/内存/磁盘等)之外,部分公司也会在应用指标(如 QPS、RT 等)及业务指标(如 GMV/日活 等)上有相应的报警需求。

作者 | 黄晓萌

01 问题背景

报警是一个公司的日常需求,常见的形态除了满足运维过程中的基础设施监控报警(CPU/内存/磁盘等)之外,部分公司也会在应用指标(如 QPS、RT 等)及业务指标(如 GMV/日活 等)上有相应的报警需求。

在业务发展初期,基础设施较少,且应用形态单一,所以处理这一类需求往往会比较粗暴直接,但是随着业务的增长,尤其发展到日活百万甚至上亿级的时候,监控指标也会呈指数级上涨,在这种情况下对于报警体系就提出了巨大的挑战,如何解决这种体量下报警的有效性和时效性就成为了 IT 治理的重中之重。本篇文章,我们将从监控指标的体量出发,详解各个阶段报警体系中遇到的各个挑战。

02 一次常规的报警流程示意图

如下图所示,一次常规意义上的报警流程,主要会包含并发检查、齐全度检查、数据追补、阈值判断等核心环节。同时,为了保证报警的时效性,基本上整个流程会是一个秒级触发的形态,具体如下:

其中,报警后台任务处理系统是我们这次讨论的重点,几个核心流程的说明如下:

  1. 并发检查:检查当前告警规则是不是在其他进程或者节点中执行中,避免有些告警规则检查耗时过长,被重复执行了或被其他的任务节点抢占执行。
  2. 齐全度检查:获取当前告警规则对应的数据源的齐全度时间,即最新数据上报到什么时间了。因为数据源数据采集和上报一定会有延时的,如果数据不齐就进行检查,很容易漏报和误报。
  3. 数据查询:从监控数据中获取该规则的数据,一般会从收集上来的日志服务(如:ElasticSearch 服务等)或者基础监控指标存储服务(如:Zabbix、Prometheus 等)中获取。
  4. 数据追补:由某些报警任务设置的策略,没有数据点的情况下怎么处理。有补0,补满和不补三种。如在针对业务数据跌零报警的场景,我们会更倾向于补 0 ;但是针对 CPU 平均值超 80% 的场景,我们会倾向于不补。
  5. 阈值判断:根据获取的数据和报警条件,判断是否需要触发报警。
  6. 告警:将告警信息通过短信、钉钉、邮件等方式通知到配置的人,以便后续有人处理。

03 进程内调度方案

一开始的业务很少的时候,报警任务也趋于少数,这个时候一般的实现都会基于一个进程内的线程池执行相关的操作,架构图如下:

把上图的“后台任务处理系统”放到一台机器上运行,能很快速的满足小规模的场景。但是等到业务量持续上涨的时候,一台机器就出现了资源瓶颈,这个时候一个下意识的反应就是扩容上面的任务处理系统,让不同的 Server 处理不同的报警规则。但是随着报警规则在不断增加,负载的持续上涨会引起 Server 也会重启或者突然挂掉。于是高可用、任务幂等执行、failover 等分布式问题又是面临的一个复杂的难题。

04 分布式调度解决方案

如果任务数达到万级别,寻求一个轻量的分布式的方案是我们的目标。分布式调度方案的基本思路都是通过单独的任务调度中心来调度任务,报警后台只管执行任务,即任务调度和任务执行隔离的思路,使得两层都能做很好的横向扩容来达到容量上涨的目的。业务实现上,每个报警规则会生成一个定时任务,这样可以保证每个报警规则负载均衡地执行。开源市场有挺多产品,比如:Quartz、xxl-job、elastic-job 等。以 quartz 为例,示意图如下:

如上图所示,quartz 的每个 Server,会加载全量的所有任务,每次任务时间到了,所有 Server 会通过数据库抢锁,抢到锁的 Server 触发该任务给报警中心。

这个架构解决了任务的分布式调度、幂等执行的问题,并且执行层可以水平扩展,在任务量低的情况下可以稳定运行。

可是从上面的架构图可以看出,Quartz 的调度主要通过轮询 DB 和通过 DB 加锁的方式而实现,这个时候整个系统的吞吐基本上和 DB 的规格和性能息息相关。经测试,如果在任务量调度频率 1 分钟级别的触发达到1万,就会出现比较明显的调度延时。

05 基于 SchedulerX 2.0 的超大规模任务调度方案

1、SchedulerX 2.0 优势

SchedulerX 2.0 是阿里巴巴自研的一款商业化分布式任务调度平台,相对于开源任务调度系统,它有几大优势:

  • 支持海量任务
  • 自研轻量级分布式跑批模型
  • 可视化任务编排
  • 商业化报警
  • 可视化日志服务

SchedulerX2.0 基础架构图

与常见方案相比,SchedulerX2.0 会将任务分布式到不同的 Server 调度,每次任务调度也不需要抢锁触发,和数据库无任何交互,没有性能瓶颈。

2、高可用能力

在分布系统中最常见的就是高可用问题,如果 SchedulerX 2.0 的某个 Server 挂了会怎么办?

如上图所示,每个应用都会做三备份,通过 zk 抢锁,一主两备,如果某台 Server 挂了,会进行 failover,由其他 Server 接管调度任务。

3、商业化报警

SchedulerX 2.0 当前支持钉钉、短信、邮件三种报警通道:

支持任务失败、超时、无可用机器报警:

以钉钉告警为例,您可以实时收到报警:

06 总结

SchedulerX 2.0 在阿里巴巴集团内支撑了所有事业群的业务,经历了多次双十一的考验,当前在公有云已接入1000+家企业,在海量任务和高可用方面有充足的经验。显然,在超大规模任务调度领域,SchedulerX 2.0 已经是目前最优解决方案之一。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

一起了解PHP中YaConf扩展的使用

发表于 2021-11-24

今天我来学习另外一个配置文件扩展。这个配置文件的写法其实与 php.ini 的这种 PHP 标准的配置格式比较类似,但是又有一些不同。不过内容非常简单,大家仅供参考。

Yaconf 配置文件及格式

Yaconf 从名字是不是看出什么端倪了?没错,和 Yaf 、Yac 一样,又是我们鸟哥的作品。不得不说大神还是为我们贡献了很多很好的作品哦。后面我们还会讲一个它的小众开源扩展,而 Yaf 扩展我们将在未来学习框架的时候再深入地进行学习。

Yaconf 的安装也是普通地扩展安装的方式,不过它需要 PHP7 以上的版本。另外,在安装之后还需要在 php.ini 文件中指定 yaconf.directory ,也就是配置文件存放的目录。这个属性是不能通过 ini_set() 配置的,也就是必须在程序运行前就载入到 PHP 运行环境中。我们按照文档的说明将它配置为 /tmp/conf ,然后在这个目录下建立自己需要的配置文件就可以了。

Yaconf 的语法非常简洁,鸟哥的作品都主打性能,所以 Yaconf 也是一个高性能的配置管理扩展。关于 Yaconf 的具体介绍可以查看文章最下方第二条链接的说明,在这里我们就看一些它的语法以及具体的使用。

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码foo="bar"
phpversion=PHP_VERSION
env=${HOME}

arr.0=1
arr.1=2
arr[]=3
arr[3]=4

map.foo=bar
map.bar=foo
map.foo.name=yaconf

看出来什么特点了吗?首先,如果是带双引号的内容,会将这个配置变量当成字符串,如果不是双引号的,则会尝试以 PHP 来进行解析。然后数组和 HashMap 这样的写法也都是完美支持的。似乎是比 php.ini 的写法强悍了一些。不过还不止。

1
2
3
4
5
ini复制代码[parent]
parent="base"
children="NULL"
[children : parent]
children="children"

嗯,你没看错,它还可以支持这样的继承写法,中括号标示的内容可以看作是一个配置片断,或者说一节内容,具体作用我们后面会看到。

获取配置内容

配置语法就是这些,接下来我们要具体看看这些配置信息要怎么读取出来。这个扩展其实就提供了两个函数,一个用于读取,一个用于查询配置是否存在,我们先来看一下如何读取数据。

1
2
3
less复制代码var_dump(Yaconf::get("test.foo")); // string(3) "bar"
var_dump(Yaconf::get("test.phpversion")); // string(5) "7.4.4"
var_dump(Yaconf::get("test.env")); // string(5) "/root"

这个函数相信不用多解释了,test 是我们的文件名,也就是在 /tmp/conf/test.ini 这个文件中,我们把上面的测试配置信息写在了这个配置里面。当然,我们也可以在这个目录中定义更多的配置文件,比如我们另外定义了一个配置文件 foo.ini ,那么就可以这么读取:

var_dump(Yaconf::get(“foo.SectionA.key”)); // string(3) “val”

对于数组配置信息来说,直接获取到的内容返回的就是数组格式的。

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
less复制代码var_dump(Yaconf::get("test.arr"));
// array(4) {
// [0]=>
// string(1) "1"
// [1]=>
// string(1) "2"
// [2]=>
// string(1) "3"
// [3]=>
// string(1) "4"
// }

var_dump(Yaconf::get("test.arr.1")); // string(1) "2"

var_dump(Yaconf::get("test.map"));
// array(2) {
// ["foo"]=>
// array(1) {
// ["name"]=>
// string(6) "yaconf"
// }
// ["bar"]=>
// string(3) "foo"
// }

var_dump(Yaconf::get("test.map.foo.name")); // string(6) "yaconf"

在获取数组内部的数据时,我们直接使用 . 来获取序列的内容就可以了。最后就是上面提到过的分片和继承的功能。

1
2
3
4
5
less复制代码var_dump(Yaconf::get("test.parent.parent")); // string(4) "base"
var_dump(Yaconf::get("test.children.parent")); // string(4) "base"

var_dump(Yaconf::get("test.parent.children")); // string(4) "NULL"
var_dump(Yaconf::get("test.children.children")); // string(8) "children"

test 是文件名,而 parent 就是我们定义在中括号里面的分片名称,接着继续点分片下面定义的配置项的名称就可以获取到这个分片下面的配置信息内容了。而继承的使用相信从代码中大家也看出来了,parent 的 parent 配置项被 children 继承后,children 中不需要再定义这个配置项就可以直接获取到父级中定义过的这个配置项内容。而 children 中重写了 children 这个配置项,所以在 children 分片中的 children 配置项显示的就是它自己定义的内容。

检测配置信息是否存在

前面说过这个扩展中一共就两个方法,第二个就是用于检测配置项是否存在的一个方法,非常简单。

var_dump(Yaconf::has(“test.foo”)); // bool(true)

var_dump(Yaconf::has(“test.baz”)); // bool(false)

总结

说实话,这个配置扩展也并不是非常常见的一个扩展应用。因为大家目前在使用的框架不管是 Laravel 还是 TP 都会有它们自己的一套配置文件格式及操作。当然,如果说你是鸟哥的忠粉或者本身公司系统是架构在 Yaf 、Yac 、Yar 之上的话,那么加上这个 Yaconf 的话就可以看作是一整套完整的高性能内部扩展架构。它们主打的特点都是性能强悍,毕竟是从底层 C 扩展的角度来提供的框架,而不是通过 Composer 来使用 PHP 编写的框架。这个我们将来在学习和讲解框架的时候说不定会拿出来单独做一个系列哦!

测试代码:

github.com/zhangyue050…一起了解PHP中YaConf扩展的使用.php

本文转载自: 掘金

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

权限服务器keto

发表于 2021-11-24

「这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战」。

ORY Keto是一种权限服务器,它实现最佳实践访问控制机制:

  • 今天可用:具有精确,全局和正则表达式匹配策略的ORY风格的访问控制策略
  • 即将推出:
  • 访问控制列表
  • 基于角色的访问控制
  • 具有上下文的基于角色的访问控制(Google / Kubernetes风格)
  • Amazon Web Services身份和访问管理策略(AWS IAM策略)
  • 每种机制都由在开放策略代理之上实现的决策引擎提供动力,并提供定义明确的管理和授权端点

1 代码下载

keto源码地址下载

官方文档简单说明
::: tip 解压说明
把下载的源码解压后放在本地%GOPATH%/src目录下

注:GOPATH为项目的运行时的工作空间位置,GOPATH其中包含三个子目录如下

  • src 目录包含Go的源文件,它们被组织成包(每个目录都对应一个包)
  • pkg 目录包含包对象
  • bin 目录包含可执行命令
    :::
    keto存放位置

2 关键词介绍

2.1 RBAC

::: tip RBAC介绍
​RBAC是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
。RBAC 认为授权实际上是Who 、What 、How 三元组之间的关系,也就是Who 对What 进行How 的操作,也就是“主体”对“客体”的操作。
然后 RBAC 又分为RBAC0、RBAC1、RBAC2、RBAC3 ,如果你不知道他们有什么区别,你可以百度百科:百度百科-RBAC ,也可以看看我的介绍。

  • Who:是权限的拥有者或主体(如:User,Role)。
  • What:是操作或对象(operation,object)。
  • How:具体的权限(Privilege,正向授权与负向授权)。

:::

2.1 ABAC

::: tip ABAC介绍
ABAC(Attribute Base Access Control) 基于属性的权限控制,不同于常见的将用户通过某种方式关联到权限的方式,ABAC则是通过动态计算一个或一组属性来是否满足某种条件来进行授权判断(可以编写简单的逻辑)。属性通常来说分为四类:用户属性(如用户年龄),环境属性(如当前时间),操作属性(如读取)和对象属性(如一篇文章,又称资源属性),所以理论上能够实现非常灵活的权限控制,几乎能满足所有类型的需求。
访问控制列表(**ACL **)是一种基于包过滤的访问控制技术,它可以根据设定的条件对接口上的数据包进行过滤,允许其通过或丢弃。访问控制列表被广泛地应用于路由器和三层交换机,借助于访问控制列表,可以有效地控制用户对网络的访问,从而最大程度地保障网络安全。
:::

2.3 采坑bug修改

bug

将url.go 中的
bug1xxiu

修改为
bug1xiu

这个问题存在是由于应用源码对字符串的解析问题,可以不写端口,采用默认的端口

3 项目运行

官方代码下载后编译成keto.exe执行,直接执行指挥出现提示页面

3.1 代码示例

1
2
3
4
5
6
7
8
yaml复制代码dsn: mysql://root:minda123@tcp(127.0.0.1)/keto?parseTime=true&multiStatements=true 
# 这里如果用默认端口就不要加端口号:3306

secrets:
system:
- admin1
- admin2
- admin3
1
2
3
4
5
6
7
go复制代码>keto.exe --config F:/awesomeProject/bin/config.yaml migrate sql -e

time="2019-12-25T16:27:28+08:00" level=info msg="Connecting with mysql://*:*@tcp(127.0.0.1)/keto?multiStatements=true"
time="2019-12-25T16:27:28+08:00" level=info msg="Connected to SQL!"
time="2019-12-25T16:27:28+08:00" level=info msg="Applying storage SQL migrations..."
time="2019-12-25T16:27:28+08:00" level=info msg="Successfully applied SQL migrations" applied_migrations=1 migration=name
time="2019-12-25T16:27:28+08:00" level=info msg="Done applying storage SQL migrations"

3.2 启动服务

1
go复制代码serve --config F:/awesomeProject/bin/config.yaml

3.3 项目API

swagger安装教程

进入项目根目录,启动swagger服务

1
go复制代码swagger serve -F=swagger F:\awesomeProject\src\github.com\ory\keto\docs\api.swagger.json

运行成功后会提示服务运行在的地址,点击进入即可看到如下页面:
canvas

3.4 主要是要用的访问策略

ACL:

访问控制列表

blog_post.create blog_post.delete blog_post.modify blog_post.read
Alice yes yes yes yes
Bob no no no yes
Peter yes no yes yes

RBAC:

RBAC

4 ORY Access Control Policies

4.1 策略准备

put请求:http://127.0.0.1:4444//engines/acp/ory/glob/policies

1
2
3
4
5
6
json复制代码{
"subjects": ["alice"],
"resources": ["blog_posts:my-first-blog-post"],
"actions": ["delete"],
"effect": "allow"
}

同样:

1
2
3
4
5
6
7
8
9
10
json复制代码{
"subjects": ["alice", "bob"],
"resources": [
"blog_posts:my-first-blog-post",
"blog_posts:2",
"blog_posts:3"
],
"actions": ["delete", "create", "read", "modify"],
"effect": "allow"
}

会在数据库生成新的记录

1
2
3
4
5
6
7
8
9
10
json复制代码{
"subjects": ["peter"],
"resources": [
"blog_posts:my-first-blog-post",
"blog_posts:2",
"blog_posts:3"
],
"actions": ["delete", "create", "read", "modify"],
"effect": "deny"
}

The : is a delimiter in ORY Access Control Policies. Other supported syntax
is:

single symbol wildcard: ?at matches cat and bat but not at
wildcard: foo:*:bar matches foo:baz:bar and foo:zab:bar but not
foo:bar nor foo:baz:baz:bar
super wildcard: foo:**:bar matches foo:baz:baz:bar, foo:baz:bar, and
foo:bar, but not foobar or foo:baz
character list: [cb]at matches cat and bat but not mat nor at.
negated character list: [!cb]at matches tat and mat but not cat
nor bat.
ranged character list: [a-c]at cat and bat but not mat nor at.
negated ranged character list: [!a-c]at matches mat and tat but not
cat nor bat.
alternatives list: {cat,bat,[mt]at} matches cat, bat, mat, tat
and nothing else.
backslash: foo\\bar matches foo\bar and nothing else. foo\bar
matches foobar and nothing else. foo\*bar matches foo*bar and nothing
else. Please note that when using JSON you need to double escape backslashes:
foo\\bar becomes {"...": "foo\\\\bar"}.

The pattern syntax is:

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
json复制代码  pattern:

{ term }

term:

* matches any sequence of non-separator characters

** matches any sequence of characters

? matches any single non-separator character

[ [ ! ] { character-range } ]

character class (must be non-empty)

{ pattern-list }

pattern alternatives

c matches character c (c != *, **, ?, \, [, {, })

\ c matches character c

character-range:

c matches character c (c != \\, -, ])

\ c matches character c

lo - hi matches character c for lo <= c <= hi

pattern-list:

pattern { , pattern }

comma-separated (without spaces) pattern

4.2 json实例

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
json复制代码{
"description": "One policy to rule them all.",
"subjects": ["users:maria:*"],
"actions": ["delete", "create", "update","modify","get","read"],
"effect": "allow",
"resources": ["resources:articles:<.*>"],
"conditions": {
"someKeyName": {
"type": "StringMatchCondition",
"options": {
"matches": "foo.+"
}
},
"someKey": {
"type": "StringPairsEqualCondition",
"options": {}
},
"myKey": {
"type": "StringEqualCondition",
"options": {
"equals": "expected-value"
}
},
"remoteIPAddress": {
"type": "CIDRCondition",
"options": {
"cidr": "192.168.0.0/16"
}
},
"this-key-will-be-matched-with-the-context": {
"type": "SomeConditionType",
"options": {
"some": "configuration options set by the condition type"
}
}
},
"context": {
"someKey": [["foo", "foo"], ["bar", "bar"]]
}
}

4.3 主要请求及其说明

参数说明

响应参数说明

Name Type Required Restrictions Description
code integer(int64) false none none
details [object] false none none
additionalProperties object false none none
message string false none none
reason string false none none
request string false none none
status string false none none

请求参数说明

Parameter In Type Required Description
flavor path string true The ORY Access Control Policy flavor. Can be “regex”, “glob”, and “exact”.

4.4 检查请求是否允许通过

请求头

1
2
3
4
5
html复制代码POST /engines/acp/ory/{flavor}/allowed HTTP/1.1

Content-Type: application/json

Accept: application/json

body

1
2
3
4
5
6
7
8
9
json复制代码{
"action": "string",
"context": {
"property1": {},
"property2": {}
},
"resource": "string",
"subject": "string"
}

4.5 参数列表

OryAccessControlPolicyAllowedInput*

Name Type Required Restrictions Description
action string false none Action is the action that is requested on the resource.
context object false none Context is the request’s environmental context.
additionalProperties object false none none
resource string false none Resource is the resource that access is requested to.
subject string false none Subject is the subject that is requesting access.

response

{"allowed":"true"} or {"allowed":"false"}

5 访问控制策略操作

5.1 获取访问控制策略集合

1
2
bash复制代码GET /engines/acp/ory/{flavor}/policies HTTP/1.1
Accept: application/json

参数列表

Parameter In Type Required Description
flavor path string true The ORY Access Control Policy flavor. Can be “regex”, “glob”, and “exact”
limit query integer(int64) false The maximum amount of policies returned.
offset query integer(int64) false The offset from where to start looking.
subject query string false The subject for whom the policies are to be listed.
resource query string false The resource for which the policies are to be listed.
action query string false The action for which policies are to be listed.

5.2 更新访问控制策略

1
2
3
go复制代码PUT /engines/acp/ory/{flavor}/policies HTTP/1.1
Content-Type: application/json
Accept: application/json

参数列表

Parameter Type Required Restrictions Description
actions [string] false none Actions is an array representing all the actions this ORY Access Policy applies to.
conditions object false none Conditions represents a keyed object of conditions under which this ORY Access Policy is active.
additionalProperties object false none none
description string false none Description is an optional, human-readable description.
effect string false none Effect is the effect of this ORY Access Policy. It can be “allow” or “deny”.
id string false none 访问策略的唯一标识,用来查询,更新和删除
resources [string] false none Resources is an array representing all the resources this ORY Access Policy applies to.
subjects [string] false none Subjects is an array representing all the subjects this ORY Access Policy applies to.

5.3 查询具体的策略

1
2
bash复制代码GET /engines/acp/ory/{flavor}/policies/{id} HTTP/1.1
Accept: application/json

5.4 删除访问控制策略

1
2
bash复制代码DELETE /engines/acp/ory/{flavor}/policies/{id} HTTP/1.1
Accept: application/json

6 访问控制策略角色操作

6.1 查询寻访问控制角色集合

1
2
bash复制代码GET /engines/acp/ory/{flavor}/roles HTTP/1.1
Accept: application/json

参数说明:

Parameter In Type Required Description
flavor path string true The ORY Access Control Policy flavor. Can be “regex”, “glob”, and “exact”
limit query integer(int64) false The maximum amount of policies returned.
offset query integer(int64) false The offset from where to start looking.
member query string false The member for which the roles are to be listed.

6.2 添加访问控制的角色

1
2
3
bash复制代码PUT /engines/acp/ory/{flavor}/roles HTTP/1.1
Content-Type: application/json
Accept: application/json

例子:

1
2
3
4
json复制代码{
"id": "string",
"members": ["string"]
}

参数列表

Parameter Type Required Description
id string false ID is the role’s unique id.
members [string] false Members is who belongs to the role.

6.3 获取访问控制角色信息

1
2
bash复制代码GET /engines/acp/ory/{flavor}/roles/{id} HTTP/1.1
Accept: application/json

6.4 删除访问控制角色信息

1
2
bash复制代码DELETE  /engines/acp/ory/{flavor}/roles/{id} HTTP/1.1
Accept: application/json

6.5 为角色添加用户

1
2
3
4
5
6
bash复制代码PUT /engines/acp/ory/{flavor}/roles/{id}/members HTTP/1.1 Content-Type: application/json Accept: application/json

请求体:
{
"members": ["string"]
}

6.6从角色中删除某个用户成员

1
bash复制代码DELETE /engines/acp/ory/{flavor}/roles/{id}/members/{member} HTTP/1.1 Accept: application/json

7 健康检查

7.1 检查存活状态

1
2
bash复制代码GET /health/alive HTTP/1.1
Accept: application/json

结果:(官方说明总是ok)

1
json复制代码{  "status": "ok" }

7.2 检查准备就绪

1
2
bash复制代码GET /health/ready HTTP/1.1
Accept: application/json

7.3 获取当前版本

1
2
bash复制代码GET /version HTTP/1.1 
Accept: application/json

8 测试样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ruby复制代码put   http://127.0.0.1:4444/engines/acp/ory/glob/policies

{
"actions": ["get","create","modify","delete"],
"conditions": {
"optionAccess": {
"type": "CIDRCondition",
"options": {
"cidr": "192.168.0.0/16"
}
}
},
"description": "test q",
"effect": "allow",
"id": "string",
"resources": [
"blog_posts:my-first-blog-post",
"blog_posts:2",
"blog_posts:3"],
"subjects": ["admin","admin1","admin2"]
}

本文转载自: 掘金

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

秒杀系统设计 秒杀系统

发表于 2021-11-24

秒杀活动是指网络商家为促销等目的组织会网上限时抢购活动,这种活动具有瞬时并发量大、库存量少和业务逻辑简单等特点。设计一个秒杀系统需要考虑的因素很多,比如对现有业务的影响、网络带宽消耗以及超卖等因素。本文会讨论秒杀系统的各个环节可能存在的问题以及解决方案。

秒杀系统

傻瓜式秒杀系统

秒杀系统的核心难点是并发量,如果不考虑并发问题,那么我们可以用如下图所示的简单的系统结构来实现秒杀系统,用户只有两个简单操作:刷新界面和秒杀按钮,服务端也只有两个服务接口:返回秒杀界面和处理秒杀逻辑。假设本文中秒杀商品有100个,参与秒杀的用户有100w个。

傻瓜式秒杀系统

但是在高并发场景下,这个系统会有很多问题,我们全文会针对这些问题一一进行优化

  1. 大量用户同时刷新界面,会对服务器的带宽造成非常大的压力;
  2. 用户在秒杀前后可以多次重复点击按钮,造成很多不必要的请求;
  3. 用户可以通过脚本进行抢购,并且抢购成功率非常高;
  4. 服务端承受高并发请求,会出现响应过慢或失败等情况;
  5. 数据库承受高并发请求,会导致连接池耗尽和响应缓慢;
  6. 如果数据库更新设计的不合理,可能会出现超卖的情况;

秒杀界面CDN

秒杀开始之前,用户都会请求秒杀界面,有的用户甚至会不断的刷新秒杀界面,100W用户可能产生上千万次秒杀界面请求。秒杀界面往往包含很多静态资源,如果这些界面请求全部通过服务器获取,会造成大量的带宽消耗,甚至造成秒杀还没开始服务器就崩了的情况。

对于网页这种静态资源的并发访问,业内早就有成熟的解决方案:内容分发网络(CDN)。我们可以在秒杀开始前,预先把网页的静态资源存放在CDN节点,用户在刷新界面时直接从CDN获取静态资源,从而降低刷新秒杀界面对服务器造成的压力。添加了CDN服务之后,秒杀界面有大量用户同时访问和刷新并不会给服务端带来多大压力。

秒杀界面CDN

秒杀按钮优化

我们知道,秒杀系统往往会有一个秒杀按钮,如果不对按钮限制,可能存在以下问题:

  • 用户在秒杀开始前点击按钮,造成很多无用请求;
  • 用户在秒杀开始后多次点击按钮,造成很多重复请求;

所以我们可以对按钮做一些限制:秒杀开始前按钮不可用,用户点击一次秒杀按钮后,按钮也进入不可用状态。这种方式无法限制通过脚本请求后端的情况,但是可以限制正常用户的多次无效点击,大大降低请求量。

秒杀按钮优化

秒杀链接优化

普通情况下,用户在点击秒杀按钮的时候,前端会请求一个固定的URL,这个URL可以在前端界面查到。对于普通不懂技术的用户来说,这没有什么问题,如果用户稍微懂点Http协议,就可以在秒杀开始前拿到URL,在秒杀开始前或开始的毫秒级时间内请求秒杀链接,不仅会给服务端带来很大的压力,还会造成不公平现象:商品都被开脚本的人抢走了。

为了避免这种现象,我们可以将URL动态化,即使秒杀系统的开发人员也无法在知晓在秒杀开始时的URL。具体实现方法是在获取秒杀URL的接口中,返回一个服务器端生成的随机数,并在下单URL中传递该参数完成下单。

秒杀链接优化

秒杀验证码

虽然说我上面通过动态URL避免了用户在秒杀开始前请求秒杀链接,但是用户还是可以通过脚本在秒杀开始的那一刻去请求秒杀连接,普通用户基本没有办法和脚本秒杀进行竞争。

我们可以引入机器难以识别的验证码,用户在请求秒杀链接之前,需要填写验证码识别的结果,验证码错误的请求直接拒绝。使用验证码不仅可以增加脚本秒杀的难度,还可以降低请求的QPS,因为请求不再是在秒杀那一刻进来,而会被分散到填写验证码的时间段内。

秒杀按钮优化

过滤请求

通过上面的步骤,我们可以减少很多重复请求和脚本请求,可以保证秒杀活动中一个人大致只会请求一次(脚本还是可以请求多次)。但是100W人参与秒杀,每人请求一次秒杀链接也有将近100W次请求,服务器还是扛不住。

仔细分析之后可以发现,秒杀的商品只有100个,最后成功的也只有100个,那么我们100W的请求是不是都有必要请求到秒杀服务器上呢?显而易见,我们没有必要把所有请求都打到秒杀服务器上,我们只需要保证有大于100个请求打到秒杀服务器就可以保证秒杀的正常进行,所以我们可以在用户端和服务端添加一层过滤层,过滤层只要保证有100个以上的请求能打到秒杀服务器端。

我们可以使用Nginx服务器来构建过滤层,一个Nginx服务器也没法抗100W的请求,我们假设每个Nginx服务器可以处理10W的请求,那么我们就需要10台Nginx。那么怎么用保证至少有100个请求可以请求到后端呢?我们可以简单的让每个Nginx服务器只通过前100个请求,后续请求直接返回降级界面。通过Nginx过滤,我们可以把100W的请求过滤为1000个请求,大大减少了服务器端的压力。

Nginx过滤请求

Redis缓存

如果通过前面的过滤,请求量依旧非常大,如果数据库无法处理这些请求量,我们就需要在数据库之上添加一层Redis缓存了。单个Redis可以处理几万的QPS,如果预估请求的QPS大于几万,我们还可以使用Redis集群模式来增加Redis的处理能力。

在Redis存放和售卖商品数目大小相同的数字,秒杀服务每次访问数据库之前,都需要先去Redis中扣减库存,扣减成功才能继续更新数据库。这样,最终到的数据库的请求数目和需要售卖商品的数目基本一致,数据库的压力可以大大减少。

Redis原子性

我们知道Redis是不支持事务的,所以可能出现扣减为负数的情况,这种情况下我们可以使用Lua脚本来保证一次扣减操作的原子性,从而保证扣减结果的正确性。

Redis缓存

异步更新数据库

通过Redis判断之后,去更新数据库的请求都是必要的请求,这些请求数据库必须要处理,但是如果数据库还是处理不过来这些请求怎么办呢?

这个时候就可以考虑削峰填谷操作了,削峰填谷最好的实践就是MQ了。经过Redis库存扣减判断之后,我们已经确保这次请求需要生成订单,我们就可以通过异步的形式通知订单服务生成订单并扣减库存。

异步更新数据库

我是御狐神,欢迎大家关注我的微信公众号:wzm2zsd

本文最先发布至微信公众号,版权所有,禁止转载!

本文转载自: 掘金

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

Rust - 变量与数据的交互方式(move) 变量与数据的

发表于 2021-11-24

这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战

上一篇文章中,对Rust的内存和分配做了简单介绍,当变量离开作用域的时候,Rust就会自动调用drop函数对内存进行回收,看起来简单的原理,但是在更复杂的场景下代码的行为可能是不可预测的,比如当有多个变量使用在堆上分配的内存时。现在让我们探索一些这样的场景。

变量与数据的交互方式 - 移动

Rust 中的多个变量可以采用一种比较独特的方式和同一个数据进行交互,如下代码所示,将变量x的值赋给y:

1
2
3
4
rust复制代码fn main() {
let x = 1;
let y = x;
}

我们大概可以推论出上述代码的原理:将1这个整数绑定给x变量,let y = x相当于创建了一个x的副本,并且将这个副本绑定给了y。现在有了两个变量,x 和 y,都等于 1。这也正是事实上发生了的,因为整数是有已知固定大小的简单值,所以这两个 1 被放入了栈中。

上面是已知固定大小的简单例子,现在看一下复杂的例子就是String。

1
2
3
4
rust复制代码fn main() {
let str1 = String::from("hello");
let str2 = str1;
}

上述代码看起来和整数的例子非常相似,所以我们可能会假设他们的运行方式也是类似的:也就是说,第二行可能会生成一个 str1 的副本并绑定到 str2 上。不过,事实上并不完全是这样。

首先我们需要知道String底层是什么样的,String在内存中由三部分组成,如下如所示,是将值hello绑定给str1的String在内存中的表现形式。一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据存储在栈上。右侧则是堆上存放内容的内存部分。

图1

图1
长度表示 String 的内容当前使用了多少字节的内存。容量是 String 从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过在当前上下文中并不重要,所以现在可以忽略容量。

当我们将 str1 赋值给 str2,String 的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据。换句话说,内存中数据的表现如下图所示。

image-20211122150620699

图2
这个表现形式看起来 并不像 下图 中的那样,如果 Rust 也拷贝了堆上的数据,那么内存看起来就是这样的。如果 Rust 这么做了,那么操作 str2 = str1 在堆上数据比较大的时候会对运行时性能造成非常大的影响。

image-20211122150730454

图3
之前我们提到过当变量离开作用域后,Rust 自动调用 drop 函数并清理变量的堆内存。不过图 2 展示了两个数据指针指向了同一位置。这就有了一个问题:当 str2 和 str1 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。

为了确保内存安全,这种场景下 Rust 有另一个独到的处理。与其尝试拷贝被分配的内存,Rust 则认为 str1 不再有效,因此 Rust 不需要在 str1 离开作用域后清理任何东西。看看在 str2 被创建之后尝试使用 str1 会发生什么:

1
2
3
4
5
6
rust复制代码fn main() {
let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);
}

运行cargo run就会报错,因为Rust禁止使用无效的引用:

1
2
3
4
5
6
7
8
9
10
11
rust复制代码error[E0382]: use of moved value: `s1`
--> src/main.rs:5:28
|
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value used here after move
|
= note: move occurs because `s1` has type `std::string::String`, which does
not implement the `Copy` trait

如果你在其他语言中听说过术语 浅拷贝(shallow copy)和 深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动(move),而不是浅拷贝。上面的例子可以解读为 s1 被 移动 到了 s2 中。那么具体发生了什么,如下图 所示。

image-20211122151302629

这就解决了二次释放的错误,因为只有 s2 是有效的,当其离开作用域,它就释放自己的内存,完毕。

另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动 的复制可以被认为对运行时性能影响较小。

结语

文章首发于微信公众号程序媛小庄,同步于掘金。

码字不易,转载请说明出处,走过路过的小伙伴们伸出可爱的小指头点个赞再走吧(╹▽╹)

本文转载自: 掘金

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

1…211212213…956

开发者博客

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