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

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


  • 首页

  • 归档

  • 搜索

微服务SpringCloud项目(六):整合oauth并使用

发表于 2021-10-11

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

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

📖前言

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
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
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--freemarker,页面渲染引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<!-- SpringBoot 监控客户端 -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>

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

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 引入数据库密码加密 -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>${jasypt.version}</version>
</dependency>

<!-- 引入mysql链接依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector}</version>
<scope>provided</scope>
</dependency>

<!-- 引入Druid依赖
java.sql.SQLFeatureNotSupportedException
http://www.vmfor.com/p/101494868463.html-->
<dependency>
<!--自动配置-->
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>${common-pool.version}</version>
</dependency>

<!--<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-micro-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>-->

<!-- 引入 MybatisPlus 依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>

<!-- 引入多数据源依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${mybatis-plus-dynamic.version}</version>
</dependency>

</dependencies>

1. 启动类添加


1
2
3
4
java复制代码@EnableFeignClients
//对外开启暴露获取token的API接口
@EnableResourceServer
@EnableDiscoveryClient

2. 创建一个授权的配置文件 AuthorizationServerConfig.java


根据官方指示我们需要创建一个配置类去实现 AuthorizationServerConfigurer 所以我们,创建一个类去继承它的实现类AuthorizationServerConfigurerAdapter,具体代码如下:

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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
java复制代码package com.cyj.dream.auth.config;

import com.cyj.dream.auth.entity.SysUser;
import com.cyj.dream.auth.persistence.service.impl.CustomUserServiceImpl;
import com.cyj.dream.core.constant.Constant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenGranter;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
import org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter;
import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

import javax.sql.DataSource;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
* @Description: 授权的配置文件
* @BelongsProject: DreamChardonnay
* @BelongsPackage: com.cyj.dream.auth.config
* @Author: ChenYongJia
* @CreateTime: 2021-09-30
* @Email: chen87647213@163.com
* @Version: 1.0
*/
// 授权认证服务中心配置
@Configuration // 配置类
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

@Autowired
private BCryptPasswordEncoder passwordEncoder;

@Autowired
private RedisConnectionFactory redisConnectionFactory;

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private DataSource dataSource;

@Autowired
private CustomUserServiceImpl userDetailsService;

/**
* accessToken 有效期 2小时
*/
private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 7200 * 12 * 7;

/**
* accessToken 有效期 2小时
*/
private static final int REFRESH_TOKEN_VALIDITY_SECONDS = 7200 * 12 * 7;

/**
* 定制化处理
* 配置tokenStore的存储方式是redis存储
* <p>
* 这里存储在数据库,大家可以结合自己的业务场景考虑将access_token存入数据库还是redis
*
* @return
*/
@Bean
public TokenStore redisTokenStore() {
RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
// redis key 前缀--DreamCloud
tokenStore.setPrefix(Constant.tokenPrefix + "_");
return tokenStore;
}

/**
* 配置客户端的管理是jdbc
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("dream").secret(passwordEncoder.encode("c83fb51ff6e807e8805c6dd9d5707365"))
// 授权码授权模式下的回调地址
.redirectUris("http://www.baidu.com")
.authorizedGrantTypes("authorization_code", "password", "refresh_token").scopes("all")
.accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
// 授权类型
.refreshTokenValiditySeconds(REFRESH_TOKEN_VALIDITY_SECONDS);
//clients.withClientDetails(new JdbcClientDetailsService(dataSource));
}

/**
* 认证服务器Endpoints配置
*
* @return
* @author ChenYongJia
* @date 2021/9/30
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer()));
endpoints.authenticationManager(authenticationManager).allowedTokenEndpointRequestMethods(HttpMethod.GET,
HttpMethod.POST, HttpMethod.PUT,
HttpMethod.DELETE)
.tokenEnhancer(tokenEnhancerChain)
//配置tokenStore管理、配置客户端详情--使用redis
.tokenStore(redisTokenStore()).userDetailsService(userDetailsService)
//配置授权模式
.tokenGranter(tokenGranter(endpoints));
//配置tokenServices的参数 +
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
//配置accessToken过期时间
defaultTokenServices.setAccessTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(2));
//配置refreshToken的过期时间
defaultTokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30));
//设置支持刷新token
defaultTokenServices.setReuseRefreshToken(true);
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setTokenStore(endpoints.getTokenStore());
defaultTokenServices.setClientDetailsService(endpoints.getClientDetailsService());
defaultTokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
endpoints.tokenServices(defaultTokenServices);
}

/**
* 认证服务器相关接口权限管理
*
* @return
* @author ChenYongJia
* @date 2021/9/30
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//配置允许表单访问
security.allowFormAuthenticationForClients()
.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("permitAll()");
}

/**
* 配置授权模式也可以添加自定义模式(不写也有默认的)
* 具体可查看AuthorizationServerEndpointsConfigurer中的getDefaultTokenGranters方法
* 以后添加一个手机验证码的功能
*
* @param endpoints
* @return
*/
private TokenGranter tokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
List<TokenGranter> list = new ArrayList<>();
//增加刷新token
list.add(new RefreshTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
//授权码模式
list.add(new AuthorizationCodeTokenGranter(endpoints.getTokenServices(), endpoints.getAuthorizationCodeServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
//客户端凭证模式
list.add(new ClientCredentialsTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
//密码模式
list.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
//隐藏式
list.add(new ImplicitTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
return new CompositeTokenGranter(list);
}

/**
* 创建一个token增强方法,增加一些我们自己想要返回的附加信息
*
* @return
*/
@Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
final Map<String, Object> additionalInfo = new HashMap<>(2);
additionalInfo.put("license", "SunnyChen-DreamChardonnay");
SysUser user = (SysUser) authentication.getUserAuthentication().getPrincipal();
if (user != null) {
additionalInfo.put("userId", user.getSysUserId());
additionalInfo.put("userPhone", user.getSysUserPhone());
additionalInfo.put("userDeptId", user.getSysUserInfoDepartmentId());
}
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
};
}

/**
* 用来做验证
*
* @return
*/
@Bean
AuthenticationManager authenticationManager() {
AuthenticationManager authenticationManager = new AuthenticationManager() {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return daoAuhthenticationProvider().authenticate(authentication);
}
};
return authenticationManager;
}

/**
* 用来做验证
*
* @return
*/
@Bean
public AuthenticationProvider daoAuhthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
return daoAuthenticationProvider;
}

}

3. 由于授权服务器本身也是资源服务器,所以也创建一个资源配置,代码如下


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

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

/**
* @Description: 资源配置器
* @BelongsProject: DreamChardonnay
* @BelongsPackage: com.cyj.dream.auth.config
* @Author: ChenYongJia
* @CreateTime: 2021-09-30
* @Email: chen87647213@163.com
* @Version: 1.0
*/
@Configuration
// 启用资源服务
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

/**
* 配置资源接口安全,http.authorizeRequests()针对的所有url,但是由于登录页面url包含在其中,这里配置会进行token校验,校验不通过返回错误json,
* 而授权码模式获取code时需要重定向登录页面,重定向过程并不能携带token,所有不能用http.authorizeRequests(),
* 而是用requestMatchers().antMatchers(""),这里配置的是需要资源接口拦截的url数组
*
* @param http
* @return void
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http //配置需要保护的资源接口
.requestMatchers().
antMatchers("/user", "/test/need_token", "/logout", "/remove", "/update", "/test/need_admin", "/test/scope")
.and().authorizeRequests().anyRequest().authenticated();
}

}

4. 创建 webSecurity 配置


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tex复制代码/*
* OK ,关于这个配置我要多说两句:
*
* 1.首先当我们要自定义Spring Security的时候我们需要继承自WebSecurityConfigurerAdapter来完成,相关配置重写对应
* 方法即可。 2.我们在这里注册CustomUserService的Bean,然后通过重写configure方法添加我们自定义的认证方式。
* 3.在configure(HttpSecurity http)方法中,我们设置了登录页面,而且登录页面任何人都可以访问,然后设置了登录失败地址,也设置了注销请求,
* 注销请求也是任何人都可以访问的。
* 4.permitAll表示该请求任何人都可以访问,.anyRequest().authenticated(),表示其他的请求都必须要有权限认证。
* 5.这里我们可以通过匹配器来匹配路径,比如antMatchers方法,假设我要管理员才可以访问admin文件夹下的内容,我可以这样来写:.
* antMatchers("/admin/**").hasRole("ROLE_ADMIN"),也可以设置admin文件夹下的文件可以有多个角色来访问,
* 写法如下:.antMatchers("/admin/**").hasAnyRole("ROLE_ADMIN","ROLE_USER")
* 6.可以通过hasIpAddress来指定某一个ip可以访问该资源,假设只允许访问ip为210.210.210.210的请求获取admin下的资源,
* 写法如下.antMatchers("/admin/**").hasIpAddress("210.210.210.210")
* 7.更多的权限控制方式参看源码:
*/

代码如下:

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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
java复制代码package com.cyj.dream.auth.config;

import com.cyj.dream.auth.handler.*;
import com.cyj.dream.auth.persistence.service.impl.CustomUserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
* @Description: WebSecurity配置
* @BelongsProject: DreamChardonnay
* @BelongsPackage: com.cyj.dream.auth.config
* @Author: ChenYongJia
* @CreateTime: 2021-09-30
* @Email: chen87647213@163.com
* @Version: 1.0
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

@Autowired
private CustomUserServiceImpl userDetailsService;

/**
* 自定义登录成功处理器
*/
@Autowired
private MyAuthenticationSuccessHandler userLoginSuccessHandler;

/**
* 自定义登录失败处理器
*/
@Autowired
private MyAuthenticationFailureHandler userLoginFailureHandler;

/**
* 自定义注销成功处理器
*/
@Autowired
private UserLogoutSuccessHandler userLogoutSuccessHandler;

/**
* 自定义暂无权限处理器
*/
@Autowired
private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;

/**
* 自定义未登录的处理器
*/
@Autowired
private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;

@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Override
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserServiceImpl();
}

/**
* 配置登录验证逻辑
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}

/**
* 认证管理--需要配置这个支持password模式
* <p>
* support password grant type
*
* @return 认证管理对象
* @throws Exception 认证异常信息
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

/**
* 安全请求配置,这里配置的是Security的部分,请求全部通过,安全拦截在资源服务器配置
* <p>
* http安全配置
*
* @param http http安全对象
* @throws Exception http安全异常信息
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置允许的请求以及跨域问题
http.authorizeRequests()
.antMatchers("/webjars/**", "/js/**", "/css/**", "/images/*", "/fonts/**", "/**/*.png", "/**/*.jpg",
"/static/**")
// .anyRequest()
.permitAll()
// 所有请求都可以访问,本地开发打开下面一行的注释
//.antMatchers("/**").permitAll()
//不进行权限验证的请求或资源(从配置文件中读取),本地开发可以,将本行注释掉
.antMatchers("/login", "/oauth/**")
.permitAll()
.antMatchers("/**")
.fullyAuthenticated()
.and()
//配置未登录自定义处理类
.httpBasic()
.authenticationEntryPoint(userAuthenticationEntryPointHandler)
.and()
.csrf()
.disable()
// 配置登录地址
.formLogin()
// 本地
//.loginPage("/login/userLogin")
//.loginProcessingUrl("/login/userLogin")
.loginPage("/login")
//配置登录成功自定义处理类
.successHandler(userLoginSuccessHandler)
//配置登录失败自定义处理类
.failureHandler(userLoginFailureHandler)
.permitAll()
.and()
//配置登出地址
.logout()
// /userInfo/loginOutByToken
.logoutUrl("/login/loginOut")
//配置用户登出自定义处理类
.logoutSuccessHandler(userLogoutSuccessHandler)
.and()
//配置没有权限自定义处理类
.exceptionHandling()
.accessDeniedHandler(userAuthAccessDeniedHandler)
.and()
// 开启跨域
.cors()
.and()
// 取消跨站请求伪造防护
.csrf().disable();
/*.cors().and()
.addFilterAt(ignoreLogoutFilter, LogoutFilter.class);*/
// 为了可以使用 iframe 内嵌页面加载
http.headers().frameOptions().sameOrigin();
// 基于Token不需要session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 禁用缓存
http.headers().cacheControl();
}

@Override
public void configure(WebSecurity web) throws Exception {
// 设置忽略资源
web.ignoring().antMatchers(
"/error",
"/v2/api-docs/**",
"/favicon.ico",
"/css/**",
"/js/**",
"/images/*",
"/fonts/**",
"/**/*.png",
"/**/*.jpg")
// 不拦截 swagger2 所进行的配置
.antMatchers("/templates/**")
.antMatchers("/static/**")
.antMatchers("/webjars/**")
.antMatchers("/swagger-ui.html/**")
.antMatchers("/v2/**")
.antMatchers("/doc.html")
.antMatchers("/api-docs-ext/**")
.antMatchers("/swagger-resources/**");
;
}

}

5. 创建一个 controller 进行访问测试


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
java复制代码package com.cyj.dream.auth.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cyj.dream.auth.entity.SysUser;
import com.cyj.dream.auth.persistence.service.ITbSysUserService;
import com.cyj.dream.core.aspect.annotation.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

/**
* @Description: 用户控制器
* @BelongsProject: DreamChardonnay
* @BelongsPackage: com.cyj.dream.auth.controller
* @Author: ChenYongJia
* @CreateTime: 2021-09-30 10:40
* @Email: chen87647213@163.com
* @Version: 1.0
*/
@Slf4j
@ResponseResult
@RestController
@RequestMapping(value = "/authUser", name = "用户控制器")
@Api(value = "authUser", tags = "用户控制器")
public class UserController {

@Autowired
private ITbSysUserService iTbSysUserService;

@ApiOperation("通过名称获取用户信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "userName", value = "用户名称", dataType = "String", required = true)
})
@RequestMapping(value = "getByName", method = RequestMethod.GET, name = "通过名称获取用户信息")
public SysUser getByName(@RequestParam(value = "userName") String userName){
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getUsername, userName);
return iTbSysUserService.getOne(wrapper);
}

@ApiOperation("获取授权的用户信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "principal", value = "当前用户", dataType = "Principal", required = true)
})
@RequestMapping(value = "current", method = RequestMethod.GET, name = "获取授权的用户信息")
public Principal user(Principal principal){
// 授权信息
return principal;
}

}

带 token 的请求

image-20200910202739906

不带 token 的请求

image-20200910202804042

带 token,但是没有ROLE_ADMIN权限

image-20200910203005094

6. 最后关于Fegin 调用服务 Token 丢失的问题


在微服务中我们经常会使用RestTemplate或Fegin来进行服务之间的调用,在这里就会出现一个问题,我们去调用别的服务的时候就会出现token丢失的情况,导致我们没有权限去访问。

所以我们需要加上一些拦截器将我们的 token 带走,针对fegin的配置代码如下:

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
java复制代码public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
// 设置请求头
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = request.getHeader(name);
requestTemplate.header(name, value);
}
}

// 设置请求体,这里主要是为了传递 access_token
Enumeration<String> parameterNames = request.getParameterNames();
StringBuilder body = new StringBuilder();
if (parameterNames != null) {
while (parameterNames.hasMoreElements()) {
String name = parameterNames.nextElement();
String value = request.getParameter(name);

// 将 Token 加入请求头
if ("access_token".equals(name)) {
requestTemplate.header("authorization", "Bearer " + value);
}

// 其它参数加入请求体
else {
body.append(name).append("=").append(value).append("&");
}
}
}

// 设置请求体
if (body.length() > 0) {
// 去掉最后一位 & 符号
body.deleteCharAt(body.length() - 1);
requestTemplate.body(body.toString());
}
}
}

然后将这个拦截器加入到我们 fegin 请求拦截器中:

1
2
3
4
5
6
7
java复制代码@Configuration
public class FeignRequestConfiguration {
@Bean
public RequestInterceptor requestInterceptor() {
return new FeignRequestInterceptor();
}
}

7. Spring Security 和 Shiro


相同点:

1:认证功能

2:授权功能

3:加密功能

4:会话管理

5:缓存支持

6:rememberMe功能…….

不同点:

优点:

1:Spring Security基于Spring开发,项目中如果使用Spring作为基础,配合Spring Security做权限更加方便,而Shiro需要和Spring进行整合开发

2:Spring Security功能比Shiro更加丰富些,例如安全防护

3:Spring Security社区资源比Shiro丰富

缺点:

1:Shiro的配置和使用比较简单,Spring Security上手复杂

2:Shiro依赖性低,不需要任何框架和容器,可以独立运行,而Spring Security依赖于Spring容器

8. 数据库表

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
sql复制代码-- used in tests that use HSQL
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);

create table oauth_client_token (
token_id VARCHAR(256),
token LONGVARBINARY,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256)
);

create table oauth_access_token (
token_id VARCHAR(256),
token LONGVARBINARY,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256),
authentication LONGVARBINARY,
refresh_token VARCHAR(256)
);

create table oauth_refresh_token (
token_id VARCHAR(256),
token LONGVARBINARY,
authentication LONGVARBINARY
);

create table oauth_code (
code VARCHAR(256), authentication LONGVARBINARY
);

create table oauth_approvals (
userId VARCHAR(256),
clientId VARCHAR(256),
scope VARCHAR(256),
status VARCHAR(10),
expiresAt TIMESTAMP,
lastModifiedAt TIMESTAMP
);

-- customized oauth_client_details table
create table ClientDetails (
appId VARCHAR(256) PRIMARY KEY,
resourceIds VARCHAR(256),
appSecret VARCHAR(256),
scope VARCHAR(256),
grantTypes VARCHAR(256),
redirectUrl VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additionalInformation VARCHAR(4096),
autoApproveScopes VARCHAR(256)
);

最后感谢大家耐心观看完毕,具体请求和一些结果返回可以参见上一章内容,留个点赞收藏是您对我最大的鼓励!


🎉总结:

  • 更多参考精彩博文请看这里:《陈永佳的博客》
  • 整合 auth 这里有一些类我没有放置,大家可以先阅读文章,后续我会把代码传到 git 上方便大家参考学习~
  • 喜欢博主的小伙伴可以加个关注、点个赞哦,持续更新嘿嘿!

本文转载自: 掘金

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

Pandas高级教程之 时间处理 简介 时间分类 Times

发表于 2021-10-11

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

「欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章」

简介

时间应该是在数据处理中经常会用到的一种数据类型,除了Numpy中datetime64 和 timedelta64 这两种数据类型之外,pandas 还整合了其他python库比如 scikits.timeseries 中的功能。

时间分类

pandas中有四种时间类型:

  1. Date times : 日期和时间,可以带时区。和标准库中的 datetime.datetime 类似。
  2. Time deltas: 绝对持续时间,和 标准库中的 datetime.timedelta 类似。
  3. Time spans: 由时间点及其关联的频率定义的时间跨度。
  4. Date offsets:基于日历计算的时间 和 dateutil.relativedelta.relativedelta 类似。

我们用一张表来表示:

类型 标量class 数组class pandas数据类型 主要创建方法
Date times Timestamp DatetimeIndex datetime64[ns] or datetime64[ns, tz] to_datetime or date_range
Time deltas Timedelta TimedeltaIndex timedelta64[ns] to_timedelta or timedelta_range
Time spans Period PeriodIndex period[freq] Period or period_range
Date offsets DateOffset None None DateOffset

看一个使用的例子:

1
2
3
4
5
6
css复制代码In [19]: pd.Series(range(3), index=pd.date_range("2000", freq="D", periods=3))
Out[19]:
2000-01-01 0
2000-01-02 1
2000-01-03 2
Freq: D, dtype: int64

看一下上面数据类型的空值:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码In [24]: pd.Timestamp(pd.NaT)
Out[24]: NaT

In [25]: pd.Timedelta(pd.NaT)
Out[25]: NaT

In [26]: pd.Period(pd.NaT)
Out[26]: NaT

# Equality acts as np.nan would
In [27]: pd.NaT == pd.NaT
Out[27]: False

Timestamp

Timestamp 是最基础的时间类型,我们可以这样创建:

1
2
3
4
5
6
7
8
css复制代码In [28]: pd.Timestamp(datetime.datetime(2012, 5, 1))
Out[28]: Timestamp('2012-05-01 00:00:00')

In [29]: pd.Timestamp("2012-05-01")
Out[29]: Timestamp('2012-05-01 00:00:00')

In [30]: pd.Timestamp(2012, 5, 1)
Out[30]: Timestamp('2012-05-01 00:00:00')

DatetimeIndex

Timestamp 作为index会自动被转换为DatetimeIndex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码In [33]: dates = [
....: pd.Timestamp("2012-05-01"),
....: pd.Timestamp("2012-05-02"),
....: pd.Timestamp("2012-05-03"),
....: ]
....:

In [34]: ts = pd.Series(np.random.randn(3), dates)

In [35]: type(ts.index)
Out[35]: pandas.core.indexes.datetimes.DatetimeIndex

In [36]: ts.index
Out[36]: DatetimeIndex(['2012-05-01', '2012-05-02', '2012-05-03'], dtype='datetime64[ns]', freq=None)

In [37]: ts
Out[37]:
2012-05-01 0.469112
2012-05-02 -0.282863
2012-05-03 -1.509059
dtype: float64

date_range 和 bdate_range

还可以使用 date_range 来创建DatetimeIndex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
css复制代码In [74]: start = datetime.datetime(2011, 1, 1)

In [75]: end = datetime.datetime(2012, 1, 1)

In [76]: index = pd.date_range(start, end)

In [77]: index
Out[77]:
DatetimeIndex(['2011-01-01', '2011-01-02', '2011-01-03', '2011-01-04',
'2011-01-05', '2011-01-06', '2011-01-07', '2011-01-08',
'2011-01-09', '2011-01-10',
...
'2011-12-23', '2011-12-24', '2011-12-25', '2011-12-26',
'2011-12-27', '2011-12-28', '2011-12-29', '2011-12-30',
'2011-12-31', '2012-01-01'],
dtype='datetime64[ns]', length=366, freq='D')

date_range 是日历范围,bdate_range 是工作日范围:

1
2
3
4
5
6
7
8
9
10
11
12
css复制代码In [78]: index = pd.bdate_range(start, end)

In [79]: index
Out[79]:
DatetimeIndex(['2011-01-03', '2011-01-04', '2011-01-05', '2011-01-06',
'2011-01-07', '2011-01-10', '2011-01-11', '2011-01-12',
'2011-01-13', '2011-01-14',
...
'2011-12-19', '2011-12-20', '2011-12-21', '2011-12-22',
'2011-12-23', '2011-12-26', '2011-12-27', '2011-12-28',
'2011-12-29', '2011-12-30'],
dtype='datetime64[ns]', length=260, freq='B')

两个方法都可以带上 start, end, 和 periods 参数。

1
2
3
ini复制代码In [84]: pd.bdate_range(end=end, periods=20)
In [83]: pd.date_range(start, end, freq="W")
In [86]: pd.date_range("2018-01-01", "2018-01-05", periods=5)

origin

使用 origin参数,可以修改 DatetimeIndex 的起点:

1
2
ini复制代码In [67]: pd.to_datetime([1, 2, 3], unit="D", origin=pd.Timestamp("1960-01-01"))
Out[67]: DatetimeIndex(['1960-01-02', '1960-01-03', '1960-01-04'], dtype='datetime64[ns]', freq=None)

默认情况下 origin='unix', 也就是起点是 1970-01-01 00:00:00.

1
2
ini复制代码In [68]: pd.to_datetime([1, 2, 3], unit="D")
Out[68]: DatetimeIndex(['1970-01-02', '1970-01-03', '1970-01-04'], dtype='datetime64[ns]', freq=None)

格式化

使用format参数可以对时间进行格式化:

1
2
3
4
5
perl复制代码In [51]: pd.to_datetime("2010/11/12", format="%Y/%m/%d")
Out[51]: Timestamp('2010-11-12 00:00:00')

In [52]: pd.to_datetime("12-11-2010 00:00", format="%d-%m-%Y %H:%M")
Out[52]: Timestamp('2010-11-12 00:00:00')

Period

Period 表示的是一个时间跨度,通常和freq一起使用:

1
2
3
4
5
css复制代码In [31]: pd.Period("2011-01")
Out[31]: Period('2011-01', 'M')

In [32]: pd.Period("2012-05", freq="D")
Out[32]: Period('2012-05-01', 'D')

Period可以直接进行运算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
less复制代码In [345]: p = pd.Period("2012", freq="A-DEC")

In [346]: p + 1
Out[346]: Period('2013', 'A-DEC')

In [347]: p - 3
Out[347]: Period('2009', 'A-DEC')

In [348]: p = pd.Period("2012-01", freq="2M")

In [349]: p + 2
Out[349]: Period('2012-05', '2M')

In [350]: p - 1
Out[350]: Period('2011-11', '2M')

注意,Period只有具有相同的freq才能进行算数运算。包括 offsets 和 timedelta

1
2
3
4
5
6
7
8
9
10
less复制代码In [352]: p = pd.Period("2014-07-01 09:00", freq="H")

In [353]: p + pd.offsets.Hour(2)
Out[353]: Period('2014-07-01 11:00', 'H')

In [354]: p + datetime.timedelta(minutes=120)
Out[354]: Period('2014-07-01 11:00', 'H')

In [355]: p + np.timedelta64(7200, "s")
Out[355]: Period('2014-07-01 11:00', 'H')

Period作为index可以自动被转换为PeriodIndex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码In [38]: periods = [pd.Period("2012-01"), pd.Period("2012-02"), pd.Period("2012-03")]

In [39]: ts = pd.Series(np.random.randn(3), periods)

In [40]: type(ts.index)
Out[40]: pandas.core.indexes.period.PeriodIndex

In [41]: ts.index
Out[41]: PeriodIndex(['2012-01', '2012-02', '2012-03'], dtype='period[M]', freq='M')

In [42]: ts
Out[42]:
2012-01 -1.135632
2012-02 1.212112
2012-03 -0.173215
Freq: M, dtype: float64

可以通过 pd.period_range 方法来创建 PeriodIndex:

1
2
3
4
5
6
7
8
css复制代码In [359]: prng = pd.period_range("1/1/2011", "1/1/2012", freq="M")

In [360]: prng
Out[360]:
PeriodIndex(['2011-01', '2011-02', '2011-03', '2011-04', '2011-05', '2011-06',
'2011-07', '2011-08', '2011-09', '2011-10', '2011-11', '2011-12',
'2012-01'],
dtype='period[M]', freq='M')

还可以通过PeriodIndex直接创建:

1
2
css复制代码In [361]: pd.PeriodIndex(["2011-1", "2011-2", "2011-3"], freq="M")
Out[361]: PeriodIndex(['2011-01', '2011-02', '2011-03'], dtype='period[M]', freq='M')

DateOffset

DateOffset表示的是频率对象。它和Timedelta很类似,表示的是一个持续时间,但是有特殊的日历规则。比如Timedelta一天肯定是24小时,而在 DateOffset中根据夏令时的不同,一天可能会有23,24或者25小时。

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
ini复制代码# This particular day contains a day light savings time transition
In [144]: ts = pd.Timestamp("2016-10-30 00:00:00", tz="Europe/Helsinki")

# Respects absolute time
In [145]: ts + pd.Timedelta(days=1)
Out[145]: Timestamp('2016-10-30 23:00:00+0200', tz='Europe/Helsinki')

# Respects calendar time
In [146]: ts + pd.DateOffset(days=1)
Out[146]: Timestamp('2016-10-31 00:00:00+0200', tz='Europe/Helsinki')

In [147]: friday = pd.Timestamp("2018-01-05")

In [148]: friday.day_name()
Out[148]: 'Friday'

# Add 2 business days (Friday --> Tuesday)
In [149]: two_business_days = 2 * pd.offsets.BDay()

In [150]: two_business_days.apply(friday)
Out[150]: Timestamp('2018-01-09 00:00:00')

In [151]: friday + two_business_days
Out[151]: Timestamp('2018-01-09 00:00:00')

In [152]: (friday + two_business_days).day_name()
Out[152]: 'Tuesday'

DateOffsets 和Frequency 运算是先关的,看一下可用的Date Offset 和它相关联的 Frequency:

Date Offset Frequency String 描述
DateOffset None 通用的offset 类
BDay or BusinessDay 'B' 工作日
CDay or CustomBusinessDay 'C' 自定义的工作日
Week 'W' 一周
WeekOfMonth 'WOM' 每个月的第几周的第几天
LastWeekOfMonth 'LWOM' 每个月最后一周的第几天
MonthEnd 'M' 日历月末
MonthBegin 'MS' 日历月初
BMonthEnd or BusinessMonthEnd 'BM' 营业月底
BMonthBegin or BusinessMonthBegin 'BMS' 营业月初
CBMonthEnd or CustomBusinessMonthEnd 'CBM' 自定义营业月底
CBMonthBegin or CustomBusinessMonthBegin 'CBMS' 自定义营业月初
SemiMonthEnd 'SM' 日历月末的第15天
SemiMonthBegin 'SMS' 日历月初的第15天
QuarterEnd 'Q' 日历季末
QuarterBegin 'QS' 日历季初
BQuarterEnd 'BQ 工作季末
BQuarterBegin 'BQS' 工作季初
FY5253Quarter 'REQ' 零售季( 52-53 week)
YearEnd 'A' 日历年末
YearBegin 'AS' or 'BYS' 日历年初
BYearEnd 'BA' 营业年末
BYearBegin 'BAS' 营业年初
FY5253 'RE' 零售年 (aka 52-53 week)
Easter None 复活节假期
BusinessHour 'BH' business hour
CustomBusinessHour 'CBH' custom business hour
Day 'D' 一天的绝对时间
Hour 'H' 一小时
Minute 'T' or 'min' 一分钟
Second 'S' 一秒钟
Milli 'L' or 'ms' 一微妙
Micro 'U' or 'us' 一毫秒
Nano 'N' 一纳秒

DateOffset还有两个方法 rollforward() 和 rollback() 可以将时间进行移动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码In [153]: ts = pd.Timestamp("2018-01-06 00:00:00")

In [154]: ts.day_name()
Out[154]: 'Saturday'

# BusinessHour's valid offset dates are Monday through Friday
In [155]: offset = pd.offsets.BusinessHour(start="09:00")

# Bring the date to the closest offset date (Monday)
In [156]: offset.rollforward(ts)
Out[156]: Timestamp('2018-01-08 09:00:00')

# Date is brought to the closest offset date first and then the hour is added
In [157]: ts + offset
Out[157]: Timestamp('2018-01-08 10:00:00')

上面的操作会自动保存小时,分钟等信息,如果想要设置为 00:00:00 , 可以调用normalize() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
css复制代码In [158]: ts = pd.Timestamp("2014-01-01 09:00")

In [159]: day = pd.offsets.Day()

In [160]: day.apply(ts)
Out[160]: Timestamp('2014-01-02 09:00:00')

In [161]: day.apply(ts).normalize()
Out[161]: Timestamp('2014-01-02 00:00:00')

In [162]: ts = pd.Timestamp("2014-01-01 22:00")

In [163]: hour = pd.offsets.Hour()

In [164]: hour.apply(ts)
Out[164]: Timestamp('2014-01-01 23:00:00')

In [165]: hour.apply(ts).normalize()
Out[165]: Timestamp('2014-01-01 00:00:00')

In [166]: hour.apply(pd.Timestamp("2014-01-01 23:30")).normalize()
Out[166]: Timestamp('2014-01-02 00:00:00')

作为index

时间可以作为index,并且作为index的时候会有一些很方便的特性。

可以直接使用时间来获取相应的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码In [99]: ts["1/31/2011"]
Out[99]: 0.11920871129693428

In [100]: ts[datetime.datetime(2011, 12, 25):]
Out[100]:
2011-12-30 0.56702
Freq: BM, dtype: float64

In [101]: ts["10/31/2011":"12/31/2011"]
Out[101]:
2011-10-31 0.271860
2011-11-30 -0.424972
2011-12-30 0.567020
Freq: BM, dtype: float64

获取全年的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码In [102]: ts["2011"]
Out[102]:
2011-01-31 0.119209
2011-02-28 -1.044236
2011-03-31 -0.861849
2011-04-29 -2.104569
2011-05-31 -0.494929
2011-06-30 1.071804
2011-07-29 0.721555
2011-08-31 -0.706771
2011-09-30 -1.039575
2011-10-31 0.271860
2011-11-30 -0.424972
2011-12-30 0.567020
Freq: BM, dtype: float64

获取某个月的数据:

1
2
3
4
less复制代码In [103]: ts["2011-6"]
Out[103]:
2011-06-30 1.071804
Freq: BM, dtype: float64

DF可以接受时间作为loc的参数:

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
yaml复制代码In [105]: dft
Out[105]:
A
2013-01-01 00:00:00 0.276232
2013-01-01 00:01:00 -1.087401
2013-01-01 00:02:00 -0.673690
2013-01-01 00:03:00 0.113648
2013-01-01 00:04:00 -1.478427
... ...
2013-03-11 10:35:00 -0.747967
2013-03-11 10:36:00 -0.034523
2013-03-11 10:37:00 -0.201754
2013-03-11 10:38:00 -1.509067
2013-03-11 10:39:00 -1.693043

[100000 rows x 1 columns]

In [106]: dft.loc["2013"]
Out[106]:
A
2013-01-01 00:00:00 0.276232
2013-01-01 00:01:00 -1.087401
2013-01-01 00:02:00 -0.673690
2013-01-01 00:03:00 0.113648
2013-01-01 00:04:00 -1.478427
... ...
2013-03-11 10:35:00 -0.747967
2013-03-11 10:36:00 -0.034523
2013-03-11 10:37:00 -0.201754
2013-03-11 10:38:00 -1.509067
2013-03-11 10:39:00 -1.693043

[100000 rows x 1 columns]

时间切片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yaml复制代码In [107]: dft["2013-1":"2013-2"]
Out[107]:
A
2013-01-01 00:00:00 0.276232
2013-01-01 00:01:00 -1.087401
2013-01-01 00:02:00 -0.673690
2013-01-01 00:03:00 0.113648
2013-01-01 00:04:00 -1.478427
... ...
2013-02-28 23:55:00 0.850929
2013-02-28 23:56:00 0.976712
2013-02-28 23:57:00 -2.693884
2013-02-28 23:58:00 -1.575535
2013-02-28 23:59:00 -1.573517

[84960 rows x 1 columns]

切片和完全匹配

考虑下面的一个精度为分的Series对象:

1
2
3
4
5
6
7
8
9
10
css复制代码In [120]: series_minute = pd.Series(
.....: [1, 2, 3],
.....: pd.DatetimeIndex(
.....: ["2011-12-31 23:59:00", "2012-01-01 00:00:00", "2012-01-01 00:02:00"]
.....: ),
.....: )
.....:

In [121]: series_minute.index.resolution
Out[121]: 'minute'

时间精度小于分的话,返回的是一个Series对象:

1
2
3
4
less复制代码In [122]: series_minute["2011-12-31 23"]
Out[122]:
2011-12-31 23:59:00 1
dtype: int64

时间精度大于分的话,返回的是一个常量:

1
2
3
4
5
less复制代码In [123]: series_minute["2011-12-31 23:59"]
Out[123]: 1

In [124]: series_minute["2011-12-31 23:59:00"]
Out[124]: 1

同样的,如果精度为秒的话,小于秒会返回一个对象,等于秒会返回常量值。

时间序列的操作

Shifting

使用shift方法可以让 time series 进行相应的移动:

1
2
3
4
5
6
7
8
9
10
ini复制代码In [275]: ts = pd.Series(range(len(rng)), index=rng)

In [276]: ts = ts[:5]

In [277]: ts.shift(1)
Out[277]:
2012-01-01 NaN
2012-01-02 0.0
2012-01-03 1.0
Freq: D, dtype: float64

通过指定 freq , 可以设置shift的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码In [278]: ts.shift(5, freq="D")
Out[278]:
2012-01-06 0
2012-01-07 1
2012-01-08 2
Freq: D, dtype: int64

In [279]: ts.shift(5, freq=pd.offsets.BDay())
Out[279]:
2012-01-06 0
2012-01-09 1
2012-01-10 2
dtype: int64

In [280]: ts.shift(5, freq="BM")
Out[280]:
2012-05-31 0
2012-05-31 1
2012-05-31 2
dtype: int64

频率转换

时间序列可以通过调用 asfreq 的方法转换其频率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yaml复制代码In [281]: dr = pd.date_range("1/1/2010", periods=3, freq=3 * pd.offsets.BDay())

In [282]: ts = pd.Series(np.random.randn(3), index=dr)

In [283]: ts
Out[283]:
2010-01-01 1.494522
2010-01-06 -0.778425
2010-01-11 -0.253355
Freq: 3B, dtype: float64

In [284]: ts.asfreq(pd.offsets.BDay())
Out[284]:
2010-01-01 1.494522
2010-01-04 NaN
2010-01-05 NaN
2010-01-06 -0.778425
2010-01-07 NaN
2010-01-08 NaN
2010-01-11 -0.253355
Freq: B, dtype: float64

asfreq还可以指定修改频率过后的填充方法:

1
2
3
4
5
6
7
8
9
10
yaml复制代码In [285]: ts.asfreq(pd.offsets.BDay(), method="pad")
Out[285]:
2010-01-01 1.494522
2010-01-04 1.494522
2010-01-05 1.494522
2010-01-06 -0.778425
2010-01-07 -0.778425
2010-01-08 -0.778425
2010-01-11 -0.253355
Freq: B, dtype: float64

Resampling 重新取样

给定的时间序列可以通过调用resample方法来重新取样:

1
2
3
4
5
6
7
8
css复制代码In [286]: rng = pd.date_range("1/1/2012", periods=100, freq="S")

In [287]: ts = pd.Series(np.random.randint(0, 500, len(rng)), index=rng)

In [288]: ts.resample("5Min").sum()
Out[288]:
2012-01-01 25103
Freq: 5T, dtype: int64

resample 可以接受各类统计方法,比如: sum, mean, std, sem, max, min, median, first, last, ohlc。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
css复制代码In [289]: ts.resample("5Min").mean()
Out[289]:
2012-01-01 251.03
Freq: 5T, dtype: float64

In [290]: ts.resample("5Min").ohlc()
Out[290]:
open high low close
2012-01-01 308 460 9 205

In [291]: ts.resample("5Min").max()
Out[291]:
2012-01-01 460
Freq: 5T, dtype: int64

本文已收录于 www.flydean.com/15-python-p…

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

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

本文转载自: 掘金

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

面试官:MySQL 是如何执行一条查询语句的?

发表于 2021-10-11

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

对于一个开发工程师来说,了解一下 MySQL 是如何执行一条查询语句的,我想是非常有必要的。

首先我们要了解一下MYSQL的体系架构是什么样子的?然后再来聊聊一条查询语句的执行流程是如何?

MYSQL体系结构

先看一张架构图,如下:

模块详解

  1. Connector:用来支持各种语言和 SQL 的交互,比如 PHP,Python,Java 的 JDBC;
  2. Management Serveices & Utilities:系统管理和控制工具,包括备份恢复、MySQL 复制、集群等;
  3. Connection Pool:连接池,管理需要缓冲的资源,包括用户密码权限线程等等;
  4. SQL Interface:用来接收用户的 SQL 命令,返回用户需要的查询结果 ;
  5. Parser:用来解析 SQL 语句;
  6. Optimizer:查询优化器;
  7. Cache and Buffer:查询缓存,除了行记录的缓存之外,还有表缓存,Key 缓存,权限缓存等等;
  8. Pluggable Storage Engines:插件式存储引擎,它提供 API 给服务层使用,跟具体的文件打交道。

架构分层

把 MySQL 分成三层,跟客户端对接的连接层,真正执行操作的服务层,和跟硬件打交道的存储引擎层。

image-20211007102305222

连接层

我们的客户端要连接到 MySQL 服务器 3306 端口,必须要跟服务端建立连接,那么管理所有的连接,验证客户端的身份和权限,这些功能就在连接层完成。

服务层

连接层会把 SQL 语句交给服务层,这里面又包含一系列的流程:

比如查询缓存的判断、根据 SQL 调用相应的接口,对我们的 SQL 语句进行词法和语法的解析(比如关键字怎么识别,别名怎么识别,语法有没有错误等等)。

然后就是优化器,MySQL 底层会根据一定的规则对我们的 SQL 语句进行优化,最后再交给执行器去执行。

存储引擎

存储引擎就是我们的数据真正存放的地方,在 MySQL 里面支持不同的存储引擎。再往下就是内存或者磁盘。

SQL的执行流程

以一条查询语句为例,我们来看下 MySQL 的工作流程是什么样的。

1
sql复制代码select name from user where id=1 and age>20;

首先咱们先来看一张图,接下来的过程都是基于这张图来讲的:

image-20211006202806875

连接

程序或者工具要操作数据库,第一步要跟数据库建立连接。

在数据库中有两种连接:

  • 短连接:短连接就是操作完毕以后,马上 close 掉。
  • 长连接:长连接可以保持打开,减少服务端创建和释放连接的消耗,后面的程序访问的时候还可以使用这个连接。

建立连接是比较麻烦的,首先要发送请求,发送了请求要去验证账号密码,验证完了要去看你所拥有的权限,所以在使用过程中,尽量使用长连接。

保持长连接会消耗内存。长时间不活动的连接,MySQL 服务器会断开。可以使用sql语句查看默认时间:

1
sql复制代码show global variables like 'wait_timeout';

这个时间是由 wait_timeout 来控制的,默认都是 28800 秒,8 小时。

查询缓存

MySQL 内部自带了一个缓存模块。执行相同的查询之后我们发现缓存没有生效,为什么?MySQL 的缓存默认是关闭的。

1
sql复制代码show variables like 'query_cache%';

默认关闭的意思就是不推荐使用,为什么 MySQL 不推荐使用它自带的缓存呢?

主要是因为 MySQL 自带的缓存的应用场景有限:

第一个是它要求 SQL 语句必须一模一样,中间多一个空格,字母大小写不同都被认为是不同的的 SQL。

第二个是表里面任何一条数据发生变化的时候,这张表所有缓存都会失效,所以对于有大量数据更新的应用,也不适合。

所以缓存还是交给 ORM 框架(比如 MyBatis 默认开启了一级缓存),或者独立的缓存服务,比如 Redis 来处理更合适。

在 MySQL 8.0 中,查询缓存已经被移除了。

语法解析和预处理

为什么一条 SQL 语句能够被识别呢?假如随便执行一个字符串 hello,服务器报了一个 1064 的错:

[Err] 1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'hello' at line 1

这个就是 MySQL 的解析器和预处理模块。

这一步主要做的事情是对语句基于 SQL 语法进行词法和语法分析和语义的解析。

词法解析

词法分析就是把一个完整的 SQL 语句打碎成一个个的单词。

比如一个简单的 SQL 语句:select name from user where id = 1 and age >20;

image-20211006224637475

它会将 select 识别出来,这是一个查询语句,接下来会将 user 也识别出来,你是想要在这个表中做查询,然后将 where 后面的条件也识别出来,原来我需要去查找这些内容。

语法分析

语法分析会对 SQL 做一些语法检查,比如单引号有没有闭合,然后根据 MySQL 定义的语法规则,根据 SQL 语句生成一个数据结构。这个数据结构我们把它叫做解析树(select_lex)。

就比如英语里面的语法 “我用 is , 你用 are ”这种,如果不对肯定是不可以的,语法分析之后发现你的 SQL 语句不符合规则,就会收到 You hava an error in your SQL syntax 的错误提示。

预处理器

如果写了一个词法和语法都正确的 SQL,但是表名或者字段不存在,会在哪里报错? 是在数据库的执行层还是解析器?比如:
select * from hello;

还是在解析的时候报错,解析 SQL 的环节里面有个预处理器。它会检查生成的解析树,解决解析器无法解析的语义。比如,它会检查表和列名是否存在,检查名字和别名, 保证没有歧义。预处理之后得到一个新的解析树。

查询优化器

一条SQL语句是不是只有一种执行方式?或者说数据库最终执行的SQL是不是就是我们发送的 SQL?

这个答案是否定的。一条 SQL 语句是可以有很多种执行方式的,最终返回相同的结果,他们是等价的。但是如果有这么多种执行方式,这些执行方式怎么得到的?最终选择哪一种去执行?根据什么判断标准去选择?

这个就是 MySQL 的查询优化器的模块(Optimizer)。 查询优化器的目的就是根据解析树生成不同的执行计划(Execution Plan),然后选 择一种最优的执行计划,MySQL 里面使用的是基于开销(cost)的优化器,那种执行计划开销最小,就用哪种。

可以使用这个命令查看查询的开销:

1
sql复制代码show status like 'Last_query_cost';

MySQL 的优化器能处理哪些优化类型呢?

举两个简单的例子:

1、当我们对多张表进行关联查询的时候,以哪个表的数据作为基准表。

2、有多个索引可以使用的时候,选择哪个索引。

实际上,对于每一种数据库来说,优化器的模块都是必不可少的,他们通过复杂的算法实现尽可能优化查询效率的目标。但是优化器也不是万能的,并不是再垃圾的 SQL 语句都能自动优化,也不是每次都能选择到最优的执行计划,大家在编写 SQL 语句的时候还是要注意。

执行计划

优化器最终会把解析树变成一个执行计划(execution_plans),执行计划是一个数据结构。当然,这个执行计划不一定是最优的执行计划,因为 MySQL 也有可能覆盖不到所有的执行计划。

我们怎么查看 MySQL 的执行计划呢?比如多张表关联查询,先查询哪张表?在执行查询的时候可能用到哪些索引,实际上用到了什么索引?

MySQL 提供了一个执行计划的工具。我们在 SQL 语句前面加上 EXPLAIN,就可以看到执行计划的信息。

1
sql复制代码EXPLAIN select name from user where id=1;

存储引擎

在介绍存储引擎先来问两个问题:

1、从逻辑的角度来说,我们的数据是放在哪里的,或者说放在一个什么结构里面?

2、执行计划在哪里执行?是谁去执行?

存储引擎基本介绍

在关系型数据库里面,数据是放在表 Table 里面的。我们可以把这个表理解成 Excel 电子表格的形式。所以我们的表在存储数据的同时,还要组织数据的存储结构,这个存储结构就是由我们的存储引擎决定的,所以我们也可以把存储引擎叫做表类型。

在 MySQL 里面,支持多种存储引擎,他们是可以替换的,所以叫做插件式的存储引擎。为什么要支持这么多存储引擎呢?一种还不够用吗?

在 MySQL 里面,每一张表都可以指定它的存储引擎,而不是一个数据库只能使用一个存储引擎。存储引擎的使用是以表为单位的。而且,创建表之后还可以修改存储引擎。

如何选择存储引擎?

  • 如果对数据一致性要求比较高,需要事务支持,可以选择 InnoDB。
  • 如果数据查询多更新少,对查询性能要求比较高,可以选择 MyISAM。
  • 如果需要一个用于查询的临时表,可以选择 Memory。
  • 如果所有的存储引擎都不能满足你的需求,并且技术能力足够,可以根据官网内部手册用 C 语言开发一个存储引擎。(dev.mysql.com/doc/interna…

执行引擎

谁使用执行计划去操作存储引擎呢?这就是执行引擎(执行器),它利用存储引擎提供的相应的 API 来完成操作。

为什么我们修改了表的存储引擎,操作方式不需要做任何改变?因为不同功能的存储引擎实现的 API 是相同的。

最后把数据返回给客户端,即使没有结果也要返回。

栗子

还是以上面的sql语句为例,再来梳理一下整个sql执行流程。

1
sql复制代码select name from user where id = 1 and age >20;
  1. 通过连接器查询当前执行者的角色是否有权限,进行查询。如果有的话,就继续往下走,如果没有的话,就会被拒绝掉,同时报出 Access denied for user 的错误信息;
  2. 接下来就是去查询缓存,首先看缓存里面有没有,如果有呢,那就没有必要向下走,直接返回给客户端结果就可以了;如果缓存中没有的话,那就去执行语法解析器和预处理模块。( MySQL 8.0 版本直接将查询缓存的整块功能都给删掉了)
  3. 语法解析器和预处理主要是分析sql语句的词法和语法是否正确,没啥问题就会进行下一步,来到查询优化器;
  4. 查询优化器就会对sql语句进行一些优化,看哪种方式是最节省开销,就会执行哪种sql语句,上面的sql有两种优化方案:
* 先查询表 user 中 id 为 1 的人的姓名,然后再从里面找年龄大于 20 岁的。
* 先查询表 user 中年龄大于 20 岁的所有人,然后再从里面找 id 为 1 的。
  1. 优化器决定选择哪个方案之后,执行引擎就去执行了。然后返回给客户端结果。

结语

如果文章对你有点帮助,还是希望你们看完动动小手指,点赞、关注和收藏。

本文转载自: 掘金

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

程序员写简历时的技术词汇拼写规范备忘录 写在前面 后端开发相

发表于 2021-10-11

写在前面

每年这个时候又到了求职的旺季。

求职前,我们都会花很多的时间在自己的技术水平提升+笔/面试的准备之上,但往往却忽略了找工作第一步所需要的一个严谨且靠谱的简历。而程序员写简历,第一步就是需要注意严谨而规范地使用各种技术词汇。

如果说平时做笔记或者写博客倒也无妨,毕竟自己看得多,但是写简历时最好还是要注意一下!

当然,也有同学会说这样是不是太吹毛求疵了,这东西对实际入职工作有啥本质影响吗?

嘿,想来想去好像也没啥,但是在筛选简历和看简历时往往会给人一种不太严谨的感觉,所以,规范一点总归是好的!

因此本文就从几个方面来梳理一下程序员写简历时常用的词汇拼写注意事项,以供必要时查阅,文末也制作了PDF离线手册文档 + 并且将Markdown文件已同步至GitHub开源仓库,有需要的可以自行获取。

当然这里列出的词汇可能有限,大家可以集思广益,欢迎一起来补充!

本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及我的程序员生活和感悟,欢迎star。


后端开发相关

规范书写 不合适拼写举例 备注
RESTful Restful、RestFul REST=Representational State Transfer
Spring spring
Spring MVC SpringMVC、Springmvc Spring MVC中间有空格!
Spring Boot Springboot、SpringBoot Spring Boot中间有空格!
Spring Cloud Springcloud、SpringCloud Spring Cloud中间有空格!
OOP oop Object Oriented Programming
PO po Persistant Object
POJO pojo Plain Ordinary Java Object
Web web、WEB
Java JAVA、java
JVM jvm、Jvm Java Virtual Machine
GC gc Garbage Collection
Java EE java ee Enterprise Edition
Java SE java se Standard Edition
Golang golang
Python python
Linux linux
Runtime runtime
DNS dns Domain Name System 域名系统
DI di Dependency Injection 依赖注入
DB db DataBase
ACID acid Atomicity+Consistency+Isolation+Durability
DDL ddl Data Definition Language 数据定义语言
DML dml Data Manipulation Language 数据操纵语言
MySQL mysql
SQLite sqlite
SQLServer sqlserver、SQLserver
NoSQL nosql、NOSQL NoSQL=Not Only SQL
JDBC Jdbc、jdbc Java Database Connectivity
ORM orm Object/Relational Mapping 对象/关系映射
JPA Jpa Java Persistence API
MyBatis mybatis、Mybatis
MyBatis-Plus MyBatisPlus myBatis-plus
Maven maven、MAVEN
Redis redis
MongoDB mongoDB
MQ mq Message Queue
RabbitMQ rabbitMQ
RocketMQ rocketMQ
ActiveMQ activeMQ
Netty netty
gRPC grpc
Dubbo dubbo
JSON Json JavaScript Object Notation
JWT jwt JSON Web Token
XML Xml Extensible Markup Language
API Api Application Programming Interface
Jenkins jk、jenkins
Docker docker
UML Uml Unified Modeling Language
SOA Soa Service Oriented Architecture
MVC Mvc Model–View–Controller
MVVM Mvvm Model-View-ViewModel
MVP Mvp Model-View-Presenter
持续更新中… - -


前端开发相关

规范书写 不合适拼写举例 备注
HTTP Http、http Hyper Text Transfer Protocol
HTTPS https、Https Hyper Text Transfer Protocol over SecureSocket Layer
DOM dom Document Object Model
JavaScript javascript、Javascript、js、JS
CSS Css、css Cascading Style Sheets
HTML Html、html Hyper Text Markup Language
jQuery jquery、JQuery
Bootstrap bootstrap
Node.js node、NodeJS、nodejs
Vue.js vue、VUE、vue.js
React react
Angular angular
SPA spa Single Page Application
MPA mpa Mutiple Page Application
持续更新中… - -


客户端开发相关

规范书写 不合适拼写举例 备注
App APP、app
Objective-C OC、oc
Xcode xcode、XCODE、XCode
iPhone iphone
iOS ios、IOS
Android android
RxJava RXJava、rxjava
Android Studio as、android studio
持续更新中… - -


大数据/云计算相关

规范书写 不合适拼写举例 备注
Hadoop hadoop
HDFS hdfs、Hdfs Hadoop Distributed File System
MapReduce mapreduce、mr
HBase hbase、Hbase
Hive hive
Yarn yarn
Kafka kafka
ZooKeeper zookeeper、zk
Storm storm
Flink flink
Elasticsearch elasticsearch、ElasticSearch、es
Logstash logstash
Kibana kibana
Kubernetes k8s 官方简写为K8s
持续更新中… - -


工具或软件相关

规范书写 不合适拼写举例 备注
SVN svn Subversion
Git GIT、git
GitHub github、Github
Intellij IDEA intellij idea、idea
Eclipse eclipse
MyEclipse myeclipse
Postman postman
持续更新中… - -


集思广益

由于时间和精力有限,这里整理的技术词汇列表毕竟有限。

这里也已经将本文梳理的词汇列表以Markdown源文件+PDF离线文档的形式开源到了GitHub仓库上,GitHub仓库名称为:「Awesome-Tech-Words」

GitHub仓库为:github.com/rd2coding/A…

欢迎大家集思广益,来参与这个开源仓库,一起提交和补充。

本文已被GitHub开源仓库「编程之路」 github.com/rd2coding/R… 收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及我的程序员生活和感悟,欢迎star。

本文转载自: 掘金

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

Netty 工作流程图分析 & 核心组件说明 & 代码案

发表于 2021-10-11

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前文:你的第一款Netty应用程序

前一篇文章写了第一款Netty入门的应用程序,本文主要就是从上文的代码结合本文的流程图进一步分析Netty的工作流程和核心组件。

最后再进一步举一个实例来让大家进一步理解。

希望能够让你有所收获!!🚀

一、Netty 工作流程

我们先来看看Netty的工作原理图,简单说一下工作流程,然后通过这张图来一一分析Netty的核心组件。

1.1、Server工作流程图:

image-20210825155927290

1.2、Server工作流程分析:

  1. server端启动时绑定本地某个端口,初始化NioServerSocketChannel.
  2. 将自己NioServerSocketChannel注册到某个BossNioEventLoopGroup的selector上。
* server端包含1个`Boss NioEventLoopGroup`和1个`Worker NioEventLoopGroup`,
* `Boss NioEventLoopGroup`专门负责接收客户端的连接,`Worker NioEventLoopGroup`专门负责网络的读写
* NioEventLoopGroup相当于1个事件循环组,这个组里包含多个事件循环NioEventLoop,每个NioEventLoop包含1个selector和1个事件循环线程。
  1. BossNioEventLoopGroup循环执行的任务:

1、轮询accept事件;

2、处理accept事件,将生成的NioSocketChannel注册到某一个WorkNioEventLoopGroup的Selector上。

3、处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用eventloop.execute或schedule执行的任务,或者其它线程提交到该eventloop的任务。
4. WorkNioEventLoopGroup循环执行的任务:

* 轮询`read和Write`事件
* 处理IO事件,在NioSocketChannel可读、可写事件发生时,回调(触发)ChannelHandler进行处理。
* 处理任务队列的任务,即 `runAllTasks`

1.3、Client工作流程图

image-20210825231934496

流程就不重复概述啦😁

二、核心模块组件

Netty的核心组件大致是以下几个:

  1. Channel 接口
  2. EventLoopGroup 接口
  3. ChannelFuture 接口
  4. ChannelHandler 接口
  5. ChannelPipeline 接口
  6. ChannelHandlerContext 接口
  7. SimpleChannelInboundHandler 抽象类
  8. Bootstrap、ServerBootstrap 类
  9. ChannelFuture 接口
  10. ChannelOption 类

2.1、Channel 接口

我们平常用到基本的 I/O 操作(bind()、connect()、read()和 write()),其本质都依赖于底层网络传输所提供的原语,在Java中就是Socket类。

Netty 的 Channel 接 口所提供的 API,大大地降低了直接使用Socket 类的复杂性。另外Channel 提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。

在调用结束后立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,支持 在I/O 操作成功、失败或取消时立马回调通知调用方。

此外,Channel 也是拥有许多预定义的、专门化实现的广泛类层次结构的根,比如:

  • LocalServerChannel:用于本地传输的ServerChannel ,允许 VM 通信。
  • EmbeddedChannel:以嵌入式方式使用的 Channel 实现的基类。
  • NioSocketChannel:异步的客户端 TCP 、Socket 连接。
  • NioServerSocketChannel:异步的服务器端 TCP、Socket 连接。
  • NioDatagramChannel: 异步的 UDP 连接。
  • NioSctpChannel:异步的客户端 Sctp 连接,它使用非阻塞模式并允许将 SctpMessage 读/写到底层 SctpChannel。
  • NioSctpServerChannel:异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。

2.2、EventLoopGroup接口

EventLoop 定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事件。

Netty 通过触发事件将 Selector 从应用程序中抽象出来,消除了所有本来将需要手动编写 的派发代码。在内部,将会为每个 Channel 分配一个 EventLoop,用以处理所有事件,包括:

  • 注册事件;
  • 将事件派发给 ChannelHandler;
  • 安排进一步的动作。

不过在这里我们不深究它,针对 Channel、EventLoop、Thread 以及 EventLoopGroup 之间的关系做一个简单说明。

  • 一个EventLoopGroup 包含一个或者多个 EventLoop;
  • 每个 EventLoop 维护着一个 Selector 实例,所以一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
  • 因此所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理,实际上消除了对于同步的需要;
  • 一个 Channel 在它的生命周期内只注册于一个 EventLoop;
  • 一个 EventLoop 可能会被分配给一个或多个Channel。
  • 通常一个服务端口即一个 ServerSocketChannel 对应一个 Selector 和一个 EventLoop 线程。BossEventLoop 负责接收客户端的连接并将 SocketChannel 交给 WorkerEventLoopGroup 来进行 IO 处理,就如上文中的流程图一样。

2.3、ChannelFuture 接口

Netty 中所有的 I/O 操作都是异步的。因为一个操作可能不会 立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。具体的实现就是通过 Future 和 ChannelFutures,其 addListener()方法注册了一个 ChannelFutureListener,以便在某个操作完成时(无论是否成功)自动触发注册的监听事件。

常见的方法有

  • Channel channel(),返回当前正在进行 IO 操作的通道
  • ChannelFuture sync(),等待异步操作执行完毕

2.4、ChannelHandler 接口

从之前的入门程序中,我们可以看到ChannelHandler在Netty中的重要性,它充当了所有处理入站和出站数据的应用程序逻辑的容器。 我们的业务逻辑也大都写在实现的字类中,另外ChannelHandler 的方法是由事件自动触发的,并不需要我们自己派发。

ChannelHandler的实现类或者实现子接口有很多。平时我们就是去继承或子接口,然后重写里面的方法。

image-20210825214929693

最常见的几种Handler:

  • ChannelInboundHandler :接收入站事件和数据
  • ChannelOutboundHandler:用于处理出站事件和数据。

常见的适配器:

  • ChannelInboundHandlerAdapter:用于处理入站IO事件

ChannelInboundHandler实现的抽象基类,它提供了所有方法的实现。
这个实现只是将操作转发到ChannelPipeline的下一个ChannelHandler 。 子类可以覆盖方法实现来改变这一

  • ChannelOutboundHandlerAdapter: 用于处理出站IO事件

我们经常需要自定义一个 Handler 类去继承 ChannelInboundHandlerAdapter,然后通过重写相应方法实现业务逻辑,我们来看看有哪些方法可以重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {
//注册事件
public void channelRegistered(ChannelHandlerContext ctx) ;
//
public void channelUnregistered(ChannelHandlerContext ctx);
//通道已经就绪
public void channelActive(ChannelHandlerContext ctx);

public void channelInactive(ChannelHandlerContext ctx) ;
//通道读取数据事件
public void channelRead(ChannelHandlerContext ctx, Object msg) ;
//通道读取数据事件完毕
public void channelReadComplete(ChannelHandlerContext ctx) ;

public void userEventTriggered(ChannelHandlerContext ctx, Object evt);
//通道可写性已更改
public void channelWritabilityChanged(ChannelHandlerContext ctx);
//异常处理
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
}

2.5、ChannelPipeline 接口

ChannelPipeline 提供了 ChannelHandler 链的容器,并定义了用于在该链上传播入站和出站事件流的 API。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。他们的组成关系如下:

image-20210825223614701

一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由ChannelHandlerContext 组成的双向链表,并且每个ChanneHandlerContext中又关联着一个ChannelHandler。

ChannelHandler 安装到 ChannelPipeline 中的过程:

  1. 一个ChannelInitializer的实现被注册到了ServerBootstrap中 ;
  2. 当 ChannelInitializer.initChannel()方法被调用时,ChannelInitializer将在 ChannelPipeline 中安装一组自定义的 ChannelHandler;
  3. ChannelInitializer 将它自己从 ChannelPipeline 中移除。

从一个客户端应用程序 的角度来看,如果事件的运动方向是从客户端到服务器端,那么我们称这些事件为出站的,反之 则称为入站的。服务端反之。

如果一个消息或者任何其他的入站事件被读取,那么它会从 ChannelPipeline 的头部 开始流动,并被传递给第一个 ChannelInboundHandler。次此handler处理完后,数据将会被传递给链中的下一个 ChannelInboundHandler。最终,数据将会到达 ChannelPipeline 的尾端,至此,所有处理就结束了。

出站事件会从尾端往前传递到最前一个出站的 handler。出站和入站两种类型的 handler互不干扰。

2.6、ChannelHandlerContext 接口

作用就是使ChannelHandler能够与其ChannelPipeline和其他处理程序交互。因为 ChannelHandlerContext保存channel相关的所有上下文信息,同时关联一个 ChannelHandler 对象, 另外,ChannelHandlerContext 可以通知ChannelPipeline的下一个ChannelHandler以及动态修改它所属的ChannelPipeline 。

2.7、SimpleChannelInboundHandler 抽象类

我们常常能够遇到应用程序会利用一个 ChannelHandler 来接收解码消息,并在这个Handler中实现业务逻辑,要写一个这样的 ChannelHandler ,我们只需要扩展抽象类 SimpleChannelInboundHandler< T > 即可, 其中T类型是我们要处理的消息的Java类型。

在SimpleChannelInboundHandler 中最重要的方法就是void channelRead0(ChannelHandlerContext ctx, T msg),

我们自己实现了这个方法之后,接收到的消息就已经被解码完的消息啦。

举个例子:

image-20210825230526952

2.8、Bootstrap、ServerBootstrap 类

Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件。

类别 Bootstrap ServerBootstrap
引导 用于引导客户端 用于引导服务端
在网络编程中作用 用于连接到远程主机和端口 用于绑定到一个本地端口
EventLoopGroup 的数目 1 2

我想大家对于最后一点可能会存有疑惑,为什么一个是1一个是2呢?

因为服务器需要两组不同的 Channel。

第一组将只包含一个 ServerChannel,代表服务 器自身的已绑定到某个本地端口的正在监听的套接字。

而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的 Channel。

这一点可以上文中的流程图。

2.9、ChannelFuture 接口

异步 Channel I/O 操作的结果。
Netty 中的所有 I/O 操作都是异步的。 这意味着任何 I/O 调用将立即返回,但不保证在调用结束时请求的 I/O 操作已完成。 相反,您将返回一个 ChannelFuture 实例,该实例为您提供有关 I/O 操作的结果或状态的信息。
ChannelFuture 要么未完成,要么已完成。 当 I/O 操作开始时,会创建一个新的未来对象。 新的未来最初是未完成的——它既没有成功,也没有失败,也没有取消,因为 I/O 操作还没有完成。 如果 I/O 操作成功完成、失败或取消,则使用更具体的信息(例如失败原因)将未来标记为已完成。 请注意,即使失败和取消也属于完成状态。

Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件

常见的方法有

  • Channel channel(),返回当前正在进行 IO 操作的通道
  • ChannelFuture sync(),等待异步操作执行完毕

2.10、ChannelOption 类

  1. Netty 在创建 Channel 实例后,一般都需要设置 ChannelOption 参数。
  2. ChannelOption 参数如下:
    • ChannelOption.SO_KEEPALIVE :一直保持连接状态
    • ChannelOption.SO_BACKLOG:对应TCP/IP协议listen 函数中的backlog参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理内,所N博求放在队刚中等待处理,backilog参数指定端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理, backlog参数指定了队列的大小。

三、应用实例

【案例】:

写一个服务端,两个或多个客户端,客户端可以相互通信。

3.1、服务端 Handler

ChannelHandler的实现类或者实现子接口有很多。平时我们就是去继承或子接口,然后重写里面的方法。

在这里我们就是继承了 SimpleChannelInboundHandler< T > ,这里面许多方法都是大都只要我们重写一下业务逻辑,触发大都是在事件发生时自动调用的,无需我们手动调用。

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
java复制代码package com.crush.atguigu.group_chat;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* @author crush
*/
public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {

/**
* 定义一个channle 组,管理所有的channel
* GlobalEventExecutor.INSTANCE) 是全局的事件执行器,是一个单例
*/
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


/**
* handlerAdded 表示连接建立,一旦连接,第一个被执行
* 将当前channel 加入到 channelGroup
* @param ctx
* @throws Exception
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
//将该客户加入聊天的信息推送给其它在线的客户端
/*
该方法会将 channelGroup 中所有的channel 遍历,并发送 消息,
我们不需要自己遍历
*/
channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + " 加入聊天" + sdf.format(new java.util.Date()) + " \n");
channelGroup.add(channel);

}

/**
* 断开连接, 将xx客户离开信息推送给当前在线的客户
* @param ctx
* @throws Exception
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

Channel channel = ctx.channel();
channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + " 离开了\n");
System.out.println("channelGroup size" + channelGroup.size());

}

/**
* 表示channel 处于活动状态, 既刚出生 提示 xx上线
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {

System.out.println(ctx.channel().remoteAddress() + " 上线了~");
}

/**
* 表示channel 处于不活动状态, 既死亡状态 提示 xx离线了
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {

System.out.println(ctx.channel().remoteAddress() + " 离线了~");
}

//读取数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

//获取到当前channel
Channel channel = ctx.channel();
//这时我们遍历channelGroup, 根据不同的情况,回送不同的消息

channelGroup.forEach(ch -> {
if (channel != ch) { //不是当前的channel,转发消息
ch.writeAndFlush("[客户]" + channel.remoteAddress() + " 发送了消息" + msg + "\n");
} else {//回显自己发送的消息给自己
ch.writeAndFlush("[自己]发送了消息" + msg + "\n");
}
});
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//关闭通道
ctx.close();
}
}

3.2、服务端 Server 启动

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.crush.atguigu.group_chat;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
* @author crush
*/
public class GroupChatServer {

/**
* //监听端口
*/
private int port;

public GroupChatServer(int port) {
this.port = port;
}

/**
* 编写run方法 处理请求
* @throws Exception
*/
public void run() throws Exception {

//创建两个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//8个NioEventLoop
EventLoopGroup workerGroup = new NioEventLoopGroup();

try {
ServerBootstrap b = new ServerBootstrap();

b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//获取到pipeline
ChannelPipeline pipeline = ch.pipeline();
//向pipeline加入解码器
pipeline.addLast("decoder", new StringDecoder());
//向pipeline加入编码器
pipeline.addLast("encoder", new StringEncoder());
//加入自己的业务处理handler
pipeline.addLast(new GroupChatServerHandler());
}
});

System.out.println("netty 服务器启动");

ChannelFuture channelFuture = b.bind(port).sync();

//监听关闭
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}

public static void main(String[] args) throws Exception {
new GroupChatServer(7000).run();
}
}

3.3、客户端 Handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package com.crush.atguigu.group_chat;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* @author crush
*/
public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {

//当前Channel 已从对方读取消息时调用。
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(msg.trim());
}
}

3.4、客户端 Server

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
java复制代码package com.crush.atguigu.group_chat;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Scanner;


/**
* @author crush
*/
public class GroupChatClient {

private final String host;
private final int port;

public GroupChatClient(String host, int port) {
this.host = host;
this.port = port;
}

public void run() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();

try {

Bootstrap bootstrap = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {

@Override
protected void initChannel(SocketChannel ch) throws Exception {

//得到pipeline
ChannelPipeline pipeline = ch.pipeline();
//加入相关handler
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
//加入自定义的handler
pipeline.addLast(new GroupChatClientHandler());
}
});

ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
//得到channel
Channel channel = channelFuture.channel();
System.out.println("-------" + channel.localAddress() + "--------");
//客户端需要输入信息
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
//通过channel 发送到服务器端
channel.writeAndFlush(msg + "\r\n");
}
} finally {
group.shutdownGracefully();
}
}

public static void main(String[] args) throws Exception {
new GroupChatClient("127.0.0.1", 7000).run();
}
}

多个客户端,cv一下即可。

3.5、测试:

测试流程是先启动 服务端 Server,再启动客户端 。

image-20211010092350555

image-20211010092356763

image-20211010092402794

四、自言自语

这篇文章应该算是个存稿了,之前忙其他事情去了😂。

今天的文章就到这里了。

你好,我是博主宁在春:主页

如若在文章中遇到疑惑,请留言或私信,或者加主页联系方式,都会尽快回复。

如若发现文章中存在问题,望你能够指正,不胜感谢。

如果觉得对你有所帮助的话,请点个赞再走吧!

欢迎大家一起在讨论区讨论哦,增加增加自己的幸运,蹭一蹭掘金官方发送的周边哦!!!

评论越走心越有可能拿奖哦!!!

详情👉掘力星计划

本文转载自: 掘金

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

k8s(五) ingress之集群统一访问入口

发表于 2021-10-10

ingress有点类似微服务网关的入口,而service又是pod的入口。看下图
image.png
举个例子,假设我们部署一个电商平台,该平台有order,user,product三个服务。我们使用deployment部署2份order服务(橙色部分),部署3份user服务(绿色部分),部署3份product服务(灰色部分)。将各自deployment部署的pod暴露出一组service,这里就形成了3个service,且service层有自己的网络。接下来我们安装一个ingress。接下来我们开始访问整个服务构成的应用。每个服务都有单独的域名,我们访问order.atguigu.com就是访问订单服务,user.atguigu.com就是访问user服务,product.atguigu.com就是商品服务。当发起该域名请求时,ingress就知道你要访问的是那个service,该service接收到请求后就负载均衡给相应的pod。以下图为例,我们如果发起的是order.atguigu.com,ingress就会将此请求转发给service a。service又将此请求负载均衡到相应的pod。要想使用ingress就要安装ingress。
image.png

1.安装ingress

1
2
3
4
5
6
7
8
9
bash复制代码wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.47.0/deploy/static/provider/baremetal/deploy.yaml

#修改镜像
vi deploy.yaml
#将image的值改为如下值:
registry.cn-hangzhou.aliyuncs.com/lfy_k8s_images/ingress-nginx-controller:v0.46.0

# 检查安装的结果
kubectl get pod,svc -n ingress-nginx

如果以上安装下载不了deploy.yaml的话,就使用ymal配置文件的方式部署,以下是deploy.yaml文件内容,自己在任意节点上新建一个ingress.yaml,然后复制以下内容(www.yuque.com/leifengyang… 第6部分的ingress安装)
创建成功
image.png
查看pod
image.png
安装成功之后等待3-5分钟的样子容器就running status,这样的话我们k8s集群就有一个统一的网关入口。
查看service,发现http的80端口映射的是31525,而https的443则映射的是30427。注意,这里的ingress也是以NodePort的方式对外暴露的。
image.png
我们可以使用任意的节点ip+ingress的端口作为集群统一访问入口。【如果你使用的是云服务记得开启安全组端口】,我使用任意节点的ip+80映射的端口访问,这就相当于集群总网关层的http端口。
image.png
以30427端口访问就是集群内部的https端口
image.png
更多ingress的使用参考官方文档
kubernetes.github.io/ingress-ngi…

2.ingress域名访问的使用

2.1 部署2份hello-service和2份nginx-demo
在任意节点下新建test-ingress.ymal,内容如下

image.png
【参照上面给出的语雀笔记】
使用命令kubectl apply -f test-ingress.ymal创建成功
image.png

2.2 创建域名访问

在任意节点下创建一个test.ymal【我这个命名不是很好,不具备业务标识,大家可以命名成ingress-rule.yaml】内容如下【参照上面给出的语雀笔记】。大致的意思就是有2个host名,分别是hello.atguigu.com和demo.atguigu.com分别与之对应的service名是hello-service和nginx-demo。
image.png
使用命令kubectl apply -f test.ymal创建成功
image.png

本文转载自: 掘金

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

Spring JPA 批量插入与更新优化

发表于 2021-10-10

Spring JPA 中批量插入和更新是使用 SimpleJpaRepository#saveAll,saveAll 会循环调用 save 方法,save 方法会根据实体 id 查找记录,记录存在则更新,不存在则插入。n 个实体需要执行 2n 条语句,因而效率较低。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Transactional
@Override
public <S extends T> S save(S entity) {

if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}

注:id 为基本类型且为 null 时,会直接插入记录,只执行 n 条语句。

此文中,将在主键自增的前提下,以借助 druid 监控,探讨 Spring JPA 批量插入与更新优化思路。

批量插入

使用 SimpleJpaRepository#saveAll,插入 5k 条记录。

image.png
总共执行事务 5000*2 次,用时 543 s。

Hibernate 本身支持批量执行,通过 spring.jpa.properties.hibernate.jdbc.batch_size 指定批处理的容量。
image.png
共执行事务 5000+5 次,用时 439 s。

利用 EntityManager 批量插入 5k 条记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码private <S extends T> void batchExecute(Iterable<S> s, Consumer<S> consumer) {
Session unwrap = entityManager.unwrap(Session.class);
try {
unwrap.getTransaction().begin();
Iterator<S> iterator = s.iterator();
int index = 0;
while (iterator.hasNext()) {
consumer.accept(iterator.next());
index++;
if (index % BATCH_SIZE == 0) {
entityManager.flush();
entityManager.clear();
}
}
if (index % BATCH_SIZE != 0) {
entityManager.flush();
entityManager.clear();
}
unwrap.getTransaction().commit();
} catch (Exception e) {
unwrap.getTransaction().rollback();
}
}

image.png
总共执行事务 5 次,用时 255 s,和 SimpleJpaRepository#saveAll 相比,节省了查询的性能消耗。

通过拼接 SQL 语句的方式,使用一条语句插入多条记录。

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
scss复制代码public void insertUsingConcat() {
StringBuilder sb = new StringBuilder("insert into t_comment(id, content, name) values ");
List<CommentPO> l = new ArrayList<>();
for (int i = 10000; i < 15000; i++) {
sb.append("(")
.append(i)
.append(",'content of demo batch#")
.append(i)
.append("','name of demo batch#")
.append(i)
.append("'),");
}
sb.deleteCharAt(sb.length() - 1);

executeQuery(sb);
}

final EntityManager entityManager = ApplicationContextHolder.getApplicationContext()
.getBean("entityManagerSecondary", EntityManager.class);

@Transactional
public void executeQuery(StringBuilder sb) {
Session unwrap = entityManager.unwrap(Session.class);
unwrap.setJdbcBatchSize(1000);
try {
unwrap.getTransaction().begin();
Query query = entityManager.createNativeQuery(sb.toString());
query.executeUpdate();
unwrap.getTransaction().commit();
} catch (Exception e) {
e.printStackTrace();
unwrap.getTransaction().rollback();
}
}

image.png
执行一次事务,用时 0.2 s。

拼接语句需要注意 sql 语句长度限制,可以通过 show VARIABLES WHERE Variable_name LIKE 'max_allowed_packet'; 查询,这是 Server 一次接受的数据包大小,通过 my.ini 配置。

批量更新

批量更新和批量插入类似,也是四种写法,结论也一致。区别仅在于 sql 写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@PutMapping("/concatUpdateClause")
public void updateUsingConcat() {
List<CommentPO> l = getDemoBatches(5000, 10000, "new");
StringBuilder sb = new StringBuilder("update t_comment set content = case");
for (CommentPO commentPO : l) {
sb.append(" when id = ").append(commentPO.getId()).append(" then '").append(commentPO.getContent())
.append("'");
}
sb.append(" else content end")
.append(" where id in (")
.append(l.stream().map(i -> String.valueOf(i.getId())).collect(Collectors.joining(",")))
.append(")");
executeQuery(sb);
}

本文转载自: 掘金

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

基于Node使用TypeORM的数据迁移

发表于 2021-10-10

最近在用Node.js练习写后端,操作数据库的方面自然用到了比较火的一款TypeORM框架,在使用迁移(migration)时,看文档查百度好容易弄出来了(感觉我点了谷歌翻译后的官方文档不够清楚啊-_-||),先记录下来。

迁移(Migration)

迁移就我的理解来说的话,就是通过我们写的代码来修改数据库的解构或者数据(用于不同环境间的数据库解构同步),好处的话:

1
2
markdown复制代码1. 通过代码编写,不用直接操作数据库;
2. 直接通过命令来操作,简单快捷。

实体

因为TypeORM的表生成是通过对应的实体来生成的,所以得先了解什么是实体。

在TypeORM里面,实体其实就是一个Class,也就是一个普通的类,只是这个类需要通过@Entity装饰器来装饰,类里面的每一个属性,对应的都是数据表的每一个字段,例如:

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
ts复制代码import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity({
name: 'article'
})
class Article {
@PrimaryGeneratedColumn()
id: number = 0;

@Column({
type: 'varchar',
length: 50,
comment: '标题'
})
title: string = '';

@Column({
type: 'varchar',
length: 100,
comment: '描述'
})
desc: string = '';
}

export default Article

这样的一个类,就可以称之为实体(或者叫实体类)。

开始迁移

开始迁移前我们需要定义一个配置文件,这里用的是ts作为配置文件来使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ts复制代码import { resolve } from 'path';
import { ConnectionOptions } from 'typeorm';

const dbConfig: ConnectionOptions = {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'myblog',
entities: [
'src/model/*.ts' // 对应的实体类位置
],
migrations: [
'src/migrations/**/*.ts' // 对应的迁移文件位置
],
cli: {
entitiesDir: 'src/model', // 实体类的目录位置
migrationsDir: 'src/migrations' // 迁移文件的目录位置
}
}

export default dbConfig;

这个配置文件也是typeorm连接数据库所需要的。

了解这些概念后,就可以通过TypeORM开始实现迁移功能,因为使用的是ts,所以我们需要在package.json里面配置几条命令,这些命令可以去官网文档里面查看:

1
2
3
json复制代码"typeorm:create": "ts-node --transpile-only ./node_modules/typeorm/cli.js --config src/config/db.config.ts migration:create -n",
"typeorm:gen": "ts-node --transpile-only ./node_modules/typeorm/cli.js --config src/config/db.config.ts migration:generate -n",
"typeorm:run": "ts-node --transpile-only ./node_modules/typeorm/cli.js migration:run --config src/config/db.config.ts"

typeorm:create

这个命令的作用就是用于生成一个migration文件,我们需要操作数据库的代码,例如执行命令:

1
shell复制代码npm run typeorm:create UpdateArticle

就会生成下面这些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
ts复制代码import {MigrationInterface, QueryRunner} from "typeorm";

export class UpdateArticle1633872542504 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`article\` MODIFY \`desc\` VARCHAR(200)`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`article\` MODIFY \`desc\` VARCHAR(100)`);
}

}

上面的一个类就是通过create命令生成的,这里面主要有两个方法:

  • up:包含迁移所需执行的代码;
  • down:包含迁移恢复的代码。

其实就是:up是我们想要去执行操作数据库的sql语句,如果这个语句有问题,执行不了的话,就需要执行down方法来恢复成原来的样子,避免数据库遭到污染(系统应该会自动执行的)。

typeorm:gen

这个命令是用来生成对应实体类的migration,通过这个migration就可以创建对应的表,例如:

1
shell复制代码npm run typeorm:gen CreateArticle

会生成一个TIMESTAMP-CreateArticle.ts文件,这个文件就已经包含了创建表的sql了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ts复制代码import {MigrationInterface, QueryRunner} from "typeorm";

export class CreateAritcle1633872275180 implements MigrationInterface {
name = 'CreateAritcle1633872275180'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`article\` (\`id\` int NOT NULL AUTO_INCREMENT, \`title\` varchar(50) NOT NULL COMMENT '标题', \`desc\` varchar(100) NOT NULL COMMENT '描述', \`view\` int NOT NULL COMMENT '浏览数量', \`good\` int NOT NULL COMMENT '点赞数量', \`message\` int NOT NULL COMMENT '留言数量', \`createTime\` varchar(13) NOT NULL COMMENT '创建时间', \`updateTime\` varchar(13) NOT NULL COMMENT '修改时间', \`userId\` int NOT NULL COMMENT '用户ID', PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
await queryRunner.query(`ALTER TABLE \`article\` ADD CONSTRAINT \`FK_636f17dadfea1ffb4a412296a28\` FOREIGN KEY (\`userId\`) REFERENCES \`user\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`article\` DROP FOREIGN KEY \`FK_636f17dadfea1ffb4a412296a28\``);
await queryRunner.query(`DROP TABLE \`article\``);
}

}

注意:上面的CreateArticle是固定的写法,表名是article,但是执行generate命令时需要在Article前加上Create表示创建表,可以是CreateUser,也可以说CreateRole,总之表名称一定要对应!

typeorm:run

表示执行迁移,上面通过create或者gen生成的文件都是迁移文件,通过执行命令:

1
shell复制代码npm run typeorm:run

然后就会执行对这些文件的逻辑(按道理,应该可以执行单个的,不过我暂时还未发现)。

暂时记录这些…

本文转载自: 掘金

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

盘点 Cloud SpringConfig 原理 Git

发表于 2021-10-10

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

作为开源框架 , springConfig 有很多功能是不适合业务场景的 , 所以我们需要通过定制重写来达到自己的业务需求的 , 这一篇就来看看 , 如果定制 SpringCloud Config 模块

二. 原理分析

2.1 处理入口

SpringCloudConfig 的入口是 EnvironmentController , 当我们输入以下地址时 , 会进行对应的处理 :

1
2
3
4
5
java复制代码/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

对应处理接口 -> EnvironmentController

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// 以上方式分别对应如下接口 : 
@RequestMapping(path = "/{name}/{profiles:.*[^-].*}", produces = MediaType.APPLICATION_JSON_VALUE)
public Environment defaultLabel(@PathVariable String name, @PathVariable String profiles)

@RequestMapping(path = "/{name}/{profiles:.*[^-].*}", produces = EnvironmentMediaType.V2_JSON)
public Environment defaultLabelIncludeOrigin(@PathVariable String name, @PathVariable String profiles)

@RequestMapping(path = "/{name}/{profiles}/{label:.*}", produces = MediaType.APPLICATION_JSON_VALUE)
public Environment labelled(@PathVariable String name, @PathVariable String profiles, @PathVariable String label)

@RequestMapping(path = "/{name}/{profiles}/{label:.*}", produces = EnvironmentMediaType.V2_JSON)
public Environment labelledIncludeOrigin(@PathVariable String name, @PathVariable String profiles,@PathVariable String label)

主入口逻辑 : getEnvironment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public Environment getEnvironment(String name, String profiles, String label, boolean includeOrigin) {

// 就如方法名 ,此处是对参数格式化/规范化
name = normalize(name);
label = normalize(label);

// 核心逻辑 , 查找对应的配置
Environment environment = this.repository.findOne(name, profiles, label, includeOrigin);

// 为空校验
if (!this.acceptEmpty && (environment == null || environment.getPropertySources().isEmpty())) {
throw new EnvironmentNotFoundException("Profile Not found");
}
return environment;
}
}

2.2 处理逻辑

处理逻辑主要在 EnvironmentEncryptorEnvironmentRepository 中进行 , 这也是我们的核心定制类

这里先来看一下整体的查询体系 :

Config-EnvironmentRepository.png

2.2.1 delegate 查找配置

注意 ,此处的delegate是可以通过自动装配改写的 ,这也意味着我们可以去实现不同的配置方式!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// C- 
public Environment findOne(String name, String profiles, String label, boolean includeOrigin) {

// 核心查询点 , 也是定制点
Environment environment = this.delegate.findOne(name, profiles, label, includeOrigin);
if (this.environmentEncryptors != null) {

for (EnvironmentEncryptor environmentEncryptor : environmentEncryptors) {
// 对配置解码
environment = environmentEncryptor.decrypt(environment);
}
}
return environment;
}

2.2.2 Git 处理流程

对应 Git 查询使用的是 MultipleJGitEnvironmentRepository , 当然还有几个其他的 , 这里先不深入 :

PS : 此处依次调用了多个 Repository , 最终是调用父类 AbstractScmEnvironmentRepository 获取 Locations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码//C- AbstractScmEnvironmentRepository
public synchronized Environment findOne(String application, String profile, String label, boolean includeOrigin) {

NativeEnvironmentRepository delegate = new NativeEnvironmentRepository(getEnvironment(),
new NativeEnvironmentProperties());

// 获取 Location , 在此环节拉取 Git 到本地 -> 详见 2.3
Locations locations = getLocations(application, profile, label);
delegate.setSearchLocations(locations.getLocations());

// 调用上面的 native 继续处理 -> NativeEnvironmentRepository -> 2.2.3
Environment result = delegate.findOne(application, profile, "", includeOrigin);

result.setVersion(locations.getVersion());
result.setLabel(label);

return this.cleaner.clean(result, getWorkingDirectory().toURI().toString(), getUri());
}

2.3 Git 文件的下载

Git 文件的下载其实不麻烦 , 其主要也是使用工具类 , 以下有相关的使用 : Git Plugin

1
2
3
4
5
java复制代码    <dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>5.1.3.201810200350-r</version>
</dependency>

在 JGitEnvironmentRepository # getLocation 环节中 , 存在一个 refresh 操作

2.3.1 refresh git 主流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public String refresh(String label) {
Git git = null;

// 拉取 Git 文件
git = createGitClient();

// 断是否需要 pull 拉取
if (shouldPull(git)) {
FetchResult fetchStatus = fetch(git, label);
if (this.deleteUntrackedBranches && fetchStatus != null) {
deleteUntrackedLocalBranches(fetchStatus.getTrackingRefUpdates(), git);
}
}

checkout(git, label);
tryMerge(git, label);

return git.getRepository().findRef("HEAD").getObjectId().getName();

}

2.3.2 拉取 Git 文件

1
2
3
4
5
6
7
8
9
10
java复制代码
// Step 1 : createGitClient 流程
在这个流程中主要2个逻辑 , 判断路径是否存在 .git , 分别调用 :
- openGitRepository :
- copyRepository :

// Step 2 : Git 核心拉取流程
- copyRepository -> cloneToBasedir : 从远端 clone 项目
- cloneToBasedir -> getCloneCommandByCloneRepository : 获取 clone 命令
- cloneToBasedir -> clone.call() : 完成 clone 流程

到了这里已经在本地下载到了本地 , 后面就是读取了

2.2.3 NativeEnvironmentRepository 的处理

在上文获取完 Location 后 , 会继续进行 delegate.findOne 进行链式调用 , 这里会进行如下调用 :

  • C- NativeEnvironmentRepository # findOne
  • C- ConfigDataEnvironmentPostProcessor # applyTo : 调用 EnvironmentPostProcessor 进行处理
  • C- ConfigDataEnvironmentPostProcessor # postProcessEnvironment
  • C- ConfigDataEnvironmentPostProcessor # getConfigDataEnvironment : 构建 ConfigDataEnvironment
1
2
3
4
5
6
7
8
java复制代码ConfigDataEnvironment getConfigDataEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader,
Collection<String> additionalProfiles) {
return new ConfigDataEnvironment(this.logFactory, this.bootstrapContext, environment, resourceLoader,
additionalProfiles, this.environmentUpdateListener);
}

// PS : 核心 , 在构造器中构建了ConfigDataLocationResolvers
this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);

2.4 文件的扫描

Git 获取数据分为2步 :

  • Step 1 : 从远程拉取配置到本地
  • Step 2 : 从本地读取配置

2.4.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
java复制代码// C- ConfigDataEnvironmentContributors
ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer,
ConfigDataActivationContext activationContext) {

// ImportPhase 是一个枚举 , 包含 BEFORE_PROFILE_ACTIVATION 和 AFTER_PROFILE_ACTIVATION 2 个属性
// BEFORE_PROFILE_ACTIVATION : 启动配置文件之前的阶段
ImportPhase importPhase = ImportPhase.get(activationContext);

ConfigDataEnvironmentContributors result = this;
int processed = 0;

// 死循环处理 , 直到路径文件其全部处理完成
while (true) {
// ConfigDataProperties 中包含一个 Location 对象
ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase);
if (contributor == null) {
return result;
}
if (contributor.getKind() == Kind.UNBOUND_IMPORT) {
Iterable<ConfigurationPropertySource> sources = Collections
.singleton(contributor.getConfigurationPropertySource());
PlaceholdersResolver placeholdersResolver = new ConfigDataEnvironmentContributorPlaceholdersResolver(
result, activationContext, true);
Binder binder = new Binder(sources, placeholdersResolver, null, null, null);
ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(binder);
result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
result.getRoot().withReplacement(contributor, bound));
continue;
}
ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext(
result, contributor, activationContext);

// 准备 Loader 容器
ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);

// 获取全部 Location 列表
List<ConfigDataLocation> imports = contributor.getImports();

// 核心逻辑 >>> 进行 resolver 处理 , 最终调用 2.3.2 resolve
Map<ConfigDataResolutionResult, ConfigData> imported = importer.resolveAndLoad(activationContext,
locationResolverContext, loaderContext, imports);

ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase,
asContributors(imported));
result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
result.getRoot().withReplacement(contributor, contributorAndChildren));
processed++;
}
}

2.4.2 resolve 解析路径

1
2
3
4
5
6
7
8
9
10
java复制代码// C- ConfigDataImporter # resolveAndLoad : 在这个方法中主要分为核心的三个步骤 

// Step 1 : 获取 Profiles 信息
Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null;

// Step 2 : resolved 解析路径
List<ConfigDataResolutionResult> resolved = resolve(locationResolverContext, profiles, locations);

// Step 3 : load 加载为 Map<ConfigDataResolutionResult, ConfigData>
return load(loaderContext, resolved);

resolved 的过程也有点绕 ,推荐看图 , 这里列出主要逻辑 :

Step 2-1 : resolved 循环 location

1
2
3
4
5
6
7
8
9
java复制代码private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolverContext locationResolverContext,
Profiles profiles, List<ConfigDataLocation> locations) {
List<ConfigDataResolutionResult> resolved = new ArrayList<>(locations.size());
// 遍历所有的 location
for (ConfigDataLocation location : locations) {
resolved.addAll(resolve(locationResolverContext, profiles, location));
}
return Collections.unmodifiableList(resolved);
}

Step 2-2 : 循环其他 resolve

这里是对多种格式的资源解析处理

1
2
3
4
5
6
7
8
9
java复制代码List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location,
Profiles profiles) {
// 循环且判断能处理
for (ConfigDataLocationResolver<?> resolver : getResolvers()) {
if (resolver.isResolvable(context, location)) {
return resolve(resolver, context, location, profiles);
}
}
}

Step 2-3 : getReference 后 循环所有资源

1
2
3
4
5
6
7
8
9
10
java复制代码// C- StandardConfigDataLocationResolver
private List<StandardConfigDataResource> resolve(Set<StandardConfigDataReference> references) {
List<StandardConfigDataResource> resolved = new ArrayList<>();

// reference 对 reference 进行循环
for (StandardConfigDataReference reference : references) {
resolved.addAll(resolve(reference));
}
return resolved;
}

image.png

image.png

Step 2-4 : 循环所有的 ConfigDataResource

1
2
3
4
5
6
7
8
9
10
11
java复制代码private List<ConfigDataResolutionResult> resolve(ConfigDataLocation location, boolean profileSpecific,
Supplier<List<? extends ConfigDataResource>> resolveAction) {
// 注意 , 2-3 在这环节发生 , 此时已经拿到了最终的资源
List<ConfigDataResource> resources = nonNullList(resolveAction.get());
List<ConfigDataResolutionResult> resolved = new ArrayList<>(resources.size());

for (ConfigDataResource resource : resources) {
resolved.add(new ConfigDataResolutionResult(location, resource, profileSpecific));
}
return resolved;
}

image.png

2.5 load 加载流程

2.5.1 load 主流程

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
java复制代码// C- ConfigDataImporter
private Map<ConfigDataResolutionResult, ConfigData> load(ConfigDataLoaderContext loaderContext,
List<ConfigDataResolutionResult> candidates) throws IOException {
Map<ConfigDataResolutionResult, ConfigData> result = new LinkedHashMap<>();

// 对所有的 candidates 解析循环
for (int i = candidates.size() - 1; i >= 0; i--) {
ConfigDataResolutionResult candidate = candidates.get(i);

// 获取 location
ConfigDataLocation location = candidate.getLocation();
// 获取所有的 Resource 资源
ConfigDataResource resource = candidate.getResource();
if (this.loaded.add(resource)) {
try {
// 调用 loaders 对 resource 进行加载
ConfigData loaded = this.loaders.load(loaderContext, resource);
if (loaded != null) {
// 这里会统一放在一个 map 中
result.put(candidate, loaded);
}
}
catch (ConfigDataNotFoundException ex) {
handle(ex, location);
}
}
}
return Collections.unmodifiableMap(result);
}

!!!!!! 我这里被坑的很惨 , 一个简单的案例怎么都加载不出来 , 有兴趣的可以看我下面的图猜一下为什么,很重要的一点!!!

image.png

2.5.2 loaders 加载

此处的家长对象主要为 ConfigDataLoaders , 最终调用对象为 StandardConfigDataLoader

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public ConfigData load(ConfigDataLoaderContext context, StandardConfigDataResource resource)
throws IOException, ConfigDataNotFoundException {

// 省略为空及不存在 Reource 抛出异常
StandardConfigDataReference reference = resource.getReference();
Resource originTrackedResource = OriginTrackedResource.of(resource.getResource(),
Origin.from(reference.getConfigDataLocation()));
String name = String.format("Config resource '%s' via location '%s'", resource,
reference.getConfigDataLocation());

// 最终通过 YamlPropertySourceLoader 对 YAML 文件进行加载
List<PropertySource<?>> propertySources = reference.getPropertySourceLoader().load(name, originTrackedResource);
return new ConfigData(propertySources);
}

最终加载出来的 PropertySources

image.png

到了这里属性就正式被加载完成

总结

这一篇已经写的很长了 , 单纯点好 , 所以 Native 和属性的使用在后面再看看 , 贡献一个流程图

Config-git.jpg

本文转载自: 掘金

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

CentOS 7单机安装Redis Cluster(3主3从

发表于 2021-10-10

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

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

首先安装单机版Redis ​

安装配置:

服务IP Redis安装目录
192.168.211.107 /usr/local/soft/redis-6.2.4/

第一步:创建数据目录

创建不同的数据目录

1
2
3
4
yaml复制代码cd /usr/local/soft/redis-6.2.4/
mkdir redis-cluster
cd redis-cluster/
mkdir 6319 6329 6339 6349 6359 6369

image.png

第二步:配置文件修改

拷贝Redis配置文件redis.conf到创建的第一个文件夹下

1
bash复制代码cp /usr/local/soft/redis-6.2.4/redis.conf  /usr/local/soft/redis-6.2.4/redis-cluster/6319

修改配置文件

1
2
bash复制代码cd /usr/local/soft/redis-6.2.4/redis-cluster/6319
vim redis.conf

如下配置文件配置项如有不懂的,可以去我的单机版安装Redis实例中查看,这里搜索这些配置可以退出编辑模式 使用/xxx(斜杆+部分字符) 来搜索,或者直接从服务器拿下来修改

1
2
3
4
5
6
7
8
9
bash复制代码port 6319
protected-mode no
daemonize yes
dir "/usr/local/soft/redis-6.2.4/redis-cluster/6319/"
cluster-enabled yes
cluster-config-file nodes-6319.conf
cluster-node-timeout 15000
appendonly yes
pidfile "/var/run/redis_6319.pid"

外网集群需要增加如下配置

1
2
3
4
5
6
bash复制代码# 各节点网卡分配的IP(公网IP)
cluster-announce-ip xx.xx.xx.xx
# 节点映射端口
cluster-announce-port ${PORT}
# 节点总线端口
cluster-announce-bus-port ${PORT}

拷贝配置文件到其余5个创建的目录

1
2
3
4
5
6
bash复制代码cd /usr/local/soft/redis-6.2.4/redis-cluster/6319/
cp redis.conf ../6329/
cp redis.conf ../6339/
cp redis.conf ../6349/
cp redis.conf ../6359/
cp redis.conf ../6369/

image.png

批量替换配置文件内容sed -i 's/原字符串/新字符串/' /xxx/xx.xx

1
2
3
4
5
6
bash复制代码cd /usr/local/soft/redis-6.2.4/redis-cluster/
sed -i 's/6319/6329/g' 6329/redis.conf
sed -i 's/6319/6339/g' 6339/redis.conf
sed -i 's/6319/6349/g' 6349/redis.conf
sed -i 's/6319/6359/g' 6359/redis.conf
sed -i 's/6319/6369/g' 6369/redis.conf

image.png

第三步:启动节点

启动6个Redis节点

1
2
3
4
5
6
7
8
bash复制代码./src/redis-server redis-cluster/6319/redis.conf
./src/redis-server redis-cluster/6329/redis.conf
./src/redis-server redis-cluster/6339/redis.conf
./src/redis-server redis-cluster/6349/redis.conf
./src/redis-server redis-cluster/6359/redis.conf
./src/redis-server redis-cluster/6369/redis.conf

ps -ef|grep redis

image.png

第四步:创建集群

使用绝对IP地址启动集群

1
2
bash复制代码cd /usr/local/soft/redis-6.2.4/src/
redis-cli --cluster create 192.168.211.107:6319 192.168.211.107:6329 192.168.211.107:6339 192.168.211.107:6349 192.168.211.107:6359 192.168.211.107:6369 --cluster-replicas 1

Redis对6个节点分配3主3从,我们直接yes确认 image.png slot分配图,这里记录下来,后续测试有用

节点 IP 槽范围
Master[0]
192.168.211.107:6319 Slots 0 - 5460
Master[1] 192.168.211.107:6329 Slots 5461 - 10922
Master[2] 192.168.211.107:6339 Slots 10923 - 16383

集群创建完成 image.png

第五步:测试集群

通过脚本批量插入key,来根据key的分布测试集群节点是否正常 创建脚本

1
2
bash复制代码cd /usr/local/soft/redis-6.2.4/redis-cluster/
vim batchKeyInsert.sh

​

**脚本内容是循环十万次往Redis中插入key ** redis-cli -h {host} -p {port} {command} 是一种客户端连接执行命令方式

redis-cli -h 192.168.211.107 -p 6319 -c -x set name$i >>redis.log

**-c **

连接集群结点时使用,此选项可防止moved和ask异常

**-x **

代表从标准输入读取数据作为该命令的最后一个参数

1
2
3
4
5
bash复制代码#!/bin/bash
for((i=0;i<100000;i++))
do
echo -en "Come on, i love java" | redis-cli -h 192.168.211.107 -p 6319 -c -x set name$i >>redis.log
done

文件赋予权限

1
bash复制代码chmod +x batchKeyInsert.sh

执行脚本(需要一点时间)

1
bash复制代码./batchKeyInsert.sh

进入三个主节点,连接客户端,查看节点的数据分布情况

1
2
3
bash复制代码cd /usr/local/soft/redis-6.2.4/src
redis-cli -p 6319
dbsize

image.png 从上面看出节点数据分布较为均匀,集群部署成功!

本文转载自: 掘金

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

1…500501502…956

开发者博客

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