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

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


  • 首页

  • 归档

  • 搜索

无处不在的 Kubernetes,难用的问题解决了吗? 难用

发表于 2021-11-18

简介: 从第三方的调研数据看,容器和 Kubernetes 已经成为云原生时代主流的选择,但实际落地的时候,却陷入了困境。我们尝试去总结了一些共通点,以及应对方案,也许能为正在落地容器技术的企业提供一些参考。

作者:望宸、木环、溪洋等

容器本质是一项隔离技术,很好的解决了他的前任 - 虚拟化未解决的问题:运行环境启动速度慢、资源利用率低,而容器技术的两个核心概念,Namespace 和 Cgroup,恰到好处的解决了这两个难题。Namespace 作为看起来是隔离的技术,替代了 Hypervise 和 GuestOS,在原本在两个 OS 上的运行环境演进成一个,运行环境更加轻量化、启动快,Cgroup 则被作为用起来是隔离的技术,限制了一个进程只能消耗整台机器的部分 CPU 和内存。

当然,容器技术之所以能流行,更因为他提供了标准化的软件开发交付物 - 容器镜像。基于容器镜像,持续交付这件事才能够真正落地。

我们还能罗列出许多使用容器技术的理由,这里就不再一一赘述。

同时,云计算解决了基础资源层的弹性伸缩,却没有解决 PaaS 层应用随基础资源层弹性伸缩而带来的批量、快速部署问题。于是,容器编排系统应运而生。

从第三方的调研数据看,容器和 Kubernetes 已经成为云原生时代主流的选择,但实际落地的时候,却陷入了困境。我们尝试去总结了一些共通点,以及应对方案,也许能为正在落地容器技术的企业提供一些参考。

难用在哪?

容器和 Kubernetes 的先进性毋庸置疑,但当大量的企业在开始拥抱容器编排领域的事实标准 Kubernetes 时,却陷入了困境。“K8s 就像一把双刃剑,既是最佳的容器编排技术,同时也存在相当高的复杂性和应用的高门槛,这个过程中往往会导致一些常见性错误”。就连 Kubernetes 的创立者和核心推动者 Google 本身都承认这个问题。

一次采访中,阿里巴巴高级技术专家张磊分析了 Kubernetes 的本质,他指出,“Kubernetes 本身是一个分布式系统而不是一个简单的 SDK 或者编程框架,这本身已经将其复杂度提升到了系统级分布式开源项目的位置。此外,Kubernetes 第一次将声明式 API 的思想在开源基础设施领域普及开来,并以此为基础提出了一系列诸如容器设计模式和控制器模型等使用范式,这些具有一定先进性和前瞻性的设计也使得 Kubernetes 项目被大众接受时会存在一定的学习周期。”

我们大致总结了 Kubernetes 的 4 大复杂性。

1、认知复杂:他和原有的后端研发体系不同,延伸出一套全新的理论,并提供了一系列全新的技术概念,并且这些概念,例如 Pod、sidecar、Service、资源管理、调度算法和 CRD 等等,主要是面向平台研发团队而非应用开发者设计,提供很多强大灵活的能力。但是,这不仅带来了陡峭的学习曲线,影响了应用开发者的使用体验,甚至在很多情况下理解不当还会引发错误操作,乃至生产故障。

2、开发复杂:K8s 使用声明式方法来编排和管理容器。为了实现这一点,需要配置一个 YAML 文件,但再复杂的应用程序中,引入新环节影响了开发者的生产力和敏捷性。此外,缺乏内置的编程模型,开发者需要依赖第三方库来处理程序间的依赖关系,这些都会影响到开发效率,并增加不必要的 DevOps 开销。

3、迁移复杂:将现有的应用程序迁移到 K8s 比较复杂,尤其是非微服务架构,在很多情况下,必须重构特定组件或整个架构,并且需要使用云原生原理重构应用程序,例如状态依赖,如写本地目录、有顺序,网络依赖,如写死 IP,数量依赖,如副本固定等等。

4、运维复杂:K8s 的声明式 API 颠覆了传统的过程式运维模式,声明式 API 对应的是面向终态的运维模式。而随着 K8s 集群规模的增长,徒手基于开源K8s,运维难度也会呈线性增长,呈现在集群管理、应用发布、监控、日志等多个环节,集群稳定性将面临极高的挑战。

是否还有别的解法?

技术总有双面性,容器革新了云计算的基础设施,成为了新的计算界面。而 Kubernetes 则搭建了一个统一的基础设施抽象层,为平台团队屏蔽掉了“计算”、“网络”、“存储”等过去我们不得不关注的基础设施概念,使得我们能够基于 Kubernetes 方便地构建出任何我们想要的垂直业务系统而无需关心任何基础设施层的细节。这正是 Kubernetes 被称为云计算界的 Linux 以及 “Platform for Platforms” 的根本原因。

但直接上手操作 Kubernetes 是否是我们应用容器技术的唯一选择呢?答案是否定的。在容器技术的演进过程中,我们也发现了不少能够降低容器编排门槛的开源项目和商业化产品,接下来,我们将从双手的解放程度由低到高,一一介绍。

围绕 Kubernetes 生态的开源工具

OAM/KubeVela 是托管在 CNCF 中的开源项目,旨在降低 K8s 在应用开发和运维上的复杂性,最初由阿里云和微软云联合发起。

KubeVela 作为开放应用架构模型 OAM 的标准实现,与底层基础设施和无关、原生可扩展,而且最重要的是它完全以应用为中心。在 KubeVela 中,“应用”被设计为整个平台的「一等公民」。应用团队只需要围绕组件、运维特征、工作流等几个跨平台、跨环境的上层抽象来进行应用的交付与管理,无需关注任何基础设施细节和差异性;平台管理员则可以随时以 IaC 的方式配置平台支持的组件类型和运维能力集等特性,以便适配任何应用托管场景。

KubeVela 是完全基于 K8s 构建的,具备天然的被集成能力和普适性,天然透出 K8s 及其生态的所有能力,而不是往上叠加抽象。因此 KubeVela 适用于那些具备一定的 K8s 平台开发和运维能力,同时希望能够使用到全套 K8s 能力,不断扩展平台能力的技术团队。

容器已经从一项隔离技术演进成一个生态,KubeVela 这类可以极大降低 K8s 使用复杂度的开源工具,会逐步释放其生命力,使开发人员无需成为 K8s 专家即可享受到云原生带来的高效与便捷。

sealer 是一款开源的分布式应用打包交付运行的方案,极大的简化了容器项目的交付复杂性和一致性问题。sealer 构建出来的产物可称之为”集群镜像”,并内嵌了 K8s,该”集群镜像”可以 push 到 registry 中共享给其他用户使用,也可以在官方仓库中找到非常通用的分布式软件直接使用。

交付是容器生态的另一个难题,面临着依赖复杂、一致性的问题,尤其是工业级的 Kubernetes 交付项目,交付周期变长、交付质量要求高,sealer 非常适合于软件开发商、ISV 等性质的企业,可将部署时间缩短至小时级别。

开放、标准化的企业级 Kubernetes 服务

大部分云厂商提供了 Kubernetes as a Service 的容器平台能力,比如 AWS EKS 和阿里云的 ACK,可以大幅简化 K8s 集群的部署、运维、网络存储、安全管理等能力,提供了通过 CNCF 标准化认证的 K8s服务,可以满足几乎全场景的工作负载需求,并提供了丰富的扩展和定制能力。此外,大部分云厂商会基于开源 Kubernetes 框架,在上层做不同程度的封装,来适配不同企业背景和不同场景下的需求,以提供发行版和 Pro 版,例如阿里云的 ACK Pro 就提供了托管 master 和全托管节点池的能力,全面整合IaaS能力,更加高效、安全、智能,将容器集群的各种细分场景最佳实践和全栈优化作为内置服务提供给企业。

从现有用户规模来看,这是大部分互联网企业落地容器技术的主流选择。

更多信息请移步至:《阿里云容器服务多项重磅发布:高效智能、安全无界的新一代平台》 。

向 Serverless 演进的 Kubernetes 服务

传统的 Kubernetes 采用以节点为中心的架构设计:节点是 Pod 的运行载体,Kubernetes 调度器在工作节点池中选择合适的 node 来运行 Pod。

而对于 Serverless Kubernetes 而言,最重要的一个概念是将容器的运行时和具体的节点运行环境解耦。用户无需关注 node 运维和安全,降低运维成本;而且极大简化了容器弹性实现,无需按照容量规划,按需创建容器应用 Pod 即可;此外 Serverless 容器运行时可以被整个云弹性计算基础设施所支撑,保障整体弹性的成本和规模。

众多云厂商也将容器和 Serverless 做进一步的融合:例如阿里云的 Serverless 容器服务 ASK、Google GKE 的 AutoPilot,以免运维的方式降低了客户在 K8s 节点和集群上的操作复杂度,无需购买服务器即可直接部署容器应用;同时,仍然可以通过 K8s 命令行和 API 来部署容器应用,充分利用了 K8s 的编排能力,并且根据应用配置的 CPU 和内存资源量进行按需付费。

这类服务非常善于处理一些 Job 类的任务,例如 AI 领域的算法模型训练,同时拥有在 K8s 环境下比较一致的开发体验,是容器服务生态非常好的补充。

更多信息可以移步至:《Serverless Kubernetes:理想,现实与未来》。

容器和 Serverless 技术加持的新一代 PaaS 服务

企业级市场的需求总是分层的、多样化的,这和技术人才的分布紧密相关,并不是每家企业都能建立一个技术实力足够强的团队,尤其是在非北上广深的城市,并且落地一项新技术时,总是分阶段规划的,这就给更多的产品形态孕育了市场空间。

K8s 虽然提供了容器应用的全生命周期管理,但是太丰富、太复杂、太灵活,这既是一种优势,有时候也是一种劣势。尤其是对于习惯了在虚拟机时代,以应用为视角来管理应用的研发运维人员而言,即便像 AWS EKS、阿里云 ASK 等已经在一定程度上降低了 K8s 的操作复杂度,他们仍然希望通过某种方式,可以进一步降低容器技术的使用门槛。

容器和 K8s 并非一定要捆绑使用,在一些新型的 PaaS 服务中,例如阿里云的 Serverless应用引擎(SAE),底层将虚拟化技术改造成容器技术,充分利用了容器的隔离技术,来提升启动时间和资源利用率,而在应用管理层,则保留了原有的微服务应用的管理范式,使用者不必学习庞大而复杂的 K8s 来管理应用。这类新型的 PaaS 服务通常还会内置全套微服务治理能力,客户无需考虑框架选型、更无需考虑数据隔离、分布式事务、熔断设计、限流降级等,也无需担心社区维护力度有限二次定制开发的问题。

此外,底层计算资源池化后,其天然的 Serverless 属性使得用户不必再单独购买和持续保有服务器,而是按 CPU 和内存资源量来配置所需的计算资源,让容器 + Serverless + PaaS 得以合三为一,使得技术先进性、资源利用率优化、不变的开发运维体验可以融合在一起。因此,相比于本文中的其他方案,这类方案的特色是提供了PaaS 体验,让新技术落地更加平稳。

大部分传统行业、一些技术能力偏向于业务层的互联网企业、和一些不希望因受制于后端而影响业务快递迭代的创业公司,大多都会倾向于 PaaS 形态的产品,抛开企业属性,PaaS 类的服务在处理以下场景更具交付优势:

  • 上线了一个新的项目,想快速验证,不要出故障,同时控制人力的投入成本;
  • 业务体量上升快,用户越来越多,业务稳定性有点 hold 不住,新版本发布、线上应用管理等环节开始有点畏手畏脚,但技术储备还无法及时应对当前变化;
  • 决定要把原有的单体架构升级成微服务架构,但由于团队缺少微服务专家,评估完项目发现升级风险比较高。

更多信息可以移步至:《打破 Serverless 落地边界,阿里云 SAE 发布 5 大新特性》。

更为极致的 Serverless 服务 - FaaS

FaaS 的出现,让具备弹性灵活诉求的业务场景,有了更好的选项。越来越多的大中型企业将传统后端领域对扩容有灵活需求的执行单元剥离出来,运行在 Serverless 架构上。

这使得 FaaS(函数计算)成为容器和 K8s 外,另一种通用算力的选择。

和 Google Cloud Run、App Runner 等 Serverless 服务一样,FaaS 的产品形态正变得越来越开放,运行上的限制越来越少,除了适合事件驱动的计算模型,也适合 Web 单体应用、Job 等场景,可以帮助用户把弹性发挥到极致,进一步提升计算资源的利用率。

例如,游戏行业的莉莉丝将函数计算应用于战斗校验,来验证玩家客户端上传的战斗是否有作弊的情况。战斗校验一般需要逐帧计算,CPU 消耗会非常高,通常 1 队 v 1 队的战斗需要 n 毫秒,而 5 队 v 5 队的战斗则需要相应 5n 毫秒,对弹性要求很高。此外,容器架构下挂载的 SLB,会因为轮询机制导致无法感知 Pod 的实际负载,由此引起的负载不均,产生死循环和稳定性风险。

函数计算的调度系统帮助莉莉丝合理安排每个请求,对于死循环问题,也贴心的提供了超时杀进程机制,并将调度系统的复杂性下沉到了基础设施。此外,函数计算深度优化后的冷启动时延大幅下降,从调度、到获取计算资源、再到服务启动,基本在 1 秒+左右。

另外,FaaS 的出现,也极大的解放了创业公司全栈工程师在 DevOps 上花费的精力,来承载小程序、网站等 Web 单体应用,例如,函数计算降低了 Node.js 等前端语言的服务器维护门槛,只要会写 JS 代码就可以维护 Node 服务。

更多信息可以移步至:《跨越行业绊脚石,阿里云函数计算发布 7 大技术突破》

合适的才是最好的

需求的越多,投入的也会越多,这是恒古不变的道理。在我们决定引入容器技术后,使用 K8s 之前,需要想清楚为什么需要 K8s。

如果我们希望充分利用 K8s 的全套能力,并且团队具备一定的技术储备,那么 KubeVela 是理想的开源选型,sealer 还能帮助我们降低交付复杂度;如果希望将 K8s 上层不同程度的封装工作交给云厂商来处理,以更高效的适配不同业务场景下的需求,那么云厂商提供的商业化的容器服务是不错的选择;如果容器和 K8s 无法契合弹性业务类的诉求,则可以选择 FaaS。

但又如果我们的应用并没有那么复杂,只是朴素的希望简化应用生命周期管理和底层基础设施,保障业务的高可用,并专注在业务开发上,那么可能就不需要使用 K8s 来编排容器应用了,毕竟 K8s 是源自 Google 的 Borg,他是用来管理 Google 海量的容器应用的。

参考文章:

  1. 《云计算的前世今生》,刘超
  2. 《灵活、高效的云原生集群管理经验:用 K8s 管理 K8s》,淮右、临石
  3. 《复杂性会成为 Kubernetes 的“致命伤”吗?》,赵钰莹
  4. 《Simplifying Kubernetes For

Developers》,Rishidot Research
5. 《KubeVela 正式开源:一个高可扩展的云原生应用平台与核心引擎》,OAM项目维护者
6. 《KubeVela 1.0 :开启可编程式应用平台的未来》,OAM项目维护者

原文链接

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

本文转载自: 掘金

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

SpringloC容器的依赖注入源码解析(3)—— doGe

发表于 2021-11-18

doGetBean源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
java复制代码protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

// 先将方法传入的name转换成容器真实的beanName
// 之前可以通过三种形式获取beanName
// 一个是原始的beanName,一个是加了&的,一个是别名
final String beanName = transformedBeanName(name);
Object bean;

// Eagerly check singleton cache for manually registered singletons.
Object sharedInstance = getSingleton(beanName);
// 加果先前已经创建过单例Bean的实例,并且调用的getBean方法传入的参数为空,则执行理面的逻辑
// args之所以要求为空是因为如果有args,则需要做进一步赋值,因此无法直接返回
if (sharedInstance != null && args == null) {
if (logger.isTraceEnabled()) {
// 如果Bean还在创建中,则说明是循环引用
if (isSingletonCurrentlyInCreation(beanName)) {
logger.trace("Returning eagerly cached instance of singleton bean '" + beanName +
"' that is not fully initialized yet - a consequence of a circular reference");
}
else {
logger.trace("Returning cached instance of singleton bean '" + beanName + "'");
}
}
// 如果是普通bean,直接返回,如果是FactoryBean,则返回他的getObject
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}

else {
// Fail if we're already creating this bean instance:
// We're assumably within a circular reference.
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}

// Check if bean definition exists in this factory.
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
// Not found -> check parent.
String nameToLookup = originalBeanName(name);
if (parentBeanFactory instanceof AbstractBeanFactory) {
return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
nameToLookup, requiredType, args, typeCheckOnly);
}
else if (args != null) {
// Delegation to parent with explicit args.
return (T) parentBeanFactory.getBean(nameToLookup, args);
}
else if (requiredType != null) {
// No args -> delegate to standard getBean method.
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
else {
return (T) parentBeanFactory.getBean(nameToLookup);
}
}

if (!typeCheckOnly) {
markBeanAsCreated(beanName);
}

try {
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);

// Guarantee initialization of beans that the current bean depends on.
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
if (isDependent(beanName, dep)) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
}
registerDependentBean(dep, beanName);
try {
getBean(dep);
}
catch (NoSuchBeanDefinitionException ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
}
}
}

// Create bean instance.
// 如果BeanDefinition为单例
if (mbd.isSingleton()) {
// 这里使用了一个匿名内部类,创建Bean实例对象,并且注册给所依赖的对象
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}

else {
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new BeanCreationException(beanName,
"Scope '" + scopeName + "' is not active for the current thread; consider " +
"defining a scoped proxy for this bean if you intend to refer to it from a singleton",
ex);
}
}
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
}

// Check if required type matches the type of the actual bean instance.
if (requiredType != null && !requiredType.isInstance(bean)) {
try {
T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
if (convertedBean == null) {
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
return convertedBean;
}
catch (TypeMismatchException ex) {
if (logger.isTraceEnabled()) {
logger.trace("Failed to convert bean '" + name + "' to required type '" +
ClassUtils.getQualifiedName(requiredType) + "'", ex);
}
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
}
return (T) bean;
}

之前讨论了doGetBean里面从缓存获取bean的代码,doGetBean方法接下来的else处理Bean的scope为prototype或者单例模式但是缓存中还不存在bean的情况:
请添加图片描述
Spring同样为scope为prototype的Bean设计了一个缓存列表

1
2
3
java复制代码if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
1
2
3
4
5
java复制代码protected boolean isPrototypeCurrentlyInCreation(String beanName) {
Object curVal = this.prototypesCurrentlyInCreation.get();
return (curVal != null &&
(curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
}

相比单例的set,这里是ThreadLocal,只记录当前线程创建出来的scope为prototype的Bean,上面的if如果是true的话证明有循环依赖。

通过了循环依赖校验之后看容器是否存在父容器,如果存在且当前容器里没有包含此bean的BeanDefinition实例,尝试去从父类容器递归查询

请添加图片描述
为了防止之前的beanName已经被转换的不成样子,将&重新加上,再调用父类的doGetBean或者getBean方法,如果父类是AbstractBeanFactory,则调用doGetBean

如果当前容器里包含了此bean的BeanDefinition实例则继续执行

1
2
3
4
5
java复制代码// typeCheckOnly 是用来判断调用 getBean() 是否仅仅是为了类型检查获取 bean,而不是为了创建Bean
if (!typeCheckOnly) {
// 如果不是仅仅做类型检查则是创建bean
markBeanAsCreated(beanName);
}

进入markBeanAsCreated方法里,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码protected void markBeanAsCreated(String beanName) {
// 双重检查锁机制
if (!this.alreadyCreated.contains(beanName)) {
synchronized (this.mergedBeanDefinitions) {
if (!this.alreadyCreated.contains(beanName)) {
// Let the bean definition get re-merged now that we're actually creating
// the bean... just in case some of its metadata changed in the meantime.
// 将原先合并之后的RootBeanDefinition的需要重新合并的状态设置为true
// 表示需要重新合并一遍,以防原数据的改动
clearMergedBeanDefinition(beanName);
// 将已经创建好的或者正在创建的Bean的名称加到alreadyCreated这个缓存中
this.alreadyCreated.add(beanName);
}
}
}
}

有一个双重锁检查机制,创建的Bean的名称加到alreadyCreated(类型Set)这个缓存中,在加入缓存之前需要将原先的MergedBeanDefinition设置上一个需要清除的标识符,目的是让后续从容器中获取BeanDefinition时重新合并子类和父类的BeanDefinition,这样就可以防止元数据被改动后,BeanDefinition还是按照原来的数据去创建

1
2
3
4
5
6
java复制代码protected void clearMergedBeanDefinition(String beanName) {
RootBeanDefinition bd = this.mergedBeanDefinitions.get(beanName);
if (bd != null) {
bd.stale = true;
}
}

clearMergedBeanDefinition回去容器中获取RootBeanDefinition实例,然后把该实例需要重新合并的状态设为true(之前提到过,只要指定了parent属性,则两个BeanDefinition就合并成一个来使用)


回到doGetBean,接下来会调用getMergedLocalBeanDefinition方法来合并子类和父类的BeanDefinition,进入到该方法里:

1
2
3
4
5
6
7
8
java复制代码protected RootBeanDefinition getMergedLocalBeanDefinition(String beanName) throws BeansException {
// Quick check on the concurrent map first, with minimal locking.
RootBeanDefinition mbd = this.mergedBeanDefinitions.get(beanName);
if (mbd != null && !mbd.stale) {
return mbd;
}
return getMergedBeanDefinition(beanName, getBeanDefinition(beanName));
}

先从mergedBeanDefinitions缓存里获取之前已经合并好的RootBeanDefinition实例,如果stale为true的话就会合并一遍BeanDefinition,随后返回。


回到doGetBean,获取到了BeanDefinition之后,就去对相关实例做合法性校验

1
java复制代码checkMergedBeanDefinition(mbd, beanName, args);

进入到checkMergedBeanDefinition方法里:

1
2
3
4
5
6
7
java复制代码protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args)
throws BeanDefinitionStoreException {

if (mbd.isAbstract()) {
throw new BeanIsAbstractException(beanName);
}
}

看一下RootBeanDefinition是否是抽象的。


回到doGetBean,尝试从BeanDefinition里获取显式的依赖关系,如果有depends-on的话就检查是否有循环依赖关系
请添加图片描述
如果没有循环依赖的话则会将相关的依赖关系注册上

1
java复制代码registerDependentBean(dep, beanName);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public void registerDependentBean(String beanName, String dependentBeanName) {
String canonicalName = canonicalName(beanName);

synchronized (this.dependentBeanMap) {
// computeIfAbsent:若key对应的value为空,会将第二个参数的返回值存入并返回
// dependentBeanMap中存放着当前Bean被引用的Bean的集合
// 比如当前需要实例化的是Bean的名字是userInfo,userInfo中有个Human类型的属性human
// 那么就有human被userInfo引用的关系 human=[userInfo]
Set<String> dependentBeans =
this.dependentBeanMap.computeIfAbsent(canonicalName, k -> new LinkedHashSet<>(8));
if (!dependentBeans.add(dependentBeanName)) {
return;
}
}

synchronized (this.dependenciesForBeanMap) {
Set<String> dependenciesForBean =
this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, k -> new LinkedHashSet<>(8));
dependenciesForBean.add(canonicalName);
}
}

先获取name,之后就进入两重注册,第一重注册往dependentBeanMap中写入键值对,key是被依赖的Bean名字,value是依赖他的Bean名字列表

第二重注册往dependenciesForBeanMap中写入键值对,键值对和上面正好是相反的


回到doGetBean,执行完显式依赖关系注册之后就会递归调用getBean(dep)来将依赖的bean创建出来,往后就是根据不同的scope进行不同的创建bean的操作了,分为Singleton、Prototype和其他。

除了Prototype是直接调用createBean(beanName, mbd, args)去创建Bean实例之外,scope=其他的将createBean封装到了一个匿名参数里
请添加图片描述
进入scope.get()方法

1
java复制代码Object get(String name, ObjectFactory<?> objectFactory);

可见后面的匿名函数实现的是ObjectFactory的getObject方法,调用createBean方法去创建适配scope的实例。

下面主要分析scope = singleton的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码if (mbd.isSingleton()) {
// 这里使用了一个匿名内部类,创建Bean实例对象,并且注册给所依赖的对象
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
// 显式从单例缓存中删除 bean实例
// 因为单例模式下为了解决循环依赖,可能它已经存在了,所以将其销毁
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

进入getSingleton方法:
请添加图片描述
也是接收一个ObjectFactory对象然后实现其getObject方法。

进入createBean:

1
2
java复制代码protected abstract Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException;

这是个抽象方法需要子类去实现。

再回到上面的getSingleton方法:

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
java复制代码public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "Bean name must not be null");
synchronized (this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
if (this.singletonsCurrentlyInDestruction) {
throw new BeanCreationNotAllowedException(beanName,
"Singleton bean creation not allowed while singletons of this factory are in destruction " +
"(Do not request a bean from a BeanFactory in a destroy method implementation!)");
}
if (logger.isDebugEnabled()) {
logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
}
beforeSingletonCreation(beanName);
boolean newSingleton = false;
boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
if (recordSuppressedExceptions) {
this.suppressedExceptions = new LinkedHashSet<>();
}
try {
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
catch (IllegalStateException ex) {
// Has the singleton object implicitly appeared in the meantime ->
// if yes, proceed with it since the exception indicates that state.
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
throw ex;
}
}
catch (BeanCreationException ex) {
if (recordSuppressedExceptions) {
for (Exception suppressedException : this.suppressedExceptions) {
ex.addRelatedCause(suppressedException);
}
}
throw ex;
}
finally {
if (recordSuppressedExceptions) {
this.suppressedExceptions = null;
}
afterSingletonCreation(beanName);
}
if (newSingleton) {
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}

先将一级缓存singletonObjects上锁,获取到锁之后再次尝试从一级缓存里去获取实例,以防别的线程创建好了,如果获取不到实例就开始做真正的创建了:

先看一下目前容器是否正在销毁所有的单例:
请添加图片描述
singletonsCurrentlyInDestruction是个flag。

通过验证之后会来到beforeSingletonCreation(beanName),进入:

1
2
3
4
5
6
7
java复制代码protected void beforeSingletonCreation(String beanName) {
//inCreationCheckExclusions 直接缓存当前不能加载的bean,
//主要用在web容器的拦截器里,所以这里可以忽略,因为肯定是不存在的
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
}

if 的第一个条件可以忽略,第二个条件是尝试将beanName放入正在创建的单例名字列表里,添加失败则会抛异常,因为bean的正常创建流程按理是进入不到这里的,如果beanName出现在正在创建的名字列表中则表明在本次操作之前对于同一个bean的创建已经在进行了,doGetBean的时候第一步从三级缓存中获取Bean实例的时候就应该已经获取到了。

在并发场景下,两个线程在三级缓存里都没获取到单例,就会来到这里。

再回到上面的getSingleton方法,接下来就可以去创建并获取bean实例了:
请添加图片描述
newSingleton设置为true方便后续调用addSingleton来添加一级缓存。

异常处理跳过,看一下finally:
请添加图片描述
进入afterSingletonCreation:

1
2
3
4
5
java复制代码protected void afterSingletonCreation(String beanName) {
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) {
throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");
}
}

由于bean实例已经创建完毕了,所以会从正在创建的bean名字列表singletonsCurrentlyInCreation中移除beanName

回到外面,接下来就会去判断是否是新创建的单例,之前标志位已经是true了,进入addSingleton:

1
2
3
4
5
6
7
8
9
java复制代码protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
// Bean实例完成创建之后,只保留一级缓存以及注册beanName的顺序,其余的清除
this.singletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}

执行完addSingleton之后就会返回完整的bean实例了


回到doGetBean的创建单例的逻辑中,返回完整的bean实例之后就会执行

1
2
java复制代码// 如果是普通bean,直接返回,是FactoryBean,返回他的getObject
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);

再看下scope为Prototype的创建:
请添加图片描述
进入beforePrototypeCreation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码protected void beforePrototypeCreation(String beanName) {
Object curVal = this.prototypesCurrentlyInCreation.get();
if (curVal == null) {
this.prototypesCurrentlyInCreation.set(beanName);
}
else if (curVal instanceof String) {
Set<String> beanNameSet = new HashSet<>(2);
beanNameSet.add((String) curVal);
beanNameSet.add(beanName);
this.prototypesCurrentlyInCreation.set(beanNameSet);
}
else {
Set<String> beanNameSet = (Set<String>) curVal;
beanNameSet.add(beanName);
}
}

向类型为ThreadLocal的prototypesCurrentlyInCreation(当前线程正在创建的Prototype的bean名字列表)去添加该bean的记录,防止循环依赖,表示这个bean正在创建中。

回到doGetBean,之后也调用了prototypeInstance = createBean(beanName, mbd, args),创建完之后将先前注册的正在创建中的Bean信息给抹除掉

1
java复制代码afterPrototypeCreation(beanName);
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码protected void afterPrototypeCreation(String beanName) {
Object curVal = this.prototypesCurrentlyInCreation.get();
if (curVal instanceof String) {
this.prototypesCurrentlyInCreation.remove();
}
else if (curVal instanceof Set) {
Set<String> beanNameSet = (Set<String>) curVal;
beanNameSet.remove(beanName);
if (beanNameSet.isEmpty()) {
this.prototypesCurrentlyInCreation.remove();
}
}
}

回到doGetBean,之后也会调用

1
java复制代码bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);

获取bean实例或者beanFactory创建的bean实例


最后在看其他的情况,
请添加图片描述
根据生命周期范围选择实例化bean的合适方法来创建bean实例,比如request就是确保每个请求生成一个实例。


doGetBean最后会做一个类型检查,校验通过后返回实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码if (requiredType != null && !requiredType.isInstance(bean)) {
try {
T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
if (convertedBean == null) {
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
return convertedBean;
}
catch (TypeMismatchException ex) {
if (logger.isTraceEnabled()) {
logger.trace("Failed to convert bean '" + name + "' to required type '" +
ClassUtils.getQualifiedName(requiredType) + "'", ex);
}
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
}
return (T) bean;

本文转载自: 掘金

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

环境篇:idea整合git配置ssh(收藏篇) idea整合

发表于 2021-11-18

idea整合git配置ssh(收藏篇)

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

git下载安装

官网下载

安装过程如无特殊要求默认选项就行

配置全局用户名和邮箱

  1. 打开git bash here 命令行界面运行下面命令
  • git 提交代码不允许匿名提交,需要配置用户名和邮箱,虽然git功能用不到这两个参数
  • user.name,user.email就是告诉其他人,你的这个提交是谁提的(叫什么名字),以及联系方式(email)
  • user.name,user.email和GitHub账号,gitlab账号可以不一致,类似于昵称
  1. 配置全局名字
1
cmd复制代码git config --global user.name "xxx"
  1. 配置全局邮箱
1
cmd复制代码git config --global user.email "xxx@qq.com"

注意!!!如果以前设置过项目的邮箱和用户名,执行上面设置全局邮箱和用户名,会把以前的设置覆盖掉,需要再单独为项目设置用户名和邮箱

项目级别的用户名和邮箱

为项目配置邮箱和用户名,需要在具体的项目根目录下敲以下命令:

1
2
cmd复制代码git config user.name yourname
git config user.email yourname@163.com

生成密钥公钥

运行下面命令一直回车就行,生成文件存放在

1
cmd复制代码 ssh-keygen -t rsa

查看公钥,在c盘自己目录下C:\Users\用户名.ssh

1
js复制代码cat ~/.ssh/id_rsa.pub

GitHub添加密钥

把id_rsa.pub内容复制到GitHub配置上。
image.png

idea配置git

idea配置git程序
image.png

配置github账号

image.png

需要GitHub授权给idea

ssh下载测试

git命令下载

1
cmd复制代码 git clone git@github.com:xxx/java8.git

使用命令行下载ssh项目链接,第一次克隆回复yes就行,会在C:\Users\用户名.ssh生成2个文件

  • known_hosts
  • known_hosts.old
    image.png

gui下载

使用idea下载:git>>clone
image.png

本文转载自: 掘金

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

PHP迭代生成器秒级导出500万数据 一、PHP迭代生成器

发表于 2021-11-18

一、PHP迭代生成器

1.PHP性能优化利器:迭代生成器 yield理解

很多PHP开发者或许都不知道生成器这个功能,可能是因为生成器是PHP 5.5.0才引入的功能,也可以是生成器作用不是很明显。但是,生成器功能的确非常有用。

2.迭代生成器的优点

直接讲概念估计你听完还是一头雾水,所以我们先来说说优点,也许能勾起你的兴趣。那么迭代生成器有哪些优点,如下:

  • 生成器会对PHP应用的性能有非常大的影响
  • PHP代码运行时节省大量的内存
  • 比较适合计算大量的数据

二、快速生成百万条测试数据

这里推荐数据库的蠕虫复制命令

先建好表后,插2条测试数据后,执行几次下列命令,即可指数增长

INSERT INTO user (username,phone,city) SELECT username,phone,city FROM user;

三、实战一(100w数据导出1个csv文件,在浏览器下载)

1.解决数据查询内存溢出

了解完生成器yield原理之后,我们还需要解决一个问题,我们常用的mysql查询函数是mysqli_query(connection,query,resultmode);,通常都是直接填写第一第二个参数就直接查询,但该函数默认的是对全部结果集进行缓存,这会导致数据过多的时候,内存也会溢出。因此,我们需要设置第三个参数为MYSQLI_USE_RESULT来逐行读取结果集。那或许您有疑问为什么不直接 遍历该mysqli_query()方法返回的结果来当每一行数据写到CSV中,而还要用yield来存储一次每行数据再写到CSV中呢?其实这是因为mysqli_query()返回的结果是mysqli_result Object形式,而fputcsv()这个方法要求第二个参数为数组。所以yield的作用就是中转站,但是他是一行行运输数据,而不是读多行来运输数据。

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
php复制代码<?php
/*
* 该方法是把数据库读出的数据进行CSV文件输出,能接受百万级别的数据输出,因为用生成器,不用担心内存溢出。
* @param string $sql 需要导出的数据SQL
* @param string $mark 生成文件的名字前缀
*
*/
function exportCsv($sql,$fileName)
{
//让程序一直运行
set_time_limit(0);
//设置程序运行内存
ini_set('memory_limit', '128M');

header('Content-Encoding: UTF-8');
header("Content-type:application/vnd.ms-excel;charset=UTF-8");
header('Content-Disposition: attachment;filename="' . $fileName . '.csv"');

//打开php标准输出流
$fp = fopen('php://output', 'a');

//添加BOM头,以UTF8编码导出CSV文件,如果文件头未添加BOM头,打开会出现乱码。
fwrite($fp, chr(0xEF).chr(0xBB).chr(0xBF));

//添加导出标题
fputcsv($fp, ['id', 'username', 'phone','city']);

foreach (getExportData($sql) as $item) {
//向csv表格中添加每一行数据
fputcsv($fp, $item);
}
fclose($fp); //每生成一个文件关闭
}


/**
* 获取要导出的数据
* 使用迭代生成器来缓存mysql查询结果,返回类型为数组
* @param $sql
* @return Generator
*/
function getExportData($sql){
//连接数据库
$con = mysqli_connect("localhost", "root", "");
//连接数据库报错信息
if (!$con) {
die('Could not connect: ' . mysqli_error());
}
mysqli_select_db($con, "demo");
mysqli_query($con,'set names utf8');
//该处用MYSQLI_USE_RESULT 就是不缓存结果集中,也是为了避免内存溢出,相当于mysql_unbuffered_query
foreach (mysqli_query($con, $sql,MYSQLI_USE_RESULT) as $row ){ //
yield $row;
}
$con->close();
}

//要导出的数据
$sql = 'SELECT id,username,phone,city FROM `user` where id < 1000000';
$mark = 'export';
exportCsv($sql,$mark);

?>

四、实战二(100w数据导出多个csv文件,在文件夹中保存)

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
php复制代码<?php

/*
* 该方法是把数据库读出的数据进行CSV文件输出,能接受百万级别的数据输出,因为用生成器,不用担心内存溢出。
* @param string $sql 需要导出的数据SQL
* @param string $mark 生成文件的名字前缀
*
*/
function putCsv($sql, $mark)
{
set_time_limit(0);
$file_num = 0; //文件名计数器
$fileNameArr = array();
$fp = fopen($mark .'_'.$file_num .'.csv', 'w'); //生成临时文件
$fileNameArr[] = $mark .'_'.$file_num .'.csv';
fwrite($fp, chr(0xEF).chr(0xBB).chr(0xBF));//转码,防止乱码
//添加导出标题
fputcsv($fp, ['id', 'type_id', 'state','addtime','usetime']);

foreach (query($sql) as $a) {
fputcsv($fp, $a);
}
fclose($fp); //每生成一个文件关闭
}


//生成器来缓存mysql查询结果,返回类型为数组
function query($sql){
//连接数据库
$con = mysqli_connect("localhost", "root", "");
//连接数据库报错信息
if (!$con) {
die('Could not connect: ' . mysqli_error());
}
mysqli_select_db($con, "demo");
mysqli_query($con,'set names utf8');
//该处用MYSQLI_USE_RESULT 就是不缓存结果集中,也是为了避免内存溢出,相当于mysql_unbuffered_query
foreach (mysqli_query($con, $sql,MYSQLI_USE_RESULT) as $row ){ //
yield $row;
}
$con->close();
}

//数据库总数据
$count = 1000000;

//文件名称
$mark = 'test';
//一个excel保存的条数
$limit = 500000;
$page = 1;

$i = 0;
do
{
$i+= $limit;
//计算偏移量
$start = ($page-1)*$limit;
$sql = "SELECT id,type_id,state,addtime,usetime FROM `shop_ercode_records` limit {$start}".','."{$limit}";
putCsv($sql,$mark.$i);
$page++;
} while ($i<$count);

?>

五、总结

  1. 浏览器只能同时下载一个文件,循环导出,浏览器最后智能下载最后一个文件
  2. 导出的数据到csv能打开查看的只到100w行,如果超过100w行,进行导出多个csv文件

本文转载自: 掘金

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

Yii2框架(二)数据库相关操作 数据无价,谨慎操作。

发表于 2021-11-18

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

Yii2.0的数据库操作。

由于我这里安装的是yii2.0-advanced版本。这个破玩意是自带数据库的,上一篇的安装中数据库也一起随之安装完成。配置啥的,也都配置完成。

不论什么框架,对数据库都是有一套自己独立封装的模型操作类。

但是我一般都不用,学习成本太高,而且他其中封装的方法啥的一般都接触不全。学起来太费劲了。反正底层执行的都是sql。

所以这里Yii2.0自己封装的数据库相关的模型操作,我就略过了。

一般我习惯使用原生sql语句来对数据库进行增删改查。而且这种方法,我觉得对后期的维护也是一种很好的选择,毕竟,原生Sql语句,应该都能看的懂。少去了学习框架这部分的时间。

正式开始:

这里要说一下,Yii2.0中外部可以访问的方法都需要加上action,别问为什么,规定。

Yii2.0大概封装了两大众方法来执行数据库操作:

1:query()和queryXXX()方法,query()方法返回的是对象一般用的较少,当然,这个是对我来说,具体看你自己的需求。

(1):query方法

1
2
3
4
5
6
7
8
php复制代码$dataReader=$command->query();
//返回CDbDataReader对象,注意这代表结果集而不是记录,可以通过以下方法来获取(遍历)记录:
// CDbDataReader::read()可以一次获取一行数据,到末尾时返回false
while(($row=$dataReader->read())!==false)
// CDbDataReader实现了迭代器接口因此可以使用foreach遍历
foreach($dataReader as $row)
// 一次性返回所有的记录(数组)
$rows=$dataReader->readAll();

我们分别写几段代码来测试Yii2.0框架中为我们提供的执行原生sql的方法。

(2):queryXXX方法

(1)查询所有数据:(二维数组)queryAll

1
2
3
4
5
6
7
8
9
php复制代码// sql语句
$sql_queryAll = "SELECT * FROM art_category";
// 创建查询对象
$command=Yii::$app->db->createCommand($sql_queryAll);
// 执行查询所有数据
$result = $command->queryAll();

echo "<pre>";
var_dump($result);

(2):查询第一行数据:(一维数组)queryOne

1
2
3
4
5
6
7
8
9
10
11
php复制代码// sql语句
$sql_queryAll = "SELECT * FROM art_category ";
// 创建查询对象
$command=Yii::$app->db->createCommand($sql_queryAll);
// 执行查询所有数据
// $result = $command->queryAll();
// 执行查询一行数据
$result = $command->queryOne();

echo "<pre>";
var_dump($result);

其余的方法平时用的不多,用的话也是在sql语句里边实现的

2:execute方法

用于数据更新(非查询)操作(INSERT, UPDATE 和 DELETE),返回的是操作受影响的记录行数

(1):增加insert()->execute

1
2
3
4
5
6
7
8
9
10
11
php复制代码// 批量写入
$insertSql = "INSERT INTO art_category (cat_name,cat_pid)
values ('xxx','32'), ('aaaaaaa','963')";

$command=Yii::$app->db->createCommand($insertSql);

// 返回写入成功条数。
$result = $command->execute();

echo "<pre>";
var_dump($result);

(2):修改update()->execute

1
2
3
4
5
6
7
8
9
10
php复制代码// 修改语句
$sql = "UPDATE art_category SET cat_name = 'camellia' WHERE id = 10";

$command=Yii::$app->db->createCommand($sql);

// 返回修改成功条数。
$result = $command->execute();

echo "<pre>";
var_dump($result);

(3):删除delete()->execute

1
2
3
4
5
6
7
8
9
10
php复制代码// 删除语句
$sql = "DELETE FROM art_category WHERE id = '10'";

$command=Yii::$app->db->createCommand($sql);

// 返回删除成功条数。
$result = $command->execute();

echo "<pre>";
var_dump($result);

执行原生sql的方法大概就是这些,后边遇到了再看吧。

然后看一下Yii2.0框架对于数据库事务的操作:

1
2
3
4
php复制代码$db = Yii::app()->db;
$dbTrans = $db->beginTransaction(); //开始事物
$dbTrans->commit(); //提交
$dbTrans->rollback(); //回滚

很简单。

最后

数据无价,谨慎操作。

有好的建议,请在下方输入你的评论。

欢迎访问个人博客
guanchao.site

欢迎访问小程序:

在这里插入图片描述

本文转载自: 掘金

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

php加密函数与解密函数

发表于 2021-11-18

去网上找了下。

Php常用的加密函数有

MD5加密(不可逆)、Crypt加密(不可逆)、Sha1加密(不可逆)、Urlencode加密(可逆)
base64编码加密(可逆)

还是见识的太少,这其中我只用过MD5和base64

最常用的还是MD5 ,MD5的加密虽不可逆,但一些简单的 两次MD5加密的字符串,花点钱还是能解密的。

为了防止解密一般都是对字符串进行MD5加密,在加密过得字符串上加一个固定字符串,再进行MD5加密。(感觉好麻烦……)

分享一个自用的加密和解密函数

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
php复制代码/** 
* 加密
* @param string $string 要加密或解密的字符串
* @param string $operation 加密 '' 解密 DECODE
* @param string $key 密钥,加密解密时保持一致
* @param int $expiry 有效时长,单位:秒
* @return string
*/
function encrypt_code($string, $expiry = 0, $key = '1234567890') {
$ckey_length = 1;
$key = md5($key ? $key : UC_KEY); //加密解密时这个是不变的
$keya = md5(substr($key, 0, 16)); //加密解密时这个是不变的
$keyb = md5(substr($key, 16, 16)); //加密解密时这个是不变的
$keyc = $ckey_length ? substr(md5(microtime()), -$ckey_length) : '';
$cryptkey = $keya . md5($keya . $keyc); //64
$key_length = strlen($cryptkey); //64

$string =sprintf('%010d', $expiry ? $expiry + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string;
$string_length = strlen($string);

$result = '';
$box = range(0, 255);

$rndkey = array();
for ($i = 0; $i <= 255; $i++) { //字母表 64位后重复 数列 范围为48~122
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}

for ($j = $i = 0; $i < 256; $i++) { //这里是一个打乱算法
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for ($a = $j = $i = 0; $i < $string_length; $i++) {
$result .= chr(ord($string[$i]) ^ ($box[$i]));

}
$str = $keyc . str_replace('=', '', base64_encode($result));
// $str =htmlentities($str, ENT_QUOTES, "UTF-8"); // curl 访问出错
return $str ;
}


/**
* 解密
* @param string $string 要加密或解密的字符串
* @param string $operation 加密 '' 解密 DECODE
* @param string $key 密钥,加密解密时保持一致
* @param int $expiry 有效时长,单位:秒
* @return string
*/
function encrypt_decode($string, $expiry = 0,$key = '1234567890') {
$ckey_length = 1;
$key = md5($key ? $key : UC_KEY); //加密解密时这个是不变的
$keya = md5(substr($key, 0, 16)); //加密解密时这个是不变的
$keyb = md5(substr($key, 16, 16)); //加密解密时这个是不变的

$keyc = $ckey_length ? substr($string, 0, $ckey_length) : '';

$cryptkey = $keya . md5($keya . $keyc); //64
$key_length = strlen($cryptkey); //64
$string = base64_decode(substr($string, $ckey_length)) ;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);

$rndkey = array();
for ($i = 0; $i <= 255; $i++) { //字母表 64位后重复 数列 范围为48~122
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for ($j = $i = 0; $i < 256; $i++) { //这里是一个打乱算法
$j = ($j + $box[$i] + $rndkey[$i]) % 256;

$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for($a = $j = $i = 0; $i < $string_length; $i++) {
$result .= chr(ord($string[$i]) ^ ($box[$i]));
}
if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)) {
return substr($result, 26);
}else{
return false;
}
}

注:加密函数与解密函数中的 $key 必须完全相同。

有好的建议,请在下方输入你的评论。

欢迎访问个人博客
guanchao.site

欢迎访问小程序:

在这里插入图片描述

本文转载自: 掘金

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

为啥RocketMQ会发送重复的消息 前言 消息重复的场景

发表于 2021-11-18

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

前言

最近发现数据库里的数据经常会重复,就想定位一下这个问题。数据的insert操作只会在接收到RocketMQ消息时,处理消息的时候落库。这时就在想是不是RocketMQ消息重复发送了?拉取日志分析一下。

image.png

不难看出,同一个messageId竟然收到了五次,并且随着时间的推移还在继续发送。不禁让人思考,RocketMQ会在什么情况下,重复发送消息呢?

消息重复的场景

消息重复的场景如下:

1)发送时消息重复
当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且Message ID也相同的消息。

2)投递时消息重复
消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,消息队列RocketMQ版的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且Message ID也相同的消息。

3)负载均衡时消息重复(包括但不限于网络抖动、Broker重启以及消费者应用重启)
当消息队列RocketMQ版的Broker或客户端重启、扩容或缩容时,会触发Rebalance,此时消费者可能会收到重复消息。

那我这属于哪一类情况呢?首先在收到重复消息的时候,我们的服务是正常的,并且RocketMQ也是一直稳定的。所以排出了1、3点。在看2点,当客户端给服务端反馈应答的时候网络闪断,意思是客户端处理完业务没有反馈应答,而网络闪断只是其中原因之一。

换个思路,如果客户端一直在处理业务,没有及时反馈应答,那RocketMQ为做啥?

RocketMQ在设置的超时时间后没有收到反馈应答,会认为是一次失败的消息消费。为了保证消息至少被消费一次,RocketMQ自然会再次发送Message ID相同的消息出来的。而我这的问题,也正是业务处理时间过长,导致的。平均处理时间在两分钟左右。而RocketMQ默认消息消费超时时间是15分钟,是由ConsumeTimeout参数决定的。从这段代码里可以看出来。

1
2
3
4
arduino复制代码/**
* 设置每条消息消费的最大超时时间,超过这个时间,这条消息将会被视为消费失败,等下次重新投递再次消费. 每个业务需要设置一个合理的值. 单位(分钟)
*/
public static final String ConsumeTimeout = "consumeTimeout";

而我们封装的RocketMQ将消息消费超时时间默认成了1分钟,导致了这个了问题

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
ini复制代码@Data
public static class Customize {
/**
* 消息监听类
*
* @see AbstractMessageListener
*/
private String listenerClassName = "tech.baoyun.starter.ons.comsume.DefaultMessageListener";
/**
* @see com.aliyun.openservices.ons.api.PropertyKeyConst#ConsumeTimeout
*/
private Integer consumeTimeout = 1;
/**
* @see com.aliyun.openservices.ons.api.PropertyKeyConst#MaxReconsumeTimes
*/
private Integer maxReconsumeTimes = 5;
/**
* @see com.aliyun.openservices.ons.api.PropertyKeyConst#ConsumeThreadNums
*/
private Integer consumeThreadNums = 10;
/**
* @see com.aliyun.openservices.ons.api.PropertyKeyConst#SuspendTimeMillis
*/
private Integer suspendTimeMillis = 60 * 1000;
}

本文转载自: 掘金

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

SpringCloud升级之路20200x版-36 验

发表于 2021-11-18

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

本系列代码地址:github.com/JoJoTec/spr…

上一节我们通过单元测试验证了线程隔离的正确性,这一节我们来验证我们断路器的正确性,主要包括:

  1. 验证配置正确加载:即我们在 Spring 配置(例如 application.yml)中的加入的 Resilience4j 的配置被正确加载应用了。
  2. 验证断路器是基于服务和方法打开的,也就是某个微服务的某个方法断路器打开但是不会影响这个微服务的其他方法调用

验证配置正确加载

与之前验证重试类似,我们可以定义不同的 FeignClient,之后检查 resilience4j 加载的断路器配置来验证线程隔离配置的正确加载。

并且,与重试配置不同的是,通过系列前面的源码分析,我们知道 spring-cloud-openfeign 的 FeignClient 其实是懒加载的。所以我们实现的断路器也是懒加载的,需要先调用,之后才会初始化断路器。所以这里我们需要先进行调用之后,再验证断路器配置。

首先定义两个 FeignClient,微服务分别是 testService1 和 testService2,contextId 分别是 testService1Client 和 testService2Client

1
2
3
4
5
6
7
8
9
10
less复制代码@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
@GetMapping("/anything")
HttpBinAnythingResponse anything();
}
@FeignClient(name = "testService2", contextId = "testService2Client")
public interface TestService2Client {
@GetMapping("/anything")
HttpBinAnythingResponse anything();
}

然后,我们增加 Spring 配置,并且给两个微服务都添加一个实例,使用 SpringExtension 编写单元测试类:

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
less复制代码//SpringExtension也包含了 Mockito 相关的 Extension,所以 @Mock 等注解也生效了
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {
//默认请求重试次数为 3
"resilience4j.retry.configs.default.maxAttempts=3",
// testService2Client 里面的所有方法请求重试次数为 2
"resilience4j.retry.configs.testService2Client.maxAttempts=2",
//默认断路器配置
"resilience4j.circuitbreaker.configs.default.slidingWindowSize=5",
"resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=2",
//testService2Client 的 断路器配置
"resilience4j.circuitbreaker.configs.testService2Client.failureRateThreshold=30",
"resilience4j.circuitbreaker.configs.testService2Client.minimumNumberOfCalls=10",

})
@Log4j2
public class OpenFeignClientTest {
@SpringBootApplication
@Configuration
public static class App {
@Bean
public DiscoveryClient discoveryClient() {
//模拟两个服务实例
ServiceInstance service1Instance1 = Mockito.spy(ServiceInstance.class);
ServiceInstance service2Instance2 = Mockito.spy(ServiceInstance.class);
Map<String, String> zone1 = Map.ofEntries(
Map.entry("zone", "zone1")
);
when(service1Instance1.getMetadata()).thenReturn(zone1);
when(service1Instance1.getInstanceId()).thenReturn("service1Instance1");
when(service1Instance1.getHost()).thenReturn("www.httpbin.org");
when(service1Instance1.getPort()).thenReturn(80);
when(service2Instance2.getInstanceId()).thenReturn("service1Instance2");
when(service2Instance2.getHost()).thenReturn("httpbin.org");
when(service2Instance2.getPort()).thenReturn(80);
DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
Mockito.when(spy.getInstances("testService1"))
.thenReturn(List.of(service1Instance1));
Mockito.when(spy.getInstances("testService2"))
.thenReturn(List.of(service2Instance2));
return spy;
}
}
}

编写测试代码,验证配置正确:

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
scss复制代码@Test
public void testConfigureCircuitBreaker() {
//防止断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
//调用下这两个 FeignClient 确保对应的 NamedContext 被初始化
testService1Client.anything();
testService2Client.anything();
//验证断路器的实际配置,符合我们的填入的配置
List<CircuitBreaker> circuitBreakers = circuitBreakerRegistry.getAllCircuitBreakers().asJava();
Set<String> collect = circuitBreakers.stream().map(CircuitBreaker::getName)
.filter(name -> {
try {
return name.contains(TestService1Client.class.getMethod("anything").toGenericString())
|| name.contains(TestService2Client.class.getMethod("anything").toGenericString());
} catch (NoSuchMethodException e) {
return false;
}
}).collect(Collectors.toSet());
Assertions.assertEquals(collect.size(), 2);
circuitBreakers.forEach(circuitBreaker -> {
if (circuitBreaker.getName().contains(TestService1Client.class.getName())) {
Assertions.assertEquals((int) circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold(), (int) DEFAULT_FAILURE_RATE_THRESHOLD);
Assertions.assertEquals(circuitBreaker.getCircuitBreakerConfig().getMinimumNumberOfCalls(), DEFAULT_MINIMUM_NUMBER_OF_CALLS);
} else if (circuitBreaker.getName().contains(TestService2Client.class.getName())) {
Assertions.assertEquals((int) circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold(), (int) TEST_SERVICE_2_FAILURE_RATE_THRESHOLD);
Assertions.assertEquals(circuitBreaker.getCircuitBreakerConfig().getMinimumNumberOfCalls(), TEST_SERVICE_2_MINIMUM_NUMBER_OF_CALLS);
}
});
}

验证断路器是基于服务和方法打开的。

我们给 TestService1Client 添加一个方法:

1
2
less复制代码@GetMapping("/status/500")
String testCircuitBreakerStatus500();

这个方法一定会调用失败,从而导致断路器打开。经过 2 次失败以上后(因为配置最少触发断路器打开的请求个数为 2),验证断路器状态:

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
scss复制代码@Test
public void testCircuitBreakerOpenBasedOnServiceAndMethod() {
//防止断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
AtomicBoolean passed = new AtomicBoolean(false);
for (int i = 0; i < 10; i++) {
//多次调用会导致断路器打开
try {
System.out.println(testService1Client.testCircuitBreakerStatus500());
} catch(Exception e) {}
List<CircuitBreaker> circuitBreakers = circuitBreakerRegistry.getAllCircuitBreakers().asJava();
circuitBreakers.stream().filter(circuitBreaker -> {
return circuitBreaker.getName().contains("testCircuitBreakerStatus500")
&& circuitBreaker.getName().contains("TestService1Client");
}).findFirst().ifPresent(circuitBreaker -> {
//验证对应微服务和方法的断路器被打开
if (circuitBreaker.getState().equals(CircuitBreaker.State.OPEN)) {
passed.set(true);
//断路器打开后,调用其他方法,不会抛出断路器打开异常
testService1Client.testAnything();
}
});
}

Assertions.assertTrue(passed.get());
}

这样,我们就成功验证了,验证断路器是基于服务和方法打开的。

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer:

本文转载自: 掘金

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

nginx 静态资源部署 【2】 nginx 静态资源部署

发表于 2021-11-18

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

nginx 静态资源部署 【2】

往期文章:

初识 Nginx

nginx 的安装

nginx 核心配置文件结构

nginx 静态资源部署 【1】

Nginx静态资源压缩

我们传送一个1M的数据和一个10M的数据哪个效率高?,答案显而易见,传输内容小,速度就会快。那么问题又来了,同样的内容,如果把大小降下来,我们脑袋里面要蹦出一个词就是”压缩”,接下来,我们来学习Nginx的静态资源压缩模块。

在Nginx的配置文件中可以通过配置gzip来对静态资源进行压缩,相关的指令可以配置在http块、server块和location块中,Nginx可以通过

1
2
3
复制代码ngx_http_gzip_module模块
ngx_http_gzip_static_module模块
ngx_http_gunzip_module模块

Gzip模块配置指令

接下来所学习的指令都来自ngx_http_gzip_module模块,该模块会在nginx安装的时候内置到nginx的安装环境中,也就是说我们可以直接使用这些指令。

1. gzip指令:该指令用于开启或者关闭gzip功能

| 语法 | gzip on|off; |
| — | — |
| 默认值 | gzip off; |
| 位置 | http、server、location… |

注意只有该指令为打开状态,下面的指令才有效果

1
2
3
csharp复制代码http{
  gzip on;
}

2. gzip_types指令:该指令可以根据响应页的MIME类型选择性地开启Gzip压缩功能

语法 gzip_types mime-type …;
默认值 gzip_types text/html;
位置 http、server、location

所选择的值可以从mime.types文件中进行查找,也可以使用”*“代表所有。

1
2
3
bash复制代码http{
gzip_types application/javascript;
}

3. gzip_comp_level指令:该指令用于设置Gzip压缩程度,级别从1-9,1表示要是程度最低,要是效率最高,9刚好相反,压缩程度最高,但是效率最低最费时间。

语法 gzip_comp_level level;
默认值 gzip_comp_level 1;
位置 http、server、location
1
2
3
ini复制代码http{
gzip_comp_level 6;
}

4. gzip_vary指令:该指令用于设置使用Gzip进行压缩发送是否携带“Vary:Accept-Encoding”头域的响应头部。主要是告诉接收方,所发送的数据经过了Gzip压缩处理

| 语法 | gzip_vary on|off; |
| — | — |
| 默认值 | gzip_vary off; |
| 位置 | http、server、location |

111.jpg

5. gzip_buffers指令:该指令用于处理请求压缩的缓冲区数量和大小。

语法 gzip_buffers number size;
默认值 gzip_buffers 32 4k
位置 http、server、location

其中number:指定Nginx服务器向系统申请缓存空间个数,size指的是每个缓存空间的大小。主要实现的是申请number个每个大小为size的内存空间。这个值的设定一般会和服务器的操作系统有关,所以建议此项不设置,使用默认值即可。

1
bash复制代码gzip_buffers 4 16K;   #缓存空间大小

6. gzip_disable指令:针对不同种类客户端发起的请求,可以选择性地开启和关闭Gzip功能。

语法 gzip_disable regex …;
默认值 —
位置 http、server、location

regex:根据客户端的浏览器标志(user-agent)来设置,支持使用正则表达式。指定的浏览器标志不使用Gzip.该指令一般是用来排除一些明显不支持Gzip的浏览器。

1
ini复制代码gzip_disable "MSIE [1-6].";

7. gzip_http_version指令:针对不同的HTTP协议版本,可以选择性地开启和关闭Gzip功能。

| 语法 | gzip_http_version 1.0|1.1; |
| — | — |
| 默认值 | gzip_http_version 1.1; |
| 位置 | http、server、location |

该指令是指定使用Gzip的HTTP最低版本,该指令一般采用默认值即可。

8. gzip_min_length指令:该指令针对传输数据的大小,可以选择性地开启和关闭Gzip功能

语法 gzip_min_length length;
默认值 gzip_min_length 20;
位置 http、server、location
1
2
css复制代码nignx计量大小的单位:bytes[字节] / kb[千字节] / M[兆]
例如: 1024 / 10k|K / 10m|M

Gzip压缩功能对大数据的压缩效果明显,但是如果要压缩的数据比较小的化,可能出现越压缩数据量越大的情况,因此我们需要根据响应内容的大小来决定是否使用Gzip功能,响应页面的大小可以通过头信息中的Content-Length来获取。但是如何使用了Chunk编码动态压缩,该指令将被忽略。建议设置为1K或以上。

9. gzip_proxied指令:该指令设置是否对服务端返回的结果进行Gzip压缩。

| 语法 | gzip_proxied off|expired|no-cache| no-store|private|no_last_modified|no_etag|auth|any; |
| — | — |
| 默认值 | gzip_proxied off; |
| 位置 | http、server、location |

off - 关闭Nginx服务器对后台服务器返回结果的Gzip压缩 expired - 启用压缩,如果header头中包含 “Expires” 头信息 no-cache - 启用压缩,如果header头中包含 “Cache-Control:no-cache” 头信息 no-store - 启用压缩,如果header头中包含 “Cache-Control:no-store” 头信息 private - 启用压缩,如果header头中包含 “Cache-Control:private” 头信息 no_last_modified - 启用压缩,如果header头中不包含 “Last-Modified” 头信息 no_etag - 启用压缩 ,如果header头中不包含 “ETag” 头信息 auth - 启用压缩 , 如果header头中包含 “Authorization” 头信息 any - 无条件启用压缩

Gzip压缩功能的实例配置

1
2
3
4
5
6
7
8
9
csharp复制代码gzip on;              #开启gzip功能
gzip_types *; #压缩源文件类型,根据具体的访问资源类型设定
gzip_comp_level 6; #gzip压缩级别
gzip_min_length 1024; #进行压缩响应页面的最小长度,content-length
gzip_buffers 4 16K; #缓存空间大小
gzip_http_version 1.1; #指定压缩响应所需要的最低HTTP请求版本
gzip_vary on; #往头信息中添加压缩标识
gzip_disable "MSIE [1-6]."; #对IE6以下的版本都不进行压缩
gzip_proxied off; #nginx作为反向代理压缩服务端返回数据的条件

这些配置在很多地方可能都会用到,所以我们可以将这些内容抽取到一个配置文件中,然后通过include指令把配置文件再次加载到nginx.conf配置文件中,方法使用。

nginx_gzip.conf

1
2
3
4
5
6
7
8
9
ini复制代码gzip on;
gzip_types *;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_buffers 4 16K;
gzip_http_version 1.1;
gzip_vary on;
gzip_disable "MSIE [1-6].";
gzip_proxied off;

nginx.conf

1
makefile复制代码include nginx_gzip.conf

Gzip和sendfile共存问题

sendfile,用来开启高效的文件传输模式,开启sendfile以后,在读取磁盘上的静态资源文件的时候,可以减少拷贝的次数,可以不经过用户进程将静态文件通过网络设备发送出去,但是Gzip要想对资源压缩,是需要经过用户进程进行操作的。所以如何解决两个设置的共存问题。

可以使用ngx_http_gzip_static_module模块的gzip_static指令来解决。

gzip_static指令

gzip_static: 检查与访问资源同名的.gz文件时,response中以gzip相关的header返回.gz文件的内容。

| 语法 | gzip_static on | off | always; |
| — | — |
| 默认值 | gzip_static off; |
| 位置 | http、server、location |

添加上述命令后,会报一个错误,unknown directive "gzip_static"主要的原因是Nginx默认是没有添加ngx_http_gzip_static_module模块。如何来添加?

添加模块到Nginx的实现步骤

(1)查询当前Nginx的配置参数

1
复制代码nginx -V

(2)将nginx安装目录下sbin目录中的nginx二进制文件进行更名

1
2
bash复制代码cd /usr/local/nginx/sbin
mv nginx nginxold

(3) 进入Nginx的安装目录

1
bash复制代码cd /root/nginx/core/nginx-1.16.1

(4)执行make clean清空之前编译的内容

1
go复制代码make clean

(5)使用configure来配置参数

1
bash复制代码./configure --with-http_gzip_static_module

(6)使用make命令进行编译

1
go复制代码make

(7) 将objs目录下的nginx二进制执行文件移动到nginx安装目录下的sbin目录中

1
bash复制代码mv objs/nginx /usr/local/nginx/sbin

(8)执行更新命令

1
go复制代码make upgrade

gzip_static测试使用

(1)直接访问http://你的IP/jquery.js

111.jpg

(2)使用gzip命令进行压缩

1
2
bash复制代码cd /usr/local/nginx/html
gzip jquery.js

(3)再次访问http://你的IP/jquery.js

112.jpg

本文转载自: 掘金

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

Hibernate + jbosstools 整合、配置、测

发表于 2021-11-18

PS:文中用的都是Eclipse配置中对应的英文提示,防止配置时找不到入口。

文章相对比较早,有一些工具没有,或者版本不对,请给我留言,看到后会逐一回复。

一、前置条件

1.Eclipse 4.4.2

下载地址:Eclipse官网 download.eclipse.org/eclipse/dow…
Eclipse下载

查看Eclipse版本

2.hibernate-release-4.3.11.Final

下载地址:Hibernate官网 hibernate.org/orm/
下载hibernate-release-4.3.11.Final的界面

3.jbosstools-4.2.3.Final_2015-03-26_23-05-30-B264-updatesite-hibernatetools

下载地址:jbosstools官网 tools.jboss.org/downloads/j…
下载jbosstools-4.2.3.Final_2015-03-26_23-05-30-B264-updatesite-hibernatetools

4.mysql 5.6

下载地址:mysql官网 www.mysql.com/
下载mysql 5.6

5.common-logging.jar Hibernate内部记录日志jar包

下载地址:common-logging.jar download.csdn.net/detail/mimi…

6.mysql-connector-java-5.1.7

下载地址:mysql-connector-java-5.1.7.zip

7.Navicat for mysql 10.1.7 企业版

下载地址:暂不提供,网上搜索即可,最好是破解版的,方便操作,其他的mysql数据库操作客户端亦可,根据个人喜好。

二、安装

1.Eclipse 4.4.2直接解压即可。

2.解压安装包文件。

可先将hibernate-release-4.3.11.Final解压至Eclipse 4.4.2的plugins目录,为后面导入库做准备。

3.打开Eclipse的软件安装界面,如下所示(按照提示进行安装):

Eclipse安装新软件
3.5 Eclipse安装离线jbosstools-4.2.3.Final.zip

三、使用Hibernate操作mysql数据库表结构

1.创建测试数据库test(略)。

2.创建测试表结构user/news,暂不需要插入任何数据(略)。

3.新建Java project工程,如下所示:

新建Java project工程1
新建Java project工程2

4.新建User Libraries

新建User Libraries1
新建User Libraries2

5.给Java project添加User Libraries,如下所示:

给工程添加User Library
添加User Library
选择在系统的Prefenrence中配置好的User Library
添加完相应的User Libraries
添加完User Libraries后的状态

6.创建Hibernate配置文件,使用默认hibernate.cfg.xml

创建hibernate.cfg.xml配置文件
最终我将hibernate.cfg.xml文件配置为如下格式。参数的作用各不相同,依据自己的喜好进行配置。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- <property name="hibernate.connection.datasource">java:comp/env/jdbc/dstest</property> -->
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=UTF-8</property>
<property name="hibernate.connection.username">[你登录mysql数据库的用户名]</property>
<property name="hibernate.connection.password">[你登录mysql数据库的密码]</property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
<property name="hibernate.show_sql">true</property>
<property name="hibernate.hbm2ddl.auto">update</property>
<property name="hibernate.c3p0.max_size">200</property>
<property name="hibernate.c3p0.min_size">2</property>
<property name="hibernate.c3p0.timeout">1800</property>
<property name="hibernate.c3p0.max_statements">50</property>

<mapping resource="db/dao/News.hbm.xml"></mapping>
<mapping resource="db/dao/User.hbm.xml"></mapping>
</session-factory>
</hibernate-configuration>

7.配置Hibernate configuration

启动hibernate configuration视图
配置hibernate.cfg.xml文件
配置hibernate.cfg.xml文件完成

8.新建Hibernate 逆向POJO实体类的反向配置文件

新建Hibernate 逆向POJO实体类的反向配置文件

9.新建Hibernate Configuration properties添加当前工程的Console Configuration文件

1) 找到显示Hibernate Configuration视图的选项

找到显示Hibernate Configuration视图的选项

2) 输入关键字选择Hibernate Configuration视图

![输入关键字选择Hibernate Configuration视图]
(img.blog.csdn.net/20150824173…)

3) 显示Hibernate Configuration视图

![显示Hibernate Configuration视图]这里写图片描述

4) Hibernate Configuration properties添加当前工程的Console Configuration文件

Hibernate Configuration properties添加当前工程的Console Configuration文件

5) 新建成功的反向工程配置文件

新建成功的反向工程配置文件

10.新增需要反向的工程

1) 选择刚才的反向配置文件的configuration properties文件

新建成功的hibernate.reveng.xml文件选择hibernate工程

2) 添加需要反向的表结构

添加需要反向的表结构
这里写图片描述

3) 填写反向表结构的信息

填写反向表结构对应的类名
生成反向类名配置文件源码

11.生成反向类文件(如果类文件提示错误,请查看包名是否有错)

注:如果工具栏里没有显示这个按钮,可以 window -> Customize Perspective,切换到Command Groups Availablity标签页,把Hibernate Code Generation给勾选上

1) 设置Hibernate code Generation Configurations配置信息->Main

设置Hibernate code Generation Configurations配置信息

2) 设置Hibernate code Generation Configurations配置信息->Exporters

设置Hibernate code Generation Configurations配置信息

3) 设置Hibernate code Generation Configurations配置信息->Refresh

设置Hibernate code Generation Configurations配置信息

4) 生成的类文件

生成的类文件

12.生成映射文件

1) 生成映射文件

生成映射文件

2) 点击下一步

Create Hibernate XML Mapping file(s)

3) 下一步

Create Hibernate XML Mapping file(s)

4) 已经生成映射配置文件User.hbm.xml

已经生成映射配置文件User.hbm.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
typescript复制代码package db.dao;

// default package
// Generated 2015-8-24 18:39:26 by Hibernate Tools 4.3.1

/**
* User generated by hbm2java
*/
public class User implements java.io.Serializable {

private Integer id;
private String name;

public User() {
}

public User(String name) {
this.name = name;
}

public Integer getId() {
return this.id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

}

添加如下代码,完成POJO的对应关系

1
2
3
4
5
6
7
8
less复制代码@Entity
@Table(name="user")
public class User implements java.io.Serializable
{
@Id @Column(name="id")
@GeneratedValue(strategy=GenerationType.IDENTITY)
......(以下代码略去)
}

5) 生成的User.hbm.xml配置文件源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<!-- Generated 2015-8-24 18:49:15 by Hibernate Tools 3.4.0.CR1 -->
<hibernate-mapping>
<class name="db.dao.User" table="USER">
<id name="id" type="java.lang.Integer">
<column name="ID" />
<generator class="assigned" />
</id>
<property name="name" type="java.lang.String">
<column name="NAME" />
</property>
</class>
</hibernate-mapping>

13.添加类映射配置文件User.hbm.xml

添加类映射配置文件User.hbm.xml
添加映射类配置文件OK

五、测试POJO类

1.新建User.java(POJO)的测试文件

新建User.java(POJO)的测试文件

2.编写User.java(POJO)的测试文件

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
java复制代码package db.dao;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;

public class UserMgr {

public static void main(String[] args)
{
// Configuation实例代表了应用程序到SQL数据库的配置信息
// 加载默认的hibernate.cfg.xml文件,若设置config()中的参数,则表示加载其他的配置文件
Configuration conf = new Configuration().configure();

@SuppressWarnings("deprecation")
ServiceRegistry serviceReg = new ServiceRegistryBuilder()
.applySettings(conf.getProperties()).buildServiceRegistry();

// 以Configuration实例创建不可变SessionFactory实例
SessionFactory sessionFac = conf.buildSessionFactory(serviceReg);

// 创建Session
Session session = sessionFac.openSession();

// 开始事务
Transaction trans = session.beginTransaction();

// 创建消息对象
User user = new User();

user.setName("测试用户名1");

// 保存信息
session.save(user);

// 提交事务
trans.commit();

// 关闭session
session.close();
sessionFac.close();
}
}

3.依据Eclipse开启调试即可。

调试UserMgr.java

4.查看数据库是否已经插入对应的数据。

连接数据库成功等一系列信息
数据写入的SQL自动生成

六、常见问题

1.Error parsing JNDI name []

请配置JNDI配置信息(如果没有配置则删除hibernate.cfg.xml中的name=””属性)

2.映射文件/资源找不到

在hibernate.cfg.xml中添加如下对应的属性,这里是指配置文件的相对路径。

3.ids for this class must be manually assigned before calling save(): db.dao.User

如果id字段在UserMgr.hbm.xml文件中被设置为assigned同时在数据库又是自增字段时,可以尝试手动通过setter设置字段值。
这里写图片描述

4.数据写入成功后为??????的解决方案

数据写入成功后为??????
在hibernate.cfg.xml文件中添加如下配置:
?useUnicode=true&characterEncoding=UTF-8
这里写图片描述

5.hibernate.cfg.xml中c3p0的常规配置

1
2
3
4
5
6
7
ini复制代码<property name="hibernate.c3p0.timeout">1800</property>
<property name="hibernate.c3p0.max_size">30</property>
<property name="hibernate.c3p0.min_size">1</property>
<property name="hibernate.c3p0.max_statements">100</property>
<property name="hibernate.c3p0.idle_test_period">3000</property>
<property name="hibernate.c3p0.acquire_increment">2</property>
<property name="hibernate.c3p0.validate">true</property>

本文转载自: 掘金

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

1…297298299…956

开发者博客

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