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

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


  • 首页

  • 归档

  • 搜索

基于消息队列 RocketMQ 的大型分布式应用上云最佳实践

发表于 2021-11-09

简介: Apache RocketMQ 作为阿里巴巴开源的支撑万亿级数据洪峰的分布式消息中间件,在众多行业广泛应用。在选型过程中,开发者一定会关注开源版与商业版的业务价值对比。 那么,今天就围绕着商业版本的消息队列 RocketMQ和开源版本 RocketMQ 进行比较,并结合实践中场景全面展示大型分布式应用的上云最佳实践。

作者|绍舒

前言

消息队列是分布式互联网架构的重要基础设施,在以下场景都有着重要的应用:

  • 应用解耦
  • 削峰填谷
  • 异步通知
  • 分布式事务
  • 大数据处理

并涉及互动直播、移动互联网&物联网,IM 实时通信、Cache 同步、日志监控等多个领域。

而本文主要围绕着商业版本的消息队列 RocketMQ,和开源版本 RocketMQ 进行比较,并结合一些实践中的场景来展示大型分布式应用的上云最佳实践。

核心能力

商业版本消息队列 RocketMQ 相比较开源版本 RocketMQ 和其他竞品,主要有以下几点优势。

  1. 开箱即用、功能丰富
  2. 高性能、无限扩展能力
  3. 可观测、免运维能力
  4. 高 SLA 和稳定性保证

开箱即用、功能丰富

消息队列 RocketMQ 提供了定时、事务、顺序等多类型消息的支持,且支持广播、集群两种消费模式;另外在协议层面,提供 TCP/HTTP 多协议支持,还提供了 TAG/SQL 属性过滤功能,极大程度地拓宽了用户的使用场景。

高性能、无限拓展能力

消息队列 RocketMQ 经受了阿里核心电商历年双十一洪峰的考验,支持千万级 TPS 消息收发和亿级消息堆积的能力,并且能够为消息提供毫秒级端到端延迟保障,另外还提供分级存储,支持海量消息的任意保存时间。

可观测、免运维能力

消息队列 RocketMQ 提供了一个可观测性大盘,支持细粒度数据大盘,提供了消息全链路生命周期追踪和查询能力,对各个指标提供了相应的监控报警功能;此外,还提供了消息回溯和死信队列功能,能够保证用户的消息能够随时回溯消费。

高 SLA 和稳定性保障

消息队列 RocketMQ 的稳定性是我们一贯、持续、稳定投入的重要领域,提供了高可用部署和多副本写入功能;另外也支持同城多 AZ 容灾和异地多活。

产品剖面

接下来,我们会从以上的产品核心能力中挑选几个剖面,并且结合具体的场景和实践来做进一步的介绍。

多消息类型支持

高可用顺序消息

商业版本消息队列 RocketMQ 使用的顺序消息我们称之为高可用顺序消息。在介绍高可用顺序消息之前,首先简要介绍下开源版本 RocketMQ 的顺序消息。

顺序消息分为两种类型,全局顺序消息和分区顺序消息。

  • 全局顺序消息:在 RocketMQ 存储层只会分配一个分区,也就是说全局顺序 Topic 的可用性跟单一副本的可用性强相关,且不具备可扩展的能力。
  • 分区顺序消息:所有消息根据 Sharding Key 进行分区。同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding Key 是顺序消息中用来区分不同分区的关键字段。

下图是分区顺序消息的应用场景,order ID 即为此时顺序消息的 Sharding Key。

可以看到,无论是全局顺序消息还是分区顺序消息,都依赖了单一分区天然的 FIFO 特性来保证顺序,因此顺序性也只能在同一个分区内保证,当此分区所在的副本不可用时,顺序消息并不具备重试到其他副本的能力,此时消息的顺序性就难以得到保证。

为了解决这一问题,我们设计并实现了高可用顺序消息。

高可用顺序消息有以下几个特点:

  • 一个逻辑顺序分区(PartitionGroup)下有多个物理分区。
  • 其中任意一个物理分区是可写的,那么整个逻辑分区是可写且有序的。
  • 我们基于 happened-before 的原则设计了一套基于分区位点的排序算法。
  • 根据该算法,消费者在消费某一逻辑分区时,会从其所属的各个物理分区中拉取消息并进行合并排序,得出正确的消息顺序流。

通过这样的设计,高可用顺序消息解决了下列几点问题:

  • 可用性问题:高可用顺序消息将具备与普通消息一致的可用性,在某副本不可用时,可快速重试至其它副本。
  • 可扩展性问题:普通顺序消息,特别是普通全局顺序消息,不具备良好的扩展能力,只能固定在特定的副本中。高可用顺序消息的逻辑顺序分区可以将物理顺序分区分散在多个副本中。
  • 热点问题:普通顺序消息根据 Key 将一类消息 Hash 至同一个分区中,热点 Key 会导致热点分区,高可用顺序消息具备横向扩展能力,可以为逻辑顺序分区添加多个物理分区来消除热点问题。
  • 单点问题:普通全局顺序消息,仅包含单分区,极易出现单点故障,高可用顺序消息可以消除全局顺序消息的单点问题。

尤其需要注意的是热点问题,在阿里巴巴内部某电商业务大促时,因发送到顺序 Topic 的某一特定的 ShardingKey 数量过多,集群中一个副本接收到了大量该 ShardingKey 的消息,导致该副本超出其负荷上限,造成了消息的延迟和堆积,一定程度上影响了业务。在使用了高可用顺序消息之后,由于其在多物理分区中的负载均衡特性,提升了集群顺序消息的承载能力,从而避免了热点问题的出现。

秒级精准定时消息

定时消息,是指客户端当前发送但希望在未来的某个时间内收到的消息。定时消息广泛应用于各类调度系统或者业务系统之中。比如支付订单,产生一个支付消息,系统通常需要在一定时间后处理该消息,判断用户是否支付成功,然后系统做相应处理。

开源版本的 RocketMQ 只支持几个指定的延迟级别,并不支持秒级精度的定时消息。而面向集团内和云上多样化的需求,开源版本的定时消息并不能满足我们的需求,因此我们推出了秒级精准定时消息。

如下图所示,我们基于时间轮设计并实现了支持任意定时时间的秒级精准定时消息,同时满足以下特性:

  • 任意定时时间
  • 超长定时时间
  • 海量定时消息
  • 删除定时消息
  • 高可用
  • 高性能

内部某用户有这样的场景,期望在未来的某一分钟的 30s 时刻处理这样一个定时请求,开源版本的定时消息并不符合其需要,而秒级精准定时消息在保证高可用、高性能的同时,满足了其业务需求。

分布式事务消息

如下图所示,在传统的事务处理中,多个系统之间的交互耦合到一个事务中,造成整体的相应时间长,回滚过程复杂,从而潜在影响了系统的可用性;而 RocketMQ 提供的分布式事务功能,在保证了系统松耦合和数据最终一致性的前提下,实现了分布式事务。

消息队列 RocketMQ 提供的事务消息处理步骤如下:

  • 发送方将半事务消息发送至消息队列 RocketMQ 版服务端。
  • 消息队列 RocketMQ 版服务端将消息持久化成功之后,向发送方返回 Ack 确认消息已经发送成功,此时消息为半事务消息。
  • 发送方开始执行本地事务逻辑。
  • 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接受该消息。

基于这样的实现,我们通过消息实现了分布式事务特性,即本地事务的执行结果会最终反应到订阅方是否能接收到该条消息。

消息队列 RocketMQ 的分布式事务消息广泛地应用于阿里巴巴核心交易链路中,通过分布式事务消息,实现了最小事务单元;交易系统和消息队列之间,组成一个事务处理;下游系统(购物车、积分、其它)相互隔离,并行处理。

分级存储

背景

随着云上客户的不断增多,存储逐渐成为 RocketMQ 运维的重要瓶颈,这包括并且不限于:

  1. 内存大小有限,服务端不能将所有用户的数据全部缓存在内存中;在多租户场景下,当有用户拉取冷数据时,会对磁盘造成较大 IO 压力,从而影响共享集群的其他用户,亟需做到数据的冷热分离。
  2. 云上有单租户定制化消息存储时长的需求。而 RocketMQ Broker 中所有用户的消息是放在一个连续文件中进行存储的,无法针对任何单一用户定制存储时长,即现有的存储结构无法满足这样的需求。
  3. 如果能对海量数据提供更低成本的存储方式,可以大幅降低云上 RocketMQ 的磁盘存储成本。

基于以上现状,分级存储方案应运而生。

架构

分级存储的整体架构如下:

  1. connector 节点负责将 broker 上的消息实时同步到 OSS 上
  2. historyNode 节点将用户对冷数据的拉取请求转发至 OSS 上
  3. 在 OSS 中是按照 Queue 粒度来组织文件结构的,即每个 Queue 会由独立的文件进行存储,从而保证了我们可以针对于租户定义消息的存储时长。

通过这样的设计,我们实现了消息数据的冷热分离。

使用场景

基于分级存储,我们进一步拓展了用户的使用场景:

  1. 自定义存储时间:在消息数据的冷热分离之后,我们将冷数据存储到 OSS 这样的存储系统中,能够实现用户自定义的存储时间。
  2. 消息审计:在消息的存储之间从数天扩展到自定义后,消息的属性从一个临时性的中转数据变成了用户的数据资产,而消息系统也从数据中枢转变成了数据仓库;用户能够基于数据仓库实现更多样的审计、分析、处理功能。
  3. 消息回放:在流计算场景中,消息回放是非常重要的一个场景;通过拓展消息的存储时间之后,流计算能够实现更加丰富的计算分析场景。

稳定性

消息队列 RocketMQ 的稳定性是我们一贯、持续、稳定投入的重要领域。在介绍我们在稳定性的最新工作之前,首先带大家回顾下 RocketMQ 高可用架构的演进路线。

高可用架构演进路线

2012 年,RocketMQ 作为阿里巴巴全新一代的消息引擎问世,并随后开源至社区,第一代 RocketMQ 高可用架构也随之诞生。如下图所示,第一代高可用架构采取当时流行的 Master-Slave 主从架构,写流量经过 Master 节点同步至 Slave 节点,读流量也经过 Master 节点并将消费记录同步至 Slave 节点。当 Master 节点不可用时,整个副本组可读不可写。

2016 年,RocketMQ 云产品正式开始商业化,云时代单点故障频发,云产品需要完全面向失败而设计,因此 RocketMQ 推出了第二代多副本架构,依托于 Zookeeper 的分布式锁和通知机制,引入 Controller 组件负责 Broker 状态的监控以及主备状态机转换,在主不可用时,备自动切换为主。第二代架构是消息云产品规模化进程中的核心高可用架构,为云产品规模化立下了汗马功劳。

2018 年,RocketMQ 社区对 Paxos 和 Raft 引入分布式协议有极大的热情,RocketMQ 研发团队在开源社区推出了基于 Raft 协议的 Dledger 存储引擎,原生支持 Raft 多副本。

RocketMQ 高可用架构已经走过了三代,在集团、公有云和专有云多样场景的实践中,我们发现这三套高可用架构都存在一些弊端:

  • 第一代主备架构只起到了冷备的作用,且主备切换需要人工介入,在大规模场景下有较大的资源浪费以及运维成本。
  • 第二代架构引入了 Zookeeper 和 Controller 节点,架构上更加复杂,在主备切换做到了自动化,但故障转移时间较长,一般是 10 秒左右完成选主。
  • 第三代 Raft 架构目前暂未在云上和阿里集团内大规模应用,且 Raft 协议就决定了需要选主,新主还需要被客户端路由发现,整个故障转移时间依然较长;另外,强一致的 Raft 版本并未支持灵活的降级策略,无法在可用性和可靠性之间做灵活的权衡。

为了应对云上日益增长的业务规模、更严苛的 SLA 要求、复杂多变的专有云部署环境,当前的消息系统需要一种架构简单、运维简单、有基于当前架构落地路径的方案,我们将其称作秒级 RTO 多副本架构。

新一代秒级 RTO 多副本架构

秒级 RTO 多副本架构是消息中间件团队设计实现的新一代高可用架构,包含副本组成机制、Failover 机制、对现有组件的侵入性修改等。

整个副本组有以下特点:

  • Strong Leader/No Election:Leader 在部署时确定,整个生命周期内不会发生切换,但可在故障时被替换。
  • 仅 Leader 支持消息写入:每一个副本组仅 Leader 接受消息写入,Leader 不可用时,整个副本组不可写入。
  • 所有的副本支持消息读取:虽然 Leader 上拥有全量的消息,Follower 上的消息量不对等,但所有的副本都支持消息的读取。
  • 灵活的副本组数量:可以基于可靠性、可用性和成本自由选择副本组的数量。
  • 灵活的 Quorum 数量:最终所有的消息都会同步到整个副本组上,但副本组内可以灵活配置写成功最小副本数。例如 2-3 模式,3 副本情况下,2 副本成功即为写成功。同时,在副本不可用的情况下,Quorum 数量也可以动态自行降级。

在上述副本组的概念下,故障转移可以复用当前 RocketMQ 客户端的机制来完成。如下图所示:

  • Producer 在主不可用时,灵活快速地切换至另一个副本组。
  • Consumer 在某个副本不可用时可快速切换至同副本组另一个副本上进行消息消费。

可观测性

健康大盘

我们在可观测性方面也做了大量的工作,为用户提供了一个消息系统的可观测性健康数据大盘。如下图所示,用户能够清晰的看到实例级别、topic 级别、group 级别的各种监控数据,能够全方面地监控、诊断问题。

消息链路追踪

另外我们还基于消息轨迹提供了消息全链路轨迹追踪功能。如下图所示,用户能够在控制台上看到完整的消息生命周期、从消息的发送、存储、到消费,整个链路都能被完整地记录下来。

应用场景

客户痛点:业务出现消费堆积的用户需要根据消息轨迹抽样数据,综合分析后才能大致判断引起问题原因,排查困难。

核心价值:提高线上运行问题排查的效率,和问题定位的准确性。直接在健康大盘上快速发现风险最高的 Topic 和 Group,并根据各个指标的变化情况快速定位原因。例如消息处理时间过长可以扩容消费者机器或优化消费业务逻辑,如果是失败率过高可以快速查看日志排除错误原因。

事件驱动

大家一定非常熟悉 Gartner,在2018年的一个评估报告里,Gartner 将 Event-Driven Model,列为了未来10大战略技术趋势之一,并且,做出了两个预测:

  • 2022年,超过 60% 的新型数字化商业解决方案,都会采用事件通知的软件模型。
  • 2022年,超过 50% 的商业组织,将会参与到EDA生态系统当中去。

同一年,CNCF 基金会也提出了 CloudEvents,意在规范不同云服务之间的事件通讯协议标准。到目前为止,CloudEvents也已经发布了多个消息中间件的绑定规范。

可见事件驱动是未来业务系统的一个重要趋势,而消息天然具备和事件的亲近性,因此消息队列 RocketMQ,是坚决拥抱事件驱动的。

谈到消息和事件,这里做一个简单的阐述:消息和事件是两种不同形态的抽象,也意味着满足不同的场景:

  • 消息:消息是比事件更通用的抽象,常用于微服务调用之间的异步解耦,微服务调用之间往往需要等到服务能力不对等时才会去通过消息对服务调用进行异步化改造;消息的内容往往绑定了较强的业务属性,消息的发送方对消息处理逻辑是有明确的预期的。
  • 事件:事件相对于消息更加具像化,代表了事情的发送、条件和状态的变化;事件源来自不同的组织和环境,所以事件总线天然需要跨组织;事件源对事件将被如何响应没有任何预期的,所以采用事件的应用架构是更彻底的解耦,采用事件的应用架构将更加具备可扩展性和灵活性。

在2020年,阿里云发布了事件总线 EventBridge 这一产品,其使命是作为云事件的枢纽,以标准化的 CloudEvents 1.0 协议连接云产品和云应用,提供中心化的事件治理和驱动能力,帮助用户轻松构建松耦合、分布式的事件驱动架构;另外,在阿里云之外的云市场上有海量垂直领域的 SaaS 服务,EventBridge 将以出色的跨产品、跨组织以及跨云的集成与被集成能力,助力客户打造一个完整的、事件驱动的、高效可控的上云新界面。

而借助事件总线 EventBridge 提供的事件源功能,我们能够打通消息到事件的链路,使得消息队列 RocketMQ 具备事件驱动的动力,从而拥抱整个事件生态。接下来我们将借助一个案例,如下图所示,为大家展示这一功能。

创建消息队列 RocketMQ 主题

创建目标服务

我们基于容器服务快速创建一个事件驱动的服务,计算负载 Deployment 的 yaml 如下,该服务能够响应事件并将结果打印到标准输出中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
yaml复制代码apiVersion: apps/v1 # for versions before 1.8.0 use apps/v1beta1
kind: Deployment
metadata:
name: eventbridge-http-target-deployment
labels:
app: eventbridge-http-target
spec:
replicas: 2
selector:
matchLabels:
app: eventbridge-http-target
template:
metadata:
labels:
app: eventbridge-http-target
spec:
containers:
- name: eb-http-target
# 下述镜像暴露了一个 HTTP 地址(/cloudevents)用于接收 CloudEvents,源码参考:https://github.com/aliyuneventbridge/simple-http-target
image: registry.cn-hangzhou.aliyuncs.com/eventbridge-public/simple-http-target:latest
ports:
- containerPort: 8080

前往容器服务控制台,进入服务与路由的服务页面,创建一个私网访问类型的 Service,并做好端口映射。

创建事件总线 EventBridge 自定义总线

我们来到事件总线 EventBridge 控制台,创建一个自定义总线 demo-with-k8s。

创建事件总线 EventBridge 自定义总线规则

我们为总线 demo-with-k8s 创建一个规则,并选择 HTTP 作为事件目标,选择专有网络类型,选中对应的 VPC、 VSwitch 以及安全组,并指定目标URL,如下图所示:

创建事件总线 EventBridge 事件源

我们为该自定义事件总线添加消息队列 RocketMQ 版的自定义事件源。

发送 RocketMQ 消息

接下来我们回到消息队列 RocketMQ 控制台,通过控制台的快速体验消息生产功能发送一条内容为 hello eventbridge 的消息到对应的主题中去。

接下来我们就可以发现,这条 RocketMQ 消息,以 CloudEvent 的形式被投递到了对应的服务中去,我们从而打通了消息到事件的链路。同时,基于我们上述提到的分级存储功能,消息队列 RocketMQ 转变成了一个能够源源不断提供事件的数据仓库,为整个事件生态提供了更加广阔的场景。

事件驱动是未来商业组织和业务系统的重要趋势,而消息队列 RocketMQ 会坚定地拥抱这一趋势,将消息融入到事件的生态中。

总结

我们选取了消息队列 RocketMQ 的几个产品剖面,从多消息类型、分级存储到稳定性、可观测性,再到面向未来的事件驱动,并结合与开源 RocketMQ 的对比,及具体应用场景的分析,为大家展示了基于消息队列 RocketMQ 的大型分布式应用上云最佳实践。

原文链接

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

本文转载自: 掘金

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

『十倍程序员』IDEA插件精选

发表于 2021-11-09

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

精选

GitToolBox

提升 Git 使用效率

Request Mapper

在全局搜索中加入url搜索

JPA Buddy

JPA增强插件

JPA Support

JPA增强插件

Translation

翻译插件,现支持Google翻译、有道翻译。

Jrebel

热部署插件

Alibaba Java Coding Guidelines

阿里巴巴Java规范插件,可实时检查代码规范,代码规范文档见: 阿里巴巴Java开发手册(泰山版).pdf

Git:github.com/alibaba/p3c…

Maven Helper

Maven依赖分析功能,非常好用。

GsonFormat

json->object,提高三方接口对接开发效率。

RestfulToolkit

Restful工具集,支持SpringMVC下的URL导航到对应方法等。

Lombok plugin

Lombok必备。GitHub

SequenceDiagram

生成方法调用的时序图

www.cnblogs.com/QuestionsZh…

String Manipulation

字符串处理,提供驼峰、下划线,base64、md5

SonarLint

静态代码质量本地扫描插件

CheckStyle-IDEA

This plugin provides both real-time and on-demand scanning of Java files with CheckStyle from within IDEA.

Codota

代码智能提示工具,提供查找代码片段的功能。强力推荐,贼好用。

PlantUML(程序员画图神器)

画UML工具,后续可以考虑用该插件画 UML 图,可以做到 UML 跟着项目走。

Rainbow Brackets

彩虹括号,在有多个括号的情况下,不会看花眼。

Convert YAML and Properties File

YAML 和 Properties 格式互转

Free Mybatis plugin

提供xml和dao层的互相跳转功能,mapper生成xml

Mybatis Log Plugin

控制台输出SQL语句

Save Actions

保存时自动格式化代码、优化import

Git Commit Template

Git提交模板

HighlightBracketPair

高亮显示光标所在代码块对应的括号

Maven Helper

使用Maven必装,依赖冲突分析

选装

.ignore

.ignore文件支持

BashSupport

Linux Bash支持

Docker integration

Docker支持

JMH plugin

JMH基准测试支持插件

ANSI Highlighter

log文件高亮支持

Ideolog

快速跳转到日志文件中Error位置等

blog.csdn.net/linzi1994/a…

CodeGlance

预览文本,参见 sublime

VisualVM Launcher

运行时启动 visualvm , jvm 本地调试、调优用

GenerateAllSetter

生成一个对象所有 set 法调用并赋上默认值,如果用了 lombok 就没啥用了。

MyBatisCodeHelperPro

mybatis代码自动生成插件(收费),强力推荐,贼好用,已用几年。

Material Theme UI

眼睛舒适的主题,部分主题真心不错,个人还是挺推荐的。

IDEA Mind Map

IDEA 思维导图工具。

Git Flow Integration

Git Flow 集成

Key Promoter X

IDEA快捷键提示工具

Grep Console

通过expression表达式过滤日志、给不同级别的日志或者给不同pattern的日志加上背景颜色与上层颜色

Presentation Assistant

快捷键操作可视化展示

JunitGenerator V2.0

自动生成junit4单元测试用例

Junit5Helper

自动生成junit5单元测试用例

HighlightBracketPair

高亮括号对

好玩推荐

leetcode-editor

力扣刷题神器,可以直接关联leetcode账号

GitHub in Idea

无需打开浏览器就能搜索github上的资源

Stock

Idea看股票涨跌走势

thief-book-idea

上班摸鱼神器

Upsource Integration

代码审查平台Upsource的idea客户端

本文转载自: 掘金

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

MySQL进阶系列:一条sql是怎么执行的

发表于 2021-11-09

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

本文是在存储引擎是InnoDB的前提下

mysql中的针对表的操作可以分为增删改查四种操作,也就是常说的crud大法,根据类型分为DML(增删改)和DQL(查);今天就说下插入和查询的语句时如何mysql中执行的。

不管是DML还是DQL都是要经过连接器,缓存,分析器,优化器,执行器调用存储引擎的API。在前四个阶段都是一样的流程,具体的可以参考 mysql基础架构篇,文章中有详细简介各个模块的作用,本文就不展开说了,后面我们详细说说执行器在存储引擎上是怎么查询和修改(删除/新增)的。

小知识:

扇区: 磁盘存储的最小单位,扇区一般大小为512Byte。

磁盘块: 文件系统与磁盘交互的的最小单位(计算机系统读写磁盘的最小单位),一个磁盘块由连续几个(2^n)扇区组成,块一般大小一般为4KB。

mysql页: mysql中和磁盘交互的最小单位称为页,默认大小是16kb,也就是4个磁盘块。也就是说mysql在进行数据读取的是默认情况下一次就是读取16kb(可以修改),即是我只查询一条大小只有1kb的数据,mysql读取的也是16kb。

一条查询语句是怎么执行的

select查询比较简单,其实就是到硬盘上按照页(16kb)把数据加载到内存,然后再去匹配where条件,找出符合条件的数据;

  1. 如果where条件没有索引,那么就是全表扫描,一次次的加载数据页到内存,然后一个个匹配。
  2. 如果where条件是主键索引,那么就会把主键索引的数据页加载到内存,然后匹配到具体的行数据返回;
  3. 如果where条件是普通索引,那么就会把普通索引的数据页加载到内存,然后匹配到符合条件的叶子结点(B+树的叶子结点),如果能够使用覆盖索引,那么就会直接返回,如果不能使用覆盖索引,则会进行回表查询(走一次主键索引查询)。覆盖索引和回表不清楚的可以参考历史文章:需要知道的索引基础知识。

一条更新语句是怎么执行的

更新语句首先要按照查询的流程执行,因为肯定要先知道是哪条记录,之后才能去更新这条记录。之后就是更新了,更新操作涉及到几个日志的记录,分别是undolog(InnoDB的回滚日志) ,redolog(InnoDB的数据持久化日志),binlog(mysql server的归档日志),下面我们看下他们之间是怎么配合完成数据更新的。

  1. 首先执行引擎按照条件找数据,如果内存中存在则直接返回,不存在查询后返回。
  2. 把查询出来的这条记录先放到undolog中,用于更新失败数据回滚,具体的使用可以参考上一篇文章多并发控制mvcc。
  3. 执行器调用存储引擎接口写入数据,在缓存池中修改这条记录,。
  4. 写redo日志,先把数据更新到redo日志的缓存中。
  5. 准备提交事务,把redo日志缓存刷入磁盘。这时候redolog 是prepare阶段(共有两个阶段prepare和commit),然后通知执行器完成。
  6. 执行器开始执行binlog写入磁盘。
  7. 然后调用存储引擎的事务提交接口,把redolog中的prepare改成commit状态,至此更新完成。
  8. 之后有IO线程以页为单位随机写入磁盘,把我们更新后的数据慢慢落入磁盘中。

两个日志是分开写的,所以很难保证两个日志数据一致,在恢复的时候尽量保证同时参考两个日志文件,如果一致才会提交,不一致丢弃

假如在步骤8的时候更新失败,两种情况

  • sql执行失败,这时候使用undolog中记录的历史数据进行恢复即可。
  • 断电或者进程重启,那么就会在下次重启的时候判断redolog是否是commit状态,如果是可以直接提交写入磁盘,如果不是commit状态,就会判断binlog是否完整(两阶段提交保证数据的一致性),如果完整那么数据有效,写入磁盘;如果不完整或者只有一个日志存在记录,直接丢弃即可。

为什么要用日志而不是直接写入数据库所在的磁盘呢

我们要更新的数据是存在磁盘的任意位置,是属于随机IO,效率是很低的,这样mysql的并发无法保证,而redo日志和binlog属于预写日志,都是顺序IO,直接写入即可,效率要高很多,即使失败了也可以根据日志恢复。

为什么要设计两阶段提交:

写入的新数据属于脏页,只有更新到磁盘才能是完整的数据

反证一下:

假如先写redo log 后写binlog: 如果在redolog写完,binlog还没写完,mysql崩溃重启。重启之后可以通过redolog恢复数据,但是binlog是没有这条数据的,所以后续用binlog备份数据或者进行主从同步的时候都会丢失这条数据,这样和原库的数据是不一致的。

假如先写binlog后写redolog: 在binlog写完之后mysql崩溃,由于redolog 没有写入,奔溃重启之后也不会也不会恢复数据,但是binlog中已经完整记录这条记录,所以之后binlog备份和主从同步会有这条数据,这样和原库又不一致了。

网上看的一张图,画的非常好,可以收藏一下

image.png

总结一下

  1. 不管是查询语句还是更新语句,都要先经过连接器,查询缓存(8.0已经去掉了),分析器,优化器,执行器。
  2. 查询语句要选按照查询条件把数据所在的整页加载到内存。
  3. 更新语句在查询的基础上利用undo log,redo log,bin log 完成数据的更新。
  4. undo log是用来sql执行失败之后回滚数据,保持事务的原子性。
  5. redo log是用于mysql崩溃恢复,保证已提交事务的ACID特性。
  6. binlog是数据记录的日志文件,用于数据备份,主从同步。
  7. 直接更新记录然后刷盘是随机IO,效率低下,所以使用redolog顺序写,提高效率。
  8. 两阶段提交能够崩溃恢复,保证数据的一致性。

欢迎评论区沟通交流


我是纪先生,用输出倒逼输入而持续学习,持续分享技术系列文章,以及全网值得收藏好文,欢迎关注公众号,做一个持续成长的技术人。

mysql进阶系列历史文章(也可以在掘金专栏中看其他相关文章)

1. MySQL进阶系列:一文了解mysql基础架构;

2. MySQL进阶系列:一文了解mysql存储引擎;

3. MySQL进阶系列:mysql中MyISAM和InnoDB有什么区别;

4. MySQL进阶系列:mysql中表设计如何更好的选择数据类型;

5. MySQL进阶系列:数据库设计中的范式究竟该如何使用;

6. MySQL进阶系列:一文详解explain各字段含义;

7. MySQL进阶系列:为什么mysql使用B+作为索引的数据结构;

8. MySQL进阶系列: 你需要知道的一些索引基础知识;

9. MySQL进阶系列:怎么创建索引更合适;

10. MySQL进阶系列:主从复制原理和配置;

11. MySQL进阶系列:join连接的原理-3种算法;

12. MySQL进阶系列:事务及事务隔离级别;

13. MySQL进阶系列:多版本并发控制mvcc的实现;

本文转载自: 掘金

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

跟着老猫来搞GO,基础进阶

发表于 2021-11-09

回顾一下上一篇博客,主要是和大家分享了GO语言的基础语法,其中包含变量定义,基本类型,条件语句,循环语句。那本篇呢就开始和大家同步一下GO语言基础的进阶。

函数的定义

上次其实在很多的DEMO中已经写出来一些函数了,但是没有讲清楚其函数定义。接下来我们同样地要举例说明一下,直接看代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func calculate(a,b int, op string) int {
switch op {
case "+":
return a + b
case "-":
return a - b
case "*":
return a * b
case "/":
return a / b
default:
panic("unsupported op")
}
}

以上是一个比较简单的计算两个整数加减乘除运算的一个函数,首先我们可以看到的是函数的定义其实也是遵循着变量的定义方式,咱们先定义函数的名称,然后才是函数的返回值。当然函数中的参数定义也是如此。

除此以外,其实GO语言相对于其他语言来说有一个比较骚的操作,就是他可以存在多个返回值。例如下面咱们写一个除法的例子,就是大家小学就学过的除不尽的时候存在余数的情况。下面我们来看一个函数。

1
2
3
go复制代码func div(a int, b int) (int,int){
return a / b, a % b
}

大家看到上面这个返回值有什么感想,其实这最终的两个返回值是没有体现任何业务意义的,咱们无法区分最终返回的结果到底是干什么用的。当然GO语言其实也发现了这个弊端,所以呢,我们的返回值的名称也是可以定义的,具体如下,我们命名除法得到的商为q,余数为r,那么我们改进之后就得到如下:

1
2
3
go复制代码func div(a int, b int) (q ,r int){
return a / b, a % b
}

如果这样的话我们main调用得到结果就可以这么获取

1
2
3
4
5
go复制代码func main() {
fmt.Println(div(4,3))
q,r := div(5,6)
fmt.Println(q,r)
}

那么此时问题又来了,如果我们只要其中的一个商,余数不要,这又是如何写呢,因为我们都知道go的语法中,定义出来的变量后面都得用到才行,否则的话会报编译错误,那其实我们直接用”_“来替换即可。具体代码块如下

1
2
3
4
go复制代码func main() {
q,_ := div(5,6)
fmt.Println(q)
}

这样话咱们就可以只获取其中一个值即可。

其实GO语言函数式编程编程的语言,函数是非常重要的,所以咱们再高端一点的函数的写法是可以将函数本身作为一个参数传入函数的,说的比较绕,其实本身开始去接受的时候也是有点难理解,老猫在此先把例子写一下,大家试着去理解一下,当然后面的话老猫会有更详细地对函数式编程的介绍,具体的例子如下

1
2
3
4
5
go复制代码func apply(op func(int,int) int,a,b int) int{
fmt.Printf("Calling %s with %d,%d\n",
runtime.FuncForPC(reflect.ValueOf(op).Pointer()).Name(),a,b)
return op(a,b)
}

我们对GO语言的函数来做一个简单的总结:

  • 返回值类型写在后面
  • 可以返回多个值
  • 函数可以作为参数
  • 没有默认参数,可变参数,重载等等

指针

相关定义

关注老猫的应该大多数是软件专业的同学,不晓得大家有没有熟悉过C语言,C语言中其实也有指针,C语言的指针相对还是比较难的。其实GO语言也有指针,相对而言比较简单,因为GO语言的指针不能运算。

指针说白了就是一个指针指向了一个值的内存地址。

GO语言中的去地址符为&,放到变量以前的话就会返回相应的变量内存地址。看个例子如下:

1
2
3
4
5
6
7
go复制代码package main

import "fmt"
func main() {
var a int = 10
fmt.Printf("变量的地址: %x\n", &a )
}

这个呢,其实就是GO语言的地址获取方式,那我们如何去访问它呢?那么我们的指针就登场了,如下代码示例

1
go复制代码var var_name *var-type

var-type为指针类型,var_name为指针变量名称,*用于指定变量是作为一个指针。那么我们再来看下面的例子:

1
2
go复制代码var ip *int        /* 指向整型*/
var fp *float32 /* 指向浮点型 */

那么以上其实就是定义了两个指针,分别指向int以及float32。

使用指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码package main

import "fmt"

func main() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */

ip = &a /* 指针变量的存储地址 */

fmt.Printf("a 变量的地址是: %x\n", &a )

/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )

/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
}

那么我们得到的结果为

1
2
3
css复制代码a 变量的地址是: 20818a220
ip 变量储存的指针地址: 20818a220
*ip 变量的值: 20

GO语言其实也会存在空指针, 当一个指针被定义后没有分配到任何变量时,它的值为 nil。 nil 指针也称为空指针。 nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。 一个指针变量通常缩写为 ptr。

如下例子

1
2
3
4
5
6
7
8
go复制代码package main

import "fmt"

func main() {
var ptr *int
fmt.Printf("ptr 的值为 : %x\n", ptr )
}

结果

1
复制代码ptr 的值为 : 0

那么我们一般对空指针的判断即为

1
2
go复制代码if(ptr != nil)     /* ptr 不是空指针 */
if(ptr == nil) /* ptr 是空指针 */

以上就是老猫带大家入一下指针的门,当然也是老猫的入门。后续,我们会在实际的例子中来慢慢体会指针的用法。

值传递以及引用传递

那么什么是值传递,什么是引用传递?我们简单地来看一段C++ 的代码,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
c++复制代码void pass_by_val(int a) {
a++;
}
void pass_by_ref(int &a){
a++;
}
int main(){
int a = 3;
pass_by_val(a);
printf("After pass_by_val:%d\n",a);
pass_by_ref(a);
printf("After pass_by_ref:%d\n",a);
}

上面两个方法,其中一个是值传递一个是引用传递,那么最终输出的结果是多少呢?大家可以先思考一下。其实答案为上面是3下面是4,那么为什么呢?

我们来看第一种,第一种的话是值传递,值传递的方式其实在上面的例子中可以这么理解,该函数是将main中的值拷贝一份放到了函数中,虽然在函数中加了1,但是外层原始的那个值还是3,所以最终输出的也还是3。

我们再来看另外一种,引用传递,从入参来看的话,其实里面的a以及外面的a所引用的都是同一个地址,所以当内部函数对a进行自增的时候,外面的函数a的值就发生了变化,变成了4。

那么我们的GO是值传递还是引用传递,其实GO语言只有值传递。

大家可能有点懵了,其实很多时候,大家不用太过纠结,因为在实际的用法中我们往往通过函数return的值就能解决相关问题。

写在最后

上面呢,其实老猫和大家分享了GO语言的函数定义,以及一个比较重要的指针的概念,在后面的学习中,我们来更加深入地去体会。在实践中去慢慢加深印象。当然上面的例子也希望大家能够照着写一下,运行着体会一下。有不理解的欢迎大家一块沟通一起进步。
我是老猫,更多内容,欢迎大家搜索关注老猫的公众号“程序员老猫”。

本文转载自: 掘金

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

15在mac电脑上同时安装jdk8和jdk11 一、jdk

发表于 2021-11-09

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

一、jdk下载地址

jdk官网下载地址:jdk.java.net/archive/

二、安装jdk

Mac的JDK都是安装到一个指定目录的:/Library/Java/JavaVirtualMachines/,因此可以在这个目录下查看自己安装的所以JDK。

image

三、配置环境变量

接下来就是配置环境变量并进行JDK版本管理,首先执行命令:vim ~/.bash_profile修改环境变量,MAC建议此处修改环境变量,而不是修改/etc/profile。

几种配置文件区别如下:

  • /etc/profile: 用于设置系统级的环境变量和启动程序,在这个文件下配置会对所有用户生效。当用户登录(login)时,文件会被执行,并从/etc/profile.d目录的配置文件中查找shell设置。一般不建议在/etc/profile文件中添加环境变量,因为在这个文件中添加的设置会对所有用户起作用。
  • ~/.bash_profile: 只有单一用户有效,文件存储位于~/.bash_profile,该文件是一个用户级的设置,可以理解为某一个用户的profile目录下。这个文件同样也可以用于配置环境变量和启动程序,但只针对单个用户有效。和profile文件类似,bash_profile也会在用户登录(login)时生效,也可以用于设置环境变理。但与profile不同,bash_profile只会对当前用户生效。

export JAVA_8_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home export JAVA_11_HOME=/Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home PATH=JAVAHOME/bin:JAVA_HOME/bin:JAVAH​OME/bin:PATH CLASSPATH=JAVAHOME/lib/tools.jar:JAVA_HOME/lib/tools.jar:JAVAH​OME/lib/tools.jar:JAVA_HOME/lib/dt.jar export JAVA_HOME=JAVA_8_HOME alias jdk8=”export JAVA_HOME=JAVA_8_HOME” alias jdk11=”export JAVA_HOME=$JAVA_11_HOME” export PATH export CLASSPATH

配置好以后,执行 source ~/.bash_profile令其生效。

四、验证效果

在终端输入jdk8,然后使用java -version验证版本;

再输入jdk11,在输入java -version验证版本;

五、解决关闭终端,配置失效的问题

出现这个问题的原因:

使用的终端是zsh,zsh加载的是 ~/.zshrc文件,而 ‘.zshrc’ 文件中并没有定义任务环境变量

解决方案:

1、终端中执行命令,打开.zshrc文件:

1
arduino复制代码open -e ~/.zshrc

2、在~/.zshrc文件最后,增加一行:

1
bash复制代码source ~/.bash_profile

3、执行命令,使其立即生效

1
bash复制代码 source ~/.zshrc

4、新建窗口,再次测试,通过。

本文转载自: 掘金

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

【Spring Boot 2x Security 入门系列

发表于 2021-11-09

前言

Spring Security是一个灵活和强大的身份验证和访问控制框架,以确保基于Spring的Java Web应用程序的安全,它是轻量级的安全框架,确保基于Spring的应用程序提供身份验证和授权支持。它与Spring MVC能够很好地集成,并配备了流行的安全算法实现捆绑在一起。本系列教程是展示Spring Security 5 的基本和高级的用法,固定URL(依据示例而定),视图和基于Spring boot/Hibernate应用方法的示例等。

特别说明:该教程未做任何关于的Spring MVC或Spring boot开发的基础指导。

一、 目标

你将构建一个Spring MVC应用(基于Spring Boot),该应用程序使用一些固定的用户,支持表单登录的形式来保护页面。

二、你需要准备什么

  • 约15分钟
  • 一个你喜欢的文本编辑器或IDE
  • JDK≥1.8
  • Maven 3.0+ or Grandle 2.3+
  • Thymeleaf模板
  • 你也可以将代码直接导入以下IDE:
    Spring Tool Suite (STS)
    IntelliJ IDEA

三、开始

  1. 创建目录结构

1
2
3
4
5
6
7
css复制代码└── src
└── main
└── java
└── com
└── spring
└── security
└── demo
  1. 使用Gradle构建

创建gradle文件:build.gradle

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
gradle复制代码buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.7.RELEASE")
}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'

jar {
baseName = 'gs-securing-web'
version = '0.1.0'
}

repositories {
mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
testCompile("junit:junit")
testCompile("org.springframework.boot:spring-boot-starter-test")
testCompile("org.springframework.security:spring-security-test")
}
  1. 使用Maven构建

目录结构不变
文件名:pom.xml

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.spring.security</groupId>
<artifactId>demo</artifactId>
<version>0.1</version>
<name>demo</name>
<description>Demo project for Spring Boot-Security</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/libs-release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/libs-release</url>
</pluginRepository>
</pluginRepositories>

</project>
  1. 创建不受保护的Web页面

在保护Web应用程序安全之前,先创建两个html页面作为测试使用,此例创建一个非常简单的Web应用程序。 放在下一节中使用Spring Security来保护它。

Web应用程序包括两个简单的视图:
主页-home.html和“Hello World”页面

4.1 “home主页”-home.html

文件路径:src/main/resources/templates/home.html

1
2
3
4
5
6
7
8
9
10
11
12
html复制代码<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security 示例</title>
</head>
<body>
<h1>欢迎!</h1>

<p>点击 <a th:href="@{/hello}">这里</a> 看看效果.</p>
</body>
</html>

4.2 “hello world页面”-hello.html

文件路径:src/main/resources/templates/hello.html

1
2
3
4
5
6
7
8
9
10
html复制代码<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security 示例-Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>

4.3 Web应用的Spring MVC配置-MvcConfig.java

文件路径:src/main/com/spring/security/config/MvcConfig.java

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.spring.security.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
* @author: Cavan.Liu
* @date: 2020-02-16 23:21:35
* @description:
*/
@Configuration
public class MvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addViewControllers(ViewControllerRegistry registry) {
super.addViewControllers(registry);

registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
  1. addViewControllers()方法(覆盖WebMvcConfigurationSupport中同名的方法)添加了四个视图控制器。
  2. 两个视图控制器引用名称为“home”的视图(在home.html中定义),另一个引用名为“hello”的视图(在hello.html中定义)。
  3. 第四个视图控制器引用另一个名为“login”的视图。该视图将在下一部分中创建。

至此,可以执行并运行应用程序,而无需登录操作。在此基础上创建好基本而简单Web应用程序后,可以准备开始添加安全性。

  1. 安装Spring Security组件

如果你希望防止用户访问未经授权的“/ hello”。 此时,如果用户点击主页上的链接,会看到问候语,由于未加上任何的防护,请求并被没有被拦截。 因此,咱们需要添加一个访问的前置条件这样用户在看到该页面之前需要先进行登录。

可以通过在应用程序中配置Spring Security来实现。 如果Spring Security在类路径上,则Spring Boot会使用“Basic认证”来自动保护所有HTTP端点。 同时,你可以进一步自定义安全设置,然后我们需要做的第一件事是将Spring Security添加到类路径中。

5.1 添加Spring Security组件

使用Gradle:
文件:build.gradle

1
2
3
4
5
gradle复制代码dependencies {
...
compile("org.springframework.boot:spring-boot-starter-security")
...
}

使用Maven(未添加版本,是因为Springboot会自动匹配最合适的版本):
文件:pom.xml

1
2
3
4
5
6
7
8
xml复制代码<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
...
</dependencies>

5.2 添加安全配置

文件路径:src/main/com/spring/security/config/WebSecurityConfig.java

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
java复制代码package com.spring.security.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* @author: Cavan.Liu
* @date: 2019-11-10 14:06:17
* @description: 继承WebSecurityConfigurerAdapter,并覆盖一些方法来设置Web安全配置的一些细节
*/
@Configuration
@EnableWebSecurity // 启用Spring Security的Web安全支持,并提供Spring MVC集成
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* Http security config
* 定义需要被保护、可直接访问的URL路径
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);

http.authorizeRequests()
// 授权访问 [/]与[/home] 目录
.antMatchers("/", "/home")
.permitAll()
.anyRequest()
.authenticated()
.and()
// 登录表单
.formLogin()
.loginPage("/login")
.permitAll()
.and()
// 登出
.logout()
.permitAll();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置后用户无法正常登录
//super.configure(auth);

// 密码编码器
PasswordEncoder pwdEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

auth.inMemoryAuthentication()
.withUser("user")
.password(pwdEncoder.encode("111111"))
.roles("USER");
}
}

@EnableWebSecurity注解的作用
启用Spring Security的Web安全支持,并提供Spring MVC集成。
它还继承了WebSecurityConfigurerAdapter类,并覆盖一些方法来设置Web安全配置的细节。

configure(HttpSecurity)方法的作用
定义了哪些URL路径应该被保护,哪些不应该被保护。具体来说,“/”和“/ home”路径被配置为不需要任何身份验证即可访问,而所有其他路径必须经过身份验证才能访问。

当用户成功登录时,它们将被重定向到先前请求的需要身份认证的页面。有一个由 loginPage()指定的自定义“/登录”页面,每个人都可以查看它。

对于configure(AuthenticationManagerBuilder) 方法,它将单个用户设置在内存中。该用户的用户名为“user”,密码为“111111”,角色为“USER”,方便当前应用的使用,更为简洁,因为此处仅仅是为了验证Security的使用方法,实际生产开发需要配置为数据库的校验。

此时,已经可以正式的创建login.html这个登录页面进行用户名和密码的校验操作。
文件路径:src/main/resources/templates/login.html

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
html复制代码<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">

<head>
<title>Spring Security 示例-登录页面</title>
</head>

<body>
<div th:if="${param.error}">
无效的用户名或密码.
</div>

<div th:if="${param.logout}">
你已经登出.
</div>

<form th:action="@{/login}" method="post">
<div>
<label> 用户名:
<input type="text" name="username"/>
</label>
</div>
<div>
<label> 密码:
<input type="password" name="password"/>
</label>
</div>
<div>
<input type="submit" value="登录"/>
</div>
</form>

</body>
</html>
  1. 此处的login页面使用的是Thymeleaf模板,并且只提供一个表单来获取用户名和密码,并将它们提交到“/login”。
  2. 根据配置,Spring Security提供了一个拦截该请求并验证用户的过滤器。
  3. 如果用户未通过认证,该页面将重定向到“/login?error”,并在页面显示相应的错误消息。
  4. 注销成功后,我们的应用程序将发送到“/ login?logout”,我们的页面显示相应的登出成功消息。
  5. 最后,我们需要向用户提供一个显示当前用户名和登出的方法。同时,更新hello.html向当前用户打印一句hello,并包含一个“注销”表单。

修改后的hello.html
文件路径:src/main/resources/templates/hello.html

1
2
3
4
5
6
7
8
9
10
11
12
13
html复制代码<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security 示例-Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="登出"/>
</form>
</body>
</html>
  1. hello.html使用Spring Security与HttpServletRequest的getRemoteUser()方法的集成来显示用户名。
  2. “登出”表单将POST请求提交到“/logout”成功注销后,便将用户重定向到“/login?logout”。

5.3 添加应用启动方法-Spring boot方式

文件路径:src/main/com/spring/security/security01Application.java

1
2
3
4
5
6
7
8
9
10
11
java复制代码package com.spring.security.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Secury01Application {
public static void main(String[] args) {
SpringApplication.run(Secury01Application.class, args);
}
}

@SpringBootApplication注解的作用(包含以下注解):

  1. @Configuration 标记了该类为Spring应用上下文定义Bean的源头。
  2. @EnableAutoConfiguration 告诉Spring Boot基于类路径,其他类,多种设置添加Bean的定义。通常你需要为Spring MVC应用添加 @EnableWebMvc注解 , 但springboot如果发现类路径下存在spring-webmvc 的依赖,其会自动添加web的支持。这便将应用标记为了一个web应用,同时激活了核心的配置如 DispatcherServlet。
  3. @ComponentScan 告诉Spring去扫描位于com.spring.security 包下的其他组件,配置和服务(components, configurations, and services),同时让它去寻找控制器(controllers)。

至此一个简单的有security保护又类似或者我们熟悉的Java入门级、通过main函数作为入口的web应用就基本构建完成。

  1. 启动应用Jar或IDEA中

使用IDEA应用启动,或者使用java -jar方式启动均可。

四、 总结

建议有一定Spring MVC或Spring boot开发经验的同学学习该教程关于Security的集成则收益会大一些。对于Springboot的一些组件学习建议除了可以看我发布的系列教程外,英文有余力的同学应该尽可能看官方提供的说明:spring.io

源码地址

github.com/Cavan2477/S…

本文转载自: 掘金

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

Java Lambda表达式

发表于 2021-11-09

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

Java lambda表达式在Java 8中是新的。Java lambda表达式是Java进入函数式编程的第一步。因此,Java lambda表达式是一个可以在不属于任何类的情况下创建的函数。Java lambda表达式可以像对象一样传递,并根据需要执行。

Java lambda表达式通常用于实现简单的事件侦听器/回调,或使用[Java Streams API]进行功能编程。Java Lambda表达式也经常用于[Java中的功能编程]

image.png

Java Lambdas和单一方法界面

函数式编程通常用于实现事件侦听器。Java中的事件侦听器通常被定义为使用单个方法的Java接口。以下是一个虚构的单方法界面示例:

1
2
3
4
5
csharp复制代码public interface StateChangeListener {

public void onStateChange(State oldState, State newState);

}

这个Java接口定义了一个方法,每当状态发生变化时(无论观察到什么),都会调用该方法。

在Java 7中,您必须实现此接口才能监听状态更改。想象一下,您有一个名为StateOwner的类,可以注册State Event侦听器。以下是一个例子:

1
2
3
4
5
typescript复制代码public class StateOwner {

public void addStateListener(StateChangeListener listener) { ... }

}

在Java 7中,您可以使用匿名接口实现添加事件侦听器,如下所示:

image.png

首先创建一个StateOwner实例。然后,StateChangeListener接口的匿名实现作为StateOwner实例上的侦听器添加。

在Java 8中,您可以使用Java lambda表达式添加事件侦听器,如下所示:

image.png

lambda表达式是这一部分:

1
csharp复制代码(oldState, newState) -> System.out.println("State changed")

lambda表达式与addStateListener()方法参数的参数类型匹配。如果lambda表达式与参数类型匹配(此处为StateChangeListener接口),则lambda表达式将转换为实现与该参数相同的接口的函数。

Java lambda表达式只能在匹配的类型为单个方法接口时使用。在上面的示例中,lambda表达式用作参数,其中参数类型是StateChangeListener接口。此接口只有一个方法。因此,lambda表达式与该接口成功匹配。

将Lambdas与接口匹配

单个方法接口有时也称为功能接口。将Java lambda表达式与函数接口匹配分为以下步骤:

  • 接口是否只有一个抽象(未实现)的方法?
  • lambda表达式的参数是否与单一方法的参数匹配?
  • lambda表达式的返回类型是否与单个方法的返回类型匹配?

如果这三个问题的答案是肯定的,那么给定的lambda表达式将与接口成功匹配。

具有默认和静态方法的接口

从Java 8中,[Java接口]可以同时包含默认方法和静态方法。默认方法和静态方法都有一个直接在接口声明中定义的实现。这意味着,Java lambda表达式可以使用多个方法实现接口——只要接口只有一个未实现(AKA抽象)方法。

换句话说,即使接口包含默认和静态方法,它仍然是一个功能接口, 只要接口只包含一个未实现(抽象)方法。以下是这个小部分的视频版本:

image.png

以下接口可以使用lambda表达式实现:

image.png

即使此接口包含3种方法,也可以通过lambda表达式实现,因为只有一个方法未实现。以下是实现的外观:

1
2
3
ini复制代码MyInterface myInterface = (String text) -> {
System.out.print(text);
};

Lambda表达式与匿名接口实现

尽管lambda表达式接近匿名接口实现,但有一些差异值得注意。

主要区别在于,匿名接口实现可以有状态(成员变量),而lambda表达式不能。看看这个界面:

1
2
3
4
5
csharp复制代码public interface MyEventConsumer {

public void consume(Object event);

}

此接口可以使用匿名接口实现实现,如下所示:

image.png

这个匿名的MyEventConsumer实现可以有自己的内部状态。看看这个重新设计:

image.png

请注意,匿名MyEventConsumer实现现在如何具有一个名为eventCount的字段。

lambda表达式不能有这样的字段。因此,lambda的表达被称为无国籍。

Lambda类型推断

在Java 8之前,在进行匿名接口实现时,您必须指定要实现的接口。以下是本文开头的匿名接口实现示例:

image.png

使用lambda表达式,通常可以从周围的代码中推断类型。例如,可以从addStateListener()方法(StateChangeListener接口上的单个方法)的方法声明中推断参数的接口类型。这被称为类型推理。编译器通过在其他地方查找类型来推断参数的类型——在这种情况下,是方法定义。以下是本文开头的示例,显示lambda表达式中没有提到StateChangeListener接口:

1
2
3
csharp复制代码stateOwner.addStateListener(
(oldState, newState) -> System.out.println("State changed")
);

在lambda表达式中,参数类型通常也可以推断。在上面的示例中,编译器可以从onStateChange()方法声明中推断其类型。因此,从onStateChange()方法的方法声明中推断出参数oldState和newState的类型。

Lambda参数

由于Java lambda表达式实际上是方法,lambda表达式可以像方法一样接受参数。前面显示的lambda表达式的(oldState, newState)部分指定lambda表达式的参数。这些参数必须与方法在单个方法界面上的参数匹配。在这种情况下,这些参数必须与StateChangeListener接口的theonStateChangeonStateChange()方法的参数匹配:

1
arduino复制代码public void onStateChange(State oldState, State newState);

lambda表达式和方法中的参数数量必须至少匹配。

其次,如果您在lambda表达式中指定了任何参数类型,这些类型也必须匹配。我还没有向您展示如何在lambda表达式参数上放置类型(本文稍后将显示),但在许多情况下,您不需要它们。

零参数

如果您将lambda表达式与lambda表达式匹配的方法不带参数,那么您可以这样编写lambda表达式:

1
csharp复制代码() -> System.out.println("Zero parameter lambda");

注意括号之间没有内容。这是为了表明lambda不带参数。

一个参数

如果您将Java lambda表达式与一个参数匹配的方法采用一个参数,您可以像这样编写lambda表达式:

1
csharp复制代码(param) -> System.out.println("One parameter: " + param);

请注意,参数列在括号内。

当lambda表达式采用单个参数时,您也可以省略括号,如下所示:

1
csharp复制代码param -> System.out.println("One parameter: " + param);

多个参数

如果您将Java lambda表达式与Java lambda表达式匹配的方法需要多个参数,则需要在括号内列出参数。以下是Java代码中的外观:

1
csharp复制代码(p1, p2) -> System.out.println("Multiple parameters: " + p1 + ", " + p2);

只有当方法采用单个参数时,才能省略括号。

参数类型

如果编译器无法从lambda匹配的功能接口方法推断参数类型,有时可能需要为lambda表达式指定参数类型。别担心,编译器会在情况发生时告诉你。以下是Java lambda参数类型示例:

1
csharp复制代码(Car car) -> System.out.println("The car is: " + car.getName());

如您所见,car参数的类型(Car)写在参数名称本身前面,就像您在其他地方的方法中声明参数或对接口进行匿名实现时一样。

Java 11的var参数类型

在Java 11中,您可以将var关键字用作参数类型。var关键字在Java 10中作为局部变量类型推断引入。来自Java 11 var也可以用于lambda参数类型。以下是在lambda表达式中使用Java var关键字作为参数类型的示例:

1
javascript复制代码Function<String, String> toLowerCase = (var input) -> input.toLowerCase();

使用上述var关键字声明的参数类型将推断为String类型,因为变量的类型声明的泛型类型设置为Function<String, String>,这意味着Function的参数类型和返回类型是String。

Lambda功能体

lambda表达式的主体,因此它所代表的函数/方法的主体,在lambda声明的->右侧指定:示例如下:

1
csharp复制代码 (oldState, newState) -> System.out.println("State changed")

如果您的lambda表达式需要由多行组成,您可以将lambda函数体包含在{ }括号中,Java在其他地方声明方法时也需要这些括号内。以下是一个例子:

image.png

从Lambda表达式返回值

您可以从Java lambda表达式返回值,就像从方法返回一样。您只需向lambda函数体添加返回语句,如下所示:

1
2
3
4
kotlin复制代码(param) -> {
System.out.println("param: " + param);
return "return value";
}

如果您的lambda表达式所做的只是计算并返回返回值,您可以以更短的方式指定返回值。取而代之的是:

1
kotlin复制代码(a1, a2) -> { return a1 > a2; }

您可以写:

1
rust复制代码(a1,a2)-> a1> a2;

然后,编译器计算出表达式a1 > a2是lambda表达式的返回值(因此lambda表达式的名称——因为表达式返回某种值)。

Lambdas作为对象

Java lambda表达式本质上是一个对象。您可以为变量分配lambda表达式,并像处理任何其他对象一样传递它。以下是一个例子:

image.png
第一个代码块显示lambda表达式实现的接口。第二个代码块显示了lambda表达式的定义,lambda表达式如何分配给变量,最后如何通过调用lambda表达式实现的接口方法来调用。

可变捕获

在某些情况下,Java lambda表达式能够访问lambda函数体外声明的变量。我这里有这个部分的视频版本:

image.png

Java lambdas可以捕获以下类型的变量:

  • 局部变量
  • 实例变量
  • 静态变量

这些变量捕获中的每一个都将在以下部分中描述。

本地变量捕获

Java lambda可以捕获lambda主体外声明的局部变量的值。为了说明这一点,首先看看这个单一的方法界面:

1
2
3
arduino复制代码public interface MyFactory {
public String create(char[] chars);
}

现在,看看这个实现MyFactory接口的lambda表达式:

1
2
3
ini复制代码MyFactory myFactory = (chars) -> {
return new String(chars);
};

目前,此lambda表达式仅引用传递给它的参数值(chars)。但我们可以改变这一点。以下是引用lambda函数体外声明的String变量的更新版本:

1
2
3
4
5
ini复制代码String myString = "Test";

MyFactory myFactory = (chars) -> {
return myString + ":" + new String(chars);
};

如您所见,lambda主体现在引用了在lambda主体之外声明的局部变量myString。只有当变量是“有效的最终的”时,并且只有当变量是“有效的最终的”时,这才有可能实现,这意味着它在分配后不会更改其值。如果myString变量的值后来发生了变化,编译器会抱怨lambda主体内部对它的引用。

实例变量捕获

lambda表达式还可以捕获创建lambda的对象中的实例变量。以下是一个例子,表明:

1
arduino复制代码public class EventConsumerImpl {private String name = "MyConsumer";public void attach(MyEventProducer eventProducer){eventProducer.listen(e -> {System.out.println(this.name);});}}

注意lambda主体中对this.name的引用。这将捕获封闭的EventConsumerImpl对象name实例变量。甚至可以在捕获实例变量后更改其值——该值将反映在lambda中。

this的语义实际上是Java lambdas与接口的匿名实现不同的领域之一。匿名接口实现可以有自己的实例变量,这些变量通过this引用引用。然而,lambda不能有自己的实例变量,因此this总是指向封闭对象。

注:上述活动消费者的设计并不特别优雅。我就是这样做的,以便能够说明实例变量捕获。

静态变量捕获

Java lambda表达式还可以捕获静态变量。这并不奇怪,因为静态变量可以在Java应用程序中的任何地方访问,前提是静态变量是可访问的(打包范围或公共的)。

以下是一个示例类,该类创建一个lambda,该lambda引用lambda主体内的静态变量:

image.png

静态变量的值也允许在lambda捕获后更改。

同样,上述类设计有点荒谬。不要想太多。该类主要用于向您展示lambda可以访问静态变量。

方法参考作为Lambdas

如果您的lambda表达式所做的只是调用另一个方法,参数传递给lambda,Java lambda实现提供了一种更短的方法来表达方法调用。首先,这里有一个单功能接口示例:

1
2
3
arduino复制代码public interface MyPrinter{
public void print(String s);
}

以下是创建实现MyPrinter接口的Java lambda实例的示例:

1
ini复制代码MyPrinter myPrinter = (s) -> { System.out.println(s); };

由于lambda主体仅由单个语句组成,我们实际上可以省略包含的{ }括号。此外,由于lambda方法只有一个参数,我们可以省略参数周围的括号( )括号。由此产生的lambda声明如下:

1
ini复制代码MyPrinter myPrinter = s -> System.out.println(s);

由于lambda主体所做的只是将字符串参数转发到System.out.println()方法,我们可以将上述lambda声明替换为方法引用。以下是lambda方法参考的外观:

1
ini复制代码MyPrinter myPrinter = System.out::println;

注意双冒号’::’。这些信号告诉Java编译器这是一个方法引用。引用的方法在双冒号后面。任何拥有引用方法的类或对象都出现在双冒号之前。

您可以参考以下类型的方法:

  • 静态方法
  • 参数对象的实例方法
  • 实例方法
  • 建筑商

以下各节涵盖了每种类型的方法引用。

静态方法参考

最容易参考的方法是静态方法。以下是单个函数接口的第一个示例:

1
2
3
arduino复制代码public interface Finder {
public int find(String s1, String s2);
}

这是一个静态方法,我们希望创建一个方法参考:

image.png

最后,这里有一个引用静态方法的Java lambda表达式:

1
ini复制代码Finder finder = MyClass::doFind;

由于Finder.find()和MyClass.doFind()方法的参数匹配,因此可以创建一个lambda表达式,实现Finder.find()并引用MyClass.doFind()方法。

参数方法参考

您还可以将其中一个参数的方法引用到lambda。想象一个如下所示的单个函数接口:

1
2
3
arduino复制代码public interface Finder {
public int find(String s1, String s2);
}

该接口旨在表示能够搜索s1以查找s2出现的组件。以下是调用String.indexOf()进行搜索的Java lambda表达式示例:

1
ini复制代码Finder finder = String::indexOf;

这相当于lambda的定义:

1
ini复制代码Finder finder = (s1, s2) -> s1.indexOf(s2);

注意快捷版本如何引用单个方法。Java编译器将尝试将引用的方法与第一个参数类型匹配,使用第二个参数类型作为引用方法的参数。

实例方法参考

第三,也可以从lambda定义中引用实例方法。首先,让我们看看单个方法接口定义:

1
2
3
arduino复制代码public interface Deserializer {
public int deserialize(String v1);
}

此接口表示能够将String“反序列化”为int的组件。

现在看看这个StringConverter类:

image.png

‘ convertToInt() ‘方法与’ deserializer ‘ ‘ deserialize() ‘方法的’ deserialize() ‘方法具有相同的签名。因此,我们可以创建一个’ stringconverter ‘的实例,并从Java lambda表达式中引用它的’ convertToInt() ‘方法,如下所示:

1
ini复制代码StringConverter stringConverter = new StringConverter();Deserializer des = stringConverter::convertToInt;

两行中的第二行创建的lambda表达式引用了在第一行创建的StringConverter实例的convertToInt方法。

构造函数参考

最后,可以引用类的构造函数。您通过写出类名,然后写上::new,就像这样:

1
arduino复制代码MyClass::new

也看看如何使用构造函数作为lambda表达式,看看这个接口定义:

1
2
3
arduino复制代码public interface Factory {
public String create(char[] val);
}

此接口的create()方法与String类中一个构造函数的签名匹配。因此,此构造函数可以用作lambda。以下是它看起来的示例:

1
ini复制代码Factory factory = String::new;

这相当于这个Java lambda表达式:

1
ini复制代码Factory factory = chars -> new String(chars);

本文转载自: 掘金

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

SSH、SHELL和终端(一) SSH是怎么工作的

发表于 2021-11-09

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

SSH是怎么工作的

本文从实际案例出发,梳理ssh相关的细节和原理。

ssh连接

作为程序员,我们每天都会执行很多次的一个非常简单的操作就是远程连接服务器。

在笔记本或PC上打开终端,敲入 ssh root@server,这样就ssh到某台服务器上了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shell复制代码MB1$ ssh root@server

~# ps $$
PID TTY STAT TIME COMMAND
28348 pts/0 Ss 0:00 -bash

~# ps -ef |grep 28348
root 988 28348 0 22:26 pts/0 00:00:00 ps -ef
root 989 28348 0 22:26 pts/0 00:00:00 grep --color=auto 28348
root 28348 28346 0 21:53 pts/0 00:00:00 -bash

~# ps -ef |grep 28346
root 28346 9709 0 21:53 ? 00:00:00 sshd: root@pts/0
root 28348 28346 0 21:53 pts/0 00:00:00 -bash

从上面几个命令可以看出,客户端远程连接上服务器之后:

  • ps $$打印当前终端的bash进程信息,Time 0:00表示刚连上去,Stat Ss 表示是一个前台程序,S表示处于sleep状态,s表示有子进程
  • 当前bash进程的PID是 28348
  • ps -ef |grep 28348显示28348的子进程有ps -ef和grep –color=auto 28348,父进程的PID是28346
  • 查看bash进程的父进程信息,它是sshd: root@pts/0
    如下图:

ssh4.png

可以看出,客户端连接ssh服务器后,ssh服务器产生了一个bash进程,这个bash进程的tty是pts/0。

pts/0是sshd软件模拟的一个虚拟tty,一个虚拟的显示器概念。下面还有tty的介绍。

连接建立过程

  • 建立tcp连接,ssh和sshd程序通过socket建立连接通道
  • sshd产生一个虚拟终端和子进程bash
  • ssh模拟在客户端终端上服务端的输入输出

通信流程

  • 客户端在ssh程序的输入发往服务端sshd
  • 服务端sshd将客户端的命令输入给虚拟终端pts/0
  • bash进程的标准输入是pts/0,则bash得到输入,开始干活
  • bash进程根据输入的shell命令产生新的子进程,如果是前台进程,则阻塞等待子进程完成
  • 子进程继承了父进程的标准输入输出,打印信息输出到终端

厘清概念

通过上面的流程分析已经有了基本的概念,但是发现这个简单案例的信息量却很大,是因为有很多概念和名词需要梳理清楚。虽然在实际学习和工作中,我们可能不会去注意或总是搞混这些概念。

shell和bash

shell是操作系统外包裹的壳,shell其实就是用户与linux系统沟通的一个桥梁。

ssh2.png

可以认为shell是一个接口,bash是shell的一种实现,shell的实现还有sh,zsh等。
我们可以在系统里面运行多个bash进程。

终端、控制台和Console

这三个词都是一样的,都是指终端。

关于tty

  • tty的电传打字机的简称,因为最早使用的输入输出就是电传打字机。后来被视频显示终端所替代。

ssh3.png

  • pts是sshd进程远程连接产生的,是一个终端模拟器

标准输入输出和终端

在linux操作系统中,外部设备用什么表示?是用文件。linux中一切设备皆是文件!

  • 系统的文件抽象用 文件描述符表示
  • SHELL程序会使用0、1、2这三个文件描述符,0号fd表示进程的标准输入,1号fd表示进程的标准输出,2表示标准错误
  • bash进程的标准输入是终端,标准输出也是终端,标准错误也是终端。

终端和进程

从终端开始运行的进程都会依附于这个终端,这个终端称为这些进程的控制终端,也就是图上的tty。
在一个终端上运行的程序可以分为前端进程和后端进程。

  • 前端程序的标准输入也是终端,会阻塞bash进程。
  • 后端程序的标准输入不从终端读取,则不会阻塞bash进程。

如果终端关闭了,则会发HUP信号给这个终端的前后端进程。HUP信号是终端被中止的时候向它所关联的进程所发出的信号,进程收到这个信号后就会中止运行。

所以这样就能理解为啥终端关闭,和终端相关的进程就关闭了。

  • bash是第一个依附于这个终端的前端程序,终端关闭则bash进程关闭
  • &启动的是后台进程,但在终端关闭的时候,这些后台线程也会收到HUP信息
  • nohup启动的后台线程会忽略终端关闭发起的HUP信息。所以如果你不希望进程被这个信号干掉的话,就可以忽略这个信号。

Session会话

上面讲的控制终端其实控制的是会话(Session)。

  • session是和shell关联的,每打开一个shell的进程,就会创建一个新的session。

ssh5.png

  • 每一次登录都会产生一个会话,可以是远程登录也可以是本地登录
  • 会话需要有系统认可的身份登录,在session里面可以切换用户
  • bash进程是这个Session的第一个进程,
  • SID和bash进程的PID一样
  • 进程会被标记上SessionID,也就是SID

结语

本文只讲了一个简单的案例并梳理这个案例里面的概念,下一篇会进一步分析shell、终端相关的原理和应用。

本文转载自: 掘金

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

dart系列之 dart语言中的异常 简介 Exceptio

发表于 2021-11-09

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

简介

Exception是程序中的异常情况,在JAVA中exception有checked Exception和unchecked Exception。那么在dart中的情况是不是一样的呢?一起来看看吧。

Exception和Error

Dart中表示异常的类有两个,分别是Exception和Error。他们两个有什么区别呢?

Exception是由VM或者dart code中抛出的。

Exception主要用来表示用户程序编写过程中产生的异常,是可以定位到的可以解决的异常。通常来说Exception中包含了足够的信息来方便用户来定位异常点。

所以Exception通常是需要被catch的。但是和java不同的是,dart中所有的异常都是unchecked 异常,也就是说dart中的异常并不强制要求被捕获,是否捕获异常是由程序员自行决定的。

构造一个异常很简单,如下所示:

1
php复制代码Exception("message")

但是dart并不推荐这样使用,因为这样构造的异常太过通用了,即使捕获到这样的异常,可以获得信息也比较少。所以dart推荐抛出自定义异常,也就是说根据业务需要去创建Exception对应的类,然后根据业务需要进行抛出。

dart中也有很多Exception的子类,比如FormatException来表示各种不同的异常情形。

同样的,在JAVA中也是这样推荐的,不要直接抛出Exception,而是根据业务需要抛出自定义的异常。

和JAVA一样,dart中的Error表示的是一个严重的错误,Error是应该在程序编写过程中需要避免的。

dart中的Error并不需要被捕获,因为发生了Error就表示程序出现了非常严重的错误,已经无法运行下去了。

所以Error是我们在程序编写过程中需要避免的。

Throw和catch

如果程序产生了异常,则可以使用Throw语句将其抛出,然后在合适的地方使用catch进行捕获。

比如我们throw一个格式异常:

1
arduino复制代码throw FormatException('这是一个格式异常');

但是在dart中,不仅仅可以throw Exception或者Error,任何一个Object都可以throw出去,如下所示:

1
arduino复制代码throw "这是一个异常!";

抛出的异常可以使用catch来捕获:

1
2
3
4
5
csharp复制代码try{
do something
}catch(e){

}

dart也可以捕获特定的异常,这种情况用on语句来表示,如下:

1
2
3
4
5
6
7
8
9
10
11
12
dart复制代码try {
someException();
} on OutOfIndexException {
// 捕获特定的异常
doSomething();
} on Exception catch (e) {
// 捕获其他的Exception
print('其他的异常: $e');
} catch (e) {
// 处理剩下的异常
print('剩下的异常: $e');
}

dart中的catch可以指定两个参数,第一个参数就是throw的异常,第二个参数是StackTrace对象:

1
2
3
4
5
dart复制代码try {
} catch (e, s) {
print('异常信息: $e');
print('堆栈信息: $s');
}

在处理完异常之后,如果想要再将其抛出,可以使用rethrow:

1
2
3
4
5
6
7
csharp复制代码void doSomething(){
try{
}catch (e) {
print('get exception');
rethrow; // rethrow这个异常
}
}

Finally

和JAVA一样,dart中也有Finally,用来进行最终的处理。Finally会在所有的catch语句执行完毕之后执行:

1
2
3
4
5
6
7
scss复制代码try {
doSomething();
} catch (e) {
print('Error: $e');
} finally {
cleanUpJob(); // 最后的清理工作
}

总结

以上就是dart中的异常和对异常的处理。

本文已收录于 <www.flydean.com>

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

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

本文转载自: 掘金

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

聊聊自定义实现的SPI如何与spring进行整合

发表于 2021-11-09

前言

上一篇文章主要聊聊如何实现一个带有拦截器功能的SPI。今天就来聊聊自定义的SPI如何与spring整合。

思考:我们实现的SPI要整合spring哪些东西?或者我们要利用spring的哪些特性实现我们哪些东西?

spring除了被大家熟知的IOC和AOP之外,还有它也提供了很丰富的扩展点,比如各种后置处理器,今天我们就聊聊大家相对熟悉的话题,如何通过自定义注解把SPI注入到spring容器中

整合思路

1、自定义注解

1
2
3
4
5
6
7
java复制代码@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Activate {

String value() default "";
}

2、自定义bean定义扫描器

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
java复制代码public class ActivateClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {


public ActivateClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
super(registry);
}


@SneakyThrows
@Override
protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) {
super.registerBeanDefinition(definitionHolder, registry);
Class clz = Class.forName(definitionHolder.getBeanDefinition().getBeanClassName());
Activate activate = AnnotationUtils.findAnnotation(clz,Activate.class);
if(ObjectUtils.isNotEmpty(activate) && StringUtils.isNotBlank(activate.value())){
String activateName = getEnvironment().resolvePlaceholders(activate.value());
registry.registerBeanDefinition(activateName,definitionHolder.getBeanDefinition());
}
}

@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return super.isCandidateComponent(beanDefinition) && beanDefinition.getMetadata()
.hasAnnotation(Activate.class.getName());
}

3、定义ImportBeanDefinitionRegistrar

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class SpiRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {

private Environment environment;


@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Set<String> basePackages = this.getBasePackages(importingClassMetadata);
String[] packages = {};
SpiBeanUtils.registerActivateInstances(registry,environment,basePackages.toArray(packages));
}

4、自定义enabled注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Target(value = ElementType.TYPE)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
@Import(SpiRegister.class)
public @interface EnableSpi {


String[] value() default {};


String[] basePackages() default {};


Class<?>[] basePackageClasses() default {};
}

示例演示

1、在需要注入到spring容器的类上加上@Activate注解

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Activate("hello-mysql")
public class SpringMysqlDialect implements SpringSqlDialect {

@Autowired
private MysqlDialectService mysqlDialectService;

@Override
public String dialect() {
return mysqlDialectService.dialect();
}


}

2、启动类上加上扫描SPI范围注解

1
2
3
java复制代码@SpringBootApplication(scanBasePackages = "com.github.lybgeek")
@EnableSpi(basePackages = "com.github.lybgeek")
public class SpiTestApplication implements ApplicationRunner

3、利用getBeansOfType进行验证

1
2
java复制代码 applicationContext.getBeansOfType(SpringSqlDialect.class)
.forEach((beanName,bean) -> System.out.println(beanName + "-->" + bean));

打印结果如下

1
java复制代码hello-mysql-->com.github.lybgeek.dialect.mysql.SpringMysqlDialect@433348bc

说明已经注入到spring容器中

总结

把项目的服务托管给spring ioc容器,可以算是与spring整合比较基础的动作,本文演示也是相对基础的一环,spring 强大的地方,在于它的扩展性,在spring bean的生命周期中,基本上随处可见扩展点,感兴趣的朋友后续可以自行体会验证

demo链接

github.com/lyb-geek/sp…

本文转载自: 掘金

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

1…390391392…956

开发者博客

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