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

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


  • 首页

  • 归档

  • 搜索

微服务改造之Openfeign的强化插件

发表于 2021-11-25

在接触 Spring Cloud 这套框架之前,笔者使用的一直是Dubbo。在转型到Spring Cloud 后,发现了一个很郁闷的问题。Spring Cloud 中的 Openfeign,相比于 Dubbo 对单个方法和单个接口(Interface)设置RPC超时时间,支持的不是特别理想。我查阅了一些资料和博客发现,现有的 Feign 只支持RPC超时设置的方式主要有以下几种

第一种配置默认的连接超时和读取超时,该配置全局生效。

1
2
arduino复制代码feign.client.config.default.connectTimeout=2000
feign.client.config.default.readTimeout=5000

第二种是设置服务级别的连接超时和读取超时,该配置对这个服务的全部接口生效。

1
2
3
4
ini复制代码# 单个服务 连接超时时间
feign.client.config.{这里填写服务名称}.connectTimeout = 5000
# 单个服务 读超时时间
feign.client.config.{这里填写服务名称}.readTimeout = 10000

以上两种的粒度都非常粗,所以使用起来并不是很理想。下面要介绍的方案避免了这个粗粒度的问题。

第三种是借助于Hystrix实现方法级别的超时时间设置:

1
2
3
4
5
6
7
8
ini复制代码#首先是开启 Hystrix
feign.hystrix.enabled = true

# 全局设置超时
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 30000

#服务级别设置超时
hystrix.command.{这里填写服务名称}.execution.isolation.thread.timeoutInMilliseconds: 30000

要对以下接口进行设置:

1
2
3
4
kotlin复制代码public interface MemberService {
@RequestMapping("/getUsers")
List<User> getUsers(String name);
}
1
2
arduino复制代码# 方法级别设置超时
hystrix.command.MemberService#getUser(String).execution.isolation.thread.timeoutInMilliseconds = 3000

分析一下一下几种方法的优劣:

首先以上几种方法都是可以使用的没有问题,只是用起来不是特别爽快,相比于Dubbo总感觉缺点东西。第一种和第二种粒度太大,如果只是有一个方法需要很长的时间,如果只是为了这个方法,而将这个服务下所有的方法都设置成同样长的超时时间,显然是不太合适的。而第三种方法又要开启Hystrix,说句老实话,Hystrix现在新的项目使用的越来越少,就像笔者现在使用的是Sentinel。而且Hystrix的超时设置比较奇特用并不是我们所理解的Rpc 连接超时和读取超时,而是执行超时时间。如果有兴趣的同学可以自己去了解一下,因为篇幅有限这里就不展开讲了。

各位看官看到这里是不是要开始吐槽了,讲了大半天了,讲的都是大家知道的,一点干货都没有,说好的强化插件呢???

各位大大不要着急,下面就要开始介绍Feign的强化插件,集成了连接超时,读取超时和重试。分为服务提供方的注解形式和服务消费方的配置形式。该插件的的作用在于补全openfeign客户端在使用的默认代理和 Sentinel 代理 的过程中无法自定义单个接口或者单个方法级别的超时时间与重试的插件。

本插件的优先级为:

1
2
go复制代码服务消费者method配置 > 服务提供者method > 服务消费者interface级配置 > 服务提供者interface级配置 > 服务消费者全局配置
注意:是否允许重试以服务提供者注解标识为最高优先级,只有服务提供方注解显示的表明可以重试,才能执行重试。如果注解没有,默认不可重试。

服务提供方 maven 引用:

1
2
3
4
5
xml复制代码            <dependency>
<groupId>org.example</groupId>
<artifactId>openfeign-strengthen-plugin-annotation</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

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
less复制代码 /**
* 注解放在 interface 上,则该 interface 下所有的方法 共享该配置
*/
@RpcInfo(connectTimeout = 2,connectTimeoutUnit = TimeUnit.SECONDS, readTimeout = 1, readTimeoutUnit = TimeUnit.SECONDS, followRedirects = false)
public interface MemberService {

/**
* 注解放在 method 上
* 则该 method RPC调用 时使用该配置。
* 方法上的配置优先级大于 interface 级别,
* 同时存在时,以 method 上的为准
* connectTimeout 连接超时时间
* connectTimeoutUnit 连接超时时间的单位 默认为毫秒
* readTimeout 读取超时时间
* readTimeoutUnit 读取超时时间单位 默认为毫秒
* followRedirects 是否重定向 默认为否
* maxAutoRetriesNextServer 切换实例的重试次数 默认为 0 次 禁止重试
* maxAutoRetries 对当前实例的重试次数 默认为 0 次 禁止重试
* isAllowedRetry 是否允许重试 默认为 false 不允许
*/
@RequestMapping("/getUsers")
@RpcInfo(connectTimeout = 2,connectTimeoutUnit = TimeUnit.SECONDS,
readTimeout = 1, readTimeoutUnit = TimeUnit.SECONDS, followRedirects = false,
maxAutoRetriesNextServer = 1, maxAutoRetries = 1, isAllowedRetry = true)
List<User> getUsers(String name);

@PostMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@RpcInfo(connectTimeout = 5,connectTimeoutUnit = TimeUnit.SECONDS, readTimeout = 2, readTimeoutUnit = TimeUnit.SECONDS)
String handleFileUpload(@RequestPart(value = "file") MultipartFile file);
}

服务消费者引入 maven 依赖:

1
2
3
4
5
xml复制代码        <dependency>
<groupId>org.example</groupId>
<artifactId>openfeign-strengthen-plugin-start</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

服务消费者方使用 application.yml 配置的形式使用,并且服务消费者超时时间配置优先级高于服务提供方配置

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
yaml复制代码  feign:
client:
rpcConfig:
#单个方法维度的配置
#接口名称 + # + 方法名称 + (参数类型, 参数类型...)
#包名 + 接口名称 + # + 方法名称 + (参数类型, 参数类型...) com.huihuang.service.MemberService#getUsers(String)
MemberService#getUsers(String):
connectTimeout: 1 #连接超时
connectTimeoutUnit: SECONDS #超时时间单位
readTimeout: 1 #读取超时
readTimeoutUnit: SECONDS
followRedirects: false #是否允许重定向
maxAutoRetries: 2 # 对当前实例的重试次数
maxAutoRetriesNextServer: 1 # 切换实例的重试次数
#按接口维度配置 优先级低于 单个方法维度的配置
#接口名称
#包名 + 接口名称 com.huihuang.service.MemberService
MemberService:
connectTimeout: 1 #连接超时
connectTimeoutUnit: SECONDS #超时时间单位
readTimeout: 1 #读取超时
readTimeoutUnit: SECONDS
followRedirects: false #是否允许重定向
maxAutoRetries: 2 # 对当前实例的重试次数
maxAutoRetriesNextServer: 1 # 切换实例的重试次数
#全局维度配置 优先级低于 单个方法维度 和 接口维度 的配置
default:
connectTimeout: 1 #连接超时
connectTimeoutUnit: SECONDS #超时时间单位
readTimeout: 1 #读取超时
readTimeoutUnit: SECONDS
followRedirects: false #是否允许重定向
maxAutoRetries: 2 # 对当前实例的重试次数
maxAutoRetriesNextServer: 1 # 切换实例的重试次数

GitHub地址:github.com/RaidenXin/o…

这是我自己遇到的问题,希望能给各位看官大大一点点帮助。如果有什么意见和建议欢迎和我联系,可以直接评论或者在我的微信公众号后台留言发给我,笔者现在在公司的职位是基础架构架构师,如果有其他的Java方面或者基础架构方面的问题也可以一起交流一下。非常感谢大家的观看。

转载请注明出处!谢谢

下面是我的公众号:

本文转载自: 掘金

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

Apache APISIX request_uri 变量控制

发表于 2021-11-25

问题描述

在 Apache APISIX 2.10.2 之前的版本中,使用 Apache APISIX Ingress Controller 中 $request_uri 变量存在「绕过部分限制」导致路径穿透风险的问题。

在使用 uri-blocker 插件进行测试场景时发现:

1
2
3
csharp复制代码$ ./apisix_request.sh "/public-service/public"
Defaulted container "apisix" out of: apisix, wait-etcd (init)
{"data":"public data"}
1
2
3
4
5
6
7
8
9
javascript复制代码$ ./apisix_request.sh "/protected-service/protected"
Defaulted container "apisix" out of: apisix, wait-etcd (init)
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>openresty</center>
</body>
</html>

在上述两种场景下,public-service 是可用的,protected-service 则被插件阻止了。后续对上述场景进行验证测试,发现两种情况均能绕过 Uri 的限制。

由于uri-blocker 插件使用 ctx.var.require_uri 变量不当,导致:

  • 攻击者可以绕过访问控制限制逻辑,访问本应该禁止的 API;
  • 自定义插件的开发者或许不知道 ngx.var.request_uri 变量是不可信任的。

影响版本

Apache APISIX 2.10.2 之前的所有版本(不包含 2.10.2)。

解决方案

该问题目前已在 2.10.2+ 版本中得到解决,请尽快更新至相关版本。

操作建议:如果在使用自定义插件时,可在使用 ngx.var.request_uri 变量前进行路径规范化的相关处理。同时额外检查下 ctx.var.upstream_uri 和

ctx.var.uri 这两个变量,虽然可能已经被规范化了,但防患于未然。

漏洞详情

漏洞公开时间:2021 年 11 月 22 日

CVE 详细信息:nvd.nist.gov/vuln/detail…

贡献者简介

该漏洞由社区用户 Marcin Niemiec(GitHub @xvnpw)发现,并及时向 Apache 软件基金会上报该漏洞。

感谢 Marcin Niemiec 对 Apache APISIX 社区的贡献。

关于 Apache APISIX

Apache APISIX 是一个动态、实时、高性能的开源 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。Apache APISIX 可以帮助企业快速、安全地处理 API 和微服务流量,包括网关、Kubernetes Ingress 和服务网格等。

Apache APISIX 落地用户(仅部分)

图片

  • Apache APISIX GitHub:github.com/apache/apis…
  • Apache APISIX 官网:apisix.apache.org/
  • Apache APISIX 文档:apisix.apache.org/zh/docs/api…

本文转载自: 掘金

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

我们如何实现“业务 100% 云原生化,让阿里中间件全面升级

发表于 2021-11-25

简介: 在今年的天猫双 11 中,中间件支撑了 5403 亿的交易量,并全面升级到了公共云架构。 此次的架构升级,是以开源为内核、以公共云为基础、以 OpenAPI 进行解偶扩展,在架构上,对开源、自研、商业化进行统一。通过采用和反哺开源、推动社区建设,通过阿里巴巴丰富的业务场景、打磨技术的性能和可用性,通过云上商业化服务更多企业、打造更好的用户体验,全方位锤炼云上产品的竞争力。

作者 | 中间件支持集团上云技术小组

2019 年,阿里巴巴核心系统 100% 运行在阿里云上。2021年,阿里巴巴业务 100% 云原生化。阿里巴巴已经成为全球首家,将所有业务都放在自家公共云上的大型科技公司。

举全集团之力,将业务全部迁移至公共云,不仅是对云的笃定,也证明了阿里云有能力应对高难度、超复杂环境下的技术挑战,为客户享受云上技术红利提供了更坚实的实践保障。

01 架构一致性,开源、自研、商业化三位一体

在今年的天猫双 11 中,中间件支撑了 5403 亿的交易量,并全面升级到了公共云架构。

此次的架构升级,是以开源为内核、以公共云为基础、以 OpenAPI 进行解偶扩展,在架构上,对开源、自研、商业化进行统一。通过采用和反哺开源、推动社区建设,通过阿里巴巴丰富的业务场景、打磨技术的性能和可用性,通过云上商业化服务更多企业、打造更好的用户体验,全方位锤炼云上产品的竞争力。

这个过程中,阿里巴巴业务的研发效率提升了 20%,CPU 资源利用率提升了 30%,应用 100%云原生化,在线业务容器可达百万规模,计算效率大幅提升,双 11 计算成本下降 30%。

接下去,我们将全方位揭秘业务 100%云化过程中,后端 BaaS 化,运行时 Mesh 化,业务侧 Serverless 化的全过程。

02 中间件后端 BaaS 化,有状态应用也可分钟级交付

以往的双 11 建站交付都是线性的。先交付 IaaS 资源,然后再交付中间件,最后再交付业务。

今年,中间件升级到公共云架构后,IaaS 资源和中间件同步交付,节省了两者串行交付的时间。中间件公共云架构运维底座全部切到 K8s 上,让有状态的中间件也能做到极致弹性,使得中间件的交付效率从天级别,降低到了分钟级,极大地提升了交付效率,降低了资源保有时间和资源成本。

后端的支撑系统也全面升级,如通过对接阿里云账号权限体系,来解决安全问题;通过对接计量计费体系,来解决 IT 资产数字化问题,为集团各个技术团队的经营者可以通过账单形式,可视化的进行成本优化。

在用户界面上,也升级支持了 IPv6,为阿里巴巴生产网全面向 IPv6 架构演进做好了准备。

03 海外业务 Mesh 化,异地多活可下沉 Sidecar

阿里巴巴海外有 AE&Lazada 等多种业务形态,异地多活体系侵入性大,技术架构不统一,从而影响了全局高可用和研发协同效率。

随着服务网格架构的演进和成熟,我们逐步将服务路由标准化,路由功能层次化,通过插件模式让业务进行扩展,让异地多活体系下沉到 Sidecar,和业务逻辑解偶,探索异地多活通用、无侵入、低成本的解决方案。今年,这套体系在海外业务得到了充分验证,为未来商业化积累了实践经验。

随着 Mesh 化服务架构的深度应用,除了异地多活功能下沉 Sidecar,阿里巴巴还基于 Mesh 化架构,统一了流量调度技术与产品架构,降低了流量调度实施和治理成本,提升服务容灾能力和线上服务治理效率,实现了更加灵活和稳定的调度规则下发及单元间切流。

04 业务侧 Serverless 化,实现研发提效 38%,弹性提升 200%

Serverless 是阿里巴巴降本提效的首选技术方案。

今年双 11,Serverless 不仅成功承载了 3 倍的峰值流量 ,支撑应用场景数量也提升了 2 倍,整体研发运维体系提升 38%,主要表现在以下两个关键点上。

1、夯实三位一体技术体系,使用阿里云函数计算 FC 支撑大促全面 Serverless 化

函数计算 FC 与阿里内部的运维体系,实现全面标准化对接,打通研发的最后一公里。首次实现了业务全链路“ FaaS + BaaS ”的 Serverless 全流程研发体系。

在函数计算进入集团之前,云上的 Serverless 技术体系一直无法融入到开发者生态,虽然功能丰富、强大,但是无法被业务使用,甚至出现了使用 Serverless 技术后,研发成本反而增高的情况。所以,在 2021 年,我们发力 Serverless-Devs 工具链,基于标准的接口与集团内部的技术社区,共同打造了专属于 Serverless 的研发体系,把云上的技术巧妙的融入到了集团。

通过双 11 大促场景作为“磨刀石”,把关键的核心技术进行进一步打磨,然后反哺给云上的商业化产品和工具链,夯实三位一体的技术体系,今年交出了满意的答卷,全面支撑 2021 天猫 双 11 各类业务场景,覆盖淘特、淘系、阿里妈妈、1688、高德和飞猪等多类业务场景,数量提升 2 倍,峰值流量总数同比增加 3 倍,实现了 50w QPS 的突破,整体研发提效达到 38%。

2、加大 Serverless 硬核技术投入,阿里内部通过天猫双 11 场景打磨,外部通过公共云输出、服务千万家企业

在 Serverless 的场景下,冷启动的速度是客户选型的关键,也是云上产品的核心竞争力,。

今年,我们加大了硬核技术研发的投入,从“弹性策略”、“镜像分发”、“容器启动” 等全方位对冷启动进行了性能提升,冷启动时间进一步缩减 60%,刚性交付能力提升 200%。在年初,函数计算刚应用于集团内部时,Runtime 层的冷启动时间在秒级别,并且需要初始化中间件,整体的冷启动时间要大于 2s,这严重制约了 Serverless 的使用场景。

所以,我们在镜像分发上,创新性发明了 Serverless Caching 。根据不同的存储服务特点,构建数据驱动、智能高效的缓存体系,实现软硬件协同优化;即便在 GB 级别镜像冷启动的场景下,函数计算也能提秒级别的交付能力。

在调度上,相比去年,增加了定时/CPU 等更多指标的弹性策略,并且基于集团内资源统一调度的能力,支撑了天猫双 11 业务的 10w 级别的实例弹性。在容器层,使用了自研的安全容器池化技术,在容器启动上,时间进一步缩小到 50ms 以内。

这些技术,都已经在双 11 场景下得到验证,也在公共云上全面输出,已经帮助我们的合作伙伴轻松应对业务高峰。

05 从 Ops 到 Dev,云原生的技术改造正进入下半场

第一时间让客户使用跟阿里巴巴一样的技术,是中间件开源、自研、商业化三位一体的初衷。这些源自三位一体的产品正帮助云上客户更好的提升 Ops 的效率。

三位一体的商业化输出包括:

  • 微服务引擎 MSE:注册&配置中心全(原生支持 Nacos/ZooKeeper/Eureka)、网关(原生支持 Ingress/Envoy)和无侵入的开源增强服务治理(原生支持 Spring Cloud/Dubbo);
  • 消息队列 MQ:原生支持 Apache RocketMQ、Apache Kafka;
  • 应用实时监控服务 ARMS:原生支持 Prometheus,提供基于开源的 Tracing 能力;
  • 应用高可用服务 AHAS:原生支持 Sentinel、ChaosBlade;
  • 函数计算 FC:支持开发者工具开源 Serverless Devs、开源可观测工具等。

云计算和云原生技术上半场更多的是解决的是 Ops 的问题,我们相信下半场更多是关注 Dev 的问题。

围绕着开发者效率的提升,中间件已经完成了 Serverless、应用运行时、低代码、云边一体,在线 IDE 等关键领域的技术布局,通过服务网格和应用运行时等技术,将非业务逻辑下沉,并且通过插件模式,形成新的研发分工,让中间件研发屏蔽底层复杂技术,让安全研发在应用运行时这层建立可信的安全防线,让高可用研发在底层通用的构建熔断、限流、降级、异地多活等能力,让业务更轻量,更聚焦业务本身开发,更高效的构建业务竞争力。

原文链接

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

本文转载自: 掘金

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

Go(四)Redis还不会使用?

发表于 2021-11-25

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

作者:lomtom

个人网站:lomtom.top,

个人公众号:博思奥园

你的支持就是我最大的动力。

redis

底层为C语言
解决hash冲突类似于1.7的hashmap

redis概念

  1. 非关系型的键值对数据库,可以根据键以O(1)的时间复杂度取出或插入关联值
  2. Reds的数据是存在内存中的
  3. 键值对中键的类型可以是字符串,整型,浮点型等,且键是唯一的
  4. 键值对中的值类型可以是 string,hash,list,set, sorted set等
  5. Reds内置了复制,磁盘持久化,LUA脚本,事务,SSL,客户端代理等功能
  6. 通过Reds哨兵和自动分区提供高可用性

应用场景

  1. 计数器
    可以对Sng进行自增自减运算,从而实现计数器功能。Reds这种内存型数据库的读写性能非常高,
    很适合存储频繁读写的计数量
  2. 分布式D生成
    利用自增特性,一次请求一个大一点的步长如incr2000,缓存在本地使用,用完再请求。
  3. 海量数据统计
    位图(btmp):存储是否参过次活动,是答已读谋篇文章,用户是否为会员,日活统计。
  4. 会话缓存
    可以使用 Redis来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就
    不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
  5. 分布式队列/阻塞队列
    List是一个双向链表,可以通过 push/push和rpop/pop写入和读取消息。可以通过使用 brpop/b|pop
    来实现阻塞队列

String(最大存储512M)

String 是redis中使用最多的存储类型

1、数据结构(3.2之前):
sds:simple dynamic string是一个二进制安全数组
sds:

  • length: 长度
  • free:剩余长度
  • char [] :{1,12,324,123}

2、数据结构(3.2之后)
根据存储的内容来


3、扩容机制
容量不够时,扩容为原来的两倍,直到1024k,不在成倍增加,而是以1024k的增加。

Go中使用Redis

Go中也有很多比较流行的并且开源Redis库,比如go-redis或redigo,在github上,分别12.3k和8.6k的star数量(截止到2021.09.03)

在这里将以go-redis为例。

安装

第一步:安装go-redis

1
2
bash复制代码请勿省略版本号
go get github.com/go-redis/redis/v8

配置连接

第二步:连接Redis服务器
连接Redis服务器有两种方法,第一种使用redis.Options,第二种就是使用redis.ParseURL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码1、第一种
import "github.com/go-redis/redis/v8"

rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})

2、第二种
opt, err := redis.ParseURL("redis://<user>:<pass>@localhost:6379/<db>")
if err != nil {
panic(err)
}

rdb := redis.NewClient(opt)

那么,我这里使用从我的配置里读取,不会使用go读取配置文件可以参考

Go(三)Go配置文件

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
bash复制代码
import (
"context"
"github.com/go-redis/redis/v8"
"log"
)

var rdb *redis.Client

// GetRedis 获取连接
func GetRedis() *redis.Client {
return rdb
}

func InitRedis() {
rdb = redis.NewClient(&redis.Options{
Addr: Redis.Host + ":" + Redis.Port,
Password: Redis.Password,
DB: Redis.DB,
})
_,err := rdb.Ping(context.Background()).Result()
if err != nil{
log.Printf("redis connect get failed.%v",err)
return
}
log.Printf("redis init success")
}

Ping() 旧版本是不需要参数的,从v8版本开始 需要参数 context.Context

编写存取逻辑

第三步:封装 存/获取值 函数

网上大部分教程都是在v8之前的,而在v8需要传入context.Context,所以在存取时需要额外增加一个参数

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
bash复制代码1、存值
var ctx = context.Background()

func SetString(key string,value interface{}) error {
if key == "" || value == nil {
return errors.New(common.ArgsIsNull)
}
rdb := config.GetRedis()
err := rdb.Set(ctx,key,value,0).Err()
if err != nil {
return errors.New(err.Error())
}
log.Infof("push key: %v, value: %v",key,value)
return nil
}


2、取值
func GetString(key string) (interface{},error) {
if key == "" {
return "",errors.New(common.ArgsIsNull)
}
rdb := config.GetRedis()
res,err := rdb.Get(ctx,key).Result()
if err == redis.Nil{
return "",errors.New(fmt.Sprintf(common.ResIsNull,key))
}
if err != nil {
return "",errors.New(err.Error())
}
return res,nil
}

说明:

  1. Set除了context、键、值外,还需要传入一个过期时间
  2. Get方法,返回错误码可能是redis当中不含有该值,所以做一个特殊处理

使用

第四步:使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码func  Test(test *testing.T)  {
err := SetString("1", "12")
if err != nil {
return
}
}
func Test1(test *testing.T) {
res,err := GetString("12")
if err != nil {
log.Debug(err)
}else {
log.Debug(res)
}
}

把值为12 push到键为1,然后在redis中查看

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码1、redis控制台
Connecting ...

Connected.
redis:0>get 1
"12"

2、代码获取
=== RUN Test1
2021-09-03 20:30:38 DEBUG redis/redis_test.go:40 12
--- PASS: Test1 (0.00s)
PASS

Redis 发布/订阅模式

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。

值得注意的是:订阅者接收不到启动之前的消息。

订阅者

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
go复制代码func Receive(key ...string) error {
if len(key) <= 0 {
return errors.New(common.ArgsIsNull)
}
rdb := config.GetRedis()
sub := rdb.Subscribe(ctx, key...)
defer time.AfterFunc(time.Second, func() {
_ = sub.Close()
})
_, err := sub.Receive(ctx)
if err != nil {
return errors.New(err.Error())
}
// 设置channel参数 https://github.com/go-redis/redis/issues/1850
ch := sub.Channel(
redis.WithChannelSize(100),
redis.WithChannelHealthCheckInterval(10*time.Second),
redis.WithChannelSendTimeout(3000 * time.Second),
)
for msg := range ch {
// todo 执行操作
fmt.Println(msg.Channel, msg.Payload)
}
return nil
}

发布者

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码func Publish(key string, value interface{}) error {
if key == "" || value == nil {
return errors.New(common.ArgsIsNull)
}
rdb := config.GetRedis()
err := rdb.Publish(ctx, key, value).Err()
if err != nil {
return errors.New(err.Error())
}
log.Infof("Publish key: %v, value: %v", key, value)
return nil
}

注:如果需要Publish自定义结构体,需要实现MarshalBinary方法。

1
2
3
4
bash复制代码type Msg struct {
Title string `form:"title" json:"title" binding:"required"` // 标题
Content string `form:"content" json:"content" binding:"required"` // 内容
}

当我尝试Publish 自定结构体,就会这样的错误提示,这告诉我们需要将我们的结构体转为二进制形式

1
bash复制代码redis: can't marshal *dto.Msg (implement encoding.BinaryMarshaler)

这里有两种解决方法:
1、我们可以把Msg 结构体实现MarshalBinary方法,Redis将自动执行所有操作

1
2
3
go复制代码func (m *Msg) MarshalBinary() (data []byte, err error) {
return json.Marshal(m)
}

2、或者可以选择在Publish前自己手动转为二进制形式

1
2
3
4
5
6
7
8
9
10
11
go复制代码func  Test3(test *testing.T)  {
msg := &dto.Msg{Title: "123123",Content: "123123"}
marshal, err1 := json.Marshal(msg)
if err1 != nil {
return
}
err := Publish(common.Sms,marshal)
if err != nil {
log.Debug(err)
}
}

Redis小技巧

1、为什么要是用mset或者mget

减少带带宽和io,因为redis需要封装resp协议(需要封装tcp协议与ip协议,各需要20byte),如果使用set或get去设置或获取(k,v)会封装两次,而是用mset k1 v1 k2 v2,就会减少封装次数,减少带宽和io。

2、现在系统有千万级的活跃用户,如何实现日活统计,为了增强用户粘性,要上线一个连续打卡发放积分的功能,怎么实现连续打卡用户统计。

使用setbit设值、getbit、bitcount统计
参考:blog.csdn.net/hgd613/arti…

本文转载自: 掘金

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

认识下GitLab CI/CD

发表于 2021-11-25

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

CI/CD

CI:Continuous integration 持续集成 开发过程中持续地将所有开发人员的代码合并到代码库主线上,然后对该主线代码进行编译、测试运行等操作对代码进行检验。

主要目的是让正在开发的软件始终处于可工作的状态,主要关注代码是否可以编译成功,以及是否可通过单元测试和验收测试等。当开发人员提交新代码,CI服务器自动对这些代码所属服务进行构建,并对其执行全面自动化测试

CD:有两种含义:

  • Continuous delivery 持续集成,让主分支保持可随时可发布的状态
  • Continuous Deployment 持续部署,在持续交付的基础上,负责人自助式向生产环境部署优质的构建版本,自动部署到生产环境上。

持续交付是持续部署的前提。

三大环境

在产品交付过程中需要对产品进行测试,在各种环境中进行各种类型的自动化测试,通过后,产品发布到线上。一般会将环境划分成三个:Test 环境,Staging 环境,Prod环境。

  • Testing 环境,测试人员进行测试的主要环境,主要关注点:功能测试,缺陷修复和回归等
  • Staging 预发布环境,为避免测试环境和生产环境因某些特性所带来的缺陷缺漏而设立的一套环境
  • Prod 生产环境,真实用户使用的环境

Gitlab CI/CD

基本概念

  • Pipelines:流水线,可以像流水线一样执行多个Job。当代码提交或MR时,gitlab将会在最新的commit 上建立一个pipeline。在同一个pipeline上产生的多个任务重,所用到的代码版本是一致的。
  • Jobs:任务,gitlab ci 系统中可以独立控制并运行的最小单位,开发者可以针对特定的commit 完成一个或多个job,从而进行CI/CD操作。
  • Schedules: 管道调度运行管道在未来,反复,特定的分支或标签。那将要承受有限的安排管道项目基于相关用户的访问。

CI/CD流程配置

在项目中创建 .gitlab-ci.yml 文件,并配置CI/CD流程,提交代码后,系统会自动寻找yml 文件,并对此次的commit 开始一个job。

.gitlab-ci.yml 是个YMAL文件,需要注意下格式

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码stages:
- test
- build
- deploy


before_script:
- apt-get install rubygems ruby-dev -y
run-test:
script:
- ruby --version

提交代码,然后在gitlab上看,你会发现你每次提交的代码都会有一个job任务,即使是不同的分支都会有不同job任务。

使用gitlab CI/CD就是这么简单。

本文转载自: 掘金

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

springboot项目日志配置以及打包 前言

发表于 2021-11-25

前言

在我们写完一个springboot的项目之后,不能避免的会涉及到配置日志以及打包的问题,因为最终项目肯定要在linux上,x86或者arm系统上运行。部署项目以及查看日志都是非常重要的。本篇就这两件事情稍微说一下。

日志配置问题

一般的我自己写的项目做日志配置的时候,就默认使用lombok的slf4j了。使用的基本姿势如下:

引入lombok依赖:

image.png

需要使用的类标识相关注解

image.png

需要的地方直接打日志就可以了

image.png

为了在部署到linux之后能看到完整的log文件,方便定位问题、分析服务状况,需要对日志做一下配置,如下,你需要使用的话只需要修改标签即可,例如我这里定义的ff/vv就是将日志生成到ff文件夹下,名称为vv.%d{yyyy-MM-dd}.log。

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds"
debug="false">
<contextName>paymentContext</contextName>
<property name="log.path" value="./logs" />
<!--输出到控制台 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{76}.%M(%line) - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<!-- 全部日志 -->
<appender name="all" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/ff/vv.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{76}.%M(%line) - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<!-- 输出到warn -->
<appender name="warn" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/ff/vv.warn.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{76}.%M(%line) - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印WARN日志 -->
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- 输出到error -->
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/ff/vv.error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{76}.%M(%line) - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印ERROR日志 -->
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<root level="info">
<appender-ref ref="console" />
<appender-ref ref="all" />
<appender-ref ref="warn" />
<appender-ref ref="error" />
</root>

</configuration>

项目打包问题

项目打包,如果单纯使用下面的spring-boot-maven-plugin,效果可能并不好。因为它会将配置文件、lib包全部打到最终的jar包里面,这显然不是想要的。原因在于,我并不打算将配置文件和lib包全部打到最终的jar包里面,因为例如想修改下配置文件,不可能全部重新打包再上传到要部署服务的系统,而是简单地修改下配置文件直接重启就可以了。又比如lib中的依赖没什么问题,仅仅是代码写的有问题,那么我只想修改下代码重新上传再重启就可以了。而不是顺带着传递庞大的lib包。

image.png

没对比或许没什么伤害,我们先看下上面的这个spring的maven打包插件打出的包的效果。

image.png

image.png

BOOT_INF下面直接携带了整个的lib。配置文件则是出现在classes下面.这不是我们想要的,这就意味着,每次我改一行代码,除了代码之外都还要重新上传整个的lib和配置文件,太糟糕了。

研究了很久,最终用apache的打包插件打出了自己想要的包,将需要的插件整个的粘贴给大家。

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
xml复制代码<build>
<plugins>
<plugin>
<!--打包时去除第三方依赖-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layout>ZIP</layout>
<includes>
<include>
<groupId>non-exists</groupId>
<artifactId>non-exists</artifactId>
</include>
</includes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<!-- 指定打包的jar包输出路径 -->
<outputDirectory>target/hhh</outputDirectory>
<excludes>
<exclude>**/static/**</exclude>
<exclude>**/templates/**</exclude>
<exclude>**/*.properties</exclude>
<exclude>**/*.xml</exclude>
<exclude>**/*.yml</exclude>
</excludes>
<forceCreation>true</forceCreation>
</configuration>
</plugin>
<!--拷贝第三方依赖文件到指定目录-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<!--target/lib是依赖jar包的输出目录,根据自己喜好配置-->
<outputDirectory>target/hhh/lib</outputDirectory>
<excludeTransitive>false</excludeTransitive>
<stripVersion>false</stripVersion>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<copy todir="target/hhh/resources">
<fileset dir="target/classes">
<exclude name="com/**" />
<exclude name="org/**" />
<include name="**/static/**" />
<include name="**/templates/**" />
<include name="**/*.properties" />
<include name="**/*.xml" />
<include name="**/*.yml" />
</fileset>
</copy>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>

<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>

</build>

现在咱们再来看下整个的打包效果。

image.png

ok.是我们期待的结果了。经过验证,打出来的文件都是正常没有问题的。整个的jar包也非常的干净!

还有一个问题需要提及下,原本的结构中。直接使用java -jar就可以启动项目了,但是现在是不行的。你需要自己指定下依赖包和配置文件!

image.png

下面粘贴下整个的一个启动、停止、状态shell脚本。

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
sh复制代码#!/bin/bash
#这里可替换为你自己的执行程序,BASE_PROJECT_NAME为部署时项目基础路径(bin盛放启动脚本,项目基础路径指的是bin的上层路径),其他代码无需更改
APP_NAME=common-project-1.0-SNAPSHOT.jar

BASE_PROJECT_NAME="icp-service"

JAVA_OPTS="-server -Xms10m -Xmx20m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC"

#使用说明,用来提示输入参数
usage() {
echo "Usage: sh 脚本名.sh [start|stop|restart|status]"
exit 1
}

#检查程序是否在运行
is_exist(){
pid=`ps -ef|grep $APP_NAME|grep -v grep|awk '{print $2}' `
#如果不存在返回1,存在返回0
if [ -z "${pid}" ]; then
return 1
else
return 0
fi
}

#启动方法
start(){
is_exist
if [ $? -eq "0" ]; then
echo "${APP_NAME} is already running. pid=${pid} ."
else
nohup java $JAVA_OPTS -cp lib/*:resources/:../$BASE_PROJECT_NAME/$APP_NAME com.xxx.CommonApplication >/dev/null 2>&1 &
echo "${APP_NAME} start success"
fi
}

#停止方法
stop(){
is_exist
if [ $? -eq "0" ]; then
kill -9 $pid
else
echo "${APP_NAME} is not running"
fi
}

#输出运行状态
status(){
is_exist
if [ $? -eq "0" ]; then
echo "${APP_NAME} is running. Pid is ${pid}"
else
echo "${APP_NAME} is NOT running."
fi
}

#重启
restart(){
stop
start
}

#根据输入参数,选择执行对应方法,不输入则执行使用说明
case "$1" in
"start")
start
;;
"stop")
stop
;;
"status")
status
;;
"restart")
restart
;;
*)
usage
;;
esac

启动命令最后这里的>/dev/null 2>&1 &,因为我在日志配置中已经指定日志的输出位置了,所以这边不再将其单独输出到某文件,有需要的话,可以自己指定文件!

本篇到此结束。

本文转载自: 掘金

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

Redis 主从架构之原理篇(三)

发表于 2021-11-25

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

上文中我们已经对 redis 的主从同步的配置文件和原理问题进行了探讨,本文将回答两个工作中经常被问到的问题,供大家赏析。

在主从复制过程中,关闭 master 的持久化会引发什么问题呢?

数据会从 master 和所有 replica 中被删除。我们用案例来说明一下:

  1. 我们设置节点 A 为 master 并关闭它的持久化设置,设置节点 B 和 C 为 replica;
  2. 当 master 崩溃时,由于系统中配置了自动重启的脚本,此时 master 会自动重启。但是由于持久化被关闭了,master 重启后其数据集合为空;
  3. 此时,如果 replica 从 master 中同步数据,就会导致 replica 中的数据也会变为空集合。

因此,我们在使用 Redis 复制功能时,强烈建议在 master 和 replica 中启用持久化。如果因为非常慢的磁盘性能导致的延迟问题而不启用持久化时,应该配置节点来避免重置后自动重启。

Redis 复制如何处理 key 的过期问题

Redis 的过期机制可以限制 key 的生存时间,该机制取决于 Redis 计算时间的能力。但是,即使使用 Lua 脚本将这些 key 变为过期的 key,Redis replicas 也能正确地复制这些 key。

为了实现这样的功能,Redis 不能依靠主从使用同步时钟,因为这是一个无法解决的并且会导致 race condition 和数据集不一致的问题,所以 Redis 使用三种主要的技术使过期的 key 的复制能够正确工作:

  • replica 不会让 key 过期,而是等待 master 让 key 过期。当一个 master 让一个 key 到期(或由于 LRU 算法将之驱逐)时,它会合成一个 DEL 命令并传输到所有的 replica;
  • 由于主驱动的原因,master 无法及时提供 DEL 命令,所以有时候 replica 的内存中仍然可能存在逻辑上已经过期的 key。为了处理这个问题,replica 使用它的逻辑时钟来报告在不违反数据一致性的前提下,读取操作的 key 不存在。用这种方法,replica 避免报告逻辑过期的 key 仍然存在。在实际应用中,使用 replica 程序进行扩展的 HTML 碎片缓存,将避免返回已经比期望的时间更早的数据项。
  • 在Lua脚本执行期间,不执行任何 key 过期操作。当一个Lua脚本运行时,从概念上讲,master 中的时间是被冻结的,这样脚本运行的时候,一个给定的键要么存在要么不存在。这可以防止 key 在脚本中间过期,保证将相同的脚本发送到 replica ,从而在二者的数据集中产生相同的效果。

一旦一个 replica 被提升为一个 master ,它将开始独立地过期 key,而不需要任何旧 master 的帮助。

以上就是今天的全部内容了,如果你有不同的意见或者更好的idea,欢迎联系阿Q,添加阿Q可以加入技术交流群参与讨论呦!

本文转载自: 掘金

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

Zookeeper4字运维指令:is not execute

发表于 2021-11-25

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

问题

zookeeper4字运维指令:is not executed because it is not in the whitelist

如果需要直接看解决办法的可以直接路过分析过程,看最后面,其实原因分析也很简单。

问题描述

本来是想用wchc查看监听路径信息的,结果使用这个指令查询的时候,zookeeper服务器返回的响应信息是:

wchc is not executed because it is not in the whitelist.

分析原因

查了下日志,找到了这条信息是从这个类org.apache.zookeeper.server.NettyServerCnxn里返回的,然后直接上github找到源码在这个类里,找到这条信息出现的方法体内,如下:

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
java复制代码/** Return if four letter word found and responded to, otw false **/
private boolean checkFourLetterWord(final Channel channel,
ChannelBuffer message, final int len) throws IOException
{
// We take advantage of the limited size of the length to look
// for cmds. They are all 4-bytes which fits inside of an int
if (!FourLetterCommands.isKnown(len)) {
return false;
}

String cmd = FourLetterCommands.getCommandString(len);

channel.setInterestOps(0).awaitUninterruptibly();
packetReceived();

final PrintWriter pwriter = new PrintWriter(
new BufferedWriter(new SendBufferWriter()));

// ZOOKEEPER-2693: don't execute 4lw if it's not enabled.
// 根据代码可以看到这个地方是用来判断是否启用这个指令,如果不启用,就会返回这条信息。
if (!FourLetterCommands.isEnabled(cmd)) {
LOG.debug("Command {} is not executed because it is not in the whitelist.", cmd);
NopCommand nopCmd = new NopCommand(pwriter, this, cmd +
" is not executed because it is not in the whitelist.");
nopCmd.start();
return true;
}

LOG.info("Processing " + cmd + " command from "
+ channel.getRemoteAddress());

if (len == FourLetterCommands.setTraceMaskCmd) {
ByteBuffer mask = ByteBuffer.allocate(8);
message.readBytes(mask);
mask.flip();
long traceMask = mask.getLong();
ZooTrace.setTextTraceLevel(traceMask);
SetTraceMaskCommand setMask = new SetTraceMaskCommand(pwriter, this, traceMask);
setMask.start();
return true;
} else {
CommandExecutor commandExecutor = new CommandExecutor();
return commandExecutor.execute(this, pwriter, len, zkServer,factory);
}
}

看我中文注释的地方,就可以确定问题点在FourLetterCommands这个类里,然后进入这个类:org.apache.zookeeper.server.command.FourLetterCommands,找到isEnabled这个方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
java复制代码 /**
* Check if the specified command is enabled.
*
* In ZOOKEEPER-2693 we introduce a configuration option to only
* allow a specific set of white listed commands to execute.
* A command will only be executed if it is also configured
* in the white list.
*
* @param command The command string.
* @return true if the specified command is enabled
*/
public synchronized static boolean isEnabled(String command) {
// whiteListInitialized这个值不用关心,它就是是否初始化过的标记,默认是false,第一次进入这个方法体后,whiteListedCommands相关命令加载完成后,就认为初始化完成了,这个值就为true了
if (whiteListInitialized) {
return whiteListedCommands.contains(command);
}

// 根据下面这几行代码,便可以看出,这些4字指令,是配置在VM变量内的,而key值是ZOOKEEPER_4LW_COMMANDS_WHITELIST,其实它的常量定义在最上面,这个常量的值为:zookeeper.4lw.commands.whitelist
//看下面的代码便知道,4字指令的格式是用逗号(,)分隔,也可以直接用*,则会把cmd2String里已经缓存的所有指令,迭代的添加到whiteListedCommands这个白名单里
String commands = System.getProperty(ZOOKEEPER_4LW_COMMANDS_WHITELIST);
if (commands != null) {
String[] list = commands.split(",");
for (String cmd : list) {
if (cmd.trim().equals("*")) {
for (Map.Entry<Integer, String> entry : cmd2String.entrySet()) {
whiteListedCommands.add(entry.getValue());
}
break;
}
if (!cmd.trim().isEmpty()) {
whiteListedCommands.add(cmd.trim());
}
}
}

// It is sad that isro and srvr are used by ZooKeeper itself. Need fix this
// before deprecating 4lw.
if (System.getProperty("readonlymode.enabled", "false").equals("true")) {
whiteListedCommands.add("isro");
}
// zkServer.sh depends on "srvr".
whiteListedCommands.add("srvr");
whiteListInitialized = true;
LOG.info("The list of known four letter word commands is : {}", Arrays.asList(cmd2String));
LOG.info("The list of enabled four letter word commands is : {}", Arrays.asList(whiteListedCommands));
return whiteListedCommands.contains(command);
}

下面是cmd2String初始化缓存的所有指令,结合上面的代码来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码    // specify all of the commands that are available
static {
cmd2String.put(confCmd, "conf");
cmd2String.put(consCmd, "cons");
cmd2String.put(crstCmd, "crst");
cmd2String.put(dirsCmd, "dirs");
cmd2String.put(dumpCmd, "dump");
cmd2String.put(enviCmd, "envi");
cmd2String.put(getTraceMaskCmd, "gtmk");
cmd2String.put(ruokCmd, "ruok");
cmd2String.put(setTraceMaskCmd, "stmk");
cmd2String.put(srstCmd, "srst");
cmd2String.put(srvrCmd, "srvr");
cmd2String.put(statCmd, "stat");
cmd2String.put(wchcCmd, "wchc");
cmd2String.put(wchpCmd, "wchp");
cmd2String.put(wchsCmd, "wchs");
cmd2String.put(mntrCmd, "mntr");
cmd2String.put(isroCmd, "isro");
cmd2String.put(telnetCloseCmd, "telnet close");
}

重点看我上面代码的中文说明,便明白,可以在启动脚本里添加VM环境变量-Dzookeeper.4lw.commands.whitelist=*,便可以把所有指令添加到白名单,我是添加在脚本的这个位置:

1
2
3
4
5
6
7
8
9
bash复制代码    ZOOMAIN="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=$JMXPORT -Dcom.sun.management.jmxremote.authenticate=$JMXAUTH -Dcom.sun.management.jmxremote.ssl=$JMXSSL -Dzookeeper.jmx.log4j.disable=$JMXLOG4J org.apache.zookeeper.server.quorum.QuorumPeerMain"
fi
else
echo "JMX disabled by user request" >&2
ZOOMAIN="org.apache.zookeeper.server.quorum.QuorumPeerMain"
fi
# 这里就是我添加的
# 如果不想添加在这里,注意位置和赋值的顺序
ZOOMAIN="-Dzookeeper.4lw.commands.whitelist=* ${ZOOMAIN}"

然后重启,问题就解决了

本文转载自: 掘金

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

Java代理模式之Java解释器模式 Java解释器模式

发表于 2021-11-25

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

Java解释器模式

解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。

介绍

意图:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。

主要解决:对于一些固定文法构建一个解释句子的解释器。

何时使用:如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。

如何解决:构建语法树,定义终结符与非终结符。

关键代码:构建环境类,包含解释器之外的一些全局信息,一般是 HashMap。

应用实例:编译器、运算表达式计算。

优点: ① 可扩展性比较好,灵活。 ② 增加了新的解释表达式的方式。 ③ 易于实现简单文法。

缺点: ① 可利用场景比较少。 ② 对于复杂的文法比较难维护。 ③ 解释器模式会引起类膨胀。 ④ 解释器模式采用递归调用方法。

使用场景: ① 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。 ② 一些重复出现的问题可以用一种简单的语言来进行表达。 ③ 一个简单语法需要解释的场景。

注意事项:可利用场景比较少,JAVA 中如果碰到可以用 expression4J 代替。

实现

我们将创建一个接口 Expression 和实现了 Expression 接口的实体类。定义作为上下文中主要解释器的 TerminalExpression 类。其他的类 OrExpression、AndExpression 用于创建组合式表达式。

InterpreterPatternDemo,我们的演示类使用 Expression 类创建规则和演示表达式的解析。

步骤 1

创建一个表达式接口。

1
2
3
arduino复制代码public interface Expression {
public boolean interpret(String context);
}

步骤 2

创建实现了上述接口的实体类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码public class TerminalExpression implements Expression {

private String data;

public TerminalExpression(String data){
this.data = data;
}

@Override
public boolean interpret(String context) {
if(context.contains(data)){
return true;
}
return false;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class OrExpression implements Expression {

private Expression expr1 = null;
private Expression expr2 = null;

public OrExpression(Expression expr1, Expression expr2) {
this.expr1 = expr1;
this.expr2 = expr2;
}

@Override
public boolean interpret(String context) {
return expr1.interpret(context) || expr2.interpret(context);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class AndExpression implements Expression {

private Expression expr1 = null;
private Expression expr2 = null;

public AndExpression(Expression expr1, Expression expr2) {
this.expr1 = expr1;
this.expr2 = expr2;
}

@Override
public boolean interpret(String context) {
return expr1.interpret(context) && expr2.interpret(context);
}
}

步骤 3

InterpreterPatternDemo 使用 Expression 类来创建规则,并解析它们。

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 InterpreterPatternDemo {

//规则:Robert 和 John 是男性
public static Expression getMaleExpression(){
Expression robert = new TerminalExpression("Robert");
Expression john = new TerminalExpression("John");
return new OrExpression(robert, john);
}

//规则:Julie 是一个已婚的女性
public static Expression getMarriedWomanExpression(){
Expression julie = new TerminalExpression("Julie");
Expression married = new TerminalExpression("Married");
return new AndExpression(julie, married);
}

public static void main(String[] args) {
Expression isMale = getMaleExpression();
Expression isMarriedWoman = getMarriedWomanExpression();

System.out.println("John is male? " + isMale.interpret("John"));
System.out.println("Julie is a married women? "
+ isMarriedWoman.interpret("Married Julie"));
}
}

步骤 4

执行程序,输出结果:

1
2
csharp复制代码John is male? true
Julie is a married women? true

本文转载自: 掘金

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

swc初体验

发表于 2021-11-25

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

swc 是什么

在开始说swc之前,我们先来了解一下Rust这门编程语言。

Rust是一种快速、可靠、内存高效的编程语言。它是 C++ 或 C 等语言的现代替代品。
Rust与其他语言最大的不同就是在内存管理方面建立了一种新的概念叫做所有权。

可能这样说比较抽象,下面有一段代码可以感受一下所有权。

1
2
3
4
5
6
7
rust复制代码fn main() {
let arr = vec![1, 2, 3];
let new_arr = arr;

// 无奖竞猜,打印啥
println!("{:?}", arr);
}

点击查看结果

其他的关于Rust的一些特性和细节就不过多介绍了,我们依旧把重心放到swc上。

对Rust感兴趣的小伙伴可以看一下我的(Rust学习笔记)

目前Rust被应用在前端工具链方面,如压缩(Terser)、编译(Babel)、格式化(Prettier)、打包(webpack)等场景中。而今天我们要讲的swc做的就是做的这样一件事。

我们来看一下swc的官方介绍:

SWC (stands for Speedy Web Compiler) is a super-fast TypeScript / JavaScript compiler written in Rust.

swc的出现其实很大一部分原因是要替换掉工程中的babel,所以babel有的功能他几乎都有。

与babel最大的区别可能就是: swc is super-fast

swc官网中还有这样一句话,体现了他的速度:

除了swc的官方宣传外,Next.js基于swc实现了一个Rust编译器,用来解析编译、打包代码。下面是Next.js结合swc之后给出的一个数据:

所以从上面这些数据也可以简单看出来swc的优势:可以提高开发效率,提升开发体验。

这也是目前很多工程选择接入它的原因。

swc 怎么用

基本使用

  1. 安装依赖:npm i -D @swc/cli @swc/core
  2. 运行命令:npx swc ./index.js -o output.js(编译单个文件)

执行过命令之后会把结果打印在标准输出里,并没有生成文件之类的。

如果想要输出的文件中需要携带参数来完成-o ouput.js或者-d dist编译到dist目录下

Bundling

  1. 根目录下需要有一个配置文件spack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码const { config } = require('@swc/core/spack');

module.exports = config({
entry: {
web: './src/index.js',
// 可以配置多入口
},
output: {
path: './bundle/',
},
module: {},
options:{},
});
  1. 运行命令npx spack进行打包

打包支持tree-shaking、Commonjs模块、提取公共模块等。

详细配置项:swc.rs/docs/config…

Plugin

swc的plugin其实就是将核心包中的一些API暴露出来,给开发者做一些自定义的操作。

看个例子,这个例子的作用就是将代码中的console.log()过滤掉,用void 0代替

用过Bable的同学看起来可能会简单一些。

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
js复制代码const Visitor = require('@swc/core/Visitor').default;
const { transformSync } = require('@swc/core');

module.exports = class ConsoleStripper extends Visitor {
visitCallExpression(expression) {
if (expression.callee.type !== 'MemberExpression') {
return expression;
}
// 判断代码类型以及对应的value是否为console
if (
expression.callee.object.type === 'Identifier' &&
expression.callee.object.value === 'console'
) {
// 如果是就替换为`void 0`
if (expression.callee.property.type === 'Identifier') {
return {
type: 'UnaryExpression',
span: expression.span,
operator: 'void',
argument: {
type: 'NumericLiteral',
span: expression.span,
value: 0,
},
};
}
}

return expression;
}
};

const out = transformSync(
`
if (foo) {
console.log("Foo")
} else {
console.log("Bar")
}`,
{
plugin: (m) => new ConsoleStripper().visitProgram(m),
}
);

值得一提的是,目前swc的插件系统是有性能问题存在的。该性能问题主要聚焦在两个方面

  1. 将AST通过Rust传递给JS时会产生通讯上的损耗
  2. 在JS中通过JSON.parese()去转换代码时所产生的损耗。

目前swc也正在着手解决这个问题。点击吃瓜

上面提到的是我认为可能比较常用的一些东西,当然除此之外swc还提供了像Jest、和wasm等工具,而且swc还提供了一个loader供开发者在webpack中使用。

swc 为什么快

因为JavaScript本身就有点慢。

我们先来看一下js的执行流程:

这其中转换为AST以及编译成字节码应该是最耗费性能的。

而swc是直接将代码根据不同平台来编译成对应的二进制文件,省略了前面最耗时的步骤。

接下来我们拿上面那个Plugin来看一下swc在代码转换过程中大概是怎么执行的

接下来的整个过程就是其实就是证实swc在编译代码的时候是直接由二进制文件中的代码来执行的。

我们将断点打在transformSync处,看一下此处的执行:

来看一下比较重要的一部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码transformSync(src, options) {

// ...

const { plugin } = options, newOptions = __rest(options, ["plugin"]);
// 是否有plugin
if (plugin) {
const m = typeof src === "string" ? this.parseSync(src, (_c = options === null || options === void 0 ? void 0 : options.jsc) === null || _c === void 0 ? void 0 : _c.parser, options.filename) : src;
return this.transformSync(plugin(m), newOptions);
}
// 最终的输出都是bindings.transformSync
return bindings.transformSync(isModule ? JSON.stringify(src) : src, isModule, toBuffer(newOptions));
}

大致执行流程:

从上面的执行图中可以看出来,我们获取到的结果,最终都是由bindings.transformSync解析完成,然后输出的结果。

从源码中可以找到bindings的入口,我们在这里打个断点,从这里看一下bindings的执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码function loadBinding(dirname, filename = 'index', packageName) {

// 获取系统信息
const triples = triples_1.platformArchTriples[PlatformName][ArchName];

// 遍历系统信息
for (const triple of triples) {
if (packageName) {
try {
// 获取到需要加载的二进制文件路径
// /Users/xx/swc-demo/node_modules/@swc/core-darwin-x64/swc.darwin-x64.node
return require(
require.resolve(
`${packageName}-${triple.platformArchABI}`,
{ paths: [dirname] }
));
}
catch (e) {
// ...
}
}
// ...
}

流程图:

我们最后得到的结果就是require进来一个二进制文件。

这个文件的路径大概是这样的:/Users/xx/swc-demo/node_modules/@swc/core-darwin-x64/swc.darwin-x64.node,当然这会根据不同电脑生成不同的路径~

来看一下,swc的包里这个文件:

为了证实它是一个二进制文件,我们可以点开看一下(这里是用了vscode的插件,才可以看到)

打开之后是这样的,这里我们就不做过多解读了,有兴趣的可以逐行翻译一下。

总结

其实从上面看来swc的执行流程并不算很复杂,当然我们看到的是编译过的代码,大家也可以尝试去看一下swc的rust源码。看完之后应该会有一个感受:

JS是世界上最美丽的语言。

ok,本篇文章就是浅出的了解一下swc是什么,有兴趣的同学可以自己下来玩一下,下面我把我自己体验时候的代码仓库放到下面,有一些debug的配置已经配好了,大家可以尝试玩一下。

  • 代码:github.com/rust-toys/s…
  • swc:swc.rs/

本文转载自: 掘金

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

1…202203204…956

开发者博客

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