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

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


  • 首页

  • 归档

  • 搜索

无需开发部署,秒建优雅的开源项目文档,这个工具用起来贼爽!

发表于 2021-09-28

之前介绍过几款搭建开源项目文档的工具,有Docsify、VuePress和Hexo。这些工具有个共同点,需要一些开发部署的工作,部署基本依赖Github Pages或者自建服务器。那么有没有那种开箱即用的工具呢,最近发现很多开源项目都用语雀来搭建文档网站,体验了一把,非常不错,推荐给大家!

SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…

语雀简介

语雀是新一代的云端知识库,可以写出像书一样的项目文档,拥有专业好用的编辑器(支持Markdown转换),方便的团队协同功能。

主要使用场景如下:

  • 个人笔记:语雀知识库具有公开私有功能,私有的知识库完全可以当在线云笔记使用,可以方便我们记录点滴,沉淀知识。
  • 专栏博客:如果你不想自己搭建博客网站的话,可以直接生成。
  • 项目文档:可以多人协作的项目文档,你见过没?
  • 官方文档:可以作为产品说明和帮助手册。

效果展示

我们先来看下成品效果,开箱即用,界面还是挺不错的!

核心概念

在使用语雀之前,我们需要对其中的概念有所了解。下面介绍下这些核心概念,然后再展示下对应的界面。

  • 首先我们来看下各个核心概念的关系图,这对理解这些概念很有帮助:

  • 空间:空间类似于公司或组织的概念,在空间中我们可以管理团队、知识库、知识等内容。

  • 团队:空间下的团队可以和你实际组织结构进行挂钩,如部门团队,项目组等。

  • 知识库:知识库是知识内容的载体,就好比一本书一样,你可以在知识库里对知识内容进行重新组织和分类。

  • 知识:知识库中的内容,可以是文档。

搭建

接下来我们使用语雀来搭建一个开源项目文档,以我的开源电商项目mall为例。

快速搭建

  • 首先我们点击左上角创建空间,用于存放团队和知识库;

  • 然后选择创建知识库,由于我们的知识库主要用于存放文档,所以选择文档知识库,然后输入名称、简介即可;

  • 创建完知识库之后,我们就可以在知识库中添加文档了,直接复制之前使用Markdown写的文档即可,然后点击右上角的立即转换;

  • 转换成功后显示效果如下,是不是比自己搭建项目文档网站简单多了?

  • 这里值得一提的是,在我们复制文章时,语雀会自动帮我们上传在线图片,很多博客网站也都有这个功能;

  • 如果你想修改知识库的文档目录的话,比如我想整个二级目录,可以使用编排目录功能;

自定义首页

其实到这里,我们已经学会了如何制作一个开源项目文档网站了,但是此时文档网站的首页只是一个目录,这样未免有些单调,如果我们想要首页内容更丰富些,我就需要创建团队,然后自定义团队的首页即可;

  • 首先我们来创建个团队,选择官方文档模板;

  • 然后填写名称、简介、头像即可;

  • 然后我们可以通过自定义首页功能,使用图形化界面来完成一个首页,下面是我已经完成好的首页效果;

  • 这里我添加了搜索、知识库、自定义内容三个模块,如果你想首页更丰富些,可以添加更多模块试试;

  • 这里最好在你的知识库的设置页,把你的知识库转移到创建的团队中去;

  • 接下来访问你的团队主页,一个标准的开源项目文档网站就诞生了。

公开访问

如果你想搭建一个私有的文档网站的话,上面的操作基本能满足需求了,但如果你想搭建一个公开的文档网站的话,还需要如下操作。

  • 很遗憾的是,在新建空间中的无论团队还是知识库,都是没法设置让互联网上的所有人访问的,仅支持如下三种访问权限;

  • 要想团队可以公开访问,我们需要在默认的个人空间中,创建知识小组,可以选择官方文档模板;

  • 此时我们可以发现,团队的可见范围变成了互联网可见,这才是真正公开的团队;

  • 之后要做的就是把之前的知识库通过导入、导出功能转移到该团队中去,导出功能在知识库设置->高级设置中,选择导出为.lakebook格式;

  • 然后在该知识小组中创建知识库,设置范围为互联网可见,并导入.lakebook文件;

  • 之后再按照之前的步骤自定义知识小组的首页,一个可以公开访问的开源项目文档网站就诞生了!

总结

通过使用语雀搭建开源项目文档的一波实践,我们可以发现这种方式确实比使用Docsify来自建网站简单太多了,既不需要编码,也不需要买服务器。怪不得很多开源项目都用它来搭建文档网站,当然语雀的功能还不只这些,用来做在线云笔记也挺好用的,如果大家感兴趣的话,可以自己探索下!

官方网站

www.yuque.com/

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

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

Spring Boot 禁用 Swagger 的三种方式

发表于 2021-09-28

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

📖摘要

1
复制代码心态好了,就没那么累了。心情好了,所见皆是明媚风景。

“一时解决不了的问题,那就利用这个契机,看清自己的局限性,对自己进行一场拨乱反正。”正如老话所说,一念放下,万般自在。如果你正被烦心事扰乱心神,不妨学会断舍离。断掉胡思乱想,社区垃圾情绪,离开负面能量。心态好了,就没那么累了。心情好了,所见皆是明媚风景。

在生产环境下,我们需要关闭swagger配置,避免暴露接口的这种危险行为。


🌂方法


🌂禁用方法1:

使用注解 @Value() 推荐使用

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

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
* @author sunny chen
* @version V1.0
* @Package com.dc.config
* @date 2018/1/16 17:33
* @Description: 主要用途:开启在线接口文档和添加相关配置
*/
@Configuration
@EnableSwagger2
public class Swagger2Config extends WebMvcConfigurerAdapter {

@Value("${swagger.enable}")
private Boolean enable;

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.enable(enable)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.dc.controller"))
.paths(PathSelectors.any())
//.paths(PathSelectors.none())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("auth系统数据接口文档")
.description("此系统为新架构Api说明文档")
.termsOfServiceUrl("")
.contact(new Contact("陈永佳 chen867647213@163.com", "", "https://blog.csdn.net/Mrs_chens"))
.version("1.0")
.build();
}

/**
* swagger ui资源映射
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");

registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}

/**
* swagger-ui.html路径映射,浏览器中使用/api-docs访问
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addRedirectViewController("/api-docs","/swagger-ui.html");
}
}

🌂禁用方法2:

使用注解 @Profile({“dev”,“test”}) 表示在开发或测试环境开启,而在生产关闭。(推荐使用)

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

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
* @author sunny chen
* @version V1.0
* @Package com.dc.config
* @date 2018/1/16 17:33
* @Description: 主要用途:开启在线接口文档和添加相关配置
*/
@Configuration
@EnableSwagger2
@Profile({“dev”,“test”})
public class Swagger2Config extends WebMvcConfigurerAdapter {

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.dc.controller"))
.paths(PathSelectors.any())
//.paths(PathSelectors.none())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("auth系统数据接口文档")
.description("此系统为新架构Api说明文档")
.termsOfServiceUrl("")
.contact(new Contact("陈永佳 chen867647213@163.com", "", "https://blog.csdn.net/Mrs_chens"))
.version("1.0")
.build();
}

/**
* swagger ui资源映射
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");

registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}

/**
* swagger-ui.html路径映射,浏览器中使用/api-docs访问
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addRedirectViewController("/api-docs","/swagger-ui.html");
}
}

🌂禁用方法3:

使用注解 @ConditionalOnProperty(name = “swagger.enable”, havingValue = “true”) 然后在测试配置或者开发配置中 添加 swagger.enable = true 即可开启,生产环境不填则默认关闭 Swagger.

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

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
* @author sunny chen
* @version V1.0
* @Package com.dc.config
* @date 2018/1/16 17:33
* @Description: 主要用途:开启在线接口文档和添加相关配置
*/
@Configuration
@EnableSwagger2
@ConditionalOnProperty(name ="enabled" ,prefix = "swagger",havingValue = "true",matchIfMissing = true)
public class Swagger2Config extends WebMvcConfigurerAdapter {

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.dc.controller"))
.paths(PathSelectors.any())
//.paths(PathSelectors.none())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("auth系统数据接口文档")
.description("此系统为新架构Api说明文档")
.termsOfServiceUrl("")
.contact(new Contact("陈永佳 chen867647213@163.com", "", "https://blog.csdn.net/Mrs_chens"))
.version("1.0")
.build();
}

/**
* swagger ui资源映射
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");

registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}

/**
* swagger-ui.html路径映射,浏览器中使用/api-docs访问
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addRedirectViewController("/api-docs","/swagger-ui.html");
}
}
1
2
3
4
yaml复制代码
#Swagger lock
swagger:
enabled: true

最后感谢大家耐心观看完毕,原创不易,留个点赞收藏是您对我最大的鼓励!


🎉总结:

  • 更多参考精彩博文请看这里:《陈永佳的博客》
  • 喜欢博主的小伙伴可以加个关注、点个赞哦,持续更新嘿嘿!

本文转载自: 掘金

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

Spring Cloud Gateway 雪崩了,我 TM

发表于 2021-09-27

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

本系列是 我TM人傻了 系列第六期[捂脸],往期精彩回顾:

  • 升级到Spring 5.3.x之后,GC次数急剧增加,我TM人傻了
  • 这个大表走索引字段查询的 SQL 怎么就成全扫描了,我TM人傻了
  • 获取异常信息里再出异常就找不到日志了,我TM人傻了
  • spring-data-redis 连接泄漏,我 TM 人傻了
  • Spring Cloud Gateway 没有链路信息,我 TM 人傻了

image

大家好,我又人傻了。这次的经验告诉我们,出来写代码偷的懒,迟早要还的。

问题现象与背景

昨晚我们的网关雪崩了一段时间,现象是:

1.不断有各种微服务报异常:在写 HTTP 响应的时候,连接已经关闭:

1
makefile复制代码reactor.netty.http.client.PrematureCloseException: Connection prematurely closed BEFORE response

2.同时还有请求还没读取完,连接已经关闭的异常:

1
lua复制代码org.springframework.http.converter.HttpMessageNotReadableException: I/O error while reading input message; nested exception is java.io.IOException: UT000128: Remote peer closed connection before all data could be read

3.前端不断有请求超时的报警,504 Gateway Time-out

4.网关进程不断健康检查失败而被重启

5.重启后的网关进程,立刻请求数量激增,每个实例峰值 2000 qps,闲时每个实例 500 qps,忙时由于有扩容也能保持每个实例在 1000 qps 以内,然后健康检查接口就很长时间没有响应,导致实例不断重启

其中,1 和 2 的问题应该是应为网关不断重启,并且由于某些原因优雅关闭失败导致强制关闭,强制关闭导致连接被强制断开从而有 1 和 2 相关的异常。

我们的网关是基于 Spring Cloud Gateway 实现的,并且有自动根据 CPU 负载扩容的机制。奇怪的是在请求数量彪增的时候,CPU 利用率并没有提高很多,保持在 60% 左右,由于 CPU 负载没有达到扩容的界限,所以一直没有自动扩容。为了快速解决问题,我们手动扩容了几个网关实例,将网关单实例负载控制在了 1000 以内,暂时解决了问题。

问题分析

为了彻底解决这个问题,我们使用 JFR 分析。首先先根据已知的线索去分析:

  1. Spring Cloud Gateway 是基于 Spring-WebFlux 实现的异步响应式网关,http 业务线程是有限的(默认是 2 * 可以使用的 CPU 个数,我们这里是 4)。
  2. 网关进程不断健康检查失败,健康检查调用的是 /actuator/health 接口,这个接口一直超时。

健康检查接口超时一般有两个原因:

  1. 健康检查接口检查某个组件的时候,阻塞住了。例如数据库如果卡住,那么可能数据库健康检查会一直没有返回。
  2. http 线程池没来得及处理健康检查请求,请求就超时了。

我们可以先去看 JFR 中的定时堆栈,看是否有 http 线程卡在健康检查上面。查看出问题后的线程堆栈,重点关注那 4 个 http 线程,结果发现这 4 个线程的堆栈基本一样,都是在执行 Redis 命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
less复制代码"reactor-http-nio-1" #68 daemon prio=5 os_prio=0 cpu=70832.99ms elapsed=199.98s tid=0x0000ffffb2f8a740 nid=0x69 waiting on condition  [0x0000fffe8adfc000]
java.lang.Thread.State: TIMED_WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@11.0.8/Native Method)
- parking to wait for <0x00000007d50eddf8> (a java.util.concurrent.CompletableFuture$Signaller)
at java.util.concurrent.locks.LockSupport.parkNanos(java.base@11.0.8/LockSupport.java:234)
at java.util.concurrent.CompletableFuture$Signaller.block(java.base@11.0.8/CompletableFuture.java:1798)
at java.util.concurrent.ForkJoinPool.managedBlock(java.base@11.0.8/ForkJoinPool.java:3128)
at java.util.concurrent.CompletableFuture.timedGet(java.base@11.0.8/CompletableFuture.java:1868)
at java.util.concurrent.CompletableFuture.get(java.base@11.0.8/CompletableFuture.java:2021)
at io.lettuce.core.protocol.AsyncCommand.await(AsyncCommand.java:83)
at io.lettuce.core.internal.Futures.awaitOrCancel(Futures.java:244)
at io.lettuce.core.FutureSyncInvocationHandler.handleInvocation(FutureSyncInvocationHandler.java:75)
at io.lettuce.core.internal.AbstractInvocationHandler.invoke(AbstractInvocationHandler.java:80)
at com.sun.proxy.$Proxy245.get(Unknown Source)
at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.get(LettuceStringCommands.java:68)
at org.springframework.data.redis.connection.DefaultedRedisConnection.get(DefaultedRedisConnection.java:267)
at org.springframework.data.redis.connection.DefaultStringRedisConnection.get(DefaultStringRedisConnection.java:406)
at org.springframework.data.redis.core.DefaultValueOperations$1.inRedis(DefaultValueOperations.java:57)
at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:60)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:222)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:189)
at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:96)
at org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java:53)
at com.jojotech.apigateway.filter.AccessCheckFilter.traced(AccessCheckFilter.java:196)
at com.jojotech.apigateway.filter.AbstractTracedFilter.filter(AbstractTracedFilter.java:39)
at org.springframework.cloud.gateway.handler.FilteringWebHandler$GatewayFilterAdapter.filter(FilteringWebHandler.java:137)
at org.springframework.cloud.gateway.filter.OrderedGatewayFilter.filter(OrderedGatewayFilter.java:44)
at org.springframework.cloud.gateway.handler.FilteringWebHandler$DefaultGatewayFilterChain.lambda$filter$0(FilteringWebHandler.java:117)
at org.springframework.cloud.gateway.handler.FilteringWebHandler$DefaultGatewayFilterChain$$Lambda$1478/0x0000000800b84c40.get(Unknown Source)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44)
at reactor.core.publisher.Mono.subscribe(Mono.java:4150)
at com.jojotech.apigateway.common.TracedMono.subscribe(TracedMono.java:24)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.Mono.subscribe(Mono.java:4150)
at com.jojotech.apigateway.common.TracedMono.subscribe(TracedMono.java:24)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.Mono.subscribe(Mono.java:4150)
at com.jojotech.apigateway.common.TracedMono.subscribe(TracedMono.java:24)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.Mono.subscribe(Mono.java:4150)
at com.jojotech.apigateway.common.TracedMono.subscribe(TracedMono.java:24)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.Mono.subscribe(Mono.java:4150)
at com.jojotech.apigateway.common.TracedMono.subscribe(TracedMono.java:24)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.Mono.subscribe(Mono.java:4150)
at com.jojotech.apigateway.common.TracedMono.subscribe(TracedMono.java:24)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.Mono.subscribe(Mono.java:4150)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:255)
at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:157)
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:73)
at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:281)
at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:860)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120)
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:73)
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1815)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:151)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120)
at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:281)
at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:860)
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79)
at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(MonoPeekTerminal.java:180)
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1815)
at reactor.core.publisher.MonoFilterWhen$MonoFilterWhenMain.onNext(MonoFilterWhen.java:149)
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2397)
at reactor.core.publisher.MonoFilterWhen$MonoFilterWhenMain.onSubscribe(MonoFilterWhen.java:112)
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54)
at reactor.core.publisher.Mono.subscribe(Mono.java:4150)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:448)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onNext(FluxConcatMap.java:250)
at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.onNext(FluxDematerialize.java:98)
at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.onNext(FluxDematerialize.java:44)
at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:270)
at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:228)
at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.request(FluxDematerialize.java:127)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:235)
at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.onSubscribe(FluxDematerialize.java:77)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:164)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86)
at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:62)
at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54)
at reactor.core.publisher.Mono.subscribe(Mono.java:4150)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:448)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:218)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:164)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at org.springframework.cloud.sleuth.instrument.web.TraceWebFilter$MonoWebFilterTrace.subscribe(TraceWebFilter.java:184)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.Mono.subscribe(Mono.java:4150)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:255)
at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64)
at reactor.netty.http.server.HttpServer$HttpServerHandle.onStateChange(HttpServer.java:915)
at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:654)
at reactor.netty.transport.ServerTransport$ChildObserver.onStateChange(ServerTransport.java:478)
at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:526)
at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:94)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:209)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at reactor.netty.http.server.logging.AccessLogHandlerH1.channelRead(AccessLogHandlerH1.java:59)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(java.base@11.0.8/Thread.java:834)

发现 http 线程没有卡在健康检查,同时其他线程也没有任何和健康检查相关的堆栈(异步环境下,健康检查也是异步的,其中某些过程可能交给其他线程)。所以,健康检查请求应该是还没被执行就超时取消了。

那么为啥会这样呢?于此同时,我还发现这里用的是 RedisTemplate,是 spring-data-redis 的同步 Redis API。我猛然想起来之前写这里的代码的时候,因为只是验证一个 key 是否存在和修改 key 的过期时间,偷懒没有用异步 API。这里是不是因为使用同步 API 阻塞了 http 线程导致的雪崩呢?

我们来验证下这个猜想:我们的项目中 redis 操作是通过 spring-data-redis + Lettuce 连接池,启用并且增加了关于 Lettuce 命令的 JFR 监控,可以参考我的这篇文章:这个 Redis 连接池的新监控方式针不戳~我再加一点佐料,截至目前我的 pull request 已经合并,这个特性会在 6.2.x 版本发布。我们看下出问题时间附近的 Redis 命令采集,如下图所示:

image

我们来简单计算下执行 Redis 命令导致的阻塞时间(我们的采集是 10s 一次,count 是命令次数,时间单位是微秒):使用这里的命令个数乘以 50% 的中位数,除以 10(因为是 10s),得出每秒因为执行 Redis 命令导致的阻塞时间:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码32*152=4864
1*860=860
5*163=815
32*176=5632
1*178=178
16959*168=2849112
774*176=136224
3144*166=521904
17343*179=3104397
702*166=116532
总和 6740518
6740518 / 10 = 674051.8 us = 0.67s

这个仅仅是使用中位数计算的阻塞时间,从图上的分布其实可以看出真正的值应该比这个大,这样很有可能每秒需要在 Redis 同步接口上阻塞的时间就超过了 1s,不断地请求,请求没有减少,从而导致了请求越积越多,最后雪崩。

并且由于是阻塞接口,线程很多时间消耗在等待 io 了,所以 CPU 上不去,导致没有自动扩容。业务高峰时,由于有设定好的预先扩容,导致网关单实例没有达到出问题的压力,所以没问题。

解决问题

我们来改写原有代码,使用同步 spring-data-redis Api 原有代码是(其实就是 spring-cloud-gateway 的 Filter 接口的核心方法 public Mono<Void> traced(ServerWebExchange exchange, GatewayFilterChain chain) 的方法体):

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
scss复制代码if (StringUtils.isBlank(token)) {
//如果 token 不存在,则根据路径决定继续请求还是返回需要登录的状态码
return continueOrUnauthorized(path, exchange, chain, headers);
} else {
try {
String accessTokenValue = redisTemplate.opsForValue().get(token);
if (StringUtils.isNotBlank(accessTokenValue)) {
//如果 accessTokenValue 不为空,则续期 4 小时,保证登录用户只要有操作就不会让 token 过期
Long expire = redisTemplate.getExpire(token);
log.info("accessTokenValue = {}, expire = {}", accessTokenValue, expire);
if (expire != null && expire < 4 * 60 * 60) {
redisTemplate.expire(token, 4, TimeUnit.HOURS);
}

//解析,获取 userId
JSONObject accessToken = JSON.parseObject(accessTokenValue);
String userId = accessToken.getString("userId");
//如果 userId 不为空才合法
if (StringUtils.isNotBlank(userId)) {
//解析 Token
HttpHeaders newHeaders = parse(accessToken);
//继续请求
return FilterUtil.changeRequestHeader(exchange, chain, newHeaders);
}
}
} catch (Exception e) {
log.error("read accessToken error: {}", e.getMessage(), e);
}
//如果 token 不合法,则根据路径决定继续请求还是返回需要登录的状态码
return continueOrUnauthorized(path, exchange, chain, headers);
}

改成使用异步:

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
scss复制代码if (StringUtils.isBlank(token)) {
return continueOrUnauthorized(path, exchange, chain, headers);
} else {
HttpHeaders finalHeaders = headers;
//必须使用 tracedPublisherFactory 包裹,否则链路信息会丢失,这里参考我的另一篇文章:Spring Cloud Gateway 没有链路信息,我 TM 人傻了
return tracedPublisherFactory.getTracedMono(
redisTemplate.opsForValue().get(token)
//必须切换线程,否则后续线程使用的还是 Redisson 的线程,如果耗时长则会影响其他使用 Redis 的业务,并且这个耗时也算在 Redis 连接命令超时中
.publishOn(Schedulers.parallel()),
exchange
).doOnSuccess(accessTokenValue -> {
if (accessTokenValue != null) {
//accessToken续期,4小时
tracedPublisherFactory.getTracedMono(redisTemplate.getExpire(token).publishOn(Schedulers.parallel()), exchange).doOnSuccess(expire -> {
log.info("accessTokenValue = {}, expire = {}", accessTokenValue, expire);
if (expire != null && expire.toHours() < 4) {
redisTemplate.expire(token, Duration.ofHours(4)).subscribe();
}
}).subscribe();
}
})
//必须转换成非 null,否则 flatmap 不会执行;也不能在末尾用 switchIfEmpty,因为整体返回的是 Mono<Void> 本来里面承载的就是空的,会导致每个请求发送两遍。
.defaultIfEmpty("")
.flatMap(accessTokenValue -> {
try {
if (StringUtils.isNotBlank(accessTokenValue)) {
JSONObject accessToken = JSON.parseObject(accessTokenValue);
String userId = accessToken.getString("userId");
if (StringUtils.isNotBlank(userId)) {
//解析 Token
HttpHeaders newHeaders = parse(accessToken);
//继续请求
return FilterUtil.changeRequestHeader(exchange, chain, newHeaders);
}
}
return continueOrUnauthorized(path, exchange, chain, finalHeaders);
} catch (Exception e) {
log.error("read accessToken error: {}", e.getMessage(), e);
return continueOrUnauthorized(path, exchange, chain, finalHeaders);
}
});
}

这里有几个注意点:

  1. Spring-Cloud-Sleuth 对于 Spring-WebFlux 中做的链路追踪优先,如果我们在 Filter 中创建新的 Flux 或者 Mono,这里面是没有链路信息的,需要我们手动加入。这个可以参考我的另一篇文章:Spring Cloud Gateway 没有链路信息,我 TM 人傻了
  2. spring-data-redis + Lettuce 连接池的组合,对于异步接口,我们最好在获取响应之后切换成别的线程池执行,否则后续线程使用的还是 Redisson 的线程,如果耗时长则会影响其他使用 Redis 的业务,并且这个耗时也算在 Redis 连接命令超时中
  3. Project Reactor 如果中间结果有 null 值,则后面的 flatmap、map 等流操作就不执行了。如果在这里终止,前端收到的响应是有问题的。所以中间结果我们要在每一步考虑 null 问题。
  4. spring-cloud-gateway 的核心 GatewayFilter 接口,核心方法返回的是 Mono<Void>。Mono 本来里面承载的就是空的,导致我们不能使用末尾的 switchIfEmpty 来简化中间步骤的 null,如果用了会导致每个请求发送两遍。

这样修改后,压测了下网关,单实例 2w qps 请求也没有出现这个问题了。

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

本文转载自: 掘金

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

Java探针技术Instrumentation

发表于 2021-09-27

Instrumentation简介

Java探针技术,通过Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在JVM上的程序,甚至能够替换和修改某些类的定义而对业务代码没有侵入

成熟应用

java领域的APM(Application Performance Management应用性能管理)工具,几乎都是基于Instrumentation实现的。

  • zipkin:Twitter公司开源的一个分布式追踪工具,被Spring Cloud Sleuth集成,使用广泛而稳定
  • skywalking:中国人吴晟(华为)开源的一款分布式追踪,分析,告警的工具,现在是Apache旗下开源项目
  • cat:大众点评开源的一款分布式链路追踪工具。

Arthas 是Alibaba开源的Java诊断工具也是基于此。

这么多优秀的产品都在使用java探针Instrumentation,是不是有兴趣深入了解一下呢。

使用流程

入口代理类

在main方法前执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class MyAgent {

static Instrumentation instrumentation;

public static void premain(String agentArgs, Instrumentation inst)
throws Exception {
try {
instrumentation = inst;
inst.addTransformer(new MyClassFileTransformer());
} catch (Exception e) {
}
}

}

需要提供一下两个方法之一,如果两个的话,前一个优先级较高

1
2
arduino复制代码public static void premain(String agentOps, Instrumentation instrumentation);
public static void premain(String agentOps);

自定义ClassFileTransformer

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
java复制代码package top.soft1010.java.myagent;

import javassist.*;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
import javassist.scopedpool.ScopedClassPool;
import javassist.scopedpool.ScopedClassPoolRepositoryImpl;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

/**
* Created by bjzhangjifu on 2021/9/27.
*/
public class MyClassFileTransformer implements ClassFileTransformer {

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("Transforming " + className);
//非自有类, 直接返回
if (!className.startsWith("top/soft1010")) {
return classfileBuffer;
}
//为null,表示由bootstrapLoader加载的,不能重写
if (loader == null) {
return classfileBuffer;
}
try {
// if (className != null && className.indexOf("/") != -1) {
// className = className.replaceAll("/", ".");
// }
CtClass ctClass = ClassPool.getDefault().makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
CtMethod[] methods = ctClass.getDeclaredMethods();
for (CtMethod method : methods) {
try {
method.insertAfter("System.out.println("--after--");");
method.insertBefore("System.out.println("--before--");");
} catch (CannotCompileException e) {
e.printStackTrace();
}
}
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}

ClassFileTransformer用于修改class文件,该操作发生在 JVM 加载 class 之前。它只有一个transform方法,实现该方法可以修改 class字节码(这里可以使用javassist,asm等),并返回修改后的 class字节码,有两点要注意:

  1. 如果此方法返回null, 表示我们不对类进行处理直接返回。否则,会用我们返回的byte[]来代替原来的类
  2. ClassFileTransformer必须添加进Instrumentation才能生效 Instrumentation#addTransformer(ClassFileTransformer)
  3. 当存在多个Transformer时,一个Transformer调用返回的byte数组将成为下一个Transformer调用的输入

运行

这里我们直接写一个main方法类,当然也可以是jar

1
2
3
4
5
6
7
8
9
typescript复制代码public class MyTest {
public static void main(String[] args) {
new MyTest().test();
}

private void test() {
System.out.println(" ----- test ----- ");
}
}

执行命令

1
复制代码java -javaagent:myagent.jar top.soft1010.java.myagent.MyTest

面试

1、加载类的时候,对字节码进行修改?

1
2
3
4
css复制代码能,使用java探针,javaagent就可以。
JavaAgent 是JDK 1.5 以后引入的,也可以叫做Java代理。
JavaAgent 是运行在 main方法之前的拦截器,它内定的方法名叫 premain ,也就是说先执行 premain 方法然后再执行 main 方法。
premain方法有个Instrumentation 参数,通过它我们可以添加自定义一个FileTransformer,这个接口只有一个方法transform 通过这个方法我们能获取加载类的字节码,当然也可以修改编辑字节码,比如在指定类的方法前后添加监控信息等。

2、说说java探针技术javaagent的应用

1
2
3
4
bash复制代码skywalking:中国人吴晟(华为)开源的一款分布式追踪,分析,告警的工具,现在是Apache旗下开源项目
cat:大众点评开源的一款分布式链路追踪工具。
arthas: 阿里开源的java诊断工具
当然好多大公司内部也有好多自己定制的apm工具也是基于此

源码

文中涉及到的代码
源码

本文转载自: 掘金

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

Mybatis

发表于 2021-09-27

博客索引

大家使用的时候都知道#{}是起到一个占位符的作用,在JDBC中也就是对应的一个预编译,也就是对应JDBC中的PreparedStatement,源码中看到示例如下:

1
2
3
4
5
6
7
8
ini复制代码
PreparedStatement pstmt = con.prepareStatement("UPDATE EMPLOYEES

SET SALARY = ? WHERE ID = ?");

pstmt.setBigDecimal(1, 153833.00)

pstmt.setInt(2, 110592)
  1. 猜想

所以Mybatis中的#{}是怎么完成预编译的效果呢?根据这里可以猜想,应该是Mybatis中将#{}最后解析成了?占位符。

  1. 验证

那就写个例子来debug看一下,我们以查询为例

1
2
3
4
5
6
7
8
9
10
11
12
13
vbnet复制代码@Data

public class A {

private Integer id;

private Integer age;

private String name;

private Date createTime;

}

Mapper如下

1
2
3
4
5
6
7
8
9
10
java复制代码
@Mapper

public interface ADAO {

@Select("SELECT * FROM A WHERE id = #{id}")

A selectA(int id);

}

测试类ADAOTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
less复制代码
@RunWith(SpringRunner.class)

@SpringBootTest

public class ADAOTest {

@Autowired

private ADAO adao;

@Test

public void selectA() {

A a = adao.selectA(2);

System.out.println(a);

}

@Select执行原理

这里只在关键地方打上断点,org.apache.ibatis.builder.SqlSourceBuilder#parse

占位符替换.png

已经完成了占位符?的替换,接下来就是获取PreparedStatement,在org.apache.ibatis.executor.SimpleExecutor#prepareStatement中。

这里的核心步骤已经标注出来了,有兴趣的话可以打个断点看看详细流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {

Statement stmt;

Connection connection = getConnection(statementLog);

// 在这一步中进行connect.prepareStatement(sql)

stmt = handler.prepare(connection, transaction.getTimeout());

// 这一步执行执行填充参数 stmt.setInt(1,"xxx");

handler.parameterize(stmt);

return stmt;

}

最后执行查询org.apache.ibatis.executor.statement.PreparedStatementHandler#query

1
2
3
4
5
6
7
8
9
10
scss复制代码
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {

PreparedStatement ps = (PreparedStatement) statement;

ps.execute();

return resultSetHandler.<E> handleResultSets(ps);

}
  1. 总结

猜想是对的,mybatis自定义的占位符#{} 最后被替换成JDBC的占位符?

所以,其实可以总结一下,其实所有的ORM框架,底层的原理都是JDBC的标准。

所以不管是Mybatis的占位符,还是JPA中的占位符,最终的执行流程都是解析成?这个占位符,别问为什么,问这就是标准。

本文转载自: 掘金

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

【设计模式系列】单例模式的N种实现

发表于 2021-09-27

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

如果你自认为已经掌握懒汉式、饿汉式、DCL、IoDH、枚举等单例实现方式,可以直接看最后的打破单例。如果不是,建议你耐心从头看完。

相信你看完本文能让你掌握Java中所有单例模式的设计方式。

单例设计模式是GOF 23中设计模式中常见的设计模式之一,不论是在我们日常开发,还是一些第三方库中几乎都能见到单例模式。包括在面试时初中级的程序员基本都会被问到单例模式。

单例模式的目的主要是为了保证在多线程场景下实例唯一的一种解决方案,实现起来还是比较简单的,但是实现方式各式各样,五花八门,今天小黑带大家梳理下单例模式的7种实现方式,并比较各有什么优缺点。

饿汉式

饿汉式,顾名思义一上来就会创建实例对象,因为很饿嘛,马上就要创建出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
 * @author java_xiaohei 公众号:小黑说Java
 * @ClassName Singleton
 * @Description 单例设计模式:饿汉式
 * @date 2021/9/27
 **/
public final class Singleton {
    // 定义变量时直接初始化
    private static Singleton instance = new Singleton();
    // 构造方法私有,不允许外部new
    private Singleton() {
    }
    // 外部通过getIntstance获取实例
    public static Singleton getInstance() {
        return instance;
    }
}

饿汉式的关键在于定义instance时直接实例化。通过饿汉式完全可以保证实例对象的线程安全。

但是有一个问题,如果该实例对象被创建之后过了很久才会被访问,那么在访问之前这个对象数据会一直存放在堆内存当中,如果实际场景中单例对象的实例数据很大,将会占用比较多的资源,这种方式则不太合适。

懒汉式

懒汉式相对饿汉式而言,区别的地方主要在创建对象的时机有区别,不会在定义变量时初始化,而是在需要使用时才进行创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码public final class Singleton {
    // 定义变量时不做初始化
    private static Singleton instance = null;
    // 构造方法私有,不允许外部new
    private Singleton() {
    }
    // 外部通过getIntstance获取实例
    public static Singleton getInstance() {
        if(instance==null){
            instance = new Singleton();
        }
        return instance;
    }
}

这种方式相对饿汉式而言,可以避免在使用对象之前创建对象造成的空间资源浪费。

上面代码在单线程场景下调用getInstance()没有问题,但是在多线程情况下,会有线程安全问题。

是因为getInstance()方法中的if判断和对instance的赋值动作不是原子操作。

如下图,有两个线程A和B先后调用getInstance(),线程A判断instance==null为true。

在线程A进行实例化对象之前,线程B拿到了CPU的执行权,这时线程B判断instance==null也为true,此时线程B也会做实例化,这样无法保证实例的唯一性。

这种懒汉式的实现方式因为不能保证线程安全,所以实际开发中不可以使用。

懒汉式+同步方法

简单的懒汉式因为存在线程安全问题,那么我们可以通过加锁的方式来保证线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public final class Singleton {
    // 定义变量时不做初始化
    private static Singleton instance = null;
    // 构造方法私有,不允许外部new
    private Singleton() {
    }
    // 方法上加synchronized保证线程安全
    public static synchronized Singleton getInstance() {
        if(instance==null){
            instance = new Singleton();
        }
        return instance;
    }
}

通过在getInstance()方法上加锁,可以保证多线程情况下只能有一个线程进入此方法,也就保证了线程安全。

但是这种方法未免有点太偷懒,在初次创建实例对象之时加锁可以避免实例对象被创建多个,但是在实例被成功创建之后,每次获取实例时都需要获取锁,这会极大的降低性能。

DCL

DCL是Double Check Lock的简写,意思是双重检查锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
csharp复制代码public final class Singleton {
    private static Singleton instance = null;
    String msg;
    private Singleton() {
        // 初始化msg
        this.msg = "对象描述信息";
    }
    public static Singleton getInstance() {
        // 第一层检查
        if (instance == null) {
            // 只能有一个线程获得Singleton.class锁
            synchronized (Singleton.class) {
                // 第二层检查
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

DCL方式在保证线程安全的做法上没有偷懒,并没有直接在方法上加锁。

第一层判断如果为true,则会执行加锁和创建实例的操作;如果第一层判断为false,表示对象已经创建完成了,那么直接返回实例就OK,避免每次请求都加锁,性能高。

在第一层判断结果为true,则需要加锁保证实例创建过程的安全性。

加锁成功后的第二层判空检查的目的是为了防止在进入第一层检查和加锁成功的过程中已经有其他线程完成实例的创建,避免重复创建。

DCL方式保证了线程安全的同时又没有损失性能,并且还是懒加载的,貌似是一种完美的解决方案。

但是这种方式在多线程情况下可能会出现空指针异常。如果你有细心看我的代码会发现上面的代码中,Singleton()构造方法中对属性msg进行了初始化赋值。

那么我们在getInstance()方法中的instance = new Singleton();可以分为三步操作。

1.new Singleton()在堆中创建一个对象;

2.msg赋值;

3.instance赋值;

但是根据JVM运行时Happens-before规则,这三步的顺序并没有前后依赖,很有可能实际运行的顺序是,3->1->2或者其他;那么就可能造成在getInstance()中instance == null的结果为true,但是实例中的msg为null;假设调用方拿到instance之后直接使用msg则会抛出空指针异常。

volatile+DCL

DCL方式虽然看起来很巧妙的实现了单例模式,但是因为JVM的运行时指令重排序导致单例对象使用过程中的异常。

解决这个问题则需要使用volatile关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码public final class Singleton {
    // volatile修饰
    private static volatile Singleton instance = null;
    String msg;
    private Singleton() {
        this.msg = "对象描述信息";
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

volatile关键字可以防止重排序的发生。如果你对volatile的底层原理感兴趣,可以看看我另一篇文章。Java内存模型

静态内部类

使用静态内部类实现单例模式的方式主要是借助于类加载机制完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码public final class Singleton {
    private Singleton() {
    }
    public static Singleton getInstance() {
        // 实际是返回静态内部类中的实例
        return Holder.instance;
    }

    private static class Holder {
        // 在静态内部类中定义instance并实例化
        private static Singleton instance = new Singleton();
    }
}

这种方式将单例对象的定义放在静态内部类Holder中,如果你对静态内部类的加载机制比较了解那这里你一定很明白了。

静态内部类并不会随着外部类的加载一起加载,只有在使用时才会加载;

而类加载的过程则直接保证了线程安全性,保证实例对象的唯一。

这种方式又被称为IoDH(Initialization Demand Holder)技术,是目前使用比较广的方式之一,也算是最好的一种单例设计模式了。

枚举

最后一种实现单例的方式是枚举。

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码public enum Singleton {
    INSTANCE;
    String data = "实例数据";

    Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

使用枚举方式实现单例是**《Effective Java》**中推荐的一种方式。枚举不可以被继承,并且线程安全,只会实例化一次。

打破单例

以上说了这么多单例的实现方式,到底能不能保证实例只被创建一次呢?可不可以被突破呢?

我们学过一种叫做反射的技术,可以来尝试能不能搞搞事情,使用饿汉式这种方式举例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
csharp复制代码public final class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {
    }
    public static Singleton getInstance() {
        return instance;
    }
}

class SingletonTest {
    public static void main(String[] args) throws Exception {
        Singleton instance = Singleton.getInstance();
        System.out.println(instance);
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton singleton = constructor.newInstance();
        System.out.println(singleton);
    }
}

运行结果:

从结果上看,简单的将构造方法私有化并不能保证实例创建一个,如果使用方反射处理构造方法可以突破这个限制。

如果要防止使用方这样操作,我们可以在构造方法上做点手脚。

1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码public final class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {
        if(Singleton.instance != null){
           throw new IllegalStateException("你不要搞事情");
        }
    }

    public static Singleton getInstance() {
        return instance;
    }
}

如果instance已经被实例化,则抛出异常,可以避免被反射突破单例。如果是懒汉式并不能保证在调用getInstance()方法之前被创建。

这里我们会发现,不管是懒汉式还是饿汉式,通过私有化构造方法都不能保证构造方法不被外部执行,那么枚举能不能被突破唯一性呢?我们再来试试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码public enum Singleton {
    INSTANCE;
    String data = "实例数据";
    Singleton() {
    }
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

class SingletonTest {
    public static void main(String[] args) throws Exception {
        Singleton instance = Singleton.getInstance();
        System.out.println(instance);
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton singleton = constructor.newInstance();
        System.out.println(singleton);
    }
}

运行结果:

从运行结果发现,枚举类型虽然从代码上有一个私有的构造方法,但是通过反射执行时并不能拿到。难道枚举没有构造方法?

小黑通过Debug获取所有构造方法发现,枚举类虽然没有无参的构造方法,但是有一个有参的方法,第一个参数为String,第二个参数为int。我猜这俩个参数应该是枚举对象的name和ordinal属性,我们再来搞个事情看看。

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码class SingletonTest {
    public static void main(String[] args) throws Exception {
        Singleton instance = Singleton.getInstance();
        System.out.println(instance);
        Constructor<?>[] declaredConstructors = Singleton.class.getDeclaredConstructors();
        Constructor<?> constructor = declaredConstructors[0];
        constructor.setAccessible(true);
        // 按照指定参数创建
        Object singleton = constructor.newInstance("INSTANCE", 1);
        System.out.println(singleton);
    }
}

执行结果:

我们发现,通过反射获取构造方法,然后按照我们Debug看到的参数进行创建对象时,抛出了异常。

异常描述为 Cannot reflectively create enum objects。不能反射创建枚举对象!!!

原来JDK早就预料到可能会有人这样搞事情来破坏枚举对象的唯一性,在构造方法中进行处理了,想必这应该是《Effective Java》中推荐的一部分原因吧。

总结

单例模式不管是在面试还是在实际的开发过程中会很高频的出现,看似简单,但是要实现一个线程安全,高性能,并且可以不被外部非法破坏,需要考虑的点还是挺多的,小黑通常会通过静态内部类实现IoDH方式或者枚举方式来设计单例。

要是觉得有用,点个赞再走吧。

本文转载自: 掘金

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

RocketMQ消费进度管理浅析

发表于 2021-09-27

幂等性的取与舍

分布式平台上幂等性相关语义的保证,是我们构造安全、可信赖系统的永恒追求。作为异步、解耦通常实现方案下的最优选,我时常思考Rocket MQ设计者经历怎样的断舍离?


众所周知消息队列关于消息消费这一概念的落地实现,大体上分为三种情形:
  • At most once
  • At least once
  • Exactly once
翻译一下就是:
  • 至多消费一次
  • 至少消费一次
  • 保证消费一次
很显然如果至多消费一次,势必造成消息丢失;至少消费一次就对我们的业务系统提出更高的要求,保证消费一次看似美好时则需要MQ系统背负沉重代价。Rocket MQ丝毫不犹豫的选择At least once。将幂等的保证大胆的交给开发者,不仅仅体现作者对MQ性能与功能两者矛盾的无奈,同时也体现了对广大开发人员的信任。

消费现状概述

上述论调虽然客观真实但不免有些悲观主义的意味,按照上文的理解我们业务体统需要倚仗ta,但我们又要时刻防备ta,因为一个不小心可能就会出纰漏,这还真是一个让人又爱又怕的存在。


读到这里,笔者似乎把ta描绘成了一个顽皮的孩子,但其实有些言重了,因为以我阅读源码的理解,业务系统没有异常,MQ所在的物理运行环境又比较健康的情况下,其实比较难以出现多次重复消费。


RocketMQ的幂等往往是由业务系统的异常逻辑,或者网络,或者不确定的运行环境破坏的。绝大多数情形下确定无疑ta依然是一个Good Boy。


按照我们对消息系统的朴素理解,消息的消费过程满足以下几个规律:
  • 虽然不会严格的按照投递顺序进行消费,但大体上保持先进先出这个趋势
  • 消息应该被精确的记录当前消费状态
  • 总有一个角色负责统计、持久化消费偏移量
带着经验主义我们看看作者都为平稳消费与进度管理做出了哪些努力。

⚠️注:Rocket MQ的顺序消费模型是可以严格保证顺序的。

OffsetStore

消息被消费后也就失去了在ProcessQueue中停留的资格,ProcessQueue会删除该消息,并返回当前的最小偏移量放置到消息进度表中。很容易想象,如果这个消费进度不加以持久化,那么每次启动都要重头消费,显然无法接受,可是如何持久化,又持久化到何处呢?


Rocket MQ支持两种订阅模式:
  • 集群消费模式:默认的消费模式,所有消息只需要被同组任一消费者消费一次即可,大家共享订阅Topic下的消费偏移量。
  • 广播消费模式:各个消费者的消费行为是完全独立的,订阅Topic下所有的消息都需要被该组下所有消费者消费。
针对两种消费模型的特性,容易发现二者并不好一概而论,理想的实现是划分为两个策略,一个集中到Broker管理,一个分散出去由消费者管理。OffsetStore接口负责相关事宜,源码应证了我们猜想。

image.png
先来看看OffsetStore接口定义:

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复制代码public interface OffsetStore {

/**
* 从消息进度存储文件加载消息进度到内存
*/
void load() throws MQClientException;

/**
* Get offset from local storage
* @return The fetched offset
*/
long readOffset(MessageQueue mq, ReadOffsetType type);

/**
* Remove offset
*/
void removeOffset(MessageQueue mq);

Map<MessageQueue, Long> cloneOffsetTable(String topic);

/**
* 更新内存中的消息进度
* Update the offset,store it in memory
*/
void updateOffset(MessageQueue mq, long offset, boolean increaseOnly);

/**
* 保留所有偏移量,可能在本地存储或远程服务器
* Persist all offsets,may be in local storage or remote name server
*/
void persistAll(Set<MessageQueue> mqs);

/**
* 保留指定消息队列偏移量,可能在本地存储或远程服务器
* Persist the offset,may be in local storage or remote name server
*/
void persist(MessageQueue mq);

/**
* 更新存储在 Broker 端的消息消费进度,使用集群模式
*/
void updateConsumeOffsetToBroker(MessageQueue mq, long offset, boolean isOneway)
throws RemotingException, MQBrokerException,
InterruptedException, MQClientException;

}
较之源码,方法排列被我调换了顺序,需要着重关注的我放到了后面。

⚠️注:如果没有Rocket MQ源码阅读经历ProcessQueue显得有些突兀,你可以将ta理解为消息在Consumer端的载体、物理队列某一个截取片段。作者如此定义ta:Queue consumption snapshot

LocalFileOffsetStore

广播模式下消息进度保留在Consumer端,文件遵守约定放置在可配置的固定目录下,文件路径如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class LocalFileOffsetStore implements OffsetStore {

/**
* 存储文件夹路径可定制
*/
public final static String LOCAL_OFFSET_STORE_DIR = System.getProperty(
"rocketmq.client.localOffsetStoreDir",
System.getProperty("user.home") + File.separator + ".rocketmq_offsets"
);

/**
* 构造方法拼接出了文件的完整路径
*/
public LocalFileOffsetStore(MQClientInstance mQClientFactory,
String groupName) {
this.mQClientFactory = mQClientFactory;
this.groupName = groupName;
this.storePath = LOCAL_OFFSET_STORE_DIR + File.separator +
this.mQClientFactory.getClientId() + File.separator +
this.groupName + File.separator +
"offsets.json";
}

}
默认在用户路径下一层创建一个".rocketmq\_offsets"文件夹,注意这里有一个细节,文件夹以"."开头,在Linux系统中属于隐藏文件,需要加-a参数才能被显示。为了便于理解,下图展示了一个文件夹路径和一个Offset持久化文件的路径。

image.png

广播模式下Consumer#start()之后会调用OffsetStore.load()来加载消费进度,其原理就是根据约定拼接处文件全路径之后读取相应文件,然后序列化为OffsetSerializeWrapper对象:
1
2
3
4
5
6
7
java复制代码public class OffsetSerializeWrapper extends RemotingSerializable {

/* 详细记录每个队列当前消费进度 */
private ConcurrentMap<MessageQueue, AtomicLong> offsetTable
= new ConcurrentHashMap<>();

}
假设我们有个发送短信的服务订阅"SMS\_prod"Topic,那么形成的Json如下所示:注意offsetTable属性也是一个Json,而且key是MessageQueue对象,valule是一个数字表示偏移量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码{
"offsetTable": {
{
"topic": "SMS_prod",
"brokerName": "broker0"
"queueId": 0
}: 100,

{
"topic": "SMS_prod",
"brokerName": "broker0"
"queueId": 1
}: 100,
}
}
既然可以在指定文件load关键信息,自然就有相关机制负责写入。还记得上文提到的persistAll方法吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public void persistAll(Set<MessageQueue> mqs) {
/* 构造OffsetSerializeWrapper对象 */
OffsetSerializeWrapper offsetSerializeWrapper =
new OffsetSerializeWrapper();
for (Map.Entry<MessageQueue, AtomicLong> entry : offsetTable.entrySet()) {
if (mqs.contains(entry.getKey())) {
AtomicLong offset = entry.getValue();
offsetSerializeWrapper.getOffsetTable().put(entry.getKey(), offset);
}
}
/* 将offsetSerializeWrapper对象序列化 */
String jsonString = offsetSerializeWrapper.toJson(true);
/* 将序列化好的offsetSerializeWrapper写入文件 */
MixAll.string2File(jsonString, this.storePath);
}
对offsets.json的相关操作都被封装在MixAll工具类中:
  • MixAll.file2String: 将文件读取出来
  • MixAll.string2File: 将序列化好的对象写入文件

RemoteBrokerOffsetStore

因为偏移量维护在Broker端,所以该实现的load方法仅仅是一个声明。构造方法不需要计算文件路径也尤为简单,二者的offsetTable属性是一致的。我们着重来看看集群消费模式下如何保存消息消费进度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public void persistAll(Set<MessageQueue> mqs) {
HashSet<MessageQueue> unusedMQ = new HashSet<>();

for (Map.Entry<MessageQueue, AtomicLong> entry : offsetTable.entrySet()) {
MessageQueue mq = entry.getKey();
AtomicLong offset = entry.getValue();
if (offset != null) {
if (mqs.contains(mq)) {
this.updateConsumeOffsetToBroker(mq, offset.get());
} else {
unusedMQ.add(mq);
}
}
}

if (!unusedMQ.isEmpty()) {
for (MessageQueue mq : unusedMQ) {
this.offsetTable.remove(mq);
}
}
}
不用深入研究,我们应该能发现至少两处不同:
  • 粒度不同:广播模式是直接一下子把整个offsetTable持久化,而集群模式细化到了entry级别。
  • 调用方式不同:广播模式是直接JVM内部调用写入文件即可,而集群模式需要RPC调用参与。
    这里有必要强调一下二者产生的offset.json文件也是有区别的,下文我会分析,同时也带大家了解该RPC过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码RPC调用栈:

RemoteBrokerOffsetStore#persistAll()
-> RemoteBrokerOffsetStore#updateConsumeOffsetToBroker()
组装好RPC请求头UpdateConsumerOffsetRequestHeader对象
-> MQClientAPIImpl#updateConsumerOffsetOneway()
组装好RPC请求对象RemotingCommand
-> NettyRemotingClient#invokeSync()
发起RPC调用

更新偏移量的RPC调用类型是RequestCode.UPDATE_CONSUMER_OFFSET
顺着这个枚举来看看Broker端的相关处理:

ConsumerManageProcessor.updateConsumerOffset()
-> ConsumerOffsetManager.commitOffset()
追踪源码发现,其实每次Consumer进行RPC调用上报自己的消费进度,Broker接收之后并没有立即进行持久化,而是直接更新到内存中。
1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码private void commitOffset(String clientHost, String key, 
int queueId, long offset) {
String key = topic + TOPIC_GROUP_SEPARATOR + group;

ConcurrentMap<Integer, Long> map = offsetTable.get(key);
if (Objects.isNull(map)) {
map = new ConcurrentHashMap<>(32);
map.put(queueId, offset);
this.offsetTable.put(key, map);
} else {
Long storeOffset = map.put(queueId, offset);
}
}
TOPIC\_GROUP\_SEPARATOR为定义的常量: "@",之前我们提到过二者json有些许区别,offsetTable的key变成了一个拼接出来的字符串,该字符串左侧是TopicName,右侧是ConsumeGroupName中间用@符号连接。方便理解,我把这个json也展示出来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
json复制代码/**
* 注意一下这个key:%RETRY%ConsumeGroup
* 笔者后期会有专门文章分析
*/
{
"offsetTable": {
"Topic@ConsumeGroup":{
0: 38,
1: 37,
2: 50,
3: 10
},
"%RETRY%ConsumeGroup": {
0: 0
}
}
}

持久化

两种文件持久化机制没有什么大的区别定时任务触发,或者消费端正常关闭执行shotdown()之前手动触发。


广播模式定时任务定义在MQClientInstance中,MQClientInstance对象在被实例化之后调用start()时启动该定时任务。定时任务的时间间隔支持配置默认是5000ms,延时10000ms之后开始执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public void start() throws MQClientException {
this.scheduledExecutorService.scheduleAtFixedRate(
() -> {
try {
MQClientInstance.this.persistAllConsumerOffset();
} catch (Exception e) {
log.error("ScheduledTask persistAllConsumerOffset exception", e);
}
},
1000 * 10,
this.clientConfig.getPersistConsumerOffsetInterval(),
TimeUnit.MILLISECONDS
);
}
集群模式定时任务定义BrokerController中,BrokerController对象在被实例化之后会有一系列初始化动作,initialize()会启动该定时任务。定时任务的时间间隔支持配置默认是5000ms,延时10000ms之后开始执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public boolean initialize() throws CloneNotSupportedException {
this.scheduledExecutorService.scheduleAtFixedRate(
() -> {
try {
BrokerController.this.consumerOffsetManager.persist();
} catch (Throwable e) {
log.error("schedule persist consumerOffset error.", e);
}
},
1000 * 10,
this.brokerConfig.getFlushConsumerOffsetInterval(),
TimeUnit.MILLISECONDS
);
}

重复消费

原理分析了那么久,我想要传达的观点就是正常使用的前提下重复消费的原因一定跟offset上报,持久化有关系。
  • 集群消费过程中Consumer意外宕机,offset没有上报导致重复消费
  • 集群消费过程中Broker意外宕机,offset没有将最新的偏移量持久化导致重复消费
  • 广播消费过程Consumer意外宕机,offset没有持久化到本地文件导致重复消费
  • offset.json文件意外损坏或删除,进度丢失导致重复消费
  • offset.json文件被篡改,进度不准确导致重复消费
    还有一种是因为开发者返回了错误的ACK标示,导致Rocket误判以为消费失败,触发重试逻辑导致的重复消费。

本文转载自: 掘金

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

玩转MYSQL数据库的约束、聚合查询、联合查询以及三种表的设

发表于 2021-09-27

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
在这里插入图片描述

@TOC


在这里插入图片描述

一、MYSQL的那些约束你掌握了几种?

在MYQSL中,有那么一些约束。比如不能为null,或者是必须唯一的。就像身份证一样,每个人的身份证号一定是唯一的,性别可以是男,也可以是女,但不是为null。

下面介绍MYSQL中的6种约束

1、NOT NULL :约束某列不能为空。

1
2
sql复制代码-- 创建一个图书表,id不能为空
create table book(id int not null);

2、UNIQUE : 约束某列的某行有唯一值。

1
2
sql复制代码-- 创建一个图书表,id唯一,不能重复
create table book(id int unique);

3、DEFAULT : 没有给列赋值时给一个默认值。

1
2
3
sql复制代码-- 创建一个语文书表,默认name为unknown
create table book(name varchar(20) default
'unknown');

4、PRIMARY KEY :NOT NUL和 UNIQUE的结合,即不能为空, 值必须唯一。一般表的ID会设成primary key,这样的好处是为表创立一个唯一的标识。能够更方便的找到表中特殊的信息。一般常与auto_increment连用,表示自增主键,

1
2
3
4
5
sql复制代码-- 创建一个图书,每个图书有自己的名称,价格,但序列号是唯一的
create table book(
id int primary key auto_increment,
name varchar(20),
price int );

5、FOREIGN KEY :外键约束。一个表的数据和另一个表匹配,增加耦合性。

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码-- 创建班级表,
create table class(
id int primary key auto_increment,
name varchar(20)
);
-- 学生表,一个学生对应一个班级,一个班级可以有多个学生
create table student(
id int primary key auto_increment,
name varchar(20),
classes_id int ,
foreign key (classes_id) references class(id)
);

6、CHECK : 保证列中的值符合特定的条件。

1
2
3
4
5
6
7
sql复制代码-- MySQL使用时不报错,但忽略该约束:
create table student(
id int primary key auto_increment,
name varchar(20),
sex varchar(1),
check (sex='男' or sex='女')
);

二、表与表之间的三种关系

1、一对一关系
eg:人和身份照号码

一个人只能拥有一个身份证号码
在这里插入图片描述
2、一对多关系
eg:学生和班级的关系

一个班级可以有多个学生,一个学生只属于一个班级
在这里插入图片描述
3、多对多关系
eg:老师和班级的关系

一个班级可以有多个老师,一个老师也可以教多个班。

三、查询

3.1 :聚合查询

聚合查询通过4中聚合函数完成。他们分别是:**
1、count() :返回查询到的数量,和查询类型是不是数字无关。

1
2
sql复制代码-- 查询图书表有多少书
select count(*) from book;

2、sum() :返回查询数据的总和,如果数据不是数字就没有意义

1
2
sql复制代码-- 查询三年二班有多少学生
select sum(student) from class;

3、avg() : 返回查询的数据的平均数,如果数据不是数字没有意义

1
2
sql复制代码-- 查询三年二班学生数学的平均分
select avg(math) from class;

4、max() : 返回查询的数据的最大值,如果数据不是数字没有意义

1
2
sql复制代码-- 查询三年二班数学的最高分
select max(math) from class;

5、min(): 返回查询的数据的最小值,如果数据不是数字没有意义

1
2
sql复制代码-- 查询三年二班数学的最低分
select min(math) from class;

3.2、GROUP BY

使用group by字句可以对指定列进行分组查询。但是需要满足,select后的
字段必须是分组依据的字段。如果其他字段想出现就必须使用聚合函数。

eg:
准备数据,创建一个学生表,含有学生id,姓名,数学和语文成绩。
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
sql复制代码create table  student(
-- 学生id
id int primary key auto_increment,
-- 学生姓名
student_name varchar(20),
-- 学生数学成绩
math int,
-- 学生语文成绩
chinese int
);

插入数据
在这里插入图片描述

1
2
3
4
sql复制代码insert into student(student_name,math,chinese) values
('张三',66,67),
('李四',44,98),
('王五',88,89);

查看当前表的数据
在这里插入图片描述

使用GROUP BY查询,学生的最高、平均、最低数学成绩
在这里插入图片描述

3.3、HAVING和group by 搭配使用

在使用group by 字句后,如果还需要条件筛选的话,就不能用where了,需要用having来筛选条件。

1
2
sql复制代码-- 查询总成绩大于150的同学
select student_name , sum(math+chinese) from student group by student_name having sum(math+chinses)>150;

结果只有王五同学总分大于150.
在这里插入图片描述

3.4、联合查询

上面的查询都是在一张表中进行查询,但实际应用的过程中,经常是在几张表中查询数据,这就用到了联合查询。多张表查询就是对这些表取笛卡尔积,在筛选有用的数据。学习联合查询之前,需要先了解什么笛卡尔积。

在这里插入图片描述
总得来说,笛卡尔积就是罗列出两张表所有可能出现的数据。

准备数据:

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
sql复制代码drop table if exists classes;
drop table if exists student;
drop table if exists course;
drop table if exists score;

create table classes (id int primary key auto_increment, name varchar(20), `desc` varchar(100));

create table student (id int primary key auto_increment, sn varchar(20), name varchar(20), qq_mail varchar(20) ,
classes_id int);

create table course(id int primary key auto_increment, name varchar(20));

create table score(score decimal(3, 1), student_id int, course_id int);

insert into classes(name, `desc`) values
('计算机系2019级1班', '学习了计算机原理、C和Java语言、数据结构和算法'),
('中文系2019级3班','学习了中国传统文学'),
('自动化2019级5班','学习了机械自动化');

insert into student(sn, name, qq_mail, classes_id) values
('09982','黑旋风李逵','xuanfeng@qq.com',1),
('00835','菩提老祖',null,1),
('00391','白素贞',null,1),
('00031','许仙','xuxian@qq.com',1),
('00054','不想毕业',null,1),
('51234','好好说话','say@qq.com',2),
('83223','tellme',null,2),
('09527','老外学中文','foreigner@qq.com',2);

insert into course(name) values
('Java'),('中国传统文化'),('计算机原理'),('语文'),('高阶数学'),('英文');

insert into score(score, student_id, course_id) values
-- 黑旋风李逵
(70.5, 1, 1),(98.5, 1, 3),(33, 1, 5),(98, 1, 6),
-- 菩提老祖
(60, 2, 1),(59.5, 2, 5),
-- 白素贞
(33, 3, 1),(68, 3, 3),(99, 3, 5),
-- 许仙
(67, 4, 1),(23, 4, 3),(56, 4, 5),(72, 4, 6),
-- 不想毕业
(81, 5, 1),(37, 5, 5),
-- 好好说话
(56, 6, 2),(43, 6, 4),(79, 6, 6),
-- tellme
(80, 7, 2),(92, 7, 6);

3.4.1内连接

语法:
1、select 字段名 from 表1 +别名(可不写) inner join 表2+别名(可不写) on
连接条件 and 其他条件

示例:
查询白素贞同学的成绩
在这里插入图片描述
查询所有同学的总成绩:
当我们写一些比较复杂的SQL语句时,非常建议大家不要一次性写完。这样很容易写错,而SQL调试不了,所以在实际写的时候可以像下面这样,一行一行的写。每次写一个条件,这样就很清楚了。

1
2
3
4
5
sql复制代码select student.sn,student.name,student.qq_mail,
sum(score.score) from student
join score
on student.id=score.student_id
group by score.student_id;

在这里插入图片描述

2、select 字段名 from 表1 +别名(可以不写) ,表2+别名(可以不写) where 连接条件 and 其他条件

示例:
查询白素贞同学的成绩:
在这里插入图片描述

查询所有学生的qq邮箱和总成绩:
在这里插入图片描述

这两种方式查询的结果相同。

3.4.2 外连接

内连接和外连接的区别,在于对”空值”的区别。如果表里没有”空值”(不是单纯的NULL,而是泛指两张表的数据对不上),那内连接和外连接就没有区别了。

eg:
先删除成绩和学生表,新建一份在加上3个同学。

1
2
3
4
5
sql复制代码create table student(id int primary key auto_increment,name varchar(20));
create table score(studentId int ,score int );
insert into student values(null,'张三');
insert into student values(null,'李四');
insert into student values(null,'王五');

先来看一下当前表的数据

1
sql复制代码select * from student;

在这里插入图片描述
再给成绩表中插入三行数据,但是做一点点区别。

1
2
3
sql复制代码insert into score values(1,55);
insert into score values(2,85);
insert into score values(4,91);

这里故意将本该是3的王五的id改成4。
在这里插入图片描述
这时候发生了数据不对应的情况。王五同学在分数表中没有成绩,id为4的同学在学生表中,没有信息。
在这里插入图片描述
这时候如果我们查询两张表的结果会怎么样呢?
查询每位同学的成绩,姓名信息。

1
2
sql复制代码select student.name,score.score from student join 
score on student.id=score.studentId;

内连接最后的结果既没有王五同学的成绩和姓名信息,也没有id为4的同学的信息。
在这里插入图片描述

内连接相当于对上面两张表取了交集。
请添加图片描述

外连接相对于内连接来说,分为左外连接和右外连接。在写法上和内连接很相似,只需要在原来的语句中对于join前再加上left或者是right就表示内连接或者外连接了

左外连接:最终结果以join左侧的表为主,尽可能的显示左侧表的信息。

1
2
3
sql复制代码select student.name,score.score from student left
join
score on student.id=score.studentId;

在这里插入图片描述
右外连接:最终结果以join右侧的表为主,尽可能的显示右侧表的信息。

1
2
3
sql复制代码select student.name,score.score from student right
join
score on student.id=score.studentId;

在这里插入图片描述

3.4.3 自连接

自连接就是同一张表连接自身进行查询。

eg:查询语文成绩比英语成绩高的信息

这次查询和以往不同的是这是针对行和行的查询,之前的例如查询每位同学的成绩,姓名信息。这都是列与列之间的关系。

既然不好解决问题,不妨换个思路,那我就不进行行和行比较了,我想办法转换成列和列比较。

1、先看看它的笛卡尔积长什么样
需要注意的是如果你直接select * from score,score ;查询的话是会报错的。需要你使用as起一个别名。

1
sql复制代码select * from score as a,score as b;

最终结果有四百行,我这里放一部分。
在这里插入图片描述

接下来在进一步筛选有用的信息,挑选出同一个同学的信息

1
sql复制代码select * from score as a,score as b where a.student_id=b.student_id;

现在缩减成了62行
在这里插入图片描述
最后直接一步到位,但是需要先回头看看语文和英语的id,一个是4,另一个是6

1
2
sql复制代码insert into course(name) values
('Java'),('中国传统文化'),('计算机原理'),('语文'),('高阶数学'),('英文');

这样我们在通过课程id筛选

1
2
sql复制代码select * from score as a,score as b 
where a.student_id=b.student_id and a.course_id=4 and b.course_id=6;

最终结果就只剩下一了。
在这里插入图片描述
再来练习一下java比计算机原理分数高的同学的信息,我们这次直接到上图的那一步。

1
2
sql复制代码select * from score as a,score as b
where a.student_id=b.student_id and a.course_id=1 and b.course_id=3;

course_id为1的是java的分数,course_id为3的是计算机原理的分数
在这里插入图片描述
这样下来,一行不仅包含了java的列,还包含了计算机原理的列。通过自连接就完成了行转列的效果。

来看看最终结果,计算机原理成绩比JAVA高的信息

1
2
sql复制代码select * from score as a,score as b
where a.student_id=b.student_id and a.course_id=1 and b.course_id=3 and a.score<b.score;

在这里插入图片描述

3.5、子查询

子查询就是其他sql语句中的查询语句。所以也叫做嵌套查询。

3.5.1单行子查询

返回一行子查询的就是单行子查询。
eg:查询和“不想毕业”同学的同班同学。

还是一步步来,先查询不想毕业同学的班级id。

1
sql复制代码select classes_id from student where name='不想毕业';

在这里插入图片描述
不想毕业同学在一班。在通过一班这个信息找他的同学。

1
sql复制代码select name from student where classes_id=1;

在这里插入图片描述
我们用两次查询得到了想要的结果,那试试子查询怎么样。

1
2
sql复制代码select name from student where
classes_id=(select classes_id from student where name='不想毕业');

在这里插入图片描述
在上面的SQL语句中先执行第二个SQL语句作为前面查询name的条件,这就是子查询。

3.5.2多行子查询

多行子查询返回的是多条记录

eg:查询语文或者英语成绩的信息
1、先查询语文或者英语成绩的课程id

1
sql复制代码select id from course where name='语文' or name='英文';

在这里插入图片描述

2、再在课程表中,根据课程id找对应的信息

1
sql复制代码select * from score where course_id=4 or course_id=6;

在这里插入图片描述
再用多行子查询试试
多行子查询得到的是多条记录,外层查询需要使用in关键字。单行子查询只返回一条记录直接用=即可。

1
2
sql复制代码 select * from score where course_id in(
select id from course where name='语文' or name='英文');

在这里插入图片描述

3.6、合并查询

把多个select查询的结果合并成一个结果就是合并查询。有些像取并集。它包含两个关键字
union : 针对重复的进行去重
union all 不会针对重复的去重
union这有一个前提是两边的查询得到的结果的列是一致的,包括数量和类型。

eg:查询id小于3,或者名字为“英文”的课程

1
sql复制代码select * from course where id<3 or name ='英文';

在这里插入图片描述

使用union查询

1
sql复制代码select * from course where id<3 union select * from course where name='英文';

效果是一样的。
在这里插入图片描述

四、结尾

上面就是这次的所有内容了,祝大家工作顺利,学习顺利。冲冲冲
在这里插入图片描述

本文转载自: 掘金

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

12306抢票算法居然是redis实现的 导读 bitmap

发表于 2021-09-27

导读

相信大家应该都有抢火车票的经验,每年年底,这都是一场盛宴。然而你有没有想过抢火车票这个算法是怎么实现的呢? 应该没有吧,咱们今天就来一一探讨。其实并没有你想的那么难

bitmap与位运算

redis的bitmap基本使用咱们之前已经介绍过了,如果不是很熟悉的朋友可以看看这里 redis bitmap的基本操作和应用

今天在这里咱们主要是先回顾一下位运算

12306抢票算法详解

我们以北京到西安这趟高铁为例,比如我的路线就是从北京到西安,车上如果只剩最后一张票了,那么如果有其他人,在北京到西安这条路线之间买任何一站,那么我都是买不了票的,换句话说,对于单个座位来说,必须是起点到目的地之间的所有站,都没有人买的话,那么才能被算是有票状态。

所以我们可以尝试用bitmap结合上位操作来实现这种场景,以上述北京到西安为例,我们把问题简化

  • 比如一个火车上只有4个座位
  • 北京到西安,一共是4站,其实是三个区间的,分别为北京->石家庄,石家庄->郑州,郑州->西安

首先我们给每个区间构建一个空位图(0为有票,1为无票)

接下来,比如有人买了一张从北京到西安的票

买票这个动作,比如被分配到的座位是编号为1的座位,那么我们直接把北京到西安的所有站,1号座位全部设置为1,如下图

接下来又有人买了一张从石家庄到西安的票

比如这次分配的是座位2,那么我们把石家庄到西安的所有票全部设置为1就行了,如下图

如何知道还剩几张票?

其实解决这个问题很简单,我们直接把上述位图做一个或操作就可以了

或操作结果有几个0,则说明还剩几张票。

总结

其实解决这个问题主要在于位图的构建,因为火车票对于某一个座位来说,只要起点到终点中间某一个区间被占用了(置为1),那么整个座位都是无效的这个特点,很容易想到用或操作的结果来判断买票结果,我们这里只用了4位是为了方便说明问题,实际中应该是火车上有多少座位,位图的长度就应该是多少。 好了,关于抢票算法我们就介绍到这里,你有没有Get到呢?或者你有没有更好的实现方法呢?

唠叨一句

大家好,我是小饭,一枚后端工程师。如果觉得文章对你有一点点帮助,欢迎分享给你的朋友,也给小饭点个赞,下面是我的公众号,打开微信搜索“程序员小饭”就可以看到,感兴趣可以关注,这是小饭坚持下去的动力,谢谢大家,我们下次见!

本文转载自: 掘金

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

Django项目连接MongoDB的三种方法

发表于 2021-09-27

有三种方法连接Django到MongoDB数据库

1.PyMongo:PyMongo 是 MongoDB 与 Django 交互的标准驱动程序。这是在 Python 中使用 MongoDB 的官方和首选方式。 PyMongo 提供了执行所有数据库操作的功能,例如搜索、删除、更新和插入。由于 PyMongo 可与 PyPI 一起使用,您可以使用 pip 命令快速安装它。

2.MongoEngine: MongoEngine 是一个 Python 对象文档映射器。它类似于关系数据库中的 Object-Relational-Mapper。 MongoEngine 具有易于学习和使用的声明式 API。

3.Djongo:如果您正在使用 SQL 这样的关系型数据库并希望迁移到 MongoDB,那么您可以使用 Djongo。在不更改 Django ORM 的情况下,Djongo 将所有 SQL 查询转换为 MongoDB 语法查询。

哪种方法连接到 Django MongoDB 更好呢?

Django 和 MongoDB 设置

为了让集成工作,你应该有一个 Django 和 MongoDB 设置。如果你的机器上有 Python,你可以使用 pip 安装 Django。如果您希望在特定环境而不是整个系统中安装 Django,您可以创建一个虚拟环境。根据您的 Python 版本使用 pip/pip3:

安装:

Windows:

1
复制代码pip install virtualenvwrapper-win

Mac OS / Linux:

1
复制代码pip install virtualenvwrapper

创建:

Windows:

1
复制代码mkvirtualenv MyProjectEnvt

Mac OS / Linux:

1
复制代码virtualenv MyProjectEnvt

激活:

Mac OS / Linux

1
bash复制代码source MyProjectEnvt/bin/activate

Windows:

1
复制代码workon MyProjectEnvt

要停用虚拟环境,您只需键入命令deactivate。 现在使用 pip install Django 安装 Django。 要启动 Django 项目,请转到要启动项目的文件夹并使用以下命令:

1
xml复制代码django-admin startproject <project_name>.

例如:

1
2
makefile复制代码C:\Users\myuser\project_files>django-admin startproject MyFirstDjangoProj
C:\Users\myuser\project_files>cd MyFirstDjangoProj

要创建应用程序,请使用以下命令:

1
复制代码python manage.py startapp myfirstapp

如果您使用的是 Python 版本 >= 3.0,请将命令中的 python 替换为 python3。 在应用程序内部,我们可以有许多模型将映射到 MongoDB 中的集合和文档。 启动项目后,所有文件都将在项目文件夹中可用。使用 python manage.py runserver 命令启动服务器。 您的 Django 设置现已完成。 如果您还没有设置 MongoDB,请使用 MongoDB Atlas 来充分利用云托管。 Atlas 与所有主要的云提供商无缝合作。

使用 PyMongo 连接 Django 和 MongoDB

PyMongo 对于将 JSON 数据写入 MongoDB 非常有效,并且允许在 Python 代码本身中使用 MongoDB 查询。我们可以使用 PyMongo 像语法一样检索字典中的数据。 使用 pip/pip3 命令轻松安装 PyMongo:

1
css复制代码pip install pymongo[snappy,gssapi,srv,tls]

如果您使用的是虚拟环境,则必须在 ..\venv\Lib\site-packages 文件夹中安装 pymongo。 此外,安装 dnspython 以使用 mongodb+srv:// URI 与命令:

1
复制代码pip install dnspython

使用 PyMongo,我们可以通过为连接实例指定正确的数据库名称来同时运行多个数据库。

让我们创建一个示例 pymongo 会话。为此,有两种方法:

1.我们可以在 utils 文件中创建一个客户端,任何想要与 MongoDB 交互的视图都可以使用它。在您的项目文件夹(与 manage.py 相同的位置)中创建一个 utils.py 文件并实例化客户端:

1
2
3
4
5
6
7
8
9
10
ini复制代码from pymongo import MongoClient
def get_db_handle(db_name, host, port, username, password):

client = MongoClient(host=host,
port=int(port),
username=username,
password=password
)
db_handle = client['db_name']
return db_handle, client

然后可以在 ./myfirstapp/view.py 中使用此方法。

2.获取连接的另一种方法是使用 connection_string:

1
2
3
ini复制代码from pymongo import MongoClient
client = pymongo.MongoClient('connection_string')
db = client['db_name']

在

1
2
ini复制代码connection_string = mongodb+srv://<username>:<password>@<atlas cluster>
/<myFirstDatabase>?retryWrites=true&w=majority

例如:

1
2
3
ini复制代码makemyrx_db = client['sample_medicines']
#collection object
medicines_collection = makemyrx_db['medicinedetails']

您可能已经在其他代码示例或教程中看到了 Connection 类。连接已被弃用,所以不要使用它。

如果您使用的是默认端口和主机,只需调用 MongoClient()。要连接到 localhost,我们可以明确指定主机和端口为:

MongoClient(‘localhost’, 27017)

或者

使用 URL 格式MongoClient(‘mongodb://localhost: 27017/’)

由于我们已经在此处创建了客户端,因此我们需要在 settings.py 文件中注释 DATABASES 部分。使用三重引号注释相同的内容。

使用 MongoEngine 连接 Django 和 MongoDB

MongoEngine 是 PyMongo 之上的 ORM 层。因此,您的系统仍然需要 PyMongo (>=3.4) 才能使用 MongoEngine。

使用 MongoEngine 连接 Django 和 MongoDB,您可以使用 ListField 和 DictField 等字段来处理巨大的非结构化 JSON 数据。

首先,使用以下命令安装 MongoEngine:

1
复制代码pip install mongoengine

正如我们在上一节中看到的,在使用 PyMongo 时,我们必须在 settings.py 中注释 DATABASES 部分。然后,要使用 MongoEngine,请添加以下内容:

1
2
ini复制代码import mongoengine
mongoengine.connect(db=db_name, host=hostname, username=username, password=pwd)

使用 MongoEngine,我们必须在 Django 应用程序的 models.py 文件中定义一个模式。 MongoDB 是无模式的。该架构仅在应用程序级别执行,从而使未来的任何更改都变得快速而轻松。 MongoEngine 类似于 Django 的默认 ORM,但在 model.py 中有以下变化:

不同之处在于我们使用的是模型,当使用 MongoEngine 时,模型被文档和字段替换。 还有许多其他工具可以与 PyMongo 一起使用。

使用 Djongo 连接 Django 和 MongoDB

Djongo 是对 PyMongo 的改进,因为开发人员无需编写冗长的查询。它将 Python 对象映射到 MongoDB 文档,即对象文档映射 (ODM)。 Djongo 确保只有干净的数据才能进入数据库。通过使用 Djongo 执行完整性检查、应用验证等,无需修改现有的 Django ORM。

安装Djongo:

1
复制代码pip install djongo

现在,转到您的项目文件夹(例如 MyFirstDjangoProj),并打开 settings.py 文件。您可以在 Textpad、Python IDE 或任何编辑器上对其进行编辑。搜索 DATABASES,并将设置更改为指向 MongoDB。 ENGINE 将是 djongo,数据库名称 (NAME) 将是您的 MongoDB 数据库名称。

1
2
3
4
5
6
arduino复制代码DATABASES = {
'default': {
'ENGINE': 'djongo',
'NAME': 'db-name',
}
}

如果您的数据库不在本地主机上或受到保护,您还应该填写 CLIENT 信息,如 HOST、USERNAME、PASSWORD 等。

1
2
3
4
5
6
7
8
9
10
python复制代码DATABASES = {
'default': {
'ENGINE': 'djongo',
'NAME': 'your-db-name',
'ENFORCE_SCHEMA': False,
'CLIENT': {
'host': 'mongodb+srv://<username>:<password>@<atlas cluster>/<myFirstDatabase>?retryWrites=true&w=majority'
}
}
}

确保在 settings.py 的 INSTALLED_APPS 设置中添加了应用名称:

1
2
3
4
5
6
7
8
9
ini复制代码INSTALLED_APPS = [
'myfirstapp',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

现在我们有了 Django 项目(和应用程序),您可以使用以下命令在 MongoDB 中创建集合:

1
2
3
4
xml复制代码python manage.py makemigrations <app-name>


python manage.py migrate

将创建集合(应用程序中的 Django 模型——请注意,我们谈论的是应用程序而不是项目)。您可以通过打开 Django Admin 来检查相同的内容。 您可以使用管理 GUI 或手动将数据插入到集合中。 要使用管理控制台,请打开浏览器并转到 http://127.0.0.1:8000/admin(或 localhost)。您应该创建一个超级用户以进入管理控制台。如果您的应用程序中没有任何模型,请按照有关如何创建和注册模型的 Django 教程进行操作。 如果您希望 Djongo 免迁移,请在您的数据库配置中设置 ENFORCE_SCHEMA: False。使用此设置,集合是动态创建的,Djongo 不会将 SQL 语句转换为 MongoDB 命令。

Django 和 MongoDB 教程

在本快速教程中,我们将演示如何使用 PyMongo 进行简单的 CRUD 操作。为此,让我们创建一个 PyMongo 会话:

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
python复制代码import pymongo
#connect_string = 'mongodb+srv://<username>:<password>@<atlas cluster>/<myFirstDatabase>?retryWrites=true&w=majority'

from django.conf import settings
my_client = pymongo.MongoClient(connect_string)

# First define the database name
dbname = my_client['sample_medicines']

# Now get/create collection name (remember that you will see the database in your mongodb cluster only after you create a collection
collection_name = dbname["medicinedetails"]

#let's create two documents
medicine_1 = {
"medicine_id": "RR000123456",
"common_name" : "Paracetamol",
"scientific_name" : "",
"available" : "Y",
"category": "fever"
}
medicine_2 = {
"medicine_id": "RR000342522",
"common_name" : "Metformin",
"scientific_name" : "",
"available" : "Y",
"category" : "type 2 diabetes"
}
# Insert the documents
collection_name.insert_many([medicine_1,medicine_2])
# Check the count
count = collection_name.count()
print(count)

# Read the documents
med_details = collection_name.find({})
# Print on the terminal
for r in med_details:
print(r["common_name"])
# Update one document
update_data = collection_name.update_one({'medicine_id':'RR000123456'}, {'$set':{'common_name':'Paracetamol 500'}})

# Delete one document
delete_data = collection_name.delete_one({'medicine_id':'RR000123456'})

接下来,您可以连接到您的 MongoDB Atlas 集群并进行验证。

下一步 现在我们知道了连接 Django 和 MongoDB 的不同方式,我们必须为我们的项目选择正确的方式。这些方法各有优缺点。 例如,如果您是从头开始,MongoEngine 是一个不错的选择,因为它可以轻松处理大量非结构化数据。如果您要在应用程序中编写大量复杂的查询,则应该使用 PyMongo。 如果您有一个 Django 项目需要从另一个数据库迁移到 MongoDB,那么 Djongo 更合适,因为这需要最少的代码更改。

本文转载自: 掘金

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

1…516517518…956

开发者博客

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